diff --git a/integrations/inputunifi/collectevents.go b/integrations/inputunifi/collectevents.go new file mode 100644 index 00000000..4940c60a --- /dev/null +++ b/integrations/inputunifi/collectevents.go @@ -0,0 +1,131 @@ +package inputunifi + +import ( + "time" + + "github.com/pkg/errors" + "github.com/unifi-poller/unifi" + "github.com/unifi-poller/webserver" +) + +/* Event collection. Events are also sent to the webserver for display. */ + +func (u *InputUnifi) collectControllerEvents(c *Controller) ([]interface{}, error) { + var ( + logs = []interface{}{} + newLogs []interface{} + ) + + // Get the sites we care about. + sites, err := u.getFilteredSites(c) + if err != nil { + return nil, errors.Wrap(err, "unifi.GetSites()") + } + + 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 { + events, err := c.Unifi.GetAlarms(sites) + if err != nil { + return logs, errors.Wrap(err, "unifi.GetAlarms()") + } + + for _, e := range events { + logs = append(logs, e) + + webserver.NewInputEvent(PluginName, c.URL+" alarms", &webserver.Event{Ts: e.Datetime, Msg: e.Msg, + Tags: map[string]string{"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 { + events, err := c.Unifi.GetAnomalies(sites, time.Now().Add(-time.Hour)) + if err != nil { + return logs, errors.Wrap(err, "unifi.GetAnomalies()") + } + + for _, e := range events { + logs = append(logs, e) + + webserver.NewInputEvent(PluginName, c.URL+" anomalies", &webserver.Event{Ts: e.Datetime, Msg: e.Anomaly, + Tags: map[string]string{"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 { + events, err := c.Unifi.GetEvents(sites, time.Hour) + if err != nil { + return logs, errors.Wrap(err, "unifi.GetEvents()") + } + + for _, e := range events { + e := redactEvent(e, c.HashPII) + logs = append(logs, e) + + webserver.NewInputEvent(PluginName, c.URL+" events", &webserver.Event{Msg: e.Msg, Ts: e.Datetime, + Tags: map[string]string{"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 { + events, err := c.Unifi.GetIDS(sites, time.Now().Add(-time.Hour)) + if err != nil { + return logs, errors.Wrap(err, "unifi.GetIDS()") + } + + for _, e := range events { + logs = append(logs, e) + + webserver.NewInputEvent(PluginName, c.URL+" ids", &webserver.Event{Ts: e.Datetime, Msg: e.Msg, + Tags: map[string]string{"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 +} diff --git a/integrations/inputunifi/collector.go b/integrations/inputunifi/collector.go index 2d6d76fb..2e1f151f 100644 --- a/integrations/inputunifi/collector.go +++ b/integrations/inputunifi/collector.go @@ -82,62 +82,6 @@ func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) { return metrics, err } -func (u *InputUnifi) collectControllerEvents(c *Controller) ([]interface{}, error) { - logs := []interface{}{} - - // Get the sites we care about. - sites, err := u.getFilteredSites(c) - if err != nil { - return nil, errors.Wrap(err, "unifi.GetSites()") - } - - if *c.SaveAnomal { - anom, err := c.Unifi.GetAnomalies(sites, time.Now().Add(-time.Hour)) - if err != nil { - return nil, errors.Wrap(err, "unifi.GetAnomalies()") - } - - for _, a := range anom { - logs = append(logs, a) - } - } - - if *c.SaveAlarms { - alarms, err := c.Unifi.GetAlarms(sites) - if err != nil { - return nil, errors.Wrap(err, "unifi.GetAlarms()") - } - - for _, a := range alarms { - logs = append(logs, a) - } - } - - if *c.SaveEvents { - events, err := c.Unifi.GetEvents(sites, time.Hour) - if err != nil { - return nil, errors.Wrap(err, "unifi.GetEvents()") - } - - for _, e := range events { - logs = append(logs, redactEvent(e, c.HashPII)) - } - } - - if *c.SaveIDS { - events, err := c.Unifi.GetIDS(sites, time.Now().Add(-time.Hour)) - if err != nil { - return nil, errors.Wrap(err, "unifi.GetIDS()") - } - - for _, e := range events { - logs = append(logs, e) - } - } - - return logs, nil -} - func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) { u.RLock() defer u.RUnlock() @@ -169,6 +113,8 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) { return nil, errors.Wrapf(err, "unifi.GetDevices(%s)", c.URL) } + defer updateWeb(m) + return u.augmentMetrics(c, m), nil } @@ -257,24 +203,6 @@ func extractDevices(metrics *Metrics) (*poller.Metrics, map[string]string, map[s return m, devices, bssdIDs } -// 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 -} - // 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 { diff --git a/integrations/inputunifi/go.mod b/integrations/inputunifi/go.mod index 1718ea14..d57ed22d 100644 --- a/integrations/inputunifi/go.mod +++ b/integrations/inputunifi/go.mod @@ -2,8 +2,13 @@ module github.com/unifi-poller/inputunifi go 1.14 +replace github.com/unifi-poller/webserver => ../webserver + +replace github.com/unifi-poller/poller => ../poller + require ( github.com/pkg/errors v0.9.1 - github.com/unifi-poller/poller v0.0.8-0.20200621214016-5d1ed3324a46 + github.com/unifi-poller/poller v0.0.8-0.20200626082958-a9a7092a5684 github.com/unifi-poller/unifi v0.0.6-0.20200625090439-421046871a37 + github.com/unifi-poller/webserver v0.0.0-20200628114213-2b89a50ff1c0 ) diff --git a/integrations/inputunifi/go.sum b/integrations/inputunifi/go.sum index 5a7d916b..c749c81e 100644 --- a/integrations/inputunifi/go.sum +++ b/integrations/inputunifi/go.sum @@ -41,6 +41,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -108,6 +110,7 @@ github.com/unifi-poller/unifi v0.0.5-0.20200622073824-4a29471d80f6/go.mod h1:L1k github.com/unifi-poller/unifi v0.0.6-0.20200625090439-421046871a37 h1:T2y8JWkjZd1vz2ZKu4vmmAk9s6PUwupuTldwhfww5xY= github.com/unifi-poller/unifi v0.0.6-0.20200625090439-421046871a37/go.mod h1:L1kMRH2buZhB31vZnRC1im7Tk/4uD3ET4biwl2faYy8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/integrations/inputunifi/input.go b/integrations/inputunifi/input.go index 9129aa27..f74da5bc 100644 --- a/integrations/inputunifi/input.go +++ b/integrations/inputunifi/input.go @@ -14,8 +14,10 @@ import ( "github.com/unifi-poller/unifi" ) +// PluginName is the name of this input plugin. +const PluginName = "unifi" + const ( - PluginName = "unifi" // PluginName is the name of this input plugin. defaultURL = "https://127.0.0.1:8443" defaultUser = "unifipoller" defaultPass = "unifipoller" @@ -27,7 +29,7 @@ type InputUnifi struct { *Config `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"` dynamic map[string]*Controller sync.Mutex // to lock the map above. - poller.Logger + Logger poller.Logger } // Controller represents the configuration for a UniFi Controller. @@ -44,7 +46,7 @@ type Controller struct { 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,omitempty" toml:"sites,omitempty" xml:"site" yaml:"sites"` + Sites []string `json:"sites" toml:"sites" xml:"site" yaml:"sites"` Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"` } @@ -57,6 +59,7 @@ type Config struct { 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 diff --git a/integrations/inputunifi/interface.go b/integrations/inputunifi/interface.go index b7df17da..6b932d47 100644 --- a/integrations/inputunifi/interface.go +++ b/integrations/inputunifi/interface.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/unifi-poller/poller" "github.com/unifi-poller/unifi" + "github.com/unifi-poller/webserver" ) var ( @@ -53,6 +54,8 @@ func (u *InputUnifi) Initialize(l poller.Logger) error { u.logController(c) } + webserver.UpdateInput(&webserver.Input{Name: PluginName, Config: formatConfig(u.Config)}) + return nil } diff --git a/integrations/inputunifi/updateweb.go b/integrations/inputunifi/updateweb.go new file mode 100644 index 00000000..60757a83 --- /dev/null +++ b/integrations/inputunifi/updateweb.go @@ -0,0 +1,185 @@ +package inputunifi + +import ( + "fmt" + "strconv" + "time" + + "github.com/unifi-poller/unifi" + "github.com/unifi-poller/webserver" +) + +/* This code reformats our data to be displayed on the built-in web interface. */ + +func updateWeb(metrics *Metrics) { + webserver.UpdateInput(&webserver.Input{ + Sites: formatSites(metrics.Sites), + Clients: formatClients(metrics.Clients), + Devices: formatDevices(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 { + fixed = append(fixed, &Controller{ + VerifySSL: c.VerifySSL, + SaveAnomal: c.SaveAnomal, + SaveAlarms: c.SaveAlarms, + 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, + }) + } + + return fixed +} + +func formatSites(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, + }) + } + + return s +} + +func formatClients(clients []*unifi.Client) (c 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 + } + + c = append(c, &webserver.Client{ + Name: client.Name, + SiteID: client.SiteID, + Source: client.SourceName, + MAC: client.Mac, + IP: client.IP, + Type: clientType, + DeviceMAC: deviceMAC, + Since: time.Unix(client.FirstSeen, 0), + Last: time.Unix(client.LastSeen, 0), + }) + } + + return c +} + +func formatDevices(devices *unifi.Devices) (d webserver.Devices) { + for _, device := range devices.UAPs { + d = append(d, &webserver.Device{ + Name: device.Name, + SiteID: device.SiteID, + Source: device.SourceName, + MAC: device.Mac, + IP: device.IP, + Type: device.Type, + Model: device.Model, + Version: device.Version, + Config: nil, + }) + } + + for _, device := range devices.UDMs { + d = append(d, &webserver.Device{ + Name: device.Name, + SiteID: device.SiteID, + Source: device.SourceName, + MAC: device.Mac, + IP: device.IP, + Type: device.Type, + Model: device.Model, + Version: device.Version, + Config: nil, + }) + } + + for _, device := range devices.USWs { + d = append(d, &webserver.Device{ + Name: device.Name, + SiteID: device.SiteID, + Source: device.SourceName, + MAC: device.Mac, + IP: device.IP, + Type: device.Type, + Model: device.Model, + Version: device.Version, + Config: nil, + }) + } + + for _, device := range devices.USGs { + d = append(d, &webserver.Device{ + Name: device.Name, + SiteID: device.SiteID, + Source: device.SourceName, + MAC: device.Mac, + IP: device.IP, + Type: device.Type, + Model: device.Model, + Version: device.Version, + 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...) +}