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,515 @@
|
|
|
1
|
+
"""Firewall zone management tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..api.client import UniFiClient
|
|
6
|
+
from ..config import APIType, Settings
|
|
7
|
+
from ..models.zbf_matrix import ZoneNetworkAssignment
|
|
8
|
+
from ..utils import ValidationError, audit_action, get_logger, validate_confirmation
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ensure_local_api(settings: Settings) -> None:
|
|
14
|
+
"""Ensure the UniFi controller is accessed via the local API for ZBF operations."""
|
|
15
|
+
if settings.api_type != APIType.LOCAL:
|
|
16
|
+
raise ValidationError(
|
|
17
|
+
"Zone-Based Firewall endpoints are only available when UNIFI_API_TYPE='local'. "
|
|
18
|
+
"Please configure a local UniFi gateway connection to use these tools."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def list_firewall_zones(
|
|
23
|
+
site_id: str,
|
|
24
|
+
settings: Settings,
|
|
25
|
+
) -> list[dict[str, Any]]:
|
|
26
|
+
"""List all firewall zones for a site.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
site_id: Site identifier
|
|
30
|
+
settings: Application settings
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of firewall zones
|
|
34
|
+
"""
|
|
35
|
+
_ensure_local_api(settings)
|
|
36
|
+
|
|
37
|
+
async with UniFiClient(settings) as client:
|
|
38
|
+
logger.info(f"Listing firewall zones for site {site_id}")
|
|
39
|
+
|
|
40
|
+
if not client.is_authenticated:
|
|
41
|
+
await client.authenticate()
|
|
42
|
+
|
|
43
|
+
resolved_site_id = await client.resolve_site_id(site_id)
|
|
44
|
+
endpoint = settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones")
|
|
45
|
+
response = await client.get(endpoint)
|
|
46
|
+
# Handle both list and dict responses
|
|
47
|
+
data = response if isinstance(response, list) else response.get("data", [])
|
|
48
|
+
|
|
49
|
+
# Return raw data - API response may not match model exactly
|
|
50
|
+
return data # type: ignore[no-any-return]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def create_firewall_zone(
|
|
54
|
+
site_id: str,
|
|
55
|
+
name: str,
|
|
56
|
+
settings: Settings,
|
|
57
|
+
description: str | None = None,
|
|
58
|
+
network_ids: list[str] | None = None,
|
|
59
|
+
confirm: bool = False,
|
|
60
|
+
dry_run: bool = False,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
"""Create a new firewall zone.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
site_id: Site identifier
|
|
66
|
+
name: Zone name
|
|
67
|
+
settings: Application settings
|
|
68
|
+
description: Zone description
|
|
69
|
+
network_ids: Network IDs to assign to this zone
|
|
70
|
+
confirm: Confirmation flag (required)
|
|
71
|
+
dry_run: If True, validate but don't execute
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Created firewall zone
|
|
75
|
+
"""
|
|
76
|
+
validate_confirmation(confirm, "create firewall zone")
|
|
77
|
+
|
|
78
|
+
_ensure_local_api(settings)
|
|
79
|
+
|
|
80
|
+
async with UniFiClient(settings) as client:
|
|
81
|
+
logger.info(f"Creating firewall zone '{name}' for site {site_id}")
|
|
82
|
+
|
|
83
|
+
if not client.is_authenticated:
|
|
84
|
+
await client.authenticate()
|
|
85
|
+
|
|
86
|
+
# Build request payload
|
|
87
|
+
# Note: networkIds is required by API (even if empty list)
|
|
88
|
+
payload: dict[str, Any] = {
|
|
89
|
+
"name": name,
|
|
90
|
+
"networkIds": network_ids if network_ids else [],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if description:
|
|
94
|
+
payload["description"] = description
|
|
95
|
+
|
|
96
|
+
if dry_run:
|
|
97
|
+
logger.info(f"[DRY RUN] Would create firewall zone with payload: {payload}")
|
|
98
|
+
return {"dry_run": True, "payload": payload}
|
|
99
|
+
|
|
100
|
+
resolved_site_id = await client.resolve_site_id(site_id)
|
|
101
|
+
response = await client.post(
|
|
102
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones"),
|
|
103
|
+
json_data=payload,
|
|
104
|
+
)
|
|
105
|
+
data = response.get("data", response)
|
|
106
|
+
|
|
107
|
+
# Audit the action
|
|
108
|
+
await audit_action(
|
|
109
|
+
settings,
|
|
110
|
+
action_type="create_firewall_zone",
|
|
111
|
+
resource_type="firewall_zone",
|
|
112
|
+
resource_id=data.get("_id", "unknown"),
|
|
113
|
+
site_id=site_id,
|
|
114
|
+
details={"name": name},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Return raw data - API response may not match model exactly
|
|
118
|
+
return data # type: ignore[no-any-return]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def update_firewall_zone(
|
|
122
|
+
site_id: str,
|
|
123
|
+
firewall_zone_id: str,
|
|
124
|
+
settings: Settings,
|
|
125
|
+
name: str | None = None,
|
|
126
|
+
description: str | None = None,
|
|
127
|
+
network_ids: list[str] | None = None,
|
|
128
|
+
confirm: bool = False,
|
|
129
|
+
dry_run: bool = False,
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
"""Update an existing firewall zone.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
site_id: Site identifier
|
|
135
|
+
firewall_zone_id: Firewall zone identifier
|
|
136
|
+
settings: Application settings
|
|
137
|
+
name: Zone name
|
|
138
|
+
description: Zone description
|
|
139
|
+
network_ids: Network IDs to assign to this zone
|
|
140
|
+
confirm: Confirmation flag (required)
|
|
141
|
+
dry_run: If True, validate but don't execute
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Updated firewall zone
|
|
145
|
+
"""
|
|
146
|
+
validate_confirmation(confirm, "update firewall zone")
|
|
147
|
+
|
|
148
|
+
_ensure_local_api(settings)
|
|
149
|
+
|
|
150
|
+
async with UniFiClient(settings) as client:
|
|
151
|
+
logger.info(f"Updating firewall zone {firewall_zone_id} for site {site_id}")
|
|
152
|
+
|
|
153
|
+
if not client.is_authenticated:
|
|
154
|
+
await client.authenticate()
|
|
155
|
+
|
|
156
|
+
resolved_site_id = await client.resolve_site_id(site_id)
|
|
157
|
+
|
|
158
|
+
# Fetch current zone to get existing networkIds if not provided
|
|
159
|
+
# API requires networkIds field to always be present
|
|
160
|
+
current_zone_response = await client.get(
|
|
161
|
+
settings.get_integration_path(
|
|
162
|
+
f"sites/{resolved_site_id}/firewall/zones/{firewall_zone_id}"
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
current_zone = current_zone_response.get("data", current_zone_response)
|
|
166
|
+
current_network_ids = current_zone.get("networkIds", [])
|
|
167
|
+
|
|
168
|
+
# Build request payload - networkIds is required by API
|
|
169
|
+
payload: dict[str, Any] = {
|
|
170
|
+
"networkIds": network_ids if network_ids is not None else current_network_ids
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if name is not None:
|
|
174
|
+
payload["name"] = name
|
|
175
|
+
if description is not None:
|
|
176
|
+
payload["description"] = description
|
|
177
|
+
|
|
178
|
+
if dry_run:
|
|
179
|
+
logger.info(f"[DRY RUN] Would update firewall zone with payload: {payload}")
|
|
180
|
+
return {"dry_run": True, "payload": payload}
|
|
181
|
+
|
|
182
|
+
response = await client.put(
|
|
183
|
+
settings.get_integration_path(
|
|
184
|
+
f"sites/{resolved_site_id}/firewall/zones/{firewall_zone_id}"
|
|
185
|
+
),
|
|
186
|
+
json_data=payload,
|
|
187
|
+
)
|
|
188
|
+
data = response.get("data", response)
|
|
189
|
+
|
|
190
|
+
# Audit the action
|
|
191
|
+
await audit_action(
|
|
192
|
+
settings,
|
|
193
|
+
action_type="update_firewall_zone",
|
|
194
|
+
resource_type="firewall_zone",
|
|
195
|
+
resource_id=firewall_zone_id,
|
|
196
|
+
site_id=site_id,
|
|
197
|
+
details=payload,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Return raw data - API response may not match model exactly
|
|
201
|
+
return data # type: ignore[no-any-return]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def assign_network_to_zone(
|
|
205
|
+
site_id: str,
|
|
206
|
+
zone_id: str,
|
|
207
|
+
network_id: str,
|
|
208
|
+
settings: Settings,
|
|
209
|
+
confirm: bool = False,
|
|
210
|
+
dry_run: bool = False,
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
"""Dynamically assign a network to a zone.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
site_id: Site identifier
|
|
216
|
+
zone_id: Zone identifier
|
|
217
|
+
network_id: Network identifier to assign
|
|
218
|
+
settings: Application settings
|
|
219
|
+
confirm: Confirmation flag (required)
|
|
220
|
+
dry_run: If True, validate but don't execute
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Network assignment information
|
|
224
|
+
"""
|
|
225
|
+
validate_confirmation(confirm, "assign network to zone")
|
|
226
|
+
|
|
227
|
+
_ensure_local_api(settings)
|
|
228
|
+
|
|
229
|
+
async with UniFiClient(settings) as client:
|
|
230
|
+
logger.info(f"Assigning network {network_id} to zone {zone_id} on site {site_id}")
|
|
231
|
+
|
|
232
|
+
if not client.is_authenticated:
|
|
233
|
+
await client.authenticate()
|
|
234
|
+
|
|
235
|
+
resolved_site_id = await client.resolve_site_id(site_id)
|
|
236
|
+
|
|
237
|
+
# Get network name
|
|
238
|
+
network_name = None
|
|
239
|
+
try:
|
|
240
|
+
network_response = await client.get(
|
|
241
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/networks/{network_id}")
|
|
242
|
+
)
|
|
243
|
+
network_data = network_response.get("data", {})
|
|
244
|
+
network_name = network_data.get("name")
|
|
245
|
+
except Exception:
|
|
246
|
+
logger.warning(f"Could not fetch network name for {network_id}")
|
|
247
|
+
|
|
248
|
+
# Update zone to include this network
|
|
249
|
+
zone_response = await client.get(
|
|
250
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones/{zone_id}")
|
|
251
|
+
)
|
|
252
|
+
zone_data = zone_response.get("data", {})
|
|
253
|
+
current_networks = zone_data.get("networks", [])
|
|
254
|
+
|
|
255
|
+
if network_id in current_networks:
|
|
256
|
+
logger.info(f"Network {network_id} already assigned to zone {zone_id}")
|
|
257
|
+
return ZoneNetworkAssignment( # type: ignore[no-any-return]
|
|
258
|
+
zone_id=zone_id,
|
|
259
|
+
network_id=network_id,
|
|
260
|
+
network_name=network_name,
|
|
261
|
+
).model_dump()
|
|
262
|
+
|
|
263
|
+
updated_networks = list(current_networks) + [network_id]
|
|
264
|
+
|
|
265
|
+
payload = {"networks": updated_networks}
|
|
266
|
+
|
|
267
|
+
if dry_run:
|
|
268
|
+
logger.info(f"[DRY RUN] Would assign network {network_id} to zone {zone_id}")
|
|
269
|
+
return {"dry_run": True, "payload": payload}
|
|
270
|
+
|
|
271
|
+
await client.put(
|
|
272
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones/{zone_id}"),
|
|
273
|
+
json_data=payload,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Audit the action
|
|
277
|
+
await audit_action(
|
|
278
|
+
settings,
|
|
279
|
+
action_type="assign_network_to_zone",
|
|
280
|
+
resource_type="zone_network_assignment",
|
|
281
|
+
resource_id=network_id,
|
|
282
|
+
site_id=site_id,
|
|
283
|
+
details={"zone_id": zone_id, "network_id": network_id},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return ZoneNetworkAssignment( # type: ignore[no-any-return]
|
|
287
|
+
zone_id=zone_id,
|
|
288
|
+
network_id=network_id,
|
|
289
|
+
network_name=network_name,
|
|
290
|
+
).model_dump()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
async def get_zone_networks(site_id: str, zone_id: str, settings: Settings) -> list[dict[str, Any]]:
|
|
294
|
+
"""List all networks in a zone.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
site_id: Site identifier
|
|
298
|
+
zone_id: Zone identifier
|
|
299
|
+
settings: Application settings
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of networks in the zone
|
|
303
|
+
"""
|
|
304
|
+
_ensure_local_api(settings)
|
|
305
|
+
|
|
306
|
+
async with UniFiClient(settings) as client:
|
|
307
|
+
logger.info(f"Listing networks in zone {zone_id} on site {site_id}")
|
|
308
|
+
|
|
309
|
+
if not client.is_authenticated:
|
|
310
|
+
await client.authenticate()
|
|
311
|
+
|
|
312
|
+
resolved_site_id = await client.resolve_site_id(site_id)
|
|
313
|
+
|
|
314
|
+
response = await client.get(
|
|
315
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones/{zone_id}")
|
|
316
|
+
)
|
|
317
|
+
zone_data = response.get("data", {})
|
|
318
|
+
network_ids = zone_data.get("networks", [])
|
|
319
|
+
|
|
320
|
+
# Fetch network details for each network ID
|
|
321
|
+
networks = []
|
|
322
|
+
for network_id in network_ids:
|
|
323
|
+
try:
|
|
324
|
+
network_response = await client.get(
|
|
325
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/networks/{network_id}")
|
|
326
|
+
)
|
|
327
|
+
network_data = network_response.get("data", {})
|
|
328
|
+
networks.append(
|
|
329
|
+
ZoneNetworkAssignment(
|
|
330
|
+
zone_id=zone_id,
|
|
331
|
+
network_id=network_id,
|
|
332
|
+
network_name=network_data.get("name"),
|
|
333
|
+
).model_dump()
|
|
334
|
+
)
|
|
335
|
+
except Exception:
|
|
336
|
+
# If network fetch fails, still include the assignment with just IDs
|
|
337
|
+
networks.append(
|
|
338
|
+
ZoneNetworkAssignment(
|
|
339
|
+
zone_id=zone_id,
|
|
340
|
+
network_id=network_id,
|
|
341
|
+
).model_dump()
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return networks
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
async def delete_firewall_zone(
|
|
348
|
+
site_id: str,
|
|
349
|
+
zone_id: str,
|
|
350
|
+
settings: Settings,
|
|
351
|
+
confirm: bool = False,
|
|
352
|
+
dry_run: bool = False,
|
|
353
|
+
) -> dict[str, Any]:
|
|
354
|
+
"""Delete a firewall zone.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
site_id: Site identifier
|
|
358
|
+
zone_id: Zone identifier to delete
|
|
359
|
+
settings: Application settings
|
|
360
|
+
confirm: Confirmation flag (required)
|
|
361
|
+
dry_run: If True, validate but don't execute
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Deletion confirmation
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
ValueError: If confirmation not provided
|
|
368
|
+
"""
|
|
369
|
+
validate_confirmation(confirm, "delete firewall zone")
|
|
370
|
+
|
|
371
|
+
_ensure_local_api(settings)
|
|
372
|
+
|
|
373
|
+
async with UniFiClient(settings) as client:
|
|
374
|
+
logger.info(f"Deleting firewall zone {zone_id} from site {site_id}")
|
|
375
|
+
|
|
376
|
+
if not client.is_authenticated:
|
|
377
|
+
await client.authenticate()
|
|
378
|
+
|
|
379
|
+
if dry_run:
|
|
380
|
+
logger.info(f"[DRY RUN] Would delete firewall zone {zone_id}")
|
|
381
|
+
return {"dry_run": True, "zone_id": zone_id, "action": "would_delete"}
|
|
382
|
+
|
|
383
|
+
resolved_site_id = await client.resolve_site_id(site_id)
|
|
384
|
+
await client.delete(
|
|
385
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones/{zone_id}")
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Audit the action
|
|
389
|
+
await audit_action(
|
|
390
|
+
settings,
|
|
391
|
+
action_type="delete_firewall_zone",
|
|
392
|
+
resource_type="firewall_zone",
|
|
393
|
+
resource_id=zone_id,
|
|
394
|
+
site_id=site_id,
|
|
395
|
+
details={"zone_id": zone_id},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return {"status": "success", "zone_id": zone_id, "action": "deleted"}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
async def unassign_network_from_zone(
|
|
402
|
+
site_id: str,
|
|
403
|
+
zone_id: str,
|
|
404
|
+
network_id: str,
|
|
405
|
+
settings: Settings,
|
|
406
|
+
confirm: bool = False,
|
|
407
|
+
dry_run: bool = False,
|
|
408
|
+
) -> dict[str, Any]:
|
|
409
|
+
"""Remove a network from a firewall zone.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
site_id: Site identifier
|
|
413
|
+
zone_id: Zone identifier
|
|
414
|
+
network_id: Network identifier to remove
|
|
415
|
+
settings: Application settings
|
|
416
|
+
confirm: Confirmation flag (required)
|
|
417
|
+
dry_run: If True, validate but don't execute
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
Network unassignment confirmation
|
|
421
|
+
|
|
422
|
+
Raises:
|
|
423
|
+
ValueError: If confirmation not provided or network not in zone
|
|
424
|
+
"""
|
|
425
|
+
validate_confirmation(confirm, "unassign network from zone")
|
|
426
|
+
|
|
427
|
+
_ensure_local_api(settings)
|
|
428
|
+
|
|
429
|
+
async with UniFiClient(settings) as client:
|
|
430
|
+
logger.info(f"Unassigning network {network_id} from zone {zone_id} on site {site_id}")
|
|
431
|
+
|
|
432
|
+
if not client.is_authenticated:
|
|
433
|
+
await client.authenticate()
|
|
434
|
+
|
|
435
|
+
resolved_site_id = await client.resolve_site_id(site_id)
|
|
436
|
+
|
|
437
|
+
# Get current zone configuration
|
|
438
|
+
zone_response = await client.get(
|
|
439
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones/{zone_id}")
|
|
440
|
+
)
|
|
441
|
+
zone_data = zone_response.get("data", {})
|
|
442
|
+
current_networks = zone_data.get("networks", [])
|
|
443
|
+
|
|
444
|
+
if network_id not in current_networks:
|
|
445
|
+
raise ValueError(f"Network {network_id} is not assigned to zone {zone_id}")
|
|
446
|
+
|
|
447
|
+
# Remove network from list
|
|
448
|
+
updated_networks = [nid for nid in current_networks if nid != network_id]
|
|
449
|
+
|
|
450
|
+
payload = {"networks": updated_networks}
|
|
451
|
+
|
|
452
|
+
if dry_run:
|
|
453
|
+
logger.info(f"[DRY RUN] Would remove network {network_id} from zone {zone_id}")
|
|
454
|
+
return {"dry_run": True, "payload": payload}
|
|
455
|
+
|
|
456
|
+
await client.put(
|
|
457
|
+
settings.get_integration_path(f"sites/{resolved_site_id}/firewall/zones/{zone_id}"),
|
|
458
|
+
json_data=payload,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Audit the action
|
|
462
|
+
await audit_action(
|
|
463
|
+
settings,
|
|
464
|
+
action_type="unassign_network_from_zone",
|
|
465
|
+
resource_type="zone_network_assignment",
|
|
466
|
+
resource_id=network_id,
|
|
467
|
+
site_id=site_id,
|
|
468
|
+
details={"zone_id": zone_id, "network_id": network_id},
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
"status": "success",
|
|
473
|
+
"zone_id": zone_id,
|
|
474
|
+
"network_id": network_id,
|
|
475
|
+
"action": "unassigned",
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
async def get_zone_statistics(
|
|
480
|
+
site_id: str,
|
|
481
|
+
zone_id: str,
|
|
482
|
+
settings: Settings,
|
|
483
|
+
) -> dict[str, Any]:
|
|
484
|
+
"""Get traffic statistics for a firewall zone.
|
|
485
|
+
|
|
486
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
487
|
+
|
|
488
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
489
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
490
|
+
|
|
491
|
+
Zone traffic statistics are not available via the API.
|
|
492
|
+
Monitor traffic via /sites/{siteId}/clients endpoint instead.
|
|
493
|
+
|
|
494
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
site_id: Site identifier
|
|
498
|
+
zone_id: Zone identifier
|
|
499
|
+
settings: Application settings
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
Zone traffic statistics including bandwidth usage and connection counts
|
|
503
|
+
|
|
504
|
+
Raises:
|
|
505
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
506
|
+
"""
|
|
507
|
+
logger.warning(
|
|
508
|
+
f"get_zone_statistics called for zone {zone_id} but endpoint does not exist in UniFi API v10.0.156."
|
|
509
|
+
)
|
|
510
|
+
raise NotImplementedError(
|
|
511
|
+
"Zone statistics endpoint does not exist in UniFi Network API v10.0.156. "
|
|
512
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
513
|
+
"Monitor traffic via /sites/{siteId}/clients endpoint instead. "
|
|
514
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
515
|
+
)
|