feat: model IOL adapters as 4-port units like the IOU node type

IOL interfaces come in 4-port units (Ethernet0/0-3, Ethernet1/0-3, ...),
addressed like IOU as (adapter_number, port_number 0-3):

- IOLDockerVM builds EthernetAdapter(interfaces=4) per adapter and asks
  the runner for adapters x 4 interfaces (num-eth).
- DockerVM threads port_number through the NIO/capture API (the compute
  routes parsed it from the URL but dropped it); single-port adapters
  keep the historical bridge{N} names and command sequence, multi-port
  adapters get one bridge per port (bridge{a}_{p}).
- The unix-socket NIO wiring addresses sockets flat across adapters:
  adapter x ports-per-adapter + port, so single-port images keep their
  exact socket layout.
- The controller port list for GNS3_IOL_RUNNER docker nodes is generated
  by StandardPortFactory with the IOU naming (Ethernet{segment0}/{port0},
  segment size 4); plain docker nodes keep eth{N}.
This commit is contained in:
YueGuobin committed 2026-09-05 21:54:51 +08:00
1 parent 9b55164064
commit 0ca9ccc637
9 files changed
+267 -110

No files matched your search

+2 -2
View File
@@ -1198,7 +1198,7 @@ async def test_start(vm, manager, free_console_port, tmpdir):
await vm.start()
mock_query.assert_called_with("POST", "containers/e90e34656842/start")
vm._add_ubridge_connection.assert_called_once_with(nio, 0)
vm._add_ubridge_connection.assert_called_once_with(nio, 0, 0)
assert vm._start_ubridge.called
assert vm._start_console.called
assert vm._start_aux.called
@@ -1253,7 +1253,7 @@ async def test_start_namespace_failed(vm, manager, free_console_port):
await vm.start()
mock_query.assert_any_call("POST", "containers/e90e34656842/start")
mock_add_ubridge_connection.assert_called_once_with(nio, 0)
mock_add_ubridge_connection.assert_called_once_with(nio, 0, 0)
assert mock_start_ubridge.called
assert vm.status == "stopped"
+52 -2
View File
@@ -259,7 +259,7 @@ async def test_start_writes_iol_config(compute_project, manager):
with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f:
config = json.load(f)
assert config["binary"] == "/binary.iol"
assert config["num-eth"] == 4
assert config["num-eth"] == 16 # 4 adapters, each a 4-port unit
assert config["num-serial"] == 0
assert config["local-app"] == 1
assert config["remote-app"] == 2
@@ -285,7 +285,7 @@ async def test_start_rewrites_config_on_adapter_change(compute_project, manager)
await vm.start()
with open(os.path.join(vm.working_dir, "config", "iol-config.json")) as f:
assert json.load(f)["num-eth"] == 8
assert json.load(f)["num-eth"] == 32 # 8 adapters × 4 ports
@pytest.mark.asyncio
@@ -460,6 +460,56 @@ async def test_add_ubridge_connection_without_nio_still_wires(compute_project, m
assert "bridge start" not in flat
# ---------------------------------------------------------------------------
# IOU-style port model — 1 adapter = 4 ethernet ports
# ---------------------------------------------------------------------------
def test_adapters_are_four_port_units(compute_project, manager):
vm = _make_vm(compute_project, manager, adapters=2)
assert vm.adapters == 2
assert len(vm._ethernet_adapters) == 2
for adapter in vm._ethernet_adapters:
assert adapter.interfaces == 4
for port_number in range(4):
assert adapter.port_exists(port_number)
assert not adapter.port_exists(4)
@pytest.mark.asyncio
async def test_wiring_addresses_ports_within_adapters(compute_project, manager):
vm = _make_vm(compute_project, manager, adapters=2)
_mock_wiring(vm)
wiring_dir = _wiring_dir(vm)
os.makedirs(wiring_dir, exist_ok=True)
# adapter 1, port 2 -> flat interface index 6 (1 * 4 + 2)
open(os.path.join(wiring_dir, "s06.sock"), "w").close()
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
await vm._add_ubridge_connection(nio, 1, port_number=2)
flat = "\n".join(str(c) for c in vm._ubridge_hypervisor.method_calls)
assert 'bridge add_nio_unix bridge1_2 ' in flat
assert f'"{os.path.join(wiring_dir, "c06.sock")}"' in flat
assert f'"{os.path.join(wiring_dir, "s06.sock")}"' in flat
assert "add_nio_udp bridge1_2 4242 127.0.0.1 4343" in flat
assert vm._ethernet_adapters[1].host_ifc == os.path.join(wiring_dir, "c06.sock")
@pytest.mark.asyncio
async def test_nio_binding_rejects_port_out_of_range(compute_project, manager):
vm = _make_vm(compute_project, manager, adapters=2)
nio = manager.create_nio({"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1"})
with pytest.raises(DockerError) as excinfo:
await vm.adapter_add_nio_binding(1, nio, port_number=4)
assert "Port 4" in str(excinfo.value)
with pytest.raises(DockerError):
await vm.adapter_add_nio_binding(9, nio, port_number=0)
# ---------------------------------------------------------------------------
# Generic GNS3_UNIX_SOCKET_NIO knob on plain VendorDockerVM
# ---------------------------------------------------------------------------