Merge pull request 'Added support for UnifiAPs' (#6) from unifi-controller into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing

Reviewed-on: #6
This commit is contained in:
nils 2021-02-05 23:18:44 +00:00
commit 7d27e69400
4 changed files with 366 additions and 89 deletions

0
go.sum Normal file
View File

394
main.go
View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
@ -8,7 +9,9 @@ import (
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"os"
"strconv"
"strings"
"time"
)
@ -16,12 +19,15 @@ import (
// types
const (
baseURL = "https://unifi.freifunk-troisdorf.de/v2.1"
iso8601 = "2006-01-02T15:04:05-0700"
baseURL = "https://unifi.freifunk-troisdorf.de/v2.1"
ucBaseURL = "https://unifi.freifunk-troisdorf.de:8443"
iso8601 = "2006-01-02T15:04:05-0700"
)
// flags
var token = flag.String("token", "", "Defines the x-auth-token")
var ucUser = flag.String("ucUser", "", "Defines the Unifi API User")
var ucPass = flag.String("ucPass", "", "Defines the Unifi API Password")
var version = "development"
var delay time.Duration = 60 * time.Second
@ -33,7 +39,12 @@ func main() {
if *token == "" {
log.Fatalln("Please specify an API token via the flag '-token'")
}
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)
go processAPIs()
@ -41,95 +52,215 @@ func main() {
serveJSON()
}
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"
}
}
func itob(i int) bool {
if i == 1 {
return true
}
return false
}
func processUcAPIs() []node {
var nodes []node
d, err := getDevices("ucDevices.json")
if err != nil {
log.Fatalln(err)
}
ucAPI := newAPI(*ucUser, *ucPass, ucBaseURL)
ucAPI.ucLogin()
sites, err := ucAPI.ucGetSites()
if err != nil {
panic(err)
}
devices, err := ucAPI.ucGetDevices(sites)
if err != nil {
panic(err)
}
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
}
}
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
}
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 {
// Variables for runtime
var links []link
var nodes []node
var links []link
d, err := getDevices()
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,
})
}
unmsNodes, links := processUNMSAPI()
ucNodes := processUcAPIs()
nodes = append(nodes, unmsNodes...)
nodes = append(nodes, ucNodes...)
// assemble final struct
o := output{
@ -150,9 +281,9 @@ func processAPIs() {
}
}
func getDevices() (devices, error) {
func getDevices(file string) (devices, error) {
// get devices from JSON file
jsonFile, err := os.Open("devices.json")
jsonFile, err := os.Open(file)
if err != nil {
return devices{}, errors.New("can't open devices.json")
}
@ -242,3 +373,88 @@ func serveJSON() {
log.Fatalln(err)
}
}
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
}

View File

@ -4,7 +4,9 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"sync"
"time"
)
@ -142,3 +144,36 @@ type apiResponse struct {
Error string `json:"error"`
Message string `json:"message"`
}
type jar struct {
lk sync.Mutex
cookies map[string][]*http.Cookie
}
type ucSite struct {
Name string `json:"desc"`
ID string `json:"name"`
}
type ucDevice struct {
IP string `json:"ip"`
Mac string `json:"mac"`
Model string `json:"model"`
Version string `json:"version"`
Gateway string `json:"gateway_mac"`
Name string `json:"name"`
State int `json:"state"`
LastSeen int `json:"last_seen"`
Uptime int `json:"uptime"`
Sysstats struct {
CPU string `json:"cpu"`
Memory string `json:"mem"`
} `json:"system-stats"`
}
type ucAPIData struct {
user string
pass string
baseURL string
client *http.Client
}

26
ucDevices.json Normal file
View File

@ -0,0 +1,26 @@
{
"devices":[
{
"name": "UAP-AC-M_S03",
"mac": "18:e8:29:56:6d:9e",
"gateway_nexthop": "18e8292f7de6",
"gateway": "a28cae6ff604",
"domain": "unifi",
"location": {
"longitude":7.148406208,
"latitude":50.817093402
}
},
{
"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
}
}
]
}