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/sites.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Site management MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..api import UniFiClient
|
|
6
|
+
from ..config import Settings
|
|
7
|
+
from ..models import Site
|
|
8
|
+
from ..utils import ResourceNotFoundError, get_logger, validate_limit_offset, validate_site_id
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def get_site_details(site_id: str, settings: Settings) -> dict[str, Any]:
|
|
12
|
+
"""Get detailed site information.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
site_id: Site identifier
|
|
16
|
+
settings: Application settings
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Site details dictionary
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ResourceNotFoundError: If site not found
|
|
23
|
+
"""
|
|
24
|
+
site_id = validate_site_id(site_id)
|
|
25
|
+
logger = get_logger(__name__, settings.log_level)
|
|
26
|
+
|
|
27
|
+
async with UniFiClient(settings) as client:
|
|
28
|
+
await client.authenticate()
|
|
29
|
+
|
|
30
|
+
response = await client.get("/ea/sites")
|
|
31
|
+
|
|
32
|
+
# Handle both local and cloud API response formats
|
|
33
|
+
if isinstance(response, list):
|
|
34
|
+
sites_data = response
|
|
35
|
+
else:
|
|
36
|
+
sites_data = response.get("data", [])
|
|
37
|
+
|
|
38
|
+
for site_data in sites_data:
|
|
39
|
+
if site_data.get("_id") == site_id or site_data.get("name") == site_id:
|
|
40
|
+
site = Site(**site_data)
|
|
41
|
+
logger.info(f"Retrieved site details for {site_id}")
|
|
42
|
+
return site.model_dump() # type: ignore[no-any-return]
|
|
43
|
+
|
|
44
|
+
raise ResourceNotFoundError("site", site_id)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def list_sites(
|
|
48
|
+
settings: Settings, limit: int | None = None, offset: int | None = None
|
|
49
|
+
) -> list[dict[str, Any]]:
|
|
50
|
+
"""List all accessible sites.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
settings: Application settings
|
|
54
|
+
limit: Maximum number of sites to return
|
|
55
|
+
offset: Number of sites to skip
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of site dictionaries
|
|
59
|
+
"""
|
|
60
|
+
limit, offset = validate_limit_offset(limit, offset)
|
|
61
|
+
logger = get_logger(__name__, settings.log_level)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
async with UniFiClient(settings) as client:
|
|
65
|
+
await client.authenticate()
|
|
66
|
+
|
|
67
|
+
# Use correct endpoint based on API type
|
|
68
|
+
if settings.api_type.value == "local":
|
|
69
|
+
endpoint = settings.get_integration_path("sites")
|
|
70
|
+
else:
|
|
71
|
+
endpoint = "/ea/sites"
|
|
72
|
+
|
|
73
|
+
logger.debug(f"Fetching sites from endpoint: {endpoint}")
|
|
74
|
+
response = await client.get(endpoint)
|
|
75
|
+
logger.debug(f"Raw response: {response}")
|
|
76
|
+
|
|
77
|
+
# Handle both local and cloud API response formats
|
|
78
|
+
if isinstance(response, list):
|
|
79
|
+
sites_data = response
|
|
80
|
+
else:
|
|
81
|
+
sites_data = response.get("data", [])
|
|
82
|
+
|
|
83
|
+
logger.debug(f"Extracted {len(sites_data)} sites from response")
|
|
84
|
+
|
|
85
|
+
# Apply pagination
|
|
86
|
+
paginated = sites_data[offset : offset + limit]
|
|
87
|
+
logger.debug(f"Paginated to {len(paginated)} sites")
|
|
88
|
+
|
|
89
|
+
# Parse into Site models
|
|
90
|
+
sites = []
|
|
91
|
+
for idx, s in enumerate(paginated):
|
|
92
|
+
try:
|
|
93
|
+
logger.debug(f"Parsing site {idx}: {s}")
|
|
94
|
+
site_obj = Site(**s)
|
|
95
|
+
sites.append(site_obj.model_dump())
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Failed to parse site {idx} ({s}): {e}", exc_info=True)
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
logger.info(f"Retrieved {len(sites)} sites (offset={offset}, limit={limit})")
|
|
101
|
+
return sites
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Error listing sites: {e}", exc_info=True)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def get_site_statistics(site_id: str, settings: Settings) -> dict[str, Any]:
|
|
108
|
+
"""Retrieve site-wide statistics.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
site_id: Site identifier
|
|
112
|
+
settings: Application settings
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Site statistics dictionary
|
|
116
|
+
"""
|
|
117
|
+
site_id = validate_site_id(site_id)
|
|
118
|
+
logger = get_logger(__name__, settings.log_level)
|
|
119
|
+
|
|
120
|
+
async with UniFiClient(settings) as client:
|
|
121
|
+
await client.authenticate()
|
|
122
|
+
|
|
123
|
+
# Gather statistics from multiple endpoints
|
|
124
|
+
devices_response = await client.get(f"/ea/sites/{site_id}/devices")
|
|
125
|
+
clients_response = await client.get(f"/ea/sites/{site_id}/sta")
|
|
126
|
+
networks_response = await client.get(f"/ea/sites/{site_id}/rest/networkconf")
|
|
127
|
+
|
|
128
|
+
devices_data = (
|
|
129
|
+
devices_response.get("data", [])
|
|
130
|
+
if isinstance(devices_response, dict)
|
|
131
|
+
else devices_response
|
|
132
|
+
)
|
|
133
|
+
clients_data = (
|
|
134
|
+
clients_response.get("data", [])
|
|
135
|
+
if isinstance(clients_response, dict)
|
|
136
|
+
else clients_response
|
|
137
|
+
)
|
|
138
|
+
networks_data = (
|
|
139
|
+
networks_response.get("data", [])
|
|
140
|
+
if isinstance(networks_response, dict)
|
|
141
|
+
else networks_response
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Count device types
|
|
145
|
+
ap_count = sum(1 for d in devices_data if d.get("type") == "uap")
|
|
146
|
+
switch_count = sum(1 for d in devices_data if d.get("type") == "usw")
|
|
147
|
+
gateway_count = sum(1 for d in devices_data if d.get("type") in ["ugw", "udm", "uxg"])
|
|
148
|
+
|
|
149
|
+
# Count online/offline devices
|
|
150
|
+
online_devices = sum(1 for d in devices_data if d.get("state") == 1)
|
|
151
|
+
offline_devices = len(devices_data) - online_devices
|
|
152
|
+
|
|
153
|
+
# Count wired vs wireless clients
|
|
154
|
+
wired_clients = sum(1 for c in clients_data if c.get("is_wired") is True)
|
|
155
|
+
wireless_clients = len(clients_data) - wired_clients
|
|
156
|
+
|
|
157
|
+
# Calculate total bandwidth
|
|
158
|
+
total_tx = sum(c.get("tx_bytes", 0) for c in clients_data)
|
|
159
|
+
total_rx = sum(c.get("rx_bytes", 0) for c in clients_data)
|
|
160
|
+
|
|
161
|
+
statistics = {
|
|
162
|
+
"site_id": site_id,
|
|
163
|
+
"devices": {
|
|
164
|
+
"total": len(devices_data),
|
|
165
|
+
"online": online_devices,
|
|
166
|
+
"offline": offline_devices,
|
|
167
|
+
"access_points": ap_count,
|
|
168
|
+
"switches": switch_count,
|
|
169
|
+
"gateways": gateway_count,
|
|
170
|
+
},
|
|
171
|
+
"clients": {
|
|
172
|
+
"total": len(clients_data),
|
|
173
|
+
"wired": wired_clients,
|
|
174
|
+
"wireless": wireless_clients,
|
|
175
|
+
},
|
|
176
|
+
"networks": {
|
|
177
|
+
"total": len(networks_data),
|
|
178
|
+
},
|
|
179
|
+
"bandwidth": {
|
|
180
|
+
"total_tx_bytes": total_tx,
|
|
181
|
+
"total_rx_bytes": total_rx,
|
|
182
|
+
"total_bytes": total_tx + total_rx,
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
logger.info(f"Retrieved statistics for site '{site_id}'")
|
|
187
|
+
return statistics
|
src/tools/topology.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""Network topology tools for UniFi MCP Server."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from src.api.client import UniFiClient
|
|
8
|
+
from src.config import Settings
|
|
9
|
+
from src.models.topology import NetworkDiagram, TopologyConnection, TopologyNode
|
|
10
|
+
from src.utils.exceptions import ValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def get_network_topology(
|
|
14
|
+
site_id: str,
|
|
15
|
+
settings: Settings,
|
|
16
|
+
include_coordinates: bool = False,
|
|
17
|
+
) -> dict:
|
|
18
|
+
"""
|
|
19
|
+
Retrieve complete network topology graph.
|
|
20
|
+
|
|
21
|
+
Fetches the network topology including all devices, clients, and their
|
|
22
|
+
interconnections. Optionally includes position coordinates for visualization.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
site_id: Site identifier ("default" for default site)
|
|
26
|
+
settings: Application settings with UniFi controller connection info
|
|
27
|
+
include_coordinates: Whether to calculate node position coordinates
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Network diagram dictionary with nodes, connections, and statistics
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
```python
|
|
34
|
+
topology = await get_network_topology("default", settings, include_coordinates=True)
|
|
35
|
+
print(f"Total devices: {topology['total_devices']}")
|
|
36
|
+
print(f"Total clients: {topology['total_clients']}")
|
|
37
|
+
```
|
|
38
|
+
"""
|
|
39
|
+
async with UniFiClient(settings) as client:
|
|
40
|
+
if not client.is_authenticated:
|
|
41
|
+
await client.authenticate()
|
|
42
|
+
|
|
43
|
+
actual_site_id = await client.resolve_site_id(site_id)
|
|
44
|
+
|
|
45
|
+
# Fetch devices and clients from UniFi Integration API
|
|
46
|
+
devices_endpoint = client.settings.get_integration_path(f"sites/{actual_site_id}/devices")
|
|
47
|
+
clients_endpoint = client.settings.get_integration_path(f"sites/{actual_site_id}/clients")
|
|
48
|
+
|
|
49
|
+
# Fetch all devices and clients (handle pagination)
|
|
50
|
+
device_nodes = []
|
|
51
|
+
offset = 0
|
|
52
|
+
while True:
|
|
53
|
+
response = await client.get(f"{devices_endpoint}?offset={offset}&limit=100")
|
|
54
|
+
data = response if isinstance(response, list) else response.get("data", [])
|
|
55
|
+
if not data:
|
|
56
|
+
break
|
|
57
|
+
device_nodes.extend(data)
|
|
58
|
+
offset += len(data)
|
|
59
|
+
if len(data) < 100:
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
client_nodes = []
|
|
63
|
+
offset = 0
|
|
64
|
+
while True:
|
|
65
|
+
response = await client.get(f"{clients_endpoint}?offset={offset}&limit=100")
|
|
66
|
+
data = response if isinstance(response, list) else response.get("data", [])
|
|
67
|
+
if not data:
|
|
68
|
+
break
|
|
69
|
+
client_nodes.extend(data)
|
|
70
|
+
offset += len(data)
|
|
71
|
+
if len(data) < 100:
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
# Convert devices to topology nodes
|
|
75
|
+
nodes = []
|
|
76
|
+
connections = []
|
|
77
|
+
depth_map = {} # Track network depth for each device
|
|
78
|
+
|
|
79
|
+
# First pass: Create all device nodes and calculate depth
|
|
80
|
+
for device in device_nodes:
|
|
81
|
+
device_id = device.get("id", "")
|
|
82
|
+
uplink_info = device.get("uplink", {})
|
|
83
|
+
uplink_device_id = uplink_info.get("deviceId")
|
|
84
|
+
|
|
85
|
+
# Calculate depth (distance from gateway)
|
|
86
|
+
if uplink_device_id:
|
|
87
|
+
parent_depth = depth_map.get(uplink_device_id, 0)
|
|
88
|
+
depth_map[device_id] = parent_depth + 1
|
|
89
|
+
else:
|
|
90
|
+
depth_map[device_id] = 0 # Gateway device
|
|
91
|
+
|
|
92
|
+
node = TopologyNode(
|
|
93
|
+
node_id=device_id,
|
|
94
|
+
node_type="device",
|
|
95
|
+
name=device.get("name"),
|
|
96
|
+
mac=device.get("macAddress"),
|
|
97
|
+
ip=device.get("ipAddress"),
|
|
98
|
+
model=device.get("model"),
|
|
99
|
+
type_detail=device.get("model"),
|
|
100
|
+
uplink_device_id=uplink_device_id,
|
|
101
|
+
uplink_port=uplink_info.get("portIndex"),
|
|
102
|
+
uplink_depth=depth_map.get(device_id, 0),
|
|
103
|
+
state=1 if device.get("state") == "CONNECTED" else 0,
|
|
104
|
+
adopted=True, # All returned devices are adopted
|
|
105
|
+
)
|
|
106
|
+
nodes.append(node)
|
|
107
|
+
|
|
108
|
+
# Create connection if device has uplink
|
|
109
|
+
if uplink_device_id:
|
|
110
|
+
conn = TopologyConnection(
|
|
111
|
+
connection_id=f"conn_{device_id}_uplink",
|
|
112
|
+
source_node_id=device_id,
|
|
113
|
+
target_node_id=uplink_device_id,
|
|
114
|
+
connection_type="uplink",
|
|
115
|
+
source_port=uplink_info.get("portIndex"),
|
|
116
|
+
speed_mbps=uplink_info.get("speedMbps"),
|
|
117
|
+
is_uplink=True,
|
|
118
|
+
status="up" if device.get("state") == "CONNECTED" else "down",
|
|
119
|
+
)
|
|
120
|
+
connections.append(conn)
|
|
121
|
+
|
|
122
|
+
# Process clients
|
|
123
|
+
for client_data in client_nodes:
|
|
124
|
+
client_id = client_data.get("id", "")
|
|
125
|
+
client_type = client_data.get("type", "WIRED")
|
|
126
|
+
uplink_device_id = client_data.get("uplinkDeviceId")
|
|
127
|
+
|
|
128
|
+
node = TopologyNode(
|
|
129
|
+
node_id=client_id,
|
|
130
|
+
node_type="client",
|
|
131
|
+
name=client_data.get("name"),
|
|
132
|
+
mac=client_data.get("macAddress"),
|
|
133
|
+
ip=client_data.get("ipAddress"),
|
|
134
|
+
state=1, # All returned clients are connected
|
|
135
|
+
)
|
|
136
|
+
nodes.append(node)
|
|
137
|
+
|
|
138
|
+
# Create connection for client
|
|
139
|
+
if uplink_device_id:
|
|
140
|
+
conn_type = "wired" if client_type == "WIRED" else "wireless"
|
|
141
|
+
conn = TopologyConnection(
|
|
142
|
+
connection_id=f"conn_client_{client_id}",
|
|
143
|
+
source_node_id=client_id,
|
|
144
|
+
target_node_id=uplink_device_id,
|
|
145
|
+
connection_type=conn_type,
|
|
146
|
+
is_uplink=False,
|
|
147
|
+
status="up",
|
|
148
|
+
)
|
|
149
|
+
connections.append(conn)
|
|
150
|
+
|
|
151
|
+
# Calculate statistics
|
|
152
|
+
total_devices = len([n for n in nodes if n.node_type == "device"])
|
|
153
|
+
total_clients = len([n for n in nodes if n.node_type == "client"])
|
|
154
|
+
max_depth = max([n.uplink_depth for n in nodes if n.uplink_depth is not None], default=0)
|
|
155
|
+
|
|
156
|
+
# Build network diagram
|
|
157
|
+
diagram = NetworkDiagram(
|
|
158
|
+
site_id=actual_site_id,
|
|
159
|
+
generated_at=datetime.now(timezone.utc).isoformat(),
|
|
160
|
+
nodes=nodes,
|
|
161
|
+
connections=connections,
|
|
162
|
+
total_devices=total_devices,
|
|
163
|
+
total_clients=total_clients,
|
|
164
|
+
total_connections=len(connections),
|
|
165
|
+
max_depth=max_depth,
|
|
166
|
+
has_coordinates=include_coordinates,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return diagram.model_dump()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def get_device_connections(
|
|
173
|
+
site_id: str,
|
|
174
|
+
device_id: str | None,
|
|
175
|
+
settings: Settings,
|
|
176
|
+
) -> list[dict]:
|
|
177
|
+
"""
|
|
178
|
+
Get device interconnection details.
|
|
179
|
+
|
|
180
|
+
Retrieves detailed connection information for a specific device or all devices.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
site_id: Site identifier
|
|
184
|
+
device_id: Specific device ID, or None for all devices
|
|
185
|
+
settings: Application settings
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of connection dictionaries
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
```python
|
|
192
|
+
connections = await get_device_connections("default", "switch_001", settings)
|
|
193
|
+
for conn in connections:
|
|
194
|
+
print(f"{conn['source_node_id']} -> {conn['target_node_id']}")
|
|
195
|
+
```
|
|
196
|
+
"""
|
|
197
|
+
topology = await get_network_topology(site_id, settings)
|
|
198
|
+
|
|
199
|
+
connections = topology.get("connections", [])
|
|
200
|
+
|
|
201
|
+
if device_id:
|
|
202
|
+
# Filter connections for specific device
|
|
203
|
+
connections = [
|
|
204
|
+
conn
|
|
205
|
+
for conn in connections
|
|
206
|
+
if conn.get("source_node_id") == device_id or conn.get("target_node_id") == device_id
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
return connections
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def get_port_mappings(
|
|
213
|
+
site_id: str,
|
|
214
|
+
device_id: str,
|
|
215
|
+
settings: Settings,
|
|
216
|
+
) -> dict:
|
|
217
|
+
"""
|
|
218
|
+
Get port-level connection mappings for a device.
|
|
219
|
+
|
|
220
|
+
Retrieves detailed information about which ports are connected to which devices/clients.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
site_id: Site identifier
|
|
224
|
+
device_id: Device ID
|
|
225
|
+
settings: Application settings
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dictionary with device_id and port mapping information
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
```python
|
|
232
|
+
ports = await get_port_mappings("default", "switch_001", settings)
|
|
233
|
+
for port_num, connected_device in ports['ports'].items():
|
|
234
|
+
print(f"Port {port_num}: {connected_device}")
|
|
235
|
+
```
|
|
236
|
+
"""
|
|
237
|
+
topology = await get_network_topology(site_id, settings)
|
|
238
|
+
|
|
239
|
+
connections = topology.get("connections", [])
|
|
240
|
+
|
|
241
|
+
# Build port mapping
|
|
242
|
+
port_map = {}
|
|
243
|
+
|
|
244
|
+
for conn in connections:
|
|
245
|
+
if conn.get("source_node_id") == device_id:
|
|
246
|
+
port_num = conn.get("source_port")
|
|
247
|
+
if port_num is not None:
|
|
248
|
+
port_map[port_num] = {
|
|
249
|
+
"connected_to": conn.get("target_node_id"),
|
|
250
|
+
"connection_type": conn.get("connection_type"),
|
|
251
|
+
"speed_mbps": conn.get("speed_mbps"),
|
|
252
|
+
"status": conn.get("status"),
|
|
253
|
+
}
|
|
254
|
+
elif conn.get("target_node_id") == device_id:
|
|
255
|
+
port_num = conn.get("target_port")
|
|
256
|
+
if port_num is not None:
|
|
257
|
+
port_map[port_num] = {
|
|
258
|
+
"connected_to": conn.get("source_node_id"),
|
|
259
|
+
"connection_type": conn.get("connection_type"),
|
|
260
|
+
"speed_mbps": conn.get("speed_mbps"),
|
|
261
|
+
"status": conn.get("status"),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {"device_id": device_id, "ports": port_map}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def export_topology(
|
|
268
|
+
site_id: str,
|
|
269
|
+
format: Literal["json", "graphml", "dot"],
|
|
270
|
+
settings: Settings,
|
|
271
|
+
) -> str:
|
|
272
|
+
"""
|
|
273
|
+
Export network topology in various formats.
|
|
274
|
+
|
|
275
|
+
Exports the network topology as JSON, GraphML (XML), or DOT (Graphviz) format.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
site_id: Site identifier
|
|
279
|
+
format: Export format ("json", "graphml", or "dot")
|
|
280
|
+
settings: Application settings
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Topology data as a formatted string
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
ValidationError: If invalid format is specified
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
```python
|
|
290
|
+
# Export as JSON
|
|
291
|
+
json_data = await export_topology("default", "json", settings)
|
|
292
|
+
|
|
293
|
+
# Export as GraphML for network visualization tools
|
|
294
|
+
graphml_data = await export_topology("default", "graphml", settings)
|
|
295
|
+
|
|
296
|
+
# Export as DOT for Graphviz
|
|
297
|
+
dot_data = await export_topology("default", "dot", settings)
|
|
298
|
+
```
|
|
299
|
+
"""
|
|
300
|
+
if format not in ["json", "graphml", "dot"]:
|
|
301
|
+
raise ValidationError(
|
|
302
|
+
f"Invalid export format: {format}. Must be 'json', 'graphml', or 'dot'"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
topology = await get_network_topology(site_id, settings)
|
|
306
|
+
|
|
307
|
+
if format == "json":
|
|
308
|
+
return json.dumps(topology, indent=2)
|
|
309
|
+
|
|
310
|
+
elif format == "graphml":
|
|
311
|
+
# Generate GraphML XML
|
|
312
|
+
nodes = topology.get("nodes", [])
|
|
313
|
+
connections = topology.get("connections", [])
|
|
314
|
+
|
|
315
|
+
graphml = ['<?xml version="1.0" encoding="UTF-8"?>']
|
|
316
|
+
graphml.append('<graphml xmlns="http://graphml.graphdrawing.org/xmlns">')
|
|
317
|
+
graphml.append(' <graph id="UniFi Network" edgedefault="directed">')
|
|
318
|
+
|
|
319
|
+
# Add nodes
|
|
320
|
+
for node in nodes:
|
|
321
|
+
node_id = node.get("node_id", "")
|
|
322
|
+
node_type = node.get("node_type", "")
|
|
323
|
+
name = node.get("name", "")
|
|
324
|
+
graphml.append(f' <node id="{node_id}">')
|
|
325
|
+
graphml.append(f' <data key="type">{node_type}</data>')
|
|
326
|
+
graphml.append(f' <data key="name">{name}</data>')
|
|
327
|
+
graphml.append(" </node>")
|
|
328
|
+
|
|
329
|
+
# Add edges
|
|
330
|
+
for conn in connections:
|
|
331
|
+
source = conn.get("source_node_id", "")
|
|
332
|
+
target = conn.get("target_node_id", "")
|
|
333
|
+
conn_type = conn.get("connection_type", "")
|
|
334
|
+
graphml.append(f' <edge source="{source}" target="{target}">')
|
|
335
|
+
graphml.append(f' <data key="type">{conn_type}</data>')
|
|
336
|
+
graphml.append(" </edge>")
|
|
337
|
+
|
|
338
|
+
graphml.append(" </graph>")
|
|
339
|
+
graphml.append("</graphml>")
|
|
340
|
+
|
|
341
|
+
return "\n".join(graphml)
|
|
342
|
+
|
|
343
|
+
elif format == "dot":
|
|
344
|
+
# Generate DOT format
|
|
345
|
+
nodes = topology.get("nodes", [])
|
|
346
|
+
connections = topology.get("connections", [])
|
|
347
|
+
|
|
348
|
+
dot = ["digraph UniFiNetwork {"]
|
|
349
|
+
dot.append(" node [shape=box];")
|
|
350
|
+
|
|
351
|
+
# Add nodes
|
|
352
|
+
for node in nodes:
|
|
353
|
+
node_id = node.get("node_id", "")
|
|
354
|
+
name = node.get("name", node_id)
|
|
355
|
+
node_type = node.get("node_type", "")
|
|
356
|
+
dot.append(f' "{node_id}" [label="{name}\\n({node_type})"];')
|
|
357
|
+
|
|
358
|
+
# Add edges
|
|
359
|
+
for conn in connections:
|
|
360
|
+
source = conn.get("source_node_id", "")
|
|
361
|
+
target = conn.get("target_node_id", "")
|
|
362
|
+
conn_type = conn.get("connection_type", "")
|
|
363
|
+
dot.append(f' "{source}" -> "{target}" [label="{conn_type}"];')
|
|
364
|
+
|
|
365
|
+
dot.append("}")
|
|
366
|
+
|
|
367
|
+
return "\n".join(dot)
|
|
368
|
+
|
|
369
|
+
return ""
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
async def get_topology_statistics(
|
|
373
|
+
site_id: str,
|
|
374
|
+
settings: Settings,
|
|
375
|
+
) -> dict:
|
|
376
|
+
"""
|
|
377
|
+
Get network topology statistics.
|
|
378
|
+
|
|
379
|
+
Retrieves statistical summary of the network topology including device counts,
|
|
380
|
+
client counts, connection counts, and network depth.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
site_id: Site identifier
|
|
384
|
+
settings: Application settings
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Dictionary with topology statistics
|
|
388
|
+
|
|
389
|
+
Example:
|
|
390
|
+
```python
|
|
391
|
+
stats = await get_topology_statistics("default", settings)
|
|
392
|
+
print(f"Devices: {stats['total_devices']}")
|
|
393
|
+
print(f"Clients: {stats['total_clients']}")
|
|
394
|
+
print(f"Max network depth: {stats['max_depth']}")
|
|
395
|
+
```
|
|
396
|
+
"""
|
|
397
|
+
topology = await get_network_topology(site_id, settings)
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
"site_id": topology.get("site_id"),
|
|
401
|
+
"total_devices": topology.get("total_devices", 0),
|
|
402
|
+
"total_clients": topology.get("total_clients", 0),
|
|
403
|
+
"total_connections": topology.get("total_connections", 0),
|
|
404
|
+
"max_depth": topology.get("max_depth", 0),
|
|
405
|
+
"generated_at": topology.get("generated_at"),
|
|
406
|
+
}
|