unpoller_unpoller/pkg/inputunifi/interface.go

339 lines
9.5 KiB
Go

package inputunifi
/* This file contains the three poller.Input interface methods. */
import (
"fmt"
"strings"
"github.com/unpoller/unifi/v5"
"github.com/unpoller/unpoller/pkg/poller"
"github.com/unpoller/unpoller/pkg/webserver"
)
var (
ErrDynamicLookupsDisabled = fmt.Errorf("filter path requested but dynamic lookups disabled")
ErrControllerNumNotFound = fmt.Errorf("controller number not found")
ErrNoFilterKindProvided = fmt.Errorf("must provide filter: devices, clients, other")
)
// Initialize gets called one time when starting up.
// Satisfies poller.Input interface.
func (u *InputUnifi) Initialize(l poller.Logger) error {
if u.Config == nil {
u.Config = &Config{Disable: true}
}
if u.Logger = l; u.Disable {
u.Logf("UniFi input plugin disabled or missing configuration!")
return nil
}
// Discover remote controllers if remote mode is enabled at config level
if u.Remote && u.RemoteAPIKey != "" {
u.Logf("Remote API mode enabled, discovering controllers...")
discovered, err := u.discoverRemoteControllers(u.RemoteAPIKey)
if err != nil {
u.LogErrorf("Failed to discover remote controllers: %v", err)
} else if len(discovered) > 0 {
// Replace controllers with discovered ones when using config-level remote mode
u.Controllers = discovered
u.Logf("Discovered %d remote controller(s)", len(discovered))
}
} else {
// Only set default controller if not using config-level remote mode
if u.setDefaults(&u.Default); len(u.Controllers) == 0 && !u.Dynamic {
u.Controllers = []*Controller{&u.Default}
}
// Check individual controllers for remote flag (per-controller remote mode)
for _, c := range u.Controllers {
if c.Remote && c.APIKey != "" && c.ConsoleID == "" {
// This controller has remote flag but no console ID, try to discover
discovered, err := u.discoverRemoteControllers(c.APIKey)
if err != nil {
u.LogErrorf("Failed to discover remote controllers for controller: %v", err)
continue
}
if len(discovered) > 0 {
// Replace this controller with discovered ones
// Remove the current one and add discovered
newControllers := []*Controller{}
for _, existing := range u.Controllers {
if existing != c {
newControllers = append(newControllers, existing)
}
}
newControllers = append(newControllers, discovered...)
u.Controllers = newControllers
}
}
}
}
if len(u.Controllers) == 0 {
u.Logf("No controllers configured. Polling dynamic controllers only! Defaults:")
u.logController(&u.Default)
}
for i, c := range u.Controllers {
if err := u.getUnifi(u.setControllerDefaults(c)); err != nil {
u.LogErrorf("Controller %d of %d Auth or Connection Error, retrying: %v", i+1, len(u.Controllers), err)
continue
}
if err := u.checkSites(c); err != nil {
u.LogErrorf("checking sites on %s: %v", c.URL, err)
}
u.Logf("Configured UniFi Controller %d of %d:", i+1, len(u.Controllers))
u.logController(c)
}
webserver.UpdateInput(&webserver.Input{Name: PluginName, Config: formatConfig(u.Config)})
return nil
}
func (u *InputUnifi) DebugInput() (bool, error) {
if u == nil || u.Config == nil {
return true, nil
}
if u.setDefaults(&u.Default); len(u.Controllers) == 0 && !u.Dynamic {
u.Controllers = []*Controller{&u.Default}
}
if len(u.Controllers) == 0 {
u.Logf("No controllers configured. Polling dynamic controllers only! Defaults:")
u.logController(&u.Default)
}
allOK := true
var allErrors error
for i, c := range u.Controllers {
if err := u.getUnifi(u.setControllerDefaults(c)); err != nil {
u.LogErrorf("Controller %d of %d Auth or Connection Error, retrying: %v", i+1, len(u.Controllers), err)
allOK = false
if allErrors != nil {
allErrors = fmt.Errorf("%v: %w", err, allErrors)
} else {
allErrors = err
}
continue
}
if err := u.checkSites(c); err != nil {
u.LogErrorf("checking sites on %s: %v", c.URL, err)
allOK = false
if allErrors != nil {
allErrors = fmt.Errorf("%v: %w", err, allErrors)
} else {
allErrors = err
}
continue
}
u.Logf("Valid UniFi Controller %d of %d:", i+1, len(u.Controllers))
u.logController(c)
}
return allOK, allErrors
}
func (u *InputUnifi) logController(c *Controller) {
mode := "Local"
if c.Remote {
mode = "Remote"
if c.ConsoleID != "" {
mode += fmt.Sprintf(" (Console: %s)", c.ConsoleID)
}
}
u.Logf(" => Mode: %s", mode)
u.Logf(" => URL: %s (verify SSL: %v, timeout: %v)", c.URL, *c.VerifySSL, c.Timeout.Duration)
if len(c.CertPaths) > 0 {
u.Logf(" => Cert Files: %s", strings.Join(c.CertPaths, ", "))
}
if c.Unifi != nil {
u.Logf(" => Version: %s (%s)", c.Unifi.ServerVersion, c.Unifi.UUID)
}
if c.Remote {
u.Logf(" => API Key: %v", c.APIKey != "")
} else {
u.Logf(" => Username: %s (has password: %v) (has api-key: %v)", c.User, c.Pass != "", c.APIKey != "")
}
u.Logf(" => Hash PII %v / Drop PII %v / Poll Sites: %s", *c.HashPII, *c.DropPII, strings.Join(c.Sites, ", "))
u.Logf(" => Save Sites %v / Save DPI %v (metrics)", *c.SaveSites, *c.SaveDPI)
u.Logf(" => Save Events %v / Save Syslog %v / Save IDs %v (logs)", *c.SaveEvents, *c.SaveSyslog, *c.SaveIDs)
u.Logf(" => Save Alarms %v / Anomalies %v / Protect Logs %v (thumbnails: %v)", *c.SaveAlarms, *c.SaveAnomal, *c.SaveProtectLogs, *c.ProtectThumbnails)
u.Logf(" => Save Rogue APs: %v", *c.SaveRogue)
u.Logf(" => Save Traffic %v", *c.SaveTraffic)
}
// Events allows you to pull only events (and IDs) from the UniFi Controller.
// This does not fully respect HashPII, but it may in the future!
// Use Filter.Path to pick a specific controller, otherwise poll them all!
func (u *InputUnifi) Events(filter *poller.Filter) (*poller.Events, error) {
if u.Disable {
return nil, nil
}
logs := []any{}
if filter == nil {
filter = &poller.Filter{}
}
var collectionErrors []error
for _, c := range u.Controllers {
if filter.Path != "" && !strings.EqualFold(c.URL, filter.Path) {
continue
}
events, err := u.collectControllerEvents(c)
if err != nil {
// Log error but continue to next controller
u.LogErrorf("Failed to collect events from controller %s: %v", c.URL, err)
collectionErrors = append(collectionErrors, fmt.Errorf("%s: %w", c.URL, err))
continue
}
logs = append(logs, events...)
}
// Return collected events even if some controllers failed
// Only return error if ALL controllers failed and we have no events
if len(logs) == 0 && len(collectionErrors) > 0 {
return nil, collectionErrors[0]
}
return &poller.Events{Logs: logs}, nil
}
// Metrics grabs all the measurements from a UniFi controller and returns them.
// Set Filter.Path to a controller URL for a specific controller (or get them all).
func (u *InputUnifi) Metrics(filter *poller.Filter) (*poller.Metrics, error) {
if u.Disable {
return nil, nil
}
metrics := &poller.Metrics{}
if filter == nil {
filter = &poller.Filter{}
}
var collectionErrors []error
// Check if the request is for an existing, configured controller (or all controllers)
for _, c := range u.Controllers {
if filter.Path != "" && !strings.EqualFold(c.URL, filter.Path) {
// continue only if we have a filter path and it doesn't match.
continue
}
m, err := u.collectController(c)
if err != nil {
// Log error but continue to next controller
u.LogErrorf("Failed to collect metrics from controller %s: %v", c.URL, err)
collectionErrors = append(collectionErrors, fmt.Errorf("%s: %w", c.URL, err))
continue
}
metrics = poller.AppendMetrics(metrics, m)
}
// If we collected data from at least one controller, return success
if len(metrics.Devices) > 0 || len(metrics.Clients) > 0 {
return metrics, nil
}
// If all controllers failed and we had errors, return the first error
if len(collectionErrors) > 0 {
return metrics, collectionErrors[0]
}
if filter.Path == "" || len(metrics.Clients) != 0 {
return metrics, nil
}
if !u.Dynamic {
return nil, ErrDynamicLookupsDisabled
}
// Attempt a dynamic metrics fetch from an unconfigured controller.
return u.dynamicController(filter)
}
// RawMetrics returns API output from the first configured UniFi controller.
// Adjust filter.Unit to pull from a controller other than the first.
func (u *InputUnifi) RawMetrics(filter *poller.Filter) ([]byte, error) {
if l := len(u.Controllers); filter.Unit >= l {
return nil, fmt.Errorf("%d controller(s) configured, '%d': %w", l, filter.Unit, ErrControllerNumNotFound)
}
c := u.Controllers[filter.Unit]
if u.isNill(c) {
u.Logf("Re-authenticating to UniFi Controller: %s", c.URL)
if err := u.getUnifi(c); err != nil {
return nil, fmt.Errorf("re-authenticating to %s: %w", c.URL, err)
}
}
if err := u.checkSites(c); err != nil {
return nil, err
}
sites, err := u.getFilteredSites(c)
if err != nil {
return nil, err
}
switch filter.Kind {
case "d", "device", "devices":
return u.getSitesJSON(c, unifi.APIDevicePath, sites)
case "client", "clients", "c":
return u.getSitesJSON(c, unifi.APIClientPath, sites)
case "other", "o":
return c.Unifi.GetJSON(filter.Path)
default:
return []byte{}, ErrNoFilterKindProvided
}
}
func (u *InputUnifi) getSitesJSON(c *Controller, path string, sites []*unifi.Site) ([]byte, error) {
allJSON := []byte{}
for _, s := range sites {
apiPath := fmt.Sprintf(path, s.Name)
u.LogDebugf("Returning Path '%s' for site: %s (%s):\n", apiPath, s.Desc, s.Name)
body, err := c.Unifi.GetJSON(apiPath)
if err != nil {
return allJSON, fmt.Errorf("controller: %w", err)
}
allJSON = append(allJSON, body...)
}
return allJSON, nil
}