Compare commits

...

43 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
Martin/Geno
27fde7cd8c [TASK] improve logging 2019-01-19 20:58:36 +01:00
Martin/Geno
abae92bb5a
[TASK] update influxdb-client 2019-01-15 20:33:59 +01:00
Martin/Geno
f5d0067eff [BUGFIX] meshviewer-ffrgb export empty array 2019-01-15 19:06:31 +01:00
Martin/Geno
6579d52bac [BUGFIX] show NaN on link (fix #149) 2019-01-04 22:51:05 +01:00
Martin/Geno
eddd556ec1
[TASK] cleanup meshviewer-ffrgb, drop vpn field 2018-12-30 03:01:42 +01:00
Martin/Geno
4031d82047
[BUGFIX] fix logging database 2018-10-07 20:15:53 +02:00
Martin/Geno
7b4e3ce221 [TASK] meshviewer-ffrgb: drop site for domain 2018-10-07 19:34:38 +02:00
Martin/Geno
d14d3386c7
[TASK] add support for gitlab 2018-08-30 10:17:56 +02:00
skorpy
4551923d46 [BUGFIX] use available memory for usage estimation
Fix memory usage estimation following this commit:
https://github.com/freifunk-gluon/gluon/pull/1517
2018-08-30 09:48:03 +02:00
Ruben Barkow
6815f3b1db Readme: alfred-announce was renamed to mesh-announce 2018-08-07 22:34:02 +02:00
Martin/Geno
b8792a2691
[BUGFIX] send_no_request 2018-07-27 20:42:03 +02:00
Martin/Geno
cf8aeeeb78 Merge branch 'influxdb-selfsign' into 'master'
[TASK] allow self-signed certification in influxdb

See merge request FreifunkBremen/yanic!135
2018-07-12 16:52:30 +02:00
Martin/Geno
9c886d497e
[TASK] allow self-signed certification in influxdb 2018-07-12 16:48:11 +02:00
Martin/Geno
71c3e5ffe4
[DOC] fix sites 2018-07-10 19:18:25 +02:00
Martin/Geno
272a034234
add fastd public key to nodeinfo response 2018-07-10 17:49:27 +02:00
Martin/Geno
2697d4c228
[BUGFIX] airtime noise signed int (fix #143) 2018-07-09 00:00:36 +02:00
Julian Kornberger
9fb98cfb33 Trash Travis-CI
CircleCI does everything but better.
2018-06-22 00:56:35 +02:00
Martin/Geno
cb6d388138
[BUGFIX] influxdb could not handle unsign int - /proc/stats (fix #140) 2018-05-16 14:56:24 +02:00
Martin/Geno
c95ae7e12a
[TASK] add procstat 2018-05-16 11:19:57 +02:00
Julian Kornberger
a05943bf0d Refactor testfiles script 2018-05-15 21:56:36 +02:00
Julian Kornberger
bd398847ba Ignore vendor/ for CI 2018-05-15 21:56:32 +02:00
Julian Kornberger
f74e045273 Add Go Dep
https://github.com/golang/dep
2018-05-15 21:56:28 +02:00
Martin/Geno
7e97b691eb [TASK] improve autoselect ip_address (#137) 2018-05-08 19:32:03 +02:00
Julian Kornberger
10f58a72ea Add DHCP statistics 2018-05-05 23:37:57 +02:00
84 changed files with 1473 additions and 376 deletions

27
.circleci/check-coverage Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
# Issue: https://github.com/mattn/goveralls/issues/20
# Source: https://github.com/uber/go-torch/blob/63da5d33a225c195fea84610e2456d5f722f3963/.test-cover.sh
cd ${GOPATH}/src/github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}
echo "mode: count" > profile.cov
FAIL=0
# Standard go tooling behavior is to ignore dirs with leading underscors
for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d | grep -v '^./vendor/');
do
if ls $dir/*.go &> /dev/null; then
go test -v -covermode=count -coverprofile=profile.tmp $dir || FAIL=$?
if [ -f profile.tmp ]
then
tail -n +2 < profile.tmp >> profile.cov
rm profile.tmp
fi
fi
done
# Failures have incomplete results, so don't send
[ "$FAIL" -ne 0 ] && exit 1
goveralls -service=circle-ci -v -coverprofile=profile.cov
bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN -f profile.cov

View File

@ -1,6 +1,6 @@
#!/bin/bash
result="$(gofmt -s -l .)"
result="$(gofmt -s -l . | grep -v '^vendor/' )"
if [ -n "$result" ]; then
echo "Go code is not formatted, run 'gofmt -s -w .'" >&2
echo "$result"

25
.circleci/check-testfiles Executable file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python
# checks if every desired package has test files
import os
import re
import sys
source_re = re.compile(".*\.go")
test_re = re.compile(".*_test\.go")
missing = False
for root, dirs, files in os.walk("."):
# ignore some paths
if root == "." or root == "./database/graphite" or root.startswith("./vendor") or root.startswith("./."):
continue
# source files but not test files?
if len(filter(source_re.match, files)) > 0 and len(filter(test_re.match, files)) == 0:
print("no test files for {}".format(root))
missing = True
if missing:
sys.exit(1)
else:
print("every package has test files")

View File

@ -6,7 +6,8 @@ jobs:
working_directory: /go/src/github.com/FreifunkBremen/yanic
steps:
- checkout
- run: go get -t -d -v ./...
- run: curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
- run: dep ensure
- run: 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
- store_artifacts:
path: /go/bin/
@ -17,20 +18,24 @@ jobs:
working_directory: /go/src/github.com/FreifunkBremen/yanic
steps:
- checkout
- run: go get -t -d -v ./...
- run: curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
- run: dep ensure
- run: go get github.com/mattn/goveralls
- run: go get golang.org/x/tools/cmd/cover
- run: ./.test-coverage circle-ci
- run: ./.circleci/check-coverage
- store_test_results:
path: ./
destination: profile.cov
- run: ./.circleci/check-gofmt
- run: ./.circleci/check-testfiles
test_race:
docker:
- image: circleci/golang:latest
working_directory: /go/src/github.com/FreifunkBremen/yanic
steps:
- checkout
- run: go get -t -d -v ./...
- run: curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
- run: dep ensure
- run: go test -race ./...
workflows:
version: 2

2
.gitignore vendored
View File

@ -24,3 +24,5 @@ _testmain.go
*.prof
webroot
/config.toml
/vendor
/bin

36
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,36 @@
image: golang:latest
stages:
- build
- test
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
- cd /go/src/github.com/FreifunkBremen/yanic
- dep ensure
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:
- bin/yanic
- config_example.toml
- config-respondd_example.toml
test-my-project:
stage: test
script:
- ./.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
script:
- go test -race ./...

View File

@ -0,0 +1,7 @@
## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] I mentioned the community
- [ ] I mentioned the database type
- [ ] I added a link to the visualization of the collected data

View File

@ -1,51 +0,0 @@
#!/bin/bash
# Issue: https://github.com/mattn/goveralls/issues/20
# Source: https://github.com/uber/go-torch/blob/63da5d33a225c195fea84610e2456d5f722f3963/.test-cover.sh
CI=$1
echo "run for $CI"
if [ "$CI" == "circle-ci" ]; then
cd ${GOPATH}/src/github.com/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}
fi
echo "mode: count" > profile.cov
FAIL=0
# Standard go tooling behavior is to ignore dirs with leading underscors
for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d);
do
if ls $dir/*.go &> /dev/null; then
go test -v -covermode=count -coverprofile=profile.tmp $dir || FAIL=$?
if [ -f profile.tmp ]
then
tail -n +2 < profile.tmp >> profile.cov
rm profile.tmp
fi
fi
done
# Failures have incomplete results, so don't send
if [ "$FAIL" -eq 0 ]; then
goveralls -service=$CI -v -coverprofile=profile.cov
bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN -f profile.cov
fi
# Test if every package has testfiles
for dir in $(find . -name "*.go" -printf '%h\0'| sort -zu | sed -z 's/$/\n/');
do
if [ "$(ls $dir/*_test.go 2> /dev/null | wc -l)" -eq "0" ]; then
echo -n "no test files for $dir";
case $dir in
'.' | './database/graphite')
echo " - but ignored";
continue
;;
*)
echo "";
FAIL=1;
;;
esac
fi
done
exit $FAIL

View File

@ -1,14 +0,0 @@
language: go
go:
- 1.9.x
- 1.10.x
install:
- go get -t github.com/FreifunkBremen/yanic/...
- go get github.com/client9/misspell/cmd/misspell
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/cover
script:
- ./.test-coverage travis-ci
- ./.travis.gofmt.sh
- misspell -error .
- go install github.com/FreifunkBremen/yanic

167
Gopkg.lock generated Normal file
View File

@ -0,0 +1,167 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:af6e785bedb62fc2abb81977c58a7a44e5cf9f7e41b8d3c8dd4d872edea0ce08"
name = "github.com/NYTimes/gziphandler"
packages = ["."]
pruneopts = "UT"
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"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:6bea4bd16434bc6699590ed025152b690d08f552eeabfdc3258bd3aff738958c"
name = "github.com/fgrosse/graphigo"
packages = ["."]
pruneopts = "UT"
revision = "55a0a92a703041a55ad5ee2c2647f9577a87fdc1"
version = "v2"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
pruneopts = "UT"
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
branch = "master"
digest = "1:50708c8fc92aec981df5c446581cf9f90ba9e2a5692118e0ce75d4534aaa14a2"
name = "github.com/influxdata/influxdb1-client"
packages = [
"models",
"pkg/escape",
"v2",
]
pruneopts = "UT"
revision = "8ff2fc3824fcb533795f9a2f233275f0bb18d6c5"
[[projects]]
digest = "1:b56c589214f01a5601e0821387db484617392d0042f26234bf2da853a2f498a1"
name = "github.com/naoina/go-stringutil"
packages = ["."]
pruneopts = "UT"
revision = "6b638e95a32d0c1131db0e7fe83775cbea4a0d0b"
version = "v0.1.0"
[[projects]]
digest = "1:f58c3d0e46b64878d00652fedba24ee879725191ab919dca7b62586859281c04"
name = "github.com/naoina/toml"
packages = [
".",
"ast",
]
pruneopts = "UT"
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"
packages = ["."]
pruneopts = "UT"
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:22799aea8fe96dd5693abdd1eaa14b1b29e3eafbdc7733fa155b3cb556c8a7ae"
name = "github.com/spf13/cobra"
packages = ["."]
pruneopts = "UT"
revision = "67fc4837d267bc9bfd6e47f77783fcc3dffc68de"
version = "v0.0.4"
[[projects]]
digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = "UT"
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
version = "v1.0.3"
[[projects]]
digest = "1:972c2427413d41a1e06ca4897e8528e5a1622894050e2f527b38ddf0f343f759"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "UT"
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",
]
solver-name = "gps-cdcl"
solver-version = 1

54
Gopkg.toml Normal file
View File

@ -0,0 +1,54 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/NYTimes/gziphandler"
version = "1.0.1"
[[constraint]]
name = "github.com/fgrosse/graphigo"
version = "2.0.0"
[[constraint]]
name = "github.com/naoina/toml"
version = "0.1.1"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/spf13/cobra"
version = "0.0.2"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.1"
[prune]
go-tests = true
unused-packages = true

View File

@ -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
```

View File

@ -8,7 +8,6 @@ __ __ _
Yet another node info collector
```
[![Build Status](https://travis-ci.org/FreifunkBremen/yanic.svg?branch=master)](https://travis-ci.org/FreifunkBremen/yanic)
[![CircleCI](https://circleci.com/gh/FreifunkBremen/yanic/tree/master.svg?style=shield)](https://circleci.com/gh/FreifunkBremen/yanic/tree/master)
[![Coverage Status](https://coveralls.io/repos/github/FreifunkBremen/yanic/badge.svg?branch=master)](https://coveralls.io/github/FreifunkBremen/yanic?branch=master)
[![codecov](https://codecov.io/gh/FreifunkBremen/yanic/branch/master/graph/badge.svg)](https://codecov.io/gh/FreifunkBremen/yanic)
@ -20,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]
@ -50,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 <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
Imports global statistics from the given RRD files (ffmap-backend).
```
Usage:
yanic import <file.rrd> [flags]
yanic import <file.rrd> <site> <domain> [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 <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
* **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/
@ -109,15 +122,13 @@ 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
Respondd for servers:
* [ffnord-alfred-announce](https://github.com/ffnord/ffnord-alfred-announce) from FreiFunkNord
* [mesh-announce](https://github.com/ffnord/mesh-announce) from ffnord
* [respondd](https://github.com/Sunz3r/ext-respondd) from Sunz3r
## License
This software is licensed under the terms of the [AGPL v3 License](LICENSE).

View File

@ -5,11 +5,12 @@ import (
"io/ioutil"
"os"
"github.com/naoina/toml"
"github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/respond"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/yanic/webserver"
"github.com/naoina/toml"
)
// Config represents the whole configuration

View File

@ -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]

View File

@ -1,12 +1,12 @@
package cmd
import (
"log"
"github.com/bdlm/log"
"github.com/spf13/cobra"
allDatabase "github.com/FreifunkBremen/yanic/database/all"
"github.com/FreifunkBremen/yanic/rrd"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/spf13/cobra"
)
// importCmd represents the import command
@ -23,11 +23,11 @@ var importCmd = &cobra.Command{
err := allDatabase.Start(config.Database)
if err != nil {
panic(err)
log.Panicf("could not connect to database: %s", err)
}
defer allDatabase.Close()
log.Println("importing RRD from", path)
log.Infof("importing RRD from %s", path)
for ds := range rrd.Read(path) {
allDatabase.Conn.InsertGlobals(

View File

@ -2,14 +2,16 @@ package cmd
import (
"encoding/json"
"log"
"fmt"
"net"
"strings"
"time"
"github.com/bdlm/log"
"github.com/spf13/cobra"
"github.com/FreifunkBremen/yanic/respond"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/spf13/cobra"
)
var (
@ -28,7 +30,10 @@ var queryCmd = &cobra.Command{
ifaces := strings.Split(args[0], ",")
dstAddress := net.ParseIP(args[1])
log.Printf("Sending request address=%s ifaces=%s", dstAddress, ifaces)
log.WithFields(map[string]interface{}{
"address": dstAddress,
"ifaces": ifaces,
}).Info("sending request")
var ifacesConfigs []respond.InterfaceConfig
for _, iface := range ifaces {
@ -52,13 +57,13 @@ var queryCmd = &cobra.Command{
for id, data := range nodes.List {
jq, err := json.Marshal(data)
if err != nil {
log.Printf("%s: %+v", id, data)
fmt.Printf("%s: %+v", id, data)
} else {
jqNeighbours, err := json.Marshal(data.Neighbours)
if err != nil {
log.Printf("%s: %s neighbours: %+v", id, string(jq), data.Neighbours)
fmt.Printf("%s: %s neighbours: %+v", id, string(jq), data.Neighbours)
} else {
log.Printf("%s: %s neighbours: %s", id, string(jq), string(jqNeighbours))
fmt.Printf("%s: %s neighbours: %s", id, string(jq), string(jqNeighbours))
}
}
}

View File

@ -2,14 +2,16 @@ package cmd
import (
"fmt"
"log"
"os"
"github.com/bdlm/log"
"github.com/bdlm/std/logger"
"github.com/spf13/cobra"
)
var (
timestamps bool
loglevel uint32
)
// RootCmd represents the base command when called without any subcommands
@ -35,12 +37,12 @@ func init() {
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
RootCmd.PersistentFlags().BoolVar(&timestamps, "timestamps", false, "Enables timestamps for log output")
RootCmd.PersistentFlags().Uint32Var(&loglevel, "loglevel", 40, "Show log message starting at level")
}
func initConfig() {
if timestamps {
log.SetFlags(log.Lshortfile)
} else {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}
log.SetLevel(logger.Level(loglevel))
log.SetFormatter(&log.TextFormatter{
DisableTimestamp: timestamps,
})
}

View File

@ -1,18 +1,19 @@
package cmd
import (
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/bdlm/log"
"github.com/spf13/cobra"
allDatabase "github.com/FreifunkBremen/yanic/database/all"
allOutput "github.com/FreifunkBremen/yanic/output/all"
"github.com/FreifunkBremen/yanic/respond"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/FreifunkBremen/yanic/webserver"
"github.com/spf13/cobra"
)
// serveCmd represents the serve command
@ -25,7 +26,7 @@ var serveCmd = &cobra.Command{
err := allDatabase.Start(config.Database)
if err != nil {
panic(err)
log.Panicf("could not connect to database: %s", err)
}
defer allDatabase.Close()
@ -34,12 +35,12 @@ var serveCmd = &cobra.Command{
err = allOutput.Start(nodes, config.Nodes)
if err != nil {
panic(err)
log.Panicf("error on init outputs: %s", err)
}
defer allOutput.Close()
if config.Webserver.Enable {
log.Println("starting webserver on", config.Webserver.Bind)
log.Infof("starting webserver on %s", config.Webserver.Bind)
srv := webserver.New(config.Webserver.Bind, config.Webserver.Webroot)
go webserver.Start(srv)
defer srv.Close()
@ -50,7 +51,7 @@ var serveCmd = &cobra.Command{
if duration := config.Respondd.Synchronize.Duration; duration > 0 {
now := time.Now()
delay := duration - now.Sub(now.Truncate(duration))
log.Printf("delaying %0.1f seconds", delay.Seconds())
log.Infof("delaying %0.1f seconds", delay.Seconds())
time.Sleep(delay)
}
@ -63,7 +64,7 @@ var serveCmd = &cobra.Command{
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigs
log.Println("received", sig)
log.Infof("received %s", sig)
},
}

View File

@ -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 - preferred 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

View File

@ -1,7 +1,7 @@
package data
// NodeInfo struct
type NodeInfo struct {
// Nodeinfo struct
type Nodeinfo struct {
NodeID string `json:"node_id"`
Network Network `json:"network"`
Owner *Owner `json:"owner"`
@ -69,8 +69,9 @@ type Software struct {
Version string `json:"version,omitempty"`
} `json:"babeld,omitempty"`
Fastd struct {
Enabled bool `json:"enabled,omitempty"`
Version string `json:"version,omitempty"`
Enabled bool `json:"enabled,omitempty"`
Version string `json:"version,omitempty"`
PublicKey string `json:"public_key,omitempty"`
} `json:"fastd,omitempty"`
Firmware struct {
Base string `json:"base,omitempty"`

View File

@ -3,6 +3,6 @@ package data
// ResponseData struct
type ResponseData struct {
Neighbours *Neighbours `json:"neighbours"`
NodeInfo *NodeInfo `json:"nodeinfo"`
Nodeinfo *Nodeinfo `json:"nodeinfo"`
Statistics *Statistics `json:"statistics"`
}

View File

@ -9,6 +9,7 @@ package data
type Statistics struct {
NodeID string `json:"node_id"`
Clients Clients `json:"clients"`
DHCP *DHCP `json:"dhcp"`
RootFsUsage float64 `json:"rootfs_usage,omitempty"`
LoadAverage float64 `json:"loadavg,omitempty"`
Memory Memory `json:"memory,omitempty"`
@ -29,8 +30,9 @@ type Statistics struct {
MgmtTx *Traffic `json:"mgmt_tx"`
MgmtRx *Traffic `json:"mgmt_rx"`
} `json:"traffic,omitempty"`
Switch map[string]*SwitchPort `json:"switch,omitempty"`
Wireless WirelessStatistics `json:"wireless,omitempty"`
Switch map[string]*SwitchPort `json:"switch,omitempty"`
Wireless WirelessStatistics `json:"wireless,omitempty"`
ProcStats *ProcStats `json:"stat,omitempty"`
}
// MeshVPNPeerLink struct
@ -64,15 +66,52 @@ type Clients struct {
Total uint32 `json:"total"`
}
// DHCP struct
type DHCP struct {
// Packet counters
Decline uint32 `json:"dhcp_decline"`
Offer uint32 `json:"dhcp_offer"`
Ack uint32 `json:"dhcp_ack"`
Nak uint32 `json:"dhcp_nak"`
Request uint32 `json:"dhcp_request"`
Discover uint32 `json:"dhcp_discover"`
Inform uint32 `json:"dhcp_inform"`
Release uint32 `json:"dhcp_release"`
LeasesAllocated uint32 `json:"leases_allocated_4"`
LeasesPruned uint32 `json:"leases_pruned_4"`
}
// Memory struct
type Memory struct {
Cached int64 `json:"cached"`
Total int64 `json:"total"`
Buffers int64 `json:"buffers"`
Free int64 `json:"free"`
Cached int64 `json:"cached"`
Total int64 `json:"total"`
Buffers int64 `json:"buffers"`
Free int64 `json:"free,omitempty"`
Available int64 `json:"available,omitempty"`
}
// SwitchPort struct
type SwitchPort struct {
Speed uint32 `json:"speed"`
}
// ProcStats struct
type ProcStats struct {
CPU ProcStatsCPU `json:"cpu"`
Intr int64 `json:"intr"`
ContextSwitches int64 `json:"ctxt"`
SoftIRQ int64 `json:"softirq"`
Processes int64 `json:"processes"`
}
// ProcStatsCPU struct
type ProcStatsCPU struct {
User int64 `json:"user"`
Nice int64 `json:"nice"`
System int64 `json:"system"`
Idle int64 `json:"idle"`
IOWait int64 `json:"iowait"`
IRQ int64 `json:"irq"`
SoftIRQ int64 `json:"softirq"`
}

View File

@ -23,7 +23,7 @@ type WirelessAirtime struct {
BusyTime uint64 `json:"busy"`
RxTime uint64 `json:"rx"`
TxTime uint64 `json:"tx"`
Noise uint32 `json:"noise"`
Noise int32 `json:"noise"`
Frequency uint32 `json:"frequency"`
}

View File

@ -2,9 +2,10 @@ package all
import (
"fmt"
"log"
"time"
"github.com/bdlm/log"
"github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/runtime"
)
@ -19,7 +20,7 @@ func Connect(allConnection map[string]interface{}) (database.Connection, error)
for dbType, conn := range database.Adapters {
configForType := allConnection[dbType]
if configForType == nil {
log.Printf("the output type '%s' has no configuration", dbType)
log.WithField("database", dbType).Infof("no configuration found")
continue
}
dbConfigs, ok := configForType.([]interface{})

View File

@ -1,11 +1,12 @@
package graphite
import (
"log"
"sync"
"github.com/FreifunkBremen/yanic/database"
"github.com/bdlm/log"
"github.com/fgrosse/graphigo"
"github.com/FreifunkBremen/yanic/database"
)
const (
@ -69,7 +70,7 @@ func (c *Connection) addWorker() {
for point := range c.points {
err := c.client.SendAll(point)
if err != nil {
log.Fatal(err)
log.WithField("database", "graphite").Fatal(err)
return
}
}

View File

@ -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 {
@ -104,6 +117,7 @@ func (c *Connection) InsertNode(node *runtime.Node) {
addField("memory.cached", stats.Memory.Cached)
addField("memory.free", stats.Memory.Free)
addField("memory.total", stats.Memory.Total)
addField("memory.available", stats.Memory.Available)
c.addPoint(fields)
}

View File

@ -1,12 +1,12 @@
package influxdb
import (
"log"
"sync"
"time"
"github.com/influxdata/influxdb/client/v2"
"github.com/influxdata/influxdb/models"
"github.com/bdlm/log"
"github.com/influxdata/influxdb1-client/models"
"github.com/influxdata/influxdb1-client/v2"
"github.com/FreifunkBremen/yanic/database"
)
@ -14,6 +14,7 @@ import (
const (
MeasurementLink = "link" // Measurement for per-link statistics
MeasurementNode = "node" // Measurement for per-node statistics
MeasurementDHCP = "dhcp" // Measurement for DHCP server statistics
MeasurementGlobal = "global" // Measurement for summarized global statistics
CounterMeasurementFirmware = "firmware" // Measurement for firmware statistics
CounterMeasurementModel = "model" // Measurement for model statistics
@ -44,6 +45,12 @@ func (c Config) Username() string {
func (c Config) Password() string {
return c["password"].(string)
}
func (c Config) InsecureSkipVerify() bool {
if d, ok := c["insecure_skip_verify"]; ok {
return d.(bool)
}
return false
}
func (c Config) Tags() map[string]interface{} {
if c["tags"] != nil {
return c["tags"].(map[string]interface{})
@ -60,9 +67,10 @@ func Connect(configuration map[string]interface{}) (database.Connection, error)
// Make client
c, err := client.NewHTTPClient(client.HTTPConfig{
Addr: config.Address(),
Username: config.Username(),
Password: config.Password(),
Addr: config.Address(),
Username: config.Username(),
Password: config.Password(),
InsecureSkipVerify: config.InsecureSkipVerify(),
})
if err != nil {
@ -92,13 +100,16 @@ func (conn *Connection) addPoint(name string, tags models.Tags, fields models.Fi
if value, ok := valueInterface.(string); ok && tags.Get([]byte(tag)) == nil {
tags.SetString(tag, value)
} else {
log.Println(name, "could not saved configured value of tag", tag)
log.WithFields(map[string]interface{}{
"name": name,
"tag": tag,
}).Warnf("count not save tag configuration on point")
}
}
}
point, err := client.NewPoint(name, tags.Map(), fields, t...)
if err != nil {
panic(err)
log.Panicf("count not save points: %s", err)
}
conn.points <- point
}
@ -148,10 +159,10 @@ func (conn *Connection) addWorker() {
// write batch now?
if bp != nil && (writeNow || closed || len(bp.Points()) >= batchMaxSize) {
log.Println("saving", len(bp.Points()), "points")
log.WithField("count", len(bp.Points())).Info("saving points")
if err = conn.client.Write(bp); err != nil {
log.Print(err)
log.Error(err)
}
writeNow = false
bp = nil

View File

@ -6,8 +6,8 @@ import (
"testing"
"time"
"github.com/influxdata/influxdb/client/v2"
"github.com/influxdata/influxdb/models"
"github.com/influxdata/influxdb1-client/models"
"github.com/influxdata/influxdb1-client/v2"
"github.com/stretchr/testify/assert"
)
@ -16,9 +16,10 @@ func TestConnect(t *testing.T) {
assert := assert.New(t)
conn, err := Connect(map[string]interface{}{
"address": "",
"username": "",
"password": "",
"address": "",
"username": "",
"password": "",
"insecure_skip_verify": true,
})
assert.Nil(conn)
assert.Error(err)

View File

@ -4,7 +4,7 @@ import (
"time"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/influxdata/influxdb/models"
"github.com/influxdata/influxdb1-client/models"
)
// InsertGlobals implementation of database

View File

@ -5,7 +5,7 @@ import (
"testing"
"time"
"github.com/influxdata/influxdb/client/v2"
"github.com/influxdata/influxdb1-client/v2"
"github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data"
@ -124,7 +124,7 @@ func createTestNodes() *runtime.Nodes {
Total: 23,
},
},
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345",
Hardware: data.Hardware{
Model: "TP-Link 841",
@ -146,7 +146,7 @@ func createTestNodes() *runtime.Nodes {
Total: 2,
},
},
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566",
Hardware: data.Hardware{
Model: "TP-Link 841",
@ -156,7 +156,7 @@ func createTestNodes() *runtime.Nodes {
nodes.AddNode(&runtime.Node{
Online: true,
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x",
VPN: true,
Hardware: data.Hardware{

View File

@ -4,7 +4,7 @@ import (
"time"
"github.com/FreifunkBremen/yanic/runtime"
models "github.com/influxdata/influxdb/models"
models "github.com/influxdata/influxdb1-client/models"
)
// InsertLink adds a link data point

View File

@ -5,8 +5,8 @@ import (
"strconv"
"time"
client "github.com/influxdata/influxdb/client/v2"
models "github.com/influxdata/influxdb/models"
models "github.com/influxdata/influxdb1-client/models"
client "github.com/influxdata/influxdb1-client/v2"
"github.com/FreifunkBremen/yanic/runtime"
)
@ -33,21 +33,30 @@ func (conn *Connection) InsertNode(node *runtime.Node) {
tags.SetString("nodeid", stats.NodeID)
fields := models.Fields{
"load": stats.LoadAverage,
"time.up": int64(stats.Uptime),
"time.idle": int64(stats.Idletime),
"proc.running": stats.Processes.Running,
"clients.wifi": stats.Clients.Wifi,
"clients.wifi24": stats.Clients.Wifi24,
"clients.wifi5": stats.Clients.Wifi5,
"clients.total": stats.Clients.Total,
"memory.buffers": stats.Memory.Buffers,
"memory.cached": stats.Memory.Cached,
"memory.free": stats.Memory.Free,
"memory.total": stats.Memory.Total,
"load": stats.LoadAverage,
"time.up": int64(stats.Uptime),
"time.idle": int64(stats.Idletime),
"proc.running": stats.Processes.Running,
"clients.wifi": stats.Clients.Wifi,
"clients.wifi24": stats.Clients.Wifi24,
"clients.wifi5": stats.Clients.Wifi5,
"clients.total": stats.Clients.Total,
"memory.buffers": stats.Memory.Buffers,
"memory.cached": stats.Memory.Cached,
"memory.free": stats.Memory.Free,
"memory.total": stats.Memory.Total,
"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)
@ -74,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 {
@ -103,8 +114,24 @@ 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
fields["stat.cpu.nice"] = procstat.CPU.Nice
fields["stat.cpu.system"] = procstat.CPU.System
fields["stat.cpu.idle"] = procstat.CPU.Idle
fields["stat.cpu.iowait"] = procstat.CPU.IOWait
fields["stat.cpu.irq"] = procstat.CPU.IRQ
fields["stat.cpu.softirq"] = procstat.CPU.SoftIRQ
fields["stat.intr"] = procstat.Intr
fields["stat.ctxt"] = procstat.ContextSwitches
fields["stat.softirq"] = procstat.SoftIRQ
fields["stat.processes"] = procstat.Processes
}
if t := stats.Traffic.Rx; t != nil {
@ -141,5 +168,30 @@ func (conn *Connection) InsertNode(node *runtime.Node) {
conn.addPoint(MeasurementNode, tags, fields, time)
// Add DHCP statistics
if dhcp := stats.DHCP; dhcp != nil {
fields := models.Fields{
"decline": dhcp.Decline,
"offer": dhcp.Offer,
"ack": dhcp.Ack,
"nak": dhcp.Nak,
"request": dhcp.Request,
"discover": dhcp.Discover,
"inform": dhcp.Inform,
"release": dhcp.Release,
"leases.allocated": dhcp.LeasesAllocated,
"leases.pruned": dhcp.LeasesPruned,
}
// Tags
tags.SetString("nodeid", stats.NodeID)
if nodeinfo := node.Nodeinfo; nodeinfo != nil {
tags.SetString("hostname", nodeinfo.Hostname)
}
conn.addPoint(MeasurementDHCP, tags, fields, time)
}
return
}

View File

@ -3,7 +3,7 @@ package influxdb
import (
"testing"
"github.com/influxdata/influxdb/client/v2"
"github.com/influxdata/influxdb1-client/v2"
"github.com/stretchr/testify/assert"
"github.com/FreifunkBremen/yanic/data"
@ -17,6 +17,12 @@ func TestToInflux(t *testing.T) {
Statistics: &data.Statistics{
NodeID: "deadbeef",
LoadAverage: 0.5,
ProcStats: &data.ProcStats{
CPU: data.ProcStatsCPU{
User: 1,
},
ContextSwitches: 3,
},
Wireless: data.WirelessStatistics{
&data.WirelessAirtime{Frequency: 5500},
},
@ -33,20 +39,8 @@ 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{
Nodeinfo: &data.Nodeinfo{
NodeID: "deadbeef",
Owner: &data.Owner{
Contact: "nobody",
@ -61,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 {
@ -75,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,
@ -83,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{
"b-interface": {},
"b-interface-mac": {},
},
},
}
neighbour := &runtime.Node{
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "foobar",
Network: data.Network{
Mac: "BAFF1E5",
@ -111,7 +126,7 @@ func TestToInflux(t *testing.T) {
// do not add a empty statistics of a node
droppednode := &runtime.Node{
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "notfound",
Network: data.Network{
Mac: "instats",
@ -138,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"])
@ -159,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)

View File

@ -7,7 +7,6 @@ package logging
*/
import (
"fmt"
"log"
"os"
"time"
@ -64,6 +63,6 @@ func (conn *Connection) Close() {
}
func (conn *Connection) log(v ...interface{}) {
log.Println(v)
fmt.Println(v...)
conn.file.WriteString(fmt.Sprintln("[", time.Now().String(), "]", v))
}

View File

@ -7,10 +7,11 @@ import (
"bufio"
"compress/flate"
"encoding/json"
"log"
"net"
"time"
"github.com/bdlm/log"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/runtime"
@ -50,7 +51,7 @@ func Connect(configuration map[string]interface{}) (database.Connection, error)
func (conn *Connection) InsertNode(node *runtime.Node) {
res := &data.ResponseData{
NodeInfo: node.Nodeinfo,
Nodeinfo: node.Nodeinfo,
Statistics: node.Statistics,
Neighbours: node.Neighbours,
}
@ -59,7 +60,7 @@ func (conn *Connection) InsertNode(node *runtime.Node) {
flater, err := flate.NewWriter(writer, flate.BestCompression)
if err != nil {
log.Printf("[database-yanic] could not create flater: %s", err)
log.Errorf("[database-yanic] could not create flater: %s", err)
return
}
defer flater.Close()
@ -69,16 +70,16 @@ func (conn *Connection) InsertNode(node *runtime.Node) {
if node.Nodeinfo != nil && node.Nodeinfo.NodeID != "" {
nodeid = node.Nodeinfo.NodeID
}
log.Printf("[database-yanic] could not encode %s node: %s", nodeid, err)
log.WithField("node_id", nodeid).Errorf("[database-yanic] could not encode node: %s", err)
return
}
err = flater.Flush()
if err != nil {
log.Printf("[database-yanic] could not compress: %s", err)
log.Errorf("[database-yanic] could not compress: %s", err)
}
err = writer.Flush()
if err != nil {
log.Printf("[database-yanic] could not send: %s", err)
log.Errorf("[database-yanic] could not send: %s", err)
}
}

View File

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

View File

@ -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
@ -61,16 +61,6 @@ collect_interval = "1m"
{% endmethod %}
### sites
{% method %}
List of sites to save stats for (empty for global only)
{% sample lang="toml" %}
```toml
sites = ["ffhb"]
```
{% endmethod %}
### [respondd.sites.example]
{% method %}
Tables of sites to save stats for (not exists for global only).
@ -120,6 +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.
(It prefers the link local address, so at babel mesh-network it should be configurated)
{% sample lang="toml" %}
```toml
ip_address = "fe80::..."
@ -139,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"
@ -396,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.
@ -504,6 +521,21 @@ path = "/var/www/html/meshviewer/data/nodelist.json"
```
{% 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]
@ -578,6 +610,7 @@ address = "http://localhost:8086"
database = "ffhb"
username = ""
password = ""
insecure_skip_verify = false
[database.connection.influxdb.tags]
tagname1 = "tagvalue 1"
system = "productive"
@ -625,6 +658,15 @@ password = ""
```
{% endmethod %}
### insecure_skip_verify
{% method %}
Skip insecure verify for self-signed certificates.
{% sample lang="toml" %}
```toml
insecure_skip_verify = true
```
{% endmethod %}
### [database.connection.influxdb.tags]
{% method %}

View File

@ -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
```

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 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`.

37
main.go
View File

@ -1,7 +1,42 @@
package main
import "github.com/FreifunkBremen/yanic/cmd"
import (
"os"
"github.com/bdlm/log"
stdLogger "github.com/bdlm/std/logger"
"github.com/FreifunkBremen/yanic/cmd"
)
type hook struct{}
func (h *hook) Fire(entry *log.Entry) error {
switch entry.Level {
case log.PanicLevel:
entry.Logger.Out = os.Stderr
case log.FatalLevel:
entry.Logger.Out = os.Stderr
case log.ErrorLevel:
entry.Logger.Out = os.Stderr
case log.WarnLevel:
entry.Logger.Out = os.Stdout
case log.InfoLevel:
entry.Logger.Out = os.Stdout
case log.DebugLevel:
entry.Logger.Out = os.Stdout
default:
}
return nil
}
func (h *hook) Levels() []stdLogger.Level {
return log.AllLevels
}
func main() {
log.AddHook(&hook{})
cmd.Execute()
}

View File

@ -1,7 +1,9 @@
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"
_ "github.com/FreifunkBremen/yanic/output/raw"
)

View File

@ -2,7 +2,8 @@ package all
import (
"fmt"
"log"
"github.com/bdlm/log"
"github.com/FreifunkBremen/yanic/output"
"github.com/FreifunkBremen/yanic/output/filter"
@ -23,7 +24,7 @@ func Register(configuration map[string]interface{}) (output.Output, error) {
for outputType, outputRegister := range output.Adapters {
configForOutput := allOutputs[outputType]
if configForOutput == nil {
log.Printf("the output type '%s' has no configuration\n", outputType)
log.WithField("output", outputType).Infof("no configuration found")
continue
}
outputConfigs, ok := configForOutput.([]interface{})

View File

@ -23,7 +23,7 @@ func TestFilterBlacklist(t *testing.T) {
assert.NoError(err)
// keep node without nodeid
n := filter.Apply(&runtime.Node{Nodeinfo: &data.NodeInfo{}})
n := filter.Apply(&runtime.Node{Nodeinfo: &data.Nodeinfo{}})
assert.NotNil(n)
// tests with blacklist
@ -31,11 +31,11 @@ func TestFilterBlacklist(t *testing.T) {
assert.NoError(err)
// 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)
// 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)
n = filter.Apply(&runtime.Node{})

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,10 @@ package filter
import (
"fmt"
"github.com/FreifunkBremen/yanic/runtime"
"github.com/bdlm/log"
"github.com/pkg/errors"
"github.com/FreifunkBremen/yanic/runtime"
)
// factory function for building a filter
@ -24,7 +26,7 @@ var filters = make(map[string]factory)
// Register registers a new filter
func Register(name string, f factory) {
if _, ok := filters[name]; ok {
panic("already registered: " + name)
log.WithField("filter", name).Panic("filter already registered")
}
filters[name] = f
}

View File

@ -68,7 +68,7 @@ func TestFilter(t *testing.T) {
nodes := &runtime.Nodes{
List: map[string]*runtime.Node{
"a": {
Nodeinfo: &data.NodeInfo{NodeID: "a"},
Nodeinfo: &data.Nodeinfo{NodeID: "a"},
},
},
}
@ -83,7 +83,7 @@ func TestFilter(t *testing.T) {
nodes = &runtime.Nodes{
List: map[string]*runtime.Node{
"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)
// 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{},
}})
assert.NotNil(n)
// 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)
// node without nodeinfo has no location -> drop it
@ -38,13 +38,13 @@ func TestFilterHasLocation(t *testing.T) {
assert.NoError(err)
// 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{},
}})
assert.Nil(n)
// 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)
// node without nodeinfo has no location -> keep it

View File

@ -18,7 +18,7 @@ func TestFilterInArea(t *testing.T) {
"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},
}})
assert.NotNil(n)
@ -28,35 +28,35 @@ func TestFilterInArea(t *testing.T) {
assert.NotNil(n)
// 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)
// 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{},
}})
assert.Nil(n)
// 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},
}})
assert.NotNil(n)
// 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},
}})
assert.Nil(n)
// 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},
}})
assert.Nil(n)
// 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},
}})
assert.Nil(n)

View File

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

View File

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

View File

@ -22,11 +22,11 @@ func TestFilterSite(t *testing.T) {
assert.NoError(err)
// 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)
// 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)
// 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

@ -2,9 +2,10 @@ package meshviewerFFRGB
import (
"fmt"
"log"
"strings"
"github.com/bdlm/log"
"github.com/FreifunkBremen/yanic/lib/jsontime"
"github.com/FreifunkBremen/yanic/runtime"
)
@ -81,7 +82,13 @@ func transform(nodes *runtime.Nodes) *Meshviewer {
if link.Type == LINK_TYPE_FALLBACK {
link.Type = linkType
} else {
log.Printf("different linktypes for '%s' - '%s' prev: '%s' new: '%s' source: '%s' target: '%s'", linkOrigin.SourceAddress, linkOrigin.TargetAddress, link.Type, linkType, typeList[linkOrigin.SourceAddress], typeList[linkOrigin.TargetAddress])
log.WithFields(map[string]interface{}{
"link": fmt.Sprintf("%s-%s", linkOrigin.SourceAddress, linkOrigin.TargetAddress),
"prev": link.Type,
"new": linkType,
"source": typeList[linkOrigin.SourceAddress],
"target": typeList[linkOrigin.TargetAddress],
}).Warn("different linktypes")
}
}
@ -93,7 +100,7 @@ func transform(nodes *runtime.Nodes) *Meshviewer {
Target: linkOrigin.TargetID,
TargetAddress: linkOrigin.TargetAddress,
SourceTQ: linkOrigin.TQ,
TargetTQ: linkOrigin.TQ,
TargetTQ: 0,
}
linkType, linkTypeFound := typeList[linkOrigin.SourceAddress]
@ -102,8 +109,10 @@ func transform(nodes *runtime.Nodes) *Meshviewer {
}
if switchSourceTarget {
link.SourceTQ = 0
link.Source = linkOrigin.TargetID
link.SourceAddress = linkOrigin.TargetAddress
link.TargetTQ = linkOrigin.TQ
link.Target = linkOrigin.SourceID
link.TargetAddress = linkOrigin.SourceAddress

View File

@ -14,7 +14,7 @@ func TestTransform(t *testing.T) {
nodes := runtime.NewNodes(&runtime.NodesConfig{})
nodes.AddNode(&runtime.Node{
Online: true,
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "node_a",
Network: data.Network{
Mac: "node:a:mac",
@ -51,7 +51,7 @@ func TestTransform(t *testing.T) {
})
nodes.AddNode(&runtime.Node{
Online: true,
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "node_c",
Network: data.Network{
Mac: "node:c:mac",
@ -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{
@ -81,7 +81,7 @@ func TestTransform(t *testing.T) {
})
nodes.AddNode(&runtime.Node{
Online: true,
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "node_b",
Network: data.Network{
Mac: "node:b:mac",
@ -117,7 +117,7 @@ func TestTransform(t *testing.T) {
})
nodes.AddNode(&runtime.Node{
Online: false,
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "node_d",
Network: data.Network{
Mac: "node:d:mac",
@ -164,7 +164,7 @@ func TestTransform(t *testing.T) {
assert.Equal("other", link.Type)
assert.Equal("node:b:mac:lan", link.TargetAddress)
assert.Equal(float32(0.2), link.SourceTQ)
assert.Equal(float32(0.2), link.TargetTQ)
assert.Equal(float32(0), link.TargetTQ)
break
case "node:a:mac:wifi":

View File

@ -32,8 +32,8 @@ type Node struct {
NodeID string `json:"node_id"`
MAC string `json:"mac"`
Addresses []string `json:"addresses"`
SiteCode string `json:"site_code,omitempty"`
DomainCode string `json:"-"`
SiteCode string `json:"-"`
DomainCode string `json:"domain"`
Hostname string `json:"hostname"`
Owner string `json:"owner,omitempty"`
Location *Location `json:"location,omitempty"`
@ -41,7 +41,6 @@ type Node struct {
Autoupdater Autoupdater `json:"autoupdater"`
Nproc int `json:"nproc"`
Model string `json:"model,omitempty"`
VPN bool `json:"vpn"`
}
// Firmware out of software
@ -79,15 +78,18 @@ func NewNode(nodes *runtime.Nodes, n *runtime.Node) *Node {
Lastseen: n.Lastseen,
IsOnline: n.Online,
IsGateway: n.IsGateway(),
Addresses: []string{},
}
if nodeinfo := n.Nodeinfo; nodeinfo != nil {
node.NodeID = nodeinfo.NodeID
node.MAC = nodeinfo.Network.Mac
node.Addresses = nodeinfo.Network.Addresses
node.SiteCode = nodeinfo.System.SiteCode
node.DomainCode = nodeinfo.System.DomainCode
node.Hostname = nodeinfo.Hostname
if addresses := nodeinfo.Network.Addresses; addresses != nil {
node.Addresses = nodeinfo.Network.Addresses
}
if owner := nodeinfo.Owner; owner != nil {
node.Owner = owner.Contact
}
@ -104,7 +106,6 @@ func NewNode(nodes *runtime.Nodes, n *runtime.Node) *Node {
}
node.Nproc = nodeinfo.Hardware.Nproc
node.Model = nodeinfo.Hardware.Model
node.VPN = nodeinfo.VPN
}
if statistic := n.Statistics; statistic != nil {
if n.Online {
@ -126,10 +127,16 @@ func NewNode(nodes *runtime.Nodes, n *runtime.Node) *Node {
/* The Meshviewer could not handle absolute memory output
* calc the used memory as a float which 100% equal 1.0
* calc is coppied from node statuspage (look discussion:
* https://github.com/FreifunkBremen/yanic/issues/35)
* https://github.com/FreifunkBremen/yanic/issues/35 and
* https://github.com/freifunk-gluon/gluon/pull/1517)
*/
if statistic.Memory.Total > 0 {
usage := 1 - (float64(statistic.Memory.Free)+float64(statistic.Memory.Buffers)+float64(statistic.Memory.Cached))/float64(statistic.Memory.Total)
usage := 0.0
if statistic.Memory.Available > 0 {
usage = 1 - float64(statistic.Memory.Available)/float64(statistic.Memory.Total)
} else {
usage = 1 - (float64(statistic.Memory.Free)+float64(statistic.Memory.Buffers)+float64(statistic.Memory.Cached))/float64(statistic.Memory.Total)
}
node.MemoryUsage = &usage
}
@ -148,5 +155,10 @@ func NewNode(nodes *runtime.Nodes, n *runtime.Node) *Node {
}
}
// fix site to domain - if empty
if node.DomainCode == "" {
node.DomainCode = node.SiteCode
}
return node
}

View File

@ -12,7 +12,21 @@ func TestRegister(t *testing.T) {
assert := assert.New(t)
nodes := runtime.NewNodes(&runtime.NodesConfig{})
node := NewNode(nodes, &runtime.Node{
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
Owner: &data.Owner{
Contact: "whoami",
},
Network: data.Network{
Mac: "blub",
Addresses: []string{"fe80::1"},
},
},
})
assert.NotNil(node)
assert.Len(node.Addresses, 1)
node = NewNode(nodes, &runtime.Node{
Nodeinfo: &data.Nodeinfo{
Owner: &data.Owner{
Contact: "whoami",
},
@ -46,6 +60,7 @@ func TestRegister(t *testing.T) {
},
})
assert.NotNil(node)
assert.NotNil(node.Addresses)
assert.Equal("whoami", node.Owner)
assert.Equal("blub", node.MAC)
assert.Equal(13.3, node.Location.Longitude)

View File

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

View File

@ -11,7 +11,7 @@ type Node struct {
Lastseen jsontime.Time `json:"lastseen"`
Flags Flags `json:"flags"`
Statistics *Statistics `json:"statistics"`
Nodeinfo *data.NodeInfo `json:"nodeinfo"`
Nodeinfo *data.Nodeinfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"-"`
}
@ -71,10 +71,16 @@ func NewStatistics(stats *data.Statistics, isOnline bool) *Statistics {
/* The Meshviewer could not handle absolute memory output
* calc the used memory as a float which 100% equal 1.0
* calc is coppied from node statuspage (look discussion:
* https://github.com/FreifunkBremen/yanic/issues/35)
* https://github.com/FreifunkBremen/yanic/issues/35 and
* https://github.com/freifunk-gluon/gluon/pull/1517)
*/
if stats.Memory.Total > 0 {
usage := 1 - (float64(stats.Memory.Free)+float64(stats.Memory.Buffers)+float64(stats.Memory.Cached))/float64(stats.Memory.Total)
usage := 0.0
if stats.Memory.Available > 0 {
usage = 1 - float64(stats.Memory.Available)/float64(stats.Memory.Total)
} else {
usage = 1 - (float64(stats.Memory.Free)+float64(stats.Memory.Buffers)+float64(stats.Memory.Cached))/float64(stats.Memory.Total)
}
output.MemoryUsage = &usage
}

View File

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

View File

@ -2,7 +2,8 @@ package meshviewer
import (
"fmt"
"log"
"github.com/bdlm/log"
"github.com/FreifunkBremen/yanic/output"
"github.com/FreifunkBremen/yanic/runtime"

View File

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

@ -5,10 +5,11 @@ import (
"compress/flate"
"encoding/json"
"fmt"
"log"
"net"
"time"
"github.com/bdlm/log"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/database"
"github.com/FreifunkBremen/yanic/lib/jsontime"
@ -67,7 +68,7 @@ func (coll *Collector) listenUDP(iface InterfaceConfig) {
} else {
addr, err = getUnicastAddr(iface.InterfaceName)
if err != nil {
log.Panic(err)
log.WithField("iface", iface.InterfaceName).Panic(err)
}
}
@ -97,7 +98,7 @@ func (coll *Collector) listenUDP(iface InterfaceConfig) {
go coll.receiver(conn)
}
// Returns a unicast address of given interface (prefer global unicast address over link local address)
// Returns a unicast address of given interface (linklocal or global unicast address)
func getUnicastAddr(ifname string) (net.IP, error) {
iface, err := net.InterfaceByName(ifname)
if err != nil {
@ -115,25 +116,23 @@ func getUnicastAddr(ifname string) (net.IP, error) {
if !ok {
continue
}
if ipnet.IP.IsGlobalUnicast() {
ip = ipnet.IP
} else if ipnet.IP.IsLinkLocalUnicast() && ip == nil {
if (ip == nil && ipnet.IP.IsGlobalUnicast()) || ipnet.IP.IsLinkLocalUnicast() {
ip = ipnet.IP
}
}
if ip != nil {
return ip, nil
}
return nil, fmt.Errorf("unable to find a unicast address for %s", ifname)
return nil, fmt.Errorf("unable to find a unicast address")
}
// Start Collector
func (coll *Collector) Start(interval time.Duration) {
if coll.interval != 0 {
panic("already started")
log.Panic("already started")
}
if interval <= 0 {
panic("invalid collector interval")
log.Panic("invalid collector interval")
}
coll.interval = interval
@ -162,7 +161,7 @@ func (coll *Collector) sendOnce() {
}
func (coll *Collector) sendMulticast() {
log.Println("sending multicasts")
log.Info("sending multicasts")
for _, conn := range coll.connections {
if conn.SendRequest {
coll.sendPacket(conn.Conn, conn.MulticastAddress)
@ -184,20 +183,23 @@ func (coll *Collector) sendUnicasts(seenBefore jsontime.Time) {
for _, node := range nodes {
send := 0
for _, conn := range coll.connections {
if node.Address.Zone != "" && conn.Conn.LocalAddr().(*net.UDPAddr).Zone != node.Address.Zone {
if node.Address.Zone != "" && conn.Conn.LocalAddr().(*net.UDPAddr).Zone != node.Address.Zone && conn.SendRequest {
continue
}
coll.sendPacket(conn.Conn, node.Address.IP)
send++
}
if send == 0 {
log.Printf("unable to find connection for %s", node.Address.Zone)
log.WithField("iface", node.Address.Zone).Error("unable to find connection")
} else {
time.Sleep(10 * time.Millisecond)
count += send
}
}
log.Printf("sending %d unicast pkg for %d nodes", count, len(nodes))
log.WithFields(map[string]interface{}{
"pkg_count": count,
"nodes_count": len(nodes),
}).Info("sending unicast pkg")
}
// SendPacket sends a UDP request to the given unicast or multicast address on the first UDP socket
@ -214,7 +216,7 @@ func (coll *Collector) sendPacket(conn *net.UDPConn, destination net.IP) {
}
if _, err := conn.WriteToUDP([]byte("GET nodeinfo statistics neighbours"), &addr); err != nil {
log.Println("WriteToUDP failed:", err)
log.WithField("address", addr.String()).Errorf("WriteToUDP failed: %s", err)
}
}
@ -236,7 +238,7 @@ func (coll *Collector) sender() {
func (coll *Collector) parser() {
for obj := range coll.queue {
if data, err := obj.parse(); err != nil {
log.Println("unable to decode response from", obj.Address.String(), err)
log.WithField("address", obj.Address.String()).Errorf("unable to decode response %s", err)
} else {
coll.saveResponse(obj.Address, data)
}
@ -258,7 +260,7 @@ func (res *Response) parse() (*data.ResponseData, error) {
func (coll *Collector) saveResponse(addr *net.UDPAddr, res *data.ResponseData) {
// Search for NodeID
var nodeID string
if val := res.NodeInfo; val != nil {
if val := res.Nodeinfo; val != nil {
nodeID = val.NodeID
} else if val := res.Neighbours; val != nil {
nodeID = val.NodeID
@ -268,7 +270,10 @@ func (coll *Collector) saveResponse(addr *net.UDPAddr, res *data.ResponseData) {
// Check length of nodeID
if len(nodeID) != 12 {
log.Printf("invalid NodeID '%s' from %s", nodeID, addr.String())
log.WithFields(map[string]interface{}{
"node_id": nodeID,
"address": addr.String(),
}).Warn("invalid NodeID")
return
}
@ -279,8 +284,8 @@ func (coll *Collector) saveResponse(addr *net.UDPAddr, res *data.ResponseData) {
if res.Neighbours != nil && res.Neighbours.NodeID != nodeID {
res.Neighbours = nil
}
if res.NodeInfo != nil && res.NodeInfo.NodeID != nodeID {
res.NodeInfo = nil
if res.Nodeinfo != nil && res.Nodeinfo.NodeID != nodeID {
res.Nodeinfo = nil
}
// Process the data and update IP address
@ -308,7 +313,14 @@ func (coll *Collector) receiver(conn *net.UDPConn) {
n, src, err := conn.ReadFromUDP(buf)
if err != nil {
log.Println("ReadFromUDP failed:", err)
if conn != nil {
log.WithFields(map[string]interface{}{
"local": conn.LocalAddr(),
"remote": conn.RemoteAddr(),
}).Errorf("ReadFromUDP failed: %s", err)
} else {
log.Errorf("ReadFromUDP failed: %s", err)
}
return
}

View File

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

View File

@ -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

View File

@ -9,6 +9,8 @@ import (
"strconv"
"strings"
"time"
"github.com/bdlm/log"
)
var linePattern = regexp.MustCompile("^<!-- ....-..-.. ..:..:.. [A-Z]+ / (\\d+) --> <row><v>([^<]+)</v><v>([^<]+)</v></row>")
@ -27,10 +29,10 @@ func Read(rrdFile string) chan Dataset {
stdout, err := cmd.StdoutPipe()
if err != nil {
panic(err)
log.Panicf("error on get stdout: %s", err)
}
if err := cmd.Start(); err != nil {
panic(err)
log.Panicf("error on start rrdtool: %s", err)
}
r := bufio.NewReader(stdout)

View File

@ -14,7 +14,7 @@ type Node struct {
Lastseen jsontime.Time `json:"lastseen"`
Online bool `json:"online"`
Statistics *data.Statistics `json:"statistics"`
Nodeinfo *data.NodeInfo `json:"nodeinfo"`
Nodeinfo *data.Nodeinfo `json:"nodeinfo"`
Neighbours *data.Neighbours `json:"-"`
}

View File

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

View File

@ -2,11 +2,12 @@ package runtime
import (
"encoding/json"
"log"
"os"
"sync"
"time"
"github.com/bdlm/log"
"github.com/FreifunkBremen/yanic/data"
"github.com/FreifunkBremen/yanic/lib/jsontime"
)
@ -63,8 +64,8 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
}
nodes.List[nodeID] = node
}
if res.NodeInfo != nil {
nodes.readIfaces(res.NodeInfo)
if res.Nodeinfo != nil {
nodes.readIfaces(res.Nodeinfo)
}
nodes.Unlock()
@ -80,7 +81,7 @@ func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
node.Lastseen = now
node.Online = true
node.Neighbours = res.Neighbours
node.Nodeinfo = res.NodeInfo
node.Nodeinfo = res.Nodeinfo
node.Statistics = res.Statistics
return node
@ -181,12 +182,12 @@ func (nodes *Nodes) expire() {
}
// 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
network := nodeinfo.Network
if nodeID == "" {
log.Println("nodeID missing in nodeinfo")
log.Warn("nodeID missing in nodeinfo")
return
}
@ -202,7 +203,7 @@ func (nodes *Nodes) readIfaces(nodeinfo *data.NodeInfo) {
}
if oldNodeID, _ := nodes.ifaceToNodeID[addr]; oldNodeID != nodeID {
if oldNodeID != "" {
log.Printf("override nodeID from %s to %s on MAC address %s", oldNodeID, nodeID, addr)
log.Warnf("override nodeID from %s to %s on MAC address %s", oldNodeID, nodeID, addr)
}
nodes.ifaceToNodeID[addr] = nodeID
}
@ -214,7 +215,7 @@ func (nodes *Nodes) load() {
if f, err := os.Open(path); err == nil { // transform data to legacy meshviewer
if err = json.NewDecoder(f).Decode(nodes); err == nil {
log.Println("loaded", len(nodes.List), "nodes")
log.Infof("loaded %d nodes", len(nodes.List))
nodes.Lock()
for _, node := range nodes.List {
@ -225,10 +226,10 @@ func (nodes *Nodes) load() {
nodes.Unlock()
} else {
log.Println("failed to unmarshal nodes:", err)
log.Errorf("failed to unmarshal nodes: %s", err)
}
} else {
log.Println("failed to load cached nodes:", err)
log.Errorf("failed to load cached nodes: %s", err)
}
}

View File

@ -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"
@ -70,13 +70,15 @@ func TestLoadAndSave(t *testing.T) {
nodes.save()
os.Remove(tmpfile.Name())
assert.PanicsWithValue("open /dev/null.tmp: permission denied", func() {
SaveJSON(nodes, "/dev/null")
assert.Panics(func() {
SaveJSON(nodes, "/proc/a")
// "open /proc/a.tmp: permission denied",
})
tmpfile, _ = ioutil.TempFile("/tmp", "nodes")
assert.PanicsWithValue("json: unsupported type: func() string", func() {
assert.Panics(func() {
SaveJSON(tmpfile.Name, tmpfile.Name())
// "json: unsupported type: func() string",
})
os.Remove(tmpfile.Name())
@ -100,7 +102,7 @@ func TestUpdateNodes(t *testing.T) {
&data.WirelessAirtime{},
},
},
NodeInfo: &data.NodeInfo{},
Nodeinfo: &data.Nodeinfo{},
}
nodes.Update("abcdef012345", res)
@ -144,10 +146,10 @@ func TestAddNode(t *testing.T) {
nodes.AddNode(&Node{})
assert.Len(nodes.List, 0)
nodes.AddNode(&Node{Nodeinfo: &data.NodeInfo{}})
nodes.AddNode(&Node{Nodeinfo: &data.Nodeinfo{}})
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)
}
@ -160,18 +162,52 @@ 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{
Nodeinfo: &data.Nodeinfo{
NodeID: "f4f26dd7a30a",
Network: data.Network{
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{
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",
@ -187,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)

View File

@ -84,7 +84,7 @@ func createTestNodes() *Nodes {
Total: 23,
},
},
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "abcdef012345",
Hardware: data.Hardware{
Model: "TP-Link 841",
@ -104,7 +104,7 @@ func createTestNodes() *Nodes {
Total: 2,
},
},
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "112233445566",
Hardware: data.Hardware{
Model: "TP-Link 841",
@ -123,7 +123,7 @@ func createTestNodes() *Nodes {
nodes.AddNode(&Node{
Online: true,
Nodeinfo: &data.NodeInfo{
Nodeinfo: &data.Nodeinfo{
NodeID: "0xdeadbeef0x",
VPN: true,
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
}

View File

@ -4,6 +4,7 @@ import (
"net/http"
"github.com/NYTimes/gziphandler"
"github.com/bdlm/log"
)
// New creates a new webserver and starts it
@ -17,6 +18,6 @@ func New(bindAddr, webroot string) *http.Server {
func Start(srv *http.Server) {
// service connections
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
panic(err)
log.Panicf("webserver crashed: %s", err)
}
}