Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 227656e28e | |||
| 63ef233357 | |||
| b4f49be2c6 | |||
| 4964317b6b | |||
| baf321ece0 | |||
| 5e8e7cd41d | |||
| 868ab64bc9 | |||
| edc86feccd | |||
| 7661bff6f1 | |||
| b21475b487 | |||
| 195a9f7a9f | |||
| ecbf4d447c | |||
| 438d422b53 | |||
| 38eb2cc352 | |||
| 1754eb6e84 | |||
| 906d005edf |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
config.y?ml
|
||||||
68
CHANGELOG.md
Normal file
68
CHANGELOG.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## v0.5.0 - 2026-01-19
|
||||||
|
### Added
|
||||||
|
- `ToughSwitch` consolidated type combining Device, System, Interfaces, VLANs, Services, Statistics, and Neighbors
|
||||||
|
- `GetToughSwitch(ctx, host)` method to fetch all data for a single ToughSwitch device in parallel
|
||||||
|
- `GetAllToughSwitches(ctx)` method to fetch consolidated data for all ToughSwitch devices
|
||||||
|
- `EdgeOS` consolidated type combining AuthInfo and Config
|
||||||
|
- `GetEdgeOS(ctx, host)` method to fetch all data for a single EdgeOS device in parallel
|
||||||
|
- `GetAllEdgeOS(ctx)` method to fetch consolidated data for all EdgeOS devices
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- CLI `get device` and `get devices` commands now return full consolidated data for both device types
|
||||||
|
- EdgeOS CLI output now includes device model, ports, PoE capability, and features from AuthInfo
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Typo in ToughSwitch struct field name (`Statistucs` → `Statistics`)
|
||||||
|
|
||||||
|
## v0.4.0 - 2026-01-18
|
||||||
|
### Added
|
||||||
|
- CLI tool (`cmd/`) for quick device queries using cobra
|
||||||
|
- `get device <name>` - fetch info from a single device
|
||||||
|
- `get devices` - fetch info from all configured devices in parallel
|
||||||
|
- YAML/JSON config file support with environment variable overlay
|
||||||
|
- Pretty print and colorized YAML output options
|
||||||
|
- `Login(ctx, host)` method to both toughswitch and edgeos clients for explicit pre-authentication
|
||||||
|
- Context helpers for storing/retrieving clients (`ToughSwitchClientFromContext`, `EdgeOSClientFromContext`)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- CLI uses zerolog's built-in context methods for logger storage
|
||||||
|
- CLI prerun creates shared clients from config and stores in context
|
||||||
|
- Updated documentation (README.md, CLAUDE.md) with CLI usage and new Login method
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Config default flag handling in CLI
|
||||||
|
|
||||||
|
## v0.3.0 - 2026-01-14
|
||||||
|
### Changed
|
||||||
|
- Renamed project from toughswitch to ubiquiti-clients
|
||||||
|
- Added EdgeOS support
|
||||||
|
|
||||||
|
## v0.2.1 - 2026-01-05
|
||||||
|
### Changed
|
||||||
|
- Minor LSP improvements to tests
|
||||||
|
- Refactor entire project edgeos -> toughswitch
|
||||||
|
|
||||||
|
## [v0.2.0] - 2026-01-04
|
||||||
|
### Added
|
||||||
|
- Thread-safe `Add` and `Del` methods to `Client` for dynamic host management.
|
||||||
|
- `RWMutex` to `Client` struct to protect `devices` map.
|
||||||
|
- `Transport` field to `Config` to allow mocking HTTP transport in tests.
|
||||||
|
- `getDeviceByHost` helper to centralize device lookup locking.
|
||||||
|
- Tests for thread safety and Add/Del functionality.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Refactored `GetAll*` methods to snapshot host keys before iteration to avoid concurrent map read/write panic.
|
||||||
|
- Improved concurrent `GetAll` operations.
|
||||||
|
|
||||||
|
## [v0.1.1] - 2026-01-04
|
||||||
|
### Fixed
|
||||||
|
- CI pipeline configuration.
|
||||||
|
|
||||||
|
## [v0.1.0] - 2026-01-04
|
||||||
|
### Added
|
||||||
|
- Initial CI pipeline setup.
|
||||||
|
- Initial release of Ubiquiti toughswitch Go Client.
|
||||||
74
CLAUDE.md
Normal file
74
CLAUDE.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build and Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build library packages
|
||||||
|
go build ./pkg/...
|
||||||
|
|
||||||
|
# Build CLI tool
|
||||||
|
go build ./cmd/...
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests for a specific package
|
||||||
|
go test ./pkg/toughswitch/... -run TestClient_AddDel
|
||||||
|
|
||||||
|
# Tidy module dependencies
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This repository provides Go client libraries for interacting with Ubiquiti network devices via reverse-engineered REST APIs, plus a CLI tool.
|
||||||
|
|
||||||
|
### Package Structure
|
||||||
|
|
||||||
|
- `pkg/toughswitch/` - Client for ToughSwitch devices (e.g., TS-8-PRO)
|
||||||
|
- `pkg/edgeos/` - Client for EdgeOS devices (EdgeRouter, EdgeSwitch)
|
||||||
|
- `cmd/` - CLI tool using cobra
|
||||||
|
|
||||||
|
### Library Client Design Pattern
|
||||||
|
|
||||||
|
Both `pkg/toughswitch` and `pkg/edgeos` follow the same multi-device client pattern:
|
||||||
|
|
||||||
|
1. **Client** - Top-level struct holding a map of `deviceClient` instances keyed by host
|
||||||
|
2. **deviceClient** - Per-device HTTP client with authentication state (token for ToughSwitch, cookies for EdgeOS)
|
||||||
|
3. **Config** - Device connection settings (host, credentials, TLS options, timeout)
|
||||||
|
|
||||||
|
Key characteristics:
|
||||||
|
- Thread-safe: Uses `sync.RWMutex` for device map access and `sync.Mutex` for per-device operations
|
||||||
|
- Auto-login: Automatically authenticates on 401 responses and retries the request
|
||||||
|
- Explicit login: `Login(ctx, host)` can be called to pre-authenticate
|
||||||
|
- Concurrent multi-device: `GetAll*` methods use `sync.WaitGroup.Go()` for parallel queries
|
||||||
|
|
||||||
|
API pattern for each package:
|
||||||
|
- `MustNew(ctx, []Config)` - Constructor that accepts multiple device configs
|
||||||
|
- `Login(ctx, host)` - Explicit authentication (also happens automatically on 401)
|
||||||
|
- `Add(cfg)` / `Del(host)` - Dynamic device management
|
||||||
|
- `Get<Resource>(ctx, host)` - Single device query
|
||||||
|
- `GetAll<Resources>(ctx)` - Parallel query across all devices, returns `map[string]*Resource`
|
||||||
|
|
||||||
|
### CLI Architecture (`cmd/`)
|
||||||
|
|
||||||
|
The CLI uses cobra with a prerun chain pattern:
|
||||||
|
|
||||||
|
- `cmd/cmd/root.go` - Root command with `PersistentPreRunE` hook
|
||||||
|
- `cmd/cmd/prerun.go` - Prerun functions executed in order: `validateConfigFile` → `prepareConfig` → `prepareLogger` → `setEnvironment` → `prepareClients`
|
||||||
|
- `cmd/cmd/client.go` - Shared `fetchDevice()` helper that retrieves clients from context
|
||||||
|
- `cmd/internal/util/context.go` - Context helpers for config, logger, and clients
|
||||||
|
- `cmd/internal/config/config.go` - Config loading from YAML/JSON files with env overlay
|
||||||
|
|
||||||
|
Context flow: Prerun creates clients based on config and stores them in context. Commands retrieve clients via `util.ToughSwitchClientFromContext(ctx)` / `util.EdgeOSClientFromContext(ctx)`.
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- **ToughSwitch**: Token-based via `x-auth-token` header from `/api/v1.0/user/login`
|
||||||
|
- **EdgeOS**: Cookie-based from `/api/login2` endpoint
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Tests use `mockTransport` implementing `http.RoundTripper` to mock HTTP responses without network calls.
|
||||||
278
README.md
278
README.md
@@ -1,13 +1,21 @@
|
|||||||
# edgeos
|
# ubiquiti-clients
|
||||||
|
|
||||||
A Go client library for interacting with Ubiquiti EdgeOS devices (specifically tested with EdgeSwitch XP / ToughSwitch) via their internal REST API.
|
Go client libraries for interacting with Ubiquiti network devices via their REST APIs, plus a CLI tool.
|
||||||
|
|
||||||
**⚠️ Disclaimer: This library is based on reverse-engineered API calls. It is not an official Ubiquiti product and is subject to change if the device firmware changes.**
|
**⚠️ Disclaimer: These libraries are based on reverse-engineered API calls. They are not official Ubiquiti products and are subject to change if the device firmware changes.**
|
||||||
|
|
||||||
## Features
|
## Packages
|
||||||
|
|
||||||
- **Authentication**: Handles login and session token management automatically.
|
### toughswitch
|
||||||
|
|
||||||
|
A client library for interacting with Ubiquiti ToughSwitch devices (specifically tested with
|
||||||
|
ToughSwitch POE Pro (TS-8-PRO)) via their internal REST API.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- **Authentication**: Handles login and session token management automatically (or explicitly via `Login`).
|
||||||
- **Multi-Device Support**: Manage multiple devices with a single client instance.
|
- **Multi-Device Support**: Manage multiple devices with a single client instance.
|
||||||
|
- **Consolidated Data**: `GetToughSwitch` fetches all device data in a single call (Device, System, Interfaces, VLANs, Services, Statistics, Neighbors).
|
||||||
- **Data Retrieval**:
|
- **Data Retrieval**:
|
||||||
- **System Information**: Hostname, uptime, firmware version, etc.
|
- **System Information**: Hostname, uptime, firmware version, etc.
|
||||||
- **Interfaces**: Status, POE settings, link speed, statistics.
|
- **Interfaces**: Status, POE settings, link speed, statistics.
|
||||||
@@ -16,15 +24,87 @@ A Go client library for interacting with Ubiquiti EdgeOS devices (specifically t
|
|||||||
- **Statistics**: Real-time throughput, errors, and resource usage.
|
- **Statistics**: Real-time throughput, errors, and resource usage.
|
||||||
- **Discovery**: Neighbor discovery via UBNT protocol.
|
- **Discovery**: Neighbor discovery via UBNT protocol.
|
||||||
|
|
||||||
## Installation
|
### edgeos
|
||||||
|
|
||||||
|
A client library for interacting with Ubiquiti EdgeOS devices (EdgeRouter, EdgeSwitch) via their REST API.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- **Authentication**: Handles login and session management automatically (or explicitly via `Login`).
|
||||||
|
- **Multi-Device Support**: Manage multiple devices with a single client instance.
|
||||||
|
- **Consolidated Data**: `GetEdgeOS` fetches all device data in a single call (AuthInfo and Config).
|
||||||
|
- **Data Retrieval**:
|
||||||
|
- **System Configuration**: Hostname, domain name, and other system settings.
|
||||||
|
- **Interface Configuration**: Ethernet and switch interface settings, including PoE.
|
||||||
|
- **VLAN Configuration**: VLAN assignments, PVID, and tagged VLANs.
|
||||||
|
- **Device Information**: Model, ports, PoE capabilities, and features.
|
||||||
|
|
||||||
|
## CLI Tool
|
||||||
|
|
||||||
|
A command-line tool is included for quick device queries.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go get gitea.libretechconsulting.com/rmcguire/edgeos-client/pkg/edgeos
|
go install gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd@latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
### Configuration
|
||||||
|
|
||||||
### Basic Example
|
Create a config file (YAML or JSON):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
logLevel: info
|
||||||
|
logFormat: console
|
||||||
|
|
||||||
|
clients:
|
||||||
|
- name: switch1
|
||||||
|
type: toughswitch
|
||||||
|
host: 192.168.1.1
|
||||||
|
user: ubnt
|
||||||
|
pass: password
|
||||||
|
insecure: true
|
||||||
|
timeout: 10s
|
||||||
|
|
||||||
|
- name: router1
|
||||||
|
type: edgeos
|
||||||
|
host: 192.168.1.2
|
||||||
|
user: ubnt
|
||||||
|
pass: password
|
||||||
|
insecure: true
|
||||||
|
timeout: 10s
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment variables can also be used (and take priority over config file):
|
||||||
|
- `LOG_LEVEL`, `LOG_FORMAT` for top-level settings
|
||||||
|
- `CLIENT_0_NAME`, `CLIENT_0_HOST`, `CLIENT_0_TYPE`, etc. for client array
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get info from a single device
|
||||||
|
cmd --config config.yaml get device switch1
|
||||||
|
|
||||||
|
# Get info from all configured devices
|
||||||
|
cmd --config config.yaml get devices
|
||||||
|
|
||||||
|
# With flags
|
||||||
|
cmd --config config.yaml get device switch1 --pretty --color
|
||||||
|
```
|
||||||
|
|
||||||
|
## Library Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For ToughSwitch
|
||||||
|
go get gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/toughswitch
|
||||||
|
|
||||||
|
# For EdgeOS
|
||||||
|
go get gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/edgeos
|
||||||
|
```
|
||||||
|
|
||||||
|
## Library Usage
|
||||||
|
|
||||||
|
### ToughSwitch Basic Example
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@@ -35,14 +115,14 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.libretechconsulting.com/rmcguire/edgeos-client/pkg/edgeos"
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/toughswitch"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// Configure your device(s)
|
// Configure your device(s)
|
||||||
configs := []edgeos.Config{
|
configs := []toughswitch.Config{
|
||||||
{
|
{
|
||||||
Host: "192.168.1.1",
|
Host: "192.168.1.1",
|
||||||
Username: "ubnt",
|
Username: "ubnt",
|
||||||
@@ -53,7 +133,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the client
|
// Initialize the client
|
||||||
client := edgeos.MustNew(ctx, configs)
|
client := toughswitch.MustNew(ctx, configs)
|
||||||
|
|
||||||
|
// Optionally pre-authenticate (otherwise happens automatically on first request)
|
||||||
|
if err := client.Login(ctx, "192.168.1.1"); err != nil {
|
||||||
|
log.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch system information
|
// Fetch system information
|
||||||
deviceHost := "192.168.1.1"
|
deviceHost := "192.168.1.1"
|
||||||
@@ -71,16 +156,87 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, iface := range ifaces {
|
for _, iface := range ifaces {
|
||||||
fmt.Printf("Interface %s: %s (POE: %s)\n",
|
fmt.Printf("Interface %s: %s (POE: %s)\n",
|
||||||
iface.Identification.ID,
|
iface.Identification.ID,
|
||||||
iface.Status.Speed,
|
iface.Status.Speed,
|
||||||
iface.Port.POE,
|
iface.Port.POE,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Retrieving Statistics
|
### EdgeOS Basic Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/edgeos"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Configure your device(s)
|
||||||
|
configs := []edgeos.Config{
|
||||||
|
{
|
||||||
|
Host: "192.168.1.1",
|
||||||
|
Username: "ubnt",
|
||||||
|
Password: "ubnt",
|
||||||
|
Insecure: true, // Set to true if using self-signed certs
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the client
|
||||||
|
client := edgeos.MustNew(ctx, configs)
|
||||||
|
|
||||||
|
// Optionally pre-authenticate (otherwise happens automatically on first request)
|
||||||
|
if err := client.Login(ctx, "192.168.1.1"); err != nil {
|
||||||
|
log.Fatalf("Failed to login: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch device information
|
||||||
|
deviceHost := "192.168.1.1"
|
||||||
|
authInfo, err := client.GetAuthInfo(ctx, deviceHost)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get auth info: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Connected to: %s (%s) with %d ports\n",
|
||||||
|
authInfo.ModelName, authInfo.Model, authInfo.Ports)
|
||||||
|
|
||||||
|
// Fetch system configuration
|
||||||
|
system, err := client.GetSystem(ctx, deviceHost)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get system config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Hostname: %s, Domain: %s\n", system.HostName, system.DomainName)
|
||||||
|
|
||||||
|
// Fetch interfaces
|
||||||
|
interfaces, err := client.GetInterfaces(ctx, deviceHost)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to get interfaces: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, config := range interfaces.Ethernet {
|
||||||
|
fmt.Printf("Interface %s: %s (Speed: %s, Duplex: %s)\n",
|
||||||
|
name,
|
||||||
|
config.Description,
|
||||||
|
config.Speed,
|
||||||
|
config.Duplex,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ToughSwitch: Retrieving Statistics
|
||||||
|
|
||||||
```go
|
```go
|
||||||
stats, err := client.GetStatistics(ctx, "192.168.1.1")
|
stats, err := client.GetStatistics(ctx, "192.168.1.1")
|
||||||
@@ -91,10 +247,10 @@ if err != nil {
|
|||||||
for _, stat := range stats {
|
for _, stat := range stats {
|
||||||
// Device level stats
|
// Device level stats
|
||||||
fmt.Printf("CPU Usage: %d%%\n", stat.Device.CPU[0].Usage)
|
fmt.Printf("CPU Usage: %d%%\n", stat.Device.CPU[0].Usage)
|
||||||
|
|
||||||
// Per-interface stats
|
// Per-interface stats
|
||||||
for _, iface := range stat.Interfaces {
|
for _, iface := range stat.Interfaces {
|
||||||
fmt.Printf("[%s] Rx: %d bps, Tx: %d bps\n",
|
fmt.Printf("[%s] Rx: %d bps, Tx: %d bps\n",
|
||||||
iface.Name,
|
iface.Name,
|
||||||
iface.Statistics.RxRate,
|
iface.Statistics.RxRate,
|
||||||
iface.Statistics.TxRate,
|
iface.Statistics.TxRate,
|
||||||
@@ -103,40 +259,104 @@ for _, stat := range stats {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Working with Multiple Devices
|
### Fetching All Device Data
|
||||||
|
|
||||||
The client is designed to handle multiple devices concurrently.
|
Use the consolidated methods to fetch all device information in a single call:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
configs := []edgeos.Config{
|
// ToughSwitch: Get everything at once
|
||||||
|
ts, err := client.GetToughSwitch(ctx, "192.168.1.1")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Model: %s\n", ts.Device.Identification.Model)
|
||||||
|
fmt.Printf("Hostname: %s\n", ts.System.Hostname)
|
||||||
|
fmt.Printf("Interfaces: %d\n", len(ts.Interfaces))
|
||||||
|
fmt.Printf("VLANs: %d\n", len(ts.VLANs.Vlans))
|
||||||
|
fmt.Printf("Neighbors: %d\n", len(ts.Neighbors))
|
||||||
|
|
||||||
|
// EdgeOS: Get everything at once
|
||||||
|
eos, err := edgeClient.GetEdgeOS(ctx, "192.168.2.1")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Model: %s (%s)\n", eos.AuthInfo.ModelName, eos.AuthInfo.Model)
|
||||||
|
fmt.Printf("Ports: %d, PoE: %v\n", eos.AuthInfo.Ports, eos.AuthInfo.PoE)
|
||||||
|
fmt.Printf("Hostname: %s\n", eos.Config.System.HostName)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Working with Multiple Devices
|
||||||
|
|
||||||
|
Both clients are designed to handle multiple devices concurrently.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ToughSwitch example
|
||||||
|
configs := []toughswitch.Config{
|
||||||
{Host: "192.168.1.1", ...},
|
{Host: "192.168.1.1", ...},
|
||||||
{Host: "192.168.1.2", ...},
|
{Host: "192.168.1.2", ...},
|
||||||
}
|
}
|
||||||
client := edgeos.MustNew(ctx, configs)
|
client := toughswitch.MustNew(ctx, configs)
|
||||||
|
|
||||||
// Get info for all configured devices in parallel
|
// Get all data for all devices in parallel
|
||||||
allSystems, err := client.GetAllSystems(ctx)
|
allSwitches, err := client.GetAllToughSwitches(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Note: This returns partial results if available, check implementation
|
log.Printf("Error fetching some devices: %v", err)
|
||||||
log.Printf("Error fetching some systems: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for host, sys := range allSystems {
|
for host, ts := range allSwitches {
|
||||||
fmt.Printf("[%s] Hostname: %s\n", host, sys.Hostname)
|
fmt.Printf("[%s] %s - %d interfaces\n", host, ts.System.Hostname, len(ts.Interfaces))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeOS example
|
||||||
|
edgeConfigs := []edgeos.Config{
|
||||||
|
{Host: "192.168.2.1", ...},
|
||||||
|
{Host: "192.168.2.2", ...},
|
||||||
|
}
|
||||||
|
edgeClient := edgeos.MustNew(ctx, edgeConfigs)
|
||||||
|
|
||||||
|
// Get all data for all devices in parallel
|
||||||
|
allEdge, err := edgeClient.GetAllEdgeOS(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error fetching some devices: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for host, eos := range allEdge {
|
||||||
|
fmt.Printf("[%s] %s (%d ports)\n", host, eos.AuthInfo.ModelName, eos.AuthInfo.Ports)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Supported Endpoints
|
## Supported Endpoints
|
||||||
|
|
||||||
|
### ToughSwitch
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
|
| `Login` | Explicit authentication (also happens automatically on 401) |
|
||||||
|
| `GetToughSwitch` | All device data combined (Device, System, Interfaces, VLANs, Services, Statistics, Neighbors) |
|
||||||
|
| `GetDevice` | Hardware and capabilities info |
|
||||||
| `GetSystem` | General system configuration and status |
|
| `GetSystem` | General system configuration and status |
|
||||||
| `GetInterfaces` | Interface configuration and status |
|
| `GetInterfaces` | Interface configuration and status |
|
||||||
| `GetVLANs` | VLAN and Trunk configuration |
|
| `GetVLANs` | VLAN and Trunk configuration |
|
||||||
| `GetServices` | State of running services (SSH, NTP, etc.) |
|
| `GetServices` | State of running services (SSH, NTP, etc.) |
|
||||||
| `GetStatistics` | Performance metrics |
|
| `GetStatistics` | Performance metrics |
|
||||||
| `GetNeighbors` | Discovered UBNT neighbors |
|
| `GetNeighbors` | Discovered UBNT neighbors |
|
||||||
| `GetDevice` | Hardware and capabilities info |
|
|
||||||
|
All `Get*` methods have corresponding `GetAll*` variants for multi-device operations.
|
||||||
|
|
||||||
|
### EdgeOS
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `Login` | Explicit authentication (also happens automatically on 401) |
|
||||||
|
| `GetEdgeOS` | All device data combined (AuthInfo and Config) |
|
||||||
|
| `GetAuthInfo` | Device authentication and feature information |
|
||||||
|
| `GetConfig` | Complete device configuration |
|
||||||
|
| `GetInterfaces` | Interface configuration (ethernet and switch) |
|
||||||
|
| `GetSystem` | System configuration (hostname, domain) |
|
||||||
|
|
||||||
|
All `Get*` methods have corresponding `GetAll*` variants for multi-device operations.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
21
cmd/LICENSE
Normal file
21
cmd/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright © 2026 Ryan McGuire <ryan@mcguire.app>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
48
cmd/cmd/client.go
Normal file
48
cmd/cmd/client.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fetchDevice retrieves device data using the pre-configured clients from context.
|
||||||
|
// It handles both ToughSwitch and EdgeOS device types.
|
||||||
|
func fetchDevice(ctx context.Context, clientConf *config.ClientConfig) (any, error) {
|
||||||
|
switch clientConf.Type {
|
||||||
|
case config.TypeToughSwitch:
|
||||||
|
return fetchToughSwitchDevice(ctx, clientConf)
|
||||||
|
case config.TypeEdgeOS:
|
||||||
|
return fetchEdgeOSDevice(ctx, clientConf)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown device type: %s", clientConf.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchToughSwitchDevice(ctx context.Context, clientConf *config.ClientConfig) (any, error) {
|
||||||
|
client := util.ToughSwitchClientFromContext(ctx)
|
||||||
|
if client == nil {
|
||||||
|
return nil, fmt.Errorf("toughswitch client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Login(ctx, clientConf.Host); err != nil {
|
||||||
|
return nil, fmt.Errorf("login failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.GetToughSwitch(ctx, clientConf.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEdgeOSDevice(ctx context.Context, clientConf *config.ClientConfig) (any, error) {
|
||||||
|
client := util.EdgeOSClientFromContext(ctx)
|
||||||
|
if client == nil {
|
||||||
|
return nil, fmt.Errorf("edgeos client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Login(ctx, clientConf.Host); err != nil {
|
||||||
|
return nil, fmt.Errorf("login failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.GetEdgeOS(ctx, clientConf.Host)
|
||||||
|
}
|
||||||
21
cmd/cmd/get.go
Normal file
21
cmd/cmd/get.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getCmd = &cobra.Command{
|
||||||
|
Use: "get",
|
||||||
|
Short: "Get device information",
|
||||||
|
Long: `Get device information from configured Ubiquiti devices.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(getCmd)
|
||||||
|
|
||||||
|
getCmd.PersistentFlags().BoolP(util.FLAG_PRETTY, util.FLAG_PRETTY_P, false,
|
||||||
|
"pretty print output with indentation")
|
||||||
|
getCmd.PersistentFlags().BoolP(util.FLAG_COLOR, util.FLAG_COLOR_P, false,
|
||||||
|
"colorize YAML output")
|
||||||
|
}
|
||||||
59
cmd/cmd/get_device.go
Normal file
59
cmd/cmd/get_device.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deviceCmd = &cobra.Command{
|
||||||
|
Use: "device <name>",
|
||||||
|
Short: "Get information from a single device",
|
||||||
|
Long: `Get information from a single configured device by name.`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: completeDeviceName,
|
||||||
|
RunE: runGetDevice,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
getCmd.AddCommand(deviceCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeDeviceName(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := util.ConfigFromContext(cmd.Context())
|
||||||
|
if conf == nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf.GetClientNames(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGetDevice(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
conf := util.ConfigFromContext(ctx)
|
||||||
|
logger := util.LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
deviceName := args[0]
|
||||||
|
clientConf := conf.GetClientByName(deviceName)
|
||||||
|
if clientConf == nil {
|
||||||
|
return fmt.Errorf("device %q not found in configuration", deviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
pretty, _ := cmd.Flags().GetBool(util.FLAG_PRETTY)
|
||||||
|
colorize, _ := cmd.Flags().GetBool(util.FLAG_COLOR)
|
||||||
|
|
||||||
|
logger.Debug().Str("device", deviceName).Str("type", string(clientConf.Type)).Msg("fetching device info")
|
||||||
|
|
||||||
|
result, err := fetchDevice(ctx, clientConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get device info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.YAMLOutput(os.Stdout, result, pretty, colorize)
|
||||||
|
}
|
||||||
81
cmd/cmd/get_devices.go
Normal file
81
cmd/cmd/get_devices.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var devicesCmd = &cobra.Command{
|
||||||
|
Use: "devices",
|
||||||
|
Short: "Get information from all configured devices",
|
||||||
|
Long: `Get information from all configured devices in parallel.`,
|
||||||
|
RunE: runGetDevices,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
getCmd.AddCommand(devicesCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceResult holds the result of fetching a single device.
|
||||||
|
type DeviceResult struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
Host string `yaml:"host"`
|
||||||
|
Data any `yaml:"data,omitempty"`
|
||||||
|
Error string `yaml:"error,omitempty"`
|
||||||
|
Failed bool `yaml:"failed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGetDevices(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
conf := util.ConfigFromContext(ctx)
|
||||||
|
logger := util.LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
pretty, _ := cmd.Flags().GetBool(util.FLAG_PRETTY)
|
||||||
|
colorize, _ := cmd.Flags().GetBool(util.FLAG_COLOR)
|
||||||
|
|
||||||
|
if len(conf.Clients) == 0 {
|
||||||
|
return fmt.Errorf("no devices configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug().Int("count", len(conf.Clients)).Msg("fetching all devices")
|
||||||
|
|
||||||
|
results := make([]DeviceResult, len(conf.Clients))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
for i, clientConf := range conf.Clients {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int, cc config.ClientConfig) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
result := DeviceResult{
|
||||||
|
Name: cc.Name,
|
||||||
|
Type: string(cc.Type),
|
||||||
|
Host: cc.Host,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := fetchDevice(ctx, &cc)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
result.Failed = true
|
||||||
|
logger.Error().Err(err).Str("device", cc.Name).Msg("failed to fetch device")
|
||||||
|
} else {
|
||||||
|
result.Data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
results[idx] = result
|
||||||
|
mu.Unlock()
|
||||||
|
}(i, clientConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return util.YAMLOutput(os.Stdout, results, pretty, colorize)
|
||||||
|
}
|
||||||
157
cmd/cmd/prerun.go
Normal file
157
cmd/cmd/prerun.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/edgeos"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/toughswitch"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// preRunFunc is a function that runs before command execution.
|
||||||
|
type preRunFunc func(cmd *cobra.Command, args []string) error
|
||||||
|
|
||||||
|
// preRunFuncs is the list of functions to run before command execution.
|
||||||
|
var preRunFuncs = []preRunFunc{
|
||||||
|
validateConfigFile,
|
||||||
|
prepareConfig,
|
||||||
|
prepareLogger,
|
||||||
|
setEnvironment,
|
||||||
|
prepareClients,
|
||||||
|
}
|
||||||
|
|
||||||
|
// preRun executes all registered pre-run functions in order.
|
||||||
|
func preRun(cmd *cobra.Command, args []string) error {
|
||||||
|
for _, fn := range preRunFuncs {
|
||||||
|
if err := fn(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEnvironment(cmd *cobra.Command, _ []string) error {
|
||||||
|
conf := util.ConfigFromContext(cmd.Context())
|
||||||
|
if conf == nil {
|
||||||
|
zerolog.Ctx(cmd.Context()).Fatal().Msg("unconfigured, nothing to do")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, device := range conf.Clients {
|
||||||
|
if device.Type == config.TypeToughSwitch {
|
||||||
|
zerolog.Ctx(cmd.Context()).Debug().
|
||||||
|
Msg("setting GODEBUG=x509negativeserial=1 for toughswitch devices")
|
||||||
|
os.Setenv("GODEBUG", "x509negativeserial=1")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareConfig(cmd *cobra.Command, _ []string) error {
|
||||||
|
configFile, _ := cmd.Root().PersistentFlags().GetString(util.FLAG_CONFIG_FILE)
|
||||||
|
conf, err := config.LoadConfig(&configFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := util.ContextWithConfig(cmd.Context(), conf)
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareLogger(cmd *cobra.Command, _ []string) error {
|
||||||
|
conf := util.ConfigFromContext(cmd.Context())
|
||||||
|
level, err := zerolog.ParseLevel(conf.LogLevel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var writer io.Writer
|
||||||
|
switch conf.LogFormat {
|
||||||
|
case "console":
|
||||||
|
writer = zerolog.NewConsoleWriter()
|
||||||
|
case "json":
|
||||||
|
writer = os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := zerolog.New(writer).Level(level)
|
||||||
|
ctx := util.ContextWithLogger(cmd.Context(), &logger)
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
|
||||||
|
logger.Trace().Any("config", conf).Msg("config and logging prepared")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateConfigFile checks that the config file exists if provided.
|
||||||
|
func validateConfigFile(cmd *cobra.Command, args []string) error {
|
||||||
|
configFile, _ := cmd.Root().PersistentFlags().GetString(util.FLAG_CONFIG_FILE)
|
||||||
|
if configFile == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(configFile, ".yaml") && !strings.HasSuffix(configFile, ".json") {
|
||||||
|
return fmt.Errorf("config file must end in .yaml or .json: %s", configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("config file does not exist: %s", configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareClients creates toughswitch and edgeos clients based on the configuration.
|
||||||
|
// Clients are only created if there are devices of that type in the config.
|
||||||
|
func prepareClients(cmd *cobra.Command, _ []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
conf := util.ConfigFromContext(ctx)
|
||||||
|
logger := zerolog.Ctx(ctx)
|
||||||
|
|
||||||
|
var tsConfigs []toughswitch.Config
|
||||||
|
var eosConfigs []edgeos.Config
|
||||||
|
|
||||||
|
for _, clientConf := range conf.Clients {
|
||||||
|
switch clientConf.Type {
|
||||||
|
case config.TypeToughSwitch:
|
||||||
|
tsConfigs = append(tsConfigs, toughswitch.Config{
|
||||||
|
Host: clientConf.Host,
|
||||||
|
Scheme: clientConf.Scheme,
|
||||||
|
Insecure: clientConf.Insecure,
|
||||||
|
Username: clientConf.User,
|
||||||
|
Password: clientConf.Pass,
|
||||||
|
Timeout: clientConf.Timeout,
|
||||||
|
})
|
||||||
|
case config.TypeEdgeOS:
|
||||||
|
eosConfigs = append(eosConfigs, edgeos.Config{
|
||||||
|
Host: clientConf.Host,
|
||||||
|
Scheme: clientConf.Scheme,
|
||||||
|
Insecure: clientConf.Insecure,
|
||||||
|
Username: clientConf.User,
|
||||||
|
Password: clientConf.Pass,
|
||||||
|
Timeout: clientConf.Timeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tsConfigs) > 0 {
|
||||||
|
logger.Debug().Int("count", len(tsConfigs)).Msg("creating toughswitch client")
|
||||||
|
tsClient := toughswitch.MustNew(ctx, tsConfigs)
|
||||||
|
ctx = util.ContextWithToughSwitchClient(ctx, tsClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(eosConfigs) > 0 {
|
||||||
|
logger.Debug().Int("count", len(eosConfigs)).Msg("creating edgeos client")
|
||||||
|
eosClient := edgeos.MustNew(ctx, eosConfigs)
|
||||||
|
ctx = util.ContextWithEdgeOSClient(ctx, eosClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
68
cmd/cmd/root.go
Normal file
68
cmd/cmd/root.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
Package cmd -- ubiquiti-clients test command
|
||||||
|
|
||||||
|
Copyright © 2026 Ryan McGuire <ryan@mcguire.app>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/util"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rootCmd represents the base command when called without any subcommands
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "cmd",
|
||||||
|
Short: "Simple CLI tool for using ubiquiti-clients packages",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
ctx, cncl := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
defer cncl()
|
||||||
|
|
||||||
|
err := rootCmd.ExecuteContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().StringP(util.FLAG_CONFIG_FILE, util.FLAG_CONFIG_FILE_P, "",
|
||||||
|
"config file for ubiquiti-clients (yaml/json), env takes priority")
|
||||||
|
|
||||||
|
rootCmd.RegisterFlagCompletionFunc(util.FLAG_CONFIG_FILE, completeConfigFile)
|
||||||
|
|
||||||
|
cobra.EnableTraverseRunHooks = true
|
||||||
|
rootCmd.PersistentPreRunE = preRun
|
||||||
|
}
|
||||||
|
|
||||||
|
// completeConfigFile provides shell completion for config file flag,
|
||||||
|
// returning only files ending in .yaml or .json.
|
||||||
|
func completeConfigFile(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return []string{"yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
|
||||||
|
}
|
||||||
80
cmd/internal/config/config.go
Normal file
80
cmd/internal/config/config.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/caarlos0/env/v11"
|
||||||
|
yaml "github.com/oasdiff/yaml3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientsConfig struct {
|
||||||
|
LogLevel string `json:"logLevel" yaml:"logLevel" env:"LOG_LEVEL" default:"warn"`
|
||||||
|
LogFormat string `json:"logFormat" yaml:"logFormat" env:"LOG_FORMAT" default:"console"`
|
||||||
|
|
||||||
|
Clients []ClientConfig `json:"clients" yaml:"clients" envPrefix:"CLIENT_"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeEdgeOS ClientType = "edgeos"
|
||||||
|
TypeToughSwitch ClientType = "toughswitch"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
Type ClientType `json:"type" yaml:"type" env:"TYPE"`
|
||||||
|
Name string `json:"name" yaml:"name" env:"NAME"`
|
||||||
|
Host string `json:"host" yaml:"host" env:"HOST"`
|
||||||
|
Scheme string `json:"scheme" yaml:"scheme" env:"SCHEME" default:"https"`
|
||||||
|
User string `json:"user" yaml:"user" env:"USER"`
|
||||||
|
Pass string `json:"pass" yaml:"pass" env:"PASS"`
|
||||||
|
Insecure bool `json:"insecure" yaml:"insecure" env:"INSECURE" default:"false"`
|
||||||
|
Timeout time.Duration `json:"timeout" yaml:"timeout" env:"TIMEOUT" default:"10s"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig will load a file if given, layering env on-top of the config
|
||||||
|
// if present. Environment variables take the form:
|
||||||
|
// - LOG_LEVEL, LOG_FORMAT for top-level settings
|
||||||
|
// - CLIENT_0_NAME, CLIENT_0_HOST, CLIENT_0_TYPE, etc. for client array
|
||||||
|
func LoadConfig(configPath *string) (*ClientsConfig, error) {
|
||||||
|
conf, err := env.ParseAs[ClientsConfig]()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse env config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configPath != nil && *configPath != "" {
|
||||||
|
file, err := os.Open(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open config file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
decoder := yaml.NewDecoder(file)
|
||||||
|
if err := decoder.Decode(&conf); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not decode config file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientByName returns a client config by its name, or nil if not found.
|
||||||
|
func (c *ClientsConfig) GetClientByName(name string) *ClientConfig {
|
||||||
|
for i := range c.Clients {
|
||||||
|
if c.Clients[i].Name == name {
|
||||||
|
return &c.Clients[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientNames returns a list of all configured client names.
|
||||||
|
func (c *ClientsConfig) GetClientNames() []string {
|
||||||
|
names := make([]string, len(c.Clients))
|
||||||
|
for i, client := range c.Clients {
|
||||||
|
names[i] = client.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
12
cmd/internal/util/constants.go
Normal file
12
cmd/internal/util/constants.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
const (
|
||||||
|
FLAG_CONFIG_FILE = "config"
|
||||||
|
FLAG_CONFIG_FILE_P = "f"
|
||||||
|
|
||||||
|
FLAG_PRETTY = "pretty"
|
||||||
|
FLAG_PRETTY_P = "p"
|
||||||
|
|
||||||
|
FLAG_COLOR = "color"
|
||||||
|
FLAG_COLOR_P = "c"
|
||||||
|
)
|
||||||
72
cmd/internal/util/context.go
Normal file
72
cmd/internal/util/context.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/internal/config"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/edgeos"
|
||||||
|
"gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/pkg/toughswitch"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKey uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ctxConfig ctxKey = iota
|
||||||
|
ctxToughSwitchClient
|
||||||
|
ctxEdgeOSClient
|
||||||
|
)
|
||||||
|
|
||||||
|
func ContextWithConfig(baseCtx context.Context, config *config.ClientsConfig) context.Context {
|
||||||
|
return context.WithValue(baseCtx, ctxConfig, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConfigFromContext(ctx context.Context) *config.ClientsConfig {
|
||||||
|
val := ctx.Value(ctxConfig)
|
||||||
|
conf, ok := val.(*config.ClientsConfig)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextWithLogger stores the logger in context using zerolog's built-in method.
|
||||||
|
func ContextWithLogger(baseCtx context.Context, logger *zerolog.Logger) context.Context {
|
||||||
|
return logger.WithContext(baseCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerFromContext retrieves the logger from context using zerolog's built-in method.
|
||||||
|
func LoggerFromContext(ctx context.Context) *zerolog.Logger {
|
||||||
|
return zerolog.Ctx(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextWithToughSwitchClient stores a toughswitch client in the context.
|
||||||
|
func ContextWithToughSwitchClient(baseCtx context.Context, client *toughswitch.Client) context.Context {
|
||||||
|
return context.WithValue(baseCtx, ctxToughSwitchClient, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToughSwitchClientFromContext retrieves the toughswitch client from context.
|
||||||
|
func ToughSwitchClientFromContext(ctx context.Context) *toughswitch.Client {
|
||||||
|
val := ctx.Value(ctxToughSwitchClient)
|
||||||
|
client, ok := val.(*toughswitch.Client)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextWithEdgeOSClient stores an edgeos client in the context.
|
||||||
|
func ContextWithEdgeOSClient(baseCtx context.Context, client *edgeos.Client) context.Context {
|
||||||
|
return context.WithValue(baseCtx, ctxEdgeOSClient, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EdgeOSClientFromContext retrieves the edgeos client from context.
|
||||||
|
func EdgeOSClientFromContext(ctx context.Context) *edgeos.Client {
|
||||||
|
val := ctx.Value(ctxEdgeOSClient)
|
||||||
|
client, ok := val.(*edgeos.Client)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
101
cmd/internal/util/output.go
Normal file
101
cmd/internal/util/output.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
yaml "github.com/oasdiff/yaml3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// YAMLOutput writes data as YAML to the given writer.
|
||||||
|
// If pretty is true, output is indented with 2 spaces.
|
||||||
|
// If colorize is true, YAML syntax is colorized.
|
||||||
|
func YAMLOutput(w io.Writer, data any, pretty, colorize bool) error {
|
||||||
|
encoder := yaml.NewEncoder(w)
|
||||||
|
defer encoder.Close()
|
||||||
|
|
||||||
|
if pretty {
|
||||||
|
encoder.SetIndent(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !colorize {
|
||||||
|
return encoder.Encode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For colored output, encode to bytes first, then colorize
|
||||||
|
var buf []byte
|
||||||
|
bufWriter := &byteWriter{buf: &buf}
|
||||||
|
bufEncoder := yaml.NewEncoder(bufWriter)
|
||||||
|
if pretty {
|
||||||
|
bufEncoder.SetIndent(2)
|
||||||
|
}
|
||||||
|
if err := bufEncoder.Encode(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bufEncoder.Close()
|
||||||
|
|
||||||
|
colorized := colorizeYAML(string(buf))
|
||||||
|
_, err := w.Write([]byte(colorized))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type byteWriter struct {
|
||||||
|
buf *[]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *byteWriter) Write(p []byte) (n int, err error) {
|
||||||
|
*b.buf = append(*b.buf, p...)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// colorizeYAML applies ANSI colors to YAML output.
|
||||||
|
func colorizeYAML(input string) string {
|
||||||
|
// Color definitions - wrap to match ReplaceAllStringFunc signature
|
||||||
|
keyColor := func(s string) string { return color.New(color.FgCyan).Sprint(s) }
|
||||||
|
stringColor := func(s string) string { return color.New(color.FgGreen).Sprint(s) }
|
||||||
|
numberColor := func(s string) string { return color.New(color.FgYellow).Sprint(s) }
|
||||||
|
boolColor := func(s string) string { return color.New(color.FgMagenta).Sprint(s) }
|
||||||
|
nullColor := func(s string) string { return color.New(color.FgRed).Sprint(s) }
|
||||||
|
|
||||||
|
// Patterns for YAML elements
|
||||||
|
keyPattern := regexp.MustCompile(`(?m)^(\s*)([a-zA-Z_][a-zA-Z0-9_-]*)(:)`)
|
||||||
|
stringPattern := regexp.MustCompile(`:\s*"([^"]*)"`)
|
||||||
|
quotedStringPattern := regexp.MustCompile(`:\s*'([^']*)'`)
|
||||||
|
numberPattern := regexp.MustCompile(`:\s*(-?\d+\.?\d*)\s*$`)
|
||||||
|
boolPattern := regexp.MustCompile(`:\s*(true|false)\s*$`)
|
||||||
|
nullPattern := regexp.MustCompile(`:\s*(null|~)\s*$`)
|
||||||
|
|
||||||
|
result := input
|
||||||
|
|
||||||
|
// Apply colors in order (specific patterns first)
|
||||||
|
result = nullPattern.ReplaceAllStringFunc(result, func(s string) string {
|
||||||
|
return regexp.MustCompile(`(null|~)`).ReplaceAllStringFunc(s, nullColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
result = boolPattern.ReplaceAllStringFunc(result, func(s string) string {
|
||||||
|
return regexp.MustCompile(`(true|false)`).ReplaceAllStringFunc(s, boolColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
result = numberPattern.ReplaceAllStringFunc(result, func(s string) string {
|
||||||
|
return regexp.MustCompile(`(-?\d+\.?\d*)\s*$`).ReplaceAllStringFunc(s, numberColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
result = stringPattern.ReplaceAllStringFunc(result, func(s string) string {
|
||||||
|
return regexp.MustCompile(`"([^"]*)"`).ReplaceAllStringFunc(s, stringColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
result = quotedStringPattern.ReplaceAllStringFunc(result, func(s string) string {
|
||||||
|
return regexp.MustCompile(`'([^']*)'`).ReplaceAllStringFunc(s, stringColor)
|
||||||
|
})
|
||||||
|
|
||||||
|
result = keyPattern.ReplaceAllStringFunc(result, func(s string) string {
|
||||||
|
matches := keyPattern.FindStringSubmatch(s)
|
||||||
|
if len(matches) >= 4 {
|
||||||
|
return matches[1] + keyColor(matches[2]) + matches[3]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
28
cmd/main.go
Normal file
28
cmd/main.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 Ryan McGuire <ryan@mcguire.app>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "gitea.libretechconsulting.com/rmcguire/ubiquiti-clients/cmd/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
18
go.mod
18
go.mod
@@ -1,3 +1,19 @@
|
|||||||
module gitea.libretechconsulting.com/rmcguire/edgeos-client
|
module gitea.libretechconsulting.com/rmcguire/ubiquiti-clients
|
||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/caarlos0/env/v11 v11.3.1
|
||||||
|
github.com/fatih/color v1.18.0
|
||||||
|
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/spf13/cobra v1.10.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.9 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
34
go.sum
Normal file
34
go.sum
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||||
|
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||||
|
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -2,284 +2,280 @@ package edgeos
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetInterfaces retrieves the interfaces for a specific device.
|
// GetConfig retrieves the complete device configuration from /api/edge/get.json for a specific device.
|
||||||
func (c *Client) GetInterfaces(ctx context.Context, host string) ([]Interface, error) {
|
func (c *Client) GetConfig(ctx context.Context, host string) (*ConfigData, error) {
|
||||||
d, ok := c.devices[host]
|
d, err := c.getDeviceByHost(host)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("device not found: %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []Interface
|
|
||||||
if err := d.do(ctx, "GET", "/api/v1.0/interfaces", nil, &out); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
var out ConfigResponse
|
||||||
|
if err := d.do(ctx, "GET", "/api/edge/get.json", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !out.Success {
|
||||||
|
return nil, errors.New("config request unsuccessful")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out.GET, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllConfigs retrieves device configuration for all devices.
|
||||||
|
func (c *Client) GetAllConfigs(ctx context.Context) (map[string]*ConfigData, error) {
|
||||||
|
results := make(map[string]*ConfigData)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetConfig(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthInfo retrieves the authentication info for a specific device.
|
||||||
|
func (c *Client) GetAuthInfo(ctx context.Context, host string) (*AuthResponse, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mu.Lock()
|
||||||
|
authInfo := d.authInfo
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if authInfo == nil {
|
||||||
|
if err := d.login(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d.mu.Lock()
|
||||||
|
authInfo = d.authInfo
|
||||||
|
d.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return authInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAuthInfo retrieves authentication info for all devices.
|
||||||
|
func (c *Client) GetAllAuthInfo(ctx context.Context) (map[string]*AuthResponse, error) {
|
||||||
|
results := make(map[string]*AuthResponse)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetAuthInfo(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterfaces retrieves the interfaces for a specific device from the config data.
|
||||||
|
func (c *Client) GetInterfaces(ctx context.Context, host string) (*InterfacesConfig, error) {
|
||||||
|
config, err := c.GetConfig(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config.Interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllInterfaces retrieves interfaces for all devices.
|
// GetAllInterfaces retrieves interfaces for all devices.
|
||||||
func (c *Client) GetAllInterfaces(ctx context.Context) (map[string][]Interface, error) {
|
func (c *Client) GetAllInterfaces(ctx context.Context) (map[string]*InterfacesConfig, error) {
|
||||||
results := make(map[string][]Interface)
|
results := make(map[string]*InterfacesConfig)
|
||||||
var mu sync.Mutex
|
var (
|
||||||
var wg sync.WaitGroup
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
// Use a buffered channel or just loop?
|
errs error
|
||||||
// Since we return error if any fails? Or partial results?
|
)
|
||||||
// Usually partial results + error or composite error.
|
|
||||||
// I will return partial results and the last error for now, or just stop on error?
|
|
||||||
// "methods to get ... for either all device"
|
|
||||||
// I will implement parallel fetch.
|
|
||||||
|
|
||||||
for host := range c.devices {
|
c.mu.RLock()
|
||||||
wg.Add(1)
|
hosts := make([]string, 0, len(c.devices))
|
||||||
go func(h string) {
|
for h := range c.devices {
|
||||||
defer wg.Done()
|
hosts = append(hosts, h)
|
||||||
res, err := c.GetInterfaces(ctx, h)
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetInterfaces(ctx, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// For now, log error or ignore?
|
mu.Lock()
|
||||||
// We should probably return an error map or just return what we have?
|
errs = errors.Join(errs, err)
|
||||||
// I will just skip failed ones for this implementation or log?
|
mu.Unlock()
|
||||||
// I'll return what succeeds.
|
|
||||||
// The prompt doesn't specify error handling strategy for "all".
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
results[h] = res
|
results[host] = res
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}(host)
|
})
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
return results, nil
|
return results, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDevice retrieves the device info for a specific device.
|
// GetSystem retrieves the system info for a specific device from the config data.
|
||||||
func (c *Client) GetDevice(ctx context.Context, host string) (*Device, error) {
|
func (c *Client) GetSystem(ctx context.Context, host string) (*SystemConfig, error) {
|
||||||
d, ok := c.devices[host]
|
config, err := c.GetConfig(ctx, host)
|
||||||
if !ok {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("device not found: %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
var out Device
|
|
||||||
if err := d.do(ctx, "GET", "/api/v1.0/device", nil, &out); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &out, nil
|
return &config.System, nil
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllDevices retrieves device info for all devices.
|
|
||||||
func (c *Client) GetAllDevices(ctx context.Context) (map[string]*Device, error) {
|
|
||||||
results := make(map[string]*Device)
|
|
||||||
var mu sync.Mutex
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for host := range c.devices {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(h string) {
|
|
||||||
defer wg.Done()
|
|
||||||
res, err := c.GetDevice(ctx, h)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mu.Lock()
|
|
||||||
results[h] = res
|
|
||||||
mu.Unlock()
|
|
||||||
}(host)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSystem retrieves the system info for a specific device.
|
|
||||||
func (c *Client) GetSystem(ctx context.Context, host string) (*System, error) {
|
|
||||||
d, ok := c.devices[host]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("device not found: %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
var out System
|
|
||||||
if err := d.do(ctx, "GET", "/api/v1.0/system", nil, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllSystems retrieves system info for all devices.
|
// GetAllSystems retrieves system info for all devices.
|
||||||
func (c *Client) GetAllSystems(ctx context.Context) (map[string]*System, error) {
|
func (c *Client) GetAllSystems(ctx context.Context) (map[string]*SystemConfig, error) {
|
||||||
results := make(map[string]*System)
|
results := make(map[string]*SystemConfig)
|
||||||
var mu sync.Mutex
|
var (
|
||||||
var wg sync.WaitGroup
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
for host := range c.devices {
|
c.mu.RLock()
|
||||||
wg.Add(1)
|
hosts := make([]string, 0, len(c.devices))
|
||||||
go func(h string) {
|
for h := range c.devices {
|
||||||
defer wg.Done()
|
hosts = append(hosts, h)
|
||||||
res, err := c.GetSystem(ctx, h)
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetSystem(ctx, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
results[h] = res
|
results[host] = res
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}(host)
|
})
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
return results, nil
|
return results, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVLANs retrieves the VLANs for a specific device.
|
// GetEdgeOS retrieves all information for a specific device,
|
||||||
func (c *Client) GetVLANs(ctx context.Context, host string) (*VLANs, error) {
|
// combining AuthInfo and Config.
|
||||||
d, ok := c.devices[host]
|
func (c *Client) GetEdgeOS(ctx context.Context, host string) (*EdgeOS, error) {
|
||||||
if !ok {
|
_, err := c.getDeviceByHost(host)
|
||||||
return nil, fmt.Errorf("device not found: %s", host)
|
if err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
var out VLANs
|
|
||||||
if err := d.do(ctx, "GET", "/api/v1.0/vlans", nil, &out); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &out, nil
|
eos := &EdgeOS{}
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch auth info and config in parallel
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetAuthInfo(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
eos.AuthInfo = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetConfig(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
eos.Config = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return eos, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllVLANs retrieves VLANs for all devices.
|
// GetAllEdgeOS retrieves all information for all devices.
|
||||||
func (c *Client) GetAllVLANs(ctx context.Context) (map[string]*VLANs, error) {
|
func (c *Client) GetAllEdgeOS(ctx context.Context) (map[string]*EdgeOS, error) {
|
||||||
results := make(map[string]*VLANs)
|
results := make(map[string]*EdgeOS)
|
||||||
var mu sync.Mutex
|
var (
|
||||||
var wg sync.WaitGroup
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
for host := range c.devices {
|
c.mu.RLock()
|
||||||
wg.Add(1)
|
hosts := make([]string, 0, len(c.devices))
|
||||||
go func(h string) {
|
for h := range c.devices {
|
||||||
defer wg.Done()
|
hosts = append(hosts, h)
|
||||||
res, err := c.GetVLANs(ctx, h)
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetEdgeOS(ctx, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
results[h] = res
|
results[host] = res
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}(host)
|
})
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
return results, nil
|
return results, errs
|
||||||
}
|
|
||||||
|
|
||||||
// GetServices retrieves the services for a specific device.
|
|
||||||
func (c *Client) GetServices(ctx context.Context, host string) (*Services, error) {
|
|
||||||
d, ok := c.devices[host]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("device not found: %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
var out Services
|
|
||||||
if err := d.do(ctx, "GET", "/api/v1.0/services", nil, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllServices retrieves services for all devices.
|
|
||||||
func (c *Client) GetAllServices(ctx context.Context) (map[string]*Services, error) {
|
|
||||||
results := make(map[string]*Services)
|
|
||||||
var mu sync.Mutex
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for host := range c.devices {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(h string) {
|
|
||||||
defer wg.Done()
|
|
||||||
res, err := c.GetServices(ctx, h)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mu.Lock()
|
|
||||||
results[h] = res
|
|
||||||
mu.Unlock()
|
|
||||||
}(host)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatistics retrieves the statistics for a specific device.
|
|
||||||
func (c *Client) GetStatistics(ctx context.Context, host string) ([]Statistics, error) {
|
|
||||||
d, ok := c.devices[host]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("device not found: %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []Statistics
|
|
||||||
if err := d.do(ctx, "GET", "/api/v1.0/statistics", nil, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllStatistics retrieves statistics for all devices.
|
|
||||||
func (c *Client) GetAllStatistics(ctx context.Context) (map[string][]Statistics, error) {
|
|
||||||
results := make(map[string][]Statistics)
|
|
||||||
var mu sync.Mutex
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for host := range c.devices {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(h string) {
|
|
||||||
defer wg.Done()
|
|
||||||
res, err := c.GetStatistics(ctx, h)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mu.Lock()
|
|
||||||
results[h] = res
|
|
||||||
mu.Unlock()
|
|
||||||
}(host)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNeighbors retrieves the neighbors for a specific device.
|
|
||||||
func (c *Client) GetNeighbors(ctx context.Context, host string) ([]Neighbor, error) {
|
|
||||||
d, ok := c.devices[host]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("device not found: %s", host)
|
|
||||||
}
|
|
||||||
|
|
||||||
var out []Neighbor
|
|
||||||
if err := d.do(ctx, "GET", "/api/v1.0/tools/discovery/neighbors", nil, &out); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllNeighbors retrieves neighbors for all devices.
|
|
||||||
func (c *Client) GetAllNeighbors(ctx context.Context) (map[string][]Neighbor, error) {
|
|
||||||
results := make(map[string][]Neighbor)
|
|
||||||
var mu sync.Mutex
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
for host := range c.devices {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(h string) {
|
|
||||||
defer wg.Done()
|
|
||||||
res, err := c.GetNeighbors(ctx, h)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mu.Lock()
|
|
||||||
results[h] = res
|
|
||||||
mu.Unlock()
|
|
||||||
}(host)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return results, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Package edgeos provides a client for interacting with Ubiquiti EdgeOS devices
|
Package edgeos provides a client for interacting with Ubiquiti EdgeOS devices
|
||||||
via their REST API. It supports authentication, token management, and
|
via their REST API. It supports authentication, session management, and
|
||||||
retrieval of system, interface, VLAN, and discovery information from
|
retrieval of system and interface configuration from one or more devices.
|
||||||
one or more devices.
|
|
||||||
*/
|
*/
|
||||||
package edgeos
|
package edgeos
|
||||||
|
|
||||||
@@ -21,14 +20,44 @@ import (
|
|||||||
|
|
||||||
// Client handles communication with EdgeOS devices.
|
// Client handles communication with EdgeOS devices.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
|
mu sync.RWMutex
|
||||||
devices map[string]*deviceClient
|
devices map[string]*deviceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type deviceClient struct {
|
type deviceClient struct {
|
||||||
config Config
|
config Config
|
||||||
client *http.Client
|
client *http.Client
|
||||||
token string
|
cookies []*http.Cookie
|
||||||
mu sync.Mutex
|
authInfo *AuthResponse
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeviceClient(cfg Config) *deviceClient {
|
||||||
|
if cfg.Scheme == "" {
|
||||||
|
cfg.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
var tr http.RoundTripper
|
||||||
|
if cfg.Transport != nil {
|
||||||
|
tr = cfg.Transport
|
||||||
|
} else {
|
||||||
|
defaultTr := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
if defaultTr.TLSClientConfig == nil {
|
||||||
|
defaultTr.TLSClientConfig = &tls.Config{}
|
||||||
|
}
|
||||||
|
defaultTr.TLSClientConfig.InsecureSkipVerify = cfg.Insecure
|
||||||
|
tr = defaultTr
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
Timeout: cfg.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &deviceClient{
|
||||||
|
config: cfg,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MustNew creates a new Client with the given configurations.
|
// MustNew creates a new Client with the given configurations.
|
||||||
@@ -37,27 +66,7 @@ func MustNew(ctx context.Context, configs []Config) *Client {
|
|||||||
devices := make(map[string]*deviceClient)
|
devices := make(map[string]*deviceClient)
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
// Use Host as the key.
|
devices[cfg.Host] = newDeviceClient(cfg)
|
||||||
// Ensure scheme is set
|
|
||||||
if cfg.Scheme == "" {
|
|
||||||
cfg.Scheme = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
|
||||||
if tr.TLSClientConfig == nil {
|
|
||||||
tr.TLSClientConfig = &tls.Config{}
|
|
||||||
}
|
|
||||||
tr.TLSClientConfig.InsecureSkipVerify = cfg.Insecure
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: tr,
|
|
||||||
Timeout: cfg.Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
devices[cfg.Host] = &deviceClient{
|
|
||||||
config: cfg,
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
@@ -65,35 +74,87 @@ func MustNew(ctx context.Context, configs []Config) *Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add adds a new device to the client.
|
||||||
|
// It returns an error if a device with the same host already exists.
|
||||||
|
func (c *Client) Add(cfg *Config) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
d := newDeviceClient(*cfg)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := c.devices[cfg.Host]; ok {
|
||||||
|
return fmt.Errorf("device already exists: %s", cfg.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.devices[cfg.Host] = d
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del removes a device from the client.
|
||||||
|
// It returns an error if the device does not exist.
|
||||||
|
func (c *Client) Del(host string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := c.devices[host]; !ok {
|
||||||
|
return fmt.Errorf("device not found: %s", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(c.devices, host)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getDeviceByHost(host string) (*deviceClient, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
d, ok := c.devices[host]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("device not found: %s", host)
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login authenticates with the device at the given host.
|
||||||
|
// This is called automatically on 401 responses, but can be called explicitly
|
||||||
|
// to pre-authenticate before making requests.
|
||||||
|
func (c *Client) Login(ctx context.Context, host string) error {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.login(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *deviceClient) login(ctx context.Context) error {
|
func (d *deviceClient) login(ctx context.Context) error {
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
defer d.mu.Unlock()
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
reqUrl := fmt.Sprintf("%s://%s/api/v1.0/user/login", d.config.Scheme, d.config.Host)
|
reqUrl := fmt.Sprintf("%s://%s/api/login2", d.config.Scheme, d.config.Host)
|
||||||
payload := map[string]string{
|
|
||||||
"username": d.config.Username,
|
data := url.Values{}
|
||||||
"password": d.config.Password,
|
data.Set("username", d.config.Username)
|
||||||
}
|
data.Set("password", d.config.Password)
|
||||||
body, err := json.Marshal(payload)
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", reqUrl, strings.NewReader(data.Encode()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBuffer(body))
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
req.URL.User = url.UserPassword(d.config.Username, d.config.Password)
|
req.Header.Set("Origin", fmt.Sprintf("%s://%s", d.config.Scheme, d.config.Host))
|
||||||
|
|
||||||
resp, err := d.client.Do(req)
|
resp, err := d.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
respPayload, err := io.ReadAll(resp.Body)
|
respPayload, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -103,23 +164,27 @@ func (d *deviceClient) login(ctx context.Context) error {
|
|||||||
return fmt.Errorf("login failed [%d]: %s", resp.StatusCode, string(respPayload))
|
return fmt.Errorf("login failed [%d]: %s", resp.StatusCode, string(respPayload))
|
||||||
}
|
}
|
||||||
|
|
||||||
token := resp.Header.Get("x-auth-token")
|
var authResp AuthResponse
|
||||||
if token == "" {
|
if err := json.Unmarshal(respPayload, &authResp); err != nil {
|
||||||
return fmt.Errorf("login failed: no token in response")
|
return fmt.Errorf("failed to parse auth response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.token = token
|
if !authResp.Authenticated {
|
||||||
|
return fmt.Errorf("authentication failed for user %s", d.config.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.authInfo = &authResp
|
||||||
|
d.cookies = resp.Cookies()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *deviceClient) do(ctx context.Context, method, path string, body any, out any) error {
|
func (d *deviceClient) do(ctx context.Context, method, path string, body any, out any) error {
|
||||||
// First attempt
|
|
||||||
err := d.doRequest(ctx, method, path, body, out)
|
err := d.doRequest(ctx, method, path, body, out)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If unauthorized, try to login and retry
|
|
||||||
if strings.Contains(err.Error(), "status 401") || strings.Contains(err.Error(), "unauthorized") {
|
if strings.Contains(err.Error(), "status 401") || strings.Contains(err.Error(), "unauthorized") {
|
||||||
if loginErr := d.login(ctx); loginErr != nil {
|
if loginErr := d.login(ctx); loginErr != nil {
|
||||||
return fmt.Errorf("re-login failed: %w", loginErr)
|
return fmt.Errorf("re-login failed: %w", loginErr)
|
||||||
@@ -131,7 +196,7 @@ func (d *deviceClient) do(ctx context.Context, method, path string, body any, ou
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *deviceClient) doRequest(ctx context.Context, method, path string, body any, out any) error {
|
func (d *deviceClient) doRequest(ctx context.Context, method, path string, body any, out any) error {
|
||||||
url := fmt.Sprintf("%s://%s%s", d.config.Scheme, d.config.Host, path)
|
reqUrl := fmt.Sprintf("%s://%s%s", d.config.Scheme, d.config.Host, path)
|
||||||
|
|
||||||
var reqBody io.Reader
|
var reqBody io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
@@ -142,23 +207,28 @@ func (d *deviceClient) doRequest(ctx context.Context, method, path string, body
|
|||||||
reqBody = bytes.NewBuffer(b)
|
reqBody = bytes.NewBuffer(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
req, err := http.NewRequestWithContext(ctx, method, reqUrl, reqBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.mu.Lock()
|
d.mu.Lock()
|
||||||
token := d.token
|
cookies := d.cookies
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
if token != "" {
|
if len(cookies) > 0 {
|
||||||
req.Header.Set("x-auth-token", token)
|
cookieURL, _ := url.Parse(reqUrl)
|
||||||
|
for _, cookie := range cookies {
|
||||||
|
if cookie.Domain == "" || strings.HasSuffix(cookieURL.Host, cookie.Domain) {
|
||||||
|
req.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some endpoints might require Content-Type even for GET if we were strict, but usually only for POST/PUT
|
|
||||||
if body != nil {
|
if body != nil {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
}
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
resp, err := d.client.Do(req)
|
resp, err := d.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,7 +241,6 @@ func (d *deviceClient) doRequest(ctx context.Context, method, path string, body
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
// Read body to see error message
|
|
||||||
b, _ := io.ReadAll(resp.Body)
|
b, _ := io.ReadAll(resp.Body)
|
||||||
return fmt.Errorf("request failed: status %d, body: %s", resp.StatusCode, string(b))
|
return fmt.Errorf("request failed: status %d, body: %s", resp.StatusCode, string(b))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package edgeos
|
package edgeos
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// Config represents the configuration for an EdgeOS device.
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Host string
|
Host string
|
||||||
Scheme string
|
Scheme string
|
||||||
@@ -10,4 +12,6 @@ type Config struct {
|
|||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
// Transport allows customizing the http transport (useful for testing or client middleware)
|
||||||
|
Transport http.RoundTripper
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,364 +1,106 @@
|
|||||||
package edgeos
|
package edgeos
|
||||||
|
|
||||||
// LoginResponse represents the response from the login endpoint.
|
// EdgeOS combines all device information into one response.
|
||||||
type LoginResponse struct {
|
type EdgeOS struct {
|
||||||
StatusCode int `json:"statusCode"`
|
AuthInfo *AuthResponse `json:"authInfo,omitempty"`
|
||||||
Error int `json:"error"`
|
Config *ConfigData `json:"config,omitempty"`
|
||||||
Detail string `json:"detail"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfaceIdentification represents identification info for an interface.
|
// AuthResponse represents the authentication response from login2 endpoint.
|
||||||
type InterfaceIdentification struct {
|
type AuthResponse struct {
|
||||||
ID string `json:"id"`
|
Username string `json:"username"`
|
||||||
Name string `json:"name"`
|
PoE bool `json:"poe"`
|
||||||
Mac string `json:"mac"`
|
StatsURL string `json:"statsUrl"`
|
||||||
Type string `json:"type"`
|
Features Features `json:"features"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Authenticated bool `json:"authenticated"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
IsLicenseAccepted bool `json:"isLicenseAccepted"`
|
||||||
|
ModelName string `json:"model_name"`
|
||||||
|
Ports int `json:"ports"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfaceStatus represents status info for an interface.
|
// Features contains device feature information.
|
||||||
type InterfaceStatus struct {
|
type Features struct {
|
||||||
Enabled bool `json:"enabled"`
|
Model string `json:"model"`
|
||||||
Plugged bool `json:"plugged"`
|
PoECap map[string]string `json:"poe_cap"`
|
||||||
CurrentSpeed string `json:"currentSpeed"`
|
Switch SwitchFeatures `json:"switch"`
|
||||||
Speed string `json:"speed"`
|
SwitchIsVLANCapable bool `json:"switchIsVLANCapable"`
|
||||||
MTU int `json:"mtu"`
|
PoE bool `json:"poe"`
|
||||||
|
Ports int `json:"ports"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfaceAddress represents an address on an interface.
|
// SwitchFeatures contains switch-specific features.
|
||||||
type InterfaceAddress struct {
|
type SwitchFeatures struct {
|
||||||
Type string `json:"type"`
|
Ports []string `json:"ports"`
|
||||||
Version string `json:"version"`
|
|
||||||
CIDR string `json:"cidr"`
|
|
||||||
EUI64 bool `json:"eui64"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfacePort represents port specific settings.
|
// ConfigResponse represents the response from /api/edge/get.json.
|
||||||
type InterfacePort struct {
|
type ConfigResponse struct {
|
||||||
STP PortSTP `json:"stp"`
|
SessionID string `json:"SESSION_ID"`
|
||||||
POE string `json:"poe"`
|
GET ConfigData `json:"GET"`
|
||||||
FlowControl bool `json:"flowControl"`
|
Success bool `json:"success"`
|
||||||
Routed bool `json:"routed"`
|
|
||||||
PingWatchdog PingWatchdog `json:"pingWatchdog"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PortSTP represents STP settings for a port.
|
// ConfigData contains the full device configuration.
|
||||||
type PortSTP struct {
|
type ConfigData struct {
|
||||||
Enabled bool `json:"enabled"`
|
Interfaces InterfacesConfig `json:"interfaces"`
|
||||||
EdgePort string `json:"edgePort"`
|
System SystemConfig `json:"system"`
|
||||||
PathCost int `json:"pathCost"`
|
|
||||||
PortPriority int `json:"portPriority"`
|
|
||||||
State string `json:"state"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PingWatchdog represents ping watchdog settings.
|
// InterfacesConfig represents the interfaces configuration.
|
||||||
type PingWatchdog struct {
|
type InterfacesConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
Ethernet map[string]EthernetConfig `json:"ethernet,omitempty"`
|
||||||
Address string `json:"address"`
|
Switch map[string]SwitchConfig `json:"switch,omitempty"`
|
||||||
FailureCount int `json:"failureCount"`
|
|
||||||
Interval int `json:"interval"`
|
|
||||||
OffDelay int `json:"offDelay"`
|
|
||||||
StartDelay int `json:"startDelay"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface represents a network interface.
|
// EthernetConfig represents an ethernet interface configuration.
|
||||||
type Interface struct {
|
type EthernetConfig struct {
|
||||||
Identification InterfaceIdentification `json:"identification"`
|
Description string `json:"description,omitempty"`
|
||||||
Status InterfaceStatus `json:"status"`
|
Duplex string `json:"duplex,omitempty"`
|
||||||
Addresses []InterfaceAddress `json:"addresses"`
|
Speed string `json:"speed,omitempty"`
|
||||||
Port InterfacePort `json:"port"`
|
PoE *PoEState `json:"poe,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceIdentification represents device identification.
|
// PoEState represents PoE output state.
|
||||||
type DeviceIdentification struct {
|
type PoEState struct {
|
||||||
Mac string `json:"mac"`
|
Output string `json:"output,omitempty"`
|
||||||
Model string `json:"model"`
|
|
||||||
Family string `json:"family"`
|
|
||||||
SubsystemID string `json:"subsystemID"`
|
|
||||||
FirmwareVersion string `json:"firmwareVersion"`
|
|
||||||
Firmware string `json:"firmware"`
|
|
||||||
Product string `json:"product"`
|
|
||||||
ServerVersion string `json:"serverVersion"`
|
|
||||||
BridgeVersion string `json:"bridgeVersion"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceCapabilityInterface represents interface capabilities.
|
// SwitchConfig represents a switch interface configuration.
|
||||||
type DeviceCapabilityInterface struct {
|
type SwitchConfig struct {
|
||||||
ID string `json:"id"`
|
Address []string `json:"address,omitempty"`
|
||||||
Type string `json:"type"`
|
MTU string `json:"mtu,omitempty"`
|
||||||
SupportBlock bool `json:"supportBlock"`
|
SwitchPort *SwitchPortConfig `json:"switch-port,omitempty"`
|
||||||
SupportDelete bool `json:"supportDelete"`
|
VIF map[string]VIFConfig `json:"vif,omitempty"`
|
||||||
SupportReset bool `json:"supportReset"`
|
|
||||||
Configurable bool `json:"configurable"`
|
|
||||||
SupportDHCPSnooping bool `json:"supportDHCPSnooping"`
|
|
||||||
SupportIsolate bool `json:"supportIsolate"`
|
|
||||||
SupportAutoEdge bool `json:"supportAutoEdge"`
|
|
||||||
MaxMTU int `json:"maxMTU"`
|
|
||||||
SupportPOE bool `json:"supportPOE"`
|
|
||||||
SupportCableTest bool `json:"supportCableTest"`
|
|
||||||
POEValues []string `json:"poeValues"`
|
|
||||||
Media string `json:"media"`
|
|
||||||
SpeedValues []string `json:"speedValues"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceCapabilities represents device capabilities.
|
// SwitchPortConfig represents switch port configuration with VLAN awareness.
|
||||||
type DeviceCapabilities struct {
|
type SwitchPortConfig struct {
|
||||||
Interfaces []DeviceCapabilityInterface `json:"interfaces"`
|
Interface map[string]InterfaceVLAN `json:"interface,omitempty"`
|
||||||
|
VLANAware string `json:"vlan-aware,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Device represents the device info.
|
// InterfaceVLAN represents VLAN configuration for an interface.
|
||||||
type Device struct {
|
type InterfaceVLAN struct {
|
||||||
ErrorCodes []any `json:"errorCodes"`
|
VLAN VLANConfig `json:"vlan,omitempty"`
|
||||||
Identification DeviceIdentification `json:"identification"`
|
|
||||||
Capabilities DeviceCapabilities `json:"capabilities"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemSTP represents system STP settings.
|
// VLANConfig represents VLAN ID configuration.
|
||||||
type SystemSTP struct {
|
type VLANConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
PVID string `json:"pvid,omitempty"`
|
||||||
Version string `json:"version"`
|
VID []string `json:"vid,omitempty"`
|
||||||
MaxAge int `json:"maxAge"`
|
|
||||||
HelloTime int `json:"helloTime"`
|
|
||||||
ForwardDelay int `json:"forwardDelay"`
|
|
||||||
Priority int `json:"priority"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemUser represents a system user.
|
// VIFConfig represents a virtual interface (VLAN) configuration.
|
||||||
type SystemUser struct {
|
type VIFConfig struct {
|
||||||
Username string `json:"username"`
|
Address []string `json:"address,omitempty"`
|
||||||
ReadOnly bool `json:"readOnly"`
|
Description string `json:"description,omitempty"`
|
||||||
SSHKeys []any `json:"sshKeys"`
|
MTU string `json:"mtu,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemManagement represents management settings.
|
// SystemConfig contains system configuration.
|
||||||
type SystemManagement struct {
|
type SystemConfig struct {
|
||||||
VlanID int `json:"vlanID"`
|
HostName string `json:"host-name,omitempty"`
|
||||||
ManagementPortOnly bool `json:"managementPortOnly"`
|
DomainName string `json:"domain-name,omitempty"`
|
||||||
Addresses []InterfaceAddress `json:"addresses"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SystemAddress represents a system-level address configuration (DNS, Gateway).
|
|
||||||
type SystemAddress struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// System represents system information.
|
|
||||||
type System struct {
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
Timezone string `json:"timezone"`
|
|
||||||
DomainName string `json:"domainName"`
|
|
||||||
FactoryDefault bool `json:"factoryDefault"`
|
|
||||||
STP SystemSTP `json:"stp"`
|
|
||||||
AnalyticsEnabled bool `json:"analyticsEnabled"`
|
|
||||||
DNSServers []SystemAddress `json:"dnsServers"`
|
|
||||||
DefaultGateway []SystemAddress `json:"defaultGateway"`
|
|
||||||
Users []SystemUser `json:"users"`
|
|
||||||
Management SystemManagement `json:"management"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trunk represents a VLAN trunk.
|
|
||||||
type Trunk struct {
|
|
||||||
Interface InterfaceIdentification `json:"interface"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VlanParticipation represents interface participation in a VLAN.
|
|
||||||
type VlanParticipation struct {
|
|
||||||
Interface InterfaceIdentification `json:"interface"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vlan represents a VLAN.
|
|
||||||
type Vlan struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
ID int `json:"id"`
|
|
||||||
Participation []VlanParticipation `json:"participation"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VLANs represents the VLAN configuration.
|
|
||||||
type VLANs struct {
|
|
||||||
Trunks []Trunk `json:"trunks"`
|
|
||||||
Vlans []Vlan `json:"vlans"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceDiscoveryResponder ...
|
|
||||||
type ServiceDiscoveryResponder struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceSSHServer ...
|
|
||||||
type ServiceSSHServer struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
SSHPort int `json:"sshPort"`
|
|
||||||
PasswordAuthentication bool `json:"passwordAuthentication"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceTelnetServer ...
|
|
||||||
type ServiceTelnetServer struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceWebServer ...
|
|
||||||
type ServiceWebServer struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
HTTPPort int `json:"httpPort"`
|
|
||||||
HTTPSPort int `json:"httpsPort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceSystemLog ...
|
|
||||||
type ServiceSystemLog struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Port int `json:"port"`
|
|
||||||
Server string `json:"server"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceNTPClient ...
|
|
||||||
type ServiceNTPClient struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
NTPServers []string `json:"ntpServers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceUNMS ...
|
|
||||||
type ServiceUNMS struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceLLDP ...
|
|
||||||
type ServiceLLDP struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceSNMPAgent ...
|
|
||||||
type ServiceSNMPAgent struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Community string `json:"community"`
|
|
||||||
Contact string `json:"contact"`
|
|
||||||
Location string `json:"location"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DDNSClient represents a dynamic DNS client configuration.
|
|
||||||
type DDNSClient struct {
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
Service string `json:"service"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceDDNS represents dynamic DNS service configuration.
|
|
||||||
type ServiceDDNS struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
Clients []DDNSClient `json:"clients"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Services represents services configuration.
|
|
||||||
type Services struct {
|
|
||||||
DiscoveryResponder ServiceDiscoveryResponder `json:"discoveryResponder"`
|
|
||||||
SSHServer ServiceSSHServer `json:"sshServer"`
|
|
||||||
TelnetServer ServiceTelnetServer `json:"telnetServer"`
|
|
||||||
WebServer ServiceWebServer `json:"webServer"`
|
|
||||||
SystemLog ServiceSystemLog `json:"systemLog"`
|
|
||||||
NTPClient ServiceNTPClient `json:"ntpClient"`
|
|
||||||
UNMS ServiceUNMS `json:"unms"`
|
|
||||||
LLDP ServiceLLDP `json:"lldp"`
|
|
||||||
SNMPAgent ServiceSNMPAgent `json:"snmpAgent"`
|
|
||||||
DDNS ServiceDDNS `json:"ddns"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InterfaceStatistics represents statistics for an interface.
|
|
||||||
type InterfaceStatistics struct {
|
|
||||||
Dropped int64 `json:"dropped"`
|
|
||||||
TxDropped int64 `json:"txDropped"`
|
|
||||||
RxDropped int64 `json:"rxDropped"`
|
|
||||||
Errors int64 `json:"errors"`
|
|
||||||
TxErrors int64 `json:"txErrors"`
|
|
||||||
RxErrors int64 `json:"rxErrors"`
|
|
||||||
Rate int64 `json:"rate"`
|
|
||||||
TxRate int64 `json:"txRate"`
|
|
||||||
RxRate int64 `json:"rxRate"`
|
|
||||||
Bytes int64 `json:"bytes"`
|
|
||||||
TxBytes int64 `json:"txBytes"`
|
|
||||||
RxBytes int64 `json:"rxBytes"`
|
|
||||||
Packets int64 `json:"packets"`
|
|
||||||
TxPackets int64 `json:"txPackets"`
|
|
||||||
RxPackets int64 `json:"rxPackets"`
|
|
||||||
PPS int64 `json:"pps"`
|
|
||||||
TxPPS int64 `json:"txPPS"`
|
|
||||||
RxPPS int64 `json:"rxPPS"`
|
|
||||||
TxBroadcast int64 `json:"txBroadcast"`
|
|
||||||
RxBroadcast int64 `json:"rxBroadcast"`
|
|
||||||
TxMulticast int64 `json:"txMulticast"`
|
|
||||||
RxMulticast int64 `json:"rxMulticast"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// InterfaceWithStats represents an interface within the statistics response.
|
|
||||||
type InterfaceWithStats struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Statistics InterfaceStatistics `json:"statistics"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CPUStat represents CPU usage statistics.
|
|
||||||
type CPUStat struct {
|
|
||||||
Identifier string `json:"identifier"`
|
|
||||||
Usage int `json:"usage"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RAMStat represents RAM usage statistics.
|
|
||||||
type RAMStat struct {
|
|
||||||
Usage int64 `json:"usage"`
|
|
||||||
Free int64 `json:"free"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StorageStat represents storage usage statistics.
|
|
||||||
type StorageStat struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
SysName string `json:"sysName"`
|
|
||||||
Used int64 `json:"used"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeviceStats represents device level stats in the statistics response.
|
|
||||||
type DeviceStats struct {
|
|
||||||
CPU []CPUStat `json:"cpu"`
|
|
||||||
RAM RAMStat `json:"ram"`
|
|
||||||
Temperatures []any `json:"temperatures"`
|
|
||||||
Storage []StorageStat `json:"storage"`
|
|
||||||
Uptime int64 `json:"uptime"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistics represents a statistics entry.
|
|
||||||
type Statistics struct {
|
|
||||||
Timestamp int64 `json:"timestamp"`
|
|
||||||
Device DeviceStats `json:"device"`
|
|
||||||
Interfaces []InterfaceWithStats `json:"interfaces"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NeighborAddress represents an address of a neighbor.
|
|
||||||
type NeighborAddress struct {
|
|
||||||
Mac string `json:"mac"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Neighbor represents a discovered neighbor.
|
|
||||||
type Neighbor struct {
|
|
||||||
Mac string `json:"mac"`
|
|
||||||
Age int `json:"age"`
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
FW string `json:"fw"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Product string `json:"product"`
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
Uptime int64 `json:"uptime"`
|
|
||||||
Configured bool `json:"configured"`
|
|
||||||
IP string `json:"ip"`
|
|
||||||
ZoneID string `json:"zoneID"`
|
|
||||||
Addresses []NeighborAddress `json:"addresses"`
|
|
||||||
}
|
|
||||||
481
pkg/toughswitch/api.go
Normal file
481
pkg/toughswitch/api.go
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
package toughswitch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetInterfaces retrieves the interfaces for a specific device.
|
||||||
|
func (c *Client) GetInterfaces(ctx context.Context, host string) ([]Interface, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []Interface
|
||||||
|
if err := d.do(ctx, "GET", "/api/v1.0/interfaces", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllInterfaces retrieves interfaces for all devices.
|
||||||
|
func (c *Client) GetAllInterfaces(ctx context.Context) (map[string][]Interface, error) {
|
||||||
|
results := make(map[string][]Interface)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetInterfaces(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDevice retrieves the device info for a specific device.
|
||||||
|
func (c *Client) GetDevice(ctx context.Context, host string) (*Device, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out Device
|
||||||
|
if err := d.do(ctx, "GET", "/api/v1.0/device", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllDevices retrieves device info for all devices.
|
||||||
|
func (c *Client) GetAllDevices(ctx context.Context) (map[string]*Device, error) {
|
||||||
|
results := make(map[string]*Device)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetDevice(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystem retrieves the system info for a specific device.
|
||||||
|
func (c *Client) GetSystem(ctx context.Context, host string) (*System, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out System
|
||||||
|
if err := d.do(ctx, "GET", "/api/v1.0/system", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllSystems retrieves system info for all devices.
|
||||||
|
func (c *Client) GetAllSystems(ctx context.Context) (map[string]*System, error) {
|
||||||
|
results := make(map[string]*System)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetSystem(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVLANs retrieves the VLANs for a specific device.
|
||||||
|
func (c *Client) GetVLANs(ctx context.Context, host string) (*VLANs, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out VLANs
|
||||||
|
if err := d.do(ctx, "GET", "/api/v1.0/vlans", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllVLANs retrieves VLANs for all devices.
|
||||||
|
func (c *Client) GetAllVLANs(ctx context.Context) (map[string]*VLANs, error) {
|
||||||
|
results := make(map[string]*VLANs)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetVLANs(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServices retrieves the services for a specific device.
|
||||||
|
func (c *Client) GetServices(ctx context.Context, host string) (*Services, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out Services
|
||||||
|
if err := d.do(ctx, "GET", "/api/v1.0/services", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllServices retrieves services for all devices.
|
||||||
|
func (c *Client) GetAllServices(ctx context.Context) (map[string]*Services, error) {
|
||||||
|
results := make(map[string]*Services)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetServices(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatistics retrieves the statistics for a specific device.
|
||||||
|
func (c *Client) GetStatistics(ctx context.Context, host string) ([]Statistics, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []Statistics
|
||||||
|
if err := d.do(ctx, "GET", "/api/v1.0/statistics", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllStatistics retrieves statistics for all devices.
|
||||||
|
func (c *Client) GetAllStatistics(ctx context.Context) (map[string][]Statistics, error) {
|
||||||
|
results := make(map[string][]Statistics)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetStatistics(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNeighbors retrieves the neighbors for a specific device.
|
||||||
|
func (c *Client) GetNeighbors(ctx context.Context, host string) ([]Neighbor, error) {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []Neighbor
|
||||||
|
if err := d.do(ctx, "GET", "/api/v1.0/tools/discovery/neighbors", nil, &out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllNeighbors retrieves neighbors for all devices.
|
||||||
|
func (c *Client) GetAllNeighbors(ctx context.Context) (map[string][]Neighbor, error) {
|
||||||
|
results := make(map[string][]Neighbor)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetNeighbors(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToughSwitch retrieves all information for a specific device,
|
||||||
|
// combining Device, System, Interfaces, VLANs, Services, Statistics, and Neighbors.
|
||||||
|
func (c *Client) GetToughSwitch(ctx context.Context, host string) (*ToughSwitch, error) {
|
||||||
|
_, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := &ToughSwitch{}
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fetch all resources in parallel
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetDevice(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.Device = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetSystem(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.System = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetInterfaces(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.Interfaces = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetVLANs(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.VLANs = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetServices(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.Services = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetStatistics(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.Statistics = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetNeighbors(ctx, host)
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.Neighbors = res
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return ts, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllToughSwitches retrieves all information for all devices.
|
||||||
|
func (c *Client) GetAllToughSwitches(ctx context.Context) (map[string]*ToughSwitch, error) {
|
||||||
|
results := make(map[string]*ToughSwitch)
|
||||||
|
var (
|
||||||
|
mu sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
hosts := make([]string, 0, len(c.devices))
|
||||||
|
for h := range c.devices {
|
||||||
|
hosts = append(hosts, h)
|
||||||
|
}
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
for _, host := range hosts {
|
||||||
|
wg.Go(func() {
|
||||||
|
res, err := c.GetToughSwitch(ctx, host)
|
||||||
|
if err != nil {
|
||||||
|
mu.Lock()
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[host] = res
|
||||||
|
mu.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return results, errs
|
||||||
|
}
|
||||||
252
pkg/toughswitch/client.go
Normal file
252
pkg/toughswitch/client.go
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
/*
|
||||||
|
Package toughswitch provides a client for interacting with Ubiquiti toughswitch devices
|
||||||
|
via their REST API. It supports authentication, token management, and
|
||||||
|
retrieval of system, interface, VLAN, and discovery information from
|
||||||
|
one or more devices.
|
||||||
|
*/
|
||||||
|
package toughswitch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client handles communication with toughswitch devices.
|
||||||
|
type Client struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
devices map[string]*deviceClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceClient struct {
|
||||||
|
config Config
|
||||||
|
client *http.Client
|
||||||
|
token string
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDeviceClient(cfg Config) *deviceClient {
|
||||||
|
// Ensure scheme is set
|
||||||
|
if cfg.Scheme == "" {
|
||||||
|
cfg.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
var tr http.RoundTripper
|
||||||
|
if cfg.Transport != nil {
|
||||||
|
tr = cfg.Transport
|
||||||
|
} else {
|
||||||
|
defaultTr := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
if defaultTr.TLSClientConfig == nil {
|
||||||
|
defaultTr.TLSClientConfig = &tls.Config{}
|
||||||
|
}
|
||||||
|
defaultTr.TLSClientConfig.InsecureSkipVerify = cfg.Insecure
|
||||||
|
tr = defaultTr
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
Timeout: cfg.Timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &deviceClient{
|
||||||
|
config: cfg,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustNew creates a new Client with the given configurations.
|
||||||
|
// It panics if a configuration is invalid (though currently we just accept all).
|
||||||
|
func MustNew(ctx context.Context, configs []Config) *Client {
|
||||||
|
devices := make(map[string]*deviceClient)
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
devices[cfg.Host] = newDeviceClient(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
devices: devices,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a new device to the client.
|
||||||
|
// It returns an error if a device with the same host already exists.
|
||||||
|
func (c *Client) Add(cfg *Config) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("config cannot be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
d := newDeviceClient(*cfg)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := c.devices[cfg.Host]; ok {
|
||||||
|
return fmt.Errorf("device already exists: %s", cfg.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.devices[cfg.Host] = d
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del removes a device from the client.
|
||||||
|
// It returns an error if the device does not exist.
|
||||||
|
func (c *Client) Del(host string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := c.devices[host]; !ok {
|
||||||
|
return fmt.Errorf("device not found: %s", host)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(c.devices, host)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getDeviceByHost(host string) (*deviceClient, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
d, ok := c.devices[host]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("device not found: %s", host)
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login authenticates with the device at the given host.
|
||||||
|
// This is called automatically on 401 responses, but can be called explicitly
|
||||||
|
// to pre-authenticate before making requests.
|
||||||
|
func (c *Client) Login(ctx context.Context, host string) error {
|
||||||
|
d, err := c.getDeviceByHost(host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.login(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceClient) login(ctx context.Context) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
reqUrl := fmt.Sprintf("%s://%s/api/v1.0/user/login", d.config.Scheme, d.config.Host)
|
||||||
|
payload := map[string]string{
|
||||||
|
"username": d.config.Username,
|
||||||
|
"password": d.config.Password,
|
||||||
|
}
|
||||||
|
body, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.URL.User = url.UserPassword(d.config.Username, d.config.Password)
|
||||||
|
|
||||||
|
resp, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
respPayload, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("login failed [%d]: %s", resp.StatusCode, string(respPayload))
|
||||||
|
}
|
||||||
|
|
||||||
|
token := resp.Header.Get("x-auth-token")
|
||||||
|
if token == "" {
|
||||||
|
return fmt.Errorf("login failed: no token in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
d.token = token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceClient) do(ctx context.Context, method, path string, body any, out any) error {
|
||||||
|
// First attempt
|
||||||
|
err := d.doRequest(ctx, method, path, body, out)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If unauthorized, try to login and retry
|
||||||
|
if strings.Contains(err.Error(), "status 401") || strings.Contains(err.Error(), "unauthorized") {
|
||||||
|
if loginErr := d.login(ctx); loginErr != nil {
|
||||||
|
return fmt.Errorf("re-login failed: %w", loginErr)
|
||||||
|
}
|
||||||
|
return d.doRequest(ctx, method, path, body, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *deviceClient) doRequest(ctx context.Context, method, path string, body any, out any) error {
|
||||||
|
url := fmt.Sprintf("%s://%s%s", d.config.Scheme, d.config.Host, path)
|
||||||
|
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
b, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewBuffer(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
d.mu.Lock()
|
||||||
|
token := d.token
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("x-auth-token", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some endpoints might require Content-Type even for GET if we were strict, but usually only for POST/PUT
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
return fmt.Errorf("status 401")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// Read body to see error message
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("request failed: status %d, body: %s", resp.StatusCode, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
if out != nil {
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
105
pkg/toughswitch/client_test.go
Normal file
105
pkg/toughswitch/client_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package toughswitch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockTransport struct {
|
||||||
|
RoundTripFunc func(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
if m.RoundTripFunc != nil {
|
||||||
|
return m.RoundTripFunc(req)
|
||||||
|
}
|
||||||
|
// Default mock response
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Body: io.NopCloser(bytes.NewBufferString("{}")),
|
||||||
|
Header: make(http.Header),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_ThreadSafety(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := MustNew(ctx, []Config{})
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
start := make(chan struct{})
|
||||||
|
|
||||||
|
// Writer: Adds and deletes hosts
|
||||||
|
wg.Go(func() {
|
||||||
|
<-start
|
||||||
|
for i := range 100 {
|
||||||
|
host := fmt.Sprintf("host-%d", i)
|
||||||
|
cfg := &Config{
|
||||||
|
Host: host,
|
||||||
|
Transport: &mockTransport{},
|
||||||
|
}
|
||||||
|
if err := client.Add(cfg); err != nil {
|
||||||
|
// verify we don't error on valid add
|
||||||
|
t.Logf("Add error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We invoke Del immediately.
|
||||||
|
if err := client.Del(host); err != nil {
|
||||||
|
t.Logf("Del error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reader: Iterates hosts
|
||||||
|
wg.Go(func() {
|
||||||
|
<-start
|
||||||
|
for range 10 {
|
||||||
|
// GetAllInterfaces iterates keys.
|
||||||
|
// With mock transport, this will succeed (returning empty structs)
|
||||||
|
// checking for race conditions.
|
||||||
|
_, _ = client.GetAllInterfaces(ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
close(start)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClient_AddDel(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client := MustNew(ctx, []Config{})
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
Host: "test-host",
|
||||||
|
Transport: &mockTransport{},
|
||||||
|
}
|
||||||
|
if err := client.Add(cfg); err != nil {
|
||||||
|
t.Fatalf("Add failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Add(cfg); err == nil {
|
||||||
|
t.Fatal("Expected error adding duplicate host, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we can retrieve it
|
||||||
|
// Mock transport returns 200 OK with empty body, so GetInterfaces should return empty slice (or error decoding if empty body is not valid JSON array? actually "{}" is valid object, but GetInterfaces expects array for /interfaces?)
|
||||||
|
// Let's check api.go: GetInterfaces calls /interfaces.
|
||||||
|
// We can customize the mock if we want to test success return.
|
||||||
|
// For this test, we just care that it doesn't return "device not found".
|
||||||
|
_, err := client.GetInterfaces(ctx, "test-host")
|
||||||
|
if err != nil && err.Error() == "device not found: test-host" {
|
||||||
|
t.Fatal("Device should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Del("test-host"); err != nil {
|
||||||
|
t.Fatalf("Del failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.Del("test-host"); err == nil {
|
||||||
|
t.Fatal("Expected error deleting non-existent host, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
17
pkg/toughswitch/config.go
Normal file
17
pkg/toughswitch/config.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package toughswitch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
Scheme string
|
||||||
|
Insecure bool
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Timeout time.Duration
|
||||||
|
// Transport allows customizing the http transport (useful for testing or client middleware)
|
||||||
|
Transport http.RoundTripper
|
||||||
|
}
|
||||||
375
pkg/toughswitch/types.go
Normal file
375
pkg/toughswitch/types.go
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
package toughswitch
|
||||||
|
|
||||||
|
// ToughSwitch combines all types into one response.
|
||||||
|
type ToughSwitch struct {
|
||||||
|
Device *Device `json:"device,omitempty"`
|
||||||
|
System *System `json:"system,omitempty"`
|
||||||
|
Interfaces []Interface `json:"interfaces,omitempty"`
|
||||||
|
Neighbors []Neighbor `json:"neighbors,omitempty"`
|
||||||
|
Statistics []Statistics `json:"statistics,omitempty"`
|
||||||
|
Services *Services `json:"services,omitempty"`
|
||||||
|
VLANs *VLANs `json:"vlans,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginResponse represents the response from the login endpoint.
|
||||||
|
type LoginResponse struct {
|
||||||
|
StatusCode int `json:"statusCode"`
|
||||||
|
Error int `json:"error"`
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceIdentification represents identification info for an interface.
|
||||||
|
type InterfaceIdentification struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Mac string `json:"mac"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceStatus represents status info for an interface.
|
||||||
|
type InterfaceStatus struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Plugged bool `json:"plugged"`
|
||||||
|
CurrentSpeed string `json:"currentSpeed"`
|
||||||
|
Speed string `json:"speed"`
|
||||||
|
MTU int `json:"mtu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceAddress represents an address on an interface.
|
||||||
|
type InterfaceAddress struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
CIDR string `json:"cidr"`
|
||||||
|
EUI64 bool `json:"eui64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfacePort represents port specific settings.
|
||||||
|
type InterfacePort struct {
|
||||||
|
STP PortSTP `json:"stp"`
|
||||||
|
POE string `json:"poe"`
|
||||||
|
FlowControl bool `json:"flowControl"`
|
||||||
|
Routed bool `json:"routed"`
|
||||||
|
PingWatchdog PingWatchdog `json:"pingWatchdog"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PortSTP represents STP settings for a port.
|
||||||
|
type PortSTP struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
EdgePort string `json:"edgePort"`
|
||||||
|
PathCost int `json:"pathCost"`
|
||||||
|
PortPriority int `json:"portPriority"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PingWatchdog represents ping watchdog settings.
|
||||||
|
type PingWatchdog struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
FailureCount int `json:"failureCount"`
|
||||||
|
Interval int `json:"interval"`
|
||||||
|
OffDelay int `json:"offDelay"`
|
||||||
|
StartDelay int `json:"startDelay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface represents a network interface.
|
||||||
|
type Interface struct {
|
||||||
|
Identification InterfaceIdentification `json:"identification"`
|
||||||
|
Status InterfaceStatus `json:"status"`
|
||||||
|
Addresses []InterfaceAddress `json:"addresses"`
|
||||||
|
Port InterfacePort `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceIdentification represents device identification.
|
||||||
|
type DeviceIdentification struct {
|
||||||
|
Mac string `json:"mac"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
SubsystemID string `json:"subsystemID"`
|
||||||
|
FirmwareVersion string `json:"firmwareVersion"`
|
||||||
|
Firmware string `json:"firmware"`
|
||||||
|
Product string `json:"product"`
|
||||||
|
ServerVersion string `json:"serverVersion"`
|
||||||
|
BridgeVersion string `json:"bridgeVersion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceCapabilityInterface represents interface capabilities.
|
||||||
|
type DeviceCapabilityInterface struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
SupportBlock bool `json:"supportBlock"`
|
||||||
|
SupportDelete bool `json:"supportDelete"`
|
||||||
|
SupportReset bool `json:"supportReset"`
|
||||||
|
Configurable bool `json:"configurable"`
|
||||||
|
SupportDHCPSnooping bool `json:"supportDHCPSnooping"`
|
||||||
|
SupportIsolate bool `json:"supportIsolate"`
|
||||||
|
SupportAutoEdge bool `json:"supportAutoEdge"`
|
||||||
|
MaxMTU int `json:"maxMTU"`
|
||||||
|
SupportPOE bool `json:"supportPOE"`
|
||||||
|
SupportCableTest bool `json:"supportCableTest"`
|
||||||
|
POEValues []string `json:"poeValues"`
|
||||||
|
Media string `json:"media"`
|
||||||
|
SpeedValues []string `json:"speedValues"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceCapabilities represents device capabilities.
|
||||||
|
type DeviceCapabilities struct {
|
||||||
|
Interfaces []DeviceCapabilityInterface `json:"interfaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device represents the device info.
|
||||||
|
type Device struct {
|
||||||
|
ErrorCodes []any `json:"errorCodes"`
|
||||||
|
Identification DeviceIdentification `json:"identification"`
|
||||||
|
Capabilities DeviceCapabilities `json:"capabilities"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemSTP represents system STP settings.
|
||||||
|
type SystemSTP struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
MaxAge int `json:"maxAge"`
|
||||||
|
HelloTime int `json:"helloTime"`
|
||||||
|
ForwardDelay int `json:"forwardDelay"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemUser represents a system user.
|
||||||
|
type SystemUser struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
ReadOnly bool `json:"readOnly"`
|
||||||
|
SSHKeys []any `json:"sshKeys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemManagement represents management settings.
|
||||||
|
type SystemManagement struct {
|
||||||
|
VlanID int `json:"vlanID"`
|
||||||
|
ManagementPortOnly bool `json:"managementPortOnly"`
|
||||||
|
Addresses []InterfaceAddress `json:"addresses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemAddress represents a system-level address configuration (DNS, Gateway).
|
||||||
|
type SystemAddress struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// System represents system information.
|
||||||
|
type System struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Timezone string `json:"timezone"`
|
||||||
|
DomainName string `json:"domainName"`
|
||||||
|
FactoryDefault bool `json:"factoryDefault"`
|
||||||
|
STP SystemSTP `json:"stp"`
|
||||||
|
AnalyticsEnabled bool `json:"analyticsEnabled"`
|
||||||
|
DNSServers []SystemAddress `json:"dnsServers"`
|
||||||
|
DefaultGateway []SystemAddress `json:"defaultGateway"`
|
||||||
|
Users []SystemUser `json:"users"`
|
||||||
|
Management SystemManagement `json:"management"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trunk represents a VLAN trunk.
|
||||||
|
type Trunk struct {
|
||||||
|
Interface InterfaceIdentification `json:"interface"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VlanParticipation represents interface participation in a VLAN.
|
||||||
|
type VlanParticipation struct {
|
||||||
|
Interface InterfaceIdentification `json:"interface"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vlan represents a VLAN.
|
||||||
|
type Vlan struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID int `json:"id"`
|
||||||
|
Participation []VlanParticipation `json:"participation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// VLANs represents the VLAN configuration.
|
||||||
|
type VLANs struct {
|
||||||
|
Trunks []Trunk `json:"trunks"`
|
||||||
|
Vlans []Vlan `json:"vlans"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceDiscoveryResponder ...
|
||||||
|
type ServiceDiscoveryResponder struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceSSHServer ...
|
||||||
|
type ServiceSSHServer struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
SSHPort int `json:"sshPort"`
|
||||||
|
PasswordAuthentication bool `json:"passwordAuthentication"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceTelnetServer ...
|
||||||
|
type ServiceTelnetServer struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceWebServer ...
|
||||||
|
type ServiceWebServer struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
HTTPPort int `json:"httpPort"`
|
||||||
|
HTTPSPort int `json:"httpsPort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceSystemLog ...
|
||||||
|
type ServiceSystemLog struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Server string `json:"server"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceNTPClient ...
|
||||||
|
type ServiceNTPClient struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
NTPServers []string `json:"ntpServers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceUNMS ...
|
||||||
|
type ServiceUNMS struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceLLDP ...
|
||||||
|
type ServiceLLDP struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceSNMPAgent ...
|
||||||
|
type ServiceSNMPAgent struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Community string `json:"community"`
|
||||||
|
Contact string `json:"contact"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DDNSClient represents a dynamic DNS client configuration.
|
||||||
|
type DDNSClient struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceDDNS represents dynamic DNS service configuration.
|
||||||
|
type ServiceDDNS struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Clients []DDNSClient `json:"clients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services represents services configuration.
|
||||||
|
type Services struct {
|
||||||
|
DiscoveryResponder ServiceDiscoveryResponder `json:"discoveryResponder"`
|
||||||
|
SSHServer ServiceSSHServer `json:"sshServer"`
|
||||||
|
TelnetServer ServiceTelnetServer `json:"telnetServer"`
|
||||||
|
WebServer ServiceWebServer `json:"webServer"`
|
||||||
|
SystemLog ServiceSystemLog `json:"systemLog"`
|
||||||
|
NTPClient ServiceNTPClient `json:"ntpClient"`
|
||||||
|
UNMS ServiceUNMS `json:"unms"`
|
||||||
|
LLDP ServiceLLDP `json:"lldp"`
|
||||||
|
SNMPAgent ServiceSNMPAgent `json:"snmpAgent"`
|
||||||
|
DDNS ServiceDDNS `json:"ddns"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceStatistics represents statistics for an interface.
|
||||||
|
type InterfaceStatistics struct {
|
||||||
|
Dropped int64 `json:"dropped"`
|
||||||
|
TxDropped int64 `json:"txDropped"`
|
||||||
|
RxDropped int64 `json:"rxDropped"`
|
||||||
|
Errors int64 `json:"errors"`
|
||||||
|
TxErrors int64 `json:"txErrors"`
|
||||||
|
RxErrors int64 `json:"rxErrors"`
|
||||||
|
Rate int64 `json:"rate"`
|
||||||
|
TxRate int64 `json:"txRate"`
|
||||||
|
RxRate int64 `json:"rxRate"`
|
||||||
|
Bytes int64 `json:"bytes"`
|
||||||
|
TxBytes int64 `json:"txBytes"`
|
||||||
|
RxBytes int64 `json:"rxBytes"`
|
||||||
|
Packets int64 `json:"packets"`
|
||||||
|
TxPackets int64 `json:"txPackets"`
|
||||||
|
RxPackets int64 `json:"rxPackets"`
|
||||||
|
PPS int64 `json:"pps"`
|
||||||
|
TxPPS int64 `json:"txPPS"`
|
||||||
|
RxPPS int64 `json:"rxPPS"`
|
||||||
|
TxBroadcast int64 `json:"txBroadcast"`
|
||||||
|
RxBroadcast int64 `json:"rxBroadcast"`
|
||||||
|
TxMulticast int64 `json:"txMulticast"`
|
||||||
|
RxMulticast int64 `json:"rxMulticast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceWithStats represents an interface within the statistics response.
|
||||||
|
type InterfaceWithStats struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Statistics InterfaceStatistics `json:"statistics"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPUStat represents CPU usage statistics.
|
||||||
|
type CPUStat struct {
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Usage int `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RAMStat represents RAM usage statistics.
|
||||||
|
type RAMStat struct {
|
||||||
|
Usage int64 `json:"usage"`
|
||||||
|
Free int64 `json:"free"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StorageStat represents storage usage statistics.
|
||||||
|
type StorageStat struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
SysName string `json:"sysName"`
|
||||||
|
Used int64 `json:"used"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceStats represents device level stats in the statistics response.
|
||||||
|
type DeviceStats struct {
|
||||||
|
CPU []CPUStat `json:"cpu"`
|
||||||
|
RAM RAMStat `json:"ram"`
|
||||||
|
Temperatures []any `json:"temperatures"`
|
||||||
|
Storage []StorageStat `json:"storage"`
|
||||||
|
Uptime int64 `json:"uptime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Statistics represents a statistics entry.
|
||||||
|
type Statistics struct {
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Device DeviceStats `json:"device"`
|
||||||
|
Interfaces []InterfaceWithStats `json:"interfaces"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NeighborAddress represents an address of a neighbor.
|
||||||
|
type NeighborAddress struct {
|
||||||
|
Mac string `json:"mac"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neighbor represents a discovered neighbor.
|
||||||
|
type Neighbor struct {
|
||||||
|
Mac string `json:"mac"`
|
||||||
|
Age int `json:"age"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
FW string `json:"fw"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Product string `json:"product"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Uptime int64 `json:"uptime"`
|
||||||
|
Configured bool `json:"configured"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
ZoneID string `json:"zoneID"`
|
||||||
|
Addresses []NeighborAddress `json:"addresses"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user