Virtual switch¶
The mock switches. Narrative documentation is in The virtual switch; this is the reference.
Server and state¶
Virtual switch: an in-memory device state plus protocol “faces” onto it.
VirtualSwitchState (state.py) is the one authoritative in-memory device
state; seed.py hand-authors a realistic instance for a given model; the
faces subpackage (Task 15+) serves that state over a real protocol (e.g.
SNMP) so both transport clients can be tested against it end-to-end.
VirtualSwitch: a mock switch server binding protocol faces to a state.
Constructed from a model key, it seeds (or defaults) a VirtualSwitchState
and, on start(), binds whichever protocol faces the model’s registry
entry supports: SNMP for managed switches, NSDP and/or HTTP for Plus
switches. Each supported backend is bound in its own independent if
block, so a {NSDP, HTTP} model binds both an NSDP face (self.port)
and an HTTP face (self.http_port) concurrently.
- class netgear_switch.virtual.server.VirtualSwitch(
- model,
- community='public',
- http_password='password',
- *,
- host='127.0.0.1',
- port=0,
- http_port=0,
Bases:
objectA virtual switch server: a seeded state plus its bound protocol faces.
hostdefaults to loopback (127.0.0.1); pass another address (e.g.0.0.0.0to expose the mock to other hosts) to bind elsewhere. Theport/http_portarguments pin the UDP (SNMP or NSDP) and HTTP listen ports respectively; the default of0asks the OS for an ephemeral port, whose actual value is readable offself.port/self.http_portafterstart().- cli_session()[source]¶
Return an in-process mock FASTPATH CLI session over this switch’s state.
Unlike the SNMP/NSDP/HTTP faces (real sockets bound in
start()), the CLI face is an in-processCliSessionneeding no socket – seevirtual.faces.cli. RaisesUnsupportedCapabilityError(viacli_spec) for a model with no CLI backend.- Return type:
- netgear_switch.virtual.server.serve_forever(switches, *, out, stop=None, ready=None)[source]¶
Start
switches, print where each is reachable, and block untilstop.Each switch is
start()``ed independently; a switch that cannot bind any face (``UnsupportedCapabilityError) or otherwise fails to start is reported onoutand skipped, so one bad model never takes the rest of the fleet down. For every switch that does come up, its model, host, bound port(s) (with transport), SNMP community and HTTP password are printed so an external tool knows exactly where and how to connect.Blocks on
stop.wait()(a freshEventis created if none is passed — in that case only aKeyboardInterruptunblocks it) until signalled, then callsstop()on every switch it started, in reverse order, even on exception. Returns the number of switches successfully served (0 means nothing bound, and the function returns immediately without blocking).ready, if given, is set once every switch has been started and its endpoints printed — a test hook so a caller can wait for “fully up” before connecting, without racing the startup prints.- Return type:
The one authoritative in-memory virtual-switch device state.
VirtualSwitchState holds everything a simulated switch “knows” about
itself — port link/admin/speed, counters, VLANs, PoE, sensors, the MAC/FDB
table, LLDP neighbours and the management IP — as small mutable *Sim
dataclasses. oid_map() projects that state onto the flat numeric
OID -> (snmp_type, value) view a protocol face (Task 15) serves and the
Task 5-9 parsers consume. This module is pure data + projection: no network.
- exception netgear_switch.virtual.state.CommitFailedError[source]¶
Bases:
ExceptionThe agent accepted the varbind’s type but refused to apply it.
Models a real SNMP
commitFailed. VERIFIED on an M4300-24X (FASTPATH 12.0.13.8): a SET of dot1qVlanStaticEgressPorts commitFails even when writing back byte-identical octets, because per-port switchport mode – not the Q-BRIDGE PortList – owns VLAN membership on that firmware.
- exception netgear_switch.virtual.state.NotWritableError[source]¶
Bases:
ExceptionThe object exists but is read-only (real agents answer notWritable).
- exception netgear_switch.virtual.state.InconsistentValueError[source]¶
Bases:
ExceptionThe agent refuses this value in the device’s current state.
Models a real SNMP
inconsistentValue. VERIFIED on the GS728TPP (sw-netgear-gs728tpp.monarto.mithis.com / 10.2.5.10, firmware 6.0.1.30, 2026-08-03): every documented way of CREATING a dot1qVlanStaticTable row is refused with exactly this error – createAndGo(4) alone, createAndGo with dot1qVlanStaticName in the same PDU, createAndWait(5) then name then active(1), setting the name column alone, and createAndGo carrying an empty 126-byte egress PortList. The same firmware happily writes an EXISTING row’s membership and accepts destroy(6), so this is specifically row creation, not a read-only table.
- netgear_switch.virtual.state.encode_port_bitmap(ports, width_bytes=8)[source]¶
Inverse of
parse.decode_port_bitmap: a port set -> a latin-1 bitmap.Delegates to the canonical bytes encoder in
protocols/snmp/write.encode_port_bitmap(single source of truth for the MSB-first bit-packing) and decodes to the latin-1strthis module’s callers expect.- Return type:
- class netgear_switch.virtual.state.PortSim(
- name,
- admin,
- link,
- speed,
- if_type=6,
- rx_octets=None,
- tx_octets=None,
- rx_ucast=None,
- tx_ucast=None,
- rx_errors=None,
- tx_errors=None,
- switchport_mode='general',
- description=None,
- flow_control=True,
- serves_etherlike=False,
- physical_mode='Auto',
- autoneg_admin='1',
- speed_admin='1000',
- duplex_admin_mode='3',
Bases:
objectOne switch port’s link/admin/speed/name plus optional HC counters.
Counters are
int | None:Nonemeans “this port does not expose this counter” and must round-trip to an absent row inoid_map()(no fabricated zero), soparse_port_statsyieldsNonethere too.
- class netgear_switch.virtual.state.UserSim(name, http_access_mode)[source]¶
Bases:
objectOne local login account, as the switch’s own pages word it.
http_access_modeis stored VERBATIM rather than derived from a privilege flag, because the same account is worded DIFFERENTLY depending on which face is asked – measured on 10.1.5.22 and 10.1.5.13, where admin reads “Super User” on userManagement.html but “Read/Write” and “Privilege-15” respectively through each switch’s ownshow users. A mock that stored one level and rendered it per face would be inventing the wording; storing what each page really emits means the reader’s own word-to-privilege mapping is what gets exercised.The CLI face has no
show usersyet. When it gains one it needs its own field here, NOT this one: the two faces genuinely disagree.
- class netgear_switch.virtual.state.ServiceSim(enabled, port=None)[source]¶
Bases:
objectOne management service’s admin state, as its own config page reports it.
portisNonewhere the page carries NO port field – which is a real per-page difference, not a gap in the mock: the m4300 SSH page publishesv_1_10_1='22'while the gsm7252ps SSH page has no such coordinate at all (measured 2026-08-03). Seeding 22 there would make the fake claim a field the device does not print.
- class netgear_switch.virtual.state.SyslogCollectorSim(host, port, severity, status=1, index=1)[source]¶
Bases:
objectOne remote syslog collector row, as the vendor host table reports it.
Field values are SEEDED from a live switch, never computed: the severity is the standard syslog number the device actually returns (6 for “info” on m4300-24x, cross-checked against its own
show logging hosts), and status 1 is what that command prints as “Active”.- index: int = 1¶
The row’s index in the switch’s own host table. SPARSE on real hardware – m4300-24x 10.1.5.13 held Index 1 and Index 3 with nothing at 2 (2026-08-05) – so the mock stores it rather than deriving it from list position. A fake that renumbered densely could never catch a position-for-index bug, which is exactly the bug that got shipped.
- class netgear_switch.virtual.state.SyslogSim(admin_mode=2, local_port=514, collectors=<factory>)[source]¶
Bases:
objectThe switch’s remote-logging state, as the vendor
.14subtree reports it.MEASURED 2026-08-02 – see docs/superpowers/specs/.
admin_modeis the device’s own enum (1 = enabled, 2 = disabled), kept as the raw integer rather than a bool so the mock emits exactly what a real agent emits and the reader’s own decoding is what gets exercised.- collectors: list[SyslogCollectorSim]¶
- class netgear_switch.virtual.state.VlanSim(
- name,
- member=<factory>,
- untagged=<factory>,
- configured_only=<factory>,
- static_row=True,
Bases:
objectOne dot1q VLAN: display name plus egress-member and untagged port sets.
member/untaggedare the CURRENT (operational) egress sets – whatshow vlan <id>prints underCurrent: Include, what the FASTPATHvlanStatus.htmlMember Ports cell lists, and what the VLAN Membership page’shiddenTagged/hiddenUnTaggedifName lists carry.- property configured: set[int]¶
current members plus
configured_only.- Type:
The CONFIGURED egress set
- class netgear_switch.virtual.state.VlanMembershipPageSim(
- slots,
- lag_slot,
- grid,
- trailing_comma=False,
- csrf=False,
- escape=False,
Bases:
objectMEASURED shape of one model’s FASTPATH “VLAN Membership” page.
Every field below is a number/flag read off a real capture of
switching/dot1q/vlan_port_cfg.html(2026-07-30), NOT derived from the port count – deliberately, for the same reasonvlan_portlist_widthis seeded rather than computed: a mock that re-derives a device constant using the code’s own formula can only ever agree with the code (principle 5).Live measurements:
model
slots
lag_slot
grid
trailing_comma
csrf
escape
gsm7252ps
116
3
gif
no
no
no
gsm7228ps
78
3
png
no
no
yes
m4300-24x
152
13
png
yes
no
yes
m4300-16x
144
13
png
yes
yes
yes
slots= physical ports first, then the LAG pseudo-interfaces (soslots - port_countLAGs: 64 / 26 / 128 / 128).lag_slotis the middle component of a LAG ifName (0/3/Non the gsm72xx,0/13/Non the M4300).gridselects which of the two firmware generations’ port grids the page renders:"gif"= the oldertoggleImageFirst+grey_[btu].gifcells (0-BASED hiddenMem index),"png"= the jQuerytogImg+switch_<state>_inactive.pngcells (1-BASED index).trailing_commareproduces the M4300 firmware appending an empty field tohiddenMem/hiddenTagged.csrfrenders the per-pageCSRFTokenthe M4300-16X requires back on every POST.escapeHTML-entity-escapes the ifName lists (1/0/49) as every firmware but the gsm7252ps does.
- class netgear_switch.virtual.state.PoeSim(admin, detect, power_mw=0, cli_status_lag_reads=0)[source]¶
Bases:
objectOne PoE port: RFC3621 admin/detect state plus vendor delivered power.
- class netgear_switch.virtual.state.SensorSim(kind, instance, raw)[source]¶
Bases:
objectOne box sensor reading (fan RPM / PSU watts / temperature).
rawis the literal wire text: either a decimal integer string or Netgear’s"Not Supported"placeholder for an unpopulated slot.
- class netgear_switch.virtual.state.EntitySim(index, phys_class, name, descr)[source]¶
Bases:
objectOne ENTITY-MIB entPhysicalTable component (index + class/name/descr).
Used only by models whose SNMP agent exposes the fan/PSU sensor INVENTORY via the standard ENTITY-MIB rather than a Netgear vendor sensor column (verified: the GS728TPP).
phys_classis the entPhysicalClass int enum (6=powerSupply, 7=fan). No live value/status exists on the wire – this is inventory only (see protocols/snmp/parse.parse_entity_sensors).
- class netgear_switch.virtual.state.MacSim(vlan, mac_bytes, bridge_port)[source]¶
Bases:
objectOne learned MAC/FDB entry: VLAN, 6-byte MAC, bridge-port index.
- class netgear_switch.virtual.state.LldpSim(time_mark, local_port, rem_idx, chassis, port_id, port_desc, sys_name)[source]¶
Bases:
objectOne lldpRemTable neighbour row group.
- class netgear_switch.virtual.state.MgmtSim(address, netmask, gateway, mode)[source]¶
Bases:
objectThe switch’s own management-IP configuration.
- class netgear_switch.virtual.state.ScpCertDeploy(
- commands=<factory>,
- copies=<factory>,
- https_disabled=False,
- https_enabled=False,
- saved=False,
Bases:
objectRecord of a FASTPATH
copy scp://SSL-cert deploy the mock CLI face received (seevirtual.faces.cli.VirtualCliFace.run_scp_copy).Purely a record of the EXEC sequence the library ISSUED – the copy commands and their
nvram:destinations, plus the HTTPS toggle + save-config steps – so a test can assert the deploy driver drove the switch correctly. It does NOT carry cert bytes: the SCP deploy pulls the PEM from a staging server the caller set up (the library only sends the copy commands), so there is no PEM to observe here. Not part of any SNMP/NSDP/HTTP projection.
- class netgear_switch.virtual.state.VirtualSwitchState(
- model_key,
- ports=<factory>,
- vlans=<factory>,
- pvids=<factory>,
- poe=<factory>,
- sensors=<factory>,
- http_sensors=None,
- entity_components=<factory>,
- macs=<factory>,
- bridge_ports=<factory>,
- lldp=<factory>,
- mgmt=<factory>,
- model_name='',
- serial='',
- firmware='',
- hostname='',
- syslog=<factory>,
- users=<factory>,
- services=<factory>,
- nsdp_password='password',
- nsdp_auth_version=1,
- nsdp_last_salt=None,
- nsdp_auth_failures=0,
- nsdp_qos_engine=None,
- nsdp_port_mirroring_dest=None,
- nsdp_port_mirroring_sources=<factory>,
- nsdp_igmp_snooping_enabled=None,
- nsdp_igmp_snooping_vlan=None,
- nsdp_broadcast_filtering=None,
- nsdp_loop_detection=None,
- nsdp_mac=b'(\xc6\x8e\x00\x00\x01',
- sys_descr='',
- sys_object_id='',
- uploaded_cert=None,
- scp_cert_deploy=None,
- reboots=0,
- dot1d_base_mac_ascii=False,
- vlan_portlist_width=None,
- switchport_mode=<factory>,
- switchport_access_vlan=<factory>,
- switchport_native_vlan=<factory>,
- switchport_allowed_vlans=<factory>,
- switchport_general_untagged=<factory>,
- switchport_general_tagged=<factory>,
- pdu_egress_writes=<factory>,
- vlan_membership_page=None,
- vlan_membership_locked_ports=frozenset({}),
Bases:
objectThe one authoritative virtual-switch device state.
A mutable holder (later slices mutate it to simulate writes); pure data plus the
oid_map()SNMP projection, no network here.- syslog: SyslogSim¶
Remote-logging state. Only meaningful for a model with a vendor subtree; oid_map() projects it under <vendor base>.14 for those models only, which is what makes gs728tpp (no vendor OIDs) correctly unable to answer.
- users: list[UserSim]¶
Local login accounts, as userManagement.html lists them. Empty for a model whose UI has no such page located, so its HTTP face 404s that URL exactly as the real switch does.
- services: dict[str, ServiceSim]¶
Management-service admin state, keyed “http”/”https”/”ssh”/”telnet”, as each service’s own config page reports it. Empty for a model whose pages have not been located.
- scp_cert_deploy: ScpCertDeploy | None = None¶
- vlan_membership_page: VlanMembershipPageSim | None = None¶
- property sysinfo_sensors: list[SensorSim]¶
The sensor set the HTTP sysInfo page renders.
Returns
http_sensorswhen a model’s web UI exposes a different sensor set than SNMP (e.g. the gsm7252ps), else falls back tosensorsso a model whose two faces agree (the M4300) is unchanged.
- oid_map()[source]¶
Project this state onto the full numeric OID -> (type, value) view.
Built directly from the exact OID layouts in
protocols.snmp.oidsso a protocol face can serve it and the Task 5-9 parsers reconstruct the seeded state from what the face returns.
- snapshot()[source]¶
Deep-copy this state, for atomic multi-varbind SET rollback.
A single SNMP SET PDU can carry several varbinds (e.g.
set_vlan_membershipwriting both the egress and untagged bitmaps in oneset_manycall) and a real agent guarantees they apply all-or-nothing.faces/snmp.py’swrite_variablessnapshots the state before applying a PDU’s varbinds and callsrestoreon this snapshot if any of them fails, so a partial mutation is never observable. Seerestore.Also marks a PDU boundary:
pdu_egress_writes(which tracks same-PDU egress writes for the S3300’s auto-untag ordering quirk) is cleared here, becausefaces/snmp.pysnapshots exactly once per PDU.- Return type:
- restore(snapshot)[source]¶
Restore this state in place from a prior
snapshot()result.Copies every dataclass field from
snapshotontoselfrather than replacingselfitself, so existing references to this exact object (e.g.VirtualSwitch.state,StateMibView._state) keep seeing the restored data.
- apply_poe_admin(port, *, on)[source]¶
Switch a PSE port’s admin state, with the coherence a real PoE switch shows: admin off -> detect=1 (unused) and the data link drops; admin on -> detect=3 (delivering).
ONE rule shared by every protocol face – the SNMP SET path (
apply_write) and the CLIpoe/no poecommands both come through here, so the mock cannot behave differently depending on which backend a test drove (which would make cross-backend write parity meaningless). Unknown port: deliberate no-op, exactly as before.
- apply_poe_reset(port)[source]¶
Re-arm PSE detection on a port (the CLI’s
poe reset).Models what the hardware does: the port is powered down and detection starts again, so it ends up DELIVERING only if a powered device is actually drawing power (
power_mw), else back to SEARCHING (2) – a reset does NOT conjure a PD onto an empty port. This is what makescycle_poelegitimately time out on an empty port, exactly as it would on real hardware, whileclear_poe_fault(which only needs the port to LEAVE the fault state) succeeds.
- apply_write(oid, value)[source]¶
Mutate this state from one SNMP SET varbind, with device coherence.
Dispatches on the OID’s column prefix. Applies the same coherence a real PoE switch shows so
cycle_poeterminates against the mock: admin off -> detect=1 (unused) + data-port link down; admin on -> detect=3 (delivering). Unhandled writable OIDs are a deliberate no-op (the write “succeeds” but reads back unchanged), which is exactly what a verify-after-write must catch. (The SNMP face layer additionally rejects a SET on an OIDis_writable_oiddoesn’t recognize at all with a proper SNMP error, before it ever reaches here — seefaces/snmp.py.)
- nsdp_tlvs(tags)[source]¶
Project this state onto NSDP read TLVs for the requested tags.
MODEL / MAC / PORT_COUNT identity is always included (a real Plus switch echoes it, and
parse_deviceneeds the model + a port count to size VLAN bitmaps). Only tags this mock knows are emitted; unknown requested tags are silently skipped, exactly as real hardware does.
- apply_nsdp_write(tag, value)[source]¶
Mutate this state from one NSDP write TLV (verify-after-write reads it back). Unknown/read-only tags are a deliberate no-op.
- is_writable_oid(oid)[source]¶
True if
oidis one this mock recognizes as SNMP-writable.Mirrors
apply_write’s dispatch prefixes on purpose (single set of column constants fromprotocols.snmp.oids, kept in sync deliberately) so the SNMP face (faces/snmp.py) can reject a SET on a genuinely unknown/read-only OID with a proper SNMP error (notWritable) instead of the always-succeeding no-opapply_writeitself deliberately allows for a recognized-but-absent instance (e.g. creating a not-yet-existing VLAN row).- Return type:
- is_oid_implemented(oid)[source]¶
True unless
oidfalls under a MIB subtree this model’s real SNMP agent never registers at all (e.g. the RFC3621 PoE MIB on a non-PoE model) – seeprotocols.snmp.oids.is_oid_implemented. Used byStateMibView/faces/snmp.pyto answernoSuchObjectfor such a request instead of silently walking into an unrelated subtree.- Return type:
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.
- netgear_switch.virtual.seed.seed_gsm7252ps()[source]¶
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
untaggedis not a subset ofmember), all 48 PoE ports, the box sensors, the management IP (10.1.5.22), base MAC, serial and firmware – fromtests/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 thehttp_sensorsset). This is a strict transcription and is guarded as one – seetests/virtual/test_state_seed.py::test_seed_gsm7252ps_matches_capture_strictly, which runs it through the samecapture_parity.assert_seed_matches_capturehelper 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
sensorsandhttp_sensorsbelow): 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.
- Return type:
- netgear_switch.virtual.seed.seed_gsm7228ps()[source]¶
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 bytest_gsm7228ps_seed.pyvia the samecapture_parity.assert_seed_matches_capturehelper 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=Truewith 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.- Return type:
- netgear_switch.virtual.seed.seed_gs110emx()[source]¶
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_fw1028transcribes. Whether 1.0.1.4 advertised the v2 write auth was never measured – this seed carries the SKU’s measurednsdp_auth_version=0x10so the v2 write path is exercisable, but anything asserting real GS110EMX behaviour AT A GIVEN FIRMWARE must useseed_gs110emx_fw1028.- Return type:
- netgear_switch.virtual.seed.seed_gs110emx_fw1028()[source]¶
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_gs110emxtranscribes, 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 (seeprotocols/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=Trueon 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.
- Return type:
- netgear_switch.virtual.seed.seed_gs305ep()[source]¶
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.- Return type:
- netgear_switch.virtual.seed.seed_gs105pe()[source]¶
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).
- Return type:
- netgear_switch.virtual.seed.seed_m4300_24x()[source]¶
Build a realistic M4300-24X (24-port, non-PoE) virtual switch state.
- Return type:
- netgear_switch.virtual.seed.seed_m4300_16x()[source]¶
Build a realistic M4300-16X (16-port, all-16 PoE) virtual switch state.
- Return type:
- netgear_switch.virtual.seed.seed_gs728tpp()[source]¶
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
wcdface renders these values back through the sameparse_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-MIBentity_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 inhttp_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).- Return type:
Protocol faces¶
Protocol faces onto a VirtualSwitchState (e.g. an SNMP agent, Task 15).
A real pysnmp v2c command-responder agent serving a StateMibView.
This wires the pure StateMibView (Task 14) into an actual pysnmp v7 agent
bound to an ephemeral UDP port on 127.0.0.1, so both transport clients
(NetsnmpCliClient and PysnmpClient) can be exercised end-to-end
against a mock switch.
pysnmp is imported lazily (only when VirtualSnmpFace.start() runs, inside
the background thread), so this module — and the rest of the virtual
package — stays importable without the [testing]/[async] extra
installed. pysnmp ships no type stubs; every reference is resolved through
importlib.import_module (returning Any), the same single-seam
pattern used by transport/aio/snmp_pysnmp.py, so mypy –strict needs no
ignore_missing_imports override for pysnmp at all.
Adaptations from the Task 15 brief’s sample code (the brief explicitly warned its snippets might be stale — they were):
The brief’s
_StateInstrumsketch usedread_vars/read_next_vars. The actual installed pysnmp v7 MIB-instrumentation-controller callback names areread_variables/read_next_variables(confirmed by readingpysnmp.smi.instrum.AbstractMibInstrumControllerand howpysnmp.entity.rfc3413.cmdrsp.{Get,Next,Bulk}CommandResponderinvoke them:self.snmpContext.get_mib_instrum(contextName).read_variables.readVars/readNextVarsexist only as deprecated old-camelCase aliases for those, never asread_vars/read_next_vars).Rather than raising
NoSuchInstanceError/EndOfMibViewError(whichGetCommandResponder/NextCommandResponderwould turn into a whole-PDUgenErr— not spec-conformant SNMPv2c behaviour, and not whatPysnmpClient/net-snmp expect), this controller embeds the realpysnmp.proto.rfc1905exception values (noSuchInstance/endOfMibView) directly into the response var-bind, exactly as a real SNMPv2c agent does and exactly what both transport clients already treat as an absent-OID / walk-terminator marker.The custom controller is a plain class (no pysnmp base class to inherit from without a static pysnmp import, which would defeat the lazy-import seam) — it only needs to duck-type
read_variables/read_next_variables, which is allcmdrspever calls on it.The engine/transport/VACM setup follows
pysnmp.entity.config’sadd_transport/add_v1_system/add_vacm_user(the real, current function names — noaddTransport/addV1SystemcamelCase, those are deprecated aliases too).
Task 17 (write path) additions, verified the same way against the installed pysnmp v7 rather than trusted from a brief:
SetCommandResponder.handle_management_operation(read viainspect.getsource) callsself.snmpContext.get_mib_instrum(contextName).write_variables— so the controller callback is namedwrite_variables(matching theread_variables/read_next_variablesnaming above), notwrite_vars.That same source shows
CommandResponderBase.process_pducatches anypysnmp.smi.error.SmiErrorraised out ofhandle_management_operationand maps its exact class throughSMI_ERROR_MAPto an SNMP error-status (WrongValueError->wrongValue,NotWritableError->notWritable) — confirming both errors below travel cleanly to the client instead of the whole-PDUgenErr/timeout a bare exception would cause.That handler then does
errorIndex = errorIndication["idx"] + 1. Passingidx=None(as sketched in the brief) would make thisNone + 1-> an unhandledTypeErrorinside pysnmp’s own error path — a worse failure than the one being guarded against.write_variablesbelow passes the real 0-based position of the failing var-bind instead.add_vacm_useralready existed for reads; granting SET access is just passingwriteSubTree=(1, 3, 6, 1)alongside the existingreadSubTree— same function, no new API.
- class netgear_switch.virtual.faces.snmp.VirtualSnmpFace(view, *, community='public', host='127.0.0.1', port=0)[source]¶
Bases:
objectA pysnmp v2c command-responder agent serving a
StateMibView.Runs the pysnmp asyncio dispatcher on a dedicated background thread with its own event loop, bound to an ephemeral UDP port on
host.- stop()[source]¶
Close the dispatcher, join the background thread, and close the agent’s UDP socket deterministically.
pysnmp’s
AsyncioDispatcher.close_dispatchercloses the asyncio transport by scheduling its real close (loop.call_soon(...)) for the next loop iteration, then immediately callsloop.stop()in that same callback — which breaksrun_forever()before that next iteration ever runs._runalready compensates for this (it pumps the loop once more afterrun_forever()returns, so pysnmp’s own deferred close normally does run before the loop is closed). Closingself._sockhere too, after the thread has fully stopped, is a deliberate belt-and-braces backstop: it guarantees the fd is closed deterministically even if that compensation ever fails to run (e.g. a future pysnmp change altering the callback ordering), rather than depending on GC to eventually close it and emit a ResourceWarning.
Pure OID responder over VirtualSwitchState.oid_map().
No pysnmp, no network: a sorted (oid_tuple, snmp_type, value) list answering exact-match GET and lexicographic GETNEXT with bisect. Task 15 wires this into the real pysnmp command responder.
- class netgear_switch.virtual.faces.mibview.StateMibView(state)[source]¶
Bases:
objectSorted view of a switch’s OID map supporting GET and GETNEXT.
- apply_write(oid, value)[source]¶
Mutate the underlying state then rebuild so reads reflect the write.
- apply_write_uncommitted(oid, value)[source]¶
Mutate the underlying state WITHOUT rebuilding the sorted view.
For an atomic multi-varbind SET (
faces/snmp.py’swrite_variables): the (relatively expensive)rebuild()is deferred until the whole PDU has committed successfully, once, rather than once per varbind. Callers MUST callrebuild()themselves once every varbind in the PDU has applied without error.
- snapshot_state()[source]¶
Snapshot the underlying state, for atomic multi-varbind SET rollback.
See
VirtualSwitchState.snapshot/restore_state.- Return type:
- restore_state(snapshot)[source]¶
Restore the underlying state in place from a prior
snapshot_state()result, discarding any writes applied since.The sorted view itself needs no rebuild after a restore: if the caller took the snapshot before making any changes and only ever reaches this on a failed atomic SET, the state (and thus the view) is back to exactly what it was before that SET began.
- is_writable_oid(oid)[source]¶
Passthrough to
VirtualSwitchState.is_writable_oid(see there).- Return type:
- is_implemented(oid)[source]¶
False if
oidfalls under a subtree root this model’s SNMP agent has no registration for at all (e.g. the RFC3621 PoE MIB on a non-PoE model) – seeVirtualSwitchState.is_oid_implemented.faces/snmp.pychecks this BEFORE callingget/get_next: a real agent answersnoSuchObjectfor such a request rather than this view’s flat bisect silently finding whatever unrelated OID happens to sort next.- Return type:
A real UDP NSDP responder serving a VirtualSwitchState.
Mirrors the pysnmp face pattern (Task 15’s VirtualSnmpFace) but for the far
simpler NSDP wire protocol: a single background thread with one UDP socket bound
to an ephemeral port on loopback (so no root, no privileged 63321/63322 bind,
no SO_BINDTODEVICE). It answers READ_REQUEST from state.nsdp_tlvs and applies
WRITE_REQUEST after validating auth.
Two write-auth schemes are modelled, selected by state.nsdp_auth_version
(advertised to clients via the AUTH_V2_ENCPASS read):
v1 — validate the XOR
PASSWORD(0x000A) TLV; a mismatch returns error byte 7 (result 0x0700).v2 — validate the 8-byte
AUTH_V2_PASSWORD(0x001A) token againstauth_v2_password(password, mac, last-issued salt). This reproduces the GS110EMX (fw 1.0.2.8) behaviour LIVE-VERIFIED here: a WRITE that LEADS with the 0x001A token (then the config TLVs) and carries the right token applies the change (error 0); a wrong token returns error 13 and, after a few rapid failures, escalates to error 14 and then goes SILENT (no reply) for a cooldown; a READ naming write-only 0x001A returns error 3. A client that offers the v1 PASSWORD TLV to a v2 state has no 0x001A token at all, so it lands in the same error-13 branch blaming 0x000A/ATTR_PASSWORD – which is exactly how the real firmware refuses v1 auth (and whatcheck_resultkeys its “use the v2 scheme” guidance off).
stop() closes the socket deterministically so no ResourceWarning is emitted
under -W error::ResourceWarning.
- class netgear_switch.virtual.faces.nsdp.VirtualNsdpFace(state, *, host='127.0.0.1', port=0)[source]¶
Bases:
objectA UDP NSDP command responder serving a
VirtualSwitchState.
A real http.server web-UI face serving a VirtualSwitchState.
Binds a ThreadingHTTPServer to an ephemeral TCP port on 127.0.0.1 and
serves the login CGI + read/write CGI pages from device state via
virtual.web. Both httpx transport clients (sync + async) are exercised
end-to-end against it with no hardware.
A real switch never fabricates a 200 for a capability it doesn’t have, so
this face 404s any request whose path is not one of this model’s populated
HttpModelSpec fields, before ever calling into virtual.web — that
module’s render_page has a deliberately permissive catch-all (see its
docstring) that is only safe to reach for a path this spec actually
advertises.
Teardown is deterministic: stop() calls shutdown() (unblocks
serve_forever), joins the server thread, then server_close() closes the
listening socket — so nothing leaks under -W error::ResourceWarning.
- class netgear_switch.virtual.faces.http.VirtualHttpFace(
- state,
- spec,
- *,
- host='127.0.0.1',
- password='password',
- rand='1234',
- port=0,
Bases:
objectA
ThreadingHTTPServerweb-UI face serving aVirtualSwitchState.
In-process mock FASTPATH CLI face (implements CliSession).
Unlike the HTTP face (which binds a real ThreadingHTTPServer so httpx clients
hit a socket), the CLI face is an IN-PROCESS transport: it implements the same
CliSession seam CliReader/CliWriter depend on and dispatches each
command string straight to the cli_fastpath renderer (reads) or to a state
mutation (writes) – no SSH server, no socket, no host keys. This is deliberate
and honest: live SSH cannot be exercised from CI (no network) and the real byte
transports are documented as transport-only, so the mock proves the
command-dispatch + parser round trip (the part that CAN be tested) rather than
standing up a paramiko server whose value would be untestable here anyway.
A VirtualSwitch exposes one via cli_session(); the session setup commands
(enable / terminal length 0) are accepted as no-op success, matching a
real shell.
CONFIGURATION commands (the vlan database / configure trees that
cli_write.CliWriter drives) mutate VirtualSwitchState itself, so the
change is immediately visible through EVERY face of the same virtual switch –
this CLI face’s own show output, the SNMP oid_map() projection, the NSDP
TLVs and the web pages – exactly as a write on real hardware is visible over
every protocol.
Two behaviours are modelled on purpose because the library’s correctness depends on them:
An accepted configuration command returns EMPTY output; anything the switch would reject returns text. (The empty/non-empty CONTRACT is live-proven on an M4300-24X; the exact wording of the rejection strings below is NOT a transcription of any capture, and nothing in the library parses them.)
vlan participation/vlan tagging/vlan pvidare accepted but completely INERT while the port is inswitchport mode access– the live finding (seecli_write.CliWriter) that makesswitchport mode generala mandatory step of every per-port CLI VLAN write. A mock that silently applied them in access mode would hide exactly the bug that finding exists to prevent.
- class netgear_switch.virtual.faces.cli.VirtualCliFace(state, spec)[source]¶
Bases:
objectAn in-process CLI session serving a
VirtualSwitchState.- run_scp_copy(command, scp_password)[source]¶
In-process stand-in for the interactive
copy scp://...step.The real
ShellDriver.run_scp_copydrives a byte-level prompt handshake (TOFU/password/(y/n)) – exercised end-to-end by the byte-level fake-shell test. This in-process face has no byte stream, so it records the copy (source URL +nvram:destination) intoScpCertDeployand reports success, letting a facade-level test assert the deploy driver issued the right commands + destinations against a seededVirtualSwitch.- Return type:
- run_write_memory(command='write memory', *, prestuff)[source]¶
In-process stand-in for a command with a
(y/n)confirm.Two commands use this transport path:
write memory(save config) andreload(reboot). They are NOT interchangeable, so the mock keeps them apart – areloadmust never look like a config save. A real reload also tears the session down; the mock cannot restart itself, so it records the request (state.reboots) and returns, which is what lets a test prove the right command was issued.- Return type:
Render FASTPATH CLI show output from a VirtualSwitchState.
The CLI analogue of virtual/web_gsm7252ps.py: pure functions turning device
state into the exact fixed-width text shapes the protocols.cli.parse parsers
consume, so a VirtualSwitch answers the FASTPATH CLI like real hardware.
Only PHYSICAL ports (ifIndex <= the model’s port_count) are ever printed – the
CPU/LAG pseudo-interfaces in state (417/418 on the gsm7252ps) never appear on a
show port all / show vlan page, exactly as on the real switch.
Values are rendered so the parsers reconstruct the SAME model objects the SNMP face projects for the ops both serve (ports/pvids/vlans/poe/macs/mgmt-IP), which is what the cross-backend test asserts. Where the two hardware interfaces genuinely diverge (LLDP has no port-desc column in the CLI; sysInfo temperatures vs SNMP fan RPM), that is documented at the renderer and the test compares the shared projection only.
- netgear_switch.virtual.cli_fastpath.port_for_iface(state, iface)[source]¶
Inverse of
_iface: the physical port an ifName addresses, else None.The mock’s CLI face needs to resolve the interface name a COMMAND carries (“show interface ethernet 1/xg49”, “interface 1/g5”) back to a port number, and it must accept exactly the names this renderer prints – otherwise the mock would answer for names the real switch does not use (or, as it used to with a hardcoded
\d+/0/(\d+)regex, reject the1/g<n>/1/xg<n>names the Smart-firmware S3300-52X really prints). Resolving through_ifacekeeps the two directions in one place.- Return type:
int | None
- netgear_switch.virtual.cli_fastpath.render_hosts(state)[source]¶
show hosts, transcribed from real output captured 2026-08-02.From m4300-24x (10.1.5.13), m4300-16x (10.1.5.20) and gsm7252ps (10.1.5.22). All three label the name exactly “Host name”, and the resolver and static-mapping sections around it are reproduced because the reader has to pick one field out of them – a mock emitting only the wanted line would not exercise that at all.
The trailing static-mapping tables are the empty form all three returned; none had a host-to-address mapping configured.
- Return type:
- netgear_switch.virtual.cli_fastpath.render_port_description(state, iface)[source]¶
show port description <iface>.Layout transcribed from live output on a GSM7252PS (10.1.5.22, 2026-08-03):
Interface....... 1/0/8 ifIndex......... 8 Description..... MAC address..... E0:91:F5:0C:D6:DD Bit Offset Val.. 8
An unset description prints the label with NOTHING after it – which is why the parser maps an empty value to None rather than “”. A port the switch does not have answers with the same rejection any unknown argument gets.
- Return type:
- netgear_switch.virtual.cli_fastpath.render_logging(state)[source]¶
show logging, transcribed from the real output captured 2026-08-05.The scalar block the reader picks two fields out of. Reproduced with its neighbours because a mock emitting only “Syslog Logging” and “Logging Client Local Port” would not exercise
_colon_fieldsat all – and it is the reason the whole block is colon-separated rather than dotted-leader, unlikeshow hosts/show network.The surrounding counters and the console/buffered rows are the shape all four switches returned; the gsm7228ps additionally prints two Persistent Logging rows, which is why the reader takes named fields rather than offsets.
- Return type:
- netgear_switch.virtual.cli_fastpath.render_logging_hosts(state)[source]¶
show logging hosts, transcribed from real output captured 2026-08-05.Header and ruler are byte-for-byte what m4300-24x, gsm7252ps and gsm7228ps all printed. The INDEX column is 1-based and positional – it is what
no logging host <index>addresses, and the reason a removal has to look the row up rather than name the address.A switch with NO collectors prints the header and ruler and nothing else, which is what makes “empty” distinguishable from “could not ask”.
- Return type:
Web-UI renderers¶
One renderer per HTML dialect, producing each model’s real pages from state.
Pure web-UI projection of VirtualSwitchState (render + apply).
The exact inverse of the Task-3 parsers and Task-4 form encoders: render_page
turns device state into the documented HTML shape the parsers consume, and
apply_form mutates the state from a POSTed form body. No network here — the
VirtualHttpFace (Task 11) wraps these in an http.server handler.
The rendered HTML is deliberately minimal but carries every field the parsers
read, including a constant CSRF hash token on each writable page. Routing
(deciding whether a requested path is one a given model’s http_spec
actually advertises, and returning a 404-equivalent for anything else) is
Task 11’s job at the I/O boundary; this module renders/applies whatever page
its caller already resolved against spec.
The FASTPATH “XUI” write-form scaffolding shared by every managed model.
Every managed page (portsConfiguration.html,
poeInterfaceConfiguration.html, ipConfiguration.html,
mgmtVlanIpv4Configuration.html) is wrapped in the SAME structure on real
firmware, and the mock reproduces it exactly because each piece is load-bearing
for the writer:
TWO
<FORM>``s. The first, ``<page>.html/a0, is the applet/redirect form and carries no data; the SECOND,<page>.html/a1, is the read+write form. A parser that grabbed the first form would find nothing.Repeating rows are
<TR p="<unit>.<row0>.<count>0">and their fields are named<unit>.<row0>.<count>.v_1_2_<column>– the row index is 0-based and the count is the RENDERED row count, not the port count (a 52-port switch’s PoE page has 48 rows). Each row also carries its owngecb*checkbox, and the firmware applies ONLY the rows whose checkbox is submitted.A trailing “redirection elements” block –
submit_flag/submit_target/err_flag/err_msg/clazz_information– and axuiButtonsDivholding the page’s buttons as DISABLED hidden inputs.An apply is
submit_flag=8(the firmware’s ownxui_operation_submit = 8, from/scripts/_xeobj_jsvars.js); a refusal comes back as HTTP 200 witherr_flag=1and a humanerr_msg.
All of it live-captured 2026-07-30 from gsm7252ps 10.1.5.22, gsm7228ps 10.1.5.11, m4300-24x 10.1.5.13 and m4300-16x 10.1.5.20:49152.
- netgear_switch.virtual.web_fastpath_xui.instance(row0, count, unit=1)[source]¶
The row field-name prefix, WITHOUT its trailing dot.
- Return type:
- netgear_switch.virtual.web_fastpath_xui.row(inst, cells, *, checkbox='gecb5')[source]¶
Wrap
cellsin the real<TR p="...">row, with its own checkbox.checkboxdiffers per firmware on real hardware (gecb5on gsm7252ps’s ports page,gecb10on gsm7228ps’s,gecb_1_2on the M4300s), so the caller passes the one that model renders – the writer scrapes it rather than constructing it, and a mock that always used one spelling would hide a scrape that had hard-coded another.- Return type:
The page’s two
class=deftestmelist-navigation rows.- Return type:
- netgear_switch.virtual.web_fastpath_xui.has_list_unit(form)[source]¶
Whether this POST carries one of the page’s
urlListUnitaliases.- Return type:
- netgear_switch.virtual.web_fastpath_xui.page(path, body, *, buttons, err_msg='', title='NETGEAR')[source]¶
One complete XUI page: both forms, the body, the redirection block and the buttons.
err_msgnon-empty renders the refusal the way the firmware does – HTTP 200 witherr_flag=1.- Return type:
- netgear_switch.virtual.web_fastpath_xui.checked_rows(form, checkbox)[source]¶
The row prefixes whose
gecbcheckbox the submittedformcarries.This is the whole selection rule on real hardware: fields for an unchecked row are ignored even when present. Reproducing it is what makes the mock able to FAIL a writer that forgot the checkbox – which is exactly how a write silently does nothing on the real switch.
- netgear_switch.virtual.web_fastpath_xui.is_apply(form)[source]¶
Whether this POST is an APPLY (
submit_flag=8) rather than a reload.- Return type:
- netgear_switch.virtual.web_fastpath_xui.pressed(form, candidates)[source]¶
Which of
candidates(button field names) this POST carries.- Return type:
str | None
- netgear_switch.virtual.web_fastpath_xui.render_mgmt_ip(state, spec, *, err_msg='')[source]¶
The model’s management-IP page, rendered from
state.mgmt.- Return type:
- netgear_switch.virtual.web_fastpath_xui.render_service_xui(state, path, service)[source]¶
One service’s config page in the XUI labelled-scalar shape.
- Return type:
- netgear_switch.virtual.web_fastpath_xui.render_service_form(state, path, service)[source]¶
One service’s config page in the PLAIN NAMED FORM shape.
Reproduces the firmware’s double-checked radio group verbatim: BOTH radios carry a checked attribute, spelled
checked="checked"on the first and a bare uppercaseCHECKEDon the second, and a browser takes the LAST. A mock that marked only the true one would let a first-match parser pass here and then misreport every real switch.- Return type:
- netgear_switch.virtual.web_fastpath_xui.render_users(state, path)[source]¶
userManagement.html, rendered fromstate.users.- Return type:
- netgear_switch.virtual.web_fastpath_xui.render_syslog(state, path, *, err_msg='')[source]¶
syslogConfiguration.html, rendered fromstate.syslog.Counters (Messages Received/Relayed/Ignored) are rendered as the live pages do but are NOT part of
SyslogConfig; they exist so the page the mock serves has the same field set as the real one.- Return type:
- netgear_switch.virtual.web_fastpath_xui.apply_mgmt_ip(state, spec, form)[source]¶
Apply a management-IP form, returning the firmware’s
err_msg(”” = ok).Reproduces the real page’s validator rather than accepting anything: the firmware answers a malformed address with HTTP 200 +
err_flag=1+ “Error: Unable to set ‘<name>’ with ‘<value>’. IP address should be in x.x.x.x form …” (the page publishes that exact string asxeValData.xv_1_1_1_635).- Return type:
- netgear_switch.virtual.web_fastpath_xui.apply_port_admin(state, form, *, checkbox, ports, count, admin_column='v_1_2_6')[source]¶
Apply
portsConfiguration.html’s Admin Mode column, honouring the per-row checkboxes. Returns the firmwareerr_msg(”” = accepted).- Return type:
- netgear_switch.virtual.web_fastpath_xui.apply_syslog_rows(state, form)[source]¶
Apply a syslog-page row ADD or DELETE, returning
err_msg(”” = ok).Reproduces what the live M4300 page does, both halves:
ADD – the
v_g_2_1_*template row with its write-only row-status (v_g_2_1_5) set to “Active”. A new row takes the next FREE index and leaves existing rows where they are, which is what makes the table sparse.DELETE – a data row whose own
v_2_1_5is set to “Delete”.
A template row submitted WITHOUT the row-status is ignored, exactly as the firmware ignores a blank global row that was never activated – that is the case a writer which forgot to set it would otherwise appear to pass.
- Return type:
The managed FASTPATH “VLAN Membership” page, rendered from + applied to state.
Reproduces switching/dot1q/vlan_port_cfg.html (GET) and its
..._rw.html form target (POST, used for BOTH a VLAN-select re-render and an
apply) as the real firmware serves them. Grounded in live captures taken
2026-07-30 from all four managed switches – gsm7252ps 10.1.5.22, gsm7228ps
(S3300-52X) 10.1.5.11, m4300-24x 10.1.5.13 and m4300-16x 10.1.5.20:49152 – which
are checked in as tests/fixtures/http/*vlan[Pp]ort[Cc]fg*.html.
The behaviours below are reproduced BECAUSE hardware does them, and each one is something a lenient mock would have hidden:
Two different views of the same VLAN.
hiddenTagged/hiddenUnTaggedare the CURRENT (operational) egress lists;hiddenMemand the port grid are the CONFIGURED participation. They genuinely differ – seestate.VlanSim.configured_only, seeded from the real GSM7252PS.``submt`` is the apply flag.
submt=0(what the VLAN<select>’sscreen_refresh()posts) re-renders WITHOUT applying; onlysubmt=16(0x10, whatsubmitform()sets) writes. A mock that applied on every POST would let a broken reader silently corrupt VLANs and still pass.The page shows whichever VLAN ``vlanId`` selected, and an unknown VLAN falls back to the lowest – so a reader that forgets to check which VLAN came back gets caught here instead of on hardware.
Two grid encodings, two index bases. Older firmware (gsm7252ps) emits
toggleImageFirst(this,<0-based slot>,...)+grey_[btu].gif; newer (S3300/M4300) emitstogImg(this,<1-based slot>,...)+switch_*.png. Both are rendered, per model, from the seededVlanMembershipPageSim.grid.LAG pseudo-interfaces occupy hiddenMem slots after the physical ports (64 on the gsm7252ps, 26 on the S3300, 128 on the M4300s) and are rendered in their own grid table. A writer that assumed
slot == port - 1and truncated the string would drop them; here it cannot.
- netgear_switch.virtual.web_fastpath_vlan.refusal(state, form)[source]¶
The
err_msgthis apply would be refused with, orNoneif allowed.Reproduces the M4300 firmware’s precondition: a port whose
switchport modeis access or trunk cannot be given explicit VLAN membership, and the web UI reports that aserr_flag=1+err_msgon an otherwise-200 page rather than an HTTP error. Quoted verbatim from 10.1.5.13 – seeVirtualSwitchState.vlan_membership_locked_ports.- Return type:
str | None
- netgear_switch.virtual.web_fastpath_vlan.render_membership(state, spec, form, *, err_msg='')[source]¶
Render the VLAN Membership page for the VLAN
formselected.- Return type:
- netgear_switch.virtual.web_fastpath_vlan.apply_membership(state, form)[source]¶
Apply a membership POST – but ONLY when
submtis the apply flag.submt=0is the VLAN<select>’s own re-render POST and must not mutate anything: the reader relies on that to page through VLANs, and a mock that wrote on every POST would let a reader silently rewrite every VLAN it read. Confirmed on hardware, by reading a VLAN twice and diffing the two responses (byte-identical) on all four switches.A port set Excluded loses its CURRENT membership too; a port set tagged/untagged becomes a current member.
configured_onlyis cleared for a port the caller explicitly set, because the caller has now stated that port’s participation outright.
M4300 “Cheetah /v1” page renderers driven by VirtualSwitchState.
Reproduces the real page encoding rather than a convenient one: every value is
a hidden input whose NAME carries the ROW INSTANCE, followed by an HTML
comment naming the field –
- <TD id=1_2_10><INPUT xid=1_2_10 TYPE=hidden NAME=1.0.24.v_1_2_10
VALUE=”Link Up”>Link Up</TD><!– baseport_LinkStatus2 –>
which is exactly what parse.parse_cheetah_rows reads back, so the mock
exercises the same field-name-addressed parsing path real hardware does.
Interface names are HTML-escaped (1/0/1) because the real
firmware escapes them – that escaping once collapsed every parsed port number
to 1, so reproducing it keeps the regression visible in CI.
- netgear_switch.virtual.web_m4300.render_ports(state, *, err_msg='')[source]¶
/v1/portsConfiguration.html– per-port admin/link/speed.COLUMN COORDINATES CORRECTED against the real page (10.1.5.13, 2026-07-30): Admin Mode is
v_1_2_6and ifIndex isv_1_2_13(adisplay:nonecolumn), and the row prefix is1.<0-based row>.<row count>. The mock used to emitv_1_2_3/v_1_2_2and1.<port>.24, which the comment-keyed read parsers tolerated but which does not exist on any switch – so a writer addressing the real Admin Mode column would have found nothing here while working on hardware.- Return type:
- netgear_switch.virtual.web_m4300.apply_ports(state, form)[source]¶
Apply a /v1/portsConfiguration POST; returns the firmware
err_msg.- Return type:
- netgear_switch.virtual.web_m4300.render_poe(state, *, err_msg='')[source]¶
/v1/poeInterfaceConfiguration.html(M4300-16X only; the 24X has no PoE and its spec leaves the path None).The cell grid is byte-identical to the gsm7252ps XE page, so that renderer is reused – but with THREE M4300-specific differences, each live-measured: the power column is decimal watts, the row checkbox is
gecb_1_2, and the reset button readsPower Cycle Port(s)rather thanRESET.- Return type:
- netgear_switch.virtual.web_m4300.render_port_statistics(state)[source]¶
/v1/portStatistics.html– FRAME counters (this UI has no octets).The virtual state stores octet counters, so the frame columns are seeded from the packet counters when present and 0 otherwise – never from the octet values, which would imply this page reports bytes when it does not.
- Return type:
- netgear_switch.virtual.web_m4300.render_pvids(state)[source]¶
/v1/portPvidConfiguration.html– per-port PVID.- Return type:
- netgear_switch.virtual.web_m4300.render_vlans(state)[source]¶
/v1/vlanStatus.html– VLANs with their egress port list.The egress list is rendered in the real firmware’s format, including the
lag Nentries that must NOT be expanded into physical ports.- Return type:
- netgear_switch.virtual.web_m4300.render_mac_table(state)[source]¶
/v1/basicAddressTable.html– the learned MAC/FDB table.- Return type:
- netgear_switch.virtual.web_m4300.render_sysinfo(state)[source]¶
/v1/base/system/management/sysInfo.html– mgmt IP, base MAC and the temperature block. Plain labelled cells (this page has no xid cells).- Return type:
GSM7252PS “XE FASTPATH” page renderers driven by VirtualSwitchState.
Reproduces the real page encoding rather than a convenient one. Every data
cell is a hidden input whose NAME carries the ROW INSTANCE and whose
id/xid carry the COLUMN COORDINATE, with NO field-name comment –
- <TD class=”def alt0” p=”1.0.520” id=1_2_10><INPUT xid=1_2_10 TYPE=hidden
NAME=1.0.52.v_1_2_10 VALUE=”Link Up”>Link Up</TD>
which is exactly what parse.parse_xe_rows reads back, so the mock
exercises the same column-coordinate-addressed parsing path real hardware
does. Two details of the real encoding are deliberately reproduced because
getting them wrong is what a parser regression would look like:
the instance prefix is
1.<row-index>.<row-count>(NOT unit/slot/port), so a parser that tried to read a port number out of it would produce nonsense here too, exactly as it does on hardware;each page’s own HEADER row is emitted with the same coordinates, so the fixture and the mock document the column map identically.
sysInfo.html is NOT an XE page (see render_sysinfo): it uses plain
bold-label/value cells and three status tables, which is what
parse_xe_labelled_values/parse_xe_sensors/parse_xe_mgmt_ip read.
- netgear_switch.virtual.web_gsm7252ps.render_ports(
- state,
- *,
- err_msg='',
- iface=_iface,
- checkbox=_PORTS_CHECKBOX,
- path='/portsConfiguration.html',
/portsConfiguration.html– per-port admin/link/speed + ifindex.This is the WRITE page as well as the read page (
set_port_enabled), so it is rendered with the real XUI scaffolding: two forms,<TR p=...>rows each carrying their owngecbcheckbox, the redirection block and the CANCEL/APPLY buttons – seeweb_fastpath_xui.- Return type:
- netgear_switch.virtual.web_gsm7252ps.apply_ports(state, form, *, checkbox=_PORTS_CHECKBOX)[source]¶
Apply a portsConfiguration POST; returns the firmware
err_msg.- Return type:
- netgear_switch.virtual.web_gsm7252ps.render_port_statistics(state)[source]¶
/portStatistics.html– PACKET counters (this page has no octets).The virtual state stores octet counters too, but they are deliberately NOT rendered here: the real page has no octet column, and emitting one would let a regression that reads bytes off this page pass CI.
- Return type:
- netgear_switch.virtual.web_gsm7252ps.render_pvids(state)[source]¶
/portPvidConfiguration.html– Configured + Current PVID columns.- Return type:
- netgear_switch.virtual.web_gsm7252ps.render_vlans(state)[source]¶
/vlanStatus.html– VLANs with their egress port list.The egress list is rendered in the real firmware’s format, including the
lag Nentries that must NOT be expanded into physical ports.- Return type:
- netgear_switch.virtual.web_gsm7252ps.render_mac_table(state)[source]¶
/basicAddressTable.html– the learned MAC/FDB table.The “Total MAC Addresses” scalar the real page carries is rendered too, with the true row count, so the reader’s anti-truncation guard is exercised against a page that is legitimately complete.
- Return type:
- netgear_switch.virtual.web_gsm7252ps.render_poe(
- state,
- *,
- watts=False,
- err_msg='',
- iface=_iface,
- checkbox=_POE_CHECKBOX,
- reset_label='RESET',
- path='/poeInterfaceConfiguration.html',
/poeInterfaceConfiguration.html– per-port PoE admin/status/power.wattsselects the “Output Power” cell format to MATCH the emulated firmware: the gsm7252ps renders integer milliwatts (watts=False, e.g. “3500”); the M4300-16X renders watts with two decimals (watts=True, e.g. “4.60”). Both decode back to the same milliwatts viaparse._poe_power_to_mw– see the parity note there.Also the WRITE page (
set_poe/cycle_poe/clear_poe_fault), so it carries the real scaffolding INCLUDING the hidden write-only “Port Reset” columnv_1_2_20(xp_1_2_20 = "write-only", enum["None","Reset"]) that every row renders asReseton real hardware, and the extra RESET buttonv_2_1_3its APPLY-sibling page does not have.- Return type:
- netgear_switch.virtual.web_gsm7252ps.apply_poe(state, form, *, checkbox=_POE_CHECKBOX, unit_required=True)[source]¶
Apply a poeInterfaceConfiguration POST; returns the firmware
err_msg.Two distinct operations share the page, exactly as on hardware: APPLY (
v_2_1_2) writes the Admin Mode column, RESET (v_2_1_3) consumes the write-onlyv_1_2_20column and re-runs detection – which on a port with no PD attached lands back inSearchingand on one that had faulted clears the fault. Only CHECKED rows are touched.unit_requiredreproduces a MEASURED per-firmware difference, not a guess. The GSM7252PS PoE rows carry no hidden Unit key column, so its firmware takes the list scope from the page’surlListUnitfield and refuses the whole row without it (defaultTrue– this is that model’s renderer). The gsm7228ps and both M4300 PoE pages DO render a per-rowv_1_2_21“Unit” key and accepted the same body with no page-level unit at all (live 2026-07-30 on 10.1.5.11 and 10.1.5.20:49152), so those renderers passFalse. Encoding the counter-example matters as much as the rule: without it the mock could not tell a writer that over-corrected.- Return type:
- netgear_switch.virtual.web_gsm7252ps.render_lldp(state)[source]¶
/lldpRemoteInventory.html– LLDP neighbours.This page has NO remote-port-DESCRIPTION column, so
LldpSim.port_descis deliberately not rendered: the mock must not expose data the real page does not have.- Return type:
- netgear_switch.virtual.web_gsm7252ps.render_sysinfo(state)[source]¶
/base/system/management/sysInfo.html– mgmt IP, base MAC and the three status tables.Renders the model’s HTTP sysInfo sensor set (
state.sysinfo_sensors), which on this device is DIFFERENT from its SNMP set: the web UI exposes a Temperature Status table (numeric degC, “N/A” for an unpopulated slot which the parser skips), a FAN Status table reporting fan HEALTH as text (“OK”/”NA”, never RPM), and a Device Status table with the RPS + Power Module operational flags. Each cell is the sensor’s literal captured page text, so the parser reads back exactly the real hardware’s HTTP sensors.- Return type:
S3300-52X-PoE+ (gsm7228ps) “XE FASTPATH” page renderers from state.
The S3300 Smart-Managed-Pro web UI shares the sibling gsm7252ps Cheetah XE
cell grid for ports/stats/PVIDs/VLANs/PoE/LLDP, so those pages are rendered by
the exact gsm7252ps renderers (see web_gsm7252ps) – the reader keys off
the ifindex/port columns, which are identical. Only three pages differ and are
rendered here, matching the real captures in
tests/fixtures/http/gsm7228ps_*.html and the S3300-specific parsers:
basicAddressTable.html– the MAC/FDB columns are SHIFTED (VLAN in v_1_2_2, not v_1_2_1) and the port ifName is HTML-entity-escaped in the Smart firmware’s1/gN/1/xgNform (/=/); the switch’s own base MAC is learned on the CPU interface, renderedc1/ status “Management”, whichparse_s3300_macsskips as non-physical (SNMP reports that same base MAC on the CPU ifIndex).sysInfo.html– exposes only theBase MAC Address(no IPv4 mgmt address on the statically-reachable page), which is allparse_s3300_mgmtreads.
Sensors are NOT served as a live table: the S3300 sysInfo has no fan/temp
readings, so get_sensors over HTTP is unsupported (SNMP only) – see
HtmlDialect.S3300 and http_read._supports_sensors.
- netgear_switch.virtual.web_gsm7228ps.render_ports(state, *, err_msg='')[source]¶
/portsConfiguration.htmlin the Smart firmware’s spelling.Same XE grid as gsm7252ps, but the Port cell is
1/g12/1/xg49, not1/0/12– live-confirmed on 10.1.5.11. It used to be aliased straight to the gsm7252ps renderer, which made the mock print1/0/Nhere and hid the fact that a writer locating its row by ifName has to handle BOTH spellings.- Return type:
- netgear_switch.virtual.web_gsm7228ps.render_poe(state, *, err_msg='')[source]¶
/poeInterfaceConfiguration.htmlin the Smart firmware’s spelling.- Return type:
- netgear_switch.virtual.web_gsm7228ps.render_vlans(state)[source]¶
/vlanStatus.html– VLANs with their egress port list (S3300 ifNames).The egress cell uses the Smart firmware’s
1/gN/1/xgNnames (whichparse_s3300_vlansreads, unlike the1/0/N-only XE expander), with LAG ifIndexes renderedlag N– not expanded into physical ports.- Return type:
- netgear_switch.virtual.web_gsm7228ps.render_mac_table(state)[source]¶
/basicAddressTable.html– the learned MAC/FDB table (S3300 columns).VLAN in v_1_2_2, MAC in v_1_2_3, escaped port ifName in v_1_2_4, status in v_1_2_5 – the shifted layout
parse_s3300_macsreads. The “Total MAC Addresses” scalar (v_1_1_1) carries the true row count so the reader’s anti-truncation guard sees a legitimately complete page.- Return type:
- netgear_switch.virtual.web_gsm7228ps.render_sysinfo(state)[source]¶
/base/system/management/sysInfo.html– Base MAC Address only.The S3300 Smart UI’s statically-reachable sysInfo exposes the switch’s base MAC (labelled cell,
aid="1_16_1_right") but NOT the IPv4 management address (that page is behind a JS-only menu), and carries no live fan/temp sensor table.parse_s3300_mgmtreads back the base MAC;get_sensorsis unsupported over HTTP for this model.- Return type:
Byte-faithful GS110EMX web-UI page templates (Gambit token session).
The literal HTML in web_gs110emx_templates.py is the REAL captured
content from a physical GS110EMX (tests/fixtures/http/gs110emx_*.html)
with only the dynamic values swapped for marker placeholders – everything
else (JS boilerplate, attribute names, and critically the malformed
never-closed <tr class="portID"> rows on interface_stats.html – see
protocols/http/parse.py’s _OPEN_ROW_RE) is copied byte-for-byte from
the capture, so the mock is byte-equivalent to real hardware whenever seeded
with the same values. Substitution is plain str.replace (not
.format()/f-strings) because the captured pages’ inline JavaScript is
full of literal {/} characters that would need escaping.
One known, deliberate byte-level deviation: the real capture’s LAST
portID row (port 10) is followed by 2 fewer whitespace characters before
</table> than every other row – a capture idiosyncrasy of the real
device, not a parsing-relevant difference. render_interface_stats renders
every row (including the last) with the same trailing whitespace for
simplicity, so a byte-diff against the original 10-port capture differs by
exactly those 2 bytes.
- netgear_switch.virtual.web_gs110emx.render_login(rand)[source]¶
GET / login page – byte-identical to gs110emx_login.html but for the
randnonce.- Return type:
- netgear_switch.virtual.web_gs110emx.render_redirect(token)[source]¶
POST /redirect.html login response – byte-identical to gs110emx_redirect.html but for the Gambit token.
token=""(a rejected login) renders a Gambit field with an empty value, whichparse.parse_gambit_tokenreads back as falsy.- Return type:
- netgear_switch.virtual.web_gs110emx.render_sysinfo(state, token)[source]¶
GET /iss/specific/sysInfo.html?Gambit=<token> – byte-identical to gs110emx_sysinfo.html but for the state-driven device identity / mgmt-IP fields.
dhcp_selectmirrors the captured page’sdata-select-valueattribute (0=static/Disable, 1=DHCP/Enable) – seeprotocols/http/parse.py’sparse_sysinfo/HttpSysInfofor the read-side grounding of this convention.- Return type:
- netgear_switch.virtual.web_gs110emx.render_port_settings(state, token)[source]¶
GET port_settings.html – port link/speed/description from state, so the HTTP port-status read matches the NSDP PORT_STATUS read on this switch.
- Return type:
- netgear_switch.virtual.web_gs110emx.apply_port_settings(state, form)[source]¶
Apply a
port_settings.htmlPOST; returns the page’s reply BODY.The real page answers an AJAX apply with a bare
SUCCESS(its own JS checksresText != "SUCCESS"), not a re-rendered page.PORT_NOis the SEMICOLON-TERMINATED selected-port list itssaveSelectedPorts()builds ("3;"), and a body that sends a bare number selects nothing and applies nothing – reproduced here, because that is exactly the mistake that a lenient mock would have hidden (it was caught on real hardware instead).- Return type:
- netgear_switch.virtual.web_gs110emx.render_pvid(state, token)[source]¶
GET vlan_pvidsetting.html – per-port PVID from state.
- Return type:
- netgear_switch.virtual.web_gs110emx.render_cf8021q(state, token)[source]¶
GET Cf8021q.html – the VLAN list (with member ports) from state. The reader only scrapes the VID column (
parse_gs110emx_vlan_ids); the member list is rendered for fidelity.- Return type:
- netgear_switch.virtual.web_gs110emx.render_vlan_membership(state, token, selected_vid)[source]¶
POST vlanMembership.html (VLAN_ID=<selected_vid>) – the per-port
hiddenMemwire codes (1=untagged, 2=tagged, 3=excluded) for the selected VLAN, plus the full VLAN <option> list. The wire codes are the SAME scheme gs305ep’s 8021qMembe.cgi uses, soparse.parse_membershipreads it back and the resulting VLANInfo matches the NSDP VLAN_MEMBERS read.- Return type:
- netgear_switch.virtual.web_gs110emx.render_interface_stats(state, token)[source]¶
GET /iss/specific/interface_stats.html?Gambit=<token> – byte-identical to gs110emx_interface_stats.html but for the per-port counters. The real device NEVER closes a
<tr class="portID">with</tr>(rows run on until the next<tr>or the table close); this reproduces that exact malformed-but-real shape row-for-row, which is whyparse.parse_interface_stats(not gs305ep’sparse_port_stats) is needed to read it back. Missing counters (None) render as0, matching the real device’s own zeroed idle-port rows.- Return type:
Raw byte-faithful GS110EMX web-UI HTML fragments (data only, no logic).
Split out of web_gs110emx.py so the long single-line string literals
captured verbatim from tests/fixtures/http/gs110emx_*.html (see that
module’s docstring) don’t drag E501/line-length linting onto the actual
render logic; this module is pure data, excluded from ruff in
pyproject.toml exactly like _version.py.
GS105PE web-UI page renderers driven by VirtualSwitchState.
Structurally faithful to the REAL captured pages (see
tests/fixtures/http/gs105pe_*.html) – same row markers, same column
order, and critically the same two quirks real firmware has, so the mock
exercises exactly the code paths real hardware does:
portStatistics.cgileaves the first counter’s<td>EMPTY and carries every counter as a hidden(hi, lo)32-bit pair (seeprotocols/http/parse.py’sparse_gs105pe_stats), and writes its rows as<tr class="portID" name="portID">– an extra attribute the other pages lack.8021qMembe.cgicarries a per-page CSRFhashand marks the currently selected VLAN with<option ... selected>; the reader reuses that page for the selected VLAN rather than re-POSTing it (which makes real hardware drop the connection).
Byte-for-byte fidelity to the capture is deliberately NOT attempted here (that
is what the fixture-driven parser tests in tests/test_http_read.py prove);
this face’s job is to serve the SAME STATE the NSDP face serves, so the
HTTP<->NSDP cross-verification is meaningful.
- netgear_switch.virtual.web_gs105pe.render_status(state)[source]¶
GET /status.cgi – per-port link + speed, the columns
parse_gs105pe_port_statusreads ([1]=port, [2]=link, [4]=speed).- Return type:
- netgear_switch.virtual.web_gs105pe.render_port_statistics(state)[source]¶
GET /portStatistics.cgi – reproduces the real page’s quirks: rows are
<tr class="portID" name="portID">, the Bytes-Received cell is rendered EMPTY, and each counter is a hidden(hi, lo)32-bit pair.- Return type:
- netgear_switch.virtual.web_gs105pe.render_pvid(state)[source]¶
GET /portPVID.cgi – [1]=port, [2]=PVID.
- Return type:
- netgear_switch.virtual.web_gs105pe.render_vlan_config(state)[source]¶
GET /8021qCf.cgi – the VLAN list as
vlanckNcheckboxes (the shape gs305ep’sparse_vlan_idsreads, which gs105pe shares).- Return type:
- netgear_switch.virtual.web_gs105pe.render_vlan_membership(state, selected_vid)[source]¶
GET/POST /8021qMembe.cgi – the per-port
hiddenMemwire codes (1=untagged, 2=tagged, 3=excluded) forselected_vid, plus the CSRFhashand a<option ... selected>marking which VLAN is shown.- Return type:
- netgear_switch.virtual.web_gs105pe.render_switch_info(state)[source]¶
GET /switch_info.cgi – device identity + mgmt-IP, in the labelled-cell and lowercase-input shape
parse_gs105pe_sysinforeads.- Return type:
GS728TPP GoAhead wcd XML-API renderers for the virtual HTTP face.
Each function renders one wcd response from a VirtualSwitchState in the
SAME shape the real switch 10.2.5.10 returns (a trailing
<DeviceConfiguration> data block of <Object type="section"> elements),
so the SAME parse.parse_goahead_* parsers that read the real captures read
the mock back – proving seed<->render<->parse round-trips with no hardware.
Only the data block matters to the parsers (they slice it out and ignore the
surrounding template), so this emits a minimal-but-faithful
<ResponseData><DeviceConfiguration>.. envelope.
- netgear_switch.virtual.web_gs728tpp.render_device_info_and_sensors(state)[source]¶
DeviceBasicInfo (cosmetic identity) + DiagnosticsUnitList (the sensors the library actually reads back via
parse_goahead_sensors).The DiagnosticsUnitList fields come from
state.sysinfo_sensors(eachSensorSimcarries the XML tag ininstanceand the wire code inraw), so the seed is the single source of truth for both faces.- Return type:
- netgear_switch.virtual.web_gs728tpp.apply_cert_import(state, xml_body)[source]¶
Accept an
SSLCryptoCertificateImportListXML upload, validate it, and record the received certificate onstate.uploaded_cert.Returns the wcd status response. Mirrors real firmware: a well-formed body carrying a non-empty
<certificate>and<privateKey>yields<statusCode>0</statusCode>and records the cert; a malformed or empty upload yields a NON-zero statusCode (so a transport/writer regression that dropped the body would be caught here rather than passing silently). DTD/ entity declarations are rejected outright (XXE hardening, matchingparse._goahead_data_block).- Return type:
- netgear_switch.virtual.web_gs728tpp.unauthenticated_response()[source]¶
What the switch answers a
wcdrequest with no valid session.CAPTURED from the live GS728TPP (10.2.5.10, firmware 6.0.1.30) by issuing a request with a stale sessionID cookie. Note what it is NOT: not a 302, not a 401, and not an empty body – it is HTTP 200 carrying a normal
<ResponseData>envelope whose ActionStatus says statusCode 4. That detail is the whole point of reproducing it: a mock that redirected instead would let the client’s session-expiry handling look correct while missing the case real hardware actually produces.- Return type:
- netgear_switch.virtual.web_gs728tpp.apply_write(state, xml_body)[source]¶
Apply one
POST wcdwrite body and return the wcd status response.The real UI writes EVERYTHING through this one endpoint, with the object name and the
actionattribute selecting the operation – so the mock must dispatch the same way rather than recognising one special upload. Each branch mirrors what the switch was observed to do on 10.2.5.10 (firmware 6.0.1.30) when the library drove that exact body.An unrecognised object is a NON-zero statusCode, never a silent success: a writer that posts a body this firmware has no handler for must fail loudly here too.
- Return type: