Files
nmon/cmd/nmon-devtools/fleet.go
jleung f85c3ef781
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
M3: Go API per UI contract, embedded UI, openapi compat check, nmon-devtools, live gate + screenshots; M4 WP1 reachability and dependency suppression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 20:34:14 +00:00

131 lines
4.5 KiB
Go

package main
import (
"fmt"
"strconv"
"strings"
"github.com/gosnmp/gosnmp"
"github.com/v10networks/nmon/internal/fixturegen"
"github.com/v10networks/nmon/internal/profile"
"github.com/v10networks/nmon/internal/simagent"
)
// demoDevice is one simulated device of `devtools seed-demo`.
type demoDevice struct {
name, site, role string
kind string // counters, big, router, ups, v1, down: what the gate and screenshots look for
rec *simagent.Recording
community string
version string // v2c unless set
hc bool // 64-bit counters, so history can be back-filled
}
// counterBase is added to every simulated 64-bit octet counter so that the
// back-filled history (about 3e13 bytes for 31 days at 100 Mbit/s) fits below
// the live counter and the join shows no reset.
const counterBase = 100_000_000_000_000
const (
oidHCIn = "1.3.6.1.2.1.31.1.1.1.6."
oidHCOut = "1.3.6.1.2.1.31.1.1.1.10."
oidUp = "1.3.6.1.2.1.1.3.0"
)
// animate returns a copy of rec whose 64-bit octet counters advance at a steady
// per-port rate from counterBase and whose uptime ticks, so successive polls see
// traffic. Counters that already advance keep their rate and only gain the base.
func animate(rec *simagent.Recording) *simagent.Recording {
recs := append([]simagent.Record(nil), rec.Records()...)
for i := range recs {
r := &recs[i]
switch {
case r.Type == gosnmp.Counter64 && (strings.HasPrefix(r.OID, oidHCIn) || strings.HasPrefix(r.OID, oidHCOut)):
recs[i] = animateCounter(*r)
case r.OID == oidUp && r.Dyn == nil:
if v, ok := r.Value.(uint32); ok {
r.Dyn, r.Value = &simagent.Dynamic{Rate: 100, Initial: uint64(v)}, nil
}
}
}
return simagent.NewRecording(recs)
}
func animateCounter(r simagent.Record) simagent.Record {
if r.Dyn != nil {
d := *r.Dyn
d.Initial += counterBase
r.Dyn = &d
return r
}
k, _ := strconv.Atoi(r.OID[strings.LastIndexByte(r.OID, '.')+1:])
dir := 1
if strings.HasPrefix(r.OID, oidHCOut) {
dir = 2
}
mbps := 1 + (k*(31+7*dir))%90
r.Dyn = &simagent.Dynamic{Rate: float64(mbps) * 125_000, Initial: counterBase + uint64(k)*1_000_000_000}
r.Value = nil
return r
}
// profileDevice builds a device that a shipped profile matches, with traffic.
func profileDevice(set *profile.Set, prof, name, site, role, kind string, ports int) (demoDevice, error) {
spec, ok := fixturegen.SpecFor(prof)
if !ok {
return demoDevice{}, fmt.Errorf("no fixture spec for profile %q", prof)
}
res, ok := set.Lookup(prof)
if !ok {
return demoDevice{}, fmt.Errorf("no profile %q", prof)
}
spec.SysName = name
if ports > 0 {
spec.Ports = ports
}
rec, err := fixturegen.FromProfile(res, spec)
if err != nil {
return demoDevice{}, fmt.Errorf("%s: %w", name, err)
}
return demoDevice{name: name, site: site, role: role, kind: kind, rec: animate(rec), community: "public", hc: true}, nil
}
// buildFleet is the small network used for live screenshots: a 48-port switch,
// routers, a firewall, a UPS, one 32-bit device, the counters fixture and one
// device whose agent is stopped after seeding, so it reads as down.
func buildFleet(set *profile.Set, countersRec string) ([]demoDevice, error) {
counters, err := simagent.LoadRecording(countersRec)
if err != nil {
return nil, err
}
fleet := []demoDevice{{
name: "yyz1-edge1", site: "yyz1", role: "edge", kind: "counters", rec: animate(counters), community: "public", hc: true,
}}
for _, p := range []struct {
prof, name, site, role, kind string
ports int
}{
{"cisco-ios", "yyz1-acc01", "yyz1", "access", "big", 48},
{"cisco-ios", "yyz1-acc02", "yyz1", "access", "router", 24},
{"cisco-ios", "ott1-acc02", "ott1", "access", "down", 24}, // its agent is stopped once seeding is done
{"juniper-junos", "yyz1-core1", "yyz1", "core", "router", 0},
{"arista-eos", "yul1-core1", "yul1", "core", "router", 0},
{"mikrotik-routeros", "ott1-gw1", "ott1", "gateway", "router", 0},
{"fortigate", "yyz1-fw1", "yyz1", "firewall", "router", 0},
{"apc-ups", "yyz1-ups1", "yyz1", "power", "ups", 0},
} {
d, err := profileDevice(set, p.prof, p.name, p.site, p.role, p.kind, p.ports)
if err != nil {
return nil, err
}
fleet = append(fleet, d)
}
// 32-bit counters on 1 Gbit/s ports: the API flags them unreliable and the UI never graphs them.
v1 := fixturegen.Generic(fixturegen.Options{Ports: 12, SysName: "yul1-acc03", Counters: true, NamePattern: "Fa0/%d"})
fleet = append(fleet, demoDevice{
name: "yul1-acc03", site: "yul1", role: "access", kind: "v1", rec: v1, community: "nmon-v1", version: "v1",
})
return fleet, nil
}