Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
299 lines
10 KiB
Go
299 lines
10 KiB
Go
// Package fixturegen builds synthetic device recordings in code and writes
|
|
// them in snmpsim's .snmprec format.
|
|
package fixturegen
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/fnv"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gosnmp/gosnmp"
|
|
|
|
"github.com/v10networks/nmon/internal/snmprec"
|
|
)
|
|
|
|
// Options shapes a Generic device. Zero values take the defaults noted.
|
|
type Options struct {
|
|
// Ports is the number of interfaces; default 8. With 4 or more, the last two
|
|
// are a GRE tunnel (ifType 131) and a VLAN subinterface.
|
|
Ports int
|
|
SysName string // default "generic"
|
|
SysObjectID string // default net-snmp's 1.3.6.1.4.1.8072.3.2.10
|
|
SysDescr string // default "nmon generic fixture"
|
|
HCCounters bool // include ifHCInOctets and ifHCOutOctets
|
|
Speed uint64 // bits per second of physical ports; default 1e9
|
|
// NamePattern formats physical port names from the 0-based port number; default "ge-0/0/%d".
|
|
NamePattern string
|
|
TunnelName string // default "gr-0/0/0"
|
|
VLANName string // default NamePattern(0) + ".100"
|
|
Loopback string // when set, a software loopback interface of this name is added after the ports
|
|
Serial string // when set, ENTITY-MIB reports a chassis (entity 1) with this serial number
|
|
Counters bool // octet counters advance at 1-100 Mbit/s per port and sysUpTime advances
|
|
// SysUpTime and HRSystemUptime override the uptime objects. Nil means
|
|
// static, or advancing at 100 ticks/s when Counters is set.
|
|
SysUpTime, HRSystemUptime *snmprec.Dynamic
|
|
}
|
|
|
|
const (
|
|
sysUpTimeStart = 8_640_000 // one day in ticks
|
|
tunnelType = 131
|
|
vlanType = 135
|
|
)
|
|
|
|
type builder struct{ recs []snmprec.Record }
|
|
|
|
func (b *builder) add(oid string, t gosnmp.Asn1BER, v any) {
|
|
b.recs = append(b.recs, snmprec.Record{OID: oid, Type: t, Value: v})
|
|
}
|
|
|
|
func (b *builder) dyn(oid string, t gosnmp.Asn1BER, d *snmprec.Dynamic) {
|
|
b.recs = append(b.recs, snmprec.Record{OID: oid, Type: t, Dyn: d})
|
|
}
|
|
|
|
func idx(prefix string, i int) string { return prefix + "." + strconv.Itoa(i) }
|
|
|
|
// Generic builds a recording with the system group, ifTable and ifXTable,
|
|
// hrProcessorLoad for two CPUs, hrStorageTable with RAM and one disk, and UCD memory.
|
|
func Generic(o Options) *snmprec.Recording {
|
|
o = o.withDefaults()
|
|
b := &builder{recs: make([]snmprec.Record, 0, 64+o.Ports*24)}
|
|
h := fnv.New32a()
|
|
_, _ = h.Write([]byte(o.SysName)) // hash.Hash.Write never returns an error
|
|
mac := h.Sum32()
|
|
|
|
b.system(o)
|
|
ports := o.portList()
|
|
b.ifTable(o, ports, mac)
|
|
b.hostResources(o)
|
|
b.ifXTable(o, ports)
|
|
if o.Serial != "" {
|
|
const ent = "1.3.6.1.2.1.47.1.1.1.1"
|
|
b.add(ent+".5.1", gosnmp.Integer, 3)
|
|
b.add(ent+".7.1", gosnmp.OctetString, []byte("Chassis"))
|
|
b.add(ent+".11.1", gosnmp.OctetString, []byte(o.Serial))
|
|
}
|
|
// UCD-SNMP memory (kB)
|
|
b.add("1.3.6.1.4.1.2021.4.5.0", gosnmp.Integer, 8388608)
|
|
b.add("1.3.6.1.4.1.2021.4.6.0", gosnmp.Integer, 6291456)
|
|
return snmprec.NewRecording(b.recs)
|
|
}
|
|
|
|
// withDefaults fills the zero values documented on Options.
|
|
func (o Options) withDefaults() Options {
|
|
if o.Ports <= 0 {
|
|
o.Ports = 8
|
|
}
|
|
if o.SysName == "" {
|
|
o.SysName = "generic"
|
|
}
|
|
if o.SysObjectID == "" {
|
|
o.SysObjectID = "1.3.6.1.4.1.8072.3.2.10"
|
|
}
|
|
if o.SysDescr == "" {
|
|
o.SysDescr = "nmon generic fixture"
|
|
}
|
|
if o.NamePattern == "" {
|
|
o.NamePattern = "ge-0/0/%d"
|
|
}
|
|
if o.TunnelName == "" {
|
|
o.TunnelName = "gr-0/0/0"
|
|
}
|
|
if o.VLANName == "" {
|
|
o.VLANName = fmt.Sprintf(o.NamePattern, 0) + ".100"
|
|
}
|
|
if o.Speed == 0 {
|
|
o.Speed = 1_000_000_000
|
|
}
|
|
return o
|
|
}
|
|
|
|
// uptime adds an uptime object: o's override d, else advancing when Counters is set, else static.
|
|
func (b *builder) uptime(o Options, oid string, d *snmprec.Dynamic) {
|
|
switch {
|
|
case d != nil:
|
|
b.dyn(oid, gosnmp.TimeTicks, d)
|
|
case o.Counters:
|
|
b.dyn(oid, gosnmp.TimeTicks, &snmprec.Dynamic{Rate: 100, Initial: sysUpTimeStart})
|
|
default:
|
|
b.add(oid, gosnmp.TimeTicks, uint32(sysUpTimeStart))
|
|
}
|
|
}
|
|
|
|
// system adds the SNMPv2-MIB system group.
|
|
func (b *builder) system(o Options) {
|
|
const sys = "1.3.6.1.2.1.1"
|
|
b.add(sys+".1.0", gosnmp.OctetString, []byte(o.SysDescr))
|
|
b.add(sys+".2.0", gosnmp.ObjectIdentifier, o.SysObjectID)
|
|
b.uptime(o, sys+".3.0", o.SysUpTime)
|
|
b.add(sys+".4.0", gosnmp.OctetString, []byte("noc@example.net"))
|
|
b.add(sys+".5.0", gosnmp.OctetString, []byte(o.SysName))
|
|
b.add(sys+".6.0", gosnmp.OctetString, []byte("lab"))
|
|
b.add(sys+".7.0", gosnmp.Integer, 78)
|
|
}
|
|
|
|
// ifPort is one interface row.
|
|
type ifPort struct {
|
|
name, alias string
|
|
typ int
|
|
speed uint64
|
|
oper int
|
|
}
|
|
|
|
// portList lays out the interfaces: physical ports, then (with 4 or more) a
|
|
// GRE tunnel and a VLAN subinterface in place of the last two, then an
|
|
// optional loopback.
|
|
func (o Options) portList() []ifPort {
|
|
ports := make([]ifPort, o.Ports)
|
|
for i := range ports {
|
|
ports[i] = ifPort{name: fmt.Sprintf(o.NamePattern, i), alias: "port " + strconv.Itoa(i+1), typ: 6, speed: o.Speed, oper: 1}
|
|
}
|
|
if o.Ports >= 4 {
|
|
ports[2].oper = 2 // admin up, oper down
|
|
ports[o.Ports-2] = ifPort{name: o.TunnelName, alias: "GRE to scrubbing provider", typ: tunnelType, oper: 1}
|
|
ports[o.Ports-1] = ifPort{name: o.VLANName, alias: "VLAN 100 customer", typ: vlanType, speed: o.Speed, oper: 1}
|
|
}
|
|
if o.Loopback != "" {
|
|
ports = append(ports, ifPort{name: o.Loopback, alias: "loopback", typ: 24, speed: 0, oper: 1})
|
|
}
|
|
return ports
|
|
}
|
|
|
|
// column adds one value per port under prefix.col.
|
|
func (b *builder) column(prefix, col string, ports []ifPort, t gosnmp.Asn1BER, f func(i int, p ifPort) any) {
|
|
for i, p := range ports {
|
|
b.add(idx(prefix+"."+col, i+1), t, f(i, p))
|
|
}
|
|
}
|
|
|
|
// octetCounter adds an octet counter column: static, or advancing at 1-100
|
|
// Mbit/s per port when Counters is set. hc selects the ifXTable 64-bit column.
|
|
func (b *builder) octetCounter(o Options, nPorts int, col string, t gosnmp.Asn1BER, hc bool, dirSalt int) {
|
|
const ift = "1.3.6.1.2.1.2.2.1"
|
|
for i := 0; i < nPorts; i++ {
|
|
oid := idx(ift+"."+col, i+1)
|
|
if hc {
|
|
oid = idx("1.3.6.1.2.1.31.1.1.1."+col, i+1)
|
|
}
|
|
if !o.Counters {
|
|
v := uint64(i+1) * 1_000_000 * uint64(dirSalt)
|
|
if hc {
|
|
b.add(oid, t, v*1000)
|
|
} else {
|
|
b.add(oid, t, uint32(v))
|
|
}
|
|
continue
|
|
}
|
|
mbps := uint64(1 + ((i+1)*(30+7*dirSalt))%100)
|
|
b.dyn(oid, t, &snmprec.Dynamic{Rate: float64(mbps * 125_000), Initial: uint64(i+1) * 1_000_000_000})
|
|
}
|
|
}
|
|
|
|
// ifTable adds IF-MIB::ifNumber and the ifTable columns.
|
|
func (b *builder) ifTable(o Options, ports []ifPort, mac uint32) {
|
|
const ifn = "1.3.6.1.2.1.2"
|
|
const ift = ifn + ".2.1"
|
|
b.add(ifn+".1.0", gosnmp.Integer, len(ports))
|
|
zero := func(int, ifPort) any { return uint32(0) }
|
|
b.column(ift, "1", ports, gosnmp.Integer, func(i int, _ ifPort) any { return i + 1 })
|
|
b.column(ift, "2", ports, gosnmp.OctetString, func(_ int, p ifPort) any { return []byte(p.name) })
|
|
b.column(ift, "3", ports, gosnmp.Integer, func(_ int, p ifPort) any { return p.typ })
|
|
b.column(ift, "4", ports, gosnmp.Integer, func(int, ifPort) any { return 1500 })
|
|
b.column(ift, "5", ports, gosnmp.Gauge32, func(_ int, p ifPort) any { return uint32(min(p.speed, 1<<32-1)) })
|
|
b.column(ift, "6", ports, gosnmp.OctetString, func(i int, p ifPort) any {
|
|
if p.typ == tunnelType || p.typ == 24 {
|
|
return []byte{}
|
|
}
|
|
return []byte{0x00, 0x50, 0x56, byte(mac >> 8), byte(mac), byte(i + 1)}
|
|
})
|
|
b.column(ift, "7", ports, gosnmp.Integer, func(int, ifPort) any { return 1 })
|
|
b.column(ift, "8", ports, gosnmp.Integer, func(_ int, p ifPort) any { return p.oper })
|
|
b.octetCounter(o, len(ports), "10", gosnmp.Counter32, false, 1)
|
|
b.column(ift, "13", ports, gosnmp.Counter32, zero)
|
|
b.column(ift, "14", ports, gosnmp.Counter32, zero)
|
|
b.octetCounter(o, len(ports), "16", gosnmp.Counter32, false, 2)
|
|
b.column(ift, "19", ports, gosnmp.Counter32, zero)
|
|
b.column(ift, "20", ports, gosnmp.Counter32, zero)
|
|
}
|
|
|
|
// hostResources adds HOST-RESOURCES-MIB: uptime, hrStorageTable (RAM and one
|
|
// disk), the processor table and hrProcessorLoad for two CPUs. (OID 25 sorts
|
|
// between ifTable (2) and ifXTable (31).)
|
|
func (b *builder) hostResources(o Options) {
|
|
const hr = "1.3.6.1.2.1.25"
|
|
b.uptime(o, hr+".1.1.0", o.HRSystemUptime)
|
|
const hrs = hr + ".2.3.1"
|
|
storage := []struct {
|
|
idx int
|
|
typ string
|
|
descr string
|
|
unit int
|
|
size int
|
|
used int
|
|
}{
|
|
{1, "1.3.6.1.2.1.25.2.1.2", "Physical memory", 1024, 8388608, 2097152},
|
|
{31, "1.3.6.1.2.1.25.2.1.4", "/", 4096, 12582912, 3145728},
|
|
}
|
|
for col := 1; col <= 6; col++ {
|
|
for _, s := range storage {
|
|
oid := idx(hrs+"."+strconv.Itoa(col), s.idx)
|
|
switch col {
|
|
case 1:
|
|
b.add(oid, gosnmp.Integer, s.idx)
|
|
case 2:
|
|
b.add(oid, gosnmp.ObjectIdentifier, s.typ)
|
|
case 3:
|
|
b.add(oid, gosnmp.OctetString, []byte(s.descr))
|
|
case 4:
|
|
b.add(oid, gosnmp.Integer, s.unit)
|
|
case 5:
|
|
b.add(oid, gosnmp.Integer, s.size)
|
|
case 6:
|
|
b.add(oid, gosnmp.Integer, s.used)
|
|
}
|
|
}
|
|
}
|
|
cpus := [2]int{196608, 196609}
|
|
loads := [2]int{12, 7}
|
|
for col := 1; col <= 3; col++ {
|
|
for i, c := range cpus {
|
|
oid := idx(hr+".3.2.1."+strconv.Itoa(col), c)
|
|
switch col {
|
|
case 1:
|
|
b.add(oid, gosnmp.Integer, c)
|
|
case 2:
|
|
b.add(oid, gosnmp.ObjectIdentifier, "1.3.6.1.2.1.25.3.1.3")
|
|
case 3:
|
|
b.add(oid, gosnmp.OctetString, []byte("CPU "+strconv.Itoa(i)))
|
|
}
|
|
}
|
|
}
|
|
for i, c := range cpus {
|
|
b.add(idx(hr+".3.3.1.2", c), gosnmp.Integer, loads[i])
|
|
}
|
|
}
|
|
|
|
// ifXTable adds ifName, the optional 64-bit octet counters, ifHighSpeed and ifAlias.
|
|
func (b *builder) ifXTable(o Options, ports []ifPort) {
|
|
const ifx = "1.3.6.1.2.1.31.1.1.1"
|
|
b.column(ifx, "1", ports, gosnmp.OctetString, func(_ int, p ifPort) any { return []byte(p.name) })
|
|
if o.HCCounters {
|
|
b.octetCounter(o, len(ports), "6", gosnmp.Counter64, true, 1)
|
|
b.octetCounter(o, len(ports), "10", gosnmp.Counter64, true, 2)
|
|
}
|
|
b.column(ifx, "15", ports, gosnmp.Gauge32, func(_ int, p ifPort) any { return uint32(p.speed / 1_000_000) })
|
|
b.column(ifx, "18", ports, gosnmp.OctetString, func(_ int, p ifPort) any { return []byte(p.alias) })
|
|
}
|
|
|
|
// ResetAfter returns an uptime Dynamic that advances at 100 ticks/s and
|
|
// restarts from 1000 ticks after d, as after a reboot.
|
|
func ResetAfter(d time.Duration) *snmprec.Dynamic {
|
|
return &snmprec.Dynamic{Rate: 100, Initial: 1000, ResetAt: d}
|
|
}
|
|
|
|
// WrapIn returns an uptime Dynamic that advances at 100 ticks/s and wraps
|
|
// through 2^32 after d, as sysUpTime does every 497 days.
|
|
func WrapIn(d time.Duration) *snmprec.Dynamic {
|
|
return &snmprec.Dynamic{Rate: 100, Initial: 1<<32 - uint64(d.Seconds()*100)}
|
|
}
|