Merge pull request #104 from davidnewhall/dn2_fixes

Improvements.
This commit is contained in:
David Newhall II 2019-08-25 21:09:47 -07:00 committed by GitHub
commit 2099cbffe9
11 changed files with 214 additions and 116 deletions

View File

@ -9,9 +9,9 @@ GHUSER="davidnewhall"
# Github repo containing homebrew formula repo. # Github repo containing homebrew formula repo.
HBREPO="golift/homebrew-mugs" HBREPO="golift/homebrew-mugs"
MAINT="David Newhall II <david at sleepers dot pro>" MAINT="David Newhall II <david at sleepers dot pro>"
VENDOR="Go Lift" VENDOR="Go Lift <code at golift dot io>"
DESC="Polls a UniFi controller and stores metrics in InfluxDB" DESC="Polls a UniFi controller and exports metrics to InfluxDB"
GOLANGCI_LINT_ARGS="--enable-all -D gochecknoglobals -e dupl" GOLANGCI_LINT_ARGS="--enable-all -D gochecknoglobals -e dupl -e G101"
# Example must exist at examples/$CONFIG_FILE.example # Example must exist at examples/$CONFIG_FILE.example
CONFIG_FILE="up.conf" CONFIG_FILE="up.conf"
LICENSE="MIT" LICENSE="MIT"

View File

@ -12,9 +12,19 @@
[![stars](https://badgen.net/github/stars/davidnewhall/unifi-poller?icon=https://simpleicons.now.sh/macys/fab&label=&color=0099ee "GitHub Stars")](https://github.com/davidnewhall/unifi-poller) [![stars](https://badgen.net/github/stars/davidnewhall/unifi-poller?icon=https://simpleicons.now.sh/macys/fab&label=&color=0099ee "GitHub Stars")](https://github.com/davidnewhall/unifi-poller)
[![travis](https://badgen.net/travis/davidnewhall/unifi-poller?icon=travis&label=build "Travis Build")](https://travis-ci.org/davidnewhall/unifi-poller) [![travis](https://badgen.net/travis/davidnewhall/unifi-poller?icon=travis&label=build "Travis Build")](https://travis-ci.org/davidnewhall/unifi-poller)
Collect your UniFi controller data and send it to an InfluxDB instance. Collect your UniFi controller data and export it to an InfluxDB instance.
[Grafana Dashboards](http://grafana.com/dashboards?search=unifi-poller) included. [Five Grafana Dashboards](http://grafana.com/dashboards?search=unifi-poller)
Updated 2019. included; with screenshots. Updated 2019.
## Installation
[See the Wiki!](https://github.com/davidnewhall/unifi-poller/wiki/Installation)
We have a special place for [Docker Users](https://github.com/davidnewhall/unifi-poller/wiki/Docker).
I'm willing to help if you have troubles.
Open an [Issue](https://github.com/davidnewhall/unifi-poller/issues) and
we'll figure out how to get things working for you. You can also check out
my [Discord server](https://discord.gg/DyVsMyt).
## Description ## Description
@ -27,8 +37,8 @@ Ubiquiti also provides a dedicated hardware device called a
[CloudKey](https://www.ui.com/unifi/unifi-cloud-key/) that runs the controller software. [CloudKey](https://www.ui.com/unifi/unifi-cloud-key/) that runs the controller software.
UniFi Poller is a small Golang application that runs on Windows, macOS, Linux or UniFi Poller is a small Golang application that runs on Windows, macOS, Linux or
Docker. It polls a UniFi controller every 30 seconds for measurements and stores Docker. It polls a UniFi controller every 30 seconds for measurements and exports
the data in an Influx database. A small setup with 2 access points, 1 switch, 1 the data to an Influx database. A small setup with 2 access points, 1 switch, 1
gateway and 40 clients produces over 3000 fields (metrics). gateway and 40 clients produces over 3000 fields (metrics).
This application requires your controller to be running all the time. If you run This application requires your controller to be running all the time. If you run
@ -38,11 +48,6 @@ a UniFi controller, there's no excuse not to install
You'll have a plethora of data at your fingertips and the ability to craft custom You'll have a plethora of data at your fingertips and the ability to craft custom
graphs to slice the data any way you choose. Good luck! graphs to slice the data any way you choose. Good luck!
## Installation
[See the Wiki!](https://github.com/davidnewhall/unifi-poller/wiki/Installation)
We have a special place for [Docker Users](https://github.com/davidnewhall/unifi-poller/wiki/Docker).
# Backstory # Backstory
Okay, so here's the deal. I found a simple piece of code on GitHub that Okay, so here's the deal. I found a simple piece of code on GitHub that
@ -54,44 +59,46 @@ with me. I probably wouldn't have made it this far if
code I started with. Many props my man. code I started with. Many props my man.
The original code pulled only the client data. This app now pulls data The original code pulled only the client data. This app now pulls data
for clients, access points, security gateways and switches. I currently for clients, access points, security gateways and switches. I used to
own two UAP-AC-PROs, one USG-3 and one US-24-250W. If your devices differ own two UAP-AC-PROs, one USG-3 and one US-24-250W, but have since upgraded
this app may miss some data. I'm willing to help and make it better. a few devices. Many other users have also provided feedback to improve this app,
Open an [Issue](https://github.com/davidnewhall/unifi-poller/issues) and and we have reports of it working on nearly every switch, AP and gateway; UDM included.
we'll figure out how to get things working for you.
# What's this data good for? # What's this data good for?
I've been trying to get my UAP data into Grafana. Sure, google search that. I've been trying to get my UAP data into Grafana. Sure, google search that.
You'll find [this](https://community.ubnt.com/t5/UniFi-Wireless/Grafana-dashboard-for-UniFi-APs-now-available/td-p/1833532). You'll find [this](https://community.ubnt.com/t5/UniFi-Wireless/Grafana-dashboard-for-UniFi-APs-now-available/td-p/1833532). What if you don't want to deal with SNMP?
And that's all you'll find. What if you don't want to deal with SNMP? Well, here you go. I've replicated 400% of what you see on those SNMP-powered
Well, here you go. I've replicated 90% of what you see on those SNMP-powered
dashboards with this Go app running on the same mac as my UniFi controller. dashboards with this Go app running on the same mac as my UniFi controller.
All without enabling SNMP nor trying to understand those OIDs. Mad props All without enabling SNMP nor trying to understand those OIDs. Mad props
to [waterside](https://community.ubnt.com/t5/user/viewprofilepage/user-id/303058) to [waterside](https://community.ubnt.com/t5/user/viewprofilepage/user-id/303058)
for making this dashboard; it gave me a fantastic start to making my own. for making this dashboard; it gave me a fantastic start to making my own dashboards.
This app is up to five dashboards now!
Update 9/2019:
Some new "prometheus exporters" are showing up. I admit I don't know much about
Prometheus, but so far the prometheus exporting apps I've seen are missing many
data points. Let me know if Prometheus is something you'd like to see support for.
I've also created [another forum post](https://community.ui.com/questions/Unifi-Poller-Store-Unifi-Controller-Metrics-in-InfluxDB-without-SNMP/58a0ea34-d2b3-41cd-93bb-d95d3896d1a1) you may use to get additional help. I've also created [another forum post](https://community.ui.com/questions/Unifi-Poller-Store-Unifi-Controller-Metrics-in-InfluxDB-without-SNMP/58a0ea34-d2b3-41cd-93bb-d95d3896d1a1) you may use to get additional help.
# Development # Development
The "What now..." section below used to be a lot larger. I've received a lot of The "What now..." section below used to be a lot longer. I've received a lot of
support, feedback and assistance from the community. Many thanks! This app is support, feedback and assistance from the community. Many thanks! This app is
extremely stable with a tiny memory and cpu footprint. I imagine one day we'll extremely stable with a tiny memory and cpu footprint. I imagine one day we'll
figure out how to make it run on a CloudKey device directly; once I have one figure out how to make it run on a CloudKey or UDM directly; once I have one
personally that will be my goal. In addition to stability, this app provides personally that will be my goal. In addition to stability, this app provides
an intuitive installation and configuration process. Maintenance is a breeze too. an intuitive installation and configuration process. Maintenance is a breeze too.
I'm not a software engineer, I'm a a firm believer in operational excellence above
all else. To that end, this app shall remain easy, intuitive and highly adaptable.
I'm totally open to add more configuration options if someone raises a need or concern.
You can control this app with puppet, chef, saltstack, homebrew or a simple bash You can control this app with puppet, chef, saltstack, homebrew or a simple bash
script if you needed to. It's available for macOS, Linux and Docker. It comes with script if you needed to. It's available for macOS, Linux and Docker. It comes with
a systemd service unit that allows you automatically start it up on most Linux a systemd service unit that allows you automatically start it up on most Linux
hosts. It works just fine on [Windows](https://github.com/davidnewhall/unifi-poller/wiki/Windows) too. hosts. It works just fine on [Windows](https://github.com/davidnewhall/unifi-poller/wiki/Windows) too.
Most people prefer Docker, and this app is right at home in that environment.
The unifi data extraction is provided as an [external library](https://godoc.org/golift.io/unifi), The UniFi data extraction is provided as an [external library](https://godoc.org/golift.io/unifi),
and you can import that code directly without futzing with this application. That and you can import that code directly without futzing with this application. That
means, if you wanted to do something like make telegraf collect your data instead means, if you wanted to do something like make telegraf collect your data instead
of UniFi Poller you can achieve that with a little bit of Go code. You could write of UniFi Poller you can achieve that with a little bit of Go code. You could write
@ -102,20 +109,11 @@ and can be used in other projects.
# What now... # What now...
### Are there other devices that need to be included? We are at a point where the application works as intended, and we are trying to
maintain the status quo. Ubiquiti releases updates, things break, we fix it;
I have: switch, router, access point. Three total, and the type structs are round and round we go. If you have new hardware or a new controller version, and
likely missing data for variants of these devices. e.g. Some UAPs have more something is not showing up, please open an
radios, I probably didn't properly account for that. Some gateways have more [Issue](https://github.com/davidnewhall/unifi-poller/issues) so we can fix it.
ports, some switches have 10Gb, etc. These are things I do not have data on
to write code for. If you have these devices, and want them graphed, open an
Issue and lets discuss.
### Radios, Frequencies, Interfaces, vAPs
My access points only seem to have two radios, one interface and vAP per radio.
I'm not sure if the graphs, as-is, provide enough insight into APs with other
configurations. Help me figure that out?
# What's it look like? # What's it look like?

View File

@ -22,17 +22,43 @@ const (
defaultUnifURL = "https://127.0.0.1:8443" defaultUnifURL = "https://127.0.0.1:8443"
) )
// These are environment variables that can be used to override configuration.
// Useful for Docker users.
const (
ENVConfigMode = "UP_POLLING_MODE"
ENVConfigInfluxDB = "UP_INFLUX_DB"
ENVConfigInfluxUser = "UP_INFLUX_USER"
ENVConfigInfluxPass = "UP_INFLUX_PASS"
ENVConfigInfluxURL = "UP_INFLUX_URL"
ENVConfigUnifiUser = "UP_UNIFI_USER"
ENVConfigUnifiPass = "UP_UNIFI_PASS"
ENVConfigUnifiBase = "UP_UNIFI_URL"
ENVConfigReAuth = "UP_REAUTHENTICATE"
ENVConfigVerifySSL = "UP_VERIFY_SSL"
ENVConfigCollectIDS = "UP_COLLECT_IDS"
ENVConfigQuiet = "UP_QUIET_MODE"
ENVConfigDebug = "UP_DEBUG_MODE"
ENVConfigInterval = "UP_POLLING_INTERVAL"
ENVConfigMaxErrors = "UP_MAX_ERRORS"
ENVConfigSites = "UP_POLL_SITES"
)
// UnifiPoller contains the application startup data, and auth info for UniFi & Influx. // UnifiPoller contains the application startup data, and auth info for UniFi & Influx.
type UnifiPoller struct { type UnifiPoller struct {
Influx influx.Client
Unifi *unifi.Unifi
Flag *Flag
Config *Config
errorCount int
LastCheck time.Time
}
// Flag represents the CLI args available and their settings.
type Flag struct {
ConfigFile string ConfigFile string
DumpJSON string DumpJSON string
ShowVer bool ShowVer bool
Flag *pflag.FlagSet *pflag.FlagSet
errorCount int
LastCheck time.Time
influx.Client
*unifi.Unifi
*Config
} }
// Metrics contains all the data from the controller and an influx endpoint to send it to. // Metrics contains all the data from the controller and an influx endpoint to send it to.

View File

@ -10,17 +10,18 @@ import (
// DumpJSONPayload prints raw json from the UniFi Controller. // DumpJSONPayload prints raw json from the UniFi Controller.
func (u *UnifiPoller) DumpJSONPayload() (err error) { func (u *UnifiPoller) DumpJSONPayload() (err error) {
u.Quiet = true u.Config.Quiet = true
u.Unifi, err = unifi.NewUnifi(&unifi.Config{ u.Unifi, err = unifi.NewUnifi(&unifi.Config{
User: u.UnifiUser, User: u.Config.UnifiUser,
Pass: u.UnifiPass, Pass: u.Config.UnifiPass,
URL: u.UnifiBase, URL: u.Config.UnifiBase,
VerifySSL: u.VerifySSL, VerifySSL: u.Config.VerifySSL,
}) })
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(os.Stderr, "[INFO] Authenticated to UniFi Controller @", u.UnifiBase, "as user", u.UnifiUser) fmt.Fprintf(os.Stderr, "[INFO] Authenticated to UniFi Controller @ %v as user %v",
u.Config.UnifiBase, u.Config.UnifiUser)
if err := u.CheckSites(); err != nil { if err := u.CheckSites(); err != nil {
return err return err
} }
@ -30,12 +31,12 @@ func (u *UnifiPoller) DumpJSONPayload() (err error) {
switch sites, err := u.GetFilteredSites(); { switch sites, err := u.GetFilteredSites(); {
case err != nil: case err != nil:
return err return err
case StringInSlice(u.DumpJSON, []string{"d", "device", "devices"}): case StringInSlice(u.Flag.DumpJSON, []string{"d", "device", "devices"}):
return u.dumpSitesJSON(unifi.DevicePath, "Devices", sites) return u.dumpSitesJSON(unifi.DevicePath, "Devices", sites)
case StringInSlice(u.DumpJSON, []string{"client", "clients", "c"}): case StringInSlice(u.Flag.DumpJSON, []string{"client", "clients", "c"}):
return u.dumpSitesJSON(unifi.ClientPath, "Clients", sites) return u.dumpSitesJSON(unifi.ClientPath, "Clients", sites)
case strings.HasPrefix(u.DumpJSON, "other "): case strings.HasPrefix(u.Flag.DumpJSON, "other "):
apiPath := strings.SplitN(u.DumpJSON, " ", 2)[1] apiPath := strings.SplitN(u.Flag.DumpJSON, " ", 2)[1]
_, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath) _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath)
return u.PrintRawAPIJSON(apiPath) return u.PrintRawAPIJSON(apiPath)
default: default:
@ -46,7 +47,8 @@ func (u *UnifiPoller) DumpJSONPayload() (err error) {
func (u *UnifiPoller) dumpSitesJSON(path, name string, sites unifi.Sites) error { func (u *UnifiPoller) dumpSitesJSON(path, name string, sites unifi.Sites) error {
for _, s := range sites { for _, s := range sites {
apiPath := fmt.Sprintf(path, s.Name) apiPath := fmt.Sprintf(path, s.Name)
_, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping %s: '%s' JSON for site: %s (%s):\n", name, apiPath, s.Desc, s.Name) _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping %s: '%s' JSON for site: %s (%s):\n",
name, apiPath, s.Desc, s.Name)
if err := u.PrintRawAPIJSON(apiPath); err != nil { if err := u.PrintRawAPIJSON(apiPath); err != nil {
return err return err
} }
@ -56,7 +58,7 @@ func (u *UnifiPoller) dumpSitesJSON(path, name string, sites unifi.Sites) error
// PrintRawAPIJSON prints the raw json for a user-provided path on a UniFi Controller. // PrintRawAPIJSON prints the raw json for a user-provided path on a UniFi Controller.
func (u *UnifiPoller) PrintRawAPIJSON(apiPath string) error { func (u *UnifiPoller) PrintRawAPIJSON(apiPath string) error {
body, err := u.GetJSON(apiPath) body, err := u.Unifi.GetJSON(apiPath)
fmt.Println(string(body)) fmt.Println(string(body))
return err return err
} }

View File

@ -11,7 +11,7 @@ import (
func (u *UnifiPoller) LogError(err error, prefix string) { func (u *UnifiPoller) LogError(err error, prefix string) {
if err != nil { if err != nil {
u.errorCount++ u.errorCount++
_ = log.Output(2, fmt.Sprintf("[ERROR] (%v/%v) %v: %v", u.errorCount, u.MaxErrors, prefix, err)) _ = log.Output(2, fmt.Sprintf("[ERROR] (%v/%v) %v: %v", u.errorCount, u.Config.MaxErrors, prefix, err))
} }
} }
@ -27,14 +27,14 @@ func StringInSlice(str string, slice []string) bool {
// Logf prints a log entry if quiet is false. // Logf prints a log entry if quiet is false.
func (u *UnifiPoller) Logf(m string, v ...interface{}) { func (u *UnifiPoller) Logf(m string, v ...interface{}) {
if !u.Quiet { if !u.Config.Quiet {
_ = log.Output(2, fmt.Sprintf("[INFO] "+m, v...)) _ = log.Output(2, fmt.Sprintf("[INFO] "+m, v...))
} }
} }
// LogDebugf prints a debug log entry if debug is true and quite is false // LogDebugf prints a debug log entry if debug is true and quite is false
func (u *UnifiPoller) LogDebugf(m string, v ...interface{}) { func (u *UnifiPoller) LogDebugf(m string, v ...interface{}) {
if u.Debug && !u.Quiet { if u.Config.Debug && !u.Config.Quiet {
_ = log.Output(2, fmt.Sprintf("[DEBUG] "+m, v...)) _ = log.Output(2, fmt.Sprintf("[DEBUG] "+m, v...))
} }
} }
@ -43,3 +43,26 @@ func (u *UnifiPoller) LogDebugf(m string, v ...interface{}) {
func (u *UnifiPoller) LogErrorf(m string, v ...interface{}) { func (u *UnifiPoller) LogErrorf(m string, v ...interface{}) {
_ = log.Output(2, fmt.Sprintf("[ERROR] "+m, v...)) _ = log.Output(2, fmt.Sprintf("[ERROR] "+m, v...))
} }
// pick returns the first non empty string in a list.
// used in a few places around this library.
func pick(strings ...string) string {
for _, s := range strings {
if s != "" {
return s
}
}
return ""
}
// parseBool returns true/false if the string is "true" or "false", otherwise returns e value.
func parseBool(s string, e bool) bool {
switch s {
case "true", "t":
return true
case "false", "f":
return false
default:
return e
}
}

View File

@ -10,6 +10,9 @@ import (
// UAPPoints generates Wireless-Access-Point datapoints for InfluxDB. // UAPPoints generates Wireless-Access-Point datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func UAPPoints(u *unifi.UAP, now time.Time) ([]*influx.Point, error) { func UAPPoints(u *unifi.UAP, now time.Time) ([]*influx.Point, error) {
if u.Stat.Ap == nil {
u.Stat.Ap = &unifi.Ap{}
}
tags := map[string]string{ tags := map[string]string{
"id": u.ID, "id": u.ID,
"mac": u.Mac, "mac": u.Mac,

View File

@ -10,6 +10,12 @@ import (
// UDMPoints generates Unifi Gateway datapoints for InfluxDB. // UDMPoints generates Unifi Gateway datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func UDMPoints(u *unifi.UDM, now time.Time) ([]*influx.Point, error) { func UDMPoints(u *unifi.UDM, now time.Time) ([]*influx.Point, error) {
if u.Stat.Sw == nil {
u.Stat.Sw = &unifi.Sw{}
}
if u.Stat.Gw == nil {
u.Stat.Gw = &unifi.Gw{}
}
tags := map[string]string{ tags := map[string]string{
"id": u.ID, "id": u.ID,
"mac": u.Mac, "mac": u.Mac,

View File

@ -11,6 +11,9 @@ import (
// USGPoints generates Unifi Gateway datapoints for InfluxDB. // USGPoints generates Unifi Gateway datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func USGPoints(u *unifi.USG, now time.Time) ([]*influx.Point, error) { func USGPoints(u *unifi.USG, now time.Time) ([]*influx.Point, error) {
if u.Stat.Gw == nil {
u.Stat.Gw = &unifi.Gw{}
}
tags := map[string]string{ tags := map[string]string{
"id": u.ID, "id": u.ID,
"mac": u.Mac, "mac": u.Mac,

View File

@ -10,6 +10,9 @@ import (
// USWPoints generates Unifi Switch datapoints for InfluxDB. // USWPoints generates Unifi Switch datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func USWPoints(u *unifi.USW, now time.Time) ([]*influx.Point, error) { func USWPoints(u *unifi.USW, now time.Time) ([]*influx.Point, error) {
if u.Stat.Sw == nil {
u.Stat.Sw = &unifi.Sw{}
}
tags := map[string]string{ tags := map[string]string{
"id": u.ID, "id": u.ID,
"mac": u.Mac, "mac": u.Mac,

View File

@ -7,6 +7,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@ -21,8 +22,9 @@ import (
// Parses flags, parses config and executes Run(). // Parses flags, parses config and executes Run().
func Start() error { func Start() error {
log.SetFlags(log.LstdFlags) log.SetFlags(log.LstdFlags)
up := &UnifiPoller{} up := &UnifiPoller{Flag: &Flag{}}
if up.ParseFlags(os.Args[1:]); up.ShowVer { up.Flag.Parse(os.Args[1:])
if up.Flag.ShowVer {
fmt.Printf("unifi-poller v%s\n", Version) fmt.Printf("unifi-poller v%s\n", Version)
return nil // don't run anything else w/ version request. return nil // don't run anything else w/ version request.
} }
@ -33,18 +35,46 @@ func Start() error {
return up.Run() return up.Run()
} }
// ParseFlags runs the parser. // Parse turns CLI arguments into data structures. Called by Start() on startup.
func (u *UnifiPoller) ParseFlags(args []string) { func (f *Flag) Parse(args []string) {
u.Flag = pflag.NewFlagSet("unifi-poller", pflag.ExitOnError) f.FlagSet = pflag.NewFlagSet("unifi-poller", pflag.ExitOnError)
u.Flag.Usage = func() { f.Usage = func() {
fmt.Println("Usage: unifi-poller [--config=filepath] [--version]") fmt.Println("Usage: unifi-poller [--config=filepath] [--version]")
u.Flag.PrintDefaults() f.PrintDefaults()
} }
u.Flag.StringVarP(&u.DumpJSON, "dumpjson", "j", "", f.StringVarP(&f.DumpJSON, "dumpjson", "j", "",
"This debug option prints a json payload and exits. See man page for more.") "This debug option prints a json payload and exits. See man page for more.")
u.Flag.StringVarP(&u.ConfigFile, "config", "c", DefaultConfFile, "Poller Config File (TOML Format)") f.StringVarP(&f.ConfigFile, "config", "c", DefaultConfFile, "Poller Config File (TOML Format)")
u.Flag.BoolVarP(&u.ShowVer, "version", "v", false, "Print the version and exit") f.BoolVarP(&f.ShowVer, "version", "v", false, "Print the version and exit")
_ = u.Flag.Parse(args) _ = f.FlagSet.Parse(args)
}
// setEnvVarOptions copies environment variables into configuration values.
// This is useful for Docker users that find it easier to pass ENV variables
// that a specific configuration file.
func (u *UnifiPoller) setEnvVarOptions() {
u.Config.Mode = pick(os.Getenv(ENVConfigMode), u.Config.Mode)
u.Config.InfluxDB = pick(os.Getenv(ENVConfigInfluxDB), u.Config.InfluxDB)
u.Config.InfluxUser = pick(os.Getenv(ENVConfigInfluxUser), u.Config.InfluxUser)
u.Config.InfluxPass = pick(os.Getenv(ENVConfigInfluxPass), u.Config.InfluxPass)
u.Config.InfluxURL = pick(os.Getenv(ENVConfigInfluxURL), u.Config.InfluxURL)
u.Config.UnifiUser = pick(os.Getenv(ENVConfigUnifiUser), u.Config.UnifiUser)
u.Config.UnifiPass = pick(os.Getenv(ENVConfigUnifiPass), u.Config.UnifiPass)
u.Config.UnifiBase = pick(os.Getenv(ENVConfigUnifiBase), u.Config.UnifiBase)
u.Config.ReAuth = parseBool(os.Getenv(ENVConfigReAuth), u.Config.ReAuth)
u.Config.VerifySSL = parseBool(os.Getenv(ENVConfigVerifySSL), u.Config.VerifySSL)
u.Config.CollectIDS = parseBool(os.Getenv(ENVConfigCollectIDS), u.Config.CollectIDS)
u.Config.Quiet = parseBool(os.Getenv(ENVConfigQuiet), u.Config.Quiet)
u.Config.Debug = parseBool(os.Getenv(ENVConfigDebug), u.Config.Debug)
if e := os.Getenv(ENVConfigInterval); e != "" {
_ = u.Config.Interval.UnmarshalText([]byte(e))
}
if e := os.Getenv(ENVConfigMaxErrors); e != "" {
u.Config.MaxErrors, _ = strconv.Atoi(e)
}
if e := os.Getenv(ENVConfigSites); e != "" {
u.Config.Sites = strings.Split(e, ",")
}
} }
// GetConfig parses and returns our configuration data. // GetConfig parses and returns our configuration data.
@ -56,21 +86,22 @@ func (u *UnifiPoller) GetConfig() error {
InfluxPass: defaultInfxPass, InfluxPass: defaultInfxPass,
InfluxDB: defaultInfxDb, InfluxDB: defaultInfxDb,
UnifiUser: defaultUnifUser, UnifiUser: defaultUnifUser,
UnifiPass: os.Getenv("UNIFI_PASSWORD"), UnifiPass: os.Getenv("UNIFI_PASSWORD"), // deprecated name.
UnifiBase: defaultUnifURL, UnifiBase: defaultUnifURL,
Interval: Duration{defaultInterval}, Interval: Duration{defaultInterval},
Sites: []string{"default"}, Sites: []string{"default"},
Quiet: u.DumpJSON != "", Quiet: u.Flag.DumpJSON != "", //s uppress the following u.Logf line.
} }
u.Logf("Loading Configuration File: %s", u.ConfigFile) u.Logf("Loading Configuration File: %s", u.Flag.ConfigFile)
switch buf, err := ioutil.ReadFile(u.ConfigFile); { defer u.setEnvVarOptions() // Set env variable overrides when done here.
switch buf, err := ioutil.ReadFile(u.Flag.ConfigFile); {
case err != nil: case err != nil:
return err return err
case strings.Contains(u.ConfigFile, ".json"): case strings.Contains(u.Flag.ConfigFile, ".json"):
return json.Unmarshal(buf, u.Config) return json.Unmarshal(buf, u.Config)
case strings.Contains(u.ConfigFile, ".xml"): case strings.Contains(u.Flag.ConfigFile, ".xml"):
return xml.Unmarshal(buf, u.Config) return xml.Unmarshal(buf, u.Config)
case strings.Contains(u.ConfigFile, ".yaml"): case strings.Contains(u.Flag.ConfigFile, ".yaml"):
return yaml.Unmarshal(buf, u.Config) return yaml.Unmarshal(buf, u.Config)
default: default:
return toml.Unmarshal(buf, u.Config) return toml.Unmarshal(buf, u.Config)
@ -79,10 +110,10 @@ func (u *UnifiPoller) GetConfig() error {
// Run invokes all the application logic and routines. // Run invokes all the application logic and routines.
func (u *UnifiPoller) Run() (err error) { func (u *UnifiPoller) Run() (err error) {
if u.DumpJSON != "" { if u.Flag.DumpJSON != "" {
return u.DumpJSONPayload() return u.DumpJSONPayload()
} }
if u.Debug { if u.Config.Debug {
log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate)
u.LogDebugf("Debug Logging Enabled") u.LogDebugf("Debug Logging Enabled")
} }
@ -90,12 +121,13 @@ func (u *UnifiPoller) Run() (err error) {
if err = u.GetUnifi(); err != nil { if err = u.GetUnifi(); err != nil {
return err return err
} }
u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", u.UnifiBase, u.ServerVersion, u.UnifiUser, u.Sites) u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v",
u.Config.UnifiBase, u.Unifi.ServerVersion, u.Config.UnifiUser, u.Config.Sites)
if err = u.GetInfluxDB(); err != nil { if err = u.GetInfluxDB(); err != nil {
return err return err
} }
u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.InfluxURL, u.InfluxUser) u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.Config.InfluxURL, u.Config.InfluxUser)
switch strings.ToLower(u.Mode) { switch strings.ToLower(u.Config.Mode) {
case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda": case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda":
u.LogDebugf("Lambda Mode Enabled") u.LogDebugf("Lambda Mode Enabled")
u.LastCheck = time.Now() u.LastCheck = time.Now()
@ -107,10 +139,10 @@ func (u *UnifiPoller) Run() (err error) {
// GetInfluxDB returns an InfluxDB interface. // GetInfluxDB returns an InfluxDB interface.
func (u *UnifiPoller) GetInfluxDB() (err error) { func (u *UnifiPoller) GetInfluxDB() (err error) {
u.Client, err = influx.NewHTTPClient(influx.HTTPConfig{ u.Influx, err = influx.NewHTTPClient(influx.HTTPConfig{
Addr: u.InfluxURL, Addr: u.Config.InfluxURL,
Username: u.InfluxUser, Username: u.Config.InfluxUser,
Password: u.InfluxPass, Password: u.Config.InfluxPass,
}) })
if err != nil { if err != nil {
return fmt.Errorf("influxdb: %v", err) return fmt.Errorf("influxdb: %v", err)
@ -122,10 +154,10 @@ func (u *UnifiPoller) GetInfluxDB() (err error) {
func (u *UnifiPoller) GetUnifi() (err error) { func (u *UnifiPoller) GetUnifi() (err error) {
// Create an authenticated session to the Unifi Controller. // Create an authenticated session to the Unifi Controller.
u.Unifi, err = unifi.NewUnifi(&unifi.Config{ u.Unifi, err = unifi.NewUnifi(&unifi.Config{
User: u.UnifiUser, User: u.Config.UnifiUser,
Pass: u.UnifiPass, Pass: u.Config.UnifiPass,
URL: u.UnifiBase, URL: u.Config.UnifiBase,
VerifySSL: u.VerifySSL, VerifySSL: u.Config.VerifySSL,
ErrorLog: u.LogErrorf, // Log all errors. ErrorLog: u.LogErrorf, // Log all errors.
DebugLog: u.LogDebugf, // Log debug messages. DebugLog: u.LogDebugf, // Log debug messages.
}) })

View File

@ -13,10 +13,10 @@ import (
// CheckSites makes sure the list of provided sites exists on the controller. // CheckSites makes sure the list of provided sites exists on the controller.
// This does not run in Lambda (run-once) mode. // This does not run in Lambda (run-once) mode.
func (u *UnifiPoller) CheckSites() error { func (u *UnifiPoller) CheckSites() error {
if strings.Contains(strings.ToLower(u.Mode), "lambda") { if strings.Contains(strings.ToLower(u.Config.Mode), "lambda") {
return nil // Skip this in lambda mode. return nil // Skip this in lambda mode.
} }
sites, err := u.GetSites() sites, err := u.Unifi.GetSites()
if err != nil { if err != nil {
return err return err
} }
@ -25,12 +25,12 @@ func (u *UnifiPoller) CheckSites() error {
msg = append(msg, site.Name+" ("+site.Desc+")") msg = append(msg, site.Name+" ("+site.Desc+")")
} }
u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", ")) u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", "))
if StringInSlice("all", u.Sites) { if StringInSlice("all", u.Config.Sites) {
u.Sites = []string{"all"} u.Config.Sites = []string{"all"}
return nil return nil
} }
FIRST: FIRST:
for _, s := range u.Sites { for _, s := range u.Config.Sites {
for _, site := range sites { for _, site := range sites {
if s == site.Name { if s == site.Name {
continue FIRST continue FIRST
@ -45,13 +45,15 @@ FIRST:
// PollController runs forever, polling UniFi, and pushing to influx. // PollController runs forever, polling UniFi, and pushing to influx.
// This is started by Run() after everything checks out. // This is started by Run() after everything checks out.
func (u *UnifiPoller) PollController() error { func (u *UnifiPoller) PollController() error {
log.Println("[INFO] Everything checks out! Poller started, interval:", u.Interval.Round(time.Second)) interval := u.Config.Interval.Round(time.Second)
ticker := time.NewTicker(u.Interval.Round(time.Second)) log.Println("[INFO] Everything checks out! Poller started, interval:", interval)
ticker := time.NewTicker(interval)
for u.LastCheck = range ticker.C { for u.LastCheck = range ticker.C {
var err error var err error
if u.ReAuth { if u.Config.ReAuth {
u.LogDebugf("Re-authenticating to UniFi Controller")
// Some users need to re-auth every interval because the cookie times out. // Some users need to re-auth every interval because the cookie times out.
if err = u.Login(); err != nil { if err = u.Unifi.Login(); err != nil {
u.LogError(err, "re-authenticating") u.LogError(err, "re-authenticating")
} }
} }
@ -59,8 +61,8 @@ func (u *UnifiPoller) PollController() error {
// Only run this if the authentication procedure didn't return error. // Only run this if the authentication procedure didn't return error.
_ = u.CollectAndReport() _ = u.CollectAndReport()
} }
if u.MaxErrors >= 0 && u.errorCount > u.MaxErrors { if u.Config.MaxErrors >= 0 && u.errorCount > u.Config.MaxErrors {
return fmt.Errorf("reached maximum error count, stopping poller (%d > %d)", u.errorCount, u.MaxErrors) return fmt.Errorf("reached maximum error count, stopping poller (%d > %d)", u.errorCount, u.Config.MaxErrors)
} }
} }
return nil return nil
@ -92,18 +94,18 @@ func (u *UnifiPoller) CollectMetrics() (*Metrics, error) {
// Get the sites we care about. // Get the sites we care about.
m.Sites, err = u.GetFilteredSites() m.Sites, err = u.GetFilteredSites()
u.LogError(err, "unifi.GetSites()") u.LogError(err, "unifi.GetSites()")
if u.CollectIDS { if u.Config.CollectIDS {
// Check back in time since twice the interval. Dups are discarded by InfluxDB. // Check back in time since twice the interval. Dups are discarded by InfluxDB.
m.IDSList, err = u.GetIDS(m.Sites, time.Now().Add(2*u.Interval.Duration), time.Now()) m.IDSList, err = u.Unifi.GetIDS(m.Sites, time.Now().Add(2*u.Config.Interval.Duration), time.Now())
u.LogError(err, "unifi.GetIDS()") u.LogError(err, "unifi.GetIDS()")
} }
// Get all the points. // Get all the points.
m.Clients, err = u.GetClients(m.Sites) m.Clients, err = u.Unifi.GetClients(m.Sites)
u.LogError(err, "unifi.GetClients()") u.LogError(err, "unifi.GetClients()")
m.Devices, err = u.GetDevices(m.Sites) m.Devices, err = u.Unifi.GetDevices(m.Sites)
u.LogError(err, "unifi.GetDevices()") u.LogError(err, "unifi.GetDevices()")
// Make a new Influx Points Batcher. // Make a new Influx Points Batcher.
m.BatchPoints, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.InfluxDB}) m.BatchPoints, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.Config.InfluxDB})
u.LogError(err, "influx.NewBatchPoints") u.LogError(err, "influx.NewBatchPoints")
return m, err return m, err
} }
@ -138,7 +140,7 @@ func (u *UnifiPoller) ReportMetrics(metrics *Metrics) error {
for _, err := range metrics.ProcessPoints() { for _, err := range metrics.ProcessPoints() {
u.LogError(err, "asset.Points()") u.LogError(err, "asset.Points()")
} }
err := u.Write(metrics.BatchPoints) err := u.Influx.Write(metrics.BatchPoints)
if err != nil { if err != nil {
return fmt.Errorf("influxdb.Write(points): %v", err) return fmt.Errorf("influxdb.Write(points): %v", err)
} }
@ -149,7 +151,7 @@ func (u *UnifiPoller) ReportMetrics(metrics *Metrics) error {
fields += len(i) fields += len(i)
} }
idsMsg := "" idsMsg := ""
if u.CollectIDS { if u.Config.CollectIDS {
idsMsg = fmt.Sprintf("IDS Events: %d, ", len(metrics.IDSList)) idsMsg = fmt.Sprintf("IDS Events: %d, ", len(metrics.IDSList))
} }
u.Logf("UniFi Measurements Recorded. Sites: %d, Clients: %d, "+ u.Logf("UniFi Measurements Recorded. Sites: %d, Clients: %d, "+
@ -212,16 +214,16 @@ func (m *Metrics) ProcessPoints() []error {
// Omits requested but unconfigured sites. Grabs the full list from the // Omits requested but unconfigured sites. Grabs the full list from the
// controller and returns the sites provided in the config file. // controller and returns the sites provided in the config file.
func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) { func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) {
sites, err := u.GetSites() sites, err := u.Unifi.GetSites()
if err != nil { if err != nil {
return nil, err return nil, err
} else if len(u.Sites) < 1 || StringInSlice("all", u.Sites) { } else if len(u.Config.Sites) < 1 || StringInSlice("all", u.Config.Sites) {
return sites, nil return sites, nil
} }
var i int var i int
for _, s := range sites { for _, s := range sites {
// Only include valid sites in the request filter. // Only include valid sites in the request filter.
if StringInSlice(s.Name, u.Sites) { if StringInSlice(s.Name, u.Config.Sites) {
sites[i] = s sites[i] = s
i++ i++
} }