mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
refactor: ruff autofixes
This commit is contained in:
455 files changed
+12322
-14762
No files matched your search
@@ -196,56 +196,35 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
# Validate input
|
||||
device_configs_list, project_id = self._validate_tool_input(tool_input)
|
||||
if (
|
||||
isinstance(device_configs_list, list)
|
||||
and len(device_configs_list) > 0
|
||||
and "error" in device_configs_list[0]
|
||||
):
|
||||
if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
device_configs_list, blocked_commands_map = (
|
||||
self._filter_forbidden_commands_from_device_configs(
|
||||
device_configs_list
|
||||
)
|
||||
device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs(
|
||||
device_configs_list
|
||||
)
|
||||
|
||||
# Expand multiline commands (e.g., banner commands with embedded newlines)
|
||||
# This converts commands like "banner motd #\nline1\nline2\n#" into
|
||||
# separate commands: ["banner motd #", "line1", "line2", "#"]
|
||||
device_configs_list = self._expand_multiline_commands(
|
||||
device_configs_list
|
||||
)
|
||||
device_configs_list = self._expand_multiline_commands(device_configs_list)
|
||||
|
||||
# Create a mapping of device names to their configuration commands
|
||||
device_configs_map = self._configs_map(device_configs_list)
|
||||
|
||||
# Prepare device hosts data
|
||||
try:
|
||||
hosts_data = self._prepare_device_hosts_data(
|
||||
device_configs_list, project_id, jwt_token=jwt_token, url=url
|
||||
)
|
||||
hosts_data = self._prepare_device_hosts_data(device_configs_list, project_id, jwt_token=jwt_token, url=url)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to prepare device hosts data: %s", e)
|
||||
return [{"status": "failed", "error": str(e)}]
|
||||
|
||||
# Check if any devices have errors (e.g., missing device_type tag)
|
||||
error_devices = {
|
||||
name: data
|
||||
for name, data in hosts_data.items()
|
||||
if "error" in data
|
||||
}
|
||||
error_devices = {name: data for name, data in hosts_data.items() if "error" in data}
|
||||
if error_devices:
|
||||
logger.error(
|
||||
"Devices with configuration errors: %s",
|
||||
list(error_devices.keys())
|
||||
)
|
||||
logger.error("Devices with configuration errors: %s", list(error_devices.keys()))
|
||||
return [
|
||||
{
|
||||
"device_name": name,
|
||||
"status": "failed",
|
||||
"error": data["error"]
|
||||
}
|
||||
{"device_name": name, "status": "failed", "error": data["error"]}
|
||||
for name, data in error_devices.items()
|
||||
]
|
||||
|
||||
@@ -275,9 +254,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
except Exception as e:
|
||||
# Overall execution failed
|
||||
logger.error(
|
||||
"Error executing configurations on all devices: %s", e
|
||||
)
|
||||
logger.error("Error executing configurations on all devices: %s", e)
|
||||
return [{"status": "failed", "error": f"Execution error: {str(e)}"}]
|
||||
|
||||
logger.info(
|
||||
@@ -287,22 +264,16 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
return results
|
||||
|
||||
def _run_all_device_configs_with_single_retry(
|
||||
self, task: Task, device_configs_map: dict[str, list[str]]
|
||||
) -> Result:
|
||||
def _run_all_device_configs_with_single_retry(self, task: Task, device_configs_map: dict[str, list[str]]) -> Result:
|
||||
"""Execute configuration commands with single retry mechanism."""
|
||||
device_name = task.host.name
|
||||
config_commands = device_configs_map.get(device_name, [])
|
||||
|
||||
if not config_commands:
|
||||
return Result(
|
||||
host=task.host, result="No configuration commands to execute"
|
||||
)
|
||||
return Result(host=task.host, result="No configuration commands to execute")
|
||||
|
||||
try:
|
||||
_result = task.run(
|
||||
task=netmiko_send_config, config_commands=config_commands
|
||||
)
|
||||
_result = task.run(task=netmiko_send_config, config_commands=config_commands)
|
||||
return Result(host=task.host, result=_result.result)
|
||||
|
||||
except ReadTimeout as e:
|
||||
@@ -322,9 +293,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
# Handle Cisco IOSv L2 where '#' prompt char may be delayed,
|
||||
# causing Netmiko failures. Implements retry logic.
|
||||
if "netmiko_send_config (failed)" in str(e):
|
||||
_result = task.run(
|
||||
task=netmiko_send_config, config_commands=config_commands
|
||||
)
|
||||
_result = task.run(task=netmiko_send_config, config_commands=config_commands)
|
||||
return Result(host=task.host, result=_result.result)
|
||||
|
||||
# Log any other exceptions with full details
|
||||
@@ -364,9 +333,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
parsed_input = json.loads(tool_input)
|
||||
logger.debug("Successfully parsed tool input from JSON string.")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(
|
||||
"Invalid JSON string received as tool input: %s", e
|
||||
)
|
||||
logger.error("Invalid JSON string received as tool input: %s", e)
|
||||
return (
|
||||
[{"status": "failed", "error": f"Invalid JSON string input from model: {e}"}],
|
||||
None,
|
||||
@@ -392,10 +359,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
return ([{"status": "failed", "error": error_msg}], None)
|
||||
|
||||
if not self._validate_project_id(project_id):
|
||||
error_msg = (
|
||||
f"Invalid project_id format: {project_id}. "
|
||||
"Expected UUID format."
|
||||
)
|
||||
error_msg = f"Invalid project_id format: {project_id}. Expected UUID format."
|
||||
logger.error(error_msg)
|
||||
return ([{"status": "failed", "error": error_msg}], None)
|
||||
|
||||
@@ -413,10 +377,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
# Handle legacy format: [...]
|
||||
elif isinstance(parsed_input, list):
|
||||
logger.warning(
|
||||
"Using legacy format without project_id. "
|
||||
"Please use new format with project_id."
|
||||
)
|
||||
logger.warning("Using legacy format without project_id. Please use new format with project_id.")
|
||||
return parsed_input, None
|
||||
|
||||
else:
|
||||
@@ -438,9 +399,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
Returns:
|
||||
True if valid UUID format, False otherwise
|
||||
"""
|
||||
uuid_pattern = (
|
||||
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
)
|
||||
uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
return bool(re.match(uuid_pattern, project_id, re.IGNORECASE))
|
||||
|
||||
def _filter_forbidden_commands_from_device_configs(
|
||||
@@ -465,9 +424,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
commands = device_config["config_commands"]
|
||||
|
||||
# Filter commands
|
||||
allowed_commands, blocked_info = filter_forbidden_commands(
|
||||
commands
|
||||
)
|
||||
allowed_commands, blocked_info = filter_forbidden_commands(commands)
|
||||
|
||||
# Update device config with allowed commands only
|
||||
filtered_config = device_config.copy()
|
||||
@@ -486,9 +443,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
return filtered_list, blocked_commands_map
|
||||
|
||||
def _expand_multiline_commands(
|
||||
self, device_configs_list: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
def _expand_multiline_commands(self, device_configs_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Expand commands that contain embedded newline characters.
|
||||
|
||||
@@ -540,9 +495,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
return expanded_list
|
||||
|
||||
def _configs_map(
|
||||
self, device_config_list: list[dict[str, Any]]
|
||||
) -> dict[str, list[str]]:
|
||||
def _configs_map(self, device_config_list: list[dict[str, Any]]) -> dict[str, list[str]]:
|
||||
"""Create a mapping of device names to their configuration commands."""
|
||||
device_configs_map = {}
|
||||
for device_config in device_config_list:
|
||||
@@ -561,15 +514,10 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Prepare device hosts data from topology information."""
|
||||
# Extract device names list
|
||||
device_names = [
|
||||
device_config["device_name"]
|
||||
for device_config in device_config_list
|
||||
]
|
||||
device_names = [device_config["device_name"] for device_config in device_config_list]
|
||||
|
||||
# Get device port information with project_id
|
||||
hosts_data = get_device_ports_from_topology(
|
||||
device_names, project_id, jwt_token=jwt_token, url=url
|
||||
)
|
||||
hosts_data = get_device_ports_from_topology(device_names, project_id, jwt_token=jwt_token, url=url)
|
||||
|
||||
if not hosts_data:
|
||||
error_msg = (
|
||||
@@ -589,9 +537,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
return hosts_data
|
||||
|
||||
def _initialize_nornir(
|
||||
self, hosts_data: dict[str, dict[str, Any]]
|
||||
) -> Nornir:
|
||||
def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir:
|
||||
"""
|
||||
Initialize Nornir with the provided hosts data.
|
||||
|
||||
@@ -616,8 +562,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
# Log device types being configured
|
||||
device_types = [
|
||||
host["connection_options"]["netmiko"]["extras"]["device_type"]
|
||||
for host in hosts_data.values()
|
||||
host["connection_options"]["netmiko"]["extras"]["device_type"] for host in hosts_data.values()
|
||||
]
|
||||
logger.info(
|
||||
"Initializing Nornir: host=%s, device_types=%s, hosts=%d",
|
||||
@@ -665,16 +610,11 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
device_result = {
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": (
|
||||
f"Device '{device_name}' not found in topology or "
|
||||
"missing console_port"
|
||||
),
|
||||
"error": (f"Device '{device_name}' not found in topology or missing console_port"),
|
||||
}
|
||||
# Add blocked commands info if any
|
||||
if blocked_commands_info:
|
||||
device_result["blocked_commands"] = list(
|
||||
blocked_commands_info.keys()
|
||||
)
|
||||
device_result["blocked_commands"] = list(blocked_commands_info.keys())
|
||||
device_result["blocked_info"] = blocked_commands_info
|
||||
results.append(device_result)
|
||||
continue
|
||||
@@ -684,15 +624,11 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
device_result = {
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": (
|
||||
f"Device '{device_name}' not found in task results"
|
||||
),
|
||||
"error": (f"Device '{device_name}' not found in task results"),
|
||||
}
|
||||
# Add blocked commands info if any
|
||||
if blocked_commands_info:
|
||||
device_result["blocked_commands"] = list(
|
||||
blocked_commands_info.keys()
|
||||
)
|
||||
device_result["blocked_commands"] = list(blocked_commands_info.keys())
|
||||
device_result["blocked_info"] = blocked_commands_info
|
||||
results.append(device_result)
|
||||
continue
|
||||
@@ -704,9 +640,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
if multi_result[0].failed:
|
||||
# Execution failed
|
||||
device_result["status"] = "failed"
|
||||
device_result["error"] = (
|
||||
f"Configuration execution failed: {multi_result[0].result}"
|
||||
)
|
||||
device_result["error"] = f"Configuration execution failed: {multi_result[0].result}"
|
||||
device_result["output"] = multi_result[0].result
|
||||
else:
|
||||
# Execution successful
|
||||
@@ -716,9 +650,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
|
||||
|
||||
# Add blocked commands info if any
|
||||
if blocked_commands_info:
|
||||
device_result["blocked_commands"] = list(
|
||||
blocked_commands_info.keys()
|
||||
)
|
||||
device_result["blocked_commands"] = list(blocked_commands_info.keys())
|
||||
device_result["blocked_info"] = blocked_commands_info
|
||||
# Update status if some commands were blocked but succeeded
|
||||
if device_result["status"] == "success":
|
||||
|
||||
@@ -199,18 +199,12 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
# Validate input
|
||||
device_configs_list, project_id = self._validate_tool_input(tool_input)
|
||||
if (
|
||||
isinstance(device_configs_list, list)
|
||||
and len(device_configs_list) > 0
|
||||
and "error" in device_configs_list[0]
|
||||
):
|
||||
if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Filter forbidden commands and store blocked commands info
|
||||
device_configs_list, blocked_commands_map = (
|
||||
self._filter_forbidden_commands_from_device_configs(
|
||||
device_configs_list
|
||||
)
|
||||
device_configs_list, blocked_commands_map = self._filter_forbidden_commands_from_device_configs(
|
||||
device_configs_list
|
||||
)
|
||||
|
||||
# Create a mapping of device names to their display commands
|
||||
@@ -218,30 +212,17 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
# Prepare device hosts data
|
||||
try:
|
||||
hosts_data = self._prepare_device_hosts_data(
|
||||
device_configs_list, project_id, jwt_token=jwt_token, url=url
|
||||
)
|
||||
hosts_data = self._prepare_device_hosts_data(device_configs_list, project_id, jwt_token=jwt_token, url=url)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to prepare device hosts data: %s", e)
|
||||
return [{"status": "failed", "error": str(e)}]
|
||||
|
||||
# Check if any devices have errors (e.g., missing device_type tag)
|
||||
error_devices = {
|
||||
name: data
|
||||
for name, data in hosts_data.items()
|
||||
if "error" in data
|
||||
}
|
||||
error_devices = {name: data for name, data in hosts_data.items() if "error" in data}
|
||||
if error_devices:
|
||||
logger.error(
|
||||
"Devices with configuration errors: %s",
|
||||
list(error_devices.keys())
|
||||
)
|
||||
logger.error("Devices with configuration errors: %s", list(error_devices.keys()))
|
||||
return [
|
||||
{
|
||||
"device_name": name,
|
||||
"status": "failed",
|
||||
"error": data["error"]
|
||||
}
|
||||
{"device_name": name, "status": "failed", "error": data["error"]}
|
||||
for name, data in error_devices.items()
|
||||
]
|
||||
|
||||
@@ -281,17 +262,13 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
return results
|
||||
|
||||
def _run_all_device_configs_with_single_retry(
|
||||
self, task: Task, device_configs_map: dict[str, list[str]]
|
||||
) -> Result:
|
||||
def _run_all_device_configs_with_single_retry(self, task: Task, device_configs_map: dict[str, list[str]]) -> Result:
|
||||
"""Execute READ-ONLY diagnostic commands with single retry."""
|
||||
device_name = task.host.name
|
||||
diagnostic_commands = device_configs_map.get(device_name, [])
|
||||
|
||||
if not diagnostic_commands:
|
||||
return Result(
|
||||
host=task.host, result="No diagnostic commands to execute"
|
||||
)
|
||||
return Result(host=task.host, result="No diagnostic commands to execute")
|
||||
|
||||
try:
|
||||
_result = task.run(
|
||||
@@ -366,9 +343,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
parsed_input = json.loads(tool_input)
|
||||
logger.info("Successfully parsed tool input from JSON string.")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(
|
||||
"Invalid JSON string received as tool input: %s", e
|
||||
)
|
||||
logger.error("Invalid JSON string received as tool input: %s", e)
|
||||
return (
|
||||
[{"status": "failed", "error": f"Invalid JSON string input from model: {e}"}],
|
||||
None,
|
||||
@@ -412,9 +387,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
# Handle legacy format: [...]
|
||||
elif isinstance(parsed_input, list):
|
||||
logger.warning(
|
||||
"Legacy input format without project_id. Use new format."
|
||||
)
|
||||
logger.warning("Legacy input format without project_id. Use new format.")
|
||||
return parsed_input, None
|
||||
|
||||
else:
|
||||
@@ -435,9 +408,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
Returns:
|
||||
True if valid UUID format, False otherwise
|
||||
"""
|
||||
uuid_pattern = (
|
||||
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
)
|
||||
uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
return bool(re.match(uuid_pattern, project_id, re.IGNORECASE))
|
||||
|
||||
def _filter_forbidden_commands_from_device_configs(
|
||||
@@ -462,9 +433,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
commands = device_config["commands"]
|
||||
|
||||
# Filter commands
|
||||
allowed_commands, blocked_info = filter_forbidden_commands(
|
||||
commands
|
||||
)
|
||||
allowed_commands, blocked_info = filter_forbidden_commands(commands)
|
||||
|
||||
# Update device config with allowed commands only
|
||||
filtered_config = device_config.copy()
|
||||
@@ -483,9 +452,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
return filtered_list, blocked_commands_map
|
||||
|
||||
def _configs_map(
|
||||
self, device_config_list: list[dict[str, Any]]
|
||||
) -> dict[str, list[str]]:
|
||||
def _configs_map(self, device_config_list: list[dict[str, Any]]) -> dict[str, list[str]]:
|
||||
"""Create a mapping of device names to their diagnostic commands."""
|
||||
device_diagnostic_map = {}
|
||||
for device_config in device_config_list:
|
||||
@@ -504,21 +471,13 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Prepare device hosts data from topology information."""
|
||||
# Extract device names list
|
||||
device_names = [
|
||||
device_config["device_name"]
|
||||
for device_config in device_config_list
|
||||
]
|
||||
device_names = [device_config["device_name"] for device_config in device_config_list]
|
||||
|
||||
# Get device port information with project_id
|
||||
hosts_data = get_device_ports_from_topology(
|
||||
device_names, project_id, jwt_token=jwt_token, url=url
|
||||
)
|
||||
hosts_data = get_device_ports_from_topology(device_names, project_id, jwt_token=jwt_token, url=url)
|
||||
|
||||
if not hosts_data:
|
||||
error_msg = (
|
||||
f"Failed to get device info from topology. "
|
||||
f"Project: {project_id}, Devices: {device_names}"
|
||||
)
|
||||
error_msg = f"Failed to get device info from topology. Project: {project_id}, Devices: {device_names}"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
# Check for missing devices
|
||||
@@ -532,9 +491,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
return hosts_data
|
||||
|
||||
def _initialize_nornir(
|
||||
self, hosts_data: dict[str, dict[str, Any]]
|
||||
) -> Nornir:
|
||||
def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> Nornir:
|
||||
"""
|
||||
Initialize Nornir with the provided hosts data.
|
||||
|
||||
@@ -559,8 +516,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
# Log device types being configured
|
||||
device_types = [
|
||||
host["connection_options"]["netmiko"]["extras"]["device_type"]
|
||||
for host in hosts_data.values()
|
||||
host["connection_options"]["netmiko"]["extras"]["device_type"] for host in hosts_data.values()
|
||||
]
|
||||
logger.info(
|
||||
"Initializing Nornir: host=%s, device_types=%s, hosts=%d",
|
||||
@@ -608,16 +564,11 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
device_result = {
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": (
|
||||
f"Device '{device_name}' not found in topology "
|
||||
"or missing console_port"
|
||||
),
|
||||
"error": (f"Device '{device_name}' not found in topology or missing console_port"),
|
||||
}
|
||||
# Add blocked commands info if any
|
||||
if blocked_commands_info:
|
||||
device_result["blocked_commands"] = list(
|
||||
blocked_commands_info.keys()
|
||||
)
|
||||
device_result["blocked_commands"] = list(blocked_commands_info.keys())
|
||||
device_result["blocked_info"] = blocked_commands_info
|
||||
results.append(device_result)
|
||||
continue
|
||||
@@ -627,15 +578,11 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
device_result = {
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": (
|
||||
f"Device '{device_name}' not found in task results"
|
||||
),
|
||||
"error": (f"Device '{device_name}' not found in task results"),
|
||||
}
|
||||
# Add blocked commands info if any
|
||||
if blocked_commands_info:
|
||||
device_result["blocked_commands"] = list(
|
||||
blocked_commands_info.keys()
|
||||
)
|
||||
device_result["blocked_commands"] = list(blocked_commands_info.keys())
|
||||
device_result["blocked_info"] = blocked_commands_info
|
||||
results.append(device_result)
|
||||
continue
|
||||
@@ -647,9 +594,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
if multi_result[0].failed:
|
||||
# Execution failed
|
||||
device_result["status"] = "failed"
|
||||
device_result["error"] = (
|
||||
f"Diagnostic command failed: {multi_result[0].result}"
|
||||
)
|
||||
device_result["error"] = f"Diagnostic command failed: {multi_result[0].result}"
|
||||
device_result["output"] = multi_result[0].result
|
||||
else:
|
||||
# Execution successful
|
||||
@@ -659,9 +604,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
|
||||
|
||||
# Add blocked commands info if any
|
||||
if blocked_commands_info:
|
||||
device_result["blocked_commands"] = list(
|
||||
blocked_commands_info.keys()
|
||||
)
|
||||
device_result["blocked_commands"] = list(blocked_commands_info.keys())
|
||||
device_result["blocked_info"] = blocked_commands_info
|
||||
# Update status if some commands were blocked but succeeded
|
||||
if device_result["status"] == "success":
|
||||
|
||||
@@ -132,9 +132,7 @@ class GNS3LinkTool(BaseTool):
|
||||
|
||||
if not isinstance(links_data, list) or len(links_data) == 0:
|
||||
logger.error("Invalid links data: must be a non-empty array")
|
||||
return [
|
||||
{"error": "Invalid links data: must be a non-empty array"}
|
||||
]
|
||||
return [{"error": "Invalid links data: must be a non-empty array"}]
|
||||
|
||||
# Build handler context (JWT + server URL from request context)
|
||||
logger.info("Connecting to GNS3 server...")
|
||||
@@ -142,14 +140,7 @@ class GNS3LinkTool(BaseTool):
|
||||
|
||||
if gns3_ctx is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return [
|
||||
{
|
||||
"error": (
|
||||
"Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
)
|
||||
}
|
||||
]
|
||||
return [{"error": ("Failed to connect to GNS3 server. Please check your configuration.")}]
|
||||
|
||||
# Fetch all nodes once for port resolution
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
@@ -172,9 +163,7 @@ class GNS3LinkTool(BaseTool):
|
||||
|
||||
# Validate link parameters
|
||||
if not all([node_id1, port1, node_id2, port2]):
|
||||
error_msg = (
|
||||
f"Missing required fields in link definition {i}"
|
||||
)
|
||||
error_msg = f"Missing required fields in link definition {i}"
|
||||
logger.error(error_msg)
|
||||
created_links.append({"error": error_msg})
|
||||
continue
|
||||
@@ -218,29 +207,15 @@ class GNS3LinkTool(BaseTool):
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": node_id1,
|
||||
"adapter_number": port1_info.get(
|
||||
"adapter_number", 0
|
||||
),
|
||||
"port_number": port1_info.get(
|
||||
"port_number", 0
|
||||
),
|
||||
"label": {
|
||||
"text": port1_info.get("short_name")
|
||||
or port1
|
||||
},
|
||||
"adapter_number": port1_info.get("adapter_number", 0),
|
||||
"port_number": port1_info.get("port_number", 0),
|
||||
"label": {"text": port1_info.get("short_name") or port1},
|
||||
},
|
||||
{
|
||||
"node_id": node_id2,
|
||||
"adapter_number": port2_info.get(
|
||||
"adapter_number", 0
|
||||
),
|
||||
"port_number": port2_info.get(
|
||||
"port_number", 0
|
||||
),
|
||||
"label": {
|
||||
"text": port2_info.get("short_name")
|
||||
or port2
|
||||
},
|
||||
"adapter_number": port2_info.get("adapter_number", 0),
|
||||
"port_number": port2_info.get("port_number", 0),
|
||||
"label": {"text": port2_info.get("short_name") or port2},
|
||||
},
|
||||
],
|
||||
"fields": ["link_id"],
|
||||
@@ -266,9 +241,7 @@ class GNS3LinkTool(BaseTool):
|
||||
created_links.append({"error": error_msg})
|
||||
|
||||
# Log final results
|
||||
success_count = len(
|
||||
[link for link in created_links if "error" not in link]
|
||||
)
|
||||
success_count = len([link for link in created_links if "error" not in link])
|
||||
logger.info(
|
||||
"Link creation completed: %d successful, %d failed",
|
||||
success_count,
|
||||
|
||||
@@ -169,9 +169,7 @@ class GNS3CreateNodeTool(BaseTool):
|
||||
# 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
|
||||
)
|
||||
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")
|
||||
@@ -187,14 +185,10 @@ class GNS3CreateNodeTool(BaseTool):
|
||||
]
|
||||
):
|
||||
logger.error(
|
||||
"Invalid input: Node %d missing or invalid "
|
||||
"template_id, x, or y.",
|
||||
"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."
|
||||
}
|
||||
return {"error": f"Node {i + 1} missing or invalid template_id, x, or y."}
|
||||
|
||||
# Build handler context (JWT + server URL from request context)
|
||||
logger.info("Connecting to GNS3 server...")
|
||||
@@ -202,15 +196,10 @@ class GNS3CreateNodeTool(BaseTool):
|
||||
|
||||
if gns3_ctx is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
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
|
||||
)
|
||||
logger.info("Creating %d nodes in project %s...", len(nodes), project_id)
|
||||
results: list[dict[str, Any]] = []
|
||||
|
||||
for i, node_data in enumerate(nodes):
|
||||
@@ -261,12 +250,8 @@ class GNS3CreateNodeTool(BaseTool):
|
||||
# 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"]
|
||||
)
|
||||
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 = {
|
||||
@@ -293,9 +278,7 @@ class GNS3CreateNodeTool(BaseTool):
|
||||
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)}"
|
||||
}
|
||||
return {"error": f"Failed to process node creation request: {str(e)}"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -46,12 +46,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Built-in templates to filter out (utility templates, not actual network devices)
|
||||
FILTERED_TEMPLATES = {
|
||||
"atm_switch", # ATM switch
|
||||
"cloud", # Cloud
|
||||
"ethernet_hub", # Ethernet hub
|
||||
"ethernet_switch", # Ethernet switch (built-in)
|
||||
"atm_switch", # ATM switch
|
||||
"cloud", # Cloud
|
||||
"ethernet_hub", # Ethernet hub
|
||||
"ethernet_switch", # Ethernet switch (built-in)
|
||||
"frame_relay_switch", # Frame Relay switch
|
||||
"nat", # NAT device
|
||||
"nat", # NAT device
|
||||
}
|
||||
|
||||
|
||||
@@ -131,17 +131,10 @@ class GNS3TemplateTool(BaseTool):
|
||||
|
||||
if gns3_server is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": (
|
||||
"Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
)
|
||||
}
|
||||
return {"error": ("Failed to connect to GNS3 server. Please check your configuration.")}
|
||||
|
||||
# Retrieve all available templates
|
||||
templates = gns3_server.http_call(
|
||||
"get", f"{gns3_server.base_url}/templates"
|
||||
).json()
|
||||
templates = gns3_server.http_call("get", f"{gns3_server.base_url}/templates").json()
|
||||
|
||||
# Filter out utility templates and extract relevant info
|
||||
template_info = []
|
||||
@@ -154,11 +147,13 @@ class GNS3TemplateTool(BaseTool):
|
||||
continue
|
||||
|
||||
# Extract name, template_id, and template_type
|
||||
template_info.append({
|
||||
"name": template.get("name", "N/A"),
|
||||
"template_id": template.get("template_id", "N/A"),
|
||||
"template_type": template.get("template_type", "N/A"),
|
||||
})
|
||||
template_info.append(
|
||||
{
|
||||
"name": template.get("name", "N/A"),
|
||||
"template_id": template.get("template_id", "N/A"),
|
||||
"template_type": template.get("template_type", "N/A"),
|
||||
}
|
||||
)
|
||||
|
||||
# Return JSON-formatted result with full logging
|
||||
result = {"templates": template_info}
|
||||
@@ -172,9 +167,7 @@ class GNS3TemplateTool(BaseTool):
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to connect to GNS3 server or retrieve templates: %s", e
|
||||
)
|
||||
logger.error("Failed to connect to GNS3 server or retrieve templates: %s", e)
|
||||
return {"error": f"Failed to retrieve templates: {str(e)}"}
|
||||
|
||||
|
||||
|
||||
@@ -198,18 +198,14 @@ class GNS3PacketFilterTool(BaseTool):
|
||||
valid_actions = ["get_available", "set", "get", "clear"]
|
||||
if action not in valid_actions:
|
||||
logger.error("Invalid action: %s. Must be one of %s", action, valid_actions)
|
||||
return {
|
||||
"error": f"Invalid action: {action}. Must be one of {valid_actions}"
|
||||
}
|
||||
return {"error": f"Invalid action: {action}. Must be one of {valid_actions}"}
|
||||
|
||||
# Validate filters for "set" action
|
||||
if action == "set":
|
||||
filters = input_data.get("filters")
|
||||
if not filters or not isinstance(filters, dict):
|
||||
logger.error("Invalid input: 'set' action requires 'filters' dict.")
|
||||
return {
|
||||
"error": "'set' action requires 'filters' dict with filter configuration."
|
||||
}
|
||||
return {"error": "'set' action requires 'filters' dict with filter configuration."}
|
||||
|
||||
# Build handler context (JWT + server URL from request context)
|
||||
logger.info("Connecting to GNS3 server...")
|
||||
@@ -217,28 +213,19 @@ class GNS3PacketFilterTool(BaseTool):
|
||||
|
||||
if gns3_ctx is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
return {"error": "Failed to connect to GNS3 server. Please check your configuration."}
|
||||
|
||||
# Execute action
|
||||
logger.info(
|
||||
"Processing packet filter action '%s' for link %s...", action, link_id
|
||||
)
|
||||
logger.info("Processing packet filter action '%s' for link %s...", action, link_id)
|
||||
if action == "get_available":
|
||||
result = self._get_available_filters(gns3_ctx, project_id, link_id)
|
||||
elif action == "set":
|
||||
filters = input_data.get("filters", {})
|
||||
result = self._set_filters(
|
||||
gns3_ctx, project_id, link_id, filters, show_filters_icon
|
||||
)
|
||||
result = self._set_filters(gns3_ctx, project_id, link_id, filters, show_filters_icon)
|
||||
elif action == "get":
|
||||
result = self._get_filters(gns3_ctx, project_id, link_id)
|
||||
elif action == "clear":
|
||||
result = self._clear_filters(
|
||||
gns3_ctx, project_id, link_id, show_filters_icon
|
||||
)
|
||||
result = self._clear_filters(gns3_ctx, project_id, link_id, show_filters_icon)
|
||||
else:
|
||||
result = {"error": f"Unknown action: {action}"}
|
||||
|
||||
@@ -252,18 +239,12 @@ class GNS3PacketFilterTool(BaseTool):
|
||||
return {"error": f"Invalid JSON input: {e}"}
|
||||
except Exception as e:
|
||||
logger.error("Failed to process packet filter request: %s", e)
|
||||
return {
|
||||
"error": f"Failed to process packet filter request: {str(e)}"
|
||||
}
|
||||
return {"error": f"Failed to process packet filter request: {str(e)}"}
|
||||
|
||||
def _get_available_filters(
|
||||
self, gns3_ctx: dict, project_id: str, link_id: str
|
||||
) -> dict[str, Any]:
|
||||
def _get_available_filters(self, gns3_ctx: dict, project_id: str, link_id: str) -> dict[str, Any]:
|
||||
"""Get available filter types for the link."""
|
||||
try:
|
||||
filters = available_filters_handler(
|
||||
{"project_id": project_id, "link_id": link_id}, gns3_ctx
|
||||
)
|
||||
filters = available_filters_handler({"project_id": project_id, "link_id": link_id}, gns3_ctx)
|
||||
if "error" in filters:
|
||||
raise RuntimeError(filters["error"])
|
||||
logger.info("Retrieved %d available filter types.", len(filters))
|
||||
@@ -307,13 +288,9 @@ class GNS3PacketFilterTool(BaseTool):
|
||||
if "Invalid" in result.stdout or "Invalid" in result.stderr:
|
||||
error_lines = []
|
||||
if "Invalid" in result.stderr:
|
||||
error_lines.extend(
|
||||
line for line in result.stderr.split("\n") if "Invalid" in line
|
||||
)
|
||||
error_lines.extend(line for line in result.stderr.split("\n") if "Invalid" in line)
|
||||
if "Invalid" in result.stdout:
|
||||
error_lines.extend(
|
||||
line for line in result.stdout.split("\n") if "Invalid" in line
|
||||
)
|
||||
error_lines.extend(line for line in result.stdout.split("\n") if "Invalid" in line)
|
||||
|
||||
# Strip interface suffix (e.g., "for interface 'lo'") for cleaner error
|
||||
error_msg_parts = []
|
||||
@@ -336,10 +313,7 @@ class GNS3PacketFilterTool(BaseTool):
|
||||
|
||||
except FileNotFoundError:
|
||||
# tshark not installed - skip validation
|
||||
logger.warning(
|
||||
"tshark not found, skipping BPF syntax validation. "
|
||||
"Install tshark to enable BPF validation."
|
||||
)
|
||||
logger.warning("tshark not found, skipping BPF syntax validation. Install tshark to enable BPF validation.")
|
||||
return {"valid": True, "error": None}
|
||||
|
||||
except Exception as e:
|
||||
@@ -414,14 +388,10 @@ class GNS3PacketFilterTool(BaseTool):
|
||||
"status": "failed",
|
||||
}
|
||||
|
||||
def _get_filters(
|
||||
self, gns3_ctx: dict, project_id: str, link_id: str
|
||||
) -> dict[str, Any]:
|
||||
def _get_filters(self, gns3_ctx: dict, project_id: str, link_id: str) -> dict[str, Any]:
|
||||
"""Get current filters configured on the link."""
|
||||
try:
|
||||
link = get_link_handler(
|
||||
{"project_id": project_id, "link_id": link_id}, gns3_ctx
|
||||
)
|
||||
link = get_link_handler({"project_id": project_id, "link_id": link_id}, gns3_ctx)
|
||||
if "error" in link:
|
||||
raise RuntimeError(link["error"])
|
||||
|
||||
|
||||
@@ -106,13 +106,8 @@ class GNS3StartNodeTool(BaseTool):
|
||||
|
||||
# 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."
|
||||
}
|
||||
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.")
|
||||
@@ -124,10 +119,7 @@ class GNS3StartNodeTool(BaseTool):
|
||||
|
||||
if gns3_ctx is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
return {"error": "Failed to connect to GNS3 server. Please check your configuration."}
|
||||
|
||||
# Verify nodes exist and capture pre-start info (one call)
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
@@ -143,14 +135,8 @@ class GNS3StartNodeTool(BaseTool):
|
||||
)
|
||||
results = []
|
||||
known_ids = [nid for nid in node_ids if nid in nodes_by_id]
|
||||
start_results = start_node_handler(
|
||||
{"project_id": project_id, "node_ids": known_ids}, gns3_ctx
|
||||
)
|
||||
start_errors = {
|
||||
r["node_id"]: r.get("error")
|
||||
for r in start_results
|
||||
if r.get("status") == "error"
|
||||
}
|
||||
start_results = start_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx)
|
||||
start_errors = {r["node_id"]: r.get("error") for r in start_results if r.get("status") == "error"}
|
||||
|
||||
# Get immediate status (likely 'starting' or 'stopped') — one call
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
@@ -160,9 +146,7 @@ class GNS3StartNodeTool(BaseTool):
|
||||
|
||||
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
|
||||
)
|
||||
logger.error("Node %s not found in project %s", node_id, project_id)
|
||||
results.append(
|
||||
{
|
||||
"node_id": node_id,
|
||||
@@ -204,9 +188,7 @@ class GNS3StartNodeTool(BaseTool):
|
||||
)
|
||||
|
||||
# Analyze results (count based on successful command sending)
|
||||
successful_nodes = [
|
||||
r for r in results if r.get("status") != "error"
|
||||
]
|
||||
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
|
||||
@@ -217,8 +199,7 @@ class GNS3StartNodeTool(BaseTool):
|
||||
"failed": len(failed_nodes),
|
||||
"nodes": results,
|
||||
"note": (
|
||||
"Start commands sent. Nodes are booting in background. "
|
||||
"Use wait_seconds, then check node status."
|
||||
"Start commands sent. Nodes are booting in background. Use wait_seconds, then check node status."
|
||||
),
|
||||
}
|
||||
|
||||
@@ -249,9 +230,7 @@ if __name__ == "__main__":
|
||||
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
|
||||
"node_ids": ["fbeda109-9a74-4d8c-a749-cc3847911a90"], # Replace with actual node UUID
|
||||
}
|
||||
)
|
||||
tool = GNS3StartNodeTool()
|
||||
|
||||
@@ -94,13 +94,8 @@ class GNS3StopNodeTool(BaseTool):
|
||||
|
||||
# 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."
|
||||
}
|
||||
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.")
|
||||
@@ -112,10 +107,7 @@ class GNS3StopNodeTool(BaseTool):
|
||||
|
||||
if gns3_ctx is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
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)
|
||||
@@ -131,14 +123,8 @@ class GNS3StopNodeTool(BaseTool):
|
||||
)
|
||||
results = []
|
||||
known_ids = [nid for nid in node_ids if nid in nodes_by_id]
|
||||
stop_results = stop_node_handler(
|
||||
{"project_id": project_id, "node_ids": known_ids}, gns3_ctx
|
||||
)
|
||||
stop_errors = {
|
||||
r["node_id"]: r.get("error")
|
||||
for r in stop_results
|
||||
if r.get("status") == "error"
|
||||
}
|
||||
stop_results = stop_node_handler({"project_id": project_id, "node_ids": known_ids}, gns3_ctx)
|
||||
stop_errors = {r["node_id"]: r.get("error") for r in stop_results if r.get("status") == "error"}
|
||||
|
||||
# Get updated status — one call
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
@@ -148,9 +134,7 @@ class GNS3StopNodeTool(BaseTool):
|
||||
|
||||
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
|
||||
)
|
||||
logger.error("Node %s not found in project %s", node_id, project_id)
|
||||
results.append(
|
||||
{
|
||||
"node_id": node_id,
|
||||
@@ -160,9 +144,7 @@ class GNS3StopNodeTool(BaseTool):
|
||||
}
|
||||
)
|
||||
elif node_id in stop_errors:
|
||||
logger.error(
|
||||
"Failed to stop node %s: %s", node_id, stop_errors[node_id]
|
||||
)
|
||||
logger.error("Failed to stop node %s: %s", node_id, stop_errors[node_id])
|
||||
results.append(
|
||||
{
|
||||
"node_id": node_id,
|
||||
@@ -187,9 +169,7 @@ class GNS3StopNodeTool(BaseTool):
|
||||
)
|
||||
|
||||
# Analyze results
|
||||
successful_nodes = [
|
||||
r for r in results if r.get("status") != "error"
|
||||
]
|
||||
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
|
||||
@@ -223,9 +203,7 @@ if __name__ == "__main__":
|
||||
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
|
||||
"node_ids": ["fbeda109-9a74-4d8c-a749-cc3847911a90"], # Replace with actual node UUID
|
||||
}
|
||||
)
|
||||
tool = GNS3StopNodeTool()
|
||||
|
||||
@@ -100,13 +100,8 @@ class GNS3SuspendNodeTool(BaseTool):
|
||||
|
||||
# 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."
|
||||
}
|
||||
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.")
|
||||
@@ -118,10 +113,7 @@ class GNS3SuspendNodeTool(BaseTool):
|
||||
|
||||
if gns3_ctx is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
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)
|
||||
@@ -137,14 +129,8 @@ class GNS3SuspendNodeTool(BaseTool):
|
||||
)
|
||||
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"
|
||||
}
|
||||
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)
|
||||
@@ -154,9 +140,7 @@ class GNS3SuspendNodeTool(BaseTool):
|
||||
|
||||
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
|
||||
)
|
||||
logger.error("Node %s not found in project %s", node_id, project_id)
|
||||
results.append(
|
||||
{
|
||||
"node_id": node_id,
|
||||
@@ -195,9 +179,7 @@ class GNS3SuspendNodeTool(BaseTool):
|
||||
)
|
||||
|
||||
# Analyze results
|
||||
successful_nodes = [
|
||||
r for r in results if r.get("status") != "error"
|
||||
]
|
||||
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
|
||||
@@ -207,10 +189,7 @@ class GNS3SuspendNodeTool(BaseTool):
|
||||
"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."
|
||||
),
|
||||
"note": ("Suspended nodes preserve their state in memory. Use resume to continue where you left off."),
|
||||
}
|
||||
|
||||
logger.info(
|
||||
@@ -235,9 +214,7 @@ if __name__ == "__main__":
|
||||
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
|
||||
"node_ids": ["fbeda109-9a74-4d8c-a749-cc3847911a90"], # Replace with actual node UUID
|
||||
}
|
||||
)
|
||||
tool = GNS3SuspendNodeTool()
|
||||
|
||||
@@ -125,9 +125,7 @@ class GNS3UpdateNodeNameTool(BaseTool):
|
||||
return {"error": f"Node {i + 1} must be a dictionary."}
|
||||
if "node_id" not in node_data or "new_name" not in node_data:
|
||||
logger.error("Node %d missing node_id or new_name.", i + 1)
|
||||
return {
|
||||
"error": f"Node {i + 1} missing node_id or new_name."
|
||||
}
|
||||
return {"error": f"Node {i + 1} missing node_id or new_name."}
|
||||
|
||||
# Build handler context (JWT + server URL from request context)
|
||||
logger.info("Connecting to GNS3 server...")
|
||||
@@ -135,10 +133,7 @@ class GNS3UpdateNodeNameTool(BaseTool):
|
||||
|
||||
if gns3_ctx is None:
|
||||
logger.error("Failed to create GNS3 connector")
|
||||
return {
|
||||
"error": "Failed to connect to GNS3 server. "
|
||||
"Please check your configuration."
|
||||
}
|
||||
return {"error": "Failed to connect to GNS3 server. Please check your configuration."}
|
||||
|
||||
# Fetch current node names in one call (old names + existence)
|
||||
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)
|
||||
@@ -208,9 +203,7 @@ class GNS3UpdateNodeNameTool(BaseTool):
|
||||
"error": "Name verification failed",
|
||||
}
|
||||
results.append(error_info)
|
||||
logger.error(
|
||||
"Failed to update node name for %s", node_id
|
||||
)
|
||||
logger.error("Failed to update node name for %s", node_id)
|
||||
|
||||
except Exception as e:
|
||||
error_info = {
|
||||
@@ -223,9 +216,7 @@ class GNS3UpdateNodeNameTool(BaseTool):
|
||||
logger.error("Failed to update node %d: %s", i + 1, e)
|
||||
|
||||
# Analyze results
|
||||
successful_nodes = [
|
||||
r for r in results if r.get("status") == "success"
|
||||
]
|
||||
successful_nodes = [r for r in results if r.get("status") == "success"]
|
||||
failed_nodes = [r for r in results if r.get("status") == "failed"]
|
||||
|
||||
# Construct final response
|
||||
|
||||
@@ -88,9 +88,7 @@ class GNS3WaitTool(BaseTool):
|
||||
if not isinstance(seconds, int) or isinstance(seconds, bool):
|
||||
return {"error": "seconds must be an integer (1-600)."}
|
||||
if not 1 <= seconds <= MAX_WAIT_SECONDS:
|
||||
return {
|
||||
"error": f"seconds must be between 1 and {MAX_WAIT_SECONDS}."
|
||||
}
|
||||
return {"error": f"seconds must be between 1 and {MAX_WAIT_SECONDS}."}
|
||||
|
||||
logger.info("Waiting %d seconds...", seconds)
|
||||
waited = 0
|
||||
|
||||
@@ -123,19 +123,25 @@ class PacketAnalysisTool(BaseTool):
|
||||
|
||||
query = query.strip()
|
||||
if len(query.split()) > 1:
|
||||
return json.dumps({
|
||||
"error": "Only one keyword allowed",
|
||||
"hint": 'Use a single keyword like "ospf.lsa" or "bgp", not multiple words',
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "Only one keyword allowed",
|
||||
"hint": 'Use a single keyword like "ospf.lsa" or "bgp", not multiple words',
|
||||
}
|
||||
)
|
||||
|
||||
tshark = subprocess.Popen(
|
||||
["tshark", "-G", "fields"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
grep = subprocess.Popen(
|
||||
["grep", "-iF", query, "-"],
|
||||
stdin=tshark.stdout, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, text=True,
|
||||
stdin=tshark.stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
text=True,
|
||||
)
|
||||
tshark.stdout.close()
|
||||
stdout, _ = grep.communicate(timeout=30)
|
||||
@@ -151,21 +157,27 @@ class PacketAnalysisTool(BaseTool):
|
||||
name = parts[2]
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
results.append({
|
||||
"field": name,
|
||||
"type": parts[3] if len(parts) > 3 else "",
|
||||
"protocol": parts[4] if len(parts) > 4 else "",
|
||||
"description": parts[7] if len(parts) > 7 else "",
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"field": name,
|
||||
"type": parts[3] if len(parts) > 3 else "",
|
||||
"protocol": parts[4] if len(parts) > 4 else "",
|
||||
"description": parts[7] if len(parts) > 7 else "",
|
||||
}
|
||||
)
|
||||
if len(results) >= 40:
|
||||
break
|
||||
|
||||
return json.dumps({
|
||||
"query": query,
|
||||
"count": len(results),
|
||||
"fields": results,
|
||||
"hint": "Use the 'field' values in tshark_args with -e",
|
||||
}, ensure_ascii=False, indent=2)
|
||||
return json.dumps(
|
||||
{
|
||||
"query": query,
|
||||
"count": len(results),
|
||||
"fields": results,
|
||||
"hint": "Use the 'field' values in tshark_args with -e",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _load_valid_tshark_fields(cls) -> set:
|
||||
@@ -235,11 +247,13 @@ class PacketAnalysisTool(BaseTool):
|
||||
i += 1
|
||||
|
||||
if invalid_fields:
|
||||
return json.dumps({
|
||||
"error": f"Invalid tshark field names: {', '.join(invalid_fields)}",
|
||||
"hint": 'Use {"action": "search_fields", "query": "<protocol>"} to look up valid field names',
|
||||
"invalid_fields": invalid_fields,
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"error": f"Invalid tshark field names: {', '.join(invalid_fields)}",
|
||||
"hint": 'Use {"action": "search_fields", "query": "<protocol>"} to look up valid field names',
|
||||
"invalid_fields": invalid_fields,
|
||||
}
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -271,8 +285,7 @@ class PacketAnalysisTool(BaseTool):
|
||||
return self._search_fields(query or "")
|
||||
|
||||
logger.info(
|
||||
f"PacketAnalysisTool invoked: project_id={project_id}, "
|
||||
f"link_id={link_id}, tshark_args={tshark_args}"
|
||||
f"PacketAnalysisTool invoked: project_id={project_id}, link_id={link_id}, tshark_args={tshark_args}"
|
||||
)
|
||||
|
||||
# Validate inputs
|
||||
@@ -429,6 +442,7 @@ class PacketAnalysisTool(BaseTool):
|
||||
"""
|
||||
# Build command: tshark -r <file> <user_args>
|
||||
import shlex
|
||||
|
||||
cmd = ["tshark", "-r", pcap_file] + shlex.split(tshark_args)
|
||||
|
||||
logger.info(f"Running tshark: {' '.join(cmd)}")
|
||||
@@ -451,10 +465,7 @@ class PacketAnalysisTool(BaseTool):
|
||||
f"tshark reported: {stderr_clean}",
|
||||
]
|
||||
if "is not a valid protocol" in stderr.lower() or "aren't valid" in stderr.lower():
|
||||
hints.append(
|
||||
'Use {"action": "search_fields", "query": "<protocol>"} '
|
||||
"to find correct field names."
|
||||
)
|
||||
hints.append('Use {"action": "search_fields", "query": "<protocol>"} to find correct field names.')
|
||||
logger.warning(f"tshark stderr: {stderr}")
|
||||
return json.dumps({"error": "tshark argument error", "hints": hints})
|
||||
|
||||
|
||||
@@ -179,11 +179,7 @@ class VPCSCommands(BaseTool):
|
||||
|
||||
# Validate input
|
||||
device_configs_list, project_id = self._validate_tool_input(tool_input)
|
||||
if (
|
||||
isinstance(device_configs_list, list)
|
||||
and len(device_configs_list) > 0
|
||||
and "error" in device_configs_list[0]
|
||||
):
|
||||
if isinstance(device_configs_list, list) and len(device_configs_list) > 0 and "error" in device_configs_list[0]:
|
||||
return device_configs_list
|
||||
|
||||
# Create a mapping of device names to their commands
|
||||
@@ -191,30 +187,17 @@ class VPCSCommands(BaseTool):
|
||||
|
||||
# Prepare device hosts data
|
||||
try:
|
||||
hosts_data = self._prepare_device_hosts_data(
|
||||
device_configs_list, project_id, jwt_token=jwt_token, url=url
|
||||
)
|
||||
hosts_data = self._prepare_device_hosts_data(device_configs_list, project_id, jwt_token=jwt_token, url=url)
|
||||
except ValueError as e:
|
||||
logger.error("Failed to prepare device hosts data: %s", e)
|
||||
return [{"status": "failed", "error": str(e)}]
|
||||
|
||||
# Check if any devices have errors (e.g., missing device)
|
||||
error_devices = {
|
||||
name: data
|
||||
for name, data in hosts_data.items()
|
||||
if "error" in data
|
||||
}
|
||||
error_devices = {name: data for name, data in hosts_data.items() if "error" in data}
|
||||
if error_devices:
|
||||
logger.error(
|
||||
"Devices with configuration errors: %s",
|
||||
list(error_devices.keys())
|
||||
)
|
||||
logger.error("Devices with configuration errors: %s", list(error_devices.keys()))
|
||||
return [
|
||||
{
|
||||
"device_name": name,
|
||||
"status": "failed",
|
||||
"error": data["error"]
|
||||
}
|
||||
{"device_name": name, "status": "failed", "error": data["error"]}
|
||||
for name, data in error_devices.items()
|
||||
]
|
||||
|
||||
@@ -253,17 +236,13 @@ class VPCSCommands(BaseTool):
|
||||
|
||||
return results
|
||||
|
||||
def _run_vpcs_commands(
|
||||
self, task: Task, device_configs_map: dict[str, list[str]]
|
||||
) -> Result:
|
||||
def _run_vpcs_commands(self, task: Task, device_configs_map: dict[str, list[str]]) -> Result:
|
||||
"""Execute VPCS commands with single retry."""
|
||||
device_name = task.host.name
|
||||
commands = device_configs_map.get(device_name, [])
|
||||
|
||||
if not commands:
|
||||
return Result(
|
||||
host=task.host, result="No commands to execute"
|
||||
)
|
||||
return Result(host=task.host, result="No commands to execute")
|
||||
|
||||
try:
|
||||
# Use netmiko_multiline for VPCS commands
|
||||
@@ -334,9 +313,7 @@ class VPCSCommands(BaseTool):
|
||||
parsed_input = json.loads(tool_input)
|
||||
logger.info("Successfully parsed tool input from JSON string.")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(
|
||||
"Invalid JSON string received as tool input: %s", e
|
||||
)
|
||||
logger.error("Invalid JSON string received as tool input: %s", e)
|
||||
return (
|
||||
[{"status": "failed", "error": f"Invalid JSON string input from model: {e}"}],
|
||||
None,
|
||||
@@ -377,10 +354,7 @@ class VPCSCommands(BaseTool):
|
||||
return device_configs, project_id
|
||||
|
||||
else:
|
||||
error_msg = (
|
||||
"Tool input must be JSON with project_id and device_configs, "
|
||||
f"got {type(parsed_input).__name__}"
|
||||
)
|
||||
error_msg = f"Tool input must be JSON with project_id and device_configs, got {type(parsed_input).__name__}"
|
||||
logger.error(error_msg)
|
||||
return ([{"status": "failed", "error": error_msg}], None)
|
||||
|
||||
@@ -394,14 +368,10 @@ class VPCSCommands(BaseTool):
|
||||
Returns:
|
||||
True if valid UUID format, False otherwise
|
||||
"""
|
||||
uuid_pattern = (
|
||||
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
)
|
||||
uuid_pattern = r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||
return bool(re.match(uuid_pattern, project_id, re.IGNORECASE))
|
||||
|
||||
def _configs_map(
|
||||
self, device_config_list: list[dict[str, Any]]
|
||||
) -> dict[str, list[str]]:
|
||||
def _configs_map(self, device_config_list: list[dict[str, Any]]) -> dict[str, list[str]]:
|
||||
"""
|
||||
Create a mapping of device names to their command lists.
|
||||
|
||||
@@ -411,10 +381,7 @@ class VPCSCommands(BaseTool):
|
||||
Returns:
|
||||
Dictionary mapping device names to command lists
|
||||
"""
|
||||
return {
|
||||
config["device_name"]: config["commands"]
|
||||
for config in device_config_list
|
||||
}
|
||||
return {config["device_name"]: config["commands"] for config in device_config_list}
|
||||
|
||||
def _prepare_device_hosts_data(
|
||||
self,
|
||||
@@ -445,18 +412,14 @@ class VPCSCommands(BaseTool):
|
||||
device_names = [config["device_name"] for config in device_configs_list]
|
||||
|
||||
# Get device port mappings from topology
|
||||
device_ports = get_device_ports_from_topology(
|
||||
device_names, project_id=project_id, jwt_token=jwt_token, url=url
|
||||
)
|
||||
device_ports = get_device_ports_from_topology(device_names, project_id=project_id, jwt_token=jwt_token, url=url)
|
||||
|
||||
# Build Nornir inventory hosts data
|
||||
hosts_data = {}
|
||||
for device_name in device_names:
|
||||
if device_name not in device_ports:
|
||||
logger.error("Device '%s' not found in topology", device_name)
|
||||
hosts_data[device_name] = {
|
||||
"error": f"Device '{device_name}' not found in topology"
|
||||
}
|
||||
hosts_data[device_name] = {"error": f"Device '{device_name}' not found in topology"}
|
||||
continue
|
||||
|
||||
port = device_ports[device_name]["port"]
|
||||
@@ -498,9 +461,7 @@ class VPCSCommands(BaseTool):
|
||||
|
||||
return hosts_data
|
||||
|
||||
def _initialize_nornir(
|
||||
self, hosts_data: dict[str, dict[str, Any]]
|
||||
) -> "Nornir":
|
||||
def _initialize_nornir(self, hosts_data: dict[str, dict[str, Any]]) -> "Nornir":
|
||||
"""
|
||||
Initialize Nornir with VPCS device inventory.
|
||||
|
||||
@@ -576,12 +537,14 @@ class VPCSCommands(BaseTool):
|
||||
|
||||
# Check if device had an error during preparation
|
||||
if device_name in hosts_data and "error" in hosts_data[device_name]:
|
||||
results.append({
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": hosts_data[device_name]["error"],
|
||||
"commands": device_config["commands"],
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": hosts_data[device_name]["error"],
|
||||
"commands": device_config["commands"],
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Get result from Nornir task
|
||||
@@ -591,28 +554,34 @@ class VPCSCommands(BaseTool):
|
||||
if host_result.failed:
|
||||
# Task failed
|
||||
error_msg = str(host_result.result) if host_result.result else "Unknown error"
|
||||
results.append({
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": error_msg,
|
||||
"commands": device_config["commands"],
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": error_msg,
|
||||
"commands": device_config["commands"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Task succeeded
|
||||
results.append({
|
||||
"device_name": device_name,
|
||||
"status": "success",
|
||||
"output": host_result.result,
|
||||
"commands": device_config["commands"],
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"device_name": device_name,
|
||||
"status": "success",
|
||||
"output": host_result.result,
|
||||
"commands": device_config["commands"],
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Device not in task result (shouldn't happen)
|
||||
results.append({
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": f"Device '{device_name}' not in task results",
|
||||
"commands": device_config["commands"],
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"device_name": device_name,
|
||||
"status": "failed",
|
||||
"error": f"Device '{device_name}' not in task results",
|
||||
"commands": device_config["commands"],
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
Reference in new issue
Block a user