Merge branch 'master' of ../inputunifi into merge-them-all
This commit is contained in:
commit
deb94ac251
|
|
@ -0,0 +1,9 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.16.x
|
||||
before_install:
|
||||
# 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
|
||||
script:
|
||||
- golangci-lint run --enable-all -D nlreturn,exhaustivestruct
|
||||
- go test ./...
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT LICENSE.
|
||||
Copyright (c) 2018-2021 David Newhall II
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# inputunifi
|
||||
|
||||
## UnPoller Input Plugin
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package inputunifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/unpoller/unifi"
|
||||
"github.com/unpoller/webserver"
|
||||
)
|
||||
|
||||
/* Event collection. Events are also sent to the webserver for display. */
|
||||
|
||||
func (u *InputUnifi) collectControllerEvents(c *Controller) ([]interface{}, error) {
|
||||
if u.isNill(c) {
|
||||
u.Logf("Re-authenticating to UniFi Controller: %s", c.URL)
|
||||
|
||||
if err := u.getUnifi(c); err != nil {
|
||||
return nil, fmt.Errorf("re-authenticating to %s: %w", c.URL, err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
logs = []interface{}{}
|
||||
newLogs []interface{}
|
||||
)
|
||||
|
||||
// Get the sites we care about.
|
||||
sites, err := u.getFilteredSites(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unifi.GetSites(): %w", err)
|
||||
}
|
||||
|
||||
type caller func([]interface{}, []*unifi.Site, *Controller) ([]interface{}, error)
|
||||
|
||||
for _, call := range []caller{u.collectIDS, u.collectAnomalies, u.collectAlarms, u.collectEvents} {
|
||||
if newLogs, err = call(logs, sites, c); err != nil {
|
||||
return logs, err
|
||||
}
|
||||
|
||||
logs = append(logs, newLogs...)
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (u *InputUnifi) collectAlarms(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) {
|
||||
if *c.SaveAlarms {
|
||||
for _, s := range sites {
|
||||
events, err := c.Unifi.GetAlarmsSite(s)
|
||||
if err != nil {
|
||||
return logs, fmt.Errorf("unifi.GetAlarms(): %w", err)
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
logs = append(logs, e)
|
||||
|
||||
webserver.NewInputEvent(PluginName, s.ID+"_alarms", &webserver.Event{
|
||||
Ts: e.Datetime, Msg: e.Msg, Tags: map[string]string{
|
||||
"type": "alarm", "key": e.Key, "site_id": e.SiteID,
|
||||
"site_name": e.SiteName, "source": e.SourceName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (u *InputUnifi) collectAnomalies(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) {
|
||||
if *c.SaveAnomal {
|
||||
for _, s := range sites {
|
||||
events, err := c.Unifi.GetAnomaliesSite(s)
|
||||
if err != nil {
|
||||
return logs, fmt.Errorf("unifi.GetAnomalies(): %w", err)
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
logs = append(logs, e)
|
||||
|
||||
webserver.NewInputEvent(PluginName, s.ID+"_anomalies", &webserver.Event{
|
||||
Ts: e.Datetime, Msg: e.Anomaly, Tags: map[string]string{
|
||||
"type": "anomaly", "site_name": e.SiteName, "source": e.SourceName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (u *InputUnifi) collectEvents(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) {
|
||||
if *c.SaveEvents {
|
||||
for _, s := range sites {
|
||||
events, err := c.Unifi.GetSiteEvents(s, time.Hour)
|
||||
if err != nil {
|
||||
return logs, fmt.Errorf("unifi.GetEvents(): %w", err)
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
e := redactEvent(e, c.HashPII)
|
||||
logs = append(logs, e)
|
||||
|
||||
webserver.NewInputEvent(PluginName, s.ID+"_events", &webserver.Event{
|
||||
Msg: e.Msg, Ts: e.Datetime, Tags: map[string]string{
|
||||
"type": "event", "key": e.Key, "site_id": e.SiteID,
|
||||
"site_name": e.SiteName, "source": e.SourceName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (u *InputUnifi) collectIDS(logs []interface{}, sites []*unifi.Site, c *Controller) ([]interface{}, error) {
|
||||
if *c.SaveIDS {
|
||||
for _, s := range sites {
|
||||
events, err := c.Unifi.GetIDSSite(s)
|
||||
if err != nil {
|
||||
return logs, fmt.Errorf("unifi.GetIDS(): %w", err)
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
logs = append(logs, e)
|
||||
|
||||
webserver.NewInputEvent(PluginName, s.ID+"_ids", &webserver.Event{
|
||||
Ts: e.Datetime, Msg: e.Msg, Tags: map[string]string{
|
||||
"type": "ids", "key": e.Key, "site_id": e.SiteID,
|
||||
"site_name": e.SiteName, "source": e.SourceName,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// redactEvent attempts to mask personally identying information from log messages.
|
||||
// This currently misses the "msg" value entirely and leaks PII information.
|
||||
func redactEvent(e *unifi.Event, hash *bool) *unifi.Event {
|
||||
if !*hash {
|
||||
return e
|
||||
}
|
||||
|
||||
// metrics.Events[i].Msg <-- not sure what to do here.
|
||||
e.DestIPGeo = unifi.IPGeo{}
|
||||
e.SourceIPGeo = unifi.IPGeo{}
|
||||
e.Host = RedactNamePII(e.Host, hash)
|
||||
e.Hostname = RedactNamePII(e.Hostname, hash)
|
||||
e.DstMAC = RedactMacPII(e.DstMAC, hash)
|
||||
e.SrcMAC = RedactMacPII(e.SrcMAC, hash)
|
||||
|
||||
return e
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
package inputunifi
|
||||
|
||||
// nolint: gosec
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/unpoller/poller"
|
||||
"github.com/unpoller/unifi"
|
||||
)
|
||||
|
||||
var ErrScrapeFilterMatchFailed = fmt.Errorf("scrape filter match failed, and filter is not http URL")
|
||||
|
||||
func (u *InputUnifi) isNill(c *Controller) bool {
|
||||
u.RLock()
|
||||
defer u.RUnlock()
|
||||
|
||||
return c.Unifi == nil
|
||||
}
|
||||
|
||||
// newDynamicCntrlr creates and saves a controller definition for further use.
|
||||
// This is called when an unconfigured controller is requested.
|
||||
func (u *InputUnifi) newDynamicCntrlr(url string) (bool, *Controller) {
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
|
||||
if c := u.dynamic[url]; c != nil {
|
||||
// it already exists.
|
||||
return false, c
|
||||
}
|
||||
|
||||
ccopy := u.Default // copy defaults into new controller
|
||||
u.dynamic[url] = &ccopy
|
||||
u.dynamic[url].URL = url
|
||||
|
||||
return true, u.dynamic[url]
|
||||
}
|
||||
|
||||
func (u *InputUnifi) dynamicController(filter *poller.Filter) (*poller.Metrics, error) {
|
||||
if !strings.HasPrefix(filter.Path, "http") {
|
||||
return nil, ErrScrapeFilterMatchFailed
|
||||
}
|
||||
|
||||
newCntrlr, c := u.newDynamicCntrlr(filter.Path)
|
||||
|
||||
if newCntrlr {
|
||||
u.Logf("Authenticating to Dynamic UniFi Controller: %s", filter.Path)
|
||||
|
||||
if err := u.getUnifi(c); err != nil {
|
||||
u.logController(c)
|
||||
return nil, fmt.Errorf("authenticating to %s: %w", filter.Path, err)
|
||||
}
|
||||
|
||||
u.logController(c)
|
||||
}
|
||||
|
||||
return u.collectController(c)
|
||||
}
|
||||
|
||||
func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) {
|
||||
if u.isNill(c) {
|
||||
u.Logf("Re-authenticating to UniFi Controller: %s", c.URL)
|
||||
|
||||
if err := u.getUnifi(c); err != nil {
|
||||
return nil, fmt.Errorf("re-authenticating to %s: %w", c.URL, err)
|
||||
}
|
||||
}
|
||||
|
||||
metrics, err := u.pollController(c)
|
||||
if err != nil {
|
||||
u.Logf("Re-authenticating to UniFi Controller: %s", c.URL)
|
||||
|
||||
if err := u.getUnifi(c); err != nil {
|
||||
return metrics, fmt.Errorf("re-authenticating to %s: %w", c.URL, err)
|
||||
}
|
||||
}
|
||||
|
||||
return metrics, err
|
||||
}
|
||||
|
||||
//nolint:cyclop
|
||||
func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
|
||||
u.RLock()
|
||||
defer u.RUnlock()
|
||||
|
||||
// Get the sites we care about.
|
||||
sites, err := u.getFilteredSites(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unifi.GetSites(): %w", err)
|
||||
}
|
||||
|
||||
m := &Metrics{TS: time.Now(), Sites: sites}
|
||||
defer updateWeb(c, m)
|
||||
|
||||
if c.SaveRogue != nil && *c.SaveRogue {
|
||||
if m.RogueAPs, err = c.Unifi.GetRogueAPs(sites); err != nil {
|
||||
return nil, fmt.Errorf("unifi.GetRogueAPs(%s): %w", c.URL, err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.SaveDPI != nil && *c.SaveDPI {
|
||||
if m.SitesDPI, err = c.Unifi.GetSiteDPI(sites); err != nil {
|
||||
return nil, fmt.Errorf("unifi.GetSiteDPI(%s): %w", c.URL, err)
|
||||
}
|
||||
|
||||
if m.ClientsDPI, err = c.Unifi.GetClientsDPI(sites); err != nil {
|
||||
return nil, fmt.Errorf("unifi.GetClientsDPI(%s): %w", c.URL, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all the points.
|
||||
if m.Clients, err = c.Unifi.GetClients(sites); err != nil {
|
||||
return nil, fmt.Errorf("unifi.GetClients(%s): %w", c.URL, err)
|
||||
}
|
||||
|
||||
if m.Devices, err = c.Unifi.GetDevices(sites); err != nil {
|
||||
return nil, fmt.Errorf("unifi.GetDevices(%s): %w", c.URL, err)
|
||||
}
|
||||
|
||||
return u.augmentMetrics(c, m), nil
|
||||
}
|
||||
|
||||
// augmentMetrics is our middleware layer between collecting metrics and writing them.
|
||||
// This is where we can manipuate the returned data or make arbitrary decisions.
|
||||
// This method currently adds parent device names to client metrics and hashes PII.
|
||||
// This method also converts our local *Metrics type into a slice of interfaces for poller.
|
||||
func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Metrics {
|
||||
if metrics == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m, devices, bssdIDs := extractDevices(metrics)
|
||||
|
||||
// These come blank, so set them here.
|
||||
for _, client := range metrics.Clients {
|
||||
if devices[client.Mac] = client.Name; client.Name == "" {
|
||||
devices[client.Mac] = client.Hostname
|
||||
}
|
||||
|
||||
client.Mac = RedactMacPII(client.Mac, c.HashPII)
|
||||
client.Name = RedactNamePII(client.Name, c.HashPII)
|
||||
client.Hostname = RedactNamePII(client.Hostname, c.HashPII)
|
||||
client.SwName = devices[client.SwMac]
|
||||
client.ApName = devices[client.ApMac]
|
||||
client.GwName = devices[client.GwMac]
|
||||
client.RadioDescription = bssdIDs[client.Bssid] + client.RadioProto
|
||||
m.Clients = append(m.Clients, client)
|
||||
}
|
||||
|
||||
for _, client := range metrics.ClientsDPI {
|
||||
// Name on Client DPI data also comes blank, find it based on MAC address.
|
||||
client.Name = devices[client.MAC]
|
||||
if client.Name == "" {
|
||||
client.Name = client.MAC
|
||||
}
|
||||
|
||||
client.Name = RedactNamePII(client.Name, c.HashPII)
|
||||
client.MAC = RedactMacPII(client.MAC, c.HashPII)
|
||||
m.ClientsDPI = append(m.ClientsDPI, client)
|
||||
}
|
||||
|
||||
for _, ap := range metrics.RogueAPs {
|
||||
// XXX: do we need augment this data?
|
||||
m.RogueAPs = append(m.RogueAPs, ap)
|
||||
}
|
||||
|
||||
if *c.SaveSites {
|
||||
for _, site := range metrics.Sites {
|
||||
m.Sites = append(m.Sites, site)
|
||||
}
|
||||
|
||||
for _, site := range metrics.SitesDPI {
|
||||
m.SitesDPI = append(m.SitesDPI, site)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// this is a helper function for augmentMetrics.
|
||||
func extractDevices(metrics *Metrics) (*poller.Metrics, map[string]string, map[string]string) {
|
||||
m := &poller.Metrics{TS: metrics.TS}
|
||||
devices := make(map[string]string)
|
||||
bssdIDs := make(map[string]string)
|
||||
|
||||
for _, r := range metrics.Devices.UAPs {
|
||||
devices[r.Mac] = r.Name
|
||||
m.Devices = append(m.Devices, r)
|
||||
|
||||
for _, v := range r.VapTable {
|
||||
bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName)
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range metrics.Devices.USGs {
|
||||
devices[r.Mac] = r.Name
|
||||
m.Devices = append(m.Devices, r)
|
||||
}
|
||||
|
||||
for _, r := range metrics.Devices.USWs {
|
||||
devices[r.Mac] = r.Name
|
||||
m.Devices = append(m.Devices, r)
|
||||
}
|
||||
|
||||
for _, r := range metrics.Devices.UDMs {
|
||||
devices[r.Mac] = r.Name
|
||||
m.Devices = append(m.Devices, r)
|
||||
}
|
||||
|
||||
for _, r := range metrics.Devices.UXGs {
|
||||
devices[r.Mac] = r.Name
|
||||
m.Devices = append(m.Devices, r)
|
||||
}
|
||||
|
||||
return m, devices, bssdIDs
|
||||
}
|
||||
|
||||
// RedactNamePII converts a name string to an md5 hash (first 24 chars only).
|
||||
// Useful for maskiing out personally identifying information.
|
||||
func RedactNamePII(pii string, hash *bool) string {
|
||||
if hash == nil || !*hash || pii == "" {
|
||||
return pii
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
|
||||
// instead of 32 characters, only use 24.
|
||||
return s[:24]
|
||||
}
|
||||
|
||||
// RedactMacPII converts a MAC address to an md5 hashed version (first 14 chars only).
|
||||
// Useful for maskiing out personally identifying information.
|
||||
func RedactMacPII(pii string, hash *bool) (output string) {
|
||||
if hash == nil || !*hash || pii == "" {
|
||||
return pii
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
|
||||
// This formats a "fake" mac address looking string.
|
||||
return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", s[:2], s[2:4], s[4:6], s[6:8], s[8:10], s[10:12], s[12:14])
|
||||
}
|
||||
|
||||
// getFilteredSites returns a list of sites to fetch data for.
|
||||
// Omits requested but unconfigured sites. Grabs the full list from the
|
||||
// controller and returns the sites provided in the config file.
|
||||
func (u *InputUnifi) getFilteredSites(c *Controller) ([]*unifi.Site, error) {
|
||||
u.RLock()
|
||||
defer u.RUnlock()
|
||||
|
||||
sites, err := c.Unifi.GetSites()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("controller: %w", err)
|
||||
} else if len(c.Sites) == 0 || StringInSlice("all", c.Sites) {
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
i := 0
|
||||
|
||||
for _, s := range sites {
|
||||
// Only include valid sites in the request filter.
|
||||
if StringInSlice(s.Name, c.Sites) {
|
||||
sites[i] = s
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return sites[:i], nil
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
module github.com/unpoller/inputunifi
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28
|
||||
github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80
|
||||
github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
|
||||
)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c h1:zqmyTlQyufRC65JnImJ6H1Sf7BDj8bG31EV919NVEQc=
|
||||
github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28 h1:YAv5naMdpOFahnxteFFRidZlrSEwLv8V2nBKJKmLmHg=
|
||||
github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28/go.mod h1:AbDp60t5WlLSRELAliMJ0RFQpm/0yXpyolVSZqNtero=
|
||||
github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80 h1:XjHGfJhMwnB63DYHgtWGJgDxLhxVcAOtf+cfuvpGoyo=
|
||||
github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80/go.mod h1:K9QFFGfZws4gzB+Popix19S/rBKqrtqI+tyPORyg3F0=
|
||||
github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf h1:HhXi3qca3kyFEFPh0mqdr0bpQs94hJvMbUJztwPtf2A=
|
||||
github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf/go.mod h1:77PywuUvspdtoRuH1htFhR3Tp0pLyWj6kJlYR4tBYho=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golift.io/cnfg v0.0.7 h1:qkNpP5Bq+5Gtoc6HcI8kapMD5zFOVan6qguxqBQF3OY=
|
||||
golift.io/cnfg v0.0.7/go.mod h1:AsB0DJe7nv0bizKaoy3e3MjjOF7upTpMOMvsfv4CNNk=
|
||||
golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE=
|
||||
golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
// Package inputunifi implements the poller.Input interface and bridges the gap between
|
||||
// metrics from the unifi library, and the augments required to pump them into unifi-poller.
|
||||
package inputunifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/unpoller/poller"
|
||||
"github.com/unpoller/unifi"
|
||||
)
|
||||
|
||||
// PluginName is the name of this input plugin.
|
||||
const PluginName = "unifi"
|
||||
|
||||
const (
|
||||
defaultURL = "https://127.0.0.1:8443"
|
||||
defaultUser = "unifipoller"
|
||||
defaultPass = "unifipoller"
|
||||
defaultSite = "all"
|
||||
)
|
||||
|
||||
// InputUnifi contains the running data.
|
||||
type InputUnifi struct {
|
||||
*Config `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"`
|
||||
dynamic map[string]*Controller
|
||||
sync.Mutex // to lock the map above.
|
||||
Logger poller.Logger
|
||||
}
|
||||
|
||||
// Controller represents the configuration for a UniFi Controller.
|
||||
// Each polled controller may have its own configuration.
|
||||
type Controller struct {
|
||||
VerifySSL *bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"`
|
||||
SaveAnomal *bool `json:"save_anomalies" toml:"save_anomalies" xml:"save_anomalies" yaml:"save_anomalies"`
|
||||
SaveAlarms *bool `json:"save_alarms" toml:"save_alarms" xml:"save_alarms" yaml:"save_alarms"`
|
||||
SaveEvents *bool `json:"save_events" toml:"save_events" xml:"save_events" yaml:"save_events"`
|
||||
SaveIDS *bool `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"`
|
||||
SaveDPI *bool `json:"save_dpi" toml:"save_dpi" xml:"save_dpi" yaml:"save_dpi"`
|
||||
SaveRogue *bool `json:"save_rogue" toml:"save_rogue" xml:"save_rogue" yaml:"save_rogue"`
|
||||
HashPII *bool `json:"hash_pii" toml:"hash_pii" xml:"hash_pii" yaml:"hash_pii"`
|
||||
SaveSites *bool `json:"save_sites" toml:"save_sites" xml:"save_sites" yaml:"save_sites"`
|
||||
CertPaths []string `json:"ssl_cert_paths" toml:"ssl_cert_paths" xml:"ssl_cert_path" yaml:"ssl_cert_paths"`
|
||||
User string `json:"user" toml:"user" xml:"user" yaml:"user"`
|
||||
Pass string `json:"pass" toml:"pass" xml:"pass" yaml:"pass"`
|
||||
URL string `json:"url" toml:"url" xml:"url" yaml:"url"`
|
||||
Sites []string `json:"sites" toml:"sites" xml:"site" yaml:"sites"`
|
||||
Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"`
|
||||
ID string `json:"id,omitempty"` // this is an output, not an input.
|
||||
}
|
||||
|
||||
// Config contains our configuration data.
|
||||
type Config struct {
|
||||
sync.RWMutex // locks the Unifi struct member when re-authing to unifi.
|
||||
Default Controller `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"`
|
||||
Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"`
|
||||
Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"`
|
||||
Controllers []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"`
|
||||
}
|
||||
|
||||
// Metrics is simply a useful container for everything.
|
||||
type Metrics struct {
|
||||
TS time.Time
|
||||
Sites []*unifi.Site
|
||||
Clients []*unifi.Client
|
||||
SitesDPI []*unifi.DPITable
|
||||
ClientsDPI []*unifi.DPITable
|
||||
RogueAPs []*unifi.RogueAP
|
||||
Devices *unifi.Devices
|
||||
}
|
||||
|
||||
func init() { // nolint: gochecknoinits
|
||||
u := &InputUnifi{
|
||||
dynamic: make(map[string]*Controller),
|
||||
}
|
||||
|
||||
poller.NewInput(&poller.InputPlugin{
|
||||
Name: PluginName,
|
||||
Input: u, // this library implements poller.Input interface for Metrics().
|
||||
Config: u, // Defines our config data interface.
|
||||
})
|
||||
}
|
||||
|
||||
// getCerts reads in cert files from disk and stores them as a slice of of byte slices.
|
||||
func (c *Controller) getCerts() ([][]byte, error) {
|
||||
if len(c.CertPaths) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b := make([][]byte, len(c.CertPaths))
|
||||
|
||||
for i, f := range c.CertPaths {
|
||||
d, err := ioutil.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading SSL cert file: %w", err)
|
||||
}
|
||||
|
||||
b[i] = d
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// getUnifi (re-)authenticates to a unifi controller.
|
||||
// If certificate files are provided, they are re-read.
|
||||
func (u *InputUnifi) getUnifi(c *Controller) error {
|
||||
u.Lock()
|
||||
defer u.Unlock()
|
||||
|
||||
if c.Unifi != nil {
|
||||
c.Unifi.CloseIdleConnections()
|
||||
}
|
||||
|
||||
certs, err := c.getCerts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create an authenticated session to the Unifi Controller.
|
||||
c.Unifi, err = unifi.NewUnifi(&unifi.Config{
|
||||
User: c.User,
|
||||
Pass: c.Pass,
|
||||
URL: c.URL,
|
||||
SSLCert: certs,
|
||||
VerifySSL: *c.VerifySSL,
|
||||
ErrorLog: u.LogErrorf, // Log all errors.
|
||||
DebugLog: u.LogDebugf, // Log debug messages.
|
||||
})
|
||||
if err != nil {
|
||||
c.Unifi = nil
|
||||
return fmt.Errorf("unifi controller: %w", err)
|
||||
}
|
||||
|
||||
u.LogDebugf("Authenticated with controller successfully, %s", c.URL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkSites makes sure the list of provided sites exists on the controller.
|
||||
// This only runs once during initialization.
|
||||
func (u *InputUnifi) checkSites(c *Controller) error {
|
||||
u.RLock()
|
||||
defer u.RUnlock()
|
||||
|
||||
if len(c.Sites) == 0 || c.Sites[0] == "" {
|
||||
c.Sites = []string{"all"}
|
||||
}
|
||||
|
||||
u.LogDebugf("Checking Controller Sites List")
|
||||
|
||||
sites, err := c.Unifi.GetSites()
|
||||
if err != nil {
|
||||
return fmt.Errorf("controller: %w", err)
|
||||
}
|
||||
|
||||
msg := []string{}
|
||||
for _, site := range sites {
|
||||
msg = append(msg, site.Name+" ("+site.Desc+")")
|
||||
}
|
||||
|
||||
u.Logf("Found %d site(s) on controller %s: %v", len(msg), c.URL, strings.Join(msg, ", "))
|
||||
|
||||
if StringInSlice("all", c.Sites) {
|
||||
c.Sites = []string{"all"}
|
||||
return nil
|
||||
}
|
||||
|
||||
keep := []string{}
|
||||
|
||||
FIRST:
|
||||
for _, s := range c.Sites {
|
||||
for _, site := range sites {
|
||||
if s == site.Name {
|
||||
keep = append(keep, s)
|
||||
continue FIRST
|
||||
}
|
||||
}
|
||||
u.LogErrorf("Configured site not found on controller %s: %v", c.URL, s)
|
||||
}
|
||||
|
||||
if c.Sites = keep; len(keep) == 0 {
|
||||
c.Sites = []string{"all"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *InputUnifi) getPassFromFile(filename string) string {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
u.LogErrorf("Reading UniFi Password File: %v", err)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
// setDefaults sets the default defaults.
|
||||
func (u *InputUnifi) setDefaults(c *Controller) { //nolint:cyclop
|
||||
t := true
|
||||
f := false
|
||||
|
||||
// Default defaults.
|
||||
if c.SaveSites == nil {
|
||||
c.SaveSites = &t
|
||||
}
|
||||
|
||||
if c.VerifySSL == nil {
|
||||
c.VerifySSL = &f
|
||||
}
|
||||
|
||||
if c.HashPII == nil {
|
||||
c.HashPII = &f
|
||||
}
|
||||
|
||||
if c.SaveDPI == nil {
|
||||
c.SaveDPI = &f
|
||||
}
|
||||
|
||||
if c.SaveRogue == nil {
|
||||
c.SaveRogue = &f
|
||||
}
|
||||
|
||||
if c.SaveIDS == nil {
|
||||
c.SaveIDS = &f
|
||||
}
|
||||
|
||||
if c.SaveEvents == nil {
|
||||
c.SaveEvents = &f
|
||||
}
|
||||
|
||||
if c.SaveAlarms == nil {
|
||||
c.SaveAlarms = &f
|
||||
}
|
||||
|
||||
if c.SaveAnomal == nil {
|
||||
c.SaveAnomal = &f
|
||||
}
|
||||
|
||||
if c.URL == "" {
|
||||
c.URL = defaultURL
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Pass, "file://") {
|
||||
c.Pass = u.getPassFromFile(strings.TrimPrefix(c.Pass, "file://"))
|
||||
}
|
||||
|
||||
if c.Pass == "" {
|
||||
c.Pass = defaultPass
|
||||
}
|
||||
|
||||
if c.User == "" {
|
||||
c.User = defaultUser
|
||||
}
|
||||
|
||||
if len(c.Sites) == 0 {
|
||||
c.Sites = []string{defaultSite}
|
||||
}
|
||||
}
|
||||
|
||||
// setControllerDefaults sets defaults for the for controllers.
|
||||
// Any missing values come from defaults (above).
|
||||
func (u *InputUnifi) setControllerDefaults(c *Controller) *Controller { //nolint:cyclop,funlen
|
||||
// Configured controller defaults.
|
||||
if c.SaveSites == nil {
|
||||
c.SaveSites = u.Default.SaveSites
|
||||
}
|
||||
|
||||
if c.VerifySSL == nil {
|
||||
c.VerifySSL = u.Default.VerifySSL
|
||||
}
|
||||
|
||||
if c.CertPaths == nil {
|
||||
c.CertPaths = u.Default.CertPaths
|
||||
}
|
||||
|
||||
if c.HashPII == nil {
|
||||
c.HashPII = u.Default.HashPII
|
||||
}
|
||||
|
||||
if c.SaveDPI == nil {
|
||||
c.SaveDPI = u.Default.SaveDPI
|
||||
}
|
||||
|
||||
if c.SaveIDS == nil {
|
||||
c.SaveIDS = u.Default.SaveIDS
|
||||
}
|
||||
|
||||
if c.SaveRogue == nil {
|
||||
c.SaveRogue = u.Default.SaveRogue
|
||||
}
|
||||
|
||||
if c.SaveEvents == nil {
|
||||
c.SaveEvents = u.Default.SaveEvents
|
||||
}
|
||||
|
||||
if c.SaveAlarms == nil {
|
||||
c.SaveAlarms = u.Default.SaveAlarms
|
||||
}
|
||||
|
||||
if c.SaveAnomal == nil {
|
||||
c.SaveAnomal = u.Default.SaveAnomal
|
||||
}
|
||||
|
||||
if c.URL == "" {
|
||||
c.URL = u.Default.URL
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Pass, "file://") {
|
||||
c.Pass = u.getPassFromFile(strings.TrimPrefix(c.Pass, "file://"))
|
||||
}
|
||||
|
||||
if c.Pass == "" {
|
||||
c.Pass = u.Default.Pass
|
||||
}
|
||||
|
||||
if c.User == "" {
|
||||
c.User = u.Default.User
|
||||
}
|
||||
|
||||
if len(c.Sites) == 0 {
|
||||
c.Sites = u.Default.Sites
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// StringInSlice returns true if a string is in a slice.
|
||||
func StringInSlice(str string, slice []string) bool {
|
||||
for _, s := range slice {
|
||||
if strings.EqualFold(s, str) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
package inputunifi
|
||||
|
||||
/* This file contains the three poller.Input interface methods. */
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/unpoller/poller"
|
||||
"github.com/unpoller/unifi"
|
||||
"github.com/unpoller/webserver"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDynamicLookupsDisabled = fmt.Errorf("filter path requested but dynamic lookups disabled")
|
||||
ErrControllerNumNotFound = fmt.Errorf("controller number not found")
|
||||
ErrNoFilterKindProvided = fmt.Errorf("must provide filter: devices, clients, other")
|
||||
)
|
||||
|
||||
// Initialize gets called one time when starting up.
|
||||
// Satisfies poller.Input interface.
|
||||
func (u *InputUnifi) Initialize(l poller.Logger) error {
|
||||
if u.Config == nil {
|
||||
u.Config = &Config{Disable: true}
|
||||
}
|
||||
|
||||
if u.Logger = l; u.Disable {
|
||||
u.Logf("UniFi input plugin disabled or missing configuration!")
|
||||
return nil
|
||||
}
|
||||
|
||||
if u.setDefaults(&u.Default); len(u.Controllers) == 0 && !u.Dynamic {
|
||||
u.Controllers = []*Controller{&u.Default}
|
||||
}
|
||||
|
||||
if len(u.Controllers) == 0 {
|
||||
u.Logf("No controllers configured. Polling dynamic controllers only! Defaults:")
|
||||
u.logController(&u.Default)
|
||||
}
|
||||
|
||||
for i, c := range u.Controllers {
|
||||
if err := u.getUnifi(u.setControllerDefaults(c)); err != nil {
|
||||
u.LogErrorf("Controller %d of %d Auth or Connection Error, retrying: %v", i+1, len(u.Controllers), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := u.checkSites(c); err != nil {
|
||||
u.LogErrorf("checking sites on %s: %v", c.URL, err)
|
||||
}
|
||||
|
||||
u.Logf("Configured UniFi Controller %d of %d:", i+1, len(u.Controllers))
|
||||
u.logController(c)
|
||||
}
|
||||
|
||||
webserver.UpdateInput(&webserver.Input{Name: PluginName, Config: formatConfig(u.Config)})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *InputUnifi) logController(c *Controller) {
|
||||
u.Logf(" => URL: %s (verify SSL: %v)", c.URL, *c.VerifySSL)
|
||||
|
||||
if len(c.CertPaths) > 0 {
|
||||
u.Logf(" => Cert Files: %s", strings.Join(c.CertPaths, ", "))
|
||||
}
|
||||
|
||||
if c.Unifi != nil {
|
||||
u.Logf(" => Version: %s (%s)", c.Unifi.ServerVersion, c.Unifi.UUID)
|
||||
}
|
||||
|
||||
u.Logf(" => Username: %s (has password: %v)", c.User, c.Pass != "")
|
||||
u.Logf(" => Hash PII / Poll Sites: %v / %s", *c.HashPII, strings.Join(c.Sites, ", "))
|
||||
u.Logf(" => Save Sites / Save DPI: %v / %v (metrics)", *c.SaveSites, *c.SaveDPI)
|
||||
u.Logf(" => Save Events / Save IDS: %v / %v (logs)", *c.SaveEvents, *c.SaveIDS)
|
||||
u.Logf(" => Save Alarms / Anomalies: %v / %v (logs)", *c.SaveAlarms, *c.SaveAnomal)
|
||||
u.Logf(" => Save Rogue APs: %v", *c.SaveRogue)
|
||||
}
|
||||
|
||||
// Events allows you to pull only events (and IDS) from the UniFi Controller.
|
||||
// This does not fully respect HashPII, but it may in the future!
|
||||
// Use Filter.Path to pick a specific controller, otherwise poll them all!
|
||||
func (u *InputUnifi) Events(filter *poller.Filter) (*poller.Events, error) {
|
||||
if u.Disable {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
logs := []interface{}{}
|
||||
|
||||
if filter == nil {
|
||||
filter = &poller.Filter{}
|
||||
}
|
||||
|
||||
for _, c := range u.Controllers {
|
||||
if filter.Path != "" && !strings.EqualFold(c.URL, filter.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
events, err := u.collectControllerEvents(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logs = append(logs, events...)
|
||||
}
|
||||
|
||||
return &poller.Events{Logs: logs}, nil
|
||||
}
|
||||
|
||||
// Metrics grabs all the measurements from a UniFi controller and returns them.
|
||||
// Set Filter.Path to a controller URL for a specific controller (or get them all).
|
||||
func (u *InputUnifi) Metrics(filter *poller.Filter) (*poller.Metrics, error) {
|
||||
if u.Disable {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
metrics := &poller.Metrics{}
|
||||
|
||||
if filter == nil {
|
||||
filter = &poller.Filter{}
|
||||
}
|
||||
|
||||
// Check if the request is for an existing, configured controller (or all controllers)
|
||||
for _, c := range u.Controllers {
|
||||
if filter.Path != "" && !strings.EqualFold(c.URL, filter.Path) {
|
||||
// continue only if we have a filter path and it doesn't match.
|
||||
continue
|
||||
}
|
||||
|
||||
m, err := u.collectController(c)
|
||||
if err != nil {
|
||||
return metrics, err
|
||||
}
|
||||
|
||||
metrics = poller.AppendMetrics(metrics, m)
|
||||
}
|
||||
|
||||
if filter.Path == "" || len(metrics.Clients) != 0 {
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
if !u.Dynamic {
|
||||
return nil, ErrDynamicLookupsDisabled
|
||||
}
|
||||
|
||||
// Attempt a dynamic metrics fetch from an unconfigured controller.
|
||||
return u.dynamicController(filter)
|
||||
}
|
||||
|
||||
// RawMetrics returns API output from the first configured UniFi controller.
|
||||
// Adjust filter.Unit to pull from a controller other than the first.
|
||||
func (u *InputUnifi) RawMetrics(filter *poller.Filter) ([]byte, error) {
|
||||
if l := len(u.Controllers); filter.Unit >= l {
|
||||
return nil, fmt.Errorf("%d controller(s) configured, '%d': %w", l, filter.Unit, ErrControllerNumNotFound)
|
||||
}
|
||||
|
||||
c := u.Controllers[filter.Unit]
|
||||
if u.isNill(c) {
|
||||
u.Logf("Re-authenticating to UniFi Controller: %s", c.URL)
|
||||
|
||||
if err := u.getUnifi(c); err != nil {
|
||||
return nil, fmt.Errorf("re-authenticating to %s: %w", c.URL, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := u.checkSites(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sites, err := u.getFilteredSites(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch filter.Kind {
|
||||
case "d", "device", "devices":
|
||||
return u.getSitesJSON(c, unifi.APIDevicePath, sites)
|
||||
case "client", "clients", "c":
|
||||
return u.getSitesJSON(c, unifi.APIClientPath, sites)
|
||||
case "other", "o":
|
||||
return c.Unifi.GetJSON(filter.Path)
|
||||
default:
|
||||
return []byte{}, ErrNoFilterKindProvided
|
||||
}
|
||||
}
|
||||
|
||||
func (u *InputUnifi) getSitesJSON(c *Controller, path string, sites []*unifi.Site) ([]byte, error) {
|
||||
allJSON := []byte{}
|
||||
|
||||
for _, s := range sites {
|
||||
apiPath := fmt.Sprintf(path, s.Name)
|
||||
u.LogDebugf("Returning Path '%s' for site: %s (%s):\n", apiPath, s.Desc, s.Name)
|
||||
|
||||
body, err := c.Unifi.GetJSON(apiPath)
|
||||
if err != nil {
|
||||
return allJSON, fmt.Errorf("controller: %w", err)
|
||||
}
|
||||
|
||||
allJSON = append(allJSON, body...)
|
||||
}
|
||||
|
||||
return allJSON, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
package inputunifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/unpoller/unifi"
|
||||
"github.com/unpoller/webserver"
|
||||
)
|
||||
|
||||
/* This code reformats our data to be displayed on the built-in web interface. */
|
||||
|
||||
func updateWeb(c *Controller, metrics *Metrics) {
|
||||
webserver.UpdateInput(&webserver.Input{
|
||||
Name: PluginName, // Forgetting this leads to 3 hours of head scratching.
|
||||
Sites: formatSites(c, metrics.Sites),
|
||||
Clients: formatClients(c, metrics.Clients),
|
||||
Devices: formatDevices(c, metrics.Devices),
|
||||
})
|
||||
}
|
||||
|
||||
func formatConfig(config *Config) *Config {
|
||||
return &Config{
|
||||
Default: *formatControllers([]*Controller{&config.Default})[0],
|
||||
Disable: config.Disable,
|
||||
Dynamic: config.Dynamic,
|
||||
Controllers: formatControllers(config.Controllers),
|
||||
}
|
||||
}
|
||||
|
||||
func formatControllers(controllers []*Controller) []*Controller {
|
||||
fixed := []*Controller{}
|
||||
|
||||
for _, c := range controllers {
|
||||
id := ""
|
||||
if c.Unifi != nil {
|
||||
id = c.Unifi.UUID
|
||||
}
|
||||
|
||||
fixed = append(fixed, &Controller{
|
||||
VerifySSL: c.VerifySSL,
|
||||
SaveAnomal: c.SaveAnomal,
|
||||
SaveAlarms: c.SaveAlarms,
|
||||
SaveRogue: c.SaveRogue,
|
||||
SaveEvents: c.SaveEvents,
|
||||
SaveIDS: c.SaveIDS,
|
||||
SaveDPI: c.SaveDPI,
|
||||
HashPII: c.HashPII,
|
||||
SaveSites: c.SaveSites,
|
||||
User: c.User,
|
||||
Pass: strconv.FormatBool(c.Pass != ""),
|
||||
URL: c.URL,
|
||||
Sites: c.Sites,
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
return fixed
|
||||
}
|
||||
|
||||
func formatSites(c *Controller, sites []*unifi.Site) (s webserver.Sites) {
|
||||
for _, site := range sites {
|
||||
s = append(s, &webserver.Site{
|
||||
ID: site.ID,
|
||||
Name: site.Name,
|
||||
Desc: site.Desc,
|
||||
Source: site.SourceName,
|
||||
Controller: c.Unifi.UUID,
|
||||
})
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func formatClients(c *Controller, clients []*unifi.Client) (d webserver.Clients) {
|
||||
for _, client := range clients {
|
||||
clientType, deviceMAC := "unknown", "unknown"
|
||||
if client.ApMac != "" {
|
||||
clientType = "wireless"
|
||||
deviceMAC = client.ApMac
|
||||
} else if client.SwMac != "" {
|
||||
clientType = "wired"
|
||||
deviceMAC = client.SwMac
|
||||
}
|
||||
|
||||
if deviceMAC == "" {
|
||||
deviceMAC = client.GwMac
|
||||
}
|
||||
|
||||
d = append(d, &webserver.Client{
|
||||
Name: client.Name,
|
||||
SiteID: client.SiteID,
|
||||
Source: client.SourceName,
|
||||
Controller: c.Unifi.UUID,
|
||||
MAC: client.Mac,
|
||||
IP: client.IP,
|
||||
Type: clientType,
|
||||
DeviceMAC: deviceMAC,
|
||||
Rx: client.RxBytes,
|
||||
Tx: client.TxBytes,
|
||||
Since: time.Unix(client.FirstSeen, 0),
|
||||
Last: time.Unix(client.LastSeen, 0),
|
||||
})
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func formatDevices(c *Controller, devices *unifi.Devices) (d webserver.Devices) { // nolint: funlen
|
||||
if devices == nil {
|
||||
return d
|
||||
}
|
||||
|
||||
for _, device := range devices.UAPs {
|
||||
d = append(d, &webserver.Device{
|
||||
Name: device.Name,
|
||||
SiteID: device.SiteID,
|
||||
Source: device.SourceName,
|
||||
Controller: c.Unifi.UUID,
|
||||
MAC: device.Mac,
|
||||
IP: device.IP,
|
||||
Type: device.Type,
|
||||
Model: device.Model,
|
||||
Version: device.Version,
|
||||
Uptime: int(device.Uptime.Val),
|
||||
Clients: int(device.NumSta.Val),
|
||||
Config: nil,
|
||||
})
|
||||
}
|
||||
|
||||
for _, device := range devices.UDMs {
|
||||
d = append(d, &webserver.Device{
|
||||
Name: device.Name,
|
||||
SiteID: device.SiteID,
|
||||
Source: device.SourceName,
|
||||
Controller: c.Unifi.UUID,
|
||||
MAC: device.Mac,
|
||||
IP: device.IP,
|
||||
Type: device.Type,
|
||||
Model: device.Model,
|
||||
Version: device.Version,
|
||||
Uptime: int(device.Uptime.Val),
|
||||
Clients: int(device.NumSta.Val),
|
||||
Config: nil,
|
||||
})
|
||||
}
|
||||
|
||||
for _, device := range devices.USWs {
|
||||
d = append(d, &webserver.Device{
|
||||
Name: device.Name,
|
||||
SiteID: device.SiteID,
|
||||
Source: device.SourceName,
|
||||
Controller: c.Unifi.UUID,
|
||||
MAC: device.Mac,
|
||||
IP: device.IP,
|
||||
Type: device.Type,
|
||||
Model: device.Model,
|
||||
Version: device.Version,
|
||||
Uptime: int(device.Uptime.Val),
|
||||
Clients: int(device.NumSta.Val),
|
||||
Config: nil,
|
||||
})
|
||||
}
|
||||
|
||||
for _, device := range devices.USGs {
|
||||
d = append(d, &webserver.Device{
|
||||
Name: device.Name,
|
||||
SiteID: device.SiteID,
|
||||
Source: device.SourceName,
|
||||
Controller: c.Unifi.UUID,
|
||||
MAC: device.Mac,
|
||||
IP: device.IP,
|
||||
Type: device.Type,
|
||||
Model: device.Model,
|
||||
Version: device.Version,
|
||||
Uptime: int(device.Uptime.Val),
|
||||
Clients: int(device.NumSta.Val),
|
||||
Config: nil,
|
||||
})
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// Logf logs a message.
|
||||
func (u *InputUnifi) Logf(msg string, v ...interface{}) {
|
||||
webserver.NewInputEvent(PluginName, PluginName, &webserver.Event{
|
||||
Ts: time.Now(),
|
||||
Msg: fmt.Sprintf(msg, v...),
|
||||
Tags: map[string]string{"type": "info"},
|
||||
})
|
||||
u.Logger.Logf(msg, v...)
|
||||
}
|
||||
|
||||
// LogErrorf logs an error message.
|
||||
func (u *InputUnifi) LogErrorf(msg string, v ...interface{}) {
|
||||
webserver.NewInputEvent(PluginName, PluginName, &webserver.Event{
|
||||
Ts: time.Now(),
|
||||
Msg: fmt.Sprintf(msg, v...),
|
||||
Tags: map[string]string{"type": "error"},
|
||||
})
|
||||
u.Logger.LogErrorf(msg, v...)
|
||||
}
|
||||
|
||||
// LogDebugf logs a debug message.
|
||||
func (u *InputUnifi) LogDebugf(msg string, v ...interface{}) {
|
||||
webserver.NewInputEvent(PluginName, PluginName, &webserver.Event{
|
||||
Ts: time.Now(),
|
||||
Msg: fmt.Sprintf(msg, v...),
|
||||
Tags: map[string]string{"type": "debug"},
|
||||
})
|
||||
u.Logger.LogDebugf(msg, v...)
|
||||
}
|
||||
Loading…
Reference in New Issue