mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-10-27 02:32:23 +02:00
Require uBridge >= 1.3.1 instead of gating the kernel datapath
Review feedback: 3.1 is still in alpha and the GNS3 VM ships a matching uBridge, so a hard minimum is simpler than a version-based degrade path, and it also covers Docker endpoints and the builtin Ethernet switch, which never went through the anchor probes. Raise minimum_required_version to 1.3.1 and drop supports_kernel_datapath and its uses; the relay fallback for capability problems (no CAP_NET_ADMIN) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
52fcc83494
commit
07b0c21fe5
@ -53,7 +53,6 @@ import socket
|
||||
import psutil
|
||||
|
||||
from gns3server.compute.error import NodeError
|
||||
from gns3server.compute.ubridge.hypervisor import KERNEL_DATAPATH_MINIMUM_VERSION
|
||||
from gns3server.compute.ubridge.ubridge_error import UbridgeError
|
||||
from gns3server.utils.tc_capabilities import FILTER_EBPF_MODES, parse_tc_capabilities, usable_ebpf_modes
|
||||
|
||||
@ -159,18 +158,8 @@ class KernelDatapathMixin:
|
||||
"Unknown command"/"Unknown module", a build without CAP_NET_ADMIN
|
||||
fails outright. The caller composes its own warning and fallback:
|
||||
the legacy relay datapath keeps working either way.
|
||||
|
||||
A build older than the kernel dataplane is refused without asking:
|
||||
1.2.3 already has the tap module, so its `tap create` succeeds,
|
||||
but the anchors it creates would then carry kernel links that need
|
||||
1.3.x commands (per-interface kernel captures, kernel markers).
|
||||
"""
|
||||
|
||||
if not self.ubridge.supports_kernel_datapath:
|
||||
return UbridgeError(
|
||||
f"uBridge {self.ubridge.version} is too old for the kernel datapath "
|
||||
f"(>= {KERNEL_DATAPATH_MINIMUM_VERSION} required)"
|
||||
)
|
||||
try:
|
||||
await self._ubridge_send(f'tap create "{probe_name}"')
|
||||
except UbridgeError as e:
|
||||
|
||||
@ -34,13 +34,6 @@ from .ubridge_hypervisor import UBridgeHypervisor
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# The kernel dataplane the persistent-TAP anchors feed (kernel links, kernel
|
||||
# markers and captures) needs uBridge >= 1.3.1: 1.3.0 introduced it, 1.3.1
|
||||
# made kernel captures per interface (`capture stop_kernel <ifc>`, which the
|
||||
# server always sends). The tap module itself is older (1.2.3), so a
|
||||
# successful `tap create` alone does not mean the dataplane is there.
|
||||
KERNEL_DATAPATH_MINIMUM_VERSION = "1.3.1"
|
||||
|
||||
|
||||
class Hypervisor(UBridgeHypervisor):
|
||||
"""
|
||||
@ -156,20 +149,6 @@ class Hypervisor(UBridgeHypervisor):
|
||||
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def supports_kernel_datapath(self):
|
||||
"""
|
||||
Whether this uBridge is recent enough for the kernel dataplane
|
||||
(persistent-TAP anchors, kernel links). False until the version is
|
||||
known, i.e. before start().
|
||||
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
if not self._version:
|
||||
return False
|
||||
return parse_version(self._version) >= parse_version(KERNEL_DATAPATH_MINIMUM_VERSION)
|
||||
|
||||
async def _check_ubridge_version(self, env=None):
|
||||
"""
|
||||
Checks if the ubridge executable version meets the minimum required.
|
||||
@ -179,10 +158,10 @@ class Hypervisor(UBridgeHypervisor):
|
||||
match = re.search(r"ubridge version ([0-9a-z\.]+)", output)
|
||||
if match:
|
||||
self._version = match.group(1)
|
||||
# uBridge >= 1.2.3 is required for the AF_UNIX control
|
||||
# channel, marker filters, builtin Ethernet Switch support,
|
||||
# and Docker TAP carrier control.
|
||||
minimum_required_version = "1.2.3"
|
||||
# uBridge >= 1.3.1 is required for the kernel datapath:
|
||||
# persistent TAP anchors, kernel links, kernel markers and
|
||||
# per-interface kernel captures.
|
||||
minimum_required_version = "1.3.1"
|
||||
if parse_version(self._version) < parse_version(minimum_required_version):
|
||||
raise UbridgeError(f"uBridge executable version must be >= {minimum_required_version}")
|
||||
else:
|
||||
|
||||
@ -45,7 +45,7 @@ from gns3server.config import Config
|
||||
from gns3server.utils.tc_capabilities import parse_tc_capabilities
|
||||
from gns3server.utils.vxlan import VXLAN_VNI_MAX, VXLAN_VNI_MIN
|
||||
|
||||
from .hypervisor import KERNEL_DATAPATH_MINIMUM_VERSION, Hypervisor
|
||||
from .hypervisor import Hypervisor
|
||||
from .ubridge_error import UbridgeError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@ -65,22 +65,6 @@ _vxlan_cache: dict[tuple[str, int, int, int], Optional[bool]] = {}
|
||||
_lock = asyncio.Lock()
|
||||
|
||||
|
||||
def _require_kernel_datapath(hypervisor):
|
||||
"""
|
||||
Refuse a uBridge older than the kernel dataplane before any anchoring
|
||||
probe asks it anything. Its own commands can still succeed — 1.2.3 has
|
||||
the tap module, so `tap create` answers OK — while the kernel links
|
||||
built on that answer need 1.3.x commands it lacks. Raised as a
|
||||
UbridgeError so the caller's usual failure path reports unknown.
|
||||
"""
|
||||
|
||||
if not hypervisor.supports_kernel_datapath:
|
||||
raise UbridgeError(
|
||||
f"uBridge {hypervisor.version} is too old for the kernel datapath "
|
||||
f"(>= {KERNEL_DATAPATH_MINIMUM_VERSION} required)"
|
||||
)
|
||||
|
||||
|
||||
async def probe_tc_capabilities(timeout: float = 15.0): # noqa: ASYNC109 - operation-specific timeout is intentional
|
||||
"""
|
||||
Probe the configured uBridge's tc module once per binary identity.
|
||||
@ -192,7 +176,6 @@ async def _probe_tap(path, config, timeout): # noqa: ASYNC109 - operation-speci
|
||||
|
||||
async def ask():
|
||||
await hypervisor.start()
|
||||
_require_kernel_datapath(hypervisor)
|
||||
await hypervisor.connect()
|
||||
await hypervisor.send(f"tap create {name}")
|
||||
try:
|
||||
@ -272,7 +255,6 @@ async def _probe_iol_tap(path, config, timeout): # noqa: ASYNC109 - operation-s
|
||||
|
||||
async def ask():
|
||||
await hypervisor.start()
|
||||
_require_kernel_datapath(hypervisor)
|
||||
await hypervisor.connect()
|
||||
await hypervisor.send(f"iol_bridge create {bridge} {_IOL_PROBE_BRIDGE_ID}")
|
||||
try:
|
||||
@ -354,7 +336,6 @@ async def _probe_bridge_tap(path, config, timeout): # noqa: ASYNC109 - operatio
|
||||
|
||||
async def ask():
|
||||
await hypervisor.start()
|
||||
_require_kernel_datapath(hypervisor)
|
||||
await hypervisor.connect()
|
||||
try:
|
||||
await hypervisor.send(f"bridge delete_nio_tap gns3brtapprobe{uuid.uuid4().hex[:4]} any")
|
||||
@ -451,7 +432,6 @@ async def _probe_vxlan(path, config, timeout): # noqa: ASYNC109 - operation-spe
|
||||
|
||||
async def ask():
|
||||
await hypervisor.start()
|
||||
_require_kernel_datapath(hypervisor)
|
||||
await hypervisor.connect()
|
||||
await hypervisor.send(f"vxlan create {name} {vni} remote=127.0.0.1 dstport={dstport}")
|
||||
try:
|
||||
|
||||
@ -122,29 +122,6 @@ async def test_prepare_tap_datapath_falls_back_without_the_tap_module(vm):
|
||||
assert emit.called
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_tap_datapath_falls_back_below_the_kernel_dataplane_version(vm):
|
||||
"""
|
||||
uBridge 1.2.3 has the tap module, so a `tap create` probe alone passes
|
||||
— and the node then runs on TAP anchors whose kernel links the build
|
||||
cannot drive. Below the kernel-dataplane version the node keeps the
|
||||
legacy socket-netdev datapath without creating any TAP, and the warning
|
||||
names the installed version.
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor.supports_kernel_datapath = False
|
||||
vm._ubridge_hypervisor.version = "1.2.3"
|
||||
with patch.object(vm.project, "emit") as emit:
|
||||
await vm._prepare_tap_datapath()
|
||||
|
||||
assert vm._tap_datapath is False
|
||||
assert vm._kernel_taps == {}
|
||||
assert not any(c.args[0].startswith("tap ") for c in vm._ubridge_send.call_args_list)
|
||||
message = emit.call_args.args[1]["message"]
|
||||
assert "1.2.3" in message
|
||||
assert "1.3.1" in message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tap_name_fits_ifnamsiz(vm):
|
||||
"""
|
||||
|
||||
@ -168,48 +168,37 @@ async def test_stop_tcp_has_no_socket_to_unlink(tmp_path, monkeypatch):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_requires_ubridge_1_2_3(tmp_path, monkeypatch):
|
||||
@pytest.mark.parametrize(
|
||||
"version",
|
||||
[
|
||||
"1.2.3", # has the tap module, but no kernel dataplane
|
||||
"1.3.0", # no per-interface capture stop_kernel
|
||||
],
|
||||
)
|
||||
async def test_requires_ubridge_1_3_1(tmp_path, monkeypatch, version):
|
||||
|
||||
hyp = _make("unix", tmp_path, monkeypatch)
|
||||
with patch(
|
||||
"gns3server.compute.ubridge.hypervisor.subprocess_check_output",
|
||||
new_callable=AsyncMock,
|
||||
return_value="ubridge version 1.2.2",
|
||||
return_value=f"ubridge version {version}",
|
||||
):
|
||||
with pytest.raises(UbridgeError, match=r">= 1\.2\.3"):
|
||||
with pytest.raises(UbridgeError, match=r">= 1\.3\.1"):
|
||||
await hyp._check_ubridge_version()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accepts_ubridge_1_2_3(tmp_path, monkeypatch):
|
||||
async def test_accepts_ubridge_1_3_1(tmp_path, monkeypatch):
|
||||
|
||||
hyp = _make("unix", tmp_path, monkeypatch)
|
||||
with patch(
|
||||
"gns3server.compute.ubridge.hypervisor.subprocess_check_output",
|
||||
new_callable=AsyncMock,
|
||||
return_value="ubridge version 1.2.3",
|
||||
return_value="ubridge version 1.3.1",
|
||||
):
|
||||
await hyp._check_ubridge_version()
|
||||
|
||||
assert hyp.version == "1.2.3"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"version, supported",
|
||||
[
|
||||
("", False), # not started yet: version unknown
|
||||
("1.2.3", False), # has the tap module, but no kernel dataplane
|
||||
("1.3.0", False), # no per-interface capture stop_kernel
|
||||
("1.3.1", True),
|
||||
("1.3.2", True),
|
||||
("1.4.0", True),
|
||||
],
|
||||
)
|
||||
def test_supports_kernel_datapath(tmp_path, monkeypatch, version, supported):
|
||||
|
||||
hyp = _make("unix", tmp_path, monkeypatch)
|
||||
hyp._version = version
|
||||
assert hyp.supports_kernel_datapath is supported
|
||||
assert hyp.version == "1.3.1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@ -50,9 +50,6 @@ class FakeHypervisor:
|
||||
# doesn't exist), None = answer 100 (an impossible reply on the probe's
|
||||
# uuid name — the probe must treat it as unknown)
|
||||
bridge_tap_error = "214-bridge 'nosuch' doesn't exist"
|
||||
# False = a build older than the kernel dataplane (uBridge < 1.3.1)
|
||||
supports_kernel_datapath = True
|
||||
version = "1.3.1"
|
||||
spawned = 0
|
||||
|
||||
def __init__(self, project, path, working_dir, transport, host, node_id):
|
||||
@ -93,8 +90,6 @@ def probe_env(monkeypatch, tmp_path):
|
||||
FakeHypervisor.error = None
|
||||
FakeHypervisor.error_prefix = None
|
||||
FakeHypervisor.bridge_tap_error = "214-bridge 'nosuch' doesn't exist"
|
||||
FakeHypervisor.supports_kernel_datapath = True
|
||||
FakeHypervisor.version = "1.3.1"
|
||||
FakeHypervisor.reply = ["netem=delay,rate;ebpf=1;cbpf=1;ebpf_modes=nth,quota,window,flow"]
|
||||
tc_probe._cache.clear()
|
||||
tc_probe._tap_cache.clear()
|
||||
@ -290,25 +285,6 @@ async def test_probe_bridge_tap_support_missing_binary(monkeypatch):
|
||||
assert FakeHypervisor.spawned == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"probe", [probe_tap_support, probe_iol_tap_support, probe_bridge_tap_support, probe_vxlan_support]
|
||||
)
|
||||
async def test_anchor_probes_unknown_below_the_kernel_dataplane_version(probe):
|
||||
"""
|
||||
uBridge 1.2.3 already has the tap module, so its `tap create` succeeds —
|
||||
but the kernel links built on that answer need commands that only 1.3.x
|
||||
has (per-interface `capture stop_kernel`, kernel markers, ...). A build
|
||||
below the kernel-dataplane version is unknown without being asked
|
||||
anything, so every node stays on the relay datapath.
|
||||
"""
|
||||
|
||||
FakeHypervisor.supports_kernel_datapath = False
|
||||
FakeHypervisor.version = "1.2.3"
|
||||
assert await probe() is None
|
||||
assert FakeHypervisor.spawned == 1
|
||||
assert FakeHypervisor.last.commands == []
|
||||
|
||||
|
||||
class _FixedUuid:
|
||||
"""A uuid4 stand-in whose .int/.hex pin the probe's random draws."""
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user