mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 12:30:13 +03:00
- Slim custom_gns3fy.py to connector-only Gns3Connector and rename to connector.py; delete the unused Node/Link/Project dataclasses and endpoint wrapper methods (~3000 lines) - Move the MCP node/link handler implementations into gns3_client/api_handlers.py as the shared REST client layer consumed by both the MCP service and copilot tools; add available_filters handler (exposed as link_available_filters MCP tool) and build_gns3_ctx() for copilot callers - Rewrite the tools_v2 node/link tools on top of the handlers: batch lifecycle actions now run in parallel, node creation is a single POST, project-wide status reads replace per-node GETs - Port Project.nodes_inventory/links_summary aggregation into project_inventory.py (output shape preserved) and rewrite the topology reader / project info tools on it, dropping the unused stats/snapshots/drawings calls - Delete the dead mcp/nodes.py and mcp/links.py (NODE_TOOLS/LINK_TOOLS had no consumers; __init__ imports handlers from api_handlers) - Retarget mcp handler tests to patch api_handlers._get_connector and replace test_custom_gns3fy.py with inventory contract tests
149 lines
5.4 KiB
Python
149 lines
5.4 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
# GNS3-Copilot - AI-powered Network Lab Assistant for GNS3
|
|
#
|
|
# This file is part of GNS3-Copilot project.
|
|
#
|
|
# GNS3-Copilot is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the
|
|
# Free Software Foundation, either version 3 of the License, or (at your
|
|
# option) any later version.
|
|
#
|
|
# GNS3-Copilot is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
# for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with GNS3-Copilot. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# Copyright (C) 2025 Yue Guobin (岳国宾)
|
|
# Author: Yue Guobin (岳国宾)
|
|
#
|
|
# Project Home: https://github.com/yueguobin/gns3-copilot
|
|
#
|
|
|
|
"""
|
|
Project inventory aggregation over the raw GNS3 REST listings.
|
|
|
|
Replaces the aggregation previously living on the ``Project`` dataclass
|
|
(``nodes_inventory`` / ``links_summary``). The output shapes are kept
|
|
field-for-field: they feed the LLM topology context and the Nornir
|
|
inventory, so any change here is consumer-visible.
|
|
"""
|
|
|
|
from typing import Any
|
|
from urllib.parse import urlparse
|
|
|
|
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import _get_connector
|
|
|
|
|
|
def build_nodes_inventory(
|
|
nodes: list[dict[str, Any]], server_host: str | None
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Build an inventory-style dict keyed by node name.
|
|
|
|
Shape (per node name):
|
|
{server, name, node_id, console_port, console_type, type, ports,
|
|
status, x, y, tags, netmiko_device_type, default_username,
|
|
default_password}
|
|
"""
|
|
inventory: dict[str, Any] = {}
|
|
for n in nodes:
|
|
inventory[n.get("name")] = {
|
|
"server": server_host,
|
|
"name": n.get("name"),
|
|
"node_id": n.get("node_id"),
|
|
"console_port": n.get("console"),
|
|
"console_type": n.get("console_type"),
|
|
"type": n.get("node_type"),
|
|
"ports": n.get("ports"),
|
|
"status": n.get("status"),
|
|
"x": n.get("x"),
|
|
"y": n.get("y"),
|
|
"tags": n.get("tags") if n.get("tags") else [],
|
|
"netmiko_device_type": n.get("netmiko_device_type"),
|
|
"default_username": n.get("default_username"),
|
|
"default_password": n.get("default_password"),
|
|
}
|
|
return inventory
|
|
|
|
|
|
def build_links_summary(
|
|
nodes: list[dict[str, Any]], links: list[dict[str, Any]]
|
|
) -> list[dict[str, str]]:
|
|
"""
|
|
Build a human/LLM-friendly link list resolving node and port names.
|
|
|
|
Shape (per link): {link_id, node_a, port_a, node_b, port_b}.
|
|
Links whose endpoints cannot be resolved are skipped, mirroring the
|
|
original Project.links_summary behavior.
|
|
"""
|
|
summary: list[dict[str, str]] = []
|
|
for link in links:
|
|
if not link.get("nodes"):
|
|
continue
|
|
side_a = link["nodes"][0]
|
|
side_b = link["nodes"][1]
|
|
try:
|
|
node_a = next(
|
|
x for x in nodes if x.get("node_id") == side_a["node_id"]
|
|
)
|
|
port_a = str(
|
|
next(
|
|
p["name"]
|
|
for p in (node_a.get("ports") or [])
|
|
if p["port_number"] == side_a["port_number"]
|
|
and p["adapter_number"] == side_a["adapter_number"]
|
|
)
|
|
)
|
|
node_b = next(
|
|
x for x in nodes if x.get("node_id") == side_b["node_id"]
|
|
)
|
|
port_b = str(
|
|
next(
|
|
p["name"]
|
|
for p in (node_b.get("ports") or [])
|
|
if p["port_number"] == side_b["port_number"]
|
|
and p["adapter_number"] == side_b["adapter_number"]
|
|
)
|
|
)
|
|
name_a = str(node_a["name"]) if node_a.get("name") else "Unknown"
|
|
name_b = str(node_b["name"]) if node_b.get("name") else "Unknown"
|
|
summary.append({
|
|
"link_id": link.get("link_id"),
|
|
"node_a": name_a,
|
|
"port_a": port_a,
|
|
"node_b": name_b,
|
|
"port_b": port_b,
|
|
})
|
|
except (StopIteration, KeyError, AttributeError):
|
|
# Prevent errors when lookups can't match data
|
|
continue
|
|
return summary
|
|
|
|
|
|
def fetch_project_inventory(
|
|
gns3_ctx: dict[str, Any], project_id: str
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Fetch a project's metadata, nodes and links and return the aggregated
|
|
inventory — the equivalent of the old ``Project.get()`` +
|
|
``nodes_inventory()`` + ``links_summary()`` sequence (minus the
|
|
stats/snapshots/drawings calls no consumer ever read).
|
|
"""
|
|
conn = _get_connector(gns3_ctx)
|
|
base = conn.base_url
|
|
project = conn.http_call("get", f"{base}/projects/{project_id}").json()
|
|
nodes = conn.http_call("get", f"{base}/projects/{project_id}/nodes").json()
|
|
links = conn.http_call("get", f"{base}/projects/{project_id}/links").json()
|
|
server_host = urlparse(gns3_ctx["server_url"]).hostname
|
|
return {
|
|
"project_id": project.get("project_id", project_id),
|
|
"name": project.get("name"),
|
|
"status": project.get("status"),
|
|
"nodes_inventory": build_nodes_inventory(nodes, server_host),
|
|
"links_summary": build_links_summary(nodes, links),
|
|
}
|