Merge pull request #6 from golift/dn2_cleaanup
Cleanup and re-arrangements.
This commit is contained in:
commit
cde65e6078
|
|
@ -8,6 +8,8 @@ data. The data is provided in a large struct you can consume in your application
|
|||
This library also contains methods to export the Unifi data in InfluxDB format,
|
||||
and this can be used as an example to base your own metrics collection methods.
|
||||
|
||||
If more features are requested, I'll certainly consider them. Do you need to do
|
||||
more than just collect data? [Let me know](https://github.com/golift/unifi/issues/new)!
|
||||
Pull requests and feedback are welcomed!
|
||||
|
||||
Here's a working example:
|
||||
|
|
@ -21,25 +23,32 @@ func main() {
|
|||
username := "admin"
|
||||
password := "superSecret1234"
|
||||
URL := "https://127.0.0.1:8443/"
|
||||
uni, err := unifi.GetController(username, password, URL, false)
|
||||
uni, err := unifi.NewUnifi(username, password, URL, false)
|
||||
if err != nil {
|
||||
log.Fatalln("Error:", err)
|
||||
}
|
||||
// Log with log.Printf or make your own interface that accepts (msg, fmt)
|
||||
uni.ErrorLog = log.Printf
|
||||
uni.DebugLog = log.Printf
|
||||
clients, err := uni.GetClients()
|
||||
|
||||
sites, err := uni.GetSites()
|
||||
if err != nil {
|
||||
log.Fatalln("Error:", err)
|
||||
}
|
||||
clients, err := uni.GetClients(sites)
|
||||
if err != nil {
|
||||
log.Fatalln("Error:", err)
|
||||
}
|
||||
devices, err := uni.GetDevices(sites)
|
||||
if err != nil {
|
||||
log.Fatalln("Error:", err)
|
||||
}
|
||||
|
||||
log.Println(len(clients.UCLs), "Clients connected:")
|
||||
for i, client := range clients.UCLs {
|
||||
log.Println(i+1, client.ID, client.Hostname, client.IP, client.Name, client.LastSeen)
|
||||
}
|
||||
devices, err := uni.GetDevices()
|
||||
if err != nil {
|
||||
log.Fatalln("Error:", err)
|
||||
}
|
||||
|
||||
log.Println(len(devices.USWs), "Unifi Switches Found")
|
||||
log.Println(len(devices.USGs), "Unifi Gateways Found")
|
||||
|
||||
|
|
@ -48,5 +57,4 @@ func main() {
|
|||
log.Println(i+1, uap.Name, uap.IP)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,15 +10,15 @@ import (
|
|||
// Points generates Unifi Client datapoints for InfluxDB.
|
||||
// These points can be passed directly to influx.
|
||||
func (c UCL) Points() ([]*influx.Point, error) {
|
||||
var points []*influx.Point
|
||||
// Fix name and hostname fields. Sometimes one or the other is blank.
|
||||
if c.Name == "" && c.Hostname != "" {
|
||||
c.Name = c.Hostname
|
||||
} else if c.Hostname == "" && c.Name != "" {
|
||||
c.Hostname = c.Name
|
||||
} else if c.Hostname == "" && c.Name == "" {
|
||||
switch {
|
||||
case c.Hostname == "" && c.Name == "":
|
||||
c.Hostname = "-no-name-"
|
||||
c.Name = "-no-name-"
|
||||
case c.Hostname == "" && c.Name != "":
|
||||
c.Hostname = c.Name
|
||||
case c.Name == "" && c.Hostname != "":
|
||||
c.Name = c.Hostname
|
||||
}
|
||||
tags := map[string]string{
|
||||
"id": c.ID,
|
||||
|
|
@ -100,7 +100,7 @@ func (c UCL) Points() ([]*influx.Point, error) {
|
|||
}
|
||||
pt, err := influx.NewPoint("clients", tags, fields, time.Now())
|
||||
if err == nil {
|
||||
points = append(points, pt)
|
||||
return nil, err
|
||||
}
|
||||
return points, err
|
||||
return []*influx.Point{pt}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package unifi
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// parseDevices parses the raw JSON from the Unifi Controller into device structures.
|
||||
func (u *Unifi) parseDevices(data []json.RawMessage) *Devices {
|
||||
devices := new(Devices)
|
||||
for _, r := range data {
|
||||
// Loop each item in the raw JSON message, detect its type and unmarshal it.
|
||||
assetType := "<type key missing>"
|
||||
if o := make(map[string]interface{}); u.unmarshalDevice("map", r, &o) != nil {
|
||||
continue
|
||||
} else if t, ok := o["type"].(string); ok {
|
||||
assetType = t
|
||||
}
|
||||
u.dLogf("Unmarshalling Device Type: %v", assetType)
|
||||
// Choose which type to unmarshal into based on the "type" json key.
|
||||
switch assetType { // Unmarshal again into the correct type..
|
||||
case "uap":
|
||||
if uap := (UAP{}); u.unmarshalDevice(assetType, r, &uap) == nil {
|
||||
devices.UAPs = append(devices.UAPs, uap)
|
||||
}
|
||||
case "ugw", "usg": // in case they ever fix the name in the api.
|
||||
if usg := (USG{}); u.unmarshalDevice(assetType, r, &usg) == nil {
|
||||
devices.USGs = append(devices.USGs, usg)
|
||||
}
|
||||
case "usw":
|
||||
if usw := (USW{}); u.unmarshalDevice(assetType, r, &usw) == nil {
|
||||
devices.USWs = append(devices.USWs, usw)
|
||||
}
|
||||
default:
|
||||
u.eLogf("unknown asset type - %v - skipping", assetType)
|
||||
}
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
// unmarshalDevice handles logging for the unmarshal operations in parseDevices().
|
||||
func (u *Unifi) unmarshalDevice(dev string, data json.RawMessage, v interface{}) (err error) {
|
||||
if err = json.Unmarshal(data, v); err != nil {
|
||||
u.eLogf("json.Unmarshal(%v): %v", dev, err)
|
||||
u.eLogf("Enable Debug Logging to output the failed payload.")
|
||||
json, err := data.MarshalJSON()
|
||||
u.dLogf("Failed Payload: %s (marshal err: %v)", json, err)
|
||||
u.dLogf("The above payload can prove useful during torubleshooting when you open an Issue:")
|
||||
u.dLogf("==- https://github.com/golift/unifi/issues/new -==")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package unifi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// This is a list of unifi API paths.
|
||||
// The %s in each string must be replaced with a Site.Name.
|
||||
const (
|
||||
// SiteList is the path to the api site list.
|
||||
SiteList string = "/api/self/sites"
|
||||
// ClientPath is Unifi Clients API Path
|
||||
ClientPath string = "/api/s/%s/stat/sta"
|
||||
// DevicePath is where we get data about Unifi devices.
|
||||
DevicePath string = "/api/s/%s/stat/device"
|
||||
// NetworkPath contains network-configuration data. Not really graphable.
|
||||
NetworkPath string = "/api/s/%s/rest/networkconf"
|
||||
// UserGroupPath contains usergroup configurations.
|
||||
UserGroupPath string = "/api/s/%s/rest/usergroup"
|
||||
// LoginPath is Unifi Controller Login API Path
|
||||
LoginPath string = "/api/login"
|
||||
)
|
||||
|
||||
// Logger is a base type to deal with changing log outputs. Create a logger
|
||||
// that matches this interface to capture debug and error logs.
|
||||
type Logger func(msg string, fmt ...interface{})
|
||||
|
||||
// dLogf logs a debug message.
|
||||
func (u *Unifi) dLogf(msg string, v ...interface{}) {
|
||||
if u.DebugLog != nil {
|
||||
u.DebugLog("[DEBUG] "+msg, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// dLogf logs an error message.
|
||||
func (u *Unifi) eLogf(msg string, v ...interface{}) {
|
||||
if u.ErrorLog != nil {
|
||||
u.ErrorLog("[ERROR] "+msg, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// Devices contains a list of all the unifi devices from a controller.
|
||||
// Contains Access points, security gateways and switches.
|
||||
type Devices struct {
|
||||
UAPs []UAP
|
||||
USGs []USG
|
||||
USWs []USW
|
||||
}
|
||||
|
||||
// Clients contains a list that contains all of the unifi clients from a controller.
|
||||
type Clients struct {
|
||||
UCLs []UCL
|
||||
}
|
||||
|
||||
// Unifi is what you get in return for providing a password! Unifi represents
|
||||
// a controller that you can make authenticated requests to. Use this to make
|
||||
// additional requests for devices, clients or other custom data.
|
||||
type Unifi struct {
|
||||
*http.Client
|
||||
baseURL string
|
||||
ErrorLog Logger
|
||||
DebugLog Logger
|
||||
}
|
||||
|
||||
// Site represents a site's data. There are more pieces to this, but this is
|
||||
// all we expose.
|
||||
type Site struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// FlexInt provides a container and unmarshalling for fields that may be
|
||||
// numbers or strings in the Unifi API.
|
||||
type FlexInt struct {
|
||||
Val float64
|
||||
Txt string
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a string or number to an integer.
|
||||
// Generally, do call this directly, it's used in the json interface.
|
||||
func (f *FlexInt) UnmarshalJSON(b []byte) error {
|
||||
var unk interface{}
|
||||
if err := json.Unmarshal(b, &unk); err != nil {
|
||||
return err
|
||||
}
|
||||
switch i := unk.(type) {
|
||||
case float64:
|
||||
f.Val = i
|
||||
f.Txt = strconv.FormatFloat(i, 'f', -1, 64)
|
||||
return nil
|
||||
case string:
|
||||
f.Txt = i
|
||||
f.Val, _ = strconv.ParseFloat(i, 64)
|
||||
return nil
|
||||
default:
|
||||
return errors.New("Cannot unmarshal to FlexInt")
|
||||
}
|
||||
}
|
||||
|
||||
// FlexBool provides a container and unmarshalling for fields that may be
|
||||
// boolean or strings in the Unifi API.
|
||||
type FlexBool struct {
|
||||
Val bool
|
||||
Txt string
|
||||
}
|
||||
|
||||
// UnmarshalJSO method converts armed/disarmed, yes/no, active/inactive or 0/1 to true/false.
|
||||
// Really it converts ready, up, t, armed, yes, active, enabled, 1, true to true. Anything else is false.
|
||||
func (f *FlexBool) UnmarshalJSON(b []byte) error {
|
||||
f.Txt = strings.Trim(string(b), `"`)
|
||||
f.Val = f.Txt == "1" || strings.EqualFold(f.Txt, "true") || strings.EqualFold(f.Txt, "yes") ||
|
||||
strings.EqualFold(f.Txt, "t") || strings.EqualFold(f.Txt, "armed") || strings.EqualFold(f.Txt, "active") ||
|
||||
strings.EqualFold(f.Txt, "enabled") || strings.EqualFold(f.Txt, "ready") || strings.EqualFold(f.Txt, "up")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package unifi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFlexInt(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := assert.New(t)
|
||||
type testReply struct {
|
||||
Five FlexInt `json:"five"`
|
||||
Seven FlexInt `json:"seven"`
|
||||
Auto FlexInt `json:"auto"`
|
||||
Channel FlexInt `json:"channel"`
|
||||
}
|
||||
var r testReply
|
||||
// test unmarshalling the custom type three times with different values.
|
||||
a.Nil(json.Unmarshal([]byte(`{"five": "5", "seven": 7, "auto": "auto"}`), &r))
|
||||
|
||||
// test number in string.
|
||||
a.EqualValues(5, r.Five.Val)
|
||||
a.EqualValues("5", r.Five.Txt)
|
||||
// test number.
|
||||
a.EqualValues(7, r.Seven.Val)
|
||||
a.EqualValues("7", r.Seven.Txt)
|
||||
// test string.
|
||||
a.EqualValues(0, r.Auto.Val)
|
||||
a.EqualValues("auto", r.Auto.Txt)
|
||||
// test (error) struct.
|
||||
a.NotNil(json.Unmarshal([]byte(`{"channel": {}}`), &r),
|
||||
"a non-string and non-number must produce an error.")
|
||||
a.EqualValues(0, r.Channel.Val)
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
/* I generally suck at InfluxDB, so if I got the tags/fields wrong,
|
||||
please send me a PR or open an Issue to address my faults. Thanks!
|
||||
*/
|
||||
var points []*influx.Point
|
||||
tags := map[string]string{
|
||||
"id": u.ID,
|
||||
"mac": u.Mac,
|
||||
|
|
@ -22,7 +21,7 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
"device_ap": u.Stat.Ap,
|
||||
"site_id": u.SiteID,
|
||||
"name": u.Name,
|
||||
"addopted": strconv.FormatBool(u.Adopted),
|
||||
"adopted": strconv.FormatBool(u.Adopted),
|
||||
"bandsteering_mode": u.BandsteeringMode,
|
||||
"board_rev": strconv.Itoa(u.BoardRev),
|
||||
"cfgversion": u.Cfgversion,
|
||||
|
|
@ -63,7 +62,7 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
"rx_bytes-d": u.RxBytesD,
|
||||
"tx_bytes": u.TxBytes,
|
||||
"tx_bytes-d": u.TxBytesD,
|
||||
"uptime": u.Uptime.Number,
|
||||
"uptime": u.Uptime.Val,
|
||||
"considered_lost_at": u.ConsideredLostAt,
|
||||
"next_heartbeat_at": u.NextHeartbeatAt,
|
||||
"scanning": u.Scanning,
|
||||
|
|
@ -174,7 +173,7 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
points = append(points, pt)
|
||||
points := []*influx.Point{pt}
|
||||
for _, p := range u.RadioTable {
|
||||
tags := map[string]string{
|
||||
"device_name": u.Name,
|
||||
|
|
@ -182,7 +181,7 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
"device_mac": u.Mac,
|
||||
"name": p.Name,
|
||||
"wlangroup_id": p.WlangroupID,
|
||||
"channel": p.Channel.String,
|
||||
"channel": p.Channel.Txt,
|
||||
"radio": p.Radio,
|
||||
}
|
||||
fields := map[string]interface{}{
|
||||
|
|
@ -197,7 +196,7 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
"min_txpower": p.MinTxpower,
|
||||
"nss": p.Nss,
|
||||
"radio_caps": p.RadioCaps,
|
||||
"tx_power": p.TxPower.Number,
|
||||
"tx_power": p.TxPower.Val,
|
||||
"tx_power_mode": p.TxPowerMode,
|
||||
}
|
||||
|
||||
|
|
@ -219,7 +218,7 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
fields["radio"] = s.Radio
|
||||
fields["state"] = s.State
|
||||
fields["radio_tx_packets"] = s.TxPackets
|
||||
fields["radio_tx_power"] = s.TxPower.Number
|
||||
fields["radio_tx_power"] = s.TxPower.Val
|
||||
fields["radio_tx_retries"] = s.TxRetries
|
||||
fields["user-num_sta"] = s.UserNumSta
|
||||
break
|
||||
|
|
@ -253,7 +252,7 @@ func (u UAP) Points() ([]*influx.Point, error) {
|
|||
fields["vap_tx_latency_max"] = s.TxLatencyMax
|
||||
fields["vap_tx_latency_min"] = s.TxLatencyMin
|
||||
fields["vap_tx_packets"] = s.TxPackets
|
||||
fields["vap_tx_power"] = s.TxPower.Number
|
||||
fields["vap_tx_power"] = s.TxPower.Val
|
||||
fields["vap_tx_retries"] = s.TxRetries
|
||||
fields["usage"] = s.Usage
|
||||
break
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package unifi
|
||||
|
||||
// UAP represents all the data from the Ubiquit Controller for a Unifi Access Point.
|
||||
// UAP represents all the data from the Ubiquiti Controller for a Unifi Access Point.
|
||||
type UAP struct {
|
||||
/* This was auto generated and then slowly edited by hand
|
||||
to get all the data types right and graphable.
|
||||
|
|
|
|||
|
|
@ -1,159 +0,0 @@
|
|||
package unifi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// SiteList is the path to the api site list.
|
||||
SiteList string = "/api/self/sites"
|
||||
// ClientPath is Unifi Clients API Path
|
||||
ClientPath string = "/api/s/%s/stat/sta"
|
||||
// DevicePath is where we get data about Unifi devices.
|
||||
DevicePath string = "/api/s/%s/stat/device"
|
||||
// NetworkPath contains network-configuration data. Not really graphable.
|
||||
NetworkPath string = "/api/s/%s/rest/networkconf"
|
||||
// UserGroupPath contains usergroup configurations.
|
||||
UserGroupPath string = "/api/s/%s/rest/usergroup"
|
||||
// LoginPath is Unifi Controller Login API Path
|
||||
LoginPath string = "/api/login"
|
||||
)
|
||||
|
||||
// Logger is a base type to deal with changing log outputs.
|
||||
type Logger func(msg string, fmt ...interface{})
|
||||
|
||||
// Devices contains a list of all the unifi devices from a controller.
|
||||
type Devices struct {
|
||||
UAPs []UAP
|
||||
USGs []USG
|
||||
USWs []USW
|
||||
}
|
||||
|
||||
// Clients conptains a list of all the unifi clients from a controller.
|
||||
type Clients struct {
|
||||
UCLs []UCL
|
||||
}
|
||||
|
||||
// Unifi is what you get in return for providing a password!
|
||||
type Unifi struct {
|
||||
*http.Client
|
||||
baseURL string
|
||||
ErrorLog Logger
|
||||
DebugLog Logger
|
||||
}
|
||||
|
||||
// FlexInt provides a container and unmarshalling for fields that may be
|
||||
// numbers or strings in the Unifi API
|
||||
type FlexInt struct {
|
||||
Number float64
|
||||
String string
|
||||
}
|
||||
|
||||
// UnmarshalJSON converts a string or number to an integer.
|
||||
func (f *FlexInt) UnmarshalJSON(b []byte) error {
|
||||
var unk interface{}
|
||||
if err := json.Unmarshal(b, &unk); err != nil {
|
||||
return err
|
||||
}
|
||||
switch i := unk.(type) {
|
||||
case float64:
|
||||
f.Number = i
|
||||
f.String = strconv.FormatFloat(i, 'f', -1, 64)
|
||||
return nil
|
||||
case string:
|
||||
f.String = i
|
||||
f.Number, _ = strconv.ParseFloat(i, 64)
|
||||
return nil
|
||||
default:
|
||||
return errors.New("Cannot unmarshal to FlexInt")
|
||||
}
|
||||
}
|
||||
|
||||
// FlexBool provides a container and unmarshalling for fields that may be
|
||||
// boolean or strings in the Unifi API
|
||||
type FlexBool struct {
|
||||
Bool bool
|
||||
String string
|
||||
}
|
||||
|
||||
// UnmarshalJSON method converts armed/disarmed, yes/no, active/inactive or 0/1 to true/false.
|
||||
// Really it converts ready, up, t, armed, yes, active, enabled, 1, true to true. Anything else is false.
|
||||
func (f *FlexBool) UnmarshalJSON(b []byte) error {
|
||||
f.String = strings.Trim(string(b), `"`)
|
||||
f.Bool = f.String == "1" || strings.EqualFold(f.String, "true") || strings.EqualFold(f.String, "yes") ||
|
||||
strings.EqualFold(f.String, "t") || strings.EqualFold(f.String, "armed") || strings.EqualFold(f.String, "active") ||
|
||||
strings.EqualFold(f.String, "enabled") || strings.EqualFold(f.String, "ready") || strings.EqualFold(f.String, "up")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetController creates a http.Client with authenticated cookies.
|
||||
// Used to make additional, authenticated requests to the APIs.
|
||||
func GetController(user, pass, url string, verifySSL bool) (*Unifi, error) {
|
||||
json := `{"username": "` + user + `","password": "` + pass + `"}`
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cookiejar.New(nil)")
|
||||
}
|
||||
u := &Unifi{
|
||||
Client: &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifySSL}},
|
||||
Jar: jar,
|
||||
},
|
||||
}
|
||||
if u.baseURL = url; strings.HasSuffix(url, "/") {
|
||||
u.baseURL = url[:len(url)-1]
|
||||
}
|
||||
req, err := u.UniReq(LoginPath, json)
|
||||
if err != nil {
|
||||
return u, errors.Wrap(err, "UniReq(LoginPath, json)")
|
||||
}
|
||||
resp, err := u.Do(req)
|
||||
if err != nil {
|
||||
return u, errors.Wrap(err, "authReq.Do(req)")
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return u, errors.Errorf("authentication failed (%v): %v (status: %v/%v)",
|
||||
user, url+LoginPath, resp.StatusCode, resp.Status)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// UniReq is a small helper function that adds an Accept header.
|
||||
// Use this if you're unmarshalling Unifi data into custom types.
|
||||
// And you're doing that... sumbut a pull request with your new struct. :)
|
||||
func (u *Unifi) UniReq(apiPath string, params string) (req *http.Request, err error) {
|
||||
if params != "" {
|
||||
req, err = http.NewRequest("POST", u.baseURL+apiPath, bytes.NewBufferString(params))
|
||||
} else {
|
||||
req, err = http.NewRequest("GET", u.baseURL+apiPath, nil)
|
||||
}
|
||||
if err == nil {
|
||||
req.Header.Add("Accept", "application/json")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// dLogf logs a debug message.
|
||||
func (u *Unifi) dLogf(msg string, v ...interface{}) {
|
||||
if u.DebugLog != nil {
|
||||
u.DebugLog("[DEBUG] "+msg, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// dLogf logs an error message.
|
||||
func (u *Unifi) eLogf(msg string, v ...interface{}) {
|
||||
if u.ErrorLog != nil {
|
||||
u.ErrorLog("[ERROR] "+msg, v...)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,63 @@
|
|||
// Package unifi provides a set of types to unload (unmarshal) Unifi Ubiquiti
|
||||
// controller data. Also provided are methods to easily get data for devices -
|
||||
// things like access points and switches, and for clients - the things
|
||||
// connected to those access points and switches. As a bonus, each device and
|
||||
// client type provided has an attached method to create InfluxDB datapoints.
|
||||
package unifi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NewUnifi creates a http.Client with authenticated cookies.
|
||||
// Used to make additional, authenticated requests to the APIs.
|
||||
// Start here.
|
||||
func NewUnifi(user, pass, url string, verifySSL bool) (*Unifi, error) {
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cookiejar.New(nil)")
|
||||
}
|
||||
u := &Unifi{baseURL: strings.TrimRight(url, "/"),
|
||||
Client: &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifySSL}},
|
||||
Jar: jar,
|
||||
},
|
||||
}
|
||||
return u, u.getController(user, pass)
|
||||
}
|
||||
|
||||
// getController is a helper method to make testsing a bit easier.
|
||||
func (u *Unifi) getController(user, pass string) error {
|
||||
// magic login.
|
||||
req, err := u.UniReq(LoginPath, `{"username": "`+user+`","password": "`+pass+`"}`)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "UniReq(LoginPath, json)")
|
||||
}
|
||||
resp, err := u.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "authReq.Do(req)")
|
||||
}
|
||||
defer func() {
|
||||
_, _ = io.Copy(ioutil.Discard, resp.Body) // avoid leaking.
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("authentication failed (user: %s): %s (status: %d/%s)",
|
||||
user, u.baseURL+LoginPath, resp.StatusCode, resp.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClients returns a response full of clients' data from the Unifi Controller.
|
||||
func (u *Unifi) GetClients(sites []Site) (*Clients, error) {
|
||||
var data []UCL
|
||||
|
|
@ -44,12 +93,6 @@ func (u *Unifi) GetDevices(sites []Site) (*Devices, error) {
|
|||
return u.parseDevices(data), nil
|
||||
}
|
||||
|
||||
// Site represents a site's data. There are more pieces to this.
|
||||
type Site struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
}
|
||||
|
||||
// GetSites returns a list of configured sites on the Unifi controller.
|
||||
func (u *Unifi) GetSites() ([]Site, error) {
|
||||
var response struct {
|
||||
|
|
@ -87,55 +130,18 @@ func (u *Unifi) GetData(methodPath string, v interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// parseDevices parses the raw JSON from the Unifi Controller into device structures.
|
||||
func (u *Unifi) parseDevices(data []json.RawMessage) *Devices {
|
||||
devices := new(Devices)
|
||||
for _, r := range data {
|
||||
// Loop each item in the raw JSON message, detect its type and unmarshal it.
|
||||
var obj map[string]interface{}
|
||||
var uap UAP
|
||||
var usg USG
|
||||
var usw USW
|
||||
|
||||
if u.unmarshalDevice("interface{}", &obj, r) != nil {
|
||||
continue
|
||||
}
|
||||
assetType := "<missing>"
|
||||
if t, ok := obj["type"].(string); ok {
|
||||
assetType = t
|
||||
}
|
||||
u.dLogf("Unmarshalling Device Type: %v", assetType)
|
||||
switch assetType { // Unmarshal again into the correct type..
|
||||
case "uap":
|
||||
if u.unmarshalDevice(assetType, &uap, r) == nil {
|
||||
devices.UAPs = append(devices.UAPs, uap)
|
||||
}
|
||||
case "ugw", "usg": // in case they ever fix the name in the api.
|
||||
if u.unmarshalDevice(assetType, &usg, r) == nil {
|
||||
devices.USGs = append(devices.USGs, usg)
|
||||
}
|
||||
case "usw":
|
||||
if u.unmarshalDevice(assetType, &usw, r) == nil {
|
||||
devices.USWs = append(devices.USWs, usw)
|
||||
}
|
||||
default:
|
||||
u.eLogf("unknown asset type - %v - skipping", assetType)
|
||||
continue
|
||||
}
|
||||
// UniReq is a small helper function that adds an Accept header.
|
||||
// Use this if you're unmarshalling Unifi data into custom types.
|
||||
// And if you're doing that... sumbut a pull request with your new struct. :)
|
||||
// This is a helper method that is exposed for convenience.
|
||||
func (u *Unifi) UniReq(apiPath string, params string) (req *http.Request, err error) {
|
||||
if params != "" {
|
||||
req, err = http.NewRequest("POST", u.baseURL+apiPath, bytes.NewBufferString(params))
|
||||
} else {
|
||||
req, err = http.NewRequest("GET", u.baseURL+apiPath, nil)
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
// unmarshalDevice handles logging for the unmarshal operations in parseDevices().
|
||||
func (u *Unifi) unmarshalDevice(device string, ptr interface{}, payload json.RawMessage) error {
|
||||
err := json.Unmarshal(payload, ptr)
|
||||
if err != nil {
|
||||
u.eLogf("json.Unmarshal(%v): %v", device, err)
|
||||
u.eLogf("Enable Debug Logging to output the failed payload.")
|
||||
json, err := payload.MarshalJSON()
|
||||
u.dLogf("Failed Payload: %s (marshal err: %v)", json, err)
|
||||
u.dLogf("The above payload can prove useful during torubleshooting when you open an Issue:")
|
||||
u.dLogf("==- https://github.com/golift/unifi/issues/new -==")
|
||||
if err == nil {
|
||||
req.Header.Add("Accept", "application/json")
|
||||
}
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package unifi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
|
@ -9,32 +8,17 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFlexInt(t *testing.T) {
|
||||
func TestNewUnifi(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := assert.New(t)
|
||||
type testReply struct {
|
||||
Five FlexInt `json:"five"`
|
||||
Seven FlexInt `json:"seven"`
|
||||
Auto FlexInt `json:"auto"`
|
||||
Channel FlexInt `json:"channel"`
|
||||
}
|
||||
var r testReply
|
||||
// test unmarshalling the custom type three times with different values.
|
||||
a.Nil(json.Unmarshal([]byte(`{"five": "5", "seven": 7, "auto": "auto"}`), &r))
|
||||
|
||||
// test number in string.
|
||||
a.EqualValues(5, r.Five.Number)
|
||||
a.EqualValues("5", r.Five.String)
|
||||
// test number.
|
||||
a.EqualValues(7, r.Seven.Number)
|
||||
a.EqualValues("7", r.Seven.String)
|
||||
// test string.
|
||||
a.EqualValues(0, r.Auto.Number)
|
||||
a.EqualValues("auto", r.Auto.String)
|
||||
// test (error) struct.
|
||||
a.NotNil(json.Unmarshal([]byte(`{"channel": {}}`), &r),
|
||||
"a non-string and non-number must produce an error.")
|
||||
a.EqualValues(0, r.Channel.Number)
|
||||
url := "http://127.0.0.1:64431"
|
||||
authReq, err := NewUnifi("user1", "pass2", url, false)
|
||||
a.NotNil(err)
|
||||
a.EqualValues(url, authReq.baseURL)
|
||||
a.Contains(err.Error(), "authReq.Do(req):", "an invalid destination should product a .Do(req) error.")
|
||||
/* TODO: OPEN web server, check parameters posted, more. This test is incomplete.
|
||||
a.EqualValues(`{"username": "user1","password": "pass2"}`, string(post_params), "user/pass json parameters improperly encoded")
|
||||
*/
|
||||
}
|
||||
|
||||
func TestUniReq(t *testing.T) {
|
||||
|
|
@ -67,16 +51,3 @@ func TestUniReq(t *testing.T) {
|
|||
a.Nil(err, "problem reading request body, POST parameters may be malformed")
|
||||
a.EqualValues(p, string(d), "POST parameters improperly encoded")
|
||||
}
|
||||
|
||||
func TestAuthController(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := assert.New(t)
|
||||
url := "http://127.0.0.1:64431"
|
||||
authReq, err := GetController("user1", "pass2", url, false)
|
||||
a.NotNil(err)
|
||||
a.EqualValues(url, authReq.baseURL)
|
||||
a.Contains(err.Error(), "authReq.Do(req):", "an invalid destination should product a .Do(req) error.")
|
||||
/* TODO: OPEN web server, check parameters posted, more. This test is incomplete.
|
||||
a.EqualValues(`{"username": "user1","password": "pass2"}`, string(post_params), "user/pass json parameters improperly encoded")
|
||||
*/
|
||||
}
|
||||
|
|
@ -10,14 +10,13 @@ import (
|
|||
// Points generates Unifi Gateway datapoints for InfluxDB.
|
||||
// These points can be passed directly to influx.
|
||||
func (u USG) Points() ([]*influx.Point, error) {
|
||||
var points []*influx.Point
|
||||
tags := map[string]string{
|
||||
"id": u.ID,
|
||||
"mac": u.Mac,
|
||||
"device_type": u.Stat.O,
|
||||
"device_oid": u.Stat.Oid,
|
||||
"site_id": u.SiteID,
|
||||
"addopted": strconv.FormatBool(u.Adopted),
|
||||
"adopted": strconv.FormatBool(u.Adopted),
|
||||
"name": u.Name,
|
||||
"adopt_ip": u.AdoptIP,
|
||||
"adopt_url": u.AdoptURL,
|
||||
|
|
@ -92,7 +91,7 @@ func (u USG) Points() ([]*influx.Point, error) {
|
|||
"wan1_rx_packets": u.Wan1.RxPackets,
|
||||
"wan1_type": u.Wan1.Type,
|
||||
"wan1_speed": u.Wan1.Speed,
|
||||
"wan1_up": u.Wan1.Up.Bool,
|
||||
"wan1_up": u.Wan1.Up.Val,
|
||||
"wan1_tx_bytes": u.Wan1.TxBytes,
|
||||
"wan1_tx_bytes-r": u.Wan1.TxBytesR,
|
||||
"wan1_tx_dropped": u.Wan1.TxDropped,
|
||||
|
|
@ -130,7 +129,7 @@ func (u USG) Points() ([]*influx.Point, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
points = append(points, pt)
|
||||
points := []*influx.Point{pt}
|
||||
for _, p := range u.NetworkTable {
|
||||
tags := map[string]string{
|
||||
"device_name": u.Name,
|
||||
|
|
@ -170,7 +169,7 @@ func (u USG) Points() ([]*influx.Point, error) {
|
|||
"rx_packets": p.RxPackets,
|
||||
"tx_bytes": p.TxBytes,
|
||||
"tx_packets": p.TxPackets,
|
||||
"up": p.Up.String,
|
||||
"up": p.Up.Txt,
|
||||
"vlan": p.Vlan,
|
||||
"dhcpd_ntp_1": p.DhcpdNtp1,
|
||||
"dhcpd_unifi_controller": p.DhcpdUnifiController,
|
||||
|
|
@ -183,5 +182,5 @@ func (u USG) Points() ([]*influx.Point, error) {
|
|||
}
|
||||
points = append(points, pt)
|
||||
}
|
||||
return points, err
|
||||
return points, nil
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package unifi
|
|||
|
||||
import "encoding/json"
|
||||
|
||||
// USG represents all the data from the Ubiquit Controller for a Unifi Security Gateway.
|
||||
// USG represents all the data from the Ubiquiti Controller for a Unifi Security Gateway.
|
||||
type USG struct {
|
||||
ID string `json:"_id"`
|
||||
UUptime float64 `json:"_uptime"`
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
// Points generates Unifi Switch datapoints for InfluxDB.
|
||||
// These points can be passed directly to influx.
|
||||
func (u USW) Points() ([]*influx.Point, error) {
|
||||
var points []*influx.Point
|
||||
tags := map[string]string{
|
||||
"id": u.ID,
|
||||
"mac": u.Mac,
|
||||
|
|
@ -18,7 +17,7 @@ func (u USW) Points() ([]*influx.Point, error) {
|
|||
"device_oid": u.Stat.Oid,
|
||||
"site_id": u.SiteID,
|
||||
"name": u.Name,
|
||||
"addopted": strconv.FormatBool(u.Adopted),
|
||||
"adopted": strconv.FormatBool(u.Adopted),
|
||||
"adopt_ip": u.AdoptIP,
|
||||
"adopt_url": u.AdoptURL,
|
||||
"cfgversion": u.Cfgversion,
|
||||
|
|
@ -112,8 +111,8 @@ func (u USW) Points() ([]*influx.Point, error) {
|
|||
// Add the port stats too.
|
||||
}
|
||||
pt, err := influx.NewPoint("usw", tags, fields, time.Now())
|
||||
if err == nil {
|
||||
points = append(points, pt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return points, err
|
||||
return []*influx.Point{pt}, nil
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package unifi
|
||||
|
||||
// USW represents all the data from the Ubiquit Controller for a Unifi Switch.
|
||||
// USW represents all the data from the Ubiquiti Controller for a Unifi Switch.
|
||||
type USW struct {
|
||||
ID string `json:"_id"`
|
||||
UUptime float64 `json:"_uptime"`
|
||||
|
|
|
|||
Loading…
Reference in New Issue