diff --git a/core/poller/.travis.yml b/core/poller/.travis.yml new file mode 100644 index 00000000..4b9efbe0 --- /dev/null +++ b/core/poller/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: +- 1.16.x +before_install: + # download super-linter: golangci-lint +- curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin latest +script: +- go test ./... +- golangci-lint run --enable-all -D exhaustivestruct,nlreturn,forbidigo diff --git a/core/poller/LICENSE b/core/poller/LICENSE new file mode 100644 index 00000000..6d5fa682 --- /dev/null +++ b/core/poller/LICENSE @@ -0,0 +1,21 @@ +MIT LICENSE. +Copyright (c) 2018-2020 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. diff --git a/core/poller/README.md b/core/poller/README.md new file mode 100644 index 00000000..c658a12f --- /dev/null +++ b/core/poller/README.md @@ -0,0 +1,26 @@ +# poller + +## UniFi Poller Core + +This module ties the inputs together with the outputs. + +Aggregates metrics on request. Provides CLI app and args parsing. + + +# Ideal + +This library has no notion of "UniFi" or controllers, or Influx, or Prometheus. +This library simply provides an input interface and an output interface. +Each interface uses an `[]interface{}` type, so any type of data can be used. +That is to say, you could write input and output plugins that work with, say, +Cisco gear, or any other network (or even non-network) data. The existing plugins +should provide ample example of how to use this library, but at some point the +godoc will improve. + +# Features + +- Automatically unmarshal's plugin config structs from config file and/or env variables. +- Initializes all "imported" plugins on startup. +- Provides input plugins a Logger, requires an interface for Metrics and Events retrieval. +- Provides Output plugins an interface to retrieve Metrics and Events, and a Logger. +- Provides automatic aggregation of Metrics and Events from multiple sources. diff --git a/core/poller/build_bsd.go b/core/poller/build_bsd.go new file mode 100644 index 00000000..0f714e8f --- /dev/null +++ b/core/poller/build_bsd.go @@ -0,0 +1,9 @@ +// +build darwin freebsd netbsd openbsd + +package poller + +// DefaultConfFile is where to find config if --config is not prvided. +const DefaultConfFile = "/etc/unifi-poller/up.conf,/usr/local/etc/unifi-poller/up.conf" + +// DefaultObjPath is the path to look for shared object libraries (plugins). +const DefaultObjPath = "/usr/local/lib/unifi-poller" diff --git a/core/poller/build_unix.go b/core/poller/build_unix.go new file mode 100644 index 00000000..e425e451 --- /dev/null +++ b/core/poller/build_unix.go @@ -0,0 +1,9 @@ +// +build !windows,!darwin,!freebsd + +package poller + +// DefaultConfFile is where to find config if --config is not prvided. +const DefaultConfFile = "/config/unifi-poller.conf,/etc/unifi-poller/up.conf" + +// DefaultObjPath is the path to look for shared object libraries (plugins). +const DefaultObjPath = "/usr/lib/unifi-poller" diff --git a/core/poller/build_windows.go b/core/poller/build_windows.go new file mode 100644 index 00000000..69d964e8 --- /dev/null +++ b/core/poller/build_windows.go @@ -0,0 +1,9 @@ +// +build windows + +package poller + +// DefaultConfFile is where to find config if --config is not prvided. +const DefaultConfFile = `C:\ProgramData\unifi-poller\up.conf` + +// DefaultObjPath is useless in this context. Bummer. +const DefaultObjPath = "PLUGINS_DO_NOT_WORK_ON_WINDOWS_SOWWWWWY" diff --git a/core/poller/commands.go b/core/poller/commands.go new file mode 100644 index 00000000..e9b9f032 --- /dev/null +++ b/core/poller/commands.go @@ -0,0 +1,56 @@ +package poller + +import ( + "fmt" + "os" + "strconv" + "strings" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/term" +) + +// PrintRawMetrics prints raw json from the UniFi Controller. This is currently +// tied into the -j CLI arg, and is probably not very useful outside that context. +func (u *UnifiPoller) PrintRawMetrics() (err error) { + split := strings.SplitN(u.Flags.DumpJSON, " ", 2) + filter := &Filter{Kind: split[0]} + + // Allows you to grab a controller other than 0 from config. + if split2 := strings.Split(filter.Kind, ":"); len(split2) > 1 { + filter.Kind = split2[0] + filter.Unit, _ = strconv.Atoi(split2[1]) + } + + // Used with "other" + if len(split) > 1 { + filter.Path = split[1] + } + + // As of now we only have one input plugin, so target that [0]. + m, err := inputs[0].RawMetrics(filter) + fmt.Println(string(m)) + + return err +} + +// PrintPasswordHash prints a bcrypt'd password. Useful for the web server. +func (u *UnifiPoller) PrintPasswordHash() (err error) { + pwd := []byte(u.Flags.HashPW) + + if u.Flags.HashPW == "-" { + fmt.Print("Enter Password: ") + + pwd, err = term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return fmt.Errorf("reading stdin: %w", err) + } + + fmt.Println() // print a newline. + } + + hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost) + fmt.Println(string(hash)) + + return err //nolint:wrapcheck +} diff --git a/core/poller/config.go b/core/poller/config.go new file mode 100644 index 00000000..4fd3af77 --- /dev/null +++ b/core/poller/config.go @@ -0,0 +1,163 @@ +package poller + +import ( + "fmt" + "os" + "path" + "plugin" + "strings" + "time" + + "github.com/spf13/pflag" + "golift.io/cnfg" + "golift.io/cnfg/cnfgfile" +) + +const ( + // AppName is the name of the application. + AppName = "unpoller" + // ENVConfigPrefix is the prefix appended to an env variable tag name. + ENVConfigPrefix = "UP" +) + +// UnifiPoller contains the application startup data, and auth info for UniFi & Influx. +type UnifiPoller struct { + Flags *Flags + *Config +} + +// Flags represents the CLI args available and their settings. +type Flags struct { + ConfigFile string + DumpJSON string + HashPW string + ShowVer bool + *pflag.FlagSet +} + +// Metrics is a type shared by the exporting and reporting packages. +type Metrics struct { + TS time.Time + Sites []interface{} + Clients []interface{} + SitesDPI []interface{} + ClientsDPI []interface{} + Devices []interface{} + RogueAPs []interface{} +} + +// Events defines the type for log entries. +type Events struct { + Logs []interface{} +} + +// Config represents the core library input data. +type Config struct { + *Poller `json:"poller" toml:"poller" xml:"poller" yaml:"poller"` +} + +// Poller is the global config values. +type Poller struct { + Plugins []string `json:"plugins" toml:"plugins" xml:"plugin" yaml:"plugins"` + Debug bool `json:"debug" toml:"debug" xml:"debug,attr" yaml:"debug"` + Quiet bool `json:"quiet" toml:"quiet" xml:"quiet,attr" yaml:"quiet"` +} + +// LoadPlugins reads-in dynamic shared libraries. +// Not used very often, if at all. +func (u *UnifiPoller) LoadPlugins() error { + for _, p := range u.Plugins { + name := strings.TrimSuffix(p, ".so") + ".so" + + if name == ".so" { + continue // Just ignore it. uhg. + } + + if _, err := os.Stat(name); os.IsNotExist(err) { + name = path.Join(DefaultObjPath, name) + } + + u.Logf("Loading Dynamic Plugin: %s", name) + + if _, err := plugin.Open(name); err != nil { + return fmt.Errorf("opening plugin: %w", err) + } + } + + return nil +} + +// ParseConfigs parses the poller config and the config for each registered output plugin. +func (u *UnifiPoller) ParseConfigs() error { + // Parse core config. + if err := u.parseInterface(u.Config); err != nil { + return err + } + + // Load dynamic plugins. + if err := u.LoadPlugins(); err != nil { + return err + } + + if err := u.parseInputs(); err != nil { + return err + } + + return u.parseOutputs() +} + +// getFirstFile returns the first file that exists and is "reachable". +func getFirstFile(files []string) (string, error) { + var err error + + for _, f := range files { + if _, err = os.Stat(f); err == nil { + return f, nil + } + } + + return "", fmt.Errorf("finding file: %w", err) +} + +// parseInterface parses the config file and environment variables into the provided interface. +func (u *UnifiPoller) parseInterface(i interface{}) error { + // Parse config file into provided interface. + if err := cnfgfile.Unmarshal(i, u.Flags.ConfigFile); err != nil { + return fmt.Errorf("cnfg unmarshal: %w", err) + } + + // Parse environment variables into provided interface. + if _, err := cnfg.UnmarshalENV(i, ENVConfigPrefix); err != nil { + return fmt.Errorf("env unmarshal: %w", err) + } + + return nil +} + +// Parse input plugin configs. +func (u *UnifiPoller) parseInputs() error { + inputSync.Lock() + defer inputSync.Unlock() + + for _, i := range inputs { + if err := u.parseInterface(i.Config); err != nil { + return err + } + } + + return nil +} + +// Parse output plugin configs. +func (u *UnifiPoller) parseOutputs() error { + outputSync.Lock() + defer outputSync.Unlock() + + for _, o := range outputs { + if err := u.parseInterface(o.Config); err != nil { + return err + } + } + + return nil +} diff --git a/core/poller/go.mod b/core/poller/go.mod new file mode 100644 index 00000000..6df3419a --- /dev/null +++ b/core/poller/go.mod @@ -0,0 +1,13 @@ +module github.com/unpoller/poller + +go 1.16 + +require ( + github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c + golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b + golift.io/cnfg v0.0.7 + golift.io/version v0.0.2 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/core/poller/go.sum b/core/poller/go.sum new file mode 100644 index 00000000..18ef79e5 --- /dev/null +++ b/core/poller/go.sum @@ -0,0 +1,33 @@ +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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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.3/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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= diff --git a/core/poller/inputs.go b/core/poller/inputs.go new file mode 100644 index 00000000..af9a983c --- /dev/null +++ b/core/poller/inputs.go @@ -0,0 +1,156 @@ +package poller + +import ( + "strings" + "sync" + "time" +) + +var ( + // These are used ot keep track of loaded input plugins. + inputs []*InputPlugin // nolint: gochecknoglobals + inputSync sync.RWMutex // nolint: gochecknoglobals +) + +// Input plugins must implement this interface. +type Input interface { + Initialize(Logger) error // Called once on startup to initialize the plugin. + Metrics(*Filter) (*Metrics, error) // Called every time new metrics are requested. + Events(*Filter) (*Events, error) // This is new. + RawMetrics(*Filter) ([]byte, error) +} + +// InputPlugin describes an input plugin's consumable interface. +type InputPlugin struct { + Name string + Config interface{} // Each config is passed into an unmarshaller later. + Input +} + +// Filter is used for metrics filters. Many fields for lots of expansion. +type Filter struct { + Type string + Term string + Name string + Role string + Kind string + Path string + Text string + Unit int + Pass bool + Skip bool + Time time.Time + Dur time.Duration +} + +// NewInput creates a metric input. This should be called by input plugins +// init() functions. +func NewInput(i *InputPlugin) { + inputSync.Lock() + defer inputSync.Unlock() + + if i == nil || i.Input == nil { + panic("nil output or method passed to poller.NewOutput") + } + + inputs = append(inputs, i) +} + +// InitializeInputs runs the passed-in initializer method for each input plugin. +func (u *UnifiPoller) InitializeInputs() error { + inputSync.RLock() + defer inputSync.RUnlock() + + for _, input := range inputs { + // This must return, or the app locks up here. + if err := input.Initialize(u); err != nil { + return err + } + } + + return nil +} + +// Events aggregates log messages (events) from one or more sources. +func (u *UnifiPoller) Events(filter *Filter) (*Events, error) { + inputSync.RLock() + defer inputSync.RUnlock() + + events := Events{} + + for _, input := range inputs { + if filter != nil && + filter.Name != "" && + !strings.EqualFold(input.Name, filter.Name) { + continue + } + + e, err := input.Events(filter) + if err != nil { + return &events, err + } + + // Logs is the only member to extend at this time. + events.Logs = append(events.Logs, e.Logs...) + } + + return &events, nil +} + +// Metrics aggregates all the measurements from filtered inputs and returns them. +// Passing a null filter returns everything! +func (u *UnifiPoller) Metrics(filter *Filter) (*Metrics, error) { + inputSync.RLock() + defer inputSync.RUnlock() + + metrics := &Metrics{} + + for _, input := range inputs { + if filter != nil && + filter.Name != "" && + !strings.EqualFold(input.Name, filter.Name) { + continue + } + + m, err := input.Metrics(filter) + if err != nil { + return metrics, err + } + + metrics = AppendMetrics(metrics, m) + } + + return metrics, nil +} + +// AppendMetrics combines the metrics from two sources. +func AppendMetrics(existing *Metrics, m *Metrics) *Metrics { + if existing == nil { + return m + } + + if m == nil { + return existing + } + + existing.SitesDPI = append(existing.SitesDPI, m.SitesDPI...) + existing.Sites = append(existing.Sites, m.Sites...) + existing.ClientsDPI = append(existing.ClientsDPI, m.ClientsDPI...) + existing.RogueAPs = append(existing.RogueAPs, m.RogueAPs...) + existing.Clients = append(existing.Clients, m.Clients...) + existing.Devices = append(existing.Devices, m.Devices...) + + return existing +} + +// Inputs allows output plugins to see the list of loaded input plugins. +func (u *UnifiPoller) Inputs() (names []string) { + inputSync.RLock() + defer inputSync.RUnlock() + + for i := range inputs { + names = append(names, inputs[i].Name) + } + + return names +} diff --git a/core/poller/logger.go b/core/poller/logger.go new file mode 100644 index 00000000..689771da --- /dev/null +++ b/core/poller/logger.go @@ -0,0 +1,35 @@ +package poller + +import ( + "fmt" + "log" +) + +// Log the command that called these commands. +const callDepth = 2 + +// Logger is passed into input packages so they may write logs. +type Logger interface { + Logf(m string, v ...interface{}) + LogErrorf(m string, v ...interface{}) + LogDebugf(m string, v ...interface{}) +} + +// Logf prints a log entry if quiet is false. +func (u *UnifiPoller) Logf(m string, v ...interface{}) { + if !u.Quiet { + _ = log.Output(callDepth, fmt.Sprintf("[INFO] "+m, v...)) + } +} + +// LogDebugf prints a debug log entry if debug is true and quite is false. +func (u *UnifiPoller) LogDebugf(m string, v ...interface{}) { + if u.Debug && !u.Quiet { + _ = log.Output(callDepth, fmt.Sprintf("[DEBUG] "+m, v...)) + } +} + +// LogErrorf prints an error log entry. +func (u *UnifiPoller) LogErrorf(m string, v ...interface{}) { + _ = log.Output(callDepth, fmt.Sprintf("[ERROR] "+m, v...)) +} diff --git a/core/poller/outputs.go b/core/poller/outputs.go new file mode 100644 index 00000000..4dd05c0d --- /dev/null +++ b/core/poller/outputs.go @@ -0,0 +1,102 @@ +package poller + +import ( + "fmt" + "sync" +) + +var ( + // These are used to keep track of loaded output plugins. + outputs []*Output // nolint: gochecknoglobals + outputSync sync.RWMutex // nolint: gochecknoglobals + errNoOutputPlugins = fmt.Errorf("no output plugins imported") + errAllOutputStopped = fmt.Errorf("all output plugins have stopped, or none enabled") +) + +// Collect is passed into output packages so they may collect metrics to output. +type Collect interface { + Logger + Metrics(*Filter) (*Metrics, error) + Events(*Filter) (*Events, error) + // These get used by the webserver output plugin. + Poller() Poller + Inputs() []string + Outputs() []string +} + +// Output defines the output data for a metric exporter like influx or prometheus. +// Output packages should call NewOutput with this struct in init(). +type Output struct { + Name string + Config interface{} // Each config is passed into an unmarshaller later. + Method func(Collect) error // Called on startup for each configured output. +} + +// NewOutput should be called by each output package's init function. +func NewOutput(o *Output) { + outputSync.Lock() + defer outputSync.Unlock() + + if o == nil || o.Method == nil { + panic("nil output or method passed to poller.NewOutput") + } + + outputs = append(outputs, o) +} + +// Poller returns the poller config. +func (u *UnifiPoller) Poller() Poller { + return *u.Config.Poller +} + +// InitializeOutputs runs all the configured output plugins. +// If none exist, or they all exit an error is returned. +func (u *UnifiPoller) InitializeOutputs() error { + count, errChan := u.runOutputMethods() + defer close(errChan) + + if count == 0 { + return errNoOutputPlugins + } + + // Wait for and return an error from any output plugin. + for err := range errChan { + if err != nil { + return err + } + + if count--; count == 0 { + return errAllOutputStopped + } + } + + return nil +} + +func (u *UnifiPoller) runOutputMethods() (int, chan error) { + // Output plugin errors go into this channel. + err := make(chan error) + + outputSync.RLock() + defer outputSync.RUnlock() + + for _, o := range outputs { + go func(o *Output) { + err <- o.Method(u) // Run each output plugin + }(o) + } + + return len(outputs), err +} + +// Outputs allows other output plugins to see the list of loaded output plugins. +func (u *UnifiPoller) Outputs() (names []string) { + outputSync.RLock() + defer outputSync.RUnlock() + + for i := range outputs { + names = append(names, outputs[i].Name) + } + + return names +} diff --git a/core/poller/start.go b/core/poller/start.go new file mode 100644 index 00000000..8b69180a --- /dev/null +++ b/core/poller/start.go @@ -0,0 +1,98 @@ +// Package poller provides the CLI interface to setup unifi-poller. +package poller + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/spf13/pflag" + "golift.io/version" +) + +// New returns a new poller struct. +func New() *UnifiPoller { + return &UnifiPoller{Config: &Config{Poller: &Poller{}}, Flags: &Flags{}} +} + +// Start begins the application from a CLI. +// Parses cli flags, parses config file, parses env vars, sets up logging, then: +// - dumps a json payload OR - executes Run(). +func (u *UnifiPoller) Start() error { + log.SetOutput(os.Stdout) + log.SetFlags(log.LstdFlags) + u.Flags.Parse(os.Args[1:]) + + if u.Flags.ShowVer { + fmt.Println(version.Print(AppName)) + return nil // don't run anything else w/ version request. + } + + if u.Flags.HashPW != "" { + return u.PrintPasswordHash() + } + + cfile, err := getFirstFile(strings.Split(u.Flags.ConfigFile, ",")) + if err != nil { + return err + } + + u.Flags.ConfigFile = cfile + if u.Flags.DumpJSON == "" { // do not print this when dumping JSON. + u.Logf("Loading Configuration File: %s", u.Flags.ConfigFile) + } + + // Parse config file and ENV variables. + if err := u.ParseConfigs(); err != nil { + return err + } + + return u.Run() +} + +// Parse turns CLI arguments into data structures. Called by Start() on startup. +func (f *Flags) Parse(args []string) { + f.FlagSet = pflag.NewFlagSet(AppName, pflag.ExitOnError) + f.Usage = func() { + fmt.Printf("Usage: %s [--config=/path/to/up.conf] [--version]", AppName) + f.PrintDefaults() + } + + f.StringVarP(&f.HashPW, "encrypt", "e", "", + "This option bcrypts a provided string. Useful for the webserver password. Use - to be prompted.") + f.StringVarP(&f.DumpJSON, "dumpjson", "j", "", + "This debug option prints a json payload and exits. See man page for more info.") + f.StringVarP(&f.ConfigFile, "config", "c", DefaultConfFile, + "Poller config file path. Separating multiple paths with a comma will load the first config file found.") + f.BoolVarP(&f.ShowVer, "version", "v", false, "Print the version and exit.") + _ = f.FlagSet.Parse(args) // pflag.ExitOnError means this will never return error. +} + +// Run picks a mode and executes the associated functions. This will do one of three things: +// 1. Start the collector routine that polls unifi and reports to influx on an interval. (default) +// 2. Run the collector one time and report the metrics to influxdb. (lambda) +// 3. Start a web server and wait for Prometheus to poll the application for metrics. +func (u *UnifiPoller) Run() error { + if u.Flags.DumpJSON != "" { + u.Config.Quiet = true + if err := u.InitializeInputs(); err != nil { + return err + } + + return u.PrintRawMetrics() + } + + if u.Debug { + log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) + u.LogDebugf("Debug Logging Enabled") + } + + log.Printf("[INFO] UniFi Poller v%v Starting Up! PID: %d", version.Version, os.Getpid()) + + if err := u.InitializeInputs(); err != nil { + return err + } + + return u.InitializeOutputs() +}