diff --git a/gns3server/agent/__init__.py b/gns3server/agent/__init__.py index dee2aaed1..082c6dd4a 100644 --- a/gns3server/agent/__init__.py +++ b/gns3server/agent/__init__.py @@ -57,7 +57,7 @@ except ImportError as e: AI_COPILOT_AVAILABLE = False # Provide stub functions that raise helpful errors - async def get_project_agent_manager(): + async def get_project_agent_manager() -> "ProjectAgentManager": """ Get the global ProjectAgentManager singleton instance. @@ -68,7 +68,7 @@ except ImportError as e: "AI Copilot is not available. Install AI dependencies with: pip install gns3-server[ai-features]" ) - class ProjectAgentManager: + class ProjectAgentManager: # type: ignore[no-redef] """ Stub class for ProjectAgentManager when AI dependencies are not installed. """ diff --git a/gns3server/agent/gns3_copilot/agent/context_manager.py b/gns3server/agent/gns3_copilot/agent/context_manager.py index 79c526dbf..5337c3d95 100644 --- a/gns3server/agent/gns3_copilot/agent/context_manager.py +++ b/gns3server/agent/gns3_copilot/agent/context_manager.py @@ -140,7 +140,7 @@ def estimate_tool_tokens(tools: list[Any]) -> int: for tool in tools: try: # Build tool schema in OpenAI format - tool_schema = { + tool_schema: dict[str, Any] = { "type": "function", "function": { "name": tool.name, diff --git a/gns3server/agent/gns3_copilot/agent/model_factory.py b/gns3server/agent/gns3_copilot/agent/model_factory.py index cfdc26854..f790646bd 100644 --- a/gns3server/agent/gns3_copilot/agent/model_factory.py +++ b/gns3server/agent/gns3_copilot/agent/model_factory.py @@ -110,7 +110,7 @@ def create_base_model( try: # Prepare parameters for init_chat_model - init_params = { + init_params: dict[str, Any] = { "model": config_vars["model_name"], "model_provider": config_vars["model_provider"], "api_key": config_vars["api_key"], @@ -177,7 +177,7 @@ def create_title_model( try: # Prepare parameters for init_chat_model - init_params = { + init_params: dict[str, Any] = { "model": config_vars["model_name"], "model_provider": config_vars["model_provider"], "api_key": config_vars["api_key"], diff --git a/gns3server/agent/gns3_copilot/chat_sessions_repository.py b/gns3server/agent/gns3_copilot/chat_sessions_repository.py index 4d18d8852..41006f299 100644 --- a/gns3server/agent/gns3_copilot/chat_sessions_repository.py +++ b/gns3server/agent/gns3_copilot/chat_sessions_repository.py @@ -125,7 +125,7 @@ class ChatSessionsRepository: project_id: str, title: str = "New Conversation", copilot_mode: Optional[str] = None, - ) -> ChatSession: + ) -> Optional[ChatSession]: """ Create a new chat session. @@ -158,6 +158,8 @@ class ChatSessionsRepository: session_id = cursor.lastrowid log.info("Created chat session: id=%s, thread_id=%s, copilot_mode=%s", session_id, thread_id, copilot_mode) + if session_id is None: + return None return await self.get_session_by_id(session_id) async def get_session_by_id(self, session_id: int) -> Optional[ChatSession]: @@ -214,7 +216,7 @@ class ChatSessionsRepository: List of ChatSession """ query = "SELECT * FROM chat_sessions" - params = [] + params: List[Any] = [] conditions = [] if user_id: @@ -268,7 +270,7 @@ class ChatSessionsRepository: Updated ChatSession or None """ updates = [] - params = [] + params: List[Any] = [] now = datetime.utcnow().isoformat() diff --git a/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py b/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py index b0934c201..7f6ca8c95 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py +++ b/gns3server/agent/gns3_copilot/gns3_client/api_handlers.py @@ -162,14 +162,14 @@ def build_gns3_ctx(jwt_token: str | None = None, url: str | None = None) -> dict } -def _filter_node_response(node: dict, fields: list[str] = None) -> dict: +def _filter_node_response(node: dict, fields: list[str] | None = None) -> dict: """Filter node response to only include requested fields.""" if not fields: fields = ["node_id", "name", "node_type", "status", "console"] return {k: node[k] for k in fields if k in node} -def _filter_link_response(link: dict, fields: list[str] = None) -> dict: +def _filter_link_response(link: dict, fields: list[str] | None = None) -> dict: """Filter link response to only include requested fields.""" if not fields: fields = LINK_DEFAULT_FIELDS @@ -278,7 +278,7 @@ def _batch_lifecycle(project_id, node_ids, action, conn, action_label): return list(pool.map(_act, node_ids)) -def start_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def start_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: return {"error": "project_id is required"} @@ -296,7 +296,7 @@ def start_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict return {"message": f"Node {node_id} started", "node_id": node_id} -def stop_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def stop_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: return {"error": "project_id is required"} @@ -314,7 +314,7 @@ def stop_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[ return {"message": f"Node {node_id} stopped", "node_id": node_id} -def suspend_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def suspend_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: return {"error": "project_id is required"} @@ -332,7 +332,7 @@ def suspend_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> di return {"message": f"Node {node_id} suspended", "node_id": node_id} -def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: @@ -401,7 +401,7 @@ def create_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic return _filter_node_response(resp, fields) -def delete_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def delete_node_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: return {"error": "project_id is required"} @@ -706,7 +706,7 @@ def available_filters_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) return conn.http_call("get", url).json() -def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: return {"error": "project_id is required"} @@ -762,7 +762,7 @@ def create_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic return _filter_link_response(resp, fields) -def delete_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def delete_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: return {"error": "project_id is required"} @@ -809,7 +809,7 @@ def update_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dic # ── Link capture / reset handlers ────────────────────────────────────── -def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any]: +def reset_link_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) -> dict[str, Any] | list[dict[str, Any]]: project_id = params.get("project_id") if not project_id: return {"error": "project_id is required"} diff --git a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py index c57dc8fee..32a4e2e45 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py +++ b/gns3server/agent/gns3_copilot/gns3_client/connector_factory.py @@ -347,7 +347,7 @@ def get_gns3_server_host() -> str: host = get_gns3_server_host() print(f"GNS3 server host: {host}") """ - url = _detect_url_for_api() + url = _detect_url_for_api() or DEFAULT_GNS3_URL # Extract host from URL # URL format: protocol://host:port diff --git a/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py b/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py index b1f958767..aeefd304a 100644 --- a/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py +++ b/gns3server/agent/gns3_copilot/gns3_client/project_inventory.py @@ -49,7 +49,7 @@ def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None) """ inventory: dict[str, Any] = {} for n in nodes: - inventory[n.get("name")] = { + inventory[n.get("name") or ""] = { "server": server_host, "name": n.get("name"), "node_id": n.get("node_id"), @@ -68,7 +68,7 @@ def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None) return inventory -def build_links_summary(nodes: list[dict[str, Any]], links: list[dict[str, Any]]) -> list[dict[str, str]]: +def build_links_summary(nodes: list[dict[str, Any]], links: list[dict[str, Any]]) -> list[dict[str, Any]]: """ Build a human/LLM-friendly link list resolving node and port names. @@ -76,7 +76,7 @@ def build_links_summary(nodes: list[dict[str, Any]], links: list[dict[str, Any]] Links whose endpoints cannot be resolved are skipped, mirroring the original Project.links_summary behavior. """ - summary: list[dict[str, str]] = [] + summary: list[dict[str, Any]] = [] for link in links: if not link.get("nodes"): continue diff --git a/gns3server/agent/gns3_copilot/skills/loader.py b/gns3server/agent/gns3_copilot/skills/loader.py index 655d76263..0295e2520 100644 --- a/gns3server/agent/gns3_copilot/skills/loader.py +++ b/gns3server/agent/gns3_copilot/skills/loader.py @@ -248,7 +248,7 @@ class SkillsLoader: continue # Use device_type from YAML content as the key # Fallback to filename stem if device_type not present - skill_key = skill_data.get("device_type") if isinstance(skill_data, dict) else None + skill_key = skill_data.get("device_type") if not skill_key: skill_key = yaml_file.stem logger.warning(f"No device_type in {yaml_file}, using filename '{skill_key}' as key") diff --git a/gns3server/agent/gns3_copilot/skills/manager.py b/gns3server/agent/gns3_copilot/skills/manager.py index 473fb2b09..1a511ec29 100644 --- a/gns3server/agent/gns3_copilot/skills/manager.py +++ b/gns3server/agent/gns3_copilot/skills/manager.py @@ -37,8 +37,10 @@ from typing import Optional, Dict, Any try: import git + + GIT_AVAILABLE = True except ImportError: - git = None + GIT_AVAILABLE = False from gns3server.config import Config from .loader import SkillsLoader @@ -66,7 +68,7 @@ class SkillsManager: - Version tracking """ - def __init__(self, repo_url: str = None, branch: str = "main", auto_update: bool = False): + def __init__(self, repo_url: Optional[str] = None, branch: str = "main", auto_update: bool = False): """ Initialize the skills manager. @@ -90,7 +92,7 @@ class SkillsManager: self._repo: Optional["git.Repo"] = None self._prompt_count = 0 - if git is None: + if not GIT_AVAILABLE: logger.warning("GitPython is not installed. Skills management features will be limited.") def initialize(self) -> bool: @@ -134,7 +136,7 @@ class SkillsManager: - Up to date → nothing - Network error → use existing files, log warning """ - if git is None or self._repo is None: + if not GIT_AVAILABLE or self._repo is None: return # Check for uncommitted changes @@ -173,7 +175,7 @@ class SkillsManager: Returns: True if successful, False otherwise """ - if git is None: + if not GIT_AVAILABLE: logger.error("GitPython is not installed. Cannot clone repository.") return False @@ -347,7 +349,7 @@ class SkillsManager: Returns: Commit hash as string, or empty string if not available """ - if git is None or self._repo is None: + if not GIT_AVAILABLE or self._repo is None: try: self._repo = git.Repo(self.local_path) except Exception: @@ -409,7 +411,7 @@ class SkillsManager: Returns: True if successful, False otherwise """ - if git is None: + if not GIT_AVAILABLE: logger.error("GitPython is not installed. Cannot rollback.") return False @@ -436,7 +438,7 @@ class SkillsManager: Returns: List of commit information dictionaries """ - if git is None: + if not GIT_AVAILABLE: return [] try: diff --git a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py index db215c6bf..ed15f0589 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py @@ -78,7 +78,7 @@ try: # so we use importlib to get the actual module import importlib - sd = importlib.import_module("netmiko.ssh_dispatcher") + sd: Any = importlib.import_module("netmiko.ssh_dispatcher") # Recalculate platforms lists to include custom device types sd.platforms = list(sd.CLASS_MAPPER.keys()) @@ -196,7 +196,7 @@ 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 len(device_configs_list) > 0 and "error" in device_configs_list[0]: return device_configs_list # Filter forbidden commands and store blocked commands info diff --git a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py index 8c4a5281d..a8eb41bc4 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py @@ -78,7 +78,7 @@ try: # so we use importlib to get the actual module import importlib - sd = importlib.import_module("netmiko.ssh_dispatcher") + sd: Any = importlib.import_module("netmiko.ssh_dispatcher") # Recalculate platforms lists to include custom device types sd.platforms = list(sd.CLASS_MAPPER.keys()) @@ -199,7 +199,7 @@ 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 len(device_configs_list) > 0 and "error" in device_configs_list[0]: return device_configs_list # Filter forbidden commands and store blocked commands info diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py index 8f926586b..ff11c6a44 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_link.py @@ -222,6 +222,8 @@ class GNS3LinkTool(BaseTool): }, gns3_ctx, ) + if isinstance(link_resp, list): + raise RuntimeError("Unexpected batch response when creating a single link") if "error" in link_resp: raise RuntimeError(link_resp["error"]) diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py index a511de21f..bdbc0bf42 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_create_node.py @@ -230,6 +230,8 @@ class GNS3CreateNodeTool(BaseTool): }, gns3_ctx, ) + if isinstance(created, list): + raise RuntimeError("Unexpected batch response when creating a single node") if "error" in created: raise RuntimeError(created["error"]) node_info = { diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py index d94a0fc1d..be7af824f 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_packet_filter.py @@ -286,7 +286,7 @@ class GNS3PacketFilterTool(BaseTool): # Check if output contains "Invalid" indicating syntax error if "Invalid" in result.stdout or "Invalid" in result.stderr: - error_lines = [] + error_lines: list[str] = [] if "Invalid" in result.stderr: error_lines.extend(line for line in result.stderr.split("\n") if "Invalid" in line) if "Invalid" in result.stdout: diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py index 8e98c7480..4086cde1b 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_start_node.py @@ -136,7 +136,11 @@ 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_errors = ( + {r["node_id"]: r.get("error") for r in start_results if r.get("status") == "error"} + if isinstance(start_results, list) + else {} + ) # Get immediate status (likely 'starting' or 'stopped') — one call listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py index 3c0d8ef2c..d6965f19b 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_stop_node.py @@ -124,7 +124,11 @@ 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_errors = ( + {r["node_id"]: r.get("error") for r in stop_results if r.get("status") == "error"} + if isinstance(stop_results, list) + else {} + ) # Get updated status — one call listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) diff --git a/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py b/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py index 74f9ba883..47a0cc4b1 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py +++ b/gns3server/agent/gns3_copilot/tools_v2/gns3_suspend_node.py @@ -130,7 +130,11 @@ 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_errors = ( + {r["node_id"]: r.get("error") for r in suspend_results if r.get("status") == "error"} + if isinstance(suspend_results, list) + else {} + ) # Get updated status — one call listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) diff --git a/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py b/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py index b9c35d141..c5f30bf79 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py +++ b/gns3server/agent/gns3_copilot/tools_v2/packet_analysis_tool.py @@ -143,7 +143,8 @@ class PacketAnalysisTool(BaseTool): stderr=subprocess.DEVNULL, text=True, ) - tshark.stdout.close() + if tshark.stdout is not None: + tshark.stdout.close() stdout, _ = grep.communicate(timeout=30) tshark.wait(timeout=5) except Exception as e: diff --git a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py index 3a8789cda..1315730c7 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py +++ b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py @@ -68,7 +68,7 @@ try: # CRITICAL: Update netmiko.ssh_dispatcher platforms lists import importlib - sd = importlib.import_module("netmiko.ssh_dispatcher") + sd: Any = importlib.import_module("netmiko.ssh_dispatcher") # Recalculate platforms lists to include custom device types sd.platforms = list(sd.CLASS_MAPPER.keys()) @@ -179,7 +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 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 @@ -386,7 +386,7 @@ class VPCSCommands(BaseTool): def _prepare_device_hosts_data( self, device_configs_list: list[dict[str, Any]], - project_id: str, + project_id: str | None, jwt_token: str | None = None, url: str | None = None, ) -> dict[str, dict[str, Any]]: @@ -415,7 +415,7 @@ class VPCSCommands(BaseTool): 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 = {} + hosts_data: dict[str, Any] = {} for device_name in device_names: if device_name not in device_ports: logger.error("Device '%s' not found in topology", device_name) diff --git a/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py b/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py index 3e951f10a..d4f7b1a38 100644 --- a/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py +++ b/gns3server/agent/gns3_copilot/utils/custom_netmiko/scripts/list_netmiko_telnet_devices.py @@ -169,7 +169,7 @@ def group_devices_by_brand(device_types: List[str]) -> Dict[str, List[str]]: Returns: Dictionary mapping brand names to device type lists """ - brands = {} + brands: dict[str, list[str]] = {} for device_type in device_types: brand = extract_brand_name(device_type) diff --git a/gns3server/agent/gns3_copilot/utils/message_converters.py b/gns3server/agent/gns3_copilot/utils/message_converters.py index 8bf18f668..367b2d956 100644 --- a/gns3server/agent/gns3_copilot/utils/message_converters.py +++ b/gns3server/agent/gns3_copilot/utils/message_converters.py @@ -37,6 +37,7 @@ from typing import Dict from langchain_core.messages import AIMessage from langchain_core.messages import HumanMessage from langchain_core.messages import SystemMessage +from langchain_core.messages import ToolCall from langchain_core.messages import ToolMessage @@ -86,7 +87,7 @@ def convert_langchain_to_openai(lc_message) -> Dict[str, Any]: tool_calls = [] for tc in lc_message.tool_calls: # Convert to dict if it's an object - tc_dict = tc if isinstance(tc, dict) else tc.model_dump() + tc_dict = tc tool_calls.append( { "id": tc_dict.get("id", str(uuid.uuid4())), @@ -139,7 +140,7 @@ def convert_openai_to_langchain(msg: Dict[str, Any]): # Restore tool calls if present if "tool_calls" in msg and msg["tool_calls"]: - tool_calls = [] + tool_calls: list[ToolCall] = [] for tc in msg["tool_calls"]: tool_calls.append( { diff --git a/gns3server/agent/gns3_copilot/utils/parse_tool_content.py b/gns3server/agent/gns3_copilot/utils/parse_tool_content.py index 4ea17912d..350a6bf50 100644 --- a/gns3server/agent/gns3_copilot/utils/parse_tool_content.py +++ b/gns3server/agent/gns3_copilot/utils/parse_tool_content.py @@ -214,9 +214,7 @@ def parse_tool_content( return content # Handle unsupported types - error_msg = ( # type: ignore[unreachable] - f"Content must be str, dict, list, int, float, bool, or None, got {type(content).__name__}" - ) + error_msg = f"Content must be str, dict, list, int, float, bool, or None, got {type(content).__name__}" logger.error(error_msg) if strict_mode: diff --git a/gns3server/agent/mcp/appliances.py b/gns3server/agent/mcp/appliances.py index 45a9b472a..6e3a3ecf5 100644 --- a/gns3server/agent/mcp/appliances.py +++ b/gns3server/agent/mcp/appliances.py @@ -108,7 +108,7 @@ def install_appliance_handler(params: dict[str, Any], gns3_ctx: dict[str, Any]) if version: request_params["version"] = version response = conn.http_call("post", url, params=request_params) - result = {"message": f"Appliance {appliance_id} installed"} + result: dict[str, Any] = {"message": f"Appliance {appliance_id} installed"} if response.content: # the install endpoint returns the created template (201); tolerate an # empty body in case an older server still replies with 204 diff --git a/gns3server/agent/web_wireshark/docker_client.py b/gns3server/agent/web_wireshark/docker_client.py index d27873fa5..00f62edd2 100644 --- a/gns3server/agent/web_wireshark/docker_client.py +++ b/gns3server/agent/web_wireshark/docker_client.py @@ -23,9 +23,16 @@ This module handles Docker API operations via aiohttp Unix socket. import asyncio import logging +import sys +from typing import Any import aiohttp +if sys.version_info >= (3, 11): + from asyncio import timeout as async_timeout +else: + from async_timeout import timeout as async_timeout + from gns3server.utils import parse_version logger = logging.getLogger(__name__) @@ -132,7 +139,7 @@ class DockerHTTPClient: url = f"http://docker/v{self._api_version}/{endpoint}" try: - async with asyncio.timeout(self.REQUEST_TIMEOUT): + async with async_timeout(self.REQUEST_TIMEOUT): async with session.request(method, url, **kwargs) as response: if response.status >= 300: error_text = await response.text() @@ -157,9 +164,9 @@ class DockerHTTPClient: return await self._request(method, endpoint, check_connection=False, **kwargs) raise - async def create_network(self, name: str, driver: str = "bridge", subnet: str = None): + async def create_network(self, name: str, driver: str = "bridge", subnet: str | None = None): """Create Docker network.""" - data = {"Name": name, "Driver": driver} + data: dict[str, Any] = {"Name": name, "Driver": driver} if subnet: data["IPAM"] = {"Config": [{"Subnet": subnet}]} await self._request("POST", "networks/create", json=data) @@ -175,7 +182,7 @@ class DockerHTTPClient: async def create_container(self, name: str, image: str, **kwargs): """Create container.""" - data = {"Image": image, "name": name, "HostConfig": {}, "NetworkingConfig": {}} + data: dict[str, Any] = {"Image": image, "name": name, "HostConfig": {}, "NetworkingConfig": {}} # Handle network config if "network" in kwargs: @@ -248,7 +255,7 @@ class DockerHTTPClient: url = f"http://docker/v{self._api_version}/containers/{container_name}/top?ps_args=aux" try: - async with asyncio.timeout(self.REQUEST_TIMEOUT): + async with async_timeout(self.REQUEST_TIMEOUT): async with session.get(url) as response: if response.status >= 300: error_text = await response.text() diff --git a/gns3server/agent/web_wireshark/manage_wireshark.py b/gns3server/agent/web_wireshark/manage_wireshark.py index 559eaadd9..5203a98d9 100755 --- a/gns3server/agent/web_wireshark/manage_wireshark.py +++ b/gns3server/agent/web_wireshark/manage_wireshark.py @@ -81,10 +81,7 @@ from gns3server.utils.uuid_validator import validate_uuid if __name__ == "__main__": sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) -try: - from gns3server.agent.web_wireshark.manager import WebWiresharkManager -except ImportError: - from manager import WebWiresharkManager +from gns3server.agent.web_wireshark.manager import WebWiresharkManager logger = logging.getLogger(__name__) diff --git a/gns3server/agent/web_wireshark/manager.py b/gns3server/agent/web_wireshark/manager.py index e17cf65c0..16f057d75 100644 --- a/gns3server/agent/web_wireshark/manager.py +++ b/gns3server/agent/web_wireshark/manager.py @@ -84,7 +84,7 @@ class WebWiresharkManager: logger.warning(f"Container {container_id[:12]} health check failed: {e}") return False - async def _exec_in_container(self, container_id: str, command: str, timeout: int = None) -> tuple: + async def _exec_in_container(self, container_id: str, command: str, timeout: Optional[int] = None) -> tuple: """Execute command in container with timeout. Args: @@ -197,7 +197,7 @@ class WebWiresharkManager: stdout, _ = await proc.communicate() # Build parent->children mapping - children_map = {} + children_map: dict[int, list[int]] = {} for line in stdout.decode().strip().split("\n"): parts = line.split(None, 2) if len(parts) < 3: @@ -326,7 +326,7 @@ class WebWiresharkManager: return # Build parent->children mapping and collect process info - children_map = {} # ppid -> [pid] + children_map: dict[int, list[int]] = {} # ppid -> [pid] process_info = {} # pid -> (ppid, command) for line in stdout.decode().strip().split("\n"): @@ -480,7 +480,7 @@ class WebWiresharkManager: logger.debug(f"Cannot get URL from Config: {e}") return None - async def _get_container_gateway_ip(self, container_id: str = None) -> Optional[str]: + async def _get_container_gateway_ip(self, container_id: Optional[str] = None) -> Optional[str]: """Get the Docker bridge gateway IP for container to access host. Args: @@ -519,7 +519,7 @@ class WebWiresharkManager: return None - async def get_container_ip(self, container_name: str, container_id: str = None) -> Optional[str]: + async def get_container_ip(self, container_name: str, container_id: Optional[str] = None) -> Optional[str]: """Get the container IP address in the wireshark network. Args: @@ -626,7 +626,7 @@ class WebWiresharkManager: project_id: str, image: str = "gns3/web-wireshark:latest", memory: str = "2g", - memory_swap: str = None, + memory_swap: Optional[str] = None, cpus: float = 1.0, pids_limit: int = 1000, ) -> str: @@ -935,10 +935,10 @@ class WebWiresharkManager: project_id: str, link_id: str, jwt_token: str, - capture_stream_url: str = None, + capture_stream_url: Optional[str] = None, image: str = "gns3/web-wireshark:latest", memory: str = "2g", - memory_swap: str = None, + memory_swap: Optional[str] = None, cpus: float = 1.0, pids_limit: int = 1000, ): diff --git a/gns3server/agent/web_wireshark/stats.py b/gns3server/agent/web_wireshark/stats.py index 78dc7b0c0..de9b66091 100644 --- a/gns3server/agent/web_wireshark/stats.py +++ b/gns3server/agent/web_wireshark/stats.py @@ -7,7 +7,7 @@ about Web Wireshark containers and sessions. import logging import subprocess -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) @@ -30,7 +30,7 @@ async def collect_webwireshark_stats(projects: List) -> Dict: """ from .manager import WebWiresharkManager - stats = {"total_containers": 0, "running_containers": 0, "active_sessions": 0, "containers": []} + stats: Dict[str, Any] = {"total_containers": 0, "running_containers": 0, "active_sessions": 0, "containers": []} # Create a single manager instance and reuse it manager = WebWiresharkManager() diff --git a/gns3server/api/routes/compute/atm_switch_nodes.py b/gns3server/api/routes/compute/atm_switch_nodes.py index 65bc1157d..1282a162d 100644 --- a/gns3server/api/routes/compute/atm_switch_nodes.py +++ b/gns3server/api/routes/compute/atm_switch_nodes.py @@ -18,6 +18,7 @@ API routes for ATM switch nodes. """ +from typing import Any import os from fastapi import APIRouter, Depends, Body, Path, status, HTTPException @@ -29,7 +30,9 @@ from gns3server import schemas from gns3server.compute.dynamips import Dynamips from gns3server.compute.dynamips.nodes.atm_switch import ATMSwitch -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or ATM switch node"} +} router = APIRouter(responses=responses) @@ -57,13 +60,13 @@ async def create_atm_switch(project_id: UUID, node_data: schemas.ATMSwitchCreate # Use the Dynamips ATM switch to simulate this node dynamips_manager = Dynamips.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) node = await dynamips_manager.create_node( - node_data.get("name"), + data.get("name"), str(project_id), - node_data.get("node_id"), + data.get("node_id"), node_type="atm_switch", - mappings=node_data.get("mappings"), + mappings=data.get("mappings"), ) return node.asdict() @@ -97,11 +100,11 @@ async def update_atm_switch( Update an ATM switch node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - if "name" in node_data and node.name != node_data["name"]: - await node.set_name(node_data["name"]) - if "mappings" in node_data: - node.mappings = node_data["mappings"] + data = jsonable_encoder(node_data, exclude_unset=True) + if "name" in data and node.name != data["name"]: + await node.set_name(data["name"]) + if "mappings" in data: + node.mappings = data["mappings"] node.updated() return node.asdict() diff --git a/gns3server/api/routes/compute/capabilities.py b/gns3server/api/routes/compute/capabilities.py index f472f6231..42154d4e1 100644 --- a/gns3server/api/routes/compute/capabilities.py +++ b/gns3server/api/routes/compute/capabilities.py @@ -39,7 +39,8 @@ def get_capabilities(request: Request) -> dict: node_types.extend(module.node_types()) # record the controller hostname or IP address - request.app.state.controller_host = request.client.host + if request.client: + request.app.state.controller_host = request.client.host return { "version": __version__, diff --git a/gns3server/api/routes/compute/cloud_nodes.py b/gns3server/api/routes/compute/cloud_nodes.py index 7baf8ca17..d0a4ead9e 100644 --- a/gns3server/api/routes/compute/cloud_nodes.py +++ b/gns3server/api/routes/compute/cloud_nodes.py @@ -23,14 +23,16 @@ import os from fastapi import APIRouter, Depends, Path, status, HTTPException from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse -from typing import Union +from typing import Any, Union from uuid import UUID from gns3server import schemas from gns3server.compute.builtin import Builtin from gns3server.compute.builtin.nodes.cloud import Cloud -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or cloud node"} +} router = APIRouter(responses=responses) @@ -57,21 +59,21 @@ async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate) -> sche """ builtin_manager = Builtin.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) node = await builtin_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), + data.get("node_id"), node_type="cloud", - ports=node_data.get("ports_mapping"), + ports=data.get("ports_mapping"), ) # add the remote console settings - node.remote_console_host = node_data.get("remote_console_host", node.remote_console_host) - node.remote_console_port = node_data.get("remote_console_port", node.remote_console_port) - node.remote_console_type = node_data.get("remote_console_type", node.remote_console_type) - node.remote_console_http_path = node_data.get("remote_console_http_path", node.remote_console_http_path) - node.usage = node_data.get("usage", "") + node.remote_console_host = data.get("remote_console_host", node.remote_console_host) + node.remote_console_port = data.get("remote_console_port", node.remote_console_port) + node.remote_console_type = data.get("remote_console_type", node.remote_console_type) + node.remote_console_http_path = data.get("remote_console_http_path", node.remote_console_http_path) + node.usage = data.get("usage", "") return node.asdict() @@ -90,8 +92,8 @@ async def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep Update a cloud node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - for name, value in node_data.items(): + data = jsonable_encoder(node_data, exclude_unset=True) + for name, value in data.items(): if hasattr(node, name) and getattr(node, name) != value: setattr(node, name, value) node.updated() @@ -178,7 +180,7 @@ async def update_cloud_nio( nio = node.get_nio(port_number) nio.filters.clear() - if nio_data.filters: + if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters: nio.filters = nio_data.filters # NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers. nio.markers = getattr(nio_data, "markers", None) or {} diff --git a/gns3server/api/routes/compute/compute.py b/gns3server/api/routes/compute/compute.py index 1a862787c..c98b1a256 100644 --- a/gns3server/api/routes/compute/compute.py +++ b/gns3server/api/routes/compute/compute.py @@ -163,7 +163,7 @@ def compute_statistics() -> dict: @router.get("/qemu/capabilities") async def get_qemu_capabilities() -> dict: - capabilities = {"kvm": []} + capabilities: dict[str, list] = {"kvm": []} kvms = await Qemu.get_kvm_archs() if kvms: capabilities["kvm"] = kvms diff --git a/gns3server/api/routes/compute/dependencies/authentication.py b/gns3server/api/routes/compute/dependencies/authentication.py index 14ba07f91..0412a4136 100644 --- a/gns3server/api/routes/compute/dependencies/authentication.py +++ b/gns3server/api/routes/compute/dependencies/authentication.py @@ -91,9 +91,9 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock if not separator: raise invalid_user_credentials_exc - username = secrets.compare_digest(username, server_settings.compute_username) - password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value()) - if not (username and password): + username_valid = secrets.compare_digest(username, server_settings.compute_username) + password_valid = secrets.compare_digest(password, server_settings.compute_password.get_secret_value()) + if not (username_valid and password_valid): raise invalid_user_credentials_exc except HTTPException as e: @@ -101,5 +101,6 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock websocket_error = {"action": "log.error", "event": {"message": err_msg}} await websocket.send_json(websocket_error) log.error(err_msg) - return await websocket.close(code=1008) + await websocket.close(code=1008) + return None return websocket diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index 6091cf37b..9dee90dda 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -24,14 +24,16 @@ from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from uuid import UUID -from typing import Union +from typing import Any, Union from gns3server import schemas from gns3server.compute.docker import Docker from gns3server.compute.docker.docker_vm import DockerVM from .dependencies.authentication import compute_authentication, ws_compute_authentication -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"} +} router = APIRouter(responses=responses) @@ -53,13 +55,13 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM: responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}}, dependencies=[Depends(compute_authentication)], ) -async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker: +async def create_docker_node(project_id: UUID, node_create: schemas.DockerCreate) -> schemas.Docker: """ Create a new Docker node. """ docker_manager = Docker.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + node_data = jsonable_encoder(node_create, exclude_unset=True) container = await docker_manager.create_node( node_data.pop("name"), str(project_id), @@ -124,7 +126,7 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker: @router.put("/{node_id}", response_model=schemas.Docker, dependencies=[Depends(compute_authentication)]) -async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker: +async def update_docker_node(node_update: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker: """ Update a Docker node. """ @@ -152,7 +154,7 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D ] changed = False - node_data = jsonable_encoder(node_data, exclude_unset=True) + node_data = jsonable_encoder(node_update, exclude_unset=True) for prop in props: # hasattr: startup_config_content only exists on IOLDockerVM if prop in node_data and hasattr(node, prop) and node_data[prop] != getattr(node, prop): diff --git a/gns3server/api/routes/compute/dynamips_nodes.py b/gns3server/api/routes/compute/dynamips_nodes.py index ce1c1b42b..ccfcd58fb 100644 --- a/gns3server/api/routes/compute/dynamips_nodes.py +++ b/gns3server/api/routes/compute/dynamips_nodes.py @@ -23,7 +23,7 @@ import os from fastapi import APIRouter, WebSocket, Body, Depends, status, HTTPException from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse -from typing import List, Union +from typing import Any, List, Optional, Union from uuid import UUID from gns3server.compute.dynamips import Dynamips @@ -32,7 +32,9 @@ from gns3server import schemas from .dependencies.authentication import compute_authentication, ws_compute_authentication -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"} +} router = APIRouter(responses=responses) @@ -64,25 +66,26 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> dynamips_manager = Dynamips.instance() platform = node_data.platform + chassis: Optional[str] if not node_data.chassis and platform in DEFAULT_CHASSIS: chassis = DEFAULT_CHASSIS[platform] else: chassis = node_data.chassis - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) vm = await dynamips_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), - dynamips_id=node_data.get("dynamips_id"), + data.get("node_id"), + dynamips_id=data.get("dynamips_id"), platform=platform, - console=node_data.get("console"), - console_type=node_data.get("console_type", "telnet"), - aux=node_data.get("aux"), - aux_type=node_data.pop("aux_type", "none"), + console=data.get("console"), + console_type=data.get("console_type", "telnet"), + aux=data.get("aux"), + aux_type=data.pop("aux_type", "none"), chassis=chassis, node_type="dynamips", ) - await dynamips_manager.update_vm_settings(vm, node_data) + await dynamips_manager.update_vm_settings(vm, data) return vm.asdict() diff --git a/gns3server/api/routes/compute/ethernet_hub_nodes.py b/gns3server/api/routes/compute/ethernet_hub_nodes.py index 350105789..150d18e64 100644 --- a/gns3server/api/routes/compute/ethernet_hub_nodes.py +++ b/gns3server/api/routes/compute/ethernet_hub_nodes.py @@ -18,6 +18,7 @@ API routes for Ethernet hub nodes. """ +from typing import Any import os from fastapi import APIRouter, Depends, Body, Path, status, HTTPException @@ -29,7 +30,9 @@ from gns3server.compute.dynamips import Dynamips from gns3server.compute.dynamips.nodes.ethernet_hub import EthernetHub from gns3server import schemas -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet hub node"} +} router = APIRouter(responses=responses) @@ -57,13 +60,13 @@ async def create_ethernet_hub(project_id: UUID, node_data: schemas.EthernetHubCr # Use the Dynamips Ethernet hub to simulate this node dynamips_manager = Dynamips.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) node = await dynamips_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), + data.get("node_id"), node_type="ethernet_hub", - ports=node_data.get("ports_mapping"), + ports=data.get("ports_mapping"), ) return node.asdict() @@ -97,11 +100,11 @@ async def update_ethernet_hub( Update an Ethernet hub. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - if "name" in node_data and node.name != node_data["name"]: - await node.set_name(node_data["name"]) - if "ports_mapping" in node_data: - node.ports_mapping = node_data["ports_mapping"] + data = jsonable_encoder(node_data, exclude_unset=True) + if "name" in data and node.name != data["name"]: + await node.set_name(data["name"]) + if "ports_mapping" in data: + node.ports_mapping = data["ports_mapping"] node.updated() return node.asdict() diff --git a/gns3server/api/routes/compute/ethernet_switch_nodes.py b/gns3server/api/routes/compute/ethernet_switch_nodes.py index bedfac0b6..9c7736e9c 100644 --- a/gns3server/api/routes/compute/ethernet_switch_nodes.py +++ b/gns3server/api/routes/compute/ethernet_switch_nodes.py @@ -22,6 +22,7 @@ through uBridge's ``brctl`` module (see ``gns3server.compute.builtin.nodes.ethernet_switch``). """ +from typing import Any import os from fastapi import APIRouter, Depends, Body, Path, status, HTTPException @@ -33,7 +34,9 @@ from gns3server.compute.builtin import Builtin from gns3server.compute.builtin.nodes.ethernet_switch import EthernetSwitch from gns3server import schemas -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or Ethernet switch node"} +} router = APIRouter(responses=responses) @@ -60,17 +63,17 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw """ builtin_manager = Builtin.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) node = await builtin_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), - console=node_data.get("console"), - console_type=node_data.get("console_type"), + data.get("node_id"), + console=data.get("console"), + console_type=data.get("console_type"), node_type="ethernet_switch", - ports=node_data.get("ports_mapping"), + ports=data.get("ports_mapping"), ) - node.usage = node_data.get("usage", "") + node.usage = data.get("usage", "") return node.asdict() @@ -100,16 +103,16 @@ async def update_ethernet_switch( Update an Ethernet switch. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - if "name" in node_data and node.name != node_data["name"]: - node.name = node_data["name"] - if "usage" in node_data: - node.usage = node_data["usage"] - if "ports_mapping" in node_data: - node.ports_mapping = node_data["ports_mapping"] + data = jsonable_encoder(node_data, exclude_unset=True) + if "name" in data and node.name != data["name"]: + node.name = data["name"] + if "usage" in data: + node.usage = data["usage"] + if "ports_mapping" in data: + node.ports_mapping = data["ports_mapping"] await node.update_port_settings() - if "console_type" in node_data: - node.console_type = node_data["console_type"] + if "console_type" in data: + node.console_type = data["console_type"] node.updated() return node.asdict() diff --git a/gns3server/api/routes/compute/frame_relay_switch_nodes.py b/gns3server/api/routes/compute/frame_relay_switch_nodes.py index f425b3200..bd8bb1f18 100644 --- a/gns3server/api/routes/compute/frame_relay_switch_nodes.py +++ b/gns3server/api/routes/compute/frame_relay_switch_nodes.py @@ -18,6 +18,7 @@ API routes for Frame Relay switch nodes. """ +from typing import Any import os from fastapi import APIRouter, Depends, Body, Path, status, HTTPException @@ -29,7 +30,9 @@ from gns3server import schemas from gns3server.compute.dynamips import Dynamips from gns3server.compute.dynamips.nodes.frame_relay_switch import FrameRelaySwitch -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or Frame Relay switch node"} +} router = APIRouter(responses=responses) @@ -59,13 +62,13 @@ async def create_frame_relay_switch( # Use the Dynamips Frame Relay switch to simulate this node dynamips_manager = Dynamips.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) node = await dynamips_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), + data.get("node_id"), node_type="frame_relay_switch", - mappings=node_data.get("mappings"), + mappings=data.get("mappings"), ) return node.asdict() @@ -99,11 +102,11 @@ async def update_frame_relay_switch( Update an Frame Relay switch node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - if "name" in node_data and node.name != node_data["name"]: - await node.set_name(node_data["name"]) - if "mappings" in node_data: - node.mappings = node_data["mappings"] + data = jsonable_encoder(node_data, exclude_unset=True) + if "name" in data and node.name != data["name"]: + await node.set_name(data["name"]) + if "mappings" in data: + node.mappings = data["mappings"] node.updated() return node.asdict() diff --git a/gns3server/api/routes/compute/iou_nodes.py b/gns3server/api/routes/compute/iou_nodes.py index 0282d0f0f..5285c07b5 100644 --- a/gns3server/api/routes/compute/iou_nodes.py +++ b/gns3server/api/routes/compute/iou_nodes.py @@ -23,7 +23,7 @@ import os from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse -from typing import Union +from typing import Any, Union from uuid import UUID from gns3server import schemas @@ -32,7 +32,9 @@ from gns3server.compute.iou.iou_vm import IOUVM from .dependencies.authentication import compute_authentication, ws_compute_authentication -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"} +} router = APIRouter(responses=responses) @@ -60,18 +62,18 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch """ iou = IOU.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) vm = await iou.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), - application_id=node_data.get("application_id"), - path=node_data.get("path"), - console=node_data.get("console"), - console_type=node_data.get("console_type", "telnet"), + data.get("node_id"), + application_id=data.get("application_id"), + path=data.get("path"), + console=data.get("console"), + console_type=data.get("console_type", "telnet"), ) - for name, value in node_data.items(): + for name, value in data.items(): if hasattr(vm, name) and getattr(vm, name) != value: if name == "application_id": continue # we must ignore this to avoid overwriting the application_id allocated by the controller @@ -79,7 +81,7 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch continue if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0): continue - if node_data.get("use_default_iou_values") and (name == "ram" or name == "nvram"): + if data.get("use_default_iou_values") and (name == "ram" or name == "nvram"): continue setattr(vm, name, value) return vm.asdict() @@ -100,8 +102,8 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de Update an IOU node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - for name, value in node_data.items(): + data = jsonable_encoder(node_data, exclude_unset=True) + for name, value in data.items(): if hasattr(node, name) and getattr(node, name) != value: if name == "application_id": continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager @@ -147,8 +149,8 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep Start an IOU node. """ - start_data = jsonable_encoder(start_data, exclude_unset=True) - for name, value in start_data.items(): + start_values = jsonable_encoder(start_data, exclude_unset=True) + for name, value in start_values.items(): if hasattr(node, name) and getattr(node, name) != value: setattr(node, name, value) @@ -223,7 +225,7 @@ async def update_iou_node_nio( nio = node.get_nio(adapter_number, port_number) nio.filters.clear() - if nio_data.filters: + if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters: nio.filters = nio_data.filters # NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers. nio.markers = getattr(nio_data, "markers", None) or {} diff --git a/gns3server/api/routes/compute/nat_nodes.py b/gns3server/api/routes/compute/nat_nodes.py index 60654b603..37d8b4401 100644 --- a/gns3server/api/routes/compute/nat_nodes.py +++ b/gns3server/api/routes/compute/nat_nodes.py @@ -23,14 +23,16 @@ import os from fastapi import APIRouter, Depends, Path, status, HTTPException from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse -from typing import Union +from typing import Any, Union from uuid import UUID from gns3server import schemas from gns3server.compute.builtin import Builtin from gns3server.compute.builtin.nodes.nat import Nat -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or NAT node"} +} router = APIRouter(responses=responses) @@ -57,16 +59,16 @@ async def create_nat_node(project_id: UUID, node_data: schemas.NATCreate) -> sch """ builtin_manager = Builtin.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) node = await builtin_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), + data.get("node_id"), node_type="nat", - ports=node_data.get("ports_mapping"), + ports=data.get("ports_mapping"), ) - node.usage = node_data.get("usage", "") + node.usage = data.get("usage", "") return node.asdict() @@ -85,8 +87,8 @@ async def update_nat_node(node_data: schemas.NATUpdate, node: Nat = Depends(dep_ Update a NAT node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - for name, value in node_data.items(): + data = jsonable_encoder(node_data, exclude_unset=True) + for name, value in data.items(): if hasattr(node, name) and getattr(node, name) != value: setattr(node, name, value) node.updated() @@ -170,7 +172,7 @@ async def update_nat_node_nio( nio = node.get_nio(port_number) nio.filters.clear() - if nio_data.filters: + if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters: nio.filters = nio_data.filters await node.update_nio(port_number, nio) return nio.asdict() diff --git a/gns3server/api/routes/compute/notifications.py b/gns3server/api/routes/compute/notifications.py index 2c6ca4ad5..ee533037b 100644 --- a/gns3server/api/routes/compute/notifications.py +++ b/gns3server/api/routes/compute/notifications.py @@ -39,14 +39,15 @@ async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(w """ if websocket: - log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket") + client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown" + log.info(f"New client {client} has connected to compute WebSocket") try: with NotificationManager.instance().queue() as queue: while True: notification = await queue.get_json(5) await websocket.send_text(notification) except (ConnectionClosed, WebSocketDisconnect): - log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket") + log.info(f"Client {client} has disconnected from compute WebSocket") except WebSocketException as e: log.warning(f"Error while sending to controller event to WebSocket client: {e}") diff --git a/gns3server/api/routes/compute/projects.py b/gns3server/api/routes/compute/projects.py index 7c1248cc5..729510055 100644 --- a/gns3server/api/routes/compute/projects.py +++ b/gns3server/api/routes/compute/projects.py @@ -44,7 +44,7 @@ from gns3server import schemas router = APIRouter() # How many clients have subscribed to notifications -_notifications_listening = {} +_notifications_listening: dict[str, int] = {} def dep_project(project_id: UUID) -> Project: @@ -74,12 +74,12 @@ def create_compute_project(project_data: schemas.ProjectCreate) -> schemas.Proje """ pm = ProjectManager.instance() - project_data = jsonable_encoder(project_data, exclude_unset=True) + data = jsonable_encoder(project_data, exclude_unset=True) project = pm.create_project( - name=project_data.get("name"), - path=project_data.get("path"), - project_id=project_data.get("project_id"), - variables=project_data.get("variables", None), + name=data.get("name"), + path=data.get("path"), + project_id=data.get("project_id"), + variables=data.get("variables", None), ) return project.asdict() @@ -261,7 +261,7 @@ async def create_batch_nios( # This is what makes builtin L2 nodes (ethernet_switch/hub/cloud/nat) # start their uBridge concurrently during project open instead of one # at a time (~0.5s each for fork + socket connect). - per_node = {} + per_node: dict[str, list[schemas.BatchNIOEntry]] = {} for entry in batch.nios: per_node.setdefault(entry.node_id, []).append(entry) @@ -308,7 +308,7 @@ async def update_batch_nios( # Group entries by node so that different nodes' uBridge processes are # updated in parallel (each node has its own AF_UNIX socket). Within a # node entries are serial to respect the per-node uBridge command lock. - per_node = {} + per_node: dict[str, list[schemas.BatchNIOEntry]] = {} for entry in batch.nios: per_node.setdefault(entry.node_id, []).append(entry) diff --git a/gns3server/api/routes/compute/qemu_nodes.py b/gns3server/api/routes/compute/qemu_nodes.py index 4f8498847..6f1794fb9 100644 --- a/gns3server/api/routes/compute/qemu_nodes.py +++ b/gns3server/api/routes/compute/qemu_nodes.py @@ -23,7 +23,7 @@ import os from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse -from typing import Union +from typing import Any, Union from uuid import UUID from gns3server import schemas @@ -36,7 +36,9 @@ import logging log = logging.getLogger(__name__) -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"} +} router = APIRouter(responses=responses) @@ -64,29 +66,29 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s """ qemu = Qemu.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) - disk_images_to_reset = set(node_data.pop("disk_images_to_reset", [])) + data = jsonable_encoder(node_data, exclude_unset=True) + disk_images_to_reset = set(data.pop("disk_images_to_reset", [])) vm = await qemu.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.pop("node_id", None), - linked_clone=node_data.get("linked_clone", True), - qemu_path=node_data.pop("qemu_path", None), - console=node_data.pop("console", None), - console_type=node_data.pop("console_type", "telnet"), - aux=node_data.get("aux"), - aux_type=node_data.pop("aux_type", "none"), - platform=node_data.pop("platform", None), + data.pop("node_id", None), + linked_clone=data.get("linked_clone", True), + qemu_path=data.pop("qemu_path", None), + console=data.pop("console", None), + console_type=data.pop("console_type", "telnet"), + aux=data.get("aux"), + aux_type=data.pop("aux_type", "none"), + platform=data.pop("platform", None), ) # update the disk image with the backing file if provided # this is needed when duplicating a node that uses backed disk images drives = ["a", "b", "c", "d"] for drive in drives: - disk_image_backing_file = node_data.get(f"hd{drive}_disk_image_backing_file") + disk_image_backing_file = data.get(f"hd{drive}_disk_image_backing_file") if disk_image_backing_file: log.debug(f"Updating disk image for drive {drive} with backing file {disk_image_backing_file}") - node_data[f"hd{drive}_disk_image"] = disk_image_backing_file + data[f"hd{drive}_disk_image"] = disk_image_backing_file # Validate every explicitly replaced disk before removing its stale # overlay. Other unresolved disks may still make this create request fail, @@ -94,7 +96,7 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s for drive in drives: disk_image_property = f"hd{drive}_disk_image" if disk_image_property in disk_images_to_reset: - replacement_image = node_data.get(disk_image_property) + replacement_image = data.get(disk_image_property) if replacement_image: vm.manager.get_abs_image_path(replacement_image, vm.working_dir) local_disk_name = f"hd{drive}_disk.qcow2" @@ -107,11 +109,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s log.info( "Removing stale linked-clone disk '%s' before using replacement image '%s'", local_disk, - node_data.get(disk_image_property), + data.get(disk_image_property), ) vm.delete_disk_image(local_disk_name) - for name, value in node_data.items(): + for name, value in data.items(): if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) @@ -133,10 +135,10 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends Update a Qemu node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) # update the console first to avoid issue if updating console type - node.console = node_data.pop("console", node.console) - for name, value in node_data.items(): + node.console = data.pop("console", node.console) + for name, value in data.items(): if hasattr(node, name) and getattr(node, name) != value: await node.update_property(name, value) node.updated() diff --git a/gns3server/api/routes/compute/virtualbox_nodes.py b/gns3server/api/routes/compute/virtualbox_nodes.py index ec040d3e4..9f137d066 100644 --- a/gns3server/api/routes/compute/virtualbox_nodes.py +++ b/gns3server/api/routes/compute/virtualbox_nodes.py @@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from uuid import UUID -from typing import Union +from typing import Any, Union from gns3server import schemas from gns3server.compute.virtualbox import VirtualBox @@ -33,7 +33,9 @@ from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM from .dependencies.authentication import compute_authentication, ws_compute_authentication -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"} +} router = APIRouter(responses=responses, deprecated=True) @@ -61,24 +63,24 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox """ vbox_manager = VirtualBox.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) vm = await vbox_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), - node_data.pop("vmname"), - linked_clone=node_data.pop("linked_clone", False), - console=node_data.get("console", None), - console_type=node_data.get("console_type", "telnet"), - adapters=node_data.get("adapters", 0), + data.get("node_id"), + data.pop("vmname"), + linked_clone=data.pop("linked_clone", False), + console=data.get("console", None), + console_type=data.get("console_type", "telnet"), + adapters=data.get("adapters", 0), ) - if "ram" in node_data: - ram = node_data.pop("ram") + if "ram" in data: + ram = data.pop("ram") if ram != vm.ram: await vm.set_ram(ram) - for name, value in node_data.items(): + for name, value in data.items(): if name != "node_id": if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) @@ -103,10 +105,10 @@ async def update_virtualbox_node( Update a VirtualBox node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - if "name" in node_data: - name = node_data.pop("name") - vmname = node_data.pop("vmname", None) + data = jsonable_encoder(node_data, exclude_unset=True) + if "name" in data: + name = data.pop("name") + vmname = data.pop("vmname", None) if name != node.name: oldname = node.name node.name = name @@ -118,20 +120,20 @@ async def update_virtualbox_node( node.updated() raise e - if "adapters" in node_data: - adapters = node_data.pop("adapters") + if "adapters" in data: + adapters = data.pop("adapters") if adapters != node.adapters: await node.set_adapters(adapters) - if "ram" in node_data: - ram = node_data.pop("ram") + if "ram" in data: + ram = data.pop("ram") if ram != node.ram: await node.set_ram(ram) # update the console first to avoid issue if updating console type - node.console = node_data.pop("console", node.console) + node.console = data.pop("console", node.console) - for name, value in node_data.items(): + for name, value in data.items(): if hasattr(node, name) and getattr(node, name) != value: setattr(node, name, value) diff --git a/gns3server/api/routes/compute/vmware_nodes.py b/gns3server/api/routes/compute/vmware_nodes.py index 0f339883e..61849da30 100644 --- a/gns3server/api/routes/compute/vmware_nodes.py +++ b/gns3server/api/routes/compute/vmware_nodes.py @@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from uuid import UUID -from typing import Union +from typing import Any, Union from gns3server import schemas from gns3server.compute.vmware import VMware @@ -32,7 +32,9 @@ from gns3server.compute.vmware.vmware_vm import VMwareVM from .dependencies.authentication import compute_authentication, ws_compute_authentication -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"} +} router = APIRouter(responses=responses, deprecated=True) @@ -60,18 +62,18 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) """ vmware_manager = VMware.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) vm = await vmware_manager.create_node( - node_data.pop("name"), + data.pop("name"), str(project_id), - node_data.get("node_id"), - node_data.pop("vmx_path"), - linked_clone=node_data.pop("linked_clone"), - console=node_data.get("console", None), - console_type=node_data.get("console_type", "telnet"), + data.get("node_id"), + data.pop("vmx_path"), + linked_clone=data.pop("linked_clone"), + console=data.get("console", None), + console_type=data.get("console_type", "telnet"), ) - for name, value in node_data.items(): + for name, value in data.items(): if name != "node_id": if hasattr(vm, name) and getattr(vm, name) != value: setattr(vm, name, value) @@ -94,10 +96,10 @@ async def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = D Update a VMware node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) # update the console first to avoid issue if updating console type - node.console = node_data.pop("console", node.console) - for name, value in node_data.items(): + node.console = data.pop("console", node.console) + for name, value in data.items(): if hasattr(node, name) and getattr(node, name) != value: setattr(node, name, value) diff --git a/gns3server/api/routes/compute/vpcs_nodes.py b/gns3server/api/routes/compute/vpcs_nodes.py index 826571dec..a9f96bdc5 100644 --- a/gns3server/api/routes/compute/vpcs_nodes.py +++ b/gns3server/api/routes/compute/vpcs_nodes.py @@ -23,7 +23,7 @@ import os from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse -from typing import Union +from typing import Any, Union from uuid import UUID from gns3server import schemas @@ -31,7 +31,9 @@ from gns3server.compute.vpcs import VPCS from gns3server.compute.vpcs.vpcs_vm import VPCSVM from .dependencies.authentication import compute_authentication, ws_compute_authentication -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"} +} router = APIRouter(responses=responses) @@ -59,14 +61,14 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s """ vpcs = VPCS.instance() - node_data = jsonable_encoder(node_data, exclude_unset=True) + data = jsonable_encoder(node_data, exclude_unset=True) vm = await vpcs.create_node( - node_data["name"], + data["name"], str(project_id), - node_data.get("node_id"), - console=node_data.get("console"), - console_type=node_data.get("console_type", "telnet"), - startup_script=node_data.get("startup_script"), + data.get("node_id"), + console=data.get("console"), + console_type=data.get("console_type", "telnet"), + startup_script=data.get("startup_script"), ) return vm.asdict() @@ -87,14 +89,14 @@ async def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends Update a VPCS node. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - name = node_data.get("name", node.name) + data = jsonable_encoder(node_data, exclude_unset=True) + name = data.get("name", node.name) if node.name != name: node.name = name - console = node_data.get("console", node.console) + console = data.get("console", node.console) if node.console != console: node.console = console - console_type = node_data.get("console_type", node.console_type) + console_type = data.get("console_type", node.console_type) if node.console_type != console_type: node.console_type = console_type node.updated() diff --git a/gns3server/api/routes/controller/acl.py b/gns3server/api/routes/controller/acl.py index beb7bea07..ccc6b7a1d 100644 --- a/gns3server/api/routes/controller/acl.py +++ b/gns3server/api/routes/controller/acl.py @@ -25,13 +25,14 @@ from fastapi import APIRouter, Depends, Request, status from fastapi.routing import APIRoute, _IncludedRouter from starlette.routing import BaseRoute, Mount from uuid import UUID -from typing import Iterator, List, Sequence +from typing import Iterator, List, Optional, Sequence from gns3server import schemas from gns3server.controller.controller_error import ControllerBadRequestError, ControllerNotFoundError from gns3server.controller import Controller +import gns3server.db.models as models from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.images import ImagesRepository @@ -96,7 +97,7 @@ async def endpoints( endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}] def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None: - if endpoint not in endpoints: + if not any(e["endpoint"] == endpoint for e in endpoints): endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type}) # projects @@ -175,7 +176,7 @@ async def endpoints( @router.get("", response_model=List[schemas.ACE], dependencies=[Depends(has_privilege("ACE.Audit"))]) -async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.ACE]: +async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[models.ACE]: """ Get all ACL entries. @@ -193,7 +194,7 @@ async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacReposi ) async def create_ace( request: Request, ace_create: schemas.ACECreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> schemas.ACE: +) -> models.ACE: """ Create a new ACL entry. @@ -224,7 +225,7 @@ async def create_ace( async def get_ace( ace_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), -) -> schemas.ACE: +) -> models.ACE: """ Get an ACL entry. @@ -240,7 +241,7 @@ async def get_ace( @router.put("/{ace_id}", response_model=schemas.ACE, dependencies=[Depends(has_privilege("ACE.Modify"))]) async def update_ace( ace_id: UUID, ace_update: schemas.ACEUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> schemas.ACE: +) -> Optional[models.ACE]: """ Update an ACL entry. diff --git a/gns3server/api/routes/controller/api_keys.py b/gns3server/api/routes/controller/api_keys.py index 235cbe7de..a3bfa4b14 100644 --- a/gns3server/api/routes/controller/api_keys.py +++ b/gns3server/api/routes/controller/api_keys.py @@ -39,7 +39,7 @@ API_KEY_PREFIX = "gns3_" API_KEY_BYTES = 32 -def _generate_api_key(api_key_id: UUID = None) -> tuple[str, str, str, UUID]: +def _generate_api_key(api_key_id: UUID | None = None) -> tuple[str, str, str, UUID]: if api_key_id is None: api_key_id = uuid4() random_bytes = secrets.token_hex(API_KEY_BYTES) diff --git a/gns3server/api/routes/controller/chat.py b/gns3server/api/routes/controller/chat.py index cc796fe26..e55e722f7 100644 --- a/gns3server/api/routes/controller/chat.py +++ b/gns3server/api/routes/controller/chat.py @@ -23,7 +23,7 @@ Nested under projects: /v3/projects/{project_id}/chat/... import json import logging import uuid -from typing import List, Optional +from typing import Any, List, Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import StreamingResponse @@ -40,7 +40,7 @@ from .dependencies.authentication import get_current_active_user log = logging.getLogger(__name__) -responses = {404: {"model": schemas.ErrorMessage, "description": "Resource not found"}} +responses: dict[int | str, dict[str, Any]] = {404: {"model": schemas.ErrorMessage, "description": "Resource not found"}} router = APIRouter(responses=responses) diff --git a/gns3server/api/routes/controller/computes.py b/gns3server/api/routes/controller/computes.py index b1173eba7..c62c859ac 100644 --- a/gns3server/api/routes/controller/computes.py +++ b/gns3server/api/routes/controller/computes.py @@ -23,6 +23,7 @@ from typing import Any, List, Union, Optional from uuid import UUID from gns3server.controller import Controller +import gns3server.db.models as models from gns3server.db.repositories.computes import ComputesRepository from gns3server.db.repositories.rbac import RbacRepository from gns3server.services.computes import ComputesService @@ -31,7 +32,7 @@ from gns3server import schemas from .dependencies.database import get_repository from .dependencies.rbac import has_privilege -responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}} +responses: dict[int | str, dict[str, Any]] = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}} router = APIRouter(responses=responses) @@ -51,14 +52,14 @@ async def create_compute( compute_create: schemas.ComputeCreate, computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), connect: Optional[bool] = False, -) -> schemas.Compute: +) -> models.Compute: """ Create a new compute on the controller. Required privilege: Compute.Allocate """ - return await ComputesService(computes_repo).create_compute(compute_create, connect) + return await ComputesService(computes_repo).create_compute(compute_create, bool(connect)) @router.post( @@ -86,7 +87,7 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None: ) async def get_compute( compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)) -) -> schemas.Compute: +) -> Union[models.Compute, dict]: """ Return a compute from the controller. @@ -104,7 +105,7 @@ async def get_compute( ) async def get_computes( computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), -) -> List[schemas.Compute]: +) -> List[models.Compute]: """ Return all computes known by the controller. @@ -124,7 +125,7 @@ async def update_compute( compute_id: Union[str, UUID], compute_update: schemas.ComputeUpdate, computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), -) -> schemas.Compute: +) -> models.Compute: """ Update a compute on the controller. diff --git a/gns3server/api/routes/controller/controller.py b/gns3server/api/routes/controller/controller.py index 843cad739..3afd00a77 100644 --- a/gns3server/api/routes/controller/controller.py +++ b/gns3server/api/routes/controller/controller.py @@ -20,7 +20,7 @@ import os import time import psutil -from fastapi import APIRouter, Request, Depends, WebSocket, WebSocketDisconnect, status +from fastapi import APIRouter, FastAPI, Request, Depends, WebSocket, WebSocketDisconnect, status from fastapi.responses import StreamingResponse from fastapi.encoders import jsonable_encoder from fastapi.routing import Mount @@ -64,7 +64,7 @@ def get_version(request: Request) -> dict: # compute subapp controller_host = None for route in request.app.routes: - if isinstance(route, Mount) and route.name == "compute": + if isinstance(route, Mount) and route.name == "compute" and isinstance(route.app, FastAPI): controller_host = route.app.state.controller_host local_server = Config.instance().settings.Server.local @@ -129,7 +129,7 @@ async def shutdown() -> None: try: future.result() except Exception as e: - log.error(f"Could not close project: {e}", exc_info=1) + log.error(f"Could not close project: {e}", exc_info=True) continue # then shutdown the server itself @@ -191,8 +191,8 @@ async def statistics() -> dict: # Node statistics - distinguish open vs closed project nodes open_project_nodes = [] closed_project_nodes = [] - node_by_type = {} - node_by_status = {} + node_by_type: Dict[str, int] = {} + node_by_status: Dict[str, int] = {} for project in projects: nodes = project.nodes.values() @@ -264,9 +264,8 @@ async def controller_http_notifications(request: Request) -> StreamingResponse: from gns3server.api.server import app - log.info( - f"New client {request.client.host}:{request.client.port} has connected to controller HTTP notification stream" - ) + client = f"{request.client.host}:{request.client.port}" if request.client else "unknown" + log.info(f"New client {client} has connected to controller HTTP notification stream") async def event_stream(): try: @@ -275,10 +274,7 @@ async def controller_http_notifications(request: Request) -> StreamingResponse: msg = await queue.get_json(5) yield f"{msg}\n".encode("utf-8") finally: - log.info( - f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP " - f"notification stream" - ) + log.info(f"Client {client} has disconnected from controller HTTP notification stream") return StreamingResponse(event_stream(), media_type="application/json") @@ -294,14 +290,15 @@ async def controller_ws_notifications( if current_user is None: return - log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket") + client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown" + log.info(f"New client {client} has connected to controller WebSocket") try: with Controller.instance().notification.controller_queue() as queue: while True: notification = await queue.get_json(5) await websocket.send_text(notification) except (ConnectionClosed, WebSocketDisconnect): - log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket") + log.info(f"Client {client} has disconnected from controller WebSocket") except WebSocketException as e: log.warning(f"Error while sending to controller event to WebSocket client: {e}") diff --git a/gns3server/api/routes/controller/dependencies/authentication.py b/gns3server/api/routes/controller/dependencies/authentication.py index 1043db717..1517e0572 100644 --- a/gns3server/api/routes/controller/dependencies/authentication.py +++ b/gns3server/api/routes/controller/dependencies/authentication.py @@ -55,7 +55,7 @@ async def get_user_from_token( user_repo: UsersRepository = Depends(get_repository(UsersRepository)), api_keys_repo: ApiKeysRepository = Depends(get_repository(ApiKeysRepository)), token: Optional[str] = Query(None, include_in_schema=False), -) -> schemas.User: +) -> models.User: if bearer_token: # bearer token is used first, then any token passed as a URL parameter @@ -169,7 +169,7 @@ async def get_current_active_user_from_websocket( websocket: WebSocket, token: str = Query(...), user_repo: UsersRepository = Depends(get_repository(UsersRepository)), -) -> Optional[schemas.User]: +) -> Optional[models.User]: # Extract requested subprotocols from headers for proper WebSocket negotiation # This is critical for protocols like xpra that require specific subprotocols @@ -238,4 +238,5 @@ async def get_current_active_user_from_websocket( websocket_error = {"action": "log.error", "event": {"message": err_msg}} await websocket.send_json(websocket_error) log.error(err_msg) - return await websocket.close(code=1008) + await websocket.close(code=1008) + return None diff --git a/gns3server/api/routes/controller/dependencies/database.py b/gns3server/api/routes/controller/dependencies/database.py index 2e859106b..86375507d 100644 --- a/gns3server/api/routes/controller/dependencies/database.py +++ b/gns3server/api/routes/controller/dependencies/database.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from typing import Callable, Type +from typing import AsyncGenerator, Callable, Type from fastapi import Depends from starlette.requests import HTTPConnection from sqlalchemy.ext.asyncio import AsyncSession @@ -22,7 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from gns3server.db.repositories.base import BaseRepository -async def get_db_session(request: HTTPConnection) -> AsyncSession: +async def get_db_session(request: HTTPConnection) -> AsyncGenerator[AsyncSession, None]: async with AsyncSession(request.app.state._db_engine, expire_on_commit=False) as session: try: @@ -32,7 +32,7 @@ async def get_db_session(request: HTTPConnection) -> AsyncSession: def get_repository(repo: Type[BaseRepository]) -> Callable: - def get_repo(db_session: AsyncSession = Depends(get_db_session)) -> Type[BaseRepository]: + def get_repo(db_session: AsyncSession = Depends(get_db_session)) -> BaseRepository: return repo(db_session) return get_repo diff --git a/gns3server/api/routes/controller/drawings.py b/gns3server/api/routes/controller/drawings.py index 42e989f0f..6df9ddd91 100644 --- a/gns3server/api/routes/controller/drawings.py +++ b/gns3server/api/routes/controller/drawings.py @@ -20,7 +20,7 @@ API routes for drawings. from fastapi import APIRouter, Depends, status from fastapi.encoders import jsonable_encoder -from typing import List +from typing import Any, List from uuid import UUID from gns3server.controller import Controller @@ -30,7 +30,9 @@ from gns3server import schemas from .dependencies.database import get_repository from .dependencies.rbac import has_privilege -responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"} +} router = APIRouter(responses=responses) diff --git a/gns3server/api/routes/controller/groups.py b/gns3server/api/routes/controller/groups.py index 606835419..1b5de02d2 100644 --- a/gns3server/api/routes/controller/groups.py +++ b/gns3server/api/routes/controller/groups.py @@ -21,7 +21,7 @@ API routes for user groups. from fastapi import APIRouter, Depends, status from uuid import UUID -from typing import List +from typing import List, Optional from gns3server import schemas from gns3server.controller.controller_error import ( @@ -31,6 +31,7 @@ from gns3server.controller.controller_error import ( ControllerForbiddenError, ) +import gns3server.db.models as models from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.rbac import RbacRepository @@ -47,7 +48,7 @@ router = APIRouter() @router.get("", response_model=List[schemas.UserGroup], dependencies=[Depends(has_privilege("Group.Audit"))]) async def get_user_groups( users_repo: UsersRepository = Depends(get_repository(UsersRepository)), -) -> List[schemas.UserGroup]: +) -> List[models.UserGroup]: """ Get all user groups. @@ -65,7 +66,7 @@ async def get_user_groups( ) async def create_user_group( user_group_create: schemas.UserGroupCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) -) -> schemas.UserGroup: +) -> models.UserGroup: """ Create a new user group. @@ -82,7 +83,7 @@ async def create_user_group( async def get_user_group( user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)), -) -> schemas.UserGroup: +) -> models.UserGroup: """ Get a user group. @@ -100,7 +101,7 @@ async def update_user_group( user_group_id: UUID, user_group_update: schemas.UserGroupUpdate, users_repo: UsersRepository = Depends(get_repository(UsersRepository)), -) -> schemas.UserGroup: +) -> Optional[models.UserGroup]: """ Update a user group. @@ -148,7 +149,7 @@ async def delete_user_group( ) async def get_user_group_members( user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) -) -> List[schemas.User]: +) -> List[models.User]: """ Get all user group members. diff --git a/gns3server/api/routes/controller/images.py b/gns3server/api/routes/controller/images.py index dd2f92588..6141e4152 100644 --- a/gns3server/api/routes/controller/images.py +++ b/gns3server/api/routes/controller/images.py @@ -40,6 +40,7 @@ from gns3server.utils.images import ( default_images_directory, get_builtin_disks, ) +import gns3server.db.models as models from gns3server.db.repositories.images import ImagesRepository from gns3server.db.repositories.templates import TemplatesRepository from gns3server.db.repositories.rbac import RbacRepository @@ -109,7 +110,7 @@ async def create_qemu_image( image_path: str, image_data: schemas.QemuDiskImageCreate, images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), -) -> schemas.Image: +) -> models.Image: """ Create a new blank Qemu image. @@ -158,7 +159,7 @@ async def get_images( images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), image_type: Optional[schemas.ImageType] = None, availability: Optional[Literal["unknown", "available", "missing", "unavailable", "invalid"]] = None, -) -> List[schemas.Image]: +) -> List[models.Image]: """ Return all images. @@ -182,7 +183,7 @@ async def upload_image( current_user: schemas.User = Depends(get_current_active_user), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), install_appliances: Optional[bool] = False, -) -> schemas.Image: +) -> models.Image: """ Upload an image. @@ -306,7 +307,7 @@ async def install_images( async def get_image( image_path: str, images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), -) -> schemas.Image: +) -> models.Image: """ Return an image. @@ -349,7 +350,7 @@ async def delete_image( templates = await images_repo.get_image_templates(image.image_id) if templates: - template_names = ", ".join([template.name for template in templates]) + template_names = ", ".join([str(template.name) for template in templates]) raise ControllerError(f"Image '{image_path}' is used by one or more templates: {template_names}") project_names = Controller.instance().find_projects_using_image(image.filename) diff --git a/gns3server/api/routes/controller/links.py b/gns3server/api/routes/controller/links.py index 1ee60a891..2f5eddee3 100644 --- a/gns3server/api/routes/controller/links.py +++ b/gns3server/api/routes/controller/links.py @@ -26,7 +26,7 @@ import aiohttp from fastapi import APIRouter, Depends, Request, status, WebSocket from fastapi.responses import FileResponse, StreamingResponse from fastapi.encoders import jsonable_encoder -from typing import List, Union +from typing import Any, List, Union from uuid import UUID, uuid4 from gns3server.controller import Controller @@ -46,7 +46,9 @@ import logging log = logging.getLogger(__name__) -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or link"} +} router = APIRouter(responses=responses) @@ -91,7 +93,7 @@ async def get_links(project_id: UUID) -> List[schemas.Link]: }, dependencies=[Depends(has_privilege("Link.Allocate"))], ) -async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link: +async def create_link(project_id: UUID, link_create: schemas.LinkCreate) -> schemas.Link: """ Create a new link. @@ -100,7 +102,7 @@ async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schema project = await Controller.instance().get_loaded_project(str(project_id)) link = await project.add_link() - link_data = jsonable_encoder(link_data, exclude_unset=True) + link_data = jsonable_encoder(link_create, exclude_unset=True) if "filters" in link_data: await link.update_filters(link_data["filters"]) if "link_style" in link_data: @@ -156,14 +158,14 @@ async def get_link(link: Link = Depends(dep_link)) -> schemas.Link: response_model_exclude_unset=True, dependencies=[Depends(has_privilege("Link.Modify"))], ) -async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link: +async def update_link(link_update: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link: """ Update a link. Required privilege: Link.Modify """ - link_data = jsonable_encoder(link_data, exclude_unset=True) + link_data = jsonable_encoder(link_update, exclude_unset=True) if "filters" in link_data: await link.update_filters(link_data["filters"]) if "link_style" in link_data: @@ -293,7 +295,7 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea pcap_streaming_url = link.pcap_streaming_url() headers = multidict.MultiDict(request.headers) headers["Host"] = compute.host - headers["Router-Host"] = request.client.host + headers["Router-Host"] = request.client.host if request.client else "" body = await request.body() async def compute_pcap_stream(): @@ -487,7 +489,7 @@ async def update_marker(marker_name: str, marker_data: schemas.MarkerUpdate, lin response_model=Union[schemas.UDPPortInfo, schemas.EthernetPortInfo], dependencies=[Depends(has_privilege("Link.Audit"))], ) -async def get_iface(link: Link = Depends(dep_link)) -> Union[schemas.UDPPortInfo, schemas.EthernetPortInfo]: +async def get_iface(link: Link = Depends(dep_link)) -> dict: """ Return iface info for links to Cloud or NAT devices. diff --git a/gns3server/api/routes/controller/llm_model_configs.py b/gns3server/api/routes/controller/llm_model_configs.py index 620f75c26..c36912a02 100644 --- a/gns3server/api/routes/controller/llm_model_configs.py +++ b/gns3server/api/routes/controller/llm_model_configs.py @@ -206,7 +206,11 @@ async def create_user_llm_model_config( # Extract config fields (excluding table-level fields) config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"}) new_config = await llm_repo.create_user_config( - user_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default + user_id, + config_create.name, + config_create.model_type, + config_fields, + is_default=bool(config_create.is_default), ) return schemas.LLMModelConfigResponse( @@ -346,6 +350,10 @@ async def set_user_default_llm_model_config( # Get the updated config config = await llm_repo.get_user_config(config_id) + if config is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" + ) return schemas.LLMModelConfigResponse( config_id=config.config_id, name=config.name, @@ -506,7 +514,11 @@ async def create_group_llm_model_config( # Extract config fields (excluding table-level fields) config_fields = config_create.model_dump(exclude={"name", "model_type", "is_default"}) new_config = await llm_repo.create_group_config( - group_id, config_create.name, config_create.model_type, config_fields, is_default=config_create.is_default + group_id, + config_create.name, + config_create.model_type, + config_fields, + is_default=bool(config_create.is_default), ) return schemas.LLMModelConfigResponse( @@ -646,6 +658,10 @@ async def set_group_default_llm_model_config( # Get the updated config config = await llm_repo.get_group_config(config_id) + if config is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=f"LLM model configuration '{config_id}' not found" + ) return schemas.LLMModelConfigResponse( config_id=config.config_id, name=config.name, diff --git a/gns3server/api/routes/controller/nodes.py b/gns3server/api/routes/controller/nodes.py index 0938ab087..8cd7c0ade 100644 --- a/gns3server/api/routes/controller/nodes.py +++ b/gns3server/api/routes/controller/nodes.py @@ -27,7 +27,7 @@ from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse from fastapi.routing import APIRoute -from typing import List, Callable, Optional +from typing import Any, List, Callable, Optional from uuid import UUID from gns3server.controller import Controller @@ -47,7 +47,7 @@ import logging log = logging.getLogger(__name__) -node_locks = {} +node_locks: dict[str, dict[str, Any]] = {} class NodeConcurrency(APIRoute): @@ -84,7 +84,9 @@ class NodeConcurrency(APIRoute): return custom_route_handler -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or node"} +} router = APIRouter(route_class=NodeConcurrency, responses=responses) @@ -127,7 +129,7 @@ def _check_node_type(node: Node, *required_types: str) -> None: }, dependencies=[Depends(has_privilege("Node.Allocate"))], ) -async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node: +async def create_node(node_create: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node: """ Create a new node. @@ -135,8 +137,8 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends( """ controller = Controller.instance() - compute = controller.get_compute(str(node_data.compute_id)) - node_data = jsonable_encoder(node_data, exclude_unset=True) + compute = controller.get_compute(str(node_create.compute_id)) + node_data = jsonable_encoder(node_create, exclude_unset=True) node = await project.add_node(compute, node_data.pop("name"), node_data.pop("node_id", None), **node_data) return node.asdict() @@ -274,14 +276,14 @@ async def get_node(node: Node = Depends(dep_node)) -> schemas.Node: response_model_exclude_unset=True, dependencies=[Depends(has_privilege("Node.Modify"))], ) -async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node: +async def update_node(node_update: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node: """ Update a node. Required privilege: Node.Modify """ - node_data = jsonable_encoder(node_data, exclude_unset=True) + node_data = jsonable_encoder(node_update, exclude_unset=True) # Ignore these because we only use them when creating a node node_data.pop("node_id", None) @@ -654,9 +656,8 @@ async def ws_console( return compute = node.compute - log.info( - f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket" - ) + client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown" + log.info(f"New client {client} has connected to controller console WebSocket") compute_host = compute.host try: @@ -690,9 +691,7 @@ async def ws_console( await ws_console_compute.send_bytes(msg["bytes"]) except WebSocketDisconnect: pass - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller console WebSocket" - ) + log.info(f"Client {client} has disconnected from controller console WebSocket") async def ws_send(ws_console_compute): """ @@ -709,10 +708,7 @@ async def ws_console( break except WebSocketDisconnect: # the client disconnected while the compute was still streaming console output - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" - f" console WebSocket" - ) + log.info(f"Client {client} has disconnected from controller console WebSocket") try: # forward WebSocket data in both directions between the client and the compute console WebSocket @@ -763,9 +759,8 @@ async def vnc_console( return compute = node.compute - log.info( - f"New client {websocket.client.host}:{websocket.client.port} has connected to controller VNC console WebSocket" - ) + client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown" + log.info(f"New client {client} has connected to controller VNC console WebSocket") compute_host = compute.host try: @@ -796,10 +791,7 @@ async def vnc_console( await vnc_console_compute.send_bytes(data) except WebSocketDisconnect: pass - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" - f" VNC console WebSocket" - ) + log.info(f"Client {client} has disconnected from controller VNC console WebSocket") async def vnc_send(vnc_console_compute): """ @@ -814,10 +806,7 @@ async def vnc_console( break except WebSocketDisconnect: # the client disconnected while the compute was still streaming VNC console output - log.info( - f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller" - f" VNC console WebSocket" - ) + log.info(f"Client {client} has disconnected from controller VNC console WebSocket") try: # forward WebSocket data in both directions between the client and the compute VNC console WebSocket diff --git a/gns3server/api/routes/controller/pools.py b/gns3server/api/routes/controller/pools.py index a5ef8e8b5..882c05264 100644 --- a/gns3server/api/routes/controller/pools.py +++ b/gns3server/api/routes/controller/pools.py @@ -21,12 +21,13 @@ API routes for resource pools. from fastapi import APIRouter, Depends, status from uuid import UUID -from typing import List +from typing import List, Optional from gns3server import schemas from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError, ControllerNotFoundError from gns3server.controller import Controller +import gns3server.db.models as models from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.pools import ResourcePoolsRepository @@ -43,7 +44,7 @@ router = APIRouter() @router.get("", response_model=List[schemas.ResourcePool], dependencies=[Depends(has_privilege("Pool.Audit"))]) async def get_resource_pools( pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), -) -> List[schemas.ResourcePool]: +) -> List[models.ResourcePool]: """ Get all resource pools. @@ -62,7 +63,7 @@ async def get_resource_pools( async def create_resource_pool( resource_pool_create: schemas.ResourcePoolCreate, pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), -) -> schemas.ResourcePool: +) -> models.ResourcePool: """ Create a new resource pool @@ -80,7 +81,7 @@ async def create_resource_pool( ) async def get_resource_pool( resource_pool_id: UUID, pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)) -) -> schemas.ResourcePool: +) -> models.ResourcePool: """ Get a resource pool. @@ -100,7 +101,7 @@ async def update_resource_pool( resource_pool_id: UUID, resource_pool_update: schemas.ResourcePoolUpdate, pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), -) -> schemas.ResourcePool: +) -> Optional[models.ResourcePool]: """ Update a resource pool. @@ -176,7 +177,7 @@ async def delete_resource_pool( async def get_pool_resources( resource_pool_id: UUID, pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)), -) -> List[schemas.Resource]: +) -> List[models.Resource]: """ Get all resource in a pool. @@ -215,13 +216,13 @@ async def add_resource_to_pool( # we only support projects in resource pools for now project = Controller.instance().get_project(str(resource_id)) - resource = await pools_repo.get_resource(resource_id) - if not resource: + db_resource = await pools_repo.get_resource(resource_id) + if not db_resource: # the resource is not in the database yet, create it resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name) - resource = await pools_repo.create_resource(resource_create) + db_resource = await pools_repo.create_resource(resource_create) - await pools_repo.add_resource_to_pool(resource_pool_id, resource) + await pools_repo.add_resource_to_pool(resource_pool_id, db_resource) @router.delete( diff --git a/gns3server/api/routes/controller/privileges.py b/gns3server/api/routes/controller/privileges.py index a070a6ee6..1f34ec1ff 100644 --- a/gns3server/api/routes/controller/privileges.py +++ b/gns3server/api/routes/controller/privileges.py @@ -16,6 +16,7 @@ API route for privileges """ from typing import List +import gns3server.db.models as models from gns3server.db.repositories.rbac import RbacRepository from .dependencies.database import get_repository from fastapi import APIRouter, Depends @@ -33,7 +34,7 @@ router = APIRouter() ) async def get_privileges( rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), -) -> List[schemas.Privilege]: +) -> List[models.Privilege]: """ Get all privileges. diff --git a/gns3server/api/routes/controller/projects.py b/gns3server/api/routes/controller/projects.py index 19c29eead..378a276b8 100644 --- a/gns3server/api/routes/controller/projects.py +++ b/gns3server/api/routes/controller/projects.py @@ -34,7 +34,7 @@ from fastapi import APIRouter, Depends, Request, Body, HTTPException, status, We from fastapi.encoders import jsonable_encoder from fastapi.responses import StreamingResponse, FileResponse from websockets.exceptions import ConnectionClosed, WebSocketException -from typing import List, Optional +from typing import Any, List, Optional from uuid import UUID from gns3server import schemas @@ -57,7 +57,9 @@ from .dependencies.rbac import has_privilege, has_privilege_on_websocket from .dependencies.authentication import get_current_active_user from .dependencies.database import get_repository -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project"} +} router = APIRouter(responses=responses) @@ -636,8 +638,8 @@ async def export_project( include_images: bool = False, reset_mac_addresses: bool = False, keep_compute_ids: bool = False, - compression: schemas.ProjectCompression = "zstd", - compression_level: int = None, + compression: schemas.ProjectCompression = schemas.ProjectCompression.zstd, + compression_level: Optional[int] = None, ) -> StreamingResponse: """ Export a project as a portable archive. @@ -650,19 +652,19 @@ async def export_project( compression_query = compression.lower() if compression_query == "zip": - compression = zipfile.ZIP_DEFLATED + zip_compression = zipfile.ZIP_DEFLATED if compression_level is not None and (compression_level < 0 or compression_level > 9): raise ControllerBadRequestError("Compression level must be between 0 and 9 for ZIP compression") elif compression_query == "none": - compression = zipfile.ZIP_STORED + zip_compression = zipfile.ZIP_STORED elif compression_query == "bzip2": - compression = zipfile.ZIP_BZIP2 + zip_compression = zipfile.ZIP_BZIP2 if compression_level is not None and (compression_level < 1 or compression_level > 9): raise ControllerBadRequestError("Compression level must be between 1 and 9 for BZIP2 compression") elif compression_query == "lzma": - compression = zipfile.ZIP_LZMA + zip_compression = zipfile.ZIP_LZMA elif compression_query == "zstd": - compression = zipfile.ZIP_ZSTANDARD + zip_compression = zipfile.ZIP_ZSTANDARD if compression_level is not None and (compression_level < 1 or compression_level > 22): raise ControllerBadRequestError("Compression level must be between 1 and 22 for Zstandard compression") @@ -681,7 +683,7 @@ async def export_project( f"Exporting project '{project.name}' with '{compression_query}' compression (level {compression_level})" ) with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir: - with aiozipstream.ZipFile(compression=compression, compresslevel=compression_level) as zstream: + with aiozipstream.ZipFile(compression=zip_compression, compresslevel=compression_level) as zstream: await export_controller_project( zstream, project, diff --git a/gns3server/api/routes/controller/roles.py b/gns3server/api/routes/controller/roles.py index aab6e5a00..3f6e854a1 100644 --- a/gns3server/api/routes/controller/roles.py +++ b/gns3server/api/routes/controller/roles.py @@ -21,7 +21,7 @@ API routes for roles. from fastapi import APIRouter, Depends, status from uuid import UUID -from typing import List +from typing import List, Optional from gns3server import schemas from gns3server.controller.controller_error import ( @@ -31,6 +31,7 @@ from gns3server.controller.controller_error import ( ControllerForbiddenError, ) +import gns3server.db.models as models from gns3server.db.repositories.rbac import RbacRepository from .dependencies.database import get_repository from .dependencies.rbac import has_privilege @@ -43,7 +44,7 @@ router = APIRouter() @router.get("", response_model=List[schemas.Role], dependencies=[Depends(has_privilege("Role.Audit"))]) -async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[schemas.Role]: +async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))) -> List[models.Role]: """ Get all roles. @@ -61,7 +62,7 @@ async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepos ) async def create_role( role_create: schemas.RoleCreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> schemas.Role: +) -> Optional[models.Role]: """ Create a new role. @@ -78,7 +79,7 @@ async def create_role( async def get_role( role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), -) -> schemas.Role: +) -> models.Role: """ Get a role. @@ -94,7 +95,7 @@ async def get_role( @router.put("/{role_id}", response_model=schemas.Role, dependencies=[Depends(has_privilege("Role.Modify"))]) async def update_role( role_id: UUID, role_update: schemas.RoleUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> schemas.Role: +) -> Optional[models.Role]: """ Update a role. @@ -142,7 +143,7 @@ async def delete_role( ) async def get_role_privileges( role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) -) -> List[schemas.Privilege]: +) -> List[models.Privilege]: """ Get all role privileges. diff --git a/gns3server/api/routes/controller/snapshots.py b/gns3server/api/routes/controller/snapshots.py index f85b5d9ef..4ec8c467c 100644 --- a/gns3server/api/routes/controller/snapshots.py +++ b/gns3server/api/routes/controller/snapshots.py @@ -24,7 +24,7 @@ import logging log = logging.getLogger() from fastapi import APIRouter, Depends, status -from typing import List +from typing import Any, List from uuid import UUID from gns3server.controller.project import Project @@ -35,7 +35,9 @@ from gns3server.controller import Controller from .dependencies.database import get_repository from .dependencies.rbac import has_privilege -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"} +} router = APIRouter(responses=responses) diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py index 84d55d693..3c35eea16 100644 --- a/gns3server/api/routes/controller/templates.py +++ b/gns3server/api/routes/controller/templates.py @@ -27,7 +27,7 @@ import logging log = logging.getLogger(__name__) from fastapi import APIRouter, Request, HTTPException, Depends, Response, status, Query -from typing import List, Optional +from typing import Any, List, Optional, Union from uuid import UUID from gns3server import schemas @@ -43,7 +43,9 @@ from .dependencies.authentication import get_current_active_user from .dependencies.rbac import has_privilege from .dependencies.database import get_repository -responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}} +responses: dict[int | str, dict[str, Any]] = { + 404: {"model": schemas.ErrorMessage, "description": "Could not find template"} +} router = APIRouter(responses=responses) @@ -57,7 +59,7 @@ router = APIRouter(responses=responses) async def create_template( template_create: schemas.TemplateCreate, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), -) -> schemas.Template: +) -> dict: """ Create a new template. @@ -80,7 +82,7 @@ async def get_template( request: Request, response: Response, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), -) -> schemas.Template: +) -> Union[dict, Response]: """ Return a template. @@ -108,7 +110,7 @@ async def update_template( template_id: UUID, template_update: schemas.TemplateUpdate, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), -) -> schemas.Template: +) -> dict: """ Update a template. @@ -125,7 +127,7 @@ async def delete_template( template_id: UUID, prune_images: Optional[bool] = False, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), - images_repo: RbacRepository = Depends(get_repository(ImagesRepository)), + images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), ) -> None: """ @@ -155,7 +157,7 @@ async def delete_template( if str(template.template_id) != str(template_id) ] if other_templates: - template_names = ", ".join([template.name for template in other_templates]) + template_names = ", ".join([str(template.name) for template in other_templates]) raise ControllerError(f"Image '{image.path}' is used by one or more templates: {template_names}") if referenced_filenames is None: @@ -193,7 +195,7 @@ async def get_templates( templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), current_user: schemas.User = Depends(get_current_active_user), tags: Optional[List[str]] = Query(None, description="Filter by tags (e.g. tags=vendor:cisco&tags=model:7200)"), -) -> List[schemas.Template]: +) -> List[dict]: """ Return all templates. @@ -244,7 +246,7 @@ async def get_templates( ) async def duplicate_template( template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)) -) -> schemas.Template: +) -> dict: """ Duplicate a template. diff --git a/gns3server/api/routes/controller/users.py b/gns3server/api/routes/controller/users.py index 1f834d712..673886195 100644 --- a/gns3server/api/routes/controller/users.py +++ b/gns3server/api/routes/controller/users.py @@ -22,7 +22,7 @@ API routes for users. from fastapi import APIRouter, Depends, HTTPException, Response, status from fastapi.security import OAuth2PasswordRequestForm from uuid import UUID -from typing import List +from typing import List, Optional from gns3server import schemas from gns3server.controller.controller_error import ( @@ -32,6 +32,7 @@ from gns3server.controller.controller_error import ( ControllerForbiddenError, ) +import gns3server.db.models as models from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.rbac import RbacRepository from gns3server.services import auth_service @@ -168,7 +169,7 @@ async def update_logged_in_user( user_update: schemas.LoggedInUserUpdate, current_user: schemas.User = Depends(get_current_active_user), users_repo: UsersRepository = Depends(get_repository(UsersRepository)), -) -> schemas.User: +) -> Optional[models.User]: """ Update the current active user. """ @@ -180,7 +181,7 @@ async def update_logged_in_user( @router.get("", response_model=List[schemas.User], dependencies=[Depends(has_privilege("User.Audit"))]) -async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[schemas.User]: +async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRepository))) -> List[models.User]: """ Get all users. @@ -198,7 +199,7 @@ async def get_users(users_repo: UsersRepository = Depends(get_repository(UsersRe ) async def create_user( user_create: schemas.UserCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) -) -> schemas.User: +) -> models.User: """ Create a new user. @@ -218,7 +219,7 @@ async def create_user( async def get_user( user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)), -) -> schemas.User: +) -> models.User: """ Get a user. @@ -236,7 +237,7 @@ async def update_user( user_id: UUID, user_update: schemas.UserUpdate, users_repo: UsersRepository = Depends(get_repository(UsersRepository)), -) -> schemas.User: +) -> Optional[models.User]: """ Update a user. @@ -287,7 +288,7 @@ async def delete_user( ) async def get_user_memberships( user_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) -) -> List[schemas.UserGroup]: +) -> List[models.UserGroup]: """ Get user memberships. diff --git a/gns3server/api/server.py b/gns3server/api/server.py index 3eaade6e8..7827aec7c 100644 --- a/gns3server/api/server.py +++ b/gns3server/api/server.py @@ -19,6 +19,8 @@ FastAPI app """ +from typing import cast + from fastapi import FastAPI, Request, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse @@ -118,7 +120,7 @@ def handle_exit(*args, **kwargs): unicorn_exit_handler(*args, **kwargs) -UvicornServer.handle_exit = handle_exit +UvicornServer.handle_exit = handle_exit # type: ignore[method-assign] # Configure self-hosting JavaScript and CSS for docs @@ -134,7 +136,7 @@ async def custom_swagger_ui_html(): ) -@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) +@app.get(cast(str, app.swagger_ui_oauth2_redirect_url), include_in_schema=False) async def swagger_ui_redirect(): return get_swagger_ui_oauth2_redirect_html() diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index 4bc1f77b5..2eedfc2c4 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -1645,7 +1645,7 @@ class Router(BaseNode): await nio.start_packet_capture(output_file, data_link_type) log.debug( 'Router "{name}" [{id}]: starting packet capture on port {slot_number}/{port_number}'.format( - name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number + name=self._name, id=self._id, slot_number=slot_number, port_number=port_number ) ) @@ -1685,7 +1685,7 @@ class Router(BaseNode): log.debug( 'Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format( - name=self._name, id=self._id, nio_name=nio.name, slot_number=slot_number, port_number=port_number + name=self._name, id=self._id, slot_number=slot_number, port_number=port_number ) ) diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index 4e329aad5..b0f99a4c2 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -139,8 +139,8 @@ class IOUVM(BaseNode): # Class-level caches shared across all IOU VM instances using the same image. # These avoid redundant subprocess calls during project loading when multiple # IOU nodes use the same image. - _loader_cache = {} # image path -> loader command list - _default_values_cache = {} # image path -> (ram, nvram) + _loader_cache: dict[str, list[str]] = {} # image path -> loader command list + _default_values_cache: dict[str, tuple[int, int]] = {} # image path -> (ram, nvram) """ IOU VM implementation. @@ -464,7 +464,11 @@ class IOUVM(BaseNode): ) self._nvram = nvram - @BaseNode.name.setter + @property + def name(self): + return self._name + + @name.setter def name(self, new_name): """ Sets the name of this IOU VM. @@ -949,7 +953,11 @@ class IOUVM(BaseNode): return True return False - @BaseNode.console_type.setter + @property + def console_type(self): + return self._console_type + + @console_type.setter def console_type(self, new_console_type): """ Sets the console type for this IOU VM. diff --git a/gns3server/compute/iou/utils/iou_export.py b/gns3server/compute/iou/utils/iou_export.py index f3b67d039..3ee1d2f5c 100644 --- a/gns3server/compute/iou/utils/iou_export.py +++ b/gns3server/compute/iou/utils/iou_export.py @@ -204,9 +204,8 @@ if __name__ == "__main__": args = parser.parse_args() try: - fd = open(args.nvram, "rb") - nvram = fd.read() - fd.close() + with open(args.nvram, "rb") as fd: + nvram = fd.read() except OSError as err: sys.stderr.write(f"Error reading file: {err}\n") sys.exit(1) @@ -218,16 +217,14 @@ if __name__ == "__main__": sys.exit(3) try: - fd = open(args.startup, "wb") - fd.write(startup) - fd.close() + with open(args.startup, "wb") as out: + out.write(startup) if args.private is not None: if private is None: sys.stderr.write("Warning: No private config\n") else: - fd = open(args.private, "wb") - fd.write(private) - fd.close() + with open(args.private, "wb") as out: + out.write(private) except OSError as err: sys.stderr.write(f"Error writing file: {err}\n") sys.exit(1) diff --git a/gns3server/compute/iou/utils/iou_import.py b/gns3server/compute/iou/utils/iou_import.py index 361ed0c54..1dbcbfba4 100644 --- a/gns3server/compute/iou/utils/iou_import.py +++ b/gns3server/compute/iou/utils/iou_import.py @@ -197,22 +197,17 @@ if __name__ == "__main__": parser.add_argument("private", metavar="private-config", nargs="?", help="private configuration") args = parser.parse_args() + nvram: bytes | None = None + private: bytes | None = None try: if args.create is None: - fd = open(args.nvram, "rb") - nvram = fd.read() - fd.close() - else: - nvram = None - fd = open(args.startup, "rb") - startup = fd.read() - fd.close() - if args.private is None: - private = None - else: - fd = open(args.private, "rb") - private = fd.read() - fd.close() + with open(args.nvram, "rb") as fd: + nvram = fd.read() + with open(args.startup, "rb") as fd: + startup = fd.read() + if args.private is not None: + with open(args.private, "rb") as fd: + private = fd.read() except OSError as err: sys.stderr.write(f"Error reading file: {err}\n") sys.exit(1) @@ -224,9 +219,8 @@ if __name__ == "__main__": sys.exit(3) try: - fd = open(args.nvram, "wb") - fd.write(nvram) - fd.close() + with open(args.nvram, "wb") as out: + out.write(nvram) except OSError as err: sys.stderr.write(f"Error writing file: {err}\n") sys.exit(1) diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index 6c9fb6154..8cb9f8016 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -1050,7 +1050,11 @@ class VirtualBoxVM(BaseNode): await self._stop_remote_console() await self._start_console() - @BaseNode.console_type.setter + @property + def console_type(self): + return self._console_type + + @console_type.setter def console_type(self, new_console_type): """ Sets the console type for this VirtualBox VM. diff --git a/gns3server/compute/vpcs/vpcs_vm.py b/gns3server/compute/vpcs/vpcs_vm.py index b42c4095a..c72405543 100644 --- a/gns3server/compute/vpcs/vpcs_vm.py +++ b/gns3server/compute/vpcs/vpcs_vm.py @@ -148,7 +148,11 @@ class VPCSVM(BaseNode): vpcs_path = shutil.which(vpcs_path) return vpcs_path - @BaseNode.name.setter + @property + def name(self): + return self._name + + @name.setter def name(self, new_name): """ Sets the name of this VPCS VM. @@ -358,7 +362,11 @@ class VPCSVM(BaseNode): if self.is_running(): await self.reset_wrap_console() - @BaseNode.console_type.setter + @property + def console_type(self): + return self._console_type + + @console_type.setter def console_type(self, new_console_type): """ Sets the console type for this VPCS VM. diff --git a/gns3server/controller/appliance.py b/gns3server/controller/appliance.py index 10d958983..d62cb3a52 100644 --- a/gns3server/controller/appliance.py +++ b/gns3server/controller/appliance.py @@ -51,6 +51,10 @@ class Appliance: def symbol(self): return self._data.get("symbol") + @symbol.setter + def symbol(self, new_symbol): + self._data["symbol"] = new_symbol + @property def name(self): return self._data.get("name") @@ -63,10 +67,6 @@ class Appliance: def versions(self): return self._data.get("versions") - @symbol.setter - def symbol(self, new_symbol): - self._data["symbol"] = new_symbol - @property def type(self): diff --git a/gns3server/controller/appliance_manager.py b/gns3server/controller/appliance_manager.py index f89422a9f..e99320202 100644 --- a/gns3server/controller/appliance_manager.py +++ b/gns3server/controller/appliance_manager.py @@ -21,7 +21,7 @@ import asyncio import platformdirs -from typing import Tuple, List +from typing import List, Optional, Tuple from aiohttp.client_exceptions import ClientError from uuid import UUID @@ -214,7 +214,7 @@ class ApplianceManager: log.info(f"Template '{template.get('name')}' has been created") return template - async def _appliance_to_template(self, appliance: Appliance, version: str = None) -> dict: + async def _appliance_to_template(self, appliance: Appliance, version: Optional[dict] = None) -> dict: """ Get template data from appliance """ @@ -318,7 +318,7 @@ class ApplianceManager: templates_repo: TemplatesRepository, rbac_repo: RbacRepository, current_user: schemas.User, - ) -> None: + ) -> dict: """ Install a new appliance """ @@ -366,7 +366,7 @@ class ApplianceManager: template_data = await self._appliance_to_template(appliance) return await self._create_template(template_data, templates_repo, rbac_repo, current_user) - def load_appliances(self, symbol_theme: str = None) -> None: + def load_appliances(self, symbol_theme: Optional[str] = None) -> None: """ Loads appliance files from disk. """ @@ -407,7 +407,7 @@ class ApplianceManager: print(f"Cannot load appliance file '{path}': {e}") continue - def _get_default_symbol(self, appliance: dict, symbol_theme: str) -> str: + def _get_default_symbol(self, appliance: dict, symbol_theme: Optional[str]) -> str: """ Returns the default symbol for a given appliance. """ diff --git a/gns3server/controller/link.py b/gns3server/controller/link.py index 72c6552d5..2a7ec02d8 100644 --- a/gns3server/controller/link.py +++ b/gns3server/controller/link.py @@ -365,7 +365,21 @@ class Link: raise NotImplementedError - async def start_marker(self, name, bpf, tag=None, direction=None, capture_node_id=None, enabled=True): + async def start_marker( + self, + name, + bpf, + tag=None, + direction=None, + data_link_type="DLT_EN10MB", + capture_node_id=None, + color=None, + highlight_duration=None, + enabled=True, + inherited_from=None, + dump=True, + memory_only=False, + ): """ Attach a traffic-insight marker to this link (base — UDPLink overrides). """ @@ -377,7 +391,19 @@ class Link: """ raise NotImplementedError - async def update_marker(self, name, bpf=None, tag=None, enabled=None, direction=_UNSET): + async def update_marker( + self, + name, + bpf=None, + tag=None, + enabled=None, + direction=_UNSET, + color=None, + highlight_duration=None, + inherited=False, + dump=True, + memory_only=False, + ): """ Update an existing marker's BPF, tag, or enabled flag. diff --git a/gns3server/controller/project.py b/gns3server/controller/project.py index 00906bc42..ad42e9323 100644 --- a/gns3server/controller/project.py +++ b/gns3server/controller/project.py @@ -479,10 +479,6 @@ class Project: def path(self): return self._path - @property - def status(self): - return self._status - @path.setter def path(self, path): check_path_allowed(path) @@ -510,6 +506,10 @@ class Project: self._path = path + @property + def status(self): + return self._status + @property def captures_directory(self): """ diff --git a/gns3server/controller/topology.py b/gns3server/controller/topology.py index 5048be4ce..f2390930a 100644 --- a/gns3server/controller/topology.py +++ b/gns3server/controller/topology.py @@ -53,7 +53,7 @@ GNS3_FILE_FORMAT_REVISION = 10 class DynamipsNodeValidation(DynamipsCreate): - name: Optional[str] = None + name: Optional[str] = None # type: ignore[assignment] def _check_topology_schema(topo, path): diff --git a/gns3server/db/models/api_keys.py b/gns3server/db/models/api_keys.py index 5cc40eb0b..ec320f9dd 100644 --- a/gns3server/db/models/api_keys.py +++ b/gns3server/db/models/api_keys.py @@ -14,7 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import uuid + from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, func +from sqlalchemy.orm import Mapped, mapped_column from .base import BaseTable, GUID @@ -22,8 +25,10 @@ from .base import BaseTable, GUID class ApiKey(BaseTable): __tablename__ = "api_keys" - api_key_id = Column(GUID, primary_key=True) - user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True) + api_key_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True) + user_id: Mapped[uuid.UUID] = mapped_column( + GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=False, index=True + ) name = Column(String(128), nullable=False) key_hash = Column(String(128), nullable=False) key_prefix = Column(String(8), nullable=False) diff --git a/gns3server/db/models/base.py b/gns3server/db/models/base.py index 1802c1771..4984b8773 100644 --- a/gns3server/db/models/base.py +++ b/gns3server/db/models/base.py @@ -16,12 +16,14 @@ # along with this program. If not, see . import uuid +from datetime import datetime +from typing import Optional from fastapi.encoders import jsonable_encoder from sqlalchemy import Column, DateTime, func, inspect from sqlalchemy.types import TypeDecorator, CHAR, VARCHAR from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): @@ -105,7 +107,9 @@ class BaseTable(Base): __abstract__ = True created_at = Column(DateTime, server_default=func.current_timestamp()) - updated_at = Column(DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp()) + updated_at: Mapped[Optional[datetime]] = mapped_column( + DateTime, server_default=func.current_timestamp(), onupdate=func.current_timestamp() + ) __mapper_args__ = {"eager_defaults": True} diff --git a/gns3server/db/models/images.py b/gns3server/db/models/images.py index d2351794a..d5b323564 100644 --- a/gns3server/db/models/images.py +++ b/gns3server/db/models/images.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from sqlalchemy import Table, Column, String, ForeignKey, BigInteger, Integer, DateTime, JSON, Boolean -from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base, BaseTable, GUID @@ -32,9 +32,9 @@ image_template_map = Table( class Image(BaseTable): __tablename__ = "images" - image_id = Column(Integer, primary_key=True, autoincrement=True) + image_id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) filename = Column(String, index=True) - path = Column(String, unique=True) + path: Mapped[str] = mapped_column(String, unique=True, nullable=True) image_type = Column(String) image_size = Column(BigInteger) checksum = Column(String, index=True) diff --git a/gns3server/db/models/llm_model_configs.py b/gns3server/db/models/llm_model_configs.py index 778d16bbc..4a95719f4 100644 --- a/gns3server/db/models/llm_model_configs.py +++ b/gns3server/db/models/llm_model_configs.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from sqlalchemy import Column, Boolean, ForeignKey, CheckConstraint, Index, Integer, String, text, JSON -from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import BaseTable, generate_uuid, GUID @@ -36,11 +36,13 @@ class LLMModelConfig(BaseTable): config_id = Column(GUID, primary_key=True, default=generate_uuid) name = Column(String(100), nullable=False) # Configuration name (table-level for indexing) model_type = Column(String(50), nullable=False) # Model type: text, vision, stt, tts, multimodal, etc. - config = Column(JSON, nullable=False) # Config fields: provider, base_url, model, temperature, api_key, etc. + config: Mapped[dict] = mapped_column( + JSON, nullable=False + ) # Config fields: provider, base_url, model, temperature, api_key, etc. user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=True) group_id = Column(GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"), nullable=True) is_default = Column(Boolean, default=False, nullable=False) - version = Column(Integer, default=0, nullable=False) # Optimistic locking version + version: Mapped[int] = mapped_column(Integer, default=0, nullable=False) # Optimistic locking version # Relationships user = relationship("User", backref="llm_model_configs") diff --git a/gns3server/db/models/pools.py b/gns3server/db/models/pools.py index d8c011cbc..cd6616429 100644 --- a/gns3server/db/models/pools.py +++ b/gns3server/db/models/pools.py @@ -15,8 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import uuid + from sqlalchemy import Table, Column, String, ForeignKey -from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base, BaseTable, generate_uuid, GUID @@ -36,7 +38,7 @@ resource_pool_map = Table( class Resource(BaseTable): __tablename__ = "resources" - resource_id = Column(GUID, primary_key=True) + resource_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True) name = Column(String, unique=True, index=True) resource_type = Column(String) resource_pools = relationship("ResourcePool", secondary=resource_pool_map, back_populates="resources") @@ -45,6 +47,6 @@ class Resource(BaseTable): class ResourcePool(BaseTable): __tablename__ = "resource_pools" - resource_pool_id = Column(GUID, primary_key=True, default=generate_uuid) + resource_pool_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid) name = Column(String, unique=True, index=True) resources = relationship("Resource", secondary=resource_pool_map, back_populates="resource_pools") diff --git a/gns3server/db/models/roles.py b/gns3server/db/models/roles.py index 73c29dacb..b56b6ef95 100644 --- a/gns3server/db/models/roles.py +++ b/gns3server/db/models/roles.py @@ -15,8 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import uuid + from sqlalchemy import Column, String, Boolean, event -from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import BaseTable, generate_uuid, GUID from .privileges import privilege_role_map @@ -29,7 +31,7 @@ log = logging.getLogger(__name__) class Role(BaseTable): __tablename__ = "roles" - role_id = Column(GUID, primary_key=True, default=generate_uuid) + role_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid) name = Column(String, unique=True, index=True) description = Column(String) is_builtin = Column(Boolean, default=False) diff --git a/gns3server/db/models/templates.py b/gns3server/db/models/templates.py index 62ea74a9e..0450be881 100644 --- a/gns3server/db/models/templates.py +++ b/gns3server/db/models/templates.py @@ -16,19 +16,26 @@ # along with this program. If not, see . +import uuid +from typing import Optional + from sqlalchemy import Boolean, Column, String, Integer, Float, ForeignKey, JSON -from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import BaseTable, generate_uuid, GUID from .images import image_template_map +def template_id_column() -> Mapped[uuid.UUID]: + return mapped_column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + + class Template(BaseTable): __tablename__ = "templates" - template_id = Column(GUID, primary_key=True, default=generate_uuid) + template_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid) name = Column(String, index=True) - version = Column(String) + version: Mapped[Optional[str]] = mapped_column(String) category = Column(String) default_name_format = Column(String) symbol = Column(String) @@ -36,7 +43,7 @@ class Template(BaseTable): usage = Column(String) netmiko_device_type = Column(String) appliance_metadata = Column(JSON) - template_type = Column(String) + template_type: Mapped[str] = mapped_column(String, nullable=True) tags = Column(JSON) compute_id = Column(String) images = relationship("Image", secondary=image_template_map, back_populates="templates") @@ -50,7 +57,7 @@ class Template(BaseTable): class CloudTemplate(Template): __tablename__ = "cloud_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() ports_mapping = Column(JSON) remote_console_host = Column(String) remote_console_port = Column(Integer) @@ -63,7 +70,7 @@ class CloudTemplate(Template): class DockerTemplate(Template): __tablename__ = "docker_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() image = Column(String) adapters = Column(Integer) mac_address = Column(String) @@ -88,10 +95,10 @@ class DockerTemplate(Template): class DynamipsTemplate(Template): __tablename__ = "dynamips_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) - platform = Column(String) + template_id: Mapped[uuid.UUID] = template_id_column() + platform: Mapped[str] = mapped_column(String, nullable=True) chassis = Column(String) - image = Column(String) + image: Mapped[str] = mapped_column(String, nullable=True) exec_area = Column(Integer) mmap = Column(Boolean) mac_addr = Column(String) @@ -130,7 +137,7 @@ class DynamipsTemplate(Template): class EthernetHubTemplate(Template): __tablename__ = "ethernet_hub_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() ports_mapping = Column(JSON) __mapper_args__ = {"polymorphic_identity": "ethernet_hub", "polymorphic_load": "selectin"} @@ -139,7 +146,7 @@ class EthernetHubTemplate(Template): class EthernetSwitchTemplate(Template): __tablename__ = "ethernet_switch_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() ports_mapping = Column(JSON) console_type = Column(String) @@ -149,8 +156,8 @@ class EthernetSwitchTemplate(Template): class IOUTemplate(Template): __tablename__ = "iou_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) - path = Column(String) + template_id: Mapped[uuid.UUID] = template_id_column() + path: Mapped[str] = mapped_column(String, nullable=True) ethernet_adapters = Column(Integer) serial_adapters = Column(Integer) ram = Column(Integer) @@ -168,7 +175,7 @@ class IOUTemplate(Template): class QemuTemplate(Template): __tablename__ = "qemu_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() qemu_path = Column(String) platform = Column(String) linked_clone = Column(Boolean) @@ -214,7 +221,7 @@ class QemuTemplate(Template): class VirtualBoxTemplate(Template): __tablename__ = "virtualbox_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() vmname = Column(String) ram = Column(Integer) linked_clone = Column(Boolean) @@ -236,7 +243,7 @@ class VirtualBoxTemplate(Template): class VMwareTemplate(Template): __tablename__ = "vmware_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() vmx_path = Column(String) linked_clone = Column(Boolean) first_port_name = Column(String) @@ -257,7 +264,7 @@ class VMwareTemplate(Template): class VPCSTemplate(Template): __tablename__ = "vpcs_templates" - template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True) + template_id: Mapped[uuid.UUID] = template_id_column() base_script_file = Column(String) console_type = Column(String) console_auto_start = Column(Boolean, default=False) diff --git a/gns3server/db/models/users.py b/gns3server/db/models/users.py index d9fc9aa29..a66d20025 100644 --- a/gns3server/db/models/users.py +++ b/gns3server/db/models/users.py @@ -15,8 +15,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from datetime import datetime +from typing import Optional + from sqlalchemy import Table, Boolean, Column, Integer, String, DateTime, ForeignKey, event -from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Base, BaseTable, generate_uuid, GUID @@ -24,6 +27,7 @@ from gns3server.config import Config from gns3server.services import auth_service import logging +import uuid log = logging.getLogger(__name__) @@ -43,8 +47,8 @@ class User(BaseTable): email = Column(String, unique=True, index=True) full_name = Column(String) hashed_password = Column(String) - last_login = Column(DateTime) - token_version = Column(Integer, default=0, nullable=False, server_default="0") + last_login: Mapped[Optional[datetime]] = mapped_column(DateTime) + token_version: Mapped[int] = mapped_column(Integer, default=0, nullable=False, server_default="0") is_active = Column(Boolean, default=True) is_superadmin = Column(Boolean, default=False) groups = relationship("UserGroup", secondary=user_group_map, back_populates="users") @@ -72,7 +76,7 @@ def create_default_super_admin(target, connection, **kw): class UserGroup(BaseTable): __tablename__ = "user_groups" - user_group_id = Column(GUID, primary_key=True, default=generate_uuid) + user_group_id: Mapped[uuid.UUID] = mapped_column(GUID, primary_key=True, default=generate_uuid) name = Column(String, unique=True, index=True) is_builtin = Column(Boolean, default=False) users = relationship("User", secondary=user_group_map, back_populates="groups") diff --git a/gns3server/db/repositories/api_keys.py b/gns3server/db/repositories/api_keys.py index d43d44dd1..1cbfdf25b 100644 --- a/gns3server/db/repositories/api_keys.py +++ b/gns3server/db/repositories/api_keys.py @@ -15,9 +15,10 @@ # along with this program. If not, see . from uuid import UUID -from typing import Optional, List +from typing import Optional, List, cast from datetime import datetime, timezone from sqlalchemy import select, update, delete, func +from sqlalchemy.engine import CursorResult from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -67,13 +68,13 @@ class ApiKeysRepository(BaseRepository): query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=True) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def restore_api_key(self, api_key_id: UUID) -> bool: query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(revoked=False) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def update_last_used(self, api_key_id: UUID) -> None: query = update(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id).values(last_used_at=func.now()) @@ -84,4 +85,4 @@ class ApiKeysRepository(BaseRepository): query = delete(models.ApiKey).where(models.ApiKey.api_key_id == api_key_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 diff --git a/gns3server/db/repositories/computes.py b/gns3server/db/repositories/computes.py index 4f3aac7f4..df9e5932d 100644 --- a/gns3server/db/repositories/computes.py +++ b/gns3server/db/repositories/computes.py @@ -16,9 +16,11 @@ # along with this program. If not, see . from uuid import UUID -from typing import Optional, List +from typing import Optional, List, Union from sqlalchemy import select, update, delete from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.engine import CursorResult +from typing import cast from .base import BaseRepository @@ -31,7 +33,7 @@ class ComputesRepository(BaseRepository): super().__init__(db_session) - async def get_compute(self, compute_id: UUID) -> Optional[models.Compute]: + async def get_compute(self, compute_id: Union[str, UUID]) -> Optional[models.Compute]: query = select(models.Compute).where(models.Compute.compute_id == compute_id) result = await self._db_session.execute(query) @@ -47,7 +49,7 @@ class ComputesRepository(BaseRepository): query = select(models.Compute) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def create_compute(self, compute_create: schemas.ComputeCreate) -> models.Compute: @@ -58,14 +60,16 @@ class ComputesRepository(BaseRepository): host=compute_create.host, port=compute_create.port, user=compute_create.user, - password=compute_create.password.get_secret_value(), + password=compute_create.password.get_secret_value() if compute_create.password else None, ) self._db_session.add(db_compute) await self._db_session.commit() await self._db_session.refresh(db_compute) return db_compute - async def update_compute(self, compute_id: UUID, compute_update: schemas.ComputeUpdate) -> Optional[models.Compute]: + async def update_compute( + self, compute_id: Union[str, UUID], compute_update: schemas.ComputeUpdate + ) -> Optional[models.Compute]: update_values = compute_update.model_dump(exclude_unset=True) if compute_update.password is not None: @@ -80,9 +84,9 @@ class ComputesRepository(BaseRepository): await self._db_session.refresh(compute_db) # force refresh of updated_at value return compute_db - async def delete_compute(self, compute_id: UUID) -> bool: + async def delete_compute(self, compute_id: Union[str, UUID]) -> bool: query = delete(models.Compute).where(models.Compute.compute_id == compute_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 diff --git a/gns3server/db/repositories/images.py b/gns3server/db/repositories/images.py index 6e9dcf7dd..e2d0f2401 100644 --- a/gns3server/db/repositories/images.py +++ b/gns3server/db/repositories/images.py @@ -19,8 +19,9 @@ import os import asyncio from datetime import datetime, timezone -from typing import Optional, List, Callable +from typing import Optional, List, Callable, cast from sqlalchemy import select, delete, update +from sqlalchemy.engine import CursorResult from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import IntegrityError from gns3server.utils.image_inventory import fingerprint, image_lock, normalized_path @@ -56,7 +57,7 @@ class ImagesRepository(BaseRepository): result = await self._db_session.execute(query.execution_options(populate_existing=refresh)) return result.scalars().one_or_none() - async def get_image_by_checksum(self, checksum: str, image_dir: str = None) -> Optional[models.Image]: + async def get_image_by_checksum(self, checksum: str, image_dir: Optional[str] = None) -> Optional[models.Image]: """ Get an image by its checksum. """ @@ -95,9 +96,9 @@ class ImagesRepository(BaseRepository): if availability: query = query.where(models.Image.availability == availability) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) - async def get_image_templates(self, image_id: int) -> Optional[List[models.Template]]: + async def get_image_templates(self, image_id: int) -> List[models.Template]: """ Get all templates that an image belongs to. """ @@ -105,7 +106,7 @@ class ImagesRepository(BaseRepository): query = select(models.Template).join(models.Template.images).filter(models.Image.image_id == image_id) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def add_image( self, image_name, image_type, image_size, path, checksum, checksum_algorithm, file_fingerprint=None @@ -135,7 +136,7 @@ class ImagesRepository(BaseRepository): await self._db_session.refresh(db_image) return db_image - async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> models.Image: + async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> Optional[models.Image]: """ Update an image. """ @@ -243,9 +244,9 @@ class ImagesRepository(BaseRepository): query = delete(models.Image).where(models.Image.filename == image_name) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 - async def prune_images(self, skip_images: list[str] = None, is_in_use: Optional[Callable] = None) -> int: + async def prune_images(self, skip_images: Optional[list[str]] = None, is_in_use: Optional[Callable] = None) -> int: """ Prune images not attached to any template. """ diff --git a/gns3server/db/repositories/llm_model_configs.py b/gns3server/db/repositories/llm_model_configs.py index f65ca8cac..2fb9188f4 100644 --- a/gns3server/db/repositories/llm_model_configs.py +++ b/gns3server/db/repositories/llm_model_configs.py @@ -16,8 +16,9 @@ # along with this program. If not, see . from uuid import UUID -from typing import Optional, List, Dict, Any +from typing import Optional, List, Dict, Any, cast from sqlalchemy import select, update, delete, and_ +from sqlalchemy.engine import CursorResult from datetime import datetime import logging @@ -63,7 +64,7 @@ class LLMModelConfigsRepository(BaseRepository): .order_by(models.LLMModelConfig.created_at) ) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def get_user_default_config(self, user_id: UUID) -> Optional[models.LLMModelConfig]: """Get a user's default LLM model configuration.""" @@ -182,7 +183,7 @@ class LLMModelConfigsRepository(BaseRepository): ) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def set_user_default_config(self, user_id: UUID, config_id: UUID) -> bool: """Set a user's default LLM model configuration.""" @@ -202,7 +203,7 @@ class LLMModelConfigsRepository(BaseRepository): ) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 # Group configuration methods @@ -222,7 +223,7 @@ class LLMModelConfigsRepository(BaseRepository): .order_by(models.LLMModelConfig.created_at) ) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def get_group_default_config(self, group_id: UUID) -> Optional[models.LLMModelConfig]: """Get a group's default LLM model configuration.""" @@ -341,7 +342,7 @@ class LLMModelConfigsRepository(BaseRepository): ) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def set_group_default_config(self, group_id: UUID, config_id: UUID) -> bool: """Set a group's default LLM model configuration.""" @@ -361,7 +362,7 @@ class LLMModelConfigsRepository(BaseRepository): ) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 # Inheritance methods @@ -441,21 +442,21 @@ class LLMModelConfigsRepository(BaseRepository): ) # Select default_config with proper priority: - # 1. User's config marked with is_default: true - # 2. Group's config marked with is_default: true - # 3. First config in the list (user configs come first) - for config in configs_with_source: - if config["is_default"] and config["source"] == "user": - default_config = config + # 1. User's entry marked with is_default: true + # 2. Group's entry marked with is_default: true + # 3. First entry in the list (user configs come first) + for entry in configs_with_source: + if entry["is_default"] and entry["source"] == "user": + default_config = entry break if default_config is None: - for config in configs_with_source: - if config["is_default"] and config["source"] == "group": - default_config = config + for entry in configs_with_source: + if entry["is_default"] and entry["source"] == "group": + default_config = entry break - # Fallback to first config if no default is marked + # Fallback to first entry if no default is marked if default_config is None and configs_with_source: default_config = configs_with_source[0] diff --git a/gns3server/db/repositories/pools.py b/gns3server/db/repositories/pools.py index 90615ee68..a9144166e 100644 --- a/gns3server/db/repositories/pools.py +++ b/gns3server/db/repositories/pools.py @@ -16,8 +16,9 @@ # along with this program. If not, see . from uuid import UUID -from typing import Optional, List, Union +from typing import Optional, List, Union, cast from sqlalchemy import select, update, delete +from sqlalchemy.engine import CursorResult from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -52,7 +53,7 @@ class ResourcePoolsRepository(BaseRepository): query = select(models.Resource) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def create_resource(self, resource: schemas.ResourceCreate) -> models.Resource: """ @@ -75,7 +76,7 @@ class ResourcePoolsRepository(BaseRepository): query = delete(models.Resource).where(models.Resource.resource_id == resource_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def get_resource_memberships(self, resource_id: UUID) -> List[models.ResourcePool]: """ @@ -89,7 +90,7 @@ class ResourcePoolsRepository(BaseRepository): ) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def get_resource_pool(self, resource_pool_id: UUID) -> Optional[models.ResourcePool]: """ @@ -116,7 +117,7 @@ class ResourcePoolsRepository(BaseRepository): query = select(models.ResourcePool) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def create_resource_pool(self, resource_pool: schemas.ResourcePoolCreate) -> models.ResourcePool: """ @@ -166,7 +167,7 @@ class ResourcePoolsRepository(BaseRepository): query = delete(models.ResourcePool).where(models.ResourcePool.resource_pool_id == resource_pool_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def add_resource_to_pool( self, resource_pool_id: UUID, resource: models.Resource @@ -225,4 +226,4 @@ class ResourcePoolsRepository(BaseRepository): ) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) diff --git a/gns3server/db/repositories/rbac.py b/gns3server/db/repositories/rbac.py index 13bfd067c..72cb1f954 100644 --- a/gns3server/db/repositories/rbac.py +++ b/gns3server/db/repositories/rbac.py @@ -17,8 +17,9 @@ from uuid import UUID from urllib.parse import urlparse -from typing import Optional, List, Union +from typing import Optional, List, Union, cast from sqlalchemy import select, update, delete +from sqlalchemy.engine import CursorResult from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -62,9 +63,9 @@ class RbacRepository(BaseRepository): query = select(models.Role).options(selectinload(models.Role.privileges)) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) - async def create_role(self, role_create: schemas.RoleCreate) -> models.Role: + async def create_role(self, role_create: schemas.RoleCreate) -> Optional[models.Role]: """ Create a new role. """ @@ -100,7 +101,7 @@ class RbacRepository(BaseRepository): query = delete(models.Role).where(models.Role.role_id == role_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def add_privilege_to_role(self, role_id: UUID, privilege: models.Privilege) -> Union[None, models.Role]: """ @@ -149,7 +150,7 @@ class RbacRepository(BaseRepository): query = select(models.Privilege).join(models.Privilege.roles).filter(models.Role.role_id == role_id) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def get_privilege(self, privilege_id: UUID) -> Optional[models.Privilege]: """ @@ -176,7 +177,7 @@ class RbacRepository(BaseRepository): query = select(models.Privilege) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def get_ace(self, ace_id: UUID) -> Optional[models.ACE]: """ @@ -203,7 +204,7 @@ class RbacRepository(BaseRepository): query = select(models.ACE) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def get_aces_for_path(self, path: str) -> List[models.ACE]: """ @@ -218,7 +219,7 @@ class RbacRepository(BaseRepository): .options(selectinload(models.ACE.user), selectinload(models.ACE.group), selectinload(models.ACE.role)) ) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def check_ace_exists(self, path: str) -> bool: """ @@ -264,7 +265,7 @@ class RbacRepository(BaseRepository): query = delete(models.ACE).where(models.ACE.ace_id == ace_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def delete_all_ace_starting_with_path(self, path: str) -> None: """ @@ -273,7 +274,7 @@ class RbacRepository(BaseRepository): query = delete(models.ACE).where(models.ACE.path.startswith(path)).execution_options(synchronize_session=False) result = await self._db_session.execute(query) - log.debug(f"{result.rowcount} ACE(s) have been deleted") + log.debug(f"{cast(CursorResult, result).rowcount} ACE(s) have been deleted") @staticmethod def _check_path_with_aces(path: str, aces) -> bool: @@ -297,7 +298,7 @@ class RbacRepository(BaseRepository): return True # only allow if the path is the original path or the ACE is set to propagate return False - async def _get_resources_in_pools(self, aces, path: str = None) -> List[models.Resource]: + async def _get_resources_in_pools(self, aces, path: Optional[str] = None) -> List[models.Resource]: """ Get all resources in pools. """ @@ -392,7 +393,7 @@ class RbacRepository(BaseRepository): all_resources = result.scalars().all() # Precompute pool_id -> set of project_ids - pool_to_projects = {} + pool_to_projects: dict[str, set[str]] = {} for r in all_resources: if r.resource_type == "project": for pool in r.resource_pools: diff --git a/gns3server/db/repositories/templates.py b/gns3server/db/repositories/templates.py index 2cca2cda9..b92dd7fd7 100644 --- a/gns3server/db/repositories/templates.py +++ b/gns3server/db/repositories/templates.py @@ -16,11 +16,13 @@ # along with this program. If not, see . import os +import uuid import logging from uuid import UUID -from typing import List, Union, Optional +from typing import List, Union, Optional, cast from sqlalchemy import select, delete +from sqlalchemy.engine import CursorResult from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from sqlalchemy.orm.session import make_transient @@ -66,7 +68,7 @@ class TemplatesRepository(BaseRepository): result = await self._db_session.execute(query) return result.scalars().first() - async def get_template_by_name_and_version(self, name: str, version: str) -> Union[None, models.Template]: + async def get_template_by_name_and_version(self, name: str, version: Optional[str]) -> Union[None, models.Template]: query = ( select(models.Template) @@ -91,7 +93,7 @@ class TemplatesRepository(BaseRepository): query = select(models.Template).options(selectinload(models.Template.images)) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def create_template(self, template_type: str, template_settings: dict) -> models.Template: @@ -102,7 +104,7 @@ class TemplatesRepository(BaseRepository): await self._db_session.refresh(db_template) return db_template - async def update_template(self, db_template: models.Template, template_settings: dict) -> schemas.Template: + async def update_template(self, db_template: models.Template, template_settings: dict) -> models.Template: # update the fields directly because update() query couldn't work for key, value in template_settings.items(): @@ -116,9 +118,9 @@ class TemplatesRepository(BaseRepository): query = delete(models.Template).where(models.Template.template_id == template_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 - async def duplicate_template(self, template_id: UUID) -> Optional[schemas.Template]: + async def duplicate_template(self, template_id: UUID) -> Optional[models.Template]: query = ( select(models.Template) @@ -130,7 +132,7 @@ class TemplatesRepository(BaseRepository): # duplicate db object with new primary key (template_id) self._db_session.expunge(db_template) make_transient(db_template) - db_template.template_id = None + db_template.template_id = uuid.uuid4() self._db_session.add(db_template) await self._db_session.commit() await self._db_session.refresh(db_template) @@ -219,4 +221,4 @@ class TemplatesRepository(BaseRepository): query = select(models.Image).join(models.Image.templates).filter(models.Template.template_id == template_id) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) diff --git a/gns3server/db/repositories/users.py b/gns3server/db/repositories/users.py index f7e098dc4..556185272 100644 --- a/gns3server/db/repositories/users.py +++ b/gns3server/db/repositories/users.py @@ -16,8 +16,9 @@ # along with this program. If not, see . from uuid import UUID -from typing import Optional, List, Union +from typing import Optional, List, Union, cast from sqlalchemy import select, update, delete, func +from sqlalchemy.engine import CursorResult from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -72,7 +73,7 @@ class UsersRepository(BaseRepository): query = select(models.User) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def create_user(self, user: schemas.UserCreate) -> models.User: """ @@ -88,7 +89,9 @@ class UsersRepository(BaseRepository): await self._db_session.refresh(db_user) return db_user - async def update_user(self, user_id: UUID, user_update: schemas.UserUpdate) -> Optional[models.User]: + async def update_user( + self, user_id: UUID, user_update: Union[schemas.UserUpdate, schemas.LoggedInUserUpdate] + ) -> Optional[models.User]: """ Update a user. """ @@ -129,7 +132,7 @@ class UsersRepository(BaseRepository): query = delete(models.User).where(models.User.user_id == user_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def authenticate_user(self, username: str, password: str) -> Optional[models.User]: """ @@ -168,7 +171,7 @@ class UsersRepository(BaseRepository): query = select(models.UserGroup).join(models.UserGroup.users).filter(models.User.user_id == user_id) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def get_user_group(self, user_group_id: UUID) -> Optional[models.UserGroup]: """ @@ -195,7 +198,7 @@ class UsersRepository(BaseRepository): query = select(models.UserGroup) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) async def create_user_group(self, user_group: schemas.UserGroupCreate) -> models.UserGroup: """ @@ -233,7 +236,7 @@ class UsersRepository(BaseRepository): query = delete(models.UserGroup).where(models.UserGroup.user_group_id == user_group_id) result = await self._db_session.execute(query) await self._db_session.commit() - return result.rowcount > 0 + return cast(CursorResult, result).rowcount > 0 async def add_member_to_user_group(self, user_group_id: UUID, user: models.User) -> Union[None, models.UserGroup]: """ @@ -285,4 +288,4 @@ class UsersRepository(BaseRepository): query = select(models.User).join(models.User.groups).filter(models.UserGroup.user_group_id == user_group_id) result = await self._db_session.execute(query) - return result.scalars().all() + return list(result.scalars().all()) diff --git a/gns3server/db/tasks.py b/gns3server/db/tasks.py index 99dd6a81d..b3f762021 100644 --- a/gns3server/db/tasks.py +++ b/gns3server/db/tasks.py @@ -177,7 +177,7 @@ async def disconnect_from_db(app: FastAPI) -> None: log.info(f"Disconnected from database") -async def get_computes(app: FastAPI) -> List[dict]: +async def get_computes(app: FastAPI) -> List[schemas.Compute]: computes = [] async with AsyncSession(app.state._db_engine) as db_session: @@ -199,8 +199,8 @@ async def update_disk_checksums(updated_disks: List[str]) -> None: for path in updated_disks: async with image_lock(path): - async with AsyncSession(app.state._db_engine, expire_on_commit=False) as session: - repository = ImagesRepository(session) + async with AsyncSession(app.state._db_engine, expire_on_commit=False) as db_session: + repository = ImagesRepository(db_session) image = await repository.get_image(path) if image: info = await read_image_info(path, image.image_type, allow_raw_image=True) @@ -234,7 +234,7 @@ async def get_user_llm_config_full(user_id: str, app: FastAPI) -> Optional[dict] from gns3server.utils.encryption import decrypt, is_encrypted try: - user_uuid = UUID(user_id) if isinstance(user_id, str) else user_id + user_uuid = UUID(user_id) async with AsyncSession(app.state._db_engine, expire_on_commit=False) as session: repo = LLMModelConfigsRepository(session) diff --git a/gns3server/schemas/compute/atm_switch_nodes.py b/gns3server/schemas/compute/atm_switch_nodes.py index 9a9db0845..9d126bfe9 100644 --- a/gns3server/schemas/compute/atm_switch_nodes.py +++ b/gns3server/schemas/compute/atm_switch_nodes.py @@ -26,8 +26,8 @@ class ATMSwitchBase(BaseModel): Common ATM switch properties. """ - name: str = None - node_id: UUID = None + name: Optional[str] = None + node_id: Optional[UUID] = None usage: Optional[str] = None mappings: Optional[dict] = None diff --git a/gns3server/schemas/compute/cloud_nodes.py b/gns3server/schemas/compute/cloud_nodes.py index 4cda4ce23..8fee188a1 100644 --- a/gns3server/schemas/compute/cloud_nodes.py +++ b/gns3server/schemas/compute/cloud_nodes.py @@ -126,7 +126,7 @@ class CloudBase(BaseModel): Common cloud node properties. """ - name: str + name: Optional[str] = None node_id: Optional[UUID] = None usage: Optional[str] = None remote_console_host: Optional[str] = Field(None, description="Remote console host or IP") @@ -144,7 +144,7 @@ class CloudCreate(CloudBase): Properties to create a cloud node. """ - pass + name: str class CloudUpdate(CloudBase): @@ -152,10 +152,11 @@ class CloudUpdate(CloudBase): Properties to update a cloud node. """ - name: Optional[str] = None + pass class Cloud(CloudBase): + name: str project_id: UUID node_id: UUID ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] diff --git a/gns3server/schemas/compute/docker_nodes.py b/gns3server/schemas/compute/docker_nodes.py index 4359df723..1745b6d2e 100644 --- a/gns3server/schemas/compute/docker_nodes.py +++ b/gns3server/schemas/compute/docker_nodes.py @@ -41,8 +41,8 @@ class DockerBase(BaseModel): # the canonical "no path" value is "/" (the creation default) return value or "/" - name: str - image: str = Field(..., description="Docker image name") + name: Optional[str] = None + image: Optional[str] = Field(None, description="Docker image name") node_id: Optional[UUID] = None console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") console_type: Optional[ConsoleType] = Field(None, description="Console type") @@ -76,6 +76,8 @@ class DockerCreate(DockerBase): Properties to create a Docker node. """ + name: str + image: str = Field(..., description="Docker image name") application_id: Optional[int] = Field( None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)" ) @@ -93,11 +95,12 @@ class DockerUpdate(DockerBase): Properties to update a Docker node. """ - name: Optional[str] = None - image: Optional[str] = Field(None, description="Docker image name") + pass class Docker(DockerBase): + name: str + image: str = Field(..., description="Docker image name") container_id: str = Field( ..., min_length=12, max_length=64, pattern="^[a-f0-9]+$", description="Docker container ID (read only)" ) diff --git a/gns3server/schemas/compute/dynamips_nodes.py b/gns3server/schemas/compute/dynamips_nodes.py index d01020a28..6292b400b 100644 --- a/gns3server/schemas/compute/dynamips_nodes.py +++ b/gns3server/schemas/compute/dynamips_nodes.py @@ -124,7 +124,6 @@ class DynamipsBase(BaseModel): node_id: Optional[UUID] = None name: Optional[str] = None dynamips_id: Optional[int] = Field(None, description="Dynamips internal ID") - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") ram: Optional[int] = Field(None, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(None, gt=0, description="Amount of NVRAM in KB") image: Optional[str] = Field(None, description="Path to the IOS image") @@ -185,11 +184,12 @@ class DynamipsUpdate(DynamipsBase): Properties to update a Dynamips node. """ - pass + platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") class Dynamips(DynamipsBase): name: str + platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") node_id: UUID project_id: UUID dynamips_id: int diff --git a/gns3server/schemas/compute/frame_relay_switch_nodes.py b/gns3server/schemas/compute/frame_relay_switch_nodes.py index 67db0bf3e..fbb86b553 100644 --- a/gns3server/schemas/compute/frame_relay_switch_nodes.py +++ b/gns3server/schemas/compute/frame_relay_switch_nodes.py @@ -26,8 +26,8 @@ class FrameRelaySwitchBase(BaseModel): Common Frame Relay switch properties. """ - name: str = None - node_id: UUID = None + name: Optional[str] = None + node_id: Optional[UUID] = None usage: Optional[str] = None mappings: Optional[dict] = None diff --git a/gns3server/schemas/compute/iou_nodes.py b/gns3server/schemas/compute/iou_nodes.py index f4dd42ce5..e51fa1a28 100644 --- a/gns3server/schemas/compute/iou_nodes.py +++ b/gns3server/schemas/compute/iou_nodes.py @@ -26,9 +26,9 @@ class IOUBase(BaseModel): Common IOU node properties. """ - name: str - path: str = Field(..., description="IOU executable path") - application_id: int = Field(..., description="Application ID for running IOU executable") + name: Optional[str] = None + path: Optional[str] = Field(None, description="IOU executable path") + application_id: Optional[int] = Field(None, description="Application ID for running IOU executable") node_id: Optional[UUID] = None usage: Optional[str] = Field(None, description="How to use the node") console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") @@ -52,7 +52,9 @@ class IOUCreate(IOUBase): Properties to create an IOU node. """ - pass + name: str + path: str = Field(..., description="IOU executable path") + application_id: int = Field(..., description="Application ID for running IOU executable") class IOUUpdate(IOUBase): @@ -60,12 +62,13 @@ class IOUUpdate(IOUBase): Properties to update an IOU node. """ - name: Optional[str] = None - path: Optional[str] = Field(None, description="IOU executable path") - application_id: Optional[int] = Field(None, description="Application ID for running IOU executable") + pass class IOU(IOUBase): + name: str + path: str = Field(..., description="IOU executable path") + application_id: int = Field(..., description="Application ID for running IOU executable") project_id: UUID = Field(..., description="Project ID") node_directory: str = Field(..., description="Path to the node working directory (read only)") command_line: str = Field(..., description="Last command line used to start IOU (read only)") diff --git a/gns3server/schemas/compute/nat_nodes.py b/gns3server/schemas/compute/nat_nodes.py index 95a703320..e3c569251 100644 --- a/gns3server/schemas/compute/nat_nodes.py +++ b/gns3server/schemas/compute/nat_nodes.py @@ -89,7 +89,7 @@ class NATBase(BaseModel): Common NAT node properties. """ - name: str + name: Optional[str] = None node_id: Optional[UUID] = None usage: Optional[str] = None ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field( @@ -102,7 +102,7 @@ class NATCreate(NATBase): Properties to create a NAT node. """ - pass + name: str class NATUpdate(NATBase): @@ -110,10 +110,11 @@ class NATUpdate(NATBase): Properties to update a NAT node. """ - name: Optional[str] = None + pass class NAT(NATBase): + name: str project_id: UUID node_id: UUID ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] diff --git a/gns3server/schemas/compute/qemu_nodes.py b/gns3server/schemas/compute/qemu_nodes.py index 4596677c8..275ac92bb 100644 --- a/gns3server/schemas/compute/qemu_nodes.py +++ b/gns3server/schemas/compute/qemu_nodes.py @@ -155,7 +155,7 @@ class QemuBase(BaseModel): Common Qemu node properties. """ - name: str + name: Optional[str] = None node_id: Optional[UUID] = None usage: Optional[str] = Field(None, description="How to use the node") linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not") @@ -219,6 +219,7 @@ class QemuCreate(QemuBase): Properties to create a Qemu node. """ + name: str disk_images_to_reset: Optional[List[str]] = Field( None, description="Disk image properties whose stale linked-clone overlays must be recreated", @@ -230,10 +231,11 @@ class QemuUpdate(QemuBase): Properties to update a Qemu node. """ - name: Optional[str] = None + pass class Qemu(QemuBase): + name: str project_id: UUID = Field(..., description="Project ID") node_directory: str = Field(..., description="Path to the node working directory (read only)") command_line: str = Field(..., description="Last command line used to start IOU (read only)") diff --git a/gns3server/schemas/compute/virtualbox_nodes.py b/gns3server/schemas/compute/virtualbox_nodes.py index 06ad9f831..d05692ebc 100644 --- a/gns3server/schemas/compute/virtualbox_nodes.py +++ b/gns3server/schemas/compute/virtualbox_nodes.py @@ -56,8 +56,8 @@ class VirtualBoxBase(BaseModel): Common VirtualBox node properties. """ - name: str - vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") + name: Optional[str] = None + vmname: Optional[str] = None node_id: Optional[UUID] = None linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not") usage: Optional[str] = Field(None, description="How to use the node") @@ -78,7 +78,8 @@ class VirtualBoxCreate(VirtualBoxBase): Properties to create a VirtualBox node. """ - pass + name: str + vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") class VirtualBoxUpdate(VirtualBoxBase): @@ -86,11 +87,12 @@ class VirtualBoxUpdate(VirtualBoxBase): Properties to update a VirtualBox node. """ - name: Optional[str] = None - vmname: Optional[str] = None + pass class VirtualBox(VirtualBoxBase): + name: str + vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") project_id: UUID = Field(..., description="Project ID") node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)") status: NodeStatus = Field(..., description="Container status (read only)") diff --git a/gns3server/schemas/compute/vmware_nodes.py b/gns3server/schemas/compute/vmware_nodes.py index ee670a2bc..f838419a3 100644 --- a/gns3server/schemas/compute/vmware_nodes.py +++ b/gns3server/schemas/compute/vmware_nodes.py @@ -62,9 +62,9 @@ class VMwareBase(BaseModel): Common VMware node properties. """ - name: str - vmx_path: str = Field(..., description="Path to the vmx file") - linked_clone: bool = Field(..., description="Whether the VM is a linked clone or not") + name: Optional[str] = None + vmx_path: Optional[str] = None + linked_clone: Optional[bool] = None node_id: Optional[UUID] = None usage: Optional[str] = Field(None, description="How to use the node") console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") @@ -83,7 +83,9 @@ class VMwareCreate(VMwareBase): Properties to create a VMware node. """ - pass + name: str + vmx_path: str = Field(..., description="Path to the vmx file") + linked_clone: bool = Field(..., description="Whether the VM is a linked clone or not") class VMwareUpdate(VMwareBase): @@ -91,12 +93,13 @@ class VMwareUpdate(VMwareBase): Properties to update a VMware node. """ - name: Optional[str] = None - vmx_path: Optional[str] = None - linked_clone: Optional[bool] = None + pass class VMware(VMwareBase): + name: str + vmx_path: str = Field(..., description="Path to the vmx file") + linked_clone: bool = Field(..., description="Whether the VM is a linked clone or not") project_id: UUID = Field(..., description="Project ID") node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)") status: NodeStatus = Field(..., description="Container status (read only)") diff --git a/gns3server/schemas/compute/vpcs_nodes.py b/gns3server/schemas/compute/vpcs_nodes.py index 30cd40960..fda0634c4 100644 --- a/gns3server/schemas/compute/vpcs_nodes.py +++ b/gns3server/schemas/compute/vpcs_nodes.py @@ -37,7 +37,7 @@ class VPCSBase(BaseModel): Common VPCS node properties. """ - name: str + name: Optional[str] = None node_id: Optional[UUID] = None usage: Optional[str] = Field(None, description="How to use the node") console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") @@ -50,7 +50,7 @@ class VPCSCreate(VPCSBase): Properties to create a VPCS node. """ - pass + name: str class VPCSUpdate(VPCSBase): @@ -58,10 +58,11 @@ class VPCSUpdate(VPCSBase): Properties to update a VPCS node. """ - name: Optional[str] = None + pass class VPCS(VPCSBase): + name: str project_id: UUID = Field(..., description="Project ID") node_directory: str = Field(..., description="Path to the node working directory (read only)") status: NodeStatus = Field(..., description="Container status (read only)") diff --git a/gns3server/schemas/controller/appliances.py b/gns3server/schemas/controller/appliances.py index a4aecaa94..3660067b8 100644 --- a/gns3server/schemas/controller/appliances.py +++ b/gns3server/schemas/controller/appliances.py @@ -782,7 +782,7 @@ Appliance = ApplianceUnion # Create a validator wrapper for convenience from pydantic import TypeAdapter -_appliance_validator = TypeAdapter(ApplianceUnion) +_appliance_validator: TypeAdapter[Union[ApplianceV1_6, ApplianceV8]] = TypeAdapter(ApplianceUnion) class ApplianceModel: diff --git a/gns3server/schemas/controller/computes.py b/gns3server/schemas/controller/computes.py index 1c87d95a0..f8612f43d 100644 --- a/gns3server/schemas/controller/computes.py +++ b/gns3server/schemas/controller/computes.py @@ -38,10 +38,10 @@ class ComputeBase(BaseModel): Data to create a compute. """ - protocol: Protocol - host: str - port: int = Field(..., gt=0, le=65535) - user: str = None + protocol: Optional[Protocol] = None + host: Optional[str] = None + port: Optional[int] = Field(None, gt=0, le=65535) + user: Optional[str] = None password: Optional[SecretStr] = None name: Optional[str] = None model_config = ConfigDict(use_enum_values=True) @@ -52,7 +52,10 @@ class ComputeCreate(ComputeBase): Data to create a compute. """ - compute_id: Union[str, uuid.UUID] = None + protocol: Protocol + host: str + port: int = Field(..., gt=0, le=65535) + compute_id: Optional[Union[str, uuid.UUID]] = None model_config = ConfigDict( json_schema_extra={ "example": {"name": "My compute", "host": "127.0.0.1", "port": 3080, "user": "user", "password": "password"} @@ -77,9 +80,6 @@ class ComputeUpdate(ComputeBase): Data to update a compute. """ - protocol: Optional[Protocol] = None - host: Optional[str] = None - port: Optional[int] = Field(None, gt=0, le=65535) user: Optional[str] = None password: Optional[SecretStr] = None model_config = ConfigDict( @@ -110,6 +110,9 @@ class Compute(DateTimeModelMixin, ComputeBase): Data returned for a compute. """ + protocol: Protocol + host: str + port: int = Field(..., gt=0, le=65535) compute_id: Union[str, uuid.UUID] name: str connected: Optional[bool] = Field(None, description="Whether the controller is connected to the compute or not") diff --git a/gns3server/schemas/controller/nodes.py b/gns3server/schemas/controller/nodes.py index 43d8ca5b4..9faf260d3 100644 --- a/gns3server/schemas/controller/nodes.py +++ b/gns3server/schemas/controller/nodes.py @@ -114,9 +114,9 @@ class NodeBase(BaseModel): Node data. """ - compute_id: Union[UUID, str] - name: str - node_type: NodeType + compute_id: Optional[Union[UUID, str]] = None + name: Optional[str] = None + node_type: Optional[NodeType] = None node_id: Optional[UUID] = None @@ -162,6 +162,9 @@ class NodeBase(BaseModel): class NodeCreate(NodeBase): + compute_id: Union[UUID, str] + name: str + node_type: NodeType node_id: UUID = Field(default_factory=uuid4) @@ -170,12 +173,13 @@ class NodeUpdate(NodeBase): Data to update a node. """ - compute_id: Optional[Union[UUID, str]] = None - name: Optional[str] = None - node_type: Optional[NodeType] = None + pass class Node(NodeBase): + compute_id: Union[UUID, str] + name: str + node_type: NodeType template_id: Optional[UUID] = Field( None, description="Template UUID from which the node has been created. Read only" ) diff --git a/gns3server/schemas/controller/projects.py b/gns3server/schemas/controller/projects.py index ddfbde02e..e1210f99f 100644 --- a/gns3server/schemas/controller/projects.py +++ b/gns3server/schemas/controller/projects.py @@ -45,7 +45,7 @@ class ProjectBase(BaseModel): Common properties for projects. """ - name: str + name: Optional[str] = None project_id: Optional[UUID] = None path: Optional[str] = Field(None, description="Project directory") auto_close: Optional[bool] = Field(None, description="Close project when last client leaves") @@ -69,7 +69,7 @@ class ProjectCreate(ProjectBase): Properties for project creation. """ - pass + name: str class ProjectDuplicate(ProjectBase): @@ -77,6 +77,7 @@ class ProjectDuplicate(ProjectBase): Properties for project duplication. """ + name: str reset_mac_addresses: Optional[bool] = Field(False, description="Reset MAC addresses for this project") @@ -85,12 +86,11 @@ class ProjectUpdate(ProjectBase): Properties for project update. """ - name: Optional[str] = None + pass class Project(ProjectBase): project_id: UUID - name: Optional[str] = None status: Optional[ProjectStatus] = None filename: Optional[str] = None created_by: Optional[str] = Field(None, description="Username of the user who created the project") diff --git a/gns3server/schemas/controller/settings.py b/gns3server/schemas/controller/settings.py index 098831f8d..87b4ab2a5 100644 --- a/gns3server/schemas/controller/settings.py +++ b/gns3server/schemas/controller/settings.py @@ -25,6 +25,7 @@ says, so exposing or writing it via the API would be useless at best and a secret leak at worst. """ +from pathlib import Path from typing import List, Optional from pydantic import ConfigDict, BaseModel, Field @@ -49,9 +50,11 @@ SECRET_MASK = "**********" class ServerSettingsResponse(ServerSettings): # plain strings instead of FilePath/DirectoryPath: paths are validated when # the settings are loaded or updated, not when echoed back to the client - secrets_dir: Optional[str] = Field(None, description="Directory where secrets are stored (e.g. the JWT secret key)") - certfile: Optional[str] = Field(None, description="SSL certificate file, requires enable_ssl") - certkey: Optional[str] = Field(None, description="SSL key file, requires enable_ssl") + secrets_dir: Optional[Path] = Field( + None, description="Directory where secrets are stored (e.g. the JWT secret key)" + ) + certfile: Optional[Path] = Field(None, description="SSL certificate file, requires enable_ssl") + certkey: Optional[Path] = Field(None, description="SSL key file, requires enable_ssl") # Optional overrides: typed as plain "str = None" in the config schema, # which fails re-validation when the value actually is None resources_path: Optional[str] = Field( diff --git a/gns3server/schemas/controller/templates/docker_templates.py b/gns3server/schemas/controller/templates/docker_templates.py index 1c7a320e3..4123630e8 100644 --- a/gns3server/schemas/controller/templates/docker_templates.py +++ b/gns3server/schemas/controller/templates/docker_templates.py @@ -22,11 +22,11 @@ from pydantic import Field from typing import Optional, List -class DockerTemplate(TemplateBase): +class DockerTemplateBase(TemplateBase): category: Optional[Category] = Category.guest default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = "docker_guest" - image: str = Field(..., description="Docker image name") + image: Optional[str] = Field(None, description="Docker image name") adapters: Optional[int] = Field(1, ge=0, le=100, description="Number of adapters") mac_address: Optional[str] = Field( "", description="Base MAC address", pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$|^$" @@ -58,5 +58,9 @@ class DockerTemplate(TemplateBase): custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") -class DockerTemplateUpdate(DockerTemplate): - image: Optional[str] = Field(None, description="Docker image name") +class DockerTemplate(DockerTemplateBase): + image: str = Field(..., description="Docker image name") + + +class DockerTemplateUpdate(DockerTemplateBase): + pass diff --git a/gns3server/schemas/controller/templates/dynamips_templates.py b/gns3server/schemas/controller/templates/dynamips_templates.py index 90180d99b..69505051d 100644 --- a/gns3server/schemas/controller/templates/dynamips_templates.py +++ b/gns3server/schemas/controller/templates/dynamips_templates.py @@ -30,12 +30,12 @@ from typing import Optional from enum import Enum -class DynamipsTemplate(TemplateBase): +class DynamipsTemplateBase(TemplateBase): category: Optional[Category] = Category.router default_name_format: Optional[str] = "R{0}" symbol: Optional[str] = "router" - platform: DynamipsPlatform = Field(..., description="Cisco router platform") - image: str = Field(..., description="Path to the IOS image") + platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") + image: Optional[str] = Field(None, description="Path to the IOS image") exec_area: Optional[int] = Field(64, ge=0, description="Exec area value") mmap: Optional[bool] = Field(True, description="MMAP feature") mac_addr: Optional[str] = Field( @@ -67,7 +67,12 @@ class DynamipsTemplate(TemplateBase): wic2: Optional[DynamipsWics] = Field(None, description="Network module WIC slot 2") -class C7200DynamipsTemplate(DynamipsTemplate): +class DynamipsTemplate(DynamipsTemplateBase): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") + + +class C7200DynamipsTemplateUpdate(DynamipsTemplateBase): ram: Optional[int] = Field(512, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(512, gt=0, description="Amount of NVRAM in KB") npe: Optional[DynamipsNPE] = Field(DynamipsNPE.npe_400, description="NPE model") @@ -75,33 +80,33 @@ class C7200DynamipsTemplate(DynamipsTemplate): sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") -class C7200DynamipsTemplateUpdate(C7200DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") - image: Optional[str] = Field(None, description="Path to the IOS image") +class C7200DynamipsTemplate(C7200DynamipsTemplateUpdate): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") -class C3725DynamipsTemplate(DynamipsTemplate): +class C3725DynamipsTemplateUpdate(DynamipsTemplateBase): ram: Optional[int] = Field(128, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(256, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") -class C3725DynamipsTemplateUpdate(C3725DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") - image: Optional[str] = Field(None, description="Path to the IOS image") +class C3725DynamipsTemplate(C3725DynamipsTemplateUpdate): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") -class C3745DynamipsTemplate(DynamipsTemplate): +class C3745DynamipsTemplateUpdate(DynamipsTemplateBase): ram: Optional[int] = Field(256, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(256, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") -class C3745DynamipsTemplateUpdate(C3745DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") - image: Optional[str] = Field(None, description="Path to the IOS image") +class C3745DynamipsTemplate(C3745DynamipsTemplateUpdate): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") class C3600ChassisType(str, Enum): @@ -110,29 +115,29 @@ class C3600ChassisType(str, Enum): chassis_3660 = "3660" -class C3600DynamipsTemplate(DynamipsTemplate): - chassis: Optional[C3600ChassisType] = Field("c3660", description="Chassis type") +class C3600DynamipsTemplateUpdate(DynamipsTemplateBase): + chassis: Optional[C3600ChassisType] = Field(C3600ChassisType.chassis_3660, description="Chassis type") ram: Optional[int] = Field(192, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(128, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") -class C3600DynamipsTemplateUpdate(C3600DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") - image: Optional[str] = Field(None, description="Path to the IOS image") +class C3600DynamipsTemplate(C3600DynamipsTemplateUpdate): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") -class C2691DynamipsTemplate(DynamipsTemplate): +class C2691DynamipsTemplateUpdate(DynamipsTemplateBase): ram: Optional[int] = Field(192, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(256, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(5, ge=0, le=100, description="I/O memory percentage") sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") -class C2691DynamipsTemplateUpdate(C2691DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") - image: Optional[str] = Field(None, description="Path to the IOS image") +class C2691DynamipsTemplate(C2691DynamipsTemplateUpdate): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") class C2600ChassisType(str, Enum): @@ -147,17 +152,17 @@ class C2600ChassisType(str, Enum): chassis_2651xm = "2651XM" -class C2600DynamipsTemplate(DynamipsTemplate): - chassis: Optional[C2600ChassisType] = Field("2651XM", description="Chassis type") +class C2600DynamipsTemplateUpdate(DynamipsTemplateBase): + chassis: Optional[C2600ChassisType] = Field(C2600ChassisType.chassis_2651xm, description="Chassis type") ram: Optional[int] = Field(160, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(128, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(15, ge=0, le=100, description="I/O memory percentage") sparsemem: Optional[bool] = Field(True, description="Sparse memory feature") -class C2600DynamipsTemplateUpdate(C2600DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") - image: Optional[str] = Field(None, description="Path to the IOS image") +class C2600DynamipsTemplate(C2600DynamipsTemplateUpdate): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") class C1700ChassisType(str, Enum): @@ -168,14 +173,14 @@ class C1700ChassisType(str, Enum): chassis_1760 = "1760" -class C1700DynamipsTemplate(DynamipsTemplate): - chassis: Optional[C1700ChassisType] = Field("1760", description="Chassis type") +class C1700DynamipsTemplateUpdate(DynamipsTemplateBase): + chassis: Optional[C1700ChassisType] = Field(C1700ChassisType.chassis_1760, description="Chassis type") ram: Optional[int] = Field(160, gt=0, description="Amount of RAM in MB") nvram: Optional[int] = Field(128, gt=0, description="Amount of NVRAM in KB") iomem: Optional[int] = Field(15, ge=0, le=100, description="I/O memory percentage") sparsemem: Optional[bool] = Field(False, description="Sparse memory feature") -class C1700DynamipsTemplateUpdate(C1700DynamipsTemplate): - platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform") - image: Optional[str] = Field(None, description="Path to the IOS image") +class C1700DynamipsTemplate(C1700DynamipsTemplateUpdate): + platform: DynamipsPlatform = Field(..., description="Cisco router platform") + image: str = Field(..., description="Path to the IOS image") diff --git a/gns3server/schemas/controller/templates/iou_templates.py b/gns3server/schemas/controller/templates/iou_templates.py index 02ab06696..36468d8aa 100644 --- a/gns3server/schemas/controller/templates/iou_templates.py +++ b/gns3server/schemas/controller/templates/iou_templates.py @@ -22,11 +22,11 @@ from pydantic import Field from typing import Optional -class IOUTemplate(TemplateBase): +class IOUTemplateBase(TemplateBase): category: Optional[Category] = Category.router default_name_format: Optional[str] = "IOU{0}" symbol: Optional[str] = "multilayer_switch" - path: str = Field(..., description="Path of IOU executable") + path: Optional[str] = Field(None, description="Path of IOU executable") ethernet_adapters: Optional[int] = Field(2, ge=0, description="Number of ethernet adapters") serial_adapters: Optional[int] = Field(2, ge=0, description="Number of serial adapters") ram: Optional[int] = Field(1024, gt=0, description="Amount of RAM in MB") @@ -44,5 +44,9 @@ class IOUTemplate(TemplateBase): ) -class IOUTemplateUpdate(IOUTemplate): - path: Optional[str] = Field(None, description="Path of IOU executable") +class IOUTemplate(IOUTemplateBase): + path: str = Field(..., description="Path of IOU executable") + + +class IOUTemplateUpdate(IOUTemplateBase): + pass diff --git a/gns3server/schemas/controller/templates/virtualbox_templates.py b/gns3server/schemas/controller/templates/virtualbox_templates.py index 3cce1cd6e..b9d966ec1 100644 --- a/gns3server/schemas/controller/templates/virtualbox_templates.py +++ b/gns3server/schemas/controller/templates/virtualbox_templates.py @@ -26,11 +26,11 @@ from pydantic import Field from typing import Optional, List -class VirtualBoxTemplate(TemplateBase): +class VirtualBoxTemplateBase(TemplateBase): category: Optional[Category] = Category.guest default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = "vbox_guest" - vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") + vmname: Optional[str] = Field(None, description="VirtualBox VM name (in VirtualBox itself)") ram: Optional[int] = Field(256, gt=0, description="Amount of RAM in MB") linked_clone: Optional[bool] = Field(False, description="Whether the VM is a linked clone or not") adapters: Optional[int] = Field( @@ -59,5 +59,9 @@ class VirtualBoxTemplate(TemplateBase): custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") -class VirtualBoxTemplateUpdate(VirtualBoxTemplate): - vmname: Optional[str] = Field(None, description="VirtualBox VM name (in VirtualBox itself)") +class VirtualBoxTemplate(VirtualBoxTemplateBase): + vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") + + +class VirtualBoxTemplateUpdate(VirtualBoxTemplateBase): + pass diff --git a/gns3server/schemas/controller/templates/vmware_templates.py b/gns3server/schemas/controller/templates/vmware_templates.py index 4aed1ee90..85d9dce96 100644 --- a/gns3server/schemas/controller/templates/vmware_templates.py +++ b/gns3server/schemas/controller/templates/vmware_templates.py @@ -27,11 +27,11 @@ from pydantic import Field from typing import Optional, List -class VMwareTemplate(TemplateBase): +class VMwareTemplateBase(TemplateBase): category: Optional[Category] = Category.guest default_name_format: Optional[str] = "{name}-{0}" symbol: Optional[str] = "vmware_guest" - vmx_path: str = Field(..., description="Path to the vmx file") + vmx_path: Optional[str] = Field(None, description="Path to the vmx file") linked_clone: Optional[bool] = Field(False, description="Whether the VM is a linked clone or not") first_port_name: Optional[str] = Field("", description="Optional name of the first networking port example: eth0") port_name_format: Optional[str] = Field( @@ -57,5 +57,9 @@ class VMwareTemplate(TemplateBase): custom_adapters: Optional[List[CustomAdapter]] = Field(default_factory=list, description="Custom adapters") -class VMwareTemplateUpdate(VMwareTemplate): - vmx_path: Optional[str] = Field(None, description="Path to the vmx file") +class VMwareTemplate(VMwareTemplateBase): + vmx_path: str = Field(..., description="Path to the vmx file") + + +class VMwareTemplateUpdate(VMwareTemplateBase): + pass diff --git a/gns3server/schemas/controller/tokens.py b/gns3server/schemas/controller/tokens.py index 29197acfa..2ac14dda6 100644 --- a/gns3server/schemas/controller/tokens.py +++ b/gns3server/schemas/controller/tokens.py @@ -25,7 +25,7 @@ class Token(BaseModel): class TokenData(BaseModel): - username: Optional[str] = None + username: str token_version: int = 0 token_use: str = "access" diff --git a/gns3server/schemas/controller/users.py b/gns3server/schemas/controller/users.py index 4fed51d49..8d8b90c13 100644 --- a/gns3server/schemas/controller/users.py +++ b/gns3server/schemas/controller/users.py @@ -80,7 +80,7 @@ class UserGroupCreate(UserGroupBase): Properties to create a user group. """ - name: Optional[str] = Field(..., min_length=3, pattern="[a-zA-Z0-9_-]+$") + name: str = Field(..., min_length=3, pattern="[a-zA-Z0-9_-]+$") class UserGroupUpdate(UserGroupBase): diff --git a/gns3server/schemas/qemu_disk_image.py b/gns3server/schemas/qemu_disk_image.py index a3ebccba6..1c0f596ad 100644 --- a/gns3server/schemas/qemu_disk_image.py +++ b/gns3server/schemas/qemu_disk_image.py @@ -78,8 +78,8 @@ class QemuDiskImageAdapterType(str, Enum): class QemuDiskImageBase(BaseModel): - format: QemuDiskImageFormat = Field(..., description="Image format type") - size: int = Field(..., description="Image size in Megabytes") + format: Optional[QemuDiskImageFormat] = Field(None, description="Image format type") + size: Optional[int] = Field(None, description="Image size in Megabytes") preallocation: Optional[QemuDiskImagePreallocation] = None cluster_size: Optional[int] = None refcount_bits: Optional[int] = None @@ -91,10 +91,9 @@ class QemuDiskImageBase(BaseModel): class QemuDiskImageCreate(QemuDiskImageBase): - pass + format: QemuDiskImageFormat = Field(..., description="Image format type") + size: int = Field(..., description="Image size in Megabytes") class QemuDiskImageUpdate(QemuDiskImageBase): - format: Optional[QemuDiskImageFormat] = Field(None, description="Image format type") - size: Optional[int] = Field(None, description="Image size in Megabytes") extend: Optional[int] = Field(None, description="Number of Megabytes to extend the image") diff --git a/gns3server/services/authentication.py b/gns3server/services/authentication.py index 3d165c55d..c507c01a8 100644 --- a/gns3server/services/authentication.py +++ b/gns3server/services/authentication.py @@ -73,21 +73,23 @@ class AuthService: encoded_jwt = jwt.encode({"alg": algorithm}, to_encode, key) return encoded_jwt - def create_access_token(self, username, token_version: int = 0, secret_key: str = None, expires_in: int = 0) -> str: + def create_access_token( + self, username, token_version: int = 0, secret_key: Optional[str] = None, expires_in: int = 0 + ) -> str: if not expires_in: expires_in = Config.instance().settings.Controller.jwt_access_token_expire_minutes return self._create_token(username, token_version, "access", expires_in, secret_key) def create_refresh_token( - self, username, token_version: int = 0, secret_key: str = None, expires_in: int = 0 + self, username, token_version: int = 0, secret_key: Optional[str] = None, expires_in: int = 0 ) -> str: if not expires_in: expires_in = Config.instance().settings.Controller.jwt_refresh_token_expire_minutes return self._create_token(username, token_version, "refresh", expires_in, secret_key) - def get_token_data(self, token: str, secret_key: str = None) -> TokenData: + def get_token_data(self, token: str, secret_key: Optional[str] = None) -> TokenData: def auth_error(detail: str) -> HTTPException: return HTTPException( @@ -105,7 +107,7 @@ class AuthService: key = OctKey.import_key(secret_key) try: payload = jwt.decode(token, key, algorithms=[algorithm]) - username: str = payload.claims.get("sub") + username: Optional[str] = payload.claims.get("sub") if username is None: raise auth_error("Invalid token: missing subject claim") # Validate the exp claim — joserfc does not validate time-based claims by default @@ -123,5 +125,5 @@ class AuthService: raise auth_error(f"Invalid token ({type(e).__name__})") return token_data - def get_username_from_token(self, token: str, secret_key: str = None) -> Optional[str]: + def get_username_from_token(self, token: str, secret_key: Optional[str] = None) -> Optional[str]: return self.get_token_data(token, secret_key).username diff --git a/gns3server/services/computes.py b/gns3server/services/computes.py index a97c8fcf4..c3e3dad80 100644 --- a/gns3server/services/computes.py +++ b/gns3server/services/computes.py @@ -43,7 +43,7 @@ class ComputesService: async def create_compute(self, compute_create: schemas.ComputeCreate, connect: bool = False) -> models.Compute: - if await self._computes_repo.get_compute(compute_create.compute_id): + if compute_create.compute_id and await self._computes_repo.get_compute(compute_create.compute_id): raise ControllerBadRequestError(f"Compute '{compute_create.compute_id}' is already registered") db_compute = await self._computes_repo.create_compute(compute_create) compute = await self._controller.add_compute( diff --git a/gns3server/services/templates.py b/gns3server/services/templates.py index f49311912..c4fd274ed 100644 --- a/gns3server/services/templates.py +++ b/gns3server/services/templates.py @@ -20,7 +20,7 @@ import pydantic from uuid import UUID from fastapi.encoders import jsonable_encoder -from typing import List +from typing import List, Optional from gns3server import schemas from gns3server.config import Config @@ -34,7 +34,7 @@ from gns3server.controller.controller_error import ( ) -TEMPLATE_TYPE_TO_SCHEMA = { +TEMPLATE_TYPE_TO_SCHEMA: dict[str, type[pydantic.BaseModel]] = { "cloud": schemas.CloudTemplate, "ethernet_hub": schemas.EthernetHubTemplate, "ethernet_switch": schemas.EthernetSwitchTemplate, @@ -47,7 +47,7 @@ TEMPLATE_TYPE_TO_SCHEMA = { "qemu": schemas.QemuTemplate, } -TEMPLATE_TYPE_TO_UPDATE_SCHEMA = { +TEMPLATE_TYPE_TO_UPDATE_SCHEMA: dict[str, type[pydantic.BaseModel]] = { "cloud": schemas.CloudTemplateUpdate, "ethernet_hub": schemas.EthernetHubTemplateUpdate, "ethernet_switch": schemas.EthernetSwitchTemplateUpdate, @@ -59,7 +59,7 @@ TEMPLATE_TYPE_TO_UPDATE_SCHEMA = { "qemu": schemas.QemuTemplateUpdate, } -DYNAMIPS_PLATFORM_TO_SCHEMA = { +DYNAMIPS_PLATFORM_TO_SCHEMA: dict[str, type[pydantic.BaseModel]] = { "c7200": schemas.C7200DynamipsTemplate, "c3745": schemas.C3745DynamipsTemplate, "c3725": schemas.C3725DynamipsTemplate, @@ -69,7 +69,7 @@ DYNAMIPS_PLATFORM_TO_SCHEMA = { "c1700": schemas.C1700DynamipsTemplate, } -DYNAMIPS_PLATFORM_TO_UPDATE_SCHEMA = { +DYNAMIPS_PLATFORM_TO_UPDATE_SCHEMA: dict[str, type[pydantic.BaseModel]] = { "c7200": schemas.C7200DynamipsTemplateUpdate, "c3745": schemas.C3745DynamipsTemplateUpdate, "c3725": schemas.C3725DynamipsTemplateUpdate, @@ -169,11 +169,12 @@ class TemplatesService: for builtin_template in BUILTIN_TEMPLATES: builtin_template["symbol"] = self._controller.symbols.resolve_symbol(builtin_template["symbol"]) - def get_builtin_template(self, template_id: UUID) -> dict: + def get_builtin_template(self, template_id: UUID) -> Optional[dict]: for builtin_template in BUILTIN_TEMPLATES: if builtin_template["template_id"] == template_id: return jsonable_encoder(builtin_template) + return None def _base_path(self): return self._templates_repo.configs_path() @@ -304,7 +305,7 @@ class TemplatesService: try: # validate the update settings update_settings = jsonable_encoder(template_update, exclude_unset=True) - if db_template.template_type == "dynamips": + if isinstance(db_template, models.DynamipsTemplate): template_schema = DYNAMIPS_PLATFORM_TO_UPDATE_SCHEMA[db_template.platform] else: template_schema = TEMPLATE_TYPE_TO_UPDATE_SCHEMA[db_template.template_type] @@ -313,9 +314,9 @@ class TemplatesService: raise ControllerBadRequestError(f"JSON schema error received while updating template: {e}") images_to_add_to_template = await self._find_images(db_template.template_type, template_settings) - if db_template.template_type == "dynamips" and "image" in template_settings: + if isinstance(db_template, models.DynamipsTemplate) and "image" in template_settings: await self._remove_image(db_template.template_id, db_template.image) - elif db_template.template_type == "iou" and "path" in template_settings: + elif isinstance(db_template, models.IOUTemplate) and "path" in template_settings: await self._remove_image(db_template.template_id, db_template.path) elif db_template.template_type == "qemu": for key in template_update.model_dump().keys(): diff --git a/gns3server/utils/asyncio/aiozipstream.py b/gns3server/utils/asyncio/aiozipstream.py index a6e1fc49b..1efafc37e 100644 --- a/gns3server/utils/asyncio/aiozipstream.py +++ b/gns3server/utils/asyncio/aiozipstream.py @@ -32,16 +32,14 @@ import asyncio import aiofiles from concurrent import futures -from zipfile import ( - structCentralDir, - structEndArchive64, - structEndArchive, - structEndArchive64Locator, - stringCentralDir, - stringEndArchive64, - stringEndArchive, - stringEndArchive64Locator, -) +structCentralDir = "<4s4B4HL2L5H2L" +structEndArchive64 = "<4sQ2H2L4Q" +structEndArchive = b"<4s4H2LH" +structEndArchive64Locator = "<4sLQL" +stringCentralDir = b"PK\x01\x02" +stringEndArchive64 = b"PK\x06\x06" +stringEndArchive = b"PK\x05\x06" +stringEndArchive64Locator = b"PK\x06\x07" ZIP_ZSTANDARD = 93 # zstandard is supported by WinZIP v24 and later, PowerArchiver 2021 and 7-Zip-zstd diff --git a/gns3server/utils/encryption.py b/gns3server/utils/encryption.py index 1f95f497b..38719a60e 100644 --- a/gns3server/utils/encryption.py +++ b/gns3server/utils/encryption.py @@ -48,7 +48,7 @@ def init_encryption(secrets_dir: str) -> None: os.makedirs(secrets_dir, exist_ok=True) with open(encryption_key_path, "w", encoding="utf-8") as f: # Use Fernet's base64-encoded key format - f.write(key.decode() if isinstance(key, bytes) else key) + f.write(key.decode()) # Set restrictive permissions (owner read/write only) os.chmod(encryption_key_path, 0o600) except OSError as e: @@ -58,7 +58,7 @@ def init_encryption(secrets_dir: str) -> None: try: with open(encryption_key_path, encoding="utf-8") as f: key_content = f.read().strip() - key_bytes = key_content.encode() if isinstance(key_content, str) else key_content + key_bytes = key_content.encode() _fernet = Fernet(key_bytes) log.debug("Encryption initialized successfully") except OSError as e: diff --git a/gns3server/utils/http_client.py b/gns3server/utils/http_client.py index 505073cf5..a6b032a82 100644 --- a/gns3server/utils/http_client.py +++ b/gns3server/utils/http_client.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import aiohttp +from pydantic import SecretStr import socket import logging @@ -28,7 +29,7 @@ class HTTPClient: HTTP client for request to computes and external services. """ - _aiohttp_client: aiohttp.ClientSession = None + _aiohttp_client: aiohttp.ClientSession | None = None @classmethod def get_client(cls, ssl_context=None) -> aiohttp.ClientSession: @@ -44,15 +45,21 @@ class HTTPClient: cls._aiohttp_client = None @classmethod - def request(cls, method: str, url: str, user: str = None, password: str = None, ssl_context=None, **kwargs): + def request( + cls, + method: str, + url: str, + user: str | None = None, + password: SecretStr | None = None, + ssl_context=None, + **kwargs, + ): client = cls.get_client(ssl_context=ssl_context) basic_auth = None if user: - if not password: - password = "" try: - basic_auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8") + basic_auth = aiohttp.BasicAuth(user, password.get_secret_value() if password else "", "utf-8") except ValueError as e: log.error(f"Basic authentication set-up error: {e}") diff --git a/gns3server/utils/images.py b/gns3server/utils/images.py index 58acc8b1e..fa4be5cb7 100644 --- a/gns3server/utils/images.py +++ b/gns3server/utils/images.py @@ -171,7 +171,7 @@ async def read_image_info(path: str, expected_image_type: str = None, allow_raw_ raise InvalidImageError(f"Cannot read image '{path}': {e}") from e -async def discover_images(image_type: str, skip_image_paths: list = None) -> List[dict]: +async def discover_images(image_type: str, skip_image_paths: list | None = None) -> List[dict]: """ Scan directories for available images """ diff --git a/gns3server/utils/packet_filter_validation.py b/gns3server/utils/packet_filter_validation.py index cc2634546..1b9481ffd 100644 --- a/gns3server/utils/packet_filter_validation.py +++ b/gns3server/utils/packet_filter_validation.py @@ -15,7 +15,7 @@ class FilterValidationError(Exception): pass -def validate_bpf_syntax(bpf_expression: str) -> Dict[str, Optional[str]]: +def validate_bpf_syntax(bpf_expression: str) -> Dict[str, Any]: """ Validate BPF filter expression syntax using tcpdump. @@ -78,7 +78,7 @@ def validate_filter_parameters(filter_type: str, values: List[Any]) -> None: """ # Define validation rules based on ubridge implementation - VALIDATION_RULES = { + VALIDATION_RULES: Dict[str, Dict[str, Any]] = { "frequency_drop": { "params_count": 1, "ranges": [(-1, 32767)], # min, max @@ -173,11 +173,11 @@ def filter_inactive_filters(filters: Dict[str, List[Any]]) -> Dict[str, List[Any active_filters = {} for filter_type, values in filters.items(): - if not values or (isinstance(values, list) and len(values) == 0): + if not values: continue # Normalize values (strip strings, convert to int) - normalized_values = [] + normalized_values: List[Any] = [] for value in values: if isinstance(value, str): normalized_values.append(value.strip("\n ")) @@ -228,7 +228,7 @@ def validate_all_filters(filters: Dict[str, List[Any]]) -> None: return for filter_type, values in filters.items(): - if not values or (isinstance(values, list) and len(values) == 0): + if not values: continue validate_filter_parameters(filter_type, values) diff --git a/gns3server/utils/websocket_to_websocket.py b/gns3server/utils/websocket_to_websocket.py index 88efdbfd2..12e410ce2 100644 --- a/gns3server/utils/websocket_to_websocket.py +++ b/gns3server/utils/websocket_to_websocket.py @@ -25,7 +25,6 @@ Similar pattern to VNC console implementation in base_node.py: import asyncio import logging -import sys from typing import Optional import aiohttp @@ -39,7 +38,7 @@ log = logging.getLogger(__name__) async def websocket_proxy( client_ws: WebSocket, target_url: str, - requested_protocols: list = None, + requested_protocols: Optional[list] = None, buffer_size: int = 65536, timeout: Optional[float] = None, ) -> None: @@ -59,7 +58,7 @@ async def websocket_proxy( Raises: aiohttp.ClientError: If connection to target fails """ - client_info = f"{client_ws.client.host}:{client_ws.client.port}" + client_info = f"{client_ws.client.host}:{client_ws.client.port}" if client_ws.client else "unknown" async def forward_client_to_target(target_ws): """Client → Target: Forward binary WebSocket data.""" @@ -101,25 +100,20 @@ async def websocket_proxy( subprotocols = requested_protocols or ["binary"] log.info(f"Client requested subprotocols: {subprotocols}") - timeout_config = {} - if timeout: - timeout_config = {"timeout": aiohttp.ClientTimeout(total=timeout)} + session_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None - async with aiohttp.ClientSession() as session: - async with session.ws_connect(target_url, protocols=subprotocols, **timeout_config) as target_ws: + async with aiohttp.ClientSession(timeout=session_timeout) as session: + async with session.ws_connect(target_url, protocols=subprotocols) as target_ws: negotiated_protocol = target_ws.protocol log.info(f"Target WebSocket negotiated protocol: {negotiated_protocol}") log.info(f"WebSocket proxy established: {client_info} → {target_url}") # Run both forwarding tasks in parallel # Similar pattern to base_node.py VNC implementation - if sys.version_info >= (3, 11, 0): - aws = [ - asyncio.create_task(forward_client_to_target(target_ws)), - asyncio.create_task(forward_target_to_client(target_ws)), - ] - else: - aws = [forward_client_to_target(target_ws), forward_target_to_client(target_ws)] + aws = [ + asyncio.create_task(forward_client_to_target(target_ws)), + asyncio.create_task(forward_target_to_client(target_ws)), + ] try: done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED) @@ -146,7 +140,7 @@ async def websocket_proxy( async def websocket_proxy_with_manual_accept( client_ws: StarletteWebSocket, target_url: str, - requested_protocols: list = None, + requested_protocols: Optional[list] = None, buffer_size: int = 65536, timeout: Optional[float] = None, ) -> None: @@ -167,7 +161,7 @@ async def websocket_proxy_with_manual_accept( Raises: aiohttp.ClientError: If connection to target fails """ - client_info = f"{client_ws.client.host}:{client_ws.client.port}" if hasattr(client_ws, "client") else "unknown" + client_info = f"{client_ws.client.host}:{client_ws.client.port}" if client_ws.client else "unknown" async def forward_client_to_target(target_ws): """Client → Target: Forward binary WebSocket data.""" @@ -213,13 +207,11 @@ async def websocket_proxy_with_manual_accept( subprotocols = requested_protocols or ["binary"] log.info(f"Client requested subprotocols: {subprotocols}") - timeout_config = {} - if timeout: - timeout_config = {"timeout": aiohttp.ClientTimeout(total=timeout)} + session_timeout = aiohttp.ClientTimeout(total=timeout) if timeout else None - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=session_timeout) as session: log.info(f"About to ws_connect to {target_url}") - ws_conn = session.ws_connect(target_url, protocols=subprotocols, **timeout_config) + ws_conn = session.ws_connect(target_url, protocols=subprotocols) log.info("ws_connect coroutine created, about to enter") async with ws_conn as target_ws: negotiated_protocol = target_ws.protocol @@ -235,13 +227,10 @@ async def websocket_proxy_with_manual_accept( log.info(f"WebSocket proxy established: {client_info} → {target_url}") # Run both forwarding tasks in parallel - if sys.version_info >= (3, 11, 0): - aws = [ - asyncio.create_task(forward_client_to_target(target_ws)), - asyncio.create_task(forward_target_to_client(target_ws)), - ] - else: - aws = [forward_client_to_target(target_ws), forward_target_to_client(target_ws)] + aws = [ + asyncio.create_task(forward_client_to_target(target_ws)), + asyncio.create_task(forward_target_to_client(target_ws)), + ] log.info(f"About to call asyncio.wait with {len(aws)} tasks") try: diff --git a/gns3server/utils/zipfile_zstd/_patcher.py b/gns3server/utils/zipfile_zstd/_patcher.py index ee2d19812..fc97d3645 100644 --- a/gns3server/utils/zipfile_zstd/_patcher.py +++ b/gns3server/utils/zipfile_zstd/_patcher.py @@ -1,8 +1,9 @@ import functools +from typing import Any class patch: - originals = {} + originals: dict[str, Any] = {} def __init__(self, host, name): self.host = host diff --git a/pyproject.toml b/pyproject.toml index c5f1ed6ff..f979b5eb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -290,124 +290,3 @@ warn_unused_ignores = true strict_equality = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] -[[tool.mypy.overrides]] -# Baseline: modules with existing type errors (error count at baseline time). -# Remove modules from this list one small PR at a time. Never add new ones. -module = [ - "gns3server.agent", # 2 - "gns3server.agent.gns3_copilot.agent.context_manager", # 5 - "gns3server.agent.gns3_copilot.agent.model_factory", # 4 - "gns3server.agent.gns3_copilot.chat_sessions_repository", # 8 - "gns3server.agent.gns3_copilot.gns3_client.api_handlers", # 10 - "gns3server.agent.gns3_copilot.gns3_client.connector_factory", # 1 - "gns3server.agent.gns3_copilot.gns3_client.project_inventory", # 2 - "gns3server.agent.gns3_copilot.skills.loader", # 1 - "gns3server.agent.gns3_copilot.skills.manager", # 4 - "gns3server.agent.gns3_copilot.tools_v2.config_tools_nornir", # 6 - "gns3server.agent.gns3_copilot.tools_v2.display_tools_nornir", # 6 - "gns3server.agent.gns3_copilot.tools_v2.gns3_packet_filter", # 1 - "gns3server.agent.gns3_copilot.tools_v2.gns3_start_node", # 3 - "gns3server.agent.gns3_copilot.tools_v2.gns3_stop_node", # 3 - "gns3server.agent.gns3_copilot.tools_v2.gns3_suspend_node", # 3 - "gns3server.agent.gns3_copilot.tools_v2.packet_analysis_tool", # 1 - "gns3server.agent.gns3_copilot.tools_v2.vpcs_tools_netmiko", # 9 - "gns3server.agent.gns3_copilot.utils.custom_netmiko.scripts.list_netmiko_telnet_devices", # 1 - "gns3server.agent.gns3_copilot.utils.message_converters", # 2 - "gns3server.agent.gns3_copilot.utils.parse_tool_content", # 1 - "gns3server.agent.mcp.appliances", # 1 - "gns3server.agent.web_wireshark.docker_client", # 5 - "gns3server.agent.web_wireshark.manage_wireshark", # 1 - "gns3server.agent.web_wireshark.manager", # 9 - "gns3server.agent.web_wireshark.stats", # 4 - "gns3server.api.routes.compute.atm_switch_nodes", # 9 - "gns3server.api.routes.compute.capabilities", # 1 - "gns3server.api.routes.compute.cloud_nodes", # 14 - "gns3server.api.routes.compute.compute", # 1 - "gns3server.api.routes.compute.dependencies.authentication", # 3 - "gns3server.api.routes.compute.docker_nodes", # 26 - "gns3server.api.routes.compute.dynamips_nodes", # 9 - "gns3server.api.routes.compute.ethernet_hub_nodes", # 9 - "gns3server.api.routes.compute.ethernet_switch_nodes", # 16 - "gns3server.api.routes.compute.frame_relay_switch_nodes", # 9 - "gns3server.api.routes.compute.iou_nodes", # 15 - "gns3server.api.routes.compute.nat_nodes", # 10 - "gns3server.api.routes.compute.notifications", # 4 - "gns3server.api.routes.compute.projects", # 7 - "gns3server.api.routes.compute.qemu_nodes", # 18 - "gns3server.api.routes.compute.virtualbox_nodes", # 20 - "gns3server.api.routes.compute.vmware_nodes", # 10 - "gns3server.api.routes.compute.vpcs_nodes", # 9 - "gns3server.api.routes.controller.acl", # 5 - "gns3server.api.routes.controller.api_keys", # 1 - "gns3server.api.routes.controller.chat", # 1 - "gns3server.api.routes.controller.computes", # 6 - "gns3server.api.routes.controller.controller", # 10 - "gns3server.api.routes.controller.dependencies.authentication", # 11 - "gns3server.api.routes.controller.dependencies.database", # 2 - "gns3server.api.routes.controller.drawings", # 1 - "gns3server.api.routes.controller.groups", # 6 - "gns3server.api.routes.controller.images", # 9 - "gns3server.api.routes.controller.links", # 27 - "gns3server.api.routes.controller.llm_model_configs", # 32 - "gns3server.api.routes.controller.nodes", # 13 - "gns3server.api.routes.controller.pools", # 8 - "gns3server.api.routes.controller.privileges", # 1 - "gns3server.api.routes.controller.projects", # 16 - "gns3server.api.routes.controller.roles", # 5 - "gns3server.api.routes.controller.snapshots", # 1 - "gns3server.api.routes.controller.templates", # 10 - "gns3server.api.routes.controller.users", # 14 - "gns3server.api.server", # 2 - "gns3server.compute.dynamips.nodes.router", # 2 - "gns3server.compute.iou.iou_vm", # 4 - "gns3server.compute.iou.utils.iou_export", # 2 - "gns3server.compute.iou.utils.iou_import", # 2 - "gns3server.compute.virtualbox.virtualbox_vm", # 1 - "gns3server.compute.vpcs.vpcs_vm", # 2 - "gns3server.controller.appliance", # 2 - "gns3server.controller.appliance_manager", # 5 - "gns3server.controller.project", # 2 - "gns3server.controller.topology", # 1 - "gns3server.db.repositories.api_keys", # 8 - "gns3server.db.repositories.computes", # 10 - "gns3server.db.repositories.images", # 14 - "gns3server.db.repositories.llm_model_configs", # 32 - "gns3server.db.repositories.pools", # 11 - "gns3server.db.repositories.rbac", # 14 - "gns3server.db.repositories.templates", # 6 - "gns3server.db.repositories.users", # 12 - "gns3server.db.tasks", # 12 - "gns3server.schemas.compute.atm_switch_nodes", # 5 - "gns3server.schemas.compute.cloud_nodes", # 1 - "gns3server.schemas.compute.docker_nodes", # 2 - "gns3server.schemas.compute.dynamips_nodes", # 1 - "gns3server.schemas.compute.frame_relay_switch_nodes", # 5 - "gns3server.schemas.compute.iou_nodes", # 3 - "gns3server.schemas.compute.nat_nodes", # 1 - "gns3server.schemas.compute.qemu_nodes", # 1 - "gns3server.schemas.compute.virtualbox_nodes", # 2 - "gns3server.schemas.compute.vmware_nodes", # 3 - "gns3server.schemas.compute.vpcs_nodes", # 1 - "gns3server.schemas.controller.appliances", # 1 - "gns3server.schemas.controller.computes", # 6 - "gns3server.schemas.controller.nodes", # 3 - "gns3server.schemas.controller.projects", # 2 - "gns3server.schemas.controller.settings", # 3 - "gns3server.schemas.controller.templates.docker_templates", # 1 - "gns3server.schemas.controller.templates.dynamips_templates", # 17 - "gns3server.schemas.controller.templates.iou_templates", # 1 - "gns3server.schemas.controller.templates.virtualbox_templates", # 1 - "gns3server.schemas.controller.templates.vmware_templates", # 1 - "gns3server.schemas.qemu_disk_image", # 2 - "gns3server.services.authentication", # 5 - "gns3server.services.computes", # 4 - "gns3server.services.templates", # 18 - "gns3server.utils.asyncio.aiozipstream", # 8 - "gns3server.utils.encryption", # 2 - "gns3server.utils.http_client", # 4 - "gns3server.utils.images", # 4 - "gns3server.utils.packet_filter_validation", # 16 - "gns3server.utils.websocket_to_websocket", # 12 - "gns3server.utils.zipfile_zstd._patcher", # 1 -] -ignore_errors = true