fix(compute): refuse a duplicate port NIO and wire before bookkeeping

The compute-side backstop of the controller's duplicate-port guard
(keep-port-links + live-port bind made the controller guard reliable;
this is the layer under it). IOU and Dynamips bound the adapter NIO
before the uBridge/hypervisor attach: a failed attach left the port's
binding replaced — the link that still owned the port lost its handle
and its host state leaked (the IOU EBUSY incident's 8 orphan TAPs).
Dynamips was worse: its wiring idempotence check silently skipped the
attach on a second binding and rebound only the bookkeeping. A port
that already carries an NIO is now refused, and both types bind the
NIO only after the wiring succeeded.
This commit is contained in:
YueGuobin committed 2026-10-06 00:21:10 +08:00
1 parent 6839b279be
commit 6e36b58a5d
4 files changed
+125 -17

No files matched your search

@@ -270,6 +270,48 @@ async def test_attach_kernel_nio_opens_the_anchor_in_the_hypervisor_and_enslaves
router._ubridge_send.assert_any_call(f'link set "{TAP00}" up')
@pytest.mark.asyncio
async def test_slot_add_nio_binding_refuses_an_occupied_port(router):
"""
The compute-side backstop of the duplicate-port guard: a second NIO for
a port that already carries one is refused here instead of overwriting
(the wiring's own idempotence check would silently skip the attach and
rebind the bookkeeping) — the occupying link's teardown needs its own
NIO handle, and its host state with it.
"""
router.status = "started"
router._kernel_taps[(0, 0)] = TAP00
first = await _nio(router)
await router.slot_add_nio_binding(0, 0, first)
assert router._slots[0].get_nio(0) is first
second = await _nio(router, bridge="gns3other")
with pytest.raises(DynamipsError, match="already has a link"):
await router.slot_add_nio_binding(0, 0, second)
assert router._slots[0].get_nio(0) is first
@pytest.mark.asyncio
async def test_slot_add_nio_binding_attach_failure_leaves_the_port_unbound(router):
"""
The bookkeeping happens last: a kernel attach that fails mid-way (the
anchor is another link's bridge port — uBridge EBUSY) must leave the
port without a binding, so the failed link creates no half-wired state.
"""
router.status = "started"
router._kernel_taps[(0, 0)] = TAP00
router._ubridge_send.side_effect = UbridgeError("208-Device or resource busy")
nio = await _nio(router)
with pytest.raises(UbridgeError):
await router.slot_add_nio_binding(0, 0, nio)
assert router._slots[0].get_nio(0) is None
@pytest.mark.asyncio
async def test_attach_kernel_nio_without_anchor_on_a_running_node_raises(router):
"""