mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
feat(docker): wire Docker-to-Docker links through a kernel bridge
Eligible links (both endpoints Docker, same compute, no filters/markers, stopped nodes, no unix-socket NIO containers) are wired as veth pairs enslaved into a per-link Linux kernel bridge (gns3 + 11 hex of the link id, filling IFNAMSIZ) instead of the uBridge UDP relay, so frames flow entirely in the kernel. - NIOBridge NIO type: the controller decides the datapath in UDPLink._prepare and both computes enslave their deterministic gv*/gc* veth pair via uBridge's docker/brctl modules - carrier/suspend = host-end admin state; both endpoints race-safely create/delete the bridge (EEXIST/EBUSY/ENOENT tolerated) - veth adapters can still attach relay (UDP) links on a running node: lazy relay bridge over AF_PACKET on the host end (add_nio_ethernet) - filters/markers/capture are rejected with explicit errors on this datapath (they live in the uBridge relay); vendor unix-socket containers are excluded; Server.enable_kernel_datapath is the kill-switch (default on) - host-end veths survive container death and are removed on stop plus swept by name on the next start (crash/stale recovery)
This commit is contained in:
13 files changed
+1031
-21
No files matched your search
@@ -0,0 +1,373 @@
|
||||
#
|
||||
# Copyright (C) 2026 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
Kernel-datapath (NIOBridge) adapter wiring for Docker containers: veth pair
|
||||
creation, per-link kernel bridge enslavement, carrier semantics, relay
|
||||
fallback and cleanup.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
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
|
||||
from gns3server.compute.compute_error import ComputeError
|
||||
from gns3server.compute.nios.nio_udp import NIOUDP
|
||||
from gns3server.compute.nios.nio_bridge import NIOBridge
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
|
||||
|
||||
BRIDGE = "gns3a1b2c3d4e5f"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def manager(port_manager):
|
||||
|
||||
m = Docker.instance()
|
||||
m.port_manager = port_manager
|
||||
return m
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def vm(compute_project, manager):
|
||||
|
||||
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", aux_type="none")
|
||||
vm._cid = "e90e34656842"
|
||||
vm.mac_address = '02:42:3d:b7:93:00'
|
||||
vm._start_interface_monitor = AsyncioMagicMock()
|
||||
vm._stop_interface_monitor = AsyncioMagicMock()
|
||||
return vm
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NIO factory
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_create_nio_bridge(vm):
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
assert isinstance(nio, NIOBridge)
|
||||
assert nio.bridge == BRIDGE
|
||||
assert nio.suspend is False
|
||||
|
||||
|
||||
def test_create_nio_bridge_rejects_filters(vm):
|
||||
|
||||
with pytest.raises(ComputeError):
|
||||
vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE, "filters": {"loss": [10]}})
|
||||
|
||||
|
||||
def test_create_nio_bridge_rejects_markers(vm):
|
||||
|
||||
with pytest.raises(ComputeError):
|
||||
vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE, "markers": {"m": {"bpf": "icmp"}}})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adapter wiring (start path)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_ubridge_kernel_connection(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._namespace = 42
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
|
||||
await vm._add_ubridge_connection(nio, 0)
|
||||
|
||||
host_ifc, guest_ifc = vm._veth_names(0, 0)
|
||||
calls = [
|
||||
# stale-pair sweep (no-ops on a clean host)
|
||||
call.send(f'docker delete_veth "{host_ifc}"'),
|
||||
call.send(f'docker delete_veth "{guest_ifc}"'),
|
||||
call.send(f'docker create_veth "{host_ifc}" "{guest_ifc}"'),
|
||||
call.send(f'link set "{host_ifc}" down'),
|
||||
call.send(f'docker set_mac_addr {guest_ifc} 02:42:3d:b7:93:00'),
|
||||
call.send(f'docker move_to_ns {guest_ifc} 42 eth0'),
|
||||
# link attach
|
||||
call.send(f'brctl create "{BRIDGE}"'),
|
||||
call.send(f'link set "{BRIDGE}" up'),
|
||||
call.send(f'brctl addif "{BRIDGE}" "{host_ifc}"'),
|
||||
call.send(f'link set "{host_ifc}" up'),
|
||||
]
|
||||
vm._ubridge_hypervisor.assert_has_calls(calls, any_order=True)
|
||||
assert vm._kernel_veths[(0, 0)] == host_ifc
|
||||
# no uBridge relay bridge on the kernel datapath
|
||||
assert "bridge0" not in vm._bridges
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_ubridge_connection_none_nio_stays_on_tap_path(vm):
|
||||
"""
|
||||
Unconnected adapters keep the relay TAP: the veth variant is only created
|
||||
for adapters whose NIO is a kernel one at start time.
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._namespace = 42
|
||||
|
||||
await vm._add_ubridge_connection(None, 0)
|
||||
|
||||
vm._ubridge_hypervisor.assert_has_calls([
|
||||
call.send("bridge create bridge0"),
|
||||
call.send("bridge add_nio_tap bridge0 tap-gns3-e0 off"),
|
||||
], any_order=True)
|
||||
assert vm._kernel_veths == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_ubridge_kernel_connection_cleans_up_on_failure(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._namespace = 42
|
||||
|
||||
async def failing_send(command):
|
||||
if "create_veth" in command and "delete" not in command:
|
||||
raise UbridgeError("could not complete netlink transaction")
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock(side_effect=failing_send)
|
||||
|
||||
with pytest.raises(UbridgeError):
|
||||
await vm._add_ubridge_connection(vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE}), 0)
|
||||
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._ubridge_send.assert_any_call(f'docker delete_veth "{host_ifc}"')
|
||||
assert (0, 0) not in vm._kernel_veths
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Link attach / detach
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_tolerates_bridge_create_race(vm):
|
||||
|
||||
# Both endpoints create the per-link bridge concurrently; the loser must
|
||||
# verify (brctl show) instead of failing.
|
||||
async def send(command):
|
||||
if 'brctl create' in command:
|
||||
raise UbridgeError("Could not create bridge: File exists")
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock(side_effect=send)
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
vm._ubridge_send.assert_any_call(f'brctl show "{BRIDGE}"')
|
||||
vm._ubridge_send.assert_any_call(f'brctl addif "{BRIDGE}" "{host_ifc}"')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_create_failure_propagates(vm):
|
||||
|
||||
async def send(command):
|
||||
if 'brctl create' in command or 'brctl show' in command:
|
||||
raise UbridgeError("Could not create bridge: No such device")
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock(side_effect=send)
|
||||
vm._kernel_veths[(0, 0)] = vm._veth_names(0, 0)[0]
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
with pytest.raises(UbridgeError):
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_kernel_without_veth_raises(vm):
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
with pytest.raises(DockerError, match="restart the node"):
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_kernel_nio(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
vm._ethernet_adapters[0].add_nio(0, vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE}))
|
||||
|
||||
await vm.adapter_remove_nio_binding(0)
|
||||
|
||||
vm._ubridge_send.assert_has_calls([
|
||||
call(f'link set "{host_ifc}" down'),
|
||||
call(f'brctl delif "{BRIDGE}" "{host_ifc}"'),
|
||||
call(f'brctl delete "{BRIDGE}"'),
|
||||
], any_order=True)
|
||||
assert vm._ethernet_adapters[0].get_nio(0) is None
|
||||
# the veth itself stays: it is the adapter interface, not the link
|
||||
assert vm._kernel_veths[(0, 0)] == host_ifc
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_kernel_nio_tolerates_peer_winning_bridge_delete(vm):
|
||||
|
||||
# Deleting a bridge that still holds the peer's port fails with EBUSY;
|
||||
# a bridge already deleted by the peer fails with ENOENT — both expected.
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
|
||||
async def send(command):
|
||||
if 'brctl delif' in command or 'brctl delete' in command:
|
||||
raise UbridgeError("busy")
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock(side_effect=send)
|
||||
vm._kernel_veths[(0, 0)] = vm._veth_names(0, 0)[0]
|
||||
vm._ethernet_adapters[0].add_nio(0, vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE}))
|
||||
|
||||
await vm.adapter_remove_nio_binding(0) # must not raise
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Carrier / suspend
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_adapter_carrier_kernel(vm):
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
|
||||
await vm._set_adapter_carrier(0, True)
|
||||
await vm._set_adapter_carrier(0, False)
|
||||
|
||||
vm._ubridge_send.assert_has_calls([
|
||||
call(f'link set "{host_ifc}" up'),
|
||||
call(f'link set "{host_ifc}" down'),
|
||||
])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UDP fallback on a veth adapter (kernel link deleted, relay link created
|
||||
# on a running node)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_udp_relays_over_veth(vm):
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
vm._ubridge_send.assert_has_calls([
|
||||
call("bridge create bridge0"),
|
||||
call(f'bridge add_nio_ethernet bridge0 "{host_ifc}"'),
|
||||
call("bridge add_nio_udp bridge0 4242 127.0.0.1 4343"),
|
||||
call("bridge start bridge0"),
|
||||
], any_order=True)
|
||||
assert "bridge0" in vm._bridges
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_nio_udp_on_veth_is_idempotent(vm):
|
||||
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
vm._bridges.add("bridge0") # relay already exists from a previous link
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
|
||||
await vm._connect_nio(0, nio)
|
||||
|
||||
for c in vm._ubridge_send.call_args_list:
|
||||
command = c.args[0]
|
||||
assert "bridge create" not in command
|
||||
assert "add_nio_ethernet" not in command
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Guards and cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_kernel_rejects_filters(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
nio.filters = {"loss": [50]}
|
||||
|
||||
with pytest.raises(DockerError, match="kernel-datapath"):
|
||||
await vm.adapter_update_nio_binding(0, nio)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_kernel_suspend_toggles_carrier(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
nio.suspend = True
|
||||
await vm.adapter_update_nio_binding(0, nio)
|
||||
vm._ubridge_send.assert_any_call(f'link set "{host_ifc}" down')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_capture_rejected_on_kernel_link(vm):
|
||||
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
|
||||
with pytest.raises(DockerError, match="kernel-datapath"):
|
||||
await vm.start_capture(0, "/tmp/capture.pcap")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_kernel_veths(vm):
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
|
||||
await vm._remove_kernel_veths()
|
||||
|
||||
vm._ubridge_send.assert_called_once_with(f'docker delete_veth "{host_ifc}"')
|
||||
assert vm._kernel_veths == {}
|
||||
|
||||
|
||||
def test_veth_names_deterministic_and_bounded(vm):
|
||||
|
||||
host_ifc, guest_ifc = vm._veth_names(3, 2)
|
||||
assert host_ifc == f"gv{vm.id.replace('-', '')[:8]}e3p2"
|
||||
assert guest_ifc == f"gc{vm.id.replace('-', '')[:8]}e3p2"
|
||||
assert host_ifc != guest_ifc
|
||||
# multi-port adapters can reach two-digit adapter/port numbers
|
||||
long_host, long_guest = vm._veth_names(12, 8)
|
||||
assert len(long_host) <= 15 and len(long_guest) <= 15
|
||||
@@ -902,3 +902,18 @@ async def test_create_reparse_refreshes_env_knobs(compute_project, manager):
|
||||
|
||||
assert vm._stop_timeout == 5
|
||||
assert vm._gns3_init is True # removed entry reset to default
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kernel-datapath (NIOBridge) rejection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unix_socket_nio_rejects_kernel_datapath_link(compute_project, manager):
|
||||
|
||||
vm = _make_vm(compute_project, manager, environment="GNS3_UNIX_SOCKET_NIO=1")
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._namespace = 42
|
||||
nio = manager.create_nio({"type": "nio_bridge", "bridge": "gns3a1b2c3d4e5f"})
|
||||
with pytest.raises(DockerError, match="kernel-datapath"):
|
||||
await vm._add_ubridge_connection(nio, 0)
|
||||
@@ -0,0 +1,227 @@
|
||||
#
|
||||
# Copyright (C) 2026 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _node(project, compute, name, node_type="docker", status="stopped", environment=None):
|
||||
node = Node(project, compute, name, node_type=node_type)
|
||||
node._ports = [EthernetPort("eth0", 0, 0, 0)]
|
||||
node._status = status
|
||||
if environment is not None:
|
||||
node._properties = {"environment": environment}
|
||||
return node
|
||||
|
||||
|
||||
async def _kernel_link(project, **kwargs):
|
||||
compute = MagicMock()
|
||||
compute.id = "compute-1"
|
||||
node1 = _node(project, compute, "docker1", **kwargs)
|
||||
node2 = _node(project, compute, "docker2", **kwargs)
|
||||
link = UDPLink(project)
|
||||
# batch=True: skip the interactive create() (the test drives _prepare()
|
||||
# itself) — matches the project-open bulk path.
|
||||
await link.add_node(node1, 0, 0, batch=True, dump=False)
|
||||
await link.add_node(node2, 0, 0, batch=True, dump=False)
|
||||
return link, node1, node2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_eligible(project):
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
assert link._kernel_datapath_eligible(node1, node2) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_for_other_node_types(project):
|
||||
|
||||
compute = MagicMock()
|
||||
compute.id = "compute-1"
|
||||
node1 = _node(project, compute, "docker1")
|
||||
node2 = _node(project, compute, "qemu1", node_type="qemu")
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node1, 0, 0, batch=True, dump=False)
|
||||
await link.add_node(node2, 0, 0, batch=True, dump=False)
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_across_computes(project):
|
||||
|
||||
compute1 = MagicMock()
|
||||
compute1.id = "compute-1"
|
||||
compute2 = MagicMock()
|
||||
compute2.id = "compute-2"
|
||||
node1 = _node(project, compute1, "docker1")
|
||||
node2 = _node(project, compute2, "docker2")
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node1, 0, 0, batch=True, dump=False)
|
||||
await link.add_node(node2, 0, 0, batch=True, dump=False)
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_with_filters(project):
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._filters = {"delay": [10, 0]}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_with_markers(project):
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._markers = {"m": {"bpf": "icmp"}}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_with_project_marker_definitions(project):
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
project._marker_definitions = {"global-m": {"bpf": "icmp"}}
|
||||
try:
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
finally:
|
||||
project._marker_definitions = {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_for_running_nodes(project):
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
node1._status = "started"
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_not_eligible_for_unix_socket_containers(project):
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
node2._properties = {"environment": "GNS3_UNIX_SOCKET_NIO=1"}
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_disabled_by_config(project):
|
||||
|
||||
settings = Config.instance().settings.Server
|
||||
settings.enable_kernel_datapath = False
|
||||
try:
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
assert link._kernel_datapath_eligible(node1, node2) is False
|
||||
finally:
|
||||
settings.enable_kernel_datapath = True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_kernel_link_emits_bridge_nios(project):
|
||||
|
||||
compute = MagicMock()
|
||||
compute.id = "compute-1"
|
||||
node1 = _node(project, compute, "docker1")
|
||||
node2 = _node(project, compute, "docker2")
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node1, 0, 0, batch=True, dump=False)
|
||||
await link.add_node(node2, 0, 0, batch=True, dump=False)
|
||||
|
||||
entries = await link._prepare()
|
||||
|
||||
assert len(entries) == 2
|
||||
expected_bridge = "gns3" + link.id.replace("-", "")[:11]
|
||||
assert len(expected_bridge) == 15
|
||||
for entry in entries:
|
||||
assert entry[3]["type"] == "nio_bridge"
|
||||
assert entry[3]["bridge"] == expected_bridge
|
||||
# no UDP port reservation on the kernel datapath
|
||||
compute.post.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_relay_link_allocates_udp_ports(project):
|
||||
|
||||
compute1 = MagicMock()
|
||||
compute1.id = "compute-1"
|
||||
compute2 = MagicMock()
|
||||
compute2.id = "compute-2"
|
||||
|
||||
async def subnet_callback(other):
|
||||
return ("192.168.1.1", "192.168.1.2")
|
||||
|
||||
compute1.get_ip_on_same_subnet.side_effect = subnet_callback
|
||||
|
||||
async def port_callback(path, data={}, **kwargs):
|
||||
response = MagicMock()
|
||||
response.json = {"udp_port": 1024}
|
||||
return response
|
||||
|
||||
compute1.post.side_effect = port_callback
|
||||
compute2.post.side_effect = port_callback
|
||||
|
||||
node1 = _node(project, compute1, "docker1")
|
||||
node2 = _node(project, compute2, "docker2")
|
||||
link = UDPLink(project)
|
||||
await link.add_node(node1, 0, 0, batch=True, dump=False)
|
||||
await link.add_node(node2, 0, 0, batch=True, dump=False)
|
||||
|
||||
entries = await link._prepare()
|
||||
|
||||
for entry in entries:
|
||||
assert entry[3]["type"] == "nio_udp"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_filters_rejected_on_kernel_link(project):
|
||||
|
||||
link, _n1, _n2 = await _kernel_link(project)
|
||||
await link._prepare()
|
||||
|
||||
with pytest.raises(ControllerError, match="kernel-datapath"):
|
||||
await link.update_filters({"delay": [10, 0]})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_marker_rejected_on_kernel_link(project):
|
||||
|
||||
link, _n1, _n2 = await _kernel_link(project)
|
||||
await link._prepare()
|
||||
|
||||
with pytest.raises(ControllerError, match="kernel-datapath"):
|
||||
await link.start_marker("m", "icmp")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_property(project):
|
||||
|
||||
link, _n1, _n2 = await _kernel_link(project)
|
||||
assert link.kernel_datapath is False
|
||||
await link._prepare()
|
||||
assert link.kernel_datapath is True
|
||||
Reference in new issue
Block a user