YueGuobin 629bb9194f
refactor: sink shared REST handlers into gns3_client, drop gns3fy wrappers
- 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
2026-08-25 09:15:32 +08:00

193 lines
6.6 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
#
"""
GNS3 Topology Reader Tool
This module provides a LangChain BaseTool to retrieve the topology of a
specific GNS3 project by project ID. Returns nodes, links, and project
metadata.
⚠️ WARNING: This module is shared with the MCP (Model Context Protocol) service.
GNS3TopologyTool._run() is called by MCP device config handlers.
The jwt_token/url parameters were added for MCP compatibility.
Modifications must be tested with BOTH gns3-copilot AND MCP.
"""
import copy
import logging
from pprint import pprint
from typing import Any
from langchain.tools import BaseTool
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import (
build_gns3_ctx,
)
from gns3server.agent.gns3_copilot.gns3_client.project_inventory import (
fetch_project_inventory,
)
# Configure logging
logger = logging.getLogger(__name__)
# Define LangChain tool class
class GNS3TopologyTool(BaseTool):
"""LangChain tool for retrieving GNS3 project topology information."""
name: str = "gns3_topology_reader"
description: str = """
Retrieves the topology of a GNS3 project including nodes and links.
Input: `project_id` (str, required): UUID of the GNS3 project.
Output: Dictionary with:
- `project_id`, `name`, `status`: Project metadata
- `nodes`: Dict of node details (node_id, name, ports, console_port,
type, etc.)
- `links`: List of link connections
Use this to understand network structure before making changes.
"""
def _run(
self,
tool_input: Any = None,
run_manager: Any = None,
project_id: str | None = None,
jwt_token: str | None = None,
url: str | None = None,
) -> dict:
"""
Synchronous method to retrieve the topology of a specific GNS3 project.
Args:
tool_input: Input parameters, typically a dict or Pydantic model
containing server_url.
run_manager: Callback manager for tool run.
project_id: The UUID of the specific GNS3 project to retrieve
topology from.
jwt_token: JWT token for authentication (used by MCP handlers).
url: GNS3 server URL (used by MCP handlers).
Returns:
dict: A dictionary containing the project ID, name, status, nodes,
and links, or an error dictionary if an exception occurs
or project_id is not provided.
"""
# Log received input
logger.info(
"Received tool_input: %s, project_id: %s", tool_input, project_id
)
try:
# Validate project_id parameter
if not project_id:
logger.error("project_id parameter is required.")
return {
"error": "project_id parameter is required. "
"Please provide a valid project UUID."
}
# Build handler context (JWT + server URL)
# jwt_token/url can be passed explicitly (e.g. from MCP handlers)
# or auto-detected (e.g. from gns3-copilot agent)
logger.debug("Connecting to GNS3 server...")
gns3_ctx = build_gns3_ctx(jwt_token=jwt_token, url=url)
if gns3_ctx is None:
logger.error("Failed to create GNS3 connector")
return {
"error": "Failed to connect to GNS3 server. Please check "
"your configuration."
}
# Use the provided project_id directly
logger.info(f"Retrieving topology for project_id: {project_id}")
inventory = fetch_project_inventory(gns3_ctx, project_id)
# Get topology JSON: includes nodes (devices), links, etc.
topology = {
"project_id": inventory["project_id"],
"name": inventory["name"],
"status": inventory["status"],
"nodes": self._clean_nodes_ports(
copy.deepcopy(inventory["nodes_inventory"])
),
"links": inventory["links_summary"],
}
# Log topology result
logger.info(
"Topology retrieved: project_id=%s, name=%s, nodes=%d, "
"links=%d",
topology.get("project_id"),
topology.get("name"),
len(topology.get("nodes", {})),
len(topology.get("links", [])),
)
logger.debug("Topology details: %s", topology)
return topology
except Exception as e:
logger.error("Error retrieving GNS3 topology: %s", str(e))
return {"error": f"Failed to retrieve topology: {str(e)}"}
def _clean_nodes_ports(self, data: dict) -> dict:
"""
Clean and simplify the nodes data structure.
Simplify each node's ports list to only keep name and short_name
fields.
"""
for node in data.values(): # Iterate through R-1, R-2, R-3, R-4
if "ports" in node and isinstance(node["ports"], list):
node["ports"] = [
{"name": port["name"], "short_name": port["short_name"]}
for port in node["ports"]
]
return data
if __name__ == "__main__":
# Test the tool
tool = GNS3TopologyTool()
# Example usage with project_id
# Replace with an actual project UUID from your GNS3 server
example_project_id = "0c0fde25-6ead-4413-a283-ea8fd2324291"
print("Testing GNS3TopologyTool with project_id...")
result = tool._run(project_id=example_project_id)
pprint(result)
# Test without project_id (should return error)
print("\nTesting without project_id (should return error)...")
error_result = tool._run()
pprint(error_result)