Compare commits

..

19 Commits

Author SHA1 Message Date
e351a9d1a5
Merge pull request #2 from FreifunkBremen/master
Updaten auf upstream
2019-11-23 13:02:40 +01:00
genofire
edfb403884
[DOC] misspell 2019-11-13 13:29:46 +01:00
genofire
f3da33b15a
[BUGFIX] package name in output raw 2019-11-13 12:37:54 +01:00
genofire
f58ad00fec
[TEST] add output raw 2019-11-13 12:21:50 +01:00
nrbffs
6d310614ca [TASK] add output raw (#169) 2019-11-13 12:14:15 +01:00
stebifan
c86e47f894
Merge pull request #1 from FreifunkBremen/master
Update Fork
2019-10-25 23:15:33 +02:00
Martin/Geno
be48b27470
[DOC] update install 2019-10-13 10:48:44 +02:00
genofire
c1188378c4
[DOC] update go version 2019-10-12 21:58:14 +02:00
Martin/Geno
99eb11f2ef
[TASK] improve neighbours stats (+ babel support) 2019-05-28 20:14:12 +02:00
Martin/Geno
6e745bf78f
[TEST] improve runtime 2019-05-21 09:56:16 +02:00
Martin/Geno
ccdeccd48f
[TEST] improve gitlab-ci 2019-05-21 09:45:04 +02:00
Martin/Geno
160364d97b
update dep 2019-05-21 09:40:59 +02:00
Martin/Geno
a466ada109
[DOCS] improve geojson 2019-05-21 09:38:54 +02:00
krombel
80d42433d8 [TASK] Add output for geojson nodes only
PR: #164
2019-05-21 09:31:18 +02:00
Martin/Geno
31267858a6
[TASK] change default listing address to ff05::2:1001
take a look at: 59a44274cb
2019-05-03 17:35:15 +02:00
lrnzo
99443cb144 [DOC] fix typo in config example
small spelling fix
2019-03-31 11:41:37 +02:00
Martin/Geno
94267cf6dd
not export logging stdout / stderr hook 2019-02-27 02:51:01 +01:00
Martin/Geno
3ef2d1ece9
[TEST] fix testdata of meshviewer-ffrgb (fixes #160) 2019-02-27 02:29:39 +01:00
Martin/Geno
bd13b99378
[TASK] rename NodeInfo to Nodeinfo (same naming overall) 2019-01-24 02:56:13 +01:00
61 changed files with 889 additions and 381 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ _testmain.go
webroot webroot
/config.toml /config.toml
/vendor /vendor
/bin

View File

@ -6,7 +6,7 @@ stages:
before_script: before_script:
- curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
- mkdir -p /go/src/github.com/FreifunkBremen/ - mkdir -p /go/src/github.com/FreifunkBremen/
- cp -R /builds/FreifunkBremen/yanic /go/src/github.com/FreifunkBremen/yanic - cp -R /builds/freifunkbremen/yanic /go/src/github.com/FreifunkBremen/yanic
- cd /go/src/github.com/FreifunkBremen/yanic - cd /go/src/github.com/FreifunkBremen/yanic
- dep ensure - dep ensure
@ -14,9 +14,13 @@ build-my-project:
stage: build stage: build
script: script:
- go install -ldflags "-X github.com/FreifunkBremen/yanic/cmd.VERSION=`git -C $GOPATH/src/github.com/FreifunkBremen/yanic rev-parse HEAD`" github.com/FreifunkBremen/yanic - go install -ldflags "-X github.com/FreifunkBremen/yanic/cmd.VERSION=`git -C $GOPATH/src/github.com/FreifunkBremen/yanic rev-parse HEAD`" github.com/FreifunkBremen/yanic
- mkdir /builds/freifunkbremen/yanic/bin/
- cp /go/bin/yanic /builds/freifunkbremen/yanic/bin/yanic
artifacts: artifacts:
paths: paths:
- /go/bin/yanic - bin/yanic
- config_example.toml
- config-respondd_example.toml
test-my-project: test-my-project:
stage: test stage: test
@ -24,6 +28,7 @@ test-my-project:
- ./.circleci/check-gofmt - ./.circleci/check-gofmt
- ./.circleci/check-testfiles - ./.circleci/check-testfiles
- go test $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt - go test $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt
- go tool cover -func=.testCoverage.txt
test-race-my-project: test-race-my-project:
stage: test stage: test

82
Gopkg.lock generated
View File

@ -2,12 +2,28 @@
[[projects]] [[projects]]
digest = "1:0803645e1f57fb5271a6edc7570b9ea59bac2e5de67957075a43f3d74c8dbd97" digest = "1:af6e785bedb62fc2abb81977c58a7a44e5cf9f7e41b8d3c8dd4d872edea0ce08"
name = "github.com/NYTimes/gziphandler" name = "github.com/NYTimes/gziphandler"
packages = ["."] packages = ["."]
pruneopts = "UT" pruneopts = "UT"
revision = "2600fb119af974220d3916a5916d6e31176aac1b" revision = "dd0439581c7657cb652dfe5c71d7d48baf39541d"
version = "v1.0.1" version = "v1.1.1"
[[projects]]
digest = "1:616fe3fc0d5eb7b89324dc5e47aef8cf05f3b0aed0bfe908e62516da4c638647"
name = "github.com/bdlm/log"
packages = ["."]
pruneopts = "UT"
revision = "b8b76dd35f1d7fdf6a49497c8ac22e6554efffed"
version = "v0.1.18"
[[projects]]
branch = "master"
digest = "1:9da00d14e7f20473d129be028aa99a2f47859aed17e58cd24c5108df76ad3959"
name = "github.com/bdlm/std"
packages = ["logger"]
pruneopts = "UT"
revision = "fd3b596111c78b7d14f1e1308ebdb1153013f1a8"
[[projects]] [[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
@ -33,35 +49,17 @@
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0" version = "v1.0"
[[projects]]
digest = "1:e0123d1a40d588fb2d5f98188927826ed5ddfa1d50e759596a6c774f71b81358"
name = "github.com/influxdata/influxdb"
packages = ["pkg/escape"]
pruneopts = "UT"
revision = "698dbc789aff13c2678357a6b93ff73dd7136571"
version = "v1.7.3"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:22a9593272bc74ef4c513e176608eeed2dee7e8bd7542bae40369cb62d907a3b" digest = "1:50708c8fc92aec981df5c446581cf9f90ba9e2a5692118e0ce75d4534aaa14a2"
name = "github.com/influxdata/influxdb1-client" name = "github.com/influxdata/influxdb1-client"
packages = [ packages = [
"models", "models",
"pkg/escape",
"v2", "v2",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "e237d47d0fc87c8ba2b7c98c349cdceca9c37e27" revision = "8ff2fc3824fcb533795f9a2f233275f0bb18d6c5"
[[projects]]
branch = "master"
digest = "1:ac6b73796941a7ab33ec69fc32f9886a6e7506998f822e3d5205377aee77284a"
name = "github.com/influxdata/platform"
packages = [
"models",
"pkg/escape",
]
pruneopts = "UT"
revision = "98469bf07613ffae6f025893761c1e7a5e96e4aa"
[[projects]] [[projects]]
digest = "1:b56c589214f01a5601e0821387db484617392d0042f26234bf2da853a2f498a1" digest = "1:b56c589214f01a5601e0821387db484617392d0042f26234bf2da853a2f498a1"
@ -82,6 +80,14 @@
revision = "e6f5723bf2a66af014955e0888881314cf294129" revision = "e6f5723bf2a66af014955e0888881314cf294129"
version = "v0.1.1" version = "v0.1.1"
[[projects]]
digest = "1:36cfc0b6facbc68c92a4d55f32eff22b872a899ab75c384ac24a08dd33bc1380"
name = "github.com/paulmach/go.geojson"
packages = ["."]
pruneopts = "UT"
revision = "a5c451da2d9c67c590ae6be92565b67698286a1a"
version = "v1.4"
[[projects]] [[projects]]
digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b"
name = "github.com/pkg/errors" name = "github.com/pkg/errors"
@ -99,12 +105,12 @@
version = "v1.0.0" version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939" digest = "1:22799aea8fe96dd5693abdd1eaa14b1b29e3eafbdc7733fa155b3cb556c8a7ae"
name = "github.com/spf13/cobra" name = "github.com/spf13/cobra"
packages = ["."] packages = ["."]
pruneopts = "UT" pruneopts = "UT"
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385" revision = "67fc4837d267bc9bfd6e47f77783fcc3dffc68de"
version = "v0.0.3" version = "v0.0.4"
[[projects]] [[projects]]
digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2" digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2"
@ -122,15 +128,37 @@
revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053"
version = "v1.3.0" version = "v1.3.0"
[[projects]]
branch = "master"
digest = "1:bbe51412d9915d64ffaa96b51d409e070665efc5194fcf145c4a27d4133107a4"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
pruneopts = "UT"
revision = "22d7a77e9e5f409e934ed268692e56707cd169e5"
[[projects]]
branch = "master"
digest = "1:160e165ef13917405d1ebfde865cb9c292b8635f83655e3932662d5586d300ee"
name = "golang.org/x/sys"
packages = [
"unix",
"windows",
]
pruneopts = "UT"
revision = "c432e742b0af385916e013f6a34e9e73d139cf82"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/NYTimes/gziphandler", "github.com/NYTimes/gziphandler",
"github.com/bdlm/log",
"github.com/bdlm/std/logger",
"github.com/fgrosse/graphigo", "github.com/fgrosse/graphigo",
"github.com/influxdata/influxdb1-client/models", "github.com/influxdata/influxdb1-client/models",
"github.com/influxdata/influxdb1-client/v2", "github.com/influxdata/influxdb1-client/v2",
"github.com/naoina/toml", "github.com/naoina/toml",
"github.com/paulmach/go.geojson",
"github.com/pkg/errors", "github.com/pkg/errors",
"github.com/spf13/cobra", "github.com/spf13/cobra",
"github.com/stretchr/testify/assert", "github.com/stretchr/testify/assert",

View File

@ -5,7 +5,7 @@
### Install ### Install
```sh ```sh
cd /usr/local/ cd /usr/local/
wget https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz -O go-release-linux-amd64.tar.gz wget https://dl.google.com/go/go1.13.1.linux-amd64.tar.gz -O go-release-linux-amd64.tar.gz
tar xvf go-release-linux-amd64.tar.gz tar xvf go-release-linux-amd64.tar.gz
rm go-release-linux-amd64.tar.gz rm go-release-linux-amd64.tar.gz
``` ```
@ -25,12 +25,6 @@ As root:
go get -v -u github.com/FreifunkBremen/yanic go get -v -u github.com/FreifunkBremen/yanic
``` ```
### allow to ping
only needed if config has `nodes.ping_count` > 0
```sh
sudo setcap cap_net_raw=+ep /opt/go/bin/yanic
```
#### Work with other databases #### Work with other databases
If you like to use another database solution than influxdb, Pull Requests are If you like to use another database solution than influxdb, Pull Requests are
welcome. Just fork this project and create another subpackage within the folder welcome. Just fork this project and create another subpackage within the folder
@ -68,7 +62,15 @@ the same directory as the `dataPath`. Change the path in the section
```sh ```sh
cp /opt/go/src/github.com/FreifunkBremen/yanic/contrib/init/linux-systemd/yanic.service /lib/systemd/system/yanic.service cp /opt/go/src/github.com/FreifunkBremen/yanic/contrib/init/linux-systemd/yanic.service /lib/systemd/system/yanic.service
systemctl daemon-reload systemctl daemon-reload
```
Before start, you should configure yanic by the file `/etc/yanic.conf`:
```sh
systemctl start yanic systemctl start yanic
```
Enable to start on boot:
```sh
systemctl enable yanic systemctl enable yanic
``` ```

View File

@ -19,25 +19,25 @@ Yet another node info collector
* Provide a little webserver for a standalone installation with a meshviewer * Provide a little webserver for a standalone installation with a meshviewer
## How it works ## How it works
In the first step Yanic sends a multicast message to the group `ff05::2:1001` and port `1001`.
In the first step Yanic sends a multicast message to the group `ff02:0:0:0:0:0:2:1001` and port `1001`.
Recently seen nodes that does not reply are requested via a unicast message. Recently seen nodes that does not reply are requested via a unicast message.
## Documentation ## Documentation
Take a look at the [git](https://github.com/FreifunkBremen/yanic/blob/master/SUMMARY.md) or [Gitbook](https://freifunkbremen.gitbooks.io/yanic/content/) Take a look at the [git](https://github.com/FreifunkBremen/yanic/blob/master/SUMMARY.md) or [Gitbook](https://freifunkbremen.gitbooks.io/yanic/content/)
# Installation
Take a look into the Documentation (see above) or for Quick Overview in [INSTALL.md](INSTALL.md).
If you like Docker you may want to take a look [here](https://github.com/christf/docker-yanic).
## Configuration ## Configuration
Read comments in [config_example.toml](config_example.toml) for more information. Read comments in [config_example.toml](config_example.toml) for more information.
## Running ## Running
Yanic provides several commands: Yanic provides several commands:
### Usage ### Usage
Run Yanic without any arguments to get the usage information: Run Yanic without any arguments to get the usage information:
``` ```
Usage: Usage:
yanic [command] yanic [command]
@ -50,57 +50,71 @@ Available Commands:
Flags: Flags:
-h, --help help for yanic -h, --help help for yanic
--loglevel uint32 Show log message starting at level (default 40)
--timestamps Enables timestamps for log output --timestamps Enables timestamps for log output
Use "yanic [command] --help" for more information about a command. Use "yanic [command] --help" for more information about a command.
``` ```
#### Serve #### Serve
Runs the yanic server
``` ```
Usage: Usage:
yanic serve [flags] yanic serve [flags]
Examples: Examples:
yanic serve --config /etc/yanic.toml yanic serve --config /etc/yanic.toml
Flags: Flags:
-c, --config string Path to configuration file (default "config.toml") -c, --config string Path to configuration file (default "config.toml")
-h, --help help for serve -h, --help help for serve
Global Flags:
--loglevel uint32 Show log message starting at level (default 40)
--timestamps Enables timestamps for log output
```
#### Query
Sends a query on the interface to the destination and waits for a response
```
Usage:
yanic query <interfaces> <destination> [flags]
Examples:
yanic query "eth0,wlan0" "fe80::eade:27ff:dead:beef"
Flags:
-h, --help help for query
--ip string ip address which is used for sending (optional - without definition used the link-local address)
--port int define a port to listen (if not set or set to 0 the kernel will use a random free port at its own)
--wait int Seconds to wait for a response (default 1)
Global Flags:
--loglevel uint32 Show log message starting at level (default 40)
--timestamps Enables timestamps for log output
``` ```
#### Import #### Import
Imports global statistics from the given RRD files (ffmap-backend).
``` ```
Usage: Usage:
yanic import <file.rrd> [flags] yanic import <file.rrd> <site> <domain> [flags]
Examples: Examples:
yanic import --config /etc/yanic.toml olddata.rrd yanic import --config /etc/yanic.toml olddata.rrd global global
Flags: Flags:
-c, --config string Path to configuration file (default "config.toml") -c, --config string Path to configuration file (default "config.toml")
-h, --help help for import -h, --help help for import
Global Flags:
--loglevel uint32 Show log message starting at level (default 40)
--timestamps Enables timestamps for log output
``` ```
#### Query
```
Usage:
yanic query <interface> <destination> [flags]
Examples:
yanic query wlan0 "fe80::eade:27ff:dead:beef"
Flags:
-h, --help help for query
--wait int Seconds to wait for a response (default 1)
```
## Communities using Yanic ## Communities using Yanic
* **Freifunk Bremen** uses InfluxDB, [Grafana](https://grafana.bremen.freifunk.net), and [Meshviewer](https://map.bremen.freifunk.net) with a patch to show state-version of `nodes.json`. * **Freifunk Bremen** uses InfluxDB, [Grafana](https://grafana.bremen.freifunk.net), and [Meshviewer](https://map.bremen.freifunk.net) with a patch to show state-version of `nodes.json`.
* **Freifunk Nord** uses [hopglass](https://github.com/hopglass/hopglass) (commit 587740a) as frontend: https://mesh.freifunknord.de/ * **Freifunk Nord** uses [hopglass](https://github.com/hopglass/hopglass) (commit 587740a) as frontend: https://mesh.freifunknord.de/
* **Freifunk Kiel** uses [Meshviewer](https://github.com/ffrgb/meshviewer/) as frontend: https://map.freifunk.in-kiel.de/ * **Freifunk Kiel** uses [Meshviewer](https://github.com/ffrgb/meshviewer/) as frontend: https://map.freifunk.in-kiel.de/
@ -108,7 +122,6 @@ Flags:
Do you know someone else using Yanic? Create a [pull request](https://github.com/FreifunkBremen/yanic/issues/new?template=community.md&title=Mention+community+$name)! Do you know someone else using Yanic? Create a [pull request](https://github.com/FreifunkBremen/yanic/issues/new?template=community.md&title=Mention+community+$name)!
## Related projects ## Related projects
Collecting data from respondd: Collecting data from respondd:
* [HopGlass Server](https://github.com/plumpudding/hopglass-server) written in Node.js * [HopGlass Server](https://github.com/plumpudding/hopglass-server) written in Node.js
@ -118,5 +131,4 @@ Respondd for servers:
## License ## License
This software is licensed under the terms of the [AGPL v3 License](LICENSE). This software is licensed under the terms of the [AGPL v3 License](LICENSE).

View File

@ -25,7 +25,7 @@ func TestReadConfig(t *testing.T) {
assert.Contains(config.Respondd.Sites["ffhb"].Domains, "city") assert.Contains(config.Respondd.Sites["ffhb"].Domains, "city")
// Test output plugins // Test output plugins
assert.Len(config.Nodes.Output, 3) assert.Len(config.Nodes.Output, 4)
outputs := config.Nodes.Output["meshviewer"].([]interface{}) outputs := config.Nodes.Output["meshviewer"].([]interface{})
assert.Len(outputs, 1) assert.Len(outputs, 1)
meshviewer := outputs[0] meshviewer := outputs[0]

View File

@ -1,6 +1,6 @@
# This is the config file for Yanic written in "Tom's Obvious, Minimal Language." # This is the config file for Yanic written in "Tom's Obvious, Minimal Language."
# syntax: https://github.com/toml-lang/toml # syntax: https://github.com/toml-lang/toml
# (if you need somethink multiple times, checkout out the [[array of table]] section) # (if you need something multiple times, checkout out the [[array of table]] section)
# Send respondd request to update information # Send respondd request to update information
[respondd] [respondd]
@ -23,14 +23,14 @@ domains = ["city"]
# name of interface on which this collector is running # name of interface on which this collector is running
ifname = "br-ffhb" ifname = "br-ffhb"
# ip address which is used for sending # ip address which is used for sending
# (optional - without definition used a address of ifname) # (optional - without definition used a address of ifname - preferred link local)
ip_address = "fd2f:5119:f2d::5" ip_address = "fd2f:5119:f2d::5"
# disable sending multicast respondd request # disable sending multicast respondd request
# (for receiving only respondd packages e.g. database respondd) # (for receiving only respondd packages e.g. database respondd)
#send_no_request = false #send_no_request = false
# multicast address to destination of respondd # multicast address to destination of respondd
# (optional - without definition used batman default ff02::2:1001) # (optional - without definition used default ff05::2:1001)
multicast_address = "ff05::2:1001" #multicast_address = "ff02::2:1001"
# define a port to listen # define a port to listen
# if not set or set to 0 the kernel will use a random free port at its own # if not set or set to 0 the kernel will use a random free port at its own
#port = 10001 #port = 10001
@ -55,12 +55,6 @@ save_interval = "5s"
# Set node to offline if not seen within this period # Set node to offline if not seen within this period
offline_after = "10m" offline_after = "10m"
## Verify if node is really down by ping last seen address of node
# send x pings to verify if node is offline (for disable set count < 1)
ping_count = 3
# timeout of sending ping to a node
ping_timeout = "1s"
## [[nodes.output.example]] ## [[nodes.output.example]]
# Each output format has its own config block and needs to be enabled by adding: # Each output format has its own config block and needs to be enabled by adding:
@ -99,6 +93,11 @@ ping_timeout = "1s"
#longitude_max = 39.72 #longitude_max = 39.72
# outputs all nodes as points into nodes.geojson
[[nodes.output.geojson]]
enable = true
path = "/var/www/html/meshviewer/data/nodes.geojson"
# definition for the new more compressed meshviewer.json # definition for the new more compressed meshviewer.json
[[nodes.output.meshviewer-ffrgb]] [[nodes.output.meshviewer-ffrgb]]
enable = true enable = true

View File

@ -1,7 +1,7 @@
package data package data
// NodeInfo struct // Nodeinfo struct
type NodeInfo struct { type Nodeinfo struct {
NodeID string `json:"node_id"` NodeID string `json:"node_id"`
Network Network `json:"network"` Network Network `json:"network"`
Owner *Owner `json:"owner"` Owner *Owner `json:"owner"`

View File

@ -3,6 +3,6 @@ package data
// ResponseData struct // ResponseData struct
type ResponseData struct { 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"`
} }

View File

@ -36,7 +36,6 @@ func (c *Connection) InsertGlobals(stats *runtime.GlobalStats, time time.Time, s
func GlobalStatsFields(name string, stats *runtime.GlobalStats) []graphigo.Metric { func GlobalStatsFields(name string, stats *runtime.GlobalStats) []graphigo.Metric {
return []graphigo.Metric{ return []graphigo.Metric{
{Name: name + ".nodes", Value: stats.Nodes}, {Name: name + ".nodes", Value: stats.Nodes},
{Name: name + ".nodes.no_respondd", Value: stats.NodesNoRespondd},
{Name: name + ".gateways", Value: stats.Gateways}, {Name: name + ".gateways", Value: stats.Gateways},
{Name: name + ".clients.total", Value: stats.Clients}, {Name: name + ".clients.total", Value: stats.Clients},
{Name: name + ".clients.wifi", Value: stats.ClientsWifi}, {Name: name + ".clients.wifi", Value: stats.ClientsWifi},

View File

@ -30,25 +30,36 @@ func (c *Connection) InsertNode(node *runtime.Node) {
fields = append(fields, graphigo.Metric{Name: node_prefix + "." + name, Value: value}) fields = append(fields, graphigo.Metric{Name: node_prefix + "." + name, Value: value})
} }
vpnInterfaces := make(map[string]bool)
for _, mIface := range nodeinfo.Network.Mesh {
for _, tunnel := range mIface.Interfaces.Tunnel {
vpnInterfaces[tunnel] = true
}
}
if neighbours := node.Neighbours; neighbours != nil { if neighbours := node.Neighbours; neighbours != nil {
vpn := 0 vpn := 0
if meshvpn := stats.MeshVPN; meshvpn != nil {
for _, group := range meshvpn.Groups {
for _, link := range group.Peers {
if link != nil && link.Established > 1 {
vpn++
}
}
}
}
addField("neighbours.vpn", vpn)
// protocol: Batman Advance // protocol: Batman Advance
batadv := 0 batadv := 0
for _, batadvNeighbours := range neighbours.Batadv { for mac, batadvNeighbours := range neighbours.Batadv {
batadv += len(batadvNeighbours.Neighbours) batadv += len(batadvNeighbours.Neighbours)
if _, ok := vpnInterfaces[mac]; ok {
vpn += len(batadvNeighbours.Neighbours)
}
} }
addField("neighbours.batadv", batadv) addField("neighbours.batadv", batadv)
// protocol: Babel
babel := 0
for _, babelNeighbours := range neighbours.Babel {
babel += len(babelNeighbours.Neighbours)
if _, ok := vpnInterfaces[babelNeighbours.LinkLocalAddress]; ok {
vpn += len(babelNeighbours.Neighbours)
}
}
addField("neighbours.babel", babel)
// protocol: LLDP // protocol: LLDP
lldp := 0 lldp := 0
for _, lldpNeighbours := range neighbours.LLDP { for _, lldpNeighbours := range neighbours.LLDP {
@ -56,8 +67,10 @@ func (c *Connection) InsertNode(node *runtime.Node) {
} }
addField("neighbours.lldp", lldp) addField("neighbours.lldp", lldp)
addField("neighbours.vpn", vpn)
// total is the sum of all protocols // total is the sum of all protocols
addField("neighbours.total", batadv+lldp) addField("neighbours.total", batadv+babel+lldp)
} }
if t := stats.Traffic.Rx; t != nil { if t := stats.Traffic.Rx; t != nil {

View File

@ -43,7 +43,6 @@ func (conn *Connection) InsertGlobals(stats *runtime.GlobalStats, time time.Time
func GlobalStatsFields(stats *runtime.GlobalStats) map[string]interface{} { func GlobalStatsFields(stats *runtime.GlobalStats) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"nodes": stats.Nodes, "nodes": stats.Nodes,
"nodes.no_respondd": stats.NodesNoRespondd,
"gateways": stats.Gateways, "gateways": stats.Gateways,
"clients.total": stats.Clients, "clients.total": stats.Clients,
"clients.wifi": stats.ClientsWifi, "clients.wifi": stats.ClientsWifi,

View File

@ -124,7 +124,7 @@ func createTestNodes() *runtime.Nodes {
Total: 23, Total: 23,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345", NodeID: "abcdef012345",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -146,7 +146,7 @@ func createTestNodes() *runtime.Nodes {
Total: 2, Total: 2,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566", NodeID: "112233445566",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -156,7 +156,7 @@ func createTestNodes() *runtime.Nodes {
nodes.AddNode(&runtime.Node{ nodes.AddNode(&runtime.Node{
Online: true, Online: true,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x", NodeID: "0xdeadbeef0x",
VPN: true, VPN: true,
Hardware: data.Hardware{ Hardware: data.Hardware{

View File

@ -48,7 +48,15 @@ func (conn *Connection) InsertNode(node *runtime.Node) {
"memory.available": stats.Memory.Available, "memory.available": stats.Memory.Available,
} }
vpnInterfaces := make(map[string]bool)
if nodeinfo := node.Nodeinfo; nodeinfo != nil { if nodeinfo := node.Nodeinfo; nodeinfo != nil {
for _, mIface := range nodeinfo.Network.Mesh {
for _, tunnel := range mIface.Interfaces.Tunnel {
vpnInterfaces[tunnel] = true
}
}
tags.SetString("hostname", nodeinfo.Hostname) tags.SetString("hostname", nodeinfo.Hostname)
if nodeinfo.System.SiteCode != "" { if nodeinfo.System.SiteCode != "" {
tags.SetString("site", nodeinfo.System.SiteCode) tags.SetString("site", nodeinfo.System.SiteCode)
@ -75,28 +83,30 @@ func (conn *Connection) InsertNode(node *runtime.Node) {
} }
} }
if neighbours := node.Neighbours; neighbours != nil { if neighbours := node.Neighbours; neighbours != nil {
// VPN Neighbours are Neighbours but includet in one protocol // VPN Neighbours are Neighbours but includet in one protocol
vpn := 0 vpn := 0
if meshvpn := stats.MeshVPN; meshvpn != nil {
for _, group := range meshvpn.Groups {
for _, link := range group.Peers {
if link != nil && link.Established > 1 {
vpn++
}
}
}
}
fields["neighbours.vpn"] = vpn
// protocol: Batman Advance // protocol: Batman Advance
batadv := 0 batadv := 0
for _, batadvNeighbours := range neighbours.Batadv { for mac, batadvNeighbours := range neighbours.Batadv {
batadv += len(batadvNeighbours.Neighbours) batadv += len(batadvNeighbours.Neighbours)
if _, ok := vpnInterfaces[mac]; ok {
vpn += len(batadvNeighbours.Neighbours)
}
} }
fields["neighbours.batadv"] = batadv fields["neighbours.batadv"] = batadv
// protocol: Babel
babel := 0
for _, babelNeighbours := range neighbours.Babel {
babel += len(babelNeighbours.Neighbours)
if _, ok := vpnInterfaces[babelNeighbours.LinkLocalAddress]; ok {
vpn += len(babelNeighbours.Neighbours)
}
}
fields["neighbours.babel"] = babel
// protocol: LLDP // protocol: LLDP
lldp := 0 lldp := 0
for _, lldpNeighbours := range neighbours.LLDP { for _, lldpNeighbours := range neighbours.LLDP {
@ -104,8 +114,11 @@ func (conn *Connection) InsertNode(node *runtime.Node) {
} }
fields["neighbours.lldp"] = lldp fields["neighbours.lldp"] = lldp
// vpn wait for babel
fields["neighbours.vpn"] = vpn
// total is the sum of all protocols // total is the sum of all protocols
fields["neighbours.total"] = batadv + lldp fields["neighbours.total"] = batadv + babel + lldp
} }
if procstat := stats.ProcStats; procstat != nil { if procstat := stats.ProcStats; procstat != nil {
fields["stat.cpu.user"] = procstat.CPU.User fields["stat.cpu.user"] = procstat.CPU.User

View File

@ -39,20 +39,8 @@ func TestToInflux(t *testing.T) {
MgmtTx: &data.Traffic{Packets: 2327}, MgmtTx: &data.Traffic{Packets: 2327},
MgmtRx: &data.Traffic{Bytes: 2331}, MgmtRx: &data.Traffic{Bytes: 2331},
}, },
MeshVPN: &data.MeshVPN{
Groups: map[string]*data.MeshVPNPeerGroup{
"ffhb": {
Peers: map[string]*data.MeshVPNPeerLink{
"vpn01": {Established: 3},
"vpn02": {},
"trash": nil,
"vpn03": {Established: 0},
}, },
}, Nodeinfo: &data.Nodeinfo{
},
},
},
Nodeinfo: &data.NodeInfo{
NodeID: "deadbeef", NodeID: "deadbeef",
Owner: &data.Owner{ Owner: &data.Owner{
Contact: "nobody", Contact: "nobody",
@ -67,6 +55,17 @@ func TestToInflux(t *testing.T) {
}, },
Network: data.Network{ Network: data.Network{
Mac: "DEADMAC", Mac: "DEADMAC",
Mesh: map[string]*data.NetworkInterface{
"bat0": {
Interfaces: struct {
Wireless []string `json:"wireless,omitempty"`
Other []string `json:"other,omitempty"`
Tunnel []string `json:"tunnel,omitempty"`
}{
Tunnel: []string{"a-interface-mac", "fe80::1"},
},
},
},
}, },
Software: data.Software{ Software: data.Software{
Autoupdater: struct { Autoupdater: struct {
@ -81,7 +80,7 @@ func TestToInflux(t *testing.T) {
Neighbours: &data.Neighbours{ Neighbours: &data.Neighbours{
NodeID: "deadbeef", NodeID: "deadbeef",
Batadv: map[string]data.BatadvNeighbours{ Batadv: map[string]data.BatadvNeighbours{
"a-interface": { "a-interface-mac": {
Neighbours: map[string]data.BatmanLink{ Neighbours: map[string]data.BatmanLink{
"BAFF1E5": { "BAFF1E5": {
Tq: 204, Tq: 204,
@ -89,14 +88,24 @@ func TestToInflux(t *testing.T) {
}, },
}, },
}, },
Babel: map[string]data.BabelNeighbours{
"wg-01": {
LinkLocalAddress: "fe80::1",
Neighbours: map[string]data.BabelLink{
"fe80::2": {
Cost: 0,
},
},
},
},
LLDP: map[string]data.LLDPNeighbours{ LLDP: map[string]data.LLDPNeighbours{
"b-interface": {}, "b-interface-mac": {},
}, },
}, },
} }
neighbour := &runtime.Node{ neighbour := &runtime.Node{
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "foobar", NodeID: "foobar",
Network: data.Network{ Network: data.Network{
Mac: "BAFF1E5", Mac: "BAFF1E5",
@ -117,7 +126,7 @@ func TestToInflux(t *testing.T) {
// do not add a empty statistics of a node // do not add a empty statistics of a node
droppednode := &runtime.Node{ droppednode := &runtime.Node{
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "notfound", NodeID: "notfound",
Network: data.Network{ Network: data.Network{
Mac: "instats", Mac: "instats",
@ -144,9 +153,10 @@ func TestToInflux(t *testing.T) {
assert.EqualValues("city", tags["domain"]) assert.EqualValues("city", tags["domain"])
assert.EqualValues(0.5, fields["load"]) assert.EqualValues(0.5, fields["load"])
assert.EqualValues(0, fields["neighbours.lldp"]) assert.EqualValues(0, fields["neighbours.lldp"])
assert.EqualValues(1, fields["neighbours.babel"])
assert.EqualValues(1, fields["neighbours.batadv"]) assert.EqualValues(1, fields["neighbours.batadv"])
assert.EqualValues(1, fields["neighbours.vpn"]) assert.EqualValues(2, fields["neighbours.vpn"])
assert.EqualValues(1, fields["neighbours.total"]) assert.EqualValues(2, fields["neighbours.total"])
assert.EqualValues(uint32(3), fields["wireless.txpower24"]) assert.EqualValues(uint32(3), fields["wireless.txpower24"])
assert.EqualValues(uint32(5500), fields["airtime11a.frequency"]) assert.EqualValues(uint32(5500), fields["airtime11a.frequency"])
@ -165,7 +175,7 @@ func TestToInflux(t *testing.T) {
assert.EqualValues("link", nPoint.Name()) assert.EqualValues("link", nPoint.Name())
assert.EqualValues(map[string]string{ assert.EqualValues(map[string]string{
"source.id": "deadbeef", "source.id": "deadbeef",
"source.addr": "a-interface", "source.addr": "a-interface-mac",
"target.id": "foobar", "target.id": "foobar",
"target.addr": "BAFF1E5", "target.addr": "BAFF1E5",
}, tags) }, tags)

View File

@ -50,7 +50,7 @@ func (conn *Connection) InsertLink(link *runtime.Link, time time.Time) {
} }
func (conn *Connection) InsertGlobals(stats *runtime.GlobalStats, time time.Time, site string, domain string) { func (conn *Connection) InsertGlobals(stats *runtime.GlobalStats, time time.Time, site string, domain string) {
conn.log("InsertGlobals: [", time.String(), "] site: ", site, " domain: ", domain, ", nodes: ", stats.Nodes, " (no respondd: ", stats.NodesNoRespondd, "), clients: ", stats.Clients, " models: ", len(stats.Models)) conn.log("InsertGlobals: [", time.String(), "] site: ", site, " domain: ", domain, ", nodes: ", stats.Nodes, ", clients: ", stats.Clients, " models: ", len(stats.Models))
} }
func (conn *Connection) PruneNodes(deleteAfter time.Duration) { func (conn *Connection) PruneNodes(deleteAfter time.Duration) {

View File

@ -51,7 +51,7 @@ func Connect(configuration map[string]interface{}) (database.Connection, error)
func (conn *Connection) InsertNode(node *runtime.Node) { func (conn *Connection) InsertNode(node *runtime.Node) {
res := &data.ResponseData{ res := &data.ResponseData{
NodeInfo: node.Nodeinfo, Nodeinfo: node.Nodeinfo,
Statistics: node.Statistics, Statistics: node.Statistics,
Neighbours: node.Neighbours, Neighbours: node.Neighbours,
} }

View File

@ -25,7 +25,7 @@ func TestStart(t *testing.T) {
assert.NoError(err) assert.NoError(err)
conn.InsertNode(&runtime.Node{ conn.InsertNode(&runtime.Node{
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "73deadbeaf13", NodeID: "73deadbeaf13",
Hostname: "inject-test", Hostname: "inject-test",
Network: data.Network{ Network: data.Network{

View File

@ -52,7 +52,7 @@ synchronize = "1m"
{% method %} {% method %}
How often send request per respondd. How often send request per respondd.
It will send UDP packets with multicast address `ff02::2:1001` and port `1001`. It will send UDP packets with multicast address `ff05::2:1001` and port `1001`.
If a node does not answer after the half time, it will request with the last know address under the port `1001`. If a node does not answer after the half time, it will request with the last know address under the port `1001`.
{% sample lang="toml" %} {% sample lang="toml" %}
```toml ```toml
@ -110,7 +110,7 @@ ifname = "br-ffhb"
{% method %} {% method %}
ip address is the own address which is used for sending. ip address is the own address which is used for sending.
If not set or set with empty string it will take an address of ifname. If not set or set with empty string it will take an address of ifname.
(If `multicast_address` is not set the link local address otherwise a global unicast address) (It prefers the link local address, so at babel mesh-network it should be configurated)
{% sample lang="toml" %} {% sample lang="toml" %}
```toml ```toml
ip_address = "fe80::..." ip_address = "fe80::..."
@ -130,8 +130,8 @@ send_no_request = true
### multicast_address ### multicast_address
{% method %} {% method %}
Multicast address to destination of respondd. Multicast address to destination of respondd.
If not set or set with empty string it will take the batman default multicast address `ff02::2:1001` If not set or set with empty string it will take the batman default multicast address `ff05::2:1001`
(Needed in babel for a mesh-network wide routeable multicast addreess `ff05::2:1001`) (Needed to set for legacy `ff02::2:1001`)
{% sample lang="toml" %} {% sample lang="toml" %}
```toml ```toml
multicast_address = "ff02::2:1001" multicast_address = "ff02::2:1001"
@ -203,8 +203,6 @@ state_path = "/var/lib/yanic/state.json"
prune_after = "7d" prune_after = "7d"
save_interval = "5s" save_interval = "5s"
offline_after = "10m" offline_after = "10m"
ping_count = 3
ping_timeout = "1s"
``` ```
{% endmethod %} {% endmethod %}
@ -248,26 +246,6 @@ offline_after = "10m"
``` ```
{% endmethod %} {% endmethod %}
### ping_count
{% method %}
Verify if node is really down by ping last seen address of node
send x pings to verify if node is offline (for disable set count < 1)
{% sample lang="toml" %}
```toml
ping_count = 3
```
{% endmethod %}
### ping_timeout
{% method %}
Timeout of sending ping to a node
{% sample lang="toml" %}
```toml
ping_timeout = "1s"
```
{% endmethod %}
## [[nodes.output.example]] ## [[nodes.output.example]]
{% method %} {% method %}
@ -409,6 +387,32 @@ longitude_max = 39.72
## [[nodes.output.geojson]]
{% method %}
The geojson output produces a geojson file which contains the location data of all monitored nodes to be used to visualize the location of the nodes.
It is optimized to be used with [UMap](https://github.com/umap-project/umap) but should work with other tools as well.
Here is a public demo provided by Freifunk Muenchen: http://u.osmfr.org/m/328494/
{% sample lang="toml" %}
```toml
[[nodes.output.geojson]]
enable = true
path = "/var/www/html/meshviewer/data/nodes.geojson"
```
{% endmethod %}
### path
{% method %}
The path, where to store nodes.geojson
{% sample lang="toml" %}
```toml
path = "/var/www/html/meshviewer/data/nodes.geojson"
```
{% endmethod %}
## [[nodes.output.meshviewer-ffrgb]] ## [[nodes.output.meshviewer-ffrgb]]
{% method %} {% method %}
The new json file format for the [meshviewer](https://github.com/ffrgb/meshviewer) developed in Regensburg. The new json file format for the [meshviewer](https://github.com/ffrgb/meshviewer) developed in Regensburg.
@ -517,6 +521,21 @@ path = "/var/www/html/meshviewer/data/nodelist.json"
``` ```
{% endmethod %} {% endmethod %}
## [[nodes.output.raw]]
{% method %}
This output takes the respondd response as sent by the node and includes it in a JSON document.
{% endmethod %}
### path
{% method %}
The path, where to store raw.json
{% sample lang="toml" %}
```toml
path = "/var/www/html/meshviewer/data/raw.json"
```
{% endmethod %}
## [database] ## [database]

View File

@ -5,14 +5,13 @@
### Install ### Install
```sh ```sh
cd /usr/local/ cd /usr/local/
wget https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz -O go-release-linux-amd64.tar.gz wget https://dl.google.com/go/go1.13.1.linux-amd64.tar.gz -O go-release-linux-amd64.tar.gz
tar xvf go-release-linux-amd64.tar.gz tar xvf go-release-linux-amd64.tar.gz
rm go-release-linux-amd64.tar.gz rm go-release-linux-amd64.tar.gz
``` ```
### Configure go ### Configure go
Add these lines in your root shell startup file (e.g. `/root/.bashrc`): Add these lines in your root shell startup file (e.g. `/root/.bashrc`):
```sh ```sh
export GOPATH=/opt/go export GOPATH=/opt/go
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
@ -26,12 +25,6 @@ As root:
go get -v -u github.com/FreifunkBremen/yanic go get -v -u github.com/FreifunkBremen/yanic
``` ```
### allow to ping
only needed if config has `nodes.ping_count` > 0
```sh
sudo setcap cap_net_raw=+ep /opt/go/bin/yanic
```
### Install ### Install
```sh ```sh
@ -40,14 +33,12 @@ systemctl daemon-reload
``` ```
Before start, you should configure yanic by the file `/etc/yanic.conf`: Before start, you should configure yanic by the file `/etc/yanic.conf`:
```sh
```
systemctl start yanic systemctl start yanic
``` ```
Enable to start on boot: Enable to start on boot:
```sh
```
systemctl enable yanic systemctl enable yanic
``` ```

View File

@ -7,7 +7,7 @@ A little overview of yanic in connection with other software:
It sends the `gluon-neighbour-info` request and collects the answers. It sends the `gluon-neighbour-info` request and collects the answers.
It will send UDP packets with multicast address `ff02:0:0:0:0:0:2:1001` and port `1001`. It will send UDP packets with multicast address `ff05:0:0:0:0:0:2:1001` and port `1001`.
If a node does not answer, it will request with the last know address under the port `1001`. If a node does not answer, it will request with the last know address under the port `1001`.

View File

@ -9,9 +9,9 @@ import (
"github.com/FreifunkBremen/yanic/cmd" "github.com/FreifunkBremen/yanic/cmd"
) )
type Hook struct{} type hook struct{}
func (hook *Hook) Fire(entry *log.Entry) error { func (h *hook) Fire(entry *log.Entry) error {
switch entry.Level { switch entry.Level {
case log.PanicLevel: case log.PanicLevel:
entry.Logger.Out = os.Stderr entry.Logger.Out = os.Stderr
@ -31,12 +31,12 @@ func (hook *Hook) Fire(entry *log.Entry) error {
return nil return nil
} }
func (hook *Hook) Levels() []stdLogger.Level { func (h *hook) Levels() []stdLogger.Level {
return log.AllLevels return log.AllLevels
} }
func main() { func main() {
log.AddHook(&Hook{}) log.AddHook(&hook{})
cmd.Execute() cmd.Execute()
} }

View File

@ -1,7 +1,9 @@
package all package all
import ( import (
_ "github.com/FreifunkBremen/yanic/output/geojson"
_ "github.com/FreifunkBremen/yanic/output/meshviewer" _ "github.com/FreifunkBremen/yanic/output/meshviewer"
_ "github.com/FreifunkBremen/yanic/output/meshviewer-ffrgb" _ "github.com/FreifunkBremen/yanic/output/meshviewer-ffrgb"
_ "github.com/FreifunkBremen/yanic/output/nodelist" _ "github.com/FreifunkBremen/yanic/output/nodelist"
_ "github.com/FreifunkBremen/yanic/output/raw"
) )

View File

@ -23,7 +23,7 @@ func TestFilterBlacklist(t *testing.T) {
assert.NoError(err) assert.NoError(err)
// keep node without nodeid // keep node without nodeid
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{}}) n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{}})
assert.NotNil(n) assert.NotNil(n)
// tests with blacklist // tests with blacklist
@ -31,11 +31,11 @@ func TestFilterBlacklist(t *testing.T) {
assert.NoError(err) assert.NoError(err)
// blacklist contains node with nodeid -> drop it // blacklist contains node with nodeid -> drop it
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{NodeID: "a"}}) n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{NodeID: "a"}})
assert.Nil(n) assert.Nil(n)
// blacklist does not contains node without nodeid -> keep it // blacklist does not contains node without nodeid -> keep it
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{}}) n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{}})
assert.NotNil(n) assert.NotNil(n)
n = filter.Apply(&runtime.Node{}) n = filter.Apply(&runtime.Node{})

View File

@ -29,7 +29,7 @@ func (config *domainAppendSite) Apply(node *runtime.Node) *runtime.Node {
Lastseen: node.Lastseen, Lastseen: node.Lastseen,
Online: node.Online, Online: node.Online,
Statistics: node.Statistics, Statistics: node.Statistics,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: nodeinfo.NodeID, NodeID: nodeinfo.NodeID,
Network: nodeinfo.Network, Network: nodeinfo.Network,
System: data.System{ System: data.System{

View File

@ -17,7 +17,7 @@ func TestFilter(t *testing.T) {
// delete owner by configuration // delete owner by configuration
filter, _ = build(true) filter, _ = build(true)
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
System: data.System{ System: data.System{
SiteCode: "ffhb", SiteCode: "ffhb",
DomainCode: "city", DomainCode: "city",
@ -30,7 +30,7 @@ func TestFilter(t *testing.T) {
// keep owner configuration // keep owner configuration
filter, _ = build(false) filter, _ = build(false)
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
System: data.System{ System: data.System{
SiteCode: "ffhb", SiteCode: "ffhb",
DomainCode: "city", DomainCode: "city",

View File

@ -29,7 +29,7 @@ func (config *domainAsSite) Apply(node *runtime.Node) *runtime.Node {
Lastseen: node.Lastseen, Lastseen: node.Lastseen,
Online: node.Online, Online: node.Online,
Statistics: node.Statistics, Statistics: node.Statistics,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: nodeinfo.NodeID, NodeID: nodeinfo.NodeID,
Network: nodeinfo.Network, Network: nodeinfo.Network,
System: data.System{ System: data.System{

View File

@ -17,7 +17,7 @@ func TestFilter(t *testing.T) {
// delete owner by configuration // delete owner by configuration
filter, _ = build(true) filter, _ = build(true)
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
System: data.System{ System: data.System{
SiteCode: "ffhb", SiteCode: "ffhb",
DomainCode: "city", DomainCode: "city",
@ -30,7 +30,7 @@ func TestFilter(t *testing.T) {
// keep owner configuration // keep owner configuration
filter, _ = build(false) filter, _ = build(false)
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
System: data.System{ System: data.System{
SiteCode: "ffhb", SiteCode: "ffhb",
DomainCode: "city", DomainCode: "city",

View File

@ -68,7 +68,7 @@ func TestFilter(t *testing.T) {
nodes := &runtime.Nodes{ nodes := &runtime.Nodes{
List: map[string]*runtime.Node{ List: map[string]*runtime.Node{
"a": { "a": {
Nodeinfo: &data.NodeInfo{NodeID: "a"}, Nodeinfo: &data.Nodeinfo{NodeID: "a"},
}, },
}, },
} }
@ -83,7 +83,7 @@ func TestFilter(t *testing.T) {
nodes = &runtime.Nodes{ nodes = &runtime.Nodes{
List: map[string]*runtime.Node{ List: map[string]*runtime.Node{
"a": { "a": {
Nodeinfo: &data.NodeInfo{NodeID: "a"}, Nodeinfo: &data.Nodeinfo{NodeID: "a"},
}, },
}, },
} }

View File

@ -20,13 +20,13 @@ func TestFilterHasLocation(t *testing.T) {
assert.NoError(err) assert.NoError(err)
// node has location (with 0,0) -> keep it // node has location (with 0,0) -> keep it
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{}, Location: &data.Location{},
}}) }})
assert.NotNil(n) assert.NotNil(n)
// node without location has no location -> drop it // node without location has no location -> drop it
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{}}) n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{}})
assert.Nil(n) assert.Nil(n)
// node without nodeinfo has no location -> drop it // node without nodeinfo has no location -> drop it
@ -38,13 +38,13 @@ func TestFilterHasLocation(t *testing.T) {
assert.NoError(err) assert.NoError(err)
// node has location (with 0,0) -> drop it // node has location (with 0,0) -> drop it
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{}, Location: &data.Location{},
}}) }})
assert.Nil(n) assert.Nil(n)
// node without location has no location -> keep it // node without location has no location -> keep it
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{}}) n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{}})
assert.NotNil(n) assert.NotNil(n)
// node without nodeinfo has no location -> keep it // node without nodeinfo has no location -> keep it

View File

@ -18,7 +18,7 @@ func TestFilterInArea(t *testing.T) {
"longitude_max": 12.0, "longitude_max": 12.0,
}) })
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{Latitude: 4.0, Longitude: 11.0}, Location: &data.Location{Latitude: 4.0, Longitude: 11.0},
}}) }})
assert.NotNil(n) assert.NotNil(n)
@ -28,35 +28,35 @@ func TestFilterInArea(t *testing.T) {
assert.NotNil(n) assert.NotNil(n)
// keep without location -> use has_location for it // keep without location -> use has_location for it
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{}}) n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{}})
assert.NotNil(n) assert.NotNil(n)
// zeros not in area (0, 0) // zeros not in area (0, 0)
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{}, Location: &data.Location{},
}}) }})
assert.Nil(n) assert.Nil(n)
// in area // in area
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{Latitude: 4.0, Longitude: 11.0}, Location: &data.Location{Latitude: 4.0, Longitude: 11.0},
}}) }})
assert.NotNil(n) assert.NotNil(n)
// over max longitude -> dropped // over max longitude -> dropped
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{Latitude: 4.0, Longitude: 13.0}, Location: &data.Location{Latitude: 4.0, Longitude: 13.0},
}}) }})
assert.Nil(n) assert.Nil(n)
// over max latitude -> dropped // over max latitude -> dropped
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{Latitude: 6.0, Longitude: 11.0}, Location: &data.Location{Latitude: 6.0, Longitude: 11.0},
}}) }})
assert.Nil(n) assert.Nil(n)
// lower then mix latitde -> dropped // lower then mix latitde -> dropped
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Location: &data.Location{Latitude: 1.0, Longitude: 2.0}, Location: &data.Location{Latitude: 1.0, Longitude: 2.0},
}}) }})
assert.Nil(n) assert.Nil(n)

View File

@ -29,7 +29,7 @@ func (no *noowner) Apply(node *runtime.Node) *runtime.Node {
Lastseen: node.Lastseen, Lastseen: node.Lastseen,
Online: node.Online, Online: node.Online,
Statistics: node.Statistics, Statistics: node.Statistics,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: nodeinfo.NodeID, NodeID: nodeinfo.NodeID,
Network: nodeinfo.Network, Network: nodeinfo.Network,
System: nodeinfo.System, System: nodeinfo.System,

View File

@ -17,7 +17,7 @@ func TestFilter(t *testing.T) {
// delete owner by configuration // delete owner by configuration
filter, _ = build(true) filter, _ = build(true)
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Owner: &data.Owner{ Owner: &data.Owner{
Contact: "blub", Contact: "blub",
}, },
@ -28,7 +28,7 @@ func TestFilter(t *testing.T) {
// keep owner configuration // keep owner configuration
filter, _ = build(false) filter, _ = build(false)
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{ n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{
Owner: &data.Owner{ Owner: &data.Owner{
Contact: "blub", Contact: "blub",
}, },

View File

@ -22,11 +22,11 @@ func TestFilterSite(t *testing.T) {
assert.NoError(err) assert.NoError(err)
// wronge node // wronge node
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{System: data.System{SiteCode: "ffxx"}}}) n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{System: data.System{SiteCode: "ffxx"}}})
assert.Nil(n) assert.Nil(n)
// right node // right node
n = filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{System: data.System{SiteCode: "ffhb"}}}) n = filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{System: data.System{SiteCode: "ffhb"}}})
assert.NotNil(n) assert.NotNil(n)
// node without data -> wrong node // node without data -> wrong node

89
output/geojson/geojson.go Normal file
View File

@ -0,0 +1,89 @@
package geojson
import (
"strconv"
"strings"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/paulmach/go.geojson"
)
const (
POINT_UMAP_CLASS = "Circle"
POINT_UMAP_ONLINE_COLOR = "Green"
POINT_UMAP_OFFLINE_COLOR = "Red"
)
func newNodePoint(n *runtime.Node) (point *geojson.Feature) {
nodeinfo := n.Nodeinfo
location := nodeinfo.Location
point = geojson.NewPointFeature([]float64{
location.Longitude,
location.Latitude,
})
point.Properties["id"] = nodeinfo.NodeID
point.Properties["name"] = nodeinfo.Hostname
point.Properties["online"] = n.Online
var description strings.Builder
if n.Online {
description.WriteString("Online\n")
if statistics := n.Statistics; statistics != nil {
point.Properties["clients"] = statistics.Clients.Total
description.WriteString("Clients: " + strconv.Itoa(int(statistics.Clients.Total)) + "\n")
}
} else {
description.WriteString("Offline\n")
}
if nodeinfo.Hardware.Model != "" {
point.Properties["model"] = nodeinfo.Hardware.Model
description.WriteString("Model: " + nodeinfo.Hardware.Model + "\n")
}
if fw := nodeinfo.Software.Firmware; fw.Release != "" {
point.Properties["firmware"] = fw.Release
description.WriteString("Firmware: " + fw.Release + "\n")
}
if nodeinfo.System.SiteCode != "" {
point.Properties["site"] = nodeinfo.System.SiteCode
description.WriteString("Site: " + nodeinfo.System.SiteCode + "\n")
}
if nodeinfo.System.DomainCode != "" {
point.Properties["domain"] = nodeinfo.System.DomainCode
description.WriteString("Domain: " + nodeinfo.System.DomainCode + "\n")
}
if owner := nodeinfo.Owner; owner != nil && owner.Contact != "" {
point.Properties["contact"] = owner.Contact
description.WriteString("Contact: " + owner.Contact + "\n")
}
point.Properties["description"] = description.String()
point.Properties["_umap_options"] = getUMapOptions(n)
return
}
func getUMapOptions(n *runtime.Node) map[string]string {
result := map[string]string{
"iconClass": POINT_UMAP_CLASS,
}
if n.Online {
result["color"] = POINT_UMAP_ONLINE_COLOR
} else {
result["color"] = POINT_UMAP_OFFLINE_COLOR
}
return result
}
func transform(nodes *runtime.Nodes) *geojson.FeatureCollection {
nodelist := geojson.NewFeatureCollection()
for _, n := range nodes.List {
if n.Nodeinfo == nil || n.Nodeinfo.Location == nil {
continue
}
point := newNodePoint(n)
if point != nil {
nodelist.Features = append(nodelist.Features, point)
}
}
return nodelist
}

View File

@ -0,0 +1,129 @@
package geojson
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/runtime"
)
const (
testNodeDescription string = "Online\nClients: 42\nModel: TP-Link 841\n" +
"Site: mysite\nDomain: domain_42\n"
)
func TestTransform(t *testing.T) {
testNodes := createTestNodes()
nodes := transform(testNodes)
assert := assert.New(t)
assert.Len(testNodes.List, 4)
assert.Len(nodes.Features, 3)
node := testNodes.List["abcdef012425"]
umap := getUMapOptions(node)
assert.Len(umap, 2)
nodePoint := newNodePoint(node)
assert.Equal(
"abcdef012425",
nodePoint.Properties["id"],
)
assert.Equal(
"TP-Link 841",
nodePoint.Properties["model"],
)
assert.Equal(
uint32(42),
nodePoint.Properties["clients"],
)
assert.Equal(
testNodeDescription,
nodePoint.Properties["description"],
)
}
func createTestNodes() *runtime.Nodes {
nodes := runtime.NewNodes(&runtime.NodesConfig{})
nodes.AddNode(&runtime.Node{
Online: true,
Statistics: &data.Statistics{
Clients: data.Clients{
Total: 42,
},
},
Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012425",
Hardware: data.Hardware{
Model: "TP-Link 841",
},
Location: &data.Location{
Latitude: 24,
Longitude: 2,
},
System: data.System{
SiteCode: "mysite",
DomainCode: "domain_42",
},
},
})
nodeData := &runtime.Node{
Online: true,
Statistics: &data.Statistics{
Clients: data.Clients{
Total: 23,
},
},
Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345",
Hardware: data.Hardware{
Model: "TP-Link 842",
},
System: data.System{
SiteCode: "mysite",
DomainCode: "domain_42",
},
},
}
nodeData.Nodeinfo.Software.Firmware.Release = "2019.1~exp42"
nodes.AddNode(nodeData)
nodes.AddNode(&runtime.Node{
Statistics: &data.Statistics{
Clients: data.Clients{
Total: 2,
},
},
Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566",
Hardware: data.Hardware{
Model: "TP-Link 843",
},
Location: &data.Location{
Latitude: 23,
Longitude: 2,
},
},
})
nodes.AddNode(&runtime.Node{
Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x",
VPN: true,
Hardware: data.Hardware{
Model: "Xeon Multi-Core",
},
Location: &data.Location{
Latitude: 23,
Longitude: 22,
},
},
})
return nodes
}

46
output/geojson/output.go Normal file
View File

@ -0,0 +1,46 @@
package geojson
import (
"errors"
"github.com/FreifunkBremen/yanic/output"
"github.com/FreifunkBremen/yanic/runtime"
)
type Output struct {
output.Output
path string
}
type Config map[string]interface{}
func (c Config) Path() string {
if path, ok := c["path"]; ok {
return path.(string)
}
return ""
}
func init() {
output.RegisterAdapter("geojson", Register)
}
func Register(configuration map[string]interface{}) (output.Output, error) {
var config Config
config = configuration
if path := config.Path(); path != "" {
return &Output{
path: path,
}, nil
}
return nil, errors.New("no path given")
}
func (o *Output) Save(nodes *runtime.Nodes) {
nodes.RLock()
defer nodes.RUnlock()
runtime.SaveJSON(transform(nodes), o.path)
}

View File

@ -0,0 +1,28 @@
package geojson
import (
"os"
"testing"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/stretchr/testify/assert"
)
func TestOutput(t *testing.T) {
assert := assert.New(t)
out, err := Register(map[string]interface{}{})
assert.Error(err)
assert.Nil(out)
out, err = Register(map[string]interface{}{
"path": "/tmp/nodes.geojson",
})
os.Remove("/tmp/nodes.geojson")
assert.NoError(err)
assert.NotNil(out)
out.Save(&runtime.Nodes{})
_, err = os.Stat("/tmp/nodes.geojson")
assert.NoError(err)
}

View File

@ -14,7 +14,7 @@ func TestTransform(t *testing.T) {
nodes := runtime.NewNodes(&runtime.NodesConfig{}) nodes := runtime.NewNodes(&runtime.NodesConfig{})
nodes.AddNode(&runtime.Node{ nodes.AddNode(&runtime.Node{
Online: true, Online: true,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "node_a", NodeID: "node_a",
Network: data.Network{ Network: data.Network{
Mac: "node:a:mac", Mac: "node:a:mac",
@ -51,7 +51,7 @@ func TestTransform(t *testing.T) {
}) })
nodes.AddNode(&runtime.Node{ nodes.AddNode(&runtime.Node{
Online: true, Online: true,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "node_c", NodeID: "node_c",
Network: data.Network{ Network: data.Network{
Mac: "node:c:mac", Mac: "node:c:mac",
@ -69,7 +69,7 @@ func TestTransform(t *testing.T) {
}, },
}, },
Neighbours: &data.Neighbours{ Neighbours: &data.Neighbours{
NodeID: "node_b", NodeID: "node_c",
Batadv: map[string]data.BatadvNeighbours{ Batadv: map[string]data.BatadvNeighbours{
"node:c:mac:lan": { "node:c:mac:lan": {
Neighbours: map[string]data.BatmanLink{ Neighbours: map[string]data.BatmanLink{
@ -81,7 +81,7 @@ func TestTransform(t *testing.T) {
}) })
nodes.AddNode(&runtime.Node{ nodes.AddNode(&runtime.Node{
Online: true, Online: true,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "node_b", NodeID: "node_b",
Network: data.Network{ Network: data.Network{
Mac: "node:b:mac", Mac: "node:b:mac",
@ -117,7 +117,7 @@ func TestTransform(t *testing.T) {
}) })
nodes.AddNode(&runtime.Node{ nodes.AddNode(&runtime.Node{
Online: false, Online: false,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "node_d", NodeID: "node_d",
Network: data.Network{ Network: data.Network{
Mac: "node:d:mac", Mac: "node:d:mac",

View File

@ -12,7 +12,7 @@ func TestRegister(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
nodes := runtime.NewNodes(&runtime.NodesConfig{}) nodes := runtime.NewNodes(&runtime.NodesConfig{})
node := NewNode(nodes, &runtime.Node{ node := NewNode(nodes, &runtime.Node{
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
Owner: &data.Owner{ Owner: &data.Owner{
Contact: "whoami", Contact: "whoami",
}, },
@ -26,7 +26,7 @@ func TestRegister(t *testing.T) {
assert.Len(node.Addresses, 1) assert.Len(node.Addresses, 1)
node = NewNode(nodes, &runtime.Node{ node = NewNode(nodes, &runtime.Node{
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
Owner: &data.Owner{ Owner: &data.Owner{
Contact: "whoami", Contact: "whoami",
}, },

View File

@ -12,7 +12,7 @@ import (
) )
type TestNode struct { type TestNode struct {
Nodeinfo *data.NodeInfo `json:"nodeinfo"` Nodeinfo *data.Nodeinfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"neighbours"` Neighbours *data.Neighbours `json:"neighbours"`
} }
@ -38,7 +38,7 @@ func testGetNodesByFile(files ...string) *runtime.Nodes {
for _, file := range files { for _, file := range files {
node := testGetNodeByFile(file) node := testGetNodeByFile(file)
nodes.Update(file, &data.ResponseData{ nodes.Update(file, &data.ResponseData{
NodeInfo: node.Nodeinfo, Nodeinfo: node.Nodeinfo,
Neighbours: node.Neighbours, Neighbours: node.Neighbours,
}) })
} }

View File

@ -11,7 +11,7 @@ type Node struct {
Lastseen jsontime.Time `json:"lastseen"` Lastseen jsontime.Time `json:"lastseen"`
Flags Flags `json:"flags"` Flags Flags `json:"flags"`
Statistics *Statistics `json:"statistics"` Statistics *Statistics `json:"statistics"`
Nodeinfo *data.NodeInfo `json:"nodeinfo"` Nodeinfo *data.Nodeinfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"-"` Neighbours *data.Neighbours `json:"-"`
} }

View File

@ -31,7 +31,7 @@ func createTestNodes() *runtime.Nodes {
Total: 23, Total: 23,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345", NodeID: "abcdef012345",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -52,7 +52,7 @@ func createTestNodes() *runtime.Nodes {
Free: 8, Free: 8,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566", NodeID: "112233445566",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -61,7 +61,7 @@ func createTestNodes() *runtime.Nodes {
}) })
nodes.AddNode(&runtime.Node{ nodes.AddNode(&runtime.Node{
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x", NodeID: "0xdeadbeef0x",
VPN: true, VPN: true,
Hardware: data.Hardware{ Hardware: data.Hardware{

View File

@ -25,7 +25,7 @@ func createTestNodes() *runtime.Nodes {
Total: 23, Total: 23,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345", NodeID: "abcdef012345",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -41,7 +41,7 @@ func createTestNodes() *runtime.Nodes {
Total: 2, Total: 2,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566", NodeID: "112233445566",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -54,7 +54,7 @@ func createTestNodes() *runtime.Nodes {
}) })
nodes.AddNode(&runtime.Node{ nodes.AddNode(&runtime.Node{
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x", NodeID: "0xdeadbeef0x",
VPN: true, VPN: true,
Hardware: data.Hardware{ Hardware: data.Hardware{

46
output/raw/output.go Normal file
View File

@ -0,0 +1,46 @@
package raw
import (
"errors"
"github.com/FreifunkBremen/yanic/output"
"github.com/FreifunkBremen/yanic/runtime"
)
type Output struct {
output.Output
path string
}
type Config map[string]interface{}
func (c Config) Path() string {
if path, ok := c["path"]; ok {
return path.(string)
}
return ""
}
func init() {
output.RegisterAdapter("raw", Register)
}
func Register(configuration map[string]interface{}) (output.Output, error) {
var config Config
config = configuration
if path := config.Path(); path != "" {
return &Output{
path: path,
}, nil
}
return nil, errors.New("no path given")
}
func (o *Output) Save(nodes *runtime.Nodes) {
nodes.RLock()
defer nodes.RUnlock()
runtime.SaveJSON(transform(nodes), o.path)
}

28
output/raw/output_test.go Normal file
View File

@ -0,0 +1,28 @@
package raw
import (
"os"
"testing"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/stretchr/testify/assert"
)
func TestOutput(t *testing.T) {
assert := assert.New(t)
out, err := Register(map[string]interface{}{})
assert.Error(err)
assert.Nil(out)
out, err = Register(map[string]interface{}{
"path": "/tmp/raw.json",
})
os.Remove("/tmp/raw.json")
assert.NoError(err)
assert.NotNil(out)
out.Save(&runtime.Nodes{})
_, err = os.Stat("/tmp/raw.json")
assert.NoError(err)
}

45
output/raw/raw.go Normal file
View File

@ -0,0 +1,45 @@
package raw
import (
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/lib/jsontime"
"github.com/FreifunkBremen/yanic/runtime"
)
// Node struct
type RawNode struct {
Firstseen jsontime.Time `json:"firstseen"`
Lastseen jsontime.Time `json:"lastseen"`
Online bool `json:"online"`
Statistics *data.Statistics `json:"statistics"`
Nodeinfo *data.Nodeinfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"neighbours"`
}
type NodeList struct {
Version string `json:"version"`
Timestamp jsontime.Time `json:"updated_at"` // Timestamp of the generation
List []*RawNode `json:"nodes"`
}
func transform(nodes *runtime.Nodes) *NodeList {
nodelist := &NodeList{
Version: "1.0.0",
Timestamp: jsontime.Now(),
}
for _, nodeOrigin := range nodes.List {
if nodeOrigin != nil {
node := &RawNode{
Firstseen: nodeOrigin.Firstseen,
Lastseen: nodeOrigin.Lastseen,
Online: nodeOrigin.Online,
Statistics: nodeOrigin.Statistics,
Nodeinfo: nodeOrigin.Nodeinfo,
Neighbours: nodeOrigin.Neighbours,
}
nodelist.List = append(nodelist.List, node)
}
}
return nodelist
}

67
output/raw/raw_test.go Normal file
View File

@ -0,0 +1,67 @@
package raw
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.NodesConfig{})
nodeData := &runtime.Node{
Statistics: &data.Statistics{
Clients: data.Clients{
Total: 23,
},
},
Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345",
Hardware: data.Hardware{
Model: "TP-Link 841",
},
},
}
nodeData.Nodeinfo.Software.Firmware.Release = "2016.1.6+entenhausen1"
nodes.AddNode(nodeData)
nodes.AddNode(&runtime.Node{
Statistics: &data.Statistics{
Clients: data.Clients{
Total: 2,
},
},
Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566",
Hardware: data.Hardware{
Model: "TP-Link 841",
},
Location: &data.Location{
Latitude: 23,
Longitude: 2,
},
},
})
nodes.AddNode(&runtime.Node{
Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x",
VPN: true,
Hardware: data.Hardware{
Model: "Xeon Multi-Core",
},
},
})
return nodes
}

View File

@ -66,7 +66,7 @@ func (coll *Collector) listenUDP(iface InterfaceConfig) {
if iface.IPAddress != "" { if iface.IPAddress != "" {
addr = net.ParseIP(iface.IPAddress) addr = net.ParseIP(iface.IPAddress)
} else { } else {
addr, err = getUnicastAddr(iface.InterfaceName, iface.MulticastAddress == "") addr, err = getUnicastAddr(iface.InterfaceName)
if err != nil { if err != nil {
log.WithField("iface", iface.InterfaceName).Panic(err) log.WithField("iface", iface.InterfaceName).Panic(err)
} }
@ -99,7 +99,7 @@ func (coll *Collector) listenUDP(iface InterfaceConfig) {
} }
// Returns a unicast address of given interface (linklocal or global unicast address) // Returns a unicast address of given interface (linklocal or global unicast address)
func getUnicastAddr(ifname string, linklocal bool) (net.IP, error) { func getUnicastAddr(ifname string) (net.IP, error) {
iface, err := net.InterfaceByName(ifname) iface, err := net.InterfaceByName(ifname)
if err != nil { if err != nil {
return nil, err return nil, err
@ -116,7 +116,7 @@ func getUnicastAddr(ifname string, linklocal bool) (net.IP, error) {
if !ok { if !ok {
continue continue
} }
if (!linklocal && ipnet.IP.IsGlobalUnicast()) || (linklocal && ipnet.IP.IsLinkLocalUnicast()) { if (ip == nil && ipnet.IP.IsGlobalUnicast()) || ipnet.IP.IsLinkLocalUnicast() {
ip = ipnet.IP ip = ipnet.IP
} }
} }
@ -260,7 +260,7 @@ func (res *Response) parse() (*data.ResponseData, error) {
func (coll *Collector) saveResponse(addr *net.UDPAddr, res *data.ResponseData) { func (coll *Collector) saveResponse(addr *net.UDPAddr, res *data.ResponseData) {
// Search for NodeID // Search for NodeID
var nodeID string var nodeID string
if val := res.NodeInfo; val != nil { if val := res.Nodeinfo; val != nil {
nodeID = val.NodeID nodeID = val.NodeID
} else if val := res.Neighbours; val != nil { } else if val := res.Neighbours; val != nil {
nodeID = val.NodeID nodeID = val.NodeID
@ -284,8 +284,8 @@ func (coll *Collector) saveResponse(addr *net.UDPAddr, res *data.ResponseData) {
if res.Neighbours != nil && res.Neighbours.NodeID != nodeID { if res.Neighbours != nil && res.Neighbours.NodeID != nodeID {
res.Neighbours = nil res.Neighbours = nil
} }
if res.NodeInfo != nil && res.NodeInfo.NodeID != nodeID { if res.Nodeinfo != nil && res.Nodeinfo.NodeID != nodeID {
res.NodeInfo = nil res.Nodeinfo = nil
} }
// Process the data and update IP address // Process the data and update IP address

View File

@ -39,5 +39,5 @@ func TestParse(t *testing.T) {
assert.NoError(err) assert.NoError(err)
assert.NotNil(data) assert.NotNil(data)
assert.Equal("f81a67a5e9c1", data.NodeInfo.NodeID) assert.Equal("f81a67a5e9c1", data.Nodeinfo.NodeID)
} }

View File

@ -6,7 +6,7 @@ import (
const ( const (
// default multicast group used by announced // default multicast group used by announced
multicastAddressDefault = "ff02:0:0:0:0:0:2:1001" multicastAddressDefault = "ff05:0:0:0:0:0:2:1001"
// default udp port used by announced // default udp port used by announced
port = 1001 port = 1001

View File

@ -13,9 +13,8 @@ type Node struct {
Firstseen jsontime.Time `json:"firstseen"` Firstseen jsontime.Time `json:"firstseen"`
Lastseen jsontime.Time `json:"lastseen"` Lastseen jsontime.Time `json:"lastseen"`
Online bool `json:"online"` Online bool `json:"online"`
NoRespondd bool `json:"no_respondd"`
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:"-"`
} }

View File

@ -13,7 +13,7 @@ func TestNode(t *testing.T) {
node := &Node{} node := &Node{}
assert.False(node.IsGateway()) assert.False(node.IsGateway())
node.Nodeinfo = &data.NodeInfo{} node.Nodeinfo = &data.Nodeinfo{}
assert.False(node.IsGateway()) assert.False(node.IsGateway())
node.Nodeinfo.VPN = true node.Nodeinfo.VPN = true

View File

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/bdlm/log" "github.com/bdlm/log"
ping "github.com/digineo/go-ping"
"github.com/FreifunkBremen/yanic/data" "github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/lib/jsontime" "github.com/FreifunkBremen/yanic/lib/jsontime"
@ -18,7 +17,6 @@ type Nodes struct {
List map[string]*Node `json:"nodes"` // the current nodemap, indexed by node ID List map[string]*Node `json:"nodes"` // the current nodemap, indexed by node ID
ifaceToNodeID map[string]string // mapping from MAC address to NodeID ifaceToNodeID map[string]string // mapping from MAC address to NodeID
config *NodesConfig config *NodesConfig
pinger *ping.Pinger
sync.RWMutex sync.RWMutex
} }
@ -29,11 +27,6 @@ func NewNodes(config *NodesConfig) *Nodes {
ifaceToNodeID: make(map[string]string), ifaceToNodeID: make(map[string]string),
config: config, config: config,
} }
p, err := ping.New("", "::")
if err != nil {
log.Warnf("ping bind failed: %s", err)
}
nodes.pinger = p
if config.StatePath != "" { if config.StatePath != "" {
nodes.load() nodes.load()
@ -71,8 +64,8 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
} }
nodes.List[nodeID] = node nodes.List[nodeID] = node
} }
if res.NodeInfo != nil { if res.Nodeinfo != nil {
nodes.readIfaces(res.NodeInfo) nodes.readIfaces(res.Nodeinfo)
} }
nodes.Unlock() nodes.Unlock()
@ -87,10 +80,9 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
// Update fields // Update fields
node.Lastseen = now node.Lastseen = now
node.Online = true node.Online = true
node.Nodeinfo = res.NodeInfo
node.Statistics = res.Statistics
node.Neighbours = res.Neighbours node.Neighbours = res.Neighbours
node.NoRespondd = false node.Nodeinfo = res.Nodeinfo
node.Statistics = res.Statistics
return node return node
} }
@ -178,33 +170,19 @@ func (nodes *Nodes) expire() {
nodes.Lock() nodes.Lock()
defer nodes.Unlock() defer nodes.Unlock()
wg := sync.WaitGroup{}
for id, node := range nodes.List { for id, node := range nodes.List {
if node.Lastseen.Before(pruneAfter) { if node.Lastseen.Before(pruneAfter) {
// expire // 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
wg.Add(1)
go func(node *Node) {
defer wg.Done()
if nodes.config.PingCount > 0 && nodes.ping(node) {
node.Online = true
node.NoRespondd = true
} else {
node.Online = false node.Online = false
node.NoRespondd = false
}
}(node)
} }
} }
wg.Wait()
log.WithField("nodes", "expire").Info("end")
} }
// adds the nodes interface addresses to the internal map // adds the nodes interface addresses to the internal map
func (nodes *Nodes) readIfaces(nodeinfo *data.NodeInfo) { func (nodes *Nodes) readIfaces(nodeinfo *data.Nodeinfo) {
nodeID := nodeinfo.NodeID nodeID := nodeinfo.NodeID
network := nodeinfo.Network network := nodeinfo.Network
@ -262,7 +240,6 @@ func (nodes *Nodes) save() {
// serialize nodes // serialize nodes
SaveJSON(nodes, nodes.config.StatePath) SaveJSON(nodes, nodes.config.StatePath)
log.WithField("nodes", "save").Info("end")
} }
// SaveJSON to path // SaveJSON to path

View File

@ -7,7 +7,5 @@ type NodesConfig struct {
SaveInterval duration.Duration `toml:"save_interval"` // Save nodes periodically SaveInterval duration.Duration `toml:"save_interval"` // Save nodes periodically
OfflineAfter duration.Duration `toml:"offline_after"` // Set node to offline if not seen within this period OfflineAfter duration.Duration `toml:"offline_after"` // Set node to offline if not seen within this period
PruneAfter duration.Duration `toml:"prune_after"` // Remove nodes after n days of inactivity PruneAfter duration.Duration `toml:"prune_after"` // Remove nodes after n days of inactivity
PingCount int `toml:"ping_count"` // send x pings to verify if node is offline (for disable count < 1)
PingTimeout duration.Duration `toml:"ping_timeout"` // timeout of sending ping to a node
Output map[string]interface{} Output map[string]interface{}
} }

View File

@ -1,39 +0,0 @@
package runtime
import (
"net"
"github.com/bdlm/log"
)
func (nodes *Nodes) ping(node *Node) bool {
logNode := log.WithField("node_id", "unknown")
if node.Nodeinfo != nil {
logNode = logNode.WithField("node_id", node.Nodeinfo.NodeID)
}
var addr *net.IPAddr
if node.Address != nil {
addr = &net.IPAddr{IP:node.Address.IP, Zone: node.Address.Zone}
} else {
logNode.Debug("error no address found")
if node.Nodeinfo != nil {
for _, addrMaybeString := range node.Nodeinfo.Network.Addresses {
if len(addrMaybeString) >= 5 && addrMaybeString[:5] != "fe80:" {
addrMaybe, err := net.ResolveIPAddr("ip6", addrMaybeString)
if err == nil {
addr = addrMaybe
}
}
}
}
}
logAddr := logNode.WithField("addr", addr.String())
_, err := nodes.pinger.PingAttempts(addr, nodes.config.PingTimeout.Duration, nodes.config.PingCount)
logAddr.WithFields(map[string]interface{}{
"success": err == nil,
}).Debug("pong")
return err == nil
}

View File

@ -1,44 +0,0 @@
package runtime
import (
"net"
"testing"
"time"
"github.com/bdlm/log"
"github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data"
)
func TestPing(t *testing.T) {
log.SetLevel(log.DebugLevel)
assert := assert.New(t)
config := &NodesConfig{
PingCount: 1,
}
config.OfflineAfter.Duration = time.Minute * 10
// to get default (100%) path of testing
// config.PruneAfter.Duration = time.Hour * 24 * 6
nodes := &Nodes{
config: config,
List: make(map[string]*Node),
ifaceToNodeID: make(map[string]string),
}
node := nodes.Update("expire", &data.ResponseData{NodeInfo: &data.NodeInfo{
NodeID: "nodeID-Lola",
Network: data.Network{Addresses: []string{"fe80::1", "fd2f::1"}},
}})
// get fallback
assert.False(nodes.ping(node))
node.Address = &net.UDPAddr{Zone: "bat0"}
// error during ping
assert.False(nodes.ping(node))
node.Address.IP = net.ParseIP("fe80::1")
// error during ping
assert.False(nodes.ping(node))
}

View File

@ -55,8 +55,8 @@ func TestLoadAndSave(t *testing.T) {
// not autoload without StatePath // not autoload without StatePath
NewNodes(config) NewNodes(config)
// Test unmarshalable /dev/null - autolead with StatePath // Test unmarshalable
config.StatePath = "/dev/null" config.StatePath = "testdata/nodes-broken.json"
nodes := NewNodes(config) nodes := NewNodes(config)
// Test unopen able // Test unopen able
config.StatePath = "/root/nodes.json" config.StatePath = "/root/nodes.json"
@ -71,8 +71,8 @@ func TestLoadAndSave(t *testing.T) {
os.Remove(tmpfile.Name()) os.Remove(tmpfile.Name())
assert.Panics(func() { assert.Panics(func() {
SaveJSON(nodes, "/dev/null") SaveJSON(nodes, "/proc/a")
// "open /dev/null.tmp: permission denied", // "open /proc/a.tmp: permission denied",
}) })
tmpfile, _ = ioutil.TempFile("/tmp", "nodes") tmpfile, _ = ioutil.TempFile("/tmp", "nodes")
@ -102,7 +102,7 @@ func TestUpdateNodes(t *testing.T) {
&data.WirelessAirtime{}, &data.WirelessAirtime{},
}, },
}, },
NodeInfo: &data.NodeInfo{}, Nodeinfo: &data.Nodeinfo{},
} }
nodes.Update("abcdef012345", res) nodes.Update("abcdef012345", res)
@ -146,10 +146,10 @@ func TestAddNode(t *testing.T) {
nodes.AddNode(&Node{}) nodes.AddNode(&Node{})
assert.Len(nodes.List, 0) assert.Len(nodes.List, 0)
nodes.AddNode(&Node{Nodeinfo: &data.NodeInfo{}}) nodes.AddNode(&Node{Nodeinfo: &data.Nodeinfo{}})
assert.Len(nodes.List, 0) assert.Len(nodes.List, 0)
nodes.AddNode(&Node{Nodeinfo: &data.NodeInfo{NodeID: "blub"}}) nodes.AddNode(&Node{Nodeinfo: &data.Nodeinfo{NodeID: "blub"}})
assert.Len(nodes.List, 1) assert.Len(nodes.List, 1)
} }
@ -162,18 +162,52 @@ func TestLinksNodes(t *testing.T) {
} }
assert.Len(nodes.List, 0) assert.Len(nodes.List, 0)
nodes.Update("f4f26dd7a300", &data.ResponseData{
Nodeinfo: &data.Nodeinfo{
NodeID: "f4f26dd7a300",
Network: data.Network{
Mac: "f4:f2:6d:d7:a3:00",
},
},
})
nodes.Update("f4f26dd7a30a", &data.ResponseData{ nodes.Update("f4f26dd7a30a", &data.ResponseData{
NodeInfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "f4f26dd7a30a", NodeID: "f4f26dd7a30a",
Network: data.Network{ Network: data.Network{
Mac: "f4:f2:6d:d7:a3:0a", Mac: "f4:f2:6d:d7:a3:0a",
}, },
}, },
Neighbours: &data.Neighbours{
NodeID: "f4f26dd7a30a",
Babel: map[string]data.BabelNeighbours{
"vx_mesh_lan": {
LinkLocalAddress: "fe80::2",
Neighbours: map[string]data.BabelLink{
"fe80::1337": {
Cost: 26214,
},
},
},
},
},
}) })
nodes.Update("f4f26dd7a30b", &data.ResponseData{ nodes.Update("f4f26dd7a30b", &data.ResponseData{
NodeInfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "f4f26dd7a30b", NodeID: "f4f26dd7a30b",
Network: data.Network{
Mesh: map[string]*data.NetworkInterface{
"babel": {
Interfaces: struct {
Wireless []string `json:"wireless,omitempty"`
Other []string `json:"other,omitempty"`
Tunnel []string `json:"tunnel,omitempty"`
}{
Other: []string{"fe80::1337"},
},
},
},
},
}, },
Neighbours: &data.Neighbours{ Neighbours: &data.Neighbours{
NodeID: "f4f26dd7a30b", NodeID: "f4f26dd7a30b",
@ -189,21 +223,35 @@ func TestLinksNodes(t *testing.T) {
}, },
}) })
node := nodes.List["f4f26dd7a30a"] // no neighbours nodeid
node := nodes.List["f4f26dd7a300"]
assert.NotNil(node) assert.NotNil(node)
links := nodes.NodeLinks(node) links := nodes.NodeLinks(node)
assert.Len(links, 0) assert.Len(links, 0)
node = nodes.List["f4f26dd7a30b"] // babel link
node = nodes.List["f4f26dd7a30a"]
assert.NotNil(node) assert.NotNil(node)
links = nodes.NodeLinks(node) links = nodes.NodeLinks(node)
assert.Len(links, 1) assert.Len(links, 1)
link := links[0] link := links[0]
assert.Equal(link.SourceID, "f4f26dd7a30b") assert.Equal("f4f26dd7a30a", link.SourceID)
assert.Equal(link.SourceAddress, "f4:f2:6d:d7:a3:0b") assert.Equal("fe80::2", link.SourceAddress)
assert.Equal(link.TargetID, "f4f26dd7a30a") assert.Equal("f4f26dd7a30b", link.TargetID)
assert.Equal(link.TargetAddress, "f4:f2:6d:d7:a3:0a") assert.Equal("fe80::1337", link.TargetAddress)
assert.Equal(link.TQ, float32(0.8)) assert.Equal(float32(0.6), link.TQ)
// batman link
node = nodes.List["f4f26dd7a30b"]
assert.NotNil(node)
links = nodes.NodeLinks(node)
assert.Len(links, 1)
link = links[0]
assert.Equal("f4f26dd7a30b", link.SourceID)
assert.Equal("f4:f2:6d:d7:a3:0b", link.SourceAddress)
assert.Equal("f4f26dd7a30a", link.TargetID)
assert.Equal("f4:f2:6d:d7:a3:0a", link.TargetAddress)
assert.Equal(float32(0.8), link.TQ)
nodeid := nodes.GetNodeIDbyAddress("f4:f2:6d:d7:a3:0a") nodeid := nodes.GetNodeIDbyAddress("f4:f2:6d:d7:a3:0a")
assert.Equal("f4f26dd7a30a", nodeid) assert.Equal("f4f26dd7a30a", nodeid)

View File

@ -17,7 +17,6 @@ type GlobalStats struct {
ClientsWifi5 uint32 ClientsWifi5 uint32
Gateways uint32 Gateways uint32
Nodes uint32 Nodes uint32
NodesNoRespondd uint32
Firmwares CounterMap Firmwares CounterMap
Models CounterMap Models CounterMap
@ -82,9 +81,6 @@ func (s *GlobalStats) Add(node *Node) {
s.ClientsWifi5 += stats.Clients.Wifi5 s.ClientsWifi5 += stats.Clients.Wifi5
s.ClientsWifi += stats.Clients.Wifi s.ClientsWifi += stats.Clients.Wifi
} }
if node.NoRespondd {
s.NodesNoRespondd++
}
if node.IsGateway() { if node.IsGateway() {
s.Gateways++ s.Gateways++
} }

View File

@ -22,7 +22,6 @@ func TestGlobalStats(t *testing.T) {
//check GLOBAL_SITE stats //check GLOBAL_SITE stats
assert.EqualValues(1, stats[GLOBAL_SITE][GLOBAL_DOMAIN].Gateways) assert.EqualValues(1, stats[GLOBAL_SITE][GLOBAL_DOMAIN].Gateways)
assert.EqualValues(3, stats[GLOBAL_SITE][GLOBAL_DOMAIN].Nodes) assert.EqualValues(3, stats[GLOBAL_SITE][GLOBAL_DOMAIN].Nodes)
assert.EqualValues(1, stats[GLOBAL_SITE][GLOBAL_DOMAIN].NodesNoRespondd)
assert.EqualValues(25, stats[GLOBAL_SITE][GLOBAL_DOMAIN].Clients) assert.EqualValues(25, stats[GLOBAL_SITE][GLOBAL_DOMAIN].Clients)
// check models // check models
@ -85,7 +84,7 @@ func createTestNodes() *Nodes {
Total: 23, Total: 23,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345", NodeID: "abcdef012345",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -100,13 +99,12 @@ func createTestNodes() *Nodes {
nodes.AddNode(&Node{ nodes.AddNode(&Node{
Online: true, Online: true,
NoRespondd: true,
Statistics: &data.Statistics{ Statistics: &data.Statistics{
Clients: data.Clients{ Clients: data.Clients{
Total: 2, Total: 2,
}, },
}, },
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566", NodeID: "112233445566",
Hardware: data.Hardware{ Hardware: data.Hardware{
Model: "TP-Link 841", Model: "TP-Link 841",
@ -125,7 +123,7 @@ func createTestNodes() *Nodes {
nodes.AddNode(&Node{ nodes.AddNode(&Node{
Online: true, Online: true,
Nodeinfo: &data.NodeInfo{ Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x", NodeID: "0xdeadbeef0x",
VPN: true, VPN: true,
Hardware: data.Hardware{ Hardware: data.Hardware{

4
runtime/testdata/nodes-broken.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
// make it invalid by this comment
"affe": 3
}