Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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}");
}