unifi-network-maps 1.4.3__py3-none-any.whl → 1.4.5__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/assets/themes/dark.yaml +2 -2
- unifi_network_maps/cli/__init__.py +2 -38
- unifi_network_maps/cli/args.py +166 -0
- unifi_network_maps/cli/main.py +18 -747
- unifi_network_maps/cli/render.py +255 -0
- unifi_network_maps/cli/runtime.py +157 -0
- unifi_network_maps/io/mkdocs_assets.py +21 -0
- unifi_network_maps/io/mock_generate.py +2 -294
- unifi_network_maps/model/mock.py +307 -0
- unifi_network_maps/render/device_ports_md.py +44 -27
- unifi_network_maps/render/legend.py +30 -0
- unifi_network_maps/render/lldp_md.py +81 -60
- unifi_network_maps/render/markdown_tables.py +21 -0
- unifi_network_maps/render/mermaid.py +72 -85
- unifi_network_maps/render/mkdocs.py +167 -0
- unifi_network_maps/render/templates/device_port_block.md.j2 +5 -0
- unifi_network_maps/render/templates/legend_compact.html.j2 +14 -0
- unifi_network_maps/render/templates/lldp_device_section.md.j2 +15 -0
- unifi_network_maps/render/templates/markdown_section.md.j2 +3 -0
- unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +30 -0
- unifi_network_maps/render/templates/mkdocs_document.md.j2 +23 -0
- unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +8 -0
- unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +2 -0
- unifi_network_maps/render/templates/mkdocs_legend.css.j2 +29 -0
- unifi_network_maps/render/templates/mkdocs_legend.js.j2 +18 -0
- unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +4 -0
- unifi_network_maps/render/templating.py +19 -0
- {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/METADATA +2 -1
- {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/RECORD +34 -14
- {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/WHEEL +0 -0
- {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/entry_points.txt +0 -0
- {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/licenses/LICENSE +0 -0
- {unifi_network_maps-1.4.3.dist-info → unifi_network_maps-1.4.5.dist-info}/top_level.txt +0 -0
|
@@ -2,298 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import random
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from typing import Any
|
|
5
|
+
from ..model.mock import MockOptions, generate_mock_payload, mock_payload_json
|
|
9
6
|
|
|
10
|
-
|
|
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
|
|
7
|
+
__all__ = ["MockOptions", "generate_mock_payload", "mock_payload_json"]
|
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
target: 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": target["mac"],
|
|
232
|
+
"port_id": f"Port {remote_port}",
|
|
233
|
+
"port_desc": remote_name,
|
|
234
|
+
"local_port_idx": local_port,
|
|
235
|
+
"local_port_name": f"Port {local_port}",
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _add_port(
|
|
241
|
+
device: dict[str, Any],
|
|
242
|
+
port_idx: int,
|
|
243
|
+
*,
|
|
244
|
+
poe_enabled: bool,
|
|
245
|
+
rng: random.Random,
|
|
246
|
+
) -> None:
|
|
247
|
+
device["port_table"].append(
|
|
248
|
+
{
|
|
249
|
+
"port_idx": port_idx,
|
|
250
|
+
"name": f"Port {port_idx}",
|
|
251
|
+
"ifname": f"eth{port_idx}",
|
|
252
|
+
"is_uplink": False,
|
|
253
|
+
"poe_enable": poe_enabled,
|
|
254
|
+
"port_poe": poe_enabled,
|
|
255
|
+
"poe_class": 4 if poe_enabled else 0,
|
|
256
|
+
"poe_power": round(rng.uniform(2.0, 6.0), 2) if poe_enabled else 0.0,
|
|
257
|
+
"poe_good": poe_enabled,
|
|
258
|
+
"poe_voltage": round(rng.uniform(44.0, 52.0), 1) if poe_enabled else 0.0,
|
|
259
|
+
"poe_current": round(rng.uniform(0.05, 0.12), 3) if poe_enabled else 0.0,
|
|
260
|
+
}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _next_core_port(state: _MockState) -> int:
|
|
265
|
+
port_idx = state.core_port_next
|
|
266
|
+
state.core_port_next += 1
|
|
267
|
+
return port_idx
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _unique_name(state: _MockState, prefix: str) -> str:
|
|
271
|
+
name = prefix
|
|
272
|
+
while name in state.used_names:
|
|
273
|
+
name = f"{prefix} {state.rng.randint(2, 9)}"
|
|
274
|
+
state.used_names.add(name)
|
|
275
|
+
return name
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _unique_client_name(state: _MockState) -> str:
|
|
279
|
+
name = state.fake.first_name()
|
|
280
|
+
while name in state.used_names:
|
|
281
|
+
name = state.fake.first_name()
|
|
282
|
+
state.used_names.add(name)
|
|
283
|
+
return name
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _unique_room(state: _MockState) -> str:
|
|
287
|
+
room = state.fake.word().title()
|
|
288
|
+
while room in state.used_rooms:
|
|
289
|
+
room = state.fake.word().title()
|
|
290
|
+
state.used_rooms.add(room)
|
|
291
|
+
return room
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _unique_mac(state: _MockState) -> str:
|
|
295
|
+
mac = state.fake.mac_address()
|
|
296
|
+
while mac in state.used_macs:
|
|
297
|
+
mac = state.fake.mac_address()
|
|
298
|
+
state.used_macs.add(mac)
|
|
299
|
+
return mac
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _unique_ip(state: _MockState) -> str:
|
|
303
|
+
ip = state.fake.ipv4_private()
|
|
304
|
+
while ip in state.used_ips:
|
|
305
|
+
ip = state.fake.ipv4_private()
|
|
306
|
+
state.used_ips.add(ip)
|
|
307
|
+
return ip
|
|
@@ -7,6 +7,8 @@ from html import escape as _escape_html
|
|
|
7
7
|
|
|
8
8
|
from ..model.ports import extract_port_number
|
|
9
9
|
from ..model.topology import ClientPortMap, Device, PortInfo, PortMap, classify_device_type
|
|
10
|
+
from .markdown_tables import markdown_table_lines
|
|
11
|
+
from .templating import render_template
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def render_device_port_overview(
|
|
@@ -17,18 +19,24 @@ def render_device_port_overview(
|
|
|
17
19
|
) -> str:
|
|
18
20
|
gateways = _collect_devices_by_type(devices, "gateway")
|
|
19
21
|
switches = _collect_devices_by_type(devices, "switch")
|
|
20
|
-
|
|
22
|
+
sections: list[str] = []
|
|
21
23
|
if gateways:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
sections.append(
|
|
25
|
+
render_template(
|
|
26
|
+
"markdown_section.md.j2",
|
|
27
|
+
title="Gateways",
|
|
28
|
+
body=_render_device_group(gateways, port_map, client_ports),
|
|
29
|
+
).rstrip()
|
|
30
|
+
)
|
|
25
31
|
if switches:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
sections.append(
|
|
33
|
+
render_template(
|
|
34
|
+
"markdown_section.md.j2",
|
|
35
|
+
title="Switches",
|
|
36
|
+
body=_render_device_group(switches, port_map, client_ports),
|
|
37
|
+
).rstrip()
|
|
38
|
+
)
|
|
39
|
+
return "\n\n".join(section for section in sections if section).rstrip() + "\n"
|
|
32
40
|
|
|
33
41
|
|
|
34
42
|
def _collect_devices_by_type(devices: list[Device], desired_type: str) -> list[Device]:
|
|
@@ -42,15 +50,18 @@ def _render_device_group(
|
|
|
42
50
|
devices: list[Device],
|
|
43
51
|
port_map: PortMap,
|
|
44
52
|
client_ports: ClientPortMap | None,
|
|
45
|
-
) ->
|
|
46
|
-
|
|
53
|
+
) -> str:
|
|
54
|
+
blocks: list[str] = []
|
|
47
55
|
for device in devices:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
blocks.append(
|
|
57
|
+
render_template(
|
|
58
|
+
"device_port_block.md.j2",
|
|
59
|
+
device_name=device.name,
|
|
60
|
+
details="\n".join(_render_device_details(device)).rstrip(),
|
|
61
|
+
ports="\n".join(_render_device_ports(device, port_map, client_ports)).rstrip(),
|
|
62
|
+
).rstrip()
|
|
63
|
+
)
|
|
64
|
+
return "\n\n".join(block for block in blocks if block)
|
|
54
65
|
|
|
55
66
|
|
|
56
67
|
def render_device_port_details(
|
|
@@ -70,17 +81,23 @@ def _render_device_ports(
|
|
|
70
81
|
client_ports: ClientPortMap | None,
|
|
71
82
|
) -> list[str]:
|
|
72
83
|
rows = _build_port_rows(device, port_map, client_ports)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
84
|
+
table_rows = [
|
|
85
|
+
[
|
|
86
|
+
_escape_cell(port_label),
|
|
87
|
+
_escape_cell(connected or "-"),
|
|
88
|
+
_escape_cell(speed),
|
|
89
|
+
_escape_cell(poe_state),
|
|
90
|
+
_escape_cell(power),
|
|
91
|
+
]
|
|
92
|
+
for port_label, connected, speed, poe_state, power in rows
|
|
78
93
|
]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
lines = ["#### Ports", ""]
|
|
95
|
+
lines.extend(
|
|
96
|
+
markdown_table_lines(
|
|
97
|
+
["Port", "Connected", "Speed", "PoE", "Power"],
|
|
98
|
+
table_rows,
|
|
83
99
|
)
|
|
100
|
+
)
|
|
84
101
|
return lines
|
|
85
102
|
|
|
86
103
|
|