From e2d886da72251613b5758218dbecdf8bd37df757 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 17 Apr 2019 00:16:22 -0700 Subject: [PATCH 1/4] Re-arrange --- core/unifi/README.md | 20 ++- core/unifi/{clients.go => clients_influx.go} | 0 core/unifi/parsers.go | 56 +++++++ core/unifi/types.go | 120 ++++++++++++++ core/unifi/types_test.go | 36 +++++ core/unifi/{uap.go => uap_influx.go} | 8 +- core/unifi/uap_type.go | 2 +- core/unifi/unidev.go | 157 ------------------- core/unifi/unifi.go | 107 +++++++------ core/unifi/{unidev_test.go => unifi_test.go} | 47 ++---- core/unifi/{usg.go => usg_influx.go} | 6 +- core/unifi/usg_type.go | 2 +- core/unifi/{usw.go => usw_influx.go} | 2 +- core/unifi/usw_type.go | 2 +- 14 files changed, 304 insertions(+), 261 deletions(-) rename core/unifi/{clients.go => clients_influx.go} (100%) create mode 100644 core/unifi/parsers.go create mode 100644 core/unifi/types.go create mode 100644 core/unifi/types_test.go rename core/unifi/{uap.go => uap_influx.go} (98%) delete mode 100644 core/unifi/unidev.go rename core/unifi/{unidev_test.go => unifi_test.go} (70%) rename core/unifi/{usg.go => usg_influx.go} (98%) rename core/unifi/{usw.go => usw_influx.go} (98%) diff --git a/core/unifi/README.md b/core/unifi/README.md index 3325c94a..cbe77503 100644 --- a/core/unifi/README.md +++ b/core/unifi/README.md @@ -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: @@ -28,18 +30,25 @@ func main() { // 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) } } - ``` diff --git a/core/unifi/clients.go b/core/unifi/clients_influx.go similarity index 100% rename from core/unifi/clients.go rename to core/unifi/clients_influx.go diff --git a/core/unifi/parsers.go b/core/unifi/parsers.go new file mode 100644 index 00000000..5b324282 --- /dev/null +++ b/core/unifi/parsers.go @@ -0,0 +1,56 @@ +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. + var obj map[string]interface{} + var uap UAP + var usg USG + var usw USW + + if u.unmarshalDevice("interface{}", &obj, r) != nil { + continue + } + assetType := "" + 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 + } + } + 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 -==") + } + return err +} diff --git a/core/unifi/types.go b/core/unifi/types.go new file mode 100644 index 00000000..61266609 --- /dev/null +++ b/core/unifi/types.go @@ -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 +} diff --git a/core/unifi/types_test.go b/core/unifi/types_test.go new file mode 100644 index 00000000..2b62cfcc --- /dev/null +++ b/core/unifi/types_test.go @@ -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) +} diff --git a/core/unifi/uap.go b/core/unifi/uap_influx.go similarity index 98% rename from core/unifi/uap.go rename to core/unifi/uap_influx.go index 81039f6e..36789de8 100644 --- a/core/unifi/uap.go +++ b/core/unifi/uap_influx.go @@ -22,7 +22,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 +63,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, @@ -182,7 +182,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 +197,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, } diff --git a/core/unifi/uap_type.go b/core/unifi/uap_type.go index 2d61b324..b612f1dd 100644 --- a/core/unifi/uap_type.go +++ b/core/unifi/uap_type.go @@ -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. diff --git a/core/unifi/unidev.go b/core/unifi/unidev.go deleted file mode 100644 index cd5d6db4..00000000 --- a/core/unifi/unidev.go +++ /dev/null @@ -1,157 +0,0 @@ -package unifi - -import ( - "bytes" - "crypto/tls" - "encoding/json" - "net/http" - "net/http/cookiejar" - "strconv" - "strings" - - "github.com/pkg/errors" -) - -const ( - // ClientPath is Unifi Clients API Path - ClientPath string = "/api/s/default/stat/sta" - // DevicePath is where we get data about Unifi devices. - DevicePath string = "/api/s/default/stat/device" - // NetworkPath contains network-configuration data. Not really graphable. - NetworkPath string = "/api/s/default/rest/networkconf" - // UserGroupPath contains usergroup configurations. - UserGroupPath string = "/api/s/default/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 -} - -// 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.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...) - } -} diff --git a/core/unifi/unifi.go b/core/unifi/unifi.go index b8b9c0e8..2ba47653 100644 --- a/core/unifi/unifi.go +++ b/core/unifi/unifi.go @@ -1,12 +1,58 @@ +// 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" "io/ioutil" + "net/http" + "net/http/cookiejar" + "strings" "github.com/pkg/errors" ) +// GetController creates a http.Client with authenticated cookies. +// Used to make additional, authenticated requests to the APIs. +// Start here. +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 +} + // GetClients returns a response full of clients' data from the Unifi Controller. func (u *Unifi) GetClients() (*Clients, error) { var response struct { @@ -55,55 +101,18 @@ func (u *Unifi) GetDevices() (*Devices, error) { return u.parseDevices(response.Data), 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 := "" - 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 } diff --git a/core/unifi/unidev_test.go b/core/unifi/unifi_test.go similarity index 70% rename from core/unifi/unidev_test.go rename to core/unifi/unifi_test.go index 35b31704..35c533c6 100644 --- a/core/unifi/unidev_test.go +++ b/core/unifi/unifi_test.go @@ -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 TestAuthController(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 := 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") + */ } 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") - */ -} diff --git a/core/unifi/usg.go b/core/unifi/usg_influx.go similarity index 98% rename from core/unifi/usg.go rename to core/unifi/usg_influx.go index 894f33b4..9b169e54 100644 --- a/core/unifi/usg.go +++ b/core/unifi/usg_influx.go @@ -17,7 +17,7 @@ func (u USG) Points() ([]*influx.Point, error) { "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 +92,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, @@ -170,7 +170,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, diff --git a/core/unifi/usg_type.go b/core/unifi/usg_type.go index 3b29103a..e8bc453f 100644 --- a/core/unifi/usg_type.go +++ b/core/unifi/usg_type.go @@ -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"` diff --git a/core/unifi/usw.go b/core/unifi/usw_influx.go similarity index 98% rename from core/unifi/usw.go rename to core/unifi/usw_influx.go index 760ba833..94a7f9df 100644 --- a/core/unifi/usw.go +++ b/core/unifi/usw_influx.go @@ -18,7 +18,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, diff --git a/core/unifi/usw_type.go b/core/unifi/usw_type.go index 7fa68de4..a0d6485c 100644 --- a/core/unifi/usw_type.go +++ b/core/unifi/usw_type.go @@ -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"` From 1fe1295a0e86cbdc7184c9b4b31d4cbc54a83887 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 17 Apr 2019 00:54:54 -0700 Subject: [PATCH 2/4] Split getController --- core/unifi/unifi.go | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/core/unifi/unifi.go b/core/unifi/unifi.go index 2ba47653..90601923 100644 --- a/core/unifi/unifi.go +++ b/core/unifi/unifi.go @@ -9,6 +9,7 @@ import ( "bytes" "crypto/tls" "encoding/json" + "io" "io/ioutil" "net/http" "net/http/cookiejar" @@ -21,36 +22,38 @@ import ( // Used to make additional, authenticated requests to the APIs. // Start here. func GetController(user, pass, url string, verifySSL bool) (*Unifi, error) { + u := &Unifi{baseURL: strings.TrimRight(url, "/")} 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)") } + jar, err := cookiejar.New(nil) + if err != nil { + return nil, errors.Wrap(err, "cookiejar.New(nil)") + } + u.Client = &http.Client{ + Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifySSL}}, + Jar: jar, + } + return u, u.getController(req) +} + +// getController is a helper method to make testsing a bit easier. +func (u *Unifi) getController(req *http.Request) error { resp, err := u.Do(req) if err != nil { - return u, errors.Wrap(err, "authReq.Do(req)") + 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 u, errors.Errorf("authentication failed (%v): %v (status: %v/%v)", - user, url+LoginPath, resp.StatusCode, resp.Status) + return errors.Errorf("authentication failed: %v (status: %v/%v)", + u.baseURL+LoginPath, resp.StatusCode, resp.Status) } - return u, nil + return nil } // GetClients returns a response full of clients' data from the Unifi Controller. From d0a146022651eeca544a43d0240d640fe3b408d4 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 17 Apr 2019 01:15:22 -0700 Subject: [PATCH 3/4] Rename the init func. --- core/unifi/README.md | 2 +- core/unifi/unifi.go | 4 ++-- core/unifi/unifi_test.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/unifi/README.md b/core/unifi/README.md index cbe77503..5ea77454 100644 --- a/core/unifi/README.md +++ b/core/unifi/README.md @@ -23,7 +23,7 @@ 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) } diff --git a/core/unifi/unifi.go b/core/unifi/unifi.go index 2267f4e6..0422a529 100644 --- a/core/unifi/unifi.go +++ b/core/unifi/unifi.go @@ -19,10 +19,10 @@ import ( "github.com/pkg/errors" ) -// GetController creates a http.Client with authenticated cookies. +// NewUnifi creates a http.Client with authenticated cookies. // Used to make additional, authenticated requests to the APIs. // Start here. -func GetController(user, pass, url string, verifySSL bool) (*Unifi, error) { +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)") diff --git a/core/unifi/unifi_test.go b/core/unifi/unifi_test.go index 35c533c6..dff7687d 100644 --- a/core/unifi/unifi_test.go +++ b/core/unifi/unifi_test.go @@ -8,11 +8,11 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAuthController(t *testing.T) { +func TestNewUnifi(t *testing.T) { t.Parallel() a := assert.New(t) url := "http://127.0.0.1:64431" - authReq, err := GetController("user1", "pass2", url, false) + 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.") From ac156bcd1231153f53d320340ad307620c581b1f Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Wed, 17 Apr 2019 02:06:06 -0700 Subject: [PATCH 4/4] a little more cleanup --- core/unifi/clients_influx.go | 16 ++++++++-------- core/unifi/parsers.go | 29 +++++++++++------------------ core/unifi/uap_influx.go | 3 +-- core/unifi/usg_influx.go | 5 ++--- core/unifi/usw_influx.go | 7 +++---- 5 files changed, 25 insertions(+), 35 deletions(-) diff --git a/core/unifi/clients_influx.go b/core/unifi/clients_influx.go index 51e0c8e2..70adf58f 100644 --- a/core/unifi/clients_influx.go +++ b/core/unifi/clients_influx.go @@ -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 } diff --git a/core/unifi/parsers.go b/core/unifi/parsers.go index 5b324282..70c2d193 100644 --- a/core/unifi/parsers.go +++ b/core/unifi/parsers.go @@ -7,47 +7,40 @@ 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 { + assetType := "" + if o := make(map[string]interface{}); u.unmarshalDevice("map", r, &o) != nil { continue - } - assetType := "" - if t, ok := obj["type"].(string); ok { + } 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 u.unmarshalDevice(assetType, &uap, r) == nil { + 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 u.unmarshalDevice(assetType, &usg, r) == nil { + if usg := (USG{}); u.unmarshalDevice(assetType, r, &usg) == nil { devices.USGs = append(devices.USGs, usg) } case "usw": - if u.unmarshalDevice(assetType, &usw, r) == nil { + if usw := (USW{}); u.unmarshalDevice(assetType, r, &usw) == nil { devices.USWs = append(devices.USWs, usw) } default: u.eLogf("unknown asset type - %v - skipping", assetType) - continue } } 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) +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 := payload.MarshalJSON() + 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 -==") diff --git a/core/unifi/uap_influx.go b/core/unifi/uap_influx.go index 36789de8..7ae5affa 100644 --- a/core/unifi/uap_influx.go +++ b/core/unifi/uap_influx.go @@ -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, @@ -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, diff --git a/core/unifi/usg_influx.go b/core/unifi/usg_influx.go index 9b169e54..a2a52779 100644 --- a/core/unifi/usg_influx.go +++ b/core/unifi/usg_influx.go @@ -10,7 +10,6 @@ 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, @@ -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, @@ -183,5 +182,5 @@ func (u USG) Points() ([]*influx.Point, error) { } points = append(points, pt) } - return points, err + return points, nil } diff --git a/core/unifi/usw_influx.go b/core/unifi/usw_influx.go index 94a7f9df..810148de 100644 --- a/core/unifi/usw_influx.go +++ b/core/unifi/usw_influx.go @@ -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, @@ -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 }