Regression coverage added with the fixes:
- capture-slot semantics: second-port refusal, failed-start rollback, and
a non-owner port's stop not reaching `capture stop_kernel`;
- QEMU stop/start teardown on a mid-body or pre-launch failure, IOU start
cleanup and filter guards, Dynamips half-attach undo plus retry;
- switch VLAN reconcile skipping a deferred anchor and rolling the
ports_mapping back on failure;
- Docker/QEMU occupied-port refusal, probe ctime invalidation, probe
survival of a failing uBridge start;
- env-marker dialects (mid-line mentions, falsy values) and the agent
merge rejecting entries without a device_name.
Test-body imports of product modules move to the shared header.
The rebase onto upstream 3.1 (mypy + ruff I001/F401 waves) left the tree
short of the newly enforced baselines in three ways:
* contextlib / UbridgeError imports dropped as false duplicates during
conflict resolution left NameErrors in iou_vm and qemu_vm (F821);
the Dynamips router grabbed the compute-level NIOTAP instead of the
hypervisor one, breaking TAP anchoring (test failure).
* unused imports (KERNEL_UNSUPPORTED_FILTERS, UbridgeError, List) and
unsorted blocks left behind where the stack's later commits moved the
last usage elsewhere — ruff I001/F401/RUF022 now enforce these.
* mypy, now baseline-free: cache dict annotations in tc_probe, the
locators candidate list, the resolved-params None branch in the MCP
runner, and validate_bpf_syntax keeps the upstream Dict[str, Any]
return (the strict Optional[str] was wrong for the "valid" bool entry).
The e2e raw-frame probe resolves its compiler via shutil.which (S607)
and zips with strict= (B905); the remaining subprocess call is S603 by
nature and gets a per-file ignore like other test files.
ruff check/format clean, mypy at upstream parity (no new errors),
full unit suite 2167 passed.
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.
IOU's only wire is the IOL fabric (Unix datagrams, 8-byte headers,
NETMAP), so it has no host netdev to enslave — the kernel datapath gives
each Ethernet bay/unit a persistent TAP anchor the uBridge IOL bridge
binds the port to (iol_bridge add_nio_tap), which links then enslave into
their per-link kernel bridge exactly like Docker's veth and QEMU's TAP.
* IOUVM joins KernelDatapathMixin: one TAP per Ethernet bay/unit
(gi{id8}e{bay}p{unit}), created at start after a capability probe and a
stale sweep, born down and address-free; serial bays never anchor.
* _networking dispatches per NIO type: a relay link keeps iol_bridge
add_nio_udp, a kernel link binds the anchor then runs _kernel_attach.
* Stop order is QEMU's reverse, because uBridge holds the anchor fds:
iol_bridge delete (releases every port NIO) -> tap delete (a held TAP
answers EBADFD) -> brctl delete -> hypervisor stop. Removing a single
link releases its fd the same way (delete_nio_tap).
* The IOU-specific marker reconcile override is gone: marker primitives
are polymorphic on the anchor (marker *_kernel on a TAP, iol_bridge
commands on an IOL location), so the base reconcile serves both.
Capture on a kernel link anchors on the TAP (capture start_kernel);
filters are tc on the anchor, never combined with the userspace
iol_bridge filters a relay link uses.
An application ID is used by IOU to generate its interface Mac addresses. They must be unique across all opened projects sharing the same computes to avoid Mac address collisions.