- InterfaceInfo struct for link discovery (name, mac, type, running) - CollectInterfaceInfo runs /interface/print (version-agnostic) - MAC addresses lowercased for consistent matching - Entries without mac-address skipped (loopback, bridge) - Preserved existing InterfaceStats traffic counter collector Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
124 lines
3.3 KiB
Go
124 lines
3.3 KiB
Go
package device
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
"strings"
|
|
|
|
routeros "github.com/go-routeros/routeros/v3"
|
|
)
|
|
|
|
// InterfaceStats holds the traffic counters for a single RouterOS interface.
|
|
type InterfaceStats struct {
|
|
Name string `json:"name"`
|
|
RxBytes int64 `json:"rx_bytes"`
|
|
TxBytes int64 `json:"tx_bytes"`
|
|
Running bool `json:"running"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// CollectInterfaces queries the RouterOS device for per-interface traffic
|
|
// counters via /interface/print.
|
|
//
|
|
// Returns a slice of InterfaceStats. On error, returns an empty slice and the
|
|
// error — the caller decides whether to skip the device or log a warning.
|
|
func CollectInterfaces(client *routeros.Client) ([]InterfaceStats, error) {
|
|
reply, err := client.Run(
|
|
"/interface/print",
|
|
"=.proplist=name,rx-byte,tx-byte,running,type",
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("running /interface/print: %w", err)
|
|
}
|
|
|
|
stats := make([]InterfaceStats, 0, len(reply.Re))
|
|
for _, sentence := range reply.Re {
|
|
m := sentence.Map
|
|
|
|
rxBytes, err := strconv.ParseInt(m["rx-byte"], 10, 64)
|
|
if err != nil {
|
|
slog.Warn("could not parse rx-byte for interface", "interface", m["name"], "value", m["rx-byte"])
|
|
rxBytes = 0
|
|
}
|
|
|
|
txBytes, err := strconv.ParseInt(m["tx-byte"], 10, 64)
|
|
if err != nil {
|
|
slog.Warn("could not parse tx-byte for interface", "interface", m["name"], "value", m["tx-byte"])
|
|
txBytes = 0
|
|
}
|
|
|
|
stats = append(stats, InterfaceStats{
|
|
Name: m["name"],
|
|
RxBytes: rxBytes,
|
|
TxBytes: txBytes,
|
|
Running: m["running"] == "true",
|
|
Type: m["type"],
|
|
})
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// InterfaceInfo holds basic interface identity data from a RouterOS device.
|
|
// This is used for link discovery — MAC addresses identify which device owns
|
|
// each end of a network link.
|
|
type InterfaceInfo struct {
|
|
Name string `json:"name"`
|
|
MacAddress string `json:"mac_address"`
|
|
Type string `json:"type"`
|
|
Running bool `json:"running"`
|
|
}
|
|
|
|
// normalizeMACAddress lowercases a MAC address for consistent matching.
|
|
func normalizeMACAddress(mac string) string {
|
|
return strings.ToLower(mac)
|
|
}
|
|
|
|
// parseRunning converts a RouterOS "true"/"false" string to a Go bool.
|
|
func parseRunning(s string) bool {
|
|
return s == "true"
|
|
}
|
|
|
|
// CollectInterfaceInfo queries the RouterOS device for all interfaces and
|
|
// returns their name, MAC address, type, and running status.
|
|
//
|
|
// The /interface/print command is version-agnostic (works on both v6 and v7).
|
|
// Entries with an empty mac-address (loopback, bridge without MAC) are skipped.
|
|
//
|
|
// Returns nil, nil when the device has no interfaces (matching the
|
|
// CollectWireless pattern — empty result is not an error).
|
|
func CollectInterfaceInfo(client *routeros.Client) ([]InterfaceInfo, error) {
|
|
reply, err := client.Run("/interface/print")
|
|
if err != nil {
|
|
slog.Debug("failed to collect interface info", "error", err)
|
|
return nil, nil
|
|
}
|
|
|
|
if len(reply.Re) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
result := make([]InterfaceInfo, 0, len(reply.Re))
|
|
for _, s := range reply.Re {
|
|
m := s.Map
|
|
mac := m["mac-address"]
|
|
if mac == "" {
|
|
continue
|
|
}
|
|
|
|
result = append(result, InterfaceInfo{
|
|
Name: m["name"],
|
|
MacAddress: normalizeMACAddress(mac),
|
|
Type: m["type"],
|
|
Running: parseRunning(m["running"]),
|
|
})
|
|
}
|
|
|
|
if len(result) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return result, nil
|
|
}
|