fix: keep upstream link-carrier commands off unix-socket NIO bridges

Upstream #2870 added TAP carrier control (set_nio_tap_carrier) and a
busybox interface-status monitor to the Docker base class. Both assume
TAP wiring: unix-socket NIO bridges carry no TAP NIO, so uBridge rejects
the carrier command ('bridge has no TAP NIO') and every link create,
update or delete on a running iol-runner node would fail; the monitor
polls eth{adapter} interfaces that do not exist in the container's
network namespace (or misreports the Docker default eth0 as adapter 0).

VendorDockerVM now short-circuits _set_adapter_carrier and the interface
monitor under GNS3_UNIX_SOCKET_NIO; TAP-wired vendor nodes keep the base
behavior.
This commit is contained in:
YueGuobin 2026-09-05 22:19:22 +08:00
parent e12ef7f272
commit 27b55b72f9
No known key found for this signature in database
2 changed files with 93 additions and 0 deletions

View File

@ -454,6 +454,38 @@ class VendorDockerVM(DockerVM):
if nio:
await self._connect_nio(adapter_number, nio, port_number)
async def _set_adapter_carrier(self, adapter_number, connected, port_number=0):
"""
Override: with GNS3_UNIX_SOCKET_NIO the bridges carry only unix and
UDP NIOs there is no TAP, and uBridge rejects the carrier command
("bridge has no TAP NIO"), which would fail every link create,
update or delete on a running node. The link state is the socket
pair itself.
"""
if not self._unix_socket_nio:
await super()._set_adapter_carrier(adapter_number, connected, port_number)
async def _start_interface_monitor(self):
"""
Override: with GNS3_UNIX_SOCKET_NIO no GNS3-managed eth interface
exists in the container's network namespace — the busybox poll would
idle forever (or misreport the Docker default eth0 as adapter 0
status). Vendor images with TAP wiring keep the base monitor.
"""
if not self._unix_socket_nio:
await super()._start_interface_monitor()
async def _stop_interface_monitor(self):
"""
Override: mirror _start_interface_monitor there is nothing to stop
when the monitor never started under unix-socket NIO.
"""
if not self._unix_socket_nio:
await super()._stop_interface_monitor()
def _cleanup_console_resources(self):
"""
Override: close the docker-exec pty socket, if any, so the next

View File

@ -610,6 +610,67 @@ async def test_generic_unix_socket_dir_honored_in_wiring(compute_project, manage
assert "add_nio_tap" not in flat
# ---------------------------------------------------------------------------
# Upstream link-carrier / interface-monitor vs unix-socket NIO
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_link_operations_never_send_tap_carrier(compute_project, manager):
"""
uBridge rejects set_nio_tap_carrier on a bridge without a TAP NIO
("bridge has no TAP NIO"), and unix-socket NIO bridges never have one:
every link create/update/delete on a running node would fail. The
vendor override must keep the carrier command out of all three paths.
"""
vm = _make_vm(compute_project, manager)
vm._ubridge_hypervisor = MagicMock() # truthy ubridge, like a started node
vm._ubridge_send = AsyncioMagicMock()
vm.status = "started"
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
await vm.adapter_add_nio_binding(0, nio, 0)
await vm.adapter_update_nio_binding(0, nio, 0)
await vm.adapter_remove_nio_binding(0, 0)
commands = [str(c.args[0]) for c in vm._ubridge_send.call_args_list]
assert any(c.startswith("bridge add_nio_udp") for c in commands)
assert not any("set_nio_tap_carrier" in c for c in commands)
@pytest.mark.asyncio
async def test_interface_monitor_disabled_under_unix_nio(compute_project, manager):
vm = _make_vm(compute_project, manager)
vm.manager.query = AsyncioMagicMock()
await vm._start_interface_monitor() # must be a no-op: no eth NICs to poll
await vm._stop_interface_monitor()
vm.manager.query.assert_not_called()
assert vm._interface_monitor_task is None
@pytest.mark.asyncio
async def test_tap_wired_vendor_keeps_carrier_and_monitor(compute_project, manager):
"""
Vendor nodes with TAP wiring (XRd, SR Linux, ...) keep the base
behavior: the guards only apply to unix-socket NIO.
"""
vm = VendorDockerVM("vendor-1", str(uuid.uuid4()), compute_project, manager, "vendor:latest",
console_type="docker_exec", environment="GNS3_SKIP_INIT=1")
with patch.object(DockerVM, "_set_adapter_carrier", new=AsyncioMagicMock()) as carrier, \
patch.object(DockerVM, "_start_interface_monitor", new=AsyncioMagicMock()) as monitor:
await vm._set_adapter_carrier(0, True)
await vm._start_interface_monitor()
# the base-class attribute is replaced by a plain mock (no descriptor
# binding), so super() calls arrive without self
carrier.assert_called_once_with(0, True, 0)
monitor.assert_called_once_with()
# ---------------------------------------------------------------------------
# Startup configuration
# ---------------------------------------------------------------------------