diff --git a/Gopkg.lock b/Gopkg.lock index 83212a0b..aa24b10e 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -107,11 +107,11 @@ [[projects]] branch = "master" - digest = "1:d54a8d89f95a4d2a5a24ce63cb1835ccdff337fde7776c87ceacb6fdbe4349ae" + digest = "1:7a90fad47972b5ae06013d4685eb2f3007e7c92609a1399d2adf59fe04cd9b63" name = "golift.io/config" packages = ["."] pruneopts = "UT" - revision = "fd8ffb02173aad2183e5555a03b1d1f909aca930" + revision = "fe642c8392dc00d72ddcc47f05a06096bd5d054b" [[projects]] digest = "1:2883cea734f2766f41ff9c9d4aefccccc53e3d44f5c8b08893b9c218cf666722" diff --git a/examples/up.xml.example b/examples/up.xml.example index ec98169d..14b2aa09 100644 --- a/examples/up.xml.example +++ b/examples/up.xml.example @@ -24,7 +24,7 @@ false - + all diff --git a/examples/up.yaml.example b/examples/up.yaml.example index 2be57012..bb8a4aa1 100644 --- a/examples/up.yaml.example +++ b/examples/up.yaml.example @@ -24,6 +24,7 @@ influxdb: verify_ssl: false unifi: + disable: false controllers: - name: "" user: "influx" diff --git a/pkg/influxunifi/influxdb.go b/pkg/influxunifi/influxdb.go index 199ea594..9bbe9c84 100644 --- a/pkg/influxunifi/influxdb.go +++ b/pkg/influxunifi/influxdb.go @@ -24,7 +24,7 @@ const ( // Config defines the data needed to store metrics in InfluxDB type Config struct { Interval config.Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` - Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` + 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"` URL string `json:"url,omitempty" toml:"url,omitempty" xml:"url" yaml:"url"` User string `json:"user,omitempty" toml:"user,omitempty" xml:"user" yaml:"user"` diff --git a/pkg/inputunifi/collector.go b/pkg/inputunifi/collector.go index 20aeafac..c9c67dcb 100644 --- a/pkg/inputunifi/collector.go +++ b/pkg/inputunifi/collector.go @@ -8,14 +8,14 @@ import ( "golift.io/unifi" ) -func (u *InputUnifi) isNill(c Controller) bool { +func (u *InputUnifi) isNill(c *Controller) bool { u.Config.RLock() defer u.Config.RUnlock() return c.Unifi == nil } -func (u *InputUnifi) collectController(c Controller) (*poller.Metrics, error) { +func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) { if u.isNill(c) { u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) @@ -32,7 +32,7 @@ func (u *InputUnifi) collectController(c Controller) (*poller.Metrics, error) { return u.pollController(c) } -func (u *InputUnifi) pollController(c Controller) (*poller.Metrics, error) { +func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) { var err error u.Config.RLock() @@ -67,7 +67,7 @@ func (u *InputUnifi) pollController(c Controller) (*poller.Metrics, error) { // augmentMetrics is our middleware layer between collecting metrics and writing them. // This is where we can manipuate the returned data or make arbitrary decisions. // This function currently adds parent device names to client metrics. -func (u *InputUnifi) augmentMetrics(c Controller, metrics *poller.Metrics) *poller.Metrics { +func (u *InputUnifi) augmentMetrics(c *Controller, metrics *poller.Metrics) *poller.Metrics { if metrics == nil || metrics.Devices == nil || metrics.Clients == nil { return metrics } @@ -113,22 +113,22 @@ func (u *InputUnifi) augmentMetrics(c Controller, metrics *poller.Metrics) *poll // getFilteredSites returns a list of sites to fetch data for. // Omits requested but unconfigured sites. Grabs the full list from the // controller and returns the sites provided in the config file. -func (u *InputUnifi) getFilteredSites(c Controller) (unifi.Sites, error) { +func (u *InputUnifi) getFilteredSites(c *Controller) (unifi.Sites, error) { u.Config.RLock() defer u.Config.RUnlock() sites, err := c.Unifi.GetSites() if err != nil { return nil, err - } else if len(c.Sites) < 1 || poller.StringInSlice("all", c.Sites) { + } else if len(c.Sites) < 1 || StringInSlice("all", c.Sites) { return sites, nil } - var i int + i := 0 for _, s := range sites { // Only include valid sites in the request filter. - if poller.StringInSlice(s.Name, c.Sites) { + if StringInSlice(s.Name, c.Sites) { sites[i] = s i++ } diff --git a/pkg/inputunifi/input.go b/pkg/inputunifi/input.go index 15a027dd..9a4877f9 100644 --- a/pkg/inputunifi/input.go +++ b/pkg/inputunifi/input.go @@ -4,6 +4,8 @@ package inputunifi import ( "fmt" + "os" + "strings" "sync" @@ -13,7 +15,7 @@ import ( // InputUnifi contains the running data. type InputUnifi struct { - Config Config `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"` + Config *Config `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"` poller.Logger } @@ -33,9 +35,9 @@ type Controller struct { // Config contains our configuration data type Config struct { - sync.RWMutex // locks the Unifi struct member when re-authing to unifi. - Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` - Controllers []Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"` + sync.RWMutex // locks the Unifi struct member when re-authing to unifi. + Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` + Controllers []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"` } func init() { @@ -48,7 +50,7 @@ func init() { } // getUnifi (re-)authenticates to a unifi controller. -func (u *InputUnifi) getUnifi(c Controller) error { +func (u *InputUnifi) getUnifi(c *Controller) error { var err error u.Config.Lock() @@ -76,3 +78,74 @@ func (u *InputUnifi) getUnifi(c Controller) error { return nil } + +// checkSites makes sure the list of provided sites exists on the controller. +// This only runs once during initialization. +func (u *InputUnifi) checkSites(c *Controller) error { + u.Config.RLock() + defer u.Config.RUnlock() + + if len(c.Sites) < 1 || c.Sites[0] == "" { + c.Sites = []string{"all"} + } + + u.LogDebugf("Checking Controller Sites List") + + sites, err := c.Unifi.GetSites() + if err != nil { + return err + } + + msg := []string{} + for _, site := range sites { + msg = append(msg, site.Name+" ("+site.Desc+")") + } + + u.Logf("Found %d site(s) on controller %s: %v", len(msg), c.Name, strings.Join(msg, ", ")) + + if StringInSlice("all", c.Sites) { + c.Sites = []string{"all"} + return nil + } + +FIRST: + for _, s := range c.Sites { + for _, site := range sites { + if s == site.Name { + continue FIRST + } + } + return fmt.Errorf("configured site not found on controller: %v", s) + } + + return nil +} + +func (u *InputUnifi) dumpSitesJSON(c *Controller, path, name string, sites unifi.Sites) ([]byte, error) { + allJSON := []byte{} + + for _, s := range sites { + 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) + + body, err := c.Unifi.GetJSON(apiPath) + if err != nil { + return allJSON, err + } + + allJSON = append(allJSON, body...) + } + + return allJSON, nil +} + +// StringInSlice returns true if a string is in a slice. +func StringInSlice(str string, slice []string) bool { + for _, s := range slice { + if strings.EqualFold(s, str) { + return true + } + } + + return false +} diff --git a/pkg/inputunifi/interface.go b/pkg/inputunifi/interface.go index 1db32906..f9b437de 100644 --- a/pkg/inputunifi/interface.go +++ b/pkg/inputunifi/interface.go @@ -1,13 +1,51 @@ package inputunifi +/* This file contains the three poller.Input interface methods. */ + import ( "fmt" + "os" "strings" "github.com/davidnewhall/unifi-poller/pkg/poller" "golift.io/unifi" ) +// Initialize gets called one time when starting up. +// Satisfies poller.Input interface. +func (u *InputUnifi) Initialize(l poller.Logger) error { + if u.Config.Disable { + l.Logf("unifi input disabled") + return nil + } + + if len(u.Config.Controllers) < 1 { + return fmt.Errorf("no unifi controllers defined for unifi input") + } + + u.Logger = l + + for i, c := range u.Config.Controllers { + if c.Name == "" { + u.Config.Controllers[i].Name = c.URL + } + + switch err := u.getUnifi(c); err { + case nil: + if err := u.checkSites(c); err != nil { + u.LogErrorf("checking sites on %s: %v", c.Name, err) + } + + u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", + c.URL, c.Unifi.ServerVersion, c.User, c.Sites) + default: + u.LogErrorf("Controller Auth or Connection failed, but continuing to retry! %s: %v", c.Name, err) + } + } + + return nil +} + // Metrics grabs all the measurements from a UniFi controller and returns them. func (u *InputUnifi) Metrics() (*poller.Metrics, error) { if u.Config.Disable { @@ -52,75 +90,35 @@ func (u *InputUnifi) Metrics() (*poller.Metrics, error) { return metrics, nil } -// Initialize gets called one time when starting up. -// Satisfies poller.Input interface. -func (u *InputUnifi) Initialize(l poller.Logger) error { - if u.Config.Disable { - l.Logf("unifi input disabled") - return nil - } +// RawMetrics returns API output from the first configured unifi controller. +func (u *InputUnifi) RawMetrics(filter poller.Filter) ([]byte, error) { + c := u.Config.Controllers[0] // We could pull the controller number from the filter. + if u.isNill(c) { + u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) - if len(u.Config.Controllers) < 1 { - return fmt.Errorf("no unifi controllers defined for unifi input") - } - - u.Logger = l - - for i, c := range u.Config.Controllers { - if c.Name == "" { - u.Config.Controllers[i].Name = c.URL - } - - switch err := u.getUnifi(c); err { - case nil: - if err := u.checkSites(c); err != nil { - u.LogErrorf("checking sites on %s: %v", c.Name, err) - } - - u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", - c.URL, c.Unifi.ServerVersion, c.User, c.Sites) - default: - u.LogErrorf("Controller Auth or Connection failed, but continuing to retry! %s: %v", c.Name, err) + if err := u.getUnifi(c); err != nil { + return nil, fmt.Errorf("re-authenticating to %s: %v", c.Name, err) } } - return nil -} + if err := u.checkSites(c); err != nil { + return nil, err + } -// checkSites makes sure the list of provided sites exists on the controller. -// This only runs once during initialization. -func (u *InputUnifi) checkSites(c Controller) error { - u.Config.RLock() - defer u.Config.RUnlock() - u.LogDebugf("Checking Controller Sites List") - - sites, err := c.Unifi.GetSites() + sites, err := u.getFilteredSites(c) if err != nil { - return err + return nil, err } - msg := []string{} - - for _, site := range sites { - msg = append(msg, site.Name+" ("+site.Desc+")") + switch filter.Type { + case "d", "device", "devices": + return u.dumpSitesJSON(c, unifi.APIDevicePath, "Devices", sites) + case "client", "clients", "c": + return u.dumpSitesJSON(c, unifi.APIClientPath, "Clients", sites) + case "other", "o": + _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", filter.Term) + return c.Unifi.GetJSON(filter.Term) + default: + return []byte{}, fmt.Errorf("must provide filter: devices, clients, other") } - - u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", ")) - - if poller.StringInSlice("all", c.Sites) { - c.Sites = []string{"all"} - return nil - } - -FIRST: - for _, s := range c.Sites { - for _, site := range sites { - if s == site.Name { - continue FIRST - } - } - return fmt.Errorf("configured site not found on controller: %v", s) - } - - return nil } diff --git a/pkg/poller/dumper.go b/pkg/poller/dumper.go index 18abe901..2a2b0dab 100644 --- a/pkg/poller/dumper.go +++ b/pkg/poller/dumper.go @@ -1,85 +1,28 @@ package poller import ( + "fmt" "strings" ) // DumpJSONPayload prints raw json from the UniFi Controller. // This only works with controller 0 (first one) in the config. func (u *UnifiPoller) DumpJSONPayload() (err error) { - if true { - return nil + u.Config.Quiet = true + + split := strings.SplitN(u.Flags.DumpJSON, " ", 2) + filter := Filter{Type: split[0]} + + if len(split) > 1 { + filter.Term = split[1] } - /* - u.Config.Quiet = true - config := u.Config.Controllers[0] - config.Unifi, err = unifi.NewUnifi(&unifi.Config{ - User: config.User, - Pass: config.Pass, - URL: config.URL, - VerifySSL: config.VerifySSL, - }) - if err != nil { - return err - } + m, err := inputs[0].RawMetrics(filter) + if err != nil { + return err + } - fmt.Fprintf(os.Stderr, "[INFO] Authenticated to UniFi Controller @ %v as user %v", config.URL, config.User) + fmt.Println(string(m)) - if err := u.CheckSites(config); err != nil { - return err - } - - config.Unifi.ErrorLog = func(m string, v ...interface{}) { - fmt.Fprintf(os.Stderr, "[ERROR] "+m, v...) - } // Log all errors to stderr. - - switch sites, err := u.GetFilteredSites(config); { - case err != nil: - return err - case StringInSlice(u.Flags.DumpJSON, []string{"d", "device", "devices"}): - return u.dumpSitesJSON(config, unifi.APIDevicePath, "Devices", sites) - case StringInSlice(u.Flags.DumpJSON, []string{"client", "clients", "c"}): - return u.dumpSitesJSON(config, unifi.APIClientPath, "Clients", sites) - case strings.HasPrefix(u.Flags.DumpJSON, "other "): - apiPath := strings.SplitN(u.Flags.DumpJSON, " ", 2)[1] - _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath) - return u.PrintRawAPIJSON(config, apiPath) - default: - return fmt.Errorf("must provide filter: devices, clients, other") - } - */ return nil } - -/* -func (u *UnifiPoller) dumpSitesJSON(c Controller, path, name string, sites unifi.Sites) error { - for _, s := range sites { - 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) - if err := u.PrintRawAPIJSON(c, apiPath); err != nil { - return err - } - } - return nil -} - -// PrintRawAPIJSON prints the raw json for a user-provided path on a UniFi Controller. -func (u *UnifiPoller) PrintRawAPIJSON(c Controller, apiPath string) error { - body, err := c.Unifi.GetJSON(apiPath) - fmt.Println(string(body)) - return err -} -*/ - -// StringInSlice returns true if a string is in a slice. -func StringInSlice(str string, slice []string) bool { - for _, s := range slice { - if strings.EqualFold(s, str) { - return true - } - } - - return false -} diff --git a/pkg/poller/inputs.go b/pkg/poller/inputs.go index c2140b01..f2d64efb 100644 --- a/pkg/poller/inputs.go +++ b/pkg/poller/inputs.go @@ -17,6 +17,7 @@ var ( type Input interface { Initialize(Logger) error // Called once on startup to initialize the plugin. Metrics() (*Metrics, error) // Called every time new metrics are requested. + RawMetrics(Filter) ([]byte, error) } // InputPlugin describes an input plugin's consumable interface. @@ -25,6 +26,12 @@ type InputPlugin struct { Input } +// Filter is used for raw metrics filters. +type Filter struct { + Type string + Term string +} + // NewInput creates a metric input. This should be called by input plugins // init() functions. func NewInput(i *InputPlugin) { diff --git a/pkg/poller/logger.go b/pkg/poller/logger.go index b498a9b5..fa983e5f 100644 --- a/pkg/poller/logger.go +++ b/pkg/poller/logger.go @@ -16,14 +16,14 @@ type Logger interface { // Logf prints a log entry if quiet is false. func (u *UnifiPoller) Logf(m string, v ...interface{}) { - if !u.Config.Quiet { + 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.Config.Debug && !u.Config.Quiet { + if u.Debug && !u.Quiet { _ = log.Output(callDepth, fmt.Sprintf("[DEBUG] "+m, v...)) } } diff --git a/pkg/poller/start.go b/pkg/poller/start.go index 9f6bedc8..e88daf24 100644 --- a/pkg/poller/start.go +++ b/pkg/poller/start.go @@ -61,10 +61,14 @@ func (f *Flags) Parse(args []string) { // 3. Start a web server and wait for Prometheus to poll the application for metrics. func (u *UnifiPoller) Run() error { if u.Flags.DumpJSON != "" { + if err := u.InitializeInputs(); err != nil { + return err + } + return u.DumpJSONPayload() } - if u.Config.Debug { + if u.Debug { log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) u.LogDebugf("Debug Logging Enabled") }