xtask/tests/installed_main_serves_kvm.rs
//! **The installed main SERVES, and a keep medium's declaration is applied** (lane B1,
//! 2026-10-21).
//!
//! R10 (2026-10-20) proved the install: the GPT, the keep boot, INSTALL-OK. It never proved
//! that the installed main serves: its harness killed QEMU at the first INSTALL-OK, and the
//! one serve attempt answered `http 000` because PID 1 died on a missing `/data/holger.ron`.
//! Nothing read the `000` as a failure.
//!
//! This proof takes two media composed through the Build Thing (`dwarves buildthing … --variant
//! holger-installer-metered` and `--variant holger-keep`), each with its own `holger_ron`:
//!
//! 1. the FRESH medium on blank disks (a system disk and a data disk, as UpCloud lays a main),
//! run until the guest itself stops — never killed at the marker;
//! 2. the laid disk booted with no medium: `GET /healthz` on the first declaration's HTTP door
//! must answer 200;
//! 3. the KEEP medium over the same disks, run until the guest stops;
//! 4. the disk booted again: the keep medium's declaration moved the HTTP door, so the new
//! door must answer 200 and the old one must not — the declaration rode the release.
//!
//! ```text
//! HOLGER_SERVE_ISO=.iso-out/holger-installer-metered.iso HOLGER_SERVE_PORT=8443 \
//! HOLGER_KEEP_ISO=.iso-out/holger-keep.iso HOLGER_KEEP_PORT=8444 \
//! cargo test -p xtask --test installed_main_serves_kvm -- --ignored --nocapture
//! ```
use std::io::{Read as _, Write as _};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
/// How long one install may take before the guest is called stuck.
const INSTALL_BUDGET: Duration = Duration::from_secs(20 * 60);
/// How long the laid disk may take to serve.
const SERVE_BUDGET: Duration = Duration::from_secs(15 * 60);
/// The host ports the guest's HTTP doors are forwarded to.
const HOST_A: u16 = 28443;
const HOST_B: u16 = 28444;
fn qemu(dir: &Path, serial: &str, iso: Option<&Path>, net: Option<(u16, u16)>) -> Child {
let mut c = Command::new("qemu-system-x86_64");
c.args([
"-enable-kvm",
"-m",
"3072",
"-smp",
"2",
"-nographic",
"-no-reboot",
])
.args([
"-drive",
&format!(
"file={},format=raw,if=virtio",
dir.join("sys.img").display()
),
])
.args([
"-drive",
&format!(
"file={},format=raw,if=virtio",
dir.join("data.img").display()
),
]);
match iso {
Some(iso) => {
c.args([
"-cdrom",
&iso.display().to_string(),
"-boot",
"d",
"-nic",
"none",
]);
}
None => {
let (a, b) = net.expect("a disk boot is reached over the network");
c.args(["-boot", "c"])
.args(["-netdev", &format!("user,id=pub,net=10.0.2.0/24,hostfwd=tcp:127.0.0.1:{HOST_A}-:{a},hostfwd=tcp:127.0.0.1:{HOST_B}-:{b}")])
.args(["-device", "virtio-net-pci,netdev=pub,mac=52:54:00:00:00:01"]);
}
}
let log = std::fs::File::create(dir.join(serial)).unwrap();
c.stdout(Stdio::from(log.try_clone().unwrap()))
.stderr(Stdio::from(log))
.stdin(Stdio::null());
c.spawn().expect("qemu-system-x86_64 runs")
}
/// Run a medium until the guest stops by itself (power-off or reboot under `-no-reboot`).
fn install(dir: &Path, iso: &Path, serial: &str) -> String {
let mut child = qemu(dir, serial, Some(iso), None);
let t = Instant::now();
let status = loop {
if let Some(s) = child.try_wait().unwrap() {
break Some(s);
}
if t.elapsed() > INSTALL_BUDGET {
let _ = child.kill();
let _ = child.wait();
break None;
}
std::thread::sleep(Duration::from_secs(2));
};
let log = std::fs::read_to_string(dir.join(serial)).unwrap_or_default();
assert!(
status.is_some(),
"{serial}: the install never stopped by itself in {INSTALL_BUDGET:?}:\n{}",
tail(&log)
);
let ok = log
.find("INSTALL-OK")
.unwrap_or_else(|| panic!("{serial}: no INSTALL-OK:\n{}", tail(&log)));
// INSTALL-OK is the install's LAST word: the data set is laid before it.
if let Some(set) = log.find("data 1×") {
assert!(
set < ok,
"{serial}: the data set was laid AFTER INSTALL-OK:\n{}",
tail(&log)
);
}
log
}
/// `GET path` on `127.0.0.1:port`: the status code, or 0.
fn get(port: u16, path: &str) -> u16 {
let Ok(mut s) =
TcpStream::connect_timeout(&([127, 0, 0, 1], port).into(), Duration::from_secs(3))
else {
return 0;
};
let _ = s.set_read_timeout(Some(Duration::from_secs(5)));
if s.write_all(
format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n").as_bytes(),
)
.is_err()
{
return 0;
}
let mut buf = [0u8; 64];
let n = s.read(&mut buf).unwrap_or(0);
std::str::from_utf8(&buf[..n])
.ok()
.and_then(|h| h.split_whitespace().nth(1))
.and_then(|c| c.parse().ok())
.unwrap_or(0)
}
/// Boot the laid disk until `serves` answers 200, then answer what `other` answers.
fn boot_and_serve(
dir: &Path,
serial: &str,
ports: (u16, u16),
serves: u16,
other: Option<u16>,
) -> (bool, u16, String) {
let mut child = qemu(dir, serial, None, Some(ports));
let t = Instant::now();
let mut served = false;
while t.elapsed() < SERVE_BUDGET {
if child.try_wait().unwrap().is_some() {
break;
}
if get(serves, "/healthz") == 200 {
served = true;
break;
}
std::thread::sleep(Duration::from_secs(5));
}
let other_code = other.map(|p| get(p, "/healthz")).unwrap_or(0);
let _ = child.kill();
let _ = child.wait();
(
served,
other_code,
std::fs::read_to_string(dir.join(serial)).unwrap_or_default(),
)
}
fn tail(s: &str) -> String {
let lines: Vec<&str> = s.lines().collect();
lines[lines.len().saturating_sub(60)..].join("\n")
}
fn env_path(k: &str) -> Option<PathBuf> {
std::env::var_os(k).map(PathBuf::from)
}
#[test]
#[ignore = "requires: KVM and two composed media (HOLGER_SERVE_ISO, HOLGER_KEEP_ISO) — a real install, a real boot of the laid disk, and a keep medium over it"]
fn the_installed_main_serves_and_a_keep_medium_applies_its_declaration() {
let (Some(fresh), Some(keep)) = (env_path("HOLGER_SERVE_ISO"), env_path("HOLGER_KEEP_ISO"))
else {
eprintln!("SKIP installed_main_serves_kvm: set HOLGER_SERVE_ISO and HOLGER_KEEP_ISO");
return;
};
let port = |k: &str, d: u16| {
std::env::var(k)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(d)
};
let (guest_a, guest_b) = (
port("HOLGER_SERVE_PORT", 8443),
port("HOLGER_KEEP_PORT", 8444),
);
assert_ne!(
guest_a, guest_b,
"the keep medium's declaration must move the door, or the proof proves nothing"
);
let dir = env_path("HOLGER_KVM_DIR").unwrap_or_else(|| {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../target/installed-main-serves-kvm")
});
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
for (name, gib) in [("sys.img", 30u64), ("data.img", 20)] {
std::fs::File::create(dir.join(name))
.unwrap()
.set_len(gib << 30)
.unwrap();
}
// 1-2. fresh: install, then the laid disk serves the first declaration.
let log = install(&dir, &fresh, "serial-fresh.log");
assert!(
log.contains("release declaration: holger.ron"),
"the fresh medium lays its declaration with the app:\n{}",
tail(&log)
);
let (served, _, boot) = boot_and_serve(
&dir,
"serial-serve-fresh.log",
(guest_a, guest_b),
HOST_A,
None,
);
assert!(
served,
"the installed main never answered 200 on its HTTP door ({guest_a}):\n{}",
tail(&boot)
);
assert!(
boot.contains("release declaration: ") && boot.contains("verified with the app"),
"korp-start lays the release declaration:\n{}",
tail(&boot)
);
// 3-4. keep: the same disks, the keep medium's declaration moves the door.
let log = install(&dir, &keep, "serial-keep.log");
assert!(
log.contains("mode upgrade"),
"the keep medium upgrades in place:\n{}",
tail(&log)
);
assert!(
log.contains("release declaration: holger.ron"),
"the keep medium lays ITS declaration:\n{}",
tail(&log)
);
let (served, old, boot) = boot_and_serve(
&dir,
"serial-serve-keep.log",
(guest_a, guest_b),
HOST_B,
Some(HOST_A),
);
assert!(
served,
"after the keep medium the main never answered 200 on the NEW door ({guest_b}):\n{}",
tail(&boot)
);
assert_ne!(
old, 200,
"the old door still serves: the keep medium's declaration was not applied"
);
println!("installed main: fresh served on {guest_a}; keep served on {guest_b}, the old door answered {old}");
}