From e0329ceee60670fb9faa7cb247640400a388b0c1 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Fri, 2 Oct 2026 15:06:52 +0800 Subject: [PATCH] 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. --- tests/e2e/README.md | 9 +++ tests/e2e/harness.py | 80 ++++++++++++++++++- tests/e2e/test_dynamips_kernel_datapath.py | 10 ++- .../test_ethernet_switch_kernel_datapath.py | 12 +-- 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/tests/e2e/README.md b/tests/e2e/README.md index b7d2b7e06..c5fcfc4b6 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -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 diff --git a/tests/e2e/harness.py b/tests/e2e/harness.py index 0cdd585c8..07391a45a 100644 --- a/tests/e2e/harness.py +++ b/tests/e2e/harness.py @@ -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) # --------------------------------------------------------------------------- diff --git a/tests/e2e/test_dynamips_kernel_datapath.py b/tests/e2e/test_dynamips_kernel_datapath.py index 0f8b82021..5e41d10eb 100644 --- a/tests/e2e/test_dynamips_kernel_datapath.py +++ b/tests/e2e/test_dynamips_kernel_datapath.py @@ -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) diff --git a/tests/e2e/test_ethernet_switch_kernel_datapath.py b/tests/e2e/test_ethernet_switch_kernel_datapath.py index 0e4a8fbfd..2cf98905d 100644 --- a/tests/e2e/test_ethernet_switch_kernel_datapath.py +++ b/tests/e2e/test_ethernet_switch_kernel_datapath.py @@ -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)