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

mikronova/draupnir

src/kvm.rs

//! **KVM backend** — fire up an appliance VM by driving **tunnr**.
//!
//! Draupnir does not reimplement VM boot. tunnr owns the QEMU + OVMF (UEFI) + KVM
//! launch and exposes a self-contained, kill-able primitive —
//! `tunnr_vm::boot_test(BootSpec) -> BootHandle`. This backend is the thin adapter
//! that maps a Draupnir [`BootSpec`] onto tunnr's `BootSpec`, calls `boot_test`,
//! and adapts the returned `BootHandle` into a [`Machine`] / [`Lifecycle`].
//!
//! The trait wiring compiles unconditionally; the live tunnr call is behind the
//! `backend-tunnr` feature (an optional path dep on `tunnr-vm`, exactly how `jera`
//! wires it — switch to a version once tunnr publishes to crates.io). Without the
//! feature every entry point returns an honest [`Error::Unsupported`] rather than
//! pretending to boot.
//!
//! **Live handles.** [`Boot::boot`] returns a plain-data [`Machine`] (an id + power
//! state), but [`Lifecycle`] needs tunnr's `BootHandle` to kill/poll the exact QEMU
//! process group. The backend therefore keeps a small registry
//! (`machine id → BootHandle`) so `power_off`/`status` address the VM that `boot`
//! actually launched.

#[cfg(feature = "backend-tunnr")]
use crate::ImageSource;
use crate::{Boot, BootSpec, CloudInit, Error, Lifecycle, Machine, PowerState, Result, Seen};

#[cfg(feature = "backend-tunnr")]
use std::collections::HashMap;
#[cfg(feature = "backend-tunnr")]
use std::sync::{Arc, Mutex};

/// The registry of live tunnr boot handles, keyed by [`Machine::id`].
#[cfg(feature = "backend-tunnr")]
type LiveHandles = Arc<Mutex<HashMap<String, tunnr_vm::BootHandle>>>;

/// The KVM boot backend (drives tunnr).
#[derive(Default, Clone)]
pub struct KvmBoot {
    /// Live tunnr handles for the VMs this backend booted, so [`Lifecycle`] can
    /// kill/poll the exact QEMU process group. `BootHandle` is a cheap `Arc` clone.
    #[cfg(feature = "backend-tunnr")]
    live: LiveHandles,
}

impl std::fmt::Debug for KvmBoot {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("KvmBoot").finish_non_exhaustive()
    }
}

impl KvmBoot {
    /// Construct the KVM backend.
    pub fn new() -> Self {
        Self::default()
    }

    /// **Pure** render of the cloud-init NoCloud seed file set —
    /// `[("user-data", …), ("meta-data", …)]` (plus `network-config` when set).
    /// A thin re-export of the owned builder [`crate::seed::seed_files`] so the
    /// seed lives in exactly one place (draupnir's [`crate::seed`] module); kept
    /// here for the KVM backend's existing call sites.
    pub fn nocloud_seed_files(ci: &CloudInit) -> Vec<(&'static str, String)> {
        crate::seed::seed_files(ci)
    }

    /// The **serial console** captured so far for a live KVM guest — read off the
    /// tunnr `BootHandle` in the registry (`tunnr_vm::BootHandle::boot_log`). This is
    /// the KVM-console surface a consumer (jera → dwarves) streams to make the boot's
    /// console **visible in the forge**: draupnir's [`Lifecycle`] exposes power state,
    /// but the boot handle it keeps alive also holds the live `-serial mon:stdio`
    /// capture, so a caller that has the [`Machine`] can read the console without
    /// re-implementing any capture. `None` when no live VM matches `machine` (or the
    /// `backend-tunnr` feature is off — no tunnr handle to read).
    #[cfg(feature = "backend-tunnr")]
    pub fn serial_log(&self, machine: &Machine) -> Option<String> {
        self.live
            .lock()
            .unwrap()
            .get(&machine.id)
            .map(|h| h.boot_log())
    }

    /// Without `backend-tunnr` there is no live tunnr handle, so no console to read.
    #[cfg(not(feature = "backend-tunnr"))]
    pub fn serial_log(&self, _machine: &Machine) -> Option<String> {
        None
    }

    /// **Dump a live guest's framebuffer to a PPM** — the evidence channel of a
    /// machine that has no serial port.
    ///
    /// The sibling of [`serial_log`](KvmBoot::serial_log): one reads the wire, this
    /// one reads the screen, and a [`crate::GuestShape::BiosFramebuffer`] guest has
    /// only the second. What lands in `dest` is exactly the pixels a human would
    /// read off a provider's VNC console — where, on such a machine, a PID-1 panic
    /// and an installer's refusal are the ONLY places anything is ever written.
    ///
    /// `Err` by name, never an empty file: the guest was not booted by this backend
    /// (`no-live-vm`), it has no display adapter (`no-framebuffer`, i.e. the
    /// `UefiSerial` shape), or the monitor socket would not take the command.
    #[cfg(feature = "backend-tunnr")]
    pub fn screendump(&self, machine: &Machine, dest: &std::path::Path) -> Result<()> {
        let g = self.live.lock().unwrap();
        let h = g.get(&machine.id).ok_or_else(|| {
            Error::Backend(format!(
                "REFUSED [no-live-vm] no live tunnr VM for {} — nothing to screendump",
                machine.id
            ))
        })?;
        h.screendump(dest)
            .map_err(|r| Error::Backend(format!("{r} (machine {})", machine.id)))
    }

    /// Without `backend-tunnr` there is no live guest, so there are no pixels.
    #[cfg(not(feature = "backend-tunnr"))]
    pub fn screendump(&self, machine: &Machine, _dest: &std::path::Path) -> Result<()> {
        Err(Error::Backend(format!(
            "REFUSED [no-live-vm] kvm backend needs the `backend-tunnr` feature (machine {})",
            machine.id
        )))
    }

    /// **Did this guest reach its success marker?** — tunnr's own verdict
    /// (`BootStatus::BootedOk`: the [`BootSpec::success_marker`] streamed past on the
    /// serial console), read off the live handle. [`Lifecycle::status`] folds that
    /// into [`PowerState::On`] — a booted guest is a powered guest — so a consumer
    /// that needs the *boot* verdict (jera's `BootStatus::BootedOk`, a pane's
    /// "PASSED") reads it here. `false` when the marker has not been seen, the VM is
    /// not one this backend booted, or `backend-tunnr` is off.
    #[cfg(feature = "backend-tunnr")]
    pub fn booted_ok(&self, machine: &Machine) -> bool {
        self.live
            .lock()
            .unwrap()
            .get(&machine.id)
            .map(|h| matches!(h.poll_status(), tunnr_vm::BootStatus::BootedOk))
            .unwrap_or(false)
    }

    /// Without `backend-tunnr` nothing boots, so nothing booted OK.
    #[cfg(not(feature = "backend-tunnr"))]
    pub fn booted_ok(&self, _machine: &Machine) -> bool {
        false
    }

    /// **Why a powered-off guest is off**, in tunnr's words: `Some("boot timed out")`
    /// for the wall-clock backstop, `Some("boot killed")` for a deliberate teardown,
    /// `Some(reason)` for a failed boot — `None` while the guest is up (or booted OK),
    /// or when no live handle matches. [`Lifecycle::status`] collapses all three into
    /// [`PowerState::Off`]; this keeps the distinction a console reader wants to show.
    #[cfg(feature = "backend-tunnr")]
    pub fn exit_reason(&self, machine: &Machine) -> Option<String> {
        let guard = self.live.lock().unwrap();
        let handle = guard.get(&machine.id)?;
        match handle.poll_status() {
            tunnr_vm::BootStatus::Failed(e) => Some(e),
            tunnr_vm::BootStatus::Killed => Some("boot killed".to_string()),
            tunnr_vm::BootStatus::TimedOut => Some("boot timed out".to_string()),
            tunnr_vm::BootStatus::Booting | tunnr_vm::BootStatus::BootedOk => None,
            // A guest with no marker channel is not off and has not failed — it
            // simply cannot be judged from the wire. Saying so is the whole
            // point of the state; folding it into `None` (all fine) or into a
            // failure string would be inventing a verdict.
            tunnr_vm::BootStatus::Unobservable(why) => Some(format!("unobservable: {why}")),
        }
    }

    /// Without `backend-tunnr` there is no live handle to ask.
    #[cfg(not(feature = "backend-tunnr"))]
    pub fn exit_reason(&self, _machine: &Machine) -> Option<String> {
        None
    }

    /// **Stop the guest and let it FLUSH** — tunnr's graceful `shutdown(grace)`
    /// (monitor power-down → ACPI → SIGKILL after `grace`), as opposed to
    /// [`Lifecycle::power_off`]'s immediate SIGKILL.
    ///
    /// The difference is load-bearing for an install: QEMU's raw drive is
    /// `cache=writeback`, so guest writes sit in QEMU's memory and a SIGKILL throws
    /// them away — measured 2026-08-16 (gunnar): INSTALL-OK, payload streamed onto
    /// p2 with one blake3, SIGKILL 4 s later, and the second boot hashed the same
    /// bytes to a different blake3. The handle is released afterwards like
    /// `power_off`; the guest is always gone when this returns.
    #[cfg(feature = "backend-tunnr")]
    pub fn shutdown(&self, machine: &Machine, grace: std::time::Duration) -> Result<()> {
        let handle = self
            .live
            .lock()
            .unwrap()
            .remove(&machine.id)
            .ok_or_else(|| Error::Backend(format!("no live tunnr VM for {}", machine.id)))?;
        let _ = handle.shutdown(grace);
        Ok(())
    }

    /// Without `backend-tunnr` there is nothing to shut down.
    #[cfg(not(feature = "backend-tunnr"))]
    pub fn shutdown(&self, _machine: &Machine, _grace: std::time::Duration) -> Result<()> {
        Err(Error::Unsupported(
            "kvm backend needs the `backend-tunnr` feature".into(),
        ))
    }

    /// Map a Draupnir [`BootSpec`] onto tunnr's `tunnr_vm::BootSpec`.
    ///
    /// Field-for-field this is the seam `jera::vm` already wires. Only compiled
    /// with `backend-tunnr` (the type comes from the optional `tunnr-vm` dep).
    #[cfg(feature = "backend-tunnr")]
    pub fn to_tunnr_spec(&self, spec: &BootSpec) -> Result<tunnr_vm::BootSpec> {
        self.to_tunnr_spec_in(spec, &crate::Firmware::search_dirs())
    }

    /// [`to_tunnr_spec`](KvmBoot::to_tunnr_spec) against an explicit OVMF search
    /// path. The in-crate seam the firmware proofs drive, so "the secboot pair
    /// reaches the pflash units" is provable against a KNOWN directory rather than
    /// against whatever the build host has in `/usr/share/OVMF`.
    #[cfg(feature = "backend-tunnr")]
    pub(crate) fn to_tunnr_spec_in(
        &self,
        spec: &BootSpec,
        ovmf_dirs: &[std::path::PathBuf],
    ) -> Result<tunnr_vm::BootSpec> {
        use std::path::PathBuf;
        use std::time::Duration;

        let (kernel, rootfs_or_disk, base_snapshot) = match &spec.image {
            ImageSource::KernelRootfs { kernel, rootfs } => (kernel.clone(), rootfs.clone(), false),
            ImageSource::Disk { kernel, disk } => {
                // tunnr attaches a non-cpio image as a virtio -drive; a kernel is
                // still required for the direct-kernel boot path and now travels
                // in the spec (validated non-empty by BootSpec::validate).
                (kernel.clone(), disk.clone(), false)
            }
            ImageSource::BootloaderDisk { disk } => {
                // A self-booting disk: hand tunnr an EMPTY kernel path so its
                // `is_direct_kernel_boot()` is false and OVMF firmware-boots the
                // disk directly (no `-kernel`), exactly as real hardware would.
                // COW-protect the base: a matrix boot of a shared, content-addressed
                // base OS disk must never mutate it (tunnr opens it snapshot=on).
                (String::new(), disk.clone(), true)
            }
            // A LIVE boot off the medium: the ISO is the whole payload, there is no
            // target disk. Empty kernel → tunnr firmware-boots it, and tunnr's own
            // `.iso` arm renders it `-cdrom <iso> -boot d`. `base_snapshot` is
            // meaningless for a read-only CD, so it stays false.
            ImageSource::Iso(iso) => (String::new(), iso.clone(), false),
            other => return Err(Error::Spec(format!("kvm backend cannot boot {other:?}"))),
        };
        // Fail by name before anything is launched: a medium that is not on disk is
        // a build that did not happen, not a boot that did not come up.
        Self::check_medium_present(spec)?;
        let mut boot =
            tunnr_vm::BootSpec::smoke(PathBuf::from(kernel), PathBuf::from(rootfs_or_disk));
        // COW-protection is the default, but it is not always right: a caller that
        // booted its OWN per-run disk copy in order to boot it a SECOND time needs the
        // writes to survive. `persist_disk` is that opt-out — it can only ever turn
        // COW off, never on, so an image source that already persists is unaffected.
        boot.base_snapshot = base_snapshot && !spec.persist_disk;
        boot.mem_mb = spec.mem_mb;
        boot.cores = spec.cores;
        // **The machine shape.** This line used to read `boot.headless = true;`,
        // which was not a choice at all: it produced `-nographic`, and tunnr on
        // its side pushed `-serial mon:stdio` and the OVMF pflash pair
        // unconditionally. Every KVM proof draupnir has ever driven therefore ran
        // on ONE machine — UEFI, serial console, no framebuffer — while a
        // console-only cloud VM (UpCloud) is the other one: SeaBIOS, no serial
        // port, VNC. An image that boots here and panics there is the defect this
        // field exists to make visible.
        match spec.guest_shape {
            crate::GuestShape::UefiSerial => {
                // `Firmware::Bios` (0.2.0, the qemu backend's SeaBIOS) on the
                // UEFI shape is two answers to one question. tunnr would boot the
                // UEFI the shape names and the BIOS the firmware names would be
                // dropped in silence — so name the contradiction instead.
                if let crate::Firmware::Bios { .. } = spec.firmware {
                    return Err(Error::Spec(
                        "REFUSED [bios-on-uefi-shape] firmware `bios` on a UefiSerial guest: \
                         the tunnr KVM backend spells a BIOS machine as \
                         `GuestShape::BiosFramebuffer` (SeaBIOS, no serial port, a VGA \
                         framebuffer) — set `with_guest_shape(GuestShape::BiosFramebuffer)`, or \
                         drop the BIOS firmware to boot UEFI."
                            .into(),
                    ));
                }
                boot = boot.with_shape(tunnr_vm::GuestShape::UefiSerial);
            }
            crate::GuestShape::BiosFramebuffer => {
                // Refuse, never substitute. A marker is detected on the serial
                // capture and nowhere else; on a machine with no serial port it
                // could never appear, so a spec that asks for both is a spec
                // whose verdict could only ever be invented.
                if let Some(marker) = &spec.success_marker {
                    return Err(Error::Spec(format!(
                        "REFUSED [no-serial-console] success_marker {marker:?} on a \
                         BiosFramebuffer guest: that machine has no serial port, so the marker \
                         could never be observed and `booted_ok` could only ever be a guess. \
                         Drop the marker and read the framebuffer \
                         (`KvmBoot::screendump`), or boot the UefiSerial shape."
                    )));
                }
                // A UEFI key store on a BIOS machine is not "ignored", it is a
                // Secure-Boot run that stopped enforcing anything. `HostDefault`
                // names nothing and `Bios` is this very machine; every OVMF choice
                // (plain UEFI included) contradicts the shape.
                let rom = match &spec.firmware {
                    crate::Firmware::HostDefault => None,
                    crate::Firmware::Bios { rom } => rom.as_ref().map(PathBuf::from),
                    other => {
                        return Err(Error::Spec(format!(
                            "REFUSED [secure-boot-on-bios] firmware `{}` on a BiosFramebuffer \
                             guest: a BIOS machine has no UEFI variable store, so the pair would \
                             simply not be used — and a firmware run whose firmware was dropped \
                             proves nothing.",
                            other.label()
                        )));
                    }
                };
                boot = boot.with_shape(tunnr_vm::GuestShape::BiosFramebuffer);
                // A named SeaBIOS blob travels to tunnr's own `-bios` seam; tunnr's
                // `check_shape` refuses it BY NAME if it is not on this host rather
                // than quietly falling back to the built-in one.
                boot.firmware = tunnr_vm::GuestFirmware::Bios { rom };
            }
        }
        // The caller's own budget first; then let a slow appliance boot be tuned
        // without a rebuild (parity with jera) — the operator override wins.
        if let Some(budget) = spec.boot_timeout {
            boot.timeout = budget;
        }
        if let Some(secs) = std::env::var("DRAUPNIR_VM_BOOT_TIMEOUT")
            .ok()
            .and_then(|s| s.parse().ok())
        {
            boot.timeout = Duration::from_secs(secs);
        }
        if !spec.cmdline.is_empty() {
            boot.kernel_cmdline = spec.cmdline.clone();
        }
        // The caller's own "booted OK" line, so tunnr's status (and `booted_ok`)
        // flips on THIS guest's marker rather than the stock one.
        if let Some(marker) = &spec.success_marker {
            boot.success_marker = marker.clone();
        }
        // cloud-init NoCloud: author the seed image and attach it through tunnr's
        // native `with_seed` seam (its `seed` field), so tunnr owns the exact
        // read-only virtio-disk argv — no hand-rolled `extra_qemu_args` drift.
        // The guest's cloud-init finds it by its `cidata` vfat label at first boot.
        if let Some(ci) = &spec.cloud_init {
            let seed = Self::build_seed_image(&Self::seed_path(&spec.name), ci)?;
            boot = boot.with_seed(seed);
        }
        // Airgap for the KVM path: a `NetMode::None` spec cuts the guest off from all
        // egress. tunnr expresses this as `NetConfig::None` (`-nic none`); every other
        // net mode leaves tunnr's default user-mode virtio-net (byte-identical). This
        // is the KVM twin of the container backend's `--network none` — the one wire a
        // real airgap (egress-isolation) VM test asserts against.
        if spec.net == crate::NetMode::None {
            boot.net = tunnr_vm::NetConfig::None;
        } else if !spec.ports.is_empty() || !spec.port_maps.is_empty() {
            // **The host→guest forwards.** tunnr's `NetConfig::User { hostfwd }`
            // takes `(host_port, guest_port)` pairs and renders each as
            // `hostfwd=tcp::<host>-:<guest>` on the user-mode netdev, which is
            // field-for-field what [`crate::PortMap`] already spells. The two
            // published forms fold into the one pair list exactly as the
            // container backend folds them (`ports` is the `host == container`
            // special case — see `PortMap::same`), so a caller writing
            // `.with_port_map(2222, 22)` gets the same wire either way and the
            // KVM and container backends do not disagree about what a published
            // port means.
            //
            // `NetMode::None` is handled above and never reaches here; the pair
            // is refused by `BootSpec::validate` rather than resolved in silence.
            // An empty forward list cannot reach this arm at all, so a spec that
            // names no port still yields tunnr's own default and the argv stays
            // byte-identical to one written before this existed.
            let hostfwd: Vec<(u16, u16)> = spec
                .ports
                .iter()
                .map(|&p| (p, p))
                .chain(spec.port_maps.iter().map(|pm| (pm.host, pm.container)))
                .collect();
            boot.net = tunnr_vm::NetConfig::User { hostfwd };
        }
        // Host-specific extra QEMU devices tunnr's spec does not model natively (the
        // Skidbladnir report / guest-agent virtio-serial ports, a data drive, a
        // `-snapshot` base-image guard, `-cpu host`) ride through verbatim. Empty
        // leaves the argv byte-identical to a spec that never named them.
        // The removable medium + boot order, then the caller's own extras LAST so a
        // caller can still override (QEMU takes the last `-boot`). Empty when the
        // spec names no medium and no boot order → byte-identical argv to before.
        boot.extra_qemu_args = Self::medium_qemu_args(spec);
        boot.extra_qemu_args.extend(spec.extra_qemu_args.iter().cloned());
        // The UEFI firmware: resolve the caller's CODE/VARS pair on THIS host and
        // hand it to tunnr's own firmware seam (`with_secure_boot_firmware`), so
        // tunnr keeps owning the pflash argv and the per-guest writable VARS copy —
        // no hand-rolled `-drive if=pflash` here. `Firmware::HostDefault` resolves to
        // None and sets nothing: tunnr's stock discovery still picks the pair and the
        // argv is byte-identical to a spec written before the field existed.
        //
        // A named-but-absent firmware fails HERE, by name, before a guest is
        // launched — the same "a build that did not happen is not a boot that did not
        // come up" rule `check_medium_present` enforces for the ISO. Substituting the
        // stock, non-enforcing pair for a Secure Boot run would turn a refusal that
        // proves something into a green that proves nothing.
        if let Some((code, vars_template)) = spec.firmware.resolve_in(ovmf_dirs)? {
            boot = boot.with_secure_boot_firmware(code, vars_template);
        }
        Ok(boot)
    }

    /// The temp path the NoCloud seed image for `name` is authored at.
    #[cfg(feature = "backend-tunnr")]
    fn seed_path(name: &str) -> std::path::PathBuf {
        std::env::temp_dir().join(format!("draupnir-{name}-seed.img"))
    }

    /// **Wait for a NAMED LINE on the guest's serial console, or a deadline — never
    /// for the VM to exit.**
    ///
    /// The KVM half of the one stage-2 observation shape ([`Seen`]); its sibling is
    /// `exe::ExeBoot::await_marker`, and both return the same type for the same
    /// reason. An appliance that boots correctly **serves forever** — waiting for
    /// QEMU to exit is waiting for the appliance to fail. So this polls the captured
    /// console (`tunnr_vm::BootHandle::boot_log`, read through
    /// [`serial_log`](KvmBoot::serial_log)) for `marker`, and reports what it saw:
    /// the marker with the console line it was on, the VM having died first, or the
    /// budget elapsing with the guest still up.
    ///
    /// All three are `Ok` — a deadline is an observation, not an error. `Err` is
    /// reserved for "this backend never booted that machine".
    ///
    /// Give the boot a real budget: `DRAUPNIR_VM_BOOT_TIMEOUT` (seconds) raises
    /// tunnr's own SIGKILL backstop, which is a *separate* clock. A budget longer
    /// than that backstop can only ever observe [`Seen::Exited`].
    pub fn await_serial_marker(
        &self,
        machine: &Machine,
        marker: &str,
        budget: std::time::Duration,
        poll: std::time::Duration,
    ) -> Result<Seen> {
        // **A wait that could never succeed is a refusal, not a deadline.** On a
        // GuestShape::BiosFramebuffer guest there is no serial port at all, so
        // this loop would poll an empty string for the whole budget and then
        // report `Seen::Deadline` — "the marker did not appear", which reads as
        // evidence about the GUEST when it is only a fact about the WIRE. Say so
        // by name instead, and point at the channel that does carry something.
        #[cfg(feature = "backend-tunnr")]
        {
            let unobservable = self
                .live
                .lock()
                .unwrap()
                .get(&machine.id)
                .and_then(|h| match h.poll_status() {
                    tunnr_vm::BootStatus::Unobservable(why) => Some(why),
                    _ => None,
                });
            if let Some(why) = unobservable {
                return Err(Error::Backend(format!(
                    "REFUSED [no-serial-console] cannot wait for {marker:?} on {}: {why}",
                    machine.id
                )));
            }
        }
        let started = std::time::Instant::now();
        let deadline = started + budget;
        loop {
            let console = self.serial_log(machine).ok_or_else(|| {
                Error::Backend(format!("no live KVM guest for {}", machine.id))
            })?;
            if let Some(line) = console.lines().find(|l| l.contains(marker)) {
                let seen = Seen::Marker {
                    line: line.to_string(),
                    after: started.elapsed(),
                };
                self.record_seen(machine, &seen, true);
                return Ok(seen);
            }
            // Powered Off means the QEMU process is gone (failed / killed / timed
            // out) — the guest cannot print the marker any more, so stop waiting for
            // something that can never arrive and report the console instead.
            if self.status(machine)? == PowerState::Off {
                let seen = Seen::Exited {
                    code: None,
                    tail: console,
                    after: started.elapsed(),
                };
                self.record_seen(machine, &seen, false);
                return Ok(seen);
            }
            let now = std::time::Instant::now();
            if now >= deadline {
                let seen = Seen::StillRunning {
                    tail: console,
                    waited: started.elapsed(),
                };
                self.record_seen(machine, &seen, false);
                return Ok(seen);
            }
            std::thread::sleep(poll.min(deadline.saturating_duration_since(now)));
        }
    }

    /// Record a console observation as a functional-status row, so a marker that
    /// never appeared is visible in nornir's matrix rather than swallowed.
    fn record_seen(&self, machine: &Machine, seen: &Seen, ok: bool) {
        crate::functional_status(
            "draupnir/kvm",
            "await_serial_marker",
            ok,
            &format!("`{}`: {}", machine.id, seen.detail()),
        );
    }

    /// **The removable-medium QEMU argv** — the ONE place a draupnir
    /// [`BootSpec`]'s [`medium`](BootSpec::medium) / [`boot_order`](crate::BootOrder)
    /// become QEMU flags. Pure (no I/O, no feature gate) so it is unit-testable with
    /// no KVM and no tunnr, exactly like tunnr's own `build_qemu_args`.
    ///
    /// Emits, in order:
    /// * `-cdrom <iso>` when the spec carries an explicit
    ///   [`medium`](BootSpec::medium) — an ISO *alongside* the payload disk, which is
    ///   the install shape. Deliberately **not** emitted when the medium comes from
    ///   an [`ImageSource::Iso`] payload: tunnr already renders that as its own
    ///   `-cdrom` (`tunnr/src/boot_primitive.rs:758-765`), and a second `-cdrom` is a
    ///   duplicate-drive error, not a merge. One writer per flag.
    /// * `-boot d|c` for [`BootOrder::Medium`](crate::BootOrder::Medium) /
    ///   [`Disk`](crate::BootOrder::Disk). Safe to repeat: QEMU takes the last
    ///   `-boot`, and for an ISO payload the value tunnr emits (`d`) is the value we
    ///   emit, so the two agree by construction rather than by coincidence.
    ///
    /// [`BootOrder::Auto`](crate::BootOrder::Auto) with no medium returns an **empty**
    /// vector — which is what makes every spec written before this existed produce a
    /// byte-identical argv.
    pub fn medium_qemu_args(spec: &BootSpec) -> Vec<String> {
        let mut args = Vec::new();
        if let Some(iso) = &spec.medium {
            // The firmware's face: El Torito off an IDE cdrom.
            if spec.medium_attach.has_cdrom() {
                args.push("-cdrom".into());
                args.push(iso.clone());
            }
            // The system's face: the SAME bytes as a read-only virtio block
            // device, for a PID 1 whose kernel has no ATA and therefore cannot
            // see the cdrom the firmware just booted it from. Read-only because
            // a medium is, and because it keeps the installer's disk-picker from
            // ever choosing it as a target. Byte-for-byte the attachment
            // `korp_installer::isoproof::attach_iso_as_virtio` already proves
            // (LAW 5: the shape is copied from the one that works, not invented).
            if spec.medium_attach.has_virtio() {
                args.push("-drive".into());
                args.push(format!("file={iso},if=virtio,format=raw,readonly=on"));
            }
        }
        if let Some(order) = spec.boot_order.qemu_value() {
            args.push("-boot".into());
            args.push(order.into());
        }
        args
    }

    /// **Fail BY NAME when the medium is not there.** A `-cdrom` pointing at a path
    /// that does not exist makes QEMU die with its own message deep inside a boot,
    /// which reads as "the appliance did not come up" rather than "you named an ISO
    /// that was never built" — the two failures a chain matrix must never confuse.
    ///
    /// Checks whatever [`BootSpec::medium_path`] resolves to (explicit medium or an
    /// [`ImageSource::Iso`] payload), so both spellings of "boot off the medium" are
    /// covered by one check. A spec with no medium is trivially `Ok` and untouched.
    /// Pure of feature gates so it is provable without KVM.
    pub fn check_medium_present(spec: &BootSpec) -> Result<()> {
        let Some(medium) = spec.medium_path() else {
            return Ok(());
        };
        if std::path::Path::new(medium).is_file() {
            Ok(())
        } else {
            Err(Error::Spec(format!(
                "boot medium `{medium}` does not exist (spec `{}`): the ISO must be on \
                 disk before a machine can be booted off it",
                spec.name
            )))
        }
    }

    /// Materialize a [`CloudInit`] into a **NoCloud seed image** at `out` — a thin
    /// call onto the owned builder [`crate::seed::build_seed_image`] (vfat, volume
    /// label `cidata`, pure-Rust `fatfs`, no `genisoimage`/`mkisofs`). The seed
    /// authoring lives in exactly one place ([`crate::seed`]); this is the KVM
    /// backend's call site.
    #[cfg(feature = "backend-tunnr")]
    pub fn build_seed_image(out: &std::path::Path, ci: &CloudInit) -> Result<std::path::PathBuf> {
        crate::seed::build_seed_image(out, ci)
    }
}

impl Boot for KvmBoot {
    fn boot(&self, spec: &BootSpec) -> Result<Machine> {
        spec.validate()?;
        #[cfg(feature = "backend-tunnr")]
        {
            let tunnr_spec = self.to_tunnr_spec(spec)?;
            let handle = tunnr_vm::boot_test(tunnr_spec)
                .map_err(|e| Error::Backend(format!("tunnr boot_test: {e}")))?;
            let id = handle.id().to_string();
            let machine = Machine::started(&id, spec);
            self.live.lock().unwrap().insert(id, handle);
            Ok(machine)
        }
        #[cfg(not(feature = "backend-tunnr"))]
        {
            let _ = spec;
            Err(Error::Unsupported(
                "kvm backend needs the `backend-tunnr` feature (drives tunnr's KVM boot primitive)"
                    .into(),
            ))
        }
    }
}

impl Lifecycle for KvmBoot {
    fn power_on(&self, machine: &Machine) -> Result<()> {
        let _ = machine;
        // tunnr's primitive is fire-and-forget (a booted VM has no re-power line);
        // the honest answer is "boot a fresh instance", not a fake success.
        Err(Error::Unsupported(
            "kvm/tunnr VMs are fire-and-forget: re-power by calling draupnir::boot() again".into(),
        ))
    }

    fn power_off(&self, machine: &Machine) -> Result<()> {
        #[cfg(feature = "backend-tunnr")]
        {
            let handle = self
                .live
                .lock()
                .unwrap()
                .remove(&machine.id)
                .ok_or_else(|| Error::Backend(format!("no live tunnr VM for {}", machine.id)))?;
            handle.kill();
            Ok(())
        }
        #[cfg(not(feature = "backend-tunnr"))]
        {
            let _ = machine;
            Err(Error::Unsupported(
                "kvm backend needs the `backend-tunnr` feature".into(),
            ))
        }
    }

    fn status(&self, machine: &Machine) -> Result<PowerState> {
        #[cfg(feature = "backend-tunnr")]
        {
            let guard = self.live.lock().unwrap();
            let Some(handle) = guard.get(&machine.id) else {
                return Ok(PowerState::Unknown);
            };
            Ok(map_status(handle.poll_status()))
        }
        #[cfg(not(feature = "backend-tunnr"))]
        {
            let _ = machine;
            Err(Error::Unsupported(
                "kvm backend needs the `backend-tunnr` feature".into(),
            ))
        }
    }
}

/// Map a tunnr `BootStatus` onto draupnir's [`PowerState`]: still booting or booted
/// OK ⇒ powered `On`; any terminal (failed/killed/timed-out) ⇒ `Off`.
#[cfg(feature = "backend-tunnr")]
fn map_status(s: tunnr_vm::BootStatus) -> PowerState {
    match s {
        // `Unobservable` is a guest we cannot JUDGE, not a guest that is off:
        // QEMU is running and the machine is powered. The verdict is missing,
        // the power state is not.
        tunnr_vm::BootStatus::Booting
        | tunnr_vm::BootStatus::BootedOk
        | tunnr_vm::BootStatus::Unobservable(_) => PowerState::On,
        tunnr_vm::BootStatus::Failed(_)
        | tunnr_vm::BootStatus::Killed
        | tunnr_vm::BootStatus::TimedOut => PowerState::Off,
    }
}

#[cfg(test)]
mod seed_tests {
    use super::*;

    #[test]
    fn nocloud_seed_files_render_user_data_and_a_default_meta_data() {
        let ci = CloudInit::user_data("#cloud-config\nruncmd:\n  - [echo, hi]\n");
        let files = KvmBoot::nocloud_seed_files(&ci);
        assert_eq!(files[0].0, "user-data");
        assert_eq!(files[0].1, "#cloud-config\nruncmd:\n  - [echo, hi]\n");
        assert_eq!(files[1].0, "meta-data");
        // A default meta-data is supplied when the spec omits it.
        assert!(
            files[1].1.contains("instance-id"),
            "default meta-data has an instance-id"
        );
        assert!(files[1].1.contains("local-hostname"));
    }

    #[test]
    fn nocloud_seed_files_pass_through_an_explicit_meta_data() {
        let ci = CloudInit {
            user_data: "#cloud-config\n".into(),
            meta_data: Some("instance-id: node-7\n".into()),
            network_config: None,
        };
        let files = KvmBoot::nocloud_seed_files(&ci);
        assert_eq!(files[1].1, "instance-id: node-7\n");
    }

    #[test]
    fn nocloud_seed_files_omit_network_config_by_default_and_emit_it_when_set() {
        // No network-config → just the two required files.
        let plain = CloudInit::user_data("#cloud-config\n");
        let files = KvmBoot::nocloud_seed_files(&plain);
        assert_eq!(files.len(), 2);
        assert!(!files.iter().any(|(n, _)| *n == "network-config"));

        // With one → a third `network-config` file carrying the document.
        let net = CloudInit::user_data("#cloud-config\n")
            .with_network_config("version: 2\nethernets:\n  eth0:\n    dhcp4: true\n");
        let files = KvmBoot::nocloud_seed_files(&net);
        assert_eq!(files.len(), 3);
        let nc = files
            .iter()
            .find(|(n, _)| *n == "network-config")
            .expect("network-config present");
        assert!(nc.1.contains("dhcp4: true"));
    }
}

/// The boot-off-a-medium argv seam. Pure — no tunnr, no KVM, no feature — so the
/// flags a medium turns into are pinned on every build, not only the one that can
/// launch a VM.
#[cfg(test)]
mod medium_tests {
    use super::*;
    use crate::BootOrder;

    #[test]
    fn a_spec_with_no_medium_and_no_boot_order_emits_nothing() {
        // The additive-parity guard, at the argv. Empty here is what makes every
        // pre-existing launch byte-identical.
        // RED-when-broken: make `medium_qemu_args` always push "-boot" and this fails.
        for spec in [
            BootSpec::kvm_kernel_rootfs("a", "/bzImage", "/rootfs.cpio.gz"),
            BootSpec::kvm_disk("b", "/bzImage", "/d.qcow2"),
            BootSpec::kvm_bootloader_disk("c", "/d.qcow2"),
        ] {
            assert!(
                KvmBoot::medium_qemu_args(&spec).is_empty(),
                "{} adds no argv",
                spec.name
            );
        }
    }

    #[test]
    fn the_install_leg_renders_cdrom_plus_boot_d() {
        let spec = BootSpec::kvm_install_from_medium("app", "/images/g.iso", "/target.qcow2");
        assert_eq!(
            KvmBoot::medium_qemu_args(&spec),
            vec!["-cdrom", "/images/g.iso", "-boot", "d"],
            "boot OFF the medium with the target disk still attached"
        );
    }

    #[test]
    fn the_boot_installed_leg_renders_boot_c_and_no_cdrom() {
        let spec = BootSpec::kvm_boot_installed_disk("app", "/target.qcow2");
        let argv = KvmBoot::medium_qemu_args(&spec);
        assert_eq!(argv, vec!["-boot", "c"]);
        assert!(
            !argv.iter().any(|a| a == "-cdrom"),
            "the medium is absent, not merely deprioritised: {argv:?}"
        );
    }

    #[test]
    fn an_iso_payload_does_not_get_a_second_cdrom() {
        // tunnr already renders an `.iso` payload as its own `-cdrom` + `-boot d`
        // (tunnr/src/boot_primitive.rs:758-765). A second `-cdrom` is a QEMU
        // duplicate-drive error, so this path must emit only the boot order.
        // RED-when-broken: render from `medium_path()` instead of the explicit
        // `medium` field and this grows a duplicate `-cdrom`.
        let spec = BootSpec::iso_boot("live", "/images/g.iso");
        assert_eq!(spec.medium_path(), Some("/images/g.iso"), "it IS a medium boot");
        assert_eq!(
            KvmBoot::medium_qemu_args(&spec),
            vec!["-boot", "d"],
            "no second -cdrom for an ISO payload"
        );
    }

    #[test]
    fn a_medium_that_is_not_on_disk_fails_by_name() {
        // "you named an ISO that was never built" must not read as "the appliance
        // did not come up". RED-when-broken: delete `check_medium_present`'s
        // is_file() test and this returns Ok.
        let ghost = BootSpec::kvm_install_from_medium(
            "app",
            "/images/never-built.iso",
            "/target.qcow2",
        );
        let err = KvmBoot::check_medium_present(&ghost).unwrap_err();
        let msg = format!("{err}");
        assert!(matches!(err, Error::Spec(_)));
        assert!(msg.contains("/images/never-built.iso"), "names the ISO: {msg}");
        assert!(msg.contains("does not exist"), "says what is wrong: {msg}");
        assert!(msg.contains("app"), "names the spec: {msg}");

        // A real file passes, and a spec with no medium is trivially fine.
        let real = std::env::current_exe().expect("the test binary is a real file");
        let ok = BootSpec::kvm_install_from_medium(
            "app",
            real.to_string_lossy(),
            "/target.qcow2",
        );
        KvmBoot::check_medium_present(&ok).unwrap();
        KvmBoot::check_medium_present(&BootSpec::kvm_bootloader_disk("n", "/d.qcow2")).unwrap();
    }

    #[test]
    fn a_directory_is_not_a_medium() {
        // `is_file`, not `exists`: a path that happens to be a directory would pass
        // an `exists()` check and then fail inside QEMU.
        let spec = BootSpec::kvm_install_from_medium("app", "/tmp", "/target.qcow2");
        assert!(KvmBoot::check_medium_present(&spec).is_err());
    }

    #[test]
    fn boot_order_maps_to_both_backends_from_one_field() {
        assert_eq!(BootOrder::Auto.qemu_value(), None);
        assert_eq!(BootOrder::Medium.qemu_value(), Some("d"));
        assert_eq!(BootOrder::Disk.qemu_value(), Some("c"));
        assert_eq!(BootOrder::Auto.redfish_target(), None);
        assert_eq!(BootOrder::Medium.redfish_target(), Some(crate::BootTarget::Cd));
        assert_eq!(BootOrder::Disk.redfish_target(), Some(crate::BootTarget::Hdd));
    }
}

#[cfg(all(test, feature = "backend-tunnr"))]
mod tests {
    use super::*;
    use crate::ImageSource;

    /// Author the NoCloud seed image, then read it back through `fatfs` and confirm
    /// the two files + their contents survive and the volume label is `cidata`
    /// (proves the guest's cloud-init will find `user-data`/`meta-data`).
    #[test]
    fn build_seed_image_writes_a_cidata_fat_with_both_files() {
        let out =
            std::env::temp_dir().join(format!("draupnir-seedtest-{}.img", std::process::id()));
        let _ = std::fs::remove_file(&out);
        let ci = CloudInit::user_data("#cloud-config\nruncmd:\n  - [echo, hi]\n");
        let path = KvmBoot::build_seed_image(&out, &ci).unwrap();
        assert_eq!(path, out);

        let img = std::fs::File::options()
            .read(true)
            .write(true)
            .open(&out)
            .unwrap();
        let fs = fatfs::FileSystem::new(img, fatfs::FsOptions::new()).unwrap();
        assert_eq!(fs.volume_label().to_ascii_lowercase(), "cidata");
        let mut names: Vec<String> = fs
            .root_dir()
            .iter()
            .map(|e| e.unwrap().file_name())
            .collect();
        names.sort();
        assert!(
            names.iter().any(|n| n == "user-data"),
            "user-data present, got {names:?}"
        );
        assert!(
            names.iter().any(|n| n == "meta-data"),
            "meta-data present, got {names:?}"
        );
        // Content survives the round-trip.
        use std::io::Read;
        let mut buf = String::new();
        fs.root_dir()
            .open_file("user-data")
            .unwrap()
            .read_to_string(&mut buf)
            .unwrap();
        assert!(
            buf.contains("runcmd"),
            "user-data content survives: {buf:?}"
        );

        let _ = std::fs::remove_file(&out);
    }

    /// A spec carrying cloud-init makes `to_tunnr_spec` attach the seed image
    /// through tunnr's native `seed` field (`with_seed`); a spec without it leaves
    /// the field `None` and adds no seed `extra_qemu_args`. This pins the
    /// seed-drift fix: the seed rides tunnr's owned seam, not hand-rolled argv.
    #[test]
    fn to_tunnr_spec_attaches_the_seed_drive_only_when_cloud_init_is_present() {
        let plain = BootSpec::kvm_kernel_rootfs("plain", "/bzImage", "/rootfs.cpio.gz");
        let t = KvmBoot::new().to_tunnr_spec(&plain).unwrap();
        assert!(t.seed.is_none(), "no seed without cloud-init");
        assert!(
            !t.extra_qemu_args.iter().any(|a| a.contains("seed.img")),
            "no hand-rolled seed argv"
        );

        let seeded = BootSpec::kvm_kernel_rootfs("seeded", "/bzImage", "/rootfs.cpio.gz")
            .with_cloud_init(CloudInit::user_data("#cloud-config\n"));
        let t = KvmBoot::new().to_tunnr_spec(&seeded).unwrap();
        // The seed rides tunnr's native `seed` field, not `extra_qemu_args`.
        let seed = t
            .seed
            .as_ref()
            .expect("seed image attached via tunnr's with_seed");
        assert!(
            seed.to_string_lossy().contains("draupnir-seeded-seed.img"),
            "the authored seed path is carried: {seed:?}"
        );
        assert!(
            !t.extra_qemu_args.iter().any(|a| a.contains("seed.img")),
            "seed no longer hand-rolled into extra_qemu_args"
        );
        let _ = std::fs::remove_file(KvmBoot::seed_path("seeded"));
    }

    /// A bootloader-in-disk spec validates (no kernel required) and maps to a
    /// tunnr spec with an EMPTY kernel — tunnr's firmware-boot path — and the
    /// disk as `rootfs_or_disk`.
    #[test]
    fn kvm_spec_maps_a_bootloader_disk_onto_an_empty_kernel_tunnr_spec() {
        let spec = BootSpec::kvm_bootloader_disk("selfboot", "/disk.qcow2");
        spec.validate()
            .expect("a bootloader-in-disk spec validates without a kernel");
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert!(
            t.kernel.as_os_str().is_empty(),
            "firmware boot: empty kernel, got {:?}",
            t.kernel
        );
        assert!(
            !t.is_direct_kernel_boot(),
            "empty kernel => tunnr firmware-boots the disk"
        );
        assert_eq!(t.rootfs_or_disk, std::path::PathBuf::from("/disk.qcow2"));
        assert!(
            t.base_snapshot,
            "a bootloader-in-disk boot COW-protects its (shared) base disk (snapshot=on)"
        );
    }

    /// RED-when-broken — `persist_disk` is the opt-out that makes **two boots of one
    /// machine** expressible.
    ///
    /// Without it the second boot of an installed disk silently gets a pristine image,
    /// re-runs first-boot provisioning, and reports plausible facts about a machine
    /// that was never installed (measured on oden 2026-08-03: after a full install the
    /// per-run disk copy was byte-identical to the base). The assertion that matters is
    /// that `persist_disk` actually reaches tunnr's drive option, not merely that the
    /// field exists.
    #[test]
    fn persist_disk_turns_the_cow_guard_off_for_a_disk_the_caller_owns() {
        let mut spec = BootSpec::kvm_bootloader_disk("selfboot", "/per-run-copy.qcow2");
        assert!(!spec.persist_disk, "COW protection stays the DEFAULT");
        assert!(
            KvmBoot::new().to_tunnr_spec(&spec).unwrap().base_snapshot,
            "default: writes are thrown away"
        );

        spec.persist_disk = true;
        assert!(
            !KvmBoot::new().to_tunnr_spec(&spec).unwrap().base_snapshot,
            "persist_disk: the caller owns this disk, so its writes must SURVIVE the boot"
        );
    }

    /// The opt-out may only ever turn COW **off**, never on: an image source that
    /// already persists must not become COW-protected by anyone setting the flag.
    #[test]
    fn persist_disk_never_turns_cow_protection_on() {
        for persist in [false, true] {
            let mut spec = BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz");
            spec.persist_disk = persist;
            assert!(
                !KvmBoot::new().to_tunnr_spec(&spec).unwrap().base_snapshot,
                "kernel+rootfs never COW-protects, persist_disk={persist}"
            );
        }
    }

    #[test]
    fn kvm_spec_maps_kernel_rootfs_mem_and_cores_onto_tunnr() {
        let mut spec = BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz");
        spec.mem_mb = 1024;
        spec.cores = 4;
        spec.cmdline = "korp.smoke=1".into();
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert_eq!(t.kernel, std::path::PathBuf::from("/bzImage"));
        assert_eq!(
            t.rootfs_or_disk,
            std::path::PathBuf::from("/rootfs.cpio.gz")
        );
        assert_eq!(t.mem_mb, 1024);
        assert_eq!(t.cores, 4);
        assert_eq!(t.display, tunnr_vm::GuestDisplay::None);
        assert_eq!(t.console, tunnr_vm::GuestConsole::Stdio);
        assert_eq!(t.firmware, tunnr_vm::GuestFirmware::Uefi);
        assert_eq!(t.kernel_cmdline, "korp.smoke=1");
    }

    #[test]
    fn kvm_spec_maps_a_disk_boot_with_its_kernel_onto_tunnr() {
        // Regression: a Disk boot must carry a real `-kernel` onto tunnr, not an
        // empty string (the old cmdline_kernel() stub returned "").
        let spec = BootSpec::kvm_disk("disky", "/bzImage", "/disk.qcow2");
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert_eq!(t.kernel, std::path::PathBuf::from("/bzImage"));
        assert_eq!(t.rootfs_or_disk, std::path::PathBuf::from("/disk.qcow2"));
        assert!(
            !t.kernel.as_os_str().is_empty(),
            "disk boot kernel must be non-empty"
        );
    }

    /// The medium must reach tunnr's actual argv, not merely exist as a field.
    #[test]
    fn the_install_leg_reaches_tunnrs_argv_with_the_target_disk_and_the_cdrom() {
        let iso = write_probe_iso("install-leg");
        let spec = BootSpec::kvm_install_from_medium(
            "app",
            iso.to_string_lossy(),
            "/target.qcow2",
        );
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        // The PAYLOAD is the target disk (firmware boot, no -kernel)...
        assert!(t.kernel.as_os_str().is_empty(), "firmware boot");
        assert_eq!(t.rootfs_or_disk, std::path::PathBuf::from("/target.qcow2"));
        // ...and the medium rides as -cdrom + -boot d.
        assert_eq!(
            t.extra_qemu_args,
            vec![
                "-cdrom".to_string(),
                iso.to_string_lossy().into_owned(),
                "-boot".to_string(),
                "d".to_string()
            ]
        );
        // The install's writes must SURVIVE, or nothing was installed.
        assert!(!t.base_snapshot, "persist_disk turns the COW guard off");
        let _ = std::fs::remove_file(&iso);
    }

    #[test]
    fn the_boot_installed_leg_reaches_tunnrs_argv_with_boot_c_and_no_cdrom() {
        let spec = BootSpec::kvm_boot_installed_disk("app", "/target.qcow2");
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert_eq!(t.extra_qemu_args, vec!["-boot".to_string(), "c".to_string()]);
        assert!(!t.base_snapshot, "the installed disk persists");
    }

    #[test]
    fn a_callers_own_extra_qemu_args_still_ride_and_ride_last() {
        // The pre-existing seam is kept whole: the caller's args are appended AFTER
        // the medium's, so a caller can still override (QEMU takes the last -boot).
        let spec = BootSpec::kvm_boot_installed_disk("app", "/target.qcow2")
            .with_extra_qemu_args(["-cpu", "host"]);
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert_eq!(
            t.extra_qemu_args,
            vec!["-boot", "c", "-cpu", "host"]
                .into_iter()
                .map(String::from)
                .collect::<Vec<_>>()
        );
    }

    #[test]
    fn an_iso_payload_maps_onto_tunnrs_own_firmware_cdrom_path() {
        // tunnr renders a `.iso` payload itself; draupnir must hand it over as the
        // payload and add only the boot order.
        let iso = write_probe_iso("payload");
        let spec = BootSpec::iso_boot("live", iso.to_string_lossy());
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert!(t.kernel.as_os_str().is_empty(), "firmware boots the ISO");
        assert_eq!(t.rootfs_or_disk, iso);
        assert_eq!(t.extra_qemu_args, vec!["-boot".to_string(), "d".to_string()]);
        // ...and tunnr's own pure argv builder turns that into the real flags.
        let argv = tunnr_vm::boot_primitive::build_qemu_args(
            &t,
            &tunnr_vm::StagedFirmware::Uefi {
                code: "/c.fd".into(),
                vars: "/v.fd".into(),
            },
            None,
        );
        let cdroms = argv.iter().filter(|a| *a == "-cdrom").count();
        assert_eq!(cdroms, 1, "exactly one -cdrom, never two: {argv:?}");
        let _ = std::fs::remove_file(&iso);
    }

    /// **The firmware choice survives the draupnir→tunnr seam and reaches the
    /// final argv** — the pflash pair, not a field that was merely set.
    ///
    /// The twin of `an_iso_payload_maps_onto_tunnrs_own_firmware_cdrom_path`: map
    /// the spec, then run tunnr's OWN staging + pure argv builder over it and read
    /// the two `-drive if=pflash` units back.
    #[test]
    fn a_secure_boot_choice_reaches_the_pflash_units_in_the_final_argv() {
        let fw = tempfile::tempdir().unwrap();
        for f in [
            "OVMF_CODE_4M.fd",
            "OVMF_VARS_4M.fd",
            "OVMF_CODE_4M.secboot.fd",
            "OVMF_VARS_4M.ms.fd",
        ] {
            std::fs::write(fw.path().join(f), b"fw").unwrap();
        }
        let dirs = [fw.path().to_path_buf()];

        let spec = BootSpec::kvm_kernel_rootfs("sb", "/vmlinuz", "/rootfs.cpio.gz")
            .with_firmware(crate::Firmware::SecureBoot);
        let t = KvmBoot::new().to_tunnr_spec_in(&spec, &dirs).unwrap();
        assert_eq!(t.ovmf_code.as_deref(), Some(fw.path().join("OVMF_CODE_4M.secboot.fd").as_path()));
        assert_eq!(
            t.ovmf_vars_template.as_deref(),
            Some(fw.path().join("OVMF_VARS_4M.ms.fd").as_path())
        );

        // tunnr's own staging: resolve the pair and COPY the vars template into
        // this guest's private workdir (a shared writable vars file across two VMs
        // is corruption waiting to happen).
        let work = tempfile::tempdir().unwrap();
        let staged = tunnr_vm::boot_primitive::stage_firmware(&t, work.path()).unwrap();
        let tunnr_vm::StagedFirmware::Uefi { code, vars } = staged.clone() else {
            panic!("a UEFI spec must stage a UEFI firmware, got {staged:?}");
        };
        assert_eq!(code, fw.path().join("OVMF_CODE_4M.secboot.fd"));
        assert_eq!(vars, work.path().join("OVMF_VARS.fd"), "per-guest vars COPY");
        assert_ne!(vars, fw.path().join("OVMF_VARS_4M.ms.fd"), "never the template itself");

        let argv = tunnr_vm::boot_primitive::build_qemu_args(&t, &staged, None);
        assert!(
            argv.contains(&format!(
                "if=pflash,format=raw,readonly=on,file={}",
                fw.path().join("OVMF_CODE_4M.secboot.fd").display()
            )),
            "the ENFORCING code image is the read-only pflash: {argv:?}"
        );
        assert!(
            argv.contains(&format!("if=pflash,format=raw,file={}", vars.display())),
            "the per-guest copy of the ms vars template is the writable pflash: {argv:?}"
        );
        assert!(
            !argv.iter().any(|a| a.contains("OVMF_VARS_4M.fd")),
            "the stock, empty vars template must never ride the enforcing code: {argv:?}"
        );
    }

    /// A firmware this host has not got is refused BY NAME before the guest is
    /// launched — never swapped for whatever OVMF happens to be installed.
    #[test]
    fn to_tunnr_spec_refuses_a_firmware_this_host_has_not_got() {
        let fw = tempfile::tempdir().unwrap();
        // Stock only — the enforcing build is absent.
        std::fs::write(fw.path().join("OVMF_CODE_4M.fd"), b"fw").unwrap();
        std::fs::write(fw.path().join("OVMF_VARS_4M.fd"), b"fw").unwrap();
        let dirs = [fw.path().to_path_buf()];

        let spec = BootSpec::kvm_kernel_rootfs("sb", "/vmlinuz", "/rootfs.cpio.gz")
            .with_firmware(crate::Firmware::SecureBoot);
        let err = KvmBoot::new().to_tunnr_spec_in(&spec, &dirs).unwrap_err().to_string();
        assert!(err.contains("OVMF_CODE_4M.secboot.fd"), "names the file: {err}");
        assert!(err.contains(&fw.path().display().to_string()), "names where it looked: {err}");
    }

    /// The default names nothing: tunnr's stock discovery still owns the pair, so
    /// the argv is byte-identical to a spec written before the field existed.
    #[test]
    fn an_unset_firmware_leaves_tunnrs_own_discovery_untouched() {
        let spec = BootSpec::kvm_kernel_rootfs("plain", "/vmlinuz", "/rootfs.cpio.gz");
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert_eq!(t.ovmf_code, None);
        assert_eq!(t.ovmf_vars_template, None);
    }

    #[test]
    fn to_tunnr_spec_refuses_a_medium_that_is_not_on_disk() {
        // The check runs BEFORE any launch, so a missing ISO is a spec fault here
        // rather than a mystery boot failure later.
        let spec = BootSpec::kvm_install_from_medium("app", "/no/such.iso", "/target.qcow2");
        let err = KvmBoot::new().to_tunnr_spec(&spec).unwrap_err();
        assert!(format!("{err}").contains("/no/such.iso"), "{err}");
    }

    /// The caller's OWN marker + budget reach tunnr's spec (the two fields a pane
    /// had to reach past draupnir for), and an unset pair leaves tunnr's stock
    /// values — byte-identical to every spec written before the fields existed.
    #[test]
    fn success_marker_and_boot_timeout_reach_tunnrs_spec_and_unset_leaves_the_defaults() {
        let stock = tunnr_vm::BootSpec::smoke(std::path::PathBuf::new(), std::path::PathBuf::new());
        let plain = BootSpec::kvm_kernel_rootfs("plain", "/bzImage", "/rootfs.cpio.gz");
        let t = KvmBoot::new().to_tunnr_spec(&plain).unwrap();
        assert_eq!(t.success_marker, stock.success_marker, "unset marker = tunnr's stock marker");
        assert_eq!(t.timeout, stock.timeout, "unset budget = tunnr's default");

        let named = BootSpec::kvm_kernel_rootfs("named", "/bzImage", "/rootfs.cpio.gz")
            .with_success_marker("INSTALLER: INSTALL-OK")
            .with_boot_timeout(std::time::Duration::from_secs(300));
        let t = KvmBoot::new().to_tunnr_spec(&named).unwrap();
        assert_eq!(t.success_marker, "INSTALLER: INSTALL-OK");
        assert_eq!(t.timeout, std::time::Duration::from_secs(300));
    }

    /// **The default shape still produces the machine every earlier boot used.**
    /// If this goes red, the new field changed an existing caller's guest.
    #[test]
    fn the_default_guest_shape_is_the_uefi_serial_machine() {
        let spec = BootSpec::kvm_kernel_rootfs("d", "/vmlinuz", "/rootfs.cpio.gz");
        assert_eq!(spec.guest_shape, crate::GuestShape::UefiSerial);
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert_eq!(t.firmware, tunnr_vm::GuestFirmware::Uefi);
        assert_eq!(t.console, tunnr_vm::GuestConsole::Stdio);
        assert_eq!(t.display, tunnr_vm::GuestDisplay::None);
    }

    /// **The console-only cloud machine reaches the final argv**: no pflash, no
    /// serial port, a real VGA. Asserted on tunnr's OWN argv writer, not on
    /// fields that were merely set.
    #[test]
    fn the_bios_framebuffer_shape_reaches_the_final_argv() {
        let spec = BootSpec::kvm_kernel_rootfs("d", "/vmlinuz", "/rootfs.cpio.gz")
            .with_guest_shape(crate::GuestShape::BiosFramebuffer);
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert!(matches!(
            t.firmware,
            tunnr_vm::GuestFirmware::Bios { rom: None }
        ));
        let argv = tunnr_vm::boot_primitive::build_qemu_args(
            &t,
            &tunnr_vm::StagedFirmware::Bios { rom: None },
            None,
        );
        assert_eq!(argv.iter().filter(|a| a.contains("if=pflash")).count(), 0);
        let after = |flag: &str| {
            argv.iter()
                .position(|a| a == flag)
                .and_then(|i| argv.get(i + 1))
                .cloned()
        };
        assert_eq!(after("-serial"), Some("none".to_string()), "{argv:?}");
        assert_eq!(after("-vga"), Some("std".to_string()), "{argv:?}");
        assert!(!argv.contains(&"-nographic".to_string()), "{argv:?}");
    }

    /// **REFUSED [no-serial-console]** — a success marker on a machine with no
    /// serial port. Refused at the draupnir→tunnr seam, before a guest exists,
    /// and NOT quietly downgraded to a boot whose verdict would be invented.
    #[test]
    fn a_marker_on_the_console_less_shape_is_refused_by_name() {
        let spec = BootSpec::kvm_kernel_rootfs("d", "/vmlinuz", "/rootfs.cpio.gz")
            .with_guest_shape(crate::GuestShape::BiosFramebuffer)
            .with_success_marker("KORP-APPLIANCE BOOT-OK");
        let e = KvmBoot::new()
            .to_tunnr_spec(&spec)
            .expect_err("a marker that can never arrive must be refused");
        let msg = e.to_string();
        assert!(msg.contains("REFUSED [no-serial-console]"), "{msg}");
        assert!(msg.contains("KORP-APPLIANCE BOOT-OK"), "{msg}");
    }

    /// **REFUSED [secure-boot-on-bios]** — an enforcing UEFI pair named on a
    /// BIOS machine. Dropping it would turn a run that must enforce signatures
    /// into one that checks nothing and still comes back green.
    #[test]
    fn secure_boot_firmware_on_the_bios_shape_is_refused_by_name() {
        let spec = BootSpec::kvm_kernel_rootfs("d", "/vmlinuz", "/rootfs.cpio.gz")
            .with_guest_shape(crate::GuestShape::BiosFramebuffer)
            .with_firmware(crate::Firmware::SecureBoot);
        let e = KvmBoot::new()
            .to_tunnr_spec(&spec)
            .expect_err("a UEFI key store on a BIOS machine must be refused");
        assert!(
            e.to_string().contains("REFUSED [secure-boot-on-bios]"),
            "{e}"
        );
    }

    /// **0.2.0's `Firmware::Bios` IS the BiosFramebuffer machine's firmware**, so
    /// the two travel together and a named SeaBIOS blob reaches tunnr's own
    /// `GuestFirmware::Bios { rom }` instead of being refused as if it were a
    /// UEFI key store.
    #[test]
    fn bios_firmware_rides_the_bios_framebuffer_shape_with_its_rom() {
        let spec = BootSpec::kvm_kernel_rootfs("d", "/vmlinuz", "/rootfs.cpio.gz")
            .with_guest_shape(crate::GuestShape::BiosFramebuffer)
            .with_firmware(crate::Firmware::Bios {
                rom: Some("/usr/share/seabios/bios-256k.bin".into()),
            });
        let t = KvmBoot::new().to_tunnr_spec(&spec).unwrap();
        assert_eq!(
            t.firmware,
            tunnr_vm::GuestFirmware::Bios {
                rom: Some("/usr/share/seabios/bios-256k.bin".into())
            }
        );
        assert_eq!(t.console, tunnr_vm::GuestConsole::None);
        assert_eq!(t.display, tunnr_vm::GuestDisplay::Framebuffer);
        assert!(t.ovmf_code.is_none() && t.ovmf_vars_template.is_none());
    }

    /// **REFUSED [bios-on-uefi-shape]** — a BIOS firmware on the default UEFI
    /// shape. tunnr would boot the UEFI the shape names and drop the BIOS in
    /// silence, so the contradiction is named instead.
    #[test]
    fn bios_firmware_on_the_uefi_serial_shape_is_refused_by_name() {
        let spec = BootSpec::kvm_kernel_rootfs("d", "/vmlinuz", "/rootfs.cpio.gz")
            .with_firmware(crate::Firmware::Bios { rom: None });
        let e = KvmBoot::new()
            .to_tunnr_spec(&spec)
            .expect_err("a BIOS firmware on a UEFI machine must be refused");
        assert!(
            e.to_string().contains("REFUSED [bios-on-uefi-shape]"),
            "{e}"
        );
    }

    /// A shape named on a backend that has no machine is refused by
    /// [`BootSpec::validate`] rather than silently dropped — the same rule the
    /// `firmware` field already carries.
    #[test]
    fn a_guest_shape_on_a_container_is_refused_by_name() {
        let spec = BootSpec::container("c", "docker.io/library/alpine:3.20")
            .with_guest_shape(crate::GuestShape::BiosFramebuffer);
        let e = spec.validate().expect_err("a container has no machine shape");
        assert!(e.to_string().contains("REFUSED [shape-not-supported]"), "{e}");
    }

    /// `screendump` on a machine this backend never booted is a named error, not
    /// an empty file a report could quote as evidence.
    #[test]
    fn a_screendump_of_an_unknown_machine_is_refused_by_name() {
        let backend = KvmBoot::new();
        let m = Machine::started(
            "never-booted",
            &BootSpec::kvm_kernel_rootfs("nb", "/vmlinuz", "/rootfs.cpio.gz"),
        );
        let e = backend
            .screendump(&m, std::path::Path::new("/tmp/nope.ppm"))
            .expect_err("no live VM → no pixels");
        assert!(e.to_string().contains("REFUSED [no-live-vm]"), "{e}");
    }

    /// `booted_ok` / `exit_reason` / `shutdown` are honest about a machine this
    /// backend never booted: no false "booted", no invented reason, a named error.
    #[test]
    fn boot_verdict_readers_are_honest_about_an_unknown_machine() {
        let kvm = KvmBoot::new();
        let spec = BootSpec::kvm_kernel_rootfs("ghost", "/bzImage", "/rootfs.cpio.gz");
        let ghost = Machine::started("ghost-1", &spec);
        assert!(!kvm.booted_ok(&ghost), "a VM that was never booted did not boot OK");
        assert_eq!(kvm.exit_reason(&ghost), None);
        let err = kvm.shutdown(&ghost, std::time::Duration::from_secs(1)).unwrap_err();
        assert!(format!("{err}").contains("ghost-1"), "{err}");
    }

    /// A throwaway file to stand in for an ISO where only its EXISTENCE is under
    /// test (the argv shape), not its contents. The real boot proof uses a real ISO.
    fn write_probe_iso(tag: &str) -> std::path::PathBuf {
        let p = std::env::temp_dir().join(format!(
            "draupnir-probe-{tag}-{}.iso",
            std::process::id()
        ));
        std::fs::write(&p, b"not really an iso").unwrap();
        p
    }

    #[test]
    fn kvm_spec_rejects_a_non_kvm_image() {
        let mut spec = BootSpec::kvm_kernel_rootfs("bad", "/k", "/r");
        spec.image = ImageSource::OciImage("redis:7".into());
        assert!(matches!(
            KvmBoot::new().to_tunnr_spec(&spec),
            Err(Error::Spec(_))
        ));
    }

    #[test]
    fn serial_log_of_an_unknown_machine_is_none() {
        // No live boot for this id → no console (the guard the jera adapter relies on
        // to return an empty drain rather than panic).
        let kvm = KvmBoot::new();
        let m = Machine {
            id: "boot-not-live".into(),
            spec_name: "x".into(),
            kind: crate::Kind::Vm,
            power: PowerState::Unknown,
        };
        assert!(kvm.serial_log(&m).is_none(), "no live handle → no console");
    }

    #[test]
    fn status_of_an_unknown_machine_is_unknown() {
        let kvm = KvmBoot::new();
        let m = Machine {
            id: "boot-does-not-exist".into(),
            spec_name: "x".into(),
            kind: crate::Kind::Vm,
            power: PowerState::Unknown,
        };
        assert_eq!(kvm.status(&m).unwrap(), PowerState::Unknown);
    }

    #[test]
    fn map_status_folds_live_states_to_on_and_every_terminal_to_off() {
        // The two live states are `On`; every terminal (failed for any reason,
        // killed, timed-out) collapses to `Off` — a booted VM that later died is
        // powered off, not still on.
        assert_eq!(map_status(tunnr_vm::BootStatus::Booting), PowerState::On);
        assert_eq!(map_status(tunnr_vm::BootStatus::BootedOk), PowerState::On);
        assert_eq!(
            map_status(tunnr_vm::BootStatus::Failed("qemu exited 1".into())),
            PowerState::Off
        );
        assert_eq!(map_status(tunnr_vm::BootStatus::Killed), PowerState::Off);
        assert_eq!(map_status(tunnr_vm::BootStatus::TimedOut), PowerState::Off);
    }

    // ── host→guest forwards on the KVM path ──────────────────────────────────
    //
    // The seam that made gunnar's install arm bypass this crate. Every one of
    // these asserts the FINAL ARGV (tunnr's own `build_qemu_args`), not that a
    // field was set: a forward that reaches `NetConfig` and not the netdev is
    // exactly the silent drop the arm's own header warned about.

    /// The `-netdev` line tunnr renders for a spec, or `None` when the spec
    /// asked for `-nic none`. Shared by the forward tests so they cannot
    /// disagree about where to look (LAW 5).
    #[cfg(feature = "backend-tunnr")]
    fn netdev_of(spec: &BootSpec) -> Option<String> {
        let dirs = [std::path::PathBuf::from("/nonexistent-ovmf")];
        let t = KvmBoot::new().to_tunnr_spec_in(spec, &dirs).unwrap();
        let argv = tunnr_vm::boot_primitive::build_qemu_args(
            &t,
            &tunnr_vm::StagedFirmware::Uefi {
                code: "/code.fd".into(),
                vars: "/vars.fd".into(),
            },
            None,
        );
        let i = argv.iter().position(|a| a == "-netdev")?;
        argv.get(i + 1).cloned()
    }

    /// **A `PortMap` reaches QEMU's `hostfwd`.** `PortMap { host, container }`
    /// is `(host_port, guest_port)`, so 2222→22 must render `tcp::2222-:22` and
    /// never the transpose — a reversed pair binds the wrong host port and the
    /// test that uses it hangs rather than fails.
    #[test]
    #[cfg(feature = "backend-tunnr")]
    fn a_port_map_reaches_the_qemu_hostfwd_in_the_final_argv() {
        let spec = BootSpec::kvm_kernel_rootfs("fwd", "/vmlinuz", "/rootfs.cpio.gz")
            .with_port_map(2222, 22)
            .with_port_map(15051, 50051);
        let netdev = netdev_of(&spec).expect("a forwarding guest has a user netdev");
        assert!(netdev.contains("hostfwd=tcp::2222-:22"), "host 2222 → guest 22: {netdev}");
        assert!(
            netdev.contains("hostfwd=tcp::15051-:50051"),
            "host 15051 → guest 50051: {netdev}"
        );
        assert!(
            !netdev.contains("tcp::22-:2222"),
            "the pair must not be transposed: {netdev}"
        );
    }

    /// The bare `ports` form is the `host == guest` special case — the same
    /// folding the container backend does, so the two backends cannot disagree
    /// about what a published port means.
    #[test]
    #[cfg(feature = "backend-tunnr")]
    fn the_bare_ports_form_forwards_a_host_port_to_the_same_guest_port() {
        let spec = BootSpec::kvm_kernel_rootfs("fwd", "/vmlinuz", "/rootfs.cpio.gz").with_port(50051);
        let netdev = netdev_of(&spec).unwrap();
        assert!(netdev.contains("hostfwd=tcp::50051-:50051"), "{netdev}");
    }

    /// **Additive parity.** A KVM spec that forwards nothing must render the
    /// netdev it rendered before this seam existed — the guard that keeps a new
    /// field from moving a byte for every existing caller.
    #[test]
    #[cfg(feature = "backend-tunnr")]
    fn a_kvm_boot_that_forwards_no_port_leaves_the_netdev_untouched() {
        let spec = BootSpec::kvm_kernel_rootfs("plain", "/vmlinuz", "/rootfs.cpio.gz");
        assert_eq!(netdev_of(&spec).as_deref(), Some("user,id=n0"));
    }

    /// The airgap still wins where it is asked for alone: `NetMode::None` is
    /// `-nic none`, and a spec that names no port never reaches the forward arm.
    #[test]
    #[cfg(feature = "backend-tunnr")]
    fn an_airgapped_kvm_boot_still_gets_no_nic_at_all() {
        let spec = BootSpec::kvm_kernel_rootfs("dark", "/vmlinuz", "/rootfs.cpio.gz")
            .with_net(crate::NetMode::None);
        assert_eq!(netdev_of(&spec), None, "`-nic none` renders no -netdev");
    }

    // ── the medium's second face ─────────────────────────────────────────────

    /// **A medium can ride BOTH trays.** The firmware boots El Torito off the
    /// cdrom; the appliance's PID 1 reads the same bytes over virtio because its
    /// kernel has no ATA. Rendering only the cdrom is what made an install
    /// through this backend die with `INSTALLER: volume: no block device carries
    /// the ISO-9660 magic (CD001)` on 2026-08-15.
    #[test]
    fn a_medium_can_be_attached_on_both_the_cdrom_and_virtio() {
        let spec = BootSpec::kvm_disk("inst", "/bzImage", "/target.raw")
            .with_medium("/boot.iso")
            .with_medium_attach(crate::MediumAttach::Both);
        let args = KvmBoot::medium_qemu_args(&spec);
        let i = args.iter().position(|a| a == "-cdrom").expect("the firmware's face");
        assert_eq!(args[i + 1], "/boot.iso");
        assert!(
            args.iter()
                .any(|a| a == "file=/boot.iso,if=virtio,format=raw,readonly=on"),
            "the system's face, read-only so the installer's disk-picker cannot \
             choose it as a target: {args:?}"
        );
    }

    /// The virtio face alone, for a guest that boots something else entirely.
    #[test]
    fn the_virtio_only_attachment_emits_no_cdrom() {
        let spec = BootSpec::kvm_disk("inst", "/bzImage", "/target.raw")
            .with_medium("/boot.iso")
            .with_medium_attach(crate::MediumAttach::Virtio);
        let args = KvmBoot::medium_qemu_args(&spec);
        assert!(!args.iter().any(|a| a == "-cdrom"), "no IDE tray: {args:?}");
        assert!(
            args.iter().any(|a| a.starts_with("file=/boot.iso,if=virtio")),
            "{args:?}"
        );
    }

    /// **Additive parity.** A spec that never names an attachment renders the
    /// cdrom and nothing else — byte-identical to every spec written before
    /// `MediumAttach` existed.
    #[test]
    fn a_medium_with_no_attachment_named_renders_the_cdrom_alone() {
        let spec = BootSpec::kvm_disk("inst", "/bzImage", "/target.raw").with_medium("/boot.iso");
        let args = KvmBoot::medium_qemu_args(&spec);
        assert_eq!(args, vec!["-cdrom".to_string(), "/boot.iso".to_string()]);
        assert!(crate::MediumAttach::default().is_default());
    }
}