diff --git a/cmd/yanic/main.go b/cmd/yanic/main.go index 115cb48..1d27404 100644 --- a/cmd/yanic/main.go +++ b/cmd/yanic/main.go @@ -9,8 +9,9 @@ import ( "time" "github.com/FreifunkBremen/yanic/database" - "github.com/FreifunkBremen/yanic/database/all" - "github.com/FreifunkBremen/yanic/meshviewer" + allDB "github.com/FreifunkBremen/yanic/database/all" + "github.com/FreifunkBremen/yanic/output" + allOutput "github.com/FreifunkBremen/yanic/output/all" "github.com/FreifunkBremen/yanic/respond" "github.com/FreifunkBremen/yanic/rrd" "github.com/FreifunkBremen/yanic/runtime" @@ -43,7 +44,7 @@ func main() { panic(err) } - connections, err = all.Connect(config.Database.Connection) + connections, err = allDB.Connect(config.Database.Connection) if err != nil { panic(err) } @@ -57,7 +58,14 @@ func main() { nodes = runtime.NewNodes(config) nodes.Start() - meshviewer.Start(config, nodes) + + outputs, err := allOutput.Register(nodes, config.Nodes.Output) + if err != nil { + panic(err) + } + + output.Start(outputs, config) + defer output.Close() if config.Webserver.Enable { log.Println("starting webserver on", config.Webserver.Bind) diff --git a/config_example.toml b/config_example.toml index d10b8ea..c5dd8ae 100644 --- a/config_example.toml +++ b/config_example.toml @@ -37,7 +37,8 @@ offline_after = "10m" prune_after = "7d" -[meshviewer] +[[nodes.output.meshviewer]] +enable = true # structur of nodes.json, which to support # version 1 is to support legacy meshviewer (which are in master branch) # i.e. https://github.com/ffnord/meshviewer/tree/master @@ -50,6 +51,27 @@ nodes_path = "/var/www/html/meshviewer/data/nodes.json" # path where to store graph.json graph_path = "/var/www/html/meshviewer/data/graph.json" +[nodes.output.meshviewer.filter] +# no_owner = true +has_location = true +blacklist = ["vpnid"] + +[nodes.output.meshviewer.filter.in_area] +latitude_min = 34.30 +latitude_max = 71.85 +longitude_min = -24.96 +longitude_max = 39.72 + +[[nodes.output.template]] +enable = false +template_path = "/var/lib/collector/html-template.tmp" +output_path = "/var/www/html/index.html" + +[[nodes.output.nodelist]] +enable = true +path = "/var/www/html/meshviewer/data/nodelist.json" + + [database] # cleaning data of measurement node, # which are older than 7d diff --git a/contrib/example-template.tmpl b/contrib/example-template.tmpl new file mode 100644 index 0000000..969da37 --- /dev/null +++ b/contrib/example-template.tmpl @@ -0,0 +1,16 @@ +function ffhbCurrentStats(data) { + $("#freifunk").html(" +
+ Nutzer: 0
+ (auf 0 Geräte verteilt)
+
+ mehr +
"); + + $("#freifunk_clients").html(data.Clients); + $("#freifunk_nodes").html(data.Nodes); +}; + +ffhbCurrentStats({{json .GlobalStatistic}}); diff --git a/data/nodeinfo.go b/data/nodeinfo.go index 8a3b96f..e6271c9 100644 --- a/data/nodeinfo.go +++ b/data/nodeinfo.go @@ -4,7 +4,7 @@ package data type NodeInfo struct { NodeID string `json:"node_id"` Network Network `json:"network"` - Owner *Owner `json:"-"` // Removed for privacy reasons + Owner *Owner `json:"owner"` System System `json:"system"` Hostname string `json:"hostname"` Location *Location `json:"location,omitempty"` diff --git a/meshviewer/nodes.go b/meshviewer/nodes.go deleted file mode 100644 index bbb09cb..0000000 --- a/meshviewer/nodes.go +++ /dev/null @@ -1,50 +0,0 @@ -package meshviewer - -import ( - "log" - "time" - - "github.com/FreifunkBremen/yanic/runtime" -) - -type nodeBuilder func(*runtime.Nodes) interface{} - -var nodeFormats = map[int]nodeBuilder{ - 1: BuildNodesV1, - 2: BuildNodesV2, -} - -// Start all services to manage Nodes -func Start(config *runtime.Config, nodes *runtime.Nodes) { - go worker(config, nodes) -} - -// Periodically saves the cached DB to json file -func worker(config *runtime.Config, nodes *runtime.Nodes) { - c := time.Tick(config.Nodes.SaveInterval.Duration) - - for range c { - saveMeshviewer(config, nodes) - } -} - -func saveMeshviewer(config *runtime.Config, nodes *runtime.Nodes) { - // Locking foo - nodes.RLock() - defer nodes.RUnlock() - if path := config.Meshviewer.NodesPath; path != "" { - version := config.Meshviewer.Version - builder := nodeFormats[version] - - if builder != nil { - runtime.SaveJSON(builder(nodes), path) - } else { - log.Panicf("invalid nodes version: %d", version) - } - - } - - if path := config.Meshviewer.GraphPath; path != "" { - runtime.SaveJSON(BuildGraph(nodes), path) - } -} diff --git a/output/all/internal.go b/output/all/internal.go new file mode 100644 index 0000000..2dea22a --- /dev/null +++ b/output/all/internal.go @@ -0,0 +1,37 @@ +package all + +import ( + "github.com/FreifunkBremen/yanic/output" + "github.com/FreifunkBremen/yanic/runtime" +) + +type Output struct { + output.Output + nodes *runtime.Nodes + list []output.Output +} + +func Register(nodes *runtime.Nodes, configuration interface{}) (output.Output, error) { + var list []output.Output + allOutputs := configuration.(map[string][]interface{}) + for outputType, outputRegister := range output.Adapters { + outputConfigs := allOutputs[outputType] + for _, config := range outputConfigs { + output, err := outputRegister(nodes, config) + if err != nil { + return nil, err + } + if output == nil { + continue + } + list = append(list, output) + } + } + return &Output{list: list, nodes: nodes}, nil +} + +func (o *Output) Save() { + for _, item := range o.list { + item.Save() + } +} diff --git a/output/all/main.go b/output/all/main.go new file mode 100644 index 0000000..5441f28 --- /dev/null +++ b/output/all/main.go @@ -0,0 +1,6 @@ +package all + +import ( + _ "github.com/FreifunkBremen/yanic/output/meshviewer" + _ "github.com/FreifunkBremen/yanic/output/template" +) diff --git a/output/internal.go b/output/internal.go new file mode 100644 index 0000000..58172ee --- /dev/null +++ b/output/internal.go @@ -0,0 +1,37 @@ +package output + +import ( + "time" + + "github.com/FreifunkBremen/yanic/runtime" +) + +var quit chan struct{} + +// Start workers of database +// WARNING: Do not override this function +// you should use New() +func Start(output Output, config *runtime.Config) { + quit = make(chan struct{}) + go saveWorker(output, config.Nodes.SaveInterval.Duration) +} + +func Close() { + if quit != nil { + close(quit) + } +} + +// save periodically to output +func saveWorker(output Output, saveInterval time.Duration) { + ticker := time.NewTicker(saveInterval) + for { + select { + case <-ticker.C: + output.Save() + case <-quit: + ticker.Stop() + return + } + } +} diff --git a/output/meshviewer/filter.go b/output/meshviewer/filter.go new file mode 100644 index 0000000..96e3397 --- /dev/null +++ b/output/meshviewer/filter.go @@ -0,0 +1,157 @@ +package meshviewer + +import ( + "github.com/FreifunkBremen/yanic/data" + "github.com/FreifunkBremen/yanic/runtime" +) + +type filter func(node *runtime.Node) *runtime.Node + +// Config Filter +type filterConfig map[string]interface{} + +func (f filterConfig) Blacklist() *map[string]interface{} { + if v, ok := f["blacklist"]; ok { + list := make(map[string]interface{}) + for _, nodeid := range v.([]interface{}) { + list[nodeid.(string)] = true + } + return &list + } + return nil +} + +func (f filterConfig) NoOwner() bool { + if v, ok := f["no_owner"]; ok { + return v.(bool) + } + return true +} +func (f filterConfig) HasLocation() *bool { + if v, ok := f["has_location"].(bool); ok { + return &v + } + return nil +} + +type area struct { + xA float64 + xB float64 + yA float64 + yB float64 +} + +func (f filterConfig) InArea() *area { + if areaConfigInt, ok := f["in_area"]; ok { + areaConfig := areaConfigInt.(map[string]interface{}) + a := area{} + if v, ok := areaConfig["latitude_min"]; ok { + a.xA = v.(float64) + } + if v, ok := areaConfig["latitude_max"]; ok { + a.xB = v.(float64) + } + if v, ok := areaConfig["longitude_min"]; ok { + a.yA = v.(float64) + } + if v, ok := areaConfig["longitude_max"]; ok { + a.yB = v.(float64) + } + return &a + } + return nil +} + +// Create Filter +func createFilter(config filterConfig) filter { + return func(n *runtime.Node) *runtime.Node { + //maybe cloning of this object is better? + node := n + + if config.NoOwner() { + node = filterNoOwner(node) + } + if ok := config.HasLocation(); ok != nil { + node = filterHasLocation(node, *ok) + } + if area := config.InArea(); area != nil { + node = filterLocationInArea(node, *area) + } + if list := config.Blacklist(); list != nil { + node = filterBlacklist(node, *list) + } + + return node + } +} + +func filterBlacklist(node *runtime.Node, list map[string]interface{}) *runtime.Node { + if node != nil { + if nodeinfo := node.Nodeinfo; nodeinfo != nil { + if _, ok := list[nodeinfo.NodeID]; !ok { + return node + } + } + } + return nil +} + +func filterNoOwner(node *runtime.Node) *runtime.Node { + if node == nil { + return nil + } + return &runtime.Node{ + Address: node.Address, + Firstseen: node.Firstseen, + Lastseen: node.Lastseen, + Online: node.Online, + Statistics: node.Statistics, + Nodeinfo: &data.NodeInfo{ + NodeID: node.Nodeinfo.NodeID, + Network: node.Nodeinfo.Network, + System: node.Nodeinfo.System, + Owner: nil, + Hostname: node.Nodeinfo.Hostname, + Location: node.Nodeinfo.Location, + Software: node.Nodeinfo.Software, + Hardware: node.Nodeinfo.Hardware, + VPN: node.Nodeinfo.VPN, + Wireless: node.Nodeinfo.Wireless, + }, + Neighbours: node.Neighbours, + } +} + +func filterHasLocation(node *runtime.Node, withLocation bool) *runtime.Node { + if node != nil { + if nodeinfo := node.Nodeinfo; nodeinfo != nil { + if withLocation { + if location := nodeinfo.Location; location != nil { + return node + } + } else { + if location := nodeinfo.Location; location == nil { + return node + } + } + } + } + return nil +} + +func filterLocationInArea(node *runtime.Node, a area) *runtime.Node { + if node != nil { + if nodeinfo := node.Nodeinfo; nodeinfo != nil { + if location := nodeinfo.Location; location != nil { + if location.Latitude >= a.xA && location.Latitude <= a.xB { + if location.Longtitude >= a.yA && location.Longtitude <= a.yB { + return node + } + } + } else { + return node + } + } + } + return nil +} diff --git a/meshviewer/graph.go b/output/meshviewer/graph.go similarity index 100% rename from meshviewer/graph.go rename to output/meshviewer/graph.go diff --git a/meshviewer/graph_test.go b/output/meshviewer/graph_test.go similarity index 96% rename from meshviewer/graph_test.go rename to output/meshviewer/graph_test.go index 39cc9fa..5d0c521 100644 --- a/meshviewer/graph_test.go +++ b/output/meshviewer/graph_test.go @@ -58,7 +58,7 @@ func testGetNodeByFile(filename string) *runtime.Node { } func testfile(name string, obj interface{}) { - file, err := ioutil.ReadFile("../runtime/testdata/" + name) + file, err := ioutil.ReadFile("../../runtime/testdata/" + name) if err != nil { panic(err) } diff --git a/meshviewer/node.go b/output/meshviewer/node.go similarity index 100% rename from meshviewer/node.go rename to output/meshviewer/node.go diff --git a/meshviewer/nodes_test.go b/output/meshviewer/nodes_test.go similarity index 85% rename from meshviewer/nodes_test.go rename to output/meshviewer/nodes_test.go index bd08d5e..2d63ad6 100644 --- a/meshviewer/nodes_test.go +++ b/output/meshviewer/nodes_test.go @@ -10,13 +10,13 @@ import ( ) func TestNodesV1(t *testing.T) { - nodes := BuildNodesV1(createTestNodes()).(*NodesV1) + nodes := BuildNodesV1(func(n *runtime.Node) *runtime.Node { return n }, createTestNodes()).(*NodesV1) assert := assert.New(t) assert.Len(nodes.List, 2) } func TestNodesV2(t *testing.T) { - nodes := BuildNodesV2(createTestNodes()).(*NodesV2) + nodes := BuildNodesV2(func(n *runtime.Node) *runtime.Node { return n }, createTestNodes()).(*NodesV2) assert := assert.New(t) assert.Len(nodes.List, 2) diff --git a/meshviewer/nodes_v1.go b/output/meshviewer/nodes_v1.go similarity index 63% rename from meshviewer/nodes_v1.go rename to output/meshviewer/nodes_v1.go index 1ffadb0..1352b41 100644 --- a/meshviewer/nodes_v1.go +++ b/output/meshviewer/nodes_v1.go @@ -14,30 +14,29 @@ type NodesV1 struct { } // BuildNodesV1 transforms data to legacy meshviewer -func BuildNodesV1(nodes *runtime.Nodes) interface{} { +func BuildNodesV1(toFilter filter, nodes *runtime.Nodes) interface{} { meshviewerNodes := &NodesV1{ Version: 1, List: make(map[string]*Node), Timestamp: jsontime.Now(), } - for nodeID := range nodes.List { - nodeOrigin := nodes.List[nodeID] - - if nodeOrigin.Statistics == nil { + for nodeID, nodeOrigin := range nodes.List { + nodeFiltere := toFilter(nodeOrigin) + if nodeOrigin.Statistics == nil || nodeFiltere == nil { continue } node := &Node{ - Firstseen: nodeOrigin.Firstseen, - Lastseen: nodeOrigin.Lastseen, + Firstseen: nodeFiltere.Firstseen, + Lastseen: nodeFiltere.Lastseen, Flags: Flags{ - Online: nodeOrigin.Online, - Gateway: nodeOrigin.IsGateway(), + Online: nodeFiltere.Online, + Gateway: nodeFiltere.IsGateway(), }, - Nodeinfo: nodeOrigin.Nodeinfo, + Nodeinfo: nodeFiltere.Nodeinfo, } - node.Statistics = NewStatistics(nodeOrigin.Statistics) + node.Statistics = NewStatistics(nodeFiltere.Statistics) meshviewerNodes.List[nodeID] = node } return meshviewerNodes diff --git a/meshviewer/nodes_v2.go b/output/meshviewer/nodes_v2.go similarity index 65% rename from meshviewer/nodes_v2.go rename to output/meshviewer/nodes_v2.go index 0500b39..290a97b 100644 --- a/meshviewer/nodes_v2.go +++ b/output/meshviewer/nodes_v2.go @@ -14,27 +14,27 @@ type NodesV2 struct { } // BuildNodesV2 transforms data to modern meshviewers -func BuildNodesV2(nodes *runtime.Nodes) interface{} { +func BuildNodesV2(toFilter filter, nodes *runtime.Nodes) interface{} { meshviewerNodes := &NodesV2{ Version: 2, Timestamp: jsontime.Now(), } - for nodeID := range nodes.List { - nodeOrigin := nodes.List[nodeID] - if nodeOrigin.Statistics == nil { + for _, nodeOrigin := range nodes.List { + nodeFiltere := toFilter(nodeOrigin) + if nodeOrigin.Statistics == nil || nodeFiltere == nil { continue } node := &Node{ - Firstseen: nodeOrigin.Firstseen, - Lastseen: nodeOrigin.Lastseen, + Firstseen: nodeFiltere.Firstseen, + Lastseen: nodeFiltere.Lastseen, Flags: Flags{ - Online: nodeOrigin.Online, - Gateway: nodeOrigin.IsGateway(), + Online: nodeFiltere.Online, + Gateway: nodeFiltere.IsGateway(), }, - Nodeinfo: nodeOrigin.Nodeinfo, + Nodeinfo: nodeFiltere.Nodeinfo, } - node.Statistics = NewStatistics(nodeOrigin.Statistics) + node.Statistics = NewStatistics(nodeFiltere.Statistics) meshviewerNodes.List = append(meshviewerNodes.List, node) } return meshviewerNodes diff --git a/output/meshviewer/output.go b/output/meshviewer/output.go new file mode 100644 index 0000000..aae5908 --- /dev/null +++ b/output/meshviewer/output.go @@ -0,0 +1,88 @@ +package meshviewer + +import ( + "log" + + "github.com/FreifunkBremen/yanic/output" + "github.com/FreifunkBremen/yanic/runtime" +) + +type Output struct { + output.Output + config Config + nodes *runtime.Nodes + builder nodeBuilder + filter filter +} + +type Config map[string]interface{} + +func (c Config) Enable() bool { + return c["enable"].(bool) +} + +func (c Config) Version() int64 { + return c["version"].(int64) +} +func (c Config) NodesPath() string { + if c["nodes_path"] == nil { + log.Panic("in configuration of [[nodes.output.meshviewer]] was no nodes_path defined", c) + } + return c["nodes_path"].(string) +} +func (c Config) GraphPath() string { + return c["graph_path"].(string) +} + +func (c Config) FilterOption() filterConfig { + if v, ok := c["filter"]; ok { + var filterMap filterConfig + filterMap = v.(map[string]interface{}) + return filterMap + } + return nil +} + +type nodeBuilder func(filter, *runtime.Nodes) interface{} + +var nodeFormats = map[int64]nodeBuilder{ + 1: BuildNodesV1, + 2: BuildNodesV2, +} + +func init() { + output.RegisterAdapter("meshviewer", Register) +} + +func Register(nodes *runtime.Nodes, configuration interface{}) (output.Output, error) { + var config Config + config = configuration.(map[string]interface{}) + if !config.Enable() { + return nil, nil + } + + builder := nodeFormats[config.Version()] + if builder == nil { + log.Panicf("invalid nodes version: %d", config.Version()) + } + + return &Output{ + nodes: nodes, + config: config, + builder: builder, + filter: createFilter(config.FilterOption()), + }, nil +} + +func (o *Output) Save() { + o.nodes.RLock() + defer o.nodes.RUnlock() + + if path := o.config.NodesPath(); path != "" { + runtime.SaveJSON(o.builder(o.filter, o.nodes), path) + } + + if path := o.config.GraphPath(); path != "" { + runtime.SaveJSON(BuildGraph(o.nodes), path) + } +} diff --git a/output/nodelist/nodelist.go b/output/nodelist/nodelist.go new file mode 100644 index 0000000..4bb54f3 --- /dev/null +++ b/output/nodelist/nodelist.go @@ -0,0 +1,64 @@ +package nodelist + +import ( + "github.com/FreifunkBremen/yanic/jsontime" + "github.com/FreifunkBremen/yanic/runtime" +) + +// NodeList rewritten after: https://github.com/ffnord/ffmap-backend/blob/c33ebf62f013e18bf71b5a38bd058847340db6b7/lib/nodelist.py +type NodeList struct { + Version string `json:"version"` + Timestamp jsontime.Time `json:"updated_at"` // Timestamp of the generation + List []*Node `json:"nodes"` +} + +type Node struct { + ID string `json:"id"` + Name string `json:"name"` + Position *Position `json:"position,omitempty"` + Status struct { + Online bool `json:"online"` + LastContact jsontime.Time `json:"lastcontact"` + Clients uint32 `json:"clients"` + } `json:"status"` +} + +type Position struct { + Lat float64 `json:"lat"` + Long float64 `json:"long"` +} + +func NewNode(n *runtime.Node) *Node { + if nodeinfo := n.Nodeinfo; nodeinfo != nil { + node := &Node{ + ID: nodeinfo.NodeID, + Name: nodeinfo.Hostname, + } + if location := nodeinfo.Location; location != nil { + node.Position = &Position{Lat: location.Latitude, Long: location.Longtitude} + } + + node.Status.Online = n.Online + node.Status.LastContact = n.Lastseen + if statistics := n.Statistics; statistics != nil { + node.Status.Clients = statistics.Clients.Total + } + return node + } + return nil +} + +func transform(nodes *runtime.Nodes) *NodeList { + nodelist := &NodeList{ + Version: "1.0.1", + Timestamp: jsontime.Now(), + } + + for _, nodeOrigin := range nodes.List { + node := NewNode(nodeOrigin) + if node != nil { + nodelist.List = append(nodelist.List, node) + } + } + return nodelist +} diff --git a/output/nodelist/nodelist_test.go b/output/nodelist/nodelist_test.go new file mode 100644 index 0000000..f9fcd8d --- /dev/null +++ b/output/nodelist/nodelist_test.go @@ -0,0 +1,60 @@ +package nodelist + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/FreifunkBremen/yanic/data" + "github.com/FreifunkBremen/yanic/runtime" +) + +func TestTransform(t *testing.T) { + nodes := transform(createTestNodes()) + + assert := assert.New(t) + assert.Len(nodes.List, 3) +} + +func createTestNodes() *runtime.Nodes { + nodes := runtime.NewNodes(&runtime.Config{}) + + nodeData := &data.ResponseData{ + Statistics: &data.Statistics{ + Clients: data.Clients{ + Total: 23, + }, + }, + NodeInfo: &data.NodeInfo{ + Hardware: data.Hardware{ + Model: "TP-Link 841", + }, + }, + } + nodeData.NodeInfo.Software.Firmware.Release = "2016.1.6+entenhausen1" + nodes.Update("abcdef012345", nodeData) + + nodes.Update("112233445566", &data.ResponseData{ + Statistics: &data.Statistics{ + Clients: data.Clients{ + Total: 2, + }, + }, + NodeInfo: &data.NodeInfo{ + Hardware: data.Hardware{ + Model: "TP-Link 841", + }, + }, + }) + + nodes.Update("0xdeadbeef0x", &data.ResponseData{ + NodeInfo: &data.NodeInfo{ + VPN: true, + Hardware: data.Hardware{ + Model: "Xeon Multi-Core", + }, + }, + }) + + return nodes +} diff --git a/output/nodelist/output.go b/output/nodelist/output.go new file mode 100644 index 0000000..760c6b4 --- /dev/null +++ b/output/nodelist/output.go @@ -0,0 +1,51 @@ +package nodelist + +import ( + goTemplate "text/template" + + "github.com/FreifunkBremen/yanic/output" + "github.com/FreifunkBremen/yanic/runtime" +) + +type Output struct { + output.Output + config Config + nodes *runtime.Nodes + template *goTemplate.Template +} + +type Config map[string]interface{} + +func (c Config) Enable() bool { + return c["enable"].(bool) +} + +func (c Config) Path() string { + return c["path"].(string) +} + +func init() { + output.RegisterAdapter("nodelist", Register) +} + +func Register(nodes *runtime.Nodes, configuration interface{}) (output.Output, error) { + var config Config + config = configuration.(map[string]interface{}) + if !config.Enable() { + return nil, nil + } + + return &Output{ + config: config, + nodes: nodes, + }, nil +} + +func (o *Output) Save() { + o.nodes.RLock() + defer o.nodes.RUnlock() + + if path := o.config.Path(); path != "" { + runtime.SaveJSON(transform(o.nodes), path) + } +} diff --git a/output/output.go b/output/output.go new file mode 100644 index 0000000..138473d --- /dev/null +++ b/output/output.go @@ -0,0 +1,19 @@ +package output + +import "github.com/FreifunkBremen/yanic/runtime" + +// Output interface to use for implementation in e.g. influxdb +type Output interface { + // InsertNode stores statistics per node + Save() +} + +// Register function with config to get a output interface +type Register func(nodes *runtime.Nodes, config interface{}) (Output, error) + +// Adapters is the list of registered output adapters +var Adapters = map[string]Register{} + +func RegisterAdapter(name string, n Register) { + Adapters[name] = n +} diff --git a/output/template/main.go b/output/template/main.go new file mode 100644 index 0000000..20899f0 --- /dev/null +++ b/output/template/main.go @@ -0,0 +1,85 @@ +package template + +import ( + "bytes" + "encoding/json" + "io" + "log" + "os" + goTemplate "text/template" + + "github.com/FreifunkBremen/yanic/output" + "github.com/FreifunkBremen/yanic/runtime" +) + +type Output struct { + output.Output + config Config + nodes *runtime.Nodes + template *goTemplate.Template +} + +type Config map[string]interface{} + +func (c Config) Enable() bool { + return c["enable"].(bool) +} + +func (c Config) TemplatePath() string { + return c["template_path"].(string) +} +func (c Config) ResultPath() string { + return c["result_path"].(string) +} + +func init() { + output.RegisterAdapter("template", Register) +} + +func Register(nodes *runtime.Nodes, configuration interface{}) (output.Output, error) { + var config Config + config = configuration.(map[string]interface{}) + if !config.Enable() { + return nil, nil + } + t := goTemplate.New("some") + t = t.Funcs(goTemplate.FuncMap{"json": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }}) + buf := bytes.NewBuffer(nil) + f, err := os.Open(config.TemplatePath()) // Error handling elided for brevity. + if err != nil { + log.Panic(err) + } + io.Copy(buf, f) // Error handling elided for brevity. + f.Close() + + s := string(buf.Bytes()) + t.Parse(s) + return &Output{ + config: config, + nodes: nodes, + template: t, + }, nil +} + +func (o *Output) Save() { + stats := runtime.NewGlobalStats(o.nodes) + if stats == nil { + log.Panic("update of [output.template] not possible invalid data for the template generated") + } + tmpFile := o.config.ResultPath() + ".tmp" + f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + log.Panic(err) + } + o.template.Execute(f, map[string]interface{}{"GlobalStatistic": stats}) + if err != nil { + log.Panic(err) + } + f.Close() + if err := os.Rename(tmpFile, o.config.ResultPath()); err != nil { + log.Panic(err) + } +} diff --git a/runtime/config.go b/runtime/config.go index b6b3846..a14933f 100644 --- a/runtime/config.go +++ b/runtime/config.go @@ -26,6 +26,7 @@ type Config struct { SaveInterval Duration `toml:"save_interval"` // Save nodes periodically OfflineAfter Duration `toml:"offline_after"` // Set node to offline if not seen within this period PruneAfter Duration `toml:"prune_after"` // Remove nodes after n days of inactivity + Output map[string][]interface{} } Meshviewer struct { Version int `toml:"version"` diff --git a/runtime/config_test.go b/runtime/config_test.go index a5110a3..0b248fb 100644 --- a/runtime/config_test.go +++ b/runtime/config_test.go @@ -18,13 +18,17 @@ func TestReadConfig(t *testing.T) { assert.Equal("eth0", config.Respondd.Interface) assert.Equal(time.Minute, config.Respondd.CollectInterval.Duration) - assert.Equal(2, config.Meshviewer.Version) - assert.Equal("/var/www/html/meshviewer/data/nodes.json", config.Meshviewer.NodesPath) - assert.Equal(time.Hour*24*7, config.Nodes.PruneAfter.Duration) assert.Equal(time.Hour*24*7, config.Database.DeleteAfter.Duration) + var meshviewer map[string]interface{} + outputs := config.Nodes.Output["meshviewer"] + assert.Len(outputs, 1, "more outputs are given") + meshviewer = outputs[0].(map[string]interface{}) + assert.Equal(int64(2), meshviewer["version"]) + assert.Equal("/var/www/html/meshviewer/data/nodes.json", meshviewer["nodes_path"]) + var influxdb map[string]interface{} dbs := config.Database.Connection["influxdb"] assert.Len(dbs, 1, "more influxdb are given") diff --git a/runtime/stats.go b/runtime/stats.go index 423a5f9..a477303 100644 --- a/runtime/stats.go +++ b/runtime/stats.go @@ -23,7 +23,7 @@ func NewGlobalStats(nodes *Nodes) (result *GlobalStats) { Models: make(CounterMap), } - nodes.Lock() + nodes.RLock() for _, node := range nodes.List { if node.Online { result.Nodes++ @@ -42,7 +42,7 @@ func NewGlobalStats(nodes *Nodes) (result *GlobalStats) { } } } - nodes.Unlock() + nodes.RUnlock() return }