Compare commits

..

99 Commits

Author SHA1 Message Date
Stefan Hoffmann
631a5e398f Merge branch 'master' of ssh://git.freifunk-rhein-sieg.net:2222/Freifunk-Troisdorf/ubnt-freifunk-map-api
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-09-25 18:34:32 +02:00
Stefan Hoffmann
e6583918c1 Fixing ghost devices in Unifi 2024-09-25 18:33:41 +02:00
6f4fc76812 Delete .drone.jsonnet
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-09-25 16:00:47 +00:00
Stefan Hoffmann
ff5cf755aa Bugfixing UISP 503 Errors.
Finetuning API Calls
2024-09-25 17:53:38 +02:00
f9fa5fe26a
Naming changes 2024-03-19 18:43:09 +01:00
73166fcedc
send influx datapoints only when enabled
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-03-18 19:34:06 +01:00
7549eaa5d0
bugfix
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2024-03-18 18:57:55 +01:00
b8087ff4d9
Changed go build
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-03-18 18:53:05 +01:00
bcc4195234
Add woodpecker CI
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2024-03-18 18:27:43 +01:00
5ba7f23776
Changed UISP API for Statistics 2024-03-18 18:13:22 +01:00
dc1bc7f135
Removed unneeded line
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-14 17:40:31 +02:00
c8c7c9e938
Error Handling
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-14 11:44:20 +02:00
21157e5fb4
Readme angepasst 2023-05-14 11:44:11 +02:00
c9b496d5eb
Fixed CPU on Map
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-05-12 19:07:16 +02:00
0411e59eed
Fixes Memory for the Map
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-05-12 18:59:38 +02:00
ae6d96a0ff
Get CPU in % from InfluxDB 2023-05-12 18:00:25 +02:00
064161584d Merge pull request 'Added InfluxDB for Gateways' (#23) from testing-statistics into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #23
2023-05-12 13:16:42 +00:00
2313dc827e
Added InfluxDB for Gateways
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-05-12 15:14:06 +02:00
0ba254b9a2
Add Addresses to Gateways
All checks were successful
continuous-integration/drone/tag Build is passing
continuous-integration/drone/push Build is passing
2023-05-12 08:17:31 +02:00
c4a7bfec1f
Bugfixing
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-05-12 07:55:58 +02:00
20b368f6f7
Add Static Gateway Configuration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-05-11 21:38:10 +02:00
452a550801
removed testing branch entry from README.md
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-04-28 23:00:16 +02:00
742a904f95
Merge branch 'testing'
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-28 22:53:35 +02:00
97097adf58
syntax guessing
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
continuous-integration/drone/pr Build is failing
2023-04-28 22:46:08 +02:00
9cf5834e53
syntax 2023-04-28 22:45:07 +02:00
812c44e2fe
added tag latest 2023-04-28 22:41:03 +02:00
968f3b3f57
more foo
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-04-28 22:37:31 +02:00
26bad6c9a8
removed wrong comma 2023-04-28 22:34:55 +02:00
3cf3cfc906
deleted unused file manifest 2023-04-28 22:31:02 +02:00
c1554cbbc9
removed unnecessary pipeline arguments, removed manifest 2023-04-28 22:30:19 +02:00
7225bf5146
changed repo name back
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2023-04-28 22:22:55 +02:00
61ace83ebd
testing more stuffz
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2023-04-28 22:09:25 +02:00
3f27d527e0
typo
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2023-04-28 21:57:24 +02:00
d9d35a1270
added testing branch 2023-04-28 21:53:05 +02:00
c20fccf6ce
typo
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-28 21:52:43 +02:00
5e5a6184f3
Changed registry 2023-04-28 21:51:32 +02:00
0ef8e0c51f
invalid reference format: repository name must be lowercase
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2023-04-28 21:34:22 +02:00
3aca4a52bf
Changed Docker Registry
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2023-04-28 21:30:46 +02:00
13529828ea Change Go to 1.20
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-04-28 18:06:57 +00:00
22c935ce2a
Check if device is online for statistics processing
Some checks failed
continuous-integration/drone/push Build is failing
2023-04-28 16:47:15 +02:00
16dd56e320
Tidy things up 2023-04-28 15:20:42 +02:00
926153b8e7 Skip DHCP Leases when device is offline
All checks were successful
continuous-integration/drone/tag Build is passing
2023-03-26 18:43:50 +02:00
3e0c70bb03 tidy up unter hempels sofa
All checks were successful
continuous-integration/drone/tag Build is passing
2023-03-20 20:49:01 +01:00
12cc8fe7f1 Add RX/TX and DHCP Leases 2023-03-20 20:15:34 +01:00
cfdca822ae Fix load
All checks were successful
continuous-integration/drone/tag Build is passing
2023-03-20 16:35:28 +01:00
bada54bd1d Bugfix
All checks were successful
continuous-integration/drone/tag Build is passing
2023-03-20 16:33:08 +01:00
75c24e389b Add VPN-Router Prefix to Routers instead of RiFu
All checks were successful
continuous-integration/drone/tag Build is passing
2023-03-20 16:26:16 +01:00
a3ecb2d8ae Bugfix, missing Links
All checks were successful
continuous-integration/drone/tag Build is passing
2023-03-20 16:00:11 +01:00
7fdaac3240 add 1h to influx time 2023-03-20 15:53:42 +01:00
e98d59bf8c First test for Influx data
All checks were successful
continuous-integration/drone/tag Build is passing
2023-03-20 15:38:48 +01:00
Stefan Hoffmann
044ad82940 Don´t use Links for Uniifi Nodes
All checks were successful
continuous-integration/drone/tag Build is passing
2021-08-04 20:08:26 +02:00
Stefan Hoffmann
e7aa06f647 Housekeeping
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-18 21:17:09 +02:00
Stefan Hoffmann
88fdc227a3 Bugfixing Meshviewer error
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-05-18 20:49:06 +02:00
Stefan Hoffmann
801ac20dfd Fix Time
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-05-06 14:49:44 +02:00
Stefan Hoffmann
abc9e01d3a Bugfixing error on empty Load or Memory
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-04-29 20:28:28 +02:00
Stefan Hoffmann
3f0d6fc3d4 Added output of Outages from UNMS
All checks were successful
continuous-integration/drone/push Build is passing
2021-04-05 12:52:35 +02:00
Stefan Hoffmann
ff9558c3ee Added some Output
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-03-26 14:37:53 +01:00
Stefan Hoffmann
a67089dcd5 Removed omitempty from loadavg and memory 2021-03-26 14:37:42 +01:00
Stefan Hoffmann
2348f11d02 bugfix
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-03-02 20:25:03 +01:00
Stefan Hoffmann
f91081b93f Added config flag to display Users
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-03-02 20:15:54 +01:00
Stefan Hoffmann
ab5939eb9d README changes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-14 19:35:19 +01:00
Stefan Hoffmann
fa2da6131c cleanup ucDevices.json 2021-02-14 19:23:59 +01:00
Stefan Hoffmann
75fdaf4659 Cleanup jsons 2021-02-14 19:23:11 +01:00
Stefan Hoffmann
82bfc049a7 Don´t show users if meshviewer is disabled 2021-02-14 19:22:38 +01:00
Stefan Hoffmann
7630e90b90 delay time = 60
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-14 00:40:40 +01:00
Stefan Hoffmann
1c1f556e24 types 2021-02-14 00:40:03 +01:00
Stefan Hoffmann
ad8088f1c1 bugfix, if/else
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-14 00:28:49 +01:00
Stefan Hoffmann
7e1841a244 Dont´t output empty fields in struct
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-13 19:12:28 +01:00
Stefan Hoffmann
efd3ba1520 erge branch 'master' of ssh://git.freifunk-rhein-sieg.net:2222/Freifunk-Troisdorf/ubnt-freifunk-map-api
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-13 18:25:50 +01:00
Stefan Hoffmann
e9615b3637 quick fix location problem 2021-02-13 18:24:57 +01:00
2a28bb7498 „.drone.jsonnet“ ändern
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2021-02-13 16:43:13 +00:00
8f227a1385 Merge pull request 'Added function to display real Clients' (#17) from merge-jsons into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #17
2021-02-13 15:48:38 +00:00
63b08c46d4 Merge branch 'master' into merge-jsons
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-13 15:48:25 +00:00
9443f951b2 Merge pull request 'Removed devices files from Dockerfile' (#18) from new-dockerfile into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #18
2021-02-13 15:48:03 +00:00
Stefan Hoffmann
fc4e19db65 Removed devices files from Dockerfile
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-13 16:46:33 +01:00
Stefan Hoffmann
7068225f7b fixed delay-time
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-13 16:12:57 +01:00
Stefan Hoffmann
b3e8fd3bdb Added function to display real Clients
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-13 16:10:41 +01:00
e4a372e29a Merge pull request 'Added Config file' (#10) from config-file into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #10
2021-02-11 19:58:27 +00:00
Stefan Hoffmann
831ce76e79 use global variable for config
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-10 19:31:31 +01:00
e292e304c3
made callUnifiAPI, processAPIs, processUNMSAPI functions for config type
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-07 21:30:40 +01:00
e98b5f1267 Merge branch 'master' into config-file
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2021-02-07 19:02:09 +00:00
46bdb4aa65 Merge pull request 'stefan-patch-1' (#15) from stefan-patch-1 into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #15
2021-02-07 17:00:32 +00:00
2d20267971 „ucDevices.json“ ändern
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-07 16:57:45 +00:00
1f12440c2f „ucDevices.json“ ändern
Some checks failed
continuous-integration/drone/push Build is failing
2021-02-07 16:53:56 +00:00
871e37dcb4 Merge pull request '„ucDevices.json“ ändern' (#13) from angro-patch-2 into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #13
2021-02-07 16:46:05 +00:00
200b226836 „ucDevices.json“ ändern
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Einpflegen Installation "Auf dem Schellerod", Troisdorfer Subnetz flu
2021-02-07 13:35:11 +00:00
5c210c04bd Merge pull request '„ucDevices.json“ ändern' (#11) from angro-patch-1 into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #11
2021-02-07 12:02:38 +00:00
3cb49cf6eb „ucDevices.json“ ändern
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Einpflegen der FF-Instalation "Graf-Galen-Straße" in Troisdorf,
Subnetz "flu", mit drei Unifi AP AC mesh in drei Stockwerken.
2021-02-07 11:42:31 +00:00
Stefan Hoffmann
3c884cf59a Changed README
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-07 11:53:39 +01:00
Stefan Hoffmann
861d1c5d5e Added enbled flag to APIs in config file
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-07 10:46:39 +01:00
Stefan Hoffmann
e133a1e95d Ignore config files!
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-06 22:45:10 +01:00
Stefan Hoffmann
a1bb128291 fixed devices url
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-06 22:44:05 +01:00
Stefan Hoffmann
737e4268fc added first support for config file
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-06 22:32:24 +01:00
47a6e52d30 Merge pull request 'Added Kita APs to ucDevices.json' (#7) from add-kitas into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #7
2021-02-06 18:17:10 +00:00
Stefan Hoffmann
a54d1c932c Added Kita APs
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2021-02-06 19:13:05 +01:00
Stefan Hoffmann
93e98bf6f7 Fixed Drone
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-06 17:59:05 +01:00
Stefan Hoffmann
e7ef51ff21 Added json Validation 2021-02-06 17:57:09 +01:00
Stefan Hoffmann
c654507720 README
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-06 12:57:30 +01:00
Stefan Hoffmann
d200846368 Added devices
All checks were successful
continuous-integration/drone/push Build is passing
2021-02-06 12:02:47 +01:00
21 changed files with 1446 additions and 737 deletions

View File

@ -1,81 +0,0 @@
local pipeline(os, arch) = {
kind: "pipeline",
name: os + "/" + arch,
platform: {
"os": os,
"arch": arch,
},
steps: [{
name: "compile " + os + "/" + arch,
image: "golang:1.15.6-alpine3.12",
environment: {
"GOOS": os,
"GOARCH": arch,
"CGO_ENABLED": "0",
},
commands: [
'go build -ldflags "-s -w -X main.version=${DRONE_TAG##v}" -trimpath -o release/' + os + "/" + arch + "/ubnt-freifunk-map-api .",
"tar -cvzf release/ubnt-freifunk-map-api_"+ os + "-" + arch + ".tar.gz -C release/" + os + "/" + arch + " ubnt-freifunk-map-api"
],
},
{
name: "gitea_release " + os + "/" + arch,
image: "plugins/gitea-release",
settings: {
api_key: { "from_secret": "gitea_api_key" },
base_url: "https://git.freifunk-rhein-sieg.net",
files: "release/*.tar.gz"
},
when: {
event: "tag"
},
},
{
name: "upload to docker hub " + os + "/" + arch,
image: "plugins/docker:" + os + "-" + arch,
settings: {
repo: "fftdf/ffmap-ubnt-api",
username: { "from_secret": "docker_username" },
password: { "from_secret": "docker_password" },
auto_tag: true,
auto_tag_suffix: os + "-" + arch
},
when: {
event: "tag"
},
},
],
};
local manifest() = {
kind: "pipeline",
type: "docker",
name: "manifest",
depends_on: ["linux/amd64"],
when: {
event: "tag"
},
steps: [
{
name: "publish",
image: "plugins/manifest",
settings: {
auto_tag: true,
ignore_missing: true,
spec: "manifest.yml",
username: { "from_secret": "docker_username" },
password: { "from_secret": "docker_password" },
},
when: {
event: "tag"
},
},
],
};
[
pipeline("linux", "amd64"),
// pipeline("linux", "arm64"),
manifest()
]

1
.gitignore vendored
View File

@ -14,3 +14,4 @@
output/* output/*
ubnt-freifunk-map-api ubnt-freifunk-map-api
config.json

25
.woodpecker.yml Normal file
View File

@ -0,0 +1,25 @@
---
platform: linux/arm64
pipeline:
build:
image: golang
environment:
- GOOS=linux
- GOARCH=amd64
commands:
- go build -ldflags "-s -w -X main.version=${CI_COMMIT_TAG}" -trimpath -o release/ubnt-freifunk-map-api .
docker:
image: woodpeckerci/plugin-docker-buildx
settings:
platforms: linux/amd64
registry: git.freifunk-rhein-sieg.net
repo: git.freifunk-rhein-sieg.net/freifunk-troisdorf/ubnt-freifunk-map-api
username:
from_secret: gitea_user
password:
from_secret: gitea_token
tags: ${CI_COMMIT_TAG}
when:
- branch: master

View File

@ -1,9 +1,7 @@
FROM alpine:3.12.3 FROM alpine:3.12.3
WORKDIR /opt/ WORKDIR /opt/
ADD ./release/*/*/ubnt-freifunk-map-api /opt/ubnt-freifunk-map-api ADD ./release/ubnt-freifunk-map-api /opt/ubnt-freifunk-map-api
ADD ./devices.json /opt/devices.json
ADD ./ucDevices.json /opt/ucDevices.json
RUN chmod +x /opt/ubnt-freifunk-map-api RUN chmod +x /opt/ubnt-freifunk-map-api
EXPOSE 3000 EXPOSE 3000

105
README.md
View File

@ -1,3 +1,104 @@
# ubnt-freifunk-map-api # Freifunk Meshviewer Unifi Access Points und Richtfunkstrecken import
Use the API of UNMS and Unifi Controller to display Unifi Hardware in Freifunk Map Dieses tool Importiert Nodes für die Freifunk Map aus den APIs UISP (Richtfunk) & Unifi (Access Points).
Ebenfalls ist der Import statischer devices möglich. Da diese alle in unerem Proxmox cluster laufen, werden Statistikdaten aus der Proxmox InfluxDB geholt.
Alle Config dateien müssen per http erreichbar sein (z.B. in einem Git)
Für Troisdorf werden diese Dateien hier gepflegt: https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-api-devices
Für die Rhein-Sieg-Map hier: https://git.freifunk-rhein-sieg.net/Freifunk-Rhein-Sieg/ubnt-api-devices
## Config
### Unifi Access Points (unifi_devices.json)
In der Datei unifi_devices.json können die Access Points gepflegt werden, die auf der Freifunk Map erscheinen sollen.
Hierzu muss die Datei im json Format erweitert werden.
Für jedes Gerät muss dieser Block angelegt werden:
```json
{
"name": "ROUTERNAME",
"mac": "00:00:00:00:00",
"gateway_nexthop": "1234567890",
"gateway": "1234567890",
"linked_to": "18:e8:29:24:17:0a",
"domain": "unifi",
"location": {
"longitude":7.148406208,
"latitude":50.817093402
},
```
Erklärung:
* name: Dient nur zur Wiedererkennung. Auf der Map erscheint der Name aus dem Unifi Controller
* mac: Die MAC Adresse des Access Points. Zu finden im Controller. (Statistik Übersicht in der Geräteansicht)
* gateway_nexthop: Die Node ID (MAC ohne :) des Freifunk Routers an den der AP angeschlossen ist.
* gateway: Im Normalfall die NodeID des Supernodes (zu finden in der MAP)
* linked_to: (Optional) Die MAC Adresse des Routers an dem der AP angeschlossen ist. Normalerweise gateway_nexthop mit Doppelpunkten. Wenn nicht gesetzt wird kein Link auf der Map angezeigt.
* domain: Die Domain in der sich der AP befindet. (tdf, inn, flu)
### UISP Richtfunkstrecken
In der Datei rifu_devices.json können die Richtfunkstrecken gepflegt werden, die auf der Freifunk Map erscheinen sollen.
```json
{
"name": "Rathaus2Bahnhof",
"mac": "18:E8:29:8E:C6:4D",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"domain": "rifu",
"location": {
"longitude":7.148406208,
"latitude":50.817093402
}
}
```
Erklärung:
* name: Dient nur zur Wiedererkennung. Auf der Map erscheint der Name aus dem UNMS Controller
* mac: Die MAC Adresse des Gerätes. Zu finden im Controller.
* gateway_nexthop: Die Node ID (MAC ohne :) des Freifunk Routers an dem das Gerät angeschlossen ist.
* gateway: Im Normalfall die NodeID des Supernodes (zu finden in der MAP)
* domain: Die Domain in der sich der AP befindet. (tdf, inn, flu)
### UISP Router
In dieser datei werden die Router (meist ER-X) gepflegt. Diese Daten werden dann ebenfalls aus der UISP API Importiert.
```json
{
"name": "Rathaus Uplink",
"mac": "18:e8:29:ad:9a:34",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"domain": "tdf",
"location": {
"longitude":7.149406208,
"latitude":50.817093402
}
},
```
### Gateways.json
Hier werden Statische Geräte eingetragen die auf dem Proxmox Cluster laufen.
```json
{
"name": "VPN01",
"fqdn": "vpn01.fftdf.de",
"mac": "00:00:00:00:00:01",
"domain": "VPN1",
"adresses": ["5.9.220.114"]
},
```
### Config.json
Es gibt 3 Module die Ein/Ausgeschatet werden können:
* UNMS
* Unifi
* Meshviewer
* Gateways
Die Funktion Meshviewer importiert die vorhandenen meshviewer.json und manipuliert dort die Userzahlen. Sobald ein Access Point einen Node aus einer Meshviwer.json als "gateway_nexthop" eingetragen hat, werden die Clients an dem verbundenen Access Point und nicht mehr am Offloader angezeigt.

36
example.config.json Normal file
View File

@ -0,0 +1,36 @@
{
"unms": {
"enabled": false,
"unmsAPIUrl": "https://uisp.freifunk-troisdorf.de/v2.1",
"APItoken": "UNMS API TOKEN",
"devicesURL": "https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/example.devices.json"
},
"unifi": [
{
"name": "Unifi Freifunk Troisdorf",
"enabled": false,
"displayusers": true,
"APIUrl": "https://unifi.freifunk-troisdorf.de",
"user": "APIuser",
"password": "PASSWORD",
"ucDevicesURL": "https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/example.ucDevices.json"
}
],
"meshviewer": {
"enabled": false,
"files": [
{
"name": "tdf4",
"URL": "http://4.fftdf.de/meshviewer.json"
},
{
"name": "tdf5",
"URL": "http://5.fftdf.de/meshviewer.json"
},
{
"name": "tdf6",
"URL": "http://6.fftdf.de/meshviewer.json"
}
]
}
}

View File

@ -0,0 +1,26 @@
{
"devices":[
{
"name": "Rathaus2Bahnhof",
"mac": "18:E8:29:8E:C6:4D",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"domain": "other",
"location": {
"longitude":7.148406208,
"latitude":50.817093402
}
},
{
"name": "Bahnhof2Rathaus",
"mac": "18:e8:29:dc:c3:7e",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"domain": "other",
"location": {
"longitude":7.150436640,
"latitude":50.814456507
}
}
]
}

View File

@ -1,92 +0,0 @@
{
"timestamp": "2021-01-01T21:16:19+0100",
"nodes": [
{
"firstseen": "2020-01-20T17:38:03+0000",
"lastseen": "2021-01-01T20:16:07+0000",
"is_online": true,
"is_gateway": false,
"clients": 0,
"clients_wifi24": 0,
"clients_wifi5": 0,
"clients_other": 0,
"rootfs_usage": 0,
"loadavg": 0.02,
"memory_usage": 0.66,
"uptime": "2020-11-22T14:02:13+0000",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"location": {
"longitude": 7.148406208,
"latitude": 50.817093402
},
"node_id": "18e8298ec64d",
"mac": "18:e8:29:8e:c6:4d",
"addresses": [
"10.188.41.164"
],
"domain": "rifu",
"hostname": "[RiFu] Port 5 - zu Bahnhof",
"owner": "Freifunk Rhein-Sieg",
"firmware": {
"base": "Ubiquiti - Stock",
"release": "8.7.1"
},
"autoupdater": {
"enabled": false,
"branch": "stable"
},
"nproc": 1,
"model": "NBE-5AC-Gen2"
},
{
"firstseen": "2020-05-05T20:08:20+0000",
"lastseen": "2021-01-01T20:15:52+0000",
"is_online": true,
"is_gateway": false,
"clients": 0,
"clients_wifi24": 0,
"clients_wifi5": 0,
"clients_other": 0,
"rootfs_usage": 0,
"loadavg": 0.27,
"memory_usage": 0.68,
"uptime": "2020-10-24T21:47:33+0000",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"location": {
"longitude": 7.15043664,
"latitude": 50.814456507
},
"node_id": "18e829dcc37e",
"mac": "18:e8:29:dc:c3:7e",
"addresses": [
"10.188.47.143"
],
"domain": "rifu",
"hostname": "[RiFu] Bahnhof-TDF",
"owner": "Freifunk Rhein-Sieg",
"firmware": {
"base": "Ubiquiti - Stock",
"release": "8.7.1"
},
"autoupdater": {
"enabled": false,
"branch": "stable"
},
"nproc": 1,
"model": "NBE-5AC-Gen2"
}
],
"links": [
{
"type": "wifi",
"source": "18e8298ec64d",
"target": "18e829dcc37e",
"source_tq": 0.315,
"target_tq": 0.315,
"source_addr": "18:e8:29:8e:c6:4d",
"target_addr": "18:e8:29:dc:c3:7e"
}
]
}

26
example.ucDevices.json Normal file
View File

@ -0,0 +1,26 @@
{
"devices":[
{
"name": "AM-01",
"mac": "18:e8:29:9c:90:ae",
"gateway_nexthop": "18e82924170a",
"gateway": "a28cae6ff604",
"linked_to": "18:e8:29:24:17:0a",
"location": {
"longitude":7.122931927,
"latitude":50.818885422
}
},
{
"name": "AM-02",
"mac": "18:e8:29:9c:96:ba",
"gateway_nexthop": "18e82924170a",
"gateway": "a28cae6ff604",
"linked_to": "18:e8:29:24:17:0a",
"location": {
"longitude":7.122777700,
"latitude":50.819022680
}
}
]
}

16
go.mod
View File

@ -1,5 +1,17 @@
module git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api module git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api
go 1.16 go 1.20
require git.nils.zone/nils/prettify v0.0.4 require (
git.nils.zone/nils/prettify v0.0.4
github.com/fatih/structs v1.1.0
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c
)
require (
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.11 // indirect
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect
)

4
go.sum
View File

@ -4,8 +4,12 @@ github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkH
github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e h1:0aewS5NTyxftZHSnFaJmWE5oCCrj4DyEXkAiMa1iZJM= github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e h1:0aewS5NTyxftZHSnFaJmWE5oCCrj4DyEXkAiMa1iZJM=
github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI= github.com/hokaccha/go-prettyjson v0.0.0-20190818114111-108c894c2c0e/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs=
github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=

63
influx.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"encoding/json"
"log"
client "github.com/influxdata/influxdb1-client/v2"
)
// Create InfluxDB Client
func influxDBClient(port string) client.Client {
c, err := client.NewHTTPClient(client.HTTPConfig{
Addr: conf.General.InfluxURL + ":" + port,
})
if err != nil {
log.Fatalln("Error: ", err)
}
return c
}
// Get a single Datapoint from InfluxDB
func getInfluxDataPoint(dp string, h string, p string) float64 {
//Build the Query
query := "SELECT last(" + dp + ") FROM system WHERE host = '" + h + "'"
c := influxDBClient(p)
q := client.NewQuery(query, "udp", "s")
response, err := c.Query(q)
if err != nil {
log.Println("Influx query error!")
}
res := 0.0
if len(response.Results) > 0 {
res, err := response.Results[0].Series[0].Values[0][1].(json.Number).Float64()
if err != nil {
log.Println("Error in type conversion")
}
return res
}
return res
}
// Send Datapoints to InfluxDB, point map and InfluxDB Port needed
func sendInfluxBatchDataPoint(point *client.Point, influxPort string) {
// Open connection to InfluxDB
bp, err := client.NewBatchPoints(client.BatchPointsConfig{
Database: "freifunk",
Precision: "s",
})
if err != nil {
log.Fatalln("Error: ", err)
}
bp.AddPoint(point)
c := influxDBClient(influxPort)
err = c.Write(bp)
if err != nil {
log.Fatal(err)
}
//
}

504
main.go
View File

@ -1,36 +1,36 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io"
"log" "log"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"strconv" "os"
"strings" "sync"
"time" "time"
_ "git.nils.zone/nils/prettify" _ "git.nils.zone/nils/prettify"
) )
// types
const ( const (
baseURL = "https://unifi.freifunk-troisdorf.de/v2.1"
ucBaseURL = "https://unifi.freifunk-troisdorf.de:8443"
iso8601 = "2006-01-02T15:04:05-0700" iso8601 = "2006-01-02T15:04:05-0700"
fetchInterval = 1 * time.Minute
) )
// flags // flags
var token = flag.String("token", "", "Defines the x-auth-token") var (
var ucUser = flag.String("ucUser", "", "Defines the Unifi API User") lastFetchTime time.Time
var ucPass = flag.String("ucPass", "", "Defines the Unifi API Password") cacheMutex sync.Mutex
cacheNodes []node
cacheLinks []link
)
var configPath = flag.String("configPath", "config.json", "Path to config.json")
var version = "development" var version = "development"
var delay time.Duration = 60 * time.Second var delay time.Duration = 60 * time.Second
var conf = loadconfig(*configPath)
func main() { func main() {
log.Printf("starting version %s...\n", version) log.Printf("starting version %s...\n", version)
@ -38,251 +38,72 @@ func main() {
flag.Parse() flag.Parse()
// check if flags are set // check if flags are set
if *token == "" { if *configPath == "" {
log.Fatalln("Please specify an API token via the flag '-token'") log.Fatalln("Please specify path to config.json flag '-configPath'")
}
if *ucPass == "" {
log.Fatalln("Please specify an API Password via the flag '-ucPass'")
}
if *ucUser == "" {
log.Fatalln("Please specify an API User via the flag '-ucUser'")
} }
// start API processing (runs in a loop) // start API processing (runs in a loop)
go processAPIs() go func() {
if err := processAPIs(); err != nil {
log.Fatalln("API processing failed, error is: ", err)
}
tick := time.Tick(delay)
for range tick {
if err := processAPIs(); err != nil {
log.Fatalln("API processing failed, error is: ", err)
}
}
}()
// start webserver on Port 3000 // start webserver on Port 3000
serveJSON() serveJSON()
} }
//switch Unifi AP Mod IDs to Names func processAPIs() error {
func lookupModels(model string) string {
switch model {
case "BZ2", "U2S48", "U2Sv2":
return "Unifi AP"
case "BZ2LR", "U2L48", "U2Lv2":
return "UniFi AP-LR"
case "U7E", "U7Ev2":
return "UniFi AP-AC"
case "U7HD", "U7SHD":
return "UniFi AP-HD"
case "UXSDM":
return "UniFi AP-BaseStationXG"
case "UCMSH":
return "AP-MeshXG"
case "U7MP":
return "AP-AC-Mesh-Pro"
case "U7LR":
return "UniFi AP-AC-LR"
case "U7LT":
return "UniFi AP-AC-Lite"
case "U7P":
return "UniFi AP-Pro"
case "U7MSH":
return "UniFi AP-AC-Mesh"
case "U7PG2":
return "UniFi AP-AC-Pro"
default:
return "Unifi Gerät"
}
}
//int to bool converter
func itob(i int) bool {
if i == 1 {
return true
}
return false
}
//Unifi Controller API processing
func processUcAPIs() ([]node, []link) {
//get list of Unifi devices to display
var nodes []node
var links []link
d, err := getDevices("ucDevices.json")
if err != nil {
log.Fatalln(err)
}
//call Unifi Controller
ucAPI := newAPI(*ucUser, *ucPass, ucBaseURL)
//login
ucAPI.ucLogin()
//get all Sites from Controller
sites, err := ucAPI.ucGetSites()
if err != nil {
panic(err)
}
//get all devices in all sites
devices, err := ucAPI.ucGetDevices(sites)
if err != nil {
panic(err)
}
//build nodes struct
for _, jsonDevice := range d.Devices {
var currentDevice ucDevice
var currentJSONDevice device
for _, device := range devices {
if strings.ToUpper(device.Mac) == strings.ToUpper(jsonDevice.MAC) {
currentDevice = device
currentJSONDevice = jsonDevice
}
}
if isRemoteMACpublished(jsonDevice.MAC, d.Devices) == true {
links = ucAddLink(jsonDevice, links)
}
load, err := strconv.ParseFloat(currentDevice.Sysstats.CPU, 64)
if err != nil {
panic(err)
}
mem, err := strconv.ParseFloat(currentDevice.Sysstats.Memory, 64)
if err != nil {
panic(err)
}
nodes = append(nodes, node{
Firstseen: "0",
Lastseen: time.Unix(int64(currentDevice.LastSeen), 0).Format(iso8601),
IsOnline: itob(currentDevice.State),
IsGateway: false,
Clients: 0,
ClientsWifi24: 0,
ClientsWifi5: 0,
ClientsOther: 0,
RootFSUsage: 0,
LoadAVG: load / 100,
MemoryUsage: mem / 100,
Uptime: time.Now().Add(-1 * time.Second * time.Duration(currentDevice.Uptime)).Format(iso8601),
GatewayNexthop: currentJSONDevice.GatewayNexthop,
Gateway: currentJSONDevice.Gateway,
Location: currentJSONDevice.Location,
NodeID: strings.ReplaceAll(currentDevice.Mac, ":", ""),
MAC: currentDevice.Mac,
Adresses: []string{currentDevice.IP},
Domain: currentJSONDevice.Domain,
Hostname: "[Unifi] " + currentDevice.Name,
Owner: "Freifunk Rhein-Sieg",
Firmware: firmware{
Base: "Ubiquiti - Stock",
Release: currentDevice.Version,
},
Autoupdater: autoupdater{
Enabled: false,
Branch: "stable",
},
NProc: 1,
Model: lookupModels(currentDevice.Model),
})
}
return nodes, links
}
//UNMS API processing (Richtfunk)
func processUNMSAPI() ([]node, []link) {
// Variables for runtime
var links []link
var nodes []node
d, err := getDevices("devices.json")
if err != nil {
log.Fatalln(err)
}
// API CALL 1
log.Println("calling API 1")
var u []unifiAPIResponse
err = callUnifiAPI("/devices", &u)
if err != nil {
log.Fatalln(err)
}
for i := range d.Devices {
var dev unifiAPIResponse
var currentDevice device
for j := range u {
if strings.ToUpper(u[j].Identification.MAC) == strings.ToUpper(d.Devices[i].MAC) {
dev = u[j]
currentDevice = d.Devices[i]
}
}
var isOnline bool = false
if dev.Overview.Status == "active" {
isOnline = true
}
// END OF API CALL 1
// API CALL 2
log.Println("calling API 2 for device", d.Devices[i].Name)
var details unifiAPIDetails
callUnifiAPI("/devices/erouters/"+dev.Identification.ID, &details)
// END OF API CALL 2
// API CALL 3
log.Println("calling API 3 for device", d.Devices[i].Name)
var airmaxes []unifiAPIAirmax
callUnifiAPI("/devices/airmaxes/"+dev.Identification.ID+"/stations", &airmaxes)
// check if remote mac address is part of our published network
for i := range airmaxes {
if isRemoteMACpublished(airmaxes[i].DeviceIdentification.MAC, d.Devices) == true {
links = addLink(dev, airmaxes[i], links)
}
}
// END OF API CALL 3
// Get info from json file (static)
nodes = append(nodes, node{
Firstseen: dev.Overview.CreatedAt.Format(iso8601),
Lastseen: dev.Overview.LastSeen.Format(iso8601),
IsOnline: isOnline,
IsGateway: false,
Clients: 0,
ClientsWifi24: 0,
ClientsWifi5: 0,
ClientsOther: 0,
RootFSUsage: 0,
LoadAVG: details.Overview.CPU / 100,
MemoryUsage: details.Overview.RAM / 100,
Uptime: dev.Identification.Started.Format(iso8601),
GatewayNexthop: currentDevice.GatewayNexthop,
Gateway: currentDevice.Gateway,
Location: currentDevice.Location,
NodeID: strings.ReplaceAll(dev.Identification.MAC, ":", ""),
MAC: dev.Identification.MAC,
Adresses: getAddresses(details.IPAddress),
Domain: currentDevice.Domain,
Hostname: "[RiFu] " + details.Identification.Name,
Owner: "Freifunk Rhein-Sieg",
Firmware: firmware{
Base: "Ubiquiti - Stock",
Release: details.Firmware.Current,
},
Autoupdater: autoupdater{
Enabled: false,
Branch: "stable",
},
NProc: 1,
Model: details.Identification.Model,
})
}
return nodes, links
}
func processAPIs() {
tick := time.Tick(delay)
for range tick {
var nodes []node var nodes []node
var links []link var links []link
unmsNodes, unmsLinks := processUNMSAPI() if conf.UISP.Enabled {
ucNodes, ucLinks := processUcAPIs() log.Println("Processing UISP")
nodes = append(nodes, unmsNodes...) //Process UISP RiFu Nodes
nodes = append(nodes, ucNodes...) uispNodes, uispLinks, err := processUISPRiFu()
links = append(links, unmsLinks...) if err != nil {
links = append(links, ucLinks...) return err
}
//Process UISP Routers (like EDGE Router)
uispRouters, err := processUISPRouter()
if err != nil {
return err
}
nodes = append(nodes, uispNodes...)
nodes = append(nodes, uispRouters...)
links = append(links, uispLinks...)
}
if len(conf.Unifi) > 0 {
log.Println("Anazahl der Unifi Server:", len(conf.Unifi))
for i := range conf.Unifi {
if conf.Unifi[i].Enabled {
log.Println("Processing Unifi-Server: ", conf.Unifi[i].Name)
//Process Unifi Nodes
unifiNodes, _, err := processUnifiAPI(i)
if err != nil {
return err
}
nodes = append(nodes, unifiNodes...)
}
}
}
if conf.Meshviewer.Enabled {
log.Println("Processing Meshviewer")
mvNodes, mvLinks := getMeshviewer()
nodes = append(nodes, mvNodes...)
links = append(links, mvLinks...)
}
if conf.Gateways.Enabled {
log.Println("Processing Gateways")
//Process Static Gateways from Json
gwNodes := processGateways()
nodes = append(nodes, gwNodes...)
}
// assemble final struct // assemble final struct
o := output{ o := output{
Timestamp: time.Now().Format(iso8601), Timestamp: time.Now().Format(iso8601),
@ -299,60 +120,55 @@ func processAPIs() {
// we're done here // we're done here
log.Println("...done") log.Println("...done")
return nil
} }
func loadconfig(file string) config {
var config config
configFile, err := os.Open(file)
if err != nil {
log.Fatalln("Failed loding Config file: ", err)
} }
jsonParse := json.NewDecoder(configFile)
if err := jsonParse.Decode(&config); err != nil {
log.Fatalln(err)
}
return config
}
// int to bool converter
func itob(i int) bool {
return i == 1
}
// function to get file from meshviewer
func getFile(url string) []byte { func getFile(url string) []byte {
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
fmt.Printf("error") log.Println("Error getting file from:", url)
} }
data := resp.Body data := resp.Body
byteValue, _ := ioutil.ReadAll(data) byteValue, _ := io.ReadAll(data)
return byteValue return byteValue
} }
func getDevices(file string) (devices, error) { // get devices from devices file on webserver (config)
func getDevices(url string) devices {
// get devices from JSON file // get devices from JSON file
jsonFile := getFile("https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/" + file) jsonFile := getFile(url)
// read file to bytes // read file to bytes
// variable for d // variable for d
var d devices var d devices
// unmarshal to struct // unmarshal to struct
json.Unmarshal(jsonFile, &d) err := json.Unmarshal(jsonFile, &d)
return d, nil
}
func callUnifiAPI(url string, i interface{}) error {
request, err := http.NewRequest(http.MethodGet, baseURL+url, nil)
if err != nil { if err != nil {
return errors.New(fmt.Sprint("can't set request", baseURL+url)) fmt.Println("can´t get devices file from " + url)
log.Fatal(err)
} }
request.Header.Set("x-auth-token", *token) return d
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("can't get request %s with x-auth-token %s", baseURL+url, *token)
}
data, err := ioutil.ReadAll(response.Body)
defer response.Body.Close()
if err != nil {
return fmt.Errorf("can't read response body: %+v", response.Body)
}
// try to fetch errors
var a apiResponse
json.Unmarshal(data, &a)
if a.StatusCode != 0 {
return fmt.Errorf("got following errorcode from API: %d %s %s", a.StatusCode, a.Error, a.Message)
}
// no error occurred, unmarshal to struct
json.Unmarshal(data, &i)
return nil
} }
// check for MAC Adress in current Devices
func isRemoteMACpublished(mac string, devices []device) bool { func isRemoteMACpublished(mac string, devices []device) bool {
for i := range devices { for i := range devices {
if devices[i].MAC == mac { if devices[i].MAC == mac {
@ -362,31 +178,6 @@ func isRemoteMACpublished(mac string, devices []device) bool {
return false return false
} }
func addLink(dev unifiAPIResponse, airmaxes unifiAPIAirmax, links []link) []link {
for i := range links {
if links[i].SourceAddr == airmaxes.DeviceIdentification.MAC {
// link already exists
return links
}
}
links = append(links, link{
Type: "wifi",
Source: strings.ReplaceAll(dev.Identification.MAC, ":", ""),
Target: strings.ReplaceAll(airmaxes.DeviceIdentification.MAC, ":", ""),
SourceTQ: airmaxes.Statistics.LinkScore,
TargetTQ: airmaxes.Statistics.LinkScore,
SourceAddr: dev.Identification.MAC,
TargetAddr: airmaxes.DeviceIdentification.MAC,
})
return links
}
func getAddresses(ip string) []string {
var adresses []string
adresses = append(adresses, strings.Split(ip, "/")[0])
return adresses
}
func serveJSON() { func serveJSON() {
fs := http.FileServer(http.Dir("./output")) fs := http.FileServer(http.Dir("./output"))
http.Handle("/", fs) http.Handle("/", fs)
@ -406,102 +197,3 @@ func httpClient() *http.Client {
client := &http.Client{Jar: jar} client := &http.Client{Jar: jar}
return client return client
} }
func newAPI(user string, pass string, baseURL string) ucAPIData {
return ucAPIData{
user: user,
pass: pass,
baseURL: baseURL,
client: httpClient(),
}
}
func (u *ucAPIData) ucCallAPI(url string, method string, body *bytes.Buffer, output interface{}) error {
req, err := http.NewRequest(method, u.baseURL+url, body)
if err != nil {
return fmt.Errorf("can't set request %s", u.baseURL+url)
}
req.Header.Set("Content-Type", "application/json")
response, err := u.client.Do(req)
if err != nil {
return fmt.Errorf("can't login %s", u.baseURL+url)
}
defer response.Body.Close()
if response.StatusCode != 200 {
return fmt.Errorf("Login failed %s", u.baseURL+url)
}
data, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}
err = json.Unmarshal(data, &output)
if err != nil {
return err
}
return nil
}
func (u *ucAPIData) ucLogin() error {
var loginData = []byte(`{"username":"` + u.user + `","password":"` + u.pass + `"}`)
url := "/api/login"
err := u.ucCallAPI(url, http.MethodPost, bytes.NewBuffer(loginData), nil)
if err != nil {
return err
}
return nil
}
func (u *ucAPIData) ucGetSites() ([]ucSite, error) {
var d struct {
Data []ucSite `json:"data"`
}
url := "/api/self/sites"
err := u.ucCallAPI(url, http.MethodGet, bytes.NewBuffer([]byte{}), &d)
if err != nil {
return []ucSite{}, err
}
return d.Data, nil
}
func (u *ucAPIData) ucGetDevices(sites []ucSite) ([]ucDevice, error) {
var d struct {
Data []ucDevice `json:"data"`
}
var s []ucDevice
for _, site := range sites {
url := "/api/s/" + site.ID + "/stat/device"
u.ucCallAPI(url, http.MethodGet, bytes.NewBuffer([]byte{}), &d)
s = append(s, d.Data...)
}
return s, nil
}
func ucAddLink(dev device, links []link) []link {
for i := range links {
if links[i].SourceAddr == dev.MAC {
// link already exists
return links
}
}
if dev.LinkedTo == "" {
//no LinkedTo in ucDevices.json
return links
}
links = append(links, link{
Type: "cable",
Source: strings.ReplaceAll(dev.MAC, ":", ""),
Target: strings.ReplaceAll(dev.GatewayNexthop, ":", ""),
SourceTQ: 1,
TargetTQ: 1,
SourceAddr: dev.MAC,
TargetAddr: dev.LinkedTo,
})
return links
}

View File

@ -1,19 +0,0 @@
image: fftdf/ffmap-ubnt-api:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
- "latest"
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
-
image: fftdf/ffmap-ubnt-api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
# -
# image: fftdf/ffmap-ubnt-api:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
# platform:
# architecture: arm64
# os: linux

160
meshviewer.go Normal file
View File

@ -0,0 +1,160 @@
package main
import (
"encoding/json"
"log"
"time"
)
func getMeshviewerJSON(url string) (mvDevices, error) {
// get devices from JSON file
jsonFile := getFile(url)
// read file to bytes
// variable for d
var n mvDevices
//var l []link
// unmarshal to struct
err := json.Unmarshal(jsonFile, &n)
if err != nil {
log.Println("can´t get Meshviewer Json file from " + url)
log.Println(err)
}
return n, nil
}
func checkMeshviewerLink(s string) bool {
mvNodes, _ := getMeshviewer()
for i := range mvNodes {
if mvNodes[i].MAC == s {
return true
}
}
return false
}
func addmvDevices(d mvDevices) ([]node, []link) {
var nodes []node
var links []link
for i := range d.Nodes {
mvNode := d.Nodes[i]
if findNodeID(mvNode.NodeID) {
mvNode.Clients = 0
mvNode.ClientsWifi24 = 0
mvNode.ClientsWifi5 = 0
mvNode.ClientsOther = 0
}
if mvNode.Location.Latitude == 0 {
nodes = append(nodes, node{
Firstseen: mvNode.Firstseen,
Lastseen: mvNode.Lastseen,
IsOnline: mvNode.IsOnline,
IsGateway: mvNode.IsGateway,
Clients: mvNode.Clients,
ClientsWifi24: mvNode.ClientsWifi24,
ClientsWifi5: mvNode.ClientsWifi5,
ClientsOther: mvNode.ClientsOther,
RootFSUsage: int(mvNode.RootfsUsage),
LoadAVG: mvNode.Loadavg,
MemoryUsage: mvNode.MemoryUsage,
Uptime: mvNode.Uptime,
GatewayNexthop: mvNode.GatewayNexthop,
Gateway: mvNode.Gateway,
NodeID: mvNode.NodeID,
MAC: mvNode.Mac,
Adresses: mvNode.Addresses,
Domain: mvNode.Domain,
Hostname: mvNode.Hostname,
Owner: mvNode.Owner,
Firmware: firmware{
Base: mvNode.Firmware.Base,
Release: mvNode.Firmware.Release,
},
Autoupdater: autoupdater{
Enabled: mvNode.Autoupdater.Enabled,
Branch: mvNode.Autoupdater.Branch,
},
NProc: mvNode.Nproc,
Model: mvNode.Model,
})
} else {
nodes = append(nodes, node{
Firstseen: mvNode.Firstseen,
Lastseen: mvNode.Lastseen,
IsOnline: mvNode.IsOnline,
IsGateway: mvNode.IsGateway,
Clients: mvNode.Clients,
ClientsWifi24: mvNode.ClientsWifi24,
ClientsWifi5: mvNode.ClientsWifi5,
ClientsOther: mvNode.ClientsOther,
RootFSUsage: int(mvNode.RootfsUsage),
LoadAVG: mvNode.Loadavg,
MemoryUsage: mvNode.MemoryUsage,
Uptime: mvNode.Uptime,
GatewayNexthop: mvNode.GatewayNexthop,
Gateway: mvNode.Gateway,
Location: &mvNode.Location,
NodeID: mvNode.NodeID,
MAC: mvNode.Mac,
Adresses: mvNode.Addresses,
Domain: mvNode.Domain,
Hostname: mvNode.Hostname,
Owner: mvNode.Owner,
Firmware: firmware{
Base: mvNode.Firmware.Base,
Release: mvNode.Firmware.Release,
},
Autoupdater: autoupdater{
Enabled: mvNode.Autoupdater.Enabled,
Branch: mvNode.Autoupdater.Branch,
},
NProc: mvNode.Nproc,
Model: mvNode.Model,
})
}
}
for i := range d.Links {
mvNode := d.Links[i]
links = append(links, link{
Type: mvNode.Type,
Source: mvNode.Source,
Target: mvNode.Target,
SourceTQ: mvNode.SourceTq,
TargetTQ: mvNode.TargetTq,
SourceAddr: mvNode.SourceAddr,
TargetAddr: mvNode.TargetAddr,
})
}
return nodes, links
}
func getMeshviewer() ([]node, []link) {
cacheMutex.Lock()
defer cacheMutex.Unlock()
// Überprüfen, ob die Daten kürzlich aktualisiert wurden
if time.Since(lastFetchTime) < fetchInterval {
return cacheNodes, cacheLinks
}
var nodes []node
var links []link
for i := range conf.Meshviewer.Files {
m, err := getMeshviewerJSON(conf.Meshviewer.Files[i].URL)
if err != nil {
return cacheNodes, cacheLinks
}
mvNodes, mvLinks := addmvDevices(m)
nodes = append(nodes, mvNodes...)
links = append(links, mvLinks...)
}
// Cache aktualisieren
cacheNodes = nodes
cacheLinks = links
lastFetchTime = time.Now()
return cacheNodes, cacheLinks
}

99
staticDevices.go Normal file
View File

@ -0,0 +1,99 @@
package main
import (
"log"
"strings"
"time"
client "github.com/influxdata/influxdb1-client/v2"
)
func processGateways() []node {
d := getDevices(conf.Gateways.GatewaysURL)
var nodes []node
for i := range d.Devices {
log.Println("Processing Static Device: ", d.Devices[i].Name)
currentDevice := d.Devices[i]
//Collect data
//Calulate Memory (%)
mem := getInfluxDataPoint("mem", currentDevice.FQDN, conf.General.ProxmoxInfluxPort)
maxmem := getInfluxDataPoint("maxmem", currentDevice.FQDN, conf.General.ProxmoxInfluxPort)
memoryMap := mem / maxmem
memory := memoryMap * 100
// Get Network
rx := getInfluxDataPoint("netin", currentDevice.FQDN, conf.General.ProxmoxInfluxPort)
tx := getInfluxDataPoint("netout", currentDevice.FQDN, conf.General.ProxmoxInfluxPort)
// Get CPU (%)
cpuMap := getInfluxDataPoint("cpu", currentDevice.FQDN, conf.General.ProxmoxInfluxPort)
cpu := cpuMap * 100
//Uptime (seconds)
uptime := getInfluxDataPoint("uptime", currentDevice.FQDN, conf.General.ProxmoxInfluxPort)
t := time.Duration(uptime * float64(time.Second))
up := time.Now().Add(-t)
// fields := map[string]interface{}{}
fields := make(map[string]any)
tags := map[string]string{
"hostname": strings.ReplaceAll(d.Devices[i].Name, " ", "-"),
"nodeid": strings.ReplaceAll(d.Devices[i].MAC, ":", ""),
}
//Build fields for InfluxDB
fields["load"] = cpu
fields["ram"] = int(memory)
fields["time.up"] = int(uptime)
//Network
fields["traffic.rx.bytes"] = int(rx)
fields["traffic.tx.bytes"] = int(tx)
point, err := client.NewPoint(
"node",
tags,
fields,
time.Now(),
)
if err != nil {
log.Fatalln("Error: ", err)
}
if conf.General.InfluxEnabled {
sendInfluxBatchDataPoint(point, conf.General.FreifunkInfluxPort)
}
//Build Nodes
nodes = append(nodes, node{
Firstseen: up.Format(iso8601),
Lastseen: time.Now().Format(iso8601),
IsOnline: true,
IsGateway: true,
Clients: 0,
ClientsWifi24: 0,
ClientsWifi5: 0,
ClientsOther: 0,
RootFSUsage: 0,
LoadAVG: cpuMap,
MemoryUsage: memoryMap,
Uptime: up.Format(iso8601),
GatewayNexthop: "",
Gateway: "",
NodeID: strings.ReplaceAll(d.Devices[i].MAC, ":", ""),
MAC: d.Devices[i].MAC,
Adresses: d.Devices[i].Adresses,
Domain: d.Devices[i].Domain,
Hostname: "[Gateway] " + d.Devices[i].Name,
Owner: "Freifunk Troisdorf",
Firmware: firmware{
Base: "KVM",
Release: "Ubuntu 22.04",
},
Autoupdater: autoupdater{
Enabled: false,
Branch: "stable",
},
NProc: 1,
Model: "KVM",
})
}
return nodes
}

218
types.go
View File

@ -6,12 +6,50 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"sync"
"time" "time"
) )
type config struct {
General struct {
InfluxEnabled bool `json:"influx_enabled"`
FreifunkInfluxPort string `json:"freifunk_influx_port"`
ProxmoxInfluxPort string `json:"proxmox_influx_port"`
InfluxURL string `json:"influx_url"`
}
UISP struct {
Enabled bool `json:"enabled"`
UnmsAPIURL string `json:"unmsAPIUrl"`
APItoken string `json:"APItoken"`
DevicesURL string `json:"devicesURL"`
RouterURL string `json:"routerURL"`
} `json:"unms"`
Unifi []UnifiServer `json:"unifi"`
Meshviewer struct {
Enabled bool `json:"enabled"`
Files []struct {
Name string `json:"name"`
URL string `json:"URL"`
} `json:"files"`
} `json:"meshviewer"`
Gateways struct {
Enabled bool `json:"enabled"`
GatewaysURL string `json:"gatewaysurl"`
} `json:"gateways"`
}
type UnifiServer struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
DisplayUsers bool `json:"displayusers"`
APIURL string `json:"APIUrl"`
User string `json:"user"`
Password string `json:"password"`
UCDevicesURL string `json:"ucDevicesURL"`
}
type device struct { type device struct {
Name string `json:"name"` Name string `json:"name"`
FQDN string `json:"fqdn"`
MAC string `json:"mac"` MAC string `json:"mac"`
GatewayNexthop string `json:"gateway_nexthop"` GatewayNexthop string `json:"gateway_nexthop"`
LinkedTo string `json:"linked_to"` LinkedTo string `json:"linked_to"`
@ -21,6 +59,7 @@ type device struct {
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
} `json:"location"` } `json:"location"`
Adresses []string `json:"adresses"`
} }
type devices struct { type devices struct {
Devices []device `json:"devices"` Devices []device `json:"devices"`
@ -52,6 +91,29 @@ type unifiAPIDetails struct {
RAM float64 `json:"ram"` RAM float64 `json:"ram"`
} `json:"overview"` } `json:"overview"`
IPAddress string `json:"ipAddress"` IPAddress string `json:"ipAddress"`
Location struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Altitude int `json:"altitude"`
} `json:"location"`
Interfaces []struct {
Identification struct {
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
Mac string `json:"mac"`
DisplayName string `json:"displayName"`
} `json:"identification"`
Statistics struct {
Rxrate int `json:"rxrate"`
Txrate int `json:"txrate"`
Rxbytes int64 `json:"rxbytes"`
Txbytes int64 `json:"txbytes"`
PoePower int `json:"poePower"`
Dropped int `json:"dropped"`
Errors int `json:"errors"`
} `json:"statistics"`
} `json:"interfaces"`
} }
type unifiAPIAirmax struct { type unifiAPIAirmax struct {
@ -77,21 +139,21 @@ type node struct {
Firstseen string `json:"firstseen"` Firstseen string `json:"firstseen"`
Lastseen string `json:"lastseen"` Lastseen string `json:"lastseen"`
IsOnline bool `json:"is_online"` IsOnline bool `json:"is_online"`
IsGateway bool `json:"is_gateway"` IsGateway bool `json:"is_gateway,omitempty"`
Clients int `json:"clients"` Clients int `json:"clients"`
ClientsWifi24 int `json:"clients_wifi24"` ClientsWifi24 int `json:"clients_wifi24"`
ClientsWifi5 int `json:"clients_wifi5"` ClientsWifi5 int `json:"clients_wifi5"`
ClientsOther int `json:"clients_other"` ClientsOther int `json:"clients_other"`
RootFSUsage int `json:"rootfs_usage"` RootFSUsage int `json:"rootfs_usage,omitempty"`
LoadAVG float64 `json:"loadavg"` LoadAVG float64 `json:"loadavg"`
MemoryUsage float64 `json:"memory_usage"` MemoryUsage float64 `json:"memory_usage"`
Uptime string `json:"uptime"` Uptime string `json:"uptime"`
GatewayNexthop string `json:"gateway_nexthop"` GatewayNexthop string `json:"gateway_nexthop"`
Gateway string `json:"gateway"` Gateway string `json:"gateway"`
Location struct { Location *struct {
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
} `json:"location"` } `json:"location,omitempty"`
NodeID string `json:"node_id"` NodeID string `json:"node_id"`
MAC string `json:"mac"` MAC string `json:"mac"`
Adresses []string `json:"addresses"` Adresses []string `json:"addresses"`
@ -140,17 +202,6 @@ func (o *output) writeToFile() error {
return nil return nil
} }
type apiResponse struct {
StatusCode int `json:"statusCode"`
Error string `json:"error"`
Message string `json:"message"`
}
type jar struct {
lk sync.Mutex
cookies map[string][]*http.Cookie
}
type ucSite struct { type ucSite struct {
Name string `json:"desc"` Name string `json:"desc"`
ID string `json:"name"` ID string `json:"name"`
@ -166,15 +217,148 @@ type ucDevice struct {
State int `json:"state"` State int `json:"state"`
LastSeen int `json:"last_seen"` LastSeen int `json:"last_seen"`
Uptime int `json:"uptime"` Uptime int `json:"uptime"`
Users int `json:"user-wlan-num_sta"`
Sysstats struct { Sysstats struct {
CPU string `json:"cpu"` CPU string `json:"cpu"`
Memory string `json:"mem"` Memory string `json:"mem"`
} `json:"system-stats"` } `json:"system-stats"`
} }
type ucAPIData struct { type UnifiAPIData struct {
user string user string
pass string pass string
baseURL string baseURL string
client *http.Client client *http.Client
} }
type mvDevices struct {
Nodes []struct {
Firstseen string `json:"firstseen"`
Lastseen string `json:"lastseen"`
IsOnline bool `json:"is_online"`
IsGateway bool `json:"is_gateway"`
Clients int `json:"clients"`
ClientsWifi24 int `json:"clients_wifi24"`
ClientsWifi5 int `json:"clients_wifi5"`
ClientsOther int `json:"clients_other"`
RootfsUsage float64 `json:"rootfs_usage"`
Loadavg float64 `json:"loadavg"`
MemoryUsage float64 `json:"memory_usage"`
Uptime string `json:"uptime"`
GatewayNexthop string `json:"gateway_nexthop,omitempty"`
Gateway string `json:"gateway,omitempty"`
NodeID string `json:"node_id"`
Mac string `json:"mac"`
Addresses []string `json:"addresses"`
Domain string `json:"domain"`
Hostname string `json:"hostname"`
Owner string `json:"owner,omitempty"`
Location struct {
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
} `json:"location,omitempty"`
Firmware struct {
Base string `json:"base"`
Release string `json:"release"`
} `json:"firmware"`
Autoupdater struct {
Enabled bool `json:"enabled"`
Branch string `json:"branch"`
} `json:"autoupdater,omitempty"`
Nproc int `json:"nproc"`
Model string `json:"model,omitempty"`
} `json:"nodes"`
Links []struct {
Type string `json:"type"`
Source string `json:"source"`
Target string `json:"target"`
SourceTq float64 `json:"source_tq"`
TargetTq float64 `json:"target_tq"`
SourceAddr string `json:"source_addr"`
TargetAddr string `json:"target_addr"`
} `json:"links"`
}
// switch Unifi AP Mod IDs to Names
func lookupModels(model string) string {
switch model {
case "BZ2", "U2S48", "U2Sv2":
return "Unifi AP"
case "BZ2LR", "U2L48", "U2Lv2":
return "UniFi AP-LR"
case "U7E", "U7Ev2":
return "UniFi AP-AC"
case "U7HD", "U7SHD":
return "UniFi AP-HD"
case "UXSDM":
return "UniFi AP-BaseStationXG"
case "UCMSH":
return "AP-MeshXG"
case "U7MP":
return "AP-AC-Mesh-Pro"
case "U7LR":
return "UniFi AP-AC-LR"
case "U7LT":
return "UniFi AP-AC-Lite"
case "U7P":
return "UniFi AP-Pro"
case "U7MSH":
return "UniFi AP-AC-Mesh"
case "U7PG2":
return "UniFi AP-AC-Pro"
default:
return "Unifi Gerät"
}
}
type UNMSLogResponse struct {
Items []struct {
ID string `json:"id"`
StartTime time.Time `json:"startTimestamp"`
EndTime time.Time `json:"endTimestamp"`
Type string `json:"type"`
Site struct {
Name string `json:"name"`
} `json:"site"`
Device struct {
ModelName string `json:"modelName"`
Name string `json:"name"`
DisplayName string `json:"displayName"`
} `json:"device"`
} `json:"items"`
}
type XY struct {
X int `json:"x"`
Y int `json:"y"`
}
type AvgMax struct {
AVG []XY `json:"avg"`
MAX []XY `json:"max"`
}
type UNMSstatistics struct {
Period int `json:"period"`
Interval struct {
Start int `json:"start"`
End int `json:"end"`
} `json:"interval"`
CPU AvgMax `json:"cpu"`
RAM AvgMax `json:"ram"`
Errors AvgMax `json:"errors"`
Interfaces []struct {
ID string `json:"id"`
Priority int `json:"priority"`
Name string `json:"name"`
Receive AvgMax `json:"receive"`
Transmit AvgMax `json:"transmit"`
} `json:"interfaces"`
}
type UNMSdhcp []struct {
Address string `json:"address"`
Hostname string `json:"hostname"`
Type string `json:"type"`
}

View File

@ -1,97 +0,0 @@
{
"devices":[
{
"name": "AM-01",
"mac": "18:e8:29:9c:90:ae",
"gateway_nexthop": "18e82924170a",
"gateway": "a28cae6ff604",
"linked_to": "18:e8:29:24:17:0a",
"location": {
"longitude":7.122931927,
"latitude":50.818885422
}
},
{
"name": "AM-02",
"mac": "18:e8:29:9c:96:ba",
"gateway_nexthop": "18e82924170a",
"gateway": "a28cae6ff604",
"location": {
"longitude":7.122777700,
"latitude":50.819022680
}
},
{
"name": "AM-03",
"mac": "18:e8:29:9c:9f:5c",
"gateway_nexthop": "18e82924170a",
"gateway": "a28cae6ff604",
"location": {
"longitude":7.122780383,
"latitude":50.819123506
}
},
{
"name": "Bahnhof Troisdorf - Ecke Bahnhof",
"mac": "e0:63:da:b0:92:99",
"gateway_nexthop": "18e829dcc37e",
"gateway": "a28cae6ff604",
"location": {
"longitude":7.150242180,
"latitude":50.814274325
}
},
{
"name": "Bahnhof Troisdorf - Ecke Straße",
"mac": "74:83:c2:36:d7:ea",
"gateway_nexthop": "18e829dcc37e",
"gateway": "a28cae6ff604",
"location": {
"longitude":7.150479555,
"latitude":50.814447186
}
},
{
"name": "Polizei - MeshPro Ost",
"mac": "78:8a:20:b0:8c:88",
"gateway_nexthop": "18e8295c534c",
"gateway": "a28cae6ff604",
"location": {
"longitude":7.153409868,
"latitude":50.813643886
}
},
{
"name": "Rathaus-Nord",
"mac": "74:83:c2:96:56:c5",
"gateway_nexthop": "18e8292f7de6",
"linked_to": "18:e8:29:2f:7d:e6",
"gateway": "a28cae6ff604",
"location": {
"longitude":7.147794664,
"latitude":50.817631439
}
},
{
"name": "Rathaus-Süd",
"mac": "74:83:c2:96:55:8d",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"location": {
"longitude":7.148724049,
"latitude":50.817368776
}
},
{
"name": "Am-Krausacker-2",
"mac": "18:e8:29:a0:6f:23",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"domain": "unifi",
"location": {
"longitude":7.148406208,
"latitude":50.817093402
}
}
]
}

284
unifi.go Normal file
View File

@ -0,0 +1,284 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"time"
client "github.com/influxdata/influxdb1-client/v2"
)
// Unifi Controller API processing
func processUnifiAPI(s int) ([]node, []link, error) {
//get list of Unifi devices to display
var nodes []node
var links []link
d := getDevices(conf.Unifi[s].UCDevicesURL)
//call Unifi Controller
ucAPI := UnifiNewAPI(conf.Unifi[s].User, conf.Unifi[s].Password, conf.Unifi[s].APIURL)
//login
if err := ucAPI.ucLogin(); err != nil {
return nil, nil, err
}
//get all Sites from Controller
sites, err := ucAPI.ucGetSites()
if err != nil {
return nil, nil, err
}
//get all devices in all sites
devices, err := ucAPI.ucGetDevices(sites)
if err != nil {
return nil, nil, err
}
//build nodes struct
//mvJson, err := getMeshviewerJSON()
for _, jsonDevice := range d.Devices {
var currentDevice ucDevice
var currentJSONDevice device
for _, device := range devices {
if strings.EqualFold(device.Mac, jsonDevice.MAC) {
currentDevice = device
currentJSONDevice = jsonDevice
}
}
if isRemoteMACpublished(jsonDevice.MAC, d.Devices) {
//hier muss gecheckt werden ob der link valide ist
if checkMeshviewerLink(jsonDevice.LinkedTo) {
links = UnifiAddLink(jsonDevice, links)
}
}
isOnline := currentDevice.State == 1
var load float64
var mem float64
var cpu float64
if isOnline {
load, err = strconv.ParseFloat(currentDevice.Sysstats.CPU, 64)
cpu = load * 100
if err != nil {
log.Println("Error psrsing CPU of device ", currentDevice.Name)
log.Println(err)
load = 0
cpu = 0
}
mem, err = strconv.ParseFloat(currentDevice.Sysstats.Memory, 64)
if err != nil {
log.Println("Error parsing Memory of device ", currentDevice.Name)
log.Println(err)
mem = 0
}
}
var model = lookupModels(currentDevice.Model)
var clients int
if conf.Unifi[s].DisplayUsers {
clients = currentDevice.Users
}
//// INFLUX START
// fields := map[string]interface{}{}
fields := make(map[string]any)
tags := map[string]string{
"hostname": strings.ReplaceAll(currentDevice.Name, " ", "-"),
"nodeid": strings.ReplaceAll(currentDevice.Mac, ":", ""),
}
// Generate fields for all network interfaces (not availible for Unifi Nodes)
//for eth := range details.Interfaces {
// interface_name_rx := ("rate.rx" + "_" + details.Interfaces[eth].Identification.Name)
// interface_name_tx := ("rate.tx" + "_" + details.Interfaces[eth].Identification.Name)
// fields[interface_name_rx] = details.Interfaces[eth].Statistics.Rxrate
// fields[interface_name_tx] = details.Interfaces[eth].Statistics.Txrate
//}
// set default values if we can't get statistics
fields["cpu"] = 0
fields["load"] = float64(0)
fields["ram"] = 0
if isOnline {
// Generate fields for all Statistics
//load := (float64(load) / float64(100))
fields["cpu"] = int(cpu)
fields["load"] = load
fields["ram"] = int(mem)
}
// Generate field for DHCP Leases
fields["clients.total"] = clients
fields["time.up"] = currentDevice.Uptime
// Generate Dataponts
point, err := client.NewPoint(
"node",
tags,
fields,
time.Now(),
)
if err != nil {
log.Fatalln("Error: ", err)
}
if conf.General.InfluxEnabled {
sendInfluxBatchDataPoint(point, conf.General.FreifunkInfluxPort)
}
// INFLUX STOP
//log.Println(currentDevice.Mac)
if currentDevice.Mac != "" {
nodes = append(nodes, node{
Firstseen: "0",
Lastseen: time.Unix(int64(currentDevice.LastSeen), 0).Format(iso8601),
IsOnline: itob(currentDevice.State),
IsGateway: false,
Clients: clients,
ClientsWifi24: 0,
ClientsWifi5: 0,
ClientsOther: clients,
RootFSUsage: 0,
LoadAVG: load / 100,
MemoryUsage: mem / 100,
Uptime: time.Now().Add(-1 * time.Second * time.Duration(currentDevice.Uptime)).Format(iso8601),
GatewayNexthop: currentJSONDevice.GatewayNexthop,
Gateway: currentJSONDevice.Gateway,
Location: &currentJSONDevice.Location,
NodeID: strings.ReplaceAll(currentDevice.Mac, ":", ""),
MAC: currentDevice.Mac,
Adresses: []string{currentDevice.IP},
Domain: currentJSONDevice.Domain,
Hostname: "[Unifi] " + currentDevice.Name,
Owner: "Freifunk Rhein-Sieg",
Firmware: firmware{
Base: "Ubiquiti - Stock",
Release: currentDevice.Version,
},
Autoupdater: autoupdater{
Enabled: false,
Branch: "stable",
},
NProc: 1,
Model: model,
})
}
}
return nodes, links, err
}
func UnifiNewAPI(user string, pass string, baseURL string) UnifiAPIData {
return UnifiAPIData{
user: user,
pass: pass,
baseURL: baseURL,
client: httpClient(),
}
}
func (u *UnifiAPIData) ucCallAPI(url string, method string, body *bytes.Buffer, output interface{}) error {
req, err := http.NewRequest(method, u.baseURL+url, body)
if err != nil {
return fmt.Errorf("can't set request %s", u.baseURL+url)
}
req.Header.Set("Content-Type", "application/json")
response, err := u.client.Do(req)
if err != nil {
return fmt.Errorf("can't login %s", u.baseURL+url)
}
defer response.Body.Close()
if response.StatusCode != 200 {
return fmt.Errorf("login failed %s", u.baseURL+url)
}
data, err := io.ReadAll(response.Body)
if err != nil {
return err
}
err = json.Unmarshal(data, &output)
if err != nil {
return err
}
return nil
}
func (u *UnifiAPIData) ucLogin() error {
var loginData = []byte(`{"username":"` + u.user + `","password":"` + u.pass + `"}`)
url := "/api/login"
err := u.ucCallAPI(url, http.MethodPost, bytes.NewBuffer(loginData), nil)
if err != nil {
return err
}
return nil
}
func (u *UnifiAPIData) ucGetSites() ([]ucSite, error) {
var d struct {
Data []ucSite `json:"data"`
}
url := "/api/self/sites"
err := u.ucCallAPI(url, http.MethodGet, bytes.NewBuffer([]byte{}), &d)
if err != nil {
return []ucSite{}, err
}
return d.Data, nil
}
func (u *UnifiAPIData) ucGetDevices(sites []ucSite) ([]ucDevice, error) {
var d struct {
Data []ucDevice `json:"data"`
}
var s []ucDevice
for _, site := range sites {
url := "/api/s/" + site.ID + "/stat/device"
u.ucCallAPI(url, http.MethodGet, bytes.NewBuffer([]byte{}), &d)
s = append(s, d.Data...)
}
return s, nil
}
func UnifiAddLink(dev device, links []link) []link {
for i := range links {
if links[i].SourceAddr == dev.MAC {
// link already exists
return links
}
}
if dev.LinkedTo == "" {
//no LinkedTo in ucDevices.json
return links
}
links = append(links, link{
Type: "cable",
Source: strings.ReplaceAll(dev.MAC, ":", ""),
Target: strings.ReplaceAll(dev.GatewayNexthop, ":", ""),
SourceTQ: 1,
TargetTQ: 1,
SourceAddr: dev.MAC,
TargetAddr: dev.LinkedTo,
})
return links
}
func findNodeID(NodeID string) bool {
for s := range conf.Unifi {
ucDev := getDevices(conf.Unifi[s].UCDevicesURL)
for i := range ucDev.Devices {
if ucDev.Devices[i].GatewayNexthop == NodeID {
return true
}
}
}
return false
}

287
unms.go Normal file
View File

@ -0,0 +1,287 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
_ "github.com/fatih/structs"
//_ "github.com/influxdata/influxdb1-client" // this is important because of the bug in go mod
client "github.com/influxdata/influxdb1-client/v2"
)
// UNMS API processing (Richtfunk)
func processUISPRiFu() ([]node, []link, error) {
// Variables for runtime
var links []link
var nodes []node
d := getDevices(conf.UISP.DevicesURL)
// API CALL 1 (get Device overview)
log.Println("Starting UISP API Crawler for Rifu devices")
log.Println("Getting device overview from UNMS API")
var u []unifiAPIResponse
if err := UnmsCallAPI("/devices", &u); err != nil {
return nil, nil, err
}
for i := range d.Devices {
time.Sleep(time.Second)
var dev unifiAPIResponse
var currentDevice device
for j := range u {
if strings.ToUpper(u[j].Identification.MAC) == strings.ToUpper(d.Devices[i].MAC) {
dev = u[j]
currentDevice = d.Devices[i]
}
}
isOnline := dev.Overview.Status == "active"
// END OF API CALL 1
// Getting details from UISP
log.Println("Getting device details for: ", d.Devices[i].Name)
var details unifiAPIDetails
if err := UnmsCallAPI("/devices/erouters/"+dev.Identification.ID, &details); err != nil {
return nil, nil, err
}
// Getting details for RiFu
log.Println("Getting details for RiFu Link for: ", d.Devices[i].Name)
var airmaxes []unifiAPIAirmax
if err := UnmsCallAPI("/devices/airmaxes/"+dev.Identification.ID+"/stations", &airmaxes); err != nil {
return nil, nil, err
}
// check if remote mac address is part of our published network
for i := range airmaxes {
if isRemoteMACpublished(airmaxes[i].DeviceIdentification.MAC, d.Devices) {
links = UnmsAddLink(dev, airmaxes[i], links)
}
}
// END OF API CALL 3
// Get info from json file (static)
nodes = append(nodes, node{
Firstseen: dev.Overview.CreatedAt.Format(iso8601),
Lastseen: dev.Overview.LastSeen.Format(iso8601),
IsOnline: isOnline,
IsGateway: false,
Clients: 0,
ClientsWifi24: 0,
ClientsWifi5: 0,
ClientsOther: 0,
RootFSUsage: 0,
LoadAVG: details.Overview.CPU / 100,
MemoryUsage: details.Overview.RAM / 100,
Uptime: dev.Identification.Started.Format(iso8601),
GatewayNexthop: currentDevice.GatewayNexthop,
Gateway: currentDevice.Gateway,
Location: &currentDevice.Location,
NodeID: strings.ReplaceAll(dev.Identification.MAC, ":", ""),
MAC: dev.Identification.MAC,
Adresses: UnmsGetAddresses(details.IPAddress),
Domain: currentDevice.Domain,
Hostname: "[RiFu] " + details.Identification.Name,
Owner: "Freifunk Rhein-Sieg",
Firmware: firmware{
Base: "Ubiquiti - Stock",
Release: details.Firmware.Current,
},
Autoupdater: autoupdater{
Enabled: false,
Branch: "stable",
},
NProc: 1,
Model: details.Identification.Model,
})
}
return nodes, links, nil
}
func processUISPRouter() ([]node, error) {
time.Sleep(time.Second)
// Variables for runtime
var nodes []node
d := getDevices(conf.UISP.RouterURL)
// API CALL 1, get all devices list from UNMS
log.Println("Get all Routers from UISP")
var u []unifiAPIResponse
if err := UnmsCallAPI("/devices", &u); err != nil {
return nil, err
}
// Get Information for devices device
for i := range d.Devices {
var dev unifiAPIResponse
var currentDevice device
for j := range u {
if strings.ToUpper(u[j].Identification.MAC) == strings.ToUpper(d.Devices[i].MAC) {
dev = u[j]
currentDevice = d.Devices[i]
}
}
isOnline := dev.Overview.Status == "active"
// API CALL FOR ROUTER DETAILS (Interface RX/TX)
log.Println("Getting details of ", d.Devices[i].Name, "from UISP API")
var details unifiAPIDetails
if err := UnmsCallAPI("/devices/erouters/"+dev.Identification.ID, &details); err != nil {
return nil, err
}
// API CALL FOR DEVICE STATISTICS (CPU, RAM)
log.Println("Getting statistics of ", d.Devices[i].Name, "from UISP API")
var statistics UNMSstatistics
if err := UnmsCallAPI("/devices/"+dev.Identification.ID+"/statistics?interval=hour", &statistics); err != nil {
return nil, err
}
// API CALL FOR DHCP LEASES
log.Println("Getting DHCP Leases of ", d.Devices[i].Name, "from UNMS API")
var dhcpleases UNMSdhcp
if isOnline {
if err := UnmsCallAPI("/devices/erouters/"+dev.Identification.ID+"/dhcp/leases", &dhcpleases); err != nil {
return nil, err
}
} else {
log.Println("Router ist offline, skipping DHCP Leases")
}
// fields := map[string]interface{}{}
fields := make(map[string]any)
tags := map[string]string{
"hostname": strings.ReplaceAll(d.Devices[i].Name, " ", "-"),
"nodeid": strings.ReplaceAll(dev.Identification.MAC, ":", ""),
}
// Generate fields for all network interfaces
for eth := range details.Interfaces {
interface_name_rx := ("rate.rx" + "_" + details.Interfaces[eth].Identification.Name)
interface_name_tx := ("rate.tx" + "_" + details.Interfaces[eth].Identification.Name)
fields[interface_name_rx] = details.Interfaces[eth].Statistics.Rxrate
fields[interface_name_tx] = details.Interfaces[eth].Statistics.Txrate
}
// set default values if we can't get statistics
fields["cpu"] = 0
fields["load"] = float64(0)
fields["ram"] = 0
if isOnline {
// Generate fields for all Statistics
load := (float64(statistics.CPU.AVG[0].Y) / float64(100))
fields["cpu"] = statistics.CPU.AVG[0].Y
fields["load"] = load
fields["ram"] = statistics.RAM.AVG[0].Y
}
// Generate field for DHCP Leases
fields["clients.total"] = len(dhcpleases)
// Generate Dataponts
point, err := client.NewPoint(
"node",
tags,
fields,
time.Now(),
)
if err != nil {
log.Fatalln("Error: ", err)
}
if conf.General.InfluxEnabled {
sendInfluxBatchDataPoint(point, conf.General.FreifunkInfluxPort)
}
// Get info from json file (static)
nodes = append(nodes, node{
Firstseen: dev.Overview.CreatedAt.Format(iso8601),
Lastseen: dev.Overview.LastSeen.Format(iso8601),
IsOnline: isOnline,
IsGateway: false,
Clients: 0,
ClientsWifi24: 0,
ClientsWifi5: 0,
ClientsOther: 0,
RootFSUsage: 0,
LoadAVG: details.Overview.CPU / 100,
MemoryUsage: details.Overview.RAM / 100,
Uptime: dev.Identification.Started.Format(iso8601),
GatewayNexthop: currentDevice.GatewayNexthop,
Gateway: currentDevice.Gateway,
Location: &currentDevice.Location,
NodeID: strings.ReplaceAll(dev.Identification.MAC, ":", ""),
MAC: dev.Identification.MAC,
Adresses: UnmsGetAddresses(details.IPAddress),
Domain: currentDevice.Domain,
Hostname: "[VPN-Router] " + details.Identification.Name,
Owner: "Freifunk Rhein-Sieg",
Firmware: firmware{
Base: "Ubiquiti - Stock",
Release: details.Firmware.Current,
},
Autoupdater: autoupdater{
Enabled: false,
Branch: "stable",
},
NProc: 1,
Model: details.Identification.Model,
})
}
return nodes, nil
}
func UnmsCallAPI(url string, i any) error {
time.Sleep(time.Second)
request, err := http.NewRequest(http.MethodGet, conf.UISP.UnmsAPIURL+url, nil)
if err != nil {
return errors.New(fmt.Sprint("can't set request", conf.UISP.UnmsAPIURL+url))
}
//log.Println(conf.UISP.UnmsAPIURL + url)
request.Header.Set("x-auth-token", conf.UISP.APItoken)
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return fmt.Errorf("can't get request %s with x-auth-token %s", conf.UISP.UnmsAPIURL+url, conf.UISP.APItoken)
}
if response.StatusCode != 200 {
log.Println("Can't call UNMS API, check token and URL. Skipping device. HTTP Status: ", response.StatusCode)
return nil
}
data, err := ioutil.ReadAll(response.Body)
defer response.Body.Close()
if err != nil {
return fmt.Errorf("can't read response body: %+v", response.Body)
}
// no error occurred, unmarshal to struct
return json.Unmarshal(data, &i)
}
func UnmsGetAddresses(ip string) []string {
var adresses []string
adresses = append(adresses, strings.Split(ip, "/")[0])
return adresses
}
func UnmsAddLink(dev unifiAPIResponse, airmaxes unifiAPIAirmax, links []link) []link {
for i := range links {
if links[i].SourceAddr == airmaxes.DeviceIdentification.MAC {
// link already exists
return links
}
}
links = append(links, link{
Type: "wifi",
Source: strings.ReplaceAll(dev.Identification.MAC, ":", ""),
Target: strings.ReplaceAll(airmaxes.DeviceIdentification.MAC, ":", ""),
SourceTQ: airmaxes.Statistics.LinkScore,
TargetTQ: airmaxes.Statistics.LinkScore,
SourceAddr: dev.Identification.MAC,
TargetAddr: airmaxes.DeviceIdentification.MAC,
})
return links
}