Merge branch '3.1' into mypy-schemas

This commit is contained in:
Jeremy Grossmann 2026-09-29 13:40:00 +02:00 committed by GitHub
commit eace280c5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 460 additions and 470 deletions

View File

@ -57,7 +57,7 @@ except ImportError as e:
AI_COPILOT_AVAILABLE = False AI_COPILOT_AVAILABLE = False
# Provide stub functions that raise helpful errors # 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. 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]" "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. Stub class for ProjectAgentManager when AI dependencies are not installed.
""" """

View File

@ -140,7 +140,7 @@ def estimate_tool_tokens(tools: list[Any]) -> int:
for tool in tools: for tool in tools:
try: try:
# Build tool schema in OpenAI format # Build tool schema in OpenAI format
tool_schema = { tool_schema: dict[str, Any] = {
"type": "function", "type": "function",
"function": { "function": {
"name": tool.name, "name": tool.name,

View File

@ -110,7 +110,7 @@ def create_base_model(
try: try:
# Prepare parameters for init_chat_model # Prepare parameters for init_chat_model
init_params = { init_params: dict[str, Any] = {
"model": config_vars["model_name"], "model": config_vars["model_name"],
"model_provider": config_vars["model_provider"], "model_provider": config_vars["model_provider"],
"api_key": config_vars["api_key"], "api_key": config_vars["api_key"],
@ -177,7 +177,7 @@ def create_title_model(
try: try:
# Prepare parameters for init_chat_model # Prepare parameters for init_chat_model
init_params = { init_params: dict[str, Any] = {
"model": config_vars["model_name"], "model": config_vars["model_name"],
"model_provider": config_vars["model_provider"], "model_provider": config_vars["model_provider"],
"api_key": config_vars["api_key"], "api_key": config_vars["api_key"],

View File

@ -125,7 +125,7 @@ class ChatSessionsRepository:
project_id: str, project_id: str,
title: str = "New Conversation", title: str = "New Conversation",
copilot_mode: Optional[str] = None, copilot_mode: Optional[str] = None,
) -> ChatSession: ) -> Optional[ChatSession]:
""" """
Create a new chat session. Create a new chat session.
@ -158,6 +158,8 @@ class ChatSessionsRepository:
session_id = cursor.lastrowid session_id = cursor.lastrowid
log.info("Created chat session: id=%s, thread_id=%s, copilot_mode=%s", session_id, thread_id, copilot_mode) 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) return await self.get_session_by_id(session_id)
async def get_session_by_id(self, session_id: int) -> Optional[ChatSession]: async def get_session_by_id(self, session_id: int) -> Optional[ChatSession]:
@ -214,7 +216,7 @@ class ChatSessionsRepository:
List of ChatSession List of ChatSession
""" """
query = "SELECT * FROM chat_sessions" query = "SELECT * FROM chat_sessions"
params = [] params: List[Any] = []
conditions = [] conditions = []
if user_id: if user_id:
@ -268,7 +270,7 @@ class ChatSessionsRepository:
Updated ChatSession or None Updated ChatSession or None
""" """
updates = [] updates = []
params = [] params: List[Any] = []
now = datetime.utcnow().isoformat() now = datetime.utcnow().isoformat()

View File

@ -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.""" """Filter node response to only include requested fields."""
if not fields: if not fields:
fields = ["node_id", "name", "node_type", "status", "console"] fields = ["node_id", "name", "node_type", "status", "console"]
return {k: node[k] for k in fields if k in node} 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.""" """Filter link response to only include requested fields."""
if not fields: if not fields:
fields = LINK_DEFAULT_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)) 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") project_id = params.get("project_id")
if not project_id: if not project_id:
return {"error": "project_id is required"} 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} 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") project_id = params.get("project_id")
if not project_id: if not project_id:
return {"error": "project_id is required"} 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} 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") project_id = params.get("project_id")
if not project_id: if not project_id:
return {"error": "project_id is required"} 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} 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") project_id = params.get("project_id")
if not 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) 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") project_id = params.get("project_id")
if not project_id: if not project_id:
return {"error": "project_id is required"} 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() 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") project_id = params.get("project_id")
if not project_id: if not project_id:
return {"error": "project_id is required"} 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) 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") project_id = params.get("project_id")
if not project_id: if not project_id:
return {"error": "project_id is required"} 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 ────────────────────────────────────── # ── 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") project_id = params.get("project_id")
if not project_id: if not project_id:
return {"error": "project_id is required"} return {"error": "project_id is required"}

View File

@ -347,7 +347,7 @@ def get_gns3_server_host() -> str:
host = get_gns3_server_host() host = get_gns3_server_host()
print(f"GNS3 server host: {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 # Extract host from URL
# URL format: protocol://host:port # URL format: protocol://host:port

View File

@ -49,7 +49,7 @@ def build_nodes_inventory(nodes: list[dict[str, Any]], server_host: str | None)
""" """
inventory: dict[str, Any] = {} inventory: dict[str, Any] = {}
for n in nodes: for n in nodes:
inventory[n.get("name")] = { inventory[n.get("name") or ""] = {
"server": server_host, "server": server_host,
"name": n.get("name"), "name": n.get("name"),
"node_id": n.get("node_id"), "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 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. 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 Links whose endpoints cannot be resolved are skipped, mirroring the
original Project.links_summary behavior. original Project.links_summary behavior.
""" """
summary: list[dict[str, str]] = [] summary: list[dict[str, Any]] = []
for link in links: for link in links:
if not link.get("nodes"): if not link.get("nodes"):
continue continue

View File

@ -248,7 +248,7 @@ class SkillsLoader:
continue continue
# Use device_type from YAML content as the key # Use device_type from YAML content as the key
# Fallback to filename stem if device_type not present # 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: if not skill_key:
skill_key = yaml_file.stem skill_key = yaml_file.stem
logger.warning(f"No device_type in {yaml_file}, using filename '{skill_key}' as key") logger.warning(f"No device_type in {yaml_file}, using filename '{skill_key}' as key")

View File

@ -37,8 +37,10 @@ from typing import Optional, Dict, Any
try: try:
import git import git
GIT_AVAILABLE = True
except ImportError: except ImportError:
git = None GIT_AVAILABLE = False
from gns3server.config import Config from gns3server.config import Config
from .loader import SkillsLoader from .loader import SkillsLoader
@ -66,7 +68,7 @@ class SkillsManager:
- Version tracking - 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. Initialize the skills manager.
@ -90,7 +92,7 @@ class SkillsManager:
self._repo: Optional["git.Repo"] = None self._repo: Optional["git.Repo"] = None
self._prompt_count = 0 self._prompt_count = 0
if git is None: if not GIT_AVAILABLE:
logger.warning("GitPython is not installed. Skills management features will be limited.") logger.warning("GitPython is not installed. Skills management features will be limited.")
def initialize(self) -> bool: def initialize(self) -> bool:
@ -134,7 +136,7 @@ class SkillsManager:
- Up to date → nothing - Up to date → nothing
- Network error → use existing files, log warning - 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 return
# Check for uncommitted changes # Check for uncommitted changes
@ -173,7 +175,7 @@ class SkillsManager:
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
if git is None: if not GIT_AVAILABLE:
logger.error("GitPython is not installed. Cannot clone repository.") logger.error("GitPython is not installed. Cannot clone repository.")
return False return False
@ -347,7 +349,7 @@ class SkillsManager:
Returns: Returns:
Commit hash as string, or empty string if not available 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: try:
self._repo = git.Repo(self.local_path) self._repo = git.Repo(self.local_path)
except Exception: except Exception:
@ -409,7 +411,7 @@ class SkillsManager:
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
if git is None: if not GIT_AVAILABLE:
logger.error("GitPython is not installed. Cannot rollback.") logger.error("GitPython is not installed. Cannot rollback.")
return False return False
@ -436,7 +438,7 @@ class SkillsManager:
Returns: Returns:
List of commit information dictionaries List of commit information dictionaries
""" """
if git is None: if not GIT_AVAILABLE:
return [] return []
try: try:

View File

@ -78,7 +78,7 @@ try:
# so we use importlib to get the actual module # so we use importlib to get the actual module
import importlib 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 # Recalculate platforms lists to include custom device types
sd.platforms = list(sd.CLASS_MAPPER.keys()) sd.platforms = list(sd.CLASS_MAPPER.keys())
@ -196,7 +196,7 @@ class ExecuteMultipleDeviceConfigCommands(BaseTool):
# Validate input # Validate input
device_configs_list, project_id = self._validate_tool_input(tool_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 return device_configs_list
# Filter forbidden commands and store blocked commands info # Filter forbidden commands and store blocked commands info

View File

@ -78,7 +78,7 @@ try:
# so we use importlib to get the actual module # so we use importlib to get the actual module
import importlib 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 # Recalculate platforms lists to include custom device types
sd.platforms = list(sd.CLASS_MAPPER.keys()) sd.platforms = list(sd.CLASS_MAPPER.keys())
@ -199,7 +199,7 @@ class ExecuteMultipleDeviceCommands(BaseTool):
# Validate input # Validate input
device_configs_list, project_id = self._validate_tool_input(tool_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 return device_configs_list
# Filter forbidden commands and store blocked commands info # Filter forbidden commands and store blocked commands info

View File

@ -222,6 +222,8 @@ class GNS3LinkTool(BaseTool):
}, },
gns3_ctx, gns3_ctx,
) )
if isinstance(link_resp, list):
raise RuntimeError("Unexpected batch response when creating a single link")
if "error" in link_resp: if "error" in link_resp:
raise RuntimeError(link_resp["error"]) raise RuntimeError(link_resp["error"])

View File

@ -230,6 +230,8 @@ class GNS3CreateNodeTool(BaseTool):
}, },
gns3_ctx, gns3_ctx,
) )
if isinstance(created, list):
raise RuntimeError("Unexpected batch response when creating a single node")
if "error" in created: if "error" in created:
raise RuntimeError(created["error"]) raise RuntimeError(created["error"])
node_info = { node_info = {

View File

@ -286,7 +286,7 @@ class GNS3PacketFilterTool(BaseTool):
# Check if output contains "Invalid" indicating syntax error # Check if output contains "Invalid" indicating syntax error
if "Invalid" in result.stdout or "Invalid" in result.stderr: if "Invalid" in result.stdout or "Invalid" in result.stderr:
error_lines = [] error_lines: list[str] = []
if "Invalid" in result.stderr: if "Invalid" in result.stderr:
error_lines.extend(line for line in result.stderr.split("\n") if "Invalid" in line) error_lines.extend(line for line in result.stderr.split("\n") if "Invalid" in line)
if "Invalid" in result.stdout: if "Invalid" in result.stdout:

View File

@ -136,7 +136,11 @@ class GNS3StartNodeTool(BaseTool):
results = [] results = []
known_ids = [nid for nid in node_ids if nid in nodes_by_id] 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_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 # Get immediate status (likely 'starting' or 'stopped') — one call
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)

View File

@ -124,7 +124,11 @@ class GNS3StopNodeTool(BaseTool):
results = [] results = []
known_ids = [nid for nid in node_ids if nid in nodes_by_id] 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_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 # Get updated status — one call
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)

View File

@ -130,7 +130,11 @@ class GNS3SuspendNodeTool(BaseTool):
results = [] results = []
known_ids = [nid for nid in node_ids if nid in nodes_by_id] 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_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 # Get updated status — one call
listing = get_nodes_handler({"project_id": project_id}, gns3_ctx) listing = get_nodes_handler({"project_id": project_id}, gns3_ctx)

View File

@ -143,7 +143,8 @@ class PacketAnalysisTool(BaseTool):
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
text=True, text=True,
) )
tshark.stdout.close() if tshark.stdout is not None:
tshark.stdout.close()
stdout, _ = grep.communicate(timeout=30) stdout, _ = grep.communicate(timeout=30)
tshark.wait(timeout=5) tshark.wait(timeout=5)
except Exception as e: except Exception as e:

View File

@ -68,7 +68,7 @@ try:
# CRITICAL: Update netmiko.ssh_dispatcher platforms lists # CRITICAL: Update netmiko.ssh_dispatcher platforms lists
import importlib 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 # Recalculate platforms lists to include custom device types
sd.platforms = list(sd.CLASS_MAPPER.keys()) sd.platforms = list(sd.CLASS_MAPPER.keys())
@ -179,7 +179,7 @@ class VPCSCommands(BaseTool):
# Validate input # Validate input
device_configs_list, project_id = self._validate_tool_input(tool_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 return device_configs_list
# Create a mapping of device names to their commands # Create a mapping of device names to their commands
@ -386,7 +386,7 @@ class VPCSCommands(BaseTool):
def _prepare_device_hosts_data( def _prepare_device_hosts_data(
self, self,
device_configs_list: list[dict[str, Any]], device_configs_list: list[dict[str, Any]],
project_id: str, project_id: str | None,
jwt_token: str | None = None, jwt_token: str | None = None,
url: str | None = None, url: str | None = None,
) -> dict[str, dict[str, Any]]: ) -> 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) device_ports = get_device_ports_from_topology(device_names, project_id=project_id, jwt_token=jwt_token, url=url)
# Build Nornir inventory hosts data # Build Nornir inventory hosts data
hosts_data = {} hosts_data: dict[str, Any] = {}
for device_name in device_names: for device_name in device_names:
if device_name not in device_ports: if device_name not in device_ports:
logger.error("Device '%s' not found in topology", device_name) logger.error("Device '%s' not found in topology", device_name)

View File

@ -169,7 +169,7 @@ def group_devices_by_brand(device_types: List[str]) -> Dict[str, List[str]]:
Returns: Returns:
Dictionary mapping brand names to device type lists Dictionary mapping brand names to device type lists
""" """
brands = {} brands: dict[str, list[str]] = {}
for device_type in device_types: for device_type in device_types:
brand = extract_brand_name(device_type) brand = extract_brand_name(device_type)

View File

@ -37,6 +37,7 @@ from typing import Dict
from langchain_core.messages import AIMessage from langchain_core.messages import AIMessage
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from langchain_core.messages import SystemMessage from langchain_core.messages import SystemMessage
from langchain_core.messages import ToolCall
from langchain_core.messages import ToolMessage from langchain_core.messages import ToolMessage
@ -86,7 +87,7 @@ def convert_langchain_to_openai(lc_message) -> Dict[str, Any]:
tool_calls = [] tool_calls = []
for tc in lc_message.tool_calls: for tc in lc_message.tool_calls:
# Convert to dict if it's an object # 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( tool_calls.append(
{ {
"id": tc_dict.get("id", str(uuid.uuid4())), "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 # Restore tool calls if present
if "tool_calls" in msg and msg["tool_calls"]: if "tool_calls" in msg and msg["tool_calls"]:
tool_calls = [] tool_calls: list[ToolCall] = []
for tc in msg["tool_calls"]: for tc in msg["tool_calls"]:
tool_calls.append( tool_calls.append(
{ {

View File

@ -214,9 +214,7 @@ def parse_tool_content(
return content return content
# Handle unsupported types # Handle unsupported types
error_msg = ( # type: ignore[unreachable] error_msg = f"Content must be str, dict, list, int, float, bool, or None, got {type(content).__name__}"
f"Content must be str, dict, list, int, float, bool, or None, got {type(content).__name__}"
)
logger.error(error_msg) logger.error(error_msg)
if strict_mode: if strict_mode:

View File

@ -108,7 +108,7 @@ def install_appliance_handler(params: dict[str, Any], gns3_ctx: dict[str, Any])
if version: if version:
request_params["version"] = version request_params["version"] = version
response = conn.http_call("post", url, params=request_params) 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: if response.content:
# the install endpoint returns the created template (201); tolerate an # the install endpoint returns the created template (201); tolerate an
# empty body in case an older server still replies with 204 # empty body in case an older server still replies with 204

View File

@ -23,9 +23,16 @@ This module handles Docker API operations via aiohttp Unix socket.
import asyncio import asyncio
import logging import logging
import sys
from typing import Any
import aiohttp 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 from gns3server.utils import parse_version
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -132,7 +139,7 @@ class DockerHTTPClient:
url = f"http://docker/v{self._api_version}/{endpoint}" url = f"http://docker/v{self._api_version}/{endpoint}"
try: try:
async with asyncio.timeout(self.REQUEST_TIMEOUT): async with async_timeout(self.REQUEST_TIMEOUT):
async with session.request(method, url, **kwargs) as response: async with session.request(method, url, **kwargs) as response:
if response.status >= 300: if response.status >= 300:
error_text = await response.text() error_text = await response.text()
@ -157,9 +164,9 @@ class DockerHTTPClient:
return await self._request(method, endpoint, check_connection=False, **kwargs) return await self._request(method, endpoint, check_connection=False, **kwargs)
raise 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.""" """Create Docker network."""
data = {"Name": name, "Driver": driver} data: dict[str, Any] = {"Name": name, "Driver": driver}
if subnet: if subnet:
data["IPAM"] = {"Config": [{"Subnet": subnet}]} data["IPAM"] = {"Config": [{"Subnet": subnet}]}
await self._request("POST", "networks/create", json=data) await self._request("POST", "networks/create", json=data)
@ -175,7 +182,7 @@ class DockerHTTPClient:
async def create_container(self, name: str, image: str, **kwargs): async def create_container(self, name: str, image: str, **kwargs):
"""Create container.""" """Create container."""
data = {"Image": image, "name": name, "HostConfig": {}, "NetworkingConfig": {}} data: dict[str, Any] = {"Image": image, "name": name, "HostConfig": {}, "NetworkingConfig": {}}
# Handle network config # Handle network config
if "network" in kwargs: 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" url = f"http://docker/v{self._api_version}/containers/{container_name}/top?ps_args=aux"
try: try:
async with asyncio.timeout(self.REQUEST_TIMEOUT): async with async_timeout(self.REQUEST_TIMEOUT):
async with session.get(url) as response: async with session.get(url) as response:
if response.status >= 300: if response.status >= 300:
error_text = await response.text() error_text = await response.text()

View File

@ -81,10 +81,7 @@ from gns3server.utils.uuid_validator import validate_uuid
if __name__ == "__main__": if __name__ == "__main__":
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) 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
from gns3server.agent.web_wireshark.manager import WebWiresharkManager
except ImportError:
from manager import WebWiresharkManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -84,7 +84,7 @@ class WebWiresharkManager:
logger.warning(f"Container {container_id[:12]} health check failed: {e}") logger.warning(f"Container {container_id[:12]} health check failed: {e}")
return False 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. """Execute command in container with timeout.
Args: Args:
@ -197,7 +197,7 @@ class WebWiresharkManager:
stdout, _ = await proc.communicate() stdout, _ = await proc.communicate()
# Build parent->children mapping # Build parent->children mapping
children_map = {} children_map: dict[int, list[int]] = {}
for line in stdout.decode().strip().split("\n"): for line in stdout.decode().strip().split("\n"):
parts = line.split(None, 2) parts = line.split(None, 2)
if len(parts) < 3: if len(parts) < 3:
@ -326,7 +326,7 @@ class WebWiresharkManager:
return return
# Build parent->children mapping and collect process info # 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) process_info = {} # pid -> (ppid, command)
for line in stdout.decode().strip().split("\n"): for line in stdout.decode().strip().split("\n"):
@ -480,7 +480,7 @@ class WebWiresharkManager:
logger.debug(f"Cannot get URL from Config: {e}") logger.debug(f"Cannot get URL from Config: {e}")
return None 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. """Get the Docker bridge gateway IP for container to access host.
Args: Args:
@ -519,7 +519,7 @@ class WebWiresharkManager:
return None 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. """Get the container IP address in the wireshark network.
Args: Args:
@ -626,7 +626,7 @@ class WebWiresharkManager:
project_id: str, project_id: str,
image: str = "gns3/web-wireshark:latest", image: str = "gns3/web-wireshark:latest",
memory: str = "2g", memory: str = "2g",
memory_swap: str = None, memory_swap: Optional[str] = None,
cpus: float = 1.0, cpus: float = 1.0,
pids_limit: int = 1000, pids_limit: int = 1000,
) -> str: ) -> str:
@ -935,10 +935,10 @@ class WebWiresharkManager:
project_id: str, project_id: str,
link_id: str, link_id: str,
jwt_token: str, jwt_token: str,
capture_stream_url: str = None, capture_stream_url: Optional[str] = None,
image: str = "gns3/web-wireshark:latest", image: str = "gns3/web-wireshark:latest",
memory: str = "2g", memory: str = "2g",
memory_swap: str = None, memory_swap: Optional[str] = None,
cpus: float = 1.0, cpus: float = 1.0,
pids_limit: int = 1000, pids_limit: int = 1000,
): ):

View File

@ -7,7 +7,7 @@ about Web Wireshark containers and sessions.
import logging import logging
import subprocess import subprocess
from typing import Dict, List, Optional from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -30,7 +30,7 @@ async def collect_webwireshark_stats(projects: List) -> Dict:
""" """
from .manager import WebWiresharkManager 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 # Create a single manager instance and reuse it
manager = WebWiresharkManager() manager = WebWiresharkManager()

View File

@ -18,6 +18,7 @@
API routes for ATM switch nodes. API routes for ATM switch nodes.
""" """
from typing import Any
import os import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException 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 import Dynamips
from gns3server.compute.dynamips.nodes.atm_switch import ATMSwitch 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) 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 # Use the Dynamips ATM switch to simulate this node
dynamips_manager = Dynamips.instance() 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 = await dynamips_manager.create_node(
node_data.get("name"), data.get("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
node_type="atm_switch", node_type="atm_switch",
mappings=node_data.get("mappings"), mappings=data.get("mappings"),
) )
return node.asdict() return node.asdict()
@ -97,11 +100,11 @@ async def update_atm_switch(
Update an ATM switch node. Update an ATM switch node.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]: if "name" in data and node.name != data["name"]:
await node.set_name(node_data["name"]) await node.set_name(data["name"])
if "mappings" in node_data: if "mappings" in data:
node.mappings = node_data["mappings"] node.mappings = data["mappings"]
node.updated() node.updated()
return node.asdict() return node.asdict()

View File

@ -39,7 +39,8 @@ def get_capabilities(request: Request) -> dict:
node_types.extend(module.node_types()) node_types.extend(module.node_types())
# record the controller hostname or IP address # 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 { return {
"version": __version__, "version": __version__,

View File

@ -23,14 +23,16 @@ import os
from fastapi import APIRouter, Depends, Path, status, HTTPException from fastapi import APIRouter, Depends, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import Union from typing import Any, Union
from uuid import UUID from uuid import UUID
from gns3server import schemas from gns3server import schemas
from gns3server.compute.builtin import Builtin from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.cloud import Cloud 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) router = APIRouter(responses=responses)
@ -57,21 +59,21 @@ async def create_cloud(project_id: UUID, node_data: schemas.CloudCreate) -> sche
""" """
builtin_manager = Builtin.instance() 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 = await builtin_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
node_type="cloud", node_type="cloud",
ports=node_data.get("ports_mapping"), ports=data.get("ports_mapping"),
) )
# add the remote console settings # add the remote console settings
node.remote_console_host = node_data.get("remote_console_host", node.remote_console_host) node.remote_console_host = 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_port = 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_type = 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.remote_console_http_path = data.get("remote_console_http_path", node.remote_console_http_path)
node.usage = node_data.get("usage", "") node.usage = data.get("usage", "")
return node.asdict() return node.asdict()
@ -90,8 +92,8 @@ async def update_cloud(node_data: schemas.CloudUpdate, node: Cloud = Depends(dep
Update a cloud node. Update a cloud node.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in node_data.items(): for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value: if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value) setattr(node, name, value)
node.updated() node.updated()
@ -178,7 +180,7 @@ async def update_cloud_nio(
nio = node.get_nio(port_number) nio = node.get_nio(port_number)
nio.filters.clear() nio.filters.clear()
if nio_data.filters: if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
nio.filters = nio_data.filters nio.filters = nio_data.filters
# NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers. # NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers.
nio.markers = getattr(nio_data, "markers", None) or {} nio.markers = getattr(nio_data, "markers", None) or {}

View File

@ -163,7 +163,7 @@ def compute_statistics() -> dict:
@router.get("/qemu/capabilities") @router.get("/qemu/capabilities")
async def get_qemu_capabilities() -> dict: async def get_qemu_capabilities() -> dict:
capabilities = {"kvm": []} capabilities: dict[str, list] = {"kvm": []}
kvms = await Qemu.get_kvm_archs() kvms = await Qemu.get_kvm_archs()
if kvms: if kvms:
capabilities["kvm"] = kvms capabilities["kvm"] = kvms

View File

@ -91,9 +91,9 @@ async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSock
if not separator: if not separator:
raise invalid_user_credentials_exc raise invalid_user_credentials_exc
username = secrets.compare_digest(username, server_settings.compute_username) username_valid = secrets.compare_digest(username, server_settings.compute_username)
password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value()) password_valid = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
if not (username and password): if not (username_valid and password_valid):
raise invalid_user_credentials_exc raise invalid_user_credentials_exc
except HTTPException as e: 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}} websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error) await websocket.send_json(websocket_error)
log.error(err_msg) log.error(err_msg)
return await websocket.close(code=1008) await websocket.close(code=1008)
return None
return websocket return websocket

View File

@ -24,14 +24,16 @@ from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
from typing import Union from typing import Any, Union
from gns3server import schemas from gns3server import schemas
from gns3server.compute.docker import Docker from gns3server.compute.docker import Docker
from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.docker.docker_vm import DockerVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication 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) 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"}}, responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
dependencies=[Depends(compute_authentication)], 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. Create a new Docker node.
""" """
docker_manager = Docker.instance() 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( container = await docker_manager.create_node(
node_data.pop("name"), node_data.pop("name"),
str(project_id), 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)]) @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. Update a Docker node.
""" """
@ -152,7 +154,7 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
] ]
changed = False changed = False
node_data = jsonable_encoder(node_data, exclude_unset=True) node_data = jsonable_encoder(node_update, exclude_unset=True)
for prop in props: for prop in props:
# hasattr: startup_config_content only exists on IOLDockerVM # hasattr: startup_config_content only exists on IOLDockerVM
if prop in node_data and hasattr(node, prop) and node_data[prop] != getattr(node, prop): if prop in node_data and hasattr(node, prop) and node_data[prop] != getattr(node, prop):

View File

@ -23,7 +23,7 @@ import os
from fastapi import APIRouter, WebSocket, Body, Depends, status, HTTPException from fastapi import APIRouter, WebSocket, Body, Depends, status, HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import List, Union from typing import Any, List, Optional, Union
from uuid import UUID from uuid import UUID
from gns3server.compute.dynamips import Dynamips from gns3server.compute.dynamips import Dynamips
@ -32,7 +32,9 @@ from gns3server import schemas
from .dependencies.authentication import compute_authentication, ws_compute_authentication 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) router = APIRouter(responses=responses)
@ -64,25 +66,26 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
dynamips_manager = Dynamips.instance() dynamips_manager = Dynamips.instance()
platform = node_data.platform platform = node_data.platform
chassis: Optional[str]
if not node_data.chassis and platform in DEFAULT_CHASSIS: if not node_data.chassis and platform in DEFAULT_CHASSIS:
chassis = DEFAULT_CHASSIS[platform] chassis = DEFAULT_CHASSIS[platform]
else: else:
chassis = node_data.chassis 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( vm = await dynamips_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
dynamips_id=node_data.get("dynamips_id"), dynamips_id=data.get("dynamips_id"),
platform=platform, platform=platform,
console=node_data.get("console"), console=data.get("console"),
console_type=node_data.get("console_type", "telnet"), console_type=data.get("console_type", "telnet"),
aux=node_data.get("aux"), aux=data.get("aux"),
aux_type=node_data.pop("aux_type", "none"), aux_type=data.pop("aux_type", "none"),
chassis=chassis, chassis=chassis,
node_type="dynamips", node_type="dynamips",
) )
await dynamips_manager.update_vm_settings(vm, node_data) await dynamips_manager.update_vm_settings(vm, data)
return vm.asdict() return vm.asdict()

View File

@ -18,6 +18,7 @@
API routes for Ethernet hub nodes. API routes for Ethernet hub nodes.
""" """
from typing import Any
import os import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException 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.compute.dynamips.nodes.ethernet_hub import EthernetHub
from gns3server import schemas 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) 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 # Use the Dynamips Ethernet hub to simulate this node
dynamips_manager = Dynamips.instance() 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 = await dynamips_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
node_type="ethernet_hub", node_type="ethernet_hub",
ports=node_data.get("ports_mapping"), ports=data.get("ports_mapping"),
) )
return node.asdict() return node.asdict()
@ -97,11 +100,11 @@ async def update_ethernet_hub(
Update an Ethernet hub. Update an Ethernet hub.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]: if "name" in data and node.name != data["name"]:
await node.set_name(node_data["name"]) await node.set_name(data["name"])
if "ports_mapping" in node_data: if "ports_mapping" in data:
node.ports_mapping = node_data["ports_mapping"] node.ports_mapping = data["ports_mapping"]
node.updated() node.updated()
return node.asdict() return node.asdict()

View File

@ -22,6 +22,7 @@ through uBridge's ``brctl`` module (see
``gns3server.compute.builtin.nodes.ethernet_switch``). ``gns3server.compute.builtin.nodes.ethernet_switch``).
""" """
from typing import Any
import os import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException 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.compute.builtin.nodes.ethernet_switch import EthernetSwitch
from gns3server import schemas 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) router = APIRouter(responses=responses)
@ -60,17 +63,17 @@ async def create_ethernet_switch(project_id: UUID, node_data: schemas.EthernetSw
""" """
builtin_manager = Builtin.instance() 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 = await builtin_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
console=node_data.get("console"), console=data.get("console"),
console_type=node_data.get("console_type"), console_type=data.get("console_type"),
node_type="ethernet_switch", 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() return node.asdict()
@ -100,16 +103,16 @@ async def update_ethernet_switch(
Update an Ethernet switch. Update an Ethernet switch.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]: if "name" in data and node.name != data["name"]:
node.name = node_data["name"] node.name = data["name"]
if "usage" in node_data: if "usage" in data:
node.usage = node_data["usage"] node.usage = data["usage"]
if "ports_mapping" in node_data: if "ports_mapping" in data:
node.ports_mapping = node_data["ports_mapping"] node.ports_mapping = data["ports_mapping"]
await node.update_port_settings() await node.update_port_settings()
if "console_type" in node_data: if "console_type" in data:
node.console_type = node_data["console_type"] node.console_type = data["console_type"]
node.updated() node.updated()
return node.asdict() return node.asdict()

View File

@ -18,6 +18,7 @@
API routes for Frame Relay switch nodes. API routes for Frame Relay switch nodes.
""" """
from typing import Any
import os import os
from fastapi import APIRouter, Depends, Body, Path, status, HTTPException 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 import Dynamips
from gns3server.compute.dynamips.nodes.frame_relay_switch import FrameRelaySwitch 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) router = APIRouter(responses=responses)
@ -59,13 +62,13 @@ async def create_frame_relay_switch(
# Use the Dynamips Frame Relay switch to simulate this node # Use the Dynamips Frame Relay switch to simulate this node
dynamips_manager = Dynamips.instance() 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 = await dynamips_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
node_type="frame_relay_switch", node_type="frame_relay_switch",
mappings=node_data.get("mappings"), mappings=data.get("mappings"),
) )
return node.asdict() return node.asdict()
@ -99,11 +102,11 @@ async def update_frame_relay_switch(
Update an Frame Relay switch node. Update an Frame Relay switch node.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data and node.name != node_data["name"]: if "name" in data and node.name != data["name"]:
await node.set_name(node_data["name"]) await node.set_name(data["name"])
if "mappings" in node_data: if "mappings" in data:
node.mappings = node_data["mappings"] node.mappings = data["mappings"]
node.updated() node.updated()
return node.asdict() return node.asdict()

View File

@ -23,7 +23,7 @@ import os
from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException from fastapi import APIRouter, WebSocket, Depends, Body, status, HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import Union from typing import Any, Union
from uuid import UUID from uuid import UUID
from gns3server import schemas 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 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) router = APIRouter(responses=responses)
@ -60,18 +62,18 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
""" """
iou = IOU.instance() 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( vm = await iou.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
application_id=node_data.get("application_id"), application_id=data.get("application_id"),
path=node_data.get("path"), path=data.get("path"),
console=node_data.get("console"), console=data.get("console"),
console_type=node_data.get("console_type", "telnet"), 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 hasattr(vm, name) and getattr(vm, name) != value:
if name == "application_id": if name == "application_id":
continue # we must ignore this to avoid overwriting the application_id allocated by the controller 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 continue
if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0): if name == "private_config_content" and (vm.private_config_content and len(vm.private_config_content) > 0):
continue 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 continue
setattr(vm, name, value) setattr(vm, name, value)
return vm.asdict() return vm.asdict()
@ -100,8 +102,8 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
Update an IOU node. Update an IOU node.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in node_data.items(): for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value: if hasattr(node, name) and getattr(node, name) != value:
if name == "application_id": if name == "application_id":
continue # we must ignore this to avoid overwriting the application_id allocated by the IOU manager 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 an IOU node.
""" """
start_data = jsonable_encoder(start_data, exclude_unset=True) start_values = jsonable_encoder(start_data, exclude_unset=True)
for name, value in start_data.items(): for name, value in start_values.items():
if hasattr(node, name) and getattr(node, name) != value: if hasattr(node, name) and getattr(node, name) != value:
setattr(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 = node.get_nio(adapter_number, port_number)
nio.filters.clear() nio.filters.clear()
if nio_data.filters: if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
nio.filters = nio_data.filters nio.filters = nio_data.filters
# NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers. # NIO type is a Union (Ethernet/TAP/UDP); only UDPNIO carries markers.
nio.markers = getattr(nio_data, "markers", None) or {} nio.markers = getattr(nio_data, "markers", None) or {}

View File

@ -23,14 +23,16 @@ import os
from fastapi import APIRouter, Depends, Path, status, HTTPException from fastapi import APIRouter, Depends, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import Union from typing import Any, Union
from uuid import UUID from uuid import UUID
from gns3server import schemas from gns3server import schemas
from gns3server.compute.builtin import Builtin from gns3server.compute.builtin import Builtin
from gns3server.compute.builtin.nodes.nat import Nat 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) 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() 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 = await builtin_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
node_type="nat", 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() return node.asdict()
@ -85,8 +87,8 @@ async def update_nat_node(node_data: schemas.NATUpdate, node: Nat = Depends(dep_
Update a NAT node. Update a NAT node.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
for name, value in node_data.items(): for name, value in data.items():
if hasattr(node, name) and getattr(node, name) != value: if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value) setattr(node, name, value)
node.updated() node.updated()
@ -170,7 +172,7 @@ async def update_nat_node_nio(
nio = node.get_nio(port_number) nio = node.get_nio(port_number)
nio.filters.clear() nio.filters.clear()
if nio_data.filters: if isinstance(nio_data, schemas.UDPNIO) and nio_data.filters:
nio.filters = nio_data.filters nio.filters = nio_data.filters
await node.update_nio(port_number, nio) await node.update_nio(port_number, nio)
return nio.asdict() return nio.asdict()

View File

@ -39,14 +39,15 @@ async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(w
""" """
if websocket: 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: try:
with NotificationManager.instance().queue() as queue: with NotificationManager.instance().queue() as queue:
while True: while True:
notification = await queue.get_json(5) notification = await queue.get_json(5)
await websocket.send_text(notification) await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect): 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: except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}") log.warning(f"Error while sending to controller event to WebSocket client: {e}")

View File

@ -44,7 +44,7 @@ from gns3server import schemas
router = APIRouter() router = APIRouter()
# How many clients have subscribed to notifications # How many clients have subscribed to notifications
_notifications_listening = {} _notifications_listening: dict[str, int] = {}
def dep_project(project_id: UUID) -> Project: def dep_project(project_id: UUID) -> Project:
@ -74,12 +74,12 @@ def create_compute_project(project_data: schemas.ProjectCreate) -> schemas.Proje
""" """
pm = ProjectManager.instance() pm = ProjectManager.instance()
project_data = jsonable_encoder(project_data, exclude_unset=True) data = jsonable_encoder(project_data, exclude_unset=True)
project = pm.create_project( project = pm.create_project(
name=project_data.get("name"), name=data.get("name"),
path=project_data.get("path"), path=data.get("path"),
project_id=project_data.get("project_id"), project_id=data.get("project_id"),
variables=project_data.get("variables", None), variables=data.get("variables", None),
) )
return project.asdict() return project.asdict()
@ -261,7 +261,7 @@ async def create_batch_nios(
# This is what makes builtin L2 nodes (ethernet_switch/hub/cloud/nat) # This is what makes builtin L2 nodes (ethernet_switch/hub/cloud/nat)
# start their uBridge concurrently during project open instead of one # start their uBridge concurrently during project open instead of one
# at a time (~0.5s each for fork + socket connect). # at a time (~0.5s each for fork + socket connect).
per_node = {} per_node: dict[str, list[schemas.BatchNIOEntry]] = {}
for entry in batch.nios: for entry in batch.nios:
per_node.setdefault(entry.node_id, []).append(entry) 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 # Group entries by node so that different nodes' uBridge processes are
# updated in parallel (each node has its own AF_UNIX socket). Within a # 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. # 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: for entry in batch.nios:
per_node.setdefault(entry.node_id, []).append(entry) per_node.setdefault(entry.node_id, []).append(entry)

View File

@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
from typing import Union from typing import Any, Union
from gns3server import schemas from gns3server import schemas
from gns3server.compute.virtualbox import VirtualBox 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 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) 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() 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( vm = await vbox_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
node_data.pop("vmname"), data.pop("vmname"),
linked_clone=node_data.pop("linked_clone", False), linked_clone=data.pop("linked_clone", False),
console=node_data.get("console", None), console=data.get("console", None),
console_type=node_data.get("console_type", "telnet"), console_type=data.get("console_type", "telnet"),
adapters=node_data.get("adapters", 0), adapters=data.get("adapters", 0),
) )
if "ram" in node_data: if "ram" in data:
ram = node_data.pop("ram") ram = data.pop("ram")
if ram != vm.ram: if ram != vm.ram:
await vm.set_ram(ram) await vm.set_ram(ram)
for name, value in node_data.items(): for name, value in data.items():
if name != "node_id": if name != "node_id":
if hasattr(vm, name) and getattr(vm, name) != value: if hasattr(vm, name) and getattr(vm, name) != value:
setattr(vm, name, value) setattr(vm, name, value)
@ -103,10 +105,10 @@ async def update_virtualbox_node(
Update a VirtualBox node. Update a VirtualBox node.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
if "name" in node_data: if "name" in data:
name = node_data.pop("name") name = data.pop("name")
vmname = node_data.pop("vmname", None) vmname = data.pop("vmname", None)
if name != node.name: if name != node.name:
oldname = node.name oldname = node.name
node.name = name node.name = name
@ -118,20 +120,20 @@ async def update_virtualbox_node(
node.updated() node.updated()
raise e raise e
if "adapters" in node_data: if "adapters" in data:
adapters = node_data.pop("adapters") adapters = data.pop("adapters")
if adapters != node.adapters: if adapters != node.adapters:
await node.set_adapters(adapters) await node.set_adapters(adapters)
if "ram" in node_data: if "ram" in data:
ram = node_data.pop("ram") ram = data.pop("ram")
if ram != node.ram: if ram != node.ram:
await node.set_ram(ram) await node.set_ram(ram)
# update the console first to avoid issue if updating console type # 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: if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value) setattr(node, name, value)

View File

@ -24,7 +24,7 @@ from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from uuid import UUID from uuid import UUID
from typing import Union from typing import Any, Union
from gns3server import schemas from gns3server import schemas
from gns3server.compute.vmware import VMware 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 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) 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() 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( vm = await vmware_manager.create_node(
node_data.pop("name"), data.pop("name"),
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
node_data.pop("vmx_path"), data.pop("vmx_path"),
linked_clone=node_data.pop("linked_clone"), linked_clone=data.pop("linked_clone"),
console=node_data.get("console", None), console=data.get("console", None),
console_type=node_data.get("console_type", "telnet"), 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 name != "node_id":
if hasattr(vm, name) and getattr(vm, name) != value: if hasattr(vm, name) and getattr(vm, name) != value:
setattr(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. 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 # 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: if hasattr(node, name) and getattr(node, name) != value:
setattr(node, name, value) setattr(node, name, value)

View File

@ -23,7 +23,7 @@ import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException from fastapi import APIRouter, WebSocket, Depends, Body, Path, status, HTTPException
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from typing import Union from typing import Any, Union
from uuid import UUID from uuid import UUID
from gns3server import schemas from gns3server import schemas
@ -31,7 +31,9 @@ from gns3server.compute.vpcs import VPCS
from gns3server.compute.vpcs.vpcs_vm import VPCSVM from gns3server.compute.vpcs.vpcs_vm import VPCSVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication 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) router = APIRouter(responses=responses)
@ -59,14 +61,14 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s
""" """
vpcs = VPCS.instance() 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( vm = await vpcs.create_node(
node_data["name"], data["name"],
str(project_id), str(project_id),
node_data.get("node_id"), data.get("node_id"),
console=node_data.get("console"), console=data.get("console"),
console_type=node_data.get("console_type", "telnet"), console_type=data.get("console_type", "telnet"),
startup_script=node_data.get("startup_script"), startup_script=data.get("startup_script"),
) )
return vm.asdict() return vm.asdict()
@ -87,14 +89,14 @@ async def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends
Update a VPCS node. Update a VPCS node.
""" """
node_data = jsonable_encoder(node_data, exclude_unset=True) data = jsonable_encoder(node_data, exclude_unset=True)
name = node_data.get("name", node.name) name = data.get("name", node.name)
if node.name != name: if node.name != name:
node.name = name node.name = name
console = node_data.get("console", node.console) console = data.get("console", node.console)
if node.console != console: if node.console != console:
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: if node.console_type != console_type:
node.console_type = console_type node.console_type = console_type
node.updated() node.updated()

View File

@ -25,13 +25,14 @@ from fastapi import APIRouter, Depends, Request, status
from fastapi.routing import APIRoute, _IncludedRouter from fastapi.routing import APIRoute, _IncludedRouter
from starlette.routing import BaseRoute, Mount from starlette.routing import BaseRoute, Mount
from uuid import UUID from uuid import UUID
from typing import Iterator, List, Sequence from typing import Iterator, List, Optional, Sequence
from gns3server import schemas from gns3server import schemas
from gns3server.controller.controller_error import ControllerBadRequestError, ControllerNotFoundError from gns3server.controller.controller_error import ControllerBadRequestError, ControllerNotFoundError
from gns3server.controller import Controller from gns3server.controller import Controller
import gns3server.db.models as models
from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository from gns3server.db.repositories.images import ImagesRepository
@ -96,7 +97,7 @@ async def endpoints(
endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}] endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}]
def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None: 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}) endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type})
# projects # projects
@ -175,7 +176,7 @@ async def endpoints(
@router.get("", response_model=List[schemas.ACE], dependencies=[Depends(has_privilege("ACE.Audit"))]) @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. Get all ACL entries.
@ -193,7 +194,7 @@ async def get_aces(rbac_repo: RbacRepository = Depends(get_repository(RbacReposi
) )
async def create_ace( async def create_ace(
request: Request, ace_create: schemas.ACECreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) request: Request, ace_create: schemas.ACECreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.ACE: ) -> models.ACE:
""" """
Create a new ACL entry. Create a new ACL entry.
@ -224,7 +225,7 @@ async def create_ace(
async def get_ace( async def get_ace(
ace_id: UUID, ace_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.ACE: ) -> models.ACE:
""" """
Get an ACL entry. 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"))]) @router.put("/{ace_id}", response_model=schemas.ACE, dependencies=[Depends(has_privilege("ACE.Modify"))])
async def update_ace( async def update_ace(
ace_id: UUID, ace_update: schemas.ACEUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) ace_id: UUID, ace_update: schemas.ACEUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.ACE: ) -> Optional[models.ACE]:
""" """
Update an ACL entry. Update an ACL entry.

View File

@ -39,7 +39,7 @@ API_KEY_PREFIX = "gns3_"
API_KEY_BYTES = 32 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: if api_key_id is None:
api_key_id = uuid4() api_key_id = uuid4()
random_bytes = secrets.token_hex(API_KEY_BYTES) random_bytes = secrets.token_hex(API_KEY_BYTES)

View File

@ -23,7 +23,7 @@ Nested under projects: /v3/projects/{project_id}/chat/...
import json import json
import logging import logging
import uuid import uuid
from typing import List, Optional from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
@ -40,7 +40,7 @@ from .dependencies.authentication import get_current_active_user
log = logging.getLogger(__name__) 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) router = APIRouter(responses=responses)

View File

@ -23,6 +23,7 @@ from typing import Any, List, Union, Optional
from uuid import UUID from uuid import UUID
from gns3server.controller import Controller from gns3server.controller import Controller
import gns3server.db.models as models
from gns3server.db.repositories.computes import ComputesRepository from gns3server.db.repositories.computes import ComputesRepository
from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services.computes import ComputesService from gns3server.services.computes import ComputesService
@ -31,7 +32,7 @@ from gns3server import schemas
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege 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) router = APIRouter(responses=responses)
@ -51,14 +52,14 @@ async def create_compute(
compute_create: schemas.ComputeCreate, compute_create: schemas.ComputeCreate,
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
connect: Optional[bool] = False, connect: Optional[bool] = False,
) -> schemas.Compute: ) -> models.Compute:
""" """
Create a new compute on the controller. Create a new compute on the controller.
Required privilege: Compute.Allocate 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( @router.post(
@ -86,7 +87,7 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None:
) )
async def get_compute( async def get_compute(
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)) compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
) -> schemas.Compute: ) -> Union[models.Compute, dict]:
""" """
Return a compute from the controller. Return a compute from the controller.
@ -104,7 +105,7 @@ async def get_compute(
) )
async def get_computes( async def get_computes(
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
) -> List[schemas.Compute]: ) -> List[models.Compute]:
""" """
Return all computes known by the controller. Return all computes known by the controller.
@ -124,7 +125,7 @@ async def update_compute(
compute_id: Union[str, UUID], compute_id: Union[str, UUID],
compute_update: schemas.ComputeUpdate, compute_update: schemas.ComputeUpdate,
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)), computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
) -> schemas.Compute: ) -> models.Compute:
""" """
Update a compute on the controller. Update a compute on the controller.

View File

@ -20,7 +20,7 @@ import os
import time import time
import psutil 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.responses import StreamingResponse
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.routing import Mount from fastapi.routing import Mount
@ -64,7 +64,7 @@ def get_version(request: Request) -> dict:
# compute subapp # compute subapp
controller_host = None controller_host = None
for route in request.app.routes: 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 controller_host = route.app.state.controller_host
local_server = Config.instance().settings.Server.local local_server = Config.instance().settings.Server.local
@ -129,7 +129,7 @@ async def shutdown() -> None:
try: try:
future.result() future.result()
except Exception as e: 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 continue
# then shutdown the server itself # then shutdown the server itself
@ -191,8 +191,8 @@ async def statistics() -> dict:
# Node statistics - distinguish open vs closed project nodes # Node statistics - distinguish open vs closed project nodes
open_project_nodes = [] open_project_nodes = []
closed_project_nodes = [] closed_project_nodes = []
node_by_type = {} node_by_type: Dict[str, int] = {}
node_by_status = {} node_by_status: Dict[str, int] = {}
for project in projects: for project in projects:
nodes = project.nodes.values() nodes = project.nodes.values()
@ -264,9 +264,8 @@ async def controller_http_notifications(request: Request) -> StreamingResponse:
from gns3server.api.server import app from gns3server.api.server import app
log.info( client = f"{request.client.host}:{request.client.port}" if request.client else "unknown"
f"New client {request.client.host}:{request.client.port} has connected to controller HTTP notification stream" log.info(f"New client {client} has connected to controller HTTP notification stream")
)
async def event_stream(): async def event_stream():
try: try:
@ -275,10 +274,7 @@ async def controller_http_notifications(request: Request) -> StreamingResponse:
msg = await queue.get_json(5) msg = await queue.get_json(5)
yield f"{msg}\n".encode("utf-8") yield f"{msg}\n".encode("utf-8")
finally: finally:
log.info( log.info(f"Client {client} has disconnected from controller HTTP notification stream")
f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP "
f"notification stream"
)
return StreamingResponse(event_stream(), media_type="application/json") return StreamingResponse(event_stream(), media_type="application/json")
@ -294,14 +290,15 @@ async def controller_ws_notifications(
if current_user is None: if current_user is None:
return 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: try:
with Controller.instance().notification.controller_queue() as queue: with Controller.instance().notification.controller_queue() as queue:
while True: while True:
notification = await queue.get_json(5) notification = await queue.get_json(5)
await websocket.send_text(notification) await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect): 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: except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}") log.warning(f"Error while sending to controller event to WebSocket client: {e}")

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Callable, Type from typing import AsyncGenerator, Callable, Type
from fastapi import Depends from fastapi import Depends
from starlette.requests import HTTPConnection from starlette.requests import HTTPConnection
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@ -22,7 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from gns3server.db.repositories.base import BaseRepository 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: async with AsyncSession(request.app.state._db_engine, expire_on_commit=False) as session:
try: try:
@ -32,7 +32,7 @@ async def get_db_session(request: HTTPConnection) -> AsyncSession:
def get_repository(repo: Type[BaseRepository]) -> Callable: 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 repo(db_session)
return get_repo return get_repo

View File

@ -20,7 +20,7 @@ API routes for drawings.
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, Depends, status
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from typing import List from typing import Any, List
from uuid import UUID from uuid import UUID
from gns3server.controller import Controller from gns3server.controller import Controller
@ -30,7 +30,9 @@ from gns3server import schemas
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege 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) router = APIRouter(responses=responses)

View File

@ -21,7 +21,7 @@ API routes for user groups.
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, Depends, status
from uuid import UUID from uuid import UUID
from typing import List from typing import List, Optional
from gns3server import schemas from gns3server import schemas
from gns3server.controller.controller_error import ( from gns3server.controller.controller_error import (
@ -31,6 +31,7 @@ from gns3server.controller.controller_error import (
ControllerForbiddenError, ControllerForbiddenError,
) )
import gns3server.db.models as models
from gns3server.db.repositories.users import UsersRepository from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository 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"))]) @router.get("", response_model=List[schemas.UserGroup], dependencies=[Depends(has_privilege("Group.Audit"))])
async def get_user_groups( async def get_user_groups(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)), users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> List[schemas.UserGroup]: ) -> List[models.UserGroup]:
""" """
Get all user groups. Get all user groups.
@ -65,7 +66,7 @@ async def get_user_groups(
) )
async def create_user_group( async def create_user_group(
user_group_create: schemas.UserGroupCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) user_group_create: schemas.UserGroupCreate, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> schemas.UserGroup: ) -> models.UserGroup:
""" """
Create a new user group. Create a new user group.
@ -82,7 +83,7 @@ async def create_user_group(
async def get_user_group( async def get_user_group(
user_group_id: UUID, user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)), users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.UserGroup: ) -> models.UserGroup:
""" """
Get a user group. Get a user group.
@ -100,7 +101,7 @@ async def update_user_group(
user_group_id: UUID, user_group_id: UUID,
user_group_update: schemas.UserGroupUpdate, user_group_update: schemas.UserGroupUpdate,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)), users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.UserGroup: ) -> Optional[models.UserGroup]:
""" """
Update a user group. Update a user group.
@ -148,7 +149,7 @@ async def delete_user_group(
) )
async def get_user_group_members( async def get_user_group_members(
user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository)) user_group_id: UUID, users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.User]: ) -> List[models.User]:
""" """
Get all user group members. Get all user group members.

View File

@ -27,7 +27,7 @@ from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request,
from fastapi.encoders import jsonable_encoder from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from typing import List, Callable, Optional from typing import Any, List, Callable, Optional
from uuid import UUID from uuid import UUID
from gns3server.controller import Controller from gns3server.controller import Controller
@ -47,7 +47,7 @@ import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
node_locks = {} node_locks: dict[str, dict[str, Any]] = {}
class NodeConcurrency(APIRoute): class NodeConcurrency(APIRoute):
@ -84,7 +84,9 @@ class NodeConcurrency(APIRoute):
return custom_route_handler 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) 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"))], 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. Create a new node.
@ -135,8 +137,8 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(
""" """
controller = Controller.instance() controller = Controller.instance()
compute = controller.get_compute(str(node_data.compute_id)) compute = controller.get_compute(str(node_create.compute_id))
node_data = jsonable_encoder(node_data, exclude_unset=True) 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) node = await project.add_node(compute, node_data.pop("name"), node_data.pop("node_id", None), **node_data)
return node.asdict() return node.asdict()
@ -274,14 +276,14 @@ async def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
response_model_exclude_unset=True, response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Node.Modify"))], 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. Update a node.
Required privilege: Node.Modify 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 # Ignore these because we only use them when creating a node
node_data.pop("node_id", None) node_data.pop("node_id", None)
@ -654,9 +656,8 @@ async def ws_console(
return return
compute = node.compute compute = node.compute
log.info( client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket" log.info(f"New client {client} has connected to controller console WebSocket")
)
compute_host = compute.host compute_host = compute.host
try: try:
@ -690,9 +691,7 @@ async def ws_console(
await ws_console_compute.send_bytes(msg["bytes"]) await ws_console_compute.send_bytes(msg["bytes"])
except WebSocketDisconnect: except WebSocketDisconnect:
pass pass
log.info( log.info(f"Client {client} has disconnected from controller console WebSocket")
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller console WebSocket"
)
async def ws_send(ws_console_compute): async def ws_send(ws_console_compute):
""" """
@ -709,10 +708,7 @@ async def ws_console(
break break
except WebSocketDisconnect: except WebSocketDisconnect:
# the client disconnected while the compute was still streaming console output # the client disconnected while the compute was still streaming console output
log.info( log.info(f"Client {client} has disconnected from controller console WebSocket")
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
f" console WebSocket"
)
try: try:
# forward WebSocket data in both directions between the client and the compute console WebSocket # forward WebSocket data in both directions between the client and the compute console WebSocket
@ -763,9 +759,8 @@ async def vnc_console(
return return
compute = node.compute compute = node.compute
log.info( client = f"{websocket.client.host}:{websocket.client.port}" if websocket.client else "unknown"
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller VNC console WebSocket" log.info(f"New client {client} has connected to controller VNC console WebSocket")
)
compute_host = compute.host compute_host = compute.host
try: try:
@ -796,10 +791,7 @@ async def vnc_console(
await vnc_console_compute.send_bytes(data) await vnc_console_compute.send_bytes(data)
except WebSocketDisconnect: except WebSocketDisconnect:
pass pass
log.info( log.info(f"Client {client} has disconnected from controller VNC console WebSocket")
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
f" VNC console WebSocket"
)
async def vnc_send(vnc_console_compute): async def vnc_send(vnc_console_compute):
""" """
@ -814,10 +806,7 @@ async def vnc_console(
break break
except WebSocketDisconnect: except WebSocketDisconnect:
# the client disconnected while the compute was still streaming VNC console output # the client disconnected while the compute was still streaming VNC console output
log.info( log.info(f"Client {client} has disconnected from controller VNC console WebSocket")
f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller"
f" VNC console WebSocket"
)
try: try:
# forward WebSocket data in both directions between the client and the compute VNC console WebSocket # forward WebSocket data in both directions between the client and the compute VNC console WebSocket

View File

@ -16,6 +16,7 @@ API route for privileges
""" """
from typing import List from typing import List
import gns3server.db.models as models
from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository from .dependencies.database import get_repository
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
@ -33,7 +34,7 @@ router = APIRouter()
) )
async def get_privileges( async def get_privileges(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> List[schemas.Privilege]: ) -> List[models.Privilege]:
""" """
Get all privileges. Get all privileges.

View File

@ -21,7 +21,7 @@ API routes for roles.
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, Depends, status
from uuid import UUID from uuid import UUID
from typing import List from typing import List, Optional
from gns3server import schemas from gns3server import schemas
from gns3server.controller.controller_error import ( from gns3server.controller.controller_error import (
@ -31,6 +31,7 @@ from gns3server.controller.controller_error import (
ControllerForbiddenError, ControllerForbiddenError,
) )
import gns3server.db.models as models
from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege 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"))]) @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. Get all roles.
@ -61,7 +62,7 @@ async def get_roles(rbac_repo: RbacRepository = Depends(get_repository(RbacRepos
) )
async def create_role( async def create_role(
role_create: schemas.RoleCreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) role_create: schemas.RoleCreate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role: ) -> Optional[models.Role]:
""" """
Create a new role. Create a new role.
@ -78,7 +79,7 @@ async def create_role(
async def get_role( async def get_role(
role_id: UUID, role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)), rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.Role: ) -> models.Role:
""" """
Get a 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"))]) @router.put("/{role_id}", response_model=schemas.Role, dependencies=[Depends(has_privilege("Role.Modify"))])
async def update_role( async def update_role(
role_id: UUID, role_update: schemas.RoleUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) role_id: UUID, role_update: schemas.RoleUpdate, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role: ) -> Optional[models.Role]:
""" """
Update a role. Update a role.
@ -142,7 +143,7 @@ async def delete_role(
) )
async def get_role_privileges( async def get_role_privileges(
role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) role_id: UUID, rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Privilege]: ) -> List[models.Privilege]:
""" """
Get all role privileges. Get all role privileges.

View File

@ -24,7 +24,7 @@ import logging
log = logging.getLogger() log = logging.getLogger()
from fastapi import APIRouter, Depends, status from fastapi import APIRouter, Depends, status
from typing import List from typing import Any, List
from uuid import UUID from uuid import UUID
from gns3server.controller.project import Project from gns3server.controller.project import Project
@ -35,7 +35,9 @@ from gns3server.controller import Controller
from .dependencies.database import get_repository from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege 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) router = APIRouter(responses=responses)

View File

@ -19,6 +19,8 @@
FastAPI app FastAPI app
""" """
from typing import cast
from fastapi import FastAPI, Request, HTTPException, status from fastapi import FastAPI, Request, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@ -118,7 +120,7 @@ def handle_exit(*args, **kwargs):
unicorn_exit_handler(*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 # 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(): async def swagger_ui_redirect():
return get_swagger_ui_oauth2_redirect_html() return get_swagger_ui_oauth2_redirect_html()

View File

@ -1645,7 +1645,7 @@ class Router(BaseNode):
await nio.start_packet_capture(output_file, data_link_type) await nio.start_packet_capture(output_file, data_link_type)
log.debug( log.debug(
'Router "{name}" [{id}]: starting packet capture on port {slot_number}/{port_number}'.format( '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( log.debug(
'Router "{name}" [{id}]: stopping packet capture on port {slot_number}/{port_number}'.format( '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
) )
) )

View File

@ -204,9 +204,8 @@ if __name__ == "__main__":
args = parser.parse_args() args = parser.parse_args()
try: try:
fd = open(args.nvram, "rb") with open(args.nvram, "rb") as fd:
nvram = fd.read() nvram = fd.read()
fd.close()
except OSError as err: except OSError as err:
sys.stderr.write(f"Error reading file: {err}\n") sys.stderr.write(f"Error reading file: {err}\n")
sys.exit(1) sys.exit(1)
@ -218,16 +217,14 @@ if __name__ == "__main__":
sys.exit(3) sys.exit(3)
try: try:
fd = open(args.startup, "wb") with open(args.startup, "wb") as out:
fd.write(startup) out.write(startup)
fd.close()
if args.private is not None: if args.private is not None:
if private is None: if private is None:
sys.stderr.write("Warning: No private config\n") sys.stderr.write("Warning: No private config\n")
else: else:
fd = open(args.private, "wb") with open(args.private, "wb") as out:
fd.write(private) out.write(private)
fd.close()
except OSError as err: except OSError as err:
sys.stderr.write(f"Error writing file: {err}\n") sys.stderr.write(f"Error writing file: {err}\n")
sys.exit(1) sys.exit(1)

View File

@ -197,22 +197,17 @@ if __name__ == "__main__":
parser.add_argument("private", metavar="private-config", nargs="?", help="private configuration") parser.add_argument("private", metavar="private-config", nargs="?", help="private configuration")
args = parser.parse_args() args = parser.parse_args()
nvram: bytes | None = None
private: bytes | None = None
try: try:
if args.create is None: if args.create is None:
fd = open(args.nvram, "rb") with open(args.nvram, "rb") as fd:
nvram = fd.read() nvram = fd.read()
fd.close() with open(args.startup, "rb") as fd:
else: startup = fd.read()
nvram = None if args.private is not None:
fd = open(args.startup, "rb") with open(args.private, "rb") as fd:
startup = fd.read() private = fd.read()
fd.close()
if args.private is None:
private = None
else:
fd = open(args.private, "rb")
private = fd.read()
fd.close()
except OSError as err: except OSError as err:
sys.stderr.write(f"Error reading file: {err}\n") sys.stderr.write(f"Error reading file: {err}\n")
sys.exit(1) sys.exit(1)
@ -224,9 +219,8 @@ if __name__ == "__main__":
sys.exit(3) sys.exit(3)
try: try:
fd = open(args.nvram, "wb") with open(args.nvram, "wb") as out:
fd.write(nvram) out.write(nvram)
fd.close()
except OSError as err: except OSError as err:
sys.stderr.write(f"Error writing file: {err}\n") sys.stderr.write(f"Error writing file: {err}\n")
sys.exit(1) sys.exit(1)

View File

@ -148,7 +148,11 @@ class VPCSVM(BaseNode):
vpcs_path = shutil.which(vpcs_path) vpcs_path = shutil.which(vpcs_path)
return vpcs_path return vpcs_path
@BaseNode.name.setter @property
def name(self):
return self._name
@name.setter
def name(self, new_name): def name(self, new_name):
""" """
Sets the name of this VPCS VM. Sets the name of this VPCS VM.
@ -358,7 +362,11 @@ class VPCSVM(BaseNode):
if self.is_running(): if self.is_running():
await self.reset_wrap_console() 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): def console_type(self, new_console_type):
""" """
Sets the console type for this VPCS VM. Sets the console type for this VPCS VM.

View File

@ -51,6 +51,10 @@ class Appliance:
def symbol(self): def symbol(self):
return self._data.get("symbol") return self._data.get("symbol")
@symbol.setter
def symbol(self, new_symbol):
self._data["symbol"] = new_symbol
@property @property
def name(self): def name(self):
return self._data.get("name") return self._data.get("name")
@ -63,10 +67,6 @@ class Appliance:
def versions(self): def versions(self):
return self._data.get("versions") return self._data.get("versions")
@symbol.setter
def symbol(self, new_symbol):
self._data["symbol"] = new_symbol
@property @property
def type(self): def type(self):

View File

@ -53,7 +53,7 @@ GNS3_FILE_FORMAT_REVISION = 10
class DynamipsNodeValidation(DynamipsCreate): class DynamipsNodeValidation(DynamipsCreate):
name: Optional[str] = None name: Optional[str] = None # type: ignore[assignment]
def _check_topology_schema(topo, path): def _check_topology_schema(topo, path):

View File

@ -26,8 +26,8 @@ class ATMSwitchBase(BaseModel):
Common ATM switch properties. Common ATM switch properties.
""" """
name: str = None name: Optional[str] = None
node_id: UUID = None node_id: Optional[UUID] = None
usage: Optional[str] = None usage: Optional[str] = None
mappings: Optional[dict] = None mappings: Optional[dict] = None

View File

@ -126,7 +126,7 @@ class CloudBase(BaseModel):
Common cloud node properties. Common cloud node properties.
""" """
name: str name: Optional[str] = None
node_id: Optional[UUID] = None node_id: Optional[UUID] = None
usage: Optional[str] = None usage: Optional[str] = None
remote_console_host: Optional[str] = Field(None, description="Remote console host or IP") 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. Properties to create a cloud node.
""" """
pass name: str
class CloudUpdate(CloudBase): class CloudUpdate(CloudBase):
@ -152,10 +152,11 @@ class CloudUpdate(CloudBase):
Properties to update a cloud node. Properties to update a cloud node.
""" """
name: Optional[str] = None pass
class Cloud(CloudBase): class Cloud(CloudBase):
name: str
project_id: UUID project_id: UUID
node_id: UUID node_id: UUID
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]]

View File

@ -41,8 +41,8 @@ class DockerBase(BaseModel):
# the canonical "no path" value is "/" (the creation default) # the canonical "no path" value is "/" (the creation default)
return value or "/" return value or "/"
name: str name: Optional[str] = None
image: str = Field(..., description="Docker image name") image: Optional[str] = Field(None, description="Docker image name")
node_id: Optional[UUID] = None node_id: Optional[UUID] = None
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
console_type: Optional[ConsoleType] = Field(None, description="Console type") console_type: Optional[ConsoleType] = Field(None, description="Console type")
@ -76,6 +76,8 @@ class DockerCreate(DockerBase):
Properties to create a Docker node. Properties to create a Docker node.
""" """
name: str
image: str = Field(..., description="Docker image name")
application_id: Optional[int] = Field( application_id: Optional[int] = Field(
None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)" 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. Properties to update a Docker node.
""" """
name: Optional[str] = None pass
image: Optional[str] = Field(None, description="Docker image name")
class Docker(DockerBase): class Docker(DockerBase):
name: str
image: str = Field(..., description="Docker image name")
container_id: str = Field( container_id: str = Field(
..., min_length=12, max_length=64, pattern="^[a-f0-9]+$", description="Docker container ID (read only)" ..., min_length=12, max_length=64, pattern="^[a-f0-9]+$", description="Docker container ID (read only)"
) )

View File

@ -124,7 +124,6 @@ class DynamipsBase(BaseModel):
node_id: Optional[UUID] = None node_id: Optional[UUID] = None
name: Optional[str] = None name: Optional[str] = None
dynamips_id: Optional[int] = Field(None, description="Dynamips internal ID") 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") 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") nvram: Optional[int] = Field(None, gt=0, description="Amount of NVRAM in KB")
image: Optional[str] = Field(None, description="Path to the IOS image") image: Optional[str] = Field(None, description="Path to the IOS image")
@ -185,11 +184,12 @@ class DynamipsUpdate(DynamipsBase):
Properties to update a Dynamips node. Properties to update a Dynamips node.
""" """
pass platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
class Dynamips(DynamipsBase): class Dynamips(DynamipsBase):
name: str name: str
platform: Optional[DynamipsPlatform] = Field(None, description="Cisco router platform")
node_id: UUID node_id: UUID
project_id: UUID project_id: UUID
dynamips_id: int dynamips_id: int

View File

@ -26,8 +26,8 @@ class FrameRelaySwitchBase(BaseModel):
Common Frame Relay switch properties. Common Frame Relay switch properties.
""" """
name: str = None name: Optional[str] = None
node_id: UUID = None node_id: Optional[UUID] = None
usage: Optional[str] = None usage: Optional[str] = None
mappings: Optional[dict] = None mappings: Optional[dict] = None

View File

@ -26,9 +26,9 @@ class IOUBase(BaseModel):
Common IOU node properties. Common IOU node properties.
""" """
name: str name: Optional[str] = None
path: str = Field(..., description="IOU executable path") path: Optional[str] = Field(None, description="IOU executable path")
application_id: int = Field(..., description="Application ID for running IOU executable") application_id: Optional[int] = Field(None, description="Application ID for running IOU executable")
node_id: Optional[UUID] = None node_id: Optional[UUID] = None
usage: Optional[str] = Field(None, description="How to use the node") usage: Optional[str] = Field(None, description="How to use the node")
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port") 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. 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): class IOUUpdate(IOUBase):
@ -60,12 +62,13 @@ class IOUUpdate(IOUBase):
Properties to update an IOU node. Properties to update an IOU node.
""" """
name: Optional[str] = None pass
path: Optional[str] = Field(None, description="IOU executable path")
application_id: Optional[int] = Field(None, description="Application ID for running IOU executable")
class IOU(IOUBase): 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") project_id: UUID = Field(..., description="Project ID")
node_directory: str = Field(..., description="Path to the node working directory (read only)") 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)") command_line: str = Field(..., description="Last command line used to start IOU (read only)")

View File

@ -89,7 +89,7 @@ class NATBase(BaseModel):
Common NAT node properties. Common NAT node properties.
""" """
name: str name: Optional[str] = None
node_id: Optional[UUID] = None node_id: Optional[UUID] = None
usage: Optional[str] = None usage: Optional[str] = None
ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field( ports_mapping: Optional[List[Union[EthernetPort, TAPPort, UDPPort]]] = Field(
@ -102,7 +102,7 @@ class NATCreate(NATBase):
Properties to create a NAT node. Properties to create a NAT node.
""" """
pass name: str
class NATUpdate(NATBase): class NATUpdate(NATBase):
@ -110,10 +110,11 @@ class NATUpdate(NATBase):
Properties to update a NAT node. Properties to update a NAT node.
""" """
name: Optional[str] = None pass
class NAT(NATBase): class NAT(NATBase):
name: str
project_id: UUID project_id: UUID
node_id: UUID node_id: UUID
ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]] ports_mapping: List[Union[EthernetPort, TAPPort, UDPPort]]

View File

@ -155,7 +155,7 @@ class QemuBase(BaseModel):
Common Qemu node properties. Common Qemu node properties.
""" """
name: str name: Optional[str] = None
node_id: Optional[UUID] = None node_id: Optional[UUID] = None
usage: Optional[str] = Field(None, description="How to use the node") 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") 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. Properties to create a Qemu node.
""" """
name: str
disk_images_to_reset: Optional[List[str]] = Field( disk_images_to_reset: Optional[List[str]] = Field(
None, None,
description="Disk image properties whose stale linked-clone overlays must be recreated", 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. Properties to update a Qemu node.
""" """
name: Optional[str] = None pass
class Qemu(QemuBase): class Qemu(QemuBase):
name: str
project_id: UUID = Field(..., description="Project ID") project_id: UUID = Field(..., description="Project ID")
node_directory: str = Field(..., description="Path to the node working directory (read only)") 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)") command_line: str = Field(..., description="Last command line used to start IOU (read only)")

View File

@ -56,8 +56,8 @@ class VirtualBoxBase(BaseModel):
Common VirtualBox node properties. Common VirtualBox node properties.
""" """
name: str name: Optional[str] = None
vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)") vmname: Optional[str] = None
node_id: Optional[UUID] = None node_id: Optional[UUID] = None
linked_clone: Optional[bool] = Field(None, description="Whether the VM is a linked clone or not") 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") usage: Optional[str] = Field(None, description="How to use the node")
@ -78,7 +78,8 @@ class VirtualBoxCreate(VirtualBoxBase):
Properties to create a VirtualBox node. Properties to create a VirtualBox node.
""" """
pass name: str
vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)")
class VirtualBoxUpdate(VirtualBoxBase): class VirtualBoxUpdate(VirtualBoxBase):
@ -86,11 +87,12 @@ class VirtualBoxUpdate(VirtualBoxBase):
Properties to update a VirtualBox node. Properties to update a VirtualBox node.
""" """
name: Optional[str] = None pass
vmname: Optional[str] = None
class VirtualBox(VirtualBoxBase): class VirtualBox(VirtualBoxBase):
name: str
vmname: str = Field(..., description="VirtualBox VM name (in VirtualBox itself)")
project_id: UUID = Field(..., description="Project ID") project_id: UUID = Field(..., description="Project ID")
node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)") node_directory: Optional[str] = Field(None, description="Path to the node working directory (read only)")
status: NodeStatus = Field(..., description="Container status (read only)") status: NodeStatus = Field(..., description="Container status (read only)")

View File

@ -80,7 +80,7 @@ class UserGroupCreate(UserGroupBase):
Properties to create a user group. 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): class UserGroupUpdate(UserGroupBase):

View File

@ -48,7 +48,7 @@ def init_encryption(secrets_dir: str) -> None:
os.makedirs(secrets_dir, exist_ok=True) os.makedirs(secrets_dir, exist_ok=True)
with open(encryption_key_path, "w", encoding="utf-8") as f: with open(encryption_key_path, "w", encoding="utf-8") as f:
# Use Fernet's base64-encoded key format # 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) # Set restrictive permissions (owner read/write only)
os.chmod(encryption_key_path, 0o600) os.chmod(encryption_key_path, 0o600)
except OSError as e: except OSError as e:
@ -58,7 +58,7 @@ def init_encryption(secrets_dir: str) -> None:
try: try:
with open(encryption_key_path, encoding="utf-8") as f: with open(encryption_key_path, encoding="utf-8") as f:
key_content = f.read().strip() 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) _fernet = Fernet(key_bytes)
log.debug("Encryption initialized successfully") log.debug("Encryption initialized successfully")
except OSError as e: except OSError as e:

View File

@ -126,7 +126,7 @@ def get_builtin_disks() -> List[str]:
return builtin_disks return builtin_disks
async def read_image_info(path: str, expected_image_type: str = None) -> dict: async def read_image_info(path: str, expected_image_type: str | None = None) -> dict:
header_magic_len = 7 header_magic_len = 7
try: try:
@ -155,7 +155,7 @@ async def read_image_info(path: str, expected_image_type: str = None) -> dict:
return image_info return image_info
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 Scan directories for available images
""" """
@ -378,9 +378,9 @@ async def write_image(
f"File '{image_path}' already exists, please choose a different name or remove the existing image" f"File '{image_path}' already exists, please choose a different name or remove the existing image"
) )
checksum = checksum.hexdigest() checksum_hex = checksum.hexdigest()
image_dir = os.path.dirname(image_path) image_dir = os.path.dirname(image_path)
duplicate_image = await images_repo.get_image_by_checksum(checksum, image_dir) duplicate_image = await images_repo.get_image_by_checksum(checksum_hex, image_dir)
if duplicate_image: if duplicate_image:
raise InvalidImageError( raise InvalidImageError(
f"Image '{duplicate_image.filename}' with the same checksum already exists in '{image_dir}'" f"Image '{duplicate_image.filename}' with the same checksum already exists in '{image_dir}'"
@ -396,5 +396,5 @@ async def write_image(
log.warning(f"Could not remove '{tmp_path}'") log.warning(f"Could not remove '{tmp_path}'")
return await images_repo.add_image( return await images_repo.add_image(
image_name, image_type, image_size, image_path, checksum, checksum_algorithm="md5" image_name, image_type, image_size, image_path, checksum_hex, checksum_algorithm="md5"
) )

View File

@ -15,7 +15,7 @@ class FilterValidationError(Exception):
pass 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. 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 # Define validation rules based on ubridge implementation
VALIDATION_RULES = { VALIDATION_RULES: Dict[str, Dict[str, Any]] = {
"frequency_drop": { "frequency_drop": {
"params_count": 1, "params_count": 1,
"ranges": [(-1, 32767)], # min, max "ranges": [(-1, 32767)], # min, max
@ -173,11 +173,11 @@ def filter_inactive_filters(filters: Dict[str, List[Any]]) -> Dict[str, List[Any
active_filters = {} active_filters = {}
for filter_type, values in filters.items(): for filter_type, values in filters.items():
if not values or (isinstance(values, list) and len(values) == 0): if not values:
continue continue
# Normalize values (strip strings, convert to int) # Normalize values (strip strings, convert to int)
normalized_values = [] normalized_values: List[Any] = []
for value in values: for value in values:
if isinstance(value, str): if isinstance(value, str):
normalized_values.append(value.strip("\n ")) normalized_values.append(value.strip("\n "))
@ -228,7 +228,7 @@ def validate_all_filters(filters: Dict[str, List[Any]]) -> None:
return return
for filter_type, values in filters.items(): for filter_type, values in filters.items():
if not values or (isinstance(values, list) and len(values) == 0): if not values:
continue continue
validate_filter_parameters(filter_type, values) validate_filter_parameters(filter_type, values)

View File

@ -294,77 +294,15 @@ enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
# Baseline: modules with existing type errors (error count at baseline time). # 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. # Remove modules from this list one small PR at a time. Never add new ones.
module = [ 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.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.images", # 9
"gns3server.api.routes.controller.llm_model_configs", # 32 "gns3server.api.routes.controller.llm_model_configs", # 32
"gns3server.api.routes.controller.nodes", # 13
"gns3server.api.routes.controller.pools", # 8 "gns3server.api.routes.controller.pools", # 8
"gns3server.api.routes.controller.privileges", # 1
"gns3server.api.routes.controller.projects", # 16 "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.templates", # 10
"gns3server.api.routes.controller.users", # 14 "gns3server.api.routes.controller.users", # 14
"gns3server.api.server", # 2
"gns3server.compute.dynamips.nodes.router", # 2
"gns3server.compute.iou.utils.iou_export", # 2
"gns3server.compute.iou.utils.iou_import", # 2
"gns3server.compute.virtualbox.virtualbox_vm", # 1 "gns3server.compute.virtualbox.virtualbox_vm", # 1
"gns3server.compute.vpcs.vpcs_vm", # 2
"gns3server.controller.appliance", # 2
"gns3server.controller.appliance_manager", # 5 "gns3server.controller.appliance_manager", # 5
"gns3server.controller.project", # 2 "gns3server.controller.project", # 2
"gns3server.controller.topology", # 1
"gns3server.db.repositories.api_keys", # 8 "gns3server.db.repositories.api_keys", # 8
"gns3server.db.repositories.computes", # 10 "gns3server.db.repositories.computes", # 10
"gns3server.db.repositories.images", # 14 "gns3server.db.repositories.images", # 14
@ -374,15 +312,6 @@ module = [
"gns3server.db.repositories.templates", # 6 "gns3server.db.repositories.templates", # 6
"gns3server.db.repositories.users", # 12 "gns3server.db.repositories.users", # 12
"gns3server.db.tasks", # 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.controller.computes", # 6 "gns3server.schemas.controller.computes", # 6
"gns3server.schemas.controller.templates.iou_templates", # 1 "gns3server.schemas.controller.templates.iou_templates", # 1
"gns3server.schemas.controller.templates.virtualbox_templates", # 1 "gns3server.schemas.controller.templates.virtualbox_templates", # 1
@ -391,10 +320,7 @@ module = [
"gns3server.services.computes", # 4 "gns3server.services.computes", # 4
"gns3server.services.templates", # 18 "gns3server.services.templates", # 18
"gns3server.utils.asyncio.aiozipstream", # 8 "gns3server.utils.asyncio.aiozipstream", # 8
"gns3server.utils.encryption", # 2
"gns3server.utils.http_client", # 4 "gns3server.utils.http_client", # 4
"gns3server.utils.images", # 4
"gns3server.utils.packet_filter_validation", # 16
"gns3server.utils.websocket_to_websocket", # 12 "gns3server.utils.websocket_to_websocket", # 12
"gns3server.utils.zipfile_zstd._patcher", # 1 "gns3server.utils.zipfile_zstd._patcher", # 1
] ]