test(e2e): Debian QEMU build+boot+unlock harness and deterministic direct transport
Extend the QEMU e2e to cover the initramfs-tools backend and add a deterministic unlock transport that avoids the flaky public-Tor onion round-trip inside QEMU. - build_image_debian.sh: debootstrap Bookworm, install the real lim/configuration/initramfs-tools hooks, LUKS + cryptsetup-initramfs + dropbear-initramfs, offline onion keys, boot-ok marker; same image.env contract as build_image.sh. - config.py: QemuSpec gains os_family / unlock_command / direct_ssh_port; Debian cmdline uses root=/dev/mapper (crypttab-baked, no cryptdevice=); direct_ssh_port adds hostfwd to guest dropbear and a plain-SSH target. - harness.py: unlock_transport="direct" default, _NullNet, per-OS build script + unlock command, up-front sudo priming with keepalive. - boot_unlock.py: background delivery worker holds the SSH session open; direct vs tor target and initial delay. - test_qemu_harness_unit.py: always-on guards for the Debian/direct branches; test_qemu_unlock_e2e.py parameterized by ARCH/OS/TRANSPORT env. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -35,10 +35,20 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
# The QEMU binary is arch-specific, so derive it from LIM_E2E_ARCH instead of
|
||||
# hardcoding x86_64 — otherwise the documented aarch64 run is never reachable.
|
||||
_ARCH = os.environ.get("LIM_E2E_ARCH", "x86_64")
|
||||
_REQUIRED = (
|
||||
# The OS family selects the build tool: pacstrap (Arch) or debootstrap (Debian).
|
||||
_OS = os.environ.get("LIM_E2E_OS", "arch")
|
||||
# "direct" (deterministic port-forward) is the default; "tor" runs the full,
|
||||
# opt-in onion transport (flaky over public Tor inside QEMU).
|
||||
_TRANSPORT = os.environ.get("LIM_E2E_TRANSPORT", "direct")
|
||||
_BUILD_TOOL = {"arch": "pacstrap", "debian": "debootstrap"}
|
||||
_REQUIRED = [
|
||||
qemu_config.qemu_binary(_ARCH),
|
||||
"cryptsetup", "pacstrap", "tor", "ssh", "ncat",
|
||||
)
|
||||
_BUILD_TOOL.get(_OS, "pacstrap"),
|
||||
"cryptsetup",
|
||||
"ssh",
|
||||
]
|
||||
if _TRANSPORT == "tor": # the onion transport also needs a Tor client + ncat
|
||||
_REQUIRED += ["tor", "ncat"]
|
||||
|
||||
|
||||
def _missing_tools() -> list[str]:
|
||||
@@ -48,25 +58,24 @@ def _missing_tools() -> list[str]:
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("LIM_E2E_QEMU") != "1" or bool(_missing_tools()),
|
||||
reason=(
|
||||
"needs LIM_E2E_QEMU=1 and "
|
||||
+ ", ".join(_REQUIRED)
|
||||
+ " (build stage needs root; slow)."
|
||||
"needs LIM_E2E_QEMU=1 and " + ", ".join(_REQUIRED) + " (build stage needs root; slow)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_full_build_boot_and_tor_unlock(tmp_path):
|
||||
def test_full_build_boot_and_unlock(tmp_path):
|
||||
chutney_path = os.environ.get("CHUTNEY_PATH")
|
||||
cfg = HarnessConfig(
|
||||
repo_root=REPO_ROOT,
|
||||
work_dir=tmp_path / "qemu-e2e",
|
||||
arch=_ARCH,
|
||||
os_family=_OS,
|
||||
unlock_transport=_TRANSPORT,
|
||||
chutney_path=Path(chutney_path) if chutney_path else None,
|
||||
)
|
||||
|
||||
spec = run(cfg)
|
||||
|
||||
# run() only returns after the marker was observed; re-assert for clarity.
|
||||
assert spec.onion_address.endswith(".onion")
|
||||
# run() only returns after the boot-ok marker was observed on the console.
|
||||
serial = spec.serial_log.read_text(errors="replace")
|
||||
assert qemu_config.BOOT_OK_MARKER in serial
|
||||
|
||||
Reference in New Issue
Block a user