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,371 @@
|
|
|
1
|
+
"""Traffic Matching List management MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..api import UniFiClient
|
|
6
|
+
from ..config import Settings
|
|
7
|
+
from ..models.traffic_matching_list import TrafficMatchingList, TrafficMatchingListCreate
|
|
8
|
+
from ..utils import (
|
|
9
|
+
ResourceNotFoundError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
get_logger,
|
|
12
|
+
log_audit,
|
|
13
|
+
validate_confirmation,
|
|
14
|
+
validate_limit_offset,
|
|
15
|
+
validate_site_id,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def list_traffic_matching_lists(
|
|
20
|
+
site_id: str,
|
|
21
|
+
settings: Settings,
|
|
22
|
+
limit: int | None = None,
|
|
23
|
+
offset: int | None = None,
|
|
24
|
+
) -> list[dict[str, Any]]:
|
|
25
|
+
"""List all traffic matching lists in a site (read-only).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
site_id: Site identifier
|
|
29
|
+
settings: Application settings
|
|
30
|
+
limit: Maximum number of lists to return
|
|
31
|
+
offset: Number of lists to skip
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of traffic matching list dictionaries
|
|
35
|
+
"""
|
|
36
|
+
site_id = validate_site_id(site_id)
|
|
37
|
+
limit, offset = validate_limit_offset(limit, offset)
|
|
38
|
+
logger = get_logger(__name__, settings.log_level)
|
|
39
|
+
|
|
40
|
+
async with UniFiClient(settings) as client:
|
|
41
|
+
await client.authenticate()
|
|
42
|
+
|
|
43
|
+
response = await client.get(f"/integration/v1/sites/{site_id}/traffic-matching-lists")
|
|
44
|
+
lists_data: list[dict[str, Any]] = response.get("data", [])
|
|
45
|
+
|
|
46
|
+
# Apply pagination
|
|
47
|
+
paginated = lists_data[offset : offset + limit]
|
|
48
|
+
|
|
49
|
+
logger.info(f"Retrieved {len(paginated)} traffic matching lists for site '{site_id}'")
|
|
50
|
+
return [TrafficMatchingList(**lst).model_dump() for lst in paginated]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def get_traffic_matching_list(
|
|
54
|
+
site_id: str,
|
|
55
|
+
list_id: str,
|
|
56
|
+
settings: Settings,
|
|
57
|
+
) -> dict[str, Any]:
|
|
58
|
+
"""Get details for a specific traffic matching list.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
site_id: Site identifier
|
|
62
|
+
list_id: Traffic matching list ID
|
|
63
|
+
settings: Application settings
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Traffic matching list dictionary
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ResourceNotFoundError: If list not found
|
|
70
|
+
"""
|
|
71
|
+
site_id = validate_site_id(site_id)
|
|
72
|
+
logger = get_logger(__name__, settings.log_level)
|
|
73
|
+
|
|
74
|
+
async with UniFiClient(settings) as client:
|
|
75
|
+
await client.authenticate()
|
|
76
|
+
|
|
77
|
+
response = await client.get(
|
|
78
|
+
f"/integration/v1/sites/{site_id}/traffic-matching-lists/{list_id}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if isinstance(response, dict) and "data" in response:
|
|
82
|
+
list_data = response["data"]
|
|
83
|
+
else:
|
|
84
|
+
list_data = response
|
|
85
|
+
|
|
86
|
+
if not list_data:
|
|
87
|
+
raise ResourceNotFoundError("traffic_matching_list", list_id)
|
|
88
|
+
|
|
89
|
+
logger.info(f"Retrieved traffic matching list {list_id}")
|
|
90
|
+
return TrafficMatchingList(**list_data).model_dump() # type: ignore[no-any-return]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def create_traffic_matching_list(
|
|
94
|
+
site_id: str,
|
|
95
|
+
list_type: str,
|
|
96
|
+
name: str,
|
|
97
|
+
items: list[str],
|
|
98
|
+
settings: Settings,
|
|
99
|
+
confirm: bool = False,
|
|
100
|
+
dry_run: bool = False,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""Create a new traffic matching list.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
site_id: Site identifier
|
|
106
|
+
list_type: List type (PORTS, IPV4_ADDRESSES, IPV6_ADDRESSES)
|
|
107
|
+
name: List name
|
|
108
|
+
items: List items (ports, IPs, etc.)
|
|
109
|
+
settings: Application settings
|
|
110
|
+
confirm: Confirmation flag (must be True to execute)
|
|
111
|
+
dry_run: If True, validate but don't create
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Created list dictionary or dry-run result
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ConfirmationRequiredError: If confirm is not True
|
|
118
|
+
ValidationError: If validation fails
|
|
119
|
+
"""
|
|
120
|
+
site_id = validate_site_id(site_id)
|
|
121
|
+
validate_confirmation(confirm, "traffic matching list operation")
|
|
122
|
+
logger = get_logger(__name__, settings.log_level)
|
|
123
|
+
|
|
124
|
+
# Validate list type
|
|
125
|
+
valid_types = ["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"]
|
|
126
|
+
if list_type not in valid_types:
|
|
127
|
+
raise ValidationError(f"Invalid list type '{list_type}'. Must be one of: {valid_types}")
|
|
128
|
+
|
|
129
|
+
# Validate items not empty
|
|
130
|
+
if not items or len(items) == 0:
|
|
131
|
+
raise ValidationError("Items list cannot be empty")
|
|
132
|
+
|
|
133
|
+
# Build list data
|
|
134
|
+
create_data = TrafficMatchingListCreate(type=list_type, name=name, items=items)
|
|
135
|
+
|
|
136
|
+
parameters = {
|
|
137
|
+
"site_id": site_id,
|
|
138
|
+
"type": list_type,
|
|
139
|
+
"name": name,
|
|
140
|
+
"items_count": len(items),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if dry_run:
|
|
144
|
+
logger.info(f"DRY RUN: Would create traffic matching list '{name}' in site '{site_id}'")
|
|
145
|
+
log_audit(
|
|
146
|
+
operation="create_traffic_matching_list",
|
|
147
|
+
parameters=parameters,
|
|
148
|
+
result="dry_run",
|
|
149
|
+
site_id=site_id,
|
|
150
|
+
dry_run=True,
|
|
151
|
+
)
|
|
152
|
+
return {"dry_run": True, "would_create": create_data.model_dump()}
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
async with UniFiClient(settings) as client:
|
|
156
|
+
await client.authenticate()
|
|
157
|
+
|
|
158
|
+
response = await client.post(
|
|
159
|
+
f"/integration/v1/sites/{site_id}/traffic-matching-lists",
|
|
160
|
+
json_data=create_data.model_dump(),
|
|
161
|
+
)
|
|
162
|
+
created_list: dict[str, Any] = response.get("data", response)
|
|
163
|
+
|
|
164
|
+
logger.info(f"Created traffic matching list '{name}' in site '{site_id}'")
|
|
165
|
+
log_audit(
|
|
166
|
+
operation="create_traffic_matching_list",
|
|
167
|
+
parameters=parameters,
|
|
168
|
+
result="success",
|
|
169
|
+
site_id=site_id,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return created_list
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Failed to create traffic matching list '{name}': {e}")
|
|
176
|
+
log_audit(
|
|
177
|
+
operation="create_traffic_matching_list",
|
|
178
|
+
parameters=parameters,
|
|
179
|
+
result="failed",
|
|
180
|
+
site_id=site_id,
|
|
181
|
+
)
|
|
182
|
+
raise
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def update_traffic_matching_list(
|
|
186
|
+
site_id: str,
|
|
187
|
+
list_id: str,
|
|
188
|
+
settings: Settings,
|
|
189
|
+
list_type: str | None = None,
|
|
190
|
+
name: str | None = None,
|
|
191
|
+
items: list[str] | None = None,
|
|
192
|
+
confirm: bool = False,
|
|
193
|
+
dry_run: bool = False,
|
|
194
|
+
) -> dict[str, Any]:
|
|
195
|
+
"""Update an existing traffic matching list.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
site_id: Site identifier
|
|
199
|
+
list_id: Traffic matching list ID
|
|
200
|
+
settings: Application settings
|
|
201
|
+
list_type: New list type
|
|
202
|
+
name: New list name
|
|
203
|
+
items: New list items
|
|
204
|
+
confirm: Confirmation flag (must be True to execute)
|
|
205
|
+
dry_run: If True, validate but don't update
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Updated list dictionary or dry-run result
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
ConfirmationRequiredError: If confirm is not True
|
|
212
|
+
ResourceNotFoundError: If list not found
|
|
213
|
+
"""
|
|
214
|
+
site_id = validate_site_id(site_id)
|
|
215
|
+
validate_confirmation(confirm, "traffic matching list operation")
|
|
216
|
+
logger = get_logger(__name__, settings.log_level)
|
|
217
|
+
|
|
218
|
+
# Validate list type if provided
|
|
219
|
+
if list_type is not None:
|
|
220
|
+
valid_types = ["PORTS", "IPV4_ADDRESSES", "IPV6_ADDRESSES"]
|
|
221
|
+
if list_type not in valid_types:
|
|
222
|
+
raise ValidationError(f"Invalid list type '{list_type}'. Must be one of: {valid_types}")
|
|
223
|
+
|
|
224
|
+
# Validate items if provided
|
|
225
|
+
if items is not None and len(items) == 0:
|
|
226
|
+
raise ValidationError("Items list cannot be empty")
|
|
227
|
+
|
|
228
|
+
parameters = {
|
|
229
|
+
"site_id": site_id,
|
|
230
|
+
"list_id": list_id,
|
|
231
|
+
"type": list_type,
|
|
232
|
+
"name": name,
|
|
233
|
+
"items_count": len(items) if items else None,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if dry_run:
|
|
237
|
+
logger.info(f"DRY RUN: Would update traffic matching list '{list_id}' in site '{site_id}'")
|
|
238
|
+
log_audit(
|
|
239
|
+
operation="update_traffic_matching_list",
|
|
240
|
+
parameters=parameters,
|
|
241
|
+
result="dry_run",
|
|
242
|
+
site_id=site_id,
|
|
243
|
+
dry_run=True,
|
|
244
|
+
)
|
|
245
|
+
return {"dry_run": True, "would_update": parameters}
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
async with UniFiClient(settings) as client:
|
|
249
|
+
await client.authenticate()
|
|
250
|
+
|
|
251
|
+
# Get existing list
|
|
252
|
+
response = await client.get(
|
|
253
|
+
f"/integration/v1/sites/{site_id}/traffic-matching-lists/{list_id}"
|
|
254
|
+
)
|
|
255
|
+
existing_list = response.get("data", response)
|
|
256
|
+
|
|
257
|
+
if not existing_list:
|
|
258
|
+
raise ResourceNotFoundError("traffic_matching_list", list_id)
|
|
259
|
+
|
|
260
|
+
# Build update data
|
|
261
|
+
update_data = existing_list.copy()
|
|
262
|
+
|
|
263
|
+
if list_type is not None:
|
|
264
|
+
update_data["type"] = list_type
|
|
265
|
+
if name is not None:
|
|
266
|
+
update_data["name"] = name
|
|
267
|
+
if items is not None:
|
|
268
|
+
update_data["items"] = items
|
|
269
|
+
|
|
270
|
+
response = await client.put(
|
|
271
|
+
f"/integration/v1/sites/{site_id}/traffic-matching-lists/{list_id}",
|
|
272
|
+
json_data=update_data,
|
|
273
|
+
)
|
|
274
|
+
updated_list: dict[str, Any] = response.get("data", response)
|
|
275
|
+
|
|
276
|
+
logger.info(f"Updated traffic matching list '{list_id}' in site '{site_id}'")
|
|
277
|
+
log_audit(
|
|
278
|
+
operation="update_traffic_matching_list",
|
|
279
|
+
parameters=parameters,
|
|
280
|
+
result="success",
|
|
281
|
+
site_id=site_id,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return updated_list
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f"Failed to update traffic matching list '{list_id}': {e}")
|
|
288
|
+
log_audit(
|
|
289
|
+
operation="update_traffic_matching_list",
|
|
290
|
+
parameters=parameters,
|
|
291
|
+
result="failed",
|
|
292
|
+
site_id=site_id,
|
|
293
|
+
)
|
|
294
|
+
raise
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def delete_traffic_matching_list(
|
|
298
|
+
site_id: str,
|
|
299
|
+
list_id: str,
|
|
300
|
+
settings: Settings,
|
|
301
|
+
confirm: bool = False,
|
|
302
|
+
dry_run: bool = False,
|
|
303
|
+
) -> dict[str, Any]:
|
|
304
|
+
"""Delete a traffic matching list.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
site_id: Site identifier
|
|
308
|
+
list_id: Traffic matching list ID
|
|
309
|
+
settings: Application settings
|
|
310
|
+
confirm: Confirmation flag (must be True to execute)
|
|
311
|
+
dry_run: If True, validate but don't delete
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Deletion result dictionary
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
ConfirmationRequiredError: If confirm is not True
|
|
318
|
+
ResourceNotFoundError: If list not found
|
|
319
|
+
"""
|
|
320
|
+
site_id = validate_site_id(site_id)
|
|
321
|
+
validate_confirmation(confirm, "traffic matching list operation")
|
|
322
|
+
logger = get_logger(__name__, settings.log_level)
|
|
323
|
+
|
|
324
|
+
parameters = {"site_id": site_id, "list_id": list_id}
|
|
325
|
+
|
|
326
|
+
if dry_run:
|
|
327
|
+
logger.info(
|
|
328
|
+
f"DRY RUN: Would delete traffic matching list '{list_id}' from site '{site_id}'"
|
|
329
|
+
)
|
|
330
|
+
log_audit(
|
|
331
|
+
operation="delete_traffic_matching_list",
|
|
332
|
+
parameters=parameters,
|
|
333
|
+
result="dry_run",
|
|
334
|
+
site_id=site_id,
|
|
335
|
+
dry_run=True,
|
|
336
|
+
)
|
|
337
|
+
return {"dry_run": True, "would_delete": list_id}
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
async with UniFiClient(settings) as client:
|
|
341
|
+
await client.authenticate()
|
|
342
|
+
|
|
343
|
+
# Verify list exists before deleting
|
|
344
|
+
try:
|
|
345
|
+
await client.get(
|
|
346
|
+
f"/integration/v1/sites/{site_id}/traffic-matching-lists/{list_id}"
|
|
347
|
+
)
|
|
348
|
+
except Exception as err:
|
|
349
|
+
raise ResourceNotFoundError("traffic_matching_list", list_id) from err
|
|
350
|
+
|
|
351
|
+
await client.delete(f"/integration/v1/sites/{site_id}/traffic-matching-lists/{list_id}")
|
|
352
|
+
|
|
353
|
+
logger.info(f"Deleted traffic matching list '{list_id}' from site '{site_id}'")
|
|
354
|
+
log_audit(
|
|
355
|
+
operation="delete_traffic_matching_list",
|
|
356
|
+
parameters=parameters,
|
|
357
|
+
result="success",
|
|
358
|
+
site_id=site_id,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return {"success": True, "deleted_list_id": list_id}
|
|
362
|
+
|
|
363
|
+
except Exception as e:
|
|
364
|
+
logger.error(f"Failed to delete traffic matching list '{list_id}': {e}")
|
|
365
|
+
log_audit(
|
|
366
|
+
operation="delete_traffic_matching_list",
|
|
367
|
+
parameters=parameters,
|
|
368
|
+
result="failed",
|
|
369
|
+
site_id=site_id,
|
|
370
|
+
)
|
|
371
|
+
raise
|
src/tools/vouchers.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Hotspot voucher management tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..api.client import UniFiClient
|
|
6
|
+
from ..config import Settings
|
|
7
|
+
from ..models import Voucher
|
|
8
|
+
from ..utils import audit_action, get_logger, validate_confirmation
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def list_vouchers(
|
|
14
|
+
site_id: str,
|
|
15
|
+
settings: Settings,
|
|
16
|
+
limit: int | None = None,
|
|
17
|
+
offset: int | None = None,
|
|
18
|
+
filter_expr: str | None = None,
|
|
19
|
+
) -> list[dict]:
|
|
20
|
+
"""List all hotspot vouchers for a site.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
site_id: Site identifier
|
|
24
|
+
settings: Application settings
|
|
25
|
+
limit: Maximum number of results
|
|
26
|
+
offset: Starting position
|
|
27
|
+
filter_expr: Filter expression
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of vouchers
|
|
31
|
+
"""
|
|
32
|
+
async with UniFiClient(settings) as client:
|
|
33
|
+
logger.info(f"Listing vouchers for site {site_id}")
|
|
34
|
+
|
|
35
|
+
if not client.is_authenticated:
|
|
36
|
+
await client.authenticate()
|
|
37
|
+
|
|
38
|
+
params: dict[str, Any] = {}
|
|
39
|
+
if limit is not None:
|
|
40
|
+
params["limit"] = limit
|
|
41
|
+
if offset is not None:
|
|
42
|
+
params["offset"] = offset
|
|
43
|
+
if filter_expr:
|
|
44
|
+
params["filter"] = filter_expr
|
|
45
|
+
|
|
46
|
+
response = await client.get(f"/integration/v1/sites/{site_id}/vouchers", params=params)
|
|
47
|
+
data = response.get("data", [])
|
|
48
|
+
|
|
49
|
+
return [Voucher(**voucher).model_dump() for voucher in data]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def get_voucher(site_id: str, voucher_id: str, settings: Settings) -> dict:
|
|
53
|
+
"""Get details for a specific voucher.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
site_id: Site identifier
|
|
57
|
+
voucher_id: Voucher identifier
|
|
58
|
+
settings: Application settings
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Voucher details
|
|
62
|
+
"""
|
|
63
|
+
async with UniFiClient(settings) as client:
|
|
64
|
+
logger.info(f"Getting voucher {voucher_id} for site {site_id}")
|
|
65
|
+
|
|
66
|
+
if not client.is_authenticated:
|
|
67
|
+
await client.authenticate()
|
|
68
|
+
|
|
69
|
+
response = await client.get(f"/integration/v1/sites/{site_id}/vouchers/{voucher_id}")
|
|
70
|
+
data = response.get("data", response)
|
|
71
|
+
|
|
72
|
+
return Voucher(**data).model_dump() # type: ignore[no-any-return]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def create_vouchers(
|
|
76
|
+
site_id: str,
|
|
77
|
+
count: int,
|
|
78
|
+
duration: int,
|
|
79
|
+
settings: Settings,
|
|
80
|
+
upload_limit_kbps: int | None = None,
|
|
81
|
+
download_limit_kbps: int | None = None,
|
|
82
|
+
upload_quota_mb: int | None = None,
|
|
83
|
+
download_quota_mb: int | None = None,
|
|
84
|
+
note: str | None = None,
|
|
85
|
+
confirm: bool = False,
|
|
86
|
+
dry_run: bool = False,
|
|
87
|
+
) -> dict:
|
|
88
|
+
"""Create new hotspot vouchers.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
site_id: Site identifier
|
|
92
|
+
count: Number of vouchers to create
|
|
93
|
+
duration: Duration in seconds
|
|
94
|
+
settings: Application settings
|
|
95
|
+
upload_limit_kbps: Upload speed limit in kbps
|
|
96
|
+
download_limit_kbps: Download speed limit in kbps
|
|
97
|
+
upload_quota_mb: Upload quota in MB
|
|
98
|
+
download_quota_mb: Download quota in MB
|
|
99
|
+
note: Admin notes
|
|
100
|
+
confirm: Confirmation flag (required)
|
|
101
|
+
dry_run: If True, validate but don't execute
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Created voucher codes
|
|
105
|
+
"""
|
|
106
|
+
validate_confirmation(confirm, "create vouchers")
|
|
107
|
+
|
|
108
|
+
async with UniFiClient(settings) as client:
|
|
109
|
+
logger.info(f"Creating {count} vouchers for site {site_id}")
|
|
110
|
+
|
|
111
|
+
if not client.is_authenticated:
|
|
112
|
+
await client.authenticate()
|
|
113
|
+
|
|
114
|
+
# Build request payload
|
|
115
|
+
payload: dict[str, Any] = {
|
|
116
|
+
"count": count,
|
|
117
|
+
"duration": duration,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if upload_limit_kbps is not None:
|
|
121
|
+
payload["uploadLimit"] = upload_limit_kbps
|
|
122
|
+
if download_limit_kbps is not None:
|
|
123
|
+
payload["downloadLimit"] = download_limit_kbps
|
|
124
|
+
if upload_quota_mb is not None:
|
|
125
|
+
payload["uploadQuota"] = upload_quota_mb
|
|
126
|
+
if download_quota_mb is not None:
|
|
127
|
+
payload["downloadQuota"] = download_quota_mb
|
|
128
|
+
if note:
|
|
129
|
+
payload["note"] = note
|
|
130
|
+
|
|
131
|
+
if dry_run:
|
|
132
|
+
logger.info(f"[DRY RUN] Would create vouchers with payload: {payload}")
|
|
133
|
+
return {"dry_run": True, "payload": payload}
|
|
134
|
+
|
|
135
|
+
response = await client.post(f"/integration/v1/sites/{site_id}/vouchers", json_data=payload)
|
|
136
|
+
data = response.get("data", response)
|
|
137
|
+
|
|
138
|
+
# Audit the action
|
|
139
|
+
await audit_action(
|
|
140
|
+
settings,
|
|
141
|
+
action_type="create_vouchers",
|
|
142
|
+
resource_type="voucher",
|
|
143
|
+
resource_id="bulk",
|
|
144
|
+
site_id=site_id,
|
|
145
|
+
details={"count": count, "duration": duration},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"success": True,
|
|
150
|
+
"count": count,
|
|
151
|
+
"vouchers": data if isinstance(data, list) else [data],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def delete_voucher(
|
|
156
|
+
site_id: str,
|
|
157
|
+
voucher_id: str,
|
|
158
|
+
settings: Settings,
|
|
159
|
+
confirm: bool = False,
|
|
160
|
+
dry_run: bool = False,
|
|
161
|
+
) -> dict:
|
|
162
|
+
"""Delete a specific voucher.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
site_id: Site identifier
|
|
166
|
+
voucher_id: Voucher identifier
|
|
167
|
+
settings: Application settings
|
|
168
|
+
confirm: Confirmation flag (required)
|
|
169
|
+
dry_run: If True, validate but don't execute
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Deletion status
|
|
173
|
+
"""
|
|
174
|
+
validate_confirmation(confirm, "delete voucher")
|
|
175
|
+
|
|
176
|
+
async with UniFiClient(settings) as client:
|
|
177
|
+
logger.info(f"Deleting voucher {voucher_id} for site {site_id}")
|
|
178
|
+
|
|
179
|
+
if not client.is_authenticated:
|
|
180
|
+
await client.authenticate()
|
|
181
|
+
|
|
182
|
+
if dry_run:
|
|
183
|
+
logger.info(f"[DRY RUN] Would delete voucher {voucher_id}")
|
|
184
|
+
return {"dry_run": True, "voucher_id": voucher_id}
|
|
185
|
+
|
|
186
|
+
await client.delete(f"/integration/v1/sites/{site_id}/vouchers/{voucher_id}")
|
|
187
|
+
|
|
188
|
+
# Audit the action
|
|
189
|
+
await audit_action(
|
|
190
|
+
settings,
|
|
191
|
+
action_type="delete_voucher",
|
|
192
|
+
resource_type="voucher",
|
|
193
|
+
resource_id=voucher_id,
|
|
194
|
+
site_id=site_id,
|
|
195
|
+
details={},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return {"success": True, "message": f"Voucher {voucher_id} deleted successfully"}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def bulk_delete_vouchers(
|
|
202
|
+
site_id: str,
|
|
203
|
+
filter_expr: str,
|
|
204
|
+
settings: Settings,
|
|
205
|
+
confirm: bool = False,
|
|
206
|
+
dry_run: bool = False,
|
|
207
|
+
) -> dict:
|
|
208
|
+
"""Bulk delete vouchers using a filter expression.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
site_id: Site identifier
|
|
212
|
+
filter_expr: Filter expression to select vouchers
|
|
213
|
+
settings: Application settings
|
|
214
|
+
confirm: Confirmation flag (required)
|
|
215
|
+
dry_run: If True, validate but don't execute
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Deletion status
|
|
219
|
+
"""
|
|
220
|
+
validate_confirmation(confirm, "bulk delete vouchers")
|
|
221
|
+
|
|
222
|
+
async with UniFiClient(settings) as client:
|
|
223
|
+
logger.info(f"Bulk deleting vouchers for site {site_id} with filter: {filter_expr}")
|
|
224
|
+
|
|
225
|
+
if not client.is_authenticated:
|
|
226
|
+
await client.authenticate()
|
|
227
|
+
|
|
228
|
+
if dry_run:
|
|
229
|
+
logger.info(f"[DRY RUN] Would bulk delete vouchers with filter: {filter_expr}")
|
|
230
|
+
return {"dry_run": True, "filter": filter_expr}
|
|
231
|
+
|
|
232
|
+
params = {"filter": filter_expr}
|
|
233
|
+
response = await client.delete(f"/integration/v1/sites/{site_id}/vouchers", params=params)
|
|
234
|
+
|
|
235
|
+
# Audit the action
|
|
236
|
+
await audit_action(
|
|
237
|
+
settings,
|
|
238
|
+
action_type="bulk_delete_vouchers",
|
|
239
|
+
resource_type="voucher",
|
|
240
|
+
resource_id="bulk",
|
|
241
|
+
site_id=site_id,
|
|
242
|
+
details={"filter": filter_expr},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"success": True,
|
|
247
|
+
"message": "Vouchers deleted successfully",
|
|
248
|
+
"deleted_count": response.get("data", {}).get("count", 0),
|
|
249
|
+
}
|
src/tools/vpn.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""VPN management MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..api import UniFiClient
|
|
6
|
+
from ..config import Settings
|
|
7
|
+
from ..models.vpn import VPNServer, VPNTunnel
|
|
8
|
+
from ..utils import get_logger, validate_limit_offset, validate_site_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def list_vpn_tunnels(
|
|
12
|
+
site_id: str,
|
|
13
|
+
settings: Settings,
|
|
14
|
+
limit: int | None = None,
|
|
15
|
+
offset: int | None = None,
|
|
16
|
+
) -> list[dict[str, Any]]:
|
|
17
|
+
"""List all site-to-site VPN tunnels in a site (read-only).
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
site_id: Site identifier
|
|
21
|
+
settings: Application settings
|
|
22
|
+
limit: Maximum number of tunnels to return
|
|
23
|
+
offset: Number of tunnels to skip
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of VPN tunnel dictionaries
|
|
27
|
+
"""
|
|
28
|
+
site_id = validate_site_id(site_id)
|
|
29
|
+
limit, offset = validate_limit_offset(limit, offset)
|
|
30
|
+
logger = get_logger(__name__, settings.log_level)
|
|
31
|
+
|
|
32
|
+
async with UniFiClient(settings) as client:
|
|
33
|
+
await client.authenticate()
|
|
34
|
+
|
|
35
|
+
response = await client.get(f"/integration/v1/sites/{site_id}/vpn/site-to-site-tunnels")
|
|
36
|
+
tunnels_data: list[dict[str, Any]] = response.get("data", [])
|
|
37
|
+
|
|
38
|
+
# Apply pagination
|
|
39
|
+
paginated = tunnels_data[offset : offset + limit]
|
|
40
|
+
|
|
41
|
+
logger.info(f"Retrieved {len(paginated)} VPN tunnels for site '{site_id}'")
|
|
42
|
+
return [VPNTunnel(**tunnel).model_dump() for tunnel in paginated]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def list_vpn_servers(
|
|
46
|
+
site_id: str,
|
|
47
|
+
settings: Settings,
|
|
48
|
+
limit: int | None = None,
|
|
49
|
+
offset: int | None = None,
|
|
50
|
+
) -> list[dict[str, Any]]:
|
|
51
|
+
"""List all VPN servers in a site (read-only).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
site_id: Site identifier
|
|
55
|
+
settings: Application settings
|
|
56
|
+
limit: Maximum number of servers to return
|
|
57
|
+
offset: Number of servers to skip
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of VPN server dictionaries
|
|
61
|
+
"""
|
|
62
|
+
site_id = validate_site_id(site_id)
|
|
63
|
+
limit, offset = validate_limit_offset(limit, offset)
|
|
64
|
+
logger = get_logger(__name__, settings.log_level)
|
|
65
|
+
|
|
66
|
+
async with UniFiClient(settings) as client:
|
|
67
|
+
await client.authenticate()
|
|
68
|
+
|
|
69
|
+
response = await client.get(f"/integration/v1/sites/{site_id}/vpn/servers")
|
|
70
|
+
servers_data: list[dict[str, Any]] = response.get("data", [])
|
|
71
|
+
|
|
72
|
+
# Apply pagination
|
|
73
|
+
paginated = servers_data[offset : offset + limit]
|
|
74
|
+
|
|
75
|
+
logger.info(f"Retrieved {len(paginated)} VPN servers for site '{site_id}'")
|
|
76
|
+
return [VPNServer(**server).model_dump() for server in paginated]
|