Files
nmon/internal/fixturegen/generic.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)}
}