Files
unpoller-unpoller-4/pkg/inputunifi/collector.go
Cody Lee 18c6e66a8e feat: add Site Magic site-to-site VPN metrics (closes #926) (#983)
* feat: add Site Magic site-to-site VPN metrics (closes #926)

Bump github.com/unpoller/unifi/v5 to v5.25.0 which adds:
- GetMagicSiteToSiteVPN / GetMagicSiteToSiteVPNSite API methods
- MagicSiteToSiteVPN types with mesh, connection, device, and status structs
- Missing VPN health fields on Site.Health (SiteToSiteNumActive/Inactive,
  SiteToSiteRxBytes/TxBytes/RxPackets/TxPackets)

Implement VPN metrics collection across all output plugins:
- Collect Site Magic VPN mesh data per-site in inputunifi pollController
- Propagate VPNMeshes through poller.Metrics / AppendMetrics
- Apply DefaultSiteNameOverride for VPN meshes in augmentMetrics /
  applySiteNameOverride
- influxunifi: vpn_mesh, vpn_mesh_connection, vpn_mesh_status tables
- promunifi: vpn_mesh_*, vpn_tunnel_*, vpn_mesh_status_* gauges
- datadogunifi: unifi.vpn_mesh.*, unifi.vpn_tunnel.*, unifi.vpn_mesh_status.*

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* feat(otelunifi): add Site Magic VPN metrics to OpenTelemetry output

Adds exportVPNMeshes to the otel output plugin, emitting the same
unifi_vpn_mesh_*, unifi_vpn_tunnel_*, and unifi_vpn_mesh_status_*
gauges as the other output plugins.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 21:08:09 -05:00

819 lines
24 KiB
Go

package inputunifi
// nolint: gosec
import (
"crypto/md5"
"fmt"
"strings"
"time"
"github.com/unpoller/unifi/v5"
"github.com/unpoller/unpoller/pkg/poller"
)
const (
historySeconds = 86400
pollDuration = time.Second * historySeconds
)
var ErrScrapeFilterMatchFailed = fmt.Errorf("scrape filter match failed, and filter is not http URL")
func (u *InputUnifi) isNill(c *Controller) bool {
u.RLock()
defer u.RUnlock()
return c.Unifi == nil
}
// newDynamicCntrlr creates and saves a controller definition for further use.
// This is called when an unconfigured controller is requested.
func (u *InputUnifi) newDynamicCntrlr(url string) (bool, *Controller) {
u.Lock()
defer u.Unlock()
if c := u.dynamic[url]; c != nil {
// it already exists.
return false, c
}
ccopy := u.Default // copy defaults into new controller
u.dynamic[url] = &ccopy
u.dynamic[url].URL = url
return true, u.dynamic[url]
}
func (u *InputUnifi) dynamicController(filter *poller.Filter) (*poller.Metrics, error) {
if !strings.HasPrefix(filter.Path, "http") {
return nil, ErrScrapeFilterMatchFailed
}
newCntrlr, c := u.newDynamicCntrlr(filter.Path)
if newCntrlr {
u.Logf("Authenticating to Dynamic UniFi Controller: %s", filter.Path)
if err := u.getUnifi(c); err != nil {
u.logController(c)
return nil, fmt.Errorf("authenticating to %s: %w", filter.Path, err)
}
u.logController(c)
}
return u.collectController(c)
}
func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) {
u.LogDebugf("Collecting controller data: %s (%s)", c.URL, c.ID)
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)
}
}
metrics, err := u.pollController(c)
if err != nil {
u.Logf("Re-authenticating to UniFi Controller: %s", c.URL)
if authErr := u.getUnifi(c); authErr != nil {
return metrics, fmt.Errorf("re-authenticating to %s: %w", c.URL, authErr)
}
// Brief delay to allow controller to process new authentication
time.Sleep(500 * time.Millisecond)
// Retry the poll after successful re-authentication
u.LogDebugf("Retrying poll after re-authentication: %s", c.URL)
metrics, err = u.pollController(c)
}
return metrics, err
}
//nolint:cyclop
func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
u.RLock()
defer u.RUnlock()
if c == nil {
return nil, fmt.Errorf("controller is nil")
}
if c.Unifi == nil {
return nil, fmt.Errorf("controller client is nil (e.g. after 429 or auth failure): %s", c.URL)
}
u.LogDebugf("Polling controller: %s (%s)", c.URL, c.ID)
// Get the sites we care about.
sites, err := u.getFilteredSites(c)
if err != nil {
return nil, fmt.Errorf("unifi.GetSites(): %w", err)
}
m := &Metrics{TS: time.Now(), Sites: sites}
// FIXME needs to be last poll time maybe
st := m.TS.Add(-1 * pollDuration)
tp := unifi.EpochMillisTimePeriod{StartEpochMillis: st.UnixMilli(), EndEpochMillis: m.TS.UnixMilli()}
if c.SaveRogue != nil && *c.SaveRogue {
if m.RogueAPs, err = c.Unifi.GetRogueAPs(sites); err != nil {
return nil, fmt.Errorf("unifi.GetRogueAPs(%s): %w", c.URL, err)
}
u.LogDebugf("Found %d RogueAPs entries", len(m.RogueAPs))
}
if c.SaveDPI != nil && *c.SaveDPI {
if m.SitesDPI, err = c.Unifi.GetSiteDPI(sites); err != nil {
return nil, fmt.Errorf("unifi.GetSiteDPI(%s): %w", c.URL, err)
}
u.LogDebugf("Found %d SitesDPI entries", len(m.SitesDPI))
if m.ClientsDPI, err = c.Unifi.GetClientsDPI(sites); err != nil {
return nil, fmt.Errorf("unifi.GetClientsDPI(%s): %w", c.URL, err)
}
u.LogDebugf("Found %d ClientsDPI entries", len(m.ClientsDPI))
}
if c.SaveTraffic != nil && *c.SaveTraffic {
if m.CountryTraffic, err = c.Unifi.GetCountryTraffic(sites, &tp); err != nil {
return nil, fmt.Errorf("unifi.GetCountryTraffic(%s): %w", c.URL, err)
}
u.LogDebugf("Found %d CountryTraffic entries", len(m.CountryTraffic))
}
if c.SaveTraffic != nil && *c.SaveTraffic && c.SaveDPI != nil && *c.SaveDPI {
clientUsageByApp, err := c.Unifi.GetClientTraffic(sites, &tp, true)
if err != nil {
return nil, fmt.Errorf("unifi.GetClientTraffic(%s): %w", c.URL, err)
}
u.LogDebugf("Found %d ClientUsageByApp entries", len(clientUsageByApp))
b4 := len(m.ClientsDPI)
u.convertToClientDPI(clientUsageByApp, m)
u.LogDebugf("Added %d ClientDPI entries for a total of %d", len(m.ClientsDPI)-b4, len(m.ClientsDPI))
}
// Get all the points.
if m.Clients, err = c.Unifi.GetClients(sites); err != nil {
return nil, fmt.Errorf("unifi.GetClients(%s): %w", c.URL, err)
}
u.LogDebugf("Found %d Clients entries", len(m.Clients))
if m.Devices, err = c.Unifi.GetDevices(sites); err != nil {
return nil, fmt.Errorf("unifi.GetDevices(%s): %w", c.URL, err)
}
u.LogDebugf("Found %d UBB, %d UXG, %d PDU, %d UCI, %d UDB, %d UAP %d USG %d USW %d UDM devices",
len(m.Devices.UBBs), len(m.Devices.UXGs),
len(m.Devices.PDUs), len(m.Devices.UCIs),
len(m.Devices.UDBs), len(m.Devices.UAPs), len(m.Devices.USGs),
len(m.Devices.USWs), len(m.Devices.UDMs))
// Get speed test results for all WANs
if m.SpeedTests, err = c.Unifi.GetSpeedTests(sites, historySeconds); err != nil {
// Don't fail collection if speed tests fail - older controllers may not have this endpoint
u.LogDebugf("unifi.GetSpeedTests(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d SpeedTests entries", len(m.SpeedTests))
}
// Get DHCP leases with associations.
// Wrapped in recover so a nil-pointer panic in the library (e.g. when a 401 causes nil devices)
// never crashes the poller. See https://github.com/unpoller/unpoller/issues/965
func() {
defer func() {
if r := recover(); r != nil {
u.LogErrorf("GetActiveDHCPLeasesWithAssociations panic recovered (see issue #965): %v", r)
}
}()
if m.DHCPLeases, err = c.Unifi.GetActiveDHCPLeasesWithAssociations(sites); err != nil {
// Don't fail collection if DHCP leases fail - older controllers may not have this endpoint
u.LogDebugf("unifi.GetActiveDHCPLeasesWithAssociations(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d DHCPLeases entries", len(m.DHCPLeases))
}
}()
// Get WAN enriched configuration
if m.WANConfigs, err = c.Unifi.GetWANEnrichedConfiguration(sites); err != nil {
// Don't fail collection if WAN config fails - older controllers may not have this endpoint
u.LogDebugf("unifi.GetWANEnrichedConfiguration(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d WAN configuration entries", len(m.WANConfigs))
}
// Get firewall policies
if m.FirewallPolicies, err = c.Unifi.GetFirewallPolicies(sites); err != nil {
// Don't fail collection if firewall policies fail - older controllers may not have this endpoint
u.LogDebugf("unifi.GetFirewallPolicies(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d FirewallPolicies entries", len(m.FirewallPolicies))
}
// Get controller system info (UniFi OS only)
if m.Sysinfos, err = c.Unifi.GetSysinfo(sites); err != nil {
// Don't fail collection if sysinfo fails - older controllers may not have this endpoint
u.LogDebugf("unifi.GetSysinfo(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d Sysinfo entries", len(m.Sysinfos))
}
// Get network topology
if m.Topologies, err = c.Unifi.GetTopology(sites); err != nil {
// Don't fail collection if topology fails - older controllers may not have this endpoint
u.LogDebugf("unifi.GetTopology(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d Topology entries", len(m.Topologies))
}
// Get port anomalies
if m.PortAnomalies, err = c.Unifi.GetPortAnomalies(sites); err != nil {
// Don't fail collection if port anomalies fail - older controllers may not have this endpoint
u.LogDebugf("unifi.GetPortAnomalies(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d PortAnomalies entries", len(m.PortAnomalies))
}
// Get Site Magic site-to-site VPN mesh data
if m.VPNMeshes, err = c.Unifi.GetMagicSiteToSiteVPN(sites); err != nil {
// Don't fail collection if VPN data fails - older controllers may not have this endpoint
u.LogDebugf("unifi.GetMagicSiteToSiteVPN(%s): %v (continuing)", c.URL, err)
} else {
u.LogDebugf("Found %d VPNMeshes entries", len(m.VPNMeshes))
}
// Update web UI only on success; call explicitly so we never run with nil c/c.Unifi (no defer).
// Recover so a panic in updateWeb (e.g. old image, race) never kills the poller.
if c != nil && c.Unifi != nil {
func() {
defer func() {
if r := recover(); r != nil {
u.LogErrorf("updateWeb panic recovered (upgrade image if this persists): %v", r)
}
}()
updateWeb(c, m)
}()
}
return u.augmentMetrics(c, m), nil
}
// FIXME this would be better implemented on FlexInt itself
func (u *InputUnifi) intToFlexInt(i int) unifi.FlexInt {
return unifi.FlexInt{
Val: float64(i),
Txt: fmt.Sprintf("%d", i),
}
}
// FIXME this would be better implemented on FlexInt itself
func (u *InputUnifi) int64ToFlexInt(i int64) unifi.FlexInt {
return unifi.FlexInt{
Val: float64(i),
Txt: fmt.Sprintf("%d", i),
}
}
func (u *InputUnifi) convertToClientDPI(clientUsageByApp []*unifi.ClientUsageByApp, metrics *Metrics) {
for _, client := range clientUsageByApp {
byApp := make([]unifi.DPIData, 0)
byCat := make([]unifi.DPIData, 0)
type catCount struct {
BytesReceived int64
BytesTransmitted int64
}
byCatMap := make(map[int]catCount)
dpiClients := make([]*unifi.DPIClient, 0)
// TODO create cat table
for _, app := range client.UsageByApp {
dpiData := unifi.DPIData{
App: u.intToFlexInt(app.Application),
Cat: u.intToFlexInt(app.Category),
Clients: dpiClients,
KnownClients: u.intToFlexInt(0),
RxBytes: u.int64ToFlexInt(app.BytesReceived),
RxPackets: u.int64ToFlexInt(0), // We don't have packets from Unifi Controller
TxBytes: u.int64ToFlexInt(app.BytesTransmitted),
TxPackets: u.int64ToFlexInt(0), // We don't have packets from Unifi Controller
}
cat, ok := byCatMap[app.Category]
if ok {
cat.BytesReceived += app.BytesReceived
cat.BytesTransmitted += app.BytesTransmitted
} else {
cat = catCount{
BytesReceived: app.BytesReceived,
BytesTransmitted: app.BytesTransmitted,
}
byCatMap[app.Category] = cat
}
byApp = append(byApp, dpiData)
}
if len(byApp) <= 1 {
byCat = byApp
} else {
for category, cat := range byCatMap {
dpiData := unifi.DPIData{
App: u.intToFlexInt(16777215), // Unknown
Cat: u.intToFlexInt(category),
Clients: dpiClients,
KnownClients: u.intToFlexInt(0),
RxBytes: u.int64ToFlexInt(cat.BytesReceived),
RxPackets: u.int64ToFlexInt(0), // We don't have packets from Unifi Controller
TxBytes: u.int64ToFlexInt(cat.BytesTransmitted),
TxPackets: u.int64ToFlexInt(0), // We don't have packets from Unifi Controller
}
byCat = append(byCat, dpiData)
}
}
dpiTable := unifi.DPITable{
ByApp: byApp,
ByCat: byCat,
MAC: client.Client.Mac,
Name: client.Client.Name,
SiteName: client.TrafficSite.SiteName,
SourceName: client.TrafficSite.SourceName,
}
metrics.ClientsDPI = append(metrics.ClientsDPI, &dpiTable)
}
}
// 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 method currently adds parent device names to client metrics and hashes PII.
// This method also converts our local *Metrics type into a slice of interfaces for poller.
func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Metrics {
if metrics == nil {
return nil
}
m, devices, bssdIDs := extractDevices(metrics)
// These come blank, so set them here.
for _, client := range metrics.Clients {
if devices[client.Mac] = client.Name; client.Name == "" {
devices[client.Mac] = client.Hostname
}
client.Mac = RedactMacPII(client.Mac, c.HashPII, c.DropPII)
client.Name = RedactNamePII(client.Name, c.HashPII, c.DropPII)
client.Hostname = RedactNamePII(client.Hostname, c.HashPII, c.DropPII)
client.SwName = devices[client.SwMac]
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)
}
for _, client := range metrics.ClientsDPI {
// Name on Client DPI data also comes blank, find it based on MAC address.
client.Name = devices[client.MAC]
if client.Name == "" {
client.Name = client.MAC
}
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)
}
for _, ap := range metrics.RogueAPs {
// XXX: do we need augment this data?
m.RogueAPs = append(m.RogueAPs, ap)
}
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)
}
for _, lease := range metrics.DHCPLeases {
// Apply site name override for DHCP leases if configured
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(lease.SiteName) {
lease.SiteName = c.DefaultSiteNameOverride
}
m.DHCPLeases = append(m.DHCPLeases, lease)
}
for _, wanConfig := range metrics.WANConfigs {
// WANEnrichedConfiguration doesn't have a SiteName field by default
// The site context is preserved via the collector's site list
m.WANConfigs = append(m.WANConfigs, wanConfig)
}
for _, sysinfo := range metrics.Sysinfos {
m.Sysinfos = append(m.Sysinfos, sysinfo)
}
for _, policy := range metrics.FirewallPolicies {
// Apply site name override for firewall policies if configured
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(policy.SiteName) {
policy.SiteName = c.DefaultSiteNameOverride
}
m.FirewallPolicies = append(m.FirewallPolicies, policy)
}
for _, topo := range metrics.Topologies {
// Apply site name override for topology if configured
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(topo.SiteName) {
topo.SiteName = c.DefaultSiteNameOverride
}
m.Topologies = append(m.Topologies, topo)
}
for _, anomaly := range metrics.PortAnomalies {
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(anomaly.SiteName) {
anomaly.SiteName = c.DefaultSiteNameOverride
}
m.PortAnomalies = append(m.PortAnomalies, anomaly)
}
for _, mesh := range metrics.VPNMeshes {
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(mesh.SiteName) {
mesh.SiteName = c.DefaultSiteNameOverride
}
m.VPNMeshes = append(m.VPNMeshes, mesh)
}
// 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.UDB:
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
}
}
}
// Apply to DHCP leases
for i := range m.DHCPLeases {
if lease, ok := m.DHCPLeases[i].(*unifi.DHCPLease); ok {
if isDefaultSiteName(lease.SiteName) {
lease.SiteName = overrideName
}
}
}
// Apply to sysinfo (controller metrics)
for i := range m.Sysinfos {
if s, ok := m.Sysinfos[i].(*unifi.Sysinfo); ok {
if isDefaultSiteName(s.SiteName) {
s.SiteName = overrideName
}
}
}
// Apply to WAN configs
for i := range m.WANConfigs {
if wanConfig, ok := m.WANConfigs[i].(*unifi.WANEnrichedConfiguration); ok {
// WAN configs don't have SiteName field, but we'll add it in the exporter
_ = wanConfig
}
}
// Apply to firewall policies
for i := range m.FirewallPolicies {
if policy, ok := m.FirewallPolicies[i].(*unifi.FirewallPolicy); ok {
if isDefaultSiteName(policy.SiteName) {
policy.SiteName = overrideName
}
}
}
for i := range m.Topologies {
if topo, ok := m.Topologies[i].(*unifi.Topology); ok {
if isDefaultSiteName(topo.SiteName) {
topo.SiteName = overrideName
}
}
}
for i := range m.PortAnomalies {
if anomaly, ok := m.PortAnomalies[i].(*unifi.PortAnomaly); ok {
if isDefaultSiteName(anomaly.SiteName) {
anomaly.SiteName = overrideName
}
}
}
for i := range m.VPNMeshes {
if mesh, ok := m.VPNMeshes[i].(*unifi.MagicSiteToSiteVPN); ok {
if isDefaultSiteName(mesh.SiteName) {
mesh.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}
devices := make(map[string]string)
bssdIDs := make(map[string]string)
for _, r := range metrics.Devices.UAPs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
for _, v := range r.VapTable {
bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName)
}
}
for _, r := range metrics.Devices.USGs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
}
for _, r := range metrics.Devices.USWs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
}
for _, r := range metrics.Devices.UDMs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
}
for _, r := range metrics.Devices.UXGs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
}
for _, r := range metrics.Devices.UBBs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
}
for _, r := range metrics.Devices.UCIs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
}
for _, r := range metrics.Devices.UDBs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
for _, v := range r.VapTable {
bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName)
}
}
for _, r := range metrics.Devices.PDUs {
devices[r.Mac] = r.Name
m.Devices = append(m.Devices, r)
}
return m, devices, bssdIDs
}
// RedactNamePII converts a name string to an md5 hash (first 24 chars only).
// Useful for maskiing out personally identifying information.
func RedactNamePII(pii string, hash *bool, dropPII *bool) string {
if dropPII != nil && *dropPII {
return ""
}
if hash == nil || !*hash || pii == "" {
return pii
}
s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
// instead of 32 characters, only use 24.
return s[:24]
}
// RedactMacPII converts a MAC address to an md5 hashed version (first 14 chars only).
// Useful for maskiing out personally identifying information.
func RedactMacPII(pii string, hash *bool, dropPII *bool) (output string) {
if dropPII != nil && *dropPII {
return ""
}
if hash == nil || !*hash || pii == "" {
return pii
}
s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
// This formats a "fake" mac address looking string.
return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", s[:2], s[2:4], s[4:6], s[6:8], s[8:10], s[10:12], s[12:14])
}
// RedactIPPII converts an IP address to an md5 hashed version (first 12 chars only).
// Useful for maskiing out personally identifying information.
func RedactIPPII(pii string, hash *bool, dropPII *bool) string {
if dropPII != nil && *dropPII {
return ""
}
if hash == nil || !*hash || pii == "" {
return pii
}
s := fmt.Sprintf("%x", md5.Sum([]byte(pii))) // nolint: gosec
// Format as a "fake" IP-like string.
return fmt.Sprintf("%s.%s.%s", s[:4], s[4:8], s[8:12])
}
// 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.Site, error) {
u.RLock()
defer u.RUnlock()
sites, err := c.Unifi.GetSites()
if err != nil {
return nil, fmt.Errorf("controller: %w", err)
}
// 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
}
i := 0
for _, s := range sites {
// Only include valid sites in the request filter.
if StringInSlice(s.Name, c.Sites) {
sites[i] = s
i++
}
}
return sites[:i], nil
}