ubnt-freifunk-map-api/main.go

660 lines
16 KiB
Go
Raw Normal View History

2020-12-29 17:52:15 +00:00
package main
import (
2021-02-05 23:15:25 +00:00
"bytes"
2021-04-05 10:52:35 +00:00
"encoding/csv"
2020-12-29 17:52:15 +00:00
"encoding/json"
"errors"
2020-12-29 17:52:15 +00:00
"flag"
"fmt"
2020-12-29 17:52:15 +00:00
"io/ioutil"
"log"
"net/http"
2021-02-05 23:15:25 +00:00
"net/http/cookiejar"
2021-02-06 21:32:24 +00:00
"os"
2021-02-05 23:15:25 +00:00
"strconv"
2020-12-29 17:52:15 +00:00
"strings"
"time"
2021-02-10 18:31:31 +00:00
_ "git.nils.zone/nils/prettify"
2020-12-29 17:52:15 +00:00
)
2021-02-10 18:31:31 +00:00
const (
iso8601 = "2006-01-02T15:04:05-0700"
)
2020-12-29 17:52:15 +00:00
// flags
2021-02-06 21:32:24 +00:00
var configPath = flag.String("configPath", "config.json", "Path to config.json")
2021-01-02 11:32:38 +00:00
var version = "development"
2021-05-06 12:49:44 +00:00
var delay time.Duration = 60 * time.Second
2021-02-10 18:31:31 +00:00
var conf = loadconfig(*configPath)
2021-02-13 15:10:41 +00:00
var ucDev = getDevices(conf.Unifi.UCDevicesURL)
2020-12-29 17:52:15 +00:00
func main() {
log.Printf("starting version %s...\n", version)
2020-12-29 17:52:15 +00:00
// parse all flags
flag.Parse()
2021-02-06 10:46:08 +00:00
// check if flags are set
2021-02-13 15:10:41 +00:00
//if *configPath == "" {
// log.Fatalln("Please specify path to config.json flag '-configPath'")
//}
// start API processing (runs in a loop)
2021-02-10 18:31:31 +00:00
go processAPIs()
2020-12-29 17:52:15 +00:00
2021-02-06 10:46:08 +00:00
// start webserver on Port 3000
serveJSON()
}
2020-12-29 17:52:15 +00:00
func loadconfig(file string) config {
var config config
2021-02-06 21:32:24 +00:00
configFile, err := os.Open(file)
if err != nil {
log.Fatalln(err)
2021-02-05 23:15:25 +00:00
}
2021-02-06 21:32:24 +00:00
jsonParse := json.NewDecoder(configFile)
jsonParse.Decode(&config)
return config
2021-02-05 23:15:25 +00:00
}
2021-02-06 10:46:08 +00:00
//int to bool converter
2021-02-05 23:15:25 +00:00
func itob(i int) bool {
if i == 1 {
return true
}
return false
}
2020-12-29 17:52:15 +00:00
2021-02-06 10:46:08 +00:00
//Unifi Controller API processing
2021-02-10 18:31:31 +00:00
func processUcAPIs() ([]node, []link) {
2021-02-06 10:46:08 +00:00
//get list of Unifi devices to display
2021-02-05 23:15:25 +00:00
var nodes []node
2021-02-06 10:46:08 +00:00
var links []link
2021-02-13 15:10:41 +00:00
d := getDevices(conf.Unifi.UCDevicesURL)
2021-02-06 10:46:08 +00:00
//call Unifi Controller
2021-02-10 18:31:31 +00:00
ucAPI := newAPI(conf.Unifi.User, conf.Unifi.Password, conf.Unifi.APIURL)
2021-02-06 10:46:08 +00:00
//login
2021-02-05 23:15:25 +00:00
ucAPI.ucLogin()
2021-02-06 10:46:08 +00:00
//get all Sites from Controller
2021-02-05 23:15:25 +00:00
sites, err := ucAPI.ucGetSites()
if err != nil {
2021-02-06 21:32:24 +00:00
log.Fatalln(err)
2021-02-05 23:15:25 +00:00
}
2021-02-06 10:46:08 +00:00
//get all devices in all sites
2021-02-05 23:15:25 +00:00
devices, err := ucAPI.ucGetDevices(sites)
if err != nil {
2021-02-06 21:32:24 +00:00
log.Fatalln(err)
2021-02-05 23:15:25 +00:00
}
2021-02-06 10:46:08 +00:00
//build nodes struct
2021-02-05 23:15:25 +00:00
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
}
}
2021-02-06 10:46:08 +00:00
if isRemoteMACpublished(jsonDevice.MAC, d.Devices) == true {
links = ucAddLink(jsonDevice, links)
}
2021-02-05 23:15:25 +00:00
load, err := strconv.ParseFloat(currentDevice.Sysstats.CPU, 64)
if err != nil {
fmt.Println("Error: ", currentDevice.Name)
//log.Fatalln(err)
load = 0
2020-12-29 17:52:15 +00:00
}
2021-02-05 23:15:25 +00:00
mem, err := strconv.ParseFloat(currentDevice.Sysstats.Memory, 64)
if err != nil {
//log.Fatalln(err)
load = 0
2020-12-29 17:52:15 +00:00
}
2021-02-13 18:12:28 +00:00
var model = lookupModels(currentDevice.Model)
var clients = currentDevice.Users
2021-03-02 19:15:54 +00:00
if conf.Unifi.DisplayUsers == false {
clients = 0
}
2021-02-05 23:15:25 +00:00
nodes = append(nodes, node{
Firstseen: "0",
Lastseen: time.Unix(int64(currentDevice.LastSeen), 0).Format(iso8601),
IsOnline: itob(currentDevice.State),
IsGateway: false,
Clients: clients,
2021-02-05 23:15:25 +00:00
ClientsWifi24: 0,
ClientsWifi5: 0,
ClientsOther: clients,
2021-02-05 23:15:25 +00:00
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,
2021-02-13 17:24:57 +00:00
Location: &currentJSONDevice.Location,
2021-02-05 23:15:25 +00:00
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,
2021-02-13 18:12:28 +00:00
Model: model,
2021-02-05 23:15:25 +00:00
})
}
2021-02-06 10:46:08 +00:00
return nodes, links
2021-02-05 23:15:25 +00:00
}
2021-02-06 10:46:08 +00:00
//UNMS API processing (Richtfunk)
2021-02-10 18:31:31 +00:00
func processUNMSAPI() ([]node, []link) {
2021-02-05 23:15:25 +00:00
// Variables for runtime
var links []link
var nodes []node
2021-02-13 15:10:41 +00:00
d := getDevices(conf.Unms.DevicesURL)
2021-02-05 23:15:25 +00:00
// API CALL 1
log.Println("calling API 1")
var u []unifiAPIResponse
2021-02-13 15:10:41 +00:00
err := callUnifiAPI("/devices", &u)
2021-02-05 23:15:25 +00:00
if err != nil {
log.Fatalln(err)
}
2021-02-05 23:15:25 +00:00
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]
2020-12-29 17:52:15 +00:00
}
2021-02-05 23:15:25 +00:00
}
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
2021-02-10 18:31:31 +00:00
callUnifiAPI("/devices/erouters/"+dev.Identification.ID, &details)
2021-02-05 23:15:25 +00:00
// END OF API CALL 2
// API CALL 3
log.Println("calling API 3 for device", d.Devices[i].Name)
var airmaxes []unifiAPIAirmax
2021-02-10 18:31:31 +00:00
callUnifiAPI("/devices/airmaxes/"+dev.Identification.ID+"/stations", &airmaxes)
2021-02-05 23:15:25 +00:00
// 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)
}
2020-12-29 17:52:15 +00:00
}
2021-02-05 23:15:25 +00:00
// 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,
2021-02-13 17:24:57 +00:00
Location: &currentDevice.Location,
2021-02-05 23:15:25 +00:00
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
}
2021-02-10 18:31:31 +00:00
func processAPIs() {
2021-02-05 23:15:25 +00:00
tick := time.Tick(delay)
for range tick {
var nodes []node
var links []link
2021-02-10 18:31:31 +00:00
if conf.Unms.Enabled == true {
2021-04-05 10:52:35 +00:00
log.Println("Processing UNMS")
2021-02-10 18:31:31 +00:00
unmsNodes, unmsLinks := processUNMSAPI()
nodes = append(nodes, unmsNodes...)
links = append(links, unmsLinks...)
}
2021-02-10 18:31:31 +00:00
if conf.Unifi.Enabled == true {
2021-04-05 10:52:35 +00:00
log.Println("Processing Unifi")
2021-02-10 18:31:31 +00:00
ucNodes, ucLinks := processUcAPIs()
nodes = append(nodes, ucNodes...)
links = append(links, ucLinks...)
}
2021-02-13 15:10:41 +00:00
if conf.Meshviewer.Enabled == true {
2021-04-05 10:52:35 +00:00
log.Println("Processing Meshviewer")
2021-02-13 15:10:41 +00:00
mvNodes, mvLinks := getMeshviewer()
nodes = append(nodes, mvNodes...)
links = append(links, mvLinks...)
}
// assemble final struct
o := output{
Timestamp: time.Now().Format(iso8601),
Nodes: nodes,
Links: links,
}
2020-12-29 17:52:15 +00:00
// create file output
log.Println("writing json file")
if err := o.writeToFile(); err != nil {
log.Fatalln(err)
}
2021-04-05 10:52:35 +00:00
// get outages to serve as .csv
l := getUNMSLogs()
err := writeOutagesToCSV(l)
if err != nil {
log.Println("Error writing outages.csv")
}
// we're done here
log.Println("...done")
}
2020-12-29 17:52:15 +00:00
}
2021-02-05 23:57:51 +00:00
func getFile(url string) []byte {
resp, err := http.Get(url)
2020-12-29 17:52:15 +00:00
if err != nil {
2021-04-05 10:52:35 +00:00
log.Println("Error getting file from:", url)
2020-12-29 17:52:15 +00:00
}
2021-02-05 23:57:51 +00:00
data := resp.Body
byteValue, _ := ioutil.ReadAll(data)
return byteValue
}
2021-02-06 10:46:08 +00:00
2021-02-13 15:10:41 +00:00
func getDevices(url string) devices {
2021-02-05 23:57:51 +00:00
// get devices from JSON file
2021-02-06 21:44:05 +00:00
jsonFile := getFile(url)
2020-12-29 17:52:15 +00:00
// read file to bytes
// variable for d
var d devices
// unmarshal to struct
2021-02-06 21:32:24 +00:00
err := json.Unmarshal(jsonFile, &d)
if err != nil {
2021-02-06 21:44:05 +00:00
fmt.Println("can´t get devices file from " + url)
2021-02-06 21:32:24 +00:00
log.Fatal(err)
}
2021-02-13 15:10:41 +00:00
return d
2020-12-29 17:52:15 +00:00
}
2021-02-10 18:31:31 +00:00
func callUnifiAPI(url string, i interface{}) error {
request, err := http.NewRequest(http.MethodGet, conf.Unms.UnmsAPIURL+url, nil)
2020-12-29 17:52:15 +00:00
if err != nil {
2021-02-10 18:31:31 +00:00
return errors.New(fmt.Sprint("can't set request", conf.Unms.UnmsAPIURL+url))
2020-12-29 17:52:15 +00:00
}
2021-02-10 18:31:31 +00:00
request.Header.Set("x-auth-token", conf.Unms.APItoken)
2020-12-29 17:52:15 +00:00
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
2021-02-10 18:31:31 +00:00
return fmt.Errorf("can't get request %s with x-auth-token %s", conf.Unms.UnmsAPIURL+url, conf.Unms.APItoken)
2021-02-06 21:32:24 +00:00
}
if response.StatusCode != 200 {
log.Fatalln("Can´t call UNMS API, check token and URL. HTTP Status: ", response.StatusCode)
2020-12-29 17:52:15 +00:00
}
data, err := ioutil.ReadAll(response.Body)
defer response.Body.Close()
2020-12-29 17:52:15 +00:00
if err != nil {
return fmt.Errorf("can't read response body: %+v", response.Body)
}
// no error occurred, unmarshal to struct
2020-12-29 17:52:15 +00:00
json.Unmarshal(data, &i)
return nil
2020-12-29 17:52:15 +00:00
}
func isRemoteMACpublished(mac string, devices []device) bool {
for i := range devices {
if devices[i].MAC == mac {
return true
}
}
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() {
fs := http.FileServer(http.Dir("./output"))
http.Handle("/", fs)
log.Println("Listening on :3000...")
err := http.ListenAndServe(":3000", nil)
if err != nil {
log.Fatalln(err)
}
}
2021-02-05 23:15:25 +00:00
func httpClient() *http.Client {
jar, err := cookiejar.New(nil)
if err != nil {
log.Fatal(err)
}
client := &http.Client{Jar: jar}
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
}
2021-02-06 10:46:08 +00:00
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, ":", ""),
2021-02-06 10:53:43 +00:00
SourceTQ: 1,
TargetTQ: 1,
2021-02-06 10:46:08 +00:00
SourceAddr: dev.MAC,
TargetAddr: dev.LinkedTo,
})
return links
}
2021-02-13 15:10:41 +00:00
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 {
fmt.Println("can´t get Meshviewer Json file from " + url)
log.Println(err)
}
return n, nil
}
func findNodeID(NodeID string) bool {
for i := range ucDev.Devices {
if ucDev.Devices[i].GatewayNexthop == NodeID {
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) == true {
mvNode.Clients = 0
mvNode.ClientsWifi24 = 0
mvNode.ClientsWifi5 = 0
mvNode.ClientsOther = 0
}
2021-02-13 17:24:57 +00:00
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 {
2021-02-13 23:28:49 +00:00
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,
})
2021-02-13 17:24:57 +00:00
}
2021-02-13 15:10:41 +00:00
}
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) {
var nodes []node
var links []link
for i := range conf.Meshviewer.Files {
2021-04-05 10:52:35 +00:00
log.Println("Hole Meshviewer JSON von: ", conf.Meshviewer.Files[i].URL)
2021-02-13 15:10:41 +00:00
m, err := getMeshviewerJSON(conf.Meshviewer.Files[i].URL)
if err != nil {
return nodes, links
}
mvNodes, mvLinks := addmvDevices(m)
nodes = append(nodes, mvNodes...)
links = append(links, mvLinks...)
}
return nodes, links
}
2021-04-05 10:52:35 +00:00
func getUNMSLogs() UNMSLogResponse {
var l UNMSLogResponse
log.Println("Get Outages from UNMS")
err := callUnifiAPI("/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
}