mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 22:11:14 +02:00
feat(gns3-copilot): add node stop and suspend tools for lab automation
- Add GNS3StopNodeTool and GNS3SuspendNodeTool to lab automation assistant mode - Update tools_v2 __init__.py to export new node control tools - Document node control tools in README with key features and implementation status - Update last modified date in documentation The new tools provide complete node lifecycle control for automated lab workflows, including stopping nodes for shutdown and suspending nodes while preserving state.
This commit is contained in:
1 parent
1cd4aeb2a2
commit
ad3621db0f
6 files changed
+1065
-2
No files matched your search
@@ -89,6 +89,8 @@ from gns3server.agent.gns3_copilot.tools_v2 import (
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3CreateNodeTool
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3LinkTool
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3StartNodeTool
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3StopNodeTool
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3SuspendNodeTool
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3TemplateTool
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import GNS3UpdateNodeNameTool
|
||||
from gns3server.agent.gns3_copilot.tools_v2 import VPCSMultiCommands
|
||||
@@ -118,6 +120,8 @@ LAB_AUTOMATION_ASSISTANT_MODE_TOOLS = [
|
||||
GNS3CreateNodeTool(), # Create new nodes in GNS3
|
||||
GNS3LinkTool(), # Create links between nodes
|
||||
GNS3StartNodeTool(), # Start GNS3 nodes
|
||||
GNS3StopNodeTool(), # Stop GNS3 nodes
|
||||
GNS3SuspendNodeTool(), # Suspend GNS3 nodes (preserve state)
|
||||
GNS3UpdateNodeNameTool(), # Update node name
|
||||
ExecuteMultipleDeviceCommands(), # Execute show/display/debug commands
|
||||
# (READ-ONLY)
|
||||
|
||||
@@ -56,6 +56,8 @@ from .gns3_create_node import GNS3CreateNodeTool
|
||||
from .gns3_get_node_temp import GNS3TemplateTool
|
||||
from .gns3_start_node import GNS3StartNodeQuickTool
|
||||
from .gns3_start_node import GNS3StartNodeTool
|
||||
from .gns3_stop_node import GNS3StopNodeTool
|
||||
from .gns3_suspend_node import GNS3SuspendNodeTool
|
||||
from .gns3_update_node_name import GNS3UpdateNodeNameTool
|
||||
from .vpcs_tools_telnetlib3 import VPCSMultiCommands
|
||||
|
||||
@@ -80,6 +82,8 @@ __all__ = [
|
||||
"GNS3LinkTool",
|
||||
"GNS3StartNodeTool",
|
||||
"GNS3StartNodeQuickTool",
|
||||
"GNS3StopNodeTool",
|
||||
"GNS3SuspendNodeTool",
|
||||
"GNS3UpdateNodeNameTool",
|
||||
"GNS3TemplateTool",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# 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 stop tool for network device shutdown.
|
||||
|
||||
Provides functionality to stop one or multiple nodes in GNS3 projects.
|
||||
"""
|
||||
|
||||
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 import Node
|
||||
from gns3server.agent.gns3_copilot.gns3_client import get_gns3_connector
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GNS3StopNodeTool(BaseTool):
|
||||
"""
|
||||
A LangChain tool to stop 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": "stopped"},
|
||||
{"node_id": "...", "name": "...", "status": "stopped"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
name: str = "stop_gns3_node"
|
||||
description: str = """
|
||||
Stops one or multiple nodes in a GNS3 project.
|
||||
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."}
|
||||
|
||||
# Initialize Gns3Connector using factory function
|
||||
logger.info("Connecting to GNS3 server...")
|
||||
gns3_server = get_gns3_connector()
|
||||
|
||||
if gns3_server is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
|
||||
# Stop all nodes and collect results
|
||||
logger.info(
|
||||
"Stopping %d nodes in project %s...",
|
||||
len(node_ids),
|
||||
project_id,
|
||||
)
|
||||
results = []
|
||||
|
||||
for node_id in node_ids:
|
||||
try:
|
||||
node = Node(
|
||||
project_id=project_id,
|
||||
node_id=node_id,
|
||||
connector=gns3_server,
|
||||
)
|
||||
# Verify node exists and get current info
|
||||
node.get()
|
||||
if not node.node_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",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Send stop command
|
||||
node.stop()
|
||||
logger.info(
|
||||
"Stop command sent for node %s (%s)",
|
||||
node_id,
|
||||
node.name,
|
||||
)
|
||||
|
||||
# Get updated status
|
||||
node.get()
|
||||
node_info = {
|
||||
"node_id": node.node_id,
|
||||
"name": node.name or "N/A",
|
||||
"status": node.status or "unknown",
|
||||
}
|
||||
results.append(node_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to stop node %s: %s", node_id, e)
|
||||
results.append(
|
||||
{
|
||||
"node_id": node_id,
|
||||
"name": "N/A",
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
|
||||
# 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,
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Stop 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 stop nodes: %s", e)
|
||||
return {"error": f"Failed to stop nodes: {str(e)}"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test with single node
|
||||
print("=== Testing single node stop ===")
|
||||
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 = GNS3StopNodeTool()
|
||||
result_single = tool._run(test_input_single)
|
||||
pprint(result_single)
|
||||
|
||||
# Test with multiple nodes
|
||||
print("\n=== Testing multiple nodes stop ===")
|
||||
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)
|
||||
@@ -0,0 +1,248 @@
|
||||
# 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 import Node
|
||||
from gns3server.agent.gns3_copilot.gns3_client import get_gns3_connector
|
||||
|
||||
# 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."}
|
||||
|
||||
# Initialize Gns3Connector using factory function
|
||||
logger.info("Connecting to GNS3 server...")
|
||||
gns3_server = get_gns3_connector()
|
||||
|
||||
if gns3_server is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
|
||||
# Suspend all nodes and collect results
|
||||
logger.info(
|
||||
"Suspending %d nodes in project %s...",
|
||||
len(node_ids),
|
||||
project_id,
|
||||
)
|
||||
results = []
|
||||
|
||||
for node_id in node_ids:
|
||||
try:
|
||||
node = Node(
|
||||
project_id=project_id,
|
||||
node_id=node_id,
|
||||
connector=gns3_server,
|
||||
)
|
||||
# Verify node exists and get current info
|
||||
node.get()
|
||||
if not node.node_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",
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Send suspend command
|
||||
node.suspend()
|
||||
logger.info(
|
||||
"Suspend command sent for node %s (%s)",
|
||||
node_id,
|
||||
node.name,
|
||||
)
|
||||
|
||||
# Get updated status
|
||||
node.get()
|
||||
node_info = {
|
||||
"node_id": node.node_id,
|
||||
"name": node.name or "N/A",
|
||||
"status": node.status or "unknown",
|
||||
}
|
||||
results.append(node_info)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to suspend node %s: %s", node_id, e)
|
||||
results.append(
|
||||
{
|
||||
"node_id": node_id,
|
||||
"name": "N/A",
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
|
||||
# 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)
|
||||
Reference in new issue
Block a user