diff --git a/docs/gns3-copilot/README.md b/docs/gns3-copilot/README.md
index a0eb27373..c64972609 100644
--- a/docs/gns3-copilot/README.md
+++ b/docs/gns3-copilot/README.md
@@ -11,7 +11,8 @@ docs/gns3-copilot/
│ ├── chat-api.md # Chat API design (SSE, session management)
│ ├── llm-model-configs.md # LLM model configuration system
│ ├── command-security.md # Command security and filtering
-│ └── context-window-management.md # Context window optimization
+│ ├── context-window-management.md # Context window optimization
+│ └── node-control-tools.md # Node start/stop tools for lab automation
├── todo/ # Planned features and designs
│ ├── jinja2-config-templates-system.md # Config template system
│ ├── config-templates-implementation-guide.md # Template implementation
@@ -71,6 +72,18 @@ Optimization strategies for handling large project contexts.
**Status:** ✅ Implemented
+### Node Control Tools (`implemented/node-control-tools.md`)
+Tools for controlling network device lifecycle in GNS3 projects.
+
+**Key Features:**
+- Start nodes with progress tracking
+- Quick start for automated workflows
+- Stop nodes for lab shutdown
+- Batch operations support
+- Mode-based access control
+
+**Status:** ✅ Implemented
+
## Planned Features
### Jinja2 Configuration Templates (`todo/jinja2-config-templates-system.md`)
@@ -133,4 +146,4 @@ When adding new documentation:
---
-_Last updated: 2026-03-10_
+_Last updated: 2026-03-11_
diff --git a/docs/gns3-copilot/implemented/node-control-tools.md b/docs/gns3-copilot/implemented/node-control-tools.md
new file mode 100644
index 000000000..fb7053315
--- /dev/null
+++ b/docs/gns3-copilot/implemented/node-control-tools.md
@@ -0,0 +1,556 @@
+# Node Control Tools
+
+## Overview
+
+GNS3-Copilot provides tools for controlling the lifecycle of network devices in GNS3 projects. These tools enable AI agents to start, stop, suspend, and manage nodes as part of automated lab management workflows.
+
+## Available Tools
+
+### GNS3StartNodeTool
+
+**Tool Name:** `start_gns3_node`
+
+**Description:** Starts one or multiple nodes in a GNS3 project with progress tracking and status monitoring.
+
+**Input:**
+```json
+{
+ "project_id": "uuid-of-project",
+ "node_ids": ["uuid-of-node-1", "uuid-of-node-2"]
+}
+```
+
+**Output:**
+```json
+{
+ "project_id": "...",
+ "total_nodes": 2,
+ "successful": 2,
+ "failed": 0,
+ "nodes": [
+ {"node_id": "...", "name": "...", "status": "started"},
+ {"node_id": "...", "name": "...", "status": "started"}
+ ]
+}
+```
+
+**Features:**
+- Batch start multiple nodes
+- Progress bar with visual feedback
+- Automatic status verification
+- Comprehensive error handling
+- ~140s base wait time + 10s per additional node
+
+**Use Cases:**
+- Automated lab deployment
+- Multi-node topology initialization
+- Lab startup automation
+
+### GNS3StartNodeQuickTool
+
+**Tool Name:** `start_gns3_node_quick`
+
+**Description:** Starts nodes in a GNS3 project WITHOUT waiting for startup completion. Suitable for automated deployment workflows where long waits would cause HTTP timeouts.
+
+**Input:**
+```json
+{
+ "project_id": "uuid-of-project",
+ "node_ids": ["uuid-of-node-1", "uuid-of-node-2"]
+}
+```
+
+**Output:**
+```json
+{
+ "project_id": "...",
+ "total_nodes": 2,
+ "successful": 2,
+ "failed": 0,
+ "nodes": [
+ {"node_id": "...", "name": "...", "status": "started"}
+ ],
+ "note": "Start commands sent. Nodes are booting in background. Check node status later."
+}
+```
+
+**Features:**
+- Sends start commands immediately
+- No waiting for startup completion
+- Returns initial status
+- Prevents HTTP timeouts in automated workflows
+
+**Use Cases:**
+- Automated CI/CD pipelines
+- Bulk node deployment
+- Workflows requiring immediate return
+
+### GNS3StopNodeTool ✨
+
+**Tool Name:** `stop_gns3_node`
+
+**Description:** Stops one or multiple nodes in a GNS3 project.
+
+**Input:**
+```json
+{
+ "project_id": "uuid-of-project",
+ "node_ids": ["uuid-of-node-1", "uuid-of-node-2"]
+}
+```
+
+**Output:**
+```json
+{
+ "project_id": "...",
+ "total_nodes": 2,
+ "successful": 2,
+ "failed": 0,
+ "nodes": [
+ {"node_id": "...", "name": "...", "status": "stopped"},
+ {"node_id": "...", "name": "...", "status": "stopped"}
+ ]
+}
+```
+
+**Features:**
+- Batch stop multiple nodes
+- Immediate API response (no waiting)
+- Automatic status verification
+- Comprehensive error handling
+- Synchronous operation
+
+**Use Cases:**
+- Lab shutdown automation
+- Resource management
+- Energy-saving workflows
+- Test cleanup procedures
+
+**Implementation Details:**
+- Calls `POST /projects/{project_id}/nodes/{node_id}/stop`
+- Verifies node existence before stopping
+- Retrieves updated status after stop command
+- Returns detailed results for each node
+
+### GNS3SuspendNodeTool ✨
+
+**Tool Name:** `suspend_gns3_node`
+
+**Description:** Suspends one or multiple nodes in a GNS3 project. Suspended nodes preserve their state in memory and can be quickly resumed.
+
+**Input:**
+```json
+{
+ "project_id": "uuid-of-project",
+ "node_ids": ["uuid-of-node-1", "uuid-of-node-2"]
+}
+```
+
+**Output:**
+```json
+{
+ "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. Use resume to continue where you left off."
+}
+```
+
+**Features:**
+- Batch suspend multiple nodes
+- Preserves device state in memory
+- Immediate API response (no waiting)
+- Fast resume capability
+- Automatic status verification
+- Comprehensive error handling
+
+**Use Cases:**
+- Temporary lab pause (continue later)
+- Save experimental state
+- Quick context switching
+- Resource optimization without losing work
+
+**Key Benefits:**
+- **Fast Resume**: Resumes in seconds vs. minutes for full restart
+- **State Preservation**: All configurations, connections, and runtime state saved
+- **Resource Efficiency**: Pauses devices without releasing memory
+- **Lab Snapshots**: Save intermediate states for later analysis
+
+**Implementation Details:**
+- Calls `POST /projects/{project_id}/nodes/{node_id}/suspend`
+- Verifies node existence before suspending
+- Retrieves updated status after suspend command
+- Returns detailed results for each node
+- Node name can be changed while suspended (unlike started state)
+
+**State Comparison:**
+
+| Operation | Result | Recovery Time | State Preserved? | Resource Usage |
+|-----------|--------|---------------|------------------|----------------|
+| **Stop** | Powered off | Slow (full boot) | ❌ No | Minimal |
+| **Suspend** | Paused | Fast (seconds) | ✅ Yes | Medium (memory) |
+| **Start** | Running | N/A | N/A | High |
+
+**When to Use Suspend vs Stop:**
+
+```python
+# ✅ Use SUSPEND when:
+# - Taking a break and continuing later today
+# - Need to test something else temporarily
+# - Want to preserve complex configuration state
+# - Quick switching between lab scenarios
+
+# ✅ Use STOP when:
+# - Done with the lab for now
+# - Need to free up system resources
+# - Finishing a complete test session
+# - Won't need the lab for a while
+```
+
+## Technical Implementation
+
+### Module Structure
+
+```
+gns3server/agent/gns3_copilot/tools_v2/
+├── gns3_start_node.py # Start tools
+├── gns3_stop_node.py # Stop tool
+└── gns3_suspend_node.py # Suspend tool
+```
+
+### API Integration
+
+The tools use the `Node` class from `custom_gns3fy`:
+
+```python
+from gns3server.agent.gns3_copilot.gns3_client import Node
+
+# Start node
+node.start()
+
+# Stop node
+node.stop()
+
+# Suspend node
+node.suspend()
+```
+
+### Progress Tracking
+
+**GNS3StartNodeTool** includes visual progress bar:
+
+```
+Starting 3 node(s), please wait...
+[===========> ] 35.0%
+```
+
+**Progress Calculation:**
+- Base duration: 140 seconds
+- Extra duration: 10 seconds per additional node
+- Formula: `total_duration = 140 + max(0, node_count - 1) * 10`
+
+**GNS3StopNodeTool** does not include progress tracking:
+- Stop operations are typically fast (< 5 seconds)
+- Immediate API response provides status feedback
+- No need for progress indication
+
+**GNS3SuspendNodeTool** does not include progress tracking:
+- Suspend operations are typically fast (< 10 seconds)
+- Immediate API response provides status feedback
+- No need for progress indication
+
+## Node State Transitions
+
+```
+ ┌─────────┐
+ │ Stopped │◀─────── stop()
+ └────┬────┘
+ │
+ │ start()
+ ▼
+ ┌──────────┐
+ │ Started │───suspend()───▶ Suspended
+ └──────────┘ │
+ ▲ │ │
+ │ │ stop() │ resume()
+ └──────┴──────────────────────────┘
+```
+
+**State Change Allowed Operations:**
+
+| Current State | Can Start? | Can Stop? | Can Suspend? | Can Rename? |
+|---------------|------------|-----------|---------------|--------------|
+| **Stopped** | ✅ Yes | ❌ No | ❌ No | ✅ Yes |
+| **Started** | ❌ No | ✅ Yes | ✅ Yes | ❌ No* |
+| **Suspended** | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes |
+
+*Except special node types: cloud, nat, ethernet_switch, ethernet_hub, frame_relay_switch, atm_switch
+
+## Copilot Mode Integration
+
+### Teaching Assistant Mode
+
+**Tools Available:**
+- `GNS3StartNodeTool` - For diagnostics requiring started nodes
+
+**Capabilities:**
+- READ-ONLY diagnostic tools
+- Cannot stop or suspend nodes (prevents disruption of active labs)
+
+### Lab Automation Assistant Mode
+
+**Tools Available:**
+- `GNS3StartNodeTool` - Full lab deployment
+- `GNS3StopNodeTool` - Full lab shutdown
+- `GNS3SuspendNodeTool` - Lab pause with state preservation ✨
+- `GNS3StartNodeQuickTool` - Fast automated deployment
+
+**Capabilities:**
+- Full diagnostic and configuration tools
+- Complete lab lifecycle management (start/stop/suspend)
+- Automated workflows with state preservation
+- Lab snapshot capabilities for later resumption
+
+## Usage Examples
+
+### Example 1: Start Nodes with Progress
+
+```python
+from gns3server.agent.gns3_copilot.tools_v2 import GNS3StartNodeTool
+
+tool = GNS3StartNodeTool()
+result = tool._run(json.dumps({
+ "project_id": "abc-123-def",
+ "node_ids": ["node-1", "node-2", "node-3"]
+}))
+
+# Output includes progress bar and final status
+```
+
+### Example 2: Quick Start for CI/CD
+
+```python
+from gns3server.agent.gns3_copilot.tools_v2 import GNS3StartNodeQuickTool
+
+tool = GNS3StartNodeQuickTool()
+result = tool._run(json.dumps({
+ "project_id": "abc-123-def",
+ "node_ids": ["node-1"]
+}))
+
+# Immediate return without waiting
+```
+
+### Example 3: Stop Nodes
+
+```python
+from gns3server.agent.gns3_copilot.tools_v2 import GNS3StopNodeTool
+
+tool = GNS3StopNodeTool()
+result = tool._run(json.dumps({
+ "project_id": "abc-123-def",
+ "node_ids": ["node-1", "node-2"]
+}))
+
+# Immediate return with stop status
+```
+
+### Example 4: Automated Lab Lifecycle
+
+```python
+# Lab deployment
+start_tool = GNS3StartNodeQuickTool()
+start_result = start_tool._run(json.dumps({
+ "project_id": project_id,
+ "node_ids": all_node_ids
+}))
+
+# ... Run tests ...
+
+# Lab shutdown
+stop_tool = GNS3StopNodeTool()
+stop_result = stop_tool._run(json.dumps({
+ "project_id": project_id,
+ "node_ids": all_node_ids
+}))
+```
+
+### Example 5: Lab Pause and Resume ✨
+
+```python
+from gns3server.agent.gns3_copilot.tools_v2 import GNS3SuspendNodeTool
+
+# Start lab
+start_tool = GNS3StartNodeTool()
+start_result = start_tool._run(json.dumps({
+ "project_id": project_id,
+ "node_ids": all_node_ids
+}))
+
+# ... Configure devices ...
+# ... Run some tests ...
+
+# Suspend lab (preserves all state)
+suspend_tool = GNS3SuspendNodeTool()
+suspend_result = suspend_tool._run(json.dumps({
+ "project_id": project_id,
+ "node_ids": all_node_ids
+}))
+# Nodes suspended - state preserved in memory
+
+# ... Take a break, work on something else ...
+
+# Resume lab (quick recovery)
+start_result = start_tool._run(json.dumps({
+ "project_id": project_id,
+ "node_ids": all_node_ids
+}))
+# Back to previous state in seconds!
+```
+
+### Example 6: Suspend While Renaming Nodes ✨
+
+```python
+from gns3server.agent.gns3_copilot.tools_v2 import (
+ GNS3SuspendNodeTool,
+ GNS3UpdateNodeNameTool
+)
+
+# Suspend nodes first (allows renaming)
+suspend_tool = GNS3SuspendNodeTool()
+suspend_result = suspend_tool._run(json.dumps({
+ "project_id": project_id,
+ "node_ids": node_ids
+}))
+
+# Now rename nodes (possible while suspended!)
+rename_tool = GNS3UpdateNodeNameTool()
+rename_result = rename_tool._run(json.dumps({
+ "project_id": project_id,
+ "nodes": [
+ {"node_id": "node-1", "new_name": "Router-Primary"},
+ {"node_id": "node-2", "new_name": "Router-Backup"}
+ ]
+}))
+
+# Resume when ready
+# Note: Cannot rename while started, but CAN rename while suspended!
+```
+
+## Error Handling
+
+All tools include comprehensive error handling:
+
+### Missing Required Fields
+
+```json
+{
+ "error": "Missing required fields: project_id and node_ids."
+}
+```
+
+### Invalid Input Type
+
+```json
+{
+ "error": "node_ids must be a list."
+}
+```
+
+### Node Not Found
+
+```json
+{
+ "node_id": "uuid",
+ "name": "N/A",
+ "status": "error",
+ "error": "Node not found"
+}
+```
+
+### Connection Error
+
+```json
+{
+ "error": "Failed to connect to GNS3 server. Please check your configuration."
+}
+```
+
+## Security Considerations
+
+### Access Control
+
+- Both tools respect GNS3's built-in access control
+- Requires valid GNS3 server authentication
+- Project-level permissions apply
+
+### Audit Logging
+
+All operations are logged:
+```python
+logger.info("Starting %d nodes in project %s...", len(node_ids), project_id)
+logger.info("Stop command sent for node %s (%s)", node_id, node.name)
+logger.info("Suspend command sent for node %s (%s)", node_id, node.name)
+```
+
+### Mode-Based Restrictions
+
+- **Teaching Assistant Mode**:
+ - Can start nodes
+ - Cannot stop, suspend (prevents disruption of active labs)
+
+- **Lab Automation Assistant Mode**:
+ - Can start, stop, and suspend nodes (full lifecycle control)
+ - Complete lab management including state preservation
+
+## Performance Characteristics
+
+| Operation | Typical Duration | Wait Time | Progress | State Preserved |
+|-----------|-----------------|-----------|----------|-----------------|
+| Start (normal) | 60-180s | ~140s base | Yes | N/A |
+| Start (quick) | < 1s | 0s | No | N/A |
+| Stop | < 5s | 0s | No | ❌ No |
+| Suspend | < 10s | 0s | No | ✅ Yes |
+
+**Notes:**
+- Stop operations are significantly faster than start operations
+- Suspend is slightly slower than stop but preserves state
+- Stop/Suspend do not require progress tracking (immediate feedback)
+- Start duration depends on node type (router, switch, PC, etc.)
+- Suspend provides fast resume capability compared to full start
+
+## Future Enhancements
+
+### Planned Features
+
+- [ ] **Resume Tool**: Explicit resume operation for suspended nodes
+- [ ] **Restart Tool**: Combined stop + start operation
+- [ ] **Bulk Status Check**: Query multiple nodes without stopping
+- [ ] **Conditional Stop/Suspend**: Operate only if node is in specific state
+- [ ] **Graceful Shutdown**: Send halt commands before stopping
+
+### Potential Improvements
+
+- [ ] Progress tracking for long suspend operations (rare but possible)
+- [ ] Concurrent suspend operations (parallel API calls)
+- [ ] Suspend node groups by name pattern
+- [ ] Dependency-aware suspend (suspend in dependency order)
+- [ ] Auto-suspend after idle timeout
+- [ ] State snapshots (save multiple suspend states)
+
+## Related Documentation
+
+- [Chat API](./chat-api.md) - Session management and SSE
+- [Command Security](./command-security.md) - Security framework
+- [LLM Model Configs](./llm-model-configs.md) - Model configuration
+
+---
+
+_Implementation Date: 2026-03-11_
+
+_Status: ✅ Implemented and Available in Lab Automation Assistant Mode_
diff --git a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py
index 4f3f47d5f..a526480e8 100644
--- a/gns3server/agent/gns3_copilot/agent/gns3_copilot.py
+++ b/gns3server/agent/gns3_copilot/agent/gns3_copilot.py
@@ -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)
diff --git a/gns3server/agent/gns3_copilot/tools_v2/__init__.py b/gns3server/agent/gns3_copilot/tools_v2/__init__.py
index d81acb873..a762b56bf 100644
--- a/gns3server/agent/gns3_copilot/tools_v2/__init__.py
+++ b/gns3server/agent/gns3_copilot/tools_v2/__init__.py
@@ -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",
]
diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py
new file mode 100644
index 000000000..66d6de515
--- /dev/null
+++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py
@@ -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 .
+#
+# 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": "", # 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": "", # 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)
diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py
new file mode 100644
index 000000000..0bef3190c
--- /dev/null
+++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py
@@ -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 .
+#
+# 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": "", # 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": "", # 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)