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
68 lines
2.9 KiB
Go
68 lines
2.9 KiB
Go
package profile
|
|
|
|
import "testing"
|
|
|
|
// The indexes below are the row indexes real agents return (the shapes seen in the public LibreNMS
|
|
// recordings of IOS XR, Junos, EOS and FRR agents), so the layouts are checked against the wire, not the MIB text.
|
|
func TestLayoutLabelsPeersFromTheIndexTheAgentReturns(t *testing.T) {
|
|
peerIdx := map[string]string{"1": "2001:db8::5", "0": "192.0.2.9"}
|
|
lookup := func(i string) (string, bool) { l, ok := peerIdx[i]; return l, ok }
|
|
cases := []struct {
|
|
name, layout, idx, want string
|
|
}{
|
|
{"bgp4-mib ipv4", "ipv4", "172.16.200.1", "172.16.200.1"},
|
|
{"cisco ipv4", "inet", "1.4.192.168.5.149", "192.168.5.149"},
|
|
{"cisco ipv6", "inet", "2.16.32.1.13.184.0.0.0.0.0.0.0.0.0.0.0.1", "2001:db8::1"},
|
|
{"cisco ipv4 prefix", "inet afisafi", "1.4.192.168.5.149.1.1", "192.168.5.149|ipv4-unicast"},
|
|
{"cisco ipv6 prefix", "inet afisafi", "2.16.32.1.13.184.0.0.0.0.0.0.0.0.0.0.0.1.2.1", "2001:db8::1|ipv6-unicast"},
|
|
{"arista ipv4", "skip inet", "1.1.4.192.168.0.2", "192.168.0.2"},
|
|
{"arista ipv6", "skip inet", "1.2.16.32.1.5.80.0.2.0.47.0.0.0.0.0.51.0.1", "2001:550:2:2f::33:1"},
|
|
{"arista prefix", "skip inet afisafi", "1.1.4.192.168.0.2.1.1", "192.168.0.2|ipv4-unicast"},
|
|
{"junos ipv4 (no length octets)", "skip skipaddr addr", "0.1.192.168.99.24.1.192.168.99.25", "192.168.99.25"},
|
|
{"junos ipv6", "skip skipaddr addr",
|
|
"0.2.253.0.0.40.0.1.0.1.0.0.0.0.0.1.0.4.2.253.0.0.40.0.1.0.1.0.0.0.0.0.1.0.5", "fd00:28:1:1::1:5"},
|
|
{"junos prefix via peer index", "peerindex afisafi", "1.2.1", "2001:db8::5|ipv6-unicast"},
|
|
{"unknown afi", "inet afisafi", "1.4.10.0.0.1.7.9", "10.0.0.1|afi7-safi9"},
|
|
}
|
|
for _, c := range cases {
|
|
l, err := ParseLayout(c.layout)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", c.name, err)
|
|
}
|
|
got, ok := l.Label(c.idx, lookup)
|
|
if !ok || got != c.want {
|
|
t.Errorf("%s: Label(%s) = %q, %v; want %q", c.name, c.idx, got, ok, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLayoutRefusesRowsThatDoNotFit(t *testing.T) {
|
|
l, _ := ParseLayout("inet")
|
|
for _, idx := range []string{"", "1.4.192.168.5", "1.4.192.168.5.149.7", "1.5.1.2.3.4.5", "9.4.1.2.3.4", "1.4.300.1.1.1", "x.y"} {
|
|
if got, ok := l.Label(idx, nil); ok {
|
|
t.Errorf("Label(%q) = %q, want no label", idx, got)
|
|
}
|
|
}
|
|
via, _ := ParseLayout("peerindex")
|
|
if got, ok := via.Label("5", func(string) (string, bool) { return "", false }); ok {
|
|
t.Errorf("unknown peer index labelled %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseLayoutRejectsMalformedLayouts(t *testing.T) {
|
|
for _, s := range []string{"", "skip", "inet inet", "inet peerindex", "afisafi inet", "bogus inet", "inet afisafi skip"} {
|
|
if _, err := ParseLayout(s); err == nil {
|
|
t.Errorf("ParseLayout(%q) accepted", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIPFromOctets(t *testing.T) {
|
|
if s, ok := IPFromOctets([]byte{192, 0, 2, 1}); !ok || s != "192.0.2.1" {
|
|
t.Errorf("v4 = %q %v", s, ok)
|
|
}
|
|
if _, ok := IPFromOctets([]byte{1, 2, 3}); ok {
|
|
t.Error("3 octets accepted")
|
|
}
|
|
}
|