216 lines
7.7 KiB
Markdown
216 lines
7.7 KiB
Markdown
# 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)
|
|
```
|