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
262 lines
9.0 KiB
Python
262 lines
9.0 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 suspend tool for pausing network devices.
|
|
|
|
Provides functionality to suspend one or multiple nodes in GNS3 projects.
|
|
Suspended nodes preserve their state in memory and can be quickly resumed.
|
|
"""
|
|
|
|
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,
|
|
get_nodes_handler,
|
|
suspend_node_handler,
|
|
)
|
|
|
|
# Configure logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class GNS3SuspendNodeTool(BaseTool):
|
|
"""
|
|
A LangChain tool to suspend one or multiple nodes in a GNS3 project.
|
|
|
|
**Input**:
|
|
A JSON object with project_id and node_ids (list of node IDs).
|
|
Example:
|
|
{
|
|
"project_id": "uuid-of-project",
|
|
"node_ids": ["uuid-of-node-1", "uuid-of-node-2"]
|
|
}
|
|
|
|
**Output**:
|
|
A dictionary with all nodes' details:
|
|
{
|
|
"project_id": "...",
|
|
"total_nodes": 2,
|
|
"successful": 2,
|
|
"failed": 0,
|
|
"nodes": [
|
|
{"node_id": "...", "name": "...", "status": "suspended"},
|
|
{"node_id": "...", "name": "...", "status": "suspended"}
|
|
]
|
|
}
|
|
|
|
**Note**: Suspended nodes preserve their state in memory and can be quickly
|
|
resumed. Use this when you want to pause a lab and continue later.
|
|
"""
|
|
|
|
name: str = "suspend_gns3_node"
|
|
description: str = """
|
|
Suspends one or multiple nodes in a GNS3 project.
|
|
Suspended nodes preserve their state and can be quickly resumed.
|
|
Use this to pause a lab temporarily and continue later.
|
|
Input: JSON with project_id and node_ids (list of node IDs).
|
|
Returns: A dict with all nodes' details (success/failure status).
|
|
"""
|
|
|
|
def _run(
|
|
self,
|
|
tool_input: str,
|
|
run_manager: CallbackManagerForToolRun | None = None,
|
|
) -> dict[str, Any]:
|
|
try:
|
|
# Parse input JSON
|
|
input_data = json.loads(tool_input)
|
|
project_id = input_data.get("project_id")
|
|
node_ids = input_data.get("node_ids")
|
|
|
|
# Validate input
|
|
if not project_id or not node_ids:
|
|
logger.error(
|
|
"Missing required fields: project_id or node_ids."
|
|
)
|
|
return {
|
|
"error": "Missing required fields: "
|
|
"project_id and node_ids."
|
|
}
|
|
|
|
if not isinstance(node_ids, list):
|
|
logger.error("node_ids must be a list.")
|
|
return {"error": "node_ids must be a list."}
|
|
|
|
# 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."
|
|
}
|
|
|
|
# Verify nodes exist and capture names (one call)
|
|
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
|
if "error" in listing:
|
|
return {"error": listing["error"]}
|
|
nodes_by_id = {n["node_id"]: n for n in listing["nodes"]}
|
|
|
|
# Suspend all nodes (parallel batch) and collect results
|
|
logger.info(
|
|
"Suspending %d nodes in project %s...",
|
|
len(node_ids),
|
|
project_id,
|
|
)
|
|
results = []
|
|
known_ids = [nid for nid in node_ids if nid in nodes_by_id]
|
|
suspend_results = suspend_node_handler(
|
|
{"project_id": project_id, "node_ids": known_ids}, gns3_ctx
|
|
)
|
|
suspend_errors = {
|
|
r["node_id"]: r.get("error")
|
|
for r in suspend_results
|
|
if r.get("status") == "error"
|
|
}
|
|
|
|
# Get updated status — one call
|
|
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
|
if "error" in listing:
|
|
return {"error": listing["error"]}
|
|
after_by_id = {n["node_id"]: n for n in listing["nodes"]}
|
|
|
|
for node_id in node_ids:
|
|
if node_id not in nodes_by_id:
|
|
logger.error(
|
|
"Node %s not found in project %s", node_id, project_id
|
|
)
|
|
results.append(
|
|
{
|
|
"node_id": node_id,
|
|
"name": "N/A",
|
|
"status": "error",
|
|
"error": "Node not found",
|
|
}
|
|
)
|
|
elif node_id in suspend_errors:
|
|
logger.error(
|
|
"Failed to suspend node %s: %s",
|
|
node_id,
|
|
suspend_errors[node_id],
|
|
)
|
|
results.append(
|
|
{
|
|
"node_id": node_id,
|
|
"name": nodes_by_id[node_id].get("name") or "N/A",
|
|
"status": "error",
|
|
"error": suspend_errors[node_id],
|
|
}
|
|
)
|
|
else:
|
|
logger.info(
|
|
"Suspend command sent for node %s (%s)",
|
|
node_id,
|
|
nodes_by_id[node_id].get("name"),
|
|
)
|
|
current = after_by_id.get(node_id, nodes_by_id[node_id])
|
|
results.append(
|
|
{
|
|
"node_id": node_id,
|
|
"name": current.get("name") or "N/A",
|
|
"status": current.get("status") or "unknown",
|
|
}
|
|
)
|
|
|
|
# Analyze results
|
|
successful_nodes = [
|
|
r for r in results if r.get("status") != "error"
|
|
]
|
|
failed_nodes = [r for r in results if r.get("status") == "error"]
|
|
|
|
# Construct final response
|
|
response = {
|
|
"project_id": project_id,
|
|
"total_nodes": len(node_ids),
|
|
"successful": len(successful_nodes),
|
|
"failed": len(failed_nodes),
|
|
"nodes": results,
|
|
"note": (
|
|
"Suspended nodes preserve their state in memory. "
|
|
"Use resume to continue where you left off."
|
|
),
|
|
}
|
|
|
|
logger.info(
|
|
"Suspend operation completed: %d successful, %d failed",
|
|
len(successful_nodes),
|
|
len(failed_nodes),
|
|
)
|
|
|
|
return response
|
|
|
|
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 suspend nodes: %s", e)
|
|
return {"error": f"Failed to suspend nodes: {str(e)}"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Test with single node
|
|
print("=== Testing single node suspend ===")
|
|
test_input_single = json.dumps(
|
|
{
|
|
"project_id": "<PROJECT_UUID>", # Replace with actual project UUID
|
|
"node_ids": [
|
|
"fbeda109-9a74-4d8c-a749-cc3847911a90"
|
|
], # Replace with actual node UUID
|
|
}
|
|
)
|
|
tool = GNS3SuspendNodeTool()
|
|
result_single = tool._run(test_input_single)
|
|
pprint(result_single)
|
|
|
|
# Test with multiple nodes
|
|
print("\n=== Testing multiple nodes suspend ===")
|
|
test_input_multiple = json.dumps(
|
|
{
|
|
"project_id": "<PROJECT_UUID>", # Replace with actual project UUID
|
|
"node_ids": [
|
|
"fbeda109-9a74-4d8c-a749-cc3847911a90",
|
|
# Replace with actual node UUIDs
|
|
"another-node-uuid-here",
|
|
"third-node-uuid-here",
|
|
],
|
|
}
|
|
)
|
|
result_multiple = tool._run(test_input_multiple)
|
|
pprint(result_multiple)
|