Compare commits

..

20 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
13 changed files with 462 additions and 275 deletions

View File

@ -1,53 +0,0 @@
local pipeline(os, arch) = {
kind: "pipeline",
name: os + "/" + arch,
platform: {
"os": os,
"arch": arch,
},
steps: [{
name: "compile " + os + "/" + arch,
image: "golang:1.20.3-alpine3.17",
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 gitea container registry",
image: "plugins/docker:latest",
settings: {
repo: "git.freifunk-rhein-sieg.net/freifunk-troisdorf/ubnt-freifunk-map-api",
registry: "git.freifunk-rhein-sieg.net",
username: { "from_secret": "docker_username" },
password: { "from_secret": "docker_password" },
tags: ["latest"],
auto_tag: true,
},
when: {
event: "tag"
},
},
],
};
[
pipeline("linux", "amd64")
]

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,7 +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
RUN chmod +x /opt/ubnt-freifunk-map-api RUN chmod +x /opt/ubnt-freifunk-map-api
EXPOSE 3000 EXPOSE 3000

View File

@ -1,12 +1,17 @@
# Freifunk Meshviewer Unifi Access Points und Richtfunkstrecken import # Freifunk Meshviewer Unifi Access Points und Richtfunkstrecken import
Dieses tool Importiert Nodes für die Freifunk Map aus den APIs UNMS (Richtfunk) & Unifi (Access Points) 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 ## Config
### Unifi Access Points (ucDevices.json) ### Unifi Access Points (unifi_devices.json)
In der Datei ucDevices.json können die Access Points gepflegt werden, die auf der Freifunk Map erscheinen sollen. 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. Hierzu muss die Datei im json Format erweitert werden.
@ -32,9 +37,9 @@ Erklärung:
* 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. * 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) * domain: Die Domain in der sich der AP befindet. (tdf, inn, flu)
### UNMS Richtfunkstrecken ### UISP Richtfunkstrecken
In der Datei devices.json können die Richtfunkstrecken gepflegt werden, die auf der Freifunk Map erscheinen sollen. In der Datei rifu_devices.json können die Richtfunkstrecken gepflegt werden, die auf der Freifunk Map erscheinen sollen.
```json ```json
{ {
@ -56,11 +61,44 @@ Erklärung:
* gateway: Im Normalfall die NodeID des Supernodes (zu finden in der MAP) * gateway: Im Normalfall die NodeID des Supernodes (zu finden in der MAP)
* domain: Die Domain in der sich der AP befindet. (tdf, inn, flu) * 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 ### Config.json
Es gibt 3 Module die Ein/Ausgeschatet werden können: Es gibt 3 Module die Ein/Ausgeschatet werden können:
* UNMS * UNMS
* Unifi * Unifi
* Meshviewer * 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. 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.

View File

@ -5,14 +5,17 @@
"APItoken": "UNMS API TOKEN", "APItoken": "UNMS API TOKEN",
"devicesURL": "https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/example.devices.json" "devicesURL": "https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/example.devices.json"
}, },
"unifi": { "unifi": [
{
"name": "Unifi Freifunk Troisdorf",
"enabled": false, "enabled": false,
"displayusers": true, "displayusers": true,
"APIUrl": "https://unifi.freifunk-troisdorf.de", "APIUrl": "https://unifi.freifunk-troisdorf.de",
"user": "APIuser", "user": "APIuser",
"password": "PASSWORD", "password": "PASSWORD",
"ucDevicesURL": "https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/example.ucDevices.json" "ucDevicesURL": "https://git.freifunk-rhein-sieg.net/Freifunk-Troisdorf/ubnt-freifunk-map-api/raw/branch/master/example.ucDevices.json"
}, }
],
"meshviewer": { "meshviewer": {
"enabled": false, "enabled": false,
"files": [ "files": [

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)
}
//
}

95
main.go
View File

@ -9,21 +9,28 @@ import (
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"os" "os"
"sync"
"time" "time"
_ "git.nils.zone/nils/prettify" _ "git.nils.zone/nils/prettify"
) )
const ( const (
iso8601 = "2006-01-02T15:04:05-0700" iso8601 = "2006-01-02T15:04:05-0700"
fetchInterval = 1 * time.Minute
) )
// flags // flags
var (
lastFetchTime time.Time
cacheMutex sync.Mutex
cacheNodes []node
cacheLinks []link
)
var configPath = flag.String("configPath", "config.json", "Path to config.json") 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) var conf = loadconfig(*configPath)
var ucDev = getDevices(conf.Unifi.UCDevicesURL)
func main() { func main() {
log.Printf("starting version %s...\n", version) log.Printf("starting version %s...\n", version)
@ -38,56 +45,52 @@ func main() {
// start API processing (runs in a loop) // start API processing (runs in a loop)
go func() { go func() {
if err := processAPIs(); err != nil { if err := processAPIs(); err != nil {
log.Fatalln("API processing failed, error is", err) log.Fatalln("API processing failed, error is: ", err)
} }
tick := time.Tick(delay) tick := time.Tick(delay)
for range tick { for range tick {
if err := processAPIs(); err != nil { if err := processAPIs(); err != nil {
log.Fatalln("API processing failed, error is", err) log.Fatalln("API processing failed, error is: ", err)
} }
} }
}() }()
//processUNMSAPIRouter()
//createMetrics(influxDBClient())
// start webserver on Port 3000 // start webserver on Port 3000
serveJSON() serveJSON()
} }
func loadconfig(file string) config {
var config config
configFile, err := os.Open(file)
if err != nil {
log.Fatalln(err)
}
jsonParse := json.NewDecoder(configFile)
jsonParse.Decode(&config)
return config
}
// int to bool converter
func itob(i int) bool {
return i == 1
}
func processAPIs() error { func processAPIs() error {
var nodes []node var nodes []node
var links []link var links []link
if conf.Unms.Enabled { if conf.UISP.Enabled {
log.Println("Processing UNMS") log.Println("Processing UISP")
unmsNodes, unmsLinks := processUNMSAPI() //Process UISP RiFu Nodes
unmsRouters, err := processUNMSAPIRouter() uispNodes, uispLinks, err := processUISPRiFu()
if err != nil { if err != nil {
return err return err
} }
nodes = append(nodes, unmsNodes...) //Process UISP Routers (like EDGE Router)
nodes = append(nodes, unmsRouters...) uispRouters, err := processUISPRouter()
links = append(links, unmsLinks...) if err != nil {
return err
}
nodes = append(nodes, uispNodes...)
nodes = append(nodes, uispRouters...)
links = append(links, uispLinks...)
} }
if conf.Unifi.Enabled { if len(conf.Unifi) > 0 {
log.Println("Processing Unifi") log.Println("Anazahl der Unifi Server:", len(conf.Unifi))
ucNodes, _ := processUcAPIs() for i := range conf.Unifi {
nodes = append(nodes, ucNodes...) 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 { if conf.Meshviewer.Enabled {
log.Println("Processing Meshviewer") log.Println("Processing Meshviewer")
@ -97,6 +100,7 @@ func processAPIs() error {
} }
if conf.Gateways.Enabled { if conf.Gateways.Enabled {
log.Println("Processing Gateways") log.Println("Processing Gateways")
//Process Static Gateways from Json
gwNodes := processGateways() gwNodes := processGateways()
nodes = append(nodes, gwNodes...) nodes = append(nodes, gwNodes...)
} }
@ -113,17 +117,30 @@ func processAPIs() error {
if err := o.writeToFile(); err != nil { if err := o.writeToFile(); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
// get outages to serve as .csv
l := getUNMSLogs()
err := writeOutagesToCSV(l)
if err != nil {
log.Println("Error writing outages.csv")
}
// we're done here // we're done here
log.Println("...done") log.Println("...done")
return nil 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 // 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)

View File

@ -3,6 +3,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"log" "log"
"time"
) )
func getMeshviewerJSON(url string) (mvDevices, error) { func getMeshviewerJSON(url string) (mvDevices, error) {
@ -129,18 +130,31 @@ func addmvDevices(d mvDevices) ([]node, []link) {
} }
func getMeshviewer() ([]node, []link) { 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 nodes []node
var links []link var links []link
for i := range conf.Meshviewer.Files { for i := range conf.Meshviewer.Files {
log.Println("Hole Meshviewer JSON von: ", conf.Meshviewer.Files[i].URL)
m, err := getMeshviewerJSON(conf.Meshviewer.Files[i].URL) m, err := getMeshviewerJSON(conf.Meshviewer.Files[i].URL)
if err != nil { if err != nil {
return nodes, links return cacheNodes, cacheLinks
} }
mvNodes, mvLinks := addmvDevices(m) mvNodes, mvLinks := addmvDevices(m)
nodes = append(nodes, mvNodes...) nodes = append(nodes, mvNodes...)
links = append(links, mvLinks...) links = append(links, mvLinks...)
} }
return nodes, links
// Cache aktualisieren
cacheNodes = nodes
cacheLinks = links
lastFetchTime = time.Now()
return cacheNodes, cacheLinks
} }

View File

@ -1,37 +0,0 @@
package main
import (
"encoding/csv"
"log"
"os"
)
func getUNMSLogs() UNMSLogResponse {
var l UNMSLogResponse
log.Println("Get Outages from UNMS")
err := UnmsCallAPI("/outages?count=100&page=1&type=outage", &l)
if err != nil {
log.Fatalln("Error calling Outages API")
}
return l
}
func writeOutagesToCSV(l UNMSLogResponse) error {
csvFile, err := os.Create("output/outages.csv")
if err != nil {
return err
}
writer := csv.NewWriter(csvFile)
for _, o := range l.Items {
var row []string
row = append(row, o.StartTime.Format("02.01.2006 15:04:05"))
row = append(row, o.EndTime.Format("02.01.2006 15:04:05"))
row = append(row, o.Site.Name)
row = append(row, o.Device.DisplayName)
writer.Write(row)
}
writer.Flush()
return nil
}

View File

@ -4,15 +4,66 @@ import (
"log" "log"
"strings" "strings"
"time" "time"
client "github.com/influxdata/influxdb1-client/v2"
) )
func processGateways() []node { func processGateways() []node {
d := getDevices(conf.Gateways.GatewaysURL) d := getDevices(conf.Gateways.GatewaysURL)
var nodes []node var nodes []node
for i := range d.Devices { for i := range d.Devices {
log.Println("Processing Static Device: ", d.Devices[i].Name) 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{ nodes = append(nodes, node{
Firstseen: time.Now().Format(iso8601), Firstseen: up.Format(iso8601),
Lastseen: time.Now().Format(iso8601), Lastseen: time.Now().Format(iso8601),
IsOnline: true, IsOnline: true,
IsGateway: true, IsGateway: true,
@ -21,13 +72,14 @@ func processGateways() []node {
ClientsWifi5: 0, ClientsWifi5: 0,
ClientsOther: 0, ClientsOther: 0,
RootFSUsage: 0, RootFSUsage: 0,
LoadAVG: 0, LoadAVG: cpuMap,
MemoryUsage: 0, MemoryUsage: memoryMap,
Uptime: time.Now().Format(iso8601), Uptime: up.Format(iso8601),
GatewayNexthop: "00:00:00:00:00", GatewayNexthop: "",
Gateway: "00:00:00:00:00", Gateway: "",
NodeID: strings.ReplaceAll(d.Devices[i].MAC, ":", ""), NodeID: strings.ReplaceAll(d.Devices[i].MAC, ":", ""),
MAC: d.Devices[i].MAC, MAC: d.Devices[i].MAC,
Adresses: d.Devices[i].Adresses,
Domain: d.Devices[i].Domain, Domain: d.Devices[i].Domain,
Hostname: "[Gateway] " + d.Devices[i].Name, Hostname: "[Gateway] " + d.Devices[i].Name,
Owner: "Freifunk Troisdorf", Owner: "Freifunk Troisdorf",

View File

@ -10,21 +10,20 @@ import (
) )
type config struct { type config struct {
Unms 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"` Enabled bool `json:"enabled"`
UnmsAPIURL string `json:"unmsAPIUrl"` UnmsAPIURL string `json:"unmsAPIUrl"`
APItoken string `json:"APItoken"` APItoken string `json:"APItoken"`
DevicesURL string `json:"devicesURL"` DevicesURL string `json:"devicesURL"`
RouterURL string `json:"routerURL"` RouterURL string `json:"routerURL"`
} `json:"unms"` } `json:"unms"`
Unifi struct { Unifi []UnifiServer `json:"unifi"`
Enabled bool `json:"enabled"`
DisplayUsers bool `json:"displayusers"`
APIURL string `json:"APIUrl"`
User string `json:"user"`
Password string `json:"password"`
UCDevicesURL string `json:"ucDevicesURL"`
} `json:"unifi"`
Meshviewer struct { Meshviewer struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Files []struct { Files []struct {
@ -38,8 +37,19 @@ type config struct {
} `json:"gateways"` } `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"`
@ -49,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"`
@ -323,21 +334,26 @@ type XY struct {
Y int `json:"y"` Y int `json:"y"`
} }
type AvgMax struct {
AVG []XY `json:"avg"`
MAX []XY `json:"max"`
}
type UNMSstatistics struct { type UNMSstatistics struct {
Period int `json:"period"` Period int `json:"period"`
Interval struct { Interval struct {
Start int `json:"start"` Start int `json:"start"`
End int `json:"end"` End int `json:"end"`
} `json:"interval"` } `json:"interval"`
CPU []XY `json:"cpu"` CPU AvgMax `json:"cpu"`
RAM []XY `json:"ram"` RAM AvgMax `json:"ram"`
Errors []XY `json:"errors"` Errors AvgMax `json:"errors"`
Interfaces []struct { Interfaces []struct {
ID string `json:"id"` ID string `json:"id"`
Priority int `json:"priority"` Priority int `json:"priority"`
Name string `json:"name"` Name string `json:"name"`
Receive []XY `json:"receive"` Receive AvgMax `json:"receive"`
Transmit []XY `json:"transmit"` Transmit AvgMax `json:"transmit"`
} `json:"interfaces"` } `json:"interfaces"`
} }

179
unifi.go
View File

@ -10,28 +10,32 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
client "github.com/influxdata/influxdb1-client/v2"
) )
// Unifi Controller API processing // Unifi Controller API processing
func processUcAPIs() ([]node, []link) { func processUnifiAPI(s int) ([]node, []link, error) {
//get list of Unifi devices to display //get list of Unifi devices to display
var nodes []node var nodes []node
var links []link var links []link
d := getDevices(conf.Unifi.UCDevicesURL) d := getDevices(conf.Unifi[s].UCDevicesURL)
//call Unifi Controller //call Unifi Controller
ucAPI := UnifiNewAPI(conf.Unifi.User, conf.Unifi.Password, conf.Unifi.APIURL) ucAPI := UnifiNewAPI(conf.Unifi[s].User, conf.Unifi[s].Password, conf.Unifi[s].APIURL)
//login //login
ucAPI.ucLogin() if err := ucAPI.ucLogin(); err != nil {
return nil, nil, err
}
//get all Sites from Controller //get all Sites from Controller
sites, err := ucAPI.ucGetSites() sites, err := ucAPI.ucGetSites()
if err != nil { if err != nil {
log.Println(err) return nil, nil, err
} }
//get all devices in all sites //get all devices in all sites
devices, err := ucAPI.ucGetDevices(sites) devices, err := ucAPI.ucGetDevices(sites)
if err != nil { if err != nil {
log.Println(err) return nil, nil, err
} }
//build nodes struct //build nodes struct
@ -45,63 +49,127 @@ func processUcAPIs() ([]node, []link) {
currentJSONDevice = jsonDevice currentJSONDevice = jsonDevice
} }
} }
if isRemoteMACpublished(jsonDevice.MAC, d.Devices) { if isRemoteMACpublished(jsonDevice.MAC, d.Devices) {
//hier muss gecheckt werden ob der link valide ist //hier muss gecheckt werden ob der link valide ist
if checkMeshviewerLink(jsonDevice.LinkedTo) { if checkMeshviewerLink(jsonDevice.LinkedTo) {
links = UnifiAddLink(jsonDevice, links) links = UnifiAddLink(jsonDevice, links)
} }
} }
load, err := strconv.ParseFloat(currentDevice.Sysstats.CPU, 64)
if err != nil { isOnline := currentDevice.State == 1
fmt.Println("Error: ", currentDevice.Name) var load float64
//log.Fatalln(err) var mem float64
load = 0 var cpu float64
} if isOnline {
mem, err := strconv.ParseFloat(currentDevice.Sysstats.Memory, 64) load, err = strconv.ParseFloat(currentDevice.Sysstats.CPU, 64)
if err != nil { cpu = load * 100
//log.Fatalln(err) if err != nil {
load = 0 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 model = lookupModels(currentDevice.Model)
var clients int var clients int
if conf.Unifi.DisplayUsers { if conf.Unifi[s].DisplayUsers {
clients = currentDevice.Users clients = currentDevice.Users
} }
nodes = append(nodes, node{
Firstseen: "0", //// INFLUX START
Lastseen: time.Unix(int64(currentDevice.LastSeen), 0).Format(iso8601), // fields := map[string]interface{}{}
IsOnline: itob(currentDevice.State), fields := make(map[string]any)
IsGateway: false, tags := map[string]string{
Clients: clients, "hostname": strings.ReplaceAll(currentDevice.Name, " ", "-"),
ClientsWifi24: 0, "nodeid": strings.ReplaceAll(currentDevice.Mac, ":", ""),
ClientsWifi5: 0, }
ClientsOther: clients, // Generate fields for all network interfaces (not availible for Unifi Nodes)
RootFSUsage: 0, //for eth := range details.Interfaces {
LoadAVG: load / 100, // interface_name_rx := ("rate.rx" + "_" + details.Interfaces[eth].Identification.Name)
MemoryUsage: mem / 100, // interface_name_tx := ("rate.tx" + "_" + details.Interfaces[eth].Identification.Name)
Uptime: time.Now().Add(-1 * time.Second * time.Duration(currentDevice.Uptime)).Format(iso8601), // fields[interface_name_rx] = details.Interfaces[eth].Statistics.Rxrate
GatewayNexthop: currentJSONDevice.GatewayNexthop, // fields[interface_name_tx] = details.Interfaces[eth].Statistics.Txrate
Gateway: currentJSONDevice.Gateway, //}
Location: &currentJSONDevice.Location,
NodeID: strings.ReplaceAll(currentDevice.Mac, ":", ""), // set default values if we can't get statistics
MAC: currentDevice.Mac, fields["cpu"] = 0
Adresses: []string{currentDevice.IP}, fields["load"] = float64(0)
Domain: currentJSONDevice.Domain, fields["ram"] = 0
Hostname: "[Unifi] " + currentDevice.Name,
Owner: "Freifunk Rhein-Sieg", if isOnline {
Firmware: firmware{ // Generate fields for all Statistics
Base: "Ubiquiti - Stock", //load := (float64(load) / float64(100))
Release: currentDevice.Version, fields["cpu"] = int(cpu)
}, fields["load"] = load
Autoupdater: autoupdater{ fields["ram"] = int(mem)
Enabled: false, }
Branch: "stable",
}, // Generate field for DHCP Leases
NProc: 1, fields["clients.total"] = clients
Model: model, 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 return nodes, links, err
} }
func UnifiNewAPI(user string, pass string, baseURL string) UnifiAPIData { func UnifiNewAPI(user string, pass string, baseURL string) UnifiAPIData {
@ -204,9 +272,12 @@ func UnifiAddLink(dev device, links []link) []link {
} }
func findNodeID(NodeID string) bool { func findNodeID(NodeID string) bool {
for i := range ucDev.Devices { for s := range conf.Unifi {
if ucDev.Devices[i].GatewayNexthop == NodeID { ucDev := getDevices(conf.Unifi[s].UCDevicesURL)
return true for i := range ucDev.Devices {
if ucDev.Devices[i].GatewayNexthop == NodeID {
return true
}
} }
} }
return false return false

100
unms.go
View File

@ -17,22 +17,23 @@ import (
) )
// UNMS API processing (Richtfunk) // UNMS API processing (Richtfunk)
func processUNMSAPI() ([]node, []link) { func processUISPRiFu() ([]node, []link, error) {
// Variables for runtime // Variables for runtime
var links []link var links []link
var nodes []node var nodes []node
d := getDevices(conf.Unms.DevicesURL) d := getDevices(conf.UISP.DevicesURL)
// API CALL 1 // API CALL 1 (get Device overview)
log.Println("calling API 1") log.Println("Starting UISP API Crawler for Rifu devices")
log.Println("Getting device overview from UNMS API")
var u []unifiAPIResponse var u []unifiAPIResponse
err := UnmsCallAPI("/devices", &u) if err := UnmsCallAPI("/devices", &u); err != nil {
if err != nil { return nil, nil, err
log.Fatalln(err)
} }
for i := range d.Devices { for i := range d.Devices {
time.Sleep(time.Second)
var dev unifiAPIResponse var dev unifiAPIResponse
var currentDevice device var currentDevice device
for j := range u { for j := range u {
@ -41,20 +42,22 @@ func processUNMSAPI() ([]node, []link) {
currentDevice = d.Devices[i] currentDevice = d.Devices[i]
} }
} }
isOnline := dev.Overview.Status == "active" isOnline := dev.Overview.Status == "active"
// END OF API CALL 1 // END OF API CALL 1
// API CALL 2 // Getting details from UISP
log.Println("calling API 2 for device", d.Devices[i].Name) log.Println("Getting device details for: ", d.Devices[i].Name)
var details unifiAPIDetails var details unifiAPIDetails
UnmsCallAPI("/devices/erouters/"+dev.Identification.ID, &details) if err := UnmsCallAPI("/devices/erouters/"+dev.Identification.ID, &details); err != nil {
// END OF API CALL 2 return nil, nil, err
}
// API CALL 3 // Getting details for RiFu
log.Println("calling API 3 for device", d.Devices[i].Name) log.Println("Getting details for RiFu Link for: ", d.Devices[i].Name)
var airmaxes []unifiAPIAirmax var airmaxes []unifiAPIAirmax
UnmsCallAPI("/devices/airmaxes/"+dev.Identification.ID+"/stations", &airmaxes) 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 // check if remote mac address is part of our published network
for i := range airmaxes { for i := range airmaxes {
if isRemoteMACpublished(airmaxes[i].DeviceIdentification.MAC, d.Devices) { if isRemoteMACpublished(airmaxes[i].DeviceIdentification.MAC, d.Devices) {
@ -98,23 +101,21 @@ func processUNMSAPI() ([]node, []link) {
Model: details.Identification.Model, Model: details.Identification.Model,
}) })
} }
return nodes, links return nodes, links, nil
} }
func processUNMSAPIRouter() ([]node, error) { func processUISPRouter() ([]node, error) {
time.Sleep(time.Second)
// Variables for runtime // Variables for runtime
var nodes []node var nodes []node
d := getDevices(conf.UISP.RouterURL)
d := getDevices(conf.Unms.RouterURL)
// API CALL 1, get all devices list from UNMS // API CALL 1, get all devices list from UNMS
log.Println("Get all devices from UNMS") log.Println("Get all Routers from UISP")
var u []unifiAPIResponse var u []unifiAPIResponse
err := UnmsCallAPI("/devices", &u) if err := UnmsCallAPI("/devices", &u); err != nil {
if err != nil {
return nil, err return nil, err
} }
// END OF API CALL 1
// Get Information for devices device // Get Information for devices device
for i := range d.Devices { for i := range d.Devices {
@ -130,19 +131,18 @@ func processUNMSAPIRouter() ([]node, error) {
isOnline := dev.Overview.Status == "active" isOnline := dev.Overview.Status == "active"
// API CALL FOR ROUTER DETAILS (Interface RX/TX) // API CALL FOR ROUTER DETAILS (Interface RX/TX)
log.Println("Getting details of ", d.Devices[i].Name, "from UNMS API") log.Println("Getting details of ", d.Devices[i].Name, "from UISP API")
var details unifiAPIDetails var details unifiAPIDetails
if err := UnmsCallAPI("/devices/erouters/"+dev.Identification.ID, &details); err != nil { if err := UnmsCallAPI("/devices/erouters/"+dev.Identification.ID, &details); err != nil {
return nil, err return nil, err
} }
// API CALL FOR DEVICE STATISTICS (CPU, RAM) // API CALL FOR DEVICE STATISTICS (CPU, RAM)
log.Println("Getting statistics of ", d.Devices[i].Name, "from UNMS API") log.Println("Getting statistics of ", d.Devices[i].Name, "from UISP API")
var statistics UNMSstatistics var statistics UNMSstatistics
if err := UnmsCallAPI("/devices/"+dev.Identification.ID+"/statistics?interval=hour", &statistics); err != nil { if err := UnmsCallAPI("/devices/"+dev.Identification.ID+"/statistics?interval=hour", &statistics); err != nil {
return nil, err return nil, err
} }
// API CALL FOR DHCP LEASES // API CALL FOR DHCP LEASES
log.Println("Getting DHCP Leases of ", d.Devices[i].Name, "from UNMS API") log.Println("Getting DHCP Leases of ", d.Devices[i].Name, "from UNMS API")
var dhcpleases UNMSdhcp var dhcpleases UNMSdhcp
@ -154,16 +154,6 @@ func processUNMSAPIRouter() ([]node, error) {
log.Println("Router ist offline, skipping DHCP Leases") log.Println("Router ist offline, skipping DHCP Leases")
} }
// Open connection to InfluxDB
bp, err := client.NewBatchPoints(client.BatchPointsConfig{
Database: "freifunk",
Precision: "s",
})
if err != nil {
log.Fatalln("Error: ", err)
}
//
// fields := map[string]interface{}{} // fields := map[string]interface{}{}
fields := make(map[string]any) fields := make(map[string]any)
tags := map[string]string{ tags := map[string]string{
@ -185,10 +175,10 @@ func processUNMSAPIRouter() ([]node, error) {
if isOnline { if isOnline {
// Generate fields for all Statistics // Generate fields for all Statistics
load := (float64(statistics.CPU[0].Y) / float64(100)) load := (float64(statistics.CPU.AVG[0].Y) / float64(100))
fields["cpu"] = statistics.CPU[0].Y fields["cpu"] = statistics.CPU.AVG[0].Y
fields["load"] = load fields["load"] = load
fields["ram"] = statistics.RAM[0].Y fields["ram"] = statistics.RAM.AVG[0].Y
} }
// Generate field for DHCP Leases // Generate field for DHCP Leases
@ -204,14 +194,9 @@ func processUNMSAPIRouter() ([]node, error) {
if err != nil { if err != nil {
log.Fatalln("Error: ", err) log.Fatalln("Error: ", err)
} }
// Add Datapoints in InfluxDB if conf.General.InfluxEnabled {
bp.AddPoint(point) sendInfluxBatchDataPoint(point, conf.General.FreifunkInfluxPort)
c := influxDBClient()
err = c.Write(bp)
if err != nil {
log.Fatal(err)
} }
// Get info from json file (static) // Get info from json file (static)
nodes = append(nodes, node{ nodes = append(nodes, node{
Firstseen: dev.Overview.CreatedAt.Format(iso8601), Firstseen: dev.Overview.CreatedAt.Format(iso8601),
@ -250,29 +235,22 @@ func processUNMSAPIRouter() ([]node, error) {
return nodes, nil return nodes, nil
} }
func influxDBClient() client.Client {
c, err := client.NewHTTPClient(client.HTTPConfig{
Addr: "http://statistik.freifunk-troisdorf.de:8886",
})
if err != nil {
log.Fatalln("Error: ", err)
}
return c
}
func UnmsCallAPI(url string, i any) error { func UnmsCallAPI(url string, i any) error {
request, err := http.NewRequest(http.MethodGet, conf.Unms.UnmsAPIURL+url, nil) time.Sleep(time.Second)
request, err := http.NewRequest(http.MethodGet, conf.UISP.UnmsAPIURL+url, nil)
if err != nil { if err != nil {
return errors.New(fmt.Sprint("can't set request", conf.Unms.UnmsAPIURL+url)) return errors.New(fmt.Sprint("can't set request", conf.UISP.UnmsAPIURL+url))
} }
request.Header.Set("x-auth-token", conf.Unms.APItoken) //log.Println(conf.UISP.UnmsAPIURL + url)
request.Header.Set("x-auth-token", conf.UISP.APItoken)
client := &http.Client{} client := &http.Client{}
response, err := client.Do(request) response, err := client.Do(request)
if err != nil { if err != nil {
return fmt.Errorf("can't get request %s with x-auth-token %s", conf.Unms.UnmsAPIURL+url, conf.Unms.APItoken) return fmt.Errorf("can't get request %s with x-auth-token %s", conf.UISP.UnmsAPIURL+url, conf.UISP.APItoken)
} }
if response.StatusCode != 200 { if response.StatusCode != 200 {
log.Fatalln("Can't call UNMS API, check token and URL. HTTP Status: ", response.StatusCode) 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) data, err := ioutil.ReadAll(response.Body)
defer response.Body.Close() defer response.Body.Close()