Initial commit: IT Nexus Web-App
This commit is contained in:
334
nexus-scanner/internal/modules/snmpmod/snmp.go
Normal file
334
nexus-scanner/internal/modules/snmpmod/snmp.go
Normal file
@@ -0,0 +1,334 @@
|
||||
// Package snmpmod polls SNMP targets for device information.
|
||||
// Supports generic devices, printers (toner/pages) and switches.
|
||||
package snmpmod
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gosnmp/gosnmp"
|
||||
|
||||
"github.com/cereda-systems/nexus-scanner/internal/config"
|
||||
"github.com/cereda-systems/nexus-scanner/internal/db"
|
||||
)
|
||||
|
||||
const modName = "snmp"
|
||||
|
||||
// OIDs queried for every device.
|
||||
var baseOIDs = map[string]string{
|
||||
"sysDescr": "1.3.6.1.2.1.1.1.0",
|
||||
"sysUpTime": "1.3.6.1.2.1.1.3.0",
|
||||
"sysName": "1.3.6.1.2.1.1.5.0",
|
||||
"sysLocation": "1.3.6.1.2.1.1.6.0",
|
||||
"sysContact": "1.3.6.1.2.1.1.4.0",
|
||||
"ifNumber": "1.3.6.1.2.1.2.1.0",
|
||||
}
|
||||
|
||||
// Switch-specific scalar OIDs (queried via Get).
|
||||
var switchScalarOIDs = map[string]string{
|
||||
"cpuLoad": "1.3.6.1.2.1.25.3.3.1.2.1", // hrProcessorLoad (first CPU)
|
||||
}
|
||||
|
||||
// Printer-specific OIDs (Kyocera / most RFC 3805 printers).
|
||||
var printerOIDs = map[string]string{
|
||||
"tonerLevel": "1.3.6.1.2.1.43.11.1.1.9.1.1",
|
||||
"tonerMax": "1.3.6.1.2.1.43.11.1.1.8.1.1",
|
||||
"pageCount": "1.3.6.1.2.1.43.10.2.1.4.1.1",
|
||||
"printerStatus": "1.3.6.1.2.1.25.3.5.1.1.1",
|
||||
}
|
||||
|
||||
// Walk base OIDs for interfaces and memory.
|
||||
const (
|
||||
oidIfOperStatus = "1.3.6.1.2.1.2.2.1.8" // 1=up, 2=down
|
||||
oidIfHCInOctets = "1.3.6.1.2.1.31.1.1.1.6" // 64-bit in bytes
|
||||
oidIfHCOutOctets = "1.3.6.1.2.1.31.1.1.1.10" // 64-bit out bytes
|
||||
oidIfDescr = "1.3.6.1.2.1.2.2.1.2" // interface name
|
||||
oidHrStorageDescr = "1.3.6.1.2.1.25.2.3.1.2"
|
||||
oidHrStorageUsed = "1.3.6.1.2.1.25.2.3.1.6"
|
||||
oidHrStorageSize = "1.3.6.1.2.1.25.2.3.1.5"
|
||||
oidHrStorageAlloc = "1.3.6.1.2.1.25.2.3.1.4"
|
||||
)
|
||||
|
||||
// Module polls SNMP targets and stores results.
|
||||
type Module struct {
|
||||
site string
|
||||
cfg config.SNMPConfig
|
||||
store *db.Store
|
||||
}
|
||||
|
||||
// New creates a new SNMP module.
|
||||
func New(site string, cfg config.SNMPConfig, store *db.Store) *Module {
|
||||
return &Module{site: site, cfg: cfg, store: store}
|
||||
}
|
||||
|
||||
func (m *Module) Name() string { return modName }
|
||||
func (m *Module) Interval() time.Duration { return m.cfg.Interval }
|
||||
|
||||
// Run polls all enabled SNMP targets.
|
||||
func (m *Module) Run(ctx context.Context) error {
|
||||
targets, err := m.store.ListSNMPTargets()
|
||||
if err != nil {
|
||||
return fmt.Errorf("list snmp targets: %w", err)
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
scanID, err := m.store.BeginScan(modName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin scan: %w", err)
|
||||
}
|
||||
|
||||
ok := 0
|
||||
for _, t := range targets {
|
||||
if !t.Enabled {
|
||||
continue
|
||||
}
|
||||
if err := m.pollTarget(ctx, t); err != nil {
|
||||
slog.Warn("snmp poll failed", "target", t.Name, "ip", t.IP, "err", err)
|
||||
} else {
|
||||
ok++
|
||||
}
|
||||
}
|
||||
|
||||
_ = m.store.EndScan(scanID, nil)
|
||||
slog.Info("snmp poll complete", "ok", ok, "total", len(targets))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) pollTarget(ctx context.Context, t db.SNMPTarget) error {
|
||||
community := t.Community
|
||||
if community == "" {
|
||||
community = m.cfg.Community
|
||||
}
|
||||
|
||||
gs := &gosnmp.GoSNMP{
|
||||
Target: t.IP,
|
||||
Port: 161,
|
||||
Community: community,
|
||||
Version: snmpVersion(t.Version),
|
||||
Timeout: 10 * time.Second,
|
||||
Retries: 1,
|
||||
MaxOids: 60,
|
||||
}
|
||||
if err := gs.Connect(); err != nil {
|
||||
return fmt.Errorf("connect: %w", err)
|
||||
}
|
||||
defer gs.Conn.Close()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// --- Base + type-specific scalar GET ---
|
||||
oids := make(map[string]string)
|
||||
for k, v := range baseOIDs {
|
||||
oids[k] = v
|
||||
}
|
||||
switch t.Type {
|
||||
case "printer":
|
||||
for k, v := range printerOIDs {
|
||||
oids[k] = v
|
||||
}
|
||||
case "switch":
|
||||
for k, v := range switchScalarOIDs {
|
||||
oids[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
oidList := make([]string, 0, len(oids))
|
||||
reverseMap := make(map[string]string)
|
||||
for name, oid := range oids {
|
||||
oidList = append(oidList, oid)
|
||||
reverseMap[oid] = name
|
||||
}
|
||||
|
||||
result, err := gs.Get(oidList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get: %w", err)
|
||||
}
|
||||
|
||||
for _, pdu := range result.Variables {
|
||||
name := resolveOID(pdu.Name, reverseMap)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
value := pduToString(pdu)
|
||||
if value == "" || value == "<nil>" || strings.HasPrefix(value, "<nil>") {
|
||||
continue
|
||||
}
|
||||
_ = m.store.UpsertSNMPResult(db.SNMPResult{
|
||||
TargetID: t.ID,
|
||||
OIDName: name,
|
||||
Value: value,
|
||||
ScannedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Switch: walk interface table + memory ---
|
||||
if t.Type == "switch" {
|
||||
m.walkSwitch(gs, t, now)
|
||||
}
|
||||
|
||||
slog.Debug("snmp polled", "target", t.Name, "ip", t.IP, "type", t.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
// walkSwitch collects interface stats and memory for switch-type targets.
|
||||
func (m *Module) walkSwitch(gs *gosnmp.GoSNMP, t db.SNMPTarget, now time.Time) {
|
||||
// Interface status walk → portsUp / portsTotal
|
||||
var portsUp, portsTotal int
|
||||
var trafficIn, trafficOut uint64
|
||||
|
||||
ifStatus := walkOID(gs, oidIfOperStatus)
|
||||
for _, v := range ifStatus {
|
||||
portsTotal++
|
||||
if v == "1" {
|
||||
portsUp++
|
||||
}
|
||||
}
|
||||
|
||||
// Traffic counters (64-bit)
|
||||
for _, v := range walkOID(gs, oidIfHCInOctets) {
|
||||
n, _ := strconv.ParseUint(v, 10, 64)
|
||||
trafficIn += n
|
||||
}
|
||||
for _, v := range walkOID(gs, oidIfHCOutOctets) {
|
||||
n, _ := strconv.ParseUint(v, 10, 64)
|
||||
trafficOut += n
|
||||
}
|
||||
|
||||
save := func(name, value string) {
|
||||
_ = m.store.UpsertSNMPResult(db.SNMPResult{
|
||||
TargetID: t.ID,
|
||||
OIDName: name,
|
||||
Value: value,
|
||||
ScannedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
if portsTotal > 0 {
|
||||
save("portsTotal", strconv.Itoa(portsTotal))
|
||||
save("portsUp", strconv.Itoa(portsUp))
|
||||
}
|
||||
if trafficIn > 0 {
|
||||
save("trafficIn", strconv.FormatUint(trafficIn, 10))
|
||||
}
|
||||
if trafficOut > 0 {
|
||||
save("trafficOut", strconv.FormatUint(trafficOut, 10))
|
||||
}
|
||||
|
||||
// Memory: find "Physical Memory" or "Real Memory" row
|
||||
descrByIdx := walkOID(gs, oidHrStorageDescr)
|
||||
usedByIdx := walkOID(gs, oidHrStorageUsed)
|
||||
sizeByIdx := walkOID(gs, oidHrStorageSize)
|
||||
allocByIdx := walkOID(gs, oidHrStorageAlloc)
|
||||
|
||||
for idx, descr := range descrByIdx {
|
||||
d := strings.ToLower(descr)
|
||||
if strings.Contains(d, "physical") || strings.Contains(d, "real") || strings.Contains(d, "ram") {
|
||||
used, _ := strconv.ParseInt(usedByIdx[idx], 10, 64)
|
||||
size, _ := strconv.ParseInt(sizeByIdx[idx], 10, 64)
|
||||
alloc, _ := strconv.ParseInt(allocByIdx[idx], 10, 64)
|
||||
if alloc <= 0 {
|
||||
alloc = 1024
|
||||
}
|
||||
usedBytes := used * alloc
|
||||
totalBytes := size * alloc
|
||||
if totalBytes > 0 {
|
||||
save("memUsed", strconv.FormatInt(usedBytes, 10))
|
||||
save("memTotal", strconv.FormatInt(totalBytes, 10))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// walkOID performs an SNMP walk and returns a map of last-OID-index → value string.
|
||||
func walkOID(gs *gosnmp.GoSNMP, baseOID string) map[string]string {
|
||||
out := make(map[string]string)
|
||||
err := gs.Walk(baseOID, func(pdu gosnmp.SnmpPDU) error {
|
||||
// Extract the index (last component of OID)
|
||||
name := strings.TrimPrefix(pdu.Name, ".")
|
||||
parts := strings.Split(name, ".")
|
||||
idx := parts[len(parts)-1]
|
||||
val := pduRawToString(pdu)
|
||||
if val != "" {
|
||||
out[idx] = val
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
slog.Debug("snmp walk failed", "oid", baseOID, "err", err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resolveOID(oidName string, reverseMap map[string]string) string {
|
||||
key := strings.TrimPrefix(oidName, ".")
|
||||
if name := reverseMap[key]; name != "" {
|
||||
return name
|
||||
}
|
||||
// Try without trailing .0
|
||||
if len(key) > 2 && key[len(key)-2:] == ".0" {
|
||||
return reverseMap[key[:len(key)-2]]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func snmpVersion(v string) gosnmp.SnmpVersion {
|
||||
switch v {
|
||||
case "v1":
|
||||
return gosnmp.Version1
|
||||
case "v3":
|
||||
return gosnmp.Version3
|
||||
default:
|
||||
return gosnmp.Version2c
|
||||
}
|
||||
}
|
||||
|
||||
// pduToString converts a PDU value to a display string (formatted).
|
||||
func pduToString(pdu gosnmp.SnmpPDU) string {
|
||||
switch pdu.Type {
|
||||
case gosnmp.OctetString:
|
||||
if b, ok := pdu.Value.([]byte); ok {
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
case gosnmp.TimeTicks:
|
||||
ticks, _ := pdu.Value.(uint32)
|
||||
d := time.Duration(ticks) * 10 * time.Millisecond
|
||||
h := int(d.Hours())
|
||||
mn := int(d.Minutes()) % 60
|
||||
return strconv.Itoa(h) + "h " + strconv.Itoa(mn) + "m"
|
||||
case gosnmp.ObjectIdentifier:
|
||||
if s, ok := pdu.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
default:
|
||||
return fmt.Sprintf("%v", pdu.Value)
|
||||
}
|
||||
return fmt.Sprintf("%v", pdu.Value)
|
||||
}
|
||||
|
||||
// pduRawToString converts a PDU value to a raw numeric/string value (for walks).
|
||||
func pduRawToString(pdu gosnmp.SnmpPDU) string {
|
||||
switch pdu.Type {
|
||||
case gosnmp.OctetString:
|
||||
if b, ok := pdu.Value.([]byte); ok {
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
case gosnmp.Integer:
|
||||
return fmt.Sprintf("%d", pdu.Value)
|
||||
case gosnmp.Counter32, gosnmp.Gauge32, gosnmp.Uinteger32:
|
||||
return fmt.Sprintf("%d", pdu.Value)
|
||||
case gosnmp.Counter64:
|
||||
return fmt.Sprintf("%d", pdu.Value)
|
||||
case gosnmp.TimeTicks:
|
||||
if t, ok := pdu.Value.(uint32); ok {
|
||||
return fmt.Sprintf("%d", t)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%v", pdu.Value)
|
||||
}
|
||||
Reference in New Issue
Block a user