add interface stats and poe power monitoring via snmp
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
55
nodes.go
55
nodes.go
@@ -14,6 +14,19 @@ type Interface struct {
|
|||||||
Name string
|
Name string
|
||||||
MAC net.HardwareAddr
|
MAC net.HardwareAddr
|
||||||
IPs map[string]net.IP
|
IPs map[string]net.IP
|
||||||
|
Stats *InterfaceStats
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterfaceStats struct {
|
||||||
|
Speed uint64 // bits per second
|
||||||
|
InErrors uint64
|
||||||
|
OutErrors uint64
|
||||||
|
PoE *PoEStats
|
||||||
|
}
|
||||||
|
|
||||||
|
type PoEStats struct {
|
||||||
|
Power float64 // watts in use
|
||||||
|
MaxPower float64 // watts allocated/negotiated
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Interface) String() string {
|
func (i *Interface) String() string {
|
||||||
@@ -31,6 +44,9 @@ func (i *Interface) String() string {
|
|||||||
if len(ips) > 0 {
|
if len(ips) > 0 {
|
||||||
parts = append(parts, fmt.Sprintf("%v", ips))
|
parts = append(parts, fmt.Sprintf("%v", ips))
|
||||||
}
|
}
|
||||||
|
if i.Stats != nil {
|
||||||
|
parts = append(parts, i.Stats.String())
|
||||||
|
}
|
||||||
|
|
||||||
result := parts[0]
|
result := parts[0]
|
||||||
for _, p := range parts[1:] {
|
for _, p := range parts[1:] {
|
||||||
@@ -39,6 +55,45 @@ func (i *Interface) String() string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InterfaceStats) String() string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if s.Speed > 0 {
|
||||||
|
if s.Speed >= 1000000000 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%dG", s.Speed/1000000000))
|
||||||
|
} else if s.Speed >= 1000000 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%dM", s.Speed/1000000))
|
||||||
|
} else {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d", s.Speed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.InErrors > 0 || s.OutErrors > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("err:%d/%d", s.InErrors, s.OutErrors))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.PoE != nil {
|
||||||
|
if s.PoE.MaxPower > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("poe:%.1f/%.1fW", s.PoE.Power, s.PoE.MaxPower))
|
||||||
|
} else {
|
||||||
|
parts = append(parts, fmt.Sprintf("poe:%.1fW", s.PoE.Power))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "[" + fmt.Sprintf("%s", joinParts(parts)) + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinParts(parts []string) string {
|
||||||
|
result := ""
|
||||||
|
for i, p := range parts {
|
||||||
|
if i > 0 {
|
||||||
|
result += " "
|
||||||
|
}
|
||||||
|
result += p
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Name string
|
Name string
|
||||||
Interfaces map[string]*Interface
|
Interfaces map[string]*Interface
|
||||||
|
|||||||
187
snmp.go
187
snmp.go
@@ -124,6 +124,7 @@ func (t *Tendrils) querySNMPDevice(node *Node, ip net.IP) {
|
|||||||
|
|
||||||
t.querySysName(snmp, node)
|
t.querySysName(snmp, node)
|
||||||
t.queryInterfaceMACs(snmp, node)
|
t.queryInterfaceMACs(snmp, node)
|
||||||
|
t.queryInterfaceStats(snmp, node)
|
||||||
t.queryBridgeMIB(snmp, node)
|
t.queryBridgeMIB(snmp, node)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +193,192 @@ func (t *Tendrils) queryInterfaceMACs(snmp *gosnmp.GoSNMP, node *Node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node) {
|
||||||
|
ifNames := t.getInterfaceNames(snmp)
|
||||||
|
|
||||||
|
ifOperStatus := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.8")
|
||||||
|
ifHighSpeed := t.getInterfaceTable(snmp, "1.3.6.1.2.1.31.1.1.1.15")
|
||||||
|
ifInErrors := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.14")
|
||||||
|
ifOutErrors := t.getInterfaceTable(snmp, "1.3.6.1.2.1.2.2.1.20")
|
||||||
|
|
||||||
|
poeStats := t.getPoEStats(snmp, ifNames)
|
||||||
|
|
||||||
|
t.nodes.mu.Lock()
|
||||||
|
defer t.nodes.mu.Unlock()
|
||||||
|
|
||||||
|
for ifIndex, name := range ifNames {
|
||||||
|
name = rewritePortName(name)
|
||||||
|
iface, exists := node.Interfaces[name]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
status, hasStatus := ifOperStatus[ifIndex]
|
||||||
|
isUp := hasStatus && status == 1
|
||||||
|
if !isUp {
|
||||||
|
iface.Stats = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := &InterfaceStats{}
|
||||||
|
|
||||||
|
if speed, ok := ifHighSpeed[ifIndex]; ok {
|
||||||
|
stats.Speed = uint64(speed) * 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
if inErr, ok := ifInErrors[ifIndex]; ok {
|
||||||
|
stats.InErrors = uint64(inErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if outErr, ok := ifOutErrors[ifIndex]; ok {
|
||||||
|
stats.OutErrors = uint64(outErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if poe, ok := poeStats[name]; ok {
|
||||||
|
stats.PoE = poe
|
||||||
|
}
|
||||||
|
|
||||||
|
iface.Stats = stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getInterfaceTable(snmp *gosnmp.GoSNMP, oid string) map[int]int {
|
||||||
|
results, err := snmp.BulkWalkAll(oid)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table := map[int]int{}
|
||||||
|
for _, result := range results {
|
||||||
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
||||||
|
var ifIndex int
|
||||||
|
if _, err := fmt.Sscanf(oidSuffix, "%d", &ifIndex); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var value int
|
||||||
|
switch v := result.Value.(type) {
|
||||||
|
case int:
|
||||||
|
value = v
|
||||||
|
case uint:
|
||||||
|
value = int(v)
|
||||||
|
case int64:
|
||||||
|
value = int(v)
|
||||||
|
case uint64:
|
||||||
|
value = int(v)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
table[ifIndex] = value
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getPoEStats(snmp *gosnmp.GoSNMP, ifNames map[int]string) map[string]*PoEStats {
|
||||||
|
statusOID := "1.3.6.1.2.1.105.1.1.1.6"
|
||||||
|
|
||||||
|
statusResults, err := snmp.BulkWalkAll(statusOID)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
powerTable := t.getPoETable(snmp, "1.3.6.1.4.1.4526.10.15.1.1.1.2")
|
||||||
|
maxPowerTable := t.getPoETable(snmp, "1.3.6.1.4.1.4526.10.15.1.1.1.1")
|
||||||
|
|
||||||
|
stats := map[string]*PoEStats{}
|
||||||
|
for _, result := range statusResults {
|
||||||
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+statusOID), ".")
|
||||||
|
parts := strings.Split(oidSuffix, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var portIndex int
|
||||||
|
if _, err := fmt.Sscanf(parts[1], "%d", &portIndex); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var status int
|
||||||
|
switch v := result.Value.(type) {
|
||||||
|
case int:
|
||||||
|
status = v
|
||||||
|
case uint:
|
||||||
|
status = int(v)
|
||||||
|
case int64:
|
||||||
|
status = int(v)
|
||||||
|
case uint64:
|
||||||
|
status = int(v)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ifName := rewritePortName(ifNames[portIndex])
|
||||||
|
if ifName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
poe := &PoEStats{}
|
||||||
|
if power, ok := powerTable[portIndex]; ok {
|
||||||
|
poe.Power = float64(power) / 1000.0
|
||||||
|
}
|
||||||
|
if maxPower, ok := maxPowerTable[portIndex]; ok {
|
||||||
|
poe.MaxPower = float64(maxPower) / 1000.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.DebugSNMP {
|
||||||
|
log.Printf("[snmp] %s: poe port %d (%s) power=%v maxpower=%v", snmp.Target, portIndex, ifName, powerTable[portIndex], maxPowerTable[portIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
if poe.Power > 0 || poe.MaxPower > 0 {
|
||||||
|
stats[ifName] = poe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) getPoETable(snmp *gosnmp.GoSNMP, oid string) map[int]int {
|
||||||
|
results, err := snmp.BulkWalkAll(oid)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
table := map[int]int{}
|
||||||
|
for _, result := range results {
|
||||||
|
oidSuffix := strings.TrimPrefix(strings.TrimPrefix(result.Name, "."+oid), ".")
|
||||||
|
parts := strings.Split(oidSuffix, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var portIndex int
|
||||||
|
if _, err := fmt.Sscanf(parts[1], "%d", &portIndex); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var value int
|
||||||
|
switch v := result.Value.(type) {
|
||||||
|
case int:
|
||||||
|
value = v
|
||||||
|
case uint:
|
||||||
|
value = int(v)
|
||||||
|
case int64:
|
||||||
|
value = int(v)
|
||||||
|
case uint64:
|
||||||
|
value = int(v)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
table[portIndex] = value
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, node *Node) {
|
func (t *Tendrils) queryBridgeMIB(snmp *gosnmp.GoSNMP, node *Node) {
|
||||||
portOID := "1.3.6.1.2.1.17.7.1.2.2.1.2"
|
portOID := "1.3.6.1.2.1.17.7.1.2.2.1.2"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user