Initial Commit
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A saved machine must be fast, must match its image, and must never be load-bearing.
|
||||
|
||||
python3 tests/test_vm_warm_start.py
|
||||
|
||||
WHY
|
||||
|
||||
Resuming a saved machine turns a 90-second Windows boot into about 30
|
||||
seconds of session setup, and the cold start of PowerShell - measured at
|
||||
over ninety seconds on a fresh boot - is inside the saved memory rather
|
||||
than in front of the agent.
|
||||
|
||||
The risk is not that it is slow, it is that it is WRONG. A saved state
|
||||
describes memory that matches one particular disk, down to the page
|
||||
cache. Resume it against an image that has changed since and nothing
|
||||
reports an error: the guest simply runs with a filesystem in memory that
|
||||
disagrees with the one on disk. So the interlock - the checksum recorded
|
||||
when the image was frozen - is the part that matters, and these cases are
|
||||
mostly about it.
|
||||
|
||||
The second rule is that this is an optimisation. An optimisation that can
|
||||
take the feature down is not one, so a saved machine that will not load
|
||||
must become a cold boot rather than a failed session.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "vm", "vmd"))
|
||||
|
||||
import session as S # noqa: E402
|
||||
|
||||
FAILS: list[str] = []
|
||||
|
||||
|
||||
def check(label: str, condition: bool, detail: str = "") -> None:
|
||||
if condition:
|
||||
print(f"ok {label}")
|
||||
else:
|
||||
FAILS.append(f"{label}: {detail}")
|
||||
print(f"FAIL {label}\n {detail}")
|
||||
|
||||
|
||||
# --- the interlock ------------------------------------------------------
|
||||
import tempfile # noqa: E402
|
||||
|
||||
work = tempfile.mkdtemp()
|
||||
S.WARM_ROOT = os.path.join(work, "warm")
|
||||
S.BASE_ROOT = os.path.join(work, "base")
|
||||
os.makedirs(os.path.join(S.WARM_ROOT, "windows"))
|
||||
os.makedirs(S.BASE_ROOT)
|
||||
|
||||
warm = os.path.join(S.WARM_ROOT, "windows")
|
||||
for name in ("windows.qcow2", "state", "OVMF_VARS.fd"):
|
||||
open(os.path.join(warm, name), "wb").write(b"x")
|
||||
with open(os.path.join(S.BASE_ROOT, "windows.sha256"), "w") as fh:
|
||||
fh.write(f"{'a' * 64} {S.BASE_ROOT}/windows.qcow2\n"
|
||||
f"{'b' * 64} {S.BASE_ROOT}/windows-OVMF_VARS.fd\n")
|
||||
|
||||
|
||||
def manifest(sha: str) -> None:
|
||||
with open(os.path.join(warm, "manifest.json"), "w") as fh:
|
||||
json.dump({"os": "windows", "base_sha256": sha}, fh)
|
||||
|
||||
|
||||
manifest("a" * 64)
|
||||
check("a saved machine matching the image is usable", S.warm_set("windows") is not None)
|
||||
|
||||
manifest("c" * 64)
|
||||
check("one taken from a different image is refused", S.warm_set("windows") is None,
|
||||
"a mismatched checksum was accepted - this is how a disk gets corrupted")
|
||||
|
||||
manifest("a" * 64)
|
||||
os.rename(os.path.join(warm, "state"), os.path.join(warm, "state.away"))
|
||||
check("one with no state file is refused", S.warm_set("windows") is None)
|
||||
os.rename(os.path.join(warm, "state.away"), os.path.join(warm, "state"))
|
||||
|
||||
os.rename(os.path.join(warm, "manifest.json"), os.path.join(warm, "m.away"))
|
||||
check("one with no manifest is refused", S.warm_set("windows") is None,
|
||||
"nothing could vouch for it and it was used anyway")
|
||||
os.rename(os.path.join(warm, "m.away"), os.path.join(warm, "manifest.json"))
|
||||
|
||||
with open(os.path.join(warm, "manifest.json"), "w") as fh:
|
||||
fh.write("{not json")
|
||||
check("an unreadable manifest is refused", S.warm_set("windows") is None)
|
||||
manifest("a" * 64)
|
||||
|
||||
# An image with no recorded checksum cannot be vouched for either. That is
|
||||
# the state an image is in before it has ever been frozen.
|
||||
os.rename(os.path.join(S.BASE_ROOT, "windows.sha256"),
|
||||
os.path.join(S.BASE_ROOT, "away"))
|
||||
check("an image with no recorded checksum is refused", S.warm_set("windows") is None,
|
||||
"there was nothing to compare against and it was used anyway")
|
||||
os.rename(os.path.join(S.BASE_ROOT, "away"),
|
||||
os.path.join(S.BASE_ROOT, "windows.sha256"))
|
||||
|
||||
check("and it is usable again once everything lines up", S.warm_set("windows") is not None)
|
||||
check("an image with no saved machine at all is simply not warm",
|
||||
S.warm_set("ubuntu") is None)
|
||||
|
||||
# --- the source, read as source -----------------------------------------
|
||||
src = open(os.path.join(os.path.dirname(__file__), "..", "vm", "vmd", "session.py")).read()
|
||||
vmd_src = open(os.path.join(os.path.dirname(__file__), "..", "vm", "vmd", "vmd.py")).read()
|
||||
qmp_src = open(os.path.join(os.path.dirname(__file__), "..", "vm", "vmd", "qmp.py")).read()
|
||||
launch = open(os.path.join(os.path.dirname(__file__), "..", "vm", "host", "owuivm-launch")).read()
|
||||
|
||||
# The share cannot be in a saved state - virtiofsd refuses migration with
|
||||
# "vhost-user backend lacks VHOST_USER_PROTOCOL_F_LOG_SHMFD" - so a warm
|
||||
# session must not be given one at boot and must plug it in afterwards.
|
||||
check("a warm start boots without the share attached",
|
||||
"'' if warm else (share or '')" in src,
|
||||
"a saved machine cannot carry one; qemu refuses to migrate with it")
|
||||
check("and the share is plugged in after the resume",
|
||||
"_attach_share" in src and "attach_share" in qmp_src)
|
||||
check("into the port the argv always carries",
|
||||
"pcie-root-port,id=vfsport" in launch and 'bus="vfsport"' in qmp_src,
|
||||
"q35's root complex is not hot-pluggable; without the port there is nowhere to put it")
|
||||
check("at the same address a cold boot uses",
|
||||
"bus=vfsport,addr=0x0" in launch and 'addr="0x0"' in qmp_src,
|
||||
"a drive letter that moves between sessions is worse than a slow boot")
|
||||
|
||||
# The fallback is the whole safety argument.
|
||||
check("a warm start that fails becomes a cold boot",
|
||||
"allow_warm=False" in vmd_src and "warm_start_failed" in vmd_src,
|
||||
"an optimisation that can take the feature down is not one")
|
||||
check("and the caller is told it happened",
|
||||
"booted from cold" in vmd_src)
|
||||
|
||||
# The clock: a resumed machine believes it is whenever it was saved.
|
||||
check("the clock is set on every session, not only resumed ones",
|
||||
"_sync_clock(session)" in src and "clock = _sync_clock" in src)
|
||||
check("and the set is verified rather than assumed",
|
||||
"info_time" in src and "within" in src,
|
||||
"the first version swallowed the error and ran 26s behind")
|
||||
|
||||
# State is written through a descriptor vmd owns.
|
||||
check("machine state is written through a descriptor, not a path",
|
||||
"save_state_to_fd" in qmp_src and "send_fds" in qmp_src,
|
||||
"qemu writes 0600 as its own user; the alternatives are a 4 GB copy "
|
||||
"or letting the sandbox write the file a later session resumes from")
|
||||
check("and the resume path only accepts this host's own saved state",
|
||||
"/var/lib/owuivm/warm/(ubuntu|windows)/state$" in launch,
|
||||
"a migration stream sets the machine's registers, memory and devices")
|
||||
|
||||
print()
|
||||
if FAILS:
|
||||
print(f"FAILED {len(FAILS)}")
|
||||
raise SystemExit(1)
|
||||
print("a saved machine is fast, is checked against its image, and cannot break a session")
|
||||
Reference in New Issue
Block a user