refactor: ruff autofixes

This commit is contained in:
Mark Paronyan committed 2026-09-27 15:55:10 +03:00
1 parent 2ac637c662
commit a0450b6bce
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