gns3-server/tests/e2e/test_dynamips_kernel_datapath.py
YueGuobin e0329ceee6
test(e2e): give the dynamips routers a detected idle-PC
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.
2026-10-06 00:21:10 +08:00

292 lines
13 KiB
Python

#
# Copyright (C) 2026 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Live end-to-end test for the Dynamips kernel datapath (pytest marker
``e2e``): two real c7200 routers on a real IOS image, one Ethernet link on
the kernel path and one serial link on the relay, driven through the
controller REST API and the IOS consoles. Real ICMP proves the datapath:
* the link reports ``kernel_datapath`` and the per-link kernel bridge on
this host has exactly the two anchor TAPs enslaved (kernel objects, not
server bookkeeping);
* a ``delay 100`` filter shows up as netem on the anchors and the measured
RTT rises by ~2x100 ms; clearing it resets the qdiscs and the RTT;
* suspend drives the anchor administratively down and the pings stop;
* capture writes a pcap full of ICMP frames;
* a serial link between the same routers stays on the relay and still
pings (the relay regression net);
* deleting / re-creating the link, and stopping / starting a router, leave
the wiring consistent;
* deleting the project leaves no anchors or bridges behind.
``test_dynamips_relay_control`` is the negative control: the same topology
on an isolated relay-configured instance wires nothing into the kernel — no
per-link bridge, nothing enslaved — and still pings, so the kernel objects
above can only come from the kernel datapath. (The anchors themselves exist
on both: they are the adapter's port interfaces, created by node start
whenever the tap module is available, exactly like QEMU's TAPs.)
"""
import re
import time
import pytest
from tests.e2e import harness
pytestmark = pytest.mark.e2e
# c7200 slot 0 is the fixed IO slot; the Ethernet PA goes in slot 1 (two
# FE ports) and the serial PA in slot 2 (four ports).
SLOTS = {"slot1": "PA-2FE-TX", "slot2": "PA-4T+"}
ETH = (1, 0)
SERIAL = (2, 0)
def _pick_image(server):
images = server.compute.dynamips_images()
image = next((name for name in images if name.startswith("c7200")), None)
if not image:
pytest.skip(f"no c7200 image available on this server (images: {images[:5]})")
return image
def _boot(server, project_id, node_id, timeout=360):
console = harness.dynamips_console(server, project_id, node_id)
console.boot_wait(timeout=timeout)
return console
def _eth_shutdown(console, shut):
console.run("conf t")
console.run("interface f1/0")
console.run("shutdown" if shut else "no shutdown")
console.run("end")
def test_dynamips_kernel_datapath():
server = harness.live_server(kernel=True)
compute = server.compute
if compute.capabilities().get("ubridge_tap") is not True:
pytest.skip("this compute's uBridge cannot create persistent TAPs (ubridge_tap != True)")
image = _pick_image(server)
idlepc = harness.dynamips_idlepc(server, image)
project = compute.create_project("dyn-e2e")
pid = project["project_id"]
t1 = t2 = None
try:
r1 = compute.create_dynamips_router(pid, "E2E-R1", image, SLOTS, idlepc=idlepc)
r2 = compute.create_dynamips_router(pid, "E2E-R2", image, SLOTS, idlepc=idlepc)
r1_id, r2_id = r1["node_id"], r2["node_id"]
t1 = harness.anchor_name(r1_id, *ETH)
t2 = harness.anchor_name(r2_id, *ETH)
# The link is created while both routers are stopped: the kernel NIO
# is bound with no anchors in existence yet and attached by node
# start (the deferred-wiring path).
link = compute.create_link(pid, (r1_id, *ETH), (r2_id, *ETH))
lid = link["link_id"]
bridge = harness.link_bridge_name(lid)
assert link["kernel_datapath"] is True, link
assert not harness.tap_exists(t1), "anchors must not exist before start"
print(".. starting routers and waiting for IOS boots")
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
compute.call("POST", f"/projects/{pid}/nodes/{r2_id}/start")
c1 = _boot(server, pid, r1_id)
c2 = _boot(server, pid, r2_id)
# Kernel objects: anchors exist and the per-link bridge has exactly
# the two of them enslaved.
assert harness.tap_exists(t1) and harness.tap_exists(t2), (t1, t2)
assert harness.bridge_members(bridge) == sorted([t1, t2]), harness.bridge_members(bridge)
# L2-anchor spec §E.1: anchors and the per-link bridge carry no L3
# identity (skipped on a uBridge without `link l2only` — the spec's
# degradation), and both ports are FORWARDING — the silent-failure
# guard against a bridge device left DOWN.
if harness.l2only_supported():
harness.assert_pure_l2(t1)
harness.assert_pure_l2(t2)
harness.assert_pure_l2(bridge)
else:
print(".. uBridge without link l2only: skipping the §E.1 assertions")
harness.assert_forwarding(t1)
harness.assert_forwarding(t2)
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1")
harness.configure_ios(c2, "R2", eth_ip="10.1.1.2")
out = c1.run("show ip int brief")
assert re.search(r"FastEthernet1/0\s+10\.1\.1\.1\s+\S+\s+\S+\s+up\s+up", out), out
print(".. both routers configured, pinging over the kernel link")
baseline = harness.wait_ping(c1, "10.1.1.2")
assert baseline["success"] == 100, baseline["raw"]
# L2-anchor spec §E.2: shut the guest interfaces (silencing IOS's own
# CDP/keepalives — the only legitimate speakers on the segment) and
# demand the host side stays silent for 5 s: without the hardening
# the host's own MLD/DAD noise floods the link (6 frames / 2 s).
if harness.l2only_supported():
for console in (c1, c2):
_eth_shutdown(console, shut=True)
harness.assert_idle_silence(t1, t2, bridge)
for console in (c1, c2):
_eth_shutdown(console, shut=False)
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
# delay 100: netem on both anchors (each impairs one direction),
# one-way ~100 ms => RTT grows by ~200 ms
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {"delay": [100]}})
assert "netem" in harness.qdiscs(t1) and "netem" in harness.qdiscs(t2), (
harness.qdiscs(t1),
harness.qdiscs(t2),
)
delayed = harness.wait_ping(c1, "10.1.1.2")
assert delayed["success"] == 100, delayed["raw"]
assert delayed["avg"] >= 150, (baseline, delayed)
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"filters": {}})
assert "netem" not in harness.qdiscs(t1), harness.qdiscs(t1)
fast = harness.wait_ping(c1, "10.1.1.2")
assert fast["success"] == 100 and fast["avg"] < 50, fast
# suspend: anchor admin-down, traffic dead; resume restores it
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"suspend": True})
assert not harness.tap_up(t1)
dead = harness.ping(c1, "10.1.1.2", repeat=3)
assert dead["success"] == 0, dead["raw"]
compute.call("PUT", f"/projects/{pid}/links/{lid}", {"suspend": False})
assert harness.tap_up(t1)
assert harness.wait_ping(c1, "10.1.1.2")["success"] == 100
# capture: AF_PACKET on the anchor writes a real pcap
capture = compute.call("POST", f"/projects/{pid}/links/{lid}/capture/start", {"data_link_type": "DLT_EN10MB"})
harness.ping(c1, "10.1.1.2")
time.sleep(1)
compute.call("POST", f"/projects/{pid}/links/{lid}/capture/stop")
path = capture["capture_file_path"]
count, ethertypes = harness.pcap_records(path)
assert count >= 4, f"{path}: {count} records"
assert "0800" in ethertypes, (path, ethertypes)
# serial link between the same routers: per-port exclusion keeps it
# on the relay, where the real traffic still crosses
serial = compute.create_link(pid, (r1_id, *SERIAL), (r2_id, *SERIAL))
assert serial["kernel_datapath"] is False, serial
harness.configure_ios(c1, "R1", serial_ip="10.2.2.1", clock=True)
harness.configure_ios(c2, "R2", serial_ip="10.2.2.2")
relay_ping = harness.wait_ping(c1, "10.2.2.2")
assert relay_ping["success"] == 100, relay_ping["raw"]
compute.call("DELETE", f"/projects/{pid}/links/{serial['link_id']}")
# delete / re-create the kernel link: anchors survive, the bridge
# goes and comes back
compute.call("DELETE", f"/projects/{pid}/links/{lid}")
assert harness.bridge_members(bridge) is None
assert harness.tap_exists(t1) and harness.tap_exists(t2)
link = compute.create_link(pid, (r1_id, *ETH), (r2_id, *ETH))
lid = link["link_id"]
bridge = harness.link_bridge_name(lid)
assert link["kernel_datapath"] is True, link
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
# stop / start keeps the whole wiring (the Dynamips hypervisor
# outlives ``vm stop``); the restarted router just reboots and is
# reconfigured
print(".. stopping and restarting R1")
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/stop")
assert harness.tap_exists(t1)
assert harness.bridge_members(bridge) == sorted([t1, t2])
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
c1.close()
c1 = _boot(server, pid, r1_id)
assert harness.bridge_members(bridge) == sorted([t1, t2])
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1")
assert harness.wait_ping(c1, "10.1.1.2", attempts=5)["success"] == 100
c1.close()
c2.close()
except BaseException:
harness.release(server, pid, failed=True)
raise
harness.release(server, pid, failed=False)
# deleting the project closes the nodes: anchors and bridges go with them
assert harness.wait_until(lambda: not harness.tap_exists(t1) and not harness.tap_exists(t2), timeout=15), (
t1,
t2,
)
assert harness.wait_until(lambda: harness.bridge_members(bridge) is None, timeout=10), bridge
def test_dynamips_relay_control():
"""Negative control: the same topology with ``enable_kernel_datapath =
false`` (an isolated instance) wires nothing into the kernel — no
per-link bridge, no enslavement — and still pings over the relay.
Anchors DO exist here (a Dynamips router creates its port TAPs at start
whenever the uBridge tap module is available — they are the adapter's
interfaces, exactly like QEMU's); the server configuration only decides
whether links are attached to them."""
server = harness.live_server(kernel=False)
compute = server.compute
image = _pick_image(server)
idlepc = harness.dynamips_idlepc(server, image)
project = compute.create_project("dyn-e2e-relay")
pid = project["project_id"]
t1 = None
try:
r1 = compute.create_dynamips_router(pid, "E2E-R1", image, SLOTS, idlepc=idlepc)
r2 = compute.create_dynamips_router(pid, "E2E-R2", image, SLOTS, idlepc=idlepc)
r1_id, r2_id = r1["node_id"], r2["node_id"]
t1 = harness.anchor_name(r1_id, *ETH)
link = compute.create_link(pid, (r1_id, *ETH), (r2_id, *ETH))
lid = link["link_id"]
assert link["kernel_datapath"] is False, link
print(".. starting routers and waiting for IOS boots (relay)")
compute.call("POST", f"/projects/{pid}/nodes/{r1_id}/start")
compute.call("POST", f"/projects/{pid}/nodes/{r2_id}/start")
c1 = _boot(server, pid, r1_id)
c2 = _boot(server, pid, r2_id)
# no per-link kernel bridge on the relay: nothing is enslaved, the
# anchor (if present) is just an idle port interface
assert harness.bridge_members(harness.link_bridge_name(lid)) is None
if harness.tap_exists(t1):
assert not harness.tap_up(t1), "an unattached relay anchor should sit down"
if harness.l2only_supported():
# the L2 hardening is a creation-time property, not a
# datapath one: relay anchors are pure L2 too
harness.assert_pure_l2(t1)
harness.configure_ios(c1, "R1", eth_ip="10.1.1.1")
harness.configure_ios(c2, "R2", eth_ip="10.1.1.2")
relay_ping = harness.wait_ping(c1, "10.1.1.2")
assert relay_ping["success"] == 100, relay_ping["raw"]
c1.close()
c2.close()
except BaseException:
harness.release(server, pid, failed=True)
raise
harness.release(server, pid, failed=False)
assert harness.wait_until(lambda: not harness.tap_exists(t1), timeout=15), t1