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
328 lines
11 KiB
Python
328 lines
11 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 node creation tool for network topology building.
|
|
|
|
Provides functionality to create multiple nodes in GNS3 projects
|
|
using specified templates and coordinates through the GNS3 API.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from pprint import pprint
|
|
from typing import Any
|
|
|
|
from langchain.tools import BaseTool
|
|
from langchain_core.callbacks import CallbackManagerForToolRun
|
|
|
|
from gns3server.agent.gns3_copilot.gns3_client.api_handlers import (
|
|
build_gns3_ctx,
|
|
create_node_handler,
|
|
)
|
|
|
|
# Configure logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GNS3CreateNodeTool(BaseTool):
|
|
"""
|
|
A LangChain tool to create multiple nodes in a GNS3 project
|
|
using specified templates and coordinates.
|
|
|
|
**Input:**
|
|
A JSON object with project_id and array of nodes with template_id,
|
|
x, y coordinates, and optional name.
|
|
|
|
Example input:
|
|
{
|
|
"project_id": "uuid-of-project",
|
|
"nodes": [
|
|
{
|
|
"template_id": "uuid-of-template",
|
|
"x": 100,
|
|
"y": -200,
|
|
"name": "R1"
|
|
},
|
|
{
|
|
"template_id": "uuid-of-template2",
|
|
"x": -200,
|
|
"y": 300,
|
|
"name": "R2"
|
|
}
|
|
]
|
|
}
|
|
|
|
**Output:**
|
|
A dictionary containing the creation results for all nodes.
|
|
Example output:
|
|
{
|
|
"project_id": "uuid-of-project",
|
|
"created_nodes": [
|
|
{
|
|
"node_id": "uuid-of-node1",
|
|
"name": "R1",
|
|
"status": "success"
|
|
},
|
|
{
|
|
"node_id": "uuid-of-node2",
|
|
"name": "R2",
|
|
"status": "success"
|
|
}
|
|
],
|
|
"total_nodes": 2,
|
|
"successful_nodes": 2,
|
|
"failed_nodes": 0
|
|
}
|
|
If error occurs during validation, returns dict with error message.
|
|
"""
|
|
|
|
name: str = "create_gns3_node"
|
|
description: str = """
|
|
Creates multiple nodes in a GNS3 project using templates and coordinates.
|
|
Input is a JSON object with project_id and array of nodes.
|
|
Each node requires: template_id, x, y. Optional: name (to set node name directly).
|
|
Example input:
|
|
{
|
|
"project_id": "uuid-of-project",
|
|
"nodes": [
|
|
{
|
|
"template_id": "uuid-of-template",
|
|
"x": 100,
|
|
"y": -200,
|
|
"name": "R1"
|
|
},
|
|
{
|
|
"template_id": "uuid-of-template2",
|
|
"x": -200,
|
|
"y": 300,
|
|
"name": "R2"
|
|
}
|
|
]
|
|
}
|
|
IMPORTANT: Ensure distance between any two nodes is greater than 250 px.
|
|
This spacing is necessary to display interface numbers clearly for better
|
|
topology visualization.
|
|
Returns a dictionary with creation results for all nodes, including
|
|
success/failure status.
|
|
If the operation fails during input validation, returns a dictionary with
|
|
an error message.
|
|
"""
|
|
|
|
def _run(
|
|
self,
|
|
tool_input: str,
|
|
run_manager: CallbackManagerForToolRun | None = None,
|
|
**kwargs: Any,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Creates nodes in a GNS3 project with templates and coordinates.
|
|
|
|
Args:
|
|
tool_input: A JSON string with project_id and an array of nodes.
|
|
run_manager: LangChain run manager (unused).
|
|
|
|
Returns:
|
|
dict: A dictionary with creation results for all nodes or an error
|
|
message.
|
|
"""
|
|
# Log received input
|
|
logger.info("Received input: %s", tool_input)
|
|
|
|
try:
|
|
# Parse input JSON
|
|
input_data = json.loads(tool_input)
|
|
project_id = input_data.get("project_id")
|
|
nodes = input_data.get("nodes", [])
|
|
|
|
# Validate input
|
|
if not project_id:
|
|
logger.error("Invalid input: Missing project_id.")
|
|
return {"error": "Missing project_id."}
|
|
|
|
if not isinstance(nodes, list) or len(nodes) == 0:
|
|
logger.error("Invalid input: nodes must be a non-empty array.")
|
|
return {"error": "nodes must be a non-empty array."}
|
|
|
|
# Validate each node in the array
|
|
for i, node_data in enumerate(nodes):
|
|
if not isinstance(node_data, dict):
|
|
logger.error(
|
|
"Invalid input: Node %d must be a dictionary.", i + 1
|
|
)
|
|
return {"error": f"Node {i + 1} must be a dictionary."}
|
|
|
|
template_id = node_data.get("template_id")
|
|
x = node_data.get("x")
|
|
y = node_data.get("y")
|
|
name = node_data.get("name")
|
|
|
|
if not all(
|
|
[
|
|
template_id,
|
|
isinstance(x, (int, float)),
|
|
isinstance(y, (int, float)),
|
|
]
|
|
):
|
|
logger.error(
|
|
"Invalid input: Node %d missing or invalid "
|
|
"template_id, x, or y.",
|
|
i + 1,
|
|
)
|
|
return {
|
|
"error": f"Node {i + 1} missing or invalid "
|
|
f"template_id, x, or y."
|
|
}
|
|
|
|
# Build handler context (JWT + server URL from request context)
|
|
logger.info("Connecting to GNS3 server...")
|
|
gns3_ctx = build_gns3_ctx()
|
|
|
|
if gns3_ctx is None:
|
|
logger.error("Failed to create GNS3 connector")
|
|
return {
|
|
"error": "Failed to connect to GNS3 server. "
|
|
"Please check your configuration."
|
|
}
|
|
|
|
# Create nodes
|
|
logger.info(
|
|
"Creating %d nodes in project %s...", len(nodes), project_id
|
|
)
|
|
results: list[dict[str, Any]] = []
|
|
|
|
for i, node_data in enumerate(nodes):
|
|
try:
|
|
template_id = node_data.get("template_id")
|
|
x = node_data.get("x")
|
|
y = node_data.get("y")
|
|
name = node_data.get("name")
|
|
|
|
logger.info(
|
|
"Creating node %d/%d with template %s at (%s, %s), name=%s...",
|
|
i + 1,
|
|
len(nodes),
|
|
template_id,
|
|
x,
|
|
y,
|
|
name,
|
|
)
|
|
|
|
# Create node via the shared REST handler
|
|
created = create_node_handler(
|
|
{
|
|
"project_id": project_id,
|
|
"template_id": template_id,
|
|
"x": x,
|
|
"y": y,
|
|
"name": name,
|
|
},
|
|
gns3_ctx,
|
|
)
|
|
if "error" in created:
|
|
raise RuntimeError(created["error"])
|
|
node_info = {
|
|
"node_id": created.get("node_id"),
|
|
"name": created.get("name"),
|
|
"status": "success",
|
|
}
|
|
|
|
results.append(node_info)
|
|
|
|
except Exception as e:
|
|
error_info = {
|
|
"error": f"Node {i + 1} creation failed: {str(e)}",
|
|
"status": "failed",
|
|
}
|
|
results.append(error_info)
|
|
logger.error("Failed to create node %d: %s", i + 1, e)
|
|
# Continue with next node even if one fails
|
|
|
|
# Calculate summary statistics
|
|
successful_nodes = len(
|
|
[r for r in results if r.get("status") == "success"]
|
|
)
|
|
failed_nodes = len(
|
|
[r for r in results if r.get("status") == "failed"]
|
|
)
|
|
|
|
# Prepare final result
|
|
final_result = {
|
|
"project_id": project_id,
|
|
"created_nodes": results,
|
|
"total_nodes": len(nodes),
|
|
"successful_nodes": successful_nodes,
|
|
"failed_nodes": failed_nodes,
|
|
}
|
|
|
|
# Log the final result
|
|
logger.info(
|
|
"Node creation completed: %d successful, %d failed, %d total.",
|
|
successful_nodes,
|
|
failed_nodes,
|
|
len(nodes),
|
|
)
|
|
|
|
# Return JSON-formatted result
|
|
return final_result
|
|
|
|
except json.JSONDecodeError as e:
|
|
logger.error("Invalid JSON input: %s", e)
|
|
return {"error": f"Invalid JSON input: {e}"}
|
|
except Exception as e:
|
|
logger.error("Failed to process node creation request: %s", e)
|
|
return {
|
|
"error": f"Failed to process node creation request: {str(e)}"
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Test the tool locally with multiple nodes
|
|
test_input = json.dumps(
|
|
{
|
|
# TODO: Replace with actual project UUID
|
|
"project_id": "d7fc094c-685e-4db1-ac11-5e33a1b2e066",
|
|
"nodes": [
|
|
{
|
|
# TODO: Replace with actual template UUID
|
|
"template_id": "b923a635-b7cc-4cb5-9a86-9357e04c02f7",
|
|
"x": 100,
|
|
"y": -200,
|
|
"name": "R1",
|
|
},
|
|
{
|
|
# TODO: Replace with actual template UUID
|
|
"template_id": "b923a635-b7cc-4cb5-9a86-9357e04c02f7",
|
|
"x": 200,
|
|
"y": -300,
|
|
"name": "R2",
|
|
},
|
|
],
|
|
}
|
|
)
|
|
tool = GNS3CreateNodeTool()
|
|
result = tool._run(test_input)
|
|
pprint(result)
|