gns3-server/tests/e2e/README.md
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

94 lines
4.9 KiB
Markdown

<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
# 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:
```bash
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`).