YueGuobin 897f50e430
test(e2e): live coverage for the standard Docker node kernel datapath
Two real Alpine containers on an isolated instance, addressed the way a
GNS3 user does it (the node's persistent /etc/network/interfaces, applied
by init.sh) and driven through the Docker daemon socket the server itself
uses. Four scenarios:

* fast path: the per-link kernel bridge enslaves exactly the two veth host
  ends, real ICMP crosses, the L2-anchor spec E.1/E.2 assertions hold
  (idle silence measured with the guests' links down -- a container's own
  boot-time IGMP report re-fires ~10 s later, so silencing is not just
  IPv6), netem delay, suspend/resume, capture, link delete/re-create,
  container stop/start (the veth host ends die with the adapter and are
  rebuilt from the NIO), zero residue on project delete;
* filter matrix, one type at a time, each asserted through real traffic
  and its tc state: netem core (delay, jitter+distribution, loss, corrupt,
  duplicate), the extensions (reorder+correl+gap, seed+limit, gemodel,
  rate), cls_bpf match-drop including a byte-size discriminator, the eBPF
  classifier (frequency_drop every-Nth and -1, quota byte-cap cutoff,
  window_drop single outage with per-seq pass/drop/pass evidence), plus
  available_filters offering the kernel-only types;
* relay control: no per-link bridge, the kernel-only types hidden;
* relay->kernel reopen upgrade: the datapath choice is flipped across a
  server restart and the project reopened -- the link is rebuilt on the
  kernel datapath and forwards.

The harness gains a Docker guest driver (a stdlib Engine API client over
the daemon socket: exec with stream demux, container id by deterministic
name -- the controller's node payload does not carry it, image inspect),
a digest-pinned image helper (cache hit = no network, miss = pull through
the server's own route, neither = skip) and Server.restart() for the
reopen scenario (which also needs the local compute's capabilities to be
up before a project open is accepted).

Live: 4/4 scenarios, 142 s for the file; full e2e suite 10/10 (1503 s);
unit suite 2149 passed; ruff clean. The matrix measures loss with 20
packets at 0.2 s spacing (143 s -> 58 s) and the jitter RTT assertion
keeps statistical margin (per-direction N(50,20); 10 packets, >= 65 ms).
2026-10-06 00:21:10 +08:00
..

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, plus GNS3_E2E_USER/GNS3_E2E_PASSWORD, default admin/admin). The server must already be configured for the datapath under test (kernel links need enable_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, dynamips and an images directory (GNS3_E2E_IMAGES, default ~/GNS3/images) available locally; 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), 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_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, suspend, capture, link delete/re-create swap, node stop/start rewiring) and a relay negative control.
  • 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 and a relay→kernel reopen upgrade (the server's datapath choice is flipped across a restart).

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 — in the Dynamips suite — an idle link with the guests silenced stays silent for 5 s (assert_idle_silence). 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).