mirror of
https://github.com/unpoller/unpoller.git
synced 2026-03-31 06:24:19 -04:00
Merge pull request #913 from brngates98/master
Add remote API support for UniFi Site Manager
This commit is contained in:
45
examples/remote_api_example.conf
Normal file
45
examples/remote_api_example.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
##############################################
|
||||
# UniFi Poller Remote API Configuration #
|
||||
# TOML FORMAT - Remote API Example #
|
||||
# Uses UniFi Site Manager API (api.ui.com) #
|
||||
##############################################
|
||||
|
||||
[poller]
|
||||
debug = false
|
||||
quiet = false
|
||||
|
||||
[unifi]
|
||||
remote = true
|
||||
remote_api_key = "YOUR_API_KEY_HERE"
|
||||
|
||||
[unifi.defaults]
|
||||
save_sites = true
|
||||
save_dpi = true
|
||||
save_events = true
|
||||
save_alarms = true
|
||||
save_anomalies = true
|
||||
save_ids = true
|
||||
save_traffic = true
|
||||
save_rogue = true
|
||||
save_syslog = true
|
||||
save_protect_logs = false
|
||||
verify_ssl = true
|
||||
|
||||
[prometheus]
|
||||
disable = false
|
||||
http_listen = "0.0.0.0:9130"
|
||||
namespace = "unpoller"
|
||||
report_errors = false
|
||||
dead_ports = false
|
||||
|
||||
[influxdb]
|
||||
disable = true
|
||||
|
||||
[webserver]
|
||||
enable = false
|
||||
|
||||
[datadog]
|
||||
enable = false
|
||||
|
||||
[loki]
|
||||
disable = true
|
||||
42
examples/remote_api_example.json
Normal file
42
examples/remote_api_example.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"poller": {
|
||||
"debug": false,
|
||||
"quiet": false
|
||||
},
|
||||
"unifi": {
|
||||
"remote": true,
|
||||
"remote_api_key": "YOUR_API_KEY_HERE",
|
||||
"defaults": {
|
||||
"save_sites": true,
|
||||
"save_dpi": true,
|
||||
"save_events": true,
|
||||
"save_alarms": true,
|
||||
"save_anomalies": true,
|
||||
"save_ids": true,
|
||||
"save_traffic": true,
|
||||
"save_rogue": true,
|
||||
"save_syslog": true,
|
||||
"save_protect_logs": false,
|
||||
"verify_ssl": true
|
||||
}
|
||||
},
|
||||
"prometheus": {
|
||||
"disable": false,
|
||||
"http_listen": "0.0.0.0:9130",
|
||||
"namespace": "unpoller",
|
||||
"report_errors": false,
|
||||
"dead_ports": false
|
||||
},
|
||||
"influxdb": {
|
||||
"disable": true
|
||||
},
|
||||
"webserver": {
|
||||
"enable": false
|
||||
},
|
||||
"datadog": {
|
||||
"enable": false
|
||||
},
|
||||
"loki": {
|
||||
"disable": true
|
||||
}
|
||||
}
|
||||
49
examples/remote_api_example.yaml
Normal file
49
examples/remote_api_example.yaml
Normal file
@@ -0,0 +1,49 @@
|
||||
##############################################
|
||||
# UniFi Poller Remote API Configuration #
|
||||
# YAML FORMAT - Remote API Example #
|
||||
# Uses UniFi Site Manager API (api.ui.com) #
|
||||
##############################################
|
||||
---
|
||||
poller:
|
||||
debug: false
|
||||
quiet: false
|
||||
|
||||
unifi:
|
||||
# Enable remote API mode - automatically discovers all consoles
|
||||
remote: true
|
||||
# Your API key from unifi.ui.com (Settings -> API Keys)
|
||||
remote_api_key: "YOUR_API_KEY_HERE"
|
||||
|
||||
defaults:
|
||||
# Enable all metric collection
|
||||
save_sites: true
|
||||
save_dpi: true
|
||||
save_events: true
|
||||
save_alarms: true
|
||||
save_anomalies: true
|
||||
save_ids: true
|
||||
save_traffic: true
|
||||
save_rogue: true
|
||||
save_syslog: true
|
||||
save_protect_logs: false
|
||||
# Remote API requires SSL verification
|
||||
verify_ssl: true
|
||||
|
||||
prometheus:
|
||||
disable: false
|
||||
http_listen: "0.0.0.0:9130"
|
||||
namespace: "unpoller"
|
||||
report_errors: false
|
||||
dead_ports: false
|
||||
|
||||
influxdb:
|
||||
disable: true
|
||||
|
||||
webserver:
|
||||
enable: false
|
||||
|
||||
datadog:
|
||||
enable: false
|
||||
|
||||
loki:
|
||||
disable: true
|
||||
7
go.mod
7
go.mod
@@ -12,7 +12,7 @@ require (
|
||||
github.com/prometheus/common v0.67.5
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/unpoller/unifi/v5 v5.5.0
|
||||
github.com/unpoller/unifi/v5 v5.6.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/term v0.39.0
|
||||
golift.io/cnfg v0.2.3
|
||||
@@ -23,7 +23,7 @@ require (
|
||||
|
||||
require (
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -46,6 +46,3 @@ require (
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
// for local iterative development only
|
||||
// replace github.com/unpoller/unifi/v5 => ../unifi
|
||||
|
||||
8
go.sum
8
go.sum
@@ -77,8 +77,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/unpoller/unifi/v5 v5.5.0 h1:MxSbCYBxgg1NN4R0w6h+ES2R4QNM1vhmwjQhJVwVDp0=
|
||||
github.com/unpoller/unifi/v5 v5.5.0/go.mod h1:pa6zv4Oyb1nFEm4qu/8CUv8Q25hQof04Wh2D0RXcTYc=
|
||||
github.com/unpoller/unifi/v5 v5.6.0 h1:qryEAJNYXG/WbjIqrIEZ/npRNBwHTsv/ZFbseYry8ug=
|
||||
github.com/unpoller/unifi/v5 v5.6.0/go.mod h1:vSIXIclPG9dpKxUp+pavfgENHWaTZXvDg7F036R1YCo=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
@@ -92,8 +92,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
||||
@@ -3,6 +3,7 @@ package inputunifi
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/unpoller/unifi/v5"
|
||||
@@ -83,6 +84,14 @@ func (u *InputUnifi) collectAnomalies(logs []any, sites []*unifi.Site, c *Contro
|
||||
}
|
||||
|
||||
for _, e := range events {
|
||||
// Apply site name override for anomalies if configured
|
||||
if c.DefaultSiteNameOverride != "" {
|
||||
lower := strings.ToLower(e.SiteName)
|
||||
if lower == "default" || strings.Contains(lower, "default") {
|
||||
e.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
}
|
||||
|
||||
logs = append(logs, e)
|
||||
|
||||
webserver.NewInputEvent(PluginName, s.ID+"_anomalies", &webserver.Event{
|
||||
|
||||
@@ -288,6 +288,12 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
client.ApName = devices[client.ApMac]
|
||||
client.GwName = devices[client.GwMac]
|
||||
client.RadioDescription = bssdIDs[client.Bssid] + client.RadioProto
|
||||
|
||||
// Apply site name override for clients if configured
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(client.SiteName) {
|
||||
client.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
m.Clients = append(m.Clients, client)
|
||||
}
|
||||
|
||||
@@ -300,6 +306,12 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
|
||||
client.Name = RedactNamePII(client.Name, c.HashPII, c.DropPII)
|
||||
client.MAC = RedactMacPII(client.MAC, c.HashPII, c.DropPII)
|
||||
|
||||
// Apply site name override for DPI clients if configured
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(client.SiteName) {
|
||||
client.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
m.ClientsDPI = append(m.ClientsDPI, client)
|
||||
}
|
||||
|
||||
@@ -310,25 +322,141 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
|
||||
if *c.SaveSites {
|
||||
for _, site := range metrics.Sites {
|
||||
// Apply site name override for sites if configured
|
||||
if c.DefaultSiteNameOverride != "" {
|
||||
if isDefaultSiteName(site.Name) {
|
||||
site.Name = c.DefaultSiteNameOverride
|
||||
}
|
||||
if isDefaultSiteName(site.SiteName) {
|
||||
site.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
}
|
||||
m.Sites = append(m.Sites, site)
|
||||
}
|
||||
|
||||
for _, site := range metrics.SitesDPI {
|
||||
// Apply site name override for DPI sites if configured
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(site.SiteName) {
|
||||
site.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
m.SitesDPI = append(m.SitesDPI, site)
|
||||
}
|
||||
}
|
||||
|
||||
for _, speedTest := range metrics.SpeedTests {
|
||||
// Apply site name override for speed tests if configured
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(speedTest.SiteName) {
|
||||
speedTest.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
m.SpeedTests = append(m.SpeedTests, speedTest)
|
||||
}
|
||||
|
||||
for _, traffic := range metrics.CountryTraffic {
|
||||
// Apply site name override for country traffic if configured
|
||||
// UsageByCountry has TrafficSite.SiteName, not SiteName directly
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(traffic.TrafficSite.SiteName) {
|
||||
traffic.TrafficSite.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
m.CountryTraffic = append(m.CountryTraffic, traffic)
|
||||
}
|
||||
|
||||
// Apply default_site_name_override to all metrics if configured.
|
||||
// This must be done AFTER all metrics are added to m, so everything is included.
|
||||
// This allows us to use the console name for Cloud Gateways while keeping
|
||||
// the actual site name ("default") for API calls.
|
||||
if c.DefaultSiteNameOverride != "" {
|
||||
applySiteNameOverride(m, c.DefaultSiteNameOverride)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// isDefaultSiteName checks if a site name represents a "default" site.
|
||||
// This handles variations like "default", "Default", "Default (default)", etc.
|
||||
func isDefaultSiteName(siteName string) bool {
|
||||
if siteName == "" {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(siteName)
|
||||
// Check for exact match or if it contains "default" as a word
|
||||
return lower == "default" || strings.Contains(lower, "default")
|
||||
}
|
||||
|
||||
// applySiteNameOverride replaces "default" site names with the override name
|
||||
// in all devices, clients, and sites. This allows us to use console names
|
||||
// for Cloud Gateways in metrics while keeping "default" for API calls.
|
||||
// This makes metrics more compatible with existing dashboards that expect
|
||||
// meaningful site names instead of "Default" or "Default (default)".
|
||||
func applySiteNameOverride(m *poller.Metrics, overrideName string) {
|
||||
// Apply to all devices - use type switch for known device types
|
||||
for i := range m.Devices {
|
||||
switch d := m.Devices[i].(type) {
|
||||
case *unifi.UAP:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.USG:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.USW:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.UDM:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.UXG:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.UBB:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.UCI:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
case *unifi.PDU:
|
||||
if isDefaultSiteName(d.SiteName) {
|
||||
d.SiteName = overrideName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to all clients
|
||||
for i := range m.Clients {
|
||||
if client, ok := m.Clients[i].(*unifi.Client); ok {
|
||||
if isDefaultSiteName(client.SiteName) {
|
||||
client.SiteName = overrideName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to sites - check both Name and SiteName fields
|
||||
for i := range m.Sites {
|
||||
if site, ok := m.Sites[i].(*unifi.Site); ok {
|
||||
if isDefaultSiteName(site.Name) {
|
||||
site.Name = overrideName
|
||||
}
|
||||
if isDefaultSiteName(site.SiteName) {
|
||||
site.SiteName = overrideName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to rogue APs
|
||||
for i := range m.RogueAPs {
|
||||
if ap, ok := m.RogueAPs[i].(*unifi.RogueAP); ok {
|
||||
if isDefaultSiteName(ap.SiteName) {
|
||||
ap.SiteName = overrideName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is a helper function for augmentMetrics.
|
||||
func extractDevices(metrics *Metrics) (*poller.Metrics, map[string]string, map[string]string) {
|
||||
m := &poller.Metrics{TS: metrics.TS}
|
||||
@@ -442,10 +570,9 @@ func (u *InputUnifi) getFilteredSites(c *Controller) ([]*unifi.Site, error) {
|
||||
return nil, fmt.Errorf("controller: %w", err)
|
||||
}
|
||||
|
||||
// Apply the default_site_name_override to the first site in the list, if configured.
|
||||
if len(sites) > 0 && c.DefaultSiteNameOverride != "" {
|
||||
sites[0].Name = c.DefaultSiteNameOverride
|
||||
}
|
||||
// Note: We do NOT override the site name here because it's used in API calls.
|
||||
// The API expects the actual site name (e.g., "default"), not the override.
|
||||
// The override will be applied later when augmenting metrics for display purposes.
|
||||
|
||||
if len(c.Sites) == 0 || StringInSlice("all", c.Sites) {
|
||||
return sites, nil
|
||||
|
||||
@@ -58,6 +58,8 @@ type Controller struct {
|
||||
URL string `json:"url" toml:"url" xml:"url" yaml:"url"`
|
||||
Sites []string `json:"sites" toml:"sites" xml:"site" yaml:"sites"`
|
||||
DefaultSiteNameOverride string `json:"default_site_name_override" toml:"default_site_name_override" xml:"default_site_name_override" yaml:"default_site_name_override"`
|
||||
Remote bool `json:"remote" toml:"remote" xml:"remote,attr" yaml:"remote"`
|
||||
ConsoleID string `json:"console_id,omitempty" toml:"console_id,omitempty" xml:"console_id,omitempty" yaml:"console_id,omitempty"`
|
||||
Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"`
|
||||
ID string `json:"id,omitempty"` // this is an output, not an input.
|
||||
}
|
||||
@@ -68,6 +70,8 @@ type Config struct {
|
||||
Default Controller `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"`
|
||||
Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"`
|
||||
Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"`
|
||||
Remote bool `json:"remote" toml:"remote" xml:"remote,attr" yaml:"remote"`
|
||||
RemoteAPIKey string `json:"remote_api_key" toml:"remote_api_key" xml:"remote_api_key" yaml:"remote_api_key"`
|
||||
Controllers []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"`
|
||||
}
|
||||
|
||||
@@ -189,7 +193,8 @@ func (u *InputUnifi) checkSites(c *Controller) error {
|
||||
FIRST:
|
||||
for _, s := range c.Sites {
|
||||
for _, site := range sites {
|
||||
if s == site.Name {
|
||||
// Case-insensitive comparison for site names
|
||||
if strings.EqualFold(s, site.Name) {
|
||||
keep = append(keep, s)
|
||||
|
||||
continue FIRST
|
||||
@@ -281,7 +286,9 @@ func (u *InputUnifi) setDefaults(c *Controller) { //nolint:cyclop
|
||||
c.SaveTraffic = &f
|
||||
}
|
||||
|
||||
if c.URL == "" {
|
||||
if c.URL == "" && !c.Remote {
|
||||
// Remote mode: URL will be set during discovery
|
||||
// Don't set a default here for remote mode
|
||||
c.URL = defaultURL
|
||||
}
|
||||
|
||||
@@ -293,6 +300,14 @@ func (u *InputUnifi) setDefaults(c *Controller) { //nolint:cyclop
|
||||
c.APIKey = u.getPassFromFile(strings.TrimPrefix(c.APIKey, "file://"))
|
||||
}
|
||||
|
||||
if c.Remote {
|
||||
// Remote mode: only API key is used, no user/pass
|
||||
// For remote mode, API key is required
|
||||
// Will be set from RemoteAPIKey in Config if not provided
|
||||
c.User = ""
|
||||
c.Pass = ""
|
||||
} else {
|
||||
// Local mode: use API key if provided, otherwise user/pass
|
||||
if c.APIKey == "" {
|
||||
if c.Pass == "" {
|
||||
c.Pass = defaultPass
|
||||
@@ -306,6 +321,7 @@ func (u *InputUnifi) setDefaults(c *Controller) { //nolint:cyclop
|
||||
c.User = ""
|
||||
c.Pass = ""
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Sites) == 0 {
|
||||
c.Sites = []string{defaultSite}
|
||||
@@ -380,8 +396,13 @@ func (u *InputUnifi) setControllerDefaults(c *Controller) *Controller { //nolint
|
||||
c.SaveAnomal = u.Default.SaveAnomal
|
||||
}
|
||||
|
||||
if c.URL == "" {
|
||||
if c.URL == "" && !c.Remote {
|
||||
// Remote mode: URL will be set during discovery
|
||||
// Don't set a default here for remote mode
|
||||
c.URL = u.Default.URL
|
||||
if c.URL == "" {
|
||||
c.URL = defaultURL
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.Pass, "file://") {
|
||||
@@ -392,19 +413,35 @@ func (u *InputUnifi) setControllerDefaults(c *Controller) *Controller { //nolint
|
||||
c.APIKey = u.getPassFromFile(strings.TrimPrefix(c.APIKey, "file://"))
|
||||
}
|
||||
|
||||
if c.Remote {
|
||||
// Remote mode: only API key is used
|
||||
if c.APIKey == "" {
|
||||
c.APIKey = u.Default.APIKey
|
||||
}
|
||||
c.User = ""
|
||||
c.Pass = ""
|
||||
} else {
|
||||
// Local mode: use API key if provided, otherwise user/pass
|
||||
if c.APIKey == "" {
|
||||
if c.Pass == "" {
|
||||
c.Pass = u.Default.Pass
|
||||
if c.Pass == "" {
|
||||
c.Pass = defaultPass
|
||||
}
|
||||
}
|
||||
|
||||
if c.User == "" {
|
||||
c.User = u.Default.User
|
||||
if c.User == "" {
|
||||
c.User = defaultUser
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// clear out user/pass combo, only use API-key
|
||||
c.User = ""
|
||||
c.Pass = ""
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Sites) == 0 {
|
||||
c.Sites = u.Default.Sites
|
||||
|
||||
@@ -30,10 +30,49 @@ func (u *InputUnifi) Initialize(l poller.Logger) error {
|
||||
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)
|
||||
@@ -114,6 +153,15 @@ func (u *InputUnifi) DebugInput() (bool, error) {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -124,7 +172,12 @@ func (u *InputUnifi) logController(c *Controller) {
|
||||
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)
|
||||
|
||||
161
pkg/inputunifi/remote.go
Normal file
161
pkg/inputunifi/remote.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package inputunifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/unpoller/unifi/v5"
|
||||
)
|
||||
|
||||
// discoverRemoteControllers discovers all controllers via remote API and creates Controller entries.
|
||||
func (u *InputUnifi) discoverRemoteControllers(apiKey string) ([]*Controller, error) {
|
||||
// Handle file:// prefix for API key
|
||||
if strings.HasPrefix(apiKey, "file://") {
|
||||
apiKey = u.getPassFromFile(strings.TrimPrefix(apiKey, "file://"))
|
||||
}
|
||||
|
||||
if apiKey == "" {
|
||||
return nil, fmt.Errorf("remote API key not provided")
|
||||
}
|
||||
|
||||
// Use library client
|
||||
client := unifi.NewRemoteAPIClient(apiKey, u.LogErrorf, u.LogDebugf, u.Logf)
|
||||
|
||||
u.Logf("Discovering remote UniFi consoles...")
|
||||
|
||||
consoles, err := client.DiscoverConsoles()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("discovering consoles: %w", err)
|
||||
}
|
||||
|
||||
if len(consoles) == 0 {
|
||||
u.Logf("No consoles found via remote API")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
u.Logf("Found %d console(s) via remote API", len(consoles))
|
||||
|
||||
var controllers []*Controller
|
||||
|
||||
for _, console := range consoles {
|
||||
consoleName := console.ConsoleName
|
||||
if consoleName == "" {
|
||||
consoleName = console.ReportedState.Name
|
||||
}
|
||||
if consoleName == "" {
|
||||
consoleName = console.ReportedState.Hostname
|
||||
}
|
||||
u.LogDebugf("Discovering sites for console: %s (%s)", console.ID, consoleName)
|
||||
|
||||
sites, err := client.DiscoverSites(console.ID)
|
||||
if err != nil {
|
||||
u.LogErrorf("Failed to discover sites for console %s: %v", console.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(sites) == 0 {
|
||||
u.LogDebugf("No sites found for console %s", console.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a controller entry for this console
|
||||
// For remote API, the base URL should point to the connector endpoint
|
||||
// The unifi library will append /proxy/network/... paths, so we need to account for that
|
||||
// For remote API with integration endpoints, we set it to the connector base
|
||||
controller := &Controller{
|
||||
Remote: true,
|
||||
ConsoleID: console.ID,
|
||||
APIKey: apiKey,
|
||||
// Set URL to connector base - the library appends /proxy/network/status
|
||||
// But for integration API we need /proxy/network/integration/v1/...
|
||||
// This may require library updates, but try connector base first
|
||||
URL: fmt.Sprintf("%s/v1/connector/consoles/%s", unifi.RemoteAPIBaseURL, console.ID),
|
||||
}
|
||||
|
||||
// Ensure defaults are set before calling setControllerDefaults
|
||||
u.setDefaults(&u.Default)
|
||||
|
||||
// Copy defaults
|
||||
controller = u.setControllerDefaults(controller)
|
||||
|
||||
// Set remote-specific defaults and ensure all boolean pointers are initialized
|
||||
t := true
|
||||
f := false
|
||||
if controller.VerifySSL == nil {
|
||||
controller.VerifySSL = &t // Remote API should verify SSL
|
||||
}
|
||||
// Ensure all boolean pointers are set (safety check)
|
||||
if controller.HashPII == nil {
|
||||
controller.HashPII = &f
|
||||
}
|
||||
if controller.DropPII == nil {
|
||||
controller.DropPII = &f
|
||||
}
|
||||
if controller.SaveSites == nil {
|
||||
controller.SaveSites = &t
|
||||
}
|
||||
if controller.SaveDPI == nil {
|
||||
controller.SaveDPI = &f
|
||||
}
|
||||
if controller.SaveEvents == nil {
|
||||
controller.SaveEvents = &f
|
||||
}
|
||||
if controller.SaveAlarms == nil {
|
||||
controller.SaveAlarms = &f
|
||||
}
|
||||
if controller.SaveAnomal == nil {
|
||||
controller.SaveAnomal = &f
|
||||
}
|
||||
if controller.SaveIDs == nil {
|
||||
controller.SaveIDs = &f
|
||||
}
|
||||
if controller.SaveTraffic == nil {
|
||||
controller.SaveTraffic = &f
|
||||
}
|
||||
if controller.SaveRogue == nil {
|
||||
controller.SaveRogue = &f
|
||||
}
|
||||
if controller.SaveSyslog == nil {
|
||||
controller.SaveSyslog = &f
|
||||
}
|
||||
if controller.SaveProtectLogs == nil {
|
||||
controller.SaveProtectLogs = &f
|
||||
}
|
||||
if controller.ProtectThumbnails == nil {
|
||||
controller.ProtectThumbnails = &f
|
||||
}
|
||||
|
||||
// Extract site names
|
||||
siteNames := make([]string, 0, len(sites))
|
||||
for _, site := range sites {
|
||||
if site.Name != "" {
|
||||
siteNames = append(siteNames, site.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// For Cloud Gateways, if the only site is "default", use the console name from hosts response
|
||||
// as the default site name override. The console name is in reportedState.name
|
||||
// (consoleName was already set above in the loop)
|
||||
|
||||
// If we only have one site and it's "default" (case-insensitive), use the console name as override
|
||||
// Note: We keep the actual site name ("default") for API calls, but set the override
|
||||
// for display/metric naming purposes.
|
||||
if len(siteNames) == 1 && strings.EqualFold(siteNames[0], "default") && consoleName != "" {
|
||||
controller.DefaultSiteNameOverride = consoleName
|
||||
// Keep the actual site name for API calls
|
||||
controller.Sites = siteNames
|
||||
u.LogDebugf("Using console name '%s' as default site name override for Cloud Gateway (API will use 'default')", consoleName)
|
||||
} else if len(siteNames) > 0 {
|
||||
controller.Sites = siteNames
|
||||
} else {
|
||||
controller.Sites = []string{"all"}
|
||||
}
|
||||
|
||||
controller.ID = console.ID
|
||||
controllers = append(controllers, controller)
|
||||
|
||||
u.Logf("Discovered console %s with %d site(s): %v", consoleName, len(sites), siteNames)
|
||||
}
|
||||
|
||||
return controllers, nil
|
||||
}
|
||||
Reference in New Issue
Block a user