feat(controller): let IOL runner containers take the kernel datapath

_is_unix_socket_docker no longer excludes the IOL runner marker: those
containers anchor on persistent TAPs now (compute side), so they
graduate through _kernel_endpoint_ready's gate instead — both
ubridge_bridge_tap (the swappable leg) and ubridge_tap (the tap module)
strict True, an unreported capability keeping the link on the relay
where it always works. The generic GNS3_UNIX_SOCKET_NIO exclusion keeps
its shape (an IOL runner environment wins the compute's class
selection, so the exclusion must not fire when both markers are set).

The switch fast path names an IOL runner peer's anchor through
kernel_anchor_type — the iol_docker key, never the gv veth namespace.

The 307d676cb regression tests move with the semantics: the capability
matrix replaces the unconditional relay, and the switch-path test also
asserts the gx naming. Its original concern stands — a compute that
cannot serve the anchor lifecycle must never be handed a kernel link
(the switch's deferred join would make it a silent dead cable).
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:09 +08:00
1 parent 4b4a0fc35f
commit 13c1ffb951
2 files changed
+80 -34

No files matched your search

+51 -21
View File
@@ -431,43 +431,73 @@ async def test_kernel_datapath_not_eligible_for_unix_socket_containers(project):
@pytest.mark.asyncio
async def test_kernel_datapath_not_eligible_for_iol_runner_containers(project):
async def test_iol_runner_containers_gate_on_the_bridge_tap_capabilities(project):
"""
The IOL runner marker is the other way into the unix-socket datapath: the
compute selects IOLDockerVM on GNS3_IOL_RUNNER and that class wires the
adapters through AF_UNIX socket pairs whatever the generic
GNS3_UNIX_SOCKET_NIO knob says — and the documented environment for
those nodes carries only the marker. Matching the knob alone let them
through to a kernel link their compute cannot serve.
The IOL runner marker selects a container whose guest leg is unix
sockets: it can still anchor kernel links (persistent TAPs through the
bridge module's swappable TAP leg), but only when its compute reports
both ubridge_bridge_tap and ubridge_tap — an unreported capability (old
uBridge, failed probe) keeps the link on the relay, where it always
works. This is also the regression guard for the compute that cannot
serve the anchor lifecycle being handed a kernel link anyway.
"""
link, node1, node2 = await _kernel_link(project)
node2._properties = {"environment": "GNS3_IOL_RUNNER=1"}
assert link._kernel_datapath_eligible(node1, node2) is False
def iol_capable_compute(bridge_tap, tap):
compute = MagicMock()
compute.id = "compute-1"
compute.capabilities = {"ubridge_bridge_tap": bridge_tap, "ubridge_tap": tap}
return compute
# the marker travels in a multi-line environment like any other knob
node2._properties = {"environment": "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=cfg.txt"}
assert link._kernel_datapath_eligible(node1, node2) is False
for environment in ("GNS3_IOL_RUNNER=1", "GNS3_IOL_RUNNER=1\nGNS3_IOL_STARTUP_CONFIG=cfg.txt"):
compute = iol_capable_compute(True, True)
node1 = _node(project, compute, "docker1")
node2 = _node(project, compute, "iol1", environment=environment)
link = await _link(project, node1, node2)
assert link._kernel_datapath_eligible(node1, node2) is True, environment
for bridge_tap, tap in ((None, True), (True, None), (False, True), (True, False)):
compute = iol_capable_compute(bridge_tap, tap)
node1 = _node(project, compute, "docker1")
node2 = _node(project, compute, "iol1", environment=environment)
link = await _link(project, node1, node2)
assert link._kernel_datapath_eligible(node1, node2) is False, (environment, bridge_tap, tap)
@pytest.mark.asyncio
async def test_kernel_datapath_not_eligible_for_an_iol_runner_container_on_a_switch(project):
async def test_iol_runner_container_on_a_switch_gates_and_names_the_iol_docker_anchor(project):
"""
The switch fast path absorbs the peer's anchor, and a container bridged
through unix sockets has none — nor will it ever. The switch defers a
join against a stopped peer by design (it must not fail the link), so
such a cable would be silently dead instead of merely slow: the gate has
to run before the switch branch, keeping the link on the relay the
switch's own port TAP serves.
On the switch fast path an IOL runner peer gates on the same capability
pair — and when it passes, the switch absorbs an anchor named under the
``iol_docker`` key (a persistent TAP, not the ``gv`` veth namespace).
Without the capabilities the link must stay on the relay: the switch
defers a join against a missing anchor by design (a link to a stopped
node must not fail), so a gate miss here is a silently dead cable, not
a late error.
"""
from gns3server.utils.kernel_anchor import kernel_anchor_name
compute = MagicMock()
compute.id = "compute-1"
compute.capabilities = {"ubridge_bridge_tap": True, "ubridge_tap": True}
switch = _node(project, compute, "sw1", node_type="ethernet_switch")
peer = _node(project, compute, "iol1", environment="GNS3_IOL_RUNNER=1")
link = await _link(project, switch, peer)
assert link._kernel_datapath_eligible(switch, peer) is True
entries = await link._prepare()
switch_nio = entries[0][3]
assert switch_nio["type"] == "nio_anchor"
assert switch_nio["anchor"] == kernel_anchor_name("iol_docker", peer.id, 0, 0)
assert switch_nio["anchor"].startswith("gx")
# a compute that cannot serve the swappable leg keeps the link relay
compute = MagicMock()
compute.id = "compute-1"
compute.capabilities = {"ubridge_tap": True, "ubridge_iol_tap": True}
switch = _node(project, compute, "sw1", node_type="ethernet_switch")
peer = _node(project, compute, "iol1", environment="GNS3_IOL_RUNNER=1")
link = await _link(project, switch, peer)
assert link._kernel_datapath_eligible(switch, peer) is False