add support for custom fields

At the moment, if one has a custom respondd module which includes custom
fields, Yanic will simply ignore these fields. Communities which have custom
fields have to maintain patches on Yanic to have them available.

This commit allows to define custom fields in the configuration file, which
will cause Yanic to also save the values of these custom fields in its internal
data structures. Output modules can then decide whether they want to include
these fields. For most cases, this should avoid the need for patches in Yanic.
This commit is contained in:
nrbffs 2019-11-17 10:44:11 +01:00 committed by genofire
parent ab798f0dd6
commit 1a1163aaa1
9 changed files with 114 additions and 15 deletions

View File

@ -45,6 +45,10 @@
name = "github.com/spf13/cobra" name = "github.com/spf13/cobra"
version = "0.0.2" version = "0.0.2"
[[constraint]]
name = "github.com/tidwall/gjson"
version = "1.3.4"
[[constraint]] [[constraint]]
name = "github.com/stretchr/testify" name = "github.com/stretchr/testify"
version = "1.2.1" version = "1.2.1"

View File

@ -10,6 +10,16 @@ synchronize = "1m"
# how often request per multicast # how often request per multicast
collect_interval = "1m" collect_interval = "1m"
# If you have custom respondd fields, you can ask Yanic to also collect these.
# NOTE: This does not automatically include these fields in the output.
# The meshviewer-ffrgb output module will include them under "custom_fields",
# but other modules may simply ignore them.
#[[respondd.custom_field]]
#name = zip
# You can use arbitrary GJSON expressions here, see https://github.com/tidwall/gjson
# We expect this expression to return a string.
#path = nodeinfo.location.zip
# table of a site to save stats for (not exists for global only) # table of a site to save stats for (not exists for global only)
#[respondd.sites.example] #[respondd.sites.example]
## list of domains on this site to save stats for (empty for global only) ## list of domains on this site to save stats for (empty for global only)

View File

@ -5,4 +5,5 @@ type ResponseData struct {
Neighbours *Neighbours `json:"neighbours"` Neighbours *Neighbours `json:"neighbours"`
Nodeinfo *Nodeinfo `json:"nodeinfo"` Nodeinfo *Nodeinfo `json:"nodeinfo"`
Statistics *Statistics `json:"statistics"` Statistics *Statistics `json:"statistics"`
CustomFields map[string]interface{} `json:"-"`
} }

View File

@ -42,6 +42,7 @@ func (no *noowner) Apply(node *runtime.Node) *runtime.Node {
Wireless: nodeinfo.Wireless, Wireless: nodeinfo.Wireless,
}, },
Neighbours: node.Neighbours, Neighbours: node.Neighbours,
CustomFields: node.CustomFields,
} }
} }
return node return node

View File

@ -5,10 +5,12 @@ import (
"compress/flate" "compress/flate"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"time" "time"
"github.com/bdlm/log" "github.com/bdlm/log"
"github.com/tidwall/gjson"
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/database" "github.com/FreifunkBremen/yanic/database"
@ -237,7 +239,7 @@ func (coll *Collector) sender() {
func (coll *Collector) parser() { func (coll *Collector) parser() {
for obj := range coll.queue { for obj := range coll.queue {
if data, err := obj.parse(); err != nil { if data, err := obj.parse(coll.config.CustomFields); err != nil {
log.WithField("address", obj.Address.String()).Errorf("unable to decode response %s", err) log.WithField("address", obj.Address.String()).Errorf("unable to decode response %s", err)
} else { } else {
coll.saveResponse(obj.Address, data) coll.saveResponse(obj.Address, data)
@ -245,14 +247,32 @@ func (coll *Collector) parser() {
} }
} }
func (res *Response) parse() (*data.ResponseData, error) { func (res *Response) parse(customFields []CustomFieldConfig) (*data.ResponseData, error) {
// Deflate // Deflate
deflater := flate.NewReader(bytes.NewReader(res.Raw)) deflater := flate.NewReader(bytes.NewReader(res.Raw))
defer deflater.Close() defer deflater.Close()
jsonData, err := ioutil.ReadAll(deflater)
if err != nil {
return nil, err
}
// Unmarshal // Unmarshal
rdata := &data.ResponseData{} rdata := &data.ResponseData{}
err := json.NewDecoder(deflater).Decode(rdata) err = json.Unmarshal(jsonData, rdata)
rdata.CustomFields = make(map[string]interface{})
if !gjson.Valid(string(jsonData)) {
log.WithField("jsonData", jsonData).Info("JSON data is invalid")
} else {
jsonParsed := gjson.Parse(string(jsonData))
for _, customField := range customFields {
field := jsonParsed.Get(customField.Path)
if field.Exists() {
rdata.CustomFields[customField.Name] = field.String()
}
}
}
return rdata, err return rdata, err
} }

View File

@ -41,10 +41,65 @@ func TestParse(t *testing.T) {
Raw: compressed, Raw: compressed,
} }
data, err := res.parse() data, err := res.parse([]CustomFieldConfig{})
assert.NoError(err) assert.NoError(err)
assert.NotNil(data) assert.NotNil(data)
assert.Equal("f81a67a5e9c1", data.Nodeinfo.NodeID) assert.Equal("f81a67a5e9c1", data.Nodeinfo.NodeID)
} }
func TestParseCustomFields(t *testing.T) {
assert := assert.New(t)
// read testdata
compressed, err := ioutil.ReadFile("testdata/nodeinfo.flated")
assert.Nil(err)
res := &Response{
Raw: compressed,
}
customFields := []CustomFieldConfig{
{
Name: "my_custom_field",
Path: "nodeinfo.hostname",
},
}
data, err := res.parse(customFields)
assert.NoError(err)
assert.NotNil(data)
assert.Equal("Trillian", data.CustomFields["my_custom_field"])
assert.Equal("Trillian", data.Nodeinfo.Hostname)
}
func TestParseCustomFieldNotExistant(t *testing.T) {
assert := assert.New(t)
// read testdata
compressed, err := ioutil.ReadFile("testdata/nodeinfo.flated")
assert.Nil(err)
res := &Response{
Raw: compressed,
}
customFields := []CustomFieldConfig{
{
Name: "some_other_field",
Path: "nodeinfo.some_field_which_doesnt_exist",
},
}
data, err := res.parse(customFields)
assert.NoError(err)
assert.NotNil(data)
_, ok := data.CustomFields["some_other_field"]
assert.Equal("Trillian", data.Nodeinfo.Hostname)
assert.False(ok)
}

View File

@ -8,6 +8,7 @@ type Config struct {
Interfaces []InterfaceConfig `toml:"interfaces"` Interfaces []InterfaceConfig `toml:"interfaces"`
Sites map[string]SiteConfig `toml:"sites"` Sites map[string]SiteConfig `toml:"sites"`
CollectInterval duration.Duration `toml:"collect_interval"` CollectInterval duration.Duration `toml:"collect_interval"`
CustomFields []CustomFieldConfig `toml:"custom_field"`
} }
func (c *Config) SitesDomains() (result map[string][]string) { func (c *Config) SitesDomains() (result map[string][]string) {
@ -29,3 +30,8 @@ type InterfaceConfig struct {
MulticastAddress string `toml:"multicast_address"` MulticastAddress string `toml:"multicast_address"`
Port int `toml:"port"` Port int `toml:"port"`
} }
type CustomFieldConfig struct {
Name string `toml:"name"`
Path string `toml:"path"`
}

View File

@ -16,6 +16,7 @@ type Node struct {
Statistics *data.Statistics `json:"statistics"` Statistics *data.Statistics `json:"statistics"`
Nodeinfo *data.Nodeinfo `json:"nodeinfo"` Nodeinfo *data.Nodeinfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"-"` Neighbours *data.Neighbours `json:"-"`
CustomFields map[string]interface{} `json:"custom_fields"`
} }
// Link represents a link between two nodes // Link represents a link between two nodes

View File

@ -83,6 +83,7 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
node.Neighbours = res.Neighbours node.Neighbours = res.Neighbours
node.Nodeinfo = res.Nodeinfo node.Nodeinfo = res.Nodeinfo
node.Statistics = res.Statistics node.Statistics = res.Statistics
node.CustomFields = res.CustomFields
return node return node
} }