unifi-network-maps 1.3.1__py3-none-any.whl → 1.4.1__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.
- unifi_network_maps/__init__.py +1 -1
- unifi_network_maps/__main__.py +8 -0
- unifi_network_maps/adapters/unifi.py +90 -9
- unifi_network_maps/cli/main.py +320 -23
- unifi_network_maps/io/mock_data.py +23 -0
- unifi_network_maps/io/mock_generate.py +299 -0
- unifi_network_maps/model/lldp.py +26 -12
- unifi_network_maps/model/topology.py +111 -3
- unifi_network_maps/render/device_ports_md.py +462 -0
- unifi_network_maps/render/lldp_md.py +33 -12
- unifi_network_maps/render/mermaid.py +62 -3
- {unifi_network_maps-1.3.1.dist-info → unifi_network_maps-1.4.1.dist-info}/METADATA +89 -15
- {unifi_network_maps-1.3.1.dist-info → unifi_network_maps-1.4.1.dist-info}/RECORD +17 -13
- {unifi_network_maps-1.3.1.dist-info → unifi_network_maps-1.4.1.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.3.1.dist-info → unifi_network_maps-1.4.1.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.3.1.dist-info → unifi_network_maps-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.3.1.dist-info → unifi_network_maps-1.4.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""Generate mock UniFi data using Faker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import random
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from faker import Faker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class MockOptions:
|
|
15
|
+
seed: int = 1337
|
|
16
|
+
switch_count: int = 1
|
|
17
|
+
ap_count: int = 2
|
|
18
|
+
wired_client_count: int = 2
|
|
19
|
+
wireless_client_count: int = 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class _MockState:
|
|
24
|
+
fake: Faker
|
|
25
|
+
rng: random.Random
|
|
26
|
+
used_macs: set[str] = field(default_factory=set)
|
|
27
|
+
used_ips: set[str] = field(default_factory=set)
|
|
28
|
+
used_names: set[str] = field(default_factory=set)
|
|
29
|
+
used_rooms: set[str] = field(default_factory=set)
|
|
30
|
+
core_port_next: int = 2
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_mock_payload(options: MockOptions) -> dict[str, list[dict[str, Any]]]:
|
|
34
|
+
state = _build_state(options.seed)
|
|
35
|
+
devices, core_switch, aps = _build_devices(options, state)
|
|
36
|
+
clients = _build_clients(options, state, core_switch, aps)
|
|
37
|
+
return {"devices": devices, "clients": clients}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def mock_payload_json(options: MockOptions) -> str:
|
|
41
|
+
payload = generate_mock_payload(options)
|
|
42
|
+
return json.dumps(payload, indent=2, sort_keys=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_state(seed: int) -> _MockState:
|
|
46
|
+
fake = Faker("en_US")
|
|
47
|
+
fake.seed_instance(seed)
|
|
48
|
+
rng = random.Random(seed)
|
|
49
|
+
return _MockState(fake=fake, rng=rng)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _build_devices(
|
|
53
|
+
options: MockOptions, state: _MockState
|
|
54
|
+
) -> tuple[list[dict[str, Any]], dict[str, Any], list[dict[str, Any]]]:
|
|
55
|
+
gateway = _build_gateway(state)
|
|
56
|
+
core_switch = _build_core_switch(state)
|
|
57
|
+
_link_gateway_to_switch(state, gateway, core_switch)
|
|
58
|
+
access_switches = _build_access_switches(options.switch_count - 1, state, core_switch)
|
|
59
|
+
aps = _build_aps(options.ap_count, state, core_switch)
|
|
60
|
+
devices = [gateway, core_switch] + access_switches + aps
|
|
61
|
+
return devices, core_switch, aps
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _build_gateway(state: _MockState) -> dict[str, Any]:
|
|
65
|
+
device = _device_base(state, "Cloud Gateway", "udm", "UniFi Dream Machine Pro", "UDM-Pro")
|
|
66
|
+
_add_port(device, 9, poe_enabled=False, rng=state.rng)
|
|
67
|
+
return device
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _build_core_switch(state: _MockState) -> dict[str, Any]:
|
|
71
|
+
return _device_base(state, "Core Switch", "usw", "UniFi Switch 24 PoE", "USW-24-PoE")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_access_switches(
|
|
75
|
+
count: int, state: _MockState, core_switch: dict[str, Any]
|
|
76
|
+
) -> list[dict[str, Any]]:
|
|
77
|
+
switches = []
|
|
78
|
+
for _ in range(max(0, count)):
|
|
79
|
+
room = _unique_room(state)
|
|
80
|
+
name = _unique_name(state, f"Switch {room}")
|
|
81
|
+
device = _device_base(state, name, "usw", "UniFi Switch Lite 8 PoE", "USW-Lite-8-PoE")
|
|
82
|
+
_link_core_device(state, core_switch, device, poe_enabled=False)
|
|
83
|
+
switches.append(device)
|
|
84
|
+
return switches
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _build_aps(count: int, state: _MockState, core_switch: dict[str, Any]) -> list[dict[str, Any]]:
|
|
88
|
+
aps = []
|
|
89
|
+
for _ in range(max(0, count)):
|
|
90
|
+
room = _unique_room(state)
|
|
91
|
+
name = _unique_name(state, f"AP {room}")
|
|
92
|
+
device = _device_base(state, name, "uap", "UniFi AP 6 Lite", "U6-Lite")
|
|
93
|
+
_add_port(device, 1, poe_enabled=True, rng=state.rng)
|
|
94
|
+
_link_core_device(state, core_switch, device, poe_enabled=True)
|
|
95
|
+
aps.append(device)
|
|
96
|
+
return aps
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _build_clients(
|
|
100
|
+
options: MockOptions,
|
|
101
|
+
state: _MockState,
|
|
102
|
+
core_switch: dict[str, Any],
|
|
103
|
+
aps: list[dict[str, Any]],
|
|
104
|
+
) -> list[dict[str, Any]]:
|
|
105
|
+
clients = []
|
|
106
|
+
clients.extend(_build_wired_clients(options.wired_client_count, state, core_switch))
|
|
107
|
+
clients.extend(_build_wireless_clients(options.wireless_client_count, state, aps))
|
|
108
|
+
return clients
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _build_wired_clients(
|
|
112
|
+
count: int, state: _MockState, core_switch: dict[str, Any]
|
|
113
|
+
) -> list[dict[str, Any]]:
|
|
114
|
+
clients = []
|
|
115
|
+
for _ in range(max(0, count)):
|
|
116
|
+
port_idx = _next_core_port(state)
|
|
117
|
+
_add_port(core_switch, port_idx, poe_enabled=False, rng=state.rng)
|
|
118
|
+
name = _unique_client_name(state)
|
|
119
|
+
clients.append(
|
|
120
|
+
{
|
|
121
|
+
"name": name,
|
|
122
|
+
"is_wired": True,
|
|
123
|
+
"sw_mac": core_switch["mac"],
|
|
124
|
+
"sw_port": port_idx,
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
return clients
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _build_wireless_clients(
|
|
131
|
+
count: int, state: _MockState, aps: list[dict[str, Any]]
|
|
132
|
+
) -> list[dict[str, Any]]:
|
|
133
|
+
if not aps:
|
|
134
|
+
return []
|
|
135
|
+
clients = []
|
|
136
|
+
for idx in range(max(0, count)):
|
|
137
|
+
ap = aps[idx % len(aps)]
|
|
138
|
+
name = _unique_client_name(state)
|
|
139
|
+
clients.append(
|
|
140
|
+
{
|
|
141
|
+
"name": name,
|
|
142
|
+
"is_wired": False,
|
|
143
|
+
"ap_mac": ap["mac"],
|
|
144
|
+
"ap_port": 1,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
return clients
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _device_base(
|
|
151
|
+
state: _MockState, name: str, dev_type: str, model_name: str, model: str
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
version = _pick_version(state, dev_type)
|
|
154
|
+
return {
|
|
155
|
+
"name": name,
|
|
156
|
+
"model_name": model_name,
|
|
157
|
+
"model": model,
|
|
158
|
+
"mac": _unique_mac(state),
|
|
159
|
+
"ip": _unique_ip(state),
|
|
160
|
+
"type": dev_type,
|
|
161
|
+
"version": version,
|
|
162
|
+
"port_table": [],
|
|
163
|
+
"lldp_info": [],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _pick_version(state: _MockState, dev_type: str) -> str:
|
|
168
|
+
versions = {
|
|
169
|
+
"udm": ["3.1.0", "3.1.1"],
|
|
170
|
+
"usw": ["7.0.0", "7.1.2"],
|
|
171
|
+
"uap": ["6.6.55", "6.7.10"],
|
|
172
|
+
}
|
|
173
|
+
return state.rng.choice(versions.get(dev_type, ["1.0.0"]))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _link_gateway_to_switch(
|
|
177
|
+
state: _MockState, gateway: dict[str, Any], core_switch: dict[str, Any]
|
|
178
|
+
) -> None:
|
|
179
|
+
_add_port(core_switch, 1, poe_enabled=False, rng=state.rng)
|
|
180
|
+
_add_lldp_link(
|
|
181
|
+
gateway,
|
|
182
|
+
core_switch,
|
|
183
|
+
local_port=9,
|
|
184
|
+
remote_port=1,
|
|
185
|
+
remote_name=core_switch["name"],
|
|
186
|
+
)
|
|
187
|
+
_add_lldp_link(
|
|
188
|
+
core_switch,
|
|
189
|
+
gateway,
|
|
190
|
+
local_port=1,
|
|
191
|
+
remote_port=9,
|
|
192
|
+
remote_name=gateway["name"],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _link_core_device(
|
|
197
|
+
state: _MockState,
|
|
198
|
+
core_switch: dict[str, Any],
|
|
199
|
+
device: dict[str, Any],
|
|
200
|
+
*,
|
|
201
|
+
poe_enabled: bool,
|
|
202
|
+
) -> None:
|
|
203
|
+
port_idx = _next_core_port(state)
|
|
204
|
+
_add_port(core_switch, port_idx, poe_enabled=poe_enabled, rng=state.rng)
|
|
205
|
+
_add_lldp_link(
|
|
206
|
+
core_switch,
|
|
207
|
+
device,
|
|
208
|
+
local_port=port_idx,
|
|
209
|
+
remote_port=1,
|
|
210
|
+
remote_name=device["name"],
|
|
211
|
+
)
|
|
212
|
+
_add_lldp_link(
|
|
213
|
+
device,
|
|
214
|
+
core_switch,
|
|
215
|
+
local_port=1,
|
|
216
|
+
remote_port=port_idx,
|
|
217
|
+
remote_name=core_switch["name"],
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _add_lldp_link(
|
|
222
|
+
source: dict[str, Any],
|
|
223
|
+
dest: dict[str, Any],
|
|
224
|
+
*,
|
|
225
|
+
local_port: int,
|
|
226
|
+
remote_port: int,
|
|
227
|
+
remote_name: str,
|
|
228
|
+
) -> None:
|
|
229
|
+
source["lldp_info"].append(
|
|
230
|
+
{
|
|
231
|
+
"chassis_id": dest["mac"],
|
|
232
|
+
"port_id": f"Port {remote_port}",
|
|
233
|
+
"port_desc": remote_name,
|
|
234
|
+
"local_port_name": f"Port {local_port}",
|
|
235
|
+
"local_port_idx": local_port,
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _add_port(
|
|
241
|
+
device: dict[str, Any], port_idx: int, *, poe_enabled: bool, rng: random.Random
|
|
242
|
+
) -> None:
|
|
243
|
+
entry = {
|
|
244
|
+
"port_idx": port_idx,
|
|
245
|
+
"name": f"Port {port_idx}",
|
|
246
|
+
"ifname": f"eth{max(0, port_idx - 1)}",
|
|
247
|
+
"poe_enable": poe_enabled,
|
|
248
|
+
"port_poe": poe_enabled,
|
|
249
|
+
}
|
|
250
|
+
if poe_enabled:
|
|
251
|
+
entry["poe_power"] = round(rng.uniform(4.5, 7.5), 1)
|
|
252
|
+
device["port_table"].append(entry)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _unique_mac(state: _MockState) -> str:
|
|
256
|
+
return _unique_value(state, state.fake.mac_address, state.used_macs)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _unique_ip(state: _MockState) -> str:
|
|
260
|
+
return _unique_value(state, state.fake.ipv4_private, state.used_ips)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _unique_room(state: _MockState) -> str:
|
|
264
|
+
def _room() -> str:
|
|
265
|
+
return state.fake.word().title()
|
|
266
|
+
|
|
267
|
+
return _unique_value(state, _room, state.used_rooms)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _unique_name(state: _MockState, candidate: str) -> str:
|
|
271
|
+
if candidate not in state.used_names:
|
|
272
|
+
state.used_names.add(candidate)
|
|
273
|
+
return candidate
|
|
274
|
+
suffix = state.rng.randint(2, 99)
|
|
275
|
+
value = f"{candidate} {suffix}"
|
|
276
|
+
state.used_names.add(value)
|
|
277
|
+
return value
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _unique_client_name(state: _MockState) -> str:
|
|
281
|
+
def _name() -> str:
|
|
282
|
+
return state.fake.hostname()
|
|
283
|
+
|
|
284
|
+
return _unique_value(state, _name, state.used_names)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _unique_value(state: _MockState, generator, used: set[str]) -> str:
|
|
288
|
+
for _ in range(100):
|
|
289
|
+
value = str(generator())
|
|
290
|
+
if value not in used:
|
|
291
|
+
used.add(value)
|
|
292
|
+
return value
|
|
293
|
+
raise ValueError("Failed to generate a unique value")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _next_core_port(state: _MockState) -> int:
|
|
297
|
+
port_idx = state.core_port_next
|
|
298
|
+
state.core_port_next += 1
|
|
299
|
+
return port_idx
|
unifi_network_maps/model/lldp.py
CHANGED
|
@@ -17,18 +17,32 @@ class LLDPEntry:
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def coerce_lldp(entry: object) -> LLDPEntry:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
entry
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
if isinstance(entry, dict):
|
|
21
|
+
chassis_id = entry.get("chassis_id") or entry.get("chassisId")
|
|
22
|
+
port_id = entry.get("port_id") or entry.get("portId")
|
|
23
|
+
port_desc = (
|
|
24
|
+
entry.get("port_desc")
|
|
25
|
+
or entry.get("portDesc")
|
|
26
|
+
or entry.get("port_descr")
|
|
27
|
+
or entry.get("portDescr")
|
|
28
|
+
)
|
|
29
|
+
local_port_name = entry.get("local_port_name") or entry.get("localPortName")
|
|
30
|
+
local_port_idx = entry.get("local_port_idx") or entry.get("localPortIdx")
|
|
31
|
+
else:
|
|
32
|
+
chassis_id = getattr(entry, "chassis_id", None) or getattr(entry, "chassisId", None)
|
|
33
|
+
port_id = getattr(entry, "port_id", None) or getattr(entry, "portId", None)
|
|
34
|
+
port_desc = (
|
|
35
|
+
getattr(entry, "port_desc", None)
|
|
36
|
+
or getattr(entry, "portDesc", None)
|
|
37
|
+
or getattr(entry, "port_descr", None)
|
|
38
|
+
or getattr(entry, "portDescr", None)
|
|
39
|
+
)
|
|
40
|
+
local_port_name = getattr(entry, "local_port_name", None) or getattr(
|
|
41
|
+
entry, "localPortName", None
|
|
42
|
+
)
|
|
43
|
+
local_port_idx = getattr(entry, "local_port_idx", None) or getattr(
|
|
44
|
+
entry, "localPortIdx", None
|
|
45
|
+
)
|
|
32
46
|
if not chassis_id or not port_id:
|
|
33
47
|
raise ValueError("LLDP entry missing chassis_id or port_id")
|
|
34
48
|
return LLDPEntry(
|
|
@@ -6,7 +6,7 @@ import logging
|
|
|
6
6
|
from collections import deque
|
|
7
7
|
from collections.abc import Iterable
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
|
-
from typing import Protocol
|
|
9
|
+
from typing import Protocol
|
|
10
10
|
|
|
11
11
|
from .labels import compose_port_label, order_edge_names
|
|
12
12
|
from .lldp import LLDPEntry, coerce_lldp, local_port_label
|
|
@@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
class Device:
|
|
20
20
|
name: str
|
|
21
21
|
model_name: str
|
|
22
|
+
model: str
|
|
22
23
|
mac: str
|
|
23
24
|
ip: str
|
|
24
25
|
type: str
|
|
@@ -74,17 +75,22 @@ class PortInfo:
|
|
|
74
75
|
port_idx: int | None
|
|
75
76
|
name: str | None
|
|
76
77
|
ifname: str | None
|
|
78
|
+
speed: int | None
|
|
79
|
+
aggregation_group: str | None
|
|
77
80
|
port_poe: bool
|
|
78
81
|
poe_enable: bool
|
|
79
82
|
poe_good: bool
|
|
80
83
|
poe_power: float | None
|
|
81
84
|
|
|
82
85
|
|
|
83
|
-
PortMap
|
|
84
|
-
PoeMap
|
|
86
|
+
type PortMap = dict[tuple[str, str], str]
|
|
87
|
+
type PoeMap = dict[tuple[str, str], bool]
|
|
88
|
+
type ClientPortMap = dict[str, list[tuple[int, str]]]
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
def _get_attr(obj: object, name: str) -> object | None:
|
|
92
|
+
if isinstance(obj, dict):
|
|
93
|
+
return obj.get(name)
|
|
88
94
|
return getattr(obj, name, None)
|
|
89
95
|
|
|
90
96
|
|
|
@@ -123,6 +129,44 @@ def _as_int(value: object | None) -> int | None:
|
|
|
123
129
|
return None
|
|
124
130
|
|
|
125
131
|
|
|
132
|
+
def _as_group_id(value: object | None) -> str | None:
|
|
133
|
+
if value is None:
|
|
134
|
+
return None
|
|
135
|
+
if isinstance(value, bool):
|
|
136
|
+
return None
|
|
137
|
+
if isinstance(value, int):
|
|
138
|
+
return str(value)
|
|
139
|
+
if isinstance(value, str):
|
|
140
|
+
return value.strip() or None
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _aggregation_group(port_entry: object) -> object | None:
|
|
145
|
+
keys = (
|
|
146
|
+
"aggregation_group",
|
|
147
|
+
"aggregation_id",
|
|
148
|
+
"aggregate_id",
|
|
149
|
+
"agg_id",
|
|
150
|
+
"lag_id",
|
|
151
|
+
"lag_group",
|
|
152
|
+
"link_aggregation_group",
|
|
153
|
+
"link_aggregation_id",
|
|
154
|
+
"aggregate",
|
|
155
|
+
"aggregated_by",
|
|
156
|
+
)
|
|
157
|
+
if isinstance(port_entry, dict):
|
|
158
|
+
for key in keys:
|
|
159
|
+
value = port_entry.get(key)
|
|
160
|
+
if value not in (None, "", False):
|
|
161
|
+
return value
|
|
162
|
+
return None
|
|
163
|
+
for key in keys:
|
|
164
|
+
value = _get_attr(port_entry, key)
|
|
165
|
+
if value not in (None, "", False):
|
|
166
|
+
return value
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
126
170
|
def _resolve_port_idx_from_lldp(lldp_entry: LLDPEntry, port_table: list[PortInfo]) -> int | None:
|
|
127
171
|
if lldp_entry.local_port_idx is not None:
|
|
128
172
|
return lldp_entry.local_port_idx
|
|
@@ -153,6 +197,8 @@ def _port_info_from_entry(port_entry: object) -> PortInfo:
|
|
|
153
197
|
port_idx = port_entry.get("port_idx") or port_entry.get("portIdx")
|
|
154
198
|
name = port_entry.get("name")
|
|
155
199
|
ifname = port_entry.get("ifname")
|
|
200
|
+
speed = port_entry.get("speed")
|
|
201
|
+
aggregation_group = _aggregation_group(port_entry)
|
|
156
202
|
port_poe = _as_bool(port_entry.get("port_poe"))
|
|
157
203
|
poe_enable = _as_bool(port_entry.get("poe_enable"))
|
|
158
204
|
poe_good = _as_bool(port_entry.get("poe_good"))
|
|
@@ -161,6 +207,8 @@ def _port_info_from_entry(port_entry: object) -> PortInfo:
|
|
|
161
207
|
port_idx = _get_attr(port_entry, "port_idx") or _get_attr(port_entry, "portIdx")
|
|
162
208
|
name = _get_attr(port_entry, "name")
|
|
163
209
|
ifname = _get_attr(port_entry, "ifname")
|
|
210
|
+
speed = _get_attr(port_entry, "speed")
|
|
211
|
+
aggregation_group = _aggregation_group(port_entry)
|
|
164
212
|
port_poe = _as_bool(_get_attr(port_entry, "port_poe"))
|
|
165
213
|
poe_enable = _as_bool(_get_attr(port_entry, "poe_enable"))
|
|
166
214
|
poe_good = _as_bool(_get_attr(port_entry, "poe_good"))
|
|
@@ -169,6 +217,8 @@ def _port_info_from_entry(port_entry: object) -> PortInfo:
|
|
|
169
217
|
port_idx=_as_int(port_idx),
|
|
170
218
|
name=str(name) if isinstance(name, str) and name.strip() else None,
|
|
171
219
|
ifname=str(ifname) if isinstance(ifname, str) and ifname.strip() else None,
|
|
220
|
+
speed=_as_int(speed),
|
|
221
|
+
aggregation_group=_as_group_id(aggregation_group),
|
|
172
222
|
port_poe=port_poe,
|
|
173
223
|
poe_enable=poe_enable,
|
|
174
224
|
poe_good=poe_good,
|
|
@@ -243,6 +293,7 @@ def _uplink_info(device: DeviceLike) -> tuple[UplinkInfo | None, UplinkInfo | No
|
|
|
243
293
|
def coerce_device(device: DeviceLike) -> Device:
|
|
244
294
|
name = _get_attr(device, "name")
|
|
245
295
|
model_name = _get_attr(device, "model_name") or _get_attr(device, "model")
|
|
296
|
+
model = _get_attr(device, "model")
|
|
246
297
|
mac = _get_attr(device, "mac")
|
|
247
298
|
ip = _get_attr(device, "ip") or _get_attr(device, "ip_address")
|
|
248
299
|
dev_type = _get_attr(device, "type") or _get_attr(device, "device_type")
|
|
@@ -268,6 +319,7 @@ def coerce_device(device: DeviceLike) -> Device:
|
|
|
268
319
|
return Device(
|
|
269
320
|
name=str(name),
|
|
270
321
|
model_name=str(model_name or ""),
|
|
322
|
+
model=str(model or ""),
|
|
271
323
|
mac=str(mac),
|
|
272
324
|
ip=str(ip or ""),
|
|
273
325
|
type=str(dev_type or ""),
|
|
@@ -528,6 +580,62 @@ def build_edges(
|
|
|
528
580
|
return edges
|
|
529
581
|
|
|
530
582
|
|
|
583
|
+
def build_port_map(devices: Iterable[Device], *, only_unifi: bool = True) -> PortMap:
|
|
584
|
+
ordered_devices = sorted(devices, key=lambda item: (item.name.lower(), item.mac.lower()))
|
|
585
|
+
index = build_device_index(ordered_devices)
|
|
586
|
+
device_by_name = {device.name: device for device in ordered_devices}
|
|
587
|
+
raw_links: list[tuple[str, str]] = []
|
|
588
|
+
seen: set[frozenset[str]] = set()
|
|
589
|
+
port_map: PortMap = {}
|
|
590
|
+
poe_map: PoeMap = {}
|
|
591
|
+
|
|
592
|
+
devices_with_lldp_edges = _collect_lldp_links(
|
|
593
|
+
ordered_devices,
|
|
594
|
+
index,
|
|
595
|
+
port_map,
|
|
596
|
+
poe_map,
|
|
597
|
+
raw_links,
|
|
598
|
+
seen,
|
|
599
|
+
only_unifi=only_unifi,
|
|
600
|
+
)
|
|
601
|
+
_collect_uplink_links(
|
|
602
|
+
ordered_devices,
|
|
603
|
+
devices_with_lldp_edges,
|
|
604
|
+
index,
|
|
605
|
+
device_by_name,
|
|
606
|
+
port_map,
|
|
607
|
+
poe_map,
|
|
608
|
+
raw_links,
|
|
609
|
+
seen,
|
|
610
|
+
include_ports=True,
|
|
611
|
+
only_unifi=only_unifi,
|
|
612
|
+
)
|
|
613
|
+
return port_map
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def build_client_port_map(
|
|
617
|
+
devices: Iterable[Device],
|
|
618
|
+
clients: Iterable[object],
|
|
619
|
+
*,
|
|
620
|
+
client_mode: str,
|
|
621
|
+
) -> ClientPortMap:
|
|
622
|
+
device_index = build_device_index(devices)
|
|
623
|
+
port_map: ClientPortMap = {}
|
|
624
|
+
for client in clients:
|
|
625
|
+
if not _client_matches_mode(client, client_mode):
|
|
626
|
+
continue
|
|
627
|
+
name = _client_display_name(client)
|
|
628
|
+
uplink_mac = _client_uplink_mac(client)
|
|
629
|
+
uplink_port = _client_uplink_port(client)
|
|
630
|
+
if not name or not uplink_mac or uplink_port is None:
|
|
631
|
+
continue
|
|
632
|
+
device_name = device_index.get(_normalize_mac(uplink_mac))
|
|
633
|
+
if not device_name:
|
|
634
|
+
continue
|
|
635
|
+
port_map.setdefault(device_name, []).append((uplink_port, name))
|
|
636
|
+
return port_map
|
|
637
|
+
|
|
638
|
+
|
|
531
639
|
def _collect_lldp_links(
|
|
532
640
|
devices: list[Device],
|
|
533
641
|
index: dict[str, str],
|