fix: repair imports lost in the 3.1 rebase and meet the new lint baseline

The rebase onto upstream 3.1 (mypy + ruff I001/F401 waves) left the tree
short of the newly enforced baselines in three ways:

* contextlib / UbridgeError imports dropped as false duplicates during
  conflict resolution left NameErrors in iou_vm and qemu_vm (F821);
  the Dynamips router grabbed the compute-level NIOTAP instead of the
  hypervisor one, breaking TAP anchoring (test failure).
* unused imports (KERNEL_UNSUPPORTED_FILTERS, UbridgeError, List) and
  unsorted blocks left behind where the stack's later commits moved the
  last usage elsewhere — ruff I001/F401/RUF022 now enforce these.
* mypy, now baseline-free: cache dict annotations in tc_probe, the
  locators candidate list, the resolved-params None branch in the MCP
  runner, and validate_bpf_syntax keeps the upstream Dict[str, Any]
  return (the strict Optional[str] was wrong for the "valid" bool entry).

The e2e raw-frame probe resolves its compiler via shutil.which (S607)
and zips with strict= (B905); the remaining subprocess call is S603 by
nature and gets a per-file ignore like other test files.

ruff check/format clean, mypy at upstream parity (no new errors),
full unit suite 2167 passed.
This commit is contained in:
YueGuobin 2026-10-05 11:54:57 +08:00
parent e13e5f3804
commit 7d04d08dba
No known key found for this signature in database
35 changed files with 165 additions and 136 deletions

View File

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

View File

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

View File

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

View File

@ -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)}]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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),
):

View File

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

View File

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

View File

@ -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]},
}
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 "<mac>=<count>" 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)