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
src/tools/wans.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""WAN connection management tools."""
|
|
2
|
+
|
|
3
|
+
from ..api.client import UniFiClient
|
|
4
|
+
from ..config import Settings
|
|
5
|
+
from ..models import WANConnection
|
|
6
|
+
from ..utils import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def list_wan_connections(site_id: str, settings: Settings) -> list[dict]:
|
|
12
|
+
"""List all WAN connections for a site.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
site_id: Site identifier
|
|
16
|
+
settings: Application settings
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
List of WAN connections
|
|
20
|
+
"""
|
|
21
|
+
async with UniFiClient(settings) as client:
|
|
22
|
+
logger.info(f"Listing WAN connections for site {site_id}")
|
|
23
|
+
|
|
24
|
+
if not client.is_authenticated:
|
|
25
|
+
await client.authenticate()
|
|
26
|
+
|
|
27
|
+
response = await client.get(f"/integration/v1/sites/{site_id}/wans")
|
|
28
|
+
data = response.get("data", [])
|
|
29
|
+
|
|
30
|
+
return [WANConnection(**wan).model_dump() for wan in data]
|
src/tools/wifi.py
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
"""WiFi network (SSID) management 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_limit_offset,
|
|
14
|
+
validate_site_id,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def list_wlans(
|
|
19
|
+
site_id: str,
|
|
20
|
+
settings: Settings,
|
|
21
|
+
limit: int | None = None,
|
|
22
|
+
offset: int | None = None,
|
|
23
|
+
) -> list[dict[str, Any]]:
|
|
24
|
+
"""List all wireless networks (SSIDs) in a site (read-only).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
site_id: Site identifier
|
|
28
|
+
settings: Application settings
|
|
29
|
+
limit: Maximum number of WLANs to return
|
|
30
|
+
offset: Number of WLANs to skip
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of WLAN dictionaries
|
|
34
|
+
"""
|
|
35
|
+
site_id = validate_site_id(site_id)
|
|
36
|
+
limit, offset = validate_limit_offset(limit, offset)
|
|
37
|
+
logger = get_logger(__name__, settings.log_level)
|
|
38
|
+
|
|
39
|
+
async with UniFiClient(settings) as client:
|
|
40
|
+
await client.authenticate()
|
|
41
|
+
|
|
42
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/wlanconf")
|
|
43
|
+
# Handle both list and dict responses
|
|
44
|
+
wlans_data: list[dict[str, Any]] = (
|
|
45
|
+
response if isinstance(response, list) else response.get("data", [])
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Apply pagination
|
|
49
|
+
paginated = wlans_data[offset : offset + limit]
|
|
50
|
+
|
|
51
|
+
logger.info(f"Retrieved {len(paginated)} WLANs for site '{site_id}'")
|
|
52
|
+
return paginated
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def create_wlan(
|
|
56
|
+
site_id: str,
|
|
57
|
+
name: str,
|
|
58
|
+
security: str,
|
|
59
|
+
settings: Settings,
|
|
60
|
+
password: str | None = None,
|
|
61
|
+
enabled: bool = True,
|
|
62
|
+
is_guest: bool = False,
|
|
63
|
+
wpa_mode: str = "wpa2",
|
|
64
|
+
wpa_enc: str = "ccmp",
|
|
65
|
+
vlan_id: int | None = None,
|
|
66
|
+
hide_ssid: bool = False,
|
|
67
|
+
confirm: bool = False,
|
|
68
|
+
dry_run: bool = False,
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""Create a new wireless network (SSID).
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
site_id: Site identifier
|
|
74
|
+
name: SSID name
|
|
75
|
+
security: Security type (open, wpapsk, wpaeap)
|
|
76
|
+
settings: Application settings
|
|
77
|
+
password: WiFi password (required for wpapsk)
|
|
78
|
+
enabled: Enable the WLAN immediately
|
|
79
|
+
is_guest: Mark as guest network
|
|
80
|
+
wpa_mode: WPA mode (wpa, wpa2, wpa3)
|
|
81
|
+
wpa_enc: WPA encryption (tkip, ccmp, ccmp-tkip)
|
|
82
|
+
vlan_id: VLAN ID for network isolation
|
|
83
|
+
hide_ssid: Hide SSID from broadcast
|
|
84
|
+
confirm: Confirmation flag (must be True to execute)
|
|
85
|
+
dry_run: If True, validate but don't create the WLAN
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Created WLAN dictionary or dry-run result
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ConfirmationRequiredError: If confirm is not True
|
|
92
|
+
ValidationError: If validation fails
|
|
93
|
+
"""
|
|
94
|
+
site_id = validate_site_id(site_id)
|
|
95
|
+
validate_confirmation(confirm, "wifi operation")
|
|
96
|
+
logger = get_logger(__name__, settings.log_level)
|
|
97
|
+
|
|
98
|
+
# Validate security type
|
|
99
|
+
valid_security = ["open", "wpapsk", "wpaeap"]
|
|
100
|
+
if security not in valid_security:
|
|
101
|
+
raise ValidationError(
|
|
102
|
+
f"Invalid security type '{security}'. Must be one of: {valid_security}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Validate password required for WPA
|
|
106
|
+
if security == "wpapsk" and not password:
|
|
107
|
+
raise ValidationError("Password required for WPA/WPA2/WPA3 security")
|
|
108
|
+
|
|
109
|
+
# Validate WPA mode
|
|
110
|
+
valid_wpa_modes = ["wpa", "wpa2", "wpa3"]
|
|
111
|
+
if wpa_mode not in valid_wpa_modes:
|
|
112
|
+
raise ValidationError(f"Invalid WPA mode '{wpa_mode}'. Must be one of: {valid_wpa_modes}")
|
|
113
|
+
|
|
114
|
+
# Validate WPA encryption
|
|
115
|
+
valid_wpa_enc = ["tkip", "ccmp", "ccmp-tkip"]
|
|
116
|
+
if wpa_enc not in valid_wpa_enc:
|
|
117
|
+
raise ValidationError(
|
|
118
|
+
f"Invalid WPA encryption '{wpa_enc}'. Must be one of: {valid_wpa_enc}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Build WLAN data
|
|
122
|
+
wlan_data = {
|
|
123
|
+
"name": name,
|
|
124
|
+
"security": security,
|
|
125
|
+
"enabled": enabled,
|
|
126
|
+
"is_guest": is_guest,
|
|
127
|
+
"hide_ssid": hide_ssid,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if security == "wpapsk":
|
|
131
|
+
wlan_data["x_passphrase"] = password
|
|
132
|
+
wlan_data["wpa_mode"] = wpa_mode
|
|
133
|
+
wlan_data["wpa_enc"] = wpa_enc
|
|
134
|
+
|
|
135
|
+
if vlan_id is not None:
|
|
136
|
+
if not 1 <= vlan_id <= 4094:
|
|
137
|
+
raise ValidationError(f"Invalid VLAN ID {vlan_id}. Must be between 1 and 4094")
|
|
138
|
+
wlan_data["vlan"] = vlan_id
|
|
139
|
+
wlan_data["vlan_enabled"] = True
|
|
140
|
+
|
|
141
|
+
# Log parameters for audit (mask password)
|
|
142
|
+
parameters = {
|
|
143
|
+
"site_id": site_id,
|
|
144
|
+
"name": name,
|
|
145
|
+
"security": security,
|
|
146
|
+
"enabled": enabled,
|
|
147
|
+
"is_guest": is_guest,
|
|
148
|
+
"vlan_id": vlan_id,
|
|
149
|
+
"hide_ssid": hide_ssid,
|
|
150
|
+
"password": "***MASKED***" if password else None,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if dry_run:
|
|
154
|
+
logger.info(f"DRY RUN: Would create WLAN '{name}' in site '{site_id}'")
|
|
155
|
+
log_audit(
|
|
156
|
+
operation="create_wlan",
|
|
157
|
+
parameters=parameters,
|
|
158
|
+
result="dry_run",
|
|
159
|
+
site_id=site_id,
|
|
160
|
+
dry_run=True,
|
|
161
|
+
)
|
|
162
|
+
# Don't include password in dry-run output
|
|
163
|
+
safe_data = {k: v for k, v in wlan_data.items() if k != "x_passphrase"}
|
|
164
|
+
return {"dry_run": True, "would_create": safe_data}
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
async with UniFiClient(settings) as client:
|
|
168
|
+
await client.authenticate()
|
|
169
|
+
|
|
170
|
+
response = await client.post(f"/ea/sites/{site_id}/rest/wlanconf", json_data=wlan_data)
|
|
171
|
+
created_wlan: dict[str, Any] = response.get("data", [{}])[0]
|
|
172
|
+
|
|
173
|
+
logger.info(f"Created WLAN '{name}' in site '{site_id}'")
|
|
174
|
+
log_audit(
|
|
175
|
+
operation="create_wlan",
|
|
176
|
+
parameters=parameters,
|
|
177
|
+
result="success",
|
|
178
|
+
site_id=site_id,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return created_wlan
|
|
182
|
+
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.error(f"Failed to create WLAN '{name}': {e}")
|
|
185
|
+
log_audit(
|
|
186
|
+
operation="create_wlan",
|
|
187
|
+
parameters=parameters,
|
|
188
|
+
result="failed",
|
|
189
|
+
site_id=site_id,
|
|
190
|
+
)
|
|
191
|
+
raise
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def update_wlan(
|
|
195
|
+
site_id: str,
|
|
196
|
+
wlan_id: str,
|
|
197
|
+
settings: Settings,
|
|
198
|
+
name: str | None = None,
|
|
199
|
+
security: str | None = None,
|
|
200
|
+
password: str | None = None,
|
|
201
|
+
enabled: bool | None = None,
|
|
202
|
+
is_guest: bool | None = None,
|
|
203
|
+
wpa_mode: str | None = None,
|
|
204
|
+
wpa_enc: str | None = None,
|
|
205
|
+
vlan_id: int | None = None,
|
|
206
|
+
hide_ssid: bool | None = None,
|
|
207
|
+
confirm: bool = False,
|
|
208
|
+
dry_run: bool = False,
|
|
209
|
+
) -> dict[str, Any]:
|
|
210
|
+
"""Update an existing wireless network.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
site_id: Site identifier
|
|
214
|
+
wlan_id: WLAN ID
|
|
215
|
+
settings: Application settings
|
|
216
|
+
name: New SSID name
|
|
217
|
+
security: New security type (open, wpapsk, wpaeap)
|
|
218
|
+
password: New WiFi password
|
|
219
|
+
enabled: Enable/disable the WLAN
|
|
220
|
+
is_guest: Mark as guest network
|
|
221
|
+
wpa_mode: New WPA mode (wpa, wpa2, wpa3)
|
|
222
|
+
wpa_enc: New WPA encryption (tkip, ccmp, ccmp-tkip)
|
|
223
|
+
vlan_id: New VLAN ID
|
|
224
|
+
hide_ssid: Hide/show SSID from broadcast
|
|
225
|
+
confirm: Confirmation flag (must be True to execute)
|
|
226
|
+
dry_run: If True, validate but don't update the WLAN
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Updated WLAN dictionary or dry-run result
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
ConfirmationRequiredError: If confirm is not True
|
|
233
|
+
ResourceNotFoundError: If WLAN not found
|
|
234
|
+
"""
|
|
235
|
+
site_id = validate_site_id(site_id)
|
|
236
|
+
validate_confirmation(confirm, "wifi operation")
|
|
237
|
+
logger = get_logger(__name__, settings.log_level)
|
|
238
|
+
|
|
239
|
+
# Validate security type if provided
|
|
240
|
+
if security is not None:
|
|
241
|
+
valid_security = ["open", "wpapsk", "wpaeap"]
|
|
242
|
+
if security not in valid_security:
|
|
243
|
+
raise ValidationError(
|
|
244
|
+
f"Invalid security type '{security}'. Must be one of: {valid_security}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Validate WPA mode if provided
|
|
248
|
+
if wpa_mode is not None:
|
|
249
|
+
valid_wpa_modes = ["wpa", "wpa2", "wpa3"]
|
|
250
|
+
if wpa_mode not in valid_wpa_modes:
|
|
251
|
+
raise ValidationError(
|
|
252
|
+
f"Invalid WPA mode '{wpa_mode}'. Must be one of: {valid_wpa_modes}"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Validate WPA encryption if provided
|
|
256
|
+
if wpa_enc is not None:
|
|
257
|
+
valid_wpa_enc = ["tkip", "ccmp", "ccmp-tkip"]
|
|
258
|
+
if wpa_enc not in valid_wpa_enc:
|
|
259
|
+
raise ValidationError(
|
|
260
|
+
f"Invalid WPA encryption '{wpa_enc}'. Must be one of: {valid_wpa_enc}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Validate VLAN ID if provided
|
|
264
|
+
if vlan_id is not None and not 1 <= vlan_id <= 4094:
|
|
265
|
+
raise ValidationError(f"Invalid VLAN ID {vlan_id}. Must be between 1 and 4094")
|
|
266
|
+
|
|
267
|
+
parameters = {
|
|
268
|
+
"site_id": site_id,
|
|
269
|
+
"wlan_id": wlan_id,
|
|
270
|
+
"name": name,
|
|
271
|
+
"security": security,
|
|
272
|
+
"enabled": enabled,
|
|
273
|
+
"is_guest": is_guest,
|
|
274
|
+
"vlan_id": vlan_id,
|
|
275
|
+
"hide_ssid": hide_ssid,
|
|
276
|
+
"password": "***MASKED***" if password else None,
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if dry_run:
|
|
280
|
+
logger.info(f"DRY RUN: Would update WLAN '{wlan_id}' in site '{site_id}'")
|
|
281
|
+
log_audit(
|
|
282
|
+
operation="update_wlan",
|
|
283
|
+
parameters=parameters,
|
|
284
|
+
result="dry_run",
|
|
285
|
+
site_id=site_id,
|
|
286
|
+
dry_run=True,
|
|
287
|
+
)
|
|
288
|
+
return {"dry_run": True, "would_update": parameters}
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
async with UniFiClient(settings) as client:
|
|
292
|
+
await client.authenticate()
|
|
293
|
+
|
|
294
|
+
# Get existing WLAN
|
|
295
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/wlanconf")
|
|
296
|
+
wlans_data: list[dict[str, Any]] = response.get("data", [])
|
|
297
|
+
|
|
298
|
+
existing_wlan = None
|
|
299
|
+
for wlan in wlans_data:
|
|
300
|
+
if wlan.get("_id") == wlan_id:
|
|
301
|
+
existing_wlan = wlan
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
if not existing_wlan:
|
|
305
|
+
raise ResourceNotFoundError("wlan", wlan_id)
|
|
306
|
+
|
|
307
|
+
# Build update data
|
|
308
|
+
update_data = existing_wlan.copy()
|
|
309
|
+
|
|
310
|
+
if name is not None:
|
|
311
|
+
update_data["name"] = name
|
|
312
|
+
if security is not None:
|
|
313
|
+
update_data["security"] = security
|
|
314
|
+
if password is not None:
|
|
315
|
+
update_data["x_passphrase"] = password
|
|
316
|
+
if enabled is not None:
|
|
317
|
+
update_data["enabled"] = enabled
|
|
318
|
+
if is_guest is not None:
|
|
319
|
+
update_data["is_guest"] = is_guest
|
|
320
|
+
if wpa_mode is not None:
|
|
321
|
+
update_data["wpa_mode"] = wpa_mode
|
|
322
|
+
if wpa_enc is not None:
|
|
323
|
+
update_data["wpa_enc"] = wpa_enc
|
|
324
|
+
if vlan_id is not None:
|
|
325
|
+
update_data["vlan"] = vlan_id
|
|
326
|
+
update_data["vlan_enabled"] = True
|
|
327
|
+
if hide_ssid is not None:
|
|
328
|
+
update_data["hide_ssid"] = hide_ssid
|
|
329
|
+
|
|
330
|
+
response = await client.put(
|
|
331
|
+
f"/ea/sites/{site_id}/rest/wlanconf/{wlan_id}", json_data=update_data
|
|
332
|
+
)
|
|
333
|
+
updated_wlan: dict[str, Any] = response.get("data", [{}])[0]
|
|
334
|
+
|
|
335
|
+
logger.info(f"Updated WLAN '{wlan_id}' in site '{site_id}'")
|
|
336
|
+
log_audit(
|
|
337
|
+
operation="update_wlan",
|
|
338
|
+
parameters=parameters,
|
|
339
|
+
result="success",
|
|
340
|
+
site_id=site_id,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return updated_wlan
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error(f"Failed to update WLAN '{wlan_id}': {e}")
|
|
347
|
+
log_audit(
|
|
348
|
+
operation="update_wlan",
|
|
349
|
+
parameters=parameters,
|
|
350
|
+
result="failed",
|
|
351
|
+
site_id=site_id,
|
|
352
|
+
)
|
|
353
|
+
raise
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
async def delete_wlan(
|
|
357
|
+
site_id: str,
|
|
358
|
+
wlan_id: str,
|
|
359
|
+
settings: Settings,
|
|
360
|
+
confirm: bool = False,
|
|
361
|
+
dry_run: bool = False,
|
|
362
|
+
) -> dict[str, Any]:
|
|
363
|
+
"""Delete a wireless network.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
site_id: Site identifier
|
|
367
|
+
wlan_id: WLAN ID
|
|
368
|
+
settings: Application settings
|
|
369
|
+
confirm: Confirmation flag (must be True to execute)
|
|
370
|
+
dry_run: If True, validate but don't delete the WLAN
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Deletion result dictionary
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ConfirmationRequiredError: If confirm is not True
|
|
377
|
+
ResourceNotFoundError: If WLAN not found
|
|
378
|
+
"""
|
|
379
|
+
site_id = validate_site_id(site_id)
|
|
380
|
+
validate_confirmation(confirm, "wifi operation")
|
|
381
|
+
logger = get_logger(__name__, settings.log_level)
|
|
382
|
+
|
|
383
|
+
parameters = {"site_id": site_id, "wlan_id": wlan_id}
|
|
384
|
+
|
|
385
|
+
if dry_run:
|
|
386
|
+
logger.info(f"DRY RUN: Would delete WLAN '{wlan_id}' from site '{site_id}'")
|
|
387
|
+
log_audit(
|
|
388
|
+
operation="delete_wlan",
|
|
389
|
+
parameters=parameters,
|
|
390
|
+
result="dry_run",
|
|
391
|
+
site_id=site_id,
|
|
392
|
+
dry_run=True,
|
|
393
|
+
)
|
|
394
|
+
return {"dry_run": True, "would_delete": wlan_id}
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
async with UniFiClient(settings) as client:
|
|
398
|
+
await client.authenticate()
|
|
399
|
+
|
|
400
|
+
# Verify WLAN exists before deleting
|
|
401
|
+
response = await client.get(f"/ea/sites/{site_id}/rest/wlanconf")
|
|
402
|
+
wlans_data: list[dict[str, Any]] = response.get("data", [])
|
|
403
|
+
|
|
404
|
+
wlan_exists = any(wlan.get("_id") == wlan_id for wlan in wlans_data)
|
|
405
|
+
if not wlan_exists:
|
|
406
|
+
raise ResourceNotFoundError("wlan", wlan_id)
|
|
407
|
+
|
|
408
|
+
response = await client.delete(f"/ea/sites/{site_id}/rest/wlanconf/{wlan_id}")
|
|
409
|
+
|
|
410
|
+
logger.info(f"Deleted WLAN '{wlan_id}' from site '{site_id}'")
|
|
411
|
+
log_audit(
|
|
412
|
+
operation="delete_wlan",
|
|
413
|
+
parameters=parameters,
|
|
414
|
+
result="success",
|
|
415
|
+
site_id=site_id,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
return {"success": True, "deleted_wlan_id": wlan_id}
|
|
419
|
+
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.error(f"Failed to delete WLAN '{wlan_id}': {e}")
|
|
422
|
+
log_audit(
|
|
423
|
+
operation="delete_wlan",
|
|
424
|
+
parameters=parameters,
|
|
425
|
+
result="failed",
|
|
426
|
+
site_id=site_id,
|
|
427
|
+
)
|
|
428
|
+
raise
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
async def get_wlan_statistics(
|
|
432
|
+
site_id: str,
|
|
433
|
+
settings: Settings,
|
|
434
|
+
wlan_id: str | None = None,
|
|
435
|
+
) -> dict[str, Any]:
|
|
436
|
+
"""Get WiFi usage statistics.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
site_id: Site identifier
|
|
440
|
+
settings: Application settings
|
|
441
|
+
wlan_id: Optional WLAN ID to filter statistics
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
WLAN statistics dictionary
|
|
445
|
+
"""
|
|
446
|
+
site_id = validate_site_id(site_id)
|
|
447
|
+
logger = get_logger(__name__, settings.log_level)
|
|
448
|
+
|
|
449
|
+
async with UniFiClient(settings) as client:
|
|
450
|
+
await client.authenticate()
|
|
451
|
+
|
|
452
|
+
# Get WLANs
|
|
453
|
+
wlans_response = await client.get(f"/ea/sites/{site_id}/rest/wlanconf")
|
|
454
|
+
wlans_data = wlans_response.get("data", [])
|
|
455
|
+
|
|
456
|
+
# Get active clients
|
|
457
|
+
clients_response = await client.get(f"/ea/sites/{site_id}/sta")
|
|
458
|
+
clients_data = clients_response.get("data", [])
|
|
459
|
+
|
|
460
|
+
# Calculate statistics per WLAN
|
|
461
|
+
wlan_stats = []
|
|
462
|
+
for wlan in wlans_data:
|
|
463
|
+
wlan_identifier = wlan.get("_id")
|
|
464
|
+
wlan_name = wlan.get("name")
|
|
465
|
+
|
|
466
|
+
# Skip if filtering by WLAN ID and this isn't it
|
|
467
|
+
if wlan_id and wlan_identifier != wlan_id:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# Count clients on this WLAN (match by essid/name)
|
|
471
|
+
clients_on_wlan = [
|
|
472
|
+
c for c in clients_data if c.get("essid") == wlan_name or c.get("is_wired") is False
|
|
473
|
+
]
|
|
474
|
+
|
|
475
|
+
# Calculate total bandwidth
|
|
476
|
+
total_tx = sum(c.get("tx_bytes", 0) for c in clients_on_wlan)
|
|
477
|
+
total_rx = sum(c.get("rx_bytes", 0) for c in clients_on_wlan)
|
|
478
|
+
|
|
479
|
+
wlan_stats.append(
|
|
480
|
+
{
|
|
481
|
+
"wlan_id": wlan_identifier,
|
|
482
|
+
"name": wlan_name,
|
|
483
|
+
"enabled": wlan.get("enabled", False),
|
|
484
|
+
"security": wlan.get("security"),
|
|
485
|
+
"is_guest": wlan.get("is_guest", False),
|
|
486
|
+
"client_count": len(clients_on_wlan),
|
|
487
|
+
"total_tx_bytes": total_tx,
|
|
488
|
+
"total_rx_bytes": total_rx,
|
|
489
|
+
"total_bytes": total_tx + total_rx,
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
logger.info(f"Retrieved WLAN statistics for site '{site_id}'")
|
|
494
|
+
|
|
495
|
+
if wlan_id:
|
|
496
|
+
return wlan_stats[0] if wlan_stats else {}
|
|
497
|
+
else:
|
|
498
|
+
return {"site_id": site_id, "wlans": wlan_stats}
|