From d2a09250d0340c2d24ccdf0eb10e16b7dc9ee395 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Thu, 22 Jan 2026 22:56:47 -0800 Subject: [PATCH] add switch-wide poe budget tracking via snmp Co-Authored-By: Claude Opus 4.5 --- nodes.go | 23 +++++++++++++++++++++-- snmp.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/nodes.go b/nodes.go index b7db5bd..a3d2a02 100644 --- a/nodes.go +++ b/nodes.go @@ -94,10 +94,16 @@ func joinParts(parts []string) string { return result } +type PoEBudget struct { + Power float64 // watts in use + MaxPower float64 // watts total budget +} + type Node struct { Name string Interfaces map[string]*Interface MACTable map[string]string // peer MAC -> local interface name + PoEBudget *PoEBudget } func (n *Node) String() string { @@ -106,13 +112,22 @@ func (n *Node) String() string { name = "??" } + var parts []string + parts = append(parts, name) + + if n.PoEBudget != nil { + parts = append(parts, fmt.Sprintf("[poe:%.0f/%.0fW]", n.PoEBudget.Power, n.PoEBudget.MaxPower)) + } + var ifaces []string for _, iface := range n.Interfaces { ifaces = append(ifaces, iface.String()) } sort.Slice(ifaces, func(i, j int) bool { return sortorder.NaturalLess(ifaces[i], ifaces[j]) }) - return fmt.Sprintf("%s {%v}", name, ifaces) + parts = append(parts, fmt.Sprintf("{%v}", ifaces)) + + return joinParts(parts) } type Nodes struct { @@ -365,7 +380,11 @@ func (n *Nodes) logNode(node *Node) { if name == "" { name = "??" } - log.Printf("[node] %s", name) + if node.PoEBudget != nil { + log.Printf("[node] %s [poe:%.0f/%.0fW]", name, node.PoEBudget.Power, node.PoEBudget.MaxPower) + } else { + log.Printf("[node] %s", name) + } var ifaceKeys []string for ifaceKey := range node.Interfaces { diff --git a/snmp.go b/snmp.go index 77efccf..45006ea 100644 --- a/snmp.go +++ b/snmp.go @@ -125,6 +125,7 @@ func (t *Tendrils) querySNMPDevice(node *Node, ip net.IP) { t.querySysName(snmp, node) t.queryInterfaceMACs(snmp, node) t.queryInterfaceStats(snmp, node) + t.queryPoEBudget(snmp, node) t.queryBridgeMIB(snmp, node) } @@ -242,6 +243,45 @@ func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node) { } } +func (t *Tendrils) queryPoEBudget(snmp *gosnmp.GoSNMP, node *Node) { + maxPowerOID := "1.3.6.1.2.1.105.1.3.1.1.2.1" + powerOID := "1.3.6.1.2.1.105.1.3.1.1.4.1" + + result, err := snmp.Get([]string{maxPowerOID, powerOID}) + if err != nil { + return + } + + var power, maxPower float64 + for _, v := range result.Variables { + var val int + switch x := v.Value.(type) { + case int: + val = x + case uint: + val = int(x) + case int64: + val = int(x) + case uint64: + val = int(x) + default: + continue + } + + if v.Name == "."+powerOID { + power = float64(val) + } else if v.Name == "."+maxPowerOID { + maxPower = float64(val) + } + } + + if maxPower > 0 { + t.nodes.mu.Lock() + node.PoEBudget = &PoEBudget{Power: power, MaxPower: maxPower} + t.nodes.mu.Unlock() + } +} + func (t *Tendrils) getInterfaceTable(snmp *gosnmp.GoSNMP, oid string) map[int]int { results, err := snmp.BulkWalkAll(oid) if err != nil {