Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7661bff6f1 | |||
| b21475b487 | |||
| 195a9f7a9f | |||
| ecbf4d447c | |||
| 438d422b53 |
12
CHANGELOG.md
12
CHANGELOG.md
@@ -2,6 +2,16 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 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.
|
||||
@@ -21,4 +31,4 @@ All notable changes to this project will be documented in this file.
|
||||
## [v0.1.0] - 2026-01-04
|
||||
### Added
|
||||
- Initial CI pipeline setup.
|
||||
- Initial release of Ubiquiti EdgeOS Go Client.
|
||||
- Initial release of Ubiquiti toughswitch Go Client.
|
||||
|
||||
151
README.md
151
README.md
@@ -1,10 +1,17 @@
|
||||
# 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.
|
||||
|
||||
**⚠️ 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
|
||||
|
||||
### 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.
|
||||
- **Multi-Device Support**: Manage multiple devices with a single client instance.
|
||||
@@ -16,15 +23,33 @@ A Go client library for interacting with Ubiquiti EdgeOS devices (specifically t
|
||||
- **Statistics**: Real-time throughput, errors, and resource usage.
|
||||
- **Discovery**: Neighbor discovery via UBNT protocol.
|
||||
|
||||
### edgeos
|
||||
|
||||
A client library for interacting with Ubiquiti EdgeOS devices (EdgeRouter, EdgeSwitch) via their REST API.
|
||||
|
||||
#### Features
|
||||
|
||||
- **Authentication**: Handles login and session management automatically.
|
||||
- **Multi-Device Support**: Manage multiple devices with a single client instance.
|
||||
- **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.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get gitea.libretechconsulting.com/rmcguire/edgeos-client/pkg/edgeos
|
||||
# For ToughSwitch
|
||||
go get gitea.libretechconsulting.com/rmcguire/toughswitch-client/pkg/toughswitch
|
||||
|
||||
# For EdgeOS
|
||||
go get gitea.libretechconsulting.com/rmcguire/toughswitch-client/pkg/edgeos
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
### ToughSwitch Basic Example
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -35,14 +60,14 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/edgeos-client/pkg/edgeos"
|
||||
"gitea.libretechconsulting.com/rmcguire/toughswitch-client/pkg/toughswitch"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Configure your device(s)
|
||||
configs := []edgeos.Config{
|
||||
configs := []toughswitch.Config{
|
||||
{
|
||||
Host: "192.168.1.1",
|
||||
Username: "ubnt",
|
||||
@@ -53,7 +78,7 @@ func main() {
|
||||
}
|
||||
|
||||
// Initialize the client
|
||||
client := edgeos.MustNew(ctx, configs)
|
||||
client := toughswitch.MustNew(ctx, configs)
|
||||
|
||||
// Fetch system information
|
||||
deviceHost := "192.168.1.1"
|
||||
@@ -80,7 +105,73 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieving Statistics
|
||||
### EdgeOS Basic Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"gitea.libretechconsulting.com/rmcguire/toughswitch-client/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)
|
||||
|
||||
// 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
|
||||
stats, err := client.GetStatistics(ctx, "192.168.1.1")
|
||||
@@ -105,29 +196,48 @@ for _, stat := range stats {
|
||||
|
||||
### Working with Multiple Devices
|
||||
|
||||
The client is designed to handle multiple devices concurrently.
|
||||
Both clients are designed to handle multiple devices concurrently.
|
||||
|
||||
```go
|
||||
configs := []edgeos.Config{
|
||||
// ToughSwitch example
|
||||
configs := []toughswitch.Config{
|
||||
{Host: "192.168.1.1", ...},
|
||||
{Host: "192.168.1.2", ...},
|
||||
}
|
||||
client := edgeos.MustNew(ctx, configs)
|
||||
client := toughswitch.MustNew(ctx, configs)
|
||||
|
||||
// Get info for all configured devices in parallel
|
||||
allSystems, err := client.GetAllSystems(ctx)
|
||||
if err != nil {
|
||||
// Note: This returns partial results if available, check implementation
|
||||
log.Printf("Error fetching some systems: %v", err)
|
||||
}
|
||||
|
||||
for host, sys := range allSystems {
|
||||
fmt.Printf("[%s] Hostname: %s\n", host, sys.Hostname)
|
||||
}
|
||||
|
||||
// EdgeOS example
|
||||
edgeConfigs := []edgeos.Config{
|
||||
{Host: "192.168.2.1", ...},
|
||||
{Host: "192.168.2.2", ...},
|
||||
}
|
||||
edgeClient := edgeos.MustNew(ctx, edgeConfigs)
|
||||
|
||||
// Get config for all configured devices in parallel
|
||||
allConfigs, err := edgeClient.GetAllConfigs(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching some configs: %v", err)
|
||||
}
|
||||
|
||||
for host, cfg := range allConfigs {
|
||||
fmt.Printf("[%s] Hostname: %s\n", host, cfg.System.HostName)
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Endpoints
|
||||
|
||||
### ToughSwitch
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `GetSystem` | General system configuration and status |
|
||||
@@ -138,6 +248,19 @@ for host, sys := range allSystems {
|
||||
| `GetNeighbors` | Discovered UBNT neighbors |
|
||||
| `GetDevice` | Hardware and capabilities info |
|
||||
|
||||
All methods have corresponding `GetAll*` variants for multi-device operations.
|
||||
|
||||
### EdgeOS
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `GetConfig` | Complete device configuration |
|
||||
| `GetAuthInfo` | Device authentication and feature information |
|
||||
| `GetInterfaces` | Interface configuration (ethernet and switch) |
|
||||
| `GetSystem` | System configuration (hostname, domain) |
|
||||
|
||||
All methods have corresponding `GetAll*` variants for multi-device operations.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,3 +1,3 @@
|
||||
module gitea.libretechconsulting.com/rmcguire/edgeos-client
|
||||
module gitea.libretechconsulting.com/rmcguire/ubiquiti-clients
|
||||
|
||||
go 1.25.5
|
||||
|
||||
@@ -6,24 +6,129 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GetInterfaces retrieves the interfaces for a specific device.
|
||||
func (c *Client) GetInterfaces(ctx context.Context, host string) ([]Interface, error) {
|
||||
// GetConfig retrieves the complete device configuration from /api/edge/get.json for a specific device.
|
||||
func (c *Client) GetConfig(ctx context.Context, host string) (*ConfigData, 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 {
|
||||
var out ConfigResponse
|
||||
if err := d.do(ctx, "GET", "/api/edge/get.json", nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return out, nil
|
||||
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.
|
||||
func (c *Client) GetAllInterfaces(ctx context.Context) (map[string][]Interface, error) {
|
||||
results := make(map[string][]Interface)
|
||||
func (c *Client) GetAllInterfaces(ctx context.Context) (map[string]*InterfacesConfig, error) {
|
||||
results := make(map[string]*InterfacesConfig)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
@@ -55,73 +160,19 @@ func (c *Client) GetAllInterfaces(ctx context.Context) (map[string][]Interface,
|
||||
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)
|
||||
// GetSystem retrieves the system info for a specific device from the config data.
|
||||
func (c *Client) GetSystem(ctx context.Context, host string) (*SystemConfig, error) {
|
||||
config, err := c.GetConfig(ctx, 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
|
||||
return &config.System, nil
|
||||
}
|
||||
|
||||
// GetAllSystems retrieves system info for all devices.
|
||||
func (c *Client) GetAllSystems(ctx context.Context) (map[string]*System, error) {
|
||||
results := make(map[string]*System)
|
||||
func (c *Client) GetAllSystems(ctx context.Context) (map[string]*SystemConfig, error) {
|
||||
results := make(map[string]*SystemConfig)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
@@ -152,199 +203,3 @@ func (c *Client) GetAllSystems(ctx context.Context) (map[string]*System, error)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/*
|
||||
Package edgeos provides a client for interacting with Ubiquiti EdgeOS devices
|
||||
via their REST API. It supports authentication, token management, and
|
||||
retrieval of system, interface, VLAN, and discovery information from
|
||||
one or more devices.
|
||||
via their REST API. It supports authentication, session management, and
|
||||
retrieval of system and interface configuration from one or more devices.
|
||||
*/
|
||||
package edgeos
|
||||
|
||||
@@ -26,14 +25,14 @@ type Client struct {
|
||||
}
|
||||
|
||||
type deviceClient struct {
|
||||
config Config
|
||||
client *http.Client
|
||||
token string
|
||||
mu sync.Mutex
|
||||
config Config
|
||||
client *http.Client
|
||||
cookies []*http.Cookie
|
||||
authInfo *AuthResponse
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newDeviceClient(cfg Config) *deviceClient {
|
||||
// Ensure scheme is set
|
||||
if cfg.Scheme == "" {
|
||||
cfg.Scheme = "https"
|
||||
}
|
||||
@@ -124,31 +123,27 @@ 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)
|
||||
reqUrl := fmt.Sprintf("%s://%s/api/login2", d.config.Scheme, d.config.Host)
|
||||
|
||||
data := url.Values{}
|
||||
data.Set("username", d.config.Username)
|
||||
data.Set("password", d.config.Password)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", reqUrl, strings.NewReader(data.Encode()))
|
||||
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("Content-Type", "application/x-www-form-urlencoded")
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
respPayload, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -158,23 +153,27 @@ func (d *deviceClient) login(ctx context.Context) error {
|
||||
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")
|
||||
var authResp AuthResponse
|
||||
if err := json.Unmarshal(respPayload, &authResp); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -186,7 +185,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 {
|
||||
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
|
||||
if body != nil {
|
||||
@@ -197,23 +196,28 @@ func (d *deviceClient) doRequest(ctx context.Context, method, path string, body
|
||||
reqBody = bytes.NewBuffer(b)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
||||
req, err := http.NewRequestWithContext(ctx, method, reqUrl, reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
token := d.token
|
||||
cookies := d.cookies
|
||||
d.mu.Unlock()
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("x-auth-token", token)
|
||||
if len(cookies) > 0 {
|
||||
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 {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := d.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -226,7 +230,6 @@ func (d *deviceClient) doRequest(ctx context.Context, method, path string, body
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config represents the configuration for an EdgeOS device.
|
||||
type Config struct {
|
||||
Host string
|
||||
Scheme string
|
||||
@@ -13,6 +12,6 @@ type Config struct {
|
||||
Username string
|
||||
Password string
|
||||
Timeout time.Duration
|
||||
// Transport allows customizing the http transport (useful for testing)
|
||||
// Transport allows customizing the http transport (useful for testing or client middleware)
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
@@ -1,364 +1,100 @@
|
||||
package edgeos
|
||||
|
||||
// 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"`
|
||||
// AuthResponse represents the authentication response from login2 endpoint.
|
||||
type AuthResponse struct {
|
||||
Username string `json:"username"`
|
||||
PoE bool `json:"poe"`
|
||||
StatsURL string `json:"statsUrl"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// Features contains device feature information.
|
||||
type Features struct {
|
||||
Model string `json:"model"`
|
||||
PoECap map[string]string `json:"poe_cap"`
|
||||
Switch SwitchFeatures `json:"switch"`
|
||||
SwitchIsVLANCapable bool `json:"switchIsVLANCapable"`
|
||||
PoE bool `json:"poe"`
|
||||
Ports int `json:"ports"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// SwitchFeatures contains switch-specific features.
|
||||
type SwitchFeatures struct {
|
||||
Ports []string `json:"ports"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// ConfigResponse represents the response from /api/edge/get.json.
|
||||
type ConfigResponse struct {
|
||||
SessionID string `json:"SESSION_ID"`
|
||||
GET ConfigData `json:"GET"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// ConfigData contains the full device configuration.
|
||||
type ConfigData struct {
|
||||
Interfaces InterfacesConfig `json:"interfaces"`
|
||||
System SystemConfig `json:"system"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// InterfacesConfig represents the interfaces configuration.
|
||||
type InterfacesConfig struct {
|
||||
Ethernet map[string]EthernetConfig `json:"ethernet,omitempty"`
|
||||
Switch map[string]SwitchConfig `json:"switch,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// EthernetConfig represents an ethernet interface configuration.
|
||||
type EthernetConfig struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Duplex string `json:"duplex,omitempty"`
|
||||
Speed string `json:"speed,omitempty"`
|
||||
PoE *PoEState `json:"poe,omitempty"`
|
||||
}
|
||||
|
||||
// Interface represents a network interface.
|
||||
type Interface struct {
|
||||
Identification InterfaceIdentification `json:"identification"`
|
||||
Status InterfaceStatus `json:"status"`
|
||||
Addresses []InterfaceAddress `json:"addresses"`
|
||||
Port InterfacePort `json:"port"`
|
||||
// PoEState represents PoE output state.
|
||||
type PoEState struct {
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// SwitchConfig represents a switch interface configuration.
|
||||
type SwitchConfig struct {
|
||||
Address []string `json:"address,omitempty"`
|
||||
MTU string `json:"mtu,omitempty"`
|
||||
SwitchPort *SwitchPortConfig `json:"switch-port,omitempty"`
|
||||
VIF map[string]VIFConfig `json:"vif,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// SwitchPortConfig represents switch port configuration with VLAN awareness.
|
||||
type SwitchPortConfig struct {
|
||||
Interface map[string]InterfaceVLAN `json:"interface,omitempty"`
|
||||
VLANAware string `json:"vlan-aware,omitempty"`
|
||||
}
|
||||
|
||||
// DeviceCapabilities represents device capabilities.
|
||||
type DeviceCapabilities struct {
|
||||
Interfaces []DeviceCapabilityInterface `json:"interfaces"`
|
||||
// InterfaceVLAN represents VLAN configuration for an interface.
|
||||
type InterfaceVLAN struct {
|
||||
VLAN VLANConfig `json:"vlan,omitempty"`
|
||||
}
|
||||
|
||||
// Device represents the device info.
|
||||
type Device struct {
|
||||
ErrorCodes []any `json:"errorCodes"`
|
||||
Identification DeviceIdentification `json:"identification"`
|
||||
Capabilities DeviceCapabilities `json:"capabilities"`
|
||||
// VLANConfig represents VLAN ID configuration.
|
||||
type VLANConfig struct {
|
||||
PVID string `json:"pvid,omitempty"`
|
||||
VID []string `json:"vid,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// VIFConfig represents a virtual interface (VLAN) configuration.
|
||||
type VIFConfig struct {
|
||||
Address []string `json:"address,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
MTU string `json:"mtu,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
// SystemConfig contains system configuration.
|
||||
type SystemConfig struct {
|
||||
HostName string `json:"host-name,omitempty"`
|
||||
DomainName string `json:"domain-name,omitempty"`
|
||||
}
|
||||
|
||||
350
pkg/toughswitch/api.go
Normal file
350
pkg/toughswitch/api.go
Normal file
@@ -0,0 +1,350 @@
|
||||
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
|
||||
}
|
||||
241
pkg/toughswitch/client.go
Normal file
241
pkg/toughswitch/client.go
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package edgeos
|
||||
package toughswitch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -34,11 +34,9 @@ func TestClient_ThreadSafety(t *testing.T) {
|
||||
start := make(chan struct{})
|
||||
|
||||
// Writer: Adds and deletes hosts
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
<-start
|
||||
for i := 0; i < 100; i++ {
|
||||
for i := range 100 {
|
||||
host := fmt.Sprintf("host-%d", i)
|
||||
cfg := &Config{
|
||||
Host: host,
|
||||
@@ -54,20 +52,18 @@ func TestClient_ThreadSafety(t *testing.T) {
|
||||
t.Logf("Del error: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
// Reader: Iterates hosts
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
<-start
|
||||
for i := 0; i < 10; i++ {
|
||||
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()
|
||||
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
|
||||
}
|
||||
364
pkg/toughswitch/types.go
Normal file
364
pkg/toughswitch/types.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package toughswitch
|
||||
|
||||
// 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