Merge branch 'master' of ../influxunifi into merge-them-all

This commit is contained in:
Cody Lee 2022-11-23 20:58:03 -06:00
commit dd916e3c37
No known key found for this signature in database
17 changed files with 1861 additions and 0 deletions

View File

@ -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

View File

@ -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.

View File

@ -0,0 +1,5 @@
# influxunifi
## UnPoller Input Plugin
Collects UniFi data from a UniFi controller using the API.

View File

@ -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,
}),
})
}

View File

@ -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)
}
}
}
}
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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=

View File

@ -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)
}
}

View File

@ -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...)
}

View File

@ -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))
}

View File

@ -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,
},
})
}
}

View File

@ -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})
}
}

View File

@ -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)
}

View File

@ -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})
}
}

View File

@ -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})
}
}

View File

@ -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.
}