"""Hand-authored ``VirtualSwitchState`` seeds, one builder per model.
``seed_gsm7252ps`` is the original, most exhaustively-documented seed: every
read op (Task 5-9) has at least one non-empty, non-vacuous example -- ports
with link/admin/speed, RX/TX counters on >=2 ports, >=1 VLAN with egress/
untagged bitmaps and PVIDs, PoE with a delivering port, fan/temperature/PSU
sensors (including a "Not Supported" fan slot), >=2 MAC/FDB entries with
their bridge-port->ifIndex mappings, >=1 LLDP neighbour, and a static
management IP. This makes the Task 16 SNMP<->SNMP equivalence test (and the
round-trip tests here) exercise real data on every parser, not an empty
table. Port 1 also carries an ifAlias description (ports 2+ deliberately
leave it unset) so the ifAlias column exercises both the present and
absent-instance paths. A sysDescr containing "GSM7252PS" plus a placeholder
sysObjectID (Task 2 model detection) round out the identity signals -- see
``VirtualSwitchState.sys_descr``/``sys_object_id``.
``seed_m4300_24x``/``seed_m4300_16x`` are transcribed directly from the
committed real-hardware captures (``tests/fixtures/captures/m4300-*.json``)
rather than hand-invented, so the M4300 pair's headline capability contrast
(24X has NO PoE, 16X has PoE on all 16 ports) is grounded, not guessed -- see
each function's docstring for exactly what is captured-real vs illustrative.
"""
from __future__ import annotations
from .state import (
EntitySim,
LldpSim,
MacSim,
MgmtSim,
PoeSim,
PortSim,
SensorSim,
ServiceSim,
SyslogCollectorSim,
SyslogSim,
UserSim,
VirtualSwitchState,
VlanMembershipPageSim,
VlanSim,
)
_POE_PORT_COUNT = 48
_TOTAL_PORT_COUNT = 52
def _port_name(port: int) -> str:
# FASTPATH ifName is "1/0/<port>" for EVERY physical port -- including the
# SFP+ uplinks 49-52. Verified against BOTH faces of the real gsm7252ps:
# the SNMP capture (tests/fixtures/captures/gsm7252ps.json) and the web UI
# (tests/fixtures/http/gsm7252ps_portsConfiguration.html) name port 49
# "1/0/49", never "1/xg1". An earlier "1/xg<n>" scheme for the uplinks was
# a fabrication that matched neither capture.
return f"1/0/{port}"
# --- gsm7252ps, TRANSCRIBED from tests/fixtures/captures/gsm7252ps.json ---
# (a real SNMP capture of 10.1.5.22) and cross-checked against that same
# switch's HTTP captures in tests/fixtures/http/gsm7252ps_*.html.
#
# port -> (admin_enabled, link_up, speed_mbps, ifAlias description)
_GSM7252PS_PORTS = {
1: (True, True, 1000, "eth0.rpi5-pmod"),
2: (True, True, 1000, "eth0.rpi4-pmod"),
3: (True, True, 1000, "eth0.reterm2"),
4: (True, True, 1000, "eth0.rpi3b-gwifi"),
5: (True, True, 1000, "eth0.rpi4-usbdev"),
6: (True, False, 0, None),
7: (True, True, 1000, "eth0.rpib-sdcard"),
8: (True, False, 0, None),
9: (True, True, 100, "eth0.rpib-serial"),
10: (True, False, 0, None),
11: (True, True, 100, "eth0.puck11"),
12: (True, True, 1000, "eth0.puck07"),
13: (True, True, 1000, "eth0.rpi5-433mhz"),
14: (True, True, 1000, "eth0.rpi-birds-welland-back"),
15: (True, False, 0, None),
16: (True, True, 1000, "eth0.rpi-sdr-pluto"),
17: (True, True, 1000, "eth0.rpi5-zigbee"),
18: (True, True, 1000, "eth0.rpi4-asus-aspeed2050-dev"),
19: (True, False, 0, None),
20: (True, True, 1000, "eth0.rpi4-hppdu-dev"),
21: (True, False, 0, None),
22: (True, True, 1000, "eth0.rpi4-precursor"),
23: (True, False, 0, None),
24: (True, True, 1000, "eth0.rpi4-gwifi"),
25: (True, True, 1000, None),
26: (True, True, 100, "eth0.rpiz-3"),
27: (True, True, 1000, "eth0.rpi4-esp"),
28: (True, False, 0, None),
29: (True, False, 0, None),
30: (True, True, 1000, "eth0.rpi5-rfbridge"),
31: (True, True, 1000, "eth0.minnow-turbot-2"),
32: (True, True, 1000, "eth0.minnow-turbot-1"),
33: (True, True, 1000, "eth0.rpi4-kindle"),
34: (True, False, 0, None),
35: (True, False, 0, None),
36: (True, False, 0, None),
37: (True, True, 1000, "eth0.rpi5-netv2"),
38: (True, True, 1000, "eth0.rpi3-netv2"),
39: (True, False, 0, None),
40: (True, False, 0, None),
41: (True, True, 100, "eth0.rpiz-serial"),
42: (True, True, 1000, "eth0.rpi-sdr-rtlsdr-v3"),
43: (True, False, 0, "end0.hifive-unmatched-1"),
44: (True, False, 0, "end0.hifive-unmatched-2"),
45: (True, True, 100, "wired.fritz-box-7270-1"),
46: (True, True, 1000, "eth0.puck12"),
47: (True, True, 1000, "p5.sw-poe-micro3"),
48: (True, False, 0, "spare.ex-cisco"),
49: (True, True, 10000, "1/0/2.sw-netgear-m4300-24x"),
50: (True, True, 10000, "1/0/49.sw-netgear-gsm7252ps-s2"),
51: (True, True, 10000, "1/0/51.sw-netgear-gsm7252ps-s2"),
52: (True, False, 10000, None),
}
# port -> (rx_octets, tx_octets, rx_packets, tx_packets, rx_errors, tx_errors)
_GSM7252PS_COUNTERS = {
1: (45747246, 912689098, 217358, 235430, 0, 0),
2: (43729612, 982042673, 227304, 287393, 0, 0),
3: (309174274, 2763396970, 2703903, 2832210, 0, 0),
4: (392406056, 1208220179, 455946, 362560, 0, 0),
5: (45296975, 1784117938, 252396, 695269, 0, 0),
6: (0, 0, 0, 0, 0, 0),
7: (45478982, 1213479258, 243846, 319720, 0, 0),
8: (0, 0, 0, 0, 0, 0),
9: (43952641, 2474560700, 203437, 525017, 188, 0),
10: (0, 0, 0, 0, 0, 0),
11: (191245854, 2353135188, 504340, 779331, 0, 0),
12: (242753298, 2405690445, 568614, 871728, 98, 0),
13: (54822647, 1790957945, 314512, 686411, 0, 0),
14: (1371471567, 2391786115, 5452154, 8842491, 0, 0),
15: (0, 0, 0, 0, 0, 0),
16: (458026172, 2161853643, 4479341, 7845883, 0, 0),
17: (48254266, 1370034013, 247191, 559339, 0, 0),
18: (107301127, 3428968332, 662009, 1912062, 0, 0),
19: (0, 0, 0, 0, 0, 0),
20: (48094973, 1600286182, 261316, 548326, 0, 0),
21: (0, 0, 0, 0, 0, 0),
22: (44983852, 1761230194, 274092, 651683, 0, 0),
23: (0, 0, 0, 0, 0, 0),
24: (60929816, 1662046751, 292512, 618541, 0, 0),
25: (860424, 432228822, 5367, 11254, 0, 0),
26: (42636836, 1207912561, 221448, 441634, 0, 0),
27: (43106121, 1783468534, 249904, 681000, 0, 0),
28: (0, 0, 0, 0, 0, 0),
29: (0, 0, 0, 0, 0, 0),
30: (46856497, 1781675389, 238217, 691884, 0, 0),
31: (34224983, 1103571183, 191771, 227719, 0, 0),
32: (36725195, 1103043544, 199490, 236217, 0, 0),
33: (41767749, 1780610707, 235994, 676190, 0, 0),
34: (0, 0, 0, 0, 0, 0),
35: (0, 0, 0, 0, 0, 0),
36: (0, 0, 0, 0, 0, 0),
37: (61212753, 1383861442, 366925, 650250, 0, 0),
38: (44488789, 1286245977, 339689, 516784, 0, 0),
39: (0, 0, 0, 0, 0, 0),
40: (0, 0, 0, 0, 0, 0),
41: (47611153, 1597050827, 285613, 729735, 0, 0),
42: (2121062532, 3300194902, 18659985, 20148348, 0, 0),
43: (0, 0, 0, 0, 0, 0),
44: (0, 0, 0, 0, 0, 0),
45: (895931591, 720798804, 1283153, 515947, 1, 0),
46: (215009366, 3079995766, 735039, 1206637, 111, 0),
47: (20987280, 2708586643, 114946, 390277, 0, 0),
48: (88601206, 3713706826, 1025558, 2517756, 0, 0),
49: (28392074220, 9325801127, 77433287, 62142947, 0, 0),
50: (278014432, 1694871324, 2069292, 2139039, 0, 0),
51: (1075552286, 2278779253, 9235681, 9547144, 0, 0),
52: (0, 0, 0, 0, 0, 0),
}
_GSM7252PS_PVIDS = {
1: 90,
2: 90,
3: 90,
4: 90,
5: 90,
6: 1,
7: 90,
8: 1,
9: 90,
10: 1,
11: 4,
12: 4,
13: 90,
14: 90,
15: 1,
16: 90,
17: 90,
18: 90,
19: 1,
20: 90,
21: 1,
22: 90,
23: 90,
24: 90,
25: 90,
26: 90,
27: 90,
28: 1,
29: 1,
30: 90,
31: 90,
32: 90,
33: 90,
34: 1,
35: 1,
36: 1,
37: 90,
38: 90,
39: 1,
40: 1,
41: 90,
42: 90,
43: 90,
44: 90,
45: 20,
46: 4,
47: 5,
48: 5,
49: 1,
50: 1,
51: 1,
52: 1,
}
# vid -> (name, member ifIndexes, untagged ifIndexes) -- a LITERAL transcription
# of dot1qVlanStaticEgressPorts / dot1qVlanStaticUntaggedPorts from
# tests/fixtures/captures/gsm7252ps.json. Both the physical ports (1-52) and the
# aggregation ifIndexes (``*range(418, 482)`` = lag 1..lag 64) are taken exactly
# as the real switch reported them -- including the genuine hardware quirk that
# ``untagged`` is NOT a subset of ``member`` (e.g. VLAN 6 lists ports 1-45 as
# untagged though only 46/47/... are egress members) and that per-VLAN LAG
# membership varies (all 64 LAGs on VLAN 1, only lag 1-2 on VLAN 90). The web UI
# lists only the physical ports; the LAG ifIndexes are kept here so a renderer
# that forgets to drop them is caught (see test_web_projection).
# Ports the real GSM7252PS reports on VLAN 1 as ``Current: Exclude /
# Configured: Include`` -- see the split applied in ``seed_gsm7252ps``. They are
# in the SNMP static-egress transcription below because that table IS the
# configured one; keeping them out of the CURRENT member set is what makes the
# mock's SNMP face and its HTTP/CLI faces disagree the same way hardware does.
_GSM7252PS_VLAN1_CONFIGURED_ONLY = (50, 51)
_GSM7252PS_VLANS = {
1: (
"default",
(
6,
8,
10,
15,
19,
21,
22,
26,
28,
29,
34,
35,
36,
39,
40,
49,
50,
51,
52,
*range(418, 482),
),
(
3,
4,
6,
7,
8,
10,
14,
15,
16,
18,
19,
20,
21,
22,
24,
25,
26,
27,
28,
29,
30,
31,
32,
34,
35,
36,
39,
40,
42,
49,
50,
51,
52,
*range(418, 482),
),
),
4: ("wifi", (11, 12, 46, 49, 50, 51), (11, 12, 46)),
5: (
"net",
(
3,
4,
5,
6,
7,
8,
10,
11,
12,
13,
14,
15,
16,
18,
19,
20,
21,
22,
24,
25,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
39,
40,
42,
46,
47,
48,
49,
50,
51,
52,
*range(418, 420),
),
(9, 45, 47, 48, *range(420, 482)),
),
6: (
"pwr",
(46, 47, 49, 50, 51, *range(418, 420)),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
7: (
"store",
(),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
*range(418, 482),
),
),
10: (
"int",
(9, 11, 12, 46, 47, 49, 50, 51, *range(418, 420)),
(
1,
2,
3,
4,
5,
6,
7,
8,
10,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
20: (
"roam",
(9, 11, 12, 45, 46, 47, 49, 50, 51, *range(418, 420)),
(
1,
2,
3,
4,
5,
6,
7,
8,
10,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
21: (
"fpgas",
(),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
*range(418, 482),
),
),
41: (
"sm",
(46, 47, 49, 50, 51, *range(418, 420)),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
89: (
"sdr",
(),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
*range(418, 482),
),
),
90: (
"iot",
(
1,
2,
3,
4,
5,
7,
9,
11,
12,
13,
14,
16,
17,
18,
20,
22,
23,
24,
25,
26,
27,
30,
31,
32,
33,
37,
38,
41,
42,
43,
44,
46,
47,
49,
50,
51,
*range(418, 420),
),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
99: (
"guest",
(9, 11, 12, 46, 47, 49, 50, 51, *range(418, 420)),
(
1,
2,
3,
4,
5,
6,
7,
8,
10,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
121: (
"t-fpgas",
(46, 47, 49, 50, 51, *range(418, 420)),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
141: (
"t-sm",
(46, 47, 49, 50, 51, *range(418, 420)),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
48,
52,
),
),
}
# port -> (admin_enabled, RFC3621 detect code, delivered power mW)
_GSM7252PS_POE = {
1: (True, 3, 3500),
2: (True, 3, 2700),
3: (True, 3, 3500),
4: (True, 3, 9000),
5: (True, 3, 5800),
6: (True, 6, 0), # otherFault: outside SNMP's 1-4 detect map -> UNKNOWN
7: (True, 3, 3900),
8: (True, 3, 1500),
9: (True, 3, 3800),
10: (True, 2, 0),
11: (True, 3, 4100),
12: (True, 3, 4700),
13: (True, 3, 4600),
14: (True, 3, 3800),
15: (True, 2, 0),
16: (True, 2, 0),
17: (True, 3, 3400),
18: (True, 3, 8500),
19: (True, 2, 0),
20: (True, 3, 6000),
21: (True, 2, 0),
22: (True, 3, 6000),
23: (True, 2, 0),
24: (True, 3, 7700),
25: (True, 3, 3700),
26: (True, 3, 1700),
27: (True, 3, 7100),
28: (True, 2, 0),
29: (True, 2, 0),
30: (True, 3, 4100),
31: (True, 3, 3800),
32: (True, 3, 3900),
33: (True, 3, 5700),
34: (True, 2, 0),
35: (True, 2, 0),
36: (True, 2, 0),
37: (True, 3, 9400),
38: (True, 3, 9900),
39: (True, 2, 0),
40: (True, 2, 0),
41: (True, 3, 3200),
42: (True, 3, 6900),
43: (True, 2, 0),
44: (True, 2, 0),
45: (True, 2, 0),
46: (True, 3, 4300),
47: (True, 3, 1900),
48: (True, 2, 0),
}
[docs]
def seed_gsm7252ps() -> VirtualSwitchState:
"""Build a GSM7252PS (52-port, 48-PoE) state from the REAL capture.
TRANSCRIBED, not invented, for everything the device's own captures show:
per-port admin/link/speed/ifAlias, per-port counters, every PVID, all 14
VLANs with their exact member/untagged ifIndex sets (physical ports AND the
per-VLAN lag 1..lag 64 ifIndexes, including the genuine hardware quirk that
``untagged`` is not a subset of ``member``), all 48 PoE ports, the box
sensors, the management IP (10.1.5.22), base MAC, serial and firmware --
from ``tests/fixtures/captures/gsm7252ps.json`` (SNMP, host 10.1.5.22) plus
that same switch's HTTP captures (``tests/fixtures/http/gsm7252ps_*.html``,
the source of the serial/firmware/hostname and the ``http_sensors`` set).
This is a strict transcription and is guarded as one -- see
``tests/virtual/test_state_seed.py::test_seed_gsm7252ps_matches_capture_strictly``,
which runs it through the same ``capture_parity.assert_seed_matches_capture``
helper the M4300 seeds use. It replaced an earlier HAND-INVENTED seed that
contradicted the captures throughout (mgmt 10.1.5.20, 2 VLANs vs the real
14, one PoE port delivering vs 30).
TWO real sensor sets, one per interface (see ``sensors`` and
``http_sensors`` below): the SNMP walk returns fan RPM + PSU watts and NO
temperature; the HTTP sysInfo page returns temperatures + fan/PSU HEALTH
text. Both are transcribed from their respective captures, neither is forced
onto the other face.
Genuinely ILLUSTRATIVE (regression traps the capture cannot express, each
documented where defined): the MAC/FDB entries and their bridge-port ->
ifIndex join, the single LLDP neighbour, and ``mgmt.mode``/``gateway`` (the
real capture reports mode "unknown" and no gateway route; the mock needs a
definite DHCP-mode OID to serve and to flip on write).
Non-physical interfaces are represented by two of the capture's 65
(ifIndex 417 "CPU Interface" and 418 "lag 1") plus the per-VLAN LAG
ifIndexes in VLAN membership, so a renderer/parser that forgets that the web
UI lists ONLY physical ports is caught.
"""
ports: dict[int, PortSim] = {}
for port, (admin, link, speed, description) in _GSM7252PS_PORTS.items():
rx_octets, tx_octets, rx_pkts, tx_pkts, rx_errs, tx_errs = _GSM7252PS_COUNTERS[
port
]
ports[port] = PortSim(
name=_port_name(port),
admin=admin,
link=link,
speed=speed,
rx_octets=rx_octets,
tx_octets=tx_octets,
rx_ucast=rx_pkts,
tx_ucast=tx_pkts,
rx_errors=rx_errs,
tx_errors=tx_errs,
description=description,
# Flow Mode reads "Disable" on all 52 ports of this model's own
# `show port all` capture. It was left at the True default while the
# CLI face happened to print a hardcoded "Disable" -- so the mock
# agreed with the device by accident and disagreed with itself.
flow_control=False,
)
# Two of the capture's non-physical interfaces (it has 65: one CPU port
# and 64 LAGs). They must never appear on a web-UI page. Both are
# transcribed from the capture: the CPU port reports no speed/ifAlias; lag 1
# is a 20 Gbit/s aggregation with a configured ifAlias.
ports[417] = PortSim(
name="CPU Interface: 0/5/1", admin=True, link=True, speed=0, if_type=1
)
ports[418] = PortSim(
name="lag 1",
admin=True,
link=True,
speed=20000,
if_type=161,
description="lag.sw-netgear-gsm7252ps-s2",
)
# member/untagged are already the EXACT captured ifIndex sets (physical
# ports plus whichever lag 1..lag 64 ifIndexes each VLAN actually carries --
# see _GSM7252PS_VLANS); nothing is added or dropped here.
vlans = {
vid: VlanSim(name=name, member=set(member), untagged=set(untagged))
for vid, (name, member, untagged) in _GSM7252PS_VLANS.items()
}
# Split the CONFIGURED-but-not-CURRENT ports out of the transcribed static
# bitmap. The table above is a literal copy of dot1qVlanStaticEgressPorts,
# which is the STATIC (configured) table -- so it includes ports the switch
# does NOT currently egress. Measured 2026-07-30 on the real GSM7252PS
# (10.1.5.22): ``show vlan 1`` reports
# 1/0/49 Include Include Untagged
# 1/0/50 Exclude Include Untagged
# 1/0/51 Exclude Include Untagged
# 1/0/52 Include Include Untagged
# and, matching that, the live HTTP reads show 1/0/50 and 1/0/51 in the VLAN
# Membership page's hiddenMem grid but NOT in its hiddenUnTagged list nor in
# vlanStatus.html's Member Ports cell. Only VLAN 1 is split here because that
# is the VLAN whose full ``show vlan`` output was captured -- the other VLANs
# get no unevidenced claim (principle: never round an inference up to a fact).
for port in _GSM7252PS_VLAN1_CONFIGURED_ONLY:
# Only PARTICIPATION moves. The untagged bitmap
# (dot1qVlanStaticUntaggedPorts) is a separate axis and genuinely still
# has these ports' bits set -- the real switch prints them as
# ``Configured: Include / Tagging: Untagged`` -- so leaving them in
# ``untagged`` is what keeps the SNMP projection byte-equal to the walk.
vlans[1].member.discard(port)
vlans[1].configured_only.add(port)
pvids = dict(_GSM7252PS_PVIDS)
poe = {
port: PoeSim(admin=admin, detect=detect, power_mw=power_mw)
for port, (admin, detect, power_mw) in _GSM7252PS_POE.items()
}
# SNMP box sensors -- a LITERAL transcription of the real walk in
# tests/fixtures/captures/gsm7252ps.json: two fan RPMs (fan0, fan2 -- the
# device has no fan1 OID) and four PSU wattages (power0..power3). This
# interface reports NO temperature at all. Nothing here is invented; the
# SNMP get_sensors output matches the capture value-for-value.
sensors = [
SensorSim(kind="fan", instance="0", raw="2850"),
SensorSim(kind="fan", instance="2", raw="2350"),
SensorSim(kind="power", instance="0", raw="49"),
SensorSim(kind="power", instance="1", raw="30"),
SensorSim(kind="power", instance="2", raw="32"),
SensorSim(kind="power", instance="3", raw="31"),
]
# HTTP sysInfo box sensors -- a SEPARATE, equally real sensor set that the
# web UI exposes and SNMP does not (see tests/fixtures/http/
# gsm7252ps_sysInfo.html). The two hardware interfaces genuinely surface
# different sensors: sysInfo.html carries a Temperature Status table
# (System/CPU/MAC-A/MAC-B degC; the MAC row reads N/A on this unit and the
# parser skips it), a FAN Status table reporting fan HEALTH as OK/NA text
# (never RPM), and a Device Status table with the RPS + Power Module
# operational flags. Every value here is transcribed from that captured
# page, not invented, and the labels are the page's own. Keeping this list
# distinct from ``sensors`` is what lets each face render ITS OWN real
# shape instead of forcing one interface's data onto the other.
http_sensors = [
SensorSim(kind="temperature", instance="System", raw="29"),
SensorSim(kind="temperature", instance="CPU", raw="49"),
SensorSim(kind="temperature", instance="MAC", raw="N/A"),
SensorSim(kind="temperature", instance="MAC-A", raw="32"),
SensorSim(kind="temperature", instance="MAC-B", raw="31"),
SensorSim(kind="fan", instance="Fan1/PWR", raw="OK"),
SensorSim(kind="fan", instance="Fan2/CPU", raw="OK"),
SensorSim(kind="fan", instance="Fan3/SYS", raw="OK"),
SensorSim(kind="fan", instance="Fan4", raw="NA"),
SensorSim(kind="fan", instance="Fan5", raw="NA"),
SensorSim(kind="power", instance="RPS", raw="Operational"),
SensorSim(kind="power", instance="Power Module", raw="Operational"),
]
macs = [
MacSim(vlan=90, mac_bytes=(0xC8, 0x00, 0x84, 0x89, 0x71, 0x70), bridge_port=10),
MacSim(vlan=1, mac_bytes=(0x00, 0x1B, 0x21, 0x3C, 0x4D, 0x5E), bridge_port=11),
]
# bridge_port 10 deliberately maps to a DIFFERENT ifIndex (110, not 10) so
# a regression that drops the dot1dBasePortIfIndex join (or falls back to
# the bridge-port number itself) is detectable: get_macs() must surface
# the mapped ifIndex 110, never the bridge port 10. bridge_port 11 stays
# identity-mapped to prove the join also passes through unmapped/1:1 rows
# unchanged.
bridge_ports = {10: 110, 11: 11}
lldp = [
LldpSim(
time_mark=75,
local_port=49,
rem_idx=7,
chassis="".join(chr(b) for b in (0xC8, 0x00, 0x84, 0x89, 0x71, 0x70)),
port_id="1/xg51",
port_desc="eth0",
sys_name="sw-cisco-shed",
),
]
# address/netmask are the captured ones. mode/gateway are NOT: the real
# capture reports mode "unknown" (the vendor DHCP-mode OID answered
# nothing) and no gateway route, but the mock must serve a definite
# writable DHCP-mode OID, so "static" + the subnet's router are the
# structural stand-ins -- never a claim about the real device.
mgmt = MgmtSim(
address="10.1.5.22", netmask="255.255.255.0", gateway="10.1.5.1", mode="static"
)
return VirtualSwitchState(
model_key="gsm7252ps",
ports=ports,
vlans=vlans,
pvids=pvids,
poe=poe,
# Real fixed Q-BRIDGE PortList width, measured LIVE (read-only) on this
# switch @10.1.5.22: dot1qVlanStaticEgressPorts is 79 bytes wide.
vlan_portlist_width=79,
# MEASURED off the real VLAN-Membership capture (2026-07-30,
# tests/fixtures/http/gsm7252ps_vlanPortCfg_vlan1.html): 116 hiddenMem
# slots = 52 ports + 64 LAGs, LAG ifNames 0/3/N, the OLDER
# toggleImageFirst/grey_[btu].gif grid, no trailing comma, no CSRFToken
# and UNescaped ifName lists (the only one of the four like that).
vlan_membership_page=VlanMembershipPageSim(slots=116, lag_slot=3, grid="gif"),
sensors=sensors,
http_sensors=http_sensors,
macs=macs,
bridge_ports=bridge_ports,
lldp=lldp,
mgmt=mgmt,
model_name="GSM7252PS",
# serial/firmware/hostname are transcribed from the real sysInfo.html
# capture of this switch (tests/fixtures/http/gsm7252ps_sysInfo.html).
serial="2BW20A47000CC",
firmware="10.0.0.53",
# MEASURED 2026-08-02 on 10.1.5.22: identical to the m4300 pair --
# enabled, port 514, one collector 10.1.5.1 info(6) 514 Active.
syslog=SyslogSim(
admin_mode=1,
local_port=514,
collectors=[SyslogCollectorSim("10.1.5.1", 514, 6)],
),
# MEASURED 2026-08-03 on 10.1.5.22's own userManagement.html. The
# wording is that PAGE's, not the CLI's -- the same switch's
# `show users` calls admin "Read/Write".
users=[UserSim("admin", "Super User"), UserSim("guest", "Read Only")],
# MEASURED 2026-08-03 on 10.1.5.22, HTTP pages and `show ip http` /
# `show ip ssh` / `show telnetcon` agreeing. Telnet really is OFF here,
# independently confirmed by TCP 23 being refused. Neither this
# switch's http nor its ssh page prints a port, so both are None --
# NOT defaulted to 80/22, which would be inventing a field.
services={
"http": ServiceSim(enabled=True),
"https": ServiceSim(enabled=True, port=443),
"ssh": ServiceSim(enabled=True),
"telnet": ServiceSim(enabled=False),
},
hostname="sw-netgear-gsm7252ps-s1.welland.mithis.com",
nsdp_mac=b"\xe0\x91\xf5\x0c\xd6\xdb", # captured System MAC Address
# Illustrative sysDescr text -- NOT a captured real firmware string;
# its only requirement is containing the model name "GSM7252PS" so
# detect_model_from_sysdescr's string matching has something real to
# key off end-to-end. sys_object_id is a plausible-looking UNVERIFIED
# virtual/test placeholder under the model's own 4526.10 vendor
# subtree -- NOT a claim about the real device's sysObjectID (no
# capture of the real value exists).
sys_descr="NETGEAR GSM7252PS Managed Switch, firmware 8.0.6.6",
sys_object_id="1.3.6.1.4.1.4526.10.100.14",
)
_GSM7228PS_PORTS: dict[int, tuple[str, bool, bool, int, str | None]] = {
1: ("1/g1", True, False, 0, None),
2: ("1/g2", True, False, 0, None),
3: ("1/g3", True, False, 0, None),
4: ("1/g4", True, False, 0, None),
5: ("1/g5", True, False, 0, None),
6: ("1/g6", True, False, 0, None),
7: ("1/g7", True, False, 0, None),
8: ("1/g8", True, False, 0, None),
9: ("1/g9", True, False, 0, None),
10: ("1/g10", True, False, 0, None),
11: ("1/g11", True, False, 0, None),
12: ("1/g12", True, False, 0, None),
13: ("1/g13", True, False, 0, None),
14: ("1/g14", True, False, 0, None),
15: ("1/g15", True, False, 0, None),
16: ("1/g16", True, False, 0, None),
17: ("1/g17", True, False, 0, None),
18: ("1/g18", True, False, 0, None),
19: ("1/g19", True, False, 0, None),
20: ("1/g20", True, False, 0, None),
21: ("1/g21", True, False, 0, None),
22: ("1/g22", True, False, 0, None),
23: ("1/g23", True, False, 0, None),
24: ("1/g24", True, False, 0, None),
25: ("1/g25", True, False, 0, None),
26: ("1/g26", True, False, 0, None),
27: ("1/g27", True, False, 0, None),
28: ("1/g28", True, False, 0, None),
29: ("1/g29", True, False, 0, None),
30: ("1/g30", True, False, 0, None),
31: ("1/g31", True, False, 0, None),
32: ("1/g32", True, False, 0, None),
33: ("1/g33", True, False, 0, None),
34: ("1/g34", True, False, 0, None),
35: ("1/g35", True, False, 0, None),
36: ("1/g36", True, False, 0, None),
37: ("1/g37", True, False, 0, None),
38: ("1/g38", True, False, 0, "class0?"),
39: ("1/g39", True, False, 0, None),
40: ("1/g40", True, False, 0, "class0?"),
41: ("1/g41", True, False, 0, "eth-local.tweed"),
42: ("1/g42", True, False, 0, "bmc.tweed"),
43: ("1/g43", True, False, 0, None),
44: ("1/g44", True, False, 0, None),
45: ("1/g45", True, False, 0, "eth0.hifive-unmatched-2"),
46: ("1/g46", True, False, 0, "eth0.hifive-unmatched-1"),
47: ("1/g47", True, False, 0, "eth0.rpi-sdr-rtlsdr-v3"),
48: ("1/g48", True, False, 0, "eth0.rpiz-serial"),
49: ("1/xg49", True, True, 1000, None),
50: ("1/xg50", True, False, 0, "cisco-shed"),
51: ("1/xg51", True, True, 10000, None),
52: ("1/xg52", True, False, 0, None),
}
_GSM7228PS_COUNTERS: dict[int, tuple[int, int, int, int, int, int]] = {
1: (0, 0, 0, 0, 0, 0),
2: (0, 0, 0, 0, 0, 0),
3: (0, 0, 0, 0, 0, 0),
4: (0, 0, 0, 0, 0, 0),
5: (0, 0, 0, 0, 0, 0),
6: (0, 0, 0, 0, 0, 0),
7: (0, 0, 0, 0, 0, 0),
8: (0, 0, 0, 0, 0, 0),
9: (0, 0, 0, 0, 0, 0),
10: (0, 0, 0, 0, 0, 0),
11: (0, 0, 0, 0, 0, 0),
12: (0, 0, 0, 0, 0, 0),
13: (0, 0, 0, 0, 0, 0),
14: (0, 0, 0, 0, 0, 0),
15: (0, 0, 0, 0, 0, 0),
16: (0, 0, 0, 0, 0, 0),
17: (0, 0, 0, 0, 0, 0),
18: (0, 0, 0, 0, 0, 0),
19: (0, 0, 0, 0, 0, 0),
20: (0, 0, 0, 0, 0, 0),
21: (0, 0, 0, 0, 0, 0),
22: (0, 0, 0, 0, 0, 0),
23: (0, 0, 0, 0, 0, 0),
24: (0, 0, 0, 0, 0, 0),
25: (0, 0, 0, 0, 0, 0),
26: (0, 0, 0, 0, 0, 0),
27: (0, 0, 0, 0, 0, 0),
28: (0, 0, 0, 0, 0, 0),
29: (0, 0, 0, 0, 0, 0),
30: (0, 0, 0, 0, 0, 0),
31: (0, 0, 0, 0, 0, 0),
32: (0, 0, 0, 0, 0, 0),
33: (0, 0, 0, 0, 0, 0),
34: (0, 0, 0, 0, 0, 0),
35: (0, 0, 0, 0, 0, 0),
36: (0, 0, 0, 0, 0, 0),
37: (0, 0, 0, 0, 0, 0),
38: (0, 0, 0, 0, 0, 0),
39: (0, 0, 0, 0, 0, 0),
40: (0, 0, 0, 0, 0, 0),
41: (0, 0, 0, 0, 0, 0),
42: (0, 0, 0, 0, 0, 0),
43: (0, 0, 0, 0, 0, 0),
44: (0, 0, 0, 0, 0, 0),
45: (0, 0, 0, 0, 0, 0),
46: (0, 0, 0, 0, 0, 0),
47: (0, 0, 0, 0, 0, 0),
48: (0, 0, 0, 0, 0, 0),
49: (492931, 9048, 0, 0, 0, 0),
50: (0, 0, 0, 0, 0, 0),
51: (5493036, 5451371, 49697, 49690, 0, 0),
52: (0, 0, 0, 0, 0, 0),
}
_GSM7228PS_VLANS: dict[int, tuple[str, tuple[int, ...], tuple[int, ...]]] = {
1: (
"Default",
(
49,
50,
51,
52,
314,
315,
316,
317,
318,
319,
320,
321,
322,
323,
324,
325,
326,
327,
328,
329,
330,
331,
332,
333,
334,
335,
336,
337,
338,
339,
),
(
49,
50,
51,
52,
314,
315,
316,
317,
318,
319,
320,
321,
322,
323,
324,
325,
326,
327,
328,
329,
330,
331,
332,
333,
334,
335,
336,
337,
338,
339,
),
),
5: ("net", (41, 49, 50, 51, 52), (41,)),
21: (
"fpgas",
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
42,
43,
44,
45,
46,
47,
),
(
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
42,
43,
44,
45,
46,
47,
),
),
121: ("t-fpgas", (48, 49, 50, 51, 52), (48,)),
4089: ("Auto-Video", (), ()),
}
_GSM7228PS_PVIDS: dict[int, int] = {
1: 21,
2: 21,
3: 21,
4: 21,
5: 21,
6: 21,
7: 21,
8: 21,
9: 21,
10: 21,
11: 21,
12: 21,
13: 21,
14: 21,
15: 21,
16: 21,
17: 21,
18: 21,
19: 21,
20: 21,
21: 21,
22: 21,
23: 21,
24: 21,
25: 21,
26: 21,
27: 21,
28: 21,
29: 21,
30: 21,
31: 21,
32: 21,
33: 21,
34: 21,
35: 21,
36: 21,
37: 21,
38: 21,
39: 21,
40: 21,
41: 5,
42: 21,
43: 21,
44: 21,
45: 21,
46: 21,
47: 21,
48: 121,
49: 1,
50: 1,
51: 1,
52: 1,
}
_GSM7228PS_POE: dict[int, tuple[bool, int, int]] = {
1: (True, 2, 0),
2: (True, 2, 0),
3: (True, 2, 0),
4: (True, 2, 0),
5: (True, 2, 0),
6: (True, 2, 0),
7: (True, 2, 0),
8: (True, 2, 0),
9: (True, 2, 0),
10: (True, 2, 0),
11: (True, 2, 0),
12: (True, 2, 0),
13: (True, 2, 0),
14: (True, 2, 0),
15: (True, 2, 0),
16: (True, 2, 0),
17: (True, 2, 0),
18: (True, 2, 0),
19: (True, 2, 0),
20: (True, 2, 0),
21: (True, 2, 0),
22: (True, 2, 0),
23: (True, 2, 0),
24: (True, 2, 0),
25: (True, 2, 0),
26: (True, 2, 0),
27: (True, 2, 0),
28: (True, 2, 0),
29: (True, 2, 0),
30: (True, 2, 0),
31: (True, 2, 0),
32: (True, 2, 0),
33: (True, 2, 0),
34: (True, 2, 0),
35: (True, 2, 0),
36: (True, 2, 0),
37: (True, 2, 0),
38: (True, 2, 0),
39: (True, 2, 0),
40: (True, 2, 0),
41: (True, 2, 0),
42: (True, 2, 0),
43: (True, 2, 0),
44: (True, 3, 400),
45: (True, 2, 0),
46: (True, 4, 0),
47: (True, 2, 0),
48: (True, 3, 700),
}
[docs]
def seed_gsm7228ps() -> VirtualSwitchState:
"""Build a GSM7228PS / S3300-52X-PoE+ (52-port, 48-PoE Smart Managed Pro)
state from the REAL capture.
TRANSCRIBED, not invented, from this model's OWN first live-hardware capture
(``tests/fixtures/captures/gsm7228ps.json``, SNMP host 10.1.5.11 =
sw-netgear-s3300-1, sysObjectID 1.3.6.1.4.1.4526.100.10.19, captured
2026-07-30): every physical port's name/admin/link/speed, all counters,
every PVID, all 5 VLANs with their exact member/untagged ifIndex sets
(physical ports plus the lag 1..lag 26 ifIndexes 314-339 that VLAN 1
carries), all 48 PoE ports (2 delivering, 1 fault, the rest searching),
the box sensors (3 fan RPM + PSU watts + temperature, under vendor
4526.11.43), the management IP (10.1.5.11) and base MAC. Guarded as a
strict transcription by ``test_gsm7228ps_seed.py`` via the same
``capture_parity.assert_seed_matches_capture`` helper the other seeds use.
This replaced an earlier HAND-INVENTED illustrative seed (mgmt 10.1.5.21,
2 VLANs, a guessed 4526.11.100.28 sysObjectID, "1/0/N" FASTPATH port names)
written before any S3300 was ever powered on -- the model is now
``verified=True`` with real ground truth behind it.
Genuinely ILLUSTRATIVE (regression traps the capture cannot express):
``mgmt.mode``/``gateway`` (the real capture reports mode "unknown" and no
gateway route, but the mock needs a definite writable DHCP-mode OID, so
"static" + the subnet router are structural stand-ins). MAC/FDB and LLDP
rows ARE transcribed from the capture but are inherently volatile, so they
are not pinned by the parity harness.
"""
ports: dict[int, PortSim] = {}
for port, (name, admin, link, speed, description) in _GSM7228PS_PORTS.items():
rx_o, tx_o, rx_p, tx_p, rx_e, tx_e = _GSM7228PS_COUNTERS[port]
ports[port] = PortSim(
name=name,
admin=admin,
link=link,
speed=speed,
rx_octets=rx_o,
tx_octets=tx_o,
rx_ucast=rx_p,
tx_ucast=tx_p,
rx_errors=rx_e,
tx_errors=tx_e,
description=description,
# "Disable" on all 52 ports of this model's `show port all`
# capture -- see the note in seed_gsm7252ps.
flow_control=False,
)
# member/untagged are the EXACT captured ifIndex sets: physical ports plus
# the lag 1..lag 26 ifIndexes (314-339) that default VLAN 1 carries. The
# LAG ifIndexes are referenced here (as q-bridge PortList bits) without a
# ports[] entry -- exactly like seed_gsm7252ps -- so the SNMP face renders
# VLAN membership independently of the physical-port-only ifTable.
vlans = {
vid: VlanSim(name=name, member=set(member), untagged=set(untagged))
for vid, (name, member, untagged) in _GSM7228PS_VLANS.items()
}
pvids = dict(_GSM7228PS_PVIDS)
poe = {
port: PoeSim(admin=admin, detect=detect, power_mw=power_mw)
for port, (admin, detect, power_mw) in _GSM7228PS_POE.items()
}
# SNMP box sensors -- a LITERAL transcription of the real walk under vendor
# 4526.11.43: three fan RPMs (fan0..fan2), one PSU wattage (power0) and one
# temperature (temperature1). Nothing invented; get_sensors matches the
# capture value-for-value.
sensors = [
SensorSim(kind="fan", instance="0", raw="4963"),
SensorSim(kind="fan", instance="1", raw="5212"),
SensorSim(kind="fan", instance="2", raw="5294"),
SensorSim(kind="power", instance="0", raw="38"),
SensorSim(kind="temperature", instance="1", raw="38"),
]
# MAC/FDB + LLDP: transcribed from the capture (volatile, not parity-pinned).
# LLDP proves the real topology -- local port 49 (1/xg49) uplinks to
# gsm7252ps-s1's port 1/0/48, port 51 to gsm7252ps-s2.
macs = [
MacSim(vlan=5, mac_bytes=(0x02, 0x00, 0x0A, 0x01, 0x05, 0x01), bridge_port=51),
MacSim(
vlan=121, mac_bytes=(0x02, 0x00, 0x0A, 0x01, 0x21, 0x01), bridge_port=51
),
MacSim(vlan=5, mac_bytes=(0x0C, 0xC4, 0x7A, 0x1B, 0xD9, 0xC7), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0x1C, 0x34, 0xDA, 0x42, 0xE8, 0x8C), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0x1C, 0x34, 0xDA, 0x42, 0xE8, 0x8D), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0x44, 0xA5, 0x6E, 0x60, 0xC5, 0xB6), bridge_port=51),
MacSim(
vlan=121, mac_bytes=(0x44, 0xA5, 0x6E, 0x60, 0xC5, 0xB6), bridge_port=51
),
MacSim(vlan=5, mac_bytes=(0x8C, 0x3B, 0xAD, 0x69, 0x1C, 0x3B), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0x8C, 0x3B, 0xAD, 0x6B, 0xBB, 0xE3), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0xAC, 0x1F, 0x6B, 0xAA, 0x50, 0x53), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0xBC, 0xA5, 0x11, 0xB8, 0xEC, 0xF1), bridge_port=51),
MacSim(
vlan=121, mac_bytes=(0xBC, 0xA5, 0x11, 0xB8, 0xEC, 0xF1), bridge_port=51
),
MacSim(vlan=5, mac_bytes=(0xBC, 0xA5, 0x11, 0xB8, 0xED, 0x42), bridge_port=51),
MacSim(
vlan=121, mac_bytes=(0xBC, 0xA5, 0x11, 0xB8, 0xED, 0x42), bridge_port=51
),
MacSim(vlan=5, mac_bytes=(0xE0, 0x91, 0xF5, 0x0C, 0xD5, 0xC7), bridge_port=51),
MacSim(vlan=1, mac_bytes=(0xE0, 0x91, 0xF5, 0x0C, 0xD5, 0xC9), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0xE0, 0x91, 0xF5, 0x0C, 0xD6, 0xDB), bridge_port=51),
MacSim(vlan=5, mac_bytes=(0x08, 0xBD, 0x43, 0x6B, 0xB8, 0xD8), bridge_port=313),
]
lldp = [
LldpSim(
time_mark=1,
local_port=49,
rem_idx=1,
chassis="".join(chr(b) for b in (0xE0, 0x91, 0xF5, 0x0C, 0xD6, 0xDB)),
port_id="1/0/48",
port_desc="spare.ex-cisco",
sys_name="sw-netgear-gsm7252ps-s1.welland.mithis.com",
),
LldpSim(
time_mark=2,
local_port=51,
rem_idx=2,
chassis="".join(chr(b) for b in (0xE0, 0x91, 0xF5, 0x0C, 0xD5, 0xC7)),
port_id="1/0/50",
port_desc="1/0/50",
sys_name="sw-netgear-gsm7252ps-s2.welland.mithis.com",
),
]
# address/netmask are the captured ones; mode/gateway are structural
# stand-ins (see docstring): the real capture reports mode "unknown" and no
# gateway route.
mgmt = MgmtSim(
address="10.1.5.11", netmask="255.255.255.0", gateway="10.1.5.1", mode="static"
)
return VirtualSwitchState(
model_key="gsm7228ps",
ports=ports,
vlans=vlans,
pvids=pvids,
# Real fixed Q-BRIDGE PortList width, measured LIVE (read-only) on this
# switch @10.1.5.11: dot1qVlanStaticEgressPorts is 45 bytes wide -- a
# third distinct width alongside the GSM7252PS's 79 and the M4300s' 131,
# and still wider than its 52 physical ports need (LAG pseudo-ports).
vlan_portlist_width=45,
poe=poe,
# MEASURED off the real VLAN-Membership capture (2026-07-30,
# tests/fixtures/http/gsm7228ps_vlanPortCfg_vlan5.html): 78 hiddenMem
# slots = 52 ports + 26 LAGs, LAG ifNames 0/3/N, the NEWER
# togImg/switch_*.png grid, HTML-escaped ifName lists, no trailing comma
# and no CSRFToken.
vlan_membership_page=VlanMembershipPageSim(
slots=78, lag_slot=3, grid="png", escape=True
),
sensors=sensors,
macs=macs,
lldp=lldp,
mgmt=mgmt,
model_name="GSM7228PS",
# MEASURED 2026-08-02 on 10.1.5.11: the vendor admin-mode column
# reads 2 (disabled) and the host table is EMPTY -- this switch has
# no collector configured, which is why get_syslog returns none.
#
# DELIBERATELY KEPT even though the live switch has since moved on (a
# re-read on 2026-08-03 over SNMP, HTTP and the CLI all returned
# enabled + one collector). The switch's own buffered log dates the
# change -- ``<14> Aug 3 02:21:02 ... %% Configuration propagation
# successful for config type 0``, the only such entry in the buffer --
# so this is an operator reconfiguring a production device between two
# reads, NOT the reader drifting from the hardware. This row is also
# the fleet's only "logging configured nowhere" case, so re-seeding it
# to match the others would delete the coverage that a genuinely empty
# host table reads as empty. See tests/virtual/test_syslog_fidelity.py.
syslog=SyslogSim(admin_mode=2, local_port=514, collectors=[]),
hostname="sw-netgear-s3300-1",
nsdp_mac=b"\x08\xbd\x43\x6b\xb8\xd8", # captured base/System MAC
# REAL captured identity: the S3300-52X-PoE+'s actual sysDescr and
# sysObjectID (1.3.6.1.4.1.4526.100.10.19 -- the product identifier,
# distinct from the 4526.11 vendor DATA subtree). This is the OID that
# parse.SYSOBJECTID_MODELS maps back to gsm7228ps for auto-detection.
sys_descr=(
"S3300-52X-PoE+ ProSAFE 48-Port Gigabit Stackable Smart Switch "
"with PoE+ and 4 10G uplinks"
),
sys_object_id="1.3.6.1.4.1.4526.100.10.19",
)
[docs]
def seed_gs110emx() -> VirtualSwitchState:
"""Build a GS110EMX (10-port Plus, NSDP+HTTP) state from the REAL capture.
Identity, mgmt-IP and per-port link/speed/description are transcribed from
this model's OWN committed captures (``tests/fixtures/http/
gs110emx_{sysinfo,port_settings,interface_stats}.html``, host 10.1.5.25):
ports 6/8/9/10 up at 100M/1G/10G/10G with port 8 described "rumpus", the
rest down; static 10.1.5.25/24 via 10.1.5.1; MAC bc:a5:11:b8:ec:f1.
Previously these were hand-invented values (hostname "plus-sw", 10.1.5.20,
the default MAC, 1M/2M counters on idle ports, all-untagged VLAN 1, PVIDs
90) that CONTRADICTED this model's own captures while tests pinned them as
if true. Now transcribed: identity, mgmt-IP, port link/speed/description,
per-port counters, VLAN 1 membership and every PVID.
STILL ILLUSTRATIVE (no capture exists, and this says so rather than
implying otherwise): VLAN 90's member/untagged sets -- only VLAN 1's
membership page was captured -- and the QoS/mirroring/IGMP/broadcast/
loop-detection tag values further down, which are test fixtures chosen so
nsdp_device() has something non-vacuous to decode on every parsed tag.
FIRMWARE CAVEAT. This state is transcribed from captures taken while
10.1.5.25 ran firmware 1.0.1.4; that unit has since been upgraded to
1.0.2.8, which is what ``seed_gs110emx_fw1028`` transcribes. Whether 1.0.1.4
advertised the v2 write auth was never measured -- this seed carries the
SKU's measured ``nsdp_auth_version=0x10`` so the v2 write path is
exercisable, but anything asserting real GS110EMX behaviour AT A GIVEN
FIRMWARE must use ``seed_gs110emx_fw1028``.
"""
real_speed = {6: 100, 8: 1000, 9: 10000, 10: 10000}
# Counters transcribed from gs110emx_interface_stats.html: traffic is on
# 6/8/9/10; ports 1-5 and 7 really are all zeros. (An earlier seed put
# 1M/2M on ports 1-2, contradicting that same capture.)
real_octets = {
6: (0, 70_892_018_242),
8: (59_921_732_691, 78_637_274_870),
9: (2_963_140_428_936, 1_189_358_575_871),
10: (1_195_417_274_187, 3_027_396_511_187),
}
ports: dict[int, PortSim] = {}
for port in range(1, 11):
sim = PortSim(
name=f"g{port}",
admin=True,
link=port in real_speed,
speed=real_speed.get(port, 0),
description="rumpus" if port == 8 else None,
)
sim.rx_octets, sim.tx_octets = real_octets.get(port, (0, 0))
sim.rx_errors = 0
ports[port] = sim
# VLAN 1 membership is TRANSCRIBED from gs110emx_vlanmembership.html
# (hiddenMem "1111111122" = ports 1-8 untagged, 9-10 tagged). VLAN 90 is
# one of the 12 VLAN IDs the real Cf8021q capture lists, but its MEMBERSHIP
# was never captured (only VLAN 1's page was), so the member/untagged sets
# for it are ILLUSTRATIVE, not observed.
vlans = {
1: VlanSim(name="", member=set(range(1, 11)), untagged=set(range(1, 9))),
90: VlanSim(name="", member={1, 2, 10}, untagged={1, 2}),
}
# Transcribed from gs110emx_pvid.html: every port is PVID 1 on this unit.
pvids = dict.fromkeys(range(1, 11), 1)
mgmt = MgmtSim(
address="10.1.5.25", netmask="255.255.255.0", gateway="10.1.5.1", mode="static"
)
return VirtualSwitchState(
model_key="gs110emx",
ports=ports,
vlans=vlans,
pvids=pvids,
mgmt=mgmt,
model_name="GS110EMX",
serial="53H60253A0032",
firmware="1.0.1.4",
hostname="sw-netgear-gs110emx1",
nsdp_mac=b"\xbc\xa5\x11\xb8\xec\xf1",
nsdp_password="password",
# LIVE-VERIFIED on this SKU: a GS110EMX advertises AUTH_V2_ENCPASS 0x10
# and REQUIRES the v2 salted challenge-response for writes -- the v1 XOR
# PASSWORD is rejected with error 13. 0x10 == auth.ENCPASS_V2. The
# measurement was taken at firmware 1.0.2.8; whether the 1.0.1.4 this
# seed transcribes also advertised 0x10 was never captured, so this is
# the SKU's known behaviour applied to an unmeasured firmware rather
# than a transcription (see seed_gs110emx_fw1028 for the measured one).
nsdp_auth_version=0x10,
# QoS/mirroring/IGMP/broadcast-filtering/loop-detection test fixtures
# (Slice 9b): illustrative, non-vacuous values so nsdp_device() has
# something real to decode on every one of the 5 newly-parsed tags.
nsdp_qos_engine=1, # port-based
nsdp_port_mirroring_dest=10,
nsdp_port_mirroring_sources=frozenset({1, 2}),
nsdp_igmp_snooping_enabled=True,
nsdp_igmp_snooping_vlan=90,
nsdp_broadcast_filtering=True,
nsdp_loop_detection=True,
)
[docs]
def seed_gs110emx_fw1028() -> VirtualSwitchState:
"""The SAME GS110EMX, as measured LIVE on 2026-07-30 at firmware 1.0.2.8.
Every value below came off the wire that day rather than out of the older
committed HTML fixtures ``seed_gs110emx`` transcribes, and each is here
because a claim in this library turned out to be wrong about it:
* ``nsdp_auth_version=0x10`` -- AUTH_V2_ENCPASS (0x0014) answers
0x00000010 and this firmware accepts ONLY the v2 salted
challenge-response. An NSDP WRITE_REQUEST bearing the v1 repeating-XOR
PASSWORD TLV is answered error=13 (then 14 on the next try) with the
header error-attribute set to 0x000A/ATTR_PASSWORD; a plaintext password
fares identically. v2 writes DO work here -- the token is an 8-byte XOR
fold of password+salt+MAC sent in a leading 0x001A TLV, cracked and
live-verified on this very unit (see ``protocols/nsdp/auth.py``).
* ports 9 and 10 at 10000 Mbps -- their PORT_STATUS speed byte is 0x06
(``09 06 01`` / ``0a 06 01``), which the decoder used to treat as an
unknown code and report as LINK DOWN.
* port descriptions "Nicole's Room" (6) and "TV Room" (8), read from tag
0xB000 and matching that switch's own Port Status page exactly.
* ``flow_control=True`` on every port, matching the page's Flow Control
column (its sibling 10.1.5.27, with the column set to Disable throughout,
answers 0x00 on all ten ports).
* the twelve VLAN ids the unit really carries, from tag 0x2800.
"""
speeds = {6: 100, 8: 1000, 9: 10000, 10: 10000}
descriptions = {6: "Nicole's Room", 8: "TV Room"}
ports = {
p: PortSim(
name=f"g{p}",
admin=True,
link=p in speeds,
speed=speeds.get(p, 0),
description=descriptions.get(p),
flow_control=True,
)
for p in range(1, 11)
}
# Read off tag 0x2800 on 10.1.5.25: VLAN 1 has all ten ports with 6/9/10
# tagged; the other eleven VLANs are all-ports-tagged.
vlans = {
1: VlanSim(name="", member=set(range(1, 11)), untagged={1, 2, 3, 4, 5, 7, 8})
}
for vid in (4, 5, 6, 7, 10, 20, 21, 41, 90, 99, 121):
vlans[vid] = VlanSim(name="", member=set(range(1, 11)), untagged=set())
return VirtualSwitchState(
model_key="gs110emx",
ports=ports,
vlans=vlans,
pvids={**dict.fromkeys(range(1, 11), 1), 6: 4},
mgmt=MgmtSim(
address="10.1.5.25",
netmask="255.255.255.0",
gateway="10.1.5.1",
mode="static",
),
model_name="GS110EMX",
serial="53H60253A0032",
firmware="1.0.2.8",
hostname="sw-netgear-gs110emx1",
nsdp_mac=b"\xbc\xa5\x11\xb8\xec\xf1",
nsdp_password="password",
nsdp_auth_version=0x10,
nsdp_qos_engine=2,
nsdp_port_mirroring_dest=0,
nsdp_port_mirroring_sources=frozenset(),
nsdp_igmp_snooping_enabled=True,
nsdp_igmp_snooping_vlan=1,
nsdp_broadcast_filtering=False,
# Tag 0x9000 (LOOP_DETECTION) is one this firmware does NOT answer at
# all -- it is absent from the measured tag inventory -- so it stays
# None, which is exactly how nsdp_tlvs() omits it.
nsdp_loop_detection=None,
)
[docs]
def seed_gs305ep() -> VirtualSwitchState:
"""Build an ILLUSTRATIVE GS305EP (5-port, PoE ports 1-4) virtual state.
HAND-INVENTED: no capture of any kind exists for gs305ep. The port speeds,
the 12800 mW PoE reading, VLAN 90 and the PVIDs are all structural test
data, NOT observed values -- same convention as ``seed_gsm7228ps``, which
says so explicitly. Only the shape is grounded: the Plus family genuinely
has no MAC/FDB, no box sensors and no LLDP over its web UI.
"""
ports = {
p: PortSim(
name=f"Port {p}", admin=p != 3, link=p == 1, speed=1000 if p == 1 else 0
)
for p in range(1, 6)
}
ports[1].rx_octets = 1_000_000
ports[1].tx_octets = 2_000_000
ports[1].rx_errors = 0
vlans = {
1: VlanSim(name="default", member={1, 2, 3, 4, 5}, untagged={3, 4, 5}),
90: VlanSim(name="iot", member={1, 2}, untagged={1, 2}),
}
pvids = {1: 90, 2: 90, 3: 1, 4: 1, 5: 1}
poe = {
1: PoeSim(admin=True, detect=3, power_mw=12_800),
2: PoeSim(admin=True, detect=1, power_mw=0),
3: PoeSim(admin=True, detect=1, power_mw=0),
4: PoeSim(admin=False, detect=1, power_mw=0),
}
return VirtualSwitchState(
model_key="gs305ep", ports=ports, vlans=vlans, pvids=pvids, poe=poe
)
[docs]
def seed_gs105pe() -> VirtualSwitchState:
"""Build a GS105PE (5-port Plus, NSDP+HTTP) virtual state from a REAL live
capture (host 10.1.5.30 / poe-micro3, 2026-07-21 -- see
netgear-m4300-http-cheetah / the gs105pe live findings). Every value below
is transcribed from the captured NsdpDevice: ports 3 (100M) and 5 (1G) up,
the rest down; VLANs 1/41/90 with their real member/untagged sets; real
PVIDs; DHCP mgmt-IP; and the QoS/mirroring/IGMP engine tags. Port mirroring
is OFF on this unit (dest 0, no sources) -- the 3-byte PORT_MIRRORING TLV
that exposed the fixed-width parser bug (see parse_port_mirroring)."""
ports = {
p: PortSim(
name=f"Port {p}",
admin=True,
link=p in (3, 5),
speed={3: 100, 5: 1000}.get(p, 0),
)
for p in range(1, 6)
}
ports[3].tx_octets = 10_246_512
ports[5].rx_octets = 29_303_468
ports[5].tx_octets = 289_149
ports[5].rx_errors = 228_666
vlans = {
1: VlanSim(name="", member={5}, untagged={5}),
41: VlanSim(name="", member={1, 2, 4, 5}, untagged={1, 2, 4}),
90: VlanSim(name="", member={3, 5}, untagged={3}),
}
pvids = {1: 41, 2: 41, 3: 90, 4: 41, 5: 1}
return VirtualSwitchState(
model_key="gs105pe",
ports=ports,
vlans=vlans,
pvids=pvids,
mgmt=MgmtSim(
address="10.1.5.30",
netmask="255.255.255.0",
gateway="10.1.5.1",
mode="dhcp",
),
model_name="GS105PE",
serial="61W19753A00A8",
firmware="V1.6.0.4",
hostname="poe-micro3",
nsdp_mac=b"\x38\x94\xed\xb7\xcd\xe0",
nsdp_password="password",
nsdp_qos_engine=2,
nsdp_port_mirroring_dest=0,
nsdp_port_mirroring_sources=frozenset(),
nsdp_igmp_snooping_enabled=True,
nsdp_igmp_snooping_vlan=1,
nsdp_broadcast_filtering=False,
nsdp_loop_detection=False,
)
def _mac_hex_to_raw(hexstr: str) -> str:
"""``"88:A2:9E:80:87:01"`` -> the 6 raw latin-1 bytes it represents.
The real-hardware captures (``tests/fixtures/captures/*.json``) store
already-PARSED values (e.g. ``MacEntry.mac``/``LLDPNeighbor.remote_chassis_id``
are colon-hex text), not the raw wire bytes. Seeding ``oid_map()`` needs
the raw bytes back (a MAC-address chassis/port-id subtype is genuinely
binary on the wire -- see ``parse._format_chassis_id``/``_format_port_id``),
so this is the exact inverse of that formatting.
"""
return "".join(chr(int(p, 16)) for p in hexstr.split(":"))
# The FASTPATH vendor switchport configuration for the two M4300s, READ OFF THE
# REAL SWITCHES on 2026-07-30 (read-only walk of
# 1.3.6.1.4.1.4526.10.1.2.8.37.1 with community "public"; 1520 rows on the -24X,
# 1440 on the -16X -- neither the GSM7252PS nor the S3300 publishes this table at
# all, 0 rows each).
#
# This is seeded, not computed, precisely so the mock is an INDEPENDENT source of
# truth: state.py derives Q-BRIDGE membership FROM these columns, so if that
# derivation rule is wrong the derived membership stops matching the captured
# membership these seeds ship alongside. tests/virtual/test_switchport_vlan_write
# .py::test_seeded_switchport_columns_reproduce_the_captured_membership pins that,
# and a second test replays all 40 live ports' columns against the live-reported
# membership as a straight data table.
#
# Tuple layout: (mode, access_vlan, native_vlan, allowed_vlans, general_untagged,
# general_tagged) where allowed_vlans is None for "all 4093" and
# the two general_* lists are columns 7/8 (notWritable participation config that
# is only in force while mode == general(3)).
_ALL_ALLOWED = None
_SwitchportRow = tuple[int, int, int, "frozenset[int] | None", set[int], set[int]]
_M4300_24X_SWITCHPORT: dict[int, _SwitchportRow] = {
# Ports 1/2 are the real uplink trunks; VLANs 3990 and 4007 happen to be
# absent from their allowed lists on the live device (both are non-existent
# VLANs, so neither affects membership -- kept because it is what was read).
1: (2, 1, 1, frozenset(set(range(1, 4094)) - {3990, 4007}), {1}, set()),
2: (2, 1, 1, frozenset(set(range(1, 4094)) - {3990, 4007}), {1}, set()),
3: (1, 5, 5, _ALL_ALLOWED, {1}, set()),
4: (1, 5, 5, _ALL_ALLOWED, {1}, set()),
# Port 5 is the most informative row on either switch: a trunk whose ACCESS
# VLAN (90) differs from its NATIVE VLAN (5) -- proving membership follows
# col4 not col3 in trunk mode -- with a genuinely sparse allowed list.
5: (
2,
90,
5,
frozenset({1, 5, 6, 7, 10, 20, 41, 90, 99, 121, 141}),
{1, 90},
set(),
),
6: (1, 90, 5, _ALL_ALLOWED, {1}, set()),
7: (1, 1, 1, _ALL_ALLOWED, {1}, set()),
8: (1, 1, 1, _ALL_ALLOWED, {1}, set()),
# 9-14: access on VLAN 5 with native 1 -- another proof col4 is ignored in
# access mode -- and col7 reads 5 here while col7 reads 1 on 15-24, which is
# why col7 cannot be a mirror of effective membership.
**{p: (1, 5, 1, _ALL_ALLOWED, {5}, set()) for p in range(9, 15)},
**{p: (1, 10, 10, _ALL_ALLOWED, {1}, set()) for p in range(15, 25)},
}
_M4300_16X_SWITCHPORT: dict[int, _SwitchportRow] = {
# 1-8 are GENERAL mode: membership comes from col7/col8, so despite an access
# VLAN of 10/90 these ports are untagged in VLAN 1 -- exactly what the
# committed capture shows.
**{p: (3, 10, 10, _ALL_ALLOWED, {1}, set()) for p in range(1, 5)},
**{p: (3, 90, 90, _ALL_ALLOWED, {1}, set()) for p in range(5, 9)},
**{p: (2, 1, 1, _ALL_ALLOWED, {1}, set()) for p in range(9, 17)},
# NOTE ports 11 and 12 read (2, 4, 4, all, {1}, {}) and
# (2, 5, 5, all-minus-5, {1,4}, {5,6,7,10,20,21,41,89,90,99,121,141}) on the
# live device TODAY -- someone has re-homed them since the committed capture
# was taken (its membership has both untagged in VLAN 1). Seeded to the shape
# the CAPTURE implies, which is identical to their sibling trunks 9/10/13-16,
# so the mock stays internally coherent. This is device config drift, NOT a
# mock/hardware behavioural difference -- the live values for both ports are
# replayed in test_live_switchport_columns_derive_the_live_membership, and
# port 12 is the case that proves a native VLAN is untagged even when it is
# absent from the allowed list.
}
def _apply_switchport_seed(
state: VirtualSwitchState, table: dict[int, _SwitchportRow]
) -> None:
"""Load a measured switchport table into ``state`` (see the tables above)."""
from .state import _all_vlans_bitmap, _vlan_bitmap_bytes
for port, (mode, access, native, allowed, gen_u, gen_t) in table.items():
state.switchport_mode[port] = mode
state.switchport_access_vlan[port] = access
state.switchport_native_vlan[port] = native
state.switchport_allowed_vlans[port] = (
_all_vlans_bitmap() if allowed is None else _vlan_bitmap_bytes(set(allowed))
)
state.switchport_general_untagged[port] = set(gen_u)
state.switchport_general_tagged[port] = set(gen_t)
# M4300-24X (28 registered ports, 0 PoE -- Fully Managed, SNMP-only): every
# value below is transcribed directly from the real captured snapshot
# (tests/fixtures/captures/m4300-24x.json, host 10.1.5.13) -- port
# name/admin/link/speed/description/counters, VLANs (all 14, full real
# member/untagged sets), PVIDs, sensors, and the base MAC. The real switch
# exposes 155 ifIndexes (24 physical + a CPU interface + 128 LAG placeholders
# + 2 VLAN interfaces); only a representative slice of the non-physical ones
# is seeded (the one real in-use LAG plus one unused placeholder, the CPU
# interface, and both VLAN interfaces) rather than all 128 mostly-identical
# unused LAGs -- the model's CAPABILITIES (port count/names/speeds, VLANs,
# PVIDs, sensors, mgmt-IP, and CRUCIALLY the absence of PoE) match the
# capture exactly. dot1dBaseBridgeAddress is VERIFIED to come back as ASCII
# colon-hex text on this exact model (see ``_mac_from_ascii_text``), so
# ``dot1d_base_mac_ascii=True`` here specifically -- NOT on m4300-16x below,
# where no such quirk has been captured.
[docs]
def seed_m4300_24x() -> VirtualSwitchState:
"""Build a realistic M4300-24X (24-port, non-PoE) virtual switch state."""
_phys = ( # port, name, admin, link, speed_mbps(0=down), description
(1, "1/0/1", True, True, 10000, "trunk.sw-cisco-shed"),
(2, "1/0/2", True, True, 10000, "trunk.gsm7252ps-s1"),
(3, "1/0/3", True, True, 1000, "bmc.big-storage"),
(4, "1/0/4", True, False, 0, "bmc.gpu"),
(5, "1/0/5", True, True, 100, "openmesh.wifi"),
(6, "1/0/6", True, True, 1000, "eth0.rpi4-ups"),
(7, "1/0/7", True, False, 0, "empty"),
(8, "1/0/8", True, False, 0, "empty"),
(9, "1/0/9", True, True, 1000, "oob1.sw-bb-25g"),
(10, "1/0/10", True, True, 1000, "oob2.sw-bb-25g"),
(11, "1/0/11", True, False, 0, "oob1.sw-bb-100g"),
(12, "1/0/12", True, False, 0, "oob2.sw-bb-100g"),
(13, "1/0/13", True, False, 0, "bmc1.nvmeof"),
(14, "1/0/14", True, False, 0, "bmc2.nvmeof"),
(15, "1/0/15", True, False, 0, "empty"),
(16, "1/0/16", True, False, 0, "empty"),
(17, "1/0/17", True, False, 0, "10g1.gpu"),
(18, "1/0/18", True, False, 0, "10g2.gpu"),
(19, "1/0/19", True, True, 10000, "10g1.big-storage"),
(20, "1/0/20", True, True, 10000, "10g2.big-storage"),
(21, "1/0/21", True, True, 10000, "lag.sw-bb-25g"),
(22, "1/0/22", True, True, 10000, "lag.sw-bb-25g"),
(23, "1/0/23", True, True, 10000, "lag.sw-bb-25g"),
(24, "1/0/24", True, True, 10000, "lag.sw-bb-25g"),
)
# (port, rx_bytes, tx_bytes, rx_errors) -- real captured ifHCIn/OutOctets
# + ifInErrors; tx_errors is 0 for every port on this capture.
_stats = {
1: (14778916968081, 11768639639224, 5),
2: (22592906553, 72917119482, 0),
3: (2762192715, 3069701383, 0),
4: (0, 0, 0),
5: (9928397370, 103562789705, 0),
6: (2936543951, 6369912656, 0),
7: (0, 0, 0),
8: (0, 0, 0),
9: (241280077, 1045875073, 0),
10: (79644425, 1532447568, 0),
11: (0, 0, 0),
12: (0, 0, 0),
13: (0, 4321, 0),
14: (0, 4385, 0),
15: (0, 0, 0),
16: (0, 0, 0),
17: (0, 0, 0),
18: (0, 0, 0),
19: (10574049492450, 7436979985884, 0),
20: (906023695499, 3169248684569, 0),
21: (46742037001, 214440657859, 0),
22: (62196040279, 2295667872290, 0),
23: (53538213549, 4490316365, 0),
24: (60910004579, 1478343156644, 0),
}
ports: dict[int, PortSim] = {}
for port, name, admin, link, speed, desc in _phys:
rx_bytes, tx_bytes, rx_errors = _stats[port]
ports[port] = PortSim(
name=name,
admin=admin,
link=link,
speed=speed,
description=desc,
rx_octets=rx_bytes,
tx_octets=tx_bytes,
rx_errors=rx_errors,
tx_errors=0,
# "Disable" in the Flow Mode column of this model's own
# `show port all` capture -- see the note in seed_gsm7252ps. This
# SKU is also where reading that column by POSITION went wrong: the
# M4300 table appends a "Stack Capable" column after Flow Mode.
flow_control=False,
)
# Representative non-physical ifIndexes (see module docstring above):
# the CPU interface, one real in-use LAG + one unused placeholder LAG,
# and the switch's two VLAN interfaces.
ports[769] = PortSim(
name="CPU Interface: 0/15/1", admin=True, link=True, speed=0, if_type=1
)
ports[770] = PortSim(
name="lag 1",
admin=True,
link=True,
speed=40000,
description="lag.sw-bb-25g",
if_type=161,
)
ports[771] = PortSim(name="lag 2", admin=True, link=False, speed=0, if_type=161)
ports[898] = PortSim(name="vlan 1", admin=True, link=True, speed=10, if_type=135)
ports[899] = PortSim(name="vlan 5", admin=True, link=True, speed=10, if_type=135)
# All 14 real VLANs, full real member/untagged port sets (including the
# 128-wide LAG range 770-897 every VLAN's trunk carries) -- `tagged` is
# always `member - untagged` (see VirtualSwitchState.nsdp_tlvs), matching
# the capture's own tagged_ports for every VLAN checked.
_lags = set(range(770, 898)) # lag 1..128 -> ifIndex 770..897
vlans = {
1: VlanSim(
name="default",
member={1, 2, 5, 7, 8} | _lags,
untagged={1, 2, 7, 8} | _lags,
),
4: VlanSim(name="wifi", member={1, 2, 770}, untagged=set()),
5: VlanSim(
name="net",
member={1, 2, 3, 4, 5, 9, 10, 11, 12, 13, 14, 770},
untagged={3, 4, 5, 9, 10, 11, 12, 13, 14},
),
6: VlanSim(name="pwr", member={1, 2, 5, 770}, untagged=set()),
7: VlanSim(name="store", member={1, 2, 5, 770}, untagged=set()),
10: VlanSim(
name="int",
member={1, 2, 5, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 770},
untagged={15, 16, 17, 18, 19, 20, 21, 22, 23, 24},
),
20: VlanSim(name="roam", member={1, 2, 5, 770}, untagged=set()),
21: VlanSim(name="fpgas", member={1, 2, 770}, untagged=set()),
41: VlanSim(name="sm", member={1, 2, 5, 770}, untagged=set()),
89: VlanSim(name="sdr", member={1, 2, 770}, untagged=set()),
90: VlanSim(name="iot", member={1, 2, 5, 6, 770}, untagged={6}),
99: VlanSim(name="guest", member={1, 2, 5, 770}, untagged=set()),
121: VlanSim(name="t-fpgas", member={1, 2, 5, 770}, untagged=set()),
141: VlanSim(name="t-sm", member={1, 2, 5, 770}, untagged=set()),
}
pvids = {
1: 1,
2: 1,
3: 5,
4: 5,
5: 5,
6: 90,
7: 1,
8: 1,
9: 5,
10: 5,
11: 5,
12: 5,
13: 5,
14: 5,
15: 10,
16: 10,
17: 10,
18: 10,
19: 10,
20: 10,
21: 10,
22: 10,
23: 10,
24: 10,
}
sensors = [
SensorSim(kind="fan", instance="0", raw="5160"),
SensorSim(kind="fan", instance="1", raw="4560"),
SensorSim(kind="power", instance="0", raw="49"),
SensorSim(kind="temperature", instance="1", raw="49"),
]
# A representative slice of the real (30-capped) captured MAC/FDB table,
# identity-mapped bridge-port -> ifIndex (see gsm7252ps's seed for the
# non-identity-mapping case; that path is already covered there).
macs = [
MacSim(vlan=1, mac_bytes=(0x00, 0x0A, 0xFA, 0x24, 0x28, 0x20), bridge_port=1),
MacSim(vlan=90, mac_bytes=(0x00, 0xE0, 0x4C, 0x68, 0x36, 0x95), bridge_port=1),
MacSim(vlan=1, mac_bytes=(0x02, 0x00, 0x0A, 0x01, 0x00, 0x01), bridge_port=1),
]
# Real LLDP neighbours (a representative few of the capture's list): mixed
# MAC-shaped (raw-bytes) and plain-text port-id subtypes on purpose, so
# both `_format_port_id` branches round-trip through the mock.
lldp = [
LldpSim(
time_mark=1,
local_port=1,
rem_idx=1,
chassis=_mac_hex_to_raw("88:A2:9E:80:87:01"),
port_id=_mac_hex_to_raw("88:A2:9E:80:87:01"),
port_desc="eth0",
sys_name="rpi-sdr-kraken",
),
LldpSim(
time_mark=1,
local_port=2,
rem_idx=1,
chassis=_mac_hex_to_raw("E0:91:F5:0C:D6:DB"),
port_id="1/0/49", # plain interface name, NOT a MAC -- text subtype
port_desc="1/0/2.sw-netgear-m4300-24x",
sys_name="sw-netgear-gsm7252ps-s1.welland.mithis.com",
),
LldpSim(
time_mark=1,
local_port=6,
rem_idx=1,
chassis=_mac_hex_to_raw("E4:5F:01:8D:F4:FD"),
port_id=_mac_hex_to_raw("E4:5F:01:8D:F4:FD"),
port_desc="eth0",
sys_name="rpi4-ups",
),
]
mgmt = MgmtSim(
address="10.1.5.13",
netmask="255.255.255.0",
gateway="10.1.5.1",
# Real capture reports mode="unknown" (the UNVERIFIED DHCP-mode OID
# -- see VendorOids.dhcp_mode_unverified); "static" is the honest,
# documented best inference for a device with a real static address,
# not itself a captured value.
mode="static",
)
state = VirtualSwitchState(
# Measured 2026-08-02: sysName and `show hosts` both report this on the
# real switch. Was empty here, which no real FASTPATH switch is.
# MEASURED 2026-08-02 on 10.1.5.13: `show logging` reports
# "Syslog Logging : enabled" and local port 514, and
# `show logging hosts` one row 10.1.5.1 / info(6) / 514 / Active.
syslog=SyslogSim(
admin_mode=1,
local_port=514,
collectors=[SyslogCollectorSim("10.1.5.1", 514, 6)],
),
# MEASURED 2026-08-03 on 10.1.5.13's own userManagement.html -- the
# SAME two words the gsm7252ps page uses, even though this switch's
# `show users` says "Privilege-15" where that one says "Read/Write".
# The web UI is the consistent face; the CLI is not.
users=[UserSim("admin", "Super User"), UserSim("guest", "Read Only")],
# MEASURED 2026-08-03 on 10.1.5.13, HTTP pages and CLI agreeing on
# every state and every port the pages print. Unlike the gsm7252ps this
# switch's http/https/ssh pages DO print their ports; its telnet page
# still does not (the CLI reports 23, the page has no such field), so
# telnet's port stays None here -- the mock must not print what the
# device does not.
services={
"http": ServiceSim(enabled=True, port=80),
"https": ServiceSim(enabled=True, port=443),
"ssh": ServiceSim(enabled=True, port=22),
"telnet": ServiceSim(enabled=True),
},
hostname="sw-netgear-m4300-24x",
model_key="m4300-24x",
ports=ports,
vlans=vlans,
pvids=pvids,
# Real fixed Q-BRIDGE PortList width, measured LIVE (read-only) on the
# M4300 @10.1.5.13: dot1qVlanStaticEgressPorts is 131 bytes wide.
vlan_portlist_width=131,
# MEASURED off the real VLAN-Membership capture (2026-07-30,
# tests/fixtures/http/m4300_vlanportcfg_vlan1.html): 152 hiddenMem slots
# = 24 ports + 128 LAGs, LAG ifNames 0/13/N, the togImg/switch_*.png
# grid, HTML-escaped ifName lists, and this firmware's TRAILING comma on
# hiddenMem/hiddenTagged. No CSRFToken on the 24X (the 16X has one).
vlan_membership_page=VlanMembershipPageSim(
slots=152, lag_slot=13, grid="png", trailing_comma=True, escape=True
),
# LIVE-PROVEN 2026-07-30 on 10.1.5.13: EVERY port on this switch is
# ``switchport mode access`` or ``trunk`` (per its own
# ``show running-config``), and the M4300 image only accepts an explicit
# VLAN-membership apply on a port in ``general`` mode. Applying anyway
# returns HTTP 200 with ``err_flag=1`` and
# ``err_msg="Unable to set VLAN membership for VLAN ( 4004 )"``. The
# sibling M4300-16X (10.1.5.20) leaves ports 1-8 with no switchport-mode
# line at all and the SAME apply succeeds there -- so this is per-port
# configuration, not a per-model capability, and the mock models it
# per-port too.
vlan_membership_locked_ports=frozenset(range(1, 25)),
poe={}, # VERIFIED: real capture's poe=[] -- this model has NO PoE.
sensors=sensors,
macs=macs,
lldp=lldp,
mgmt=mgmt,
# Real captured base MAC (mgmt_ip.base_mac in the capture).
nsdp_mac=bytes.fromhex("8C3BAD6BBBE0"),
# VERIFIED on this exact model (see field docstring + parse.py's
# _mac_from_ascii_text): dot1dBaseBridgeAddress comes back as ASCII
# colon-hex text on the real M4300-24X, not raw OCTET STRING bytes.
dot1d_base_mac_ascii=True,
# Illustrative sysDescr/sysObjectID (Task 2 model detection) -- same
# honesty convention as seed_gsm7252ps: sysDescr just needs to
# contain the real model name; sysObjectID has no known real value
# (no OID->model table exists) so this is a placeholder under the
# model's own vendor subtree, never a claim about real hardware.
sys_descr="NETGEAR M4300-24X (XSM4324CS) Managed Switch",
sys_object_id="1.3.6.1.4.1.4526.10.100.24",
)
# The real vendor switchport configuration behind the captured membership.
_apply_switchport_seed(state, _M4300_24X_SWITCHPORT)
return state
# M4300-16X (16 registered ports, all 16 PoE -- Fully Managed, SNMP-only):
# same transcription approach as seed_m4300_24x, from
# tests/fixtures/captures/m4300-16x.json (host unrecorded in that capture).
# The real capture's mgmt_ip.address is None (no static IP was ever
# discovered over this OID chain on that device) -- honestly left unseeded
# (the default blank 0.0.0.0/dhcp MgmtSim) rather than inventing one; the
# real captured base MAC is kept, so get_mgmt_ip().base_mac is still real.
# dot1d_base_mac_ascii is NOT set here: only the M4300-24X's ASCII-text quirk
# has been captured/verified (see seed_m4300_24x and parse.py's
# _mac_from_ascii_text docstring) -- this model uses the standard raw-bytes
# encoding.
[docs]
def seed_m4300_16x() -> VirtualSwitchState:
"""Build a realistic M4300-16X (16-port, all-16 PoE) virtual switch state."""
_phys = ( # port, name, admin, link, speed_mbps(0=down)
(1, "1/0/1", True, False, 0),
(2, "1/0/2", True, False, 0),
(3, "1/0/3", True, False, 0),
(4, "1/0/4", True, False, 0),
(5, "1/0/5", True, False, 0),
(6, "1/0/6", True, False, 0),
(7, "1/0/7", True, False, 0),
(8, "1/0/8", True, False, 0),
(9, "1/0/9", True, False, 0),
(10, "1/0/10", True, False, 0),
(11, "1/0/11", True, True, 1000),
(12, "1/0/12", True, True, 1000),
(13, "1/0/13", True, False, 0),
(14, "1/0/14", True, False, 0),
(15, "1/0/15", True, False, 0),
(16, "1/0/16", True, True, 10000),
)
# (port, rx_bytes, tx_bytes) -- real captured ifHCIn/OutOctets; every
# port's ifInErrors/ifOutErrors is 0 on this capture.
_stats = {
1: (0, 0),
2: (0, 0),
3: (0, 0),
4: (0, 0),
5: (0, 0),
6: (0, 0),
7: (0, 0),
8: (0, 0),
9: (0, 0),
10: (0, 0),
11: (0, 7813924),
12: (30388, 7819868),
13: (0, 0),
14: (0, 0),
15: (0, 0),
16: (3347925876, 7868391),
}
ports: dict[int, PortSim] = {}
for port, name, admin, link, speed in _phys:
rx_bytes, tx_bytes = _stats[port]
ports[port] = PortSim(
name=name,
admin=admin,
link=link,
speed=speed,
rx_octets=rx_bytes,
tx_octets=tx_bytes,
rx_errors=0,
tx_errors=0,
# "Disable" on all 16 ports of this model's own `show port all`
# capture -- see the note in seed_gsm7252ps.
flow_control=False,
)
ports[769] = PortSim(
name="CPU Interface: 0/15/1", admin=True, link=True, speed=0, if_type=1
)
ports[770] = PortSim(name="lag 1", admin=True, link=False, speed=0, if_type=161)
ports[898] = PortSim(name="vlan 5", admin=True, link=True, speed=10, if_type=135)
_lags = set(range(770, 898))
_uplink_ports = {9, 10, 11, 12, 13, 14, 15, 16}
vlans = {
1: VlanSim(
name="default",
member=set(range(1, 17)) | _lags,
untagged=set(range(1, 17)) | _lags,
),
4: VlanSim(name="wifi", member=set(_uplink_ports), untagged=set()),
5: VlanSim(name="net", member=set(_uplink_ports), untagged=set()),
6: VlanSim(name="pwr", member=set(_uplink_ports), untagged=set()),
7: VlanSim(name="store", member=set(_uplink_ports), untagged=set()),
10: VlanSim(name="int", member=set(_uplink_ports), untagged=set()),
20: VlanSim(name="roam", member=set(_uplink_ports), untagged=set()),
21: VlanSim(name="fpgas", member=set(_uplink_ports), untagged=set()),
41: VlanSim(name="sm", member=set(_uplink_ports), untagged=set()),
89: VlanSim(name="sdr", member=set(_uplink_ports), untagged=set()),
90: VlanSim(name="iot", member=set(_uplink_ports), untagged=set()),
99: VlanSim(name="guest", member=set(_uplink_ports), untagged=set()),
121: VlanSim(name="t-fpgas", member=set(_uplink_ports), untagged=set()),
141: VlanSim(name="t-sm", member=set(_uplink_ports), untagged=set()),
}
pvids = dict.fromkeys(range(1, 17), 1)
poe = {
1: PoeSim(admin=True, detect=2, power_mw=0),
2: PoeSim(admin=True, detect=2, power_mw=0),
3: PoeSim(admin=True, detect=2, power_mw=0),
4: PoeSim(admin=True, detect=2, power_mw=0),
5: PoeSim(admin=True, detect=2, power_mw=0),
6: PoeSim(admin=True, detect=2, power_mw=0),
7: PoeSim(admin=True, detect=2, power_mw=0),
8: PoeSim(admin=True, detect=2, power_mw=0),
9: PoeSim(admin=True, detect=2, power_mw=0),
10: PoeSim(admin=True, detect=2, power_mw=0),
11: PoeSim(admin=True, detect=3, power_mw=5000), # delivering
12: PoeSim(admin=True, detect=3, power_mw=2100), # delivering
13: PoeSim(admin=True, detect=2, power_mw=0),
14: PoeSim(admin=True, detect=2, power_mw=0),
15: PoeSim(admin=True, detect=2, power_mw=0),
16: PoeSim(admin=True, detect=2, power_mw=0),
}
sensors = [
SensorSim(kind="fan", instance="0", raw="4200"),
SensorSim(kind="fan", instance="1", raw="4080"),
SensorSim(kind="power", instance="0", raw="40"),
SensorSim(kind="power", instance="1", raw="42"),
SensorSim(kind="temperature", instance="1", raw="42"),
]
macs = [
MacSim(vlan=1, mac_bytes=(0x80, 0xCC, 0x9C, 0x91, 0x4F, 0x8C), bridge_port=12),
MacSim(vlan=90, mac_bytes=(0x00, 0x08, 0xA2, 0x09, 0xEF, 0xED), bridge_port=16),
MacSim(vlan=1, mac_bytes=(0x00, 0x0A, 0xFA, 0x24, 0x28, 0x1F), bridge_port=16),
]
lldp = [
LldpSim(
time_mark=1,
local_port=12,
rem_idx=1,
chassis=_mac_hex_to_raw("80:CC:9C:91:4F:8C"),
port_id="5", # plain numeric device-port label, NOT a MAC
port_desc="Device Port 5",
sys_name="sw-poe-micro2",
),
LldpSim(
time_mark=1,
local_port=16,
rem_idx=1,
chassis=_mac_hex_to_raw("00:0A:FA:24:28:25"),
port_id=_mac_hex_to_raw("00:0A:FA:24:28:1F"),
port_desc="eth8",
sys_name="ten64.welland.mithis.com",
),
]
state = VirtualSwitchState(
# Measured 2026-08-02: sysName and `show hosts` both report this on the
# real switch. Was empty here, which no real FASTPATH switch is.
hostname="sw-netgear-m4300-16x-poe-s2",
model_key="m4300-16x",
ports=ports,
vlans=vlans,
pvids=pvids,
# Real fixed Q-BRIDGE PortList width, measured LIVE (read-only) on this
# switch @10.1.5.20: dot1qVlanStaticEgressPorts is 131 bytes wide.
vlan_portlist_width=131,
# MEASURED off the real VLAN-Membership capture (2026-07-30,
# tests/fixtures/http/m4300_16x_vlanportcfg_vlan4.html): 144 hiddenMem
# slots = 16 ports + 128 LAGs, LAG ifNames 0/13/N, togImg/switch_*.png
# grid, escaped ifName lists, trailing comma -- AND the per-page
# CSRFToken only this AV-era firmware carries (csrf=True), which the form
# builder must echo back.
vlan_membership_page=VlanMembershipPageSim(
slots=144,
lag_slot=13,
grid="png",
trailing_comma=True,
csrf=True,
escape=True,
),
poe=poe, # VERIFIED: real capture -- all 16 ports PoE-capable.
sensors=sensors,
macs=macs,
lldp=lldp,
# mgmt left at the default blank MgmtSim -- honest: the real capture's
# mgmt_ip.address was None (see module docstring above).
nsdp_mac=bytes.fromhex("8C3BAD691C38"), # real captured base MAC
sys_descr="NETGEAR M4300-16X (XSM4316) Managed Switch",
sys_object_id="1.3.6.1.4.1.4526.10.100.16",
)
# The real vendor switchport configuration behind the captured membership.
_apply_switchport_seed(state, _M4300_16X_SWITCHPORT)
return state
# Ports carrying every access VLAN as a tagged member on this unit: g1-g25 and
# g27 (the two SFP uplinks g26/g28 are absent from the captured membership).
_GS728TPP_TRUNK = set(range(1, 26)) | {27}
# Ports untagged in VLAN 1 (their access/native VLAN is 1) -- from the captured
# per-port JoinVLANList.
_GS728TPP_VLAN1 = {
2,
4,
6,
7,
8,
9,
10,
11,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
24,
25,
27,
}
[docs]
def seed_gs728tpp() -> VirtualSwitchState:
"""Build a GS728TPP (28-port Smart Managed Pro, SNMP+HTTP) virtual state
from REAL captures of the live switch 10.2.5.10 (2026-07-29 --
tmp/gs728tpp_ground_truth.json). The HTTP GoAhead ``wcd`` face renders these
values back through the same ``parse_goahead_*`` parsers the real captures
exercise. Ports g1-g28 (7 up: g2/g5/g12/g23/g24/g26/g28), the real 12 VLANs
with their member/untagged sets, real PVIDs, 24 PoE+ ports (all Searching,
0 mW on this idle unit), a subset of the real dynamic FDB, 4 LLDP neighbours,
the box DiagnosticsUnitList sensors (fan1/2 OK, fan3-5 absent, both PSU rows
OK, temp unreported) and the static mgmt-IP.
SNMP is now grounded in a real live walk (10.2.5.10, 2026-07-29 --
tmp/gs728tpp_snmp_full.json): this agent implements ZERO Netgear vendor OIDs
and serves everything via standard MIBs (registry snmp_vendor_base=None). So
``state.sensors`` (the vendor SNMP box-sensor set) stays empty; instead the
fan/PSU sensor INVENTORY is exposed via the standard ENTITY-MIB
``entity_components`` (Main/Redundant PowerSupply + Fan1/Fan2, the real
entPhysicalIndex/Class/Name/Descr rows from the capture) -- inventory ONLY,
no live value over SNMP. The HTTP sysInfo sensors (with live health status)
live in ``http_sensors``; that status is the real HTTP-only difference. PoE
per-port mW is likewise a vendor column this agent lacks, so SNMP get_poe
reports power_mw=None (vs HTTP's live 0)."""
up = {2, 5, 12, 23, 24, 26, 28}
speed100 = {5, 12, 23}
# CONFIGURED speed, from the same capture (tests/fixtures/http/
# gs728tpp_ports.xml): the 24 copper ports auto-negotiate
# (autoNegotiationAdminEnabled 1) while the four SFP uplinks 25-28 are
# FORCED to 1000 full (2). Every one of the 28 reports speedAdmin 1000 and
# duplexAdminMode 3 regardless, so the flag is the only thing telling the
# two apart -- which is exactly what the decoder has to get right, and what
# a seed with a uniform value could never exercise.
forced_1000 = {25, 26, 27, 28}
ports = {
p: PortSim(
name=f"g{p}",
admin=True,
link=p in up,
speed=100 if p in speed100 else 1000,
# This agent DOES serve the EtherLike duplex/pause columns (unlike
# the GSM7252PS), and every port reads flow control OFF -- measured
# 2026-08-03 across all 28 ports on both backends at once.
serves_etherlike=True,
flow_control=False,
autoneg_admin="2" if p in forced_1000 else "1",
)
for p in range(1, 29)
}
# The eight LAG pseudo-interfaces, MEASURED 2026-08-02 on the live switch:
# ifName "po 1".."po 8" at ifIndex 1000..1007, ifType 161 (ieee8023adLag).
# dot1dBasePortIfIndex is identity-mapped there, so those same numbers are
# the Q-BRIDGE PortList bit positions. They are seeded because they are what
# the bitmaps actually contain: without them the mock cannot reproduce the
# phantom "member port 1000" SNMP get_vlans used to report (parse_vlans).
ports.update(
{
idx: PortSim(
name=f"po {idx - 999}", admin=True, link=False, speed=0, if_type=161
)
for idx in range(1000, 1008)
}
)
_lags = set(range(1000, 1008))
# VLAN 1 is untagged on the access ports; every other VLAN is carried tagged
# on the trunk set, except the untagged sets captured below.
#
# LAG membership is MEASURED (2026-08-02): VLAN 1's current-table bitmap
# sets all eight LAG bits, while every configured VLAN's static bitmap sets
# bit 1000 alone. VLAN 1 also has NO dot1qVlanStaticTable row on this switch
# -- static_row=False -- which is precisely the VLAN a static-table-only
# reader lost.
vlans = {
1: VlanSim(
name="",
member=set(_GS728TPP_VLAN1) | _lags,
untagged=set(_GS728TPP_VLAN1) | _lags,
static_row=False,
),
2: VlanSim(name="Voice VLAN", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
# Bit 1000 alone: the live static bitmap for VLAN 3 is all-zero except
# that LAG bit, i.e. a VLAN whose ONLY member is a LAG.
3: VlanSim(name="Auto Video VLAN", member={1000}, untagged=set()),
5: VlanSim(
name="net", member=_GS728TPP_TRUNK | {1000}, untagged={3, 5, 12, 23}
),
6: VlanSim(name="pwr", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
7: VlanSim(name="store", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
10: VlanSim(name="int", member=_GS728TPP_TRUNK | {1000}, untagged={1}),
20: VlanSim(name="roam", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
31: VlanSim(name="fpgas", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
41: VlanSim(name="sm", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
90: VlanSim(name="iot", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
99: VlanSim(name="guest", member=_GS728TPP_TRUNK | {1000}, untagged=set()),
}
pvids = {
1: 10,
2: 1,
3: 5,
4: 1,
5: 5,
6: 1,
7: 1,
8: 1,
9: 1,
10: 1,
11: 1,
12: 5,
13: 1,
14: 1,
15: 1,
16: 1,
17: 1,
18: 1,
19: 1,
20: 1,
21: 1,
22: 1,
23: 5,
24: 1,
25: 1,
26: 1,
27: 1,
28: 1,
}
# The real dot1qPvid walk returns 36 rows, not 28: the eight LAGs carry a
# PVID too (all 1, measured 2026-08-02). parse_pvids already filters them
# out by ifType; seeding them keeps that filter honest work rather than a
# no-op against a mock that never presents the case.
pvids.update(dict.fromkeys(range(1000, 1008), 1))
poe = {p: PoeSim(admin=True, detect=2, power_mw=0) for p in range(1, 25)}
# A subset of the real dynamic FDB (VLANs 1 and 5, on physical ports).
macs = [
MacSim(vlan=1, mac_bytes=(0x00, 0x0A, 0xFA, 0x24, 0x28, 0xD8), bridge_port=24),
MacSim(vlan=1, mac_bytes=(0x02, 0x00, 0x0A, 0x02, 0x00, 0x01), bridge_port=24),
MacSim(vlan=1, mac_bytes=(0x02, 0x00, 0x0A, 0x02, 0x01, 0x01), bridge_port=24),
MacSim(vlan=1, mac_bytes=(0x2C, 0xCF, 0x67, 0xBB, 0x49, 0xA1), bridge_port=2),
MacSim(vlan=5, mac_bytes=(0x02, 0x00, 0x0A, 0x02, 0x00, 0x01), bridge_port=24),
MacSim(vlan=5, mac_bytes=(0x02, 0x00, 0x0A, 0x02, 0x05, 0x01), bridge_port=24),
MacSim(vlan=5, mac_bytes=(0xAC, 0x86, 0x74, 0x07, 0x94, 0x98), bridge_port=12),
MacSim(vlan=5, mac_bytes=(0xAC, 0x86, 0x74, 0x07, 0x94, 0x9F), bridge_port=12),
MacSim(vlan=5, mac_bytes=(0xAC, 0x86, 0x74, 0x07, 0x95, 0x80), bridge_port=23),
MacSim(vlan=5, mac_bytes=(0xAC, 0x86, 0x74, 0x07, 0x95, 0x87), bridge_port=23),
MacSim(vlan=5, mac_bytes=(0xAC, 0x86, 0x74, 0x07, 0x95, 0x88), bridge_port=5),
MacSim(vlan=5, mac_bytes=(0xAC, 0x86, 0x74, 0x07, 0x95, 0x8F), bridge_port=5),
]
_ten64 = "ten64.monarto.mithis.com"
# Chassis/port-id are the MAC-address LLDP subtype -> stored as the 6 raw
# bytes (like every other seed), so the SNMP face emits proper binary
# lldpRemChassisId/lldpRemPortId and the wcd web face decodes them to the
# real captured lowercase colon-hex. Values transcribed from the live LLDP
# capture (tmp/gs728tpp_ground_truth.json).
lldp = [
LldpSim(
time_mark=0,
local_port=2,
rem_idx=1,
chassis=_mac_hex_to_raw("2c:cf:67:bb:49:a1"),
port_id=_mac_hex_to_raw("2c:cf:67:bb:49:a1"),
port_desc="eth0",
sys_name="reterm1",
),
LldpSim(
time_mark=0,
local_port=24,
rem_idx=2,
chassis=_mac_hex_to_raw("00:0a:fa:24:28:d1"),
port_id=_mac_hex_to_raw("00:0a:fa:24:28:d8"),
port_desc="eth7",
sys_name=_ten64,
),
LldpSim(
time_mark=0,
local_port=26,
rem_idx=3,
chassis=_mac_hex_to_raw("00:0a:fa:24:28:d1"),
port_id=_mac_hex_to_raw("00:0a:fa:24:28:d9"),
port_desc="eth8",
sys_name=_ten64,
),
LldpSim(
time_mark=0,
local_port=28,
rem_idx=4,
chassis=_mac_hex_to_raw("00:0a:fa:24:28:d1"),
port_id=_mac_hex_to_raw("00:0a:fa:24:28:da"),
port_desc="eth9",
sys_name=_ten64,
),
]
# DiagnosticsUnitList wire fields (tag in ``instance``, code in ``raw``):
# 1=OK, 5=N/A(absent). Both PSU rows and fan1/fan2 report OK; fan3-5 and the
# temperature reading are absent on this unit.
http_sensors = [
SensorSim(kind="power", instance="mainPSStatus", raw="1"),
SensorSim(kind="power", instance="redundantPSStatus", raw="1"),
SensorSim(kind="fan", instance="fan1Status", raw="1"),
SensorSim(kind="fan", instance="fan2Status", raw="1"),
SensorSim(kind="fan", instance="fan3Status", raw="5"),
SensorSim(kind="fan", instance="fan4Status", raw="5"),
SensorSim(kind="fan", instance="fan5Status", raw="5"),
SensorSim(kind="temperature", instance="tempSensorValue", raw="0"),
SensorSim(kind="temperature", instance="tempSensorStatus", raw="2"),
]
# ENTITY-MIB entPhysical inventory (real entPhysicalIndex/Class/Name/Descr
# rows from the live capture): the two PSUs (class 6=powerSupply) and two
# fans (class 7=fan). This is the ONLY place SNMP names these components --
# no live value/status exists anywhere in this agent's SNMP.
entity_components = [
EntitySim(
index=67109185, phys_class=6, name="Main PowerSupply", descr="PowerSupply"
),
EntitySim(
index=67109186,
phys_class=6,
name="Redundant PowerSupply",
descr="PowerSupply",
),
EntitySim(index=67109249, phys_class=7, name="Fan1", descr="Fan"),
EntitySim(index=67109250, phys_class=7, name="Fan2", descr="Fan"),
]
return VirtualSwitchState(
model_key="gs728tpp",
ports=ports,
vlans=vlans,
pvids=pvids,
poe=poe,
macs=macs,
lldp=lldp,
http_sensors=http_sensors,
entity_components=entity_components,
mgmt=MgmtSim(
address="10.2.5.10",
netmask="255.255.255.0",
gateway="10.2.5.1",
mode="static",
),
model_name="GS728TPP",
serial="3AR476520016D",
firmware="6.0.1.30",
# The device's REAL PortList width, measured off the wire 2026-08-02:
# every dot1qVlanStatic/Current bitmap is 126 bytes (1008 bits), which
# is what makes room for the LAG bits at 1000-1007. Seeded, never
# derived -- a mock that recomputed it with the writer's own formula
# could only ever agree with the writer (principle 5).
vlan_portlist_width=126,
hostname="sw-netgear-gs728tpp",
nsdp_mac=b"\xb0\x39\x56\x77\x54\x29",
sys_descr="Netgear GS728TPP ProSafe Smart Managed Pro Switch",
# Real captured sysObjectID (1.3.6.1.4.1.4526.100.4.27): a bare
# identifier under 4526.100, NOT a vendor OID subtree the agent serves
# -- a walk of 1.3.6.1.4.1.4526 answers noSuchObject on this switch.
sys_object_id="1.3.6.1.4.1.4526.100.4.27",
)