Two real IOU routers on a real L3 image (the image's own IOL fabric, the
real IOS CLI on the server's telnet console), driving the anchor TAPs the
kernel datapath builds on: anchors born with the node (one per Ethernet
bay/unit), real ICMP through the [fabric <-> TAP] port bridge, netem
delay, suspend, capture, link delete/re-create, node stop/start rewiring,
the serial link staying on the relay (serial bays never anchor) and a
relay negative control on an isolated instance. Every scenario ends with
no anchors, bridges or tmpdirs left behind.
harness: create_iou_node/iou_images/iou_anchor_name, and the IOS console
helper is now ios_console (dynamips, IOU and the IOL containers share it).
Without one every c7200 busy-waits a full CPU core for the whole scenario
-- the tests create raw nodes and no template supplies a value (measured:
constant ~100 % per router, up to ~6 minutes per dynamips scenario).
harness.dynamips_idlepc() detects a value through the controller's
auto_idlepc endpoint (the GUI idle-PC finder path: a throwaway project
boots the image, candidate values are validated by measuring actual CPU
usage) and caches it in ~/.cache/gns3-e2e/idlepc.json keyed by the image
checksum, so the cost (~100 s here) is paid once per image, not per run.
GNS3_E2E_IDLEPC overrides detection; any failure returns None and the
scenarios run without one rather than fail on a CPU optimisation.
All four scenarios that boot c7200 routers (dynamips kernel/relay,
ethernet switch kernel/relay) pass the value at creation. Live: detected
0x60189234 for the M11 image (238 s first run including detection); the
routers drop from ~100 % during boot to 3-8 % steady (sampled per
process), and a re-run reports the cached value with no detection.
The spec's gns3-server alignment section promised three assertions the
suites never carried; they land in harness.py and run in every kernel
scenario (plus the relay controls' anchor checks):
- E.1 assert_pure_l2: anchors and bridges carry no L3 identity (addrgenmode
none, no address in /proc/net/if_inet6, no ip -o -4 addr) -- the kernel
scenarios' anchors and per-link/switch bridges, relay-mode anchors and
the switch's relay TAPs alike (hardening is a creation-time property,
not a datapath one);
- E.2 assert_idle_silence (Dynamips suite): guest interfaces shut to
silence IOS's own CDP/keepalives, settle 2.5 s, then a 5 s window of
zero traffic over both anchors and the per-link bridge;
- assert_forwarding: every attached port reports brport/state == 3 -- a
bridge left DOWN keeps its ports disabled and forwards nothing, with no
error anywhere; the assertion is the only signal.
harness.l2only_supported() probes a throwaway uBridge once (link l2only
against a nonexistent device: 208 = command present, 202-Unknown command
= old build); without the command the assertions skip -- the spec's
degradation contract, mirroring the tc-capability skip.
The first live run caught a fifth-creator gap: the switch's relay port
TAPs (minted implicitly by bridge add_nio_tap) carried a live fe80::.
Delivered uBridge-side per the spec (fb72758: the transient TAP is
hardened at creation, attaches untouched); the server needed no change,
and the relay-control E.1 check is that creator's end-to-end coverage.
The Ethernet switch's kernel fast path: instead of a link crossing the
switch's per-port uBridge relay, the switch absorbs the peer's anchor into
its own kernel bridge. The peer's interface (Docker veth host end, QEMU
TAP, IOU port TAP, Dynamips port TAP) becomes the switch port, and frames
cross anchor -> switch bridge -> switch ports entirely in the kernel.
Wire format: the switch end receives nio_anchor ('join this interface to
my own bridge' — the mirror of nio_bridge), the peer end nio_bridge with a
null bridge ('your anchor is bridged elsewhere': carrier, markers and
impairments only — never bridge membership, and never a bridge delete, on
an interface this node does not own). Anchor names come from the shared
naming contract (utils.kernel_anchor), a pure function of node type, id,
adapter and port — the controller computes the peer's anchor while the
node may be stopped, which is what makes links to stopped nodes wireable.
Stopped peers defer the join; after any node of a link starts
(UDPLink.node_started) the controller re-pushes the switch NIO, and the
switch completes — or repairs, when a peer restart replaced its interface
under the same name — the join against the kernel's actual membership
instead of assuming it.
VLAN changes now edit membership in place: only the ports whose mode/VLAN
actually changed are touched, each losing exactly the previous mode's
entries the new mode does not want (the dot1q admits-all entry is one
range operation in both directions), and no port ever leaves the bridge to
change its VLANs. This replaces the old 'reset by re-enslaving' shortcut
(delif+addif on every wired port, added when the ports were the switch's
own TAPs): on absorbed anchors, re-enslaving an interface owned by another
node is where live testing watched links die — anchors occasionally lost
their bridge membership right after a ports_mapping update (later updates
then failed with delif EINVAL), reproducibly with qemu, IOU and Dynamips
peers and never with Docker's veths.
Eligibility: the switch side needs no capability of its own (its bridge
exists by construction); the peer must be able to anchor. Switch-to-switch
cascades stay on the relay — a kernel interface belongs to one bridge, and
joining two bridges needs a veth pair, deferred.
Verified live on a real server: qemu peers through four VLAN cycles with
both anchors enslaved throughout; a mixed switch (2 IOU + 2 Dynamips, four
kernel ports); Docker peers with real ICMP isolating and recovering (with
both anchors still enslaved); VPCS relay ports likewise in place.