diff --git a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py index 485f02dbc..4bf507b25 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/config_tools_nornir.py @@ -47,6 +47,7 @@ from nornir.core.task import AggregatedResult, Result, Task from nornir_netmiko.tasks import netmiko_send_config from gns3server.agent.gns3_copilot.gns3_client import get_gns3_server_host + # Import custom Netmiko device types for GNS3 emulation # This registers gns3_huawei_telnet_ce and other custom device types # NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered diff --git a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py index 1569c224e..afdeac035 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py +++ b/gns3server/agent/gns3_copilot/tools_v2/display_tools_nornir.py @@ -47,6 +47,7 @@ from nornir.core.task import AggregatedResult, Result, Task from nornir_netmiko.tasks import netmiko_multiline from gns3server.agent.gns3_copilot.gns3_client import get_gns3_server_host + # Import custom Netmiko device types for GNS3 emulation # This registers gns3_huawei_telnet_ce and other custom device types # NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered diff --git a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py index 2444453f3..31ed0c916 100644 --- a/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py +++ b/gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py @@ -47,6 +47,7 @@ from nornir.core.task import AggregatedResult, Result, Task from nornir_netmiko.tasks import netmiko_multiline from gns3server.agent.gns3_copilot.gns3_client import get_gns3_server_host + # Import custom Netmiko device types for GNS3 emulation # This registers gns3_vpcs_telnet and other custom device types # NOTE: Must be imported BEFORE any Nornir operations to ensure device types are registered diff --git a/gns3server/agent/mcp/__init__.py b/gns3server/agent/mcp/__init__.py index 232eb0193..00bef7e88 100644 --- a/gns3server/agent/mcp/__init__.py +++ b/gns3server/agent/mcp/__init__.py @@ -346,13 +346,13 @@ def _run_handler_sync(handler, params: dict[str, Any]) -> list[dict[str, Any]]: # Name-shaped project_id/node_id values become UUIDs before the handler # runs; a name that matches nothing fails fast with the known names. - params, resolution_error = resolve_locators(params, run) - if resolution_error is not None: + resolved_params, resolution_error = resolve_locators(params, run) + if resolved_params is None: result = resolution_error else: - result = handler(params, ctx) + result = handler(resolved_params, ctx) # backstop for UUID transcription slips: "Did you mean ...?" - result = add_did_you_mean(result, params, run) + result = add_did_you_mean(result, resolved_params, run) return [{"type": "text", "text": json.dumps(result, ensure_ascii=False, default=str)}] diff --git a/gns3server/agent/mcp/locators.py b/gns3server/agent/mcp/locators.py index 4bffb8014..727e0b208 100644 --- a/gns3server/agent/mcp/locators.py +++ b/gns3server/agent/mcp/locators.py @@ -37,9 +37,10 @@ distance 2 earns a "Did you mean ..." hint. import re from typing import Any, Callable -from .projects import list_projects_handler from gns3server.agent.gns3_copilot.gns3_client.api_handlers import get_links_handler, get_nodes_handler +from .projects import list_projects_handler + _UUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$") _ID_IN_TEXT_RE = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}") @@ -94,9 +95,7 @@ def resolve_locators( return params, None -def add_did_you_mean( - result: Any, params: dict[str, Any], run: Callable[[Callable, dict], Any] -) -> Any: +def add_did_you_mean(result: Any, params: dict[str, Any], run: Callable[[Callable, dict], Any]) -> Any: """ Append a "Did you mean ...?" hint to an error reply that names a non-existent UUID, when exactly one in-scope id is within edit @@ -118,7 +117,11 @@ def add_did_you_mean( hints = [] for bad in bad_ids: - close = [(cid, label) for cid, label in candidates if bad != cid and _edit_distance_at_most(bad.lower(), cid.lower(), 2)] + close = [ + (cid, label) + for cid, label in candidates + if bad != cid and _edit_distance_at_most(bad.lower(), cid.lower(), 2) + ] if len(close) == 1: cid, label = close[0] hints.append(f"Did you mean {label} ({cid})?") @@ -177,7 +180,7 @@ def _collect_candidates(params: dict[str, Any], run: Callable[[Callable, dict], just yields what was gathered so far. """ - candidates = [] + candidates: list[tuple[str, str]] = [] try: projects = _as_items(run(list_projects_handler, {}), "projects") or [] candidates.extend((p["project_id"], f"project '{p.get('name', '?')}'") for p in projects if p.get("project_id")) diff --git a/gns3server/api/routes/compute/capabilities.py b/gns3server/api/routes/compute/capabilities.py index 9e6a2d2bd..d0f969a81 100644 --- a/gns3server/api/routes/compute/capabilities.py +++ b/gns3server/api/routes/compute/capabilities.py @@ -28,8 +28,8 @@ from gns3server.compute import MODULES from gns3server.compute.ubridge.tc_probe import ( probe_bridge_tap_support, probe_iol_tap_support, - probe_tc_capabilities, probe_tap_support, + probe_tc_capabilities, ) from gns3server.utils.path import get_default_project_directory from gns3server.utils.tc_capabilities import usable_ebpf_modes diff --git a/gns3server/api/routes/compute/docker_nodes.py b/gns3server/api/routes/compute/docker_nodes.py index 5e7f1af98..a66084f5b 100644 --- a/gns3server/api/routes/compute/docker_nodes.py +++ b/gns3server/api/routes/compute/docker_nodes.py @@ -264,8 +264,10 @@ async def duplicate_docker_node( dependencies=[Depends(compute_authentication)], ) async def create_docker_node_nio( - adapter_number: int, port_number: int, nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO], - node: DockerVM = Depends(dep_node) + adapter_number: int, + port_number: int, + nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO], + node: DockerVM = Depends(dep_node), ) -> Union[schemas.UDPNIO, schemas.BridgeNIO]: """ Add a NIO (Network Input/Output) to the node. @@ -284,8 +286,10 @@ async def create_docker_node_nio( dependencies=[Depends(compute_authentication)], ) async def update_docker_node_nio( - adapter_number: int, port_number: int, nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO], - node: DockerVM = Depends(dep_node) + adapter_number: int, + port_number: int, + nio_data: Union[schemas.UDPNIO, schemas.BridgeNIO], + node: DockerVM = Depends(dep_node), ) -> Union[schemas.UDPNIO, schemas.BridgeNIO]: """ Update a NIO (Network Input/Output) on the node. diff --git a/gns3server/api/routes/compute/ethernet_switch_nodes.py b/gns3server/api/routes/compute/ethernet_switch_nodes.py index 47b8b8604..ba10182d8 100644 --- a/gns3server/api/routes/compute/ethernet_switch_nodes.py +++ b/gns3server/api/routes/compute/ethernet_switch_nodes.py @@ -103,20 +103,20 @@ async def update_ethernet_switch( Update an Ethernet switch. """ - node_data = jsonable_encoder(node_data, exclude_unset=True) - if "name" in node_data and node.name != node_data["name"]: - node.name = node_data["name"] - if "usage" in node_data: - node.usage = node_data["usage"] - if "ports_mapping" in node_data: + data = jsonable_encoder(node_data, exclude_unset=True) + if "name" in data and node.name != data["name"]: + node.name = data["name"] + if "usage" in data: + node.usage = data["usage"] + if "ports_mapping" in data: # capture the mapping before the setter replaces it: the VLAN # reconcile diffs old against new to touch only what changed, in # place (the setter itself keeps the port-count guard) previous_mapping = [dict(port) for port in node.ports_mapping] - node.ports_mapping = node_data["ports_mapping"] + node.ports_mapping = data["ports_mapping"] await node.update_port_settings(previous_mapping) - if "console_type" in node_data: - node.console_type = node_data["console_type"] + if "console_type" in data: + node.console_type = data["console_type"] node.updated() return node.asdict() diff --git a/gns3server/compute/docker/docker_kernel_datapath.py b/gns3server/compute/docker/docker_kernel_datapath.py index f0b215035..06ce57b44 100644 --- a/gns3server/compute/docker/docker_kernel_datapath.py +++ b/gns3server/compute/docker/docker_kernel_datapath.py @@ -24,14 +24,13 @@ markers, attach/detach, adapter carrier — lives in """ import contextlib +import logging from gns3server.compute.kernel_datapath import KernelDatapathMixin from gns3server.compute.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError - -from .docker_error import DockerError from gns3server.utils.kernel_anchor import anchor_suffix, kernel_anchor_name -import logging +from .docker_error import DockerError log = logging.getLogger(__name__) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 9d123478c..3d6f611cb 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -33,14 +33,13 @@ import aiohttp import psutil from gns3server.compute.compute_error import ComputeError -from gns3server.compute.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError +from gns3server.compute.ubridge.ubridge_error import UbridgeNamespaceError from gns3server.utils import int_to_macaddress, macaddress_to_int from gns3server.utils.asyncio import monitor_process, wait_for_file_creation, wait_run_in_executor from gns3server.utils.asyncio.raw_command_server import AsyncioRawCommandServer from gns3server.utils.asyncio.ssh_server import AsyncioSSHServer from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer from gns3server.utils.hostname import is_rfc1123_hostname_valid -from gns3server.utils.packet_filter_validation import KERNEL_UNSUPPORTED_FILTERS from ..adapters.ethernet_adapter import EthernetAdapter from ..base_node import BaseNode diff --git a/gns3server/compute/docker/iol_docker_vm.py b/gns3server/compute/docker/iol_docker_vm.py index 212ef1390..a41b66e56 100644 --- a/gns3server/compute/docker/iol_docker_vm.py +++ b/gns3server/compute/docker/iol_docker_vm.py @@ -672,4 +672,3 @@ class IOLDockerVM(VendorDockerVM): if self._kernel_host_ifc(adapter_number, port_number) is not None: await KernelDatapathMixin._set_adapter_carrier(self, adapter_number, connected, port_number) - diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index f9e4e8bae..05c055b13 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -42,10 +42,10 @@ from ...base_node import BaseNode from ...error import NodeError from ...kernel_datapath import KernelDatapathMixin from ...nios.nio_bridge import NIOBridge -from ...nios.nio_tap import NIOTAP from ...ubridge.ubridge_error import UbridgeError from ..adapters.adapter import ETHERNET_ADAPTERS, ETHERNET_WICS from ..dynamips_error import DynamipsError +from ..nios.nio_tap import NIOTAP class Router(KernelDatapathMixin, BaseNode): diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index af3d82c1a..ec7aed43b 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -22,6 +22,7 @@ order to run an IOU VM. import asyncio import binascii import configparser +import contextlib import functools import glob import hashlib @@ -951,9 +952,7 @@ class IOUVM(KernelDatapathMixin, BaseNode): raise self._kernel_error( "Bay {bay}/{unit} of IOU '{name}' has no TAP anchor to carry a kernel link " "(serial ports stay on the relay datapath, and a node whose uBridge lacks " - "iol_bridge add_nio_tap runs relay-only)".format( - bay=adapter_number, unit=port_number, name=self._name - ) + "iol_bridge add_nio_tap runs relay-only)".format(bay=adapter_number, unit=port_number, name=self._name) ) await self._ubridge_send( 'iol_bridge add_nio_tap {name} {iol_id} {bay} {unit} "{tap}"'.format( diff --git a/gns3server/compute/kernel_datapath.py b/gns3server/compute/kernel_datapath.py index b73f1afc3..09e80ec4a 100644 --- a/gns3server/compute/kernel_datapath.py +++ b/gns3server/compute/kernel_datapath.py @@ -41,17 +41,16 @@ A node type using this mixin provides: failures surface as ``DockerError`` / ``QemuError`` rather than ``NodeError``. """ -import os import contextlib +import logging +import os -from gns3server.utils.tc_capabilities import FILTER_EBPF_MODES, parse_tc_capabilities, usable_ebpf_modes -from gns3server.compute.ubridge.ubridge_error import UbridgeError from gns3server.compute.error import NodeError +from gns3server.compute.ubridge.ubridge_error import UbridgeError +from gns3server.utils.tc_capabilities import FILTER_EBPF_MODES, parse_tc_capabilities, usable_ebpf_modes from .nios.nio_bridge import NIOBridge -import logging - log = logging.getLogger(__name__) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 873986ace..410094348 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -20,6 +20,7 @@ order to run a QEMU VM. """ import asyncio +import contextlib import json import logging import math @@ -53,6 +54,7 @@ from ..kernel_datapath import KernelDatapathMixin from ..nios.nio_bridge import NIOBridge from ..nios.nio_tap import NIOTAP from ..nios.nio_udp import NIOUDP +from ..ubridge.ubridge_error import UbridgeError from .qemu_error import QemuError from .utils.qcow2 import Qcow2, Qcow2Error from .utils.ziputils import pack_zip, unpack_zip diff --git a/gns3server/compute/ubridge/tc_probe.py b/gns3server/compute/ubridge/tc_probe.py index 7e5e19d1c..fdff74d6d 100644 --- a/gns3server/compute/ubridge/tc_probe.py +++ b/gns3server/compute/ubridge/tc_probe.py @@ -32,29 +32,31 @@ failure (missing binary, old build answering "Unknown command", timeout) means unknown, reported as None and cached as such. """ -import os -import uuid -import shutil import asyncio -import tempfile -import logging import contextlib +import logging +import os +import shutil +import tempfile +import uuid +from typing import Optional from gns3server.config import Config from gns3server.utils.tc_capabilities import parse_tc_capabilities + from .hypervisor import Hypervisor from .ubridge_error import UbridgeError log = logging.getLogger(__name__) # binary identity -> parsed report (None = probed and unusable) -_cache = {} +_cache: dict[tuple[str, int, int], Optional[dict[str, str]]] = {} # binary identity -> whether the tap module works (None = unknown) -_tap_cache = {} +_tap_cache: dict[tuple[str, int, int], Optional[bool]] = {} # binary identity -> whether iol_bridge can bind a port to a TAP (None = unknown) -_iol_tap_cache = {} +_iol_tap_cache: dict[tuple[str, int, int], Optional[bool]] = {} # binary identity -> whether the bridge module can release a named TAP NIO (None = unknown) -_bridge_tap_cache = {} +_bridge_tap_cache: dict[tuple[str, int, int], Optional[bool]] = {} _lock = asyncio.Lock() @@ -252,9 +254,7 @@ async def _probe_iol_tap(path, config, timeout): await hypervisor.send(f"iol_bridge create {bridge} {_IOL_PROBE_BRIDGE_ID}") try: await hypervisor.send(f"tap create {tap}") - await hypervisor.send( - f"iol_bridge add_nio_tap {bridge} {_IOL_PROBE_BRIDGE_ID - 1} 0 0 {tap}" - ) + await hypervisor.send(f"iol_bridge add_nio_tap {bridge} {_IOL_PROBE_BRIDGE_ID - 1} 0 0 {tap}") await hypervisor.send(f"iol_bridge delete_nio_tap {bridge} 0 0") finally: # iol_bridge delete releases every port NIO (the TAP fd), diff --git a/gns3server/controller/udp_link.py b/gns3server/controller/udp_link.py index 391bb04e3..8e607f1b5 100644 --- a/gns3server/controller/udp_link.py +++ b/gns3server/controller/udp_link.py @@ -23,7 +23,6 @@ from gns3server.config import Config from gns3server.utils.application_id import is_iol_runner_environment from gns3server.utils.kernel_anchor import kernel_anchor_name, kernel_anchor_type, kernel_cascade_names from gns3server.utils.packet_filter_validation import ( - KERNEL_UNSUPPORTED_FILTERS, split_kernel_only_features, validate_bpf_syntax, ) diff --git a/gns3server/schemas/__init__.py b/gns3server/schemas/__init__.py index e66a39a4a..0a26874a5 100644 --- a/gns3server/schemas/__init__.py +++ b/gns3server/schemas/__init__.py @@ -101,7 +101,17 @@ from .compute.iou_nodes import IOU, IOUCreate, IOUStart, IOUUpdate from .compute.nat_nodes import NAT, NATCreate, NATUpdate # Compute schemas -from .compute.nios import AnchorNIO, TAPNIO, UDPNIO, BridgeNIO, BatchNIOCreate, BatchNIOEntry, EthernetNIO, MarkerRebuild, MarkerToggle +from .compute.nios import ( + TAPNIO, + UDPNIO, + AnchorNIO, + BatchNIOCreate, + BatchNIOEntry, + BridgeNIO, + EthernetNIO, + MarkerRebuild, + MarkerToggle, +) from .compute.qemu_nodes import Qemu, QemuCreate, QemuUpdate from .compute.virtualbox_nodes import VirtualBox, VirtualBoxCreate, VirtualBoxUpdate from .compute.vmware_nodes import VMware, VMwareCreate, VMwareUpdate @@ -159,12 +169,12 @@ __all__ = [ "ATMSwitch", "ATMSwitchCreate", "ATMSwitchUpdate", + "AnchorNIO", "ApiKeyCreate", "Appliance", "ApplianceVersion", "ApplianceVersionV8", "AutoIdlePC", - "AnchorNIO", "BatchNIOCreate", "BatchNIOEntry", "BridgeNIO", diff --git a/gns3server/schemas/controller/computes.py b/gns3server/schemas/controller/computes.py index 4924827e5..5742e4391 100644 --- a/gns3server/schemas/controller/computes.py +++ b/gns3server/schemas/controller/computes.py @@ -16,11 +16,12 @@ import uuid from enum import Enum -from typing import Any, List, Optional, Union +from typing import Any, Optional, Union from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator from .base import DateTimeModelMixin + # The capabilities payload has one definition (controller.capabilities): a # second, private copy here silently stripped every field it did not know # about from GET /computes/{id} — which is how ubridge_tap and diff --git a/gns3server/utils/packet_filter_validation.py b/gns3server/utils/packet_filter_validation.py index c9c3a6d96..cee97e59c 100644 --- a/gns3server/utils/packet_filter_validation.py +++ b/gns3server/utils/packet_filter_validation.py @@ -5,7 +5,7 @@ Packet filter parameter validation utilities. import logging import re import subprocess -from typing import Any, Dict, List, Optional, Set, Tuple +from typing import Any, Dict, List, Set, Tuple log = logging.getLogger(__name__) @@ -47,7 +47,7 @@ _RATE_UNITS = { _RATE_MAX_BITS = 100 * 1000**3 -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. @@ -142,9 +142,7 @@ def _validate_int_parameter(filter_type: str, rules: dict, i: int, value: Any) - else: int_value = int(value) except (ValueError, TypeError): - raise FilterValidationError( - f"{filter_type} parameter {rules['names'][i]} must be an integer, got: {value}" - ) + raise FilterValidationError(f"{filter_type} parameter {rules['names'][i]} must be an integer, got: {value}") # Range validation min_val, max_val = rules["ranges"][i] @@ -441,7 +439,9 @@ def validate_all_filters(filters: Dict[str, List[Any]]) -> None: if "reorder" in filters and "delay" not in filters: raise FilterValidationError("reorder requires delay") if "gemodel" in filters and "packet_loss" in filters: - raise FilterValidationError("gemodel and packet_loss are mutually exclusive (both map to the netem loss keyword)") + raise FilterValidationError( + "gemodel and packet_loss are mutually exclusive (both map to the netem loss keyword)" + ) window = filters.get("window_drop") if window and len(window) >= 4 and int(window[3]) < int(window[1]): raise FilterValidationError("window_drop period must be greater than or equal to the outage length") diff --git a/pyproject.toml b/pyproject.toml index 65c67d1bd..fc2d9b861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -254,6 +254,7 @@ extend-immutable-calls = [ "tests/controller/test_project.py" = ["RUF015"] "tests/controller/test_snapshot.py" = ["RUF015"] "tests/controller/test_udp_link.py" = ["B006"] +"tests/e2e/test_docker_link_local_frames.py" = ["S603"] "tests/test_server.py" = ["E722"] "tests/test_topologies.py" = ["B011"] "tests/test_utils.py" = ["F403", "F405"] diff --git a/tests/agent/mcp/test_device_tools.py b/tests/agent/mcp/test_device_tools.py index e68075d3c..3a4308053 100644 --- a/tests/agent/mcp/test_device_tools.py +++ b/tests/agent/mcp/test_device_tools.py @@ -246,7 +246,9 @@ class TestDuplicateDeviceMerging: nornir = MagicMock() nornir.run.return_value = _aggregated_result({"R1": "merged output"}) with ( - patch(f"{CONFIG_MOD}.get_device_ports_from_topology", return_value={"R1": {"port": 5000, "node_type": "iou"}}), + patch( + f"{CONFIG_MOD}.get_device_ports_from_topology", return_value={"R1": {"port": 5000, "node_type": "iou"}} + ), patch.object(ExecuteMultipleDeviceConfigCommands, "_initialize_nornir", return_value=nornir), ): result = ExecuteMultipleDeviceConfigCommands()._run( @@ -310,7 +312,9 @@ class TestDuplicateDeviceMerging: nornir = MagicMock() nornir.run.return_value = _aggregated_result({"R1": "ok"}) with ( - patch(f"{DISPLAY_MOD}.get_device_ports_from_topology", return_value={"R1": {"port": 5000, "node_type": "iou"}}), + patch( + f"{DISPLAY_MOD}.get_device_ports_from_topology", return_value={"R1": {"port": 5000, "node_type": "iou"}} + ), patch.object(ExecuteMultipleDeviceCommands, "_initialize_nornir", return_value=nornir), patch(f"{DISPLAY_MOD}.filter_forbidden_commands", side_effect=fake_filter), ): diff --git a/tests/agent/mcp/test_locators.py b/tests/agent/mcp/test_locators.py index ee4bd1cee..993000ab8 100644 --- a/tests/agent/mcp/test_locators.py +++ b/tests/agent/mcp/test_locators.py @@ -69,8 +69,12 @@ def test_uuid_params_pass_through_without_calls(): def test_project_name_resolved(): """A name-shaped project_id becomes the project's UUID.""" - run, calls = _run_factory(projects=[{"project_id": "11111111-1111-1111-1111-111111111111", "name": "other"}, - {"project_id": PROJECT_ID, "name": "my-lab"}]) + run, calls = _run_factory( + projects=[ + {"project_id": "11111111-1111-1111-1111-111111111111", "name": "other"}, + {"project_id": PROJECT_ID, "name": "my-lab"}, + ] + ) params, error = resolve_locators({"project_id": "my-lab"}, run) assert error is None assert params["project_id"] == PROJECT_ID @@ -88,8 +92,9 @@ def test_project_name_not_found_lists_known_names(): def test_node_name_resolved_within_project(): - run, calls = _run_factory(projects=[{"project_id": PROJECT_ID, "name": "my-lab"}], - nodes=[{"node_id": NODE_ID, "name": "FRRDocker-4"}]) + run, calls = _run_factory( + projects=[{"project_id": PROJECT_ID, "name": "my-lab"}], nodes=[{"node_id": NODE_ID, "name": "FRRDocker-4"}] + ) params, error = resolve_locators({"project_id": "my-lab", "node_id": "FRRDocker-4"}, run) assert error is None assert params["project_id"] == PROJECT_ID @@ -125,11 +130,10 @@ def test_handler_failure_during_resolution_is_reported(): def test_did_you_mean_appended_for_transcription_slip(): """The 2c9e -> 3b9e class of typo earns exactly one hint.""" - run, _calls = _run_factory(projects=[{"project_id": PROJECT_ID, "name": "my-lab"}], - nodes=[{"node_id": NODE_ID, "name": "FRRDocker-4"}]) - result = add_did_you_mean( - {"error": f"Node ID {NODE_ID_TYPO} doesn't exist"}, {"project_id": PROJECT_ID}, run + run, _calls = _run_factory( + projects=[{"project_id": PROJECT_ID, "name": "my-lab"}], nodes=[{"node_id": NODE_ID, "name": "FRRDocker-4"}] ) + result = add_did_you_mean({"error": f"Node ID {NODE_ID_TYPO} doesn't exist"}, {"project_id": PROJECT_ID}, run) assert f"Did you mean node 'FRRDocker-4' ({NODE_ID})" in result["error"] # original message preserved assert NODE_ID_TYPO in result["error"] @@ -144,9 +148,7 @@ def test_no_hint_when_too_far_or_ambiguous(): two = [NODE_ID, "b78a2630-2b9e-4b43-9397-c855c8053d1a"] # both within distance 2 of the typo run2, _calls2 = _run_factory(nodes=[{"node_id": i, "name": f"n{i}"} for i in two]) - result2 = add_did_you_mean( - {"error": f"Node ID {NODE_ID_TYPO} doesn't exist"}, {"project_id": PROJECT_ID}, run2 - ) + result2 = add_did_you_mean({"error": f"Node ID {NODE_ID_TYPO} doesn't exist"}, {"project_id": PROJECT_ID}, run2) assert "Did you mean" not in result2["error"] diff --git a/tests/api/routes/compute/test_capabilities.py b/tests/api/routes/compute/test_capabilities.py index 97d7948c5..e0017cedf 100644 --- a/tests/api/routes/compute/test_capabilities.py +++ b/tests/api/routes/compute/test_capabilities.py @@ -170,7 +170,9 @@ class TestCapabilitiesRoutes: async def fake_bridge_tap_probe(): return True - monkeypatch.setattr("gns3server.api.routes.compute.capabilities.probe_bridge_tap_support", fake_bridge_tap_probe) + monkeypatch.setattr( + "gns3server.api.routes.compute.capabilities.probe_bridge_tap_support", fake_bridge_tap_probe + ) monkeypatch.setattr("gns3server.api.routes.compute.capabilities.probe_tap_support", _no_tap_probe) monkeypatch.setattr("gns3server.api.routes.compute.capabilities.probe_iol_tap_support", _no_tap_probe) response = await compute_client.get(app.url_path_for("compute:get_capabilities")) diff --git a/tests/compute/docker/test_docker_kernel_datapath.py b/tests/compute/docker/test_docker_kernel_datapath.py index f54be3dc9..c50c35c6a 100644 --- a/tests/compute/docker/test_docker_kernel_datapath.py +++ b/tests/compute/docker/test_docker_kernel_datapath.py @@ -23,23 +23,18 @@ fallback and cleanup. import os import uuid -from unittest.mock import MagicMock, call +from unittest.mock import MagicMock, call, patch import pytest import pytest_asyncio -from unittest.mock import patch - -from tests.utils import AsyncioMagicMock - -from gns3server.compute.docker import Docker -from gns3server.compute.docker.docker_vm import DockerVM -from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error from gns3server.compute.compute_error import ComputeError -from gns3server.compute.nios.nio_udp import NIOUDP +from gns3server.compute.docker import Docker +from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error +from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.nios.nio_bridge import NIOBridge from gns3server.compute.ubridge.ubridge_error import UbridgeError - +from tests.utils import AsyncioMagicMock BRIDGE = "gns3a1b2c3d4e5f" @@ -1373,7 +1368,13 @@ def test_create_nio_udp_rejects_kernel_only_filters(vm): with pytest.raises(ComputeError, match="kernel-datapath"): vm.manager.create_nio( - {"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", "filters": {"gemodel": [100, 0, 30]}} + { + "type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1", + "filters": {"gemodel": [100, 0, 30]}, + } ) diff --git a/tests/compute/docker/test_iol_docker_kernel_datapath.py b/tests/compute/docker/test_iol_docker_kernel_datapath.py index 8c7a4f7db..b3f692706 100644 --- a/tests/compute/docker/test_iol_docker_kernel_datapath.py +++ b/tests/compute/docker/test_iol_docker_kernel_datapath.py @@ -14,19 +14,15 @@ Image-free: everything is asserted against the uBridge command stream and the anchor naming contract. """ -import uuid +from unittest.mock import MagicMock, patch import pytest import pytest_asyncio -from unittest.mock import MagicMock, patch - -from tests.utils import AsyncioMagicMock - from gns3server.compute.docker import Docker from gns3server.compute.docker.docker_error import DockerError from gns3server.compute.docker.iol_docker_vm import IOLDockerVM - +from tests.utils import AsyncioMagicMock NODE_ID = "00010203-0405-0607-0809-0a0b0c0d0e0f" TAP00 = "gx00010203e0p0" # the vm fixture's node id, bay 0 unit 0 @@ -296,7 +292,9 @@ async def test_remove_nio_binding_kernel_releases_the_port_leg_first(vm): commands = _commands(vm) assert f"bridge stop {PORT_BRIDGE}" in commands assert f'bridge delete_nio_tap {PORT_BRIDGE} "{TAP00}"' in commands - assert commands.index(f"bridge stop {PORT_BRIDGE}") < commands.index(f'bridge delete_nio_tap {PORT_BRIDGE} "{TAP00}"') + assert commands.index(f"bridge stop {PORT_BRIDGE}") < commands.index( + f'bridge delete_nio_tap {PORT_BRIDGE} "{TAP00}"' + ) assert f'tc reset "{TAP00}"' in commands assert f'brctl delif "{BRIDGE}" "{TAP00}"' in commands assert f'brctl delete "{BRIDGE}"' in commands diff --git a/tests/compute/docker/test_vendor_docker_vm.py b/tests/compute/docker/test_vendor_docker_vm.py index f508bcf29..b45ba8c0e 100644 --- a/tests/compute/docker/test_vendor_docker_vm.py +++ b/tests/compute/docker/test_vendor_docker_vm.py @@ -908,6 +908,7 @@ async def test_create_reparse_refreshes_env_knobs(compute_project, manager): # Kernel-datapath (NIOBridge) rejection # --------------------------------------------------------------------------- + @pytest.mark.asyncio async def test_unix_socket_nio_rejects_kernel_datapath_link(compute_project, manager): diff --git a/tests/compute/dynamips/test_dynamips_kernel_datapath.py b/tests/compute/dynamips/test_dynamips_kernel_datapath.py index 885780524..d09932eea 100644 --- a/tests/compute/dynamips/test_dynamips_kernel_datapath.py +++ b/tests/compute/dynamips/test_dynamips_kernel_datapath.py @@ -25,27 +25,24 @@ hypervisor's fds released before the TAPs are unpersisted — QEMU's order, since here it is Dynamips that holds them). """ +import contextlib import os import shutil -import contextlib +from unittest.mock import MagicMock, patch import pytest import pytest_asyncio -from unittest.mock import MagicMock, patch - -from tests.utils import AsyncioMagicMock - from gns3server.compute.dynamips import Dynamips -from gns3server.compute.dynamips.nodes.router import Router -from gns3server.compute.dynamips.dynamips_error import DynamipsError from gns3server.compute.dynamips.adapters.gt96100_fe import GT96100_FE from gns3server.compute.dynamips.adapters.pa_4t import PA_4T from gns3server.compute.dynamips.adapters.pa_fe_tx import PA_FE_TX from gns3server.compute.dynamips.adapters.wic_1enet import WIC_1ENET -from gns3server.compute.ubridge.ubridge_error import UbridgeError +from gns3server.compute.dynamips.dynamips_error import DynamipsError +from gns3server.compute.dynamips.nodes.router import Router from gns3server.compute.nios.nio_bridge import NIOBridge - +from gns3server.compute.ubridge.ubridge_error import UbridgeError +from tests.utils import AsyncioMagicMock NODE_ID = "00010203-0405-0607-0809-0a0b0c0d0e0f" BRIDGE = "gns3a1b2c3d4e5f" @@ -237,7 +234,7 @@ async def test_prepare_tap_datapath_wires_nios_bound_while_stopped(router): hypervisor = _hypervisor_commands(router) assert any(c.startswith("nio create_tap tap-") and c.endswith(f"{TAP00}") for c in hypervisor) - assert any(c.startswith(f"vm slot_add_nio_binding \"R1\" 0 0 tap-") for c in hypervisor) + assert any(c.startswith('vm slot_add_nio_binding "R1" 0 0 tap-') for c in hypervisor) router._ubridge_send.assert_any_call(f'brctl addif "{BRIDGE}" "{TAP00}"') assert (0, 0) in router._tap_nios @@ -489,9 +486,7 @@ async def test_stop_ubridge_releases_hypervisor_fds_before_deleting_taps(router) router._ubridge_send.reset_mock() router._hypervisor.commands.clear() - with patch( - "gns3server.compute.base_node.BaseNode._stop_ubridge", new=AsyncioMagicMock() - ) as stop: + with patch("gns3server.compute.base_node.BaseNode._stop_ubridge", new=AsyncioMagicMock()) as stop: await router._stop_ubridge() hypervisor = _hypervisor_commands(router) diff --git a/tests/compute/iou/test_iou_kernel_datapath.py b/tests/compute/iou/test_iou_kernel_datapath.py index 40645b279..427f19502 100644 --- a/tests/compute/iou/test_iou_kernel_datapath.py +++ b/tests/compute/iou/test_iou_kernel_datapath.py @@ -22,19 +22,16 @@ it, kernel-link enslavement, the relay fallback for a uBridge without the command, and the reverse-of-QEMU stop order (uBridge holds the anchor fds). """ +from unittest.mock import MagicMock, patch + import pytest import pytest_asyncio -from unittest.mock import MagicMock, patch - -from tests.utils import AsyncioMagicMock - from gns3server.compute.iou import IOU -from gns3server.compute.iou.iou_vm import IOUVM from gns3server.compute.iou.iou_error import IOUError -from gns3server.compute.nios.nio_bridge import NIOBridge +from gns3server.compute.iou.iou_vm import IOUVM from gns3server.compute.ubridge.ubridge_error import UbridgeError - +from tests.utils import AsyncioMagicMock NODE_ID = "00010203-0405-0607-0809-0a0b0c0d0e0f" BRIDGE = "gns3a1b2c3d4e5f" diff --git a/tests/compute/qemu/test_qemu_kernel_datapath.py b/tests/compute/qemu/test_qemu_kernel_datapath.py index ccee4796c..ba6ea96ec 100644 --- a/tests/compute/qemu/test_qemu_kernel_datapath.py +++ b/tests/compute/qemu/test_qemu_kernel_datapath.py @@ -24,21 +24,16 @@ legacy socket-netdev fallback for a uBridge without the tap module. import asyncio import os import stat -import sys +from unittest.mock import MagicMock, patch import pytest import pytest_asyncio -from unittest.mock import MagicMock, patch - -from tests.utils import asyncio_patch, AsyncioMagicMock - from gns3server.compute.qemu import Qemu -from gns3server.compute.qemu.qemu_vm import QemuVM from gns3server.compute.qemu.qemu_error import QemuError -from gns3server.compute.nios.nio_bridge import NIOBridge +from gns3server.compute.qemu.qemu_vm import QemuVM from gns3server.compute.ubridge.ubridge_error import UbridgeError - +from tests.utils import AsyncioMagicMock, asyncio_patch BRIDGE = "gns3a1b2c3d4e5f" TAP0 = "gq00010203e0p0" # the vm fixture's node id, adapter 0 @@ -272,7 +267,7 @@ async def test_update_nio_binding_reapplies_filters_on_the_anchor(vm): vm._ubridge_send.assert_any_call(f'tc reset "{TAP0}"') vm._ubridge_send.assert_any_call(f'tc netem set "{TAP0}" delay 10') - assert not any(f"brctl addif" in str(c) for c in vm._ubridge_send.call_args_list) + assert not any("brctl addif" in str(c) for c in vm._ubridge_send.call_args_list) @pytest.mark.asyncio diff --git a/tests/compute/ubridge/test_tc_probe.py b/tests/compute/ubridge/test_tc_probe.py index 9408c70c0..1cdeba245 100644 --- a/tests/compute/ubridge/test_tc_probe.py +++ b/tests/compute/ubridge/test_tc_probe.py @@ -25,8 +25,8 @@ from gns3server.compute.ubridge import tc_probe from gns3server.compute.ubridge.tc_probe import ( probe_bridge_tap_support, probe_iol_tap_support, - probe_tc_capabilities, probe_tap_support, + probe_tc_capabilities, ) from gns3server.compute.ubridge.ubridge_error import UbridgeError @@ -65,7 +65,7 @@ class FakeHypervisor: if self.error: raise UbridgeError(self.error) if self.error_prefix and command.startswith(self.error_prefix): - raise UbridgeError(f"202-Unknown command") + raise UbridgeError("202-Unknown command") if command.startswith("bridge delete_nio_tap "): if FakeHypervisor.bridge_tap_error: raise UbridgeError(FakeHypervisor.bridge_tap_error) diff --git a/tests/controller/test_compute.py b/tests/controller/test_compute.py index 17b96b4ad..6a8f3b27b 100644 --- a/tests/controller/test_compute.py +++ b/tests/controller/test_compute.py @@ -381,7 +381,18 @@ async def test_json(compute): "disk_usage_percent": 0, "connected": True, "last_error": None, - "capabilities": {"version": "", "platform": "", "cpus": 0, "memory": 0, "disk_size": 0, "node_types": [], "ubridge_tc": None, "ubridge_tap": None, "ubridge_iol_tap": None, "ubridge_bridge_tap": None}, + "capabilities": { + "version": "", + "platform": "", + "cpus": 0, + "memory": 0, + "disk_size": 0, + "node_types": [], + "ubridge_tc": None, + "ubridge_tap": None, + "ubridge_iol_tap": None, + "ubridge_bridge_tap": None, + }, } assert compute.asdict(topology_dump=True) == { "compute_id": "my_compute_id", diff --git a/tests/controller/test_kernel_datapath_link.py b/tests/controller/test_kernel_datapath_link.py index 9c0ababfe..7d326f896 100644 --- a/tests/controller/test_kernel_datapath_link.py +++ b/tests/controller/test_kernel_datapath_link.py @@ -20,14 +20,15 @@ Controller-side kernel-datapath link selection: eligibility of Docker-to- Docker links, NIO emission and the filters/markers guards. """ -import pytest from unittest.mock import MagicMock, patch +import pytest + from gns3server.config import Config -from gns3server.controller.udp_link import UDPLink -from gns3server.controller.ports.ethernet_port import EthernetPort -from gns3server.controller.node import Node from gns3server.controller.controller_error import ControllerError +from gns3server.controller.node import Node +from gns3server.controller.ports.ethernet_port import EthernetPort +from gns3server.controller.udp_link import UDPLink def _node(project, compute, name, node_type="docker", status="stopped", environment=None): diff --git a/tests/e2e/harness.py b/tests/e2e/harness.py index a7f821565..9ebb0e351 100644 --- a/tests/e2e/harness.py +++ b/tests/e2e/harness.py @@ -903,7 +903,9 @@ class DockerDaemon: ) response = conn.getresponse() payload = response.read() - assert response.status == 200, f"docker cp into {container_id[:12]} -> HTTP {response.status}: {payload[:300]!r}" + assert response.status == 200, ( + f"docker cp into {container_id[:12]} -> HTTP {response.status}: {payload[:300]!r}" + ) except (OSError, http.client.HTTPException) as e: raise AssertionError(f"docker cp into {container_id[:12]} failed: {e}") from None finally: diff --git a/tests/e2e/test_docker_link_local_frames.py b/tests/e2e/test_docker_link_local_frames.py index bd4b2a394..8ac4a5404 100644 --- a/tests/e2e/test_docker_link_local_frames.py +++ b/tests/e2e/test_docker_link_local_frames.py @@ -47,6 +47,7 @@ The negative control for the old behaviour is the uBridge suite's import functools import io import os +import shutil import subprocess import tarfile import tempfile @@ -246,9 +247,12 @@ def _llprobe(): binary = os.path.join(directory, "llprobe") with open(source, "w") as f: f.write(_LLPROBE_C) + cc = shutil.which("cc") + if cc is None: + pytest.skip("cannot build the raw-frame probe (no C compiler found)") try: - subprocess.run(["cc", "-static", "-O2", "-o", binary, source], check=True, capture_output=True) - except (FileNotFoundError, subprocess.CalledProcessError) as e: + subprocess.run([cc, "-static", "-O2", "-o", binary, source], check=True, capture_output=True) + except subprocess.CalledProcessError as e: pytest.skip(f"cannot build the raw-frame probe (cc -static): {e}") return binary @@ -270,9 +274,7 @@ def _upload_probe(daemon, *container_ids): def _recv_counts(daemon, container_id, window, macs): """Run the receiver to completion and parse its "=" lines.""" - code, output = daemon.exec( - container_id, ["/tmp/llprobe", "recv", "eth0", str(window), *macs], timeout=window + 30 - ) + code, output = daemon.exec(container_id, ["/tmp/llprobe", "recv", "eth0", str(window), *macs], timeout=window + 30) assert code == 0, output return {line.split("=")[0]: int(line.split("=")[1]) for line in output.strip().splitlines() if "=" in line} @@ -316,7 +318,7 @@ def test_docker_link_local_frames(): print(".. starting containers") for node in nodes: compute.call("POST", f"/projects/{pid}/nodes/{node['node_id']}/start") - for node, address in zip(nodes, (R1_IP, R2_IP)): + for node, address in zip(nodes, (R1_IP, R2_IP), strict=True): harness.docker_wait_address(daemon, node["container_id"], address) a1 = harness.docker_anchor_name(n1["node_id"], *ETH)