test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
"""Boot a built image in QEMU and unlock its LUKS root over Tor.
|
|
|
|
|
|
|
|
|
|
This is the live half of the harness. QEMU runs rootless (user-mode net);
|
|
|
|
|
the passphrase is delivered exactly as an operator would: SSH through Tor to
|
|
|
|
|
the initramfs dropbear, piping the passphrase into the encryptssh prompt.
|
|
|
|
|
Success is observed on the serial console, not inferred.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import contextlib
|
|
|
|
|
import subprocess
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from tests.e2e.qemu import config
|
|
|
|
|
from tests.e2e.qemu.config import QemuSpec
|
|
|
|
|
|
|
|
|
|
# One generous budget covers guest boot + tor bootstrap + onion publish +
|
|
|
|
|
# unlock. TCG (no KVM) is slow, so keep it roomy.
|
2026-07-22 17:03:16 +02:00
|
|
|
UNLOCK_TIMEOUT = 480
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
DELIVERY_INTERVAL = 8
|
2026-07-22 17:03:16 +02:00
|
|
|
# Hold off the first delivery until the guest onion has had time to publish.
|
|
|
|
|
# A connect to a not-yet-published onion makes the client Tor cache the failed
|
|
|
|
|
# descriptor fetch and back off, so early hammering can lock us out for
|
|
|
|
|
# minutes; waiting until the onion is up means the first fetch succeeds.
|
|
|
|
|
INITIAL_DELIVERY_DELAY = 100
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
_SSH_ATTEMPT_TIMEOUT = 45
|
|
|
|
|
_POLL_INTERVAL = 2
|
|
|
|
|
_PROMPT_WAIT = 4 # let the encryptssh prompt appear before sending
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _serial_contains(spec: QemuSpec, needle: str) -> bool:
|
|
|
|
|
log = spec.serial_log
|
|
|
|
|
return log.is_file() and needle in log.read_text(errors="replace")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _stream_new_serial(spec: QemuSpec, offset: int) -> int:
|
|
|
|
|
"""Print serial output written since ``offset``; return the new offset.
|
|
|
|
|
|
|
|
|
|
Streams the guest's boot messages (netconf, the tor hook, dropbear,
|
|
|
|
|
encryptssh) live so the operator sees the VM working, not a black box.
|
|
|
|
|
"""
|
|
|
|
|
log = spec.serial_log
|
|
|
|
|
if not log.is_file():
|
|
|
|
|
return offset
|
|
|
|
|
data = log.read_bytes()
|
|
|
|
|
if len(data) > offset:
|
|
|
|
|
print(data[offset:].decode(errors="replace"), end="", flush=True)
|
|
|
|
|
return len(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _serial_tail(spec: QemuSpec, lines: int = 15) -> str:
|
|
|
|
|
if not spec.serial_log.is_file():
|
|
|
|
|
return "(no serial output captured)"
|
|
|
|
|
tail = spec.serial_log.read_text(errors="replace").splitlines()[-lines:]
|
|
|
|
|
return "serial tail:\n " + "\n ".join(tail)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _qemu_stderr_path(spec: QemuSpec) -> Path:
|
|
|
|
|
return spec.work_dir / "qemu.stderr.log"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _qemu_stderr_tail(spec: QemuSpec, lines: int = 10) -> str:
|
|
|
|
|
path = _qemu_stderr_path(spec)
|
|
|
|
|
if not path.is_file() or not path.read_text(errors="replace").strip():
|
|
|
|
|
return ""
|
|
|
|
|
tail = path.read_text(errors="replace").splitlines()[-lines:]
|
|
|
|
|
return "\nqemu stderr:\n " + "\n ".join(tail)
|
|
|
|
|
|
|
|
|
|
|
2026-07-22 17:03:16 +02:00
|
|
|
def _ssh_log_path(spec: QemuSpec) -> Path:
|
|
|
|
|
return spec.work_dir / "ssh-delivery.log"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ssh_log_tail(spec: QemuSpec, lines: int = 15) -> str:
|
|
|
|
|
path = _ssh_log_path(spec)
|
|
|
|
|
if not path.is_file() or not path.read_text(errors="replace").strip():
|
|
|
|
|
return ""
|
|
|
|
|
tail = path.read_text(errors="replace").splitlines()[-lines:]
|
|
|
|
|
return "\nssh delivery log:\n " + "\n ".join(tail)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _client_tor_tail(spec: QemuSpec, lines: int = 20) -> str:
|
|
|
|
|
"""Return the host Tor client's HS log — onion descriptor fetch/rendezvous."""
|
|
|
|
|
path = spec.work_dir / "tor-client-hs.log"
|
|
|
|
|
if not path.is_file() or not path.read_text(errors="replace").strip():
|
|
|
|
|
return ""
|
|
|
|
|
lines_out = [
|
|
|
|
|
line
|
|
|
|
|
for line in path.read_text(errors="replace").splitlines()
|
|
|
|
|
if any(k in line.lower() for k in ("desc", "rend", "intro", "hs_", "onion"))
|
|
|
|
|
][-lines:]
|
|
|
|
|
return "\nclient tor HS log:\n " + "\n ".join(lines_out)
|
|
|
|
|
|
|
|
|
|
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
def _deliver_worker(spec: QemuSpec) -> None:
|
|
|
|
|
"""Feed the passphrase over an SSH-through-Tor session, held open.
|
|
|
|
|
|
|
|
|
|
Runs in a background thread so the main loop keeps streaming serial. The
|
|
|
|
|
channel is deliberately kept OPEN after sending: closing stdin immediately
|
|
|
|
|
(as a plain pipe does) tears the session down before cryptsetup finishes
|
2026-07-22 17:03:16 +02:00
|
|
|
its argon2 KDF, killing the unlock mid-flight. Session output is appended to
|
|
|
|
|
ssh-delivery.log so a failed unlock (onion unreachable, cryptroot-unlock
|
|
|
|
|
error) is diagnosable.
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
"""
|
2026-07-22 17:03:16 +02:00
|
|
|
log_file = _ssh_log_path(spec).open("a")
|
|
|
|
|
log_file.write("--- delivery attempt ---\n")
|
|
|
|
|
log_file.flush()
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
proc = subprocess.Popen(
|
|
|
|
|
config.ssh_argv(spec),
|
2026-07-22 17:03:16 +02:00
|
|
|
stdin=subprocess.PIPE,
|
|
|
|
|
stdout=log_file,
|
|
|
|
|
stderr=log_file,
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
time.sleep(_PROMPT_WAIT)
|
|
|
|
|
if proc.poll() is None and proc.stdin is not None:
|
|
|
|
|
try:
|
|
|
|
|
proc.stdin.write(spec.passphrase + "\n")
|
|
|
|
|
proc.stdin.flush()
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
proc.wait(timeout=_SSH_ATTEMPT_TIMEOUT)
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
proc.kill()
|
|
|
|
|
finally:
|
|
|
|
|
if proc.stdin is not None:
|
|
|
|
|
with contextlib.suppress(OSError):
|
|
|
|
|
proc.stdin.close()
|
2026-07-22 17:03:16 +02:00
|
|
|
log_file.close()
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def boot_and_unlock(spec: QemuSpec) -> None:
|
|
|
|
|
"""Boot the image, unlock it over Tor, and confirm the real system booted.
|
|
|
|
|
|
|
|
|
|
Returns normally only when the boot-ok marker appears on the serial
|
|
|
|
|
console; the passphrase is (re)delivered on each poll until then. Raises
|
|
|
|
|
with the serial tail on QEMU death or timeout.
|
|
|
|
|
"""
|
|
|
|
|
if spec.serial_log.exists():
|
|
|
|
|
spec.serial_log.unlink()
|
2026-07-22 17:03:16 +02:00
|
|
|
if spec.direct_ssh_port:
|
|
|
|
|
target = f"127.0.0.1:{spec.direct_ssh_port} -> guest dropbear (direct)"
|
|
|
|
|
# dropbear comes up within seconds; no need to wait for an onion.
|
|
|
|
|
initial_delay = 15
|
|
|
|
|
else:
|
|
|
|
|
target = f"{spec.onion_address}:22 (Tor onion)"
|
|
|
|
|
initial_delay = INITIAL_DELIVERY_DELAY
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
print(f"\n========== booting in QEMU ({spec.arch}, accel={spec.accel}) ==========")
|
2026-07-22 17:03:16 +02:00
|
|
|
print(f"Unlock target: {target} (budget {UNLOCK_TIMEOUT}s)")
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
print("---- guest serial console ----", flush=True)
|
|
|
|
|
# QEMU's own diagnostics (bad -machine/-cpu, /dev/kvm denied, image busy)
|
|
|
|
|
# go to stderr; capture them so an early exit is debuggable, not opaque.
|
|
|
|
|
qemu_stderr = _qemu_stderr_path(spec).open("wb")
|
|
|
|
|
qemu = subprocess.Popen(
|
|
|
|
|
config.qemu_argv(spec),
|
2026-07-22 17:03:16 +02:00
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
stderr=qemu_stderr,
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
deadline = time.monotonic() + UNLOCK_TIMEOUT
|
|
|
|
|
offset = 0
|
2026-07-22 17:03:16 +02:00
|
|
|
# Hold off the first delivery until the unlock endpoint is up (see above).
|
|
|
|
|
next_delivery = time.monotonic() + initial_delay
|
|
|
|
|
print(
|
|
|
|
|
f"[harness] waiting {initial_delay}s for the unlock endpoint before delivering...",
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
delivery: threading.Thread | None = None
|
|
|
|
|
while time.monotonic() < deadline:
|
|
|
|
|
offset = _stream_new_serial(spec, offset)
|
|
|
|
|
if _serial_contains(spec, config.BOOT_OK_MARKER):
|
|
|
|
|
print("\n[harness] boot-ok marker seen — LUKS root unlocked and booted.")
|
|
|
|
|
return
|
|
|
|
|
if qemu.poll() is not None:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"QEMU exited early (code {qemu.returncode}).\n"
|
|
|
|
|
f"{_serial_tail(spec)}{_qemu_stderr_tail(spec)}"
|
|
|
|
|
)
|
|
|
|
|
# One delivery at a time; each holds its SSH session open in the
|
|
|
|
|
# background while we keep streaming serial and watching the marker.
|
|
|
|
|
idle = delivery is None or not delivery.is_alive()
|
|
|
|
|
if idle and time.monotonic() >= next_delivery:
|
2026-07-22 17:03:16 +02:00
|
|
|
print("\n[harness] delivering passphrase (holding session open) ...", flush=True)
|
|
|
|
|
delivery = threading.Thread(target=_deliver_worker, args=(spec,), daemon=True)
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
delivery.start()
|
|
|
|
|
next_delivery = time.monotonic() + DELIVERY_INTERVAL
|
|
|
|
|
time.sleep(_POLL_INTERVAL)
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
f"Boot-ok marker never appeared within {UNLOCK_TIMEOUT}s.\n"
|
2026-07-22 17:03:16 +02:00
|
|
|
f"{_serial_tail(spec, lines=45)}{_qemu_stderr_tail(spec)}"
|
|
|
|
|
f"{_ssh_log_tail(spec)}{_client_tor_tail(spec)}"
|
test(e2e): full virtualized build+boot+unlock QEMU harness
Opt-in via LIM_E2E_QEMU=1. Models the whole process on a virtio VM (the
Pi's USB-gadget net can't be emulated, so it models the software stack,
not the board): builds a LUKS image carrying the real lim initcpio Tor
artifacts, boots it in QEMU rootless, lets the real netconf/tor/dropbear/
encryptssh chain publish the onion, delivers the passphrase over Tor, and
asserts the boot-ok marker on the serial console. Supports a private
offline Tor network via chutney.
The pure command builders (config.qemu_argv/kernel_cmdline/ssh_argv,
qemu_binary), the env parser, and drift guards that keep build_image.sh
aligned with the harness run in the normal suite — no QEMU/root/network.
The build stage needs root; harness.py primes sudo up front, keeps the
credential warm, and reclaims work-dir ownership on every exit path so
the pytest tmp cleanup never trips on root-owned files. QEMU stderr is
captured so an early exit is debuggable, and each teardown step is
fault-isolated so none masks the real error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 18:50:55 +02:00
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
qemu.terminate()
|
|
|
|
|
try:
|
|
|
|
|
qemu.wait(timeout=15)
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
qemu.kill()
|
|
|
|
|
qemu.wait(timeout=10)
|
|
|
|
|
qemu_stderr.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_env_file(path: Path) -> dict[str, str]:
|
|
|
|
|
"""Read the KEY=VALUE image.env written by build_image.sh into a dict."""
|
|
|
|
|
result: dict[str, str] = {}
|
|
|
|
|
for line in path.read_text().splitlines():
|
|
|
|
|
if "=" in line and not line.startswith("#"):
|
|
|
|
|
key, value = line.split("=", 1)
|
|
|
|
|
result[key.strip()] = value.strip()
|
|
|
|
|
return result
|