Backend operations¶
The model-driven readers and writers: one pair per backend, each mapping a facade call to that protocol’s mechanism for that model. Every one can be constructed directly and pointed at a client of your choosing — including one aimed at the virtual switch.
SNMP¶
Model-driven SNMP read operations over a sync or async client.
- netgear_switch.snmp_read.read_system_info(client)[source]¶
Identify a switch’s model via SNMP: sysObjectID first, then sysDescr.
Deliberately NOT a method on
SnmpReader: every other read in this module requires a already-knownSwitchModel(_require_snmpgates the reader’s construction), but model identification exists precisely for the case where the caller does NOT yet know/trust the model – so this takes a bare, unboundSnmpClientinstead. A real-capture-confirmed sysObjectID (parse.detect_model_from_sysobjectid) wins when present; otherwise the sysDescr text heuristic (parse.detect_model_from_sysdescr) is tried. Both are honesty-constrained (never guess;key=Nonemeans genuinely unidentified – an unregistered model or a non-Netgear device).- Return type:
- async netgear_switch.snmp_read.async_read_system_info(client)[source]¶
Async twin of
read_system_info– see there.- Return type:
- class netgear_switch.snmp_read.SnmpReader(client, model)[source]¶
Bases:
object- get_hostname()[source]¶
The switch’s host name, from the standard MIB-II
sysNamescalar.Standard, so this works on every SNMP model – including
gs728tpp, which publishes no Netgear vendor subtree at all.- Return type:
- get_syslog()[source]¶
Remote-logging configuration: whether it is on, and where it sends.
VENDOR columns, so a model with no Netgear subtree cannot serve this.
gs728tppis exactly that model – a walk of1.3.6.1.4.1.4526answersnoSuchObject– and it is refused by name rather than returned empty, which would read as “no collectors configured”.- Return type:
- get_system_info()[source]¶
Identify this switch’s model via sysDescr (see
read_system_info).Reuses this reader’s already-connected client. Unlike every other method here, the result does NOT depend on
self.modelmatching the real device – useful to confirm/discover a switch’s real model via a reader that was (possibly wrongly) constructed against a different model key.- Return type:
- get_users()[source]¶
This backend does not serve local user accounts.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- Return type:
- class netgear_switch.snmp_read.AsyncSnmpReader(client, model)[source]¶
Bases:
object- async get_users()[source]¶
This backend does not serve local user accounts.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- Return type:
Model-driven SNMP write/control over a write-capable sync or async client.
Parallel to snmp_read.py. Every write performs the SET then re-reads and
verifies (WriteVerificationError with before/after on mismatch — a real
commitFailed surfaces as an SnmpError from the transport first).
Disruptive writes to a protected_ports port are refused unless force=True
(design spec §6).
- netgear_switch.snmp_write.decode_vlan_bitmap(bitmap)[source]¶
Inverse of
_vlan_bitmap: which VLAN ids a switchport bitmap names.
- class netgear_switch.snmp_write.PoeCycleTimeouts(off_timeout=30.0, on_timeout=60.0, poll_interval=2.0)[source]¶
Bases:
objectInjectable PoE-cycle deadlines (seconds). Defaults match design spec §6; tests pass tiny values so cycles run fast against the coherent mock.
- class netgear_switch.snmp_write.SnmpWriter(client, model, *, protected_ports=frozenset())[source]¶
Bases:
objectSynchronous SNMP write facade over one switch.
- cycle_poe(
- port,
- *,
- force=False,
- timeouts=_DEFAULT_POE_TIMEOUTS,
- sleep=time.sleep,
- clock=time.monotonic,
- clear_poe_fault(
- port,
- *,
- force=False,
- timeouts=_DEFAULT_POE_TIMEOUTS,
- sleep=time.sleep,
- clock=time.monotonic,
- set_port_description(port, description, *, force=False)[source]¶
Set a port’s
ifAlias, the standard per-port description column.WRITABILITY MEASURED 2026-08-03 on a GS728TPP (10.2.5.10, firmware 6.0.1.30): a SET of ifAlias.17 was accepted and read straight back through
get_ports.Clearing it (
description="") is the case that needed transport work rather than a new OID:snmpset ... s ""is refused by the net-snmp CLI itself, so the transport sends an empty OCTET STRING as an empty hex string instead (see_set_argv). Without that, a description could be set and never removed.
- set_port_speed(port, speed, *, force=False)[source]¶
This backend cannot configure a port’s speed.
Refused by name rather than approximated. What SNMP offers here is
ifSpeed/ifHighSpeed, and those report the rate the link NEGOTIATED – writing one would be writing a counter, not a setting. The column that would genuinely serve this is MAU-MIB’sifMauDefaultType/ifMauAutoNegAdminStatus(mib-2.26); no switch here has been walked for it, so its presence is UNKNOWN rather than absent, and the 2026-08-03 OID sweep does not settle it (that sweep covered the 4526 VENDOR subtree only). Use a CLI backend, or establish the MAU subtree first.
- set_flow_control(port, enabled, *, force=False)[source]¶
This backend cannot configure flow control.
Refused by name. EtherLike-MIB’s
dot3PauseAdminModeis the column that would serve this, and it is READ on the one model that publishes it (the GS728TPP) – but no SET has ever been issued against it here, so whether the agent accepts one is unknown. This library does not offer a write it has never seen succeed.
- add_syslog_collector(host, *, port=514, severity=6, force=False)[source]¶
This agent will not CREATE a syslog host row. MEASURED, not assumed.
Probed on m4300-24x 10.1.5.13 (FASTPATH 12.0.13.8, 2026-08-05) with the Read/Write community, against a free index. Five mechanisms, five refusals, with the agent’s own SMI error-status:
createAndGo(4) + every column, one PDU -> inconsistentValue createAndWait(5) alone -> inconsistentValue createAndGo(4) alone -> inconsistentValue the value columns alone (auto-create?) -> commitFailed active(1) at a row that does not exist -> commitFailed
The same agent ACCEPTS a SET of every column of an EXISTING row, and accepts
destroy– seeremove_syslog_collector– so this is the agent declining row creation specifically, not a permissions problem. (The first run of that probe used the READ community and “refused” everything, which is CLAUDE.md principle 4’s own example. Ask the switch withshow snmpcommunity.)Same shape as the GS728TPP’s refusal to create a VLAN row. Add over a CLI backend, where the command is the device’s own running-config line.
- remove_syslog_collector(host, *, force=False)[source]¶
Remove a collector by writing RowStatus
destroy(6)to its row.LIVE-VERIFIED on m4300-24x 10.1.5.13 (2026-08-05): a throwaway collector added over the CLI was destroyed with a single SET of
<base>.14.1.4.5.1.7.<index> = 6, and the switch’s ownshow logging hostsconfirmed the row was gone.Note the asymmetry, which is the agent’s and not this library’s: it DESTROYS rows but refuses to CREATE them (see
add_syslog_collector).<index>is the table’s own row index – the OID instance, whichget_syslogsurfaces asSyslogServer.index. It is SPARSE, so it is read fresh here and never derived from a row’s position; deriving it addresses the wrong row, and the agent accepts that as a silent no-op.
- set_syslog_enabled(enabled, *, force=False)[source]¶
Turn remote syslog on or off.
Writes the vendor logging admin-mode column (
<base>.14.1.4.1.0), whose enum is1 = enabled, 2 = disabled– established from captured CLI rather than assumed, seeoids.VendorOids.syslog_admin_mode.WRITABILITY MEASURED 2026-08-02 by SETting each switch the value it already held, which cannot change device state but still distinguishes a writable column from a read-only one: m4300-24x (10.1.5.13), gsm7252ps (10.1.5.22) and gsm7228ps (10.1.5.11) all accepted it.
Deliberately narrower than
get_syslogreads. Adding or removing a COLLECTOR means creating a row in the host table, which needs a row-status write that has not been driven against hardware; offering it here on the strength of the read alone would be the inference this project refuses.Not force-gated: toggling log delivery cannot strand a switch and is reversible by writing the old value back.
- set_hostname(name, *, force=False)[source]¶
Set the switch’s host name via the standard MIB-II
sysName.GROUNDED, unlike
set_mgmt_ipbelow:sysNamewas confirmed writable on every SNMP model in this fleet on 2026-08-02, by SETting each switch the value it already held. Seeoids.SYS_NAMEfor the hosts and communities, and for why this is NOT the same value as the FASTPATHhostnamerunning-config directive.Not force-gated: renaming a switch cannot strand it the way a mgmt-IP write can, and it is trivially reversible by writing the old name back.
forceis accepted so the signature matches every other writer.
- set_mgmt_ip(address, netmask, gateway, *, force=False)[source]¶
Set the switch’s own management IP (address/netmask/gateway).
UNVERIFIED write path (see oids.VendorOids mgmt_write_* fields): the exact writable OIDs are placeholders pending Slice 7 hardware capture, so this is force-gated (a wrong mgmt-IP write can strand the switch — design spec §11.1). DHCP-mode switching is intentionally NOT offered here because even its read OID is unverified; do not fabricate it.
- class netgear_switch.snmp_write.AsyncSnmpWriter(client, model, *, protected_ports=frozenset())[source]¶
Bases:
objectAsynchronous SNMP write facade (mirror of SnmpWriter).
- async cycle_poe(
- port,
- *,
- force=False,
- timeouts=_DEFAULT_POE_TIMEOUTS,
- sleep=asyncio.sleep,
- clock=time.monotonic,
- async clear_poe_fault(
- port,
- *,
- force=False,
- timeouts=_DEFAULT_POE_TIMEOUTS,
- sleep=asyncio.sleep,
- clock=time.monotonic,
- async set_port_description(port, description, *, force=False)[source]¶
Async twin of
SnmpWriter.set_port_description– see it.
- async set_port_speed(port, speed, *, force=False)[source]¶
This backend cannot configure a port’s speed.
Refused by name rather than approximated. What SNMP offers here is
ifSpeed/ifHighSpeed, and those report the rate the link NEGOTIATED – writing one would be writing a counter, not a setting. The column that would genuinely serve this is MAU-MIB’sifMauDefaultType/ifMauAutoNegAdminStatus(mib-2.26); no switch here has been walked for it, so its presence is UNKNOWN rather than absent, and the 2026-08-03 OID sweep does not settle it (that sweep covered the 4526 VENDOR subtree only). Use a CLI backend, or establish the MAU subtree first.
- async set_flow_control(port, enabled, *, force=False)[source]¶
This backend cannot configure flow control.
Refused by name. EtherLike-MIB’s
dot3PauseAdminModeis the column that would serve this, and it is READ on the one model that publishes it (the GS728TPP) – but no SET has ever been issued against it here, so whether the agent accepts one is unknown. This library does not offer a write it has never seen succeed.
- async add_syslog_collector(host, *, port=514, severity=6, force=False)[source]¶
Async twin of
SnmpWriter.add_syslog_collector– see it for the five measured refusals.
- async remove_syslog_collector(host, *, force=False)[source]¶
Async twin of
SnmpWriter.remove_syslog_collector– see it.
- async set_syslog_enabled(enabled, *, force=False)[source]¶
Async twin of
SnmpWriter.set_syslog_enabled– see there.
- async set_hostname(name, *, force=False)[source]¶
Async twin of
SnmpWriter.set_hostname– see there.
- async set_mgmt_ip(address, netmask, gateway, *, force=False)[source]¶
Set the switch’s own management IP (address/netmask/gateway).
UNVERIFIED write path (see oids.VendorOids mgmt_write_* fields): the exact writable OIDs are placeholders pending Slice 7 hardware capture, so this is force-gated (a wrong mgmt-IP write can strand the switch — design spec §11.1). DHCP-mode switching is intentionally NOT offered here because even its read OID is unverified; do not fabricate it.
NSDP¶
Model-driven NSDP read operations over a sync or async client.
Parallel to snmp_read.py. Maps NSDP TLVs onto the SAME public models
types: port link/speed/flow-control, per-port descriptions, byte/CRC
statistics, VLAN membership, PVID and management IP.
MAC/FDB, LLDP, sensors and PoE raise UnsupportedCapabilityError, and that
is now MEASURED rather than asserted – see _NO_MACS & co. below for the
captured evidence and how to reproduce it.
- class netgear_switch.nsdp_read.NsdpReader(client, model)[source]¶
Bases:
objectSynchronous NSDP read facade over one switch.
- get_hostname()[source]¶
The switch’s host name, from the NSDP
HOSTNAMEtag (0x0003).The same value SNMP would report as
sysName– except that a Plus switch has no SNMP agent at all, which is why NSDP carries it.A switch that has never been named answers the tag with nothing, and that is a real answer rather than a failure: unlike SNMP’s
sysName, which is a mandatory scalar, this tag is genuinely optional.- Return type:
- get_device()[source]¶
Return the COMPLETE raw
NsdpDevicefor this switch: every tagparse_deviceknows how to decode, in one round trip. Unlike the otherget_*ops above, this returns the NSDP-native shape (including the raw port-status speed byte) rather than mapping onto the sharedmodelstypes – callers that need the full protocol surface (e.g. gdoc2netcfg’s DiscoveryDB) use this instead of the per-field ops.- Return type:
- get_users()[source]¶
This backend does not serve local user accounts.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- Return type:
- get_services()[source]¶
This backend does not serve management-service state.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- Return type:
- class netgear_switch.nsdp_read.AsyncNsdpReader(client, model)[source]¶
Bases:
objectAsynchronous NSDP read facade (mirror of NsdpReader).
- async get_users()[source]¶
This backend does not serve local user accounts.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- Return type:
- async get_services()[source]¶
This backend does not serve management-service state.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- Return type:
Model-driven NSDP write/control over a write-capable sync or async client.
Parallel to snmp_write.py. Every write performs the NSDP WRITE_REQUEST
(authenticated with whichever scheme the switch advertises – v1 XOR or the v2
salted challenge-response) then re-reads to verify (WriteVerificationError
with before/after on mismatch; a bad password / transport error surfaces as
NsdpError from the client first). Disruptive per-port writes to a
protected_ports port are refused unless force=True.
VLAN create/delete ARE implemented here (create = write the VLAN_MEMBERS tag
for a not-yet-existing VLAN id; delete = the VLAN_DESTROY action tag 0x2C00),
replacing a previous unproven “NSDP has no VLAN create/destroy tag” refusal –
see protocols/nsdp/write.py::vlan_destroy_tlv for the ngadmin evidence.
FIRMWARE NOTE, measured 2026-07-29/30: GS110EMX firmware 1.0.2.8 refuses the v1
XOR password outright (WRITE_REQUEST answered error=13/14 on ATTR_PASSWORD) and
requires the v2 salted auth. That scheme is implemented and live-verified (see
protocols/nsdp/auth.py), so writes DO work on that firmware; the client
picks the scheme from AUTH_V2_ENCPASS automatically. check_result names the
blamed attribute when a switch still refuses.
- class netgear_switch.nsdp_write.NsdpWriter(client, model, *, password, protected_ports=frozenset())[source]¶
Bases:
objectSynchronous NSDP write facade over one switch.
- set_hostname(name, *, force=False)[source]¶
Set the switch’s host name over NSDP (tag 0x0003).
The Plus family’s only write route for this: those switches have no SNMP agent and no CLI, so without this they cannot be renamed at all.
Not force-gated – renaming cannot strand a switch, and it is reversible by writing the old name back. Verified by re-reading the tag.
- set_port_description(port, description, *, force=False)[source]¶
Set a port’s description over NSDP tag 0xB000 (
PORT_NAME).The READ encoding is measured on three real GS110EMX units – one TLV per port, byte 0 the port number and the rest the description – and the write is that same shape (
port_name_tlv). The write itself has NOT been exercised against hardware: the three Plus units in this fleet were powered off when it was attempted. Verify-after-write below is the guard that makes that safe to ship – a wrong shape cannot pass silently.
- create_vlan(vlan, name, *, force=False)[source]¶
Create
vlanby writing an EMPTY VLAN_MEMBERS record for it.NSDP has no separate “add VLAN” action: the 802.1Q VLAN table is the set of VLAN ids that have a VLAN_MEMBERS (0x2800) record, so writing one for an id the switch does not yet list is the create. (ngadmin does the same thing –
ngadmin_setVLANDotConfonly ever writes the membership attribute, and its VLAN list is whatever comes back from reading it.)nameis accepted and ignored: the tag carries a VLAN id and two port bitmaps and no name field, and there is no name tag in the measured inventory – so a name is silently unstorable here rather than pretended.
- delete_vlan(vlan, *, force=False)[source]¶
Delete
vlanwith the VLAN_DESTROY action tag (0x2C00).Grounded in ngadmin’s
ngadmin_VLANDestroy– seeprotocols/nsdp/write.py::vlan_destroy_tlv. Deleting a VLAN drops every member port out of it, so it is force-gated exactly like the other disruptive writes.
- set_port_speed(port, speed, *, force=False)[source]¶
This backend cannot configure a port’s speed.
Refused by name rather than approximated: NSDP’s per-port speed byte is a LINK-STATE code, not a setting – its own value 0x00 is
DOWN(seeprotocols.nsdp.types.LinkSpeed), which a configuration field could not mean. No speed/duplex ADMIN tag has been identified in the tag inventory captured from live GS110EMX units.
- set_flow_control(port, enabled, *, force=False)[source]¶
This backend cannot configure flow control.
Refused by name: NSDP’s PORT_STATUS carries a flow-control byte that this library READS, but no write TLV for it has been identified in the tag inventory captured from live GS110EMX units.
- add_syslog_collector(host, *, port=514, severity=6, force=False)[source]¶
This backend cannot add a syslog collector.
Refused by name: NSDP has no logging surface at all. That is measured absence – an exhaustive tag sweep of a live GS110EMX turned up no syslog tag of any kind, which is the same finding that keeps NSDP off
get_syslog.
- remove_syslog_collector(host, *, force=False)[source]¶
This backend cannot remove a syslog collector.
Refused by name: NSDP has no logging surface at all. That is measured absence – an exhaustive tag sweep of a live GS110EMX turned up no syslog tag of any kind, which is the same finding that keeps NSDP off
get_syslog.
- class netgear_switch.nsdp_write.AsyncNsdpWriter(client, model, *, password, protected_ports=frozenset())[source]¶
Bases:
objectAsynchronous NSDP write facade (mirror of NsdpWriter).
- async set_port_description(port, description, *, force=False)[source]¶
Async twin of
NsdpWriter.set_port_description– see it.
- async create_vlan(vlan, name, *, force=False)[source]¶
Async twin of
NsdpWriter.create_vlan– see there.
- async set_hostname(name, *, force=False)[source]¶
This backend does not serve a host-name write.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- async set_port_speed(port, speed, *, force=False)[source]¶
This backend cannot configure a port’s speed.
Refused by name rather than approximated: NSDP’s per-port speed byte is a LINK-STATE code, not a setting – its own value 0x00 is
DOWN(seeprotocols.nsdp.types.LinkSpeed), which a configuration field could not mean. No speed/duplex ADMIN tag has been identified in the tag inventory captured from live GS110EMX units.
- async set_flow_control(port, enabled, *, force=False)[source]¶
This backend cannot configure flow control.
Refused by name: NSDP’s PORT_STATUS carries a flow-control byte that this library READS, but no write TLV for it has been identified in the tag inventory captured from live GS110EMX units.
- async add_syslog_collector(host, *, port=514, severity=6, force=False)[source]¶
This backend cannot add a syslog collector.
Refused by name: NSDP has no logging surface at all. That is measured absence – an exhaustive tag sweep of a live GS110EMX turned up no syslog tag of any kind, which is the same finding that keeps NSDP off
get_syslog.
- async remove_syslog_collector(host, *, force=False)[source]¶
This backend cannot remove a syslog collector.
Refused by name: NSDP has no logging surface at all. That is measured absence – an exhaustive tag sweep of a live GS110EMX turned up no syslog tag of any kind, which is the same finding that keeps NSDP off
get_syslog.
HTTP web UI¶
Model-driven web-UI read operations over a sync or async HttpSession.
Parallel to snmp_read.py/nsdp_read.py. Construction is gated on
HttpModelSpec.reads_verified: a model whose web reads are still
UNVERIFIED-pending-capture (gsm7228ps cheetah/S3300) refuses to construct
rather than return fabricated data – the facade never gets a
plausible-but-wrong result from an unverified scrape. Ops a model’s HTTP
surface genuinely does not expose (e.g. gs110emx has no PoE, so PoE/MAC/LLDP/
sensor reads; see protocols/http/endpoints.py) raise
UnsupportedCapabilityError honestly instead of silently returning [],
via _require_path’s per-op None-path check below. gs110emx’s web UI
DOES cover the full NSDP read surface (ports/stats/VLANs/PVIDs/mgmt-IP) – the
port/stats/PVID/VLAN-list parsers are selected by HttpModelSpec.html_dialect
and get_mgmt_ip by sysinfo_path because gs110emx’s pages use a
different HTML dialect than gs305ep’s (see protocols/http/parse.py).
The gsm7252ps (XE_FASTPATH) web UI covers EVERY read op this library has –
ports/stats/PVIDs/VLANs/MACs/PoE/LLDP plus sensors and mgmt-IP from
sysInfo.html – so no op is carved out for that model. Its spec says
reads_verified=True: the HTTP output was cross-verified against SNMP on the
live switch (10.1.5.22). The honesty gate (which refuses to construct while
reads_verified is False) still guards models that have NOT been
cross-verified, e.g. gsm7228ps.
All page-path selection and HTML-to-model conversion lives in the
module-level helpers below (pure, I/O-free); HttpReader/AsyncHttpReader
differ only in whether session.get_page/post_form is awaited.
- netgear_switch.http_read.fastpath_membership_paths(spec, model_key)[source]¶
(GET page, POST target)for the managed FASTPATH VLAN-membership page.Both must be populated for a managed model; a
Nonehere is a spec defect, not a device limitation, so it raises with the field name rather than degrading the read (principle 1: fail loud).
- class netgear_switch.http_read.HttpReader(session, model)[source]¶
Bases:
objectSynchronous web-UI read facade over one switch.
- read_fastpath_membership(vlan)[source]¶
One VLAN’s membership page from the managed FASTPATH web UI.
The GET shows whichever VLAN the firmware last selected, so any other VLAN needs the form POST the browser’s own
screen_refresh()makes: the full field set withsubmt=0, which re-renders WITHOUT applying (confirmed live – re-reading a VLAN returned a byte-identical page). Shared byget_vlansandHttpWriter.set_vlan_membership.- Return type:
- get_hostname()[source]¶
The switch’s host name, from its device-identity page.
Only the two dialects whose identity page actually carries the field can serve this: gs110emx’s
sysInfo.htmland gs105pe’sswitch_info.cgi, both of which already expose it asHttpSysInfo.switch_name. Every other dialect’s identity page has no such field, and is refused by name rather than returning “” – an empty string is a real host name on a switch that has never been named, so it must not double as “this backend cannot tell you”.- Return type:
- get_users()[source]¶
Local login accounts, from this model’s user-management page.
Refuses by name on a model whose UI has no such page located, rather than returning empty: an empty answer would be indistinguishable from a switch that genuinely has no accounts.
- Return type:
- get_services()[source]¶
Management-service state, one page per service.
Refuses by name unless ALL FOUR pages are located for this model – see
_service_paths.- Return type:
- class netgear_switch.http_read.AsyncHttpReader(session, model)[source]¶
Bases:
objectAsynchronous web-UI read facade (mirror of
HttpReader).- async read_fastpath_membership(vlan)[source]¶
Async twin of
HttpReader.read_fastpath_membership(see its docs).- Return type:
- async get_users()[source]¶
Local login accounts, from this model’s user-management page.
Refuses by name on a model whose UI has no such page located, rather than returning empty: an empty answer would be indistinguishable from a switch that genuinely has no accounts.
- Return type:
- async get_services()[source]¶
Management-service state, one page per service.
Refuses by name unless ALL FOUR pages are located for this model – see
_service_paths.- Return type:
Model-driven web-UI write operations with verify-after-write + guards.
Parallel to snmp_write.py. Every mutating op: (1) enforces
protected_ports on disruptive ports unless force=True; (2) GETs the
target page to scrape the fresh CSRF hash; (3) POSTs the encoded form;
(4) re-GETs and re-parses to confirm the change actually took — raising
WriteVerificationError(before, after) on divergence, NEVER silently
succeeding.
set_port_enabled, set_mgmt_ip and clear_poe_fault used to raise
UnsupportedCapabilityError for EVERY model. They were missing
implementations, not device limitations, and are now built:
set_port_enabled— the managed models’portsConfiguration.htmlAdmin Mode column (LIVE-VERIFIED on all four: gsm7252ps, gsm7228ps, m4300-24x, m4300-16x), and the GS110EMX’s differently-shapedport_settings.htmlPhysical Mode POST (LIVE-VERIFIED on 10.1.5.26).clear_poe_fault— the managed PoE page’s hidden write-only “Port Reset” column driven by its RESET button (LIVE-VERIFIED on gsm7252ps, gsm7228ps and m4300-16x), and the Plus UI’sPoEPortConfig.cgireset.set_mgmt_ip— each managed model’s own management-IP form. The APPLY is deliberately NOT live-verified: doing so would move a real switch’s management address and drop the session mid-write. See the method docstring for exactly what is and is not proven.
Where an op still raises for a model, the refusal names captured device output:
the M4300-24X’s PoE page has zero rows because the SKU has no PSE, and the
GS110EMX has no PoE page at all. gsm7252ps used to be on that list – its PoE
form was recorded as answering err_flag=1 to every write – and it did not
belong there: the body was missing the page’s own list-unit field, and once it
rides along the write lands (see endpoints.py and
forms.xui_row_apply_form).
VLAN membership on the MANAGED (FASTPATH/Cheetah) models — gsm7252ps,
gsm7228ps/S3300 and both M4300 SKUs — goes through
_set_fastpath_membership against the live-discovered
switching/dot1q/vlan_port_cfg_rw.html endpoint; the Plus-class models keep
the 8021qMembe.cgi path. That is a fifth step for these pages: they answer
HTTP 200 even when they REFUSE the write, reporting it in a hidden
err_flag/err_msg pair, so the apply response is checked for that before
verification (see _raise_on_fastpath_err_flag) and the switch’s own message
is what the caller gets.
- class netgear_switch.http_write.HttpWriter(session, model, *, protected_ports=frozenset())[source]¶
Bases:
object- set_port_description(port, description, *, force=False)[source]¶
Label a port through the ports page’s
interfaceDescription.XML-API only for now: that page carries the field and the read side already parses it. The FASTPATH XUI port pages have a description column too, but its cell id has not been captured, and guessing one would post into an unknown cell.
- clear_poe_fault(port, *, force=False, timeouts=None)[source]¶
Clear a PoE fault on
portby re-running the port’s PoE detection.On the managed FASTPATH models this is the page’s own hidden write-only “Port Reset” column driven by its RESET button – the same mechanism the CLI backend uses (
poe reset) and the same oneSnmpWriteremulates with an admin off/on re-arm. On the Plus-class CGI UI it is thePoEPortConfig.cgireset form, identical tocycle_poe: a Plus switch has no separate “clear fault” action, the fault clears when detection re-runs.
- set_port_enabled(port, enabled, *, force=False)[source]¶
Set port
port’s admin mode throughportsConfiguration.html.LIVE-VERIFIED 2026-07-30 on ALL FOUR managed switches, each on a link-down, undescribed port, as disable -> re-read -> enable -> re-read: gsm7252ps 10.1.5.22 port 36, gsm7228ps 10.1.5.11 port 12 (
1/g12), m4300-24x 10.1.5.13 port 16, m4300-16x 10.1.5.20:49152 port 15. In every case the apply answerederr_flag=0and a full re-read of the table showed the target row’s Admin Mode cell changed and EVERY other cell of every other row byte-identical.
- set_mgmt_ip(address, netmask, gateway, *, force=False)[source]¶
Set the switch’s STATIC management address through its web UI.
The page and field names are per model (see
XuiMgmtIpFields), both live-captured 2026-07-30. Disruptive by definition – it moves the address the caller is talking to – so it needsforce=True, and the capability is resolved BEFORE the force gate exactly likereboot.The APPLY on this page is UNVERIFIED against live hardware, and deliberately so. Every other write in this file was proven by doing it to a real switch and reading it back; this one was not, because applying it to any of the four reachable switches would have moved that switch’s management address, dropped the session mid-write and risked stranding a device on a network nobody could reach. What IS verified live on all four: the page exists and is the right one, its field names/values are the device’s own (read back through
get_mgmt_ip), and the surrounding machinery – thesubmit_flag=8apply flag, the whole-form echo, the button field, theerr_flagrefusal check – is the same machinery proven byset_port_enabledandset_vlan_membershipon these exact pages. The verify-after-write below is therefore real: if the switch refuses, the caller is told.
- upload_certificate(cert_pem, key_pem, *, force=False)[source]¶
Upload an HTTPS SSL server certificate (combined cert+key PEM).
GROUNDED for gsm7228ps/S3300 (multipart form) and gs728tpp (GoAhead XML-API) – see endpoints.py and
_cert_upload_xml. A model whose real mechanism is a non-HTTP SCP copy (m4300/gsm7252ps) raises NotImplementedError pointing atupload_certificate_scp; a model with no known mechanism raises UnsupportedCapabilityError. Disruptive (replaces the running certificate), soforce=Trueis required – capability is resolved BEFORE the force gate, mirroringreboot.
- set_hostname(name, *, force=False)[source]¶
Set the host name, where this dialect’s identity page carries one.
Two dialects, and they are nothing alike:
GoAhead XML API –
DeviceBasicInfo/deviceNameIS the host name (MEASURED: it reads byte-for-byte what SNMP reports through sysName).GS110EMX – an ordinary form POST, but the host name shares that form with the MANAGEMENT ADDRESS, so it is a read-modify-write. See
_set_gs110emx_hostname.
Every other dialect is refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- set_port_speed(port, speed, *, force=False)[source]¶
Set a port’s speed/duplex through the ports page’s admin fields.
XML-API only. That page’s
Standard802_3ListcarriesautoNegotiationAdminEnabled/speedAdmin/duplexAdminMode, the read side already parses them, and the exact encoding is transcribed from the page’s own submit JS (seegoahead.port_speed_body). The FASTPATH XUI port pages have a Speed control too, but its cell id has not been captured, and guessing one would post into an unknown cell.A rate the page’s own dropdown does not offer is refused by name. That list is device evidence, not a house rule: the
slctPortSpeed<option>set is 10/100 half-or-full, 1000 FULL ONLY, and Auto. Note this UI DOES offer a forced 1000 where the FASTPATH CLI does not – which is exactly why that refusal lives inCliWriterand not inPortSpeed.Disruptive – applying a speed bounces the link – so it honours
protected_ports.
- set_flow_control(port, enabled, *, force=False)[source]¶
This backend cannot configure flow control on this UI.
Refused by name, and this one is a MEASURED absence rather than an unsearched one. The GoAhead ports page publishes
flowControlAdminType/flowControlOperTypebut has no control for either: itsslct*selects are Admin Mode and Port Speed only, and its submit builder emits no flow-control field at all (see tests/fixtures/http/gs728tpp_ports.xml). Flow control lives on a different page of that UI which has not been captured, and the FASTPATH XUI equivalent has not either.
- add_syslog_collector(host, *, port=514, severity=6, force=False)[source]¶
This UI will not accept a collector ADD – established, not assumed.
The template row is real and reachable: the page renders
v_g_2_1_1..``v_g_2_1_7`` in the served HTML, and this library can build the body. What the FIRMWARE does with it, driven live against m4300-24x 10.1.5.13 on 2026-08-05, is refuse – HTTP 200 withError! Failed to Set '<field>' with '<value>'lines, and the collector table unchanged (checked through the switch’s own CLI, which is an independent witness):the body as first built -> failed on ‘Host Address’ and ‘Port’
plus IP Address Type “IPv4” -> those two passed; the two ENUMS then failed (‘IP Address Type’, ‘Severity Filter’)
plus enums as INDICES instead of labels (xa_2_1_4 lists Emergency..Debug in standard syslog order, xa_2_1_7 lists Unknown,IPv4,IPv6,DNS, so IPv4 = 1) -> the enums passed and ‘Host Address’ failed again
plus row-status “Add” instead of “Active” (the page’s own xeleValue_2_1_5) -> unchanged
So three of the four fields can be made to stick and the address cannot, which says something in the create path is still unaccounted for. Rather than keep guessing at a production switch – the trial-and-error principle 4 exists to stop – this refuses, and the remaining step is ONE capture of a real browser Add submission to diff against.
The DELETE path on the same page DOES work and is live-verified; see
remove_syslog_collector. Add over a CLI backend.
- remove_syslog_collector(host, *, force=False)[source]¶
Remove a collector by marking its row-status
Delete.The page’s Delete action array writes
"Delete"into the same write-only cell an Add sets to"Active"(xa_4_3_1->"2_1_5|g_2_1_5"). The row is addressed by its OWN rendered fields and checkbox, so no index arithmetic is involved – unlike the CLI and SNMP routes, whose sparse table index bit once already.
- class netgear_switch.http_write.AsyncHttpWriter(session, model, *, protected_ports=frozenset())[source]¶
Bases:
object- async set_port_description(port, description, *, force=False)[source]¶
Async twin of
HttpWriter.set_port_description– see it.
- async clear_poe_fault(port, *, force=False, timeouts=None)[source]¶
Async twin of
HttpWriter.clear_poe_fault(see its docs).
- async set_port_enabled(port, enabled, *, force=False)[source]¶
Async twin of
HttpWriter.set_port_enabled(see its docs).
- async set_mgmt_ip(address, netmask, gateway, *, force=False)[source]¶
Async twin of
HttpWriter.set_mgmt_ip– INCLUDING its honesty caveat: the apply is unverified against live hardware, because verifying it would have moved a real switch’s management address. See the sync twin’s docstring for exactly what is and is not proven.
- async set_hostname(name, *, force=False)[source]¶
This backend does not serve a host-name write.
Refused by name rather than returned empty: an empty answer here would be indistinguishable from a switch that genuinely has none.
- async set_port_speed(port, speed, *, force=False)[source]¶
Async twin of
HttpWriter.set_port_speed– see it.
- async set_flow_control(port, enabled, *, force=False)[source]¶
This backend cannot configure flow control on this UI.
Refused by name, and this one is a MEASURED absence rather than an unsearched one. The GoAhead ports page publishes
flowControlAdminType/flowControlOperTypebut has no control for either: itsslct*selects are Admin Mode and Port Speed only, and its submit builder emits no flow-control field at all (see tests/fixtures/http/gs728tpp_ports.xml). Flow control lives on a different page of that UI which has not been captured, and the FASTPATH XUI equivalent has not either.
- async add_syslog_collector(host, *, port=514, severity=6, force=False)[source]¶
This backend cannot add a syslog collector.
Refused by name. The M4300 syslog page DOES declare the mechanism – cell 2_1_5 is a write-only
L7_ROW_STATUS_twhose DELETE button writes “Delete” and whose APPLY writes “Active” – but no POST for a FASTPATH-XUI row add has ever been captured, and the existing XUI write path covers only the CSRF-hash dialects. The envelope would have to be invented. The gsm7252ps/gsm7228ps pages do not even declare the cells.
- async remove_syslog_collector(host, *, force=False)[source]¶
This backend cannot remove a syslog collector.
Refused by name. The M4300 syslog page DOES declare the mechanism – cell 2_1_5 is a write-only
L7_ROW_STATUS_twhose DELETE button writes “Delete” and whose APPLY writes “Active” – but no POST for a FASTPATH-XUI row add has ever been captured, and the existing XUI write path covers only the CSRF-hash dialects. The envelope would have to be invented. The gsm7252ps/gsm7228ps pages do not even declare the cells.
FASTPATH CLI¶
Model-driven CLI read operations over a CliSession.
The CLI analogue of http_read.py: dispatches each get_* op to its
FASTPATH show command (via protocols.cli.commands.cli_spec) over a
CliSession transport, and parses the returned text with the pure functions
in protocols.cli.parse. Works unchanged against a real SSH/telnet/console
session or the in-process mock face (virtual.faces.cli.VirtualCliFace).
Unlike HttpReader, construction is NOT gated on reads_verified: the
reader is always usable directly (mock tests, and a future live-verified flip),
and it is the FACADE that refuses live CLI dispatch while reads_verified is
False (see _dispatch.cli_reads_supported / SyncSwitch._reader_for).
Ops a FASTPATH model’s CLI genuinely lacks raise UnsupportedCapabilityError
honestly rather than fabricating: get_poe on the non-PoE M4300-24X (PoE port
count 0) is the only such carve-out – every other show command exists on
every FASTPATH switch.
- class netgear_switch.cli_read.CliReader(session, model)[source]¶
Bases:
objectSynchronous FASTPATH-CLI read facade over one switch.
- get_services()[source]¶
Which management services are enabled, and on which ports.
Three commands, because the switch splits it that way – and the telnet one is
show telnetcon, notshow telnet. Seeparse.parse_servicesfor why that distinction matters.- Return type:
- get_users()[source]¶
The switch’s local login accounts, from
show users.The access-mode wording differs between firmware images, so
SwitchUser.access_modekeeps the raw text andprivilegedcarries the normalised reading – seeparse.parse_users.- Return type:
- get_syslog()[source]¶
Remote-logging configuration, from
show logging+ its host table.Two commands because the switch splits it that way: the globals live in
show loggingand the collectors inshow logging hosts. The host table’s column set differs by firmware – seeparse.parse_syslog.- Return type:
- get_hostname()[source]¶
The switch’s host name, from
show hosts.See
parse.parse_hostnamefor why this command and notshow running-config: the two report different values, and only this one agrees with SNMP’ssysName.- Return type:
Model-driven FASTPATH CLI write/control over a CliSession.
Two write paths live here:
CliWriter– the VLAN write backend (create/delete a VLAN, set a port’s membership, set a port’s PVID) over the config-mode CLI. The CLI analogue ofsnmp_write.SnmpWriter/http_write.HttpWriter: every write is followed by a read-back throughcli_read.CliReaderand raisesWriteVerificationError(carrying before/after) on divergence, never a silent success.deploy_certificate_scp– the SSL-certificate deploy overcopy scp://, for the Fully Managed FASTPATH line (M4300 / GSM7252PS) whose firmware takes an HTTPS server certificate over SCP rather than an HTTP form.
Both REUSE the library’s existing CLI transport: plain commands go through
CliSession.run; the interactive copy and write memory steps go through
CliSession.run_scp_copy / run_write_memory (the byte-level interactive
driving lives on the shared ShellDriver – no new transport). There is no
async twin: all three CLI transports (paramiko SSH, telnet, pyserial console) are
synchronous, so AsyncSwitch has no CLI backend at all.
HONESTY, per path:
CliWriteris LIVE-VERIFIED on all four CLI models (2026-07-30), each on a link-down, undescribed port and each restored to a byte-identical running-config with its PVID and VLAN membership unchanged and the throwaway VLAN 4001 deleted:model
host
ops driven through this module
gsm7252ps
10.1.5.22
VLAN create/tag/untag/exclude/delete, PVID, port admin
m4300-24x
10.1.5.13
same (no PoE hardware on this SKU)
m4300-16x
10.1.5.20
same + PoE off/on + cycle_poe
gsm7228ps
10.1.5.11
same + PoE off/on + cycle_poe (telnet 60000, “1/g<n>”)
set_mgmt_ipandrebootare the two deliberate exceptions: their command syntax is confirmed from each device’s own help output, but neither was executed – a mgmt-IP change drops the very session issuing it and a reboot would interrupt production traffic. Both say so at the method.The cert deploy is GROUNDED in the working certbot-hook
FastpathScpUpdater(seetmp/certbot_hook_prior_art.py) and MOCK-TESTED end-to-end, but NOT live-verified – a real run is a production write that needs a staging SCP server to pull the PEM from, which CI has neither the hardware nor the network for. The library only SENDS the copy commands; the CALLER stages the PEM on the SCP source first (per the user’s decision), exactly as the certbot hook’smaindoes.
- netgear_switch.cli_write.scp_source_url(scp_source, remote_dir, filename)[source]¶
Build the
scp://source URL for one staged PEM.Mirrors
FastpathScpUpdater._source_url: an ABSOLUTE staging path so FASTPATH’s scp client requests the exact path the SCP source’s ForceCommand wrapper authorises (no home-relative ambiguity).scp_sourceis theuser@host[:port]the caller’s staging server answers on.- Return type:
- netgear_switch.cli_write.deploy_certificate_scp(
- session,
- *,
- scp_source,
- scp_password,
- remote_dir,
- base,
- chain,
- writemem_stuff,
Run the 5-step FASTPATH cert-deploy EXEC sequence over
session.sessionmust already be set up (enable + paging off – the transport does this on connect). The staged PEMs are named<base>-server.pem(and, whenchainis set,<base>-root.pem) underremote_diron the SCP source.Steps (grounded in
FastpathScpUpdater.upload_certificate):no ip http secure-server– HTTPS must be off to accept the sslpem.copy scp://<src>/<base>-server.pem nvram:sslpem-server(interactive).optional
copy scp://<src>/<base>-root.pem nvram:sslpem-root(CA chain).ip http secure-server– re-enable; loads the new cert, no reboot.write memory– persist, with the per-model confirm (writemem_stuff).
- class netgear_switch.cli_write.CliWriter(session, model, *, protected_ports=frozenset())[source]¶
Bases:
objectSynchronous FASTPATH-CLI write facade over one switch.
Offers the SAME write surface as
SnmpWriter/HttpWriter– VLAN lifecycle, per-port membership and PVID, PoE admin/cycle/fault-clear, port admin state, the management IP and reboot – because the point of several backends is that the CALLER chooses one (CLAUDE.md principle 2). Every command is carried per model inprotocols.cli.commands.CliModelSpecand was confirmed against each reachable switch’s own context-sensitive help on 2026-07-30 (gsm7252ps 10.1.5.22, m4300-24x 10.1.5.13, m4300-16x 10.1.5.20, gsm7228ps/S3300 10.1.5.11), so the S3300’s1/g<n>/1/xg<n>interface naming, the M4300’sip management addressand the gsm7252ps’s ABSENTswitchport modeare each handled as that firmware really behaves.VLAN command sequences, PROVEN BY HAND on the M4300-24X before being encoded:
create –
vlan database/vlan <vid>/vlan name <vid> <name>/exitdelete –
vlan database/no vlan <vid>/exitmembership –
configure/interface <iface>/switchport mode generalthen eithervlan participation include <vid>plusvlan tagging <vid>(TAGGED) orno vlan tagging <vid>(UNTAGGED), orvlan participation exclude <vid>(EXCLUDED) /exit/exitPVID –
configure/interface <iface>/switchport mode general/vlan pvid <vid>/exit/exit
Other ops, per model (all four confirmed from the devices’ own help):
PoE –
configure/interface <iface>/poe|no poe|poe reset/exit/exitport admin –
no shutdown|shutdownin interface config modemanagement IP – privileged-EXEC
network parms <ip> <mask> <gw>(gsm7252ps, gsm7228ps) or global-configip management address <ip> <mask>+ip default-gateway <gw>(M4300 12.0.x, which has nonetwork parms)reboot – privileged-EXEC
reloadwith its(y/n)confirm
THE SWITCHPORT-MODE FINDING (why
switchport mode generalis sent first): on real hardware thevlan participation/vlan taggingcommands are accepted into running-config while the port is inswitchport mode accessbut stay completely INERT –show vlan <vid>keeps reportingExclude/Autodetectfor that port. They only take effect inswitchport mode general. This was proven by before/aftershow running-config interface 1/0/4+show vlan 4007on 10.1.5.13. So every per-port VLAN op ensures general mode first (idempotent: re-issuing it on an already-general port is a no-op). That is a deliberate, unavoidable side effect of a CLI membership write and is why the mock reproduces the inert behaviour too (virtual.faces.cli). The gsm7252ps XE image is the exception – it has noswitchport modecommand at all, so the step is omitted there rather than being rejected (see_general_mode).CONSEQUENCE WORTH KNOWING (measured on 10.1.5.20 while live-verifying this backend): a port moved to general mode stops honouring any
switchport access vlan/switchport trunk native vlanlines that were sitting INERT in its config, and later sendingswitchport mode accessACTIVATES them – leaving the port in a different state than it started in (its PVID jumped from 1 to the configured access VLAN 10). The command that genuinely restores the prior behaviour isno switchport mode, verified live: it removes the mode line and the port went back to PVID 1 / VLAN 1 untagged with a byte-identical running-config.Two further deliberate behaviours:
FASTPATH answers an ACCEPTED configuration command with EMPTY output, so any text back (
% Invalid input,ERROR: ...) is treated as a failure and raised asCliCommandError– never swallowed.NOTHING is persisted: no
write memoryis issued, so these writes change the running config only, exactly like this library’s SNMP/HTTP VLAN writes. A caller that wants them to survive a reboot must save separately.
- create_vlan(vlan, name, *, force=False)[source]¶
Create VLAN
vlannamedname(vlan databasesequence).Creating an EMPTY VLAN adds no port membership, so it is non-disruptive and needs no
force; the parameter exists for signature symmetry withdelete_vlan(mirroringSnmpWriter.create_vlan).namegoes on the wire as a bare token – FASTPATH’svlan nametakes no spaces, and a name it rejects surfaces asCliCommandErrorrather than being silently mangled.
- delete_vlan(vlan, *, force=False)[source]¶
Delete VLAN
vlan(vlan database/no vlan <vid>).Refuses up front if the VLAN does not exist (a precondition failure, NOT a verification divergence – no command has been sent yet, mirroring
SnmpWriter.delete_vlan), and refuses withoutforcewhen the VLAN still carries a protected port, since deleting it strips that port’s membership.
- set_vlan_membership(vlan, port, mode, *, force=False)[source]¶
Set
port’s participation invlantomode.Verifies the TARGET PORT’s participation only, deliberately: unlike the SNMP writer – which SETs whole egress/untagged PortList bitmaps and so must verify both bitmaps in full – these commands address exactly one interface, and forcing general mode can legitimately move that port’s membership in OTHER VLANs (its former access VLAN), which is not this VLAN’s business.
- set_port_description(port, description, *, force=False)[source]¶
Label
port(description '<text>'), or clear it with"".The command form is the firmware’s own: a live GSM7252PS (10.1.5.22, 2026-08-03) renders its 38 labelled ports in
show running-configasdescription 'eth0.rpi5-pmod'– single-quoted – and the negation is the standardno description.Cosmetic, so no
switchport mode generalpreamble is needed (unlike the VLAN commands, whose effect depends on the port’s mode) and it is not force-gated beyond the protected-port guard.
- set_port_speed(port, speed, *, force=False)[source]¶
Force
port’s speed/duplex, or return it to auto-negotiation.DISRUPTIVE: changing either setting bounces the link, so this honours
protected_portsexactly asset_pviddoes.Both command forms were PROVEN BY EXECUTION on gsm7252ps 10.1.5.22 port 1/0/8 (link-down, undescribed, 2026-08-03) and the port restored to a byte-identical running-config afterwards –
speed 100 full-duplexmoved Physical Mode to100 Full,speed automoved it back.A forced 1000 is refused HERE, before anything is sent, because it is not a rate this grammar has: 1000BASE-T makes auto-negotiation mandatory, and the firmware encodes that by omitting 1000 from the forced rates while keeping it among the advertised ones. Measured, not inferred –
speed 1000 full-duplexon the port above answered “% Invalid input detected at ‘^’ marker.” and left Physical Mode untouched. Refusing by name says WHY; letting it through would produce the same failure with none of the explanation.Every OTHER rate is sent unchecked, deliberately: the forced rates a port offers follow its PHY rather than the firmware (1/0/8 offered 10/100/10G, an m4300-24x 10GBASE-T port offered 100/10G), so the switch is the only authority worth asking. One it rejects comes back as
CliCommandErrorcarrying the device’s own words.
- set_flow_control(port, enabled, *, force=False)[source]¶
Turn IEEE 802.3x flow control on or off for
port.flowcontrol/no flowcontrolin interface config mode – bare toggles, and PROVEN as a round trip on gsm7252ps 10.1.5.22 port 1/0/8 (2026-08-03): the first added aflowcontrolline to running-config and moved Flow Mode from Disable to Enable, the second removed it and returned the column, with the port left byte-identical.Verified against
show port all’s Flow Mode column, which is the CONFIGURED setting: it moved on a port whose link was DOWN throughout, so it cannot be reporting a negotiated result.Disruptive enough to honour
protected_ports: enabling pause frames changes how a link behaves under congestion.
- set_pvid(port, vlan, *, force=False)[source]¶
Set
port’s ingress PVID tovlan(vlan pvid <vid>).Changing a port’s PVID re-homes its untagged traffic, so it is disruptive and honours
protected_ports(mirroringSnmpWriter.set_pvid).switchport mode generalis sent first for the same reason asset_vlan_membership: in access mode the port’s PVID follows its access VLAN, sovlan pvidcannot take effect.Refuses up front if the VLAN does not exist, exactly as
set_vlan_membershipdoes – a precondition failure, so no command is sent. This used to be left to the switch, on the assumption it would reject the command. That assumption does not hold generally: MEASURED on a GS728TPP (10.2.5.10, firmware 6.0.1.30), the equivalent write is ACCEPTED and reads back, leaving the port pointing at a VLAN that is not there – which no amount of verify-after-write can catch.
- set_poe(
- port,
- on,
- *,
- force=False,
- timeouts=None,
- sleep=time.sleep,
- clock=time.monotonic,
Enable/disable PoE on
port(poe/no poe).Verified through
show poe port info all, whose Status column readsDisabledfor an admin-off port – that command has NO admin column, so admin state is read from Status (seeparse.parse_poe).The read-back POLLS, and that is not defensive padding: Status is a DETECTION state, so it lags the admin write. Measured on the real M4300-16X (10.1.5.20, FASTPATH 12.0.19.15, 2026-07-30): immediately after
poere-enabled port 1/0/1 the table still saidDisabled, and the same port readSearching(admin enabled) moments later. A single immediate read therefore reported a WORKING write as a verification failure. Disabling is observable at once, but the same polled check covers both directions.sleep/clockare injectable so tests do not wait.
- cycle_poe(
- port,
- *,
- force=False,
- timeouts=None,
- sleep=time.sleep,
- clock=time.monotonic,
Power-cycle PoE on
portand wait for it to deliver again.
- clear_poe_fault(
- port,
- *,
- force=False,
- timeouts=None,
- sleep=time.sleep,
- clock=time.monotonic,
Clear a PoE fault on
port: re-arm detection, then poll until the port has LEFT the fault state (delivering or searching), exactly the recovery predicateSnmpWriter.clear_poe_faultuses.
- set_port_enabled(port, enabled, *, force=False)[source]¶
Enable/disable
port(no shutdown/shutdown).Command form confirmed on all four CLI models (“shutdown ?” -> <cr>). Verified through
show port all’s Admin Mode column.
- set_hostname(name, *, force=False)[source]¶
Set the switch’s host name, via global-config
hostname <name>.Not force-gated: renaming cannot strand the switch and is reversible by writing the old name back, unlike
set_mgmt_ipbelow which drops the session that issues it.forceis accepted so the signature matches every other writer.Verified by re-reading
show hosts. That command, rather thanshow running-config, is deliberate and load-bearing here: the two report different values on real hardware (seeprotocols.cli.parse.parse_hostname), andshow hostsis the one that agrees with SNMP, so a CLI write verified this way is also observable over SNMP.Nothing is persisted – no
write memory– exactly like every other write in this module.
- set_mgmt_ip(address, netmask, gateway, *, force=False)[source]¶
Set the switch’s own management IP.
Per-model dialect (both confirmed against the devices’ own help on 2026-07-30 – see
CliModelSpec.mgmt_ip_*):gsm7252ps / gsm7228ps – privileged EXEC
network parms <ip> <mask> <gateway>m4300-24x / -16x – global config
ip management address <ip> <mask>thenip default-gateway <gw>(these images have nonetwork parmsat all)
force=Trueis required: unlike the SNMP path (whose write OIDs are placeholders), these commands are the switch’s real documented ones – but the op can still strand the switch, and it will normally drop the very CLI session issuing it (the session is reached over the address being changed), so the read-back may itself fail. Deliberately NOT live-tested for that reason.
- reboot(*, force=False)[source]¶
Reboot the switch (
reload, privileged EXEC).Command confirmed on all four models (“reload ?” ->
<cr>/<unit>).reloadasks a(y/n)confirm, so it is issued through the same confirm-answering transport pathwrite memoryuses.HONESTY: this is the one write here that CANNOT be verified – the switch stops answering by definition, and the CLI session dies with it (a dropped session IS the success signal, which is why
CliTransportErroris not treated as a failure). It is also the one write deliberately NOT exercised on live hardware: these switches carry production traffic. Callers must re-poll the switch themselves.
- set_syslog_enabled(enabled, *, force=False)[source]¶
Turn remote logging on or off (
logging syslogin global config).The positive form is VERBATIM from every switch’s own
show running-config(all four FASTPATH models print the bare linelogging syslog, read 2026-08-05). The negation is the standard FASTPATHnoand is inferred – seelogging_no_syslog_cmd; a wrong form is rejected by the device and raised, never swallowed.Not force-gated: it changes where log messages go, not how traffic is switched, and is reversible by writing the old value back.
- add_syslog_collector(host, *, port=514, severity=6, force=False)[source]¶
Add a remote syslog collector.
logging host "<address>" <ipv4|ipv6|dns> <port> <severity-word>, which is VERBATIM the line all four FASTPATH models print in their ownshow running-config(read 2026-08-05 – read-only, no?).Refuses up front if a collector for
hostalready exists: FASTPATH would otherwise add a SECOND row for the same address, and the caller who asked for “send logs here” would silently get duplicate delivery. A precondition failure, so no command is sent (mirroringset_pvid).Not force-gated, for the same reason as
set_syslog_enabled.
- remove_syslog_collector(host, *, force=False)[source]¶
Remove the remote syslog collector for
host.logging host remove <index>– a SUBCOMMAND, not a negation, and addressed by the table’s OWN Index column rather than by address. Read from a fresh table immediately before the write, never cached.THE INDEX IS SPARSE, and it is not the row’s position. Measured on m4300-24x 10.1.5.13 (2026-08-05): after some churn the table held Index 1 and Index 3, with nothing at 2. This method used to enumerate the rows and count – which addressed Index 2, a row that did not exist. The switch ACCEPTED that removal as a no-op and the collector survived, so the bug was silent until the table was read column-wise.
The obvious
no logging host <index>is WRONG and was corrected the expensive way – a live gsm7252ps rejected it, and every address spelling too, leaving a throwaway collector on the switch untillogging host ?showedremove/reconfigureas subcommands. The verify-after-write did its job (the failure was loud and the state was recoverable), but the lesson is that a FASTPATH negation is not a safe inference.LIVE-VERIFIED 2026-08-05 on all four CLI models – gsm7252ps 10.1.5.22, m4300-24x 10.1.5.13, m4300-16x 10.1.5.20 and gsm7228ps 10.1.5.11 (over telnet 60000) – each by adding 192.0.2.1 (TEST-NET-1, routes nowhere), reading it back, removing it, and proving
show logging hostsbyte-identical to its prior state with the production 10.1.5.1 collector untouched throughout. Nowrite memory.Refuses up front if no such collector exists, rather than sending a removal for a row that is not there.