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.
Files changed (66) hide show
  1. netgear_switch/__init__.py +132 -0
  2. netgear_switch/_dispatch.py +178 -0
  3. netgear_switch/_version.py +24 -0
  4. netgear_switch/aio_api.py +529 -0
  5. netgear_switch/cli/__init__.py +1 -0
  6. netgear_switch/cli/capture.py +131 -0
  7. netgear_switch/cli/context.py +39 -0
  8. netgear_switch/cli/format.py +201 -0
  9. netgear_switch/cli/main.py +484 -0
  10. netgear_switch/cli/resolve.py +108 -0
  11. netgear_switch/cli/safety.py +71 -0
  12. netgear_switch/config.py +184 -0
  13. netgear_switch/errors.py +52 -0
  14. netgear_switch/http_read.py +174 -0
  15. netgear_switch/http_write.py +420 -0
  16. netgear_switch/models.py +156 -0
  17. netgear_switch/nsdp_read.py +221 -0
  18. netgear_switch/nsdp_write.py +315 -0
  19. netgear_switch/protocols/__init__.py +1 -0
  20. netgear_switch/protocols/http/__init__.py +1 -0
  21. netgear_switch/protocols/http/crypt.py +29 -0
  22. netgear_switch/protocols/http/endpoints.py +165 -0
  23. netgear_switch/protocols/http/forms.py +77 -0
  24. netgear_switch/protocols/http/parse.py +238 -0
  25. netgear_switch/protocols/http/session.py +29 -0
  26. netgear_switch/protocols/nsdp/__init__.py +7 -0
  27. netgear_switch/protocols/nsdp/auth.py +33 -0
  28. netgear_switch/protocols/nsdp/client.py +67 -0
  29. netgear_switch/protocols/nsdp/parsers.py +209 -0
  30. netgear_switch/protocols/nsdp/protocol.py +201 -0
  31. netgear_switch/protocols/nsdp/types.py +137 -0
  32. netgear_switch/protocols/nsdp/write.py +98 -0
  33. netgear_switch/protocols/snmp/__init__.py +1 -0
  34. netgear_switch/protocols/snmp/client.py +88 -0
  35. netgear_switch/protocols/snmp/oids.py +125 -0
  36. netgear_switch/protocols/snmp/parse.py +777 -0
  37. netgear_switch/protocols/snmp/write.py +112 -0
  38. netgear_switch/py.typed +0 -0
  39. netgear_switch/registry.py +227 -0
  40. netgear_switch/snmp_read.py +226 -0
  41. netgear_switch/snmp_write.py +625 -0
  42. netgear_switch/sync_api.py +557 -0
  43. netgear_switch/transport/__init__.py +1 -0
  44. netgear_switch/transport/aio/__init__.py +1 -0
  45. netgear_switch/transport/aio/nsdp_udp.py +152 -0
  46. netgear_switch/transport/aio/snmp_pysnmp.py +247 -0
  47. netgear_switch/transport/http/__init__.py +1 -0
  48. netgear_switch/transport/http/client.py +217 -0
  49. netgear_switch/transport/sync/__init__.py +1 -0
  50. netgear_switch/transport/sync/nsdp_udp.py +109 -0
  51. netgear_switch/transport/sync/snmp_netsnmp_cli.py +257 -0
  52. netgear_switch/virtual/__init__.py +8 -0
  53. netgear_switch/virtual/faces/__init__.py +2 -0
  54. netgear_switch/virtual/faces/http.py +164 -0
  55. netgear_switch/virtual/faces/mibview.py +92 -0
  56. netgear_switch/virtual/faces/nsdp.py +124 -0
  57. netgear_switch/virtual/faces/snmp.py +412 -0
  58. netgear_switch/virtual/seed.py +220 -0
  59. netgear_switch/virtual/server.py +106 -0
  60. netgear_switch/virtual/state.py +615 -0
  61. netgear_switch/virtual/web.py +210 -0
  62. python_netgear_switch_library-0.0.post154.dist-info/METADATA +85 -0
  63. python_netgear_switch_library-0.0.post154.dist-info/RECORD +66 -0
  64. python_netgear_switch_library-0.0.post154.dist-info/WHEEL +4 -0
  65. python_netgear_switch_library-0.0.post154.dist-info/entry_points.txt +2 -0
  66. 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()