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.
Live end-to-end tests
Tests under this directory run against a real server: real uBridge, real
kernel interfaces (privileged), real emulator binaries and real images. They
verify what unit tests cannot — that a datapath actually forwards traffic,
that kernel bridges actually enslave anchors, that tc impairments actually
shape packets. They are marked e2e and excluded from normal test runs
(see tests/pytest.ini); run them explicitly:
pytest -m e2e tests/e2e -s # -s shows the progress prints
pytest -m e2e tests/e2e -k dynamips # one scenario
Two run modes, chosen per test through harness.live_server(kernel=...):
- Target an existing server — set
GNS3_E2E_URL(e.g.http://127.0.0.1:3080/v3, plusGNS3_E2E_USER/GNS3_E2E_PASSWORD, default admin/admin). The server must already be configured for the datapath under test (kernel links needenable_kernel_datapath = True). - Isolated instance (the default) — the harness starts its own server
process on a free port with its own config directory (the controller DB
and every path follow the config), so nothing on the machine is touched.
It needs
ubridge,dynamipsand an images directory (GNS3_E2E_IMAGES, default~/GNS3/images) available locally — the IOU scenario takes its L3 image from that directory'sIOUsubfolder, the QEMU scenario its L3 IOSv qcow2 from theQEMUsubfolder; the Docker scenarios additionally need the Docker daemon the server talks to.
Either way each test creates its own project and deletes it again. Set
GNS3_E2E_KEEP=1 to keep the project (and a started instance) on failure
and print where to look at it.
The dynamips scenarios give their routers an idle-PC: detected once per
image through the controller's auto_idlepc (a throwaway project and a
real CPU-usage measurement — the GUI's idle-PC finder path) and cached in
~/.cache/gns3-e2e/idlepc.json keyed by the image checksum. Without one,
each c7200 burns a full CPU core for the whole scenario — the e2e
creates raw nodes, so no template supplies a value. Set GNS3_E2E_IDLEPC
to skip the detection and use the given value (detection failing is never
fatal: the routers simply run without one).
harness.py holds the shared pieces — REST client, server lifecycle, the
IOS console driver, the Docker guest driver (a tiny Docker Engine API
client over the daemon socket, plus the digest-pinned image helper), a
WebSocket notification collector (a sync facade over websockets' sync
client for asserting on live notification streams), host-side kernel
inspections (bridge membership, tap admin state, tc qdiscs, the L2-anchor
spec §E assertions) — so a new node type adds only its own scenario module.
Current scenarios:
test_dynamips_kernel_datapath.py— two real c7200 routers: kernel link (anchors, per-link bridge, netem delay, suspend, capture, delete/re-create, stop/start) plus a serial link staying on the relay, and a negative control on an isolated relay-configured instance.test_ethernet_switch_kernel_datapath.py— a real Ethernet switch (kernel bridge) between two c7200 routers: deferred joins on boot, VLAN isolation, absorbed-anchor filters/suspend/capture, VPCS peers staying on the relay, and a relay negative control.test_ethernet_switch_cascade.py— two Ethernet switches cascaded through the link-owned veth pair (gs<link_id>0/1, each end enslaved into its own switch's bridge): a dot1q trunk cascade carrying two VLANs at once across two c7200 routers per switch, the symmetric access/trunk mode flip on the cascade ends, two-sided netem (one end per direction), suspend, link delete/re-create (the pair dies with the link, the replacement mints new names), zero residue.test_iol_docker_kernel_datapath.py— two real iol-xe containers (Cisco CML iol-runner images): kernel link through the port bridge's swappable TAP leg (anchors at start, netem delay, the TAP-anchor classifier spot check for bpf match-drop and the eBPF every-nth mode, suspend, capture, link delete/re-create swap, node stop/start rewiring) and a relay negative control.test_iou_kernel_datapath.py— two real IOU routers (a real IOU image from the images directory, the real IOS CLI on the server's telnet console): kernel link through the [IOL fabric ↔ TAP] port bridge (same lifecycle as the iol-xe containers) including the TAP-anchor classifier spot check and a filter surviving a node restart, a serial link staying on the relay (serial bays never anchor), and a relay negative control whose filters ride the IOU-specificiol_bridge add_packet_filterengine (delay and frequency_drop measured through real traffic, capture and markers oniol_bridge start_captureand the port'smarkfilter).test_qemu_kernel_datapath.py— two real IOSv routers (a real qcow2 image from the images directory'sQEMUsubfolder, linked clone, the real IOS CLI on the server's telnet console): the anchor TAP is QEMU's own netdev (no port bridge in between) — the full kernel lifecycle plus a relay negative control where the anchor carries as the relay's AF_PACKET endpoint. IOSv boots a real IOS: this is the slowest scenario pair (~6½ min kernel, ~5 min relay).test_docker_kernel_datapath.py— two real Alpine containers (the plain veth datapath): the per-link kernel bridge enslaves exactly the two veth host ends, real ICMP crosses, the filter matrix runs one type at a time (netem core and extensions, cls_bpf match-drop, the eBPF classifier — each measured through real traffic), plus a relay negative control whose own filters (delay, frequency_drop, bpf) really shape the wire as uBridge userspace bridge filters and whose capture and markers ride the relay engine (bridge start_capture/ the bridgemarkfilter), and a relay→kernel reopen upgrade (the server's datapath choice is flipped across a restart).test_marker_kernel_datapath.py— two real Alpine containers on a kernel-only link with traffic-insight markers attached to a veth anchor (marker add_kernel):marker.matchsignals collected off the dedicated marker WebSocket with their full identity (filter/tag/link_id/node_id/len and the tx/rx direction set of a ping), the main project channel asserted free of matches, BPF and direction enforcement, host pcap exactness, pause/resume silence counting against a still-firing sibling, tag replay (timeline, display filter, link narrowing, window, the 409 gate) and marker delete taking pcap plus signals with it.
The Docker scenarios run a digest-pinned alpine:3, so every developer
tests the same bytes: a cached copy in the local Docker daemon costs no
network, a cache miss pulls through the server's own image-pull route, and
with neither cache nor registry the scenario skips (GNS3_E2E_DOCKER_IMAGE
overrides the reference). Their guests are addressed the way a GNS3 user
does it — the node's persistent /etc/network/interfaces, which the
container's init.sh applies at boot — and driven through the Docker
daemon socket the server itself uses.
Every kernel scenario also asserts the L2-anchor spec's server-side
guarantees (docs/design/ubridge-l2-anchor-spec.md §E): anchors and bridges
carry no L3 identity (assert_pure_l2), joined ports are FORWARDING
(assert_forwarding), and an idle link with the guests silenced stays
silent for 5 s (assert_idle_silence) — the guests are the IOS consoles'
interfaces shut, the Docker guests' links down, or all four cascade routers
silenced, whichever the scenario runs. These skip on a uBridge without
link l2only (probed once by harness.l2only_supported()).
Set GNS3_E2E_DEBUG=1 to run the isolated instance with debug logging (the
uBridge command stream lands in its server.log).