feat(prepare-win): pre-snapshot idle CPU/RAM check (abort if busy)
After the final reboot and just before the graceful shutdown for the snapshot, sample the guest's idle CPU and used-RAM percentages (CIM Win32_PerfFormattedData — locale-independent — averaged over ~5s after a settle delay). If either exceeds its threshold, abort prepare without taking the snapshot, so a busy/leaky template isn't frozen into every clone. New options: --max-idle-cpu (10%), --max-idle-ram (50%), --idle-settle (30s), --skip-idle-check.
This commit is contained in:
@@ -448,6 +448,8 @@ def _make_transport() -> MagicMock:
|
||||
)
|
||||
|
||||
def _run(script: str = "", **kwargs: object) -> WinRmResult:
|
||||
if "PercentProcessorTime" in script: # pre-snapshot idle check
|
||||
return WinRmResult(stdout='{"cpu":2.0,"ram":21.0}', stderr="", returncode=0)
|
||||
if "ConvertTo-Json" in script:
|
||||
return WinRmResult(stdout=val_json, stderr="", returncode=0)
|
||||
return WinRmResult(stdout="CI_EXITCODE:0", stderr="", returncode=0)
|
||||
@@ -530,6 +532,7 @@ def _patch_prepare(monkeypatch: pytest.MonkeyPatch, backend: MagicMock, transpor
|
||||
monkeypatch.setattr(tmpl_mod, "_wait_tcp", lambda *_: True)
|
||||
monkeypatch.setattr(tmpl_mod, "_wait_tcp_down", lambda *_a, **_k: True)
|
||||
monkeypatch.setattr(tmpl_mod, "_wait_winrm", lambda *_: None)
|
||||
monkeypatch.setattr(tmpl_mod, "_check_guest_idle", lambda *_a, **_k: None)
|
||||
monkeypatch.setattr(tmpl_mod.time, "sleep", lambda _: None)
|
||||
monkeypatch.setattr(tmpl_mod.subprocess, "run", lambda *a, **_: None)
|
||||
|
||||
@@ -2228,3 +2231,30 @@ def test_clean_force_artifacts_noop_on_clean_dir(tmp_path: Path) -> None:
|
||||
|
||||
assert removed == []
|
||||
assert (tmp_path / "LinuxBuild2404.vmdk").is_file()
|
||||
|
||||
|
||||
def test_check_guest_idle_ok() -> None:
|
||||
transport = MagicMock()
|
||||
transport.run.return_value = WinRmResult(stdout='{"cpu":3.0,"ram":22.5}', stderr="", returncode=0)
|
||||
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0) # no raise
|
||||
|
||||
|
||||
def test_check_guest_idle_cpu_over() -> None:
|
||||
transport = MagicMock()
|
||||
transport.run.return_value = WinRmResult(stdout='{"cpu":42.0,"ram":20.0}', stderr="", returncode=0)
|
||||
with pytest.raises(click.ClickException, match=r"CPU 42\.0% > 10\.0%"):
|
||||
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0)
|
||||
|
||||
|
||||
def test_check_guest_idle_ram_over() -> None:
|
||||
transport = MagicMock()
|
||||
transport.run.return_value = WinRmResult(stdout='{"cpu":1.0,"ram":80.0}', stderr="", returncode=0)
|
||||
with pytest.raises(click.ClickException, match=r"RAM 80\.0% > 50\.0%"):
|
||||
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0)
|
||||
|
||||
|
||||
def test_check_guest_idle_unparseable() -> None:
|
||||
transport = MagicMock()
|
||||
transport.run.return_value = WinRmResult(stdout="not json", stderr="", returncode=0)
|
||||
with pytest.raises(click.ClickException, match="could not parse"):
|
||||
tmpl_mod._check_guest_idle(transport, max_cpu=10.0, max_ram=50.0, settle_s=0)
|
||||
|
||||
Reference in New Issue
Block a user