Files
nmon/CLAUDE.md
Vibe Coder 7173360346 Add the MAC/ARP/FDB table job, the lookup API, BGP peer collection and bgp_peer_down
Owner request: finding where a host is plugged in from a MAC or IP is a must,
and BGP peer monitoring is wanted.

L2 job (internal/l2): a job of its own, off the 60 s poll path (its sessions
are path "l2"), walks BRIDGE-MIB, Q-BRIDGE-MIB, CISCO-VTP-MIB (a per-VLAN
context walk, enabled by the cisco-ios profile flag l2.per_vlan_context, not by
detection) and the IP-MIB neighbour tables (IPv4 ARP and IPv6 ND, IPv4-only
fallback) every l2.interval, with its own concurrency cap and a per-device
deadline. It yields to the poller like the rollup job, and a device that times
out is skipped for the cycle, counted and named on /poller/health. Migration 14
adds fdb_entries, arp_entries, vlans and l2_walks with first/last-seen history,
batched upserts, daily pruning by last_seen (l2.keep) and backup coverage.

API (additive): /l2/search ranks edge ports first (a port is an uplink above
l2.edge_max_macs MACs, with an LLDP neighbour, or as a LAG), plus the device
fdb/arp/l2/l2-ports tables and /interfaces/{id}/macs; vendor and mac_kind are
computed at query time from the optional registry.

BGP: profiles label peer rows by decoding the table index (index_label.layout),
so each peer, IPv4 or IPv6, is one component whatever MIB feeds it; AS, local
AS, router id and description are component attributes, never labels.
CISCO-BGP4 (cisco-ios, cisco-nxos), Juniper BGP4-V2 and Arista BGP4V2 replace
the optional generic BGP4-MIB metrics, which edgeos, fortigate, pfsense and vyos
switch on. /bgp/peers lists sessions down first. The built-in rule
bgp_peer_down (critical, not established for 2 minutes) fires only where a
policy binds it; its notification names the peer's AS and description.

Fixtures are generated (Q-BRIDGE with IPv4/IPv6 neighbours, BRIDGE-MIB only,
Cisco per-VLAN contexts via a new simagent contexts feature) and checked
against real LibreNMS recordings. DEVIATIONS 50-55 record the departures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4PdJUPrakM8fHvN8jYv2x
2026-10-24 01:30:32 +00:00

9.4 KiB
Raw Permalink Blame History

nmon: agent onboarding

nmon is a lightweight LibreNMS replacement for ISPs and IT teams. It is one static Go binary that polls devices (SNMP, ICMP, traps, push ingest, Proxmox, checks), stores metrics in two embedded VictoriaMetrics child processes (hot: raw 30 d; long: rollups up to 10 y), keeps inventory in SQLite or Postgres via bun, alerts with dependency suppression, and serves a React UI and JSON API from the same binary. Module: github.com/v10networks/nmon, Go 1.27.

Read first (source of truth, in this order)

  1. docs/spec/spec.md and docs/spec/build-detail.md: the owner's spec. The contract.
  2. docs/DEVIATIONS.md: every approved departure from the spec, with the reason. Check it before "fixing" something to match the spec.
  3. docs/MAINTAINABILITY.md: hard rule, enforced by lint. Read it before writing code.
  4. docs/milestones/: per-milestone scope, decisions and gate logs (M1–M6, v1.0.1-device-tabs.md), plus deferred items and post-v1 additions in README.md.
  5. docs/operations.md (running nmon), docs/dev.md (UI embedding, openapi check, devtools), docs/coverage.md (what each profile collects vs. the spec).

Non-negotiables

  • make lint must be clean, Go (golangci-lint v2.14.0 via .golangci.yml, plus scripts/lint-filesize.sh) and web (ESLint, --max-warnings 0).
    • Size limits: functions ≤ 80 lines, cyclomatic ≤ 15, cognitive ≤ 20; Go files ≤ 600 lines, web files ≤ 400.
    • //nolint only with the linter's name and a reason.
  • Layering (depguard):
    • No SQL or bun outside internal/store.
    • Only cmd/* imports internal/app, and only internal/app imports internal/api.
    • Collection paths don't import each other: poller, discovery, traps, ingest, checks, rollup, alert, l2. Shared code goes in a lower package (e.g. snmptarget, longq, snmprec).
    • cmd/nmon must not link dev/test machinery (simagent, fixturegen, fixtures, librenms, testutil); TestNmonDoesNotLinkDevMachinery enforces this.
  • Time comes only from internal/clock (forbidigo bans time.Now/Sleep/After… outside clock, cmd/ and tests). Tests use clock.NewFake.
  • Frozen contracts:
    • Metric names and labels: SNMP series carry only device=<slug> and iface=<ULID>/component. Long-store rollups use v (highest wins; read via internal/longq.LatestVersion).
    • API routes: scripts/openapi-check.sh fails on incompatible changes vs the last tag. Additive changes only.
  • Migrations are frozen: never edit an applied migration. Add internal/store/<YYYYMMDDNNNNNN>_<name>.go with the next number, using frozen struct copies (see existing files). Add new store methods as a sub-interface in a new file, plus one embed line in store.go.
  • API types in the UI are generated, never hand-written:
    1. go run ./cmd/nmon openapi > openapi.json
    2. cd web && npm run gen:api (writes web/src/api/schema.d.ts)
    3. Use client.GET/POST.
  • Only web/src/components/TrafficGraph.tsx imports uPlot. Single-series graphs use its format prop.
  • IPv6 first. Everything works on v6 and v4; tests default to ::1.
  • Every port is polled. Only a profile's exclude_ifname drops one. 32-bit counters on fast links are flagged unreliable and excluded from sums, p95 and rollups, never graphed as trustworthy.
  • Rollups store rates, never raw counters. A grep test enforces this.

Layout

Path What
cmd/nmon the product binary (serve, migrate, discover, profile test/record/new, backup, restore, key rotate, unlock, store restart, secret set, rollup repair, openapi)
cmd/nmon-devtools dev-only binary (backfill, seed-demo, mocks for OIDC/Proxmox, webhook sink); never shipped
cmd/nmon-librenms-import offline tool: LibreNMS definitions → draft profiles
internal/app wiring: one start* per subsystem, stops in reverse order, SIGHUP reload
internal/api huma v2 handlers, RFC 9457 errors, paging (default 100, max 1,000); slugs and ULIDs only, never numeric ids or secrets
internal/auth OIDC (no login UI: browsers 302 to the IdP), sessions + CSRF, API tokens, break-glass (HTTP Basic on /auth/breakglass), route policy
internal/store bun models, migrations, every SQL query
internal/vmsup embedded VictoriaMetrics: extract, checksum, supervise, write queue, querier
internal/{poller,discovery,traps,ingest,checks,proxmox,rollup,p95,groups,reach,deps,alert,notify} collection, storage jobs, reachability, alerting
internal/{l2,hostaddr,oui} the MAC/ARP/FDB table job, MAC and IP parsing, the optional IEEE vendor registry
internal/profile + profiles/*.yaml device profiles (YAML, numeric OIDs, category/label/of/display per metric)
internal/mib SMI parser; MIBs are a sidecar in mibs/, never embedded
internal/{simagent,snmprec,fixturegen,fixtures,profiletest} the SNMP simulator, .snmprec format, fixture generator, expectation harness
web/ React 19 + TS + Vite + Tailwind/shadcn + TanStack Query/Router; built into internal/webui
testdata/snmprec/ per-profile and behaviour fixtures, each with *.expect.yaml and optional *.agent.yaml
deploy/ansible/ playbook (systemd, TLS modes, break-glass/OIDC, MIB sidecar)

Commands

make fetch-vm          # once: download and checksum the VictoriaMetrics binaries (needed for vmembed and integration)
make build             # web build + static nmon with the UI and VictoriaMetrics embedded
make devtools importer # the dev and import binaries
make lint              # Go + web; must be clean
go test -race ./...                                   # unit tests (fast)
go test -race -tags 'integration vmembed' ./...       # real VictoriaMetrics children
./scripts/test-pg.sh                                  # store suite on a throwaway Postgres 16 (Docker; safe to run concurrently)
NMON_LIBRENMS_DIR=/home/svcuser/src/librenms go test ./internal/profile/... ./internal/fixtures/...   # real-device recordings
go run ./testdata/gen                                 # regenerate fixtures (a test fails on drift)
scripts/openapi-check.sh                              # API compatibility vs the last tag
make gate-m1 … gate-m6                                # end-to-end gates (slow, ~10 min each)
cd web && npm run lint && npm run test && npm run build && npm run e2e   # e2e = Playwright on a mocked API

Adding things

  • A device profile:
    • Add profiles/<name>.yaml that extends: generic, with match (sysObjectID prefix and/or sysDescr regex) and priority. Higher wins; watch overlaps such as Linux-based sysDescrs and shared enterprise numbers.
    • Use numeric OIDs, verified against mibs/, with a provenance comment. Every pollable non-interface metric needs a category; the orphan test enforces it.
    • Then run go run ./testdata/gen for the fixture and expect file, add matcher tests, and check against LibreNMS recordings in /home/svcuser/src/librenms/tests/snmpsim.
    • That repo is GPL and read-only reference material. Never copy code or YAML from it.
  • An API route:
    • Put the handler in a new file in internal/api. State-changing routes are admin by default; declare read-only POSTs as viewer on the auth policy (internal/app/authwire.go). TestViewerCannotMutate walks every route automatically.
    • Regenerate openapi.json and the web schema, then run scripts/openapi-check.sh.
  • A UI page:
    • Use the generated client in web/src/api/*Hooks.ts, NavRow for clickable rows, and RequireAdmin or role checks for mutating controls.
    • Mock handlers and fixtures live in web/e2e/mock/ for the Playwright mock suite.
  • A deviation from the spec: add a row to docs/DEVIATIONS.md explaining what and why.

Gotchas

  • Concurrent agents:
    • Stage only your own paths, never git add -A.
    • openapi.json and web/src/api/schema.d.ts are shared, so regenerate them just before committing.
    • Two agents writing a migration must agree on numbers first.
  • Screenshots and Playwright:
    • Full Playwright runs rewrite the committed screenshots under docs/screenshots/*. Revert the ones you didn't mean to change (git checkout -- docs/screenshots/<dir>).
    • Use a non-default E2E_PORT if a vite preview may already be running, and kill any preview server you start.
  • Gate scripts:
    • Gate scripts run make lint first; a lint finding anywhere fails every gate.
    • --insecure-http with no sign-in configured runs dev-open mode (everyone is admin). The gates rely on it; never ship it.
  • Ingest: reserved metric prefixes (nmon_, if, hr, sys + uppercase/digit) and the labels device/iface/v/push are stripped or refused.
  • ICMP: nmon serve --icmp off, or an unsupported unprivileged ICMP socket, makes SNMP alone decide reachability, with an icmp_unsupported warning.

Working style expected here

  • Commit messages: imperative summary, a body explaining why, and the trailers the session asks for.
  • Don't claim something passed unless you ran it. Report skipped checks explicitly.
  • Owner preferences in force:
    • Maintainability is a hard rule.
    • MIBs are a sidecar.
    • The LibreNMS importer and devtools are separate binaries.
    • No login UI (OIDC redirect only).
    • Interface-down alerts are opt-in (per port or via groups).
    • The 2,000-device load test is skipped (approved deviation).
    • Web push is v2.
    • BMC profiles are deferred.