Merge branch '3.1' into mypy-agent

This commit is contained in:
Jeremy Grossmann 2026-09-29 13:19:30 +02:00 committed by GitHub
commit 468ad4e85f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 149 additions and 180 deletions

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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,59 +294,29 @@ 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.gns3_copilot.agent.context_manager", # 5
"gns3server.agent.gns3_copilot.agent.model_factory", # 4
"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.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.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.mcp.appliances", # 1
"gns3server.agent.web_wireshark.docker_client", # 5 "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.atm_switch_nodes", # 9
"gns3server.api.routes.compute.capabilities", # 1
"gns3server.api.routes.compute.cloud_nodes", # 14 "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.ethernet_switch_nodes", # 16
"gns3server.api.routes.compute.frame_relay_switch_nodes", # 9 "gns3server.api.routes.compute.frame_relay_switch_nodes", # 9
"gns3server.api.routes.compute.iou_nodes", # 15 "gns3server.api.routes.compute.iou_nodes", # 15
"gns3server.api.routes.compute.nat_nodes", # 10 "gns3server.api.routes.compute.nat_nodes", # 10
"gns3server.api.routes.compute.notifications", # 4
"gns3server.api.routes.compute.projects", # 7 "gns3server.api.routes.compute.projects", # 7
"gns3server.api.routes.compute.virtualbox_nodes", # 20 "gns3server.api.routes.compute.virtualbox_nodes", # 20
"gns3server.api.routes.compute.vmware_nodes", # 10 "gns3server.api.routes.compute.vmware_nodes", # 10
"gns3server.api.routes.compute.vpcs_nodes", # 9 "gns3server.api.routes.compute.vpcs_nodes", # 9
"gns3server.api.routes.controller.acl", # 5 "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.computes", # 6
"gns3server.api.routes.controller.controller", # 10 "gns3server.api.routes.controller.controller", # 10
"gns3server.api.routes.controller.dependencies.authentication", # 11 "gns3server.api.routes.controller.dependencies.authentication", # 11
"gns3server.api.routes.controller.dependencies.database", # 2 "gns3server.api.routes.controller.dependencies.database", # 2
"gns3server.api.routes.controller.drawings", # 1
"gns3server.api.routes.controller.groups", # 6 "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.api.server", # 2
@ -358,7 +328,6 @@ module = [
"gns3server.controller.appliance", # 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
@ -368,13 +337,10 @@ 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.cloud_nodes", # 1
"gns3server.schemas.compute.docker_nodes", # 2 "gns3server.schemas.compute.docker_nodes", # 2
"gns3server.schemas.compute.dynamips_nodes", # 1
"gns3server.schemas.compute.frame_relay_switch_nodes", # 5 "gns3server.schemas.compute.frame_relay_switch_nodes", # 5
"gns3server.schemas.compute.iou_nodes", # 3 "gns3server.schemas.compute.iou_nodes", # 3
"gns3server.schemas.compute.nat_nodes", # 1
"gns3server.schemas.compute.qemu_nodes", # 1 "gns3server.schemas.compute.qemu_nodes", # 1
"gns3server.schemas.compute.virtualbox_nodes", # 2 "gns3server.schemas.compute.virtualbox_nodes", # 2
"gns3server.schemas.compute.vmware_nodes", # 3 "gns3server.schemas.compute.vmware_nodes", # 3
@ -392,10 +358,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
] ]