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
|
||||
MAC net.HardwareAddr
|
||||
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 {
|
||||
@@ -31,6 +44,9 @@ func (i *Interface) String() string {
|
||||
if len(ips) > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%v", ips))
|
||||
}
|
||||
if i.Stats != nil {
|
||||
parts = append(parts, i.Stats.String())
|
||||
}
|
||||
|
||||
result := parts[0]
|
||||
for _, p := range parts[1:] {
|
||||
@@ -39,6 +55,45 @@ func (i *Interface) String() string {
|
||||
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 {
|
||||
Name string
|
||||
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.queryInterfaceMACs(snmp, node)
|
||||
t.queryInterfaceStats(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) {
|
||||
portOID := "1.3.6.1.2.1.17.7.1.2.2.1.2"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user