iflow-mcp_enuno-unifi-mcp-server 0.2.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.
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/METADATA +1282 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/RECORD +81 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/WHEEL +4 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/licenses/LICENSE +201 -0
- src/__init__.py +3 -0
- src/__main__.py +6 -0
- src/api/__init__.py +5 -0
- src/api/client.py +727 -0
- src/api/site_manager_client.py +176 -0
- src/cache.py +483 -0
- src/config/__init__.py +5 -0
- src/config/config.py +321 -0
- src/main.py +2234 -0
- src/models/__init__.py +126 -0
- src/models/acl.py +41 -0
- src/models/backup.py +272 -0
- src/models/client.py +74 -0
- src/models/device.py +53 -0
- src/models/dpi.py +50 -0
- src/models/firewall_policy.py +123 -0
- src/models/firewall_zone.py +28 -0
- src/models/network.py +62 -0
- src/models/qos_profile.py +458 -0
- src/models/radius.py +141 -0
- src/models/reference_data.py +34 -0
- src/models/site.py +59 -0
- src/models/site_manager.py +120 -0
- src/models/topology.py +138 -0
- src/models/traffic_flow.py +137 -0
- src/models/traffic_matching_list.py +56 -0
- src/models/voucher.py +42 -0
- src/models/vpn.py +73 -0
- src/models/wan.py +48 -0
- src/models/zbf_matrix.py +49 -0
- src/resources/__init__.py +8 -0
- src/resources/clients.py +111 -0
- src/resources/devices.py +102 -0
- src/resources/networks.py +93 -0
- src/resources/site_manager.py +64 -0
- src/resources/sites.py +86 -0
- src/tools/__init__.py +25 -0
- src/tools/acls.py +328 -0
- src/tools/application.py +42 -0
- src/tools/backups.py +1173 -0
- src/tools/client_management.py +505 -0
- src/tools/clients.py +203 -0
- src/tools/device_control.py +325 -0
- src/tools/devices.py +354 -0
- src/tools/dpi.py +241 -0
- src/tools/dpi_tools.py +89 -0
- src/tools/firewall.py +417 -0
- src/tools/firewall_policies.py +430 -0
- src/tools/firewall_zones.py +515 -0
- src/tools/network_config.py +388 -0
- src/tools/networks.py +190 -0
- src/tools/port_forwarding.py +263 -0
- src/tools/qos.py +1070 -0
- src/tools/radius.py +763 -0
- src/tools/reference_data.py +107 -0
- src/tools/site_manager.py +466 -0
- src/tools/site_vpn.py +95 -0
- src/tools/sites.py +187 -0
- src/tools/topology.py +406 -0
- src/tools/traffic_flows.py +1062 -0
- src/tools/traffic_matching_lists.py +371 -0
- src/tools/vouchers.py +249 -0
- src/tools/vpn.py +76 -0
- src/tools/wans.py +30 -0
- src/tools/wifi.py +498 -0
- src/tools/zbf_matrix.py +326 -0
- src/utils/__init__.py +88 -0
- src/utils/audit.py +213 -0
- src/utils/exceptions.py +114 -0
- src/utils/helpers.py +159 -0
- src/utils/logger.py +105 -0
- src/utils/sanitize.py +244 -0
- src/utils/validators.py +160 -0
- src/webhooks/__init__.py +6 -0
- src/webhooks/handlers.py +196 -0
- src/webhooks/receiver.py +290 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Network configuration MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..api import UniFiClient
|
|
6
|
+
from ..config import Settings
|
|
7
|
+
from ..utils import (
|
|
8
|
+
ResourceNotFoundError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
get_logger,
|
|
11
|
+
log_audit,
|
|
12
|
+
validate_confirmation,
|
|
13
|
+
validate_site_id,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def create_network(
|
|
18
|
+
site_id: str,
|
|
19
|
+
name: str,
|
|
20
|
+
vlan_id: int,
|
|
21
|
+
subnet: str,
|
|
22
|
+
settings: Settings,
|
|
23
|
+
purpose: str = "corporate",
|
|
24
|
+
dhcp_enabled: bool = True,
|
|
25
|
+
dhcp_start: str | None = None,
|
|
26
|
+
dhcp_stop: str | None = None,
|
|
27
|
+
dhcp_dns_1: str | None = None,
|
|
28
|
+
dhcp_dns_2: str | None = None,
|
|
29
|
+
dhcp_dns_3: str | None = None,
|
|
30
|
+
dhcp_dns_4: str | None = None,
|
|
31
|
+
domain_name: str | None = None,
|
|
32
|
+
confirm: bool = False,
|
|
33
|
+
dry_run: bool = False,
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Create a new network/VLAN.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
site_id: Site identifier
|
|
39
|
+
name: Network name
|
|
40
|
+
vlan_id: VLAN ID (1-4094)
|
|
41
|
+
subnet: Network subnet in CIDR notation (e.g., "192.168.1.0/24")
|
|
42
|
+
settings: Application settings
|
|
43
|
+
purpose: Network purpose (corporate, guest, vlan-only)
|
|
44
|
+
dhcp_enabled: Enable DHCP server
|
|
45
|
+
dhcp_start: DHCP range start IP
|
|
46
|
+
dhcp_stop: DHCP range stop IP
|
|
47
|
+
dhcp_dns_1: Primary DNS server
|
|
48
|
+
dhcp_dns_2: Secondary DNS server
|
|
49
|
+
domain_name: Domain name for DHCP
|
|
50
|
+
confirm: Confirmation flag (must be True to execute)
|
|
51
|
+
dry_run: If True, validate but don't create the network
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Created network dictionary or dry-run result
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ConfirmationRequiredError: If confirm is not True
|
|
58
|
+
ValidationError: If validation fails
|
|
59
|
+
"""
|
|
60
|
+
site_id = validate_site_id(site_id)
|
|
61
|
+
validate_confirmation(confirm, "network configuration operation")
|
|
62
|
+
logger = get_logger(__name__, settings.log_level)
|
|
63
|
+
|
|
64
|
+
# Validate VLAN ID
|
|
65
|
+
if not 1 <= vlan_id <= 4094:
|
|
66
|
+
raise ValidationError(f"Invalid VLAN ID {vlan_id}. Must be between 1 and 4094")
|
|
67
|
+
|
|
68
|
+
# Validate purpose
|
|
69
|
+
valid_purposes = ["corporate", "guest", "vlan-only", "wan"]
|
|
70
|
+
if purpose not in valid_purposes:
|
|
71
|
+
raise ValidationError(f"Invalid purpose '{purpose}'. Must be one of: {valid_purposes}")
|
|
72
|
+
|
|
73
|
+
# Validate subnet format
|
|
74
|
+
if "/" not in subnet:
|
|
75
|
+
raise ValidationError(f"Invalid subnet '{subnet}'. Must be in CIDR notation")
|
|
76
|
+
|
|
77
|
+
# Build network data
|
|
78
|
+
network_data = {
|
|
79
|
+
"name": name,
|
|
80
|
+
"purpose": purpose,
|
|
81
|
+
"vlan": vlan_id,
|
|
82
|
+
"ip_subnet": subnet,
|
|
83
|
+
"dhcpd_enabled": dhcp_enabled,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if dhcp_enabled:
|
|
87
|
+
if dhcp_start:
|
|
88
|
+
network_data["dhcpd_start"] = dhcp_start
|
|
89
|
+
if dhcp_stop:
|
|
90
|
+
network_data["dhcpd_stop"] = dhcp_stop
|
|
91
|
+
if dhcp_dns_1:
|
|
92
|
+
network_data["dhcpd_dns_1"] = dhcp_dns_1
|
|
93
|
+
if dhcp_dns_2:
|
|
94
|
+
network_data["dhcpd_dns_2"] = dhcp_dns_2
|
|
95
|
+
if dhcp_dns_3:
|
|
96
|
+
network_data["dhcpd_dns_3"] = dhcp_dns_3
|
|
97
|
+
if dhcp_dns_4:
|
|
98
|
+
network_data["dhcpd_dns_4"] = dhcp_dns_4
|
|
99
|
+
if domain_name:
|
|
100
|
+
network_data["domain_name"] = domain_name
|
|
101
|
+
|
|
102
|
+
# Log parameters for audit
|
|
103
|
+
parameters = {
|
|
104
|
+
"site_id": site_id,
|
|
105
|
+
"name": name,
|
|
106
|
+
"vlan_id": vlan_id,
|
|
107
|
+
"subnet": subnet,
|
|
108
|
+
"purpose": purpose,
|
|
109
|
+
"dhcp_enabled": dhcp_enabled,
|
|
110
|
+
"dhcp_start": dhcp_start,
|
|
111
|
+
"dhcp_stop": dhcp_stop,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if dry_run:
|
|
115
|
+
logger.info(f"DRY RUN: Would create network '{name}' in site '{site_id}'")
|
|
116
|
+
log_audit(
|
|
117
|
+
operation="create_network",
|
|
118
|
+
parameters=parameters,
|
|
119
|
+
result="dry_run",
|
|
120
|
+
site_id=site_id,
|
|
121
|
+
dry_run=True,
|
|
122
|
+
)
|
|
123
|
+
return {"dry_run": True, "would_create": network_data}
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
async with UniFiClient(settings) as client:
|
|
127
|
+
await client.authenticate()
|
|
128
|
+
|
|
129
|
+
response = await client.post(
|
|
130
|
+
f"/ea/sites/{site_id}/rest/networkconf", json_data=network_data
|
|
131
|
+
)
|
|
132
|
+
created_network: dict[str, Any] = response.get("data", [{}])[0]
|
|
133
|
+
|
|
134
|
+
logger.info(f"Created network '{name}' in site '{site_id}'")
|
|
135
|
+
log_audit(
|
|
136
|
+
operation="create_network",
|
|
137
|
+
parameters=parameters,
|
|
138
|
+
result="success",
|
|
139
|
+
site_id=site_id,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return created_network
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Failed to create network '{name}': {e}")
|
|
146
|
+
log_audit(
|
|
147
|
+
operation="create_network",
|
|
148
|
+
parameters=parameters,
|
|
149
|
+
result="failed",
|
|
150
|
+
site_id=site_id,
|
|
151
|
+
)
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def update_network(
|
|
156
|
+
site_id: str,
|
|
157
|
+
network_id: str,
|
|
158
|
+
settings: Settings,
|
|
159
|
+
name: str | None = None,
|
|
160
|
+
vlan_id: int | None = None,
|
|
161
|
+
subnet: str | None = None,
|
|
162
|
+
purpose: str | None = None,
|
|
163
|
+
dhcp_enabled: bool | None = None,
|
|
164
|
+
dhcp_start: str | None = None,
|
|
165
|
+
dhcp_stop: str | None = None,
|
|
166
|
+
dhcp_dns_1: str | None = None,
|
|
167
|
+
dhcp_dns_2: str | None = None,
|
|
168
|
+
dhcp_dns_3: str | None = None,
|
|
169
|
+
dhcp_dns_4: str | None = None,
|
|
170
|
+
domain_name: str | None = None,
|
|
171
|
+
confirm: bool = False,
|
|
172
|
+
dry_run: bool = False,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
"""Update an existing network.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
site_id: Site identifier
|
|
178
|
+
network_id: Network ID
|
|
179
|
+
settings: Application settings
|
|
180
|
+
name: New network name
|
|
181
|
+
vlan_id: New VLAN ID (1-4094)
|
|
182
|
+
subnet: New subnet in CIDR notation
|
|
183
|
+
purpose: New purpose (corporate, guest, vlan-only)
|
|
184
|
+
dhcp_enabled: Enable/disable DHCP
|
|
185
|
+
dhcp_start: New DHCP range start IP
|
|
186
|
+
dhcp_stop: New DHCP range stop IP
|
|
187
|
+
dhcp_dns_1: New primary DNS server
|
|
188
|
+
dhcp_dns_2: New secondary DNS server
|
|
189
|
+
domain_name: New domain name
|
|
190
|
+
confirm: Confirmation flag (must be True to execute)
|
|
191
|
+
dry_run: If True, validate but don't update the network
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Updated network dictionary or dry-run result
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
ConfirmationRequiredError: If confirm is not True
|
|
198
|
+
ResourceNotFoundError: If network not found
|
|
199
|
+
"""
|
|
200
|
+
site_id = validate_site_id(site_id)
|
|
201
|
+
validate_confirmation(confirm, "network configuration operation")
|
|
202
|
+
logger = get_logger(__name__, settings.log_level)
|
|
203
|
+
|
|
204
|
+
# Validate VLAN ID if provided
|
|
205
|
+
if vlan_id is not None and not 1 <= vlan_id <= 4094:
|
|
206
|
+
raise ValidationError(f"Invalid VLAN ID {vlan_id}. Must be between 1 and 4094")
|
|
207
|
+
|
|
208
|
+
# Validate purpose if provided
|
|
209
|
+
if purpose is not None:
|
|
210
|
+
valid_purposes = ["corporate", "guest", "vlan-only", "wan"]
|
|
211
|
+
if purpose not in valid_purposes:
|
|
212
|
+
raise ValidationError(f"Invalid purpose '{purpose}'. Must be one of: {valid_purposes}")
|
|
213
|
+
|
|
214
|
+
# Validate subnet format if provided
|
|
215
|
+
if subnet is not None and "/" not in subnet:
|
|
216
|
+
raise ValidationError(f"Invalid subnet '{subnet}'. Must be in CIDR notation")
|
|
217
|
+
|
|
218
|
+
parameters = {
|
|
219
|
+
"site_id": site_id,
|
|
220
|
+
"network_id": network_id,
|
|
221
|
+
"name": name,
|
|
222
|
+
"vlan_id": vlan_id,
|
|
223
|
+
"subnet": subnet,
|
|
224
|
+
"purpose": purpose,
|
|
225
|
+
"dhcp_enabled": dhcp_enabled,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if dry_run:
|
|
229
|
+
logger.info(f"DRY RUN: Would update network '{network_id}' in site '{site_id}'")
|
|
230
|
+
log_audit(
|
|
231
|
+
operation="update_network",
|
|
232
|
+
parameters=parameters,
|
|
233
|
+
result="dry_run",
|
|
234
|
+
site_id=site_id,
|
|
235
|
+
dry_run=True,
|
|
236
|
+
)
|
|
237
|
+
return {"dry_run": True, "would_update": parameters}
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
async with UniFiClient(settings) as client:
|
|
241
|
+
await client.authenticate()
|
|
242
|
+
|
|
243
|
+
# Get existing network
|
|
244
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/networkconf")
|
|
245
|
+
if isinstance(response, list):
|
|
246
|
+
networks_data: list[dict[str, Any]] = response
|
|
247
|
+
else:
|
|
248
|
+
networks_data = response.get("data", [])
|
|
249
|
+
|
|
250
|
+
existing_network = None
|
|
251
|
+
for network in networks_data:
|
|
252
|
+
if network.get("_id") == network_id:
|
|
253
|
+
existing_network = network
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
if not existing_network:
|
|
257
|
+
raise ResourceNotFoundError("network", network_id)
|
|
258
|
+
|
|
259
|
+
# Build update data
|
|
260
|
+
update_data = existing_network.copy()
|
|
261
|
+
|
|
262
|
+
if name is not None:
|
|
263
|
+
update_data["name"] = name
|
|
264
|
+
if vlan_id is not None:
|
|
265
|
+
update_data["vlan"] = vlan_id
|
|
266
|
+
if subnet is not None:
|
|
267
|
+
update_data["ip_subnet"] = subnet
|
|
268
|
+
if purpose is not None:
|
|
269
|
+
update_data["purpose"] = purpose
|
|
270
|
+
if dhcp_enabled is not None:
|
|
271
|
+
update_data["dhcpd_enabled"] = dhcp_enabled
|
|
272
|
+
if dhcp_start is not None:
|
|
273
|
+
update_data["dhcpd_start"] = dhcp_start
|
|
274
|
+
if dhcp_stop is not None:
|
|
275
|
+
update_data["dhcpd_stop"] = dhcp_stop
|
|
276
|
+
if dhcp_dns_1 is not None:
|
|
277
|
+
update_data["dhcpd_dns_1"] = dhcp_dns_1
|
|
278
|
+
if dhcp_dns_2 is not None:
|
|
279
|
+
update_data["dhcpd_dns_2"] = dhcp_dns_2
|
|
280
|
+
if dhcp_dns_3 is not None:
|
|
281
|
+
update_data["dhcpd_dns_3"] = dhcp_dns_3
|
|
282
|
+
if dhcp_dns_4 is not None:
|
|
283
|
+
update_data["dhcpd_dns_4"] = dhcp_dns_4
|
|
284
|
+
if domain_name is not None:
|
|
285
|
+
update_data["domain_name"] = domain_name
|
|
286
|
+
|
|
287
|
+
response = await client.put(
|
|
288
|
+
f"/ea/sites/{site_id}/rest/networkconf/{network_id}", json_data=update_data
|
|
289
|
+
)
|
|
290
|
+
if isinstance(response, list):
|
|
291
|
+
updated_network: dict[str, Any] = response[0] if response else {}
|
|
292
|
+
else:
|
|
293
|
+
updated_network = response.get("data", [{}])[0]
|
|
294
|
+
|
|
295
|
+
logger.info(f"Updated network '{network_id}' in site '{site_id}'")
|
|
296
|
+
log_audit(
|
|
297
|
+
operation="update_network",
|
|
298
|
+
parameters=parameters,
|
|
299
|
+
result="success",
|
|
300
|
+
site_id=site_id,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return updated_network
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Failed to update network '{network_id}': {e}")
|
|
307
|
+
log_audit(
|
|
308
|
+
operation="update_network",
|
|
309
|
+
parameters=parameters,
|
|
310
|
+
result="failed",
|
|
311
|
+
site_id=site_id,
|
|
312
|
+
)
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
async def delete_network(
|
|
317
|
+
site_id: str,
|
|
318
|
+
network_id: str,
|
|
319
|
+
settings: Settings,
|
|
320
|
+
confirm: bool = False,
|
|
321
|
+
dry_run: bool = False,
|
|
322
|
+
) -> dict[str, Any]:
|
|
323
|
+
"""Delete a network.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
site_id: Site identifier
|
|
327
|
+
network_id: Network ID
|
|
328
|
+
settings: Application settings
|
|
329
|
+
confirm: Confirmation flag (must be True to execute)
|
|
330
|
+
dry_run: If True, validate but don't delete the network
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Deletion result dictionary
|
|
334
|
+
|
|
335
|
+
Raises:
|
|
336
|
+
ConfirmationRequiredError: If confirm is not True
|
|
337
|
+
ResourceNotFoundError: If network not found
|
|
338
|
+
"""
|
|
339
|
+
site_id = validate_site_id(site_id)
|
|
340
|
+
validate_confirmation(confirm, "network configuration operation")
|
|
341
|
+
logger = get_logger(__name__, settings.log_level)
|
|
342
|
+
|
|
343
|
+
parameters = {"site_id": site_id, "network_id": network_id}
|
|
344
|
+
|
|
345
|
+
if dry_run:
|
|
346
|
+
logger.info(f"DRY RUN: Would delete network '{network_id}' from site '{site_id}'")
|
|
347
|
+
log_audit(
|
|
348
|
+
operation="delete_network",
|
|
349
|
+
parameters=parameters,
|
|
350
|
+
result="dry_run",
|
|
351
|
+
site_id=site_id,
|
|
352
|
+
dry_run=True,
|
|
353
|
+
)
|
|
354
|
+
return {"dry_run": True, "would_delete": network_id}
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
async with UniFiClient(settings) as client:
|
|
358
|
+
await client.authenticate()
|
|
359
|
+
|
|
360
|
+
# Verify network exists before deleting
|
|
361
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/networkconf")
|
|
362
|
+
networks_data: list[dict[str, Any]] = response.get("data", [])
|
|
363
|
+
|
|
364
|
+
network_exists = any(net.get("_id") == network_id for net in networks_data)
|
|
365
|
+
if not network_exists:
|
|
366
|
+
raise ResourceNotFoundError("network", network_id)
|
|
367
|
+
|
|
368
|
+
response = await client.delete(f"/ea/sites/{site_id}/rest/networkconf/{network_id}")
|
|
369
|
+
|
|
370
|
+
logger.info(f"Deleted network '{network_id}' from site '{site_id}'")
|
|
371
|
+
log_audit(
|
|
372
|
+
operation="delete_network",
|
|
373
|
+
parameters=parameters,
|
|
374
|
+
result="success",
|
|
375
|
+
site_id=site_id,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return {"success": True, "deleted_network_id": network_id}
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.error(f"Failed to delete network '{network_id}': {e}")
|
|
382
|
+
log_audit(
|
|
383
|
+
operation="delete_network",
|
|
384
|
+
parameters=parameters,
|
|
385
|
+
result="failed",
|
|
386
|
+
site_id=site_id,
|
|
387
|
+
)
|
|
388
|
+
raise
|
src/tools/networks.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Network information MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..api import UniFiClient
|
|
6
|
+
from ..config import Settings
|
|
7
|
+
from ..models import Network
|
|
8
|
+
from ..utils import ResourceNotFoundError, get_logger, validate_limit_offset, validate_site_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_network_details(site_id: str, network_id: str, settings: Settings) -> dict[str, Any]:
|
|
12
|
+
"""Get detailed network configuration.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
site_id: Site identifier
|
|
16
|
+
network_id: Network identifier
|
|
17
|
+
settings: Application settings
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Network details dictionary
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ResourceNotFoundError: If network not found
|
|
24
|
+
"""
|
|
25
|
+
site_id = validate_site_id(site_id)
|
|
26
|
+
logger = get_logger(__name__, settings.log_level)
|
|
27
|
+
|
|
28
|
+
async with UniFiClient(settings) as client:
|
|
29
|
+
await client.authenticate()
|
|
30
|
+
|
|
31
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/networkconf")
|
|
32
|
+
networks_data = response.get("data", []) if isinstance(response, dict) else response
|
|
33
|
+
|
|
34
|
+
for network_data in networks_data:
|
|
35
|
+
if network_data.get("_id") == network_id:
|
|
36
|
+
network = Network(**network_data)
|
|
37
|
+
logger.info(f"Retrieved network details for {network_id}")
|
|
38
|
+
return network.model_dump() # type: ignore[no-any-return]
|
|
39
|
+
|
|
40
|
+
raise ResourceNotFoundError("network", network_id)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def list_vlans(
|
|
44
|
+
site_id: str,
|
|
45
|
+
settings: Settings,
|
|
46
|
+
limit: int | None = None,
|
|
47
|
+
offset: int | None = None,
|
|
48
|
+
) -> list[dict[str, Any]]:
|
|
49
|
+
"""List all VLANs in a site.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
site_id: Site identifier
|
|
53
|
+
settings: Application settings
|
|
54
|
+
limit: Maximum number of VLANs to return
|
|
55
|
+
offset: Number of VLANs to skip
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of VLAN dictionaries
|
|
59
|
+
"""
|
|
60
|
+
site_id = validate_site_id(site_id)
|
|
61
|
+
limit, offset = validate_limit_offset(limit, offset)
|
|
62
|
+
logger = get_logger(__name__, settings.log_level)
|
|
63
|
+
|
|
64
|
+
async with UniFiClient(settings) as client:
|
|
65
|
+
await client.authenticate()
|
|
66
|
+
|
|
67
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/networkconf")
|
|
68
|
+
networks_data = response.get("data", []) if isinstance(response, dict) else response
|
|
69
|
+
|
|
70
|
+
# Return all networks (not just those with vlan_id set)
|
|
71
|
+
# Local gateway API may not populate vlan_id for all network types
|
|
72
|
+
logger.debug(f"Found {len(networks_data)} networks before pagination")
|
|
73
|
+
|
|
74
|
+
# Apply pagination
|
|
75
|
+
paginated = networks_data[offset : offset + limit]
|
|
76
|
+
|
|
77
|
+
# Parse into Network models
|
|
78
|
+
networks = [Network(**n).model_dump() for n in paginated]
|
|
79
|
+
|
|
80
|
+
logger.info(f"Retrieved {len(networks)} VLANs for site '{site_id}'")
|
|
81
|
+
return networks
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def get_subnet_info(site_id: str, network_id: str, settings: Settings) -> dict[str, Any]:
|
|
85
|
+
"""Get subnet and DHCP information for a network.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
site_id: Site identifier
|
|
89
|
+
network_id: Network identifier
|
|
90
|
+
settings: Application settings
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Subnet and DHCP information dictionary
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ResourceNotFoundError: If network not found
|
|
97
|
+
"""
|
|
98
|
+
site_id = validate_site_id(site_id)
|
|
99
|
+
logger = get_logger(__name__, settings.log_level)
|
|
100
|
+
|
|
101
|
+
async with UniFiClient(settings) as client:
|
|
102
|
+
await client.authenticate()
|
|
103
|
+
|
|
104
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/networkconf")
|
|
105
|
+
networks_data = response.get("data", []) if isinstance(response, dict) else response
|
|
106
|
+
|
|
107
|
+
for network_data in networks_data:
|
|
108
|
+
if network_data.get("_id") == network_id:
|
|
109
|
+
# Extract subnet and DHCP information
|
|
110
|
+
subnet_info = {
|
|
111
|
+
"network_id": network_id,
|
|
112
|
+
"name": network_data.get("name"),
|
|
113
|
+
"ip_subnet": network_data.get("ip_subnet"),
|
|
114
|
+
"vlan_id": network_data.get("vlan_id"),
|
|
115
|
+
"dhcpd_enabled": network_data.get("dhcpd_enabled", False),
|
|
116
|
+
"dhcpd_start": network_data.get("dhcpd_start"),
|
|
117
|
+
"dhcpd_stop": network_data.get("dhcpd_stop"),
|
|
118
|
+
"dhcpd_leasetime": network_data.get("dhcpd_leasetime"),
|
|
119
|
+
"dhcpd_dns_1": network_data.get("dhcpd_dns_1"),
|
|
120
|
+
"dhcpd_dns_2": network_data.get("dhcpd_dns_2"),
|
|
121
|
+
"dhcpd_dns_3": network_data.get("dhcpd_dns_3"),
|
|
122
|
+
"dhcpd_dns_4": network_data.get("dhcpd_dns_4"),
|
|
123
|
+
"dhcpd_gateway": network_data.get("dhcpd_gateway"),
|
|
124
|
+
"domain_name": network_data.get("domain_name"),
|
|
125
|
+
}
|
|
126
|
+
logger.info(f"Retrieved subnet info for network {network_id}")
|
|
127
|
+
return subnet_info
|
|
128
|
+
|
|
129
|
+
raise ResourceNotFoundError("network", network_id)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def get_network_statistics(site_id: str, settings: Settings) -> dict[str, Any]:
|
|
133
|
+
"""Retrieve network usage statistics for a site.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
site_id: Site identifier
|
|
137
|
+
settings: Application settings
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Network statistics dictionary
|
|
141
|
+
"""
|
|
142
|
+
site_id = validate_site_id(site_id)
|
|
143
|
+
logger = get_logger(__name__, settings.log_level)
|
|
144
|
+
|
|
145
|
+
async with UniFiClient(settings) as client:
|
|
146
|
+
await client.authenticate()
|
|
147
|
+
|
|
148
|
+
# Get network configurations
|
|
149
|
+
networks_response = await client.get(f"/ea/sites/{site_id}/rest/networkconf")
|
|
150
|
+
networks_data = (
|
|
151
|
+
networks_response.get("data", [])
|
|
152
|
+
if isinstance(networks_response, dict)
|
|
153
|
+
else networks_response
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Get active clients to count usage per network
|
|
157
|
+
clients_response = await client.get(f"/ea/sites/{site_id}/sta")
|
|
158
|
+
clients_data = (
|
|
159
|
+
clients_response.get("data", [])
|
|
160
|
+
if isinstance(clients_response, dict)
|
|
161
|
+
else clients_response
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Calculate statistics per network
|
|
165
|
+
network_stats = []
|
|
166
|
+
for network in networks_data:
|
|
167
|
+
network_id = network.get("_id")
|
|
168
|
+
vlan_id = network.get("vlan_id")
|
|
169
|
+
|
|
170
|
+
# Count clients on this network
|
|
171
|
+
clients_on_network = [c for c in clients_data if c.get("vlan") == vlan_id]
|
|
172
|
+
|
|
173
|
+
# Calculate total bandwidth
|
|
174
|
+
total_tx = sum(c.get("tx_bytes", 0) for c in clients_on_network)
|
|
175
|
+
total_rx = sum(c.get("rx_bytes", 0) for c in clients_on_network)
|
|
176
|
+
|
|
177
|
+
network_stats.append(
|
|
178
|
+
{
|
|
179
|
+
"network_id": network_id,
|
|
180
|
+
"name": network.get("name"),
|
|
181
|
+
"vlan_id": vlan_id,
|
|
182
|
+
"client_count": len(clients_on_network),
|
|
183
|
+
"total_tx_bytes": total_tx,
|
|
184
|
+
"total_rx_bytes": total_rx,
|
|
185
|
+
"total_bytes": total_tx + total_rx,
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
logger.info(f"Retrieved network statistics for site '{site_id}'")
|
|
190
|
+
return {"site_id": site_id, "networks": network_stats}
|