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