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,209 @@
|
|
|
1
|
+
"""Per-tag NSDP byte parsers. Lifted from ``gdoc2netcfg/src/nsdp/parsers.py``,
|
|
2
|
+
plus ``ports_to_bitmap`` (the write-path inverse of ``bitmap_to_ports``).
|
|
3
|
+
|
|
4
|
+
Every parser is total over the bytes it accepts and raises ``ValueError`` on a
|
|
5
|
+
wrong length or a bad prefix, so a malformed TLV surfaces early rather than
|
|
6
|
+
producing a silently-wrong value.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import socket
|
|
11
|
+
import struct
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from .protocol import Tag
|
|
15
|
+
from .types import (
|
|
16
|
+
LinkSpeed,
|
|
17
|
+
NsdpDevice,
|
|
18
|
+
NsdpIgmpSnooping,
|
|
19
|
+
NsdpPortMirroring,
|
|
20
|
+
NsdpPortPvid,
|
|
21
|
+
NsdpPortStatistics,
|
|
22
|
+
NsdpPortStatus,
|
|
23
|
+
NsdpVlanMembership,
|
|
24
|
+
VLANEngine,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from collections.abc import Iterable
|
|
29
|
+
|
|
30
|
+
from .protocol import NSDPPacket
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_ipv4(data: bytes) -> str:
|
|
34
|
+
if len(data) != 4:
|
|
35
|
+
raise ValueError(f"IPv4 TLV must be 4 bytes, got {len(data)}")
|
|
36
|
+
return socket.inet_ntoa(data)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_mac(data: bytes) -> str:
|
|
40
|
+
if len(data) != 6:
|
|
41
|
+
raise ValueError(f"MAC TLV must be 6 bytes, got {len(data)}")
|
|
42
|
+
return ":".join(f"{b:02x}" for b in data)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_port_status(data: bytes) -> NsdpPortStatus:
|
|
46
|
+
if len(data) != 3:
|
|
47
|
+
raise ValueError(f"PORT_STATUS TLV must be 3 bytes, got {len(data)}")
|
|
48
|
+
return NsdpPortStatus(port_id=data[0], speed=LinkSpeed.from_byte(data[1]))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_port_statistics(data: bytes) -> NsdpPortStatistics:
|
|
52
|
+
if len(data) != 49:
|
|
53
|
+
raise ValueError(f"PORT_STATISTICS TLV must be 49 bytes, got {len(data)}")
|
|
54
|
+
rx, tx, crc = struct.unpack_from(">QQQ", data, 1)
|
|
55
|
+
return NsdpPortStatistics(
|
|
56
|
+
port_id=data[0], bytes_received=rx, bytes_sent=tx, crc_errors=crc
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_port_pvid(data: bytes) -> NsdpPortPvid:
|
|
61
|
+
if len(data) != 3:
|
|
62
|
+
raise ValueError(f"PORT_PVID TLV must be 3 bytes, got {len(data)}")
|
|
63
|
+
return NsdpPortPvid(port_id=data[0], vlan_id=struct.unpack_from(">H", data, 1)[0])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_serial(data: bytes) -> str:
|
|
67
|
+
if not data or data[0] != 0x01:
|
|
68
|
+
raise ValueError(f"SERIAL_NUMBER TLV: unexpected prefix byte {data[:1]!r}")
|
|
69
|
+
return data[1:].decode("ascii", errors="replace").rstrip("\x00")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def bitmap_to_ports(bitmap: bytes) -> frozenset[int]:
|
|
73
|
+
"""MSB-first, 1-based: byte 0 bit 0x80 = port 1 ... 0x01 = port 8."""
|
|
74
|
+
ports: set[int] = set()
|
|
75
|
+
for byte_idx, byte_val in enumerate(bitmap):
|
|
76
|
+
for bit in range(8):
|
|
77
|
+
if byte_val & (0x80 >> bit):
|
|
78
|
+
ports.add(byte_idx * 8 + bit + 1)
|
|
79
|
+
return frozenset(ports)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def ports_to_bitmap(ports: Iterable[int], width_bytes: int) -> bytes:
|
|
83
|
+
"""Inverse of ``bitmap_to_ports`` for the write path (same MSB-first layout)."""
|
|
84
|
+
data = bytearray(width_bytes)
|
|
85
|
+
for p in ports:
|
|
86
|
+
byte_idx, bit = divmod(p - 1, 8)
|
|
87
|
+
while byte_idx >= len(data):
|
|
88
|
+
data.append(0)
|
|
89
|
+
data[byte_idx] |= 0x80 >> bit
|
|
90
|
+
return bytes(data)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_vlan_members(data: bytes, port_count: int = 8) -> NsdpVlanMembership:
|
|
94
|
+
bitmap_bytes = (port_count + 7) // 8
|
|
95
|
+
expected = 2 + bitmap_bytes * 2
|
|
96
|
+
if len(data) < expected:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"VLAN_MEMBERS TLV must be >={expected} bytes for {port_count} ports, "
|
|
99
|
+
f"got {len(data)}"
|
|
100
|
+
)
|
|
101
|
+
vlan_id = struct.unpack_from(">H", data, 0)[0]
|
|
102
|
+
member = data[2 : 2 + bitmap_bytes]
|
|
103
|
+
tagged = data[2 + bitmap_bytes : 2 + bitmap_bytes * 2]
|
|
104
|
+
return NsdpVlanMembership(
|
|
105
|
+
vlan_id=vlan_id,
|
|
106
|
+
member_ports=bitmap_to_ports(member),
|
|
107
|
+
tagged_ports=bitmap_to_ports(tagged),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _decode_str(data: bytes) -> str:
|
|
112
|
+
return data.decode("ascii", errors="replace").rstrip("\x00")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def parse_port_mirroring(data: bytes) -> NsdpPortMirroring:
|
|
116
|
+
"""Parse NSDP tag 0x5C00 (4 bytes: dest_port(1) + source bitmap(3)).
|
|
117
|
+
|
|
118
|
+
Lifted from ``gdoc2netcfg/src/nsdp/parsers.py::parse_port_mirroring``.
|
|
119
|
+
"""
|
|
120
|
+
if len(data) != 4:
|
|
121
|
+
raise ValueError(f"PORT_MIRRORING TLV must be 4 bytes, got {len(data)}")
|
|
122
|
+
dest_port = data[0]
|
|
123
|
+
# Bytes 1-3 are the source-port bitmap (MSB first).
|
|
124
|
+
source_ports = bitmap_to_ports(data[1:4])
|
|
125
|
+
return NsdpPortMirroring(destination_port=dest_port, source_ports=source_ports)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_igmp_snooping(data: bytes) -> NsdpIgmpSnooping:
|
|
129
|
+
"""Parse NSDP tag 0x6800 (>=2 bytes: unknown, enabled, [unknown, vlan?]).
|
|
130
|
+
|
|
131
|
+
Lifted from ``gdoc2netcfg/src/nsdp/parsers.py::parse_igmp_snooping``.
|
|
132
|
+
"""
|
|
133
|
+
if len(data) < 2:
|
|
134
|
+
raise ValueError(f"IGMP_SNOOPING TLV must be >= 2 bytes, got {len(data)}")
|
|
135
|
+
enabled = bool(data[1])
|
|
136
|
+
vlan_id = None
|
|
137
|
+
if len(data) >= 4:
|
|
138
|
+
vlan_id = data[3] if data[3] != 0 else None
|
|
139
|
+
return NsdpIgmpSnooping(enabled=enabled, vlan_id=vlan_id)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_device(packet: NSDPPacket) -> NsdpDevice:
|
|
143
|
+
"""Aggregate a READ_RESPONSE packet's TLVs into an NsdpDevice."""
|
|
144
|
+
model: str | None = None
|
|
145
|
+
mac: str | None = None
|
|
146
|
+
fields: dict[str, object] = {}
|
|
147
|
+
port_status: list[NsdpPortStatus] = []
|
|
148
|
+
port_stats: list[NsdpPortStatistics] = []
|
|
149
|
+
vlan_members: list[NsdpVlanMembership] = []
|
|
150
|
+
pvids: list[NsdpPortPvid] = []
|
|
151
|
+
port_count = 8
|
|
152
|
+
# First pass to learn the real port count (bitmaps need it).
|
|
153
|
+
for tlv in packet.tlvs:
|
|
154
|
+
if tlv.tag == Tag.PORT_COUNT and tlv.value:
|
|
155
|
+
port_count = tlv.value[0]
|
|
156
|
+
for tlv in packet.tlvs:
|
|
157
|
+
if tlv.tag == Tag.MODEL:
|
|
158
|
+
model = _decode_str(tlv.value)
|
|
159
|
+
elif tlv.tag == Tag.MAC:
|
|
160
|
+
mac = parse_mac(tlv.value)
|
|
161
|
+
elif tlv.tag == Tag.HOSTNAME:
|
|
162
|
+
fields["hostname"] = _decode_str(tlv.value)
|
|
163
|
+
elif tlv.tag == Tag.IP_ADDRESS:
|
|
164
|
+
fields["ip"] = parse_ipv4(tlv.value)
|
|
165
|
+
elif tlv.tag == Tag.NETMASK:
|
|
166
|
+
fields["netmask"] = parse_ipv4(tlv.value)
|
|
167
|
+
elif tlv.tag == Tag.GATEWAY:
|
|
168
|
+
fields["gateway"] = parse_ipv4(tlv.value)
|
|
169
|
+
elif tlv.tag == Tag.FIRMWARE_VER_1:
|
|
170
|
+
fields["firmware_version"] = _decode_str(tlv.value)
|
|
171
|
+
elif tlv.tag == Tag.DHCP_MODE and tlv.value:
|
|
172
|
+
fields["dhcp_enabled"] = bool(tlv.value[0])
|
|
173
|
+
elif tlv.tag == Tag.PORT_COUNT and tlv.value:
|
|
174
|
+
fields["port_count"] = tlv.value[0]
|
|
175
|
+
elif tlv.tag == Tag.SERIAL_NUMBER:
|
|
176
|
+
fields["serial_number"] = parse_serial(tlv.value)
|
|
177
|
+
elif tlv.tag == Tag.VLAN_ENGINE and tlv.value:
|
|
178
|
+
fields["vlan_engine"] = VLANEngine(tlv.value[0])
|
|
179
|
+
elif tlv.tag == Tag.PORT_STATUS:
|
|
180
|
+
port_status.append(parse_port_status(tlv.value))
|
|
181
|
+
elif tlv.tag == Tag.PORT_STATISTICS:
|
|
182
|
+
port_stats.append(parse_port_statistics(tlv.value))
|
|
183
|
+
elif tlv.tag == Tag.VLAN_MEMBERS:
|
|
184
|
+
vlan_members.append(parse_vlan_members(tlv.value, port_count))
|
|
185
|
+
elif tlv.tag == Tag.PORT_PVID:
|
|
186
|
+
pvids.append(parse_port_pvid(tlv.value))
|
|
187
|
+
elif tlv.tag == Tag.QOS_ENGINE and tlv.value:
|
|
188
|
+
fields["qos_engine"] = tlv.value[0]
|
|
189
|
+
elif tlv.tag == Tag.PORT_MIRRORING:
|
|
190
|
+
fields["port_mirroring"] = parse_port_mirroring(tlv.value)
|
|
191
|
+
elif tlv.tag == Tag.IGMP_SNOOPING:
|
|
192
|
+
fields["igmp_snooping"] = parse_igmp_snooping(tlv.value)
|
|
193
|
+
elif tlv.tag == Tag.BROADCAST_FILTERING and tlv.value:
|
|
194
|
+
fields["broadcast_filtering"] = bool(tlv.value[0])
|
|
195
|
+
elif tlv.tag == Tag.LOOP_DETECTION and tlv.value:
|
|
196
|
+
fields["loop_detection"] = bool(tlv.value[0])
|
|
197
|
+
if model is None:
|
|
198
|
+
raise ValueError("no MODEL tag in NSDP response")
|
|
199
|
+
if mac is None:
|
|
200
|
+
mac = parse_mac(packet.server_mac)
|
|
201
|
+
return NsdpDevice(
|
|
202
|
+
model=model,
|
|
203
|
+
mac=mac,
|
|
204
|
+
port_status=tuple(port_status),
|
|
205
|
+
port_statistics=tuple(port_stats),
|
|
206
|
+
vlan_members=tuple(vlan_members),
|
|
207
|
+
port_pvids=tuple(pvids),
|
|
208
|
+
**fields, # type: ignore[arg-type]
|
|
209
|
+
)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""NSDP wire codec: 32-byte header, TLV entries, and packet encode/decode.
|
|
2
|
+
|
|
3
|
+
Lifted verbatim (field-for-field) from ``gdoc2netcfg/src/nsdp/protocol.py``.
|
|
4
|
+
The header is ``struct`` layout ``>BB H 4s 6s 6s HH 4s 4s`` (32 bytes): version
|
|
5
|
+
(always 0x01), operation, result, reserved(4), client MAC(6), server MAC(6),
|
|
6
|
+
reserved(2), sequence, signature ``b"NSDP"`` at offset 0x18, reserved(4). Each
|
|
7
|
+
TLV is ``>HH`` (tag, length) followed by ``length`` value bytes; a packet ends
|
|
8
|
+
with the ``0xFFFF 0x0000`` end-of-marker.
|
|
9
|
+
|
|
10
|
+
This module is a pure, zero-dependency codec: no sockets, no I/O. The write
|
|
11
|
+
path (``Op.WRITE_REQUEST``/``Op.WRITE_RESPONSE`` and ``NSDPPacket.add_tlv``
|
|
12
|
+
with a non-empty value) is new relative to the read-only prior-art client,
|
|
13
|
+
but uses the exact same wire layout.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from enum import IntEnum
|
|
20
|
+
|
|
21
|
+
NSDP_SIGNATURE = b"NSDP"
|
|
22
|
+
HEADER_SIZE = 32
|
|
23
|
+
HEADER_FORMAT = ">BB H 4s 6s 6s HH 4s 4s"
|
|
24
|
+
END_MARKER = struct.pack(">HH", 0xFFFF, 0x0000) # b"\xff\xff\x00\x00"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Op(IntEnum):
|
|
28
|
+
"""NSDP operation codes (header byte 1).
|
|
29
|
+
|
|
30
|
+
READ_REQUEST/RESPONSE are used for discovery and property queries.
|
|
31
|
+
WRITE_REQUEST/RESPONSE are used to modify switch configuration
|
|
32
|
+
(requires authentication via Tag.PASSWORD or Tag.AUTH_V2_PASSWORD).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
READ_REQUEST = 0x01
|
|
36
|
+
READ_RESPONSE = 0x02
|
|
37
|
+
WRITE_REQUEST = 0x03
|
|
38
|
+
WRITE_RESPONSE = 0x04
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Tag(IntEnum):
|
|
42
|
+
"""NSDP TLV tag identifiers.
|
|
43
|
+
|
|
44
|
+
Each tag represents a switch property. Tags are 16-bit unsigned integers
|
|
45
|
+
encoded big-endian in the packet. See
|
|
46
|
+
``gdoc2netcfg/docs/nsdp-protocol.md`` (TLV Tag Registry) for byte-level
|
|
47
|
+
encoding details of each tag's value field.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
# Packet markers
|
|
51
|
+
START_OF_MARK = 0x0000
|
|
52
|
+
END_OF_MARK = 0xFFFF
|
|
53
|
+
|
|
54
|
+
# Device identity
|
|
55
|
+
MODEL = 0x0001
|
|
56
|
+
HOSTNAME = 0x0003
|
|
57
|
+
MAC = 0x0004
|
|
58
|
+
LOCATION = 0x0005
|
|
59
|
+
IP_ADDRESS = 0x0006
|
|
60
|
+
NETMASK = 0x0007
|
|
61
|
+
GATEWAY = 0x0008
|
|
62
|
+
DHCP_MODE = 0x000B
|
|
63
|
+
FIRMWARE_VER_1 = 0x000D
|
|
64
|
+
FIRMWARE_VER_2 = 0x000E
|
|
65
|
+
PORT_COUNT = 0x6000
|
|
66
|
+
SERIAL_NUMBER = 0x7800
|
|
67
|
+
|
|
68
|
+
# Authentication
|
|
69
|
+
PASSWORD = 0x000A
|
|
70
|
+
AUTH_V2_SALT = 0x0017
|
|
71
|
+
AUTH_V2_PASSWORD = 0x001A
|
|
72
|
+
|
|
73
|
+
# Port information
|
|
74
|
+
PORT_STATUS = 0x0C00
|
|
75
|
+
PORT_STATISTICS = 0x1000
|
|
76
|
+
|
|
77
|
+
# VLAN
|
|
78
|
+
VLAN_ENGINE = 0x2000
|
|
79
|
+
VLAN_MEMBERS = 0x2800
|
|
80
|
+
PORT_PVID = 0x3000
|
|
81
|
+
|
|
82
|
+
# QoS
|
|
83
|
+
QOS_ENGINE = 0x3400
|
|
84
|
+
PORT_QOS_PRIORITY = 0x3800
|
|
85
|
+
|
|
86
|
+
# Traffic control
|
|
87
|
+
INGRESS_RATE_LIMIT = 0x4C00
|
|
88
|
+
EGRESS_RATE_LIMIT = 0x5000
|
|
89
|
+
BROADCAST_FILTERING = 0x5400
|
|
90
|
+
BROADCAST_BANDWIDTH = 0x5800
|
|
91
|
+
PORT_MIRRORING = 0x5C00
|
|
92
|
+
|
|
93
|
+
# IGMP
|
|
94
|
+
IGMP_SNOOPING = 0x6800
|
|
95
|
+
BLOCK_UNKNOWN_MULTICAST = 0x6C00
|
|
96
|
+
IGMPV3_HEADER_VALIDATION = 0x7000
|
|
97
|
+
IGMP_STATIC_ROUTER_PORTS = 0x8000
|
|
98
|
+
|
|
99
|
+
# Other
|
|
100
|
+
LOOP_DETECTION = 0x9000
|
|
101
|
+
ACTIVE_FIRMWARE = 0x000C
|
|
102
|
+
|
|
103
|
+
# Actions (write-only)
|
|
104
|
+
REBOOT = 0x0013
|
|
105
|
+
FACTORY_RESET = 0x0400
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class TLVEntry:
|
|
110
|
+
"""One NSDP TLV: a 2-byte tag, 2-byte length, then that many value bytes."""
|
|
111
|
+
|
|
112
|
+
tag: Tag | int
|
|
113
|
+
value: bytes = b""
|
|
114
|
+
|
|
115
|
+
def encode(self) -> bytes:
|
|
116
|
+
return struct.pack(">HH", int(self.tag), len(self.value)) + self.value
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def decode(cls, data: bytes) -> tuple[TLVEntry, int]:
|
|
120
|
+
if len(data) < 4:
|
|
121
|
+
raise ValueError("NSDP TLV shorter than its 4-byte header")
|
|
122
|
+
tag_raw, length = struct.unpack_from(">HH", data, 0)
|
|
123
|
+
if len(data) < 4 + length:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"NSDP TLV declares {length} value bytes but only "
|
|
126
|
+
f"{len(data) - 4} are present"
|
|
127
|
+
)
|
|
128
|
+
value = data[4 : 4 + length]
|
|
129
|
+
tag: Tag | int
|
|
130
|
+
try:
|
|
131
|
+
tag = Tag(tag_raw)
|
|
132
|
+
except ValueError:
|
|
133
|
+
tag = tag_raw # unknown/uncatalogued tag: keep the raw int
|
|
134
|
+
return cls(tag=tag, value=value), 4 + length
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class NSDPPacket:
|
|
139
|
+
"""A full NSDP datagram: a fixed header plus a list of TLVs."""
|
|
140
|
+
|
|
141
|
+
op: Op
|
|
142
|
+
client_mac: bytes
|
|
143
|
+
server_mac: bytes = b"\x00" * 6
|
|
144
|
+
sequence: int = 0
|
|
145
|
+
result: int = 0
|
|
146
|
+
tlvs: list[TLVEntry] = field(default_factory=list)
|
|
147
|
+
|
|
148
|
+
def add_tlv(self, tag: Tag | int, value: bytes = b"") -> None:
|
|
149
|
+
self.tlvs.append(TLVEntry(tag=tag, value=value))
|
|
150
|
+
|
|
151
|
+
def encode(self) -> bytes:
|
|
152
|
+
header = struct.pack(
|
|
153
|
+
HEADER_FORMAT,
|
|
154
|
+
0x01,
|
|
155
|
+
int(self.op),
|
|
156
|
+
self.result,
|
|
157
|
+
b"\x00" * 4,
|
|
158
|
+
self.client_mac,
|
|
159
|
+
self.server_mac,
|
|
160
|
+
0,
|
|
161
|
+
self.sequence,
|
|
162
|
+
NSDP_SIGNATURE,
|
|
163
|
+
b"\x00" * 4,
|
|
164
|
+
)
|
|
165
|
+
body = b"".join(t.encode() for t in self.tlvs)
|
|
166
|
+
return header + body + END_MARKER
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def decode(cls, data: bytes) -> NSDPPacket:
|
|
170
|
+
if len(data) < HEADER_SIZE:
|
|
171
|
+
raise ValueError(f"NSDP packet shorter than {HEADER_SIZE}-byte header")
|
|
172
|
+
(
|
|
173
|
+
_version,
|
|
174
|
+
op_raw,
|
|
175
|
+
result,
|
|
176
|
+
_reserved1,
|
|
177
|
+
client_mac,
|
|
178
|
+
server_mac,
|
|
179
|
+
_reserved2,
|
|
180
|
+
sequence,
|
|
181
|
+
signature,
|
|
182
|
+
_reserved3,
|
|
183
|
+
) = struct.unpack(HEADER_FORMAT, data[:HEADER_SIZE])
|
|
184
|
+
if signature != NSDP_SIGNATURE:
|
|
185
|
+
raise ValueError(f"bad NSDP signature {signature!r}")
|
|
186
|
+
tlvs: list[TLVEntry] = []
|
|
187
|
+
offset = HEADER_SIZE
|
|
188
|
+
while offset + 4 <= len(data):
|
|
189
|
+
entry, consumed = TLVEntry.decode(data[offset:])
|
|
190
|
+
if entry.tag == Tag.END_OF_MARK:
|
|
191
|
+
break
|
|
192
|
+
tlvs.append(entry)
|
|
193
|
+
offset += consumed
|
|
194
|
+
return cls(
|
|
195
|
+
op=Op(op_raw),
|
|
196
|
+
client_mac=client_mac,
|
|
197
|
+
server_mac=server_mac,
|
|
198
|
+
sequence=sequence,
|
|
199
|
+
result=result,
|
|
200
|
+
tlvs=tlvs,
|
|
201
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""NSDP-native parsed value types. Lifted from ``gdoc2netcfg/src/nsdp/types.py``.
|
|
2
|
+
|
|
3
|
+
These are the raw protocol shapes the parsers return; ``nsdp_read.py`` maps them
|
|
4
|
+
onto the shared ``models.py`` types. Named with an ``Nsdp`` prefix so they never
|
|
5
|
+
collide with the public ``models`` dataclasses.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import IntEnum
|
|
11
|
+
|
|
12
|
+
_MBPS = {
|
|
13
|
+
0x00: 0, 0x01: 10, 0x02: 10, 0x03: 100, 0x04: 100, 0x05: 1000, 0xFF: 10000,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LinkSpeed(IntEnum):
|
|
18
|
+
DOWN = 0x00
|
|
19
|
+
HALF_10M = 0x01
|
|
20
|
+
FULL_10M = 0x02
|
|
21
|
+
HALF_100M = 0x03
|
|
22
|
+
FULL_100M = 0x04
|
|
23
|
+
GIGABIT = 0x05
|
|
24
|
+
# ASSUMED/UNVERIFIED — the reference spec states 2.5G/5G/10G speed byte
|
|
25
|
+
# values are undocumented and require a hardware capture; 0xFF is carried
|
|
26
|
+
# over from prior art without independent confirmation.
|
|
27
|
+
TEN_GIGABIT = 0xFF
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_byte(cls, value: int) -> LinkSpeed:
|
|
31
|
+
try:
|
|
32
|
+
return cls(value)
|
|
33
|
+
except ValueError:
|
|
34
|
+
return cls.DOWN # unknown 2.5G/5G codes: report DOWN, never raise
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def speed_mbps(self) -> int:
|
|
38
|
+
return _MBPS.get(int(self), 0)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VLANEngine(IntEnum):
|
|
42
|
+
DISABLED = 0
|
|
43
|
+
BASIC_PORT = 1
|
|
44
|
+
ADVANCED_PORT = 2
|
|
45
|
+
BASIC_802_1Q = 3
|
|
46
|
+
ADVANCED_802_1Q = 4
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class NsdpPortStatus:
|
|
51
|
+
port_id: int
|
|
52
|
+
speed: LinkSpeed
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class NsdpPortStatistics:
|
|
57
|
+
port_id: int
|
|
58
|
+
bytes_received: int
|
|
59
|
+
bytes_sent: int
|
|
60
|
+
crc_errors: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class NsdpVlanMembership:
|
|
65
|
+
vlan_id: int
|
|
66
|
+
member_ports: frozenset[int]
|
|
67
|
+
tagged_ports: frozenset[int] = frozenset()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def untagged_ports(self) -> frozenset[int]:
|
|
71
|
+
return self.member_ports - self.tagged_ports
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class NsdpPortPvid:
|
|
76
|
+
port_id: int
|
|
77
|
+
vlan_id: int
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class NsdpPortMirroring:
|
|
82
|
+
"""Port mirroring configuration (NSDP tag 0x5C00).
|
|
83
|
+
|
|
84
|
+
Lifted from ``gdoc2netcfg/src/nsdp/types.py::PortMirroring``.
|
|
85
|
+
|
|
86
|
+
Attributes:
|
|
87
|
+
destination_port: Port receiving mirrored traffic (0 = disabled).
|
|
88
|
+
source_ports: Ports being mirrored.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
destination_port: int
|
|
92
|
+
source_ports: frozenset[int] = frozenset()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class NsdpIgmpSnooping:
|
|
97
|
+
"""IGMP snooping configuration (NSDP tag 0x6800).
|
|
98
|
+
|
|
99
|
+
Lifted from ``gdoc2netcfg/src/nsdp/types.py::IGMPSnooping``.
|
|
100
|
+
|
|
101
|
+
Attributes:
|
|
102
|
+
enabled: Whether IGMP snooping is enabled.
|
|
103
|
+
vlan_id: VLAN for IGMP snooping (if applicable); ``None`` when the
|
|
104
|
+
wire value is 0 (no VLAN association).
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
enabled: bool
|
|
108
|
+
vlan_id: int | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class NsdpDevice:
|
|
113
|
+
model: str
|
|
114
|
+
mac: str
|
|
115
|
+
hostname: str | None = None
|
|
116
|
+
ip: str | None = None
|
|
117
|
+
netmask: str | None = None
|
|
118
|
+
gateway: str | None = None
|
|
119
|
+
firmware_version: str | None = None
|
|
120
|
+
dhcp_enabled: bool | None = None
|
|
121
|
+
port_count: int | None = None
|
|
122
|
+
serial_number: str | None = None
|
|
123
|
+
vlan_engine: VLANEngine | None = None
|
|
124
|
+
port_status: tuple[NsdpPortStatus, ...] = ()
|
|
125
|
+
port_statistics: tuple[NsdpPortStatistics, ...] = ()
|
|
126
|
+
vlan_members: tuple[NsdpVlanMembership, ...] = ()
|
|
127
|
+
port_pvids: tuple[NsdpPortPvid, ...] = field(default_factory=tuple)
|
|
128
|
+
# QoS engine mode (tag 0x3400): 0=disabled, 1=port-based, 2=802.1p.
|
|
129
|
+
qos_engine: int | None = None
|
|
130
|
+
# Port mirroring configuration (tag 0x5C00).
|
|
131
|
+
port_mirroring: NsdpPortMirroring | None = None
|
|
132
|
+
# IGMP snooping configuration (tag 0x6800).
|
|
133
|
+
igmp_snooping: NsdpIgmpSnooping | None = None
|
|
134
|
+
# Whether broadcast storm filtering is enabled (tag 0x5400).
|
|
135
|
+
broadcast_filtering: bool | None = None
|
|
136
|
+
# Whether loop detection is enabled (tag 0x9000).
|
|
137
|
+
loop_detection: bool | None = None
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""NSDP request framing + value-TLV encoders for the read and write paths.
|
|
2
|
+
|
|
3
|
+
Pure: builds ``NSDPPacket`` objects, no I/O. The write path (absent from the
|
|
4
|
+
lifted ``gdoc2netcfg`` package, which was read-only) prepends a v1 ``PASSWORD``
|
|
5
|
+
TLV — a real switch rejects an unauthenticated or wrongly-authenticated write
|
|
6
|
+
with result 0x0700, which the transport turns into an ``NsdpError`` (Task 4).
|
|
7
|
+
|
|
8
|
+
UNVERIFIED write path (mirrors ``snmp_write.py``'s house style for its
|
|
9
|
+
mgmt_write_* OIDs). This entire NSDP write path — the WRITE_REQUEST value-TLV
|
|
10
|
+
encodings here plus the v1 XOR auth in ``auth.py`` — is a from-scratch addition
|
|
11
|
+
with ZERO verification against real hardware: the lifted ``gdoc2netcfg/src/nsdp``
|
|
12
|
+
prior art is READ-ONLY, so nothing in it exercises writes. It stays UNVERIFIED
|
|
13
|
+
pending a real capture (Slice 7 capture utility / a real-hardware run);
|
|
14
|
+
verify-after-write in ``nsdp_write.py`` is the runtime guard against a silently
|
|
15
|
+
wrong encoding. Critically, the reference spec
|
|
16
|
+
(``gdoc2netcfg/docs/nsdp-protocol.md``) marks ``PORT_PVID`` (0x3000) and
|
|
17
|
+
``VLAN_MEMBERS`` (0x2800) as READ-ONLY (R), unlike hostname/ip/netmask/gateway/
|
|
18
|
+
dhcp_mode/vlan_engine (R/W). Writing PVID/VLAN membership via NSDP may therefore
|
|
19
|
+
be REJECTED by real hardware — the switch may only accept those changes via the
|
|
20
|
+
``vlan_engine`` (or other R/W) tags or via HTTP. Do NOT read the ``pvid_tlv`` /
|
|
21
|
+
``vlan_members_tlv`` encoders here as confirmation that those tags are writable;
|
|
22
|
+
their writability is unconfirmed and must be settled by a hardware capture.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import socket
|
|
27
|
+
import struct
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
from .auth import encode_password_v1
|
|
31
|
+
from .parsers import ports_to_bitmap
|
|
32
|
+
from .protocol import NSDPPacket, Op, Tag, TLVEntry
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Iterable
|
|
36
|
+
|
|
37
|
+
RESULT_SUCCESS = 0x0000
|
|
38
|
+
RESULT_BAD_PASSWORD = 0x0700
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_read_request(
|
|
42
|
+
client_mac: bytes, server_mac: bytes, sequence: int, tags: list[Tag]
|
|
43
|
+
) -> NSDPPacket:
|
|
44
|
+
pkt = NSDPPacket(
|
|
45
|
+
op=Op.READ_REQUEST,
|
|
46
|
+
client_mac=client_mac,
|
|
47
|
+
server_mac=server_mac,
|
|
48
|
+
sequence=sequence,
|
|
49
|
+
)
|
|
50
|
+
for tag in tags:
|
|
51
|
+
pkt.add_tlv(tag) # length-0 TLV = "please read this"
|
|
52
|
+
return pkt
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_write_request(
|
|
56
|
+
client_mac: bytes,
|
|
57
|
+
server_mac: bytes,
|
|
58
|
+
sequence: int,
|
|
59
|
+
password: str,
|
|
60
|
+
tlvs: list[TLVEntry],
|
|
61
|
+
) -> NSDPPacket:
|
|
62
|
+
pkt = NSDPPacket(
|
|
63
|
+
op=Op.WRITE_REQUEST,
|
|
64
|
+
client_mac=client_mac,
|
|
65
|
+
server_mac=server_mac,
|
|
66
|
+
sequence=sequence,
|
|
67
|
+
)
|
|
68
|
+
pkt.tlvs.append(TLVEntry(Tag.PASSWORD, encode_password_v1(password)))
|
|
69
|
+
pkt.tlvs.extend(tlvs)
|
|
70
|
+
return pkt
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def pvid_tlv(port: int, vlan: int) -> TLVEntry:
|
|
74
|
+
return TLVEntry(Tag.PORT_PVID, bytes([port]) + struct.pack(">H", vlan))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def vlan_members_tlv(
|
|
78
|
+
vlan: int, members: Iterable[int], tagged: Iterable[int], port_count: int
|
|
79
|
+
) -> TLVEntry:
|
|
80
|
+
width = (port_count + 7) // 8
|
|
81
|
+
value = (
|
|
82
|
+
struct.pack(">H", vlan)
|
|
83
|
+
+ ports_to_bitmap(members, width)
|
|
84
|
+
+ ports_to_bitmap(tagged, width)
|
|
85
|
+
)
|
|
86
|
+
return TLVEntry(Tag.VLAN_MEMBERS, value)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def ipv4_tlv(tag: Tag, dotted: str) -> TLVEntry:
|
|
90
|
+
return TLVEntry(tag, socket.inet_aton(dotted))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def dhcp_tlv(enabled: bool) -> TLVEntry:
|
|
94
|
+
return TLVEntry(Tag.DHCP_MODE, b"\x01" if enabled else b"\x00")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def reboot_tlv() -> TLVEntry:
|
|
98
|
+
return TLVEntry(Tag.REBOOT, b"")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""SNMP protocol logic (pure, I/O-free)."""
|