diff --git a/.gitignore b/.gitignore index 894526c..be417c3 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ _testmain.go webroot /config.toml /vendor +/bin \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d588e65..29c8a99 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ stages: before_script: - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - 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 - dep ensure @@ -14,9 +14,13 @@ build-my-project: stage: build 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 + - mkdir /builds/freifunkbremen/yanic/bin/ + - cp /go/bin/yanic /builds/freifunkbremen/yanic/bin/yanic artifacts: paths: - - /go/bin/yanic + - bin/yanic + - config_example.toml + - config-respondd_example.toml test-my-project: stage: test @@ -24,6 +28,7 @@ test-my-project: - ./.circleci/check-gofmt - ./.circleci/check-testfiles - go test $(go list ./... | grep -v /vendor/) -v -coverprofile .testCoverage.txt + - go tool cover -func=.testCoverage.txt test-race-my-project: stage: test diff --git a/Gopkg.lock b/Gopkg.lock index bfec60e..e6a44e6 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,12 +2,28 @@ [[projects]] - digest = "1:0803645e1f57fb5271a6edc7570b9ea59bac2e5de67957075a43f3d74c8dbd97" + digest = "1:af6e785bedb62fc2abb81977c58a7a44e5cf9f7e41b8d3c8dd4d872edea0ce08" name = "github.com/NYTimes/gziphandler" packages = ["."] pruneopts = "UT" - revision = "2600fb119af974220d3916a5916d6e31176aac1b" - version = "v1.0.1" + revision = "dd0439581c7657cb652dfe5c71d7d48baf39541d" + 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]] digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" @@ -33,35 +49,17 @@ revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" version = "v1.0" -[[projects]] - digest = "1:e0123d1a40d588fb2d5f98188927826ed5ddfa1d50e759596a6c774f71b81358" - name = "github.com/influxdata/influxdb" - packages = ["pkg/escape"] - pruneopts = "UT" - revision = "698dbc789aff13c2678357a6b93ff73dd7136571" - version = "v1.7.3" - [[projects]] branch = "master" - digest = "1:22a9593272bc74ef4c513e176608eeed2dee7e8bd7542bae40369cb62d907a3b" + digest = "1:50708c8fc92aec981df5c446581cf9f90ba9e2a5692118e0ce75d4534aaa14a2" name = "github.com/influxdata/influxdb1-client" packages = [ "models", + "pkg/escape", "v2", ] pruneopts = "UT" - revision = "e237d47d0fc87c8ba2b7c98c349cdceca9c37e27" - -[[projects]] - branch = "master" - digest = "1:ac6b73796941a7ab33ec69fc32f9886a6e7506998f822e3d5205377aee77284a" - name = "github.com/influxdata/platform" - packages = [ - "models", - "pkg/escape", - ] - pruneopts = "UT" - revision = "98469bf07613ffae6f025893761c1e7a5e96e4aa" + revision = "8ff2fc3824fcb533795f9a2f233275f0bb18d6c5" [[projects]] digest = "1:b56c589214f01a5601e0821387db484617392d0042f26234bf2da853a2f498a1" @@ -82,6 +80,14 @@ revision = "e6f5723bf2a66af014955e0888881314cf294129" version = "v0.1.1" +[[projects]] + digest = "1:36cfc0b6facbc68c92a4d55f32eff22b872a899ab75c384ac24a08dd33bc1380" + name = "github.com/paulmach/go.geojson" + packages = ["."] + pruneopts = "UT" + revision = "a5c451da2d9c67c590ae6be92565b67698286a1a" + version = "v1.4" + [[projects]] digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b" name = "github.com/pkg/errors" @@ -99,12 +105,12 @@ version = "v1.0.0" [[projects]] - digest = "1:645cabccbb4fa8aab25a956cbcbdf6a6845ca736b2c64e197ca7cbb9d210b939" + digest = "1:22799aea8fe96dd5693abdd1eaa14b1b29e3eafbdc7733fa155b3cb556c8a7ae" name = "github.com/spf13/cobra" packages = ["."] pruneopts = "UT" - revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385" - version = "v0.0.3" + revision = "67fc4837d267bc9bfd6e47f77783fcc3dffc68de" + version = "v0.0.4" [[projects]] digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2" @@ -122,15 +128,37 @@ revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" 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] analyzer-name = "dep" analyzer-version = 1 input-imports = [ "github.com/NYTimes/gziphandler", + "github.com/bdlm/log", + "github.com/bdlm/std/logger", "github.com/fgrosse/graphigo", "github.com/influxdata/influxdb1-client/models", "github.com/influxdata/influxdb1-client/v2", "github.com/naoina/toml", + "github.com/paulmach/go.geojson", "github.com/pkg/errors", "github.com/spf13/cobra", "github.com/stretchr/testify/assert", diff --git a/INSTALL.md b/INSTALL.md index 8d890e1..4ae77c7 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -5,7 +5,7 @@ ### Install ```sh 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 rm go-release-linux-amd64.tar.gz ``` @@ -62,7 +62,15 @@ the same directory as the `dataPath`. Change the path in the section ```sh cp /opt/go/src/github.com/FreifunkBremen/yanic/contrib/init/linux-systemd/yanic.service /lib/systemd/system/yanic.service systemctl daemon-reload +``` + +Before start, you should configure yanic by the file `/etc/yanic.conf`: +```sh systemctl start yanic +``` + +Enable to start on boot: +```sh systemctl enable yanic ``` diff --git a/README.md b/README.md index dd71f0f..1d4c8a5 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,25 @@ Yet another node info collector * Provide a little webserver for a standalone installation with a meshviewer ## How it works - -In the first step Yanic sends a multicast message to the group `ff02:0:0:0:0:0:2:1001` and port `1001`. +In the first step Yanic sends a multicast message to the group `ff05::2:1001` and port `1001`. Recently seen nodes that does not reply are requested via a unicast message. ## Documentation 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 Read comments in [config_example.toml](config_example.toml) for more information. ## Running - Yanic provides several commands: ### Usage - Run Yanic without any arguments to get the usage information: - ``` Usage: yanic [command] @@ -49,58 +49,72 @@ Available Commands: serve Runs the yanic server Flags: - -h, --help help for yanic - --timestamps Enables timestamps for log output + -h, --help help for yanic + --loglevel uint32 Show log message starting at level (default 40) + --timestamps Enables timestamps for log output Use "yanic [command] --help" for more information about a command. ``` #### Serve - +Runs the yanic server ``` Usage: yanic serve [flags] Examples: - yanic serve --config /etc/yanic.toml +yanic serve --config /etc/yanic.toml Flags: -c, --config string Path to configuration file (default "config.toml") -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 [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 - +Imports global statistics from the given RRD files (ffmap-backend). ``` Usage: - yanic import [flags] + yanic import [flags] Examples: - yanic import --config /etc/yanic.toml olddata.rrd +yanic import --config /etc/yanic.toml olddata.rrd global global Flags: -c, --config string Path to configuration file (default "config.toml") -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 [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 - * **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 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)! ## Related projects - Collecting data from respondd: * [HopGlass Server](https://github.com/plumpudding/hopglass-server) written in Node.js @@ -118,5 +131,4 @@ Respondd for servers: ## License - This software is licensed under the terms of the [AGPL v3 License](LICENSE). diff --git a/cmd/config_test.go b/cmd/config_test.go index ce6cd61..4499a5c 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -25,7 +25,7 @@ func TestReadConfig(t *testing.T) { assert.Contains(config.Respondd.Sites["ffhb"].Domains, "city") // Test output plugins - assert.Len(config.Nodes.Output, 3) + assert.Len(config.Nodes.Output, 4) outputs := config.Nodes.Output["meshviewer"].([]interface{}) assert.Len(outputs, 1) meshviewer := outputs[0] diff --git a/config_example.toml b/config_example.toml index a5a3b53..c23da7c 100644 --- a/config_example.toml +++ b/config_example.toml @@ -1,6 +1,6 @@ # This is the config file for Yanic written in "Tom's Obvious, Minimal Language." # 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 [respondd] @@ -23,14 +23,14 @@ domains = ["city"] # name of interface on which this collector is running ifname = "br-ffhb" # ip address which is used for sending -# (optional - without definition used a address of ifname) +# (optional - without definition used a address of ifname - prefered link local) ip_address = "fd2f:5119:f2d::5" # disable sending multicast respondd request # (for receiving only respondd packages e.g. database respondd) #send_no_request = false # multicast address to destination of respondd -# (optional - without definition used batman default ff02::2:1001) -multicast_address = "ff05::2:1001" +# (optional - without definition used default ff05::2:1001) +#multicast_address = "ff02::2:1001" # define a port to listen # if not set or set to 0 the kernel will use a random free port at its own #port = 10001 @@ -93,6 +93,11 @@ offline_after = "10m" #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 [[nodes.output.meshviewer-ffrgb]] enable = true diff --git a/database/graphite/node.go b/database/graphite/node.go index d5db231..5f9dca7 100644 --- a/database/graphite/node.go +++ b/database/graphite/node.go @@ -30,25 +30,36 @@ func (c *Connection) InsertNode(node *runtime.Node) { 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 { 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 batadv := 0 - for _, batadvNeighbours := range neighbours.Batadv { + for mac, batadvNeighbours := range neighbours.Batadv { batadv += len(batadvNeighbours.Neighbours) + if _, ok := vpnInterfaces[mac]; ok { + vpn += len(batadvNeighbours.Neighbours) + } } 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 lldp := 0 for _, lldpNeighbours := range neighbours.LLDP { @@ -56,8 +67,10 @@ func (c *Connection) InsertNode(node *runtime.Node) { } addField("neighbours.lldp", lldp) + addField("neighbours.vpn", vpn) + // 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 { diff --git a/database/influxdb/node.go b/database/influxdb/node.go index daf3c83..9c69d5c 100644 --- a/database/influxdb/node.go +++ b/database/influxdb/node.go @@ -48,7 +48,15 @@ func (conn *Connection) InsertNode(node *runtime.Node) { "memory.available": stats.Memory.Available, } + vpnInterfaces := make(map[string]bool) + 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) if 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 { // VPN Neighbours are Neighbours but includet in one protocol 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 batadv := 0 - for _, batadvNeighbours := range neighbours.Batadv { + for mac, batadvNeighbours := range neighbours.Batadv { batadv += len(batadvNeighbours.Neighbours) + if _, ok := vpnInterfaces[mac]; ok { + vpn += len(batadvNeighbours.Neighbours) + } } 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 lldp := 0 for _, lldpNeighbours := range neighbours.LLDP { @@ -104,8 +114,11 @@ func (conn *Connection) InsertNode(node *runtime.Node) { } fields["neighbours.lldp"] = lldp + // vpn wait for babel + fields["neighbours.vpn"] = vpn + // total is the sum of all protocols - fields["neighbours.total"] = batadv + lldp + fields["neighbours.total"] = batadv + babel + lldp } if procstat := stats.ProcStats; procstat != nil { fields["stat.cpu.user"] = procstat.CPU.User diff --git a/database/influxdb/node_test.go b/database/influxdb/node_test.go index f2286dd..cfc5768 100644 --- a/database/influxdb/node_test.go +++ b/database/influxdb/node_test.go @@ -39,18 +39,6 @@ func TestToInflux(t *testing.T) { MgmtTx: &data.Traffic{Packets: 2327}, 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{ NodeID: "deadbeef", @@ -67,6 +55,17 @@ func TestToInflux(t *testing.T) { }, Network: data.Network{ 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{ Autoupdater: struct { @@ -81,7 +80,7 @@ func TestToInflux(t *testing.T) { Neighbours: &data.Neighbours{ NodeID: "deadbeef", Batadv: map[string]data.BatadvNeighbours{ - "a-interface": { + "a-interface-mac": { Neighbours: map[string]data.BatmanLink{ "BAFF1E5": { Tq: 204, @@ -89,8 +88,18 @@ 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{ - "b-interface": {}, + "b-interface-mac": {}, }, }, } @@ -144,9 +153,10 @@ func TestToInflux(t *testing.T) { assert.EqualValues("city", tags["domain"]) assert.EqualValues(0.5, fields["load"]) assert.EqualValues(0, fields["neighbours.lldp"]) + assert.EqualValues(1, fields["neighbours.babel"]) assert.EqualValues(1, fields["neighbours.batadv"]) - assert.EqualValues(1, fields["neighbours.vpn"]) - assert.EqualValues(1, fields["neighbours.total"]) + assert.EqualValues(2, fields["neighbours.vpn"]) + assert.EqualValues(2, fields["neighbours.total"]) assert.EqualValues(uint32(3), fields["wireless.txpower24"]) assert.EqualValues(uint32(5500), fields["airtime11a.frequency"]) @@ -165,7 +175,7 @@ func TestToInflux(t *testing.T) { assert.EqualValues("link", nPoint.Name()) assert.EqualValues(map[string]string{ "source.id": "deadbeef", - "source.addr": "a-interface", + "source.addr": "a-interface-mac", "target.id": "foobar", "target.addr": "BAFF1E5", }, tags) diff --git a/docs/docs_configuration.md b/docs/docs_configuration.md index 4c5d406..06e3658 100644 --- a/docs/docs_configuration.md +++ b/docs/docs_configuration.md @@ -52,7 +52,7 @@ synchronize = "1m" {% method %} 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`. {% sample lang="toml" %} ```toml @@ -110,7 +110,7 @@ ifname = "br-ffhb" {% method %} 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 `multicast_address` is not set the link local address otherwise a global unicast address) +(It preferes the link local address, so at babel mesh-network it should be configurated) {% sample lang="toml" %} ```toml ip_address = "fe80::..." @@ -130,8 +130,8 @@ send_no_request = true ### multicast_address {% method %} 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` -(Needed in babel for a mesh-network wide routeable multicast addreess `ff05::2:1001`) +If not set or set with empty string it will take the batman default multicast address `ff05::2:1001` +(Needed to set for legacy `ff02::2:1001`) {% sample lang="toml" %} ```toml multicast_address = "ff02::2:1001" @@ -387,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]] {% method %} The new json file format for the [meshviewer](https://github.com/ffrgb/meshviewer) developed in Regensburg. diff --git a/docs/docs_install.md b/docs/docs_install.md index 388eade..98f46cd 100644 --- a/docs/docs_install.md +++ b/docs/docs_install.md @@ -5,14 +5,13 @@ ### Install ```sh 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 rm go-release-linux-amd64.tar.gz ``` ### Configure go Add these lines in your root shell startup file (e.g. `/root/.bashrc`): - ```sh export GOPATH=/opt/go export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin @@ -34,14 +33,12 @@ systemctl daemon-reload ``` Before start, you should configure yanic by the file `/etc/yanic.conf`: - -``` +```sh systemctl start yanic ``` Enable to start on boot: - -``` +```sh systemctl enable yanic ``` diff --git a/docs/home_about.md b/docs/home_about.md index 86bcea1..91f2623 100644 --- a/docs/home_about.md +++ b/docs/home_about.md @@ -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 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`. diff --git a/main.go b/main.go index 6e63ad2..86e6718 100644 --- a/main.go +++ b/main.go @@ -9,9 +9,9 @@ import ( "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 { case log.PanicLevel: entry.Logger.Out = os.Stderr @@ -31,12 +31,12 @@ func (hook *Hook) Fire(entry *log.Entry) error { return nil } -func (hook *Hook) Levels() []stdLogger.Level { +func (h *hook) Levels() []stdLogger.Level { return log.AllLevels } func main() { - log.AddHook(&Hook{}) + log.AddHook(&hook{}) cmd.Execute() } diff --git a/output/all/main.go b/output/all/main.go index 07c9dae..aa104ea 100644 --- a/output/all/main.go +++ b/output/all/main.go @@ -1,6 +1,7 @@ package all import ( + _ "github.com/FreifunkBremen/yanic/output/geojson" _ "github.com/FreifunkBremen/yanic/output/meshviewer" _ "github.com/FreifunkBremen/yanic/output/meshviewer-ffrgb" _ "github.com/FreifunkBremen/yanic/output/nodelist" diff --git a/output/geojson/geojson.go b/output/geojson/geojson.go new file mode 100644 index 0000000..d237cfc --- /dev/null +++ b/output/geojson/geojson.go @@ -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 +} diff --git a/output/geojson/geojson_test.go b/output/geojson/geojson_test.go new file mode 100644 index 0000000..6c0f4d4 --- /dev/null +++ b/output/geojson/geojson_test.go @@ -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 +} diff --git a/output/geojson/output.go b/output/geojson/output.go new file mode 100644 index 0000000..d3c1068 --- /dev/null +++ b/output/geojson/output.go @@ -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) +} diff --git a/output/geojson/output_test.go b/output/geojson/output_test.go new file mode 100644 index 0000000..aa1f171 --- /dev/null +++ b/output/geojson/output_test.go @@ -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) +} diff --git a/output/meshviewer-ffrgb/meshviewer_test.go b/output/meshviewer-ffrgb/meshviewer_test.go index 956bc75..b73ddb6 100644 --- a/output/meshviewer-ffrgb/meshviewer_test.go +++ b/output/meshviewer-ffrgb/meshviewer_test.go @@ -69,7 +69,7 @@ func TestTransform(t *testing.T) { }, }, Neighbours: &data.Neighbours{ - NodeID: "node_b", + NodeID: "node_c", Batadv: map[string]data.BatadvNeighbours{ "node:c:mac:lan": { Neighbours: map[string]data.BatmanLink{ diff --git a/respond/collector.go b/respond/collector.go index 929d58a..39d1d45 100644 --- a/respond/collector.go +++ b/respond/collector.go @@ -66,7 +66,7 @@ func (coll *Collector) listenUDP(iface InterfaceConfig) { if iface.IPAddress != "" { addr = net.ParseIP(iface.IPAddress) } else { - addr, err = getUnicastAddr(iface.InterfaceName, iface.MulticastAddress == "") + addr, err = getUnicastAddr(iface.InterfaceName) if err != nil { 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) -func getUnicastAddr(ifname string, linklocal bool) (net.IP, error) { +func getUnicastAddr(ifname string) (net.IP, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err @@ -116,7 +116,7 @@ func getUnicastAddr(ifname string, linklocal bool) (net.IP, error) { if !ok { continue } - if (!linklocal && ipnet.IP.IsGlobalUnicast()) || (linklocal && ipnet.IP.IsLinkLocalUnicast()) { + if (ip == nil && ipnet.IP.IsGlobalUnicast()) || ipnet.IP.IsLinkLocalUnicast() { ip = ipnet.IP } } diff --git a/respond/respond.go b/respond/respond.go index 9564b8e..a8e3927 100644 --- a/respond/respond.go +++ b/respond/respond.go @@ -6,7 +6,7 @@ import ( const ( // 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 port = 1001 diff --git a/runtime/nodes_test.go b/runtime/nodes_test.go index 0212afc..a6c6c1d 100644 --- a/runtime/nodes_test.go +++ b/runtime/nodes_test.go @@ -55,8 +55,8 @@ func TestLoadAndSave(t *testing.T) { // not autoload without StatePath NewNodes(config) - // Test unmarshalable /dev/null - autolead with StatePath - config.StatePath = "/dev/null" + // Test unmarshalable + config.StatePath = "testdata/nodes-broken.json" nodes := NewNodes(config) // Test unopen able config.StatePath = "/root/nodes.json" @@ -71,8 +71,8 @@ func TestLoadAndSave(t *testing.T) { os.Remove(tmpfile.Name()) assert.Panics(func() { - SaveJSON(nodes, "/dev/null") - // "open /dev/null.tmp: permission denied", + SaveJSON(nodes, "/proc/a") + // "open /proc/a.tmp: permission denied", }) tmpfile, _ = ioutil.TempFile("/tmp", "nodes") @@ -162,6 +162,14 @@ func TestLinksNodes(t *testing.T) { } 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{ Nodeinfo: &data.Nodeinfo{ NodeID: "f4f26dd7a30a", @@ -169,11 +177,37 @@ func TestLinksNodes(t *testing.T) { 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{ Nodeinfo: &data.Nodeinfo{ 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{ 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) links := nodes.NodeLinks(node) assert.Len(links, 0) - node = nodes.List["f4f26dd7a30b"] + // babel link + node = nodes.List["f4f26dd7a30a"] assert.NotNil(node) links = nodes.NodeLinks(node) assert.Len(links, 1) link := links[0] - assert.Equal(link.SourceID, "f4f26dd7a30b") - assert.Equal(link.SourceAddress, "f4:f2:6d:d7:a3:0b") - assert.Equal(link.TargetID, "f4f26dd7a30a") - assert.Equal(link.TargetAddress, "f4:f2:6d:d7:a3:0a") - assert.Equal(link.TQ, float32(0.8)) + assert.Equal("f4f26dd7a30a", link.SourceID) + assert.Equal("fe80::2", link.SourceAddress) + assert.Equal("f4f26dd7a30b", link.TargetID) + assert.Equal("fe80::1337", link.TargetAddress) + 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") assert.Equal("f4f26dd7a30a", nodeid) diff --git a/runtime/testdata/nodes-broken.json b/runtime/testdata/nodes-broken.json new file mode 100644 index 0000000..2ff2996 --- /dev/null +++ b/runtime/testdata/nodes-broken.json @@ -0,0 +1,4 @@ +{ + // make it invalid by this comment + "affe": 3 +}