[TASK] Make yanic more modular for multiple databases (#33)

This commit is contained in:
Geno 2017-04-10 18:54:12 +02:00 committed by GitHub
parent 5502c0affe
commit f135249795
36 changed files with 869 additions and 456 deletions

View File

@ -29,6 +29,10 @@ export PATH=$PATH:$GOPATH/bin
go get -v -u github.com/FreifunkBremen/yanic/cmd/... go get -v -u github.com/FreifunkBremen/yanic/cmd/...
``` ```
#### Work with other databases
If you did like a other database solution like influxdb,
you are welcome to create another subpackage from database in your fork like the logging.
### Configurate ### Configurate
```sh ```sh
cp /opt/go/src/github.com/FreifunkBremen/yanic/config_example.toml /etc/yanic.conf cp /opt/go/src/github.com/FreifunkBremen/yanic/config_example.toml /etc/yanic.conf

View File

@ -6,8 +6,8 @@ import (
"os" "os"
"time" "time"
"github.com/FreifunkBremen/yanic/models"
"github.com/FreifunkBremen/yanic/respond" "github.com/FreifunkBremen/yanic/respond"
"github.com/FreifunkBremen/yanic/runtime"
) )
// Usage: respond-query wlp4s0 "[fe80::eade:27ff:dead:beef%wlp4s0]:1001" // Usage: respond-query wlp4s0 "[fe80::eade:27ff:dead:beef%wlp4s0]:1001"
@ -17,7 +17,7 @@ func main() {
log.Printf("Sending request address=%s iface=%s", dstAddress, iface) log.Printf("Sending request address=%s iface=%s", dstAddress, iface)
nodes := models.NewNodes(&models.Config{}) nodes := runtime.NewNodes(&runtime.Config{})
collector := respond.NewCollector(nil, nodes, iface, 0) collector := respond.NewCollector(nil, nodes, iface, 0)
collector.SendPacket(net.ParseIP(dstAddress)) collector.SendPacket(net.ParseIP(dstAddress))

View File

@ -8,18 +8,20 @@ import (
"syscall" "syscall"
"github.com/FreifunkBremen/yanic/database" "github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/models" "github.com/FreifunkBremen/yanic/database/all"
"github.com/FreifunkBremen/yanic/meshviewer"
"github.com/FreifunkBremen/yanic/respond" "github.com/FreifunkBremen/yanic/respond"
"github.com/FreifunkBremen/yanic/rrd" "github.com/FreifunkBremen/yanic/rrd"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/yanic/webserver" "github.com/FreifunkBremen/yanic/webserver"
) )
var ( var (
configFile string configFile string
config *models.Config config *runtime.Config
collector *respond.Collector collector *respond.Collector
db *database.DB connections database.Connection
nodes *models.Nodes nodes *runtime.Nodes
) )
func main() { func main() {
@ -33,24 +35,31 @@ func main() {
if !timestamps { if !timestamps {
log.SetFlags(0) log.SetFlags(0)
} }
log.Println("Yanic say hello")
config = models.ReadConfigFile(configFile) config, err := runtime.ReadConfigFile(configFile)
if err != nil {
if config.Influxdb.Enable { panic(err)
db = database.New(config)
defer db.Close()
if importPath != "" {
importRRD(importPath)
return
}
} }
nodes = models.NewNodes(config) connections, err = all.Connect(config.Database.Connection)
if err != nil {
panic(err)
}
database.Start(connections, config)
defer database.Close(connections)
if connections != nil && importPath != "" {
importRRD(importPath)
return
}
nodes = runtime.NewNodes(config)
nodes.Start() nodes.Start()
meshviewer.Start(config, nodes)
if config.Respondd.Enable { if config.Respondd.Enable {
collector = respond.NewCollector(db, nodes, config.Respondd.Interface, config.Respondd.Port) collector = respond.NewCollector(connections, nodes, config.Respondd.Interface, config.Respondd.Port)
collector.Start(config.Respondd.CollectInterval.Duration) collector.Start(config.Respondd.CollectInterval.Duration)
defer collector.Close() defer collector.Close()
} }
@ -71,12 +80,10 @@ func main() {
func importRRD(path string) { func importRRD(path string) {
log.Println("importing RRD from", path) log.Println("importing RRD from", path)
for ds := range rrd.Read(path) { for ds := range rrd.Read(path) {
db.AddPoint( connections.AddStatistics(
database.MeasurementGlobal, &runtime.GlobalStats{
nil, Nodes: uint32(ds.Nodes),
map[string]interface{}{ Clients: uint32(ds.Clients),
"nodes": uint32(ds.Nodes),
"clients.total": uint32(ds.Clients),
}, },
ds.Time, ds.Time,
) )

View File

@ -18,20 +18,9 @@ bind = "127.0.0.1:8080"
webroot = "/var/www/html/meshviewer" webroot = "/var/www/html/meshviewer"
[nodes] [nodes]
enable = true 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
# version 2 is to support new version of meshviewer (which are in legacy develop branch or newer)
# i.e. https://github.com/ffnord/meshviewer/tree/dev
# https://github.com/ffrgb/meshviewer/tree/develop
nodes_version = 2
# path where to store nodes.json
nodes_path = "/var/www/html/meshviewer/data/nodes.json"
# path where to store graph.json
graph_path = "/var/www/html/meshviewer/data/graph.json"
# state-version of nodes.json to store cached data, # state-version of nodes.json to store cached data,
# these is the directly collected respondd data # these is the directly collected respondd data
state_path = "/var/lib/collector/state.json" state_path = "/var/lib/collector/state.json"
@ -46,20 +35,39 @@ offline_after = "10m"
prune_after = "7d" prune_after = "7d"
[meshviewer]
# 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
# version 2 is to support new version of meshviewer (which are in legacy develop branch or newer)
# i.e. https://github.com/ffnord/meshviewer/tree/dev
# https://github.com/ffrgb/meshviewer/tree/develop
version = 2
# path where to store nodes.json
nodes_path = "/var/www/html/meshviewer/data/nodes.json"
# path where to store graph.json
graph_path = "/var/www/html/meshviewer/data/graph.json"
[database]
# cleaning data of measurement node,
# which are older than 7d
delete_after = "7d"
# how often run the cleaning
delete_interval = "1h"
# Save collected data to InfluxDB # Save collected data to InfluxDB
# there would be the following measurments: # there would be the following measurments:
# node: store node spezific data i.e. clients memory, airtime # node: store node spezific data i.e. clients memory, airtime
# global: store global data, i.e. count of clients and nodes # global: store global data, i.e. count of clients and nodes
# firmware: store count of nodes tagged with firmware # firmware: store count of nodes tagged with firmware
# model: store count of nodes tagged with hardware model # model: store count of nodes tagged with hardware model
[influxdb] [[database.connection.influxdb]]
enable = false enable = false
address = "http://localhost:8086" address = "http://localhost:8086"
database = "ffhb" database = "ffhb"
username = "" username = ""
password = "" password = ""
# cleaning data of measurement node,
# which are older than 7d [[database.connection.logging]]
delete_after = "7d" enable = false
# how often run the cleaning path = "/var/log/yanic.log"
delete_interval = "1h"

55
database/all/internal.go Normal file
View File

@ -0,0 +1,55 @@
package all
import (
"time"
"github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/runtime"
)
type Connection struct {
database.Connection
list []database.Connection
}
func Connect(configuration interface{}) (database.Connection, error) {
var list []database.Connection
allConnection := configuration.(map[string][]interface{})
for dbType, conn := range database.Adapters {
dbConfigs := allConnection[dbType]
for _, config := range dbConfigs {
connected, err := conn(config)
if err != nil {
return nil, err
}
if connected == nil {
continue
}
list = append(list, connected)
}
}
return &Connection{list: list}, nil
}
func (conn *Connection) AddNode(nodeID string, node *runtime.Node) {
for _, item := range conn.list {
item.AddNode(nodeID, node)
}
}
func (conn *Connection) AddStatistics(stats *runtime.GlobalStats, time time.Time) {
for _, item := range conn.list {
item.AddStatistics(stats, time)
}
}
func (conn *Connection) DeleteNode(deleteAfter time.Duration) {
for _, item := range conn.list {
item.DeleteNode(deleteAfter)
}
}
func (conn *Connection) Close() {
for _, item := range conn.list {
item.Close()
}
}

6
database/all/main.go Normal file
View File

@ -0,0 +1,6 @@
package all
import (
_ "github.com/FreifunkBremen/yanic/database/influxdb"
_ "github.com/FreifunkBremen/yanic/database/logging"
)

View File

@ -1,165 +1,31 @@
package database package database
import ( import (
"fmt"
"log"
"sync"
"time" "time"
"github.com/influxdata/influxdb/client/v2" "github.com/FreifunkBremen/yanic/runtime"
imodels "github.com/influxdata/influxdb/models"
"github.com/FreifunkBremen/yanic/models"
) )
const ( // Connection interface to use for implementation in e.g. influxdb
MeasurementNode = "node" // Measurement for per-node statistics type Connection interface {
MeasurementGlobal = "global" // Measurement for summarized global statistics // AddNode data for a single node
MeasurementFirmware = "firmware" // Measurement for firmware statistics AddNode(nodeID string, node *runtime.Node)
MeasurementModel = "model" // Measurement for model statistics AddStatistics(stats *runtime.GlobalStats, time time.Time)
batchMaxSize = 500
batchTimeout = 5 * time.Second
)
type DB struct { DeleteNode(deleteAfter time.Duration)
config *models.Config
client client.Client Close()
points chan *client.Point
wg sync.WaitGroup
quit chan struct{}
} }
func New(config *models.Config) *DB { // Connect function with config to get DB connection interface
// Make client type Connect func(config interface{}) (Connection, error)
c, err := client.NewHTTPClient(client.HTTPConfig{
Addr: config.Influxdb.Address,
Username: config.Influxdb.Username,
Password: config.Influxdb.Password,
})
if err != nil { /*
panic(err) * for selfbinding in use of the package all
} */
db := &DB{ var Adapters = map[string]Connect{}
config: config,
client: c,
points: make(chan *client.Point, 1000),
quit: make(chan struct{}),
}
db.wg.Add(1) func AddDatabaseType(name string, n Connect) {
go db.addWorker() Adapters[name] = n
go db.deleteWorker()
return db
}
func (db *DB) DeletePoints() {
query := fmt.Sprintf("delete from %s where time < now() - %ds", MeasurementNode, db.config.Influxdb.DeleteAfter.Duration/time.Second)
db.client.Query(client.NewQuery(query, db.config.Influxdb.Database, "m"))
}
func (db *DB) AddPoint(name string, tags imodels.Tags, fields imodels.Fields, time time.Time) {
point, err := client.NewPoint(name, tags.Map(), fields, time)
if err != nil {
panic(err)
}
db.points <- point
}
// Saves the values of a CounterMap in the database.
// The key are used as 'value' tag.
// The value is used as 'counter' field.
func (db *DB) AddCounterMap(name string, m models.CounterMap) {
now := time.Now()
for key, count := range m {
db.AddPoint(
name,
imodels.Tags{
imodels.Tag{Key: []byte("value"), Value: []byte(key)},
},
imodels.Fields{"count": count},
now,
)
}
}
// Add data for a single node
func (db *DB) Add(nodeID string, node *models.Node) {
tags, fields := node.ToInflux()
db.AddPoint(MeasurementNode, tags, fields, time.Now())
}
// Close all connection and clean up
func (db *DB) Close() {
close(db.quit)
close(db.points)
db.wg.Wait()
db.client.Close()
}
// prunes node-specific data periodically
func (db *DB) deleteWorker() {
ticker := time.NewTicker(db.config.Influxdb.DeleteInterval.Duration)
for {
select {
case <-ticker.C:
db.DeletePoints()
case <-db.quit:
ticker.Stop()
return
}
}
}
// stores data points in batches into the influxdb
func (db *DB) addWorker() {
bpConfig := client.BatchPointsConfig{
Database: db.config.Influxdb.Database,
Precision: "m",
}
var bp client.BatchPoints
var err error
var writeNow, closed bool
timer := time.NewTimer(batchTimeout)
for !closed {
// wait for new points
select {
case point, ok := <-db.points:
if ok {
if bp == nil {
// create new batch
timer.Reset(batchTimeout)
if bp, err = client.NewBatchPoints(bpConfig); err != nil {
log.Fatal(err)
}
}
bp.AddPoint(point)
} else {
closed = true
}
case <-timer.C:
if bp == nil {
timer.Reset(batchTimeout)
} else {
writeNow = true
}
}
// write batch now?
if bp != nil && (writeNow || closed || len(bp.Points()) >= batchMaxSize) {
log.Println("saving", len(bp.Points()), "points")
if err = db.client.Write(bp); err != nil {
log.Fatal(err)
}
writeNow = false
bp = nil
}
}
timer.Stop()
db.wg.Done()
} }

View File

@ -0,0 +1,182 @@
package influxdb
import (
"fmt"
"log"
"sync"
"time"
"github.com/influxdata/influxdb/client/v2"
"github.com/influxdata/influxdb/models"
"github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/runtime"
)
const (
MeasurementNode = "node" // Measurement for per-node statistics
MeasurementGlobal = "global" // Measurement for summarized global statistics
CounterMeasurementFirmware = "firmware" // Measurement for firmware statistics
CounterMeasurementModel = "model" // Measurement for model statistics
batchMaxSize = 500
batchTimeout = 5 * time.Second
)
type Connection struct {
database.Connection
config Config
client client.Client
points chan *client.Point
wg sync.WaitGroup
}
type Config map[string]interface{}
func (c Config) Enable() bool {
return c["enable"].(bool)
}
func (c Config) Address() string {
return c["address"].(string)
}
func (c Config) Database() string {
return c["database"].(string)
}
func (c Config) Username() string {
return c["username"].(string)
}
func (c Config) Password() string {
return c["password"].(string)
}
func init() {
database.AddDatabaseType("influxdb", Connect)
}
func Connect(configuration interface{}) (database.Connection, error) {
var config Config
config = configuration.(map[string]interface{})
if !config.Enable() {
return nil, nil
}
// Make client
c, err := client.NewHTTPClient(client.HTTPConfig{
Addr: config.Address(),
Username: config.Username(),
Password: config.Password(),
})
if err != nil {
return nil, err
}
db := &Connection{
config: config,
client: c,
points: make(chan *client.Point, 1000),
}
db.wg.Add(1)
go db.addWorker()
return db, nil
}
func (conn *Connection) DeleteNode(deleteAfter time.Duration) {
query := fmt.Sprintf("delete from %s where time < now() - %ds", MeasurementNode, deleteAfter/time.Second)
conn.client.Query(client.NewQuery(query, conn.config.Database(), "m"))
}
func (conn *Connection) addPoint(name string, tags models.Tags, fields models.Fields, time time.Time) {
point, err := client.NewPoint(name, tags.Map(), fields, time)
if err != nil {
panic(err)
}
conn.points <- point
}
// Saves the values of a CounterMap in the database.
// The key are used as 'value' tag.
// The value is used as 'counter' field.
func (conn *Connection) addCounterMap(name string, m runtime.CounterMap) {
now := time.Now()
for key, count := range m {
conn.addPoint(
name,
models.Tags{
models.Tag{Key: []byte("value"), Value: []byte(key)},
},
models.Fields{"count": count},
now,
)
}
}
// AddStatistics implementation of database
func (conn *Connection) AddStatistics(stats *runtime.GlobalStats, time time.Time) {
conn.addPoint(MeasurementGlobal, nil, GlobalStatsFields(stats), time)
conn.addCounterMap(CounterMeasurementModel, stats.Models)
conn.addCounterMap(CounterMeasurementFirmware, stats.Firmwares)
}
// AddNode implementation of database
func (conn *Connection) AddNode(nodeID string, node *runtime.Node) {
tags, fields := nodeToInflux(node)
conn.addPoint(MeasurementNode, tags, fields, time.Now())
}
// Close all connection and clean up
func (conn *Connection) Close() {
close(conn.points)
conn.wg.Wait()
conn.client.Close()
}
// stores data points in batches into the influxdb
func (conn *Connection) addWorker() {
bpConfig := client.BatchPointsConfig{
Database: conn.config.Database(),
Precision: "m",
}
var bp client.BatchPoints
var err error
var writeNow, closed bool
timer := time.NewTimer(batchTimeout)
for !closed {
// wait for new points
select {
case point, ok := <-conn.points:
if ok {
if bp == nil {
// create new batch
timer.Reset(batchTimeout)
if bp, err = client.NewBatchPoints(bpConfig); err != nil {
log.Fatal(err)
}
}
bp.AddPoint(point)
} else {
closed = true
}
case <-timer.C:
if bp == nil {
timer.Reset(batchTimeout)
} else {
writeNow = true
}
}
// write batch now?
if bp != nil && (writeNow || closed || len(bp.Points()) >= batchMaxSize) {
log.Println("saving", len(bp.Points()), "points")
if err = conn.client.Write(bp); err != nil {
log.Fatal(err)
}
writeNow = false
bp = nil
}
}
timer.Stop()
conn.wg.Done()
}

View File

@ -1,29 +1,15 @@
package models package influxdb
import ( import (
"net"
"strconv" "strconv"
imodels "github.com/influxdata/influxdb/models" models "github.com/influxdata/influxdb/models"
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/yanic/jsontime"
"github.com/FreifunkBremen/yanic/meshviewer"
) )
// Node struct // NodeToInflux Returns tags and fields for InfluxDB
type Node struct { func nodeToInflux(node *runtime.Node) (tags models.Tags, fields models.Fields) {
Address net.IP `json:"address"` // the last known IP address
Firstseen jsontime.Time `json:"firstseen"`
Lastseen jsontime.Time `json:"lastseen"`
Flags meshviewer.Flags `json:"flags"`
Statistics *data.Statistics `json:"statistics"`
Nodeinfo *data.NodeInfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"-"`
}
// ToInflux Returns tags and fields for InfluxDB
func (node *Node) ToInflux() (tags imodels.Tags, fields imodels.Fields) {
stats := node.Statistics stats := node.Statistics
tags.SetString("nodeid", stats.NodeID) tags.SetString("nodeid", stats.NodeID)

View File

@ -1,4 +1,4 @@
package models package influxdb
import ( import (
"testing" "testing"
@ -6,12 +6,13 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/runtime"
) )
func TestToInflux(t *testing.T) { func TestToInflux(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
node := Node{ node := &runtime.Node{
Statistics: &data.Statistics{ Statistics: &data.Statistics{
NodeID: "foobar", NodeID: "foobar",
LoadAverage: 0.5, LoadAverage: 0.5,
@ -65,7 +66,7 @@ func TestToInflux(t *testing.T) {
}, },
} }
tags, fields := node.ToInflux() tags, fields := nodeToInflux(node)
assert.Equal("foobar", tags.GetString("nodeid")) assert.Equal("foobar", tags.GetString("nodeid"))
assert.Equal("nobody", tags.GetString("owner")) assert.Equal("nobody", tags.GetString("owner"))

View File

@ -0,0 +1,17 @@
package influxdb
import (
"github.com/FreifunkBremen/yanic/runtime"
)
// GlobalStatsFields returns fields for InfluxDB
func GlobalStatsFields(stats *runtime.GlobalStats) map[string]interface{} {
return map[string]interface{}{
"nodes": stats.Nodes,
"gateways": stats.Gateways,
"clients.total": stats.Clients,
"clients.wifi": stats.ClientsWifi,
"clients.wifi24": stats.ClientsWifi24,
"clients.wifi5": stats.ClientsWifi5,
}
}

View File

@ -0,0 +1,63 @@
package influxdb
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/runtime"
)
func TestGlobalStats(t *testing.T) {
stats := runtime.NewGlobalStats(createTestNodes())
assert := assert.New(t)
fields := GlobalStatsFields(stats)
// check fields
assert.EqualValues(3, fields["nodes"])
}
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
}

40
database/internal.go Normal file
View File

@ -0,0 +1,40 @@
package database
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(conn Connection, config *runtime.Config) {
quit = make(chan struct{})
go deleteWorker(conn, config.Database.DeleteInterval.Duration, config.Database.DeleteAfter.Duration)
}
func Close(conn Connection) {
if quit != nil {
close(quit)
}
if conn != nil {
conn.Close()
}
}
// prunes node-specific data periodically
func deleteWorker(conn Connection, deleteInterval time.Duration, deleteAfter time.Duration) {
ticker := time.NewTicker(deleteInterval)
for {
select {
case <-ticker.C:
conn.DeleteNode(deleteAfter)
case <-quit:
ticker.Stop()
return
}
}
}

71
database/logging/file.go Normal file
View File

@ -0,0 +1,71 @@
package logging
/**
* This database type is just for,
* - debugging without a influxconn
* - example for other developers for new databases
*/
import (
"fmt"
"log"
"os"
"time"
"github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/runtime"
)
type Connection struct {
database.Connection
config Config
file *os.File
}
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() {
database.AddDatabaseType("logging", Connect)
}
func Connect(configuration interface{}) (database.Connection, error) {
var config Config
config = configuration.(map[string]interface{})
if !config.Enable() {
return nil, nil
}
file, err := os.OpenFile(config.Path(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
return &Connection{config: config, file: file}, nil
}
func (conn *Connection) AddNode(nodeID string, node *runtime.Node) {
conn.log("AddNode: [", nodeID, "] clients: ", node.Statistics.Clients.Total)
}
func (conn *Connection) AddStatistics(stats *runtime.GlobalStats, time time.Time) {
conn.log("AddStatistics: [", time.String(), "] nodes: ", stats.Nodes, ", clients: ", stats.Clients, " models: ", len(stats.Models))
}
func (conn *Connection) DeleteNode(deleteAfter time.Duration) {
conn.log("DeleteNode")
}
func (conn *Connection) Close() {
conn.log("Close")
conn.file.Close()
}
func (conn *Connection) log(v ...interface{}) {
log.Println(v)
conn.file.WriteString(fmt.Sprintln("[", time.Now().String(), "]", v))
}

View File

@ -1,8 +1,10 @@
package models package meshviewer
import ( import (
"fmt" "fmt"
"strings" "strings"
"github.com/FreifunkBremen/yanic/runtime"
) )
// Graph a struct for all links between the nodes // Graph a struct for all links between the nodes
@ -40,7 +42,7 @@ type graphBuilder struct {
} }
// BuildGraph transform from nodes (Neighbours) to Graph // BuildGraph transform from nodes (Neighbours) to Graph
func (nodes *Nodes) BuildGraph() *Graph { func BuildGraph(nodes *runtime.Nodes) *Graph {
builder := &graphBuilder{ builder := &graphBuilder{
macToID: make(map[string]string), macToID: make(map[string]string),
idToMac: make(map[string]string), idToMac: make(map[string]string),
@ -56,7 +58,7 @@ func (nodes *Nodes) BuildGraph() *Graph {
return graph return graph
} }
func (builder *graphBuilder) readNodes(nodes map[string]*Node) { func (builder *graphBuilder) readNodes(nodes map[string]*runtime.Node) {
// Fill mac->id map // Fill mac->id map
for sourceID, node := range nodes { for sourceID, node := range nodes {
if nodeinfo := node.Nodeinfo; nodeinfo != nil { if nodeinfo := node.Nodeinfo; nodeinfo != nil {
@ -90,7 +92,7 @@ func (builder *graphBuilder) readNodes(nodes map[string]*Node) {
// Add links // Add links
for sourceID, node := range nodes { for sourceID, node := range nodes {
if node.Flags.Online { if node.Online {
if neighbours := node.Neighbours; neighbours != nil { if neighbours := node.Neighbours; neighbours != nil {
// Batman neighbours // Batman neighbours
for _, batadvNeighbours := range neighbours.Batadv { for _, batadvNeighbours := range neighbours.Batadv {

View File

@ -1,4 +1,4 @@
package models package meshviewer
import ( import (
"encoding/json" "encoding/json"
@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/runtime"
) )
type TestNode struct { type TestNode struct {
@ -19,7 +20,7 @@ func TestGenerateGraph(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
nodes := testGetNodesByFile("node1.json", "node2.json", "node3.json", "node4.json") nodes := testGetNodesByFile("node1.json", "node2.json", "node3.json", "node4.json")
graph := nodes.BuildGraph() graph := BuildGraph(nodes)
assert.NotNil(graph) assert.NotNil(graph)
assert.Equal(1, graph.Version, "Wrong Version") assert.Equal(1, graph.Version, "Wrong Version")
assert.NotNil(graph.Batadv, "no Batadv") assert.NotNil(graph.Batadv, "no Batadv")
@ -30,10 +31,10 @@ func TestGenerateGraph(t *testing.T) {
// TODO more tests required // TODO more tests required
} }
func testGetNodesByFile(files ...string) *Nodes { func testGetNodesByFile(files ...string) *runtime.Nodes {
nodes := &Nodes{ nodes := &runtime.Nodes{
List: make(map[string]*Node), List: make(map[string]*runtime.Node),
} }
for _, file := range files { for _, file := range files {
@ -47,17 +48,17 @@ func testGetNodesByFile(files ...string) *Nodes {
return nodes return nodes
} }
func testGetNodeByFile(filename string) *Node { func testGetNodeByFile(filename string) *runtime.Node {
testnode := &TestNode{} testnode := &TestNode{}
testfile(filename, testnode) testfile(filename, testnode)
return &Node{ return &runtime.Node{
Nodeinfo: testnode.Nodeinfo, Nodeinfo: testnode.Nodeinfo,
Neighbours: testnode.Neighbours, Neighbours: testnode.Neighbours,
} }
} }
func testfile(name string, obj interface{}) { func testfile(name string, obj interface{}) {
file, err := ioutil.ReadFile("testdata/" + name) file, err := ioutil.ReadFile("../runtime/testdata/" + name)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -21,22 +21,6 @@ type Flags struct {
Gateway bool `json:"gateway"` Gateway bool `json:"gateway"`
} }
// NodesV1 struct, to support legacy meshviewer (which are in master branch)
// i.e. https://github.com/ffnord/meshviewer/tree/master
type NodesV1 struct {
Version int `json:"version"`
Timestamp jsontime.Time `json:"timestamp"` // Timestamp of the generation
List map[string]*Node `json:"nodes"` // the current nodemap, indexed by node ID
}
// NodesV2 struct, to support new version of meshviewer (which are in legacy develop branch or newer)
// i.e. https://github.com/ffnord/meshviewer/tree/dev or https://github.com/ffrgb/meshviewer/tree/develop
type NodesV2 struct {
Version int `json:"version"`
Timestamp jsontime.Time `json:"timestamp"` // Timestamp of the generation
List []*Node `json:"nodes"` // the current nodemap, as array
}
// Statistics a meshviewer spezifisch struct, diffrent from respondd // Statistics a meshviewer spezifisch struct, diffrent from respondd
type Statistics struct { type Statistics struct {
NodeID string `json:"node_id"` NodeID string `json:"node_id"`

117
meshviewer/nodes.go Normal file
View File

@ -0,0 +1,117 @@
package meshviewer
import (
"log"
"time"
"github.com/FreifunkBremen/yanic/jsontime"
"github.com/FreifunkBremen/yanic/runtime"
)
// NodesV1 struct, to support legacy meshviewer (which are in master branch)
// i.e. https://github.com/ffnord/meshviewer/tree/master
type NodesV1 struct {
Version int `json:"version"`
Timestamp jsontime.Time `json:"timestamp"` // Timestamp of the generation
List map[string]*Node `json:"nodes"` // the current nodemap, indexed by node ID
}
// NodesV2 struct, to support new version of meshviewer (which are in legacy develop branch or newer)
// i.e. https://github.com/ffnord/meshviewer/tree/dev or https://github.com/ffrgb/meshviewer/tree/develop
type NodesV2 struct {
Version int `json:"version"`
Timestamp jsontime.Time `json:"timestamp"` // Timestamp of the generation
List []*Node `json:"nodes"` // the current nodemap, as array
}
// GetNodesV1 transform data to legacy meshviewer
func GetNodesV1(nodes *runtime.Nodes) *NodesV1 {
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 {
continue
}
node := &Node{
Firstseen: nodeOrigin.Firstseen,
Lastseen: nodeOrigin.Lastseen,
Flags: Flags{
Online: nodeOrigin.Online,
Gateway: nodeOrigin.Gateway,
},
Nodeinfo: nodeOrigin.Nodeinfo,
}
node.Statistics = NewStatistics(nodeOrigin.Statistics)
meshviewerNodes.List[nodeID] = node
}
return meshviewerNodes
}
// GetNodesV2 transform data to modern meshviewers
func GetNodesV2(nodes *runtime.Nodes) *NodesV2 {
meshviewerNodes := &NodesV2{
Version: 2,
Timestamp: jsontime.Now(),
}
for nodeID := range nodes.List {
nodeOrigin := nodes.List[nodeID]
if nodeOrigin.Statistics == nil {
continue
}
node := &Node{
Firstseen: nodeOrigin.Firstseen,
Lastseen: nodeOrigin.Lastseen,
Flags: Flags{
Online: nodeOrigin.Online,
Gateway: nodeOrigin.Gateway,
},
Nodeinfo: nodeOrigin.Nodeinfo,
}
node.Statistics = NewStatistics(nodeOrigin.Statistics)
meshviewerNodes.List = append(meshviewerNodes.List, node)
}
return meshviewerNodes
}
// 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
switch version {
case 1:
runtime.SaveJSON(GetNodesV1(nodes), path)
case 2:
runtime.SaveJSON(GetNodesV2(nodes), path)
default:
log.Panicf("invalid nodes version: %d", version)
}
}
if path := config.Meshviewer.GraphPath; path != "" {
runtime.SaveJSON(BuildGraph(nodes), path)
}
}

66
meshviewer/nodes_test.go Normal file
View File

@ -0,0 +1,66 @@
package meshviewer
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/runtime"
)
func TestNodesV1(t *testing.T) {
nodes := GetNodesV1(createTestNodes())
assert := assert.New(t)
assert.Len(nodes.List, 2)
}
func TestNodesV2(t *testing.T) {
nodes := GetNodesV2(createTestNodes())
assert := assert.New(t)
assert.Len(nodes.List, 2)
}
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
}

View File

@ -1,56 +0,0 @@
package models
import (
"io/ioutil"
"github.com/influxdata/toml"
)
//Config the config File of this daemon
type Config struct {
Respondd struct {
Enable bool
Interface string
Port int
CollectInterval Duration
}
Webserver struct {
Enable bool
Bind string
Webroot string
}
Nodes struct {
Enable bool
NodesVersion int
NodesPath string
GraphPath string
StatePath string
SaveInterval Duration // Save nodes periodically
OfflineAfter Duration // Set node to offline if not seen within this period
PruneAfter Duration // Remove nodes after n days of inactivity
}
Influxdb struct {
Enable bool
Address string
Database string
Username string
Password string
DeleteInterval Duration // Delete stats of nodes every n minutes
DeleteAfter Duration // Delete stats of nodes till now-deletetill n minutes
}
}
// ReadConfigFile reads a config model from path of a yml file
func ReadConfigFile(path string) *Config {
config := &Config{}
file, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
if err := toml.Unmarshal(file, config); err != nil {
panic(err)
}
return config
}

View File

@ -1,23 +0,0 @@
package models
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestReadConfig(t *testing.T) {
assert := assert.New(t)
config := ReadConfigFile("../config_example.toml")
assert.NotNil(config)
assert.True(config.Respondd.Enable)
assert.Equal("eth0", config.Respondd.Interface)
assert.Equal(time.Minute, config.Respondd.CollectInterval.Duration)
assert.Equal(2, config.Nodes.NodesVersion)
assert.Equal("/var/www/html/meshviewer/data/nodes.json", config.Nodes.NodesPath)
assert.Equal(time.Hour*24*7, config.Nodes.PruneAfter.Duration)
}

View File

@ -4,15 +4,15 @@ import (
"bytes" "bytes"
"compress/flate" "compress/flate"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"net" "net"
"time" "time"
"fmt"
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/database" "github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/jsontime" "github.com/FreifunkBremen/yanic/jsontime"
"github.com/FreifunkBremen/yanic/models" "github.com/FreifunkBremen/yanic/runtime"
) )
// Collector for a specificle respond messages // Collector for a specificle respond messages
@ -20,14 +20,14 @@ type Collector struct {
connection *net.UDPConn // UDP socket connection *net.UDPConn // UDP socket
queue chan *Response // received responses queue chan *Response // received responses
iface string iface string
db *database.DB db database.Connection
nodes *models.Nodes nodes *runtime.Nodes
interval time.Duration // Interval for multicast packets interval time.Duration // Interval for multicast packets
stop chan interface{} stop chan interface{}
} }
// NewCollector creates a Collector struct // NewCollector creates a Collector struct
func NewCollector(db *database.DB, nodes *models.Nodes, iface string, port int) *Collector { func NewCollector(db database.Connection, nodes *runtime.Nodes, iface string, port int) *Collector {
linkLocalAddr, err := getLinkLocalAddr(iface) linkLocalAddr, err := getLinkLocalAddr(iface)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
@ -125,7 +125,7 @@ func (coll *Collector) sendUnicasts(seenBefore jsontime.Time) {
seenAfter := seenBefore.Add(-time.Minute * 10) seenAfter := seenBefore.Add(-time.Minute * 10)
// Select online nodes that has not been seen recently // Select online nodes that has not been seen recently
nodes := coll.nodes.Select(func(n *models.Node) bool { nodes := coll.nodes.Select(func(n *runtime.Node) bool {
return n.Lastseen.After(seenAfter) && n.Lastseen.Before(seenBefore) && n.Address != nil return n.Lastseen.After(seenAfter) && n.Lastseen.Before(seenBefore) && n.Address != nil
}) })
@ -210,7 +210,7 @@ func (coll *Collector) saveResponse(addr net.UDPAddr, res *data.ResponseData) {
// Store statistics in InfluxDB // Store statistics in InfluxDB
if coll.db != nil && node.Statistics != nil { if coll.db != nil && node.Statistics != nil {
coll.db.Add(nodeID, node) coll.db.AddNode(nodeID, node)
} }
} }
@ -249,9 +249,7 @@ func (coll *Collector) globalStatsWorker() {
// saves global statistics // saves global statistics
func (coll *Collector) saveGlobalStats() { func (coll *Collector) saveGlobalStats() {
stats := models.NewGlobalStats(coll.nodes) stats := runtime.NewGlobalStats(coll.nodes)
coll.db.AddPoint(database.MeasurementGlobal, nil, stats.Fields(), time.Now()) coll.db.AddStatistics(stats, time.Now())
coll.db.AddCounterMap(database.MeasurementFirmware, stats.Firmwares)
coll.db.AddCounterMap(database.MeasurementModel, stats.Models)
} }

56
runtime/config.go Normal file
View File

@ -0,0 +1,56 @@
package runtime
import (
"io/ioutil"
"github.com/BurntSushi/toml"
)
//Config the config File of this daemon
type Config struct {
Respondd struct {
Enable bool `toml:"enable"`
Interface string `toml:"interface"`
Port int `toml:"port"`
CollectInterval Duration `toml:"collect_interval"`
}
Webserver struct {
Enable bool `toml:"enable"`
Bind string `toml:"bind"`
Webroot string `toml:"webroot"`
}
Nodes struct {
Enable bool `toml:"enable"`
StatePath string `toml:"state_path"`
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
}
Meshviewer struct {
Version int `toml:"version"`
NodesPath string `toml:"nodes_path"`
GraphPath string `toml:"graph_path"`
}
Database struct {
DeleteInterval Duration `toml:"delete_interval"` // Delete stats of nodes every n minutes
DeleteAfter Duration `toml:"delete_after"` // Delete stats of nodes till now-deletetill n minutes
Connection map[string][]interface{}
}
}
// ReadConfigFile reads a config model from path of a yml file
func ReadConfigFile(path string) (config *Config, err error) {
config = &Config{}
file, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
err = toml.Unmarshal(file, config)
if err != nil {
panic(err)
}
return
}

33
runtime/config_test.go Normal file
View File

@ -0,0 +1,33 @@
package runtime
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestReadConfig(t *testing.T) {
assert := assert.New(t)
config, err := ReadConfigFile("../config_example.toml")
assert.NoError(err, "no error during reading")
assert.NotNil(config)
assert.True(config.Respondd.Enable)
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 influxdb map[string]interface{}
dbs := config.Database.Connection["influxdb"]
assert.Len(dbs, 1, "more influxdb are given")
influxdb = dbs[0].(map[string]interface{})
assert.Equal(influxdb["database"], "ffhb")
}

View File

@ -1,4 +1,4 @@
package models package runtime
import ( import (
"fmt" "fmt"
@ -16,15 +16,21 @@ type Duration struct {
} }
// UnmarshalTOML parses a duration string. // UnmarshalTOML parses a duration string.
func (d *Duration) UnmarshalTOML(data []byte) error { func (d *Duration) UnmarshalTOML(dataInterface interface{}) error {
var data string
switch dataInterface.(type) {
case string:
data = dataInterface.(string)
default:
return fmt.Errorf("invalid duration: \"%s\"", dataInterface)
}
// " + int + unit + " // " + int + unit + "
if len(data) < 4 { if len(data) < 2 {
return fmt.Errorf("invalid duration: %s", data) return fmt.Errorf("invalid duration: \"%s\"", data)
} }
unit := data[len(data)-2] unit := data[len(data)-1]
value, err := strconv.Atoi(string(data[1 : len(data)-2])) value, err := strconv.Atoi(string(data[:len(data)-1]))
if err != nil { if err != nil {
return fmt.Errorf("unable to parse duration %s: %s", data, err) return fmt.Errorf("unable to parse duration %s: %s", data, err)
} }

View File

@ -1,4 +1,4 @@
package models package runtime
import ( import (
"testing" "testing"
@ -34,7 +34,7 @@ func TestDuration(t *testing.T) {
for _, test := range tests { for _, test := range tests {
d := Duration{} d := Duration{}
err := d.UnmarshalTOML([]byte("\"" + test.input + "\"")) err := d.UnmarshalTOML(test.input)
duration := d.Duration duration := d.Duration
if test.err == "" { if test.err == "" {

20
runtime/node.go Normal file
View File

@ -0,0 +1,20 @@
package runtime
import (
"net"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/jsontime"
)
// Node struct
type Node struct {
Address net.IP `json:"address"` // the last known IP address
Firstseen jsontime.Time `json:"firstseen"`
Lastseen jsontime.Time `json:"lastseen"`
Online bool `json:"online"`
Gateway bool `json:"gateway"`
Statistics *data.Statistics `json:"statistics"`
Nodeinfo *data.NodeInfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"-"`
}

View File

@ -1,4 +1,4 @@
package models package runtime
import ( import (
"encoding/json" "encoding/json"
@ -9,7 +9,6 @@ import (
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/jsontime" "github.com/FreifunkBremen/yanic/jsontime"
"github.com/FreifunkBremen/yanic/meshviewer"
) )
// Nodes struct: cache DB of Node's structs // Nodes struct: cache DB of Node's structs
@ -54,7 +53,7 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
nodes.Unlock() nodes.Unlock()
node.Lastseen = now node.Lastseen = now
node.Flags.Online = true node.Online = true
// Update neighbours // Update neighbours
if val := res.Neighbours; val != nil { if val := res.Neighbours; val != nil {
@ -64,7 +63,7 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
// Update nodeinfo // Update nodeinfo
if val := res.NodeInfo; val != nil { if val := res.NodeInfo; val != nil {
node.Nodeinfo = val node.Nodeinfo = val
node.Flags.Gateway = val.VPN node.Gateway = val.VPN
} }
// Update statistics // Update statistics
@ -81,57 +80,6 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
return node return node
} }
// GetNodesV1 transform data to legacy meshviewer
func (nodes *Nodes) GetNodesV1() *meshviewer.NodesV1 {
meshviewerNodes := &meshviewer.NodesV1{
Version: 1,
List: make(map[string]*meshviewer.Node),
Timestamp: jsontime.Now(),
}
for nodeID := range nodes.List {
nodeOrigin := nodes.List[nodeID]
if nodeOrigin.Statistics == nil {
continue
}
node := &meshviewer.Node{
Firstseen: nodeOrigin.Firstseen,
Lastseen: nodeOrigin.Lastseen,
Flags: nodeOrigin.Flags,
Nodeinfo: nodeOrigin.Nodeinfo,
}
node.Statistics = meshviewer.NewStatistics(nodeOrigin.Statistics)
meshviewerNodes.List[nodeID] = node
}
return meshviewerNodes
}
// GetNodesV2 transform data to modern meshviewers
func (nodes *Nodes) GetNodesV2() *meshviewer.NodesV2 {
meshviewerNodes := &meshviewer.NodesV2{
Version: 2,
Timestamp: jsontime.Now(),
}
for nodeID := range nodes.List {
nodeOrigin := nodes.List[nodeID]
if nodeOrigin.Statistics == nil {
continue
}
node := &meshviewer.Node{
Firstseen: nodeOrigin.Firstseen,
Lastseen: nodeOrigin.Lastseen,
Flags: nodeOrigin.Flags,
Nodeinfo: nodeOrigin.Nodeinfo,
}
node.Statistics = meshviewer.NewStatistics(nodeOrigin.Statistics)
meshviewerNodes.List = append(meshviewerNodes.List, node)
}
return meshviewerNodes
}
// Select selects a list of nodes to be returned // Select selects a list of nodes to be returned
func (nodes *Nodes) Select(f func(*Node) bool) []*Node { func (nodes *Nodes) Select(f func(*Node) bool) []*Node {
nodes.RLock() nodes.RLock()
@ -180,7 +128,7 @@ func (nodes *Nodes) expire() {
delete(nodes.List, id) delete(nodes.List, id)
} else if node.Lastseen.Before(offlineAfter) { } else if node.Lastseen.Before(offlineAfter) {
// set to offline // set to offline
node.Flags.Online = false node.Online = false
} }
} }
} }
@ -205,26 +153,11 @@ func (nodes *Nodes) save() {
defer nodes.RUnlock() defer nodes.RUnlock()
// serialize nodes // serialize nodes
save(nodes, nodes.config.Nodes.StatePath) SaveJSON(nodes, nodes.config.Nodes.StatePath)
if path := nodes.config.Nodes.NodesPath; path != "" {
version := nodes.config.Nodes.NodesVersion
switch version {
case 1:
save(nodes.GetNodesV1(), path)
case 2:
save(nodes.GetNodesV2(), path)
default:
log.Panicf("invalid nodes version: %d", version)
}
}
if path := nodes.config.Nodes.GraphPath; path != "" {
save(nodes.BuildGraph(), path)
}
} }
func save(input interface{}, outputFile string) { // SaveJSON to path
func SaveJSON(input interface{}, outputFile string) {
tmpFile := outputFile + ".tmp" tmpFile := outputFile + ".tmp"
f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)

View File

@ -1,4 +1,4 @@
package models package runtime
import ( import (
"io/ioutil" "io/ioutil"
@ -38,11 +38,11 @@ func TestExpire(t *testing.T) {
// one offline? // one offline?
assert.NotNil(nodes.List["offline"]) assert.NotNil(nodes.List["offline"])
assert.False(nodes.List["offline"].Flags.Online) assert.False(nodes.List["offline"].Online)
// one online? // one online?
assert.NotNil(nodes.List["online"]) assert.NotNil(nodes.List["online"])
assert.True(nodes.List["online"].Flags.Online) assert.True(nodes.List["online"].Online)
} }
func TestLoadAndSave(t *testing.T) { func TestLoadAndSave(t *testing.T) {
@ -55,7 +55,7 @@ func TestLoadAndSave(t *testing.T) {
nodes.load() nodes.load()
tmpfile, _ := ioutil.TempFile("/tmp", "nodes") tmpfile, _ := ioutil.TempFile("/tmp", "nodes")
save(nodes, tmpfile.Name()) SaveJSON(nodes, tmpfile.Name())
os.Remove(tmpfile.Name()) os.Remove(tmpfile.Name())
assert.Len(nodes.List, 1) assert.Len(nodes.List, 1)

View File

@ -1,4 +1,4 @@
package models package runtime
// CounterMap to manage multiple values // CounterMap to manage multiple values
type CounterMap map[string]uint32 type CounterMap map[string]uint32
@ -25,7 +25,7 @@ func NewGlobalStats(nodes *Nodes) (result *GlobalStats) {
nodes.Lock() nodes.Lock()
for _, node := range nodes.List { for _, node := range nodes.List {
if node.Flags.Online { if node.Online {
result.Nodes++ result.Nodes++
if stats := node.Statistics; stats != nil { if stats := node.Statistics; stats != nil {
result.Clients += stats.Clients.Total result.Clients += stats.Clients.Total
@ -33,7 +33,7 @@ func NewGlobalStats(nodes *Nodes) (result *GlobalStats) {
result.ClientsWifi5 += stats.Clients.Wifi5 result.ClientsWifi5 += stats.Clients.Wifi5
result.ClientsWifi += stats.Clients.Wifi result.ClientsWifi += stats.Clients.Wifi
} }
if node.Flags.Gateway { if node.Gateway {
result.Gateways++ result.Gateways++
} }
if info := node.Nodeinfo; info != nil { if info := node.Nodeinfo; info != nil {
@ -54,15 +54,3 @@ func (m CounterMap) Increment(key string) {
m[key] = val + 1 m[key] = val + 1
} }
} }
// Fields returns fields for InfluxDB
func (stats *GlobalStats) Fields() map[string]interface{} {
return map[string]interface{}{
"nodes": stats.Nodes,
"gateways": stats.Gateways,
"clients.total": stats.Clients,
"clients.wifi": stats.ClientsWifi,
"clients.wifi24": stats.ClientsWifi24,
"clients.wifi5": stats.ClientsWifi5,
}
}

View File

@ -1,4 +1,4 @@
package models package runtime
import ( import (
"testing" "testing"
@ -24,24 +24,6 @@ func TestGlobalStats(t *testing.T) {
// check firmwares // check firmwares
assert.Len(stats.Firmwares, 1) assert.Len(stats.Firmwares, 1)
assert.EqualValues(1, stats.Firmwares["2016.1.6+entenhausen1"]) assert.EqualValues(1, stats.Firmwares["2016.1.6+entenhausen1"])
fields := stats.Fields()
// check fields
assert.EqualValues(3, fields["nodes"])
}
func TestNodesV1(t *testing.T) {
nodes := createTestNodes().GetNodesV1()
assert := assert.New(t)
assert.Len(nodes.List, 2)
}
func TestNodesV2(t *testing.T) {
nodes := createTestNodes().GetNodesV2()
assert := assert.New(t)
assert.Len(nodes.List, 2)
} }
func createTestNodes() *Nodes { func createTestNodes() *Nodes {