# Architecture Module: `forge.lthn.ai/core/go-netops` ## Overview go-netops is a thin Go wrapper around the [unpoller/unifi](https://github.com/unpoller/unifi) SDK that provides a simplified interface for querying UniFi network controllers. It handles authentication, TLS configuration, multi-site filtering, and config resolution from file, environment, and CLI flags. ## Package Structure ``` go-netops/ ├── unifi/ │ ├── client.go # Client struct and constructor (New) │ ├── clients.go # Connected client queries with filtering │ ├── config.go # Config resolution + persistence (NewFromConfig, ResolveConfig, SaveConfig) │ ├── devices.go # Infrastructure device queries (UAP, USW, USG, UDM, UXG) │ ├── networks.go # Network configuration queries (VLANs, DHCP, firewall zones) │ ├── routes.go # Gateway routing table queries │ ├── sites.go # Site listing │ ├── client_test.go # Client creation tests │ └── config_test.go # Config resolution and persistence tests ├── go.mod └── go.sum ``` All exported types live in the `unifi` package under the `unifi/` subdirectory. ## Key Types ### Client The central type. Wraps the unpoller SDK client with config-based authentication. ```go type Client struct { api *uf.Unifi url string } func New(url, user, pass, apikey string, insecure bool) (*Client, error) func NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey string, flagInsecure *bool) (*Client, error) func (c *Client) API() *uf.Unifi func (c *Client) URL() string ``` Two constructors are provided: - **`New`** — direct credentials, useful for tests and scripts. - **`NewFromConfig`** — three-tier config resolution (file, env, flags), used by CLI commands. ### ClientFilter Controls which connected clients are returned from a query. ```go type ClientFilter struct { Site string // Filter by site name (empty = all sites) Wired bool // Show only wired clients Wireless bool // Show only wireless clients } func (c *Client) GetClients(filter ClientFilter) ([]*uf.Client, error) ``` ### DeviceInfo Flat representation of any UniFi infrastructure device, abstracting over the SDK's separate UAP/USW/USG/UDM/UXG containers. ```go type DeviceInfo struct { Name string IP string Mac string Model string Version string Type string // uap, usw, usg, udm, uxg Status int // 1 = online } func (c *Client) GetDevices(siteName string) (*uf.Devices, error) func (c *Client) GetDeviceList(siteName, deviceType string) ([]DeviceInfo, error) ``` - **`GetDevices`** returns the raw SDK container (for callers that need full detail). - **`GetDeviceList`** returns a flat, uniform slice with optional type filtering. ### NetworkConf Network configuration entry covering VLANs, DHCP, DNS, firewall zones, and WAN settings. ```go type NetworkConf struct { ID string `json:"_id"` Name string `json:"name"` Purpose string `json:"purpose"` // wan, corporate, remote-user-vpn IPSubnet string `json:"ip_subnet"` // CIDR VLAN int `json:"vlan"` VLANEnabled bool `json:"vlan_enabled"` Enabled bool `json:"enabled"` NetworkGroup string `json:"networkgroup"` // LAN, WAN, WAN2 NetworkIsolationEnabled bool `json:"network_isolation_enabled"` InternetAccessEnabled bool `json:"internet_access_enabled"` IsNAT bool `json:"is_nat"` DHCPEnabled bool `json:"dhcpd_enabled"` DHCPStart string `json:"dhcpd_start"` DHCPStop string `json:"dhcpd_stop"` DHCPDNS1 string `json:"dhcpd_dns_1"` DHCPDNS2 string `json:"dhcpd_dns_2"` DHCPDNSEnabled bool `json:"dhcpd_dns_enabled"` MDNSEnabled bool `json:"mdns_enabled"` FirewallZoneID string `json:"firewall_zone_id"` GatewayType string `json:"gateway_type"` VPNType string `json:"vpn_type"` WANType string `json:"wan_type"` // pppoe, dhcp, static WANNetworkGroup string `json:"wan_networkgroup"` } func (c *Client) GetNetworks(siteName string) ([]NetworkConf, error) ``` Uses the raw controller REST API (`/api/s/{site}/rest/networkconf`) since the unpoller SDK does not wrap this endpoint. ### Route Gateway routing table entry with prefix, next-hop, interface, type, and metrics. ```go type Route struct { Network string `json:"pfx"` // CIDR prefix NextHop string `json:"nh"` // Next-hop address or interface Interface string `json:"intf"` // Interface name (e.g. "br0", "eth4") Type string `json:"type"` // S=static, C=connected, K=kernel, B=bgp, O=ospf Distance int `json:"distance"` // Administrative distance Metric int `json:"metric"` Uptime int `json:"uptime"` // Seconds Selected bool `json:"fib"` // In forwarding table } func (c *Client) GetRoutes(siteName string) ([]Route, error) func RouteTypeName(code string) string ``` Also uses the raw controller API (`/api/s/{site}/stat/routing`). ## Design Decisions ### Thin Wrapper The package deliberately stays thin. It delegates transport, session management, and cookie handling to the unpoller SDK, adding only: - **Config resolution** — three-tier priority (config file, env vars, CLI flags). - **TLS flexibility** — `insecure` flag for self-signed certificates on home lab controllers. - **Flat types** — `DeviceInfo` and `NetworkConf` flatten the SDK's nested/typed structures into uniform slices for easier consumption by CLI commands and templates. - **Raw API access** — `GetNetworks` and `GetRoutes` call controller endpoints the SDK does not wrap, parsing the JSON directly. ### Config Resolution Order All credential resolution follows the same three-tier pattern used across the core framework: 1. **Config file** — `~/.core/config.yaml` (keys: `unifi.url`, `unifi.user`, `unifi.pass`, `unifi.apikey`, `unifi.insecure`) 2. **Environment variables** — `UNIFI_URL`, `UNIFI_USER`, `UNIFI_PASS`, `UNIFI_APIKEY`, `UNIFI_INSECURE` 3. **CLI flags** — highest priority, override everything ### Site Filtering Most query methods accept a `siteName` parameter. An empty string queries all sites. The `getSitesForFilter` helper resolves site names and returns an error for unknown sites, preventing silent data omission. ### TLS Configuration The client enforces TLS 1.2 as the minimum version regardless of the `insecure` flag. The `insecure` flag only controls certificate verification, not protocol version. ## API Patterns ### Constructor Pattern ```go // Direct credentials client, err := unifi.New("https://10.69.1.1", "admin", "pass", "", true) // Config-resolved (CLI usage) client, err := unifi.NewFromConfig(flagURL, flagUser, flagPass, flagAPIKey, &flagInsecure) ``` ### Query Pattern All query methods follow the same shape: accept optional filters, return typed slices, wrap errors with `log.E`. ```go clients, err := client.GetClients(unifi.ClientFilter{Site: "default", Wireless: true}) devices, err := client.GetDeviceList("default", "uap") networks, err := client.GetNetworks("default") routes, err := client.GetRoutes("default") sites, err := client.GetSites() ``` ### Config Persistence ```go // Save credentials err := unifi.SaveConfig("https://10.69.1.1", "admin", "pass", "", nil) // Resolve credentials (without creating a client) url, user, pass, apikey, insecure, err := unifi.ResolveConfig("", "", "", "", nil) ```