Files
unpoller_unpoller/pkg/poller/config.go
T
Cody LeeandClaude Opus 4.7 d2948b8bd0 feat: upgrade unifi to v5.26.0 and add Integration/v1 + new legacy metrics
Adds 21 new data types from unifi v5.26.0 across all metric output plugins
(InfluxDB, Prometheus, DataDog). Per-site Integration/v1 calls are gated on
API key configuration and only run for user-configured sites; ErrEndpointNotFound
is handled gracefully so older firmware continues to work without log spam.

Also migrates events collection (collectAlarms, collectAnomalies, collectEvents,
collectIDs, collectProtectLogs) to handle Network 10.x+ endpoint removals via
ErrEndpointNotFound, with debug-level logging to avoid per-poll noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:45:47 -05:00

250 lines
7.1 KiB
Go

package poller
import (
"fmt"
"os"
"path"
"plugin"
"runtime"
"strings"
"time"
"github.com/spf13/pflag"
"golift.io/cnfg"
"golift.io/cnfgfile"
)
const (
// AppName is the name of the application.
AppName = "unpoller"
// ENVConfigPrefix is the prefix appended to an env variable tag name.
ENVConfigPrefix = "UP"
)
// DefaultConfFile is where to find config if --config is not provided.
func DefaultConfFile() string {
switch runtime.GOOS {
case "windows":
return `C:\ProgramData\unifi-poller\up.conf`
case "darwin":
fallthrough
case "freebsd":
fallthrough
case "netbsd":
fallthrough
case "openbsd":
return "/etc/unpoller/up.conf,/etc/unifi-poller/up.conf,/usr/local/etc/unifi-poller/up.conf,up.conf"
default:
// linux and everything else
return "/etc/unpoller/up.conf,/config/unifi-poller.conf,/etc/unifi-poller/up.conf,up.conf"
}
}
// DefaultObjPath is the path to look for shared object libraries (plugins).
func DefaultObjPath() string {
switch runtime.GOOS {
case "windows":
// DefaultObjPath is useless in this context. Bummer.
return "PLUGINS_DO_NOT_WORK_ON_WINDOWS_SOWWWWWY"
case "darwin":
fallthrough
case "freebsd":
fallthrough
case "netbsd":
fallthrough
case "openbsd":
return "/usr/local/lib/unpoller"
default:
// linux and everything else
return "/usr/local/lib/unpoller"
}
}
// 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
DebugIO bool
Health bool
Discover bool
DiscoverOutput string
*pflag.FlagSet
}
// ControllerStatus carries the per-controller poll result (up/down) so that
// output plugins can expose a health gauge without knowing UniFi internals.
type ControllerStatus struct {
// Source is a stable identifier for the controller (URL or configured ID).
Source string
// Up is true when the last poll of this controller succeeded.
Up bool
}
// Metrics is a type shared by the exporting and reporting packages.
type Metrics struct {
TS time.Time
Sites []any
Clients []any
SitesDPI []any
ClientsDPI []any
Devices []any
RogueAPs []any
SpeedTests []any
CountryTraffic []any
DHCPLeases []any
WANConfigs []any
Sysinfos []any
FirewallPolicies []any
Topologies []any
PortAnomalies []any
VPNMeshes []any
ControllerStatuses []ControllerStatus
// Added in v5.26.0 integration:
WANStatuses []any // *unifi.WANStatus — per-site WAN failover state
PortForwards []any // *unifi.PortForward — port forwarding rules
SSLCertificates []any // *unifi.SSLCertificate — controller SSL cert info
UPSDevices []any // *unifi.UPSDeviceSelector — UPS selector list
IntegrationDevStats []any // *unifi.IntegrationDeviceStats — CPU/mem/radio/uplink per device
WifiBroadcasts []any // *unifi.WifiBroadcast — WiFi SSID broadcasts
FirewallZones []any // *unifi.FirewallZone — firewall zones
ACLRules []any // *unifi.ACLRule — access control rules
VPNServers []any // *unifi.VPNServer — VPN server inventory
SiteToSiteTunnels []any // *unifi.SiteToSiteTunnel — site-to-site VPN tunnels
LAGs []any // *unifi.LAG — link aggregation groups
MCLAGDomains []any // *unifi.MCLAGDomain — multi-chassis LAG domains
SwitchStacks []any // *unifi.SwitchStack — switch stacks
DNSPolicies []any // *unifi.DNSPolicy — DNS policies
RADIUSProfiles []any // *unifi.RADIUSProfile — RADIUS profiles
TrafficMatchingLists []any // *unifi.TrafficMatchingList — traffic matching lists
HotspotVouchers []any // *unifi.HotspotVoucher — hotspot vouchers
DPIApplications []any // *unifi.DPIApplication — DPI app catalogue (global)
DPICategories []any // *unifi.DPICategory — DPI category catalogue (global)
PendingDevices []any // *unifi.PendingDevice — devices awaiting adoption (global)
Countries []any // *unifi.Country — country list for geo-filters (global)
}
// Events defines the type for log entries.
type Events struct {
Logs []any
}
// 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"`
LogUnknownTypes bool `json:"log_unknown_types" toml:"log_unknown_types" xml:"log_unknown_types" yaml:"log_unknown_types"`
}
// 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 any) 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
}