draupnir 0.1.9

Draupnir — the nordisk boot/provisioning library: fire up a runtime from one BootSpec across three backends (KVM via tunnr · OCI container · Redfish bare-metal virtual-media) and drive its power lifecycle. Odin's ring that drips eight identical copies → boot a fleet of identical machines from one ISO.
Documentation
//! **BOOT OFF A MEDIUM — the real one.** A genuine QEMU/KVM guest, launched
//! through draupnir's [`KvmBoot`] from a [`BootSpec`] that names an ISO, with the
//! guest's own serial console read back as the evidence.
//!
//! This is the proof the ISO chain could not have before 2026-08-15: draupnir's
//! `ImageSource::Iso` was legal on the Redfish backend only, so there was no value
//! a caller could hand `KvmBoot` that meant "boot off this medium". `iso-kvm` was
//! unreachable by construction, and with it every downstream stage.
//!
//! # What it asserts, and what it refuses to
//!
//! It asserts **applied output**: lines that the firmware and the guest actually
//! printed on `ttyS0`, captured by tunnr off `-serial mon:stdio`. Not that a
//! function returned `Ok`, not that a field round-tripped — a UEFI banner and a
//! kernel boot line that only exist if a machine really executed code off that CD.
//!
//! It waits for a **named marker or a deadline**, never for the VM to exit. An
//! appliance that boots correctly serves forever; a test that waits for termination
//! is a test that only passes when the appliance is broken.
//!
//! # Running it
//!
//! ```text
//! DRAUPNIR_VM_BOOT_TIMEOUT=120 PROOF_ISO=/path/to/appliance.iso \
//!   cargo test --features backend-tunnr --test boot_off_medium_proof -- --ignored --nocapture
//! ```
//!
//! `#[ignore]` because it needs `/dev/kvm`, `qemu-system-x86_64`, OVMF and a real
//! ISO. When a prerequisite is missing it **skips LOUDLY, by name** — printing
//! which file or device was absent — and never returns a green that stands for a
//! boot that did not happen.
#![cfg(feature = "backend-tunnr")]

use std::path::Path;
use std::time::Duration;

use draupnir::kvm::KvmBoot;
use draupnir::{Boot, BootOrder, BootSpec, Lifecycle, Seen};

/// The ISO this proof boots. Overridable, because the appliance ISO's path is a
/// property of the box, not of the test.
fn proof_iso() -> String {
    std::env::var("PROOF_ISO")
        .unwrap_or_else(|_| "/home/rickard/Hämtningar/gunnar-appliance-x86_64.iso".to_string())
}

/// The console substring that means **the firmware executed code off the medium**
/// — UEFI naming the CD/DVD device it chose to boot. This one is generic to QEMU,
/// not to any appliance: any bootable ISO produces it, and nothing else does.
const MEDIUM_MARKER: &str = "UEFI QEMU DVD-ROM";

/// The console substring that means **the system that came off the medium reached
/// userspace and started serving**. Booting the firmware is not the claim; a
/// running appliance is. Overridable, because this half is necessarily specific to
/// the ISO under test — the default matches the gunnar appliance on this box.
fn proof_marker() -> String {
    std::env::var("PROOF_MARKER").unwrap_or_else(|_| "gunnar ssh listener bound".to_string())
}

/// Report a missing prerequisite by name and return `true` (skip). Never silent:
/// the reason is printed, so a skipped proof is legible in the log rather than
/// looking like a pass.
fn missing(what: &str, path: &str) -> bool {
    eprintln!("SKIP boot_off_medium_proof: {what} not present at `{path}` — this proof did NOT run");
    true
}

fn prerequisites_absent(iso: &str) -> bool {
    if !Path::new("/dev/kvm").exists() {
        return missing("/dev/kvm", "/dev/kvm");
    }
    if !Path::new(iso).is_file() {
        return missing("the boot medium (ISO)", iso);
    }
    false
}

/// **Boot a real machine off a real ISO and read its console back.**
#[test]
#[ignore = "REAL BOOT: needs /dev/kvm, qemu-system-x86_64, OVMF and an ISO on disk"]
fn a_kvm_guest_really_boots_off_the_medium_and_says_so_on_its_console() {
    let iso = proof_iso();
    if prerequisites_absent(&iso) {
        return;
    }

    // ONE spec. It names the medium and says to boot off it — nothing about QEMU,
    // nothing about tunnr. The same value routes to real metal via `.on_metal(bmc)`.
    let spec = BootSpec::iso_boot("iso-kvm-proof", &iso);
    spec.validate().expect("the medium spec is well formed");
    assert_eq!(spec.boot_order, BootOrder::Medium);
    assert_eq!(spec.medium_path(), Some(iso.as_str()));

    let backend = KvmBoot::new();
    let machine = backend.boot(&spec).expect("a real QEMU/KVM guest starts");
    println!("BOOTED off the medium: id={} iso={iso}", machine.id);

    // Half 1 — the FIRMWARE really chose the CD. Any bootable ISO prints this and
    // a disk boot never does, so it is the load-bearing "off the medium" evidence.
    let firmware = backend
        .await_serial_marker(
            &machine,
            MEDIUM_MARKER,
            Duration::from_secs(60),
            Duration::from_millis(250),
        )
        .expect("the guest this backend booted is addressable");

    // Half 2 — the system that came off it reached USERSPACE and is serving.
    // Booting firmware is not the claim; a running appliance is.
    let marker = proof_marker();
    let serving = backend
        .await_serial_marker(
            &machine,
            &marker,
            Duration::from_secs(120),
            Duration::from_millis(250),
        )
        .expect("the guest is still addressable");

    // A thing that keeps running is the NORMAL case: observe that it is STILL up
    // after the marker, and never wait for it to exit.
    let still_up = backend.status(&machine);

    // Print the WHOLE console before asserting, so a failure is diagnosable from
    // the log rather than from a boolean.
    let console = backend.serial_log(&machine).unwrap_or_default();
    println!("---------------- SERIAL CONSOLE ({} bytes) ----------------", console.len());
    println!("{console}");
    println!("---------------- END SERIAL CONSOLE ----------------");
    println!("FIRMWARE: {}", firmware.detail());
    println!("SERVING : {}", serving.detail());
    println!("STATUS  : {still_up:?}");

    // Tear the machine down before asserting, so a failed assertion never leaks a
    // QEMU process onto the box.
    let _ = backend.power_off(&machine);

    assert!(
        firmware.saw_marker(),
        "the FIRMWARE never named the DVD-ROM — this was not a boot off the medium.\n\
         observed: {}\nconsole:\n{console}",
        firmware.detail()
    );
    match &serving {
        Seen::Marker { line, after } => {
            println!("PROOF: booted off the medium and reached userspace in {after:?}");
            println!("PROOF: `{}`", line.trim());
        }
        other => panic!(
            "the system off the medium never printed `{marker}`.\nobserved: {}\nconsole:\n{console}",
            other.detail()
        ),
    }
    assert_eq!(
        still_up.unwrap(),
        draupnir::PowerState::On,
        "the appliance is still SERVING after it announced itself — an appliance that \
         had to exit for this test to pass would be a broken appliance"
    );

    // The console must be real output, not an empty string that trivially contains
    // nothing — an empty capture would make any negative assertion vacuous.
    assert!(
        console.len() > 4096,
        "the serial capture is a real boot, got {} bytes",
        console.len()
    );
}

/// **THE RED, measured on a real machine: a "booted off the disk" claim must go
/// red when the disk is BLANK.**
///
/// The positive proof above is only worth what its negative is worth. If a blank
/// disk also produced an appliance banner, the banner would be evidence of nothing.
/// So: boot the *second-lifetime* spec — `-boot c`, no medium — against a disk that
/// has never been installed to, and assert that **neither** marker appears. No
/// DVD-ROM line (there is no medium to fall back to, which is the whole reason the
/// medium must be absent) and no appliance (there is nothing on the disk to run).
///
/// This is the sabotage that cannot be reverted away: the disk really is blank.
#[test]
#[ignore = "REAL BOOT: needs /dev/kvm, qemu-system-x86_64 and OVMF"]
fn a_blank_disk_boots_no_appliance_and_names_no_medium() {
    if !Path::new("/dev/kvm").exists() {
        missing("/dev/kvm", "/dev/kvm");
        return;
    }
    // A genuinely blank 256 MiB raw disk — created with std::fs, no `qemu-img`
    // subprocess (tunnr renders a non-`.qcow2` payload as `format=raw`).
    let blank = std::env::temp_dir().join(format!("draupnir-blank-{}.raw", std::process::id()));
    let f = std::fs::File::create(&blank).expect("create the blank disk");
    f.set_len(256 * 1024 * 1024).expect("size the blank disk");
    drop(f);

    let spec = BootSpec::kvm_boot_installed_disk("blank-disk-red", blank.to_string_lossy());
    spec.validate().expect("the second-lifetime spec is well formed");
    assert_eq!(spec.medium_path(), None, "the medium is absent, as the leg requires");

    let backend = KvmBoot::new();
    let machine = backend.boot(&spec).expect("QEMU starts even with nothing to boot");

    // Wait on a deadline, not on exit — the same discipline as the positive proof.
    let seen = backend
        .await_serial_marker(
            &machine,
            &proof_marker(),
            Duration::from_secs(25),
            Duration::from_millis(500),
        )
        .expect("addressable");
    let console = backend.serial_log(&machine).unwrap_or_default();
    println!("---------------- BLANK-DISK CONSOLE ({} bytes) ----------------", console.len());
    println!("{console}");
    println!("---------------- END BLANK-DISK CONSOLE ----------------");
    println!("SEEN: {}", seen.detail());
    let _ = backend.power_off(&machine);
    let _ = std::fs::remove_file(&blank);

    assert!(
        !seen.saw_marker(),
        "a BLANK disk produced an appliance — then the positive proof proves nothing: {}",
        seen.detail()
    );
    assert!(
        !console.contains(MEDIUM_MARKER),
        "the medium-less leg still booted a DVD-ROM — the medium was NOT absent:\n{console}"
    );
    assert!(
        !console.contains("GUNNAR APPLIANCE"),
        "a blank disk printed the appliance banner:\n{console}"
    );
    println!("RED CONFIRMED: blank disk, no medium line, no appliance.");
}

/// **RED, by construction: a medium that is not on disk fails BY NAME, before a
/// single QEMU process is spawned.**
///
/// Not `#[ignore]`d and needing no KVM: the whole point is that it never reaches a
/// backend. A build that did not produce its ISO must read as exactly that, not as
/// "the appliance did not come up" — those are different bugs with different owners.
#[test]
fn a_spec_naming_a_medium_that_does_not_exist_fails_by_name_without_booting() {
    let ghost = "/nonexistent/never-built-by-anyone.iso";
    let spec = BootSpec::iso_boot("ghost-iso", ghost);
    // It is a well-formed SPEC — the path is only wrong about the world.
    spec.validate().expect("a nonexistent path is not a malformed spec");

    let err = KvmBoot::new()
        .boot(&spec)
        .expect_err("a medium that is not on disk cannot be booted");
    let msg = format!("{err}");
    assert!(msg.contains(ghost), "the failure NAMES the missing ISO: {msg}");
    assert!(msg.contains("does not exist"), "and says what is wrong: {msg}");
    assert!(
        msg.contains("ghost-iso"),
        "and names the spec that asked for it: {msg}"
    );
}

/// **RED, by construction: "boot the installed disk" cannot be claimed while the
/// installer medium is still attached.**
///
/// Honesty rule 1. With the ISO in the tray a machine that fails to boot its
/// freshly-installed disk falls through to the installer and comes up looking
/// exactly like a success — which is how an installer chain reports READY for a
/// system it never installed. The two legs must be two specs, and this proves the
/// combined one is refused.
#[test]
fn the_boot_installed_leg_cannot_secretly_keep_the_installer_medium() {
    let honest = BootSpec::kvm_boot_installed_disk("appliance", "/var/lib/appliance.qcow2");
    honest.validate().expect("the medium-less second lifetime is legal");
    assert_eq!(honest.medium_path(), None);

    let lie = honest.clone().with_medium("/images/installer.iso");
    let err = lie
        .validate()
        .expect_err("boot-from-disk with the medium attached is refused");
    let msg = format!("{err}");
    assert!(msg.contains("/images/installer.iso"), "names the medium: {msg}");
    assert!(msg.contains("ABSENT"), "states the rule: {msg}");
    assert!(msg.contains("second VM lifetime"), "names the fix: {msg}");
}