diff --git a/core/unifi/.travis.yml b/core/unifi/.travis.yml index b0d7f087..24c1ed7e 100644 --- a/core/unifi/.travis.yml +++ b/core/unifi/.travis.yml @@ -7,7 +7,7 @@ before_install: - curl -sLo $GOPATH/bin/dep https://github.com/golang/dep/releases/download/v0.5.3/dep-linux-amd64 - chmod +x $GOPATH/bin/dep # download super-linter: golangci-lint -- curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin latest +- curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.16.0 install: - dep ensure script: diff --git a/core/unifi/README.md b/core/unifi/README.md index 177519ef..c0cf00e9 100644 --- a/core/unifi/README.md +++ b/core/unifi/README.md @@ -45,8 +45,8 @@ func main() { } log.Println(len(sites), "Unifi Sites Found: ", sites) - log.Println(len(clients.UCLs), "Clients connected:") - for i, client := range clients.UCLs { + log.Println(len(clients), "Clients connected:") + for i, client := range clients { log.Println(i+1, client.ID, client.Hostname, client.IP, client.Name, client.LastSeen) } diff --git a/core/unifi/clients_influx.go b/core/unifi/clients_influx.go index 9219c785..e59bbcf7 100644 --- a/core/unifi/clients_influx.go +++ b/core/unifi/clients_influx.go @@ -9,7 +9,7 @@ import ( // Points generates Unifi Client datapoints for InfluxDB. // These points can be passed directly to influx. -func (c UCL) Points() ([]*influx.Point, error) { +func (c Client) Points() ([]*influx.Point, error) { // Fix name and hostname fields. Sometimes one or the other is blank. switch { case c.Hostname == "" && c.Name == "": diff --git a/core/unifi/clients_type.go b/core/unifi/clients_type.go index d0a21c04..662a4ca6 100644 --- a/core/unifi/clients_type.go +++ b/core/unifi/clients_type.go @@ -1,7 +1,10 @@ package unifi -// UCL defines all the data a connected-network client contains. -type UCL struct { +// Clients contains a list that contains all of the unifi clients from a controller. +type Clients []Client + +// Client defines all the data a connected-network client contains. +type Client struct { ID string `json:"_id"` IsGuestByUAP FlexBool `json:"_is_guest_by_uap"` IsGuestByUGW FlexBool `json:"_is_guest_by_ugw"` diff --git a/core/unifi/parsers.go b/core/unifi/parsers.go index 672187ea..a2b97ce5 100644 --- a/core/unifi/parsers.go +++ b/core/unifi/parsers.go @@ -13,7 +13,7 @@ func (u *Unifi) parseDevices(data []json.RawMessage, siteName string) *Devices { } else if t, ok := o["type"].(string); ok { assetType = t } - u.dLogf("Unmarshalling Device Type: %v", assetType) + u.dLogf("Unmarshalling Device Type: %v, site %s ", assetType, siteName) // Choose which type to unmarshal into based on the "type" json key. switch assetType { // Unmarshal again into the correct type.. case "uap": diff --git a/core/unifi/site_influx.go b/core/unifi/site_influx.go new file mode 100644 index 00000000..bd219ba9 --- /dev/null +++ b/core/unifi/site_influx.go @@ -0,0 +1,81 @@ +package unifi + +import ( + "strings" + "time" + + influx "github.com/influxdata/influxdb1-client/v2" +) + +// Points generates Unifi Sites' datapoints for InfluxDB. +// These points can be passed directly to influx. +func (u Site) Points() ([]*influx.Point, error) { + points := []*influx.Point{} + for _, s := range u.Health { + tags := map[string]string{ + "id": u.ID, + "name": u.Name, + "desc": u.Desc, + "status": s.Status, + "subsystem": s.Subsystem, + "wan_ip": s.WanIP, + "netmask": s.Netmask, + "gw_name": s.GwName, + "gw_mac": s.GwMac, + "gw_version": s.GwVersion, + "speedtest_status": s.SpeedtestStatus, + "lan_ip": s.LanIP, + "remote_user_enabled": s.RemoteUserEnabled.Txt, + "site_to_site_enabled": s.SiteToSiteEnabled.Txt, + "nameservers": strings.Join(s.Nameservers, ","), + "gateways": strings.Join(s.Gateways, ","), + "num_new_alarms": u.NumNewAlarms.Txt, + "attr_hidden_id": u.AttrHiddenID, + "attr_no_delete": u.AttrNoDelete.Txt, + } + fields := map[string]interface{}{ + "attr_hidden_id": u.AttrHiddenID, + "attr_no_delete": u.AttrNoDelete.Val, + "num_user": s.NumUser.Val, + "num_guest": s.NumGuest.Val, + "num_iot": s.NumIot.Val, + "tx_bytes-r": s.TxBytesR.Val, + "rx_bytes-r": s.RxBytesR.Val, + "status": s.Status, + "num_ap": s.NumAp.Val, + "num_adopted": s.NumAdopted.Val, + "num_disabled": s.NumDisabled.Val, + "num_disconnected": s.NumDisconnected.Val, + "num_pending": s.NumPending.Val, + "num_gw": s.NumGw.Val, + "wan_ip": s.WanIP, + "num_sta": s.NumSta.Val, + "gw_cpu": s.GwSystemStats.CPU.Val, + "gw_mem": s.GwSystemStats.Mem.Val, + "gw_uptime": s.GwSystemStats.Uptime.Val, + "latency": s.Latency.Val, + "uptime": s.Uptime.Val, + "drops": s.Drops.Val, + "xput_up": s.XputUp.Val, + "xput_down": s.XputDown.Val, + "speedtest_ping": s.SpeedtestPing.Val, + "speedtest_lastrun": s.SpeedtestLastrun.Val, + "num_sw": s.NumSw.Val, + "remote_user_num_active": s.RemoteUserNumActive.Val, + "remote_user_num_inactive": s.RemoteUserNumInactive.Val, + "remote_user_rx_bytes": s.RemoteUserRxBytes.Val, + "remote_user_tx_bytes": s.RemoteUserTxBytes.Val, + "remote_user_rx_packets": s.RemoteUserRxPackets.Val, + "remote_user_tx_packets": s.RemoteUserTxPackets.Val, + "num_new_alarms": u.NumNewAlarms.Val, + "nameservers": len(s.Nameservers), + "gateways": len(s.Gateways), + } + pt, err := influx.NewPoint("subsystems", tags, fields, time.Now()) + if err != nil { + return points, err + } + points = append(points, pt) + } + return points, nil +} diff --git a/core/unifi/site_type.go b/core/unifi/site_type.go new file mode 100644 index 00000000..23eba62c --- /dev/null +++ b/core/unifi/site_type.go @@ -0,0 +1,60 @@ +package unifi + +// Sites is a struct to match Devices and Clients. +type Sites []Site + +// Site represents a site's data. +type Site struct { + ID string `json:"_id"` + Name string `json:"name"` + Desc string `json:"desc"` + AttrHiddenID string `json:"attr_hidden_id"` + AttrNoDelete FlexBool `json:"attr_no_delete"` + Health []struct { + Subsystem string `json:"subsystem"` + NumUser FlexInt `json:"num_user,omitempty"` + NumGuest FlexInt `json:"num_guest,omitempty"` + NumIot FlexInt `json:"num_iot,omitempty"` + TxBytesR FlexInt `json:"tx_bytes-r,omitempty"` + RxBytesR FlexInt `json:"rx_bytes-r,omitempty"` + Status string `json:"status"` + NumAp FlexInt `json:"num_ap,omitempty"` + NumAdopted FlexInt `json:"num_adopted,omitempty"` + NumDisabled FlexInt `json:"num_disabled,omitempty"` + NumDisconnected FlexInt `json:"num_disconnected,omitempty"` + NumPending FlexInt `json:"num_pending,omitempty"` + NumGw FlexInt `json:"num_gw,omitempty"` + WanIP string `json:"wan_ip,omitempty"` + Gateways []string `json:"gateways,omitempty"` + Netmask string `json:"netmask,omitempty"` + Nameservers []string `json:"nameservers,omitempty"` + NumSta FlexInt `json:"num_sta,omitempty"` + GwMac string `json:"gw_mac,omitempty"` + GwName string `json:"gw_name,omitempty"` + GwSystemStats struct { + CPU FlexInt `json:"cpu"` + Mem FlexInt `json:"mem"` + Uptime FlexInt `json:"uptime"` + } `json:"gw_system-stats,omitempty"` + GwVersion string `json:"gw_version,omitempty"` + Latency FlexInt `json:"latency,omitempty"` + Uptime FlexInt `json:"uptime,omitempty"` + Drops FlexInt `json:"drops,omitempty"` + XputUp FlexInt `json:"xput_up,omitempty"` + XputDown FlexInt `json:"xput_down,omitempty"` + SpeedtestStatus string `json:"speedtest_status,omitempty"` + SpeedtestLastrun FlexInt `json:"speedtest_lastrun,omitempty"` + SpeedtestPing FlexInt `json:"speedtest_ping,omitempty"` + LanIP string `json:"lan_ip,omitempty"` + NumSw FlexInt `json:"num_sw,omitempty"` + RemoteUserEnabled FlexBool `json:"remote_user_enabled,omitempty"` + RemoteUserNumActive FlexInt `json:"remote_user_num_active,omitempty"` + RemoteUserNumInactive FlexInt `json:"remote_user_num_inactive,omitempty"` + RemoteUserRxBytes FlexInt `json:"remote_user_rx_bytes,omitempty"` + RemoteUserTxBytes FlexInt `json:"remote_user_tx_bytes,omitempty"` + RemoteUserRxPackets FlexInt `json:"remote_user_rx_packets,omitempty"` + RemoteUserTxPackets FlexInt `json:"remote_user_tx_packets,omitempty"` + SiteToSiteEnabled FlexBool `json:"site_to_site_enabled,omitempty"` + } `json:"health"` + NumNewAlarms FlexInt `json:"num_new_alarms"` +} diff --git a/core/unifi/types.go b/core/unifi/types.go index 61266609..a2795ade 100644 --- a/core/unifi/types.go +++ b/core/unifi/types.go @@ -12,8 +12,10 @@ import ( // This is a list of unifi API paths. // The %s in each string must be replaced with a Site.Name. const ( + // StatusPath shows Controller version. + StatusPath string = "/status" // SiteList is the path to the api site list. - SiteList string = "/api/self/sites" + SiteList string = "/api/stat/sites" // ClientPath is Unifi Clients API Path ClientPath string = "/api/s/%s/stat/sta" // DevicePath is where we get data about Unifi devices. @@ -52,11 +54,6 @@ type Devices struct { 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. @@ -67,11 +64,11 @@ type Unifi struct { 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"` +// Server is the /status endpoint from the Unifi controller. +type Server struct { + Up FlexBool `json:"up"` + ServerVersion string `json:"server_version"` + UUID string `json:"uuid"` } // FlexInt provides a container and unmarshalling for fields that may be @@ -109,12 +106,15 @@ type FlexBool struct { 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. +// UnmarshalJSON method converts armed/disarmed, yes/no, active/inactive or 0/1 to true/false. +// Really it converts ready, ok, 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), `"`) + if f.Txt = strings.Trim(string(b), `"`); f.Txt == "" { + f.Txt = "false" + } 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") + strings.EqualFold(f.Txt, "enabled") || strings.EqualFold(f.Txt, "ready") || strings.EqualFold(f.Txt, "up") || + strings.EqualFold(f.Txt, "ok") return nil } diff --git a/core/unifi/unifi.go b/core/unifi/unifi.go index b7e45f6c..aacd9cf3 100644 --- a/core/unifi/unifi.go +++ b/core/unifi/unifi.go @@ -58,15 +58,23 @@ func (u *Unifi) getController(user, pass string) error { return nil } +// GetServer returns the controller's version and UUID. +func (u *Unifi) GetServer() (Server, error) { + var response struct { + Data Server `json:"meta"` + } + err := u.GetData(StatusPath, &response) + return response.Data, err +} + // GetClients returns a response full of clients' data from the Unifi Controller. -func (u *Unifi) GetClients(sites []Site) (*Clients, error) { - data := make([]UCL, 0) +func (u *Unifi) GetClients(sites []Site) (Clients, error) { + data := make([]Client, 0) for _, site := range sites { var response struct { - Data []UCL `json:"data"` + Data []Client `json:"data"` } - u.dLogf("Polling Site '%s' (%s) Clients", site.Name, site.Desc) - u.dLogf("Unmarshalling Device Type: ucl") + u.dLogf("Polling Controller, retreiving Unifi Clients, site %s (%s) ", site.Name, site.Desc) clientPath := fmt.Sprintf(ClientPath, site.Name) if err := u.GetData(clientPath, &response); err != nil { return nil, err @@ -76,14 +84,13 @@ func (u *Unifi) GetClients(sites []Site) (*Clients, error) { } data = append(data, response.Data...) } - return &Clients{UCLs: data}, nil + return data, nil } // GetDevices returns a response full of devices' data from the Unifi Controller. func (u *Unifi) GetDevices(sites []Site) (*Devices, error) { devices := new(Devices) for _, site := range sites { - u.dLogf("Polling Site '%s' (%s) Devices", site.Name, site.Desc) var response struct { Data []json.RawMessage `json:"data"` } @@ -116,7 +123,7 @@ func (u *Unifi) GetDevices(sites []Site) (*Devices, error) { } // GetSites returns a list of configured sites on the Unifi controller. -func (u *Unifi) GetSites() ([]Site, error) { +func (u *Unifi) GetSites() (Sites, error) { var response struct { Data []Site `json:"data"` } @@ -127,7 +134,7 @@ func (u *Unifi) GetSites() ([]Site, error) { for i := range response.Data { sites = append(sites, response.Data[i].Name) } - u.dLogf("Found %d sites: %s", len(sites), strings.Join(sites, ",")) + u.dLogf("Found %d site(s): %s", len(sites), strings.Join(sites, ",")) return response.Data, nil }