python-netgear-switch-library 0.0.post154__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netgear_switch/__init__.py +132 -0
- netgear_switch/_dispatch.py +178 -0
- netgear_switch/_version.py +24 -0
- netgear_switch/aio_api.py +529 -0
- netgear_switch/cli/__init__.py +1 -0
- netgear_switch/cli/capture.py +131 -0
- netgear_switch/cli/context.py +39 -0
- netgear_switch/cli/format.py +201 -0
- netgear_switch/cli/main.py +484 -0
- netgear_switch/cli/resolve.py +108 -0
- netgear_switch/cli/safety.py +71 -0
- netgear_switch/config.py +184 -0
- netgear_switch/errors.py +52 -0
- netgear_switch/http_read.py +174 -0
- netgear_switch/http_write.py +420 -0
- netgear_switch/models.py +156 -0
- netgear_switch/nsdp_read.py +221 -0
- netgear_switch/nsdp_write.py +315 -0
- netgear_switch/protocols/__init__.py +1 -0
- netgear_switch/protocols/http/__init__.py +1 -0
- netgear_switch/protocols/http/crypt.py +29 -0
- netgear_switch/protocols/http/endpoints.py +165 -0
- netgear_switch/protocols/http/forms.py +77 -0
- netgear_switch/protocols/http/parse.py +238 -0
- netgear_switch/protocols/http/session.py +29 -0
- netgear_switch/protocols/nsdp/__init__.py +7 -0
- netgear_switch/protocols/nsdp/auth.py +33 -0
- netgear_switch/protocols/nsdp/client.py +67 -0
- netgear_switch/protocols/nsdp/parsers.py +209 -0
- netgear_switch/protocols/nsdp/protocol.py +201 -0
- netgear_switch/protocols/nsdp/types.py +137 -0
- netgear_switch/protocols/nsdp/write.py +98 -0
- netgear_switch/protocols/snmp/__init__.py +1 -0
- netgear_switch/protocols/snmp/client.py +88 -0
- netgear_switch/protocols/snmp/oids.py +125 -0
- netgear_switch/protocols/snmp/parse.py +777 -0
- netgear_switch/protocols/snmp/write.py +112 -0
- netgear_switch/py.typed +0 -0
- netgear_switch/registry.py +227 -0
- netgear_switch/snmp_read.py +226 -0
- netgear_switch/snmp_write.py +625 -0
- netgear_switch/sync_api.py +557 -0
- netgear_switch/transport/__init__.py +1 -0
- netgear_switch/transport/aio/__init__.py +1 -0
- netgear_switch/transport/aio/nsdp_udp.py +152 -0
- netgear_switch/transport/aio/snmp_pysnmp.py +247 -0
- netgear_switch/transport/http/__init__.py +1 -0
- netgear_switch/transport/http/client.py +217 -0
- netgear_switch/transport/sync/__init__.py +1 -0
- netgear_switch/transport/sync/nsdp_udp.py +109 -0
- netgear_switch/transport/sync/snmp_netsnmp_cli.py +257 -0
- netgear_switch/virtual/__init__.py +8 -0
- netgear_switch/virtual/faces/__init__.py +2 -0
- netgear_switch/virtual/faces/http.py +164 -0
- netgear_switch/virtual/faces/mibview.py +92 -0
- netgear_switch/virtual/faces/nsdp.py +124 -0
- netgear_switch/virtual/faces/snmp.py +412 -0
- netgear_switch/virtual/seed.py +220 -0
- netgear_switch/virtual/server.py +106 -0
- netgear_switch/virtual/state.py +615 -0
- netgear_switch/virtual/web.py +210 -0
- python_netgear_switch_library-0.0.post154.dist-info/METADATA +85 -0
- python_netgear_switch_library-0.0.post154.dist-info/RECORD +66 -0
- python_netgear_switch_library-0.0.post154.dist-info/WHEEL +4 -0
- python_netgear_switch_library-0.0.post154.dist-info/entry_points.txt +2 -0
- python_netgear_switch_library-0.0.post154.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Hand-authored ``VirtualSwitchState`` seed for the GSM7252PS.
|
|
2
|
+
|
|
3
|
+
Every read op (Task 5-9) has at least one non-empty, non-vacuous example in
|
|
4
|
+
this seed: ports with link/admin/speed, RX/TX counters on >=2 ports, >=1
|
|
5
|
+
VLAN with egress/untagged bitmaps and PVIDs, PoE with a delivering port,
|
|
6
|
+
fan/temperature/PSU sensors (including a "Not Supported" fan slot), >=2
|
|
7
|
+
MAC/FDB entries with their bridge-port->ifIndex mappings, >=1 LLDP
|
|
8
|
+
neighbour, and a static management IP. This makes the Task 16 SNMP<->SNMP
|
|
9
|
+
equivalence test (and the round-trip tests here) exercise real data on every
|
|
10
|
+
parser, not an empty table. Port 1 also carries an ifAlias description (ports
|
|
11
|
+
2+ deliberately leave it unset) so the ifAlias column exercises both the
|
|
12
|
+
present and absent-instance paths. A sysDescr containing "GSM7252PS" plus a
|
|
13
|
+
placeholder sysObjectID (Task 2 model detection) round out the identity
|
|
14
|
+
signals -- see ``VirtualSwitchState.sys_descr``/``sys_object_id``.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from .state import (
|
|
19
|
+
LldpSim,
|
|
20
|
+
MacSim,
|
|
21
|
+
MgmtSim,
|
|
22
|
+
PoeSim,
|
|
23
|
+
PortSim,
|
|
24
|
+
SensorSim,
|
|
25
|
+
VirtualSwitchState,
|
|
26
|
+
VlanSim,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_POE_PORT_COUNT = 48
|
|
30
|
+
_TOTAL_PORT_COUNT = 52
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _port_name(port: int) -> str:
|
|
34
|
+
if port <= _POE_PORT_COUNT:
|
|
35
|
+
return f"1/0/{port}"
|
|
36
|
+
return f"1/xg{port - _POE_PORT_COUNT}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def seed_gsm7252ps() -> VirtualSwitchState:
|
|
40
|
+
"""Build a realistic GSM7252PS (52-port, 48-PoE) virtual switch state."""
|
|
41
|
+
ports: dict[int, PortSim] = {}
|
|
42
|
+
for port in range(1, _TOTAL_PORT_COUNT + 1):
|
|
43
|
+
sim = PortSim(
|
|
44
|
+
name=_port_name(port),
|
|
45
|
+
admin=True,
|
|
46
|
+
link=port != 3, # port 3 is admin-up but link-down
|
|
47
|
+
speed=1000,
|
|
48
|
+
)
|
|
49
|
+
if port in (1, 2):
|
|
50
|
+
sim.rx_octets = 1_000_000
|
|
51
|
+
sim.tx_octets = 2_000_000
|
|
52
|
+
sim.rx_ucast = 8_000
|
|
53
|
+
sim.tx_ucast = 9_000
|
|
54
|
+
sim.rx_errors = 0
|
|
55
|
+
sim.tx_errors = 0
|
|
56
|
+
if port == 1:
|
|
57
|
+
sim.description = "uplink-to-core" # port 2+ left None: absent ifAlias
|
|
58
|
+
ports[port] = sim
|
|
59
|
+
|
|
60
|
+
vlans = {
|
|
61
|
+
1: VlanSim(
|
|
62
|
+
name="default",
|
|
63
|
+
member=set(range(1, _TOTAL_PORT_COUNT + 1)),
|
|
64
|
+
untagged=set(range(3, _TOTAL_PORT_COUNT + 1)),
|
|
65
|
+
),
|
|
66
|
+
90: VlanSim(name="iot", member={1, 2, 10}, untagged={1, 2}),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pvids = dict.fromkeys(range(1, _TOTAL_PORT_COUNT + 1), 1)
|
|
70
|
+
pvids[1] = 90
|
|
71
|
+
pvids[2] = 90
|
|
72
|
+
|
|
73
|
+
poe: dict[int, PoeSim] = {}
|
|
74
|
+
for port in range(1, _POE_PORT_COUNT + 1):
|
|
75
|
+
if port == 1:
|
|
76
|
+
poe[port] = PoeSim(admin=True, detect=3, power_mw=12_800)
|
|
77
|
+
else:
|
|
78
|
+
poe[port] = PoeSim(admin=True, detect=1, power_mw=0)
|
|
79
|
+
|
|
80
|
+
sensors = [
|
|
81
|
+
SensorSim(kind="fan", instance="0", raw="3500"),
|
|
82
|
+
SensorSim(kind="fan", instance="1", raw="Not Supported"),
|
|
83
|
+
SensorSim(kind="fan", instance="2", raw="3450"),
|
|
84
|
+
SensorSim(kind="power", instance="0", raw="53"),
|
|
85
|
+
SensorSim(kind="temperature", instance="0", raw="45"),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
macs = [
|
|
89
|
+
MacSim(vlan=90, mac_bytes=(0xC8, 0x00, 0x84, 0x89, 0x71, 0x70), bridge_port=10),
|
|
90
|
+
MacSim(vlan=1, mac_bytes=(0x00, 0x1B, 0x21, 0x3C, 0x4D, 0x5E), bridge_port=11),
|
|
91
|
+
]
|
|
92
|
+
# bridge_port 10 deliberately maps to a DIFFERENT ifIndex (110, not 10) so
|
|
93
|
+
# a regression that drops the dot1dBasePortIfIndex join (or falls back to
|
|
94
|
+
# the bridge-port number itself) is detectable: get_macs() must surface
|
|
95
|
+
# the mapped ifIndex 110, never the bridge port 10. bridge_port 11 stays
|
|
96
|
+
# identity-mapped to prove the join also passes through unmapped/1:1 rows
|
|
97
|
+
# unchanged.
|
|
98
|
+
bridge_ports = {10: 110, 11: 11}
|
|
99
|
+
|
|
100
|
+
lldp = [
|
|
101
|
+
LldpSim(
|
|
102
|
+
time_mark=75,
|
|
103
|
+
local_port=49,
|
|
104
|
+
rem_idx=7,
|
|
105
|
+
chassis="".join(chr(b) for b in (0xC8, 0x00, 0x84, 0x89, 0x71, 0x70)),
|
|
106
|
+
port_id="1/xg51",
|
|
107
|
+
port_desc="eth0",
|
|
108
|
+
sys_name="sw-cisco-shed",
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
mgmt = MgmtSim(
|
|
113
|
+
address="10.1.5.20", netmask="255.255.255.0", gateway="10.1.5.1", mode="static"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return VirtualSwitchState(
|
|
117
|
+
model_key="gsm7252ps",
|
|
118
|
+
ports=ports,
|
|
119
|
+
vlans=vlans,
|
|
120
|
+
pvids=pvids,
|
|
121
|
+
poe=poe,
|
|
122
|
+
sensors=sensors,
|
|
123
|
+
macs=macs,
|
|
124
|
+
bridge_ports=bridge_ports,
|
|
125
|
+
lldp=lldp,
|
|
126
|
+
mgmt=mgmt,
|
|
127
|
+
# Illustrative sysDescr text -- NOT a captured real firmware string;
|
|
128
|
+
# its only requirement is containing the model name "GSM7252PS" so
|
|
129
|
+
# detect_model_from_sysdescr's string matching has something real to
|
|
130
|
+
# key off end-to-end. sys_object_id is a plausible-looking UNVERIFIED
|
|
131
|
+
# virtual/test placeholder under the model's own 4526.10 vendor
|
|
132
|
+
# subtree -- NOT a claim about the real device's sysObjectID (no
|
|
133
|
+
# capture of the real value exists).
|
|
134
|
+
sys_descr="NETGEAR GSM7252PS Managed Switch, firmware 8.0.6.6",
|
|
135
|
+
sys_object_id="1.3.6.1.4.1.4526.10.100.14",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def seed_gs110emx() -> VirtualSwitchState:
|
|
140
|
+
"""Build a realistic GS110EMX (10-port Plus, NSDP) virtual switch state."""
|
|
141
|
+
ports: dict[int, PortSim] = {}
|
|
142
|
+
for port in range(1, 11):
|
|
143
|
+
speed = 10000 if port in (9, 10) else 1000
|
|
144
|
+
sim = PortSim(
|
|
145
|
+
name=f"g{port}",
|
|
146
|
+
admin=True,
|
|
147
|
+
link=port != 3, # port 3 admin-up but link-down
|
|
148
|
+
speed=speed,
|
|
149
|
+
)
|
|
150
|
+
if port in (1, 2):
|
|
151
|
+
sim.rx_octets = 1_000_000
|
|
152
|
+
sim.tx_octets = 2_000_000
|
|
153
|
+
sim.rx_errors = 0
|
|
154
|
+
ports[port] = sim
|
|
155
|
+
|
|
156
|
+
vlans = {
|
|
157
|
+
1: VlanSim(name="", member=set(range(1, 11)), untagged=set(range(1, 11))),
|
|
158
|
+
90: VlanSim(name="", member={1, 2, 10}, untagged={1, 2}),
|
|
159
|
+
}
|
|
160
|
+
pvids = dict.fromkeys(range(1, 11), 1)
|
|
161
|
+
pvids[1] = 90
|
|
162
|
+
pvids[2] = 90
|
|
163
|
+
|
|
164
|
+
mgmt = MgmtSim(
|
|
165
|
+
address="10.1.5.20", netmask="255.255.255.0", gateway="10.1.5.1", mode="static"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return VirtualSwitchState(
|
|
169
|
+
model_key="gs110emx",
|
|
170
|
+
ports=ports,
|
|
171
|
+
vlans=vlans,
|
|
172
|
+
pvids=pvids,
|
|
173
|
+
mgmt=mgmt,
|
|
174
|
+
model_name="GS110EMX",
|
|
175
|
+
serial="53H6025EA0083",
|
|
176
|
+
firmware="1.0.0.7",
|
|
177
|
+
hostname="plus-sw",
|
|
178
|
+
nsdp_password="password",
|
|
179
|
+
# QoS/mirroring/IGMP/broadcast-filtering/loop-detection test fixtures
|
|
180
|
+
# (Slice 9b): illustrative, non-vacuous values so nsdp_device() has
|
|
181
|
+
# something real to decode on every one of the 5 newly-parsed tags.
|
|
182
|
+
nsdp_qos_engine=1, # port-based
|
|
183
|
+
nsdp_port_mirroring_dest=10,
|
|
184
|
+
nsdp_port_mirroring_sources=frozenset({1, 2}),
|
|
185
|
+
nsdp_igmp_snooping_enabled=True,
|
|
186
|
+
nsdp_igmp_snooping_vlan=90,
|
|
187
|
+
nsdp_broadcast_filtering=True,
|
|
188
|
+
nsdp_loop_detection=True,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def seed_gs305ep() -> VirtualSwitchState:
|
|
193
|
+
"""Build a realistic GS305EP (5-port, PoE ports 1-4) virtual switch state.
|
|
194
|
+
|
|
195
|
+
Plus family: no MAC/FDB, no box sensors, no LLDP (web UI exposes none).
|
|
196
|
+
Port 1 delivers PoE (12800 mW); VLAN 90 carries ports 1,2 (untagged 1,2).
|
|
197
|
+
"""
|
|
198
|
+
ports = {
|
|
199
|
+
p: PortSim(
|
|
200
|
+
name=f"Port {p}", admin=p != 3, link=p == 1, speed=1000 if p == 1 else 0
|
|
201
|
+
)
|
|
202
|
+
for p in range(1, 6)
|
|
203
|
+
}
|
|
204
|
+
ports[1].rx_octets = 1_000_000
|
|
205
|
+
ports[1].tx_octets = 2_000_000
|
|
206
|
+
ports[1].rx_errors = 0
|
|
207
|
+
vlans = {
|
|
208
|
+
1: VlanSim(name="default", member={1, 2, 3, 4, 5}, untagged={3, 4, 5}),
|
|
209
|
+
90: VlanSim(name="iot", member={1, 2}, untagged={1, 2}),
|
|
210
|
+
}
|
|
211
|
+
pvids = {1: 90, 2: 90, 3: 1, 4: 1, 5: 1}
|
|
212
|
+
poe = {
|
|
213
|
+
1: PoeSim(admin=True, detect=3, power_mw=12_800),
|
|
214
|
+
2: PoeSim(admin=True, detect=1, power_mw=0),
|
|
215
|
+
3: PoeSim(admin=True, detect=1, power_mw=0),
|
|
216
|
+
4: PoeSim(admin=False, detect=1, power_mw=0),
|
|
217
|
+
}
|
|
218
|
+
return VirtualSwitchState(
|
|
219
|
+
model_key="gs305ep", ports=ports, vlans=vlans, pvids=pvids, poe=poe
|
|
220
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# src/netgear_switch/virtual/server.py
|
|
2
|
+
"""``VirtualSwitch``: a mock switch server binding protocol faces to a state.
|
|
3
|
+
|
|
4
|
+
Constructed from a model key, it seeds (or defaults) a ``VirtualSwitchState``
|
|
5
|
+
and, on ``start()``, binds whichever protocol faces the model's registry
|
|
6
|
+
entry supports: SNMP for managed switches, NSDP and/or HTTP for Plus
|
|
7
|
+
switches. Each supported backend is bound in its own independent ``if``
|
|
8
|
+
block, so a ``{NSDP, HTTP}`` model binds both an NSDP face (``self.port``)
|
|
9
|
+
and an HTTP face (``self.http_port``) concurrently.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from ..errors import UnsupportedCapabilityError
|
|
14
|
+
from ..protocols.http.endpoints import http_spec
|
|
15
|
+
from ..registry import Backend, get_model
|
|
16
|
+
from .faces.http import VirtualHttpFace
|
|
17
|
+
from .faces.mibview import StateMibView
|
|
18
|
+
from .faces.nsdp import VirtualNsdpFace
|
|
19
|
+
from .faces.snmp import VirtualSnmpFace
|
|
20
|
+
from .seed import seed_gs110emx, seed_gs305ep, seed_gsm7252ps
|
|
21
|
+
from .state import VirtualSwitchState
|
|
22
|
+
|
|
23
|
+
# Model key -> hand-authored seed builder. Models without a seed here get a
|
|
24
|
+
# blank (but valid) VirtualSwitchState — every VirtualSwitchState field has a
|
|
25
|
+
# default, so construction never fails even for a model no one has seeded yet.
|
|
26
|
+
_SEEDS = {
|
|
27
|
+
"gsm7252ps": seed_gsm7252ps,
|
|
28
|
+
"gs110emx": seed_gs110emx,
|
|
29
|
+
"gs305ep": seed_gs305ep,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_state(model: str) -> VirtualSwitchState:
|
|
34
|
+
seed = _SEEDS.get(model)
|
|
35
|
+
return seed() if seed is not None else VirtualSwitchState(model_key=model)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VirtualSwitch:
|
|
39
|
+
"""A virtual switch server: a seeded state plus its bound protocol faces."""
|
|
40
|
+
|
|
41
|
+
host: str = "127.0.0.1"
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, model: str, community: str = "public", http_password: str = "password"
|
|
45
|
+
) -> None:
|
|
46
|
+
self._model_info = get_model(model) # raises UnknownModelError early
|
|
47
|
+
self.model = model
|
|
48
|
+
self.community = community
|
|
49
|
+
self.http_password = http_password
|
|
50
|
+
self.state: VirtualSwitchState = _build_state(model)
|
|
51
|
+
self.nsdp_password = self.state.nsdp_password
|
|
52
|
+
self.port: int = 0
|
|
53
|
+
self.http_port: int = 0
|
|
54
|
+
self._snmp_face: VirtualSnmpFace | None = None
|
|
55
|
+
self._nsdp_face: VirtualNsdpFace | None = None
|
|
56
|
+
self._http_face: VirtualHttpFace | None = None
|
|
57
|
+
|
|
58
|
+
def start(self) -> None:
|
|
59
|
+
"""Bind every protocol face this model's registry entry supports."""
|
|
60
|
+
if Backend.SNMP in self._model_info.backends:
|
|
61
|
+
view = StateMibView(self.state)
|
|
62
|
+
face = VirtualSnmpFace(view, community=self.community, host=self.host)
|
|
63
|
+
self.port = face.start()
|
|
64
|
+
self._snmp_face = face
|
|
65
|
+
if Backend.NSDP in self._model_info.backends:
|
|
66
|
+
nsdp_face = VirtualNsdpFace(self.state, host=self.host)
|
|
67
|
+
self.port = nsdp_face.start()
|
|
68
|
+
self._nsdp_face = nsdp_face
|
|
69
|
+
if Backend.HTTP in self._model_info.backends:
|
|
70
|
+
http_face = VirtualHttpFace(
|
|
71
|
+
self.state,
|
|
72
|
+
http_spec(self._model_info),
|
|
73
|
+
host=self.host,
|
|
74
|
+
password=self.http_password,
|
|
75
|
+
)
|
|
76
|
+
self.http_port = http_face.start()
|
|
77
|
+
self._http_face = http_face
|
|
78
|
+
if (
|
|
79
|
+
self._snmp_face is None
|
|
80
|
+
and self._nsdp_face is None
|
|
81
|
+
and self._http_face is None
|
|
82
|
+
):
|
|
83
|
+
raise UnsupportedCapabilityError(
|
|
84
|
+
f"model {self.model!r} has no bindable protocol face"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def stop(self) -> None:
|
|
88
|
+
"""Stop every bound face. Safe to call if start() failed or never ran."""
|
|
89
|
+
if self._snmp_face is not None:
|
|
90
|
+
self._snmp_face.stop()
|
|
91
|
+
self._snmp_face = None
|
|
92
|
+
if self._nsdp_face is not None:
|
|
93
|
+
self._nsdp_face.stop()
|
|
94
|
+
self._nsdp_face = None
|
|
95
|
+
if self._http_face is not None:
|
|
96
|
+
self._http_face.stop()
|
|
97
|
+
self._http_face = None
|
|
98
|
+
self.port = 0
|
|
99
|
+
self.http_port = 0
|
|
100
|
+
|
|
101
|
+
def __enter__(self) -> VirtualSwitch:
|
|
102
|
+
self.start()
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
106
|
+
self.stop()
|