Merge branch 'master' of ../influxunifi into merge-them-all
This commit is contained in:
		
						commit
						dd916e3c37
					
				|  | @ -0,0 +1,9 @@ | ||||||
|  | language: go | ||||||
|  | go: | ||||||
|  | - 1.15.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: | ||||||
|  | - go test ./... | ||||||
|  | - golangci-lint run --enable-all -D exhaustivestruct,nlreturn | ||||||
|  | @ -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,5 @@ | ||||||
|  | # influxunifi | ||||||
|  | 
 | ||||||
|  | ## UnPoller Input Plugin | ||||||
|  | 
 | ||||||
|  | Collects UniFi data from a UniFi controller using the API. | ||||||
|  | @ -0,0 +1,87 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	alarmT   = item("Alarm") | ||||||
|  | 	anomalyT = item("Anomaly") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // batchAlarms generates alarm datapoints for InfluxDB.
 | ||||||
|  | func (u *InfluxUnifi) batchAlarms(r report, event *unifi.Alarm) { // nolint:dupl
 | ||||||
|  | 	if time.Since(event.Datetime) > u.Interval.Duration+time.Second { | ||||||
|  | 		return // The event is older than our interval, ignore it.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fields := map[string]interface{}{ | ||||||
|  | 		"dest_port":            event.DestPort, | ||||||
|  | 		"src_port":             event.SrcPort, | ||||||
|  | 		"dest_ip":              event.DestIP, | ||||||
|  | 		"dst_mac":              event.DstMAC, | ||||||
|  | 		"host":                 event.Host, | ||||||
|  | 		"msg":                  event.Msg, | ||||||
|  | 		"src_ip":               event.SrcIP, | ||||||
|  | 		"src_mac":              event.SrcMAC, | ||||||
|  | 		"dstip_asn":            event.DestIPGeo.Asn, | ||||||
|  | 		"dstip_latitude":       event.DestIPGeo.Latitude, | ||||||
|  | 		"dstip_longitude":      event.DestIPGeo.Longitude, | ||||||
|  | 		"dstip_city":           event.DestIPGeo.City, | ||||||
|  | 		"dstip_continent_code": event.DestIPGeo.ContinentCode, | ||||||
|  | 		"dstip_country_code":   event.DestIPGeo.CountryCode, | ||||||
|  | 		"dstip_country_name":   event.DestIPGeo.CountryName, | ||||||
|  | 		"dstip_organization":   event.DestIPGeo.Organization, | ||||||
|  | 		"srcip_asn":            event.SourceIPGeo.Asn, | ||||||
|  | 		"srcip_latitude":       event.SourceIPGeo.Latitude, | ||||||
|  | 		"srcip_longitude":      event.SourceIPGeo.Longitude, | ||||||
|  | 		"srcip_city":           event.SourceIPGeo.City, | ||||||
|  | 		"srcip_continent_code": event.SourceIPGeo.ContinentCode, | ||||||
|  | 		"srcip_country_code":   event.SourceIPGeo.CountryCode, | ||||||
|  | 		"srcip_country_name":   event.SourceIPGeo.CountryName, | ||||||
|  | 		"srcip_organization":   event.SourceIPGeo.Organization, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.addCount(alarmT) | ||||||
|  | 	r.send(&metric{ | ||||||
|  | 		Table:  "unifi_alarm", | ||||||
|  | 		TS:     event.Datetime, | ||||||
|  | 		Fields: cleanFields(fields), | ||||||
|  | 		Tags: cleanTags(map[string]string{ | ||||||
|  | 			"site_name":  event.SiteName, | ||||||
|  | 			"source":     event.SourceName, | ||||||
|  | 			"in_iface":   event.InIface, | ||||||
|  | 			"event_type": event.EventType, | ||||||
|  | 			"subsystem":  event.Subsystem, | ||||||
|  | 			"archived":   event.Archived.Txt, | ||||||
|  | 			"usgip":      event.USGIP, | ||||||
|  | 			"proto":      event.Proto, | ||||||
|  | 			"key":        event.Key, | ||||||
|  | 			"catname":    event.Catname, | ||||||
|  | 			"app_proto":  event.AppProto, | ||||||
|  | 			"action":     event.InnerAlertAction, | ||||||
|  | 		}), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // batchAnomaly generates Anomalies from UniFi for InfluxDB.
 | ||||||
|  | func (u *InfluxUnifi) batchAnomaly(r report, event *unifi.Anomaly) { | ||||||
|  | 	if time.Since(event.Datetime) > u.Interval.Duration+time.Second { | ||||||
|  | 		return // The event is older than our interval, ignore it.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.addCount(anomalyT) | ||||||
|  | 	r.send(&metric{ | ||||||
|  | 		TS:     event.Datetime, | ||||||
|  | 		Table:  "unifi_anomaly", | ||||||
|  | 		Fields: map[string]interface{}{"msg": event.Anomaly}, | ||||||
|  | 		Tags: cleanTags(map[string]string{ | ||||||
|  | 			"application": "unifi_anomaly", | ||||||
|  | 			"source":      event.SourceName, | ||||||
|  | 			"site_name":   event.SiteName, | ||||||
|  | 			"device_mac":  event.DeviceMAC, | ||||||
|  | 		}), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,183 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // batchClient generates Unifi Client datapoints for InfluxDB.
 | ||||||
|  | // These points can be passed directly to influx.
 | ||||||
|  | func (u *InfluxUnifi) batchClient(r report, s *unifi.Client) { // nolint: funlen
 | ||||||
|  | 	tags := map[string]string{ | ||||||
|  | 		"mac":         s.Mac, | ||||||
|  | 		"site_name":   s.SiteName, | ||||||
|  | 		"source":      s.SourceName, | ||||||
|  | 		"ap_name":     s.ApName, | ||||||
|  | 		"gw_name":     s.GwName, | ||||||
|  | 		"sw_name":     s.SwName, | ||||||
|  | 		"oui":         s.Oui, | ||||||
|  | 		"radio_name":  s.RadioName, | ||||||
|  | 		"radio":       s.Radio, | ||||||
|  | 		"radio_proto": s.RadioProto, | ||||||
|  | 		"name":        s.Name, | ||||||
|  | 		"fixed_ip":    s.FixedIP, | ||||||
|  | 		"sw_port":     s.SwPort.Txt, | ||||||
|  | 		"os_class":    s.OsClass.Txt, | ||||||
|  | 		"os_name":     s.OsName.Txt, | ||||||
|  | 		"dev_cat":     s.DevCat.Txt, | ||||||
|  | 		"dev_id":      s.DevID.Txt, | ||||||
|  | 		"dev_vendor":  s.DevVendor.Txt, | ||||||
|  | 		"dev_family":  s.DevFamily.Txt, | ||||||
|  | 		"is_wired":    s.IsWired.Txt, | ||||||
|  | 		"is_guest":    s.IsGuest.Txt, | ||||||
|  | 		"use_fixedip": s.UseFixedIP.Txt, | ||||||
|  | 		"channel":     s.Channel.Txt, | ||||||
|  | 		"vlan":        s.Vlan.Txt, | ||||||
|  | 	} | ||||||
|  | 	fields := map[string]interface{}{ | ||||||
|  | 		"anomalies":         s.Anomalies, | ||||||
|  | 		"ip":                s.IP, | ||||||
|  | 		"essid":             s.Essid, | ||||||
|  | 		"bssid":             s.Bssid, | ||||||
|  | 		"channel":           s.Channel.Val, | ||||||
|  | 		"hostname":          s.Name, | ||||||
|  | 		"radio_desc":        s.RadioDescription, | ||||||
|  | 		"satisfaction":      s.Satisfaction.Val, | ||||||
|  | 		"bytes_r":           s.BytesR, | ||||||
|  | 		"ccq":               s.Ccq, | ||||||
|  | 		"noise":             s.Noise, | ||||||
|  | 		"note":              s.Note, | ||||||
|  | 		"powersave_enabled": s.PowersaveEnabled, | ||||||
|  | 		"roam_count":        s.RoamCount, | ||||||
|  | 		"rssi":              s.Rssi, | ||||||
|  | 		"rx_bytes":          s.RxBytes, | ||||||
|  | 		"rx_bytes_r":        s.RxBytesR, | ||||||
|  | 		"rx_packets":        s.RxPackets, | ||||||
|  | 		"rx_rate":           s.RxRate, | ||||||
|  | 		"signal":            s.Signal, | ||||||
|  | 		"tx_bytes":          s.TxBytes, | ||||||
|  | 		"tx_bytes_r":        s.TxBytesR, | ||||||
|  | 		"tx_packets":        s.TxPackets, | ||||||
|  | 		"tx_retries":        s.TxRetries, | ||||||
|  | 		"tx_power":          s.TxPower, | ||||||
|  | 		"tx_rate":           s.TxRate, | ||||||
|  | 		"uptime":            s.Uptime, | ||||||
|  | 		"wifi_tx_attempts":  s.WifiTxAttempts, | ||||||
|  | 		"wired-rx_bytes":    s.WiredRxBytes, | ||||||
|  | 		"wired-rx_bytes-r":  s.WiredRxBytesR, | ||||||
|  | 		"wired-rx_packets":  s.WiredRxPackets, | ||||||
|  | 		"wired-tx_bytes":    s.WiredTxBytes, | ||||||
|  | 		"wired-tx_bytes-r":  s.WiredTxBytesR, | ||||||
|  | 		"wired-tx_packets":  s.WiredTxPackets, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.send(&metric{Table: "clients", Tags: tags, Fields: fields}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // totalsDPImap: controller, site, name (app/cat name), dpi.
 | ||||||
|  | type totalsDPImap map[string]map[string]map[string]unifi.DPIData | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchClientDPI(r report, v interface{}, appTotal, catTotal totalsDPImap) { | ||||||
|  | 	s, ok := v.(*unifi.DPITable) | ||||||
|  | 	if !ok { | ||||||
|  | 		u.LogErrorf("invalid type given to batchClientDPI: %T", v) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, dpi := range s.ByApp { | ||||||
|  | 		category := unifi.DPICats.Get(dpi.Cat) | ||||||
|  | 		application := unifi.DPIApps.GetApp(dpi.Cat, dpi.App) | ||||||
|  | 		fillDPIMapTotals(appTotal, application, s.SourceName, s.SiteName, dpi) | ||||||
|  | 		fillDPIMapTotals(catTotal, category, s.SourceName, s.SiteName, dpi) | ||||||
|  | 
 | ||||||
|  | 		r.send(&metric{ | ||||||
|  | 			Table: "clientdpi", | ||||||
|  | 			Tags: map[string]string{ | ||||||
|  | 				"category":    category, | ||||||
|  | 				"application": application, | ||||||
|  | 				"name":        s.Name, | ||||||
|  | 				"mac":         s.MAC, | ||||||
|  | 				"site_name":   s.SiteName, | ||||||
|  | 				"source":      s.SourceName, | ||||||
|  | 			}, | ||||||
|  | 			Fields: map[string]interface{}{ | ||||||
|  | 				"tx_packets": dpi.TxPackets, | ||||||
|  | 				"rx_packets": dpi.RxPackets, | ||||||
|  | 				"tx_bytes":   dpi.TxBytes, | ||||||
|  | 				"rx_bytes":   dpi.RxBytes, | ||||||
|  | 			}, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // fillDPIMapTotals fills in totals for categories and applications. maybe clients too.
 | ||||||
|  | // This allows less processing in InfluxDB to produce total transfer data per cat or app.
 | ||||||
|  | func fillDPIMapTotals(m totalsDPImap, name, controller, site string, dpi unifi.DPIData) { | ||||||
|  | 	if m[controller] == nil { | ||||||
|  | 		m[controller] = make(map[string]map[string]unifi.DPIData) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if m[controller][site] == nil { | ||||||
|  | 		m[controller][site] = make(map[string]unifi.DPIData) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	existing := m[controller][site][name] | ||||||
|  | 	existing.TxPackets += dpi.TxPackets | ||||||
|  | 	existing.RxPackets += dpi.RxPackets | ||||||
|  | 	existing.TxBytes += dpi.TxBytes | ||||||
|  | 	existing.RxBytes += dpi.RxBytes | ||||||
|  | 	m[controller][site][name] = existing | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func reportClientDPItotals(r report, appTotal, catTotal totalsDPImap) { | ||||||
|  | 	type all []struct { | ||||||
|  | 		kind string | ||||||
|  | 		val  totalsDPImap | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// This produces 7000+ metrics per site. Disabled for now.
 | ||||||
|  | 	if appTotal != nil { | ||||||
|  | 		appTotal = nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// This can allow us to aggregate other data types later, like `name` or `mac`, or anything else unifi adds.
 | ||||||
|  | 	a := all{ | ||||||
|  | 		// This produces 7000+ metrics per site. Disabled for now.
 | ||||||
|  | 		{ | ||||||
|  | 			kind: "application", | ||||||
|  | 			val:  appTotal, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			kind: "category", | ||||||
|  | 			val:  catTotal, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, k := range a { | ||||||
|  | 		for controller, s := range k.val { | ||||||
|  | 			for site, c := range s { | ||||||
|  | 				for name, m := range c { | ||||||
|  | 					newMetric := &metric{ | ||||||
|  | 						Table: "clientdpi", | ||||||
|  | 						Tags: map[string]string{ | ||||||
|  | 							"category":    "TOTAL", | ||||||
|  | 							"application": "TOTAL", | ||||||
|  | 							"name":        "TOTAL", | ||||||
|  | 							"mac":         "TOTAL", | ||||||
|  | 							"site_name":   site, | ||||||
|  | 							"source":      controller, | ||||||
|  | 						}, | ||||||
|  | 						Fields: map[string]interface{}{ | ||||||
|  | 							"tx_packets": m.TxPackets, | ||||||
|  | 							"rx_packets": m.RxPackets, | ||||||
|  | 							"tx_bytes":   m.TxBytes, | ||||||
|  | 							"rx_bytes":   m.RxBytes, | ||||||
|  | 						}, | ||||||
|  | 					} | ||||||
|  | 					newMetric.Tags[k.kind] = name | ||||||
|  | 
 | ||||||
|  | 					r.send(newMetric) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,185 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // These constants are used as names for printed/logged counters.
 | ||||||
|  | const ( | ||||||
|  | 	eventT = item("Event") | ||||||
|  | 	idsT   = item("IDS") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // batchIDS generates intrusion detection datapoints for InfluxDB.
 | ||||||
|  | func (u *InfluxUnifi) batchIDS(r report, i *unifi.IDS) { // nolint:dupl
 | ||||||
|  | 	if time.Since(i.Datetime) > u.Interval.Duration+time.Second { | ||||||
|  | 		return // The event is older than our interval, ignore it.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fields := map[string]interface{}{ | ||||||
|  | 		"dest_port":            i.DestPort, | ||||||
|  | 		"src_port":             i.SrcPort, | ||||||
|  | 		"dest_ip":              i.DestIP, | ||||||
|  | 		"dst_mac":              i.DstMAC, | ||||||
|  | 		"host":                 i.Host, | ||||||
|  | 		"msg":                  i.Msg, | ||||||
|  | 		"src_ip":               i.SrcIP, | ||||||
|  | 		"src_mac":              i.SrcMAC, | ||||||
|  | 		"dstip_asn":            i.DestIPGeo.Asn, | ||||||
|  | 		"dstip_latitude":       i.DestIPGeo.Latitude, | ||||||
|  | 		"dstip_longitude":      i.DestIPGeo.Longitude, | ||||||
|  | 		"dstip_city":           i.DestIPGeo.City, | ||||||
|  | 		"dstip_continent_code": i.DestIPGeo.ContinentCode, | ||||||
|  | 		"dstip_country_code":   i.DestIPGeo.CountryCode, | ||||||
|  | 		"dstip_country_name":   i.DestIPGeo.CountryName, | ||||||
|  | 		"dstip_organization":   i.DestIPGeo.Organization, | ||||||
|  | 		"srcip_asn":            i.SourceIPGeo.Asn, | ||||||
|  | 		"srcip_latitude":       i.SourceIPGeo.Latitude, | ||||||
|  | 		"srcip_longitude":      i.SourceIPGeo.Longitude, | ||||||
|  | 		"srcip_city":           i.SourceIPGeo.City, | ||||||
|  | 		"srcip_continent_code": i.SourceIPGeo.ContinentCode, | ||||||
|  | 		"srcip_country_code":   i.SourceIPGeo.CountryCode, | ||||||
|  | 		"srcip_country_name":   i.SourceIPGeo.CountryName, | ||||||
|  | 		"srcip_organization":   i.SourceIPGeo.Organization, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.addCount(idsT) | ||||||
|  | 	r.send(&metric{ | ||||||
|  | 		Table:  "unifi_ids", | ||||||
|  | 		TS:     i.Datetime, | ||||||
|  | 		Fields: cleanFields(fields), | ||||||
|  | 		Tags: cleanTags(map[string]string{ | ||||||
|  | 			"site_name":  i.SiteName, | ||||||
|  | 			"source":     i.SourceName, | ||||||
|  | 			"in_iface":   i.InIface, | ||||||
|  | 			"event_type": i.EventType, | ||||||
|  | 			"subsystem":  i.Subsystem, | ||||||
|  | 			"archived":   i.Archived.Txt, | ||||||
|  | 			"usgip":      i.USGIP, | ||||||
|  | 			"proto":      i.Proto, | ||||||
|  | 			"key":        i.Key, | ||||||
|  | 			"catname":    i.Catname, | ||||||
|  | 			"app_proto":  i.AppProto, | ||||||
|  | 			"action":     i.InnerAlertAction, | ||||||
|  | 		}), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // batchEvents generates events from UniFi for InfluxDB.
 | ||||||
|  | func (u *InfluxUnifi) batchEvent(r report, i *unifi.Event) { // nolint: funlen
 | ||||||
|  | 	if time.Since(i.Datetime) > u.Interval.Duration+time.Second { | ||||||
|  | 		return // The event is older than our interval, ignore it.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fields := map[string]interface{}{ | ||||||
|  | 		"msg":                  i.Msg,          // contains user[] or guest[] or admin[]
 | ||||||
|  | 		"duration":             i.Duration.Val, // probably microseconds?
 | ||||||
|  | 		"guest":                i.Guest,        // mac address
 | ||||||
|  | 		"user":                 i.User,         // mac address
 | ||||||
|  | 		"host":                 i.Host,         // usg device?
 | ||||||
|  | 		"hostname":             i.Hostname,     // client name
 | ||||||
|  | 		"dest_port":            i.DestPort, | ||||||
|  | 		"src_port":             i.SrcPort, | ||||||
|  | 		"bytes":                i.Bytes.Val, | ||||||
|  | 		"dest_ip":              i.DestIP, | ||||||
|  | 		"dst_mac":              i.DstMAC, | ||||||
|  | 		"ip":                   i.IP, | ||||||
|  | 		"src_ip":               i.SrcIP, | ||||||
|  | 		"src_mac":              i.SrcMAC, | ||||||
|  | 		"dstip_asn":            i.DestIPGeo.Asn, | ||||||
|  | 		"dstip_latitude":       i.DestIPGeo.Latitude, | ||||||
|  | 		"dstip_longitude":      i.DestIPGeo.Longitude, | ||||||
|  | 		"dstip_city":           i.DestIPGeo.City, | ||||||
|  | 		"dstip_continent_code": i.DestIPGeo.ContinentCode, | ||||||
|  | 		"dstip_country_code":   i.DestIPGeo.CountryCode, | ||||||
|  | 		"dstip_country_name":   i.DestIPGeo.CountryName, | ||||||
|  | 		"dstip_organization":   i.DestIPGeo.Organization, | ||||||
|  | 		"srcip_asn":            i.SourceIPGeo.Asn, | ||||||
|  | 		"srcip_latitude":       i.SourceIPGeo.Latitude, | ||||||
|  | 		"srcip_longitude":      i.SourceIPGeo.Longitude, | ||||||
|  | 		"srcip_city":           i.SourceIPGeo.City, | ||||||
|  | 		"srcip_continent_code": i.SourceIPGeo.ContinentCode, | ||||||
|  | 		"srcip_country_code":   i.SourceIPGeo.CountryCode, | ||||||
|  | 		"srcip_country_name":   i.SourceIPGeo.CountryName, | ||||||
|  | 		"srcip_organization":   i.SourceIPGeo.Organization, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.addCount(eventT) | ||||||
|  | 	r.send(&metric{ | ||||||
|  | 		TS:     i.Datetime, | ||||||
|  | 		Table:  "unifi_events", | ||||||
|  | 		Fields: cleanFields(fields), | ||||||
|  | 		Tags: cleanTags(map[string]string{ | ||||||
|  | 			"admin":        i.Admin, // username
 | ||||||
|  | 			"site_name":    i.SiteName, | ||||||
|  | 			"source":       i.SourceName, | ||||||
|  | 			"ap_from":      i.ApFrom, | ||||||
|  | 			"ap_to":        i.ApTo, | ||||||
|  | 			"ap":           i.Ap, | ||||||
|  | 			"ap_name":      i.ApName, | ||||||
|  | 			"gw":           i.Gw, | ||||||
|  | 			"gw_name":      i.GwName, | ||||||
|  | 			"sw":           i.Sw, | ||||||
|  | 			"sw_name":      i.SwName, | ||||||
|  | 			"catname":      i.Catname, | ||||||
|  | 			"radio":        i.Radio, | ||||||
|  | 			"radio_from":   i.RadioFrom, | ||||||
|  | 			"radio_to":     i.RadioTo, | ||||||
|  | 			"key":          i.Key, | ||||||
|  | 			"in_iface":     i.InIface, | ||||||
|  | 			"event_type":   i.EventType, | ||||||
|  | 			"subsystem":    i.Subsystem, | ||||||
|  | 			"ssid":         i.SSID, | ||||||
|  | 			"is_admin":     i.IsAdmin.Txt, | ||||||
|  | 			"channel":      i.Channel.Txt, | ||||||
|  | 			"channel_from": i.ChannelFrom.Txt, | ||||||
|  | 			"channel_to":   i.ChannelTo.Txt, | ||||||
|  | 			"usgip":        i.USGIP, | ||||||
|  | 			"network":      i.Network, | ||||||
|  | 			"app_proto":    i.AppProto, | ||||||
|  | 			"proto":        i.Proto, | ||||||
|  | 			"action":       i.InnerAlertAction, | ||||||
|  | 		}), | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // cleanTags removes any tag that is empty.
 | ||||||
|  | func cleanTags(tags map[string]string) map[string]string { | ||||||
|  | 	for i := range tags { | ||||||
|  | 		if tags[i] == "" { | ||||||
|  | 			delete(tags, i) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return tags | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // cleanFields removes any field with a default (or empty) value.
 | ||||||
|  | func cleanFields(fields map[string]interface{}) map[string]interface{} { //nolint:cyclop
 | ||||||
|  | 	for s := range fields { | ||||||
|  | 		switch v := fields[s].(type) { | ||||||
|  | 		case nil: | ||||||
|  | 			delete(fields, s) | ||||||
|  | 		case int, int64, float64: | ||||||
|  | 			if v == 0 { | ||||||
|  | 				delete(fields, s) | ||||||
|  | 			} | ||||||
|  | 		case unifi.FlexBool: | ||||||
|  | 			if v.Txt == "" { | ||||||
|  | 				delete(fields, s) | ||||||
|  | 			} | ||||||
|  | 		case unifi.FlexInt: | ||||||
|  | 			if v.Txt == "" { | ||||||
|  | 				delete(fields, s) | ||||||
|  | 			} | ||||||
|  | 		case string: | ||||||
|  | 			if v == "" { | ||||||
|  | 				delete(fields, s) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return fields | ||||||
|  | } | ||||||
|  | @ -0,0 +1,12 @@ | ||||||
|  | module github.com/unpoller/influxunifi | ||||||
|  | 
 | ||||||
|  | go 1.16 | ||||||
|  | 
 | ||||||
|  | require ( | ||||||
|  | 	github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab | ||||||
|  | 	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 | ||||||
|  | 	golift.io/cnfg v0.0.7 | ||||||
|  | ) | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | 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/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab h1:HqW4xhhynfjrtEiiSGcQUd6vrK23iMam1FO8rI7mwig= | ||||||
|  | github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= | ||||||
|  | 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,298 @@ | ||||||
|  | // Package influxunifi provides the methods to turn UniFi measurements into influx
 | ||||||
|  | // data-points with appropriate tags and fields.
 | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"log" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	influx "github.com/influxdata/influxdb1-client/v2" | ||||||
|  | 	"github.com/unpoller/poller" | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | 	"github.com/unpoller/webserver" | ||||||
|  | 	"golift.io/cnfg" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // PluginName is the name of this plugin.
 | ||||||
|  | const PluginName = "influxdb" | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	defaultInterval   = 30 * time.Second | ||||||
|  | 	minimumInterval   = 10 * time.Second | ||||||
|  | 	defaultInfluxDB   = "unifi" | ||||||
|  | 	defaultInfluxUser = "unifipoller" | ||||||
|  | 	defaultInfluxURL  = "http://127.0.0.1:8086" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Config defines the data needed to store metrics in InfluxDB.
 | ||||||
|  | type Config struct { | ||||||
|  | 	Interval  cnfg.Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` | ||||||
|  | 	URL       string        `json:"url,omitempty" toml:"url,omitempty" xml:"url" yaml:"url"` | ||||||
|  | 	User      string        `json:"user,omitempty" toml:"user,omitempty" xml:"user" yaml:"user"` | ||||||
|  | 	Pass      string        `json:"pass,omitempty" toml:"pass,omitempty" xml:"pass" yaml:"pass"` | ||||||
|  | 	DB        string        `json:"db,omitempty" toml:"db,omitempty" xml:"db" yaml:"db"` | ||||||
|  | 	Disable   bool          `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"` | ||||||
|  | 	VerifySSL bool          `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` | ||||||
|  | 	// Save data for dead ports? ie. ports that are down or disabled.
 | ||||||
|  | 	DeadPorts bool `json:"dead_ports" toml:"dead_ports" xml:"dead_ports" yaml:"dead_ports"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // InfluxDB allows the data to be nested in the config file.
 | ||||||
|  | type InfluxDB struct { | ||||||
|  | 	*Config `json:"influxdb" toml:"influxdb" xml:"influxdb" yaml:"influxdb"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // InfluxUnifi is returned by New() after you provide a Config.
 | ||||||
|  | type InfluxUnifi struct { | ||||||
|  | 	Collector poller.Collect | ||||||
|  | 	influx    influx.Client | ||||||
|  | 	LastCheck time.Time | ||||||
|  | 	*InfluxDB | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type metric struct { | ||||||
|  | 	Table  string | ||||||
|  | 	Tags   map[string]string | ||||||
|  | 	Fields map[string]interface{} | ||||||
|  | 	TS     time.Time | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func init() { // nolint: gochecknoinits
 | ||||||
|  | 	u := &InfluxUnifi{InfluxDB: &InfluxDB{}, LastCheck: time.Now()} | ||||||
|  | 
 | ||||||
|  | 	poller.NewOutput(&poller.Output{ | ||||||
|  | 		Name:   PluginName, | ||||||
|  | 		Config: u.InfluxDB, | ||||||
|  | 		Method: u.Run, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // PollController runs forever, polling UniFi and pushing to InfluxDB
 | ||||||
|  | // This is started by Run() or RunBoth() after everything checks out.
 | ||||||
|  | func (u *InfluxUnifi) PollController() { | ||||||
|  | 	interval := u.Interval.Round(time.Second) | ||||||
|  | 	ticker := time.NewTicker(interval) | ||||||
|  | 	log.Printf("[INFO] Poller->InfluxDB started, interval: %v, dp: %v, db: %s, url: %s", | ||||||
|  | 		interval, u.DeadPorts, u.DB, u.URL) | ||||||
|  | 
 | ||||||
|  | 	for u.LastCheck = range ticker.C { | ||||||
|  | 		metrics, err := u.Collector.Metrics(&poller.Filter{Name: "unifi"}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			u.LogErrorf("metric fetch for InfluxDB failed: %v", err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		events, err := u.Collector.Events(&poller.Filter{Name: "unifi", Dur: interval}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			u.LogErrorf("event fetch for InfluxDB failed: %v", err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		report, err := u.ReportMetrics(metrics, events) | ||||||
|  | 		if err != nil { | ||||||
|  | 			// XXX: reset and re-auth? not sure..
 | ||||||
|  | 			u.LogErrorf("%v", err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		u.Logf("UniFi Metrics Recorded. %v", report) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Run runs a ticker to poll the unifi server and update influxdb.
 | ||||||
|  | func (u *InfluxUnifi) Run(c poller.Collect) error { | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	if u.Collector = c; u.Config == nil || u.Disable { | ||||||
|  | 		u.Logf("InfluxDB config missing (or disabled), InfluxDB output disabled!") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	u.setConfigDefaults() | ||||||
|  | 
 | ||||||
|  | 	u.influx, err = influx.NewHTTPClient(influx.HTTPConfig{ | ||||||
|  | 		Addr:      u.URL, | ||||||
|  | 		Username:  u.User, | ||||||
|  | 		Password:  u.Pass, | ||||||
|  | 		TLSConfig: &tls.Config{InsecureSkipVerify: !u.VerifySSL}, // nolint: gosec
 | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("making client: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fake := *u.Config | ||||||
|  | 	fake.Pass = strconv.FormatBool(fake.Pass != "") | ||||||
|  | 
 | ||||||
|  | 	webserver.UpdateOutput(&webserver.Output{Name: PluginName, Config: fake}) | ||||||
|  | 	u.PollController() | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) setConfigDefaults() { | ||||||
|  | 	if u.URL == "" { | ||||||
|  | 		u.URL = defaultInfluxURL | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if u.User == "" { | ||||||
|  | 		u.User = defaultInfluxUser | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if strings.HasPrefix(u.Pass, "file://") { | ||||||
|  | 		u.Pass = u.getPassFromFile(strings.TrimPrefix(u.Pass, "file://")) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if u.Pass == "" { | ||||||
|  | 		u.Pass = defaultInfluxUser | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if u.DB == "" { | ||||||
|  | 		u.DB = defaultInfluxDB | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if u.Interval.Duration == 0 { | ||||||
|  | 		u.Interval = cnfg.Duration{Duration: defaultInterval} | ||||||
|  | 	} else if u.Interval.Duration < minimumInterval { | ||||||
|  | 		u.Interval = cnfg.Duration{Duration: minimumInterval} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	u.Interval = cnfg.Duration{Duration: u.Interval.Duration.Round(time.Second)} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) getPassFromFile(filename string) string { | ||||||
|  | 	b, err := ioutil.ReadFile(filename) | ||||||
|  | 	if err != nil { | ||||||
|  | 		u.LogErrorf("Reading InfluxDB Password File: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return strings.TrimSpace(string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ReportMetrics batches all device and client data into influxdb data points.
 | ||||||
|  | // Call this after you've collected all the data you care about.
 | ||||||
|  | // Returns an error if influxdb calls fail, otherwise returns a report.
 | ||||||
|  | func (u *InfluxUnifi) ReportMetrics(m *poller.Metrics, e *poller.Events) (*Report, error) { | ||||||
|  | 	r := &Report{ | ||||||
|  | 		Metrics: m, | ||||||
|  | 		Events:  e, | ||||||
|  | 		ch:      make(chan *metric), | ||||||
|  | 		Start:   time.Now(), | ||||||
|  | 		Counts:  &Counts{Val: make(map[item]int)}, | ||||||
|  | 	} | ||||||
|  | 	defer close(r.ch) | ||||||
|  | 
 | ||||||
|  | 	var err error | ||||||
|  | 
 | ||||||
|  | 	// Make a new Influx Points Batcher.
 | ||||||
|  | 	r.bp, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.DB}) | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("influx.NewBatchPoint: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	go u.collect(r, r.ch) | ||||||
|  | 	// Batch all the points.
 | ||||||
|  | 	u.loopPoints(r) | ||||||
|  | 	r.wg.Wait() // wait for all points to finish batching!
 | ||||||
|  | 
 | ||||||
|  | 	// Send all the points.
 | ||||||
|  | 	if err = u.influx.Write(r.bp); err != nil { | ||||||
|  | 		return nil, fmt.Errorf("influxdb.Write(points): %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.Elapsed = time.Since(r.Start) | ||||||
|  | 
 | ||||||
|  | 	return r, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // collect runs in a go routine and batches all the points.
 | ||||||
|  | func (u *InfluxUnifi) collect(r report, ch chan *metric) { | ||||||
|  | 	for m := range ch { | ||||||
|  | 		if m.TS.IsZero() { | ||||||
|  | 			m.TS = r.metrics().TS | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		pt, err := influx.NewPoint(m.Table, m.Tags, m.Fields, m.TS) | ||||||
|  | 		if err == nil { | ||||||
|  | 			r.batch(m, pt) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		r.error(err) | ||||||
|  | 		r.done() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // loopPoints kicks off 3 or 7 go routines to process metrics and send them
 | ||||||
|  | // to the collect routine through the metric channel.
 | ||||||
|  | func (u *InfluxUnifi) loopPoints(r report) { | ||||||
|  | 	m := r.metrics() | ||||||
|  | 
 | ||||||
|  | 	for _, s := range m.RogueAPs { | ||||||
|  | 		u.switchExport(r, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, s := range m.Sites { | ||||||
|  | 		u.switchExport(r, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, s := range m.SitesDPI { | ||||||
|  | 		u.batchSiteDPI(r, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, s := range m.Clients { | ||||||
|  | 		u.switchExport(r, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, s := range m.Devices { | ||||||
|  | 		u.switchExport(r, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, s := range r.events().Logs { | ||||||
|  | 		u.switchExport(r, s) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	appTotal := make(totalsDPImap) | ||||||
|  | 	catTotal := make(totalsDPImap) | ||||||
|  | 
 | ||||||
|  | 	for _, s := range m.ClientsDPI { | ||||||
|  | 		u.batchClientDPI(r, s, appTotal, catTotal) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	reportClientDPItotals(r, appTotal, catTotal) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) switchExport(r report, v interface{}) { //nolint:cyclop
 | ||||||
|  | 	switch v := v.(type) { | ||||||
|  | 	case *unifi.RogueAP: | ||||||
|  | 		u.batchRogueAP(r, v) | ||||||
|  | 	case *unifi.UAP: | ||||||
|  | 		u.batchUAP(r, v) | ||||||
|  | 	case *unifi.USW: | ||||||
|  | 		u.batchUSW(r, v) | ||||||
|  | 	case *unifi.USG: | ||||||
|  | 		u.batchUSG(r, v) | ||||||
|  | 	case *unifi.UXG: | ||||||
|  | 		u.batchUXG(r, v) | ||||||
|  | 	case *unifi.UDM: | ||||||
|  | 		u.batchUDM(r, v) | ||||||
|  | 	case *unifi.Site: | ||||||
|  | 		u.batchSite(r, v) | ||||||
|  | 	case *unifi.Client: | ||||||
|  | 		u.batchClient(r, v) | ||||||
|  | 	case *unifi.Event: | ||||||
|  | 		u.batchEvent(r, v) | ||||||
|  | 	case *unifi.IDS: | ||||||
|  | 		u.batchIDS(r, v) | ||||||
|  | 	case *unifi.Alarm: | ||||||
|  | 		u.batchAlarms(r, v) | ||||||
|  | 	case *unifi.Anomaly: | ||||||
|  | 		u.batchAnomaly(r, v) | ||||||
|  | 	default: | ||||||
|  | 		u.LogErrorf("invalid export type: %T", v) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	"github.com/unpoller/webserver" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Logf logs a message.
 | ||||||
|  | func (u *InfluxUnifi) Logf(msg string, v ...interface{}) { | ||||||
|  | 	webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ | ||||||
|  | 		Ts:   time.Now(), | ||||||
|  | 		Msg:  fmt.Sprintf(msg, v...), | ||||||
|  | 		Tags: map[string]string{"type": "info"}, | ||||||
|  | 	}) | ||||||
|  | 	u.Collector.Logf(msg, v...) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LogErrorf logs an error message.
 | ||||||
|  | func (u *InfluxUnifi) LogErrorf(msg string, v ...interface{}) { | ||||||
|  | 	webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ | ||||||
|  | 		Ts:   time.Now(), | ||||||
|  | 		Msg:  fmt.Sprintf(msg, v...), | ||||||
|  | 		Tags: map[string]string{"type": "error"}, | ||||||
|  | 	}) | ||||||
|  | 	u.Collector.LogErrorf(msg, v...) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LogDebugf logs a debug message.
 | ||||||
|  | func (u *InfluxUnifi) LogDebugf(msg string, v ...interface{}) { | ||||||
|  | 	webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ | ||||||
|  | 		Ts:   time.Now(), | ||||||
|  | 		Msg:  fmt.Sprintf(msg, v...), | ||||||
|  | 		Tags: map[string]string{"type": "debug"}, | ||||||
|  | 	}) | ||||||
|  | 	u.Collector.LogDebugf(msg, v...) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,113 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | 
 | ||||||
|  | 	influx "github.com/influxdata/influxdb1-client/v2" | ||||||
|  | 	"github.com/unpoller/poller" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Report is returned to the calling procedure after everything is processed.
 | ||||||
|  | type Report struct { | ||||||
|  | 	Metrics *poller.Metrics | ||||||
|  | 	Events  *poller.Events | ||||||
|  | 	Errors  []error | ||||||
|  | 	Counts  *Counts | ||||||
|  | 	Start   time.Time | ||||||
|  | 	Elapsed time.Duration | ||||||
|  | 	ch      chan *metric | ||||||
|  | 	wg      sync.WaitGroup | ||||||
|  | 	bp      influx.BatchPoints | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Counts holds counters and has a lock to deal with routines.
 | ||||||
|  | type Counts struct { | ||||||
|  | 	Val map[item]int | ||||||
|  | 	sync.RWMutex | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // report is an internal interface that can be mocked and overridden for tests.
 | ||||||
|  | type report interface { | ||||||
|  | 	add() | ||||||
|  | 	done() | ||||||
|  | 	send(m *metric) | ||||||
|  | 	error(err error) | ||||||
|  | 	batch(m *metric, pt *influx.Point) | ||||||
|  | 	metrics() *poller.Metrics | ||||||
|  | 	events() *poller.Events | ||||||
|  | 	addCount(item, ...int) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Report) metrics() *poller.Metrics { | ||||||
|  | 	return r.Metrics | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Report) events() *poller.Events { | ||||||
|  | 	return r.Events | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Report) add() { | ||||||
|  | 	r.wg.Add(1) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Report) done() { | ||||||
|  | 	r.wg.Done() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Report) send(m *metric) { | ||||||
|  | 	r.wg.Add(1) | ||||||
|  | 	r.ch <- m | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /* The following methods are not thread safe. */ | ||||||
|  | 
 | ||||||
|  | type item string | ||||||
|  | 
 | ||||||
|  | func (r *Report) addCount(name item, counts ...int) { | ||||||
|  | 	r.Counts.Lock() | ||||||
|  | 	defer r.Counts.Unlock() | ||||||
|  | 
 | ||||||
|  | 	if len(counts) == 0 { | ||||||
|  | 		r.Counts.Val[name]++ | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, c := range counts { | ||||||
|  | 		r.Counts.Val[name] += c | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Report) error(err error) { | ||||||
|  | 	if err != nil { | ||||||
|  | 		r.Errors = append(r.Errors, err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // These constants are used as names for printed/logged counters.
 | ||||||
|  | const ( | ||||||
|  | 	pointT = item("Point") | ||||||
|  | 	fieldT = item("Fields") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (r *Report) batch(m *metric, p *influx.Point) { | ||||||
|  | 	r.addCount(pointT) | ||||||
|  | 	r.addCount(fieldT, len(m.Fields)) | ||||||
|  | 	r.bp.AddPoint(p) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (r *Report) String() string { | ||||||
|  | 	r.Counts.RLock() | ||||||
|  | 	defer r.Counts.RUnlock() | ||||||
|  | 
 | ||||||
|  | 	m, c := r.Metrics, r.Counts.Val | ||||||
|  | 
 | ||||||
|  | 	return fmt.Sprintf("Site: %d, Client: %d, "+ | ||||||
|  | 		"Gateways: %d, %s: %d, %s: %d, %s/%s/%s/%s: %d/%d/%d/%d, "+ | ||||||
|  | 		"DPI Site/Client: %d/%d, %s: %d, %s: %d, Err: %d, Dur: %v", | ||||||
|  | 		len(m.Sites), len(m.Clients), | ||||||
|  | 		c[udmT]+c[usgT]+c[uxgT], uapT, c[uapT], uswT, c[uswT], | ||||||
|  | 		idsT, eventT, alarmT, anomalyT, c[idsT], c[eventT], c[alarmT], c[anomalyT], | ||||||
|  | 		len(m.SitesDPI), len(m.ClientsDPI), pointT, c[pointT], fieldT, c[fieldT], | ||||||
|  | 		len(r.Errors), r.Elapsed.Round(time.Millisecond)) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,84 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // batchSite generates Unifi Sites' datapoints for InfluxDB.
 | ||||||
|  | // These points can be passed directly to influx.
 | ||||||
|  | func (u *InfluxUnifi) batchSite(r report, s *unifi.Site) { | ||||||
|  | 	for _, h := range s.Health { | ||||||
|  | 		tags := map[string]string{ | ||||||
|  | 			"name":      s.Name, | ||||||
|  | 			"site_name": s.SiteName, | ||||||
|  | 			"source":    s.SourceName, | ||||||
|  | 			"desc":      s.Desc, | ||||||
|  | 			"status":    h.Status, | ||||||
|  | 			"subsystem": h.Subsystem, | ||||||
|  | 			"wan_ip":    h.WanIP, | ||||||
|  | 			"gw_name":   h.GwName, | ||||||
|  | 			"lan_ip":    h.LanIP, | ||||||
|  | 		} | ||||||
|  | 		fields := map[string]interface{}{ | ||||||
|  | 			"num_user":                 h.NumUser.Val, | ||||||
|  | 			"num_guest":                h.NumGuest.Val, | ||||||
|  | 			"num_iot":                  h.NumIot.Val, | ||||||
|  | 			"tx_bytes-r":               h.TxBytesR.Val, | ||||||
|  | 			"rx_bytes-r":               h.RxBytesR.Val, | ||||||
|  | 			"num_ap":                   h.NumAp.Val, | ||||||
|  | 			"num_adopted":              h.NumAdopted.Val, | ||||||
|  | 			"num_disabled":             h.NumDisabled.Val, | ||||||
|  | 			"num_disconnected":         h.NumDisconnected.Val, | ||||||
|  | 			"num_pending":              h.NumPending.Val, | ||||||
|  | 			"num_gw":                   h.NumGw.Val, | ||||||
|  | 			"wan_ip":                   h.WanIP, | ||||||
|  | 			"num_sta":                  h.NumSta.Val, | ||||||
|  | 			"gw_cpu":                   h.GwSystemStats.CPU.Val, | ||||||
|  | 			"gw_mem":                   h.GwSystemStats.Mem.Val, | ||||||
|  | 			"gw_uptime":                h.GwSystemStats.Uptime.Val, | ||||||
|  | 			"latency":                  h.Latency.Val, | ||||||
|  | 			"uptime":                   h.Uptime.Val, | ||||||
|  | 			"drops":                    h.Drops.Val, | ||||||
|  | 			"xput_up":                  h.XputUp.Val, | ||||||
|  | 			"xput_down":                h.XputDown.Val, | ||||||
|  | 			"speedtest_ping":           h.SpeedtestPing.Val, | ||||||
|  | 			"speedtest_lastrun":        h.SpeedtestLastrun.Val, | ||||||
|  | 			"num_sw":                   h.NumSw.Val, | ||||||
|  | 			"remote_user_num_active":   h.RemoteUserNumActive.Val, | ||||||
|  | 			"remote_user_num_inactive": h.RemoteUserNumInactive.Val, | ||||||
|  | 			"remote_user_rx_bytes":     h.RemoteUserRxBytes.Val, | ||||||
|  | 			"remote_user_tx_bytes":     h.RemoteUserTxBytes.Val, | ||||||
|  | 			"remote_user_rx_packets":   h.RemoteUserRxPackets.Val, | ||||||
|  | 			"remote_user_tx_packets":   h.RemoteUserTxPackets.Val, | ||||||
|  | 			"num_new_alarms":           s.NumNewAlarms.Val, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		r.send(&metric{Table: "subsystems", Tags: tags, Fields: fields}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchSiteDPI(r report, v interface{}) { | ||||||
|  | 	s, ok := v.(*unifi.DPITable) | ||||||
|  | 	if !ok { | ||||||
|  | 		u.LogErrorf("invalid type given to batchSiteDPI: %T", v) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, dpi := range s.ByApp { | ||||||
|  | 		r.send(&metric{ | ||||||
|  | 			Table: "sitedpi", | ||||||
|  | 			Tags: map[string]string{ | ||||||
|  | 				"category":    unifi.DPICats.Get(dpi.Cat), | ||||||
|  | 				"application": unifi.DPIApps.GetApp(dpi.Cat, dpi.App), | ||||||
|  | 				"site_name":   s.SiteName, | ||||||
|  | 				"source":      s.SourceName, | ||||||
|  | 			}, | ||||||
|  | 			Fields: map[string]interface{}{ | ||||||
|  | 				"tx_packets": dpi.TxPackets, | ||||||
|  | 				"rx_packets": dpi.RxPackets, | ||||||
|  | 				"tx_bytes":   dpi.TxBytes, | ||||||
|  | 				"rx_bytes":   dpi.RxBytes, | ||||||
|  | 			}, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,227 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // uapT is used as a name for printed/logged counters.
 | ||||||
|  | const uapT = item("UAP") | ||||||
|  | 
 | ||||||
|  | // batchRogueAP generates metric points for neighboring access points.
 | ||||||
|  | func (u *InfluxUnifi) batchRogueAP(r report, s *unifi.RogueAP) { | ||||||
|  | 	if s.Age.Val == 0 { | ||||||
|  | 		return // only keep metrics for things that are recent.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	r.send(&metric{ | ||||||
|  | 		Table: "uap_rogue", | ||||||
|  | 		Tags: map[string]string{ | ||||||
|  | 			"security":   s.Security, | ||||||
|  | 			"oui":        s.Oui, | ||||||
|  | 			"band":       s.Band, | ||||||
|  | 			"mac":        s.Bssid, | ||||||
|  | 			"ap_mac":     s.ApMac, | ||||||
|  | 			"radio":      s.Radio, | ||||||
|  | 			"radio_name": s.RadioName, | ||||||
|  | 			"site_name":  s.SiteName, | ||||||
|  | 			"name":       s.Essid, | ||||||
|  | 			"source":     s.SourceName, | ||||||
|  | 		}, | ||||||
|  | 		Fields: map[string]interface{}{ | ||||||
|  | 			"age":         s.Age.Val, | ||||||
|  | 			"bw":          s.Bw.Val, | ||||||
|  | 			"center_freq": s.CenterFreq.Val, | ||||||
|  | 			"channel":     s.Channel, | ||||||
|  | 			"freq":        s.Freq.Val, | ||||||
|  | 			"noise":       s.Noise.Val, | ||||||
|  | 			"rssi":        s.Rssi.Val, | ||||||
|  | 			"rssi_age":    s.RssiAge.Val, | ||||||
|  | 			"signal":      s.Signal.Val, | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // batchUAP generates Wireless-Access-Point datapoints for InfluxDB.
 | ||||||
|  | // These points can be passed directly to influx.
 | ||||||
|  | func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { | ||||||
|  | 	if !s.Adopted.Val || s.Locating.Val { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tags := map[string]string{ | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields := Combine(u.processUAPstats(s.Stat.Ap), u.batchSysStats(s.SysStats, s.SystemStats)) | ||||||
|  | 	fields["ip"] = s.IP | ||||||
|  | 	fields["bytes"] = s.Bytes.Val | ||||||
|  | 	fields["last_seen"] = s.LastSeen.Val | ||||||
|  | 	fields["rx_bytes"] = s.RxBytes.Val | ||||||
|  | 	fields["tx_bytes"] = s.TxBytes.Val | ||||||
|  | 	fields["uptime"] = s.Uptime.Val | ||||||
|  | 	fields["user-num_sta"] = int(s.UserNumSta.Val) | ||||||
|  | 	fields["guest-num_sta"] = int(s.GuestNumSta.Val) | ||||||
|  | 	fields["num_sta"] = s.NumSta.Val | ||||||
|  | 
 | ||||||
|  | 	r.addCount(uapT) | ||||||
|  | 	r.send(&metric{Table: "uap", Tags: tags, Fields: fields}) | ||||||
|  | 	u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats) | ||||||
|  | 	u.processVAPTable(r, tags, s.VapTable) | ||||||
|  | 	u.batchPortTable(r, tags, s.PortTable) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) processUAPstats(ap *unifi.Ap) map[string]interface{} { | ||||||
|  | 	if ap == nil { | ||||||
|  | 		return map[string]interface{}{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Accumulative Statistics.
 | ||||||
|  | 	return map[string]interface{}{ | ||||||
|  | 		"stat_user-rx_packets":  ap.UserRxPackets.Val, | ||||||
|  | 		"stat_guest-rx_packets": ap.GuestRxPackets.Val, | ||||||
|  | 		"stat_rx_packets":       ap.RxPackets.Val, | ||||||
|  | 		"stat_user-rx_bytes":    ap.UserRxBytes.Val, | ||||||
|  | 		"stat_guest-rx_bytes":   ap.GuestRxBytes.Val, | ||||||
|  | 		"stat_rx_bytes":         ap.RxBytes.Val, | ||||||
|  | 		"stat_user-rx_errors":   ap.UserRxErrors.Val, | ||||||
|  | 		"stat_guest-rx_errors":  ap.GuestRxErrors.Val, | ||||||
|  | 		"stat_rx_errors":        ap.RxErrors.Val, | ||||||
|  | 		"stat_user-rx_dropped":  ap.UserRxDropped.Val, | ||||||
|  | 		"stat_guest-rx_dropped": ap.GuestRxDropped.Val, | ||||||
|  | 		"stat_rx_dropped":       ap.RxDropped.Val, | ||||||
|  | 		"stat_user-rx_crypts":   ap.UserRxCrypts.Val, | ||||||
|  | 		"stat_guest-rx_crypts":  ap.GuestRxCrypts.Val, | ||||||
|  | 		"stat_rx_crypts":        ap.RxCrypts.Val, | ||||||
|  | 		"stat_user-rx_frags":    ap.UserRxFrags.Val, | ||||||
|  | 		"stat_guest-rx_frags":   ap.GuestRxFrags.Val, | ||||||
|  | 		"stat_rx_frags":         ap.RxFrags.Val, | ||||||
|  | 		"stat_user-tx_packets":  ap.UserTxPackets.Val, | ||||||
|  | 		"stat_guest-tx_packets": ap.GuestTxPackets.Val, | ||||||
|  | 		"stat_tx_packets":       ap.TxPackets.Val, | ||||||
|  | 		"stat_user-tx_bytes":    ap.UserTxBytes.Val, | ||||||
|  | 		"stat_guest-tx_bytes":   ap.GuestTxBytes.Val, | ||||||
|  | 		"stat_tx_bytes":         ap.TxBytes.Val, | ||||||
|  | 		"stat_user-tx_errors":   ap.UserTxErrors.Val, | ||||||
|  | 		"stat_guest-tx_errors":  ap.GuestTxErrors.Val, | ||||||
|  | 		"stat_tx_errors":        ap.TxErrors.Val, | ||||||
|  | 		"stat_user-tx_dropped":  ap.UserTxDropped.Val, | ||||||
|  | 		"stat_guest-tx_dropped": ap.GuestTxDropped.Val, | ||||||
|  | 		"stat_tx_dropped":       ap.TxDropped.Val, | ||||||
|  | 		"stat_user-tx_retries":  ap.UserTxRetries.Val, | ||||||
|  | 		"stat_guest-tx_retries": ap.GuestTxRetries.Val, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // processVAPTable creates points for Wifi Radios. This works with several types of UAP-capable devices.
 | ||||||
|  | func (u *InfluxUnifi) processVAPTable(r report, t map[string]string, vt unifi.VapTable) { // nolint: funlen
 | ||||||
|  | 	for _, s := range vt { | ||||||
|  | 		tags := map[string]string{ | ||||||
|  | 			"device_name": t["name"], | ||||||
|  | 			"site_name":   t["site_name"], | ||||||
|  | 			"source":      t["source"], | ||||||
|  | 			"ap_mac":      s.ApMac, | ||||||
|  | 			"bssid":       s.Bssid, | ||||||
|  | 			"id":          s.ID, | ||||||
|  | 			"name":        s.Name, | ||||||
|  | 			"radio_name":  s.RadioName, | ||||||
|  | 			"radio":       s.Radio, | ||||||
|  | 			"essid":       s.Essid, | ||||||
|  | 			"site_id":     s.SiteID, | ||||||
|  | 			"usage":       s.Usage, | ||||||
|  | 			"state":       s.State, | ||||||
|  | 			"is_guest":    s.IsGuest.Txt, | ||||||
|  | 		} | ||||||
|  | 		fields := map[string]interface{}{ | ||||||
|  | 			"ccq":                       s.Ccq, | ||||||
|  | 			"mac_filter_rejections":     s.MacFilterRejections, | ||||||
|  | 			"num_satisfaction_sta":      s.NumSatisfactionSta.Val, | ||||||
|  | 			"avg_client_signal":         s.AvgClientSignal.Val, | ||||||
|  | 			"satisfaction":              s.Satisfaction.Val, | ||||||
|  | 			"satisfaction_now":          s.SatisfactionNow.Val, | ||||||
|  | 			"num_sta":                   s.NumSta, | ||||||
|  | 			"channel":                   s.Channel.Val, | ||||||
|  | 			"rx_bytes":                  s.RxBytes.Val, | ||||||
|  | 			"rx_crypts":                 s.RxCrypts.Val, | ||||||
|  | 			"rx_dropped":                s.RxDropped.Val, | ||||||
|  | 			"rx_errors":                 s.RxErrors.Val, | ||||||
|  | 			"rx_frags":                  s.RxFrags.Val, | ||||||
|  | 			"rx_nwids":                  s.RxNwids.Val, | ||||||
|  | 			"rx_packets":                s.RxPackets.Val, | ||||||
|  | 			"tx_bytes":                  s.TxBytes.Val, | ||||||
|  | 			"tx_dropped":                s.TxDropped.Val, | ||||||
|  | 			"tx_errors":                 s.TxErrors.Val, | ||||||
|  | 			"tx_packets":                s.TxPackets.Val, | ||||||
|  | 			"tx_power":                  s.TxPower.Val, | ||||||
|  | 			"tx_retries":                s.TxRetries.Val, | ||||||
|  | 			"tx_combined_retries":       s.TxCombinedRetries.Val, | ||||||
|  | 			"tx_data_mpdu_bytes":        s.TxDataMpduBytes.Val, | ||||||
|  | 			"tx_rts_retries":            s.TxRtsRetries.Val, | ||||||
|  | 			"tx_success":                s.TxSuccess.Val, | ||||||
|  | 			"tx_total":                  s.TxTotal.Val, | ||||||
|  | 			"tx_tcp_goodbytes":          s.TxTCPStats.Goodbytes.Val, | ||||||
|  | 			"tx_tcp_lat_avg":            s.TxTCPStats.LatAvg.Val, | ||||||
|  | 			"tx_tcp_lat_max":            s.TxTCPStats.LatMax.Val, | ||||||
|  | 			"tx_tcp_lat_min":            s.TxTCPStats.LatMin.Val, | ||||||
|  | 			"rx_tcp_goodbytes":          s.RxTCPStats.Goodbytes.Val, | ||||||
|  | 			"rx_tcp_lat_avg":            s.RxTCPStats.LatAvg.Val, | ||||||
|  | 			"rx_tcp_lat_max":            s.RxTCPStats.LatMax.Val, | ||||||
|  | 			"rx_tcp_lat_min":            s.RxTCPStats.LatMin.Val, | ||||||
|  | 			"wifi_tx_latency_mov_avg":   s.WifiTxLatencyMov.Avg.Val, | ||||||
|  | 			"wifi_tx_latency_mov_max":   s.WifiTxLatencyMov.Max.Val, | ||||||
|  | 			"wifi_tx_latency_mov_min":   s.WifiTxLatencyMov.Min.Val, | ||||||
|  | 			"wifi_tx_latency_mov_total": s.WifiTxLatencyMov.Total.Val, | ||||||
|  | 			"wifi_tx_latency_mov_cuont": s.WifiTxLatencyMov.TotalCount.Val, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		r.send(&metric{Table: "uap_vaps", Tags: tags, Fields: fields}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) processRadTable(r report, t map[string]string, rt unifi.RadioTable, rts unifi.RadioTableStats) { | ||||||
|  | 	for _, p := range rt { | ||||||
|  | 		tags := map[string]string{ | ||||||
|  | 			"device_name": t["name"], | ||||||
|  | 			"site_name":   t["site_name"], | ||||||
|  | 			"source":      t["source"], | ||||||
|  | 			"channel":     p.Channel.Txt, | ||||||
|  | 			"radio":       p.Radio, | ||||||
|  | 		} | ||||||
|  | 		fields := map[string]interface{}{ | ||||||
|  | 			"current_antenna_gain": p.CurrentAntennaGain.Val, | ||||||
|  | 			"ht":                   p.Ht.Txt, | ||||||
|  | 			"max_txpower":          p.MaxTxpower.Val, | ||||||
|  | 			"min_txpower":          p.MinTxpower.Val, | ||||||
|  | 			"nss":                  p.Nss.Val, | ||||||
|  | 			"radio_caps":           p.RadioCaps.Val, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for _, t := range rts { | ||||||
|  | 			if t.Name == p.Name { | ||||||
|  | 				fields["ast_be_xmit"] = t.AstBeXmit.Val | ||||||
|  | 				fields["channel"] = t.Channel.Val | ||||||
|  | 				fields["cu_self_rx"] = t.CuSelfRx.Val | ||||||
|  | 				fields["cu_self_tx"] = t.CuSelfTx.Val | ||||||
|  | 				fields["cu_total"] = t.CuTotal.Val | ||||||
|  | 				fields["extchannel"] = t.Extchannel.Val | ||||||
|  | 				fields["gain"] = t.Gain.Val | ||||||
|  | 				fields["guest-num_sta"] = t.GuestNumSta.Val | ||||||
|  | 				fields["num_sta"] = t.NumSta.Val | ||||||
|  | 				fields["radio"] = t.Radio | ||||||
|  | 				fields["tx_packets"] = t.TxPackets.Val | ||||||
|  | 				fields["tx_power"] = t.TxPower.Val | ||||||
|  | 				fields["tx_retries"] = t.TxRetries.Val | ||||||
|  | 				fields["user-num_sta"] = t.UserNumSta.Val | ||||||
|  | 
 | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		r.send(&metric{Table: "uap_radios", Tags: tags, Fields: fields}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,179 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // udmT is used as a name for printed/logged counters.
 | ||||||
|  | const udmT = item("UDM") | ||||||
|  | 
 | ||||||
|  | // Combine concatenates N maps. This will delete things if not used with caution.
 | ||||||
|  | func Combine(in ...map[string]interface{}) map[string]interface{} { | ||||||
|  | 	out := make(map[string]interface{}) | ||||||
|  | 
 | ||||||
|  | 	for i := range in { | ||||||
|  | 		for k := range in[i] { | ||||||
|  | 			out[k] = in[i][k] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return out | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // batchSysStats is used by all device types.
 | ||||||
|  | func (u *InfluxUnifi) batchSysStats(s unifi.SysStats, ss unifi.SystemStats) map[string]interface{} { | ||||||
|  | 	m := map[string]interface{}{ | ||||||
|  | 		"loadavg_1":     s.Loadavg1.Val, | ||||||
|  | 		"loadavg_5":     s.Loadavg5.Val, | ||||||
|  | 		"loadavg_15":    s.Loadavg15.Val, | ||||||
|  | 		"mem_used":      s.MemUsed.Val, | ||||||
|  | 		"mem_buffer":    s.MemBuffer.Val, | ||||||
|  | 		"mem_total":     s.MemTotal.Val, | ||||||
|  | 		"cpu":           ss.CPU.Val, | ||||||
|  | 		"mem":           ss.Mem.Val, | ||||||
|  | 		"system_uptime": ss.Uptime.Val, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for k, v := range ss.Temps { | ||||||
|  | 		temp, _ := strconv.Atoi(strings.Split(v, " ")[0]) | ||||||
|  | 		k = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(k, " ", "_"), ")", ""), "(", "") | ||||||
|  | 
 | ||||||
|  | 		if temp != 0 && k != "" { | ||||||
|  | 			m["temp_"+strings.ToLower(k)] = temp | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return m | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchUDMtemps(temps []unifi.Temperature) map[string]interface{} { | ||||||
|  | 	output := make(map[string]interface{}) | ||||||
|  | 
 | ||||||
|  | 	for _, t := range temps { | ||||||
|  | 		output["temp_"+t.Name] = t.Value | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return output | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchUDMstorage(storage []*unifi.Storage) map[string]interface{} { | ||||||
|  | 	output := make(map[string]interface{}) | ||||||
|  | 
 | ||||||
|  | 	for _, t := range storage { | ||||||
|  | 		output["storage_"+t.Name+"_size"] = t.Size.Val | ||||||
|  | 		output["storage_"+t.Name+"_used"] = t.Used.Val | ||||||
|  | 
 | ||||||
|  | 		if t.Size.Val != 0 && t.Used.Val != 0 && t.Used.Val < t.Size.Val { | ||||||
|  | 			output["storage_"+t.Name+"_pct"] = t.Used.Val / t.Size.Val * 100 //nolint:gomnd
 | ||||||
|  | 		} else { | ||||||
|  | 			output["storage_"+t.Name+"_pct"] = 0 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return output | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // batchUDM generates Unifi Gateway datapoints for InfluxDB.
 | ||||||
|  | // These points can be passed directly to influx.
 | ||||||
|  | func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { // nolint: funlen
 | ||||||
|  | 	if !s.Adopted.Val || s.Locating.Val { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tags := map[string]string{ | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields := Combine( | ||||||
|  | 		u.batchUDMstorage(s.Storage), | ||||||
|  | 		u.batchUDMtemps(s.Temperatures), | ||||||
|  | 		u.batchUSGstats(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), | ||||||
|  | 		u.batchSysStats(s.SysStats, s.SystemStats), | ||||||
|  | 		map[string]interface{}{ | ||||||
|  | 			"source":        s.SourceName, | ||||||
|  | 			"ip":            s.IP, | ||||||
|  | 			"bytes":         s.Bytes.Val, | ||||||
|  | 			"last_seen":     s.LastSeen.Val, | ||||||
|  | 			"license_state": s.LicenseState, | ||||||
|  | 			"guest-num_sta": s.GuestNumSta.Val, | ||||||
|  | 			"rx_bytes":      s.RxBytes.Val, | ||||||
|  | 			"tx_bytes":      s.TxBytes.Val, | ||||||
|  | 			"uptime":        s.Uptime.Val, | ||||||
|  | 			"state":         s.State.Val, | ||||||
|  | 			"user-num_sta":  s.UserNumSta.Val, | ||||||
|  | 			"version":       s.Version, | ||||||
|  | 			"num_desktop":   s.NumDesktop.Val, | ||||||
|  | 			"num_handheld":  s.NumHandheld.Val, | ||||||
|  | 			"num_mobile":    s.NumMobile.Val, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	r.addCount(udmT) | ||||||
|  | 	r.send(&metric{Table: "usg", Tags: tags, Fields: fields}) | ||||||
|  | 	u.batchNetTable(r, tags, s.NetworkTable) | ||||||
|  | 	u.batchUSGwans(r, tags, s.Wan1, s.Wan2) | ||||||
|  | 
 | ||||||
|  | 	tags = map[string]string{ | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields = Combine( | ||||||
|  | 		u.batchUSWstat(s.Stat.Sw), | ||||||
|  | 		map[string]interface{}{ | ||||||
|  | 			"guest-num_sta": s.GuestNumSta.Val, | ||||||
|  | 			"ip":            s.IP, | ||||||
|  | 			"bytes":         s.Bytes.Val, | ||||||
|  | 			"last_seen":     s.LastSeen.Val, | ||||||
|  | 			"rx_bytes":      s.RxBytes.Val, | ||||||
|  | 			"tx_bytes":      s.TxBytes.Val, | ||||||
|  | 			"uptime":        s.Uptime.Val, | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 	r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) | ||||||
|  | 	u.batchPortTable(r, tags, s.PortTable) // udm has a usw in it.
 | ||||||
|  | 
 | ||||||
|  | 	if s.Stat.Ap == nil { | ||||||
|  | 		return // we're done now. the following code process UDM (non-pro) UAP data.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tags = map[string]string{ | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields = u.processUAPstats(s.Stat.Ap) | ||||||
|  | 	fields["ip"] = s.IP | ||||||
|  | 	fields["bytes"] = s.Bytes.Val | ||||||
|  | 	fields["last_seen"] = s.LastSeen.Val | ||||||
|  | 	fields["rx_bytes"] = s.RxBytes.Val | ||||||
|  | 	fields["tx_bytes"] = s.TxBytes.Val | ||||||
|  | 	fields["uptime"] = s.Uptime.Val | ||||||
|  | 	fields["state"] = s.State | ||||||
|  | 	fields["user-num_sta"] = int(s.UserNumSta.Val) | ||||||
|  | 	fields["guest-num_sta"] = int(s.GuestNumSta.Val) | ||||||
|  | 	fields["num_sta"] = s.NumSta.Val | ||||||
|  | 
 | ||||||
|  | 	r.send(&metric{Table: "uap", Tags: tags, Fields: fields}) | ||||||
|  | 	u.processRadTable(r, tags, *s.RadioTable, *s.RadioTableStats) | ||||||
|  | 	u.processVAPTable(r, tags, *s.VapTable) | ||||||
|  | } | ||||||
|  | @ -0,0 +1,147 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // usgT is used as a name for printed/logged counters.
 | ||||||
|  | const usgT = item("USG") | ||||||
|  | 
 | ||||||
|  | // batchUSG generates Unifi Gateway datapoints for InfluxDB.
 | ||||||
|  | // These points can be passed directly to influx.
 | ||||||
|  | func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) { | ||||||
|  | 	if !s.Adopted.Val || s.Locating.Val { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tags := map[string]string{ | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields := Combine( | ||||||
|  | 		u.batchUDMtemps(s.Temperatures), | ||||||
|  | 		u.batchSysStats(s.SysStats, s.SystemStats), | ||||||
|  | 		u.batchUSGstats(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), | ||||||
|  | 		map[string]interface{}{ | ||||||
|  | 			"ip":            s.IP, | ||||||
|  | 			"bytes":         s.Bytes.Val, | ||||||
|  | 			"last_seen":     s.LastSeen.Val, | ||||||
|  | 			"license_state": s.LicenseState, | ||||||
|  | 			"guest-num_sta": s.GuestNumSta.Val, | ||||||
|  | 			"rx_bytes":      s.RxBytes.Val, | ||||||
|  | 			"tx_bytes":      s.TxBytes.Val, | ||||||
|  | 			"uptime":        s.Uptime.Val, | ||||||
|  | 			"state":         s.State.Val, | ||||||
|  | 			"user-num_sta":  s.UserNumSta.Val, | ||||||
|  | 			"version":       s.Version, | ||||||
|  | 			"num_desktop":   s.NumDesktop.Val, | ||||||
|  | 			"num_handheld":  s.NumHandheld.Val, | ||||||
|  | 			"num_mobile":    s.NumMobile.Val, | ||||||
|  | 			//			"speedtest_rundate": time.Unix(int64(s.SpeedtestStatus.Rundate.Val), 0).String(),
 | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	r.addCount(usgT) | ||||||
|  | 	r.send(&metric{Table: "usg", Tags: tags, Fields: fields}) | ||||||
|  | 	u.batchNetTable(r, tags, s.NetworkTable) | ||||||
|  | 	u.batchUSGwans(r, tags, s.Wan1, s.Wan2) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchUSGstats(ss unifi.SpeedtestStatus, gw *unifi.Gw, ul unifi.Uplink) map[string]interface{} { | ||||||
|  | 	if gw == nil { | ||||||
|  | 		return map[string]interface{}{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return map[string]interface{}{ | ||||||
|  | 		"uplink_latency":                 ul.Latency.Val, | ||||||
|  | 		"uplink_speed":                   ul.Speed.Val, | ||||||
|  | 		"speedtest-status_latency":       ss.Latency.Val, | ||||||
|  | 		"speedtest-status_runtime":       ss.Runtime.Val, | ||||||
|  | 		"speedtest-status_rundate":       ss.Rundate.Val, | ||||||
|  | 		"speedtest-status_ping":          ss.StatusPing.Val, | ||||||
|  | 		"speedtest-status_xput_download": ss.XputDownload.Val, | ||||||
|  | 		"speedtest-status_xput_upload":   ss.XputUpload.Val, | ||||||
|  | 		"lan-rx_bytes":                   gw.LanRxBytes.Val, | ||||||
|  | 		"lan-rx_packets":                 gw.LanRxPackets.Val, | ||||||
|  | 		"lan-tx_bytes":                   gw.LanTxBytes.Val, | ||||||
|  | 		"lan-tx_packets":                 gw.LanTxPackets.Val, | ||||||
|  | 		"lan-rx_dropped":                 gw.LanRxDropped.Val, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchUSGwans(r report, tags map[string]string, wans ...unifi.Wan) { | ||||||
|  | 	for _, wan := range wans { | ||||||
|  | 		if !wan.Up.Val { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		tags := map[string]string{ | ||||||
|  | 			"device_name": tags["name"], | ||||||
|  | 			"site_name":   tags["site_name"], | ||||||
|  | 			"source":      tags["source"], | ||||||
|  | 			"ip":          wan.IP, | ||||||
|  | 			"purpose":     wan.Name, | ||||||
|  | 			"mac":         wan.Mac, | ||||||
|  | 			"ifname":      wan.Ifname, | ||||||
|  | 			"type":        wan.Type, | ||||||
|  | 			"up":          wan.Up.Txt, | ||||||
|  | 			"enabled":     wan.Enable.Txt, | ||||||
|  | 		} | ||||||
|  | 		fields := map[string]interface{}{ | ||||||
|  | 			"bytes-r":      wan.BytesR.Val, | ||||||
|  | 			"full_duplex":  wan.FullDuplex.Val, | ||||||
|  | 			"gateway":      wan.Gateway, | ||||||
|  | 			"max_speed":    wan.MaxSpeed.Val, | ||||||
|  | 			"rx_bytes":     wan.RxBytes.Val, | ||||||
|  | 			"rx_bytes-r":   wan.RxBytesR.Val, | ||||||
|  | 			"rx_dropped":   wan.RxDropped.Val, | ||||||
|  | 			"rx_errors":    wan.RxErrors.Val, | ||||||
|  | 			"rx_broadcast": wan.RxBroadcast.Val, | ||||||
|  | 			"rx_multicast": wan.RxMulticast.Val, | ||||||
|  | 			"rx_packets":   wan.RxPackets.Val, | ||||||
|  | 			"speed":        wan.Speed.Val, | ||||||
|  | 			"tx_bytes":     wan.TxBytes.Val, | ||||||
|  | 			"tx_bytes-r":   wan.TxBytesR.Val, | ||||||
|  | 			"tx_dropped":   wan.TxDropped.Val, | ||||||
|  | 			"tx_errors":    wan.TxErrors.Val, | ||||||
|  | 			"tx_packets":   wan.TxPackets.Val, | ||||||
|  | 			"tx_broadcast": wan.TxBroadcast.Val, | ||||||
|  | 			"tx_multicast": wan.TxMulticast.Val, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		r.send(&metric{Table: "usg_wan_ports", Tags: tags, Fields: fields}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchNetTable(r report, tags map[string]string, nt unifi.NetworkTable) { | ||||||
|  | 	for _, p := range nt { | ||||||
|  | 		tags := map[string]string{ | ||||||
|  | 			"device_name": tags["name"], | ||||||
|  | 			"site_name":   tags["site_name"], | ||||||
|  | 			"source":      tags["source"], | ||||||
|  | 			"up":          p.Up.Txt, | ||||||
|  | 			"enabled":     p.Enabled.Txt, | ||||||
|  | 			"ip":          p.IP, | ||||||
|  | 			"mac":         p.Mac, | ||||||
|  | 			"name":        p.Name, | ||||||
|  | 			"domain_name": p.DomainName, | ||||||
|  | 			"purpose":     p.Purpose, | ||||||
|  | 			"is_guest":    p.IsGuest.Txt, | ||||||
|  | 		} | ||||||
|  | 		fields := map[string]interface{}{ | ||||||
|  | 			"num_sta":    p.NumSta.Val, | ||||||
|  | 			"rx_bytes":   p.RxBytes.Val, | ||||||
|  | 			"rx_packets": p.RxPackets.Val, | ||||||
|  | 			"tx_bytes":   p.TxBytes.Val, | ||||||
|  | 			"tx_packets": p.TxPackets.Val, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		r.send(&metric{Table: "usg_networks", Tags: tags, Fields: fields}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,133 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // uswT is used as a name for printed/logged counters.
 | ||||||
|  | const uswT = item("USW") | ||||||
|  | 
 | ||||||
|  | // batchUSW generates Unifi Switch datapoints for InfluxDB.
 | ||||||
|  | // These points can be passed directly to influx.
 | ||||||
|  | func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) { | ||||||
|  | 	if !s.Adopted.Val || s.Locating.Val { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tags := map[string]string{ | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields := Combine( | ||||||
|  | 		u.batchUSWstat(s.Stat.Sw), | ||||||
|  | 		u.batchSysStats(s.SysStats, s.SystemStats), | ||||||
|  | 		map[string]interface{}{ | ||||||
|  | 			"guest-num_sta":       s.GuestNumSta.Val, | ||||||
|  | 			"ip":                  s.IP, | ||||||
|  | 			"bytes":               s.Bytes.Val, | ||||||
|  | 			"fan_level":           s.FanLevel.Val, | ||||||
|  | 			"general_temperature": s.GeneralTemperature.Val, | ||||||
|  | 			"last_seen":           s.LastSeen.Val, | ||||||
|  | 			"rx_bytes":            s.RxBytes.Val, | ||||||
|  | 			"tx_bytes":            s.TxBytes.Val, | ||||||
|  | 			"uptime":              s.Uptime.Val, | ||||||
|  | 			"state":               s.State.Val, | ||||||
|  | 			"user-num_sta":        s.UserNumSta.Val, | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 	r.addCount(uswT) | ||||||
|  | 	r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) | ||||||
|  | 	u.batchPortTable(r, tags, s.PortTable) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (u *InfluxUnifi) batchUSWstat(sw *unifi.Sw) map[string]interface{} { | ||||||
|  | 	if sw == nil { | ||||||
|  | 		return map[string]interface{}{} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return map[string]interface{}{ | ||||||
|  | 		"stat_bytes":      sw.Bytes.Val, | ||||||
|  | 		"stat_rx_bytes":   sw.RxBytes.Val, | ||||||
|  | 		"stat_rx_crypts":  sw.RxCrypts.Val, | ||||||
|  | 		"stat_rx_dropped": sw.RxDropped.Val, | ||||||
|  | 		"stat_rx_errors":  sw.RxErrors.Val, | ||||||
|  | 		"stat_rx_frags":   sw.RxFrags.Val, | ||||||
|  | 		"stat_rx_packets": sw.TxPackets.Val, | ||||||
|  | 		"stat_tx_bytes":   sw.TxBytes.Val, | ||||||
|  | 		"stat_tx_dropped": sw.TxDropped.Val, | ||||||
|  | 		"stat_tx_errors":  sw.TxErrors.Val, | ||||||
|  | 		"stat_tx_packets": sw.TxPackets.Val, | ||||||
|  | 		"stat_tx_retries": sw.TxRetries.Val, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | //nolint:funlen
 | ||||||
|  | func (u *InfluxUnifi) batchPortTable(r report, t map[string]string, pt []unifi.Port) { | ||||||
|  | 	for _, p := range pt { | ||||||
|  | 		if !u.DeadPorts && (!p.Up.Val || !p.Enable.Val) { | ||||||
|  | 			continue // only record UP ports.
 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		tags := map[string]string{ | ||||||
|  | 			"site_name":      t["site_name"], | ||||||
|  | 			"device_name":    t["name"], | ||||||
|  | 			"source":         t["source"], | ||||||
|  | 			"type":           t["type"], | ||||||
|  | 			"name":           p.Name, | ||||||
|  | 			"poe_mode":       p.PoeMode, | ||||||
|  | 			"port_poe":       p.PortPoe.Txt, | ||||||
|  | 			"port_idx":       p.PortIdx.Txt, | ||||||
|  | 			"port_id":        t["name"] + " Port " + p.PortIdx.Txt, | ||||||
|  | 			"poe_enable":     p.PoeEnable.Txt, | ||||||
|  | 			"flowctrl_rx":    p.FlowctrlRx.Txt, | ||||||
|  | 			"flowctrl_tx":    p.FlowctrlTx.Txt, | ||||||
|  | 			"media":          p.Media, | ||||||
|  | 			"has_sfp":        p.SFPFound.Txt, | ||||||
|  | 			"sfp_compliance": p.SFPCompliance, | ||||||
|  | 			"sfp_serial":     p.SFPSerial, | ||||||
|  | 			"sfp_vendor":     p.SFPVendor, | ||||||
|  | 			"sfp_part":       p.SFPPart, | ||||||
|  | 		} | ||||||
|  | 		fields := map[string]interface{}{ | ||||||
|  | 			"dbytes_r":     p.BytesR.Val, | ||||||
|  | 			"rx_broadcast": p.RxBroadcast.Val, | ||||||
|  | 			"rx_bytes":     p.RxBytes.Val, | ||||||
|  | 			"rx_bytes-r":   p.RxBytesR.Val, | ||||||
|  | 			"rx_dropped":   p.RxDropped.Val, | ||||||
|  | 			"rx_errors":    p.RxErrors.Val, | ||||||
|  | 			"rx_multicast": p.RxMulticast.Val, | ||||||
|  | 			"rx_packets":   p.RxPackets.Val, | ||||||
|  | 			"speed":        p.Speed.Val, | ||||||
|  | 			"stp_pathcost": p.StpPathcost.Val, | ||||||
|  | 			"tx_broadcast": p.TxBroadcast.Val, | ||||||
|  | 			"tx_bytes":     p.TxBytes.Val, | ||||||
|  | 			"tx_bytes-r":   p.TxBytesR.Val, | ||||||
|  | 			"tx_dropped":   p.TxDropped.Val, | ||||||
|  | 			"tx_errors":    p.TxErrors.Val, | ||||||
|  | 			"tx_multicast": p.TxMulticast.Val, | ||||||
|  | 			"tx_packets":   p.TxPackets.Val, | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if p.PoeEnable.Val && p.PortPoe.Val { | ||||||
|  | 			fields["poe_current"] = p.PoeCurrent.Val | ||||||
|  | 			fields["poe_power"] = p.PoePower.Val | ||||||
|  | 			fields["poe_voltage"] = p.PoeVoltage.Val | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if p.SFPFound.Val { | ||||||
|  | 			fields["sfp_current"] = p.SFPCurrent.Val | ||||||
|  | 			fields["sfp_voltage"] = p.SFPVoltage.Val | ||||||
|  | 			fields["sfp_temperature"] = p.SFPTemperature.Val | ||||||
|  | 			fields["sfp_txpower"] = p.SFPTxpower.Val | ||||||
|  | 			fields["sfp_rxpower"] = p.SFPRxpower.Val | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		r.send(&metric{Table: "usw_ports", Tags: tags, Fields: fields}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,80 @@ | ||||||
|  | package influxunifi | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"github.com/unpoller/unifi" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // uxgT is used as a name for printed/logged counters.
 | ||||||
|  | const uxgT = item("UXG") | ||||||
|  | 
 | ||||||
|  | // batchUXG generates 10Gb Unifi Gateway datapoints for InfluxDB.
 | ||||||
|  | // These points can be passed directly to influx.
 | ||||||
|  | func (u *InfluxUnifi) batchUXG(r report, s *unifi.UXG) { // nolint: funlen
 | ||||||
|  | 	if !s.Adopted.Val || s.Locating.Val { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	tags := map[string]string{ | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields := Combine( | ||||||
|  | 		u.batchUDMstorage(s.Storage), | ||||||
|  | 		u.batchUDMtemps(s.Temperatures), | ||||||
|  | 		u.batchUSGstats(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), | ||||||
|  | 		u.batchSysStats(s.SysStats, s.SystemStats), | ||||||
|  | 		map[string]interface{}{ | ||||||
|  | 			"source":        s.SourceName, | ||||||
|  | 			"ip":            s.IP, | ||||||
|  | 			"bytes":         s.Bytes.Val, | ||||||
|  | 			"last_seen":     s.LastSeen.Val, | ||||||
|  | 			"license_state": s.LicenseState, | ||||||
|  | 			"guest-num_sta": s.GuestNumSta.Val, | ||||||
|  | 			"rx_bytes":      s.RxBytes.Val, | ||||||
|  | 			"tx_bytes":      s.TxBytes.Val, | ||||||
|  | 			"uptime":        s.Uptime.Val, | ||||||
|  | 			"state":         s.State.Val, | ||||||
|  | 			"user-num_sta":  s.UserNumSta.Val, | ||||||
|  | 			"version":       s.Version, | ||||||
|  | 			"num_desktop":   s.NumDesktop.Val, | ||||||
|  | 			"num_handheld":  s.NumHandheld.Val, | ||||||
|  | 			"num_mobile":    s.NumMobile.Val, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	r.addCount(uxgT) | ||||||
|  | 	r.send(&metric{Table: "usg", Tags: tags, Fields: fields}) | ||||||
|  | 	u.batchNetTable(r, tags, s.NetworkTable) | ||||||
|  | 	u.batchUSGwans(r, tags, s.Wan1, s.Wan2) | ||||||
|  | 
 | ||||||
|  | 	tags = map[string]string{ | ||||||
|  | 		"mac":       s.Mac, | ||||||
|  | 		"site_name": s.SiteName, | ||||||
|  | 		"source":    s.SourceName, | ||||||
|  | 		"name":      s.Name, | ||||||
|  | 		"version":   s.Version, | ||||||
|  | 		"model":     s.Model, | ||||||
|  | 		"serial":    s.Serial, | ||||||
|  | 		"type":      s.Type, | ||||||
|  | 	} | ||||||
|  | 	fields = Combine( | ||||||
|  | 		u.batchUSWstat(s.Stat.Sw), | ||||||
|  | 		map[string]interface{}{ | ||||||
|  | 			"guest-num_sta": s.GuestNumSta.Val, | ||||||
|  | 			"ip":            s.IP, | ||||||
|  | 			"bytes":         s.Bytes.Val, | ||||||
|  | 			"last_seen":     s.LastSeen.Val, | ||||||
|  | 			"rx_bytes":      s.RxBytes.Val, | ||||||
|  | 			"tx_bytes":      s.TxBytes.Val, | ||||||
|  | 			"uptime":        s.Uptime.Val, | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 	r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) | ||||||
|  | 	u.batchPortTable(r, tags, s.PortTable) // udm has a usw in it.
 | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue