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.
This commit is contained in:
YueGuobin 2026-10-02 15:06:52 +08:00
parent f13da59f23
commit e0329ceee6
No known key found for this signature in database
4 changed files with 101 additions and 10 deletions

View File

@ -33,6 +33,15 @@ 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, host-side kernel inspections (bridge membership, tap
admin state, tc qdiscs, the L2-anchor spec §E assertions) — so a new node

View File

@ -39,6 +39,9 @@ Environment knobs:
* ``GNS3_E2E_USER`` / ``GNS3_E2E_PASSWORD`` — credentials for the target.
* ``GNS3_E2E_IMAGES`` — images directory the isolated instance uses
(default ``~/GNS3/images``); also where the dynamips test finds its image.
* ``GNS3_E2E_IDLEPC`` — use this idle-PC value for the dynamips routers
instead of detecting one (see ``dynamips_idlepc``; without an idle-PC each
router burns a full CPU core for the whole scenario).
* ``GNS3_E2E_KEEP=1`` — keep the project (and the isolated instance) when a
test fails, and print their ids, for inspection.
"""
@ -121,8 +124,10 @@ class Compute:
def create_project(self, name="e2e"):
return self.call("POST", "/projects", {"name": f"{name}-{int(time.time())}"})
def create_dynamips_router(self, project_id, name, image, slots, platform="c7200", ram=512):
def create_dynamips_router(self, project_id, name, image, slots, platform="c7200", ram=512, idlepc=None):
properties = {"platform": platform, "image": image, "ram": ram}
if idlepc:
properties["idlepc"] = idlepc
properties.update(slots)
return self.call(
"POST",
@ -350,6 +355,79 @@ def release(server, project_id, failed=False):
server.stop()
# ---------------------------------------------------------------------------
# Dynamips idle-PC (CPU)
# ---------------------------------------------------------------------------
# Detected values, keyed by image identity; the detection needs a throwaway
# boot, so it is worth caching across runs.
IDLEPC_CACHE_PATH = os.path.join(os.path.expanduser("~/.cache/gns3-e2e"), "idlepc.json")
def dynamips_idlepc(server, image, platform="c7200", ram=512):
"""The idle-PC value the scenarios' dynamips routers should be created
with — detected once per image and cached.
Without one, dynamips busy-waits and burns a *full CPU core per router*
for the whole scenario: the e2e creates raw nodes, and no template
supplies an idle-PC. Detection goes through the controller's
``auto_idlepc`` endpoint — the GUI's idle-PC finder path: a throwaway
project boots the image, candidate values are validated by measuring the
actual process CPU (first candidate under 70 % wins) — and the result is
cached in ``~/.cache/gns3-e2e/idlepc.json`` keyed by the image's
checksum, so the cost is paid once per image, not once per run.
``GNS3_E2E_IDLEPC`` overrides detection and cache entirely. Any failure
returns None: the scenarios then run without an idle-PC (and burn the
cores) rather than fail on a CPU optimisation.
"""
override = os.environ.get("GNS3_E2E_IDLEPC")
if override:
return override
checksum = None
for entry in server.compute.call("GET", "/computes/local/dynamips/images"):
if entry["filename"] == image:
checksum = entry.get("md5sum")
break
key = f"{image}:{checksum}" if checksum else image
cache = {}
try:
with open(IDLEPC_CACHE_PATH) as f:
cache = json.load(f)
except (OSError, ValueError):
pass
if cache.get(key):
print(f".. idle-PC {cache[key]} for {image} (cached)")
return cache[key]
print(f".. detecting an idle-PC for {image} (once per image — this takes a few minutes)")
try:
result = server.compute.call(
"POST",
"/computes/local/dynamips/auto_idlepc",
{"platform": platform, "image": image, "ram": ram},
timeout=600,
)
except Exception as e:
print(f".. idle-PC detection failed ({e}); the routers will burn a core each")
return None
idlepc = (result or {}).get("idlepc")
if not idlepc:
print(f".. no idle-PC value found for {image}; the routers will burn a core each")
return None
cache[key] = idlepc
try:
os.makedirs(os.path.dirname(IDLEPC_CACHE_PATH), exist_ok=True)
with open(IDLEPC_CACHE_PATH, "w") as f:
json.dump(cache, f, indent=2, sort_keys=True)
except OSError as e:
print(f".. could not write the idle-PC cache {IDLEPC_CACHE_PATH}: {e}")
print(f".. idle-PC {idlepc} for {image} (cached for later runs)")
return idlepc
# ---------------------------------------------------------------------------
# IOS console (dynamips telnet console)
# ---------------------------------------------------------------------------

View File

@ -84,13 +84,14 @@ def test_dynamips_kernel_datapath():
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)
r2 = compute.create_dynamips_router(pid, "E2E-R2", image, SLOTS)
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)
@ -244,13 +245,14 @@ def test_dynamips_relay_control():
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)
r2 = compute.create_dynamips_router(pid, "E2E-R2", image, SLOTS)
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)

View File

@ -67,7 +67,7 @@ def _switch_bridge_name(switch_id):
return "gns3" + switch_id.replace("-", "")[:6]
def _create_topology(compute, pid, image, name):
def _create_topology(compute, pid, image, name, idlepc=None):
switch = compute.call(
"POST",
f"/projects/{pid}/nodes",
@ -83,8 +83,8 @@ def _create_topology(compute, pid, image, name):
},
},
)
r1 = compute.create_dynamips_router(pid, f"{name}-R1", image, SLOTS)
r2 = compute.create_dynamips_router(pid, f"{name}-R2", image, SLOTS)
r1 = compute.create_dynamips_router(pid, f"{name}-R1", image, SLOTS, idlepc=idlepc)
r2 = compute.create_dynamips_router(pid, f"{name}-R2", image, SLOTS, idlepc=idlepc)
return switch, r1, r2
@ -100,12 +100,13 @@ def test_ethernet_switch_kernel_fast_path():
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("esw-e2e")
pid = project["project_id"]
sw_bridge = None
try:
switch, r1, r2 = _create_topology(compute, pid, image, "E2E")
switch, r1, r2 = _create_topology(compute, pid, image, "E2E", idlepc)
switch_id = switch["node_id"]
r1_id, r2_id = r1["node_id"], r2["node_id"]
sw_bridge = _switch_bridge_name(switch_id)
@ -262,12 +263,13 @@ def test_ethernet_switch_relay_control():
server = harness.live_server(kernel=False)
compute = server.compute
image = _pick_image(server)
idlepc = harness.dynamips_idlepc(server, image)
project = compute.create_project("esw-e2e-relay")
pid = project["project_id"]
sw_bridge = None
try:
switch, r1, r2 = _create_topology(compute, pid, image, "E2E")
switch, r1, r2 = _create_topology(compute, pid, image, "E2E", idlepc)
sw_bridge = _switch_bridge_name(switch["node_id"])
r1_id, r2_id = r1["node_id"], r2["node_id"]
a1 = harness.anchor_name(r1_id, *ETH)