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

mikronova/draupnir

src/lib.rs

//! # Draupnir — the nordisk boot / provisioning library
//!
//! Draupnir is the **low-level engine that fires up a runtime instance** from a
//! single [`BootSpec`], across several engines, and drives its power lifecycle.
//!
//! **What** is started ([`Kind`]: VM, container, bare metal, program) lives on the
//! spec; **which engine** starts it is the [`Boot`] implementation the caller passes
//! to [`boot`]. The engines today:
//!
//! - [`kvm`] — a **KVM/appliance VM**, booted by driving **tunnr**'s
//!   `tunnr_vm::boot_test(BootSpec) -> BootHandle` primitive (feature
//!   `backend-tunnr`). Draupnir does **not** reimplement VM boot — it wires the
//!   [`Boot`] trait against tunnr.
//! - [`container`] — an **OCI container**, brought up over a container runtime.
//! - [`redfish`] — **bare metal**, provisioned out-of-band through a BMC's
//!   **Redfish** REST API (iLO / iDRAC / OpenBMC): insert a virtual-media ISO,
//!   set the one-time boot override to that media, power the node on. This is
//!   the capability Draupnir uniquely owns.
//! - [`exe`] — a **plain executable** run as a child process on this host: the
//!   non-VM, non-container shape (the `exe` build-thing kind).
//!
//! ## Booting OFF a medium (2026-08-15)
//!
//! A [`BootSpec`] can name a removable [`medium`](BootSpec::medium) and a
//! [`boot_order`](BootOrder), which is what makes an appliance ISO runnable at all:
//!
//! - [`BootSpec::iso_boot`] — live-boot an ISO. **One spec**, routable to the KVM
//!   backend as it stands or to real metal with [`on_metal`](BootSpec::on_metal),
//!   so `iso-kvm` and `iso-metal` are two runs of one claim rather than two claims.
//! - [`BootSpec::kvm_install_from_medium`] — boot off the ISO with a blank target
//!   disk attached (the INSTALL leg).
//! - [`BootSpec::kvm_boot_installed_disk`] — boot that disk with the medium
//!   **absent** (the SECOND VM lifetime). [`validate`](BootSpec::validate) refuses a
//!   spec that claims to boot the installed system while still holding the medium.
//!
//! A spec that names no medium and no boot order behaves exactly as it did before
//! these fields existed — both default to "emit nothing".
//!
//! ## Waiting: a thing that keeps running is the NORMAL case
//!
//! [`exe::ExeBoot::await_marker`] and [`kvm::KvmBoot::await_serial_marker`] wait for
//! a **named marker or a deadline**, never for the instance to terminate, and both
//! return the same [`Seen`] describing what was actually observed. An appliance that
//! boots correctly serves forever; waiting for it to exit is waiting for it to fail.
//!
//! ## Where Draupnir sits
//!
//! Draupnir is the **shared low-level boot lib**. Two high-level consumers depend
//! on it **directly** and neither duplicates its boot code:
//!
//! - **jera** (edda's job handler) → depends on Draupnir for job instances
//!   (`process | VM | container`). jera stays thin: just job policy.
//! - **Skidbladnir** (service/systemd, airgap, orchestration) → depends on
//!   Draupnir for service instances.
//!
//! Draupnir itself knows nothing about jobs or services; it just fires up and
//! controls instances. For its KVM backend it calls **down** into tunnr.
//!
//! ```text
//!   jera ─────────┐
//!                 ├──▶ Draupnir ──▶ { tunnr (KVM) | OCI runtime | Redfish BMC }
//!   Skidbladnir ──┘
//! ```
//!
//! ## The mythological nod
//!
//! Draupnir is Odin's gold ring that drips **eight identical copies** of itself
//! every ninth night. Here that is a *natural extension, not the core*: booting a
//! **fleet of identical machines from one ISO** — [`plan_fleet`] fans one
//! [`BootSpec`] out into N identical specs (bare-metal fleet provisioning via
//! Redfish). The core is one library, three boot backends.

use std::collections::BTreeMap;
use std::fmt;
use std::path::PathBuf;
use std::time::Duration;

pub mod container;
pub mod exe;
pub mod kvm;
/// draupnir's OWN qemu — the argv, the launch, the screen and the disk. See the
/// module doc for why the machine shape belongs here and not in tunnr.
pub mod qemu;
pub mod redfish;
/// **Draupnir's own BMC** — the Redfish *service* end, fronting KVM
/// ([`redfish_server`](crate::redfish_server)). Behind the `redfish-server` feature,
/// because a client-only build must never grow a listening socket.
#[cfg(feature = "redfish-server")]
pub mod redfish_server;
pub mod seed;
#[cfg(feature = "backend-youki")]
pub mod youki;

/// Draupnir's result alias.
pub type Result<T> = std::result::Result<T, Error>;

/// **Introspection / emit marker** — record one functional-status row for the
/// nornir test matrix (the constellation-wide introspection-coverage gate).
/// Wraps `nornir_testmatrix::functional_status` behind the optional `testmatrix`
/// feature: ON, it emits a real matrix row nornir reads back; OFF, it is a
/// compiled-out `#[inline]` no-op with NO nornir dependency, so the lean default
/// build never pulls it. `component` is the reporting unit (e.g. `"draupnir/seed"`),
/// `check` what it verified, `ok` the verdict, `detail` a short human note. Mirrors
/// the sibling constellation crates (skidbladnir, ordning-core, korp-collectors).
#[inline]
pub fn functional_status(component: &str, check: &str, ok: bool, detail: &str) {
    #[cfg(feature = "testmatrix")]
    nornir_testmatrix::functional_status(component, check, ok, detail);
    #[cfg(not(feature = "testmatrix"))]
    {
        let _ = (component, check, ok, detail);
    }
}

/// **Drain the buffered functional-status rows** as `(component, check, ok)` triples
/// (feature `testmatrix`) — returns AND clears the process-global buffer nornir's
/// matrix reads back. Exposed so a test can assert that a lifecycle surface emitted
/// the expected GREEN/RED verdict (a failed boot/probe must land as a RED row). It
/// reuses `nornir-testmatrix`'s own buffer verbatim — no twin. When the feature is
/// off there is no buffer, so this is not compiled.
#[cfg(feature = "testmatrix")]
pub fn drain_status_rows() -> Vec<(String, String, bool)> {
    nornir_testmatrix::drain_functional_rows()
        .into_iter()
        .map(|r| (r.suite, r.test_name, r.status == "pass"))
        .collect()
}

/// Everything that can go wrong firing up or controlling an instance.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
    /// The requested backend is not compiled in (build with its feature) or not
    /// available on this host.
    Unsupported(String),
    /// A live backend (tunnr / OCI runtime / Redfish BMC) reported a failure.
    Backend(String),
    /// The [`BootSpec`] is internally inconsistent (e.g. an ISO image handed to
    /// the KVM backend, or a Redfish spec with no BMC endpoint).
    Spec(String),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Unsupported(m) => write!(f, "draupnir: unsupported: {m}"),
            Error::Backend(m) => write!(f, "draupnir: backend error: {m}"),
            Error::Spec(m) => write!(f, "draupnir: invalid boot spec: {m}"),
        }
    }
}

impl std::error::Error for Error {}

/// **What** a [`BootSpec`] starts — the kind of instance, never the engine that
/// starts it.
///
/// The engine is the [`Boot`] implementation the caller hands to [`boot`]: a
/// [`Kind::Vm`] can be fired by [`kvm::KvmBoot`] (QEMU via tunnr) today, and by an
/// incus or KubeVirt engine tomorrow, from the same spec. Until 0.2.0 this enum was
/// `Backend { Kvm, Container, Redfish, Exe }`, which named an engine for two of its
/// four members (`Kvm`, `Redfish`) and a kind for the other two — so an engine that
/// runs both VMs and containers had no honest place in it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
    /// A virtual machine with its own kernel.
    Vm,
    /// An OCI container.
    Container,
    /// A **bare-metal** node, provisioned out-of-band (Redfish BMC).
    BareMetal,
    /// A **plain executable** run as a child process on this host — the non-VM,
    /// non-container shape ([`exe::ExeBoot`]). This is the `exe` build-thing kind:
    /// a binary stage 1 produced, started here, its stdout/stderr streamed, and
    /// awaited on a **named marker or a deadline** — never on termination, because
    /// a server that serves forever never terminates.
    Exe,
}

/// The bootable payload — the *source* an instance is fired up from.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImageSource {
    /// A kernel + rootfs/initramfs pair (the KVM appliance path → tunnr).
    KernelRootfs {
        /// Kernel image path (`-kernel`).
        kernel: String,
        /// Rootfs/initramfs path (`-initrd`).
        rootfs: String,
    },
    /// A bootable disk image, qcow2 or raw (the KVM disk path → tunnr). tunnr's
    /// direct-kernel launch still needs a `-kernel`, so the disk carries the
    /// kernel to boot it with explicitly (there is no in-image bootloader path).
    Disk {
        /// Kernel image path (`-kernel`) used to direct-boot the disk.
        kernel: String,
        /// Bootable disk image path (qcow2 or raw), attached as a virtio drive.
        disk: String,
    },
    /// A self-booting disk image with an **in-image bootloader** (the KVM
    /// firmware-boot path → tunnr). No `-kernel`: OVMF boots the disk directly,
    /// exactly as real hardware would. tunnr firmware-boots whenever the kernel
    /// path is empty (`is_direct_kernel_boot()` false), so this variant carries
    /// only the disk.
    BootloaderDisk {
        /// Bootable disk image path (qcow2 or raw), attached as a virtio drive
        /// and booted by firmware (no `-kernel`).
        disk: String,
    },
    /// An OCI image reference, e.g. `docker.io/library/redis:7` (container path).
    OciImage(String),
    /// A **bootable ISO**, booted *off the medium*: served as Redfish **virtual
    /// media** to a BMC (bare metal), or attached to a KVM guest as a `-cdrom`
    /// the firmware boots from ([`BootOrder::Medium`]).
    ///
    /// It suits **both** [`Kind::BareMetal`] and [`Kind::Vm`] — deliberately,
    /// and this is the whole of honesty rule 2: *one* spec, routed to the faked
    /// KVM backend or to real metal, so "it works in the demo" and "it works on the
    /// customer's server" stop being different claims. tunnr already renders an ISO
    /// payload as `-cdrom <iso> -boot d`, so the KVM leg is its existing firmware
    /// path, not a new one. Until 2026-08-15 `suits` rejected `(Iso, Kvm)`, which is
    /// exactly the wall that kept the ISO chain from ever running.
    Iso(String),
    /// A **plain executable** on this host — the `exe` build-thing kind, run as a
    /// child process by [`exe::ExeBoot`] (suits [`Kind::Exe`] only).
    ///
    /// `args` are the process's own argv (argv[1..]); they live here rather than on
    /// [`BootSpec::cmd`] because `cmd` is the container entrypoint override and is
    /// validated container-only — an exe's arguments are part of *what it is*, not a
    /// container knob.
    Executable {
        /// Path to the binary to run (stage 1's artifact).
        path: String,
        /// Arguments passed to it (argv[1..]).
        args: Vec<String>,
    },
}

impl ImageSource {
    /// Whether this payload is a legal source for `backend` — the KVM backend
    /// boots kernel+rootfs, a disk, **or an ISO off the medium**; the container
    /// backend an OCI image; Redfish an ISO; and [`Kind::Exe`] an
    /// [`Executable`](ImageSource::Executable). Used by [`BootSpec::validate`].
    ///
    /// `(Iso, Kvm)` is legal as of 2026-08-15 — the one deliberate widening in this
    /// pass. It was rejected before, which is precisely why the ISO chain could not
    /// run under KVM at all; tunnr has rendered an ISO payload as `-cdrom <iso>
    /// -boot d` since it was written (`tunnr/src/boot_primitive.rs:758-765`), so
    /// nothing new is being invented here — only un-forbidden.
    pub fn suits(&self, kind: Kind) -> bool {
        matches!(
            (self, kind),
            (ImageSource::KernelRootfs { .. }, Kind::Vm)
                | (ImageSource::Disk { .. }, Kind::Vm)
                | (ImageSource::BootloaderDisk { .. }, Kind::Vm)
                | (ImageSource::OciImage(_), Kind::Container)
                | (ImageSource::Iso(_), Kind::BareMetal)
                | (ImageSource::Iso(_), Kind::Vm)
                | (ImageSource::Executable { .. }, Kind::Exe)
        )
    }
}

/// A BMC (baseboard management controller) endpoint — the out-of-band Redfish
/// service on a bare-metal node (iLO / iDRAC / OpenBMC).
///
/// The secret (password / session token) is supplied out of band at drive time
/// and is deliberately **not** a field here, so a [`BootSpec`] never carries a
/// credential.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BmcEndpoint {
    /// Base URL of the Redfish service, e.g. `https://bmc-42.dc.example`.
    pub host: String,
    /// Redfish account username.
    pub username: String,
    /// The Redfish `ComputerSystem` resource id, e.g. `System.Embedded.1`.
    pub system_id: String,
}

/// The one-time boot device a Redfish node is overridden to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootTarget {
    /// Boot from virtual media / CD (the ISO we inserted).
    Cd,
    /// Network / PXE.
    Pxe,
    /// The local disk.
    Hdd,
    /// Drop into BIOS/UEFI setup.
    BiosSetup,
}

/// **Which device the firmware is told to boot from** — the boot-order half of
/// the boot-off-a-medium seam ([`BootSpec::boot_order`]).
///
/// This exists because *install* and *boot-the-installed-system* are two different
/// machines, not two phases of one. Honesty rule 1 of the chain driver: install in
/// one VM off the medium, **detach the medium, power-cycle, boot the disk in a
/// SECOND VM**. A caller must be able to say those two things as two distinct
/// specs, and a spec that claims to boot the installed disk must be *unable* to
/// carry the installer medium — otherwise "it booted" can silently be the installer
/// booting a second time.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BootOrder {
    /// **Unchanged**: draupnir emits no boot-order of its own and the backend keeps
    /// whatever it already did (tunnr's payload-shaped default; a Redfish node's
    /// existing boot list). This is the [`Default`] — a spec that never named a boot
    /// order produces a byte-identical launch to one written before this field
    /// existed.
    #[default]
    Auto,
    /// Boot **off the removable medium** — QEMU `-boot d`, Redfish
    /// [`BootTarget::Cd`]. The install leg.
    Medium,
    /// Boot **off the local disk** — QEMU `-boot c`, Redfish [`BootTarget::Hdd`].
    /// The boot-the-installed-system leg; [`BootSpec::validate`] refuses this
    /// together with a medium, by construction.
    Disk,
    /// **The CD first, the disk after it** — QEMU `-boot order=dc`, Redfish
    /// [`BootTarget::Cd`] (a one-time CD override falls back to the disk the
    /// same way).
    ///
    /// This is the cloud's own order and the mechanism of behaviour 17: a server
    /// whose medium is still inserted boots the installer AGAIN, forever, and
    /// nothing inside the guest can end it — only an eject from outside. The
    /// estate could not express that at all while `Medium` (`-boot d`) and
    /// `Disk` (`-boot c`) were the only orders: `d` alone cannot fall through to
    /// the disk, so an install that finished looked like a machine that had no
    /// system.
    MediumThenDisk,
    /// **The disk first, the CD after it** — QEMU `-boot order=cd`, the cloud's
    /// `boot_order = "disk,cdrom"` (UpCloud: "disk / cdrom / network or comma
    /// separated combination").
    ///
    /// The fast install shape (2026-09-26, "5 minutes"): a BLANK boot disk is
    /// not bootable, so the firmware falls through to the medium and the
    /// installer runs; the installer REBOOTS when it is done, the disk now
    /// boots, and nothing outside has to stop, detach, re-order or start the
    /// machine. So a machine in this order is not `-no-reboot`: a guest reboot
    /// is a reset in place, as the cloud's hypervisor does it, and the firmware
    /// reads this order again — the whole mechanism.
    DiskThenMedium,

}

/// **Which tray the removable medium rides in** — see
/// [`BootSpec::medium_attach`].
///
/// A medium has two faces and they are not interchangeable. The firmware reads
/// El Torito off an **IDE cdrom**; a PID 1 with no ATA driver finds the same
/// bytes only on **virtio**, by their `CD001` magic. Which face is needed is a
/// property of the guest KERNEL, not of the medium, so it cannot be inferred
/// here and has to be said.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MediumAttach {
    /// `-cdrom <medium>` — the IDE cdrom, and nothing else. What every spec did
    /// before this word existed, and what UpCloud does to an uploaded ISO, so it
    /// stays the [`Default`]: an untouched spec renders a byte-identical launch.
    #[default]
    Cdrom,
    /// `-drive file=<medium>,if=virtio,format=raw,readonly=on` — the virtio face
    /// alone, for a guest whose firmware boots something else and whose kernel
    /// has no ATA at all.
    Virtio,
    /// **Both faces of one medium**, cdrom first. The shape an appliance install
    /// actually needs, and the one this crate could not express until
    /// 2026-09-05: OVMF boots El Torito off the cdrom while the appliance's
    /// PID 1 reads the volume over virtio.
    ///
    /// MEASURED 2026-08-15 (gunnar), which is why this variant exists: driving
    /// the install through this backend with `Cdrom` alone died with
    /// `INSTALLER: volume: no block device carries the ISO-9660 magic (CD001)`
    /// — the appliance kernel is the `nuked` preset and vetoes ATA/AHCI/SCSI
    /// outright, so the cdrom the firmware had just booted from was invisible to
    /// the system that firmware started. The same trap bit a real deploy one
    /// cloud over (UpCloud, 2026-08-28: an ISO attached only as an IDE cdrom is
    /// "not a bootable disk"; the fix there was a second, virtio attachment of
    /// the same image).
    Both,
}

impl MediumAttach {
    /// Whether this is [`Cdrom`](MediumAttach::Cdrom) — the additive-parity
    /// guard (a spec that never named an attachment must render as before).
    pub fn is_default(self) -> bool {
        matches!(self, MediumAttach::Cdrom)
    }

    /// Whether the firmware gets an El-Torito cdrom to boot from.
    pub fn has_cdrom(self) -> bool {
        matches!(self, MediumAttach::Cdrom | MediumAttach::Both)
    }

    /// Whether the medium is also readable as a virtio block device.
    pub fn has_virtio(self) -> bool {
        matches!(self, MediumAttach::Virtio | MediumAttach::Both)
    }
}

impl BootOrder {
    /// The QEMU `-boot` value this renders to, or [`None`] for [`Auto`](BootOrder::Auto)
    /// (no `-boot` emitted → the launch is byte-identical to before this field).
    pub fn qemu_value(self) -> Option<&'static str> {
        match self {
            BootOrder::Auto => None,
            BootOrder::Medium => Some("d"),
            BootOrder::Disk => Some("c"),
            // `order=dc` and not `dc`: the long spelling is what QEMU documents
            // for a LIST, and it is the spelling the oden measurement ran.
            BootOrder::MediumThenDisk => Some("order=dc"),
            BootOrder::DiskThenMedium => Some("order=cd"),
        }
    }

    /// The Redfish one-time [`BootTarget`] this renders to, or [`None`] for
    /// [`Auto`](BootOrder::Auto) (leave the node's boot list alone).
    ///
    /// This is the *other half* of honesty rule 2: the SAME [`BootSpec`] expresses
    /// "boot the medium" once, and each backend translates it into its own
    /// vocabulary — `-boot d` here, `BootSourceOverrideTarget: "Cd"` there.
    pub fn redfish_target(self) -> Option<BootTarget> {
        match self {
            BootOrder::Auto => None,
            BootOrder::Medium => Some(BootTarget::Cd),
            BootOrder::Disk => Some(BootTarget::Hdd),
            // A one-time CD override IS "the CD first, the disk after it" on a
            // Redfish node: the override applies to one boot and the node's own
            // list takes over, which is the same fall-through `order=dc` gives.
            BootOrder::MediumThenDisk => Some(BootTarget::Cd),
            // No one-time override can say "disk, and the CD if the disk is blank";
            // the node's own list (disk first) is the nearest, so no override.
            BootOrder::DiskThenMedium => None,
        }
    }

    /// **The inverse of [`redfish_target`](BootOrder::redfish_target)** — read a
    /// Redfish `BootSourceOverrideTarget` back as a boot order.
    ///
    /// It lives here, next to its forward direction, because draupnir owns *both*
    /// ends of the Redfish protocol: the client turns a `BootOrder` into a
    /// `BootSourceOverrideTarget`, and [`redfish_server`](crate::redfish_server) —
    /// draupnir's own BMC — has to turn the received target back into the boot order
    /// it will actually apply. Two functions in two modules would be two mappings,
    /// and the one that mattered would be the one nobody tested.
    ///
    /// [`None`] (no override) is [`Auto`](BootOrder::Auto). A target draupnir does
    /// not model as a boot order — `Pxe`, `BiosSetup`, and every other DMTF
    /// `BootSource` token — yields [`Option::None`]: a service that cannot honour a
    /// target must **refuse it by name**, never silently substitute one it can.
    pub fn from_redfish_target(target: Option<BootTarget>) -> Option<BootOrder> {
        match target {
            None => Some(BootOrder::Auto),
            Some(BootTarget::Cd) => Some(BootOrder::Medium),
            Some(BootTarget::Hdd) => Some(BootOrder::Disk),
            Some(BootTarget::Pxe) | Some(BootTarget::BiosSetup) => None,
        }
    }
}

/// **The UEFI firmware a KVM guest boots on** — the pflash CODE/VARS pair, chosen
/// as ONE thing.
///
/// The host ships the halves as separate files (`OVMF_CODE_4M.fd`,
/// `OVMF_VARS_4M.fd`, `OVMF_CODE_4M.secboot.fd`, `OVMF_VARS_4M.ms.fd`), and a
/// caller handed two paths can pair them wrongly: the signature-ENFORCING CODE
/// with the stock, empty VARS template boots a guest that enforces against an
/// empty key store — i.e. it looks like Secure Boot and refuses nothing. That
/// combination is not discouraged here, it is **unrepresentable**: each variant
/// names both halves, so the pairing is a property of the type (the same shape as
/// [`BootOrder`], which cannot express "boot the disk with the installer medium
/// still in the tray").
///
/// The paths themselves are a **host fact**, resolved at boot time by
/// [`resolve`](Firmware::resolve) — `/usr/share/OVMF` is where Debian/Ubuntu put
/// them, Fedora uses `/usr/share/edk2/ovmf`, Arch `/usr/share/edk2-ovmf/x64`, and
/// `DRAUPNIR_OVMF_DIR` overrides the lot. A declared firmware that is not on this
/// host is **refused by name** — never substituted, because a guest that quietly
/// boots non-enforcing firmware when Secure Boot was asked for turns "the unsigned
/// image was refused" into a green that proved nothing.
///
/// Only the [`kvm`] backend has pflash: [`validate`](BootSpec::validate) refuses a
/// non-default firmware on any other backend rather than let it vanish.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Firmware {
    /// **Unchanged**: draupnir names no firmware and tunnr's own stock discovery
    /// (`tunnr_vm::locate_ovmf`) picks the pair, exactly as every boot before this
    /// field existed. The [`Default`] — the launch is byte-identical.
    #[default]
    HostDefault,
    /// **Stock UEFI**, named explicitly: `OVMF_CODE_4M.fd` + `OVMF_VARS_4M.fd`.
    /// This firmware verifies NOTHING — it is the ordinary UEFI boot. Choosing it
    /// by name (rather than [`HostDefault`](Firmware::HostDefault)) says "plain
    /// UEFI, and fail loudly if this host has not got it".
    Uefi,
    /// **Secure Boot with the Microsoft keys enrolled**:
    /// `OVMF_CODE_4M.secboot.fd` + `OVMF_VARS_4M.ms.fd`. The CODE build that
    /// actually enforces signatures, paired with the vars template whose PK/KEK/db
    /// are pre-populated — the shape under which a shim/GRUB/kernel chain signed by
    /// the UEFI CA boots and an unsigned image is REFUSED.
    SecureBoot,
    /// **Secure Boot with the caller's OWN key store**: the enforcing
    /// `OVMF_CODE_4M.secboot.fd` paired with a vars template the caller enrolled
    /// itself (`korp_installer::secureboot::enroll_platform_keys` returns exactly
    /// such an image). The CODE half is still fixed by the type — an enrolled key
    /// store on non-enforcing firmware is the misconfiguration this whole enum
    /// exists to make impossible.
    ///
    /// tunnr copies the template into the guest's private workdir, so the enrolled
    /// store is never written back to by a boot.
    SecureBootEnrolled {
        /// Path to the enrolled `OVMF_VARS` image (PK/KEK/db already populated).
        vars_template: String,
    },
    /// **SeaBIOS — the legacy BIOS path, and the one a cloud actually gives you.**
    ///
    /// No pflash at all. `rom` names the BIOS image (`Some`, e.g.
    /// `/usr/share/seabios/bios-256k.bin`) or leaves QEMU to use its own built-in
    /// SeaBIOS (`None`). Either way OVMF is neither searched for nor staged, so a
    /// BIOS guest boots on a host with no `ovmf` package.
    ///
    /// It exists because **UpCloud offers no firmware knob and every guest there
    /// boots SeaBIOS**, while every KVM proof this estate has run was UEFI. A
    /// proof on OVMF exercises a boot path the real box never takes — measured on
    /// oden 2026-09-20 against `gunnar-installer-open-fullk.iso`; the clause table
    /// is in `mock-upcloud`'s `kvm` module. [`enforces_secure_boot`] is false and
    /// says so: a BIOS machine cannot verify anything.
    ///
    /// [`enforces_secure_boot`]: Firmware::enforces_secure_boot
    Bios {
        /// The SeaBIOS ROM image, or `None` for QEMU's built-in.
        rom: Option<String>,
    },
}

/// **Whether the guest has a serial console at all** — see
/// [`BootSpec::serial`].
///
/// Every KVM proof this estate has run captured a serial console and keyed its
/// verdict off a marker line on it. A cloud guest has NO serial console: a PID-1
/// panic there reaches nobody and exists only as pixels. A harness that quietly
/// gave such a guest a serial port would make behaviour 13 untestable while
/// reporting it tested — the always-yes defect this estate has now found four
/// times.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SerialConsole {
    /// **Unchanged**: `-serial mon:stdio`, captured, and a marker can be observed
    /// on it. The [`Default`] — every spec written before this field renders the
    /// launch it always did.
    #[default]
    Stdio,
    /// `-serial none`. Nothing narrates the boot; a caller that wants a verdict
    /// must read the screen.
    None,
}

/// **What the guest's screen is, and who looks at it** — see
/// [`BootSpec::display`].
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Display {
    /// **Unchanged**: the backend's own default (tunnr's `headless`), which is
    /// `-nographic` — no framebuffer at all. The [`Default`].
    #[default]
    Auto,
    /// **A framebuffer, and only that**: `-vga std` plus `-display none`, and
    /// `-vnc <spec>` when `vnc` is set (e.g. `127.0.0.1:91`). The screen exists,
    /// nobody sits in front of it, and a screendump can hash it.
    ///
    /// `-nographic` would have removed the one surface a PID-1 panic appears on,
    /// which is why "headless" and "no screen" had to stop being the same word.
    Framebuffer {
        /// The QEMU `-vnc` value, or `None` for a screen nothing serves.
        vnc: Option<String>,
    },
}

/// **The QEMU machine type** — see [`BootSpec::machine`].
///
/// `Q35` is what draupnir has always rendered and stays the default, so an
/// existing caller's argv does not move. `Pc` (i440fx, PIIX IDE) is the machine
/// UpCloud hands out (MEASURED, ledger L46): its `-cdrom` sits on a real IDE
/// channel, where on q35 it rides AHCI — a kernel without ATA reads neither, a
/// kernel with only AHCI reads one and not the other, so a proof on the wrong
/// chipset proves the wrong machine.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MachineType {
    #[default]
    Q35,
    Pc,
}

impl MachineType {
    /// The `-machine` type word.
    pub fn qemu_value(self) -> &'static str {
        match self {
            MachineType::Q35 => "q35",
            MachineType::Pc => "pc",
        }
    }
}

/// **What the guest's real-time clock reads** — see [`BootSpec::rtc`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Rtc {
    /// **Unchanged**: QEMU's default, the host's own clock. The [`Default`].
    #[default]
    Host,
    /// The RTC starts `seconds` away from the host's clock (`-rtc base=<t>`,
    /// rendered as UTC). `+7200` is the skew every yvra VM boots with and the
    /// reason `gunnar-clock` exists — a rig whose guests boot at the right time
    /// makes that machinery look unnecessary.
    SkewedBy { seconds: i64 },
}

/// **Which MACHINE a KVM guest is** — firmware, serial console and framebuffer
/// as one named choice, because they are not independent knobs: they describe a
/// class of real machine, and mixing them by hand is how a proof ends up on a
/// machine nobody ships.
///
/// MEASURED, and the reason this type exists: `kvm.rs` set `headless = true` and
/// tunnr's `build_qemu_args` pushed `-serial mon:stdio` and the OVMF pflash pair
/// **unconditionally**. So every KVM proof in this constellation ran on a UEFI
/// machine with a serial console and no framebuffer — while a console-only cloud
/// VM (UpCloud) is SeaBIOS, has no serial port, and shows the guest only through
/// VNC. Booting `gunnar-installer-open-fullk.iso` in that shape produced **zero
/// bytes on stdout**: every line the installer had to say was on the framebuffer
/// and nowhere else. A proof that cannot be run on the customer's machine is not
/// a proof of the customer's machine.
///
/// Mapped onto tunnr's own `GuestShape` by
/// [`kvm::KvmBoot::to_tunnr_spec`], which REFUSES rather than approximates when
/// the rest of the spec contradicts the shape.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GuestShape {
    /// **UEFI + captured serial console + no framebuffer.** What every KVM boot
    /// here has silently used, and the right machine for a local marker-driven
    /// smoke boot: `success_marker` works because there is a wire to carry it.
    #[default]
    UefiSerial,
    /// **SeaBIOS + no serial port + a VGA framebuffer.** The machine a
    /// console-only cloud VM actually is. A guest in this shape can carry no
    /// `BOOT-OK` marker at all — its evidence is
    /// [`kvm::KvmBoot::screendump`], or whatever the guest itself reports over
    /// the network. Naming a [`BootSpec::success_marker`] together with it is a
    /// named refusal, not a marker that is quietly ignored.
    BiosFramebuffer,
}

/// Directories searched for OVMF images, in order, when
/// [`Firmware::resolve`] is asked for a pair. Debian/Ubuntu first (the
/// constellation's own hosts), then Fedora, Arch and the QEMU share dir.
/// [`OVMF_DIR_ENV`] prepends to this list.
pub const OVMF_SEARCH_DIRS: &[&str] = &[
    "/usr/share/OVMF",
    "/usr/share/ovmf",
    "/usr/share/edk2/ovmf",
    "/usr/share/edk2-ovmf/x64",
    "/usr/share/qemu",
];

/// Operator override for the OVMF search path — a `:`-separated list of
/// directories, tried BEFORE [`OVMF_SEARCH_DIRS`]. The escape hatch for a distro
/// (or a container image) that puts the firmware somewhere draupnir does not know,
/// so a host layout never needs a rebuild — the same shape as
/// `DRAUPNIR_VM_BOOT_TIMEOUT`.
pub const OVMF_DIR_ENV: &str = "DRAUPNIR_OVMF_DIR";

impl Firmware {
    /// The CODE-image basenames this choice accepts, in preference order (the `_4M`
    /// spelling is Debian/Ubuntu's; the bare one is everyone else's).
    fn code_candidates(&self) -> &'static [&'static str] {
        match self {
            // A BIOS machine has no CODE image to find: `Bios` is resolved by
            // `resolve_in` before it ever gets here.
            Firmware::HostDefault | Firmware::Bios { .. } => &[],
            Firmware::Uefi => &["OVMF_CODE_4M.fd", "OVMF_CODE.fd"],
            Firmware::SecureBoot | Firmware::SecureBootEnrolled { .. } => {
                &["OVMF_CODE_4M.secboot.fd", "OVMF_CODE.secboot.fd"]
            }
        }
    }

    /// The VARS-**template** basenames this choice accepts, in preference order.
    /// Empty for [`SecureBootEnrolled`](Firmware::SecureBootEnrolled), whose vars
    /// image is an explicit path rather than a discovered file.
    fn vars_candidates(&self) -> &'static [&'static str] {
        match self {
            Firmware::HostDefault
            | Firmware::SecureBootEnrolled { .. }
            | Firmware::Bios { .. } => &[],
            Firmware::Uefi => &["OVMF_VARS_4M.fd", "OVMF_VARS.fd"],
            Firmware::SecureBoot => &["OVMF_VARS_4M.ms.fd", "OVMF_VARS.ms.fd"],
        }
    }

    /// Does this firmware actually **enforce** image signatures? The question a
    /// caller asks before claiming a Secure Boot result: only an enforcing CODE
    /// build can turn "the unsigned image was refused" into a measurement.
    pub fn enforces_secure_boot(&self) -> bool {
        matches!(
            self,
            Firmware::SecureBoot | Firmware::SecureBootEnrolled { .. }
        )
    }

    /// A short, stable label — what a GUI firmware picker shows, and what a config
    /// file spells. Paired with [`from_label`](Firmware::from_label).
    pub fn label(&self) -> &'static str {
        match self {
            Firmware::HostDefault => "host-default",
            Firmware::Uefi => "uefi",
            Firmware::SecureBoot => "secure-boot",
            Firmware::SecureBootEnrolled { .. } => "secure-boot-enrolled",
            Firmware::Bios { .. } => "bios",
        }
    }

    /// The choices a picker offers without further input — the three that need no
    /// caller-supplied path. [`SecureBootEnrolled`](Firmware::SecureBootEnrolled)
    /// is deliberately absent: it carries a vars image only the caller can name.
    pub fn pickable() -> [Firmware; 3] {
        [Firmware::HostDefault, Firmware::Uefi, Firmware::SecureBoot]
    }

    /// Read a [`label`](Firmware::label) back. Unknown text is [`None`] — a config
    /// that names a firmware this crate does not model must be refused, never
    /// silently downgraded to the host default.
    pub fn from_label(label: &str) -> Option<Firmware> {
        match label {
            "host-default" => Some(Firmware::HostDefault),
            "uefi" => Some(Firmware::Uefi),
            "secure-boot" => Some(Firmware::SecureBoot),
            "bios" => Some(Firmware::Bios { rom: None }),
            _ => None,
        }
    }

    /// The directories to search on THIS host: `$DRAUPNIR_OVMF_DIR` (`:`-separated)
    /// first, then [`OVMF_SEARCH_DIRS`].
    pub fn search_dirs() -> Vec<PathBuf> {
        let mut dirs: Vec<PathBuf> = std::env::var(OVMF_DIR_ENV)
            .unwrap_or_default()
            .split(':')
            .filter(|s| !s.is_empty())
            .map(PathBuf::from)
            .collect();
        dirs.extend(OVMF_SEARCH_DIRS.iter().map(PathBuf::from));
        dirs
    }

    /// Resolve this choice to the `(CODE, VARS-template)` pair on this host, or
    /// [`None`] for [`HostDefault`](Firmware::HostDefault) (nothing named → tunnr's
    /// own discovery, byte-identical argv).
    ///
    /// Searches [`search_dirs`](Firmware::search_dirs). A firmware that is not
    /// there is an [`Error::Spec`] naming the files looked for and the directories
    /// searched — never a fall-back to a different firmware.
    pub fn resolve(&self) -> Result<Option<(PathBuf, PathBuf)>> {
        self.resolve_in(&Self::search_dirs())
    }

    /// [`resolve`](Firmware::resolve) against an explicit directory list — the seam
    /// the unit tests drive, so firmware resolution is provable against a known
    /// directory instead of against whatever the build host has installed.
    pub fn resolve_in(&self, dirs: &[PathBuf]) -> Result<Option<(PathBuf, PathBuf)>> {
        // Neither of these has a pflash pair: `HostDefault` leaves the choice to
        // the backend, and `Bios` HAS no OVMF by definition. Returning `None` for
        // the BIOS case is not a fallback to UEFI — the caller that receives it
        // must emit no pflash at all, which is what `qemu::argv` does.
        if matches!(self, Firmware::HostDefault | Firmware::Bios { .. }) {
            return Ok(None);
        }
        let code = Self::find_one(self.label(), "CODE", self.code_candidates(), dirs)?;
        let vars = match self {
            // The caller named its own enrolled key store: it is a path, not a
            // discovery. Refuse it by that exact path if it is not on disk — a
            // Secure Boot run without the keys it says it enrolled is not a run.
            Firmware::SecureBootEnrolled { vars_template } => {
                let p = PathBuf::from(vars_template);
                if !p.is_file() {
                    return Err(Error::Spec(format!(
                        "firmware `{}`: the enrolled OVMF_VARS image `{}` is not on this \
                         host — a Secure Boot guest cannot be given a key store that does \
                         not exist, and booting it on the stock template would enforce \
                         against an empty db",
                        self.label(),
                        p.display()
                    )));
                }
                p
            }
            _ => Self::find_one(self.label(), "VARS", self.vars_candidates(), dirs)?,
        };
        Ok(Some((code, vars)))
    }

    /// First existing `<dir>/<candidate>` over the cartesian product, or an
    /// [`Error::Spec`] that names every candidate and every directory tried.
    fn find_one(
        label: &str,
        half: &str,
        candidates: &[&str],
        dirs: &[PathBuf],
    ) -> Result<PathBuf> {
        for dir in dirs {
            for c in candidates {
                let p = dir.join(c);
                if p.is_file() {
                    return Ok(p);
                }
            }
        }
        Err(Error::Spec(format!(
            "firmware `{label}`: no OVMF {half} image on this host — looked for [{}] in [{}]. \
             Install the firmware (Debian/Ubuntu: `ovmf`) or point {OVMF_DIR_ENV} at it; \
             draupnir will NOT substitute a different firmware for the one you named",
            candidates.join(", "),
            dirs.iter()
                .map(|d| d.display().to_string())
                .collect::<Vec<_>>()
                .join(", ")
        )))
    }
}

/// The power state of an instance.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PowerState {
    /// Running.
    On,
    /// Powered off.
    Off,
    /// Not yet observed / indeterminate.
    Unknown,
}

/// The container **network mode** — how the OCI backend attaches the container to
/// a network. Only the [`container`](BootSpec::container) backend acts on it
/// (KVM/Redfish carry no container network). [`Default`](NetMode::Default) is the
/// runtime default (podman/Docker's own choice — **no** `--network` flag, so the
/// created `HostConfig.network_mode` stays unset and the create body is byte-
/// identical to a spec that never named a net mode). The airgap case is
/// [`None`](NetMode::None): it renders `--network none` and cuts the container off
/// from all egress — the load-bearing wire for Skidbladnir's airgap container route.
///
/// Mirrors jera's `ContainerSpec` `NetMode` field-for-field so a jera container-run
/// routes its net choice through this **one** draupnir OCI engine — jera passes its
/// rendered value across with [`NetMode::from_oci_value`]`(jera_spec.net.oci_value())`
/// (no cross-repo variant coupling; the wire is the OCI string `"none"`/`"host"`/`"bridge"`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NetMode {
    /// The runtime default — **no** `--network` flag; `HostConfig.network_mode`
    /// stays unset (byte-identical to a spec that never set a net mode).
    #[default]
    Default,
    /// Airgap: `--network none` — the container gets no network (loopback only).
    None,
    /// Share the host network namespace (`--network host`).
    Host,
    /// The default bridge network (`--network bridge`).
    Bridge,
}

impl NetMode {
    /// The OCI `HostConfig.network_mode` value this renders to, or [`Option::None`]
    /// for [`Default`](NetMode::Default) (leaves the field unset → the runtime
    /// default). `Some("none")` is the airgap value the container backend threads
    /// onto the create body.
    pub fn oci_value(self) -> Option<&'static str> {
        match self {
            NetMode::Default => Option::None,
            NetMode::None => Some("none"),
            NetMode::Host => Some("host"),
            NetMode::Bridge => Some("bridge"),
        }
    }

    /// Whether this is [`Default`](NetMode::Default) (no net flag) — the additive-
    /// parity guard (a default-net spec must produce the unchanged create body).
    pub fn is_default(self) -> bool {
        matches!(self, NetMode::Default)
    }

    /// Reconstruct a `NetMode` from an OCI network-mode string — jera's
    /// `oci_value()` output: `Some("none"|"host"|"bridge")` maps to the matching
    /// variant; [`Option::None`] (or any unrecognised value) maps to
    /// [`Default`](NetMode::Default). This is the **cross-repo wire** jera passes
    /// without depending on draupnir's variant names.
    pub fn from_oci_value(value: Option<&str>) -> Self {
        match value {
            Some("none") => NetMode::None,
            Some("host") => NetMode::Host,
            Some("bridge") => NetMode::Bridge,
            _ => NetMode::Default,
        }
    }
}

/// **cloud-init NoCloud provisioning** for a KVM appliance boot: the `user-data`
/// (and optional `meta-data`) authored into a small FAT seed image (volume label
/// `cidata`) the guest's cloud-init picks up at first boot.
///
/// Only the [`kvm`] backend consumes it — containers have no init firstboot and
/// the Redfish path provisions the metal itself. It is pure data (zero deps); the
/// seed *image* is authored by the KVM adapter behind `backend-tunnr`.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CloudInit {
    /// The cloud-init `user-data` document (typically begins `#cloud-config`).
    pub user_data: String,
    /// The `meta-data` document; when `None` a minimal default carrying an
    /// `instance-id`/`local-hostname` is supplied by the seed builder.
    pub meta_data: Option<String>,
    /// The optional NoCloud `network-config` document (cloud-init network schema).
    /// `None` → no `network-config` file is written to the seed and the guest keeps
    /// its default (usually DHCP). Present → authored as the third seed file.
    pub network_config: Option<String>,
}

impl CloudInit {
    /// A NoCloud provision from a `user-data` document (default `meta-data`, no
    /// `network-config`).
    pub fn user_data(user_data: impl Into<String>) -> Self {
        Self {
            user_data: user_data.into(),
            meta_data: None,
            network_config: None,
        }
    }

    /// Attach a NoCloud `network-config` document (builder style).
    pub fn with_network_config(mut self, network_config: impl Into<String>) -> Self {
        self.network_config = Some(network_config.into());
        self
    }
}

/// A container **published-port mapping** — a distinct `host:container` pair
/// (podman `-p HOST:CONTAINER`). This is the general publish form: a service
/// listening on a **fixed port inside** the container (FalkorDB always binds
/// `6379`, Spark-Connect `15002`) can be published on a **different host port**
/// so several isolated copies coexist on one host — e.g. per-zone offsets
/// (`Demo` on `6379`, `Test` on `6380`, `Prod` on `6381`) that all reach the
/// same in-container `6379`. Carried on [`BootSpec::port_maps`].
///
/// The single-port field ([`BootSpec::ports`], a bare `u16`) is exactly the
/// `host == container` special case and stays the byte-identical default — a
/// spec that uses only `ports` renders the same create body it always did. Both
/// forms are published (`ports` as `host==container`, `port_maps` as the pair),
/// so a spec may carry either or both.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct PortMap {
    /// The **host** port the publish binds on the host (the `HOST` in `-p
    /// HOST:CONTAINER`); this is what a client on the host connects to.
    pub host: u16,
    /// The **container** port the service listens on *inside* the container
    /// (the `CONTAINER` in `-p HOST:CONTAINER`); fixed by the image/service.
    pub container: u16,
}

impl PortMap {
    /// A distinct `host:container` publish — the general form (the host port
    /// may differ from the in-container port, e.g. a per-zone offset).
    pub fn new(host: u16, container: u16) -> Self {
        Self { host, container }
    }

    /// The `host == container` publish — the single-port special case (the same
    /// mapping the bare-`u16` [`BootSpec::ports`] form produces).
    pub fn same(port: u16) -> Self {
        Self {
            host: port,
            container: port,
        }
    }
}

impl From<u16> for PortMap {
    /// A bare port maps `host == container` (the single-port form).
    fn from(port: u16) -> Self {
        Self::same(port)
    }
}

impl From<(u16, u16)> for PortMap {
    /// A `(host, container)` tuple is the distinct-mapping form.
    fn from((host, container): (u16, u16)) -> Self {
        Self::new(host, container)
    }
}

/// A **self-contained boot request**. One shape fires up any backend; the
/// [`backend`](BootSpec::backend) selects the driver and [`validate`] enforces
/// that the [`image`](BootSpec::image) (and, for Redfish, the [`bmc`]) match.
///
/// [`bmc`]: BootSpec::bmc
// NB: `PartialEq` only (not `Eq`): the container `cpus` cap is an `Option<f64>`,
// and `f64` is not `Eq`. Every `==`/`assert_eq!` on a `BootSpec` needs only
// `PartialEq`; `BootSpec` is never used as a hash/btree key, so dropping `Eq` is
// additive (no consumer required it).
#[derive(Debug, Clone, PartialEq)]
pub struct BootSpec {
    /// Human/instance name (also the fleet-member prefix).
    pub name: String,
    /// **What** this starts. The engine that starts it is the [`Boot`] passed to
    /// [`boot`], not a field here.
    pub kind: Kind,
    /// The bootable payload source.
    pub image: ImageSource,
    /// Guest/appliance RAM in MiB (ignored by the bare-metal Redfish path).
    pub mem_mb: u32,
    /// vCPU count (ignored by the bare-metal Redfish path).
    pub cores: u32,
    /// Kernel/boot command line, if the backend takes one.
    pub cmdline: String,
    /// Container command / entrypoint override (empty = the image's own default).
    /// Only the [`container`] backend acts on it; ignored by KVM/Redfish.
    pub cmd: Vec<String>,
    /// Container ports to publish, each bound to the **same host port**
    /// (`host == container`) — the single-port form. Only the [`container`]
    /// backend acts on them; ignored by KVM/Redfish. For a **distinct**
    /// `host:container` publish (a per-zone host offset onto a fixed in-container
    /// port) use [`port_maps`](BootSpec::port_maps); both are published.
    pub ports: Vec<u16>,
    /// Container **distinct-mapping** published ports — each a [`PortMap`]
    /// `{ host, container }` rendered as podman `-p host:container` /
    /// `HostConfig.port_bindings[container/tcp] = host`. This is the form that
    /// publishes a **different host port than the in-container port**: FalkorDB
    /// listens on `6379` inside every zone's container, but `Test` publishes it
    /// on host `6380` and `Prod` on `6381` (`PortMap::new(6380, 6379)`), so the
    /// zones don't collide on the host yet each reaches the fixed in-container
    /// port. Empty (the default) leaves the create body **byte-identical** to a
    /// spec that never named it; it composes with [`ports`](BootSpec::ports)
    /// (the `host == container` form) — both sets are published. Only the
    /// [`container`] backend acts on it; ignored by KVM/Redfish.
    pub port_maps: Vec<PortMap>,
    /// Environment for the instance (container env; appliance kernel env).
    pub env: BTreeMap<String, String>,
    /// The BMC endpoint — **required** for [`Kind::BareMetal`], `None` otherwise.
    pub bmc: Option<BmcEndpoint>,
    /// Optional cloud-init NoCloud provisioning. Consumed by the [`kvm`] backend,
    /// which authors it into a seed image the guest reads at first boot; ignored by
    /// the container/Redfish backends. `None` → no seed is attached.
    pub cloud_init: Option<CloudInit>,
    /// Container **network mode** — how the OCI backend attaches the container to a
    /// network. Only the [`container`] backend acts on it (KVM/Redfish ignore it).
    /// [`NetMode::Default`] (the field's [`Default`]) leaves the create body
    /// byte-identical to a spec that never named a net mode; [`NetMode::None`]
    /// renders `--network none` (the airgap wire). Added additively — every existing
    /// constructor defaults it to [`NetMode::Default`].
    pub net: NetMode,
    /// Container **CPU quota** (podman `--cpus`) — how many host cores the container
    /// may use. `None` (the default) leaves it **unconstrained**, so the container
    /// sees **all** host cores: the deliberate default for hot infra (FalkorDB's
    /// OpenMP pool, a Spark executor) that must never be throttled to one core.
    /// `Some(n)` caps it at `n` cores (rendered as `HostConfig.nano_cpus = n * 1e9`).
    /// Only the [`container`] backend acts on it (KVM sizing is [`mem_mb`]/[`cores`]);
    /// `None` keeps the create body byte-identical to a spec that never named it.
    ///
    /// [`cores`]: BootSpec::cores
    pub cpus: Option<f64>,
    /// Container **memory limit** in MiB (podman `--memory`). `None` (the default)
    /// leaves it **unconstrained** (the container may use the box's memory) — the hot-
    /// infra default. `Some(m)` caps it at `m` MiB (`HostConfig.memory = m * 1MiB`).
    /// Only the [`container`] backend acts on it; the KVM guest RAM is the separate
    /// [`mem_mb`](BootSpec::mem_mb). `None` keeps the create body byte-identical.
    pub mem_limit_mb: Option<u32>,
    /// Container **security options** — podman/Docker `--security-opt`, rendered
    /// verbatim onto `HostConfig.security_opt`. Only the [`container`] backend acts
    /// on it (KVM/Redfish ignore it); **empty (the default) keeps the create body
    /// byte-identical** to a spec that never named one, so nothing that exists today
    /// moves.
    ///
    /// It exists because a container's *default* syscall filter is not a property of
    /// the workload and cannot be discovered from inside it. podman's stock
    /// `/usr/share/containers/seccomp.json` allow-lists no `io_uring_*` syscall, so
    /// `io_uring_setup` inside a stock container returns **ENOSYS** — measured on
    /// oden 2026-08-11, kernel 7.0.0-29, with `/proc/sys/kernel/io_uring_disabled=0`
    /// and the identical probe returning a valid fd on the host and under
    /// `--security-opt seccomp=unconfined`. A caller benchmarking an io_uring code
    /// path therefore has no way to run it at all without this field, and "the arm
    /// refused to start" is indistinguishable from "the kernel lacks it".
    ///
    /// Each entry is one `--security-opt` value, e.g. `seccomp=/path/profile.json`,
    /// `seccomp=unconfined`, `label=disable`, `no-new-privileges`. draupnir does not
    /// interpret them: the daemon owns their meaning, and a value this crate parsed
    /// would be a second, staler copy of that grammar.
    pub security_opt: Vec<String>,
    /// Container **bind mounts** — podman/Docker `-v` strings, `host:container[:opts]`
    /// verbatim, where `host` is a directory OR a named volume. Only the
    /// [`container`] backend acts on it (KVM/Redfish ignore it); **empty (the
    /// default) keeps the create body byte-identical** to a spec that never named one.
    ///
    /// It exists because the mounts of a long-lived container used to have NO way
    /// in. [`ContainerBoot::boot_with_binds`](container::ContainerBoot::boot_with_binds)
    /// took them as a separate argument, so only the caller that knew to reach past
    /// [`Boot::boot`] could pass any — and the generic `Boot` runner in jera, which
    /// boots every long-lived container korp starts, could not. MEASURED on oden
    /// 2026-09-10: korp's FalkorDB spec declared a named volume at `/data` (and a unit
    /// test proved the DECLARATION), while `podman inspect` of the live container
    /// showed `Mounts: []`, `podman volume ls` was empty, and the store recreated at
    /// 19:09:44 answered `DBSIZE 0` — every graph it had ever held died with its
    /// container. A mount is part of WHAT is booted, so it lives on the spec.
    pub binds: Vec<String>,
    /// **Container process USER** — podman `--user`, rendered verbatim onto the
    /// create body's `User` (`uid`, `uid:gid` or a name the image knows). `None`
    /// (the default) leaves the image's own `USER` in place and keeps the create
    /// body **byte-identical** to a spec that never named one. Only the
    /// [`container`] backend acts on it (KVM/Redfish have no such notion, and
    /// [`validate`](BootSpec::validate) refuses it there rather than drop it).
    ///
    /// It exists because *who the process is* decides **who owns the files it
    /// writes to a bind mount**, and that is not a property of the image. Under
    /// **rootless** podman, container UID 0 maps to the invoking host user, so
    /// `Some("0:0")` is what makes a build's output land on the mount owned by
    /// the account that then has to read, copy and publish it. An image that
    /// runs as its own unprivileged user writes files owned by a subuid the host
    /// account cannot even read — the build "succeeds" and its artifact is
    /// unreachable. Njord's appliance build (`deployer-server`'s build engine)
    /// had `--user=0:0` on its `podman run` argv for exactly that reason and
    /// could not move onto this crate without it.
    pub user: Option<String>,
    /// **Container WORKING DIRECTORY** — podman `--workdir`, rendered verbatim
    /// onto the create body's `WorkingDir`. `None` (the default) leaves the
    /// image's own `WORKDIR` and keeps the create body **byte-identical**. Only
    /// the [`container`] backend acts on it; refused elsewhere by
    /// [`validate`](BootSpec::validate) for the same reason as [`user`].
    ///
    /// It exists because a build command's **relative** paths are the interface:
    /// `cargo build` finds its manifest by walking up from the cwd, and
    /// `cargo-leptos` resolves its `site-root` relative to the project dir, so
    /// the directory the process starts in decides *where the output lands*, not
    /// merely where it looks. Rewriting the argv to absolute paths is not an
    /// equivalent workaround: it changes the tool's own relative-path answers.
    ///
    /// [`user`]: BootSpec::user
    pub working_dir: Option<String>,
    /// podman `--cap-add`: capabilities the container needs beyond the
    /// runtime default (`NET_ADMIN` for a guest that raises its own WireGuard
    /// interface — nordisk-pairing's fleet test, 2026-09-24). Empty = none,
    /// and the create body stays byte-identical to one that never named any.
    /// Only the [`container`] backend acts on it.
    pub cap_add: Vec<String>,
    /// podman `--device`: host device nodes handed into the container
    /// (`/dev/net/tun` for the same guest). Empty = none. Container backend only.
    pub devices: Vec<String>,
    /// podman `--cap-drop` (`ALL` drops every capability). Empty = the runtime
    /// default set, and the create body stays byte-identical. Container backend only.
    pub cap_drop: Vec<String>,
    /// podman `--read-only`: the container's root filesystem is read-only (it writes
    /// only to its mounts and tmpfs). `false` = the runtime default. Container only.
    pub read_only_rootfs: bool,
    /// podman `--tmpfs`, as `(path, options)` (`("/tmp", "rw,size=1024m,mode=1777")`).
    /// Empty = none. Container backend only.
    pub tmpfs: Vec<(String, String)>,
    /// The image must already be local: never pulled (`--pull=never`). A missing image
    /// is refused by name. `false` = pulled when missing (the old behaviour).
    pub never_pull: bool,
    /// **KVM extra QEMU args** — appended verbatim after tunnr's own argv (via
    /// `tunnr_vm::BootSpec::extra_qemu_args`). Only the [`kvm`] backend acts on it
    /// (containers/Redfish ignore it); it is the seam a KVM consumer uses to attach
    /// host-specific QEMU devices tunnr's spec does not model natively — e.g.
    /// Skidbladnir's `org.skidbladnir.report` virtio-serial report port, its
    /// `org.qemu.guest_agent.0` guest-agent port, a read-only data drive, an install
    /// `-cdrom`, `-cpu host`, and a `-snapshot` guard that keeps a shared,
    /// content-addressed base image from being mutated. Empty (the default) leaves
    /// the tunnr argv **byte-identical** to a spec that never named it.
    pub extra_qemu_args: Vec<String>,
    /// **KVM: must the guest's writes to the boot disk SURVIVE the boot?**
    ///
    /// `false` (the default) keeps the long-standing behaviour: an
    /// [`ImageSource::BootloaderDisk`] is opened `snapshot=on`, so guest writes land
    /// in a throwaway overlay and a shared, content-addressed base OS image can never
    /// be mutated by a boot. That is the right default — it is what stops a matrix
    /// run from poisoning the image every later run depends on.
    ///
    /// `true` says the caller owns this disk (typically a per-run copy or an overlay
    /// it made itself) and needs it to persist, because the *point* of the boot is
    /// what the guest wrote. Without this there is no way to express **two boots of
    /// one machine**: boot once to install, boot again to see what systemd does with
    /// the installed system at startup. Under the COW default the second boot silently
    /// gets a pristine image, re-runs first-boot provisioning, and produces
    /// plausible-looking 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
    /// (md5) to the base, and boot B replayed the whole of boot A.
    ///
    /// Only the [`kvm`] backend acts on it, and only for a boot whose disk would
    /// otherwise be COW-protected; every other [`ImageSource`] already persists.
    /// `false` leaves the tunnr spec **byte-identical** to a spec that never named it.
    pub persist_disk: bool,
    /// **The removable boot MEDIUM** — an installer/appliance ISO attached *in
    /// addition to* the [`image`](BootSpec::image) payload, so a machine can be
    /// booted OFF the medium while a separate, blank target disk is present to be
    /// installed onto. KVM renders it `-cdrom <iso>`; Redfish inserts it as virtual
    /// media. `None` (the default) leaves every launch byte-identical to a spec
    /// written before this field existed.
    ///
    /// The distinction from [`image`](BootSpec::image) is the whole point.
    /// `ImageSource::Iso` is a spec whose *only* payload is the ISO — a live boot,
    /// nothing to install onto. `medium` is the *install* shape: `image` is the
    /// blank disk that will receive the system, `medium` is the ISO doing the
    /// installing. Read them together through [`medium_path`](BootSpec::medium_path),
    /// which is the single accessor both backends use.
    ///
    /// It is meaningless on [`Kind::Container`] and [`Kind::Exe`] (no
    /// firmware, no drive tray), so [`validate`](BootSpec::validate) rejects it
    /// there rather than let it vanish silently.
    pub medium: Option<String>,
    /// **Which tray the [`medium`](BootSpec::medium) rides in** — see
    /// [`MediumAttach`]. [`MediumAttach::Cdrom`] (the default) renders exactly
    /// what this crate rendered before the field existed.
    pub medium_attach: MediumAttach,
    /// **Which device the firmware boots from** — see [`BootOrder`].
    /// [`BootOrder::Auto`] (the default) emits nothing and changes no existing
    /// launch. [`BootOrder::Disk`] together with a [`medium`](BootSpec::medium) is
    /// refused by [`validate`](BootSpec::validate): that combination is the exact
    /// shape of the lie honesty rule 1 exists to stop.
    pub boot_order: BootOrder,
    /// **KVM: the console line that means "this guest booted OK".**
    ///
    /// Handed to tunnr's `BootSpec::success_marker`, so the boot primitive's own
    /// status flips to booted-OK the moment the line streams past on the serial
    /// console, and [`kvm::KvmBoot::booted_ok`] can read that back. `None` (the
    /// default) leaves tunnr's stock marker in place — byte-identical to every spec
    /// written before this field existed. An appliance that prints
    /// `KORP-APPLIANCE BOOT-OK`, an installer that prints `INSTALLER: INSTALL-OK`
    /// and a live medium that prints `APP-ALIVE` are three different claims, and a
    /// consumer that could not name its own marker had to reach past draupnir to
    /// tunnr to make any of them — which is the layer-skip this field closes.
    /// Only the [`kvm`] backend acts on it.
    pub success_marker: Option<String>,
    /// **KVM: the wall-clock budget for the boot** (tunnr's SIGKILL backstop).
    ///
    /// `None` (the default) keeps tunnr's default (60 s) — byte-identical to before.
    /// A 5-minute install and a 2-minute appliance boot are not the same budget, and
    /// a spec that cannot say so is killed at 60 s by a backstop it never saw. The
    /// operator override `DRAUPNIR_VM_BOOT_TIMEOUT` (seconds) still wins over this
    /// field, so a slow box can be tuned without a rebuild, exactly as before.
    /// Only the [`kvm`] backend acts on it.
    pub boot_timeout: Option<Duration>,
    /// **KVM: the UEFI firmware the guest boots on** — see [`Firmware`].
    ///
    /// [`Firmware::HostDefault`] (the default) names nothing and leaves tunnr's own
    /// stock OVMF discovery in place, so the argv is byte-identical to a spec
    /// written before this field existed. Any other choice resolves a CODE/VARS
    /// pair on this host and hands it to tunnr's `ovmf_code` /
    /// `ovmf_vars_template` seam; an absent firmware is refused by name at
    /// [`to_tunnr_spec`](kvm::KvmBoot::to_tunnr_spec) time, before the guest is
    /// launched.
    ///
    /// It exists because those two tunnr fields have been unreachable from every
    /// layer above tunnr: nothing in draupnir or jera could set them, so no caller
    /// could choose a firmware at all — a Secure Boot boot was expressible only by
    /// reaching past draupnir straight into tunnr, which is the layer-skip this
    /// field closes (the same defect [`success_marker`](BootSpec::success_marker)
    /// closed for the boot verdict).
    pub firmware: Firmware,

    /// **Whether this guest has a serial console** — see [`SerialConsole`].
    /// [`SerialConsole::Stdio`] (the default) is what every KVM boot here has
    /// always had. Only the [`kvm`] backend acts on it.
    pub serial: SerialConsole,

    /// **What this guest's screen is** — see [`Display`]. [`Display::Auto`] (the
    /// default) leaves the backend's own choice alone; [`Display::Framebuffer`]
    /// keeps `-vga std` and drops only the display, which is the only mode in
    /// which an invisible panic is visible. Only the [`kvm`] backend acts on it.
    pub display: Display,

    /// **What this guest's clock reads at boot** — see [`Rtc`]. [`Rtc::Host`] (the
    /// default) is QEMU's own default and renders nothing. Only the [`kvm`]
    /// backend acts on it.
    pub rtc: Rtc,

    /// **The DNS server the user-mode network hands the guest** (`-netdev
    /// user,…,dns=<addr>`). `None` (the default) renders nothing and leaves
    /// slirp's own `.3`, so an existing spec's launch is unchanged.
    ///
    /// `Some("10.0.2.99")` is how behaviour 18 is modelled: slirp serves DNS on
    /// its own `.3` and nothing else, so the guest's queries leave and **no reply
    /// ever arrives** — which is what `systemd-timesyncd` experiences on UpCloud,
    /// and why gunnar-clock syncs from the front over a signed nonce instead.
    /// Only the [`kvm`] backend acts on it.
    pub guest_dns: Option<String>,

    /// **Where the qemu backend binds its TCP forwards on the HOST.** `None`
    /// (the default) renders `hostfwd=tcp::H-:G` exactly as before — every
    /// host interface. `Some("127.0.0.1")` renders `hostfwd=tcp:127.0.0.1:H-:G`,
    /// which is what a rig needs whose guest's ports must not be reachable
    /// from the network it sits on (an appliance whose bootstrap window is open
    /// to anyone reaching 50051). Only the qemu backend acts on it.
    pub forward_bind: Option<String>,

    /// **The chipset** ([`MachineType`]). `Q35` by default — the argv every
    /// existing caller already gets; [`cloud_machine_shape`](BootSpec::cloud_machine_shape)
    /// selects `Pc`. Only the qemu backend acts on it.
    pub machine: MachineType,

    /// **KVM: which MACHINE this guest is** — see [`GuestShape`].
    ///
    /// [`GuestShape::UefiSerial`] (the default) is what every KVM boot draupnir
    /// has ever driven, so an untouched spec's argv is byte-identical.
    /// [`GuestShape::BiosFramebuffer`] is the console-only cloud machine, and it
    /// is refused rather than approximated when the rest of the spec contradicts
    /// it (a success marker, or a named UEFI firmware).
    ///
    /// It exists because the shape was never a field: `kvm.rs` hard-coded
    /// `headless = true` and tunnr pushed `-serial mon:stdio` plus the OVMF
    /// pflash pair unconditionally, so no caller at any layer could ask for the
    /// machine its appliance is actually sold on. Only the [`kvm`] (tunnr)
    /// backend acts on it; draupnir's own `qemu` backend spells the same
    /// machine through [`firmware`](BootSpec::firmware) /
    /// [`serial`](BootSpec::serial) / [`display`](BootSpec::display) (see
    /// [`cloud_machine_shape`](BootSpec::cloud_machine_shape)). On the tunnr
    /// path a [`Firmware::Bios`] must travel WITH
    /// [`GuestShape::BiosFramebuffer`] — the two are refused apart.
    pub guest_shape: GuestShape,
}

impl BootSpec {
    /// **The one writer of a default [`BootSpec`]** — every constructor below is
    /// this plus the two or three fields that make it that kind of boot.
    ///
    /// It exists because the five public constructors were five verbatim copies of
    /// the same 20-line field list (REUSE-law: >10 duplicated lines means share), and
    /// every new field had to be added five times or one constructor silently drifted.
    /// The `medium`/`boot_order` pair added 2026-08-15 is added here **once**, which
    /// is also what makes "a spec that names no medium behaves exactly as before" a
    /// property of the type rather than of five sites agreeing.
    ///
    /// KVM sizing (512 MiB / 2 cores) is applied only to [`Kind::Vm`]: the
    /// container, Redfish and exe paths carry no VM sizing and stay `0`, exactly the
    /// "lean by design" defaults their constructors always produced.
    fn of(name: impl Into<String>, kind: Kind, image: ImageSource) -> Self {
        let (mem_mb, cores) = if kind == Kind::Vm {
            (512, 2)
        } else {
            (0, 0)
        };
        Self {
            name: name.into(),
            kind,
            image,
            mem_mb,
            cores,
            cmdline: String::new(),
            cmd: Vec::new(),
            ports: Vec::new(),
            port_maps: Vec::new(),
            env: BTreeMap::new(),
            bmc: None,
            cloud_init: None,
            net: NetMode::Default,
            cpus: None,
            mem_limit_mb: None,
            security_opt: Vec::new(),
            binds: Vec::new(),
            user: None,
            working_dir: None,
            cap_add: Vec::new(),
            devices: Vec::new(),
            cap_drop: Vec::new(),
            read_only_rootfs: false,
            tmpfs: Vec::new(),
            never_pull: false,
            extra_qemu_args: Vec::new(),
            persist_disk: false,
            medium: None,
            medium_attach: MediumAttach::default(),
            boot_order: BootOrder::Auto,
            success_marker: None,
            boot_timeout: None,
            firmware: Firmware::HostDefault,
            // The machine-shape fields default to the machine draupnir always
            // booted: a serial console, the backend's own display choice, the
            // host's clock and slirp's own DNS. Pinned byte-for-byte by
            // `qemu::tests::the_default_spec_renders_the_argv_draupnir_always_built`.
            serial: SerialConsole::Stdio,
            display: Display::Auto,
            rtc: Rtc::Host,
            guest_dns: None,
            forward_bind: None,
            machine: MachineType::Q35,
            guest_shape: GuestShape::UefiSerial,
        }
    }

    /// A KVM appliance boot from a kernel + rootfs (defaults: 512 MiB, 2 cores).
    pub fn kvm_kernel_rootfs(
        name: impl Into<String>,
        kernel: impl Into<String>,
        rootfs: impl Into<String>,
    ) -> Self {
        Self::of(
            name,
            Kind::Vm,
            ImageSource::KernelRootfs {
                kernel: kernel.into(),
                rootfs: rootfs.into(),
            },
        )
    }

    /// A KVM boot from a bootable **disk image** direct-launched with `kernel`
    /// (defaults: 512 MiB, 2 cores). tunnr attaches the disk as a virtio drive;
    /// the kernel is required because there is no in-image bootloader path.
    pub fn kvm_disk(
        name: impl Into<String>,
        kernel: impl Into<String>,
        disk: impl Into<String>,
    ) -> Self {
        Self::of(
            name,
            Kind::Vm,
            ImageSource::Disk {
                kernel: kernel.into(),
                disk: disk.into(),
            },
        )
    }

    /// A KVM boot from a **self-booting disk image** — a bootloader-in-disk
    /// (firmware) boot with **no `-kernel`** (defaults: 512 MiB, 2 cores). tunnr
    /// attaches the disk as a virtio drive and lets OVMF boot it directly, exactly
    /// as real hardware would; there is no direct-kernel launch.
    pub fn kvm_bootloader_disk(name: impl Into<String>, disk: impl Into<String>) -> Self {
        Self::of(
            name,
            Kind::Vm,
            ImageSource::BootloaderDisk { disk: disk.into() },
        )
    }

    /// A container boot from an OCI image reference (e.g. a redis service).
    pub fn container(name: impl Into<String>, oci_image: impl Into<String>) -> Self {
        Self::of(
            name,
            Kind::Container,
            ImageSource::OciImage(oci_image.into()),
        )
    }

    /// A bare-metal Redfish boot: an ISO served as virtual media to a BMC node.
    ///
    /// The boot order stays [`BootOrder::Auto`] so this constructor is unchanged
    /// from every call written before boot order existed — the Redfish backend has
    /// always set a one-time [`BootTarget::Cd`] override itself. Say
    /// [`BootOrder::Medium`] explicitly (or use [`iso_boot`](BootSpec::iso_boot)
    /// + [`on_metal`](BootSpec::on_metal)) when the spec must *also* be routable to
    /// KVM.
    pub fn redfish_iso(name: impl Into<String>, iso: impl Into<String>, bmc: BmcEndpoint) -> Self {
        Self {
            bmc: Some(bmc),
            ..Self::of(name, Kind::BareMetal, ImageSource::Iso(iso.into()))
        }
    }

    /// **Boot OFF a medium — the backend-agnostic ISO spec.** One spec, routable to
    /// the KVM backend as it stands or to real metal with [`on_metal`](BootSpec::on_metal),
    /// which is honesty rule 2 in a constructor: "it works in the demo" and "it works
    /// on the customer's server" become the same claim about the same value.
    ///
    /// Defaults to [`Kind::Vm`] with [`BootOrder::Medium`] (`-cdrom <iso> -boot d`),
    /// 512 MiB / 2 cores. There is no target disk: this is a *live* boot of the ISO.
    /// For the install shape (ISO **plus** a blank disk to install onto) use
    /// [`kvm_install_from_medium`](BootSpec::kvm_install_from_medium).
    pub fn iso_boot(name: impl Into<String>, iso: impl Into<String>) -> Self {
        Self {
            boot_order: BootOrder::Medium,
            ..Self::of(name, Kind::Vm, ImageSource::Iso(iso.into()))
        }
    }

    /// **Route this spec to real metal** (builder style): flip the backend to
    /// [`Kind::BareMetal`] and attach the BMC, changing nothing else.
    ///
    /// The point is what it does *not* touch — image, medium and boot order carry
    /// over verbatim, so `iso_boot(..)` and `iso_boot(..).on_metal(bmc)` are the same
    /// boot claim aimed at two different machines. That is the `iso-kvm` / `iso-metal`
    /// pair the chain driver keeps as distinct rows: distinct *runs*, one spec.
    pub fn on_metal(mut self, bmc: BmcEndpoint) -> Self {
        self.kind = Kind::BareMetal;
        self.bmc = Some(bmc);
        self
    }

    /// **The INSTALL leg**: boot off `iso` with `target_disk` present and blank, so
    /// the installer has somewhere to install to. KVM renders it as a firmware boot
    /// of a virtio `target_disk` plus `-cdrom <iso> -boot d`.
    ///
    /// [`persist_disk`](BootSpec::persist_disk) is **`true`** here, unlike every other
    /// disk constructor, and that is load-bearing: the default COW guard would send
    /// the installer's writes to a throwaway overlay, so the install would "succeed"
    /// and leave the disk byte-identical to the blank one it started from (measured on
    /// oden 2026-08-03). An install whose writes are discarded is the purest form of
    /// the green we must never accept.
    pub fn kvm_install_from_medium(
        name: impl Into<String>,
        iso: impl Into<String>,
        target_disk: impl Into<String>,
    ) -> Self {
        Self {
            medium: Some(iso.into()),
            boot_order: BootOrder::Medium,
            persist_disk: true,
            ..Self::of(
                name,
                Kind::Vm,
                ImageSource::BootloaderDisk {
                    disk: target_disk.into(),
                },
            )
        }
    }

    /// **The BOOT-INSTALLED leg**: firmware-boot `disk` with **no medium at all**
    /// (`-boot c`) — the second, separate VM lifetime.
    ///
    /// This is deliberately a different constructor rather than a flag on the install
    /// spec, because the two must be two *values*: the driver's
    /// `an_installer_ready_green_is_refused_inside_one_vm_lifetime`
    /// (`edda/crates/nornir-testmatrix/src/chain.rs:1111`) refuses a READY green that
    /// was observed inside the installing machine. [`validate`](BootSpec::validate)
    /// enforces the same thing from below: a [`BootOrder::Disk`] spec that carries a
    /// medium is refused, so "I detached the ISO" cannot be merely claimed.
    ///
    /// `persist_disk` is `true`: the whole subject of this boot is what the previous
    /// lifetime wrote to that disk.
    pub fn kvm_boot_installed_disk(name: impl Into<String>, disk: impl Into<String>) -> Self {
        Self {
            boot_order: BootOrder::Disk,
            persist_disk: true,
            ..Self::of(
                name,
                Kind::Vm,
                ImageSource::BootloaderDisk { disk: disk.into() },
            )
        }
    }

    /// **Run a plain executable** — the `exe` build-thing kind ([`exe::ExeBoot`]).
    /// `path` is the binary stage 1 produced; `args` is its argv[1..].
    pub fn exe<I, S>(name: impl Into<String>, path: impl Into<String>, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        Self::of(
            name,
            Kind::Exe,
            ImageSource::Executable {
                path: path.into(),
                args: args.into_iter().map(Into::into).collect(),
            },
        )
    }

    /// **The medium this spec boots off, wherever it was named** — the single
    /// accessor both the KVM and the Redfish backend read, so the two can never
    /// disagree about what "the medium" is (REUSE-law: one reader, not a twin per
    /// backend).
    ///
    /// Returns the explicit [`medium`](BootSpec::medium) when set, else the
    /// [`image`](BootSpec::image) when it is an [`ImageSource::Iso`] (a live ISO boot
    /// *is* a boot off the medium), else [`None`].
    pub fn medium_path(&self) -> Option<&str> {
        match (&self.medium, &self.image) {
            (Some(m), _) => Some(m.as_str()),
            (None, ImageSource::Iso(iso)) => Some(iso.as_str()),
            _ => None,
        }
    }

    /// Attach a removable boot [`medium`](BootSpec::medium) (builder style) — an ISO
    /// alongside the payload disk. Does **not** set the boot order; pair it with
    /// [`with_boot_order`](BootSpec::with_boot_order) or use
    /// [`kvm_install_from_medium`](BootSpec::kvm_install_from_medium).
    pub fn with_medium(mut self, medium: impl Into<String>) -> Self {
        self.medium = Some(medium.into());
        self
    }

    /// Set the [`medium_attach`](BootSpec::medium_attach) (builder style) — which
    /// face(s) of the medium the guest gets. [`MediumAttach::Both`] is the one an
    /// appliance install needs when its kernel has no ATA.
    pub fn with_medium_attach(mut self, attach: MediumAttach) -> Self {
        self.medium_attach = attach;
        self
    }

    /// Set the [`boot_order`](BootSpec::boot_order) (builder style).
    pub fn with_boot_order(mut self, order: BootOrder) -> Self {
        self.boot_order = order;
        self
    }

    /// The path of whatever this spec boots — the kernel's rootfs, the disk, or
    /// the ISO — for the "fail by name before anything is launched" check.
    pub fn image_path(&self) -> Option<String> {
        match &self.image {
            ImageSource::KernelRootfs { rootfs, .. } => Some(rootfs.clone()),
            ImageSource::Disk { disk, .. } => Some(disk.clone()),
            ImageSource::BootloaderDisk { disk } => Some(disk.clone()),
            ImageSource::Iso(iso) => Some(iso.clone()),
            _ => None,
        }
    }

    /// Every host→guest **TCP** forward this spec publishes, as `(host, guest)`
    /// pairs — the two published forms folded into one list, exactly as the KVM
    /// and container backends already fold them. There is deliberately no UDP
    /// twin: inbound UDP does not arrive on a cloud guest (behaviour 18), and a
    /// rig that forwarded one would be a rig where NTP works.
    pub fn tcp_forwards(&self) -> Vec<(u16, u16)> {
        self.ports
            .iter()
            .map(|&p| (p, p))
            .chain(self.port_maps.iter().map(|pm| (pm.host, pm.container)))
            .collect()
    }

    /// **The machine a cloud hands out, in one word** (builder style).
    ///
    /// SeaBIOS, no serial console, a framebuffer served over `vnc`, an RTC two
    /// hours ahead, slirp's DNS pointed at an address that answers nothing, the
    /// CD first while it is inserted, and writes that survive. The six clauses
    /// travel together on purpose: a guest that got two of them would be a
    /// fourth machine shape nobody measured, and the estate has already paid for
    /// proving things on a machine it was not going to run on.
    ///
    /// The boot order is only set when a medium is present — `order=dc` with an
    /// empty tray is `c` with extra words, and [`validate`](BootSpec::validate)
    /// refuses it.
    pub fn cloud_machine_shape(mut self, vnc: Option<String>) -> Self {
        self.firmware = Firmware::Bios {
            rom: std::path::Path::new(crate::qemu::SEABIOS_ROM)
                .is_file()
                .then(|| crate::qemu::SEABIOS_ROM.to_string()),
        };
        self.serial = SerialConsole::None;
        self.display = Display::Framebuffer { vnc };
        self.rtc = Rtc::SkewedBy {
            seconds: crate::qemu::CLOUD_RTC_SKEW_SECONDS,
        };
        self.guest_dns = Some(crate::qemu::CLOUD_DNS.to_string());
        // i440fx with PIIX IDE, the cdrom on a real IDE channel (ledger L46,
        // L49). A caller that wants q35 sets `machine` after this.
        self.machine = MachineType::Pc;
        if self.medium_path().is_some() {
            self.boot_order = BootOrder::MediumThenDisk;
        }
        // An install that wrote into a throwaway overlay would prove nothing.
        self.persist_disk = true;
        self
    }

    /// Choose the guest's UEFI [`firmware`](BootSpec::firmware) (builder style) —
    /// the call a firmware picker makes, e.g.
    /// `spec.with_firmware(Firmware::SecureBoot)`. The CODE/VARS pair travels as
    /// one choice; see [`Firmware`] for why it cannot be two paths.
    pub fn with_firmware(mut self, firmware: Firmware) -> Self {
        self.firmware = firmware;
        self
    }

    /// Name the **guest machine shape** (builder style) — see [`GuestShape`].
    /// Only the [`kvm`] backend acts on it; [`GuestShape::UefiSerial`] (unset) is
    /// the machine every earlier boot used.
    pub fn with_guest_shape(mut self, shape: GuestShape) -> Self {
        self.guest_shape = shape;
        self
    }

    /// Name the **KVM success marker** (builder style) — see
    /// [`success_marker`](BootSpec::success_marker). Only the [`kvm`] backend acts on
    /// it; `None`/unset leaves tunnr's stock marker.
    pub fn with_success_marker(mut self, marker: impl Into<String>) -> Self {
        self.success_marker = Some(marker.into());
        self
    }

    /// Set the **KVM boot budget** (builder style) — see
    /// [`boot_timeout`](BootSpec::boot_timeout). Only the [`kvm`] backend acts on it;
    /// unset keeps tunnr's default.
    pub fn with_boot_timeout(mut self, budget: Duration) -> Self {
        self.boot_timeout = Some(budget);
        self
    }

    /// Set an environment variable (builder style).
    pub fn with_env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
        self.env.insert(key.into(), val.into());
        self
    }

    /// Set the container command / entrypoint override (builder style). Only the
    /// [`container`] backend acts on it.
    pub fn with_cmd<I, S>(mut self, cmd: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.cmd = cmd.into_iter().map(Into::into).collect();
        self
    }

    /// Publish a container port (builder style), bound to the same host port. Only
    /// the [`container`] backend acts on it.
    pub fn with_port(mut self, port: u16) -> Self {
        self.ports.push(port);
        self
    }

    /// Publish a **distinct** `host:container` mapping (builder style) — the host
    /// port may differ from the in-container port (a per-zone offset onto a fixed
    /// service port; see [`port_maps`](BootSpec::port_maps) and [`PortMap`]). Only
    /// the [`container`] backend acts on it.
    pub fn with_port_map(mut self, host: u16, container: u16) -> Self {
        self.port_maps.push(PortMap::new(host, container));
        self
    }

    /// Attach cloud-init NoCloud provisioning (builder style). Only the KVM backend
    /// acts on it — it authors a seed image the guest reads at first boot.
    pub fn with_cloud_init(mut self, ci: CloudInit) -> Self {
        self.cloud_init = Some(ci);
        self
    }

    /// Set the container **network mode** (builder style). Only the [`container`]
    /// backend acts on it; [`NetMode::None`] is the airgap `--network none` case.
    /// [`NetMode::Default`] (unchanged) leaves the create body byte-identical.
    pub fn with_net(mut self, net: NetMode) -> Self {
        self.net = net;
        self
    }

    /// Set the **KVM extra QEMU args** (builder style) — see
    /// [`extra_qemu_args`](BootSpec::extra_qemu_args). Only the [`kvm`] backend acts
    /// on it (appended verbatim to tunnr's argv); empty leaves the argv byte-identical.
    pub fn with_extra_qemu_args<I, S>(mut self, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.extra_qemu_args = args.into_iter().map(Into::into).collect();
        self
    }

    /// Set the container **CPU quota** (podman `--cpus`, builder style) — see
    /// [`cpus`](BootSpec::cpus). `None` (unset) leaves the container **unconstrained**
    /// (all host cores); `Some(n)` caps it at `n` cores. Only the [`container`]
    /// backend acts on it.
    pub fn with_cpus(mut self, cpus: f64) -> Self {
        self.cpus = Some(cpus);
        self
    }

    /// Set the container **memory limit** in MiB (podman `--memory`, builder style)
    /// — see [`mem_limit_mb`](BootSpec::mem_limit_mb). `Some(m)` caps it at `m` MiB;
    /// unset leaves it unconstrained. Only the [`container`] backend acts on it.
    pub fn with_mem_limit_mb(mut self, mem_mb: u32) -> Self {
        self.mem_limit_mb = Some(mem_mb);
        self
    }

    /// Add one container **security option** (podman `--security-opt`, builder
    /// style) — see [`security_opt`](BootSpec::security_opt). Appends rather than
    /// replaces, because `--security-opt` is a repeatable flag and a setter that
    /// silently dropped the previous value would be the quiet kind of wrong.
    pub fn with_security_opt(mut self, opt: impl Into<String>) -> Self {
        self.security_opt.push(opt.into());
        self
    }

    /// Add one container bind mount, `host:container[:opts]` (`-v` semantics; `host`
    /// may be a named volume). See [`BootSpec::binds`].
    pub fn with_bind(mut self, bind: impl Into<String>) -> Self {
        self.binds.push(bind.into());
        self
    }

    /// Run the container's process as `user` (podman `--user`: `uid`, `uid:gid`
    /// or a name) — see [`BootSpec::user`] for why a bind-mounting caller has to
    /// be able to say this. Replaces rather than appends: `--user` is a single
    /// value, unlike `--security-opt`.
    pub fn with_user(mut self, user: impl Into<String>) -> Self {
        self.user = Some(user.into());
        self
    }

    /// Drop `cap` (podman `--cap-drop`; `ALL` drops every capability). Appends.
    pub fn with_cap_drop(mut self, cap: impl Into<String>) -> Self {
        self.cap_drop.push(cap.into());
        self
    }

    /// A read-only root filesystem (podman `--read-only`).
    pub fn with_read_only_rootfs(mut self) -> Self {
        self.read_only_rootfs = true;
        self
    }

    /// A tmpfs at `path` with podman `--tmpfs` options (`rw,size=1024m,mode=1777`).
    pub fn with_tmpfs(mut self, path: impl Into<String>, options: impl Into<String>) -> Self {
        self.tmpfs.push((path.into(), options.into()));
        self
    }

    /// The image must already be local; it is never pulled (podman `--pull=never`).
    pub fn never_pulled(mut self) -> Self {
        self.never_pull = true;
        self
    }

    /// Start the container's process in `dir` (podman `--workdir`) — see
    /// [`BootSpec::working_dir`]. Replaces; there is only one cwd.
    pub fn with_working_dir(mut self, dir: impl Into<String>) -> Self {
        self.working_dir = Some(dir.into());
        self
    }

    /// Reject an internally inconsistent spec **before** touching a backend:
    /// the image must suit the backend, a Redfish spec must carry a BMC (and no
    /// other backend may), every required payload path/ref is non-empty, the
    /// instance `name` is non-empty, a Redfish `bmc` carries non-empty
    /// host/username/system-id, a KVM spec is sized (`mem_mb`/`cores` > 0), the
    /// container-only `cmd`/`ports` are not set on a non-container backend, and
    /// every published container `port` is non-zero and listed at most once.
    /// This is pure and unit-tested.
    pub fn validate(&self) -> Result<()> {
        if let Some((p, _)) = self.tmpfs.iter().find(|(p, _)| !p.starts_with('/')) {
            return Err(Error::Spec(format!("a tmpfs mount needs an absolute path, not {p:?}")));
        }
        // Reject an empty/all-whitespace required string up front so a mistyped
        // spec fails here with a clear message, not verbatim-passed to a backend
        // that only fails deep in a runtime call (tunnr's direct-kernel launch
        // needs a real `-kernel`; the OCI daemon a real image ref; Redfish a real
        // ISO and a reachable BMC). `require` trims, so whitespace is caught too.
        let require = |what: &str, val: &str| -> Result<()> {
            if val.trim().is_empty() {
                Err(Error::Spec(format!(
                    "a {:?} boot needs a non-empty {what}",
                    self.kind
                )))
            } else {
                Ok(())
            }
        };
        // The instance name is the `Machine` id prefix and the fleet-member prefix
        // (`plan_fleet` mints `"{name}-{i}"`); a blank one yields `"-1"`/`"-2"`
        // members and a leading-dash id, so reject it on every backend.
        require("instance name", &self.name)?;
        if !self.image.suits(self.kind) {
            return Err(Error::Spec(format!(
                "{:?} image is not bootable by the {:?} backend",
                self.image, self.kind
            )));
        }
        match &self.image {
            ImageSource::KernelRootfs { kernel, rootfs } => {
                require("kernel path", kernel)?;
                require("rootfs path", rootfs)?;
            }
            ImageSource::Disk { kernel, disk } => {
                require("kernel path", kernel)?;
                require("disk path", disk)?;
            }
            // A bootloader-in-disk boot is kernel-less by design (firmware boots
            // the disk), so require only the disk path — NOT a kernel.
            ImageSource::BootloaderDisk { disk } => require("disk path", disk)?,
            ImageSource::OciImage(image) => require("OCI image reference", image)?,
            ImageSource::Iso(iso) => require("ISO path", iso)?,
            ImageSource::Executable { path, .. } => require("executable path", path)?,
        }
        // ── the boot-off-a-medium seam ──────────────────────────────────────────
        // A medium is a firmware/drive-tray concept: only a machine (KVM guest or
        // Redfish node) has one. On a container or an exe it would vanish without a
        // trace, exactly like the container-only knobs rejected below.
        if self.medium.is_some()
            && !matches!(self.kind, Kind::Vm | Kind::BareMetal)
        {
            return Err(Error::Spec(format!(
                "a {:?} boot has no drive tray (medium is Kvm/Redfish-only)",
                self.kind
            )));
        }
        if let Some(medium) = &self.medium {
            require("medium (ISO) path", medium)?;
        }
        // UEFI pflash is a QEMU-guest concept: a container has no firmware at all,
        // and a Redfish node's firmware is the physical board's, not ours to pick.
        // Naming one anywhere else would be silently dropped — the same disappearing
        // knob `medium` is rejected for above.
        if self.firmware != Firmware::HostDefault && self.kind != Kind::Vm {
            return Err(Error::Spec(format!(
                "a {:?} boot has no UEFI pflash (firmware is Kvm-only; got `{}`)",
                self.kind,
                self.firmware.label()
            )));
        }
        // A guest SHAPE is a QEMU-machine concept too: a container has no
        // firmware, no serial port and no framebuffer, and a Redfish node's
        // machine is the physical board. Naming a shape anywhere else would be
        // silently dropped — the same disappearing knob `firmware` is rejected
        // for above.
        if self.guest_shape != GuestShape::UefiSerial && self.kind != Kind::Vm {
            return Err(Error::Spec(format!(
                "REFUSED [shape-not-supported] a {:?} boot has no machine shape to choose \
                 (guest_shape is Vm-only; got `{:?}`)",
                self.kind, self.guest_shape
            )));
        }
        // A boot order is likewise a machine concept.
        if self.boot_order != BootOrder::Auto
            && !matches!(self.kind, Kind::Vm | Kind::BareMetal)
        {
            return Err(Error::Spec(format!(
                "a {:?} boot has no firmware boot order (boot_order is Kvm/Redfish-only)",
                self.kind
            )));
        }
        // "Boot off the medium" with no medium anywhere is a spec that cannot do what
        // it says. Named by field so the failure is actionable, not a generic reject.
        // `order=dc` with nothing in the tray is `c` with extra words: the CD leg
        // cannot happen, so a spec that says "the CD is first" would be claiming a
        // machine shape it has not got. Same refusal, same reason, as `Medium`.
        if self.boot_order == BootOrder::MediumThenDisk && self.medium_path().is_none() {
            return Err(Error::Spec(
                "boot_order = MediumThenDisk (-boot order=dc) but no medium: the CD-first                  leg cannot happen with an empty tray — set `medium`, or say                  BootOrder::Disk if the tray is meant to be empty"
                    .into(),
            ));
        }
        if self.boot_order == BootOrder::DiskThenMedium && self.medium_path().is_none() {
            return Err(Error::Spec(
                "boot_order = DiskThenMedium (-boot order=cd) but no medium: the fall-through to the CD \
                 cannot happen with an empty tray — set `medium`, or say BootOrder::Disk"
                    .into(),
            ));
        }
        if self.boot_order == BootOrder::Medium && self.medium_path().is_none() {
            return Err(Error::Spec(
                "boot_order = Medium but no medium: set `medium` (an ISO alongside the \
                 payload disk) or make the image an ImageSource::Iso"
                    .into(),
            ));
        }
        // HONESTY RULE 1, enforced by construction. "Boot the installed disk" and
        // "the installer medium is still in the tray" cannot both be true: with the
        // ISO present, a machine that fails to boot its freshly-installed disk falls
        // through to the installer and comes up looking exactly like a success. The
        // driver refuses a READY green observed inside one VM lifetime
        // (edda/crates/nornir-testmatrix/src/chain.rs:1111); this refuses the SPEC
        // that would let it be claimed in the first place — a caller must build a
        // second, medium-less spec, which is a second boot.
        if self.boot_order == BootOrder::Disk && self.medium_path().is_some() {
            return Err(Error::Spec(format!(
                "boot_order = Disk but the medium `{}` is still attached: booting the \
                 INSTALLED system means the medium is ABSENT — build a separate spec \
                 (BootSpec::kvm_boot_installed_disk) for the second VM lifetime",
                self.medium_path().unwrap_or_default()
            )));
        }
        // A KVM VM is booted with the spec's RAM/vCPU verbatim (the tunnr adapter
        // sets `mem_mb`/`cores` from these); 0 would launch a 0-RAM/0-CPU guest
        // that dies at boot, so a KVM spec must be sized. Container/Redfish carry
        // no VM sizing (both default to 0 by design) and are exempt.
        if self.kind == Kind::Vm {
            if self.mem_mb == 0 {
                return Err(Error::Spec("a Kvm boot needs mem_mb > 0 (VM RAM)".into()));
            }
            if self.cores == 0 {
                return Err(Error::Spec("a Kvm boot needs cores > 0 (vCPUs)".into()));
            }
        }
        // `cmd` (entrypoint override) and `ports` (published ports) are
        // container-only knobs: only the container backend runs the cmd and
        // publishes the ports — KVM/Redfish silently ignore both. Setting either
        // on a non-container backend is a misconfiguration that would vanish
        // without a trace (the caller asked to run a command / expose a port that
        // never happens), so reject it up front — parity with a `bmc` on a
        // non-Redfish backend (rejected below) and the same shape as the empty /
        // zero / duplicate required-field checks.
        if self.kind != Kind::Container {
            if !self.cmd.is_empty() {
                return Err(Error::Spec(format!(
                    "a {:?} boot takes no container cmd (cmd is container-only)",
                    self.kind
                )));
            }
            // Ports are container vocabulary EXCEPT on KVM, for the same reason
            // `NetMode::None` is (below): tunnr's user-mode NIC has modelled
            // `(host, guest)` TCP forwards since it was written
            // (`NetConfig::User { hostfwd }`, rendered `hostfwd=tcp::<h>-:<g>`),
            // and [`PortMap`] is already exactly that pair. Until 2026-09-05 this
            // rejection was the reason a caller who needed to REACH a service in
            // the guest could not go through draupnir at all: gunnar's KVM install
            // arm codes straight against `tunnr_vm::boot_test` and says so in its
            // own header, which is a second KVM path in the constellation — the
            // thing this crate exists to prevent. A forward that is dropped in
            // silence is worse than one that is refused, and it was neither: it
            // was refused on the container's behalf on a backend that can serve it.
            //
            // Redfish provisions metal and Exe runs a binary; neither publishes a
            // port, so both still reject.
            let ports_ok = (self.ports.is_empty() && self.port_maps.is_empty())
                || self.kind == Kind::Vm;
            if !ports_ok {
                return Err(Error::Spec(format!(
                    "a {:?} boot publishes no ports (ports are container-only, save a \
                     KVM boot — tunnr's user-mode NIC forwards host→guest)",
                    self.kind
                )));
            }
            // A forward needs a NIC to arrive on. `NetMode::None` is `-nic none`
            // on the KVM path, so the two together are a contradiction the caller
            // has to resolve — and resolving it here (silently dropping one) is
            // how a test that believes it can reach the guest hangs on a closed
            // port instead of failing with a reason.
            if self.kind == Kind::Vm
                && self.net == NetMode::None
                && (!self.ports.is_empty() || !self.port_maps.is_empty())
            {
                return Err(Error::Spec(
                    "a KVM boot cannot both cut the guest off (NetMode::None → `-nic none`) \
                     and forward a port to it — drop the port maps or the airgap"
                        .into(),
                ));
            }
            // `net` is container vocabulary EXCEPT the airgap: the kvm backend
            // honours `NetMode::None` as tunnr's `-nic none` (no egress NIC), the
            // exact KVM twin of the container's `--network none`. Every OTHER
            // non-default mode (Host/Bridge) is still container-only — on KVM it
            // would vanish without a trace, so those stay rejected. Redfish
            // provisions metal and honours none of them.
            let net_ok = self.net.is_default()
                || (self.kind == Kind::Vm && self.net == NetMode::None);
            if !net_ok {
                return Err(Error::Spec(format!(
                    "a {:?} boot takes no container network mode (net is container-only, \
                     save `NetMode::None` on Kvm — the airgap)",
                    self.kind
                )));
            }
            // `cpus` (podman `--cpus`) and `mem_limit_mb` (podman `--memory`) are
            // container resource knobs the OCI backend threads onto `HostConfig`;
            // KVM sizing is the separate `mem_mb`/`cores`, and Redfish provisions the
            // metal itself — so a container CPU/memory cap on a non-container backend
            // is a misconfiguration that would vanish without a trace. Reject it up
            // front (parity with cmd/ports/net above).
            if self.cpus.is_some() {
                return Err(Error::Spec(format!(
                    "a {:?} boot takes no container cpus quota (cpus is container-only)",
                    self.kind
                )));
            }
            if self.mem_limit_mb.is_some() {
                return Err(Error::Spec(format!(
                    "a {:?} boot takes no container memory limit (mem_limit_mb is container-only)",
                    self.kind
                )));
            }
            // `user` and `working_dir` are the container process's identity and
            // cwd — knobs only the OCI create body carries. A KVM guest runs its
            // own init as its own root and a Redfish node is a board; setting
            // either there would vanish without a trace, which is the same
            // disappearing-knob failure cmd/ports/net/caps are refused for above.
            if self.user.is_some() {
                return Err(Error::Spec(format!(
                    "a {:?} boot takes no container user (user is container-only)",
                    self.kind
                )));
            }
            if self.working_dir.is_some() {
                return Err(Error::Spec(format!(
                    "a {:?} boot takes no container working directory (working_dir is container-only)",
                    self.kind
                )));
            }
        }
        // A named-but-blank `user`/`working_dir` is not a choice, it is a typo:
        // the daemon would take `""` as "unset" and the caller would believe the
        // process runs as somebody, or starts somewhere, that it does not.
        if let Some(u) = &self.user {
            require("container user (uid, uid:gid or a name)", u)?;
        }
        if let Some(w) = &self.working_dir {
            require("container working directory", w)?;
        }
        // A container CPU/memory cap, when set, must be positive: `--cpus 0` /
        // `--memory 0` are not a real cap (they would either error at the daemon or
        // mean "unlimited", which is what `None` already expresses). Reject a
        // non-positive value here rather than pass a nonsense limit to the engine.
        if let Some(c) = self.cpus {
            if !c.is_finite() || c <= 0.0 {
                return Err(Error::Spec(
                    "a container cpus quota must be a finite value > 0 (use None for all host cores)".into(),
                ));
            }
        }
        if self.mem_limit_mb == Some(0) {
            return Err(Error::Spec(
                "a container memory limit must be > 0 MiB (use None for unconstrained)".into(),
            ));
        }
        // Published container ports are exposed and host-bound verbatim by the
        // container backend (`ports`: `{p}/tcp` → host_port `{p}`; `port_maps`:
        // `{container}/tcp` → host_port `{host}`). Port `0` is never a real
        // published port (it would expose `0/tcp` / bind host port 0, which is
        // not what any caller means), and the same **host** port bound twice —
        // whether from `ports`, `port_maps`, or a mix — is a self-colliding host
        // binding, so reject both up front (parity with the other required-field
        // checks). By the guard above, a non-empty ports set here is necessarily
        // a container spec. Two different host ports mapping to the same
        // *container* port is fine (that is exactly the multi-zone case).
        if !self.ports.is_empty() || !self.port_maps.is_empty() {
            let mut seen_host = std::collections::BTreeSet::new();
            // `ports` (the host==container single-port form).
            for &p in &self.ports {
                if p == 0 {
                    return Err(Error::Spec("a published container port must be > 0".into()));
                }
                if !seen_host.insert(p) {
                    return Err(Error::Spec(format!(
                        "published container port {p} is listed twice"
                    )));
                }
            }
            // `port_maps` (the distinct host:container form): both ends must be a
            // real port, and the host port must not collide with any already
            // bound (from `ports` or an earlier map).
            for pm in &self.port_maps {
                if pm.host == 0 || pm.container == 0 {
                    return Err(Error::Spec(
                        "a published container port map needs host > 0 and container > 0".into(),
                    ));
                }
                if !seen_host.insert(pm.host) {
                    return Err(Error::Spec(format!(
                        "published container host port {} is bound twice",
                        pm.host
                    )));
                }
            }
        }
        match (self.kind, &self.bmc) {
            (Kind::BareMetal, None) => {
                Err(Error::Spec("Redfish boot needs a BMC endpoint".into()))
            }
            (Kind::BareMetal, Some(bmc)) => {
                // The BMC fields are woven into the Redfish REST URLs the backend
                // drives; a blank host/username/system-id yields a malformed
                // request that only fails on the wire, so require them here too
                // (parity with the non-empty ISO path check above).
                require("BMC host", &bmc.host)?;
                require("BMC username", &bmc.username)?;
                require("BMC system id", &bmc.system_id)?;
                Ok(())
            }
            (_, Some(_)) => Err(Error::Spec(
                "only the Redfish backend takes a BMC endpoint".into(),
            )),
            (_, None) => Ok(()),
        }
    }
}

// ---------------------------------------------------------------------------
// What a wait actually SAW — the shared stage-2 observation type.
// ---------------------------------------------------------------------------

/// **What was actually observed** while waiting on a running instance — the return
/// of [`exe::ExeBoot::await_marker`] and of [`kvm::KvmBoot::await_serial_marker`].
///
/// Note what is missing: there is no `Ok`/`Fail`, no "ready". Every one of these
/// outcomes can be the right answer depending on what the caller asked, and a type
/// that guessed would be inventing the verdict the caller exists to make. It carries
/// the evidence instead.
///
/// It lives at the crate root, not inside a backend, because it is the **one**
/// stage-2 observation shape: a process's captured stdout/stderr and a VM's serial
/// console are the same question ("did the named line appear before the deadline?")
/// asked of two different streams, and a per-backend copy would be two definitions
/// of "seen" that drift.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Seen {
    /// The named marker appeared in the captured output. Carries the **whole line**
    /// it appeared on — the applied output, not the fact that a boolean flipped.
    Marker {
        /// The full output line containing the marker.
        line: String,
        /// How long after the wait started it appeared.
        after: Duration,
    },
    /// The deadline elapsed with the process **still running** and the marker not
    /// seen. This is not automatically a failure: for a server, still-running is the
    /// healthy state, and the caller may well have been waiting to confirm exactly
    /// that. Carries the tail of what was captured so the caller can say *why*.
    StillRunning {
        /// The last lines captured, so a report can quote real output.
        tail: String,
        /// The budget that elapsed.
        waited: Duration,
    },
    /// The process **terminated** before the marker appeared. For a server this is
    /// the real failure, and the exit status plus tail is the evidence.
    Exited {
        /// Exit code, or `None` if it was killed by a signal.
        code: Option<i32>,
        /// The last lines captured.
        tail: String,
        /// How long it ran.
        after: Duration,
    },
}

impl Seen {
    /// Whether the named marker was observed — the one question with a single
    /// honest boolean answer.
    pub fn saw_marker(&self) -> bool {
        matches!(self, Seen::Marker { .. })
    }

    /// A one-line, quotable summary carrying the applied output (the matched line,
    /// or the tail that was captured instead) — what a matrix cell's `detail` should
    /// say so a red names what happened rather than that something did not.
    pub fn detail(&self) -> String {
        match self {
            Seen::Marker { line, after } => {
                format!("marker seen after {after:?}: {}", line.trim())
            }
            Seen::StillRunning { tail, waited } => format!(
                "still running after {waited:?}, marker NOT seen; last output: {}",
                one_line(tail)
            ),
            Seen::Exited { code, tail, after } => format!(
                "process EXITED (code {code:?}) after {after:?} without the marker; \
                 last output: {}",
                one_line(tail)
            ),
        }
    }
}

/// Squash a captured tail onto one line so it can live in a matrix cell.
fn one_line(s: &str) -> String {
    let joined = s
        .lines()
        .rev()
        .take(3)
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect::<Vec<_>>()
        .join(" ⏎ ");
    if joined.trim().is_empty() {
        "<no output>".into()
    } else {
        joined
    }
}

/// A booted (or booting) instance handle — what a [`Boot::boot`] returns and
/// what [`Lifecycle`] acts on.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Machine {
    /// Backend-scoped instance id (VM handle / container id / Redfish system id).
    pub id: String,
    /// The [`BootSpec::name`] this was fired up from.
    pub spec_name: String,
    /// What it is.
    pub kind: Kind,
    /// Last-observed power state.
    pub power: PowerState,
}

impl Machine {
    /// Record a freshly fired-up instance (power state assumed `On`).
    pub fn started(id: impl Into<String>, spec: &BootSpec) -> Self {
        Self {
            id: id.into(),
            spec_name: spec.name.clone(),
            kind: spec.kind,
            power: PowerState::On,
        }
    }
}

/// **Fire up** an instance from a [`BootSpec`]. One trait, three implementations
/// ([`kvm::KvmBoot`], [`container::ContainerBoot`], [`redfish::RedfishBoot`]).
pub trait Boot {
    /// Boot the instance described by `spec`, returning its live [`Machine`].
    fn boot(&self, spec: &BootSpec) -> Result<Machine>;
}

/// Drive an instance's **power lifecycle** after it is fired up.
pub trait Lifecycle {
    /// Power the instance on.
    fn power_on(&self, machine: &Machine) -> Result<()>;
    /// Power the instance off.
    fn power_off(&self, machine: &Machine) -> Result<()>;
    /// Observe the instance's current power state.
    fn status(&self, machine: &Machine) -> Result<PowerState>;
}

/// **Redfish virtual-media + boot-override** control — the out-of-band steps that
/// make a bare-metal node boot our ISO. Only the [`redfish::RedfishBoot`] backend
/// implements it; the KVM/container backends have no BMC.
pub trait VirtualMedia {
    /// Attach `iso` to the node as Redfish virtual media (CD/DVD).
    fn insert_media(&self, node: &BmcEndpoint, iso: &str) -> Result<()>;
    /// Detach any virtual media from the node.
    fn eject_media(&self, node: &BmcEndpoint) -> Result<()>;
    /// Set the node's **one-time** boot override to `target`.
    fn set_boot_override(&self, node: &BmcEndpoint, target: BootTarget) -> Result<()>;
}

/// **The unifying entry point** — fire up one instance from a [`BootSpec`] across
/// *whichever* backend is handed in. It [`validate`](BootSpec::validate)s the spec
/// first (so a mismatched image/BMC is rejected before any backend is touched),
/// then delegates to the backend's [`Boot::boot`]. The same `spec` boots the same
/// image on a [`kvm::KvmBoot`], a [`container::ContainerBoot`], or a
/// [`redfish::RedfishBoot`] — one call, three backends.
pub fn boot(spec: &BootSpec, backend: &dyn Boot) -> Result<Machine> {
    spec.validate()?;
    let outcome = backend.boot(spec);
    // Record the boot verdict as a functional-status row: GREEN when the instance
    // came up, RED when the backend failed to fire it — the boot surface nornir's
    // matrix reads back (a failed boot must be visible, never swallowed).
    functional_status(
        "draupnir/boot",
        "boot",
        outcome.is_ok(),
        &match &outcome {
            Ok(m) => format!("instance `{}` booted", m.id),
            Err(e) => format!("instance `{}` failed to boot: {e}", spec.name),
        },
    );
    outcome
}

/// **The ring drips eight copies** — fan one [`BootSpec`] out into `n` identical
/// specs, each with a distinct `"{name}-{i}"` name (1-based), for booting a
/// fleet of identical machines from one image/ISO.
///
/// Pure bookkeeping: it plans the fleet; the caller boots each member through the
/// backend. Unit-tested.
pub fn plan_fleet(spec: &BootSpec, n: usize) -> Vec<BootSpec> {
    (1..=n)
        .map(|i| {
            let mut member = spec.clone();
            member.name = format!("{}-{i}", spec.name);
            member
        })
        .collect()
}

/// **Drip a fleet from one image** — [`plan_fleet`] the spec into `n` members and
/// [`boot`] each through `backend`, returning a per-member result (a partial fleet
/// is observable: some members may boot while a later one errors).
pub fn boot_fleet(spec: &BootSpec, n: usize, backend: &dyn Boot) -> Vec<Result<Machine>> {
    plan_fleet(spec, n)
        .iter()
        .map(|member| boot(member, backend))
        .collect()
}

// ---------------------------------------------------------------------------
// Power-state readback — confirm a booted instance actually reached a state.
// ---------------------------------------------------------------------------

/// Knobs for [`await_power_state`] / [`boot_and_await`]: how long to wait for the
/// instance to reach the target power state and how often to poll it. [`Default`]
/// waits **indefinitely** (parity with [`container::RunOptions`]) and polls every
/// 200 ms — a bounded budget ([`WaitOptions::bounded`]) is recommended for a boot
/// readback so a node that never comes up is a timeout, not a hang.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WaitOptions {
    /// Overall budget before giving up. `None` = wait forever for the state.
    /// `Some(d)` returns an [`Error::Backend`] if the state is not reached within `d`.
    pub timeout: Option<std::time::Duration>,
    /// How often [`Lifecycle::status`] is polled while waiting.
    pub poll_interval: std::time::Duration,
}

impl Default for WaitOptions {
    fn default() -> Self {
        Self {
            timeout: None,
            poll_interval: std::time::Duration::from_millis(200),
        }
    }
}

impl WaitOptions {
    /// Wait indefinitely, polling on `poll_interval`.
    pub fn poll_every(poll_interval: std::time::Duration) -> Self {
        Self {
            timeout: None,
            poll_interval,
        }
    }

    /// Cap the wait at `timeout`, polling on `poll_interval`.
    pub fn bounded(timeout: std::time::Duration, poll_interval: std::time::Duration) -> Self {
        Self {
            timeout: Some(timeout),
            poll_interval,
        }
    }
}

/// **Confirm an instance reached a power state** — poll a [`Lifecycle`]'s
/// [`status`](Lifecycle::status) until it reports `want`, returning `Ok(())` the
/// moment it does. This is the cross-backend **boot-status readback** seam: a
/// [`Boot::boot`] fires an instance up but returns before it has actually powered
/// on (a KVM guest is still booting, a Redfish node is still POSTing, a container is
/// still being scheduled), so a consumer (jera / Skidbladnir) that needs to *know*
/// the instance is up polls this — the power-lifecycle analogue of the container
/// [`run_to_completion`](container::run_to_completion) drive loop, but written
/// against the plain [`Lifecycle`] trait so it drives **any** backend (KVM,
/// container, Redfish) and a mock in a unit test with no live instance.
///
/// A live backend's own `status` error propagates as `Err` (the readback failed).
/// A `want` of [`PowerState::Unknown`] is a nonsensical target (`Unknown` means "not
/// observed") and is rejected as an [`Error::Spec`] before polling. With a bounded
/// [`WaitOptions`] a state never reached is an [`Error::Backend`] timeout carrying
/// the last-observed state; with the default (unbounded) options it waits forever.
pub fn await_power_state<L: Lifecycle>(
    lifecycle: &L,
    machine: &Machine,
    want: PowerState,
    opts: &WaitOptions,
) -> Result<()> {
    if want == PowerState::Unknown {
        return Err(Error::Spec(
            "cannot await PowerState::Unknown (it means \"not observed\")".into(),
        ));
    }
    let deadline = opts.timeout.map(|t| std::time::Instant::now() + t);
    loop {
        let observed = lifecycle.status(machine)?;
        if observed == want {
            functional_status(
                "draupnir/lifecycle",
                "await_power_state",
                true,
                &format!("instance {} reached {want:?}", machine.id),
            );
            return Ok(());
        }
        if let Some(dl) = deadline {
            if std::time::Instant::now() >= dl {
                functional_status(
                    "draupnir/lifecycle",
                    "await_power_state",
                    false,
                    &format!("instance {} never reached {want:?}", machine.id),
                );
                return Err(Error::Backend(format!(
                    "instance `{}` did not reach {want:?} within {:?} (last observed {observed:?})",
                    machine.id,
                    opts.timeout.unwrap()
                )));
            }
        }
        std::thread::sleep(opts.poll_interval);
    }
}

/// **Boot an instance and confirm it is up** — the one-call provision seam jera /
/// Skidbladnir want: [`validate`](BootSpec::validate) + [`boot`] the `spec` on
/// `backend`, then [`await_power_state`] it to [`PowerState::On`], returning the
/// live [`Machine`] only once it has actually powered on. A validation or boot
/// failure short-circuits before any wait (the backend is never touched on an
/// invalid spec — [`boot`] enforces that); a boot that never comes up within a
/// bounded [`WaitOptions`] is a timeout [`Error::Backend`]. Written against
/// `Boot + Lifecycle` so it drives any backend and a mock alike.
pub fn boot_and_await<B>(backend: &B, spec: &BootSpec, opts: &WaitOptions) -> Result<Machine>
where
    B: Boot + Lifecycle,
{
    let machine = boot(spec, backend)?;
    await_power_state(backend, &machine, PowerState::On, opts)?;
    Ok(machine)
}

/// **Roll a booted instance back** — tear down an instance a boot brought up (or a
/// half-finished provision) by powering it off through the backend's [`Lifecycle`],
/// recording the teardown verdict as a functional-status row. This is the *undo*
/// seam jera / Skidbladnir call when a provision must be reverted: a boot that never
/// reached [`PowerState::On`], a [`wait_ready`](container::ContainerBoot::wait_ready)
/// that timed out, or a fleet member being reclaimed. GREEN when the backend confirms
/// the power-off; a live backend failure to power it down is a **RED** row AND a
/// propagated [`Err`] — a rollback that could not complete must be visible, never
/// swallowed. Written against the plain [`Lifecycle`] trait so it rolls back **any**
/// backend (KVM / container / Redfish) and a mock in a unit test alike — one shared
/// teardown, never a per-backend twin.
pub fn rollback<L: Lifecycle>(lifecycle: &L, machine: &Machine) -> Result<()> {
    let outcome = lifecycle.power_off(machine);
    functional_status(
        "draupnir/lifecycle",
        "rollback",
        outcome.is_ok(),
        &match &outcome {
            Ok(()) => format!("instance `{}` rolled back (powered off)", machine.id),
            Err(e) => format!("instance `{}` rollback failed: {e}", machine.id),
        },
    );
    outcome
}

// ---------------------------------------------------------------------------
// Fleet-level boot-status readback — boot N members and roll up who is ready.
// ---------------------------------------------------------------------------

/// The boot-readback verdict for **one** fleet member in a
/// [`boot_fleet_and_await`] rollup — the three ways a member can land.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MemberOutcome {
    /// Booted **and confirmed** at [`PowerState::On`] within the wait budget — the
    /// member is ready. Carries the live [`Machine`] handle.
    Up(Machine),
    /// The backend **did not get the member to [`PowerState::On`]** within the
    /// bounded budget: the readback timed out (booted but never powered on) or the
    /// backend failed / went unreachable while bringing it up (an [`Error::Backend`]
    /// — the fleet's "this node did not come up in time" bucket). Carries the
    /// failure detail. Counts as **failed**, never blocks the rest of the rollup.
    Timeout(String),
    /// The member could **not even be launched**: its spec was invalid or the
    /// backend is not available ([`Error::Spec`] / [`Error::Unsupported`]) — a hard
    /// misconfiguration that waiting can never fix. Carries the failure detail.
    /// Counts as **failed**.
    Error(String),
}

/// One member's line in a [`FleetReadback`]: its name paired with its verdict.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemberReadback {
    /// The fleet member's name (`"{spec.name}-{i}"`, as minted by [`plan_fleet`]).
    pub name: String,
    /// Its boot-readback [`outcome`](MemberOutcome).
    pub outcome: MemberOutcome,
}

/// **The fleet-level boot-readback rollup** returned by [`boot_fleet_and_await`]:
/// one verdict per member (in fleet order `node-1`, `node-2`, …) plus the aggregate
/// ready/failed tallies a dispatcher (jera) reads to decide whether the fleet is up.
///
/// It is *infallible by construction* — a dead or misconfigured member is a
/// per-member [`MemberOutcome::Timeout`]/[`Error`](MemberOutcome::Error) line, never
/// an early return, so a partial fleet is always observable (like [`boot_fleet`]).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FleetReadback {
    /// Per-member verdicts, in fleet order.
    pub members: Vec<MemberReadback>,
}

impl FleetReadback {
    /// How many members booted **and** confirmed [`PowerState::On`].
    pub fn ready(&self) -> usize {
        self.members
            .iter()
            .filter(|m| matches!(m.outcome, MemberOutcome::Up(_)))
            .count()
    }

    /// How many members did **not** confirm up (timeout **or** error) — the
    /// complement of [`ready`](Self::ready).
    pub fn failed(&self) -> usize {
        self.members.len() - self.ready()
    }

    /// `true` only when the fleet is non-empty and **every** member is up.
    pub fn all_ready(&self) -> bool {
        !self.members.is_empty() && self.failed() == 0
    }
}

/// **Boot a fleet and roll up who is actually ready** — the fleet-level analogue of
/// [`boot_and_await`], and the multi-machine provision-readback seam jera's
/// dispatcher wants (jera Roster/WorkPayload → draupnir boots the fleet → this rolls
/// up who came up). It [`plan_fleet`]s `spec` into `n` members and drives each
/// through [`boot_and_await`] (validate → [`boot`] → confirm [`PowerState::On`]),
/// classifying every member's result into a [`MemberOutcome`] and returning the
/// [`FleetReadback`] rollup (per-member verdict + aggregate ready/failed counts).
///
/// **Pass a bounded [`WaitOptions`]** ([`WaitOptions::bounded`]): the per-member
/// timeout is what keeps one dead node from hanging the whole rollup — each member is
/// awaited independently within `opts`, so a member that never powers on rolls up as
/// a [`MemberOutcome::Timeout`] while the healthy members are [`Up`](MemberOutcome::Up)
/// (the default unbounded `WaitOptions` would block forever on the first dead node).
/// Written against `Boot + Lifecycle` so it drives any backend (KVM / container /
/// Redfish) and a mock alike, exactly like [`boot_and_await`] — no per-backend twin.
pub fn boot_fleet_and_await<B>(
    spec: &BootSpec,
    n: usize,
    backend: &B,
    opts: &WaitOptions,
) -> FleetReadback
where
    B: Boot + Lifecycle,
{
    let members = plan_fleet(spec, n)
        .iter()
        .map(|member| classify_member(backend, member, opts))
        .collect();
    let rollup = FleetReadback { members };
    functional_status(
        "draupnir/lifecycle",
        "boot_fleet_and_await",
        rollup.all_ready(),
        &format!(
            "fleet of {n}: {} ready, {} failed",
            rollup.ready(),
            rollup.failed()
        ),
    );
    rollup
}

/// **Boot one fleet member and classify how it landed** — the shared per-member step
/// behind both the serial [`boot_fleet_and_await`] and the parallel
/// [`boot_fleet_and_await_parallel`], so the two paths produce a **byte-identical**
/// [`MemberReadback`] for the same member by construction (there is exactly ONE
/// classification, never a twin — L5). Drives the member through [`boot_and_await`]
/// (validate → [`boot`] → confirm [`PowerState::On`]) and buckets the result:
/// `Ok` → [`Up`](MemberOutcome::Up); `Err(Backend)` → [`Timeout`](MemberOutcome::Timeout)
/// (the backend did not get it up in budget — a readback timeout or a transient boot
/// failure); `Err(Spec | Unsupported)` → [`Error`](MemberOutcome::Error) (a hard
/// misconfiguration / no backend — waiting cannot help).
fn classify_member<B>(backend: &B, member: &BootSpec, opts: &WaitOptions) -> MemberReadback
where
    B: Boot + Lifecycle,
{
    let outcome = match boot_and_await(backend, member, opts) {
        Ok(machine) => MemberOutcome::Up(machine),
        // A backend failure to bring the member up in time (readback timeout or a
        // live backend error while booting/polling) — the node did not come up
        // within the budget.
        Err(Error::Backend(msg)) => MemberOutcome::Timeout(msg),
        // A hard misconfiguration (invalid spec) or an unavailable backend — waiting
        // cannot help, so it is an error, not a timeout.
        Err(Error::Spec(msg)) | Err(Error::Unsupported(msg)) => MemberOutcome::Error(msg),
    };
    MemberReadback {
        name: member.name.clone(),
        outcome,
    }
}

/// **Boot a fleet CONCURRENTLY and roll up who is ready** — the parallel sibling of
/// [`boot_fleet_and_await`]. Where the serial call awaits members one-at-a-time (so a
/// fleet's wall-clock is the *sum* of the per-member waits), this fans every member
/// onto the **gatling fork-join pool** ([`gatling::gatling_forkjoin::gatling_for_each`]
/// — the constellation's one sanctioned threading home, ROOT-LAW #0), one worker per
/// member, so `n` nodes are booted and awaited **at the same time** and the
/// wall-clock collapses to roughly the *slowest* member's wait, not the sum. This is
/// the real win for jera's multi-machine dispatcher: booting a fleet of `n` nodes with
/// a 5-minute per-member budget takes ~5 minutes, not ~`5n` minutes.
///
/// It produces an **identical [`FleetReadback`]** to the serial call for the same
/// inputs — same per-member [`MemberOutcome`], same **fleet order** (`node-1`…`node-n`,
/// not completion order: the threads are joined back in fleet order), same aggregate
/// tallies — because both paths classify each member through the one shared
/// [`classify_member`] step. It is likewise *infallible by construction*: a dead or
/// misconfigured member is its own per-member line, and — because each member is
/// awaited on its **own** thread within the bounded `opts` — one dead node cannot hang
/// the others (they finish independently and are joined). **Pass a bounded
/// [`WaitOptions`]** for the same reason as the serial call.
///
/// Requires `B: Sync` (the backend is shared `&B` across the gatling workers); all three
/// real backends ([`kvm::KvmBoot`], [`container::ContainerBoot`], [`redfish::RedfishBoot`])
/// satisfy it. A backend that is not `Sync` simply uses the serial
/// [`boot_fleet_and_await`] instead — the serial path stays the fully-general fallback
/// (L2: the working path is kept, never replaced).
pub fn boot_fleet_and_await_parallel<B>(
    spec: &BootSpec,
    n: usize,
    backend: &B,
    opts: &WaitOptions,
) -> FleetReadback
where
    B: Boot + Lifecycle + Sync,
{
    let plan = plan_fleet(spec, n);
    // Fan each member onto the gatling fork-join pool (the constellation's ONE
    // sanctioned home for threading — ROOT-LAW #0: no bare `std::thread::spawn`).
    // `gatling_for_each` self-dispatches units and returns results in **index
    // order**, so the rollup stays deterministic and order-identical to the serial
    // path regardless of which member finishes first — identical effect to the
    // former `std::thread::scope` fan-out.
    //
    // Worker count is pinned to `plan.len()` (ONE worker per member), NOT the
    // default core count: `classify_member` is **blocking-IO** (boot a backend +
    // poll it up), not CPU work, so every member must have its own thread parked
    // on its own boot at the same time for the fleet wall-clock to collapse to the
    // *slowest* member (the whole point of this call). A core-bounded pool would
    // serialise `n > cores` members and break that guarantee. This is deliberately
    // exempt from the core-saturation law, which governs CPU hot paths, not
    // IO-bound waiters.
    let members: Vec<MemberReadback> =
        gatling::gatling_forkjoin::gatling_for_each(plan.len(), plan.len(), |i| {
            classify_member(backend, &plan[i], opts)
        });
    let rollup = FleetReadback { members };
    functional_status(
        "draupnir/lifecycle",
        "boot_fleet_and_await_parallel",
        rollup.all_ready(),
        &format!(
            "fleet of {n} (parallel): {} ready, {} failed",
            rollup.ready(),
            rollup.failed()
        ),
    );
    rollup
}

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

    fn bmc() -> BmcEndpoint {
        BmcEndpoint {
            host: "https://bmc-42.dc.example".into(),
            username: "admin".into(),
            system_id: "System.Embedded.1".into(),
        }
    }

    #[test]
    fn image_source_suits_the_right_backend() {
        assert!(ImageSource::Disk {
            kernel: "/bzImage".into(),
            disk: "/d.qcow2".into()
        }
        .suits(Kind::Vm));
        assert!(ImageSource::OciImage("redis:7".into()).suits(Kind::Container));
        assert!(ImageSource::Iso("/boot.iso".into()).suits(Kind::BareMetal));
        assert!(ImageSource::Executable {
            path: "/bin/true".into(),
            args: Vec::new()
        }
        .suits(Kind::Exe));
        // An ISO suits BOTH machine backends — changed 2026-08-15, and the point of
        // the change. `(Iso, Kvm)` used to be asserted here as REJECTED, which is
        // literally the wall that made the ISO chain unrunnable: there was no way to
        // hand the KVM backend an ISO at all. One spec must route to the faked KVM
        // machine and to real metal (honesty rule 2), so the pairing is legal on both.
        assert!(
            ImageSource::Iso("/boot.iso".into()).suits(Kind::Vm),
            "an ISO is bootable under KVM (-cdrom): this is the iso-kvm leg"
        );
        // Cross pairings that remain nonsense are still rejected.
        assert!(!ImageSource::OciImage("redis:7".into()).suits(Kind::BareMetal));
        assert!(!ImageSource::Iso("/boot.iso".into()).suits(Kind::Container));
        assert!(!ImageSource::OciImage("redis:7".into()).suits(Kind::Exe));
        assert!(!ImageSource::Executable {
            path: "/bin/true".into(),
            args: Vec::new()
        }
        .suits(Kind::Vm));
    }

    #[test]
    fn valid_specs_pass_validation() {
        BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz")
            .validate()
            .unwrap();
        BootSpec::container("cache", "docker.io/library/redis:7")
            .validate()
            .unwrap();
        BootSpec::redfish_iso("node-42", "/images/installer.iso", bmc())
            .validate()
            .unwrap();
        BootSpec::kvm_disk("disky", "/bzImage", "/disk.qcow2")
            .validate()
            .unwrap();
    }

    #[test]
    fn disk_boot_without_a_kernel_is_rejected() {
        // The whole point of the fix: a Disk spec that carries no kernel would
        // pass an empty `-kernel` to tunnr and fail at runtime, so reject it here.
        let mut spec = BootSpec::kvm_disk("disky", "", "/disk.qcow2");
        assert!(matches!(spec.validate(), Err(Error::Spec(_))));
        // ...and an empty disk path is likewise rejected.
        spec = BootSpec::kvm_disk("disky", "/bzImage", "");
        assert!(matches!(spec.validate(), Err(Error::Spec(_))));
    }

    #[test]
    fn empty_required_payload_paths_are_rejected_on_every_image_source() {
        // Parity with the Disk checks: an empty required path/ref on ANY image
        // source is rejected at validate() rather than handed empty to a backend.
        // RED-when-broken — drop any arm's `require` and one of these passes.

        // KernelRootfs: empty kernel, then empty rootfs.
        let mut kr = BootSpec::kvm_kernel_rootfs("kr", "", "/rootfs.cpio.gz");
        assert!(
            matches!(kr.validate(), Err(Error::Spec(_))),
            "empty kernel rejected"
        );
        kr = BootSpec::kvm_kernel_rootfs("kr", "/bzImage", "   ");
        assert!(
            matches!(kr.validate(), Err(Error::Spec(_))),
            "whitespace rootfs rejected"
        );

        // OciImage: an empty image reference is not bootable.
        let oci = BootSpec::container("cache", "");
        assert!(
            matches!(oci.validate(), Err(Error::Spec(_))),
            "empty OCI ref rejected"
        );

        // Iso: an empty ISO path is not bootable (BMC present so only the ISO fails).
        let iso = BootSpec::redfish_iso("node", "  ", bmc());
        assert!(
            matches!(iso.validate(), Err(Error::Spec(_))),
            "empty ISO path rejected"
        );

        // The well-formed constructors still pass (no regression).
        BootSpec::kvm_kernel_rootfs("kr", "/bzImage", "/rootfs.cpio.gz")
            .validate()
            .unwrap();
        BootSpec::container("cache", "redis:7").validate().unwrap();
        BootSpec::redfish_iso("node", "/boot.iso", bmc())
            .validate()
            .unwrap();
    }

    #[test]
    fn blank_instance_name_is_rejected_on_every_backend() {
        // The name is the Machine-id / fleet-member prefix; a blank one yields
        // "-1"/"-2" members and a leading-dash id, so reject it up front.
        // RED-when-broken — drop the `require("instance name", …)` and these pass.
        let mut vm = BootSpec::kvm_kernel_rootfs("", "/bzImage", "/rootfs.cpio.gz");
        assert!(
            matches!(vm.validate(), Err(Error::Spec(_))),
            "blank KVM name rejected"
        );
        vm = BootSpec::kvm_kernel_rootfs("   ", "/bzImage", "/rootfs.cpio.gz");
        assert!(
            matches!(vm.validate(), Err(Error::Spec(_))),
            "whitespace KVM name rejected"
        );

        let ctr = BootSpec::container("", "redis:7");
        assert!(
            matches!(ctr.validate(), Err(Error::Spec(_))),
            "blank container name rejected"
        );

        let node = BootSpec::redfish_iso("", "/boot.iso", bmc());
        assert!(
            matches!(node.validate(), Err(Error::Spec(_))),
            "blank Redfish name rejected"
        );
    }

    #[test]
    fn kvm_spec_without_ram_or_cpus_is_rejected() {
        // A KVM VM is booted with the spec's RAM/vCPU verbatim; 0 launches a dead
        // guest. RED-when-broken — drop either sizing guard and one of these passes.
        let mut vm = BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz");
        vm.mem_mb = 0;
        assert!(
            matches!(vm.validate(), Err(Error::Spec(_))),
            "0 MiB RAM rejected"
        );
        vm = BootSpec::kvm_disk("disky", "/bzImage", "/disk.qcow2");
        vm.cores = 0;
        assert!(
            matches!(vm.validate(), Err(Error::Spec(_))),
            "0 vCPUs rejected"
        );
        // Container/Redfish carry no VM sizing (both default to 0) and stay valid.
        BootSpec::container("cache", "redis:7").validate().unwrap();
        BootSpec::redfish_iso("node", "/boot.iso", bmc())
            .validate()
            .unwrap();
    }

    #[test]
    fn redfish_spec_with_a_blank_bmc_field_is_rejected() {
        // Parity with the ISO-path check: each BMC field is woven into a Redfish
        // URL, so a blank one is rejected at validate(), not on the wire.
        // RED-when-broken — drop any `require("BMC …", …)` and its arm passes.
        let mut spec = BootSpec::redfish_iso("node", "/boot.iso", bmc());
        spec.bmc = Some(BmcEndpoint {
            host: "  ".into(),
            ..bmc()
        });
        assert!(
            matches!(spec.validate(), Err(Error::Spec(_))),
            "blank BMC host rejected"
        );

        spec = BootSpec::redfish_iso("node", "/boot.iso", bmc());
        spec.bmc = Some(BmcEndpoint {
            username: String::new(),
            ..bmc()
        });
        assert!(
            matches!(spec.validate(), Err(Error::Spec(_))),
            "blank BMC username rejected"
        );

        spec = BootSpec::redfish_iso("node", "/boot.iso", bmc());
        spec.bmc = Some(BmcEndpoint {
            system_id: String::new(),
            ..bmc()
        });
        assert!(
            matches!(spec.validate(), Err(Error::Spec(_))),
            "blank BMC system id rejected"
        );

        // A fully-populated BMC still validates (no regression).
        BootSpec::redfish_iso("node", "/boot.iso", bmc())
            .validate()
            .unwrap();
    }

    #[test]
    fn container_spec_with_a_zero_or_duplicate_port_is_rejected() {
        // Published ports are exposed and host-bound verbatim; `0` is never a real
        // published port and a doubled port self-collides. RED-when-broken — drop
        // either guard and its arm passes.
        let zero = BootSpec::container("cache", "redis:7").with_port(0);
        assert!(
            matches!(zero.validate(), Err(Error::Spec(_))),
            "0 published port rejected"
        );

        let dup = BootSpec::container("cache", "redis:7")
            .with_port(8080)
            .with_port(8080);
        assert!(
            matches!(dup.validate(), Err(Error::Spec(_))),
            "duplicate published port rejected"
        );

        // A mix with a single 0 among valid ports is still rejected.
        let mixed = BootSpec::container("cache", "redis:7")
            .with_port(8080)
            .with_port(0);
        assert!(
            matches!(mixed.validate(), Err(Error::Spec(_))),
            "0 among valid ports rejected"
        );

        // A valid, distinct port set passes; no ports at all passes (no regression).
        BootSpec::container("cache", "redis:7")
            .with_port(8080)
            .with_port(8443)
            .validate()
            .unwrap();
        BootSpec::container("cache", "redis:7").validate().unwrap();
    }

    #[test]
    fn container_port_map_validates_distinct_host_container_and_host_collisions() {
        // The distinct `host:container` form (the per-zone offset fix): a `Test`
        // zone publishes FalkorDB on host 6380 → container 6379. RED-when-broken —
        // drop a guard and its arm passes.

        // A valid distinct map passes — this is korp's Test/Prod zone shape.
        BootSpec::container("falkor", "docker.io/falkordb/falkordb:v4.20.0")
            .with_port_map(6380, 6379)
            .validate()
            .unwrap();

        // Host 0 or container 0 is not a real published port.
        let host0 = BootSpec::container("f", "img:1").with_port_map(0, 6379);
        assert!(
            matches!(host0.validate(), Err(Error::Spec(_))),
            "host 0 rejected"
        );
        let cont0 = BootSpec::container("f", "img:1").with_port_map(6380, 0);
        assert!(
            matches!(cont0.validate(), Err(Error::Spec(_))),
            "container 0 rejected"
        );

        // The same HOST port bound twice self-collides — across two maps...
        let dup_map = BootSpec::container("f", "img:1")
            .with_port_map(6380, 6379)
            .with_port_map(6380, 15002);
        assert!(
            matches!(dup_map.validate(), Err(Error::Spec(_))),
            "duplicate host port across maps rejected"
        );
        // ...and across the `ports` + `port_maps` fields (a host 8080 in both).
        let dup_mix = BootSpec::container("f", "img:1")
            .with_port(8080)
            .with_port_map(8080, 80);
        assert!(
            matches!(dup_mix.validate(), Err(Error::Spec(_))),
            "host port shared by ports+port_maps rejected"
        );

        // Two DIFFERENT host ports mapping to the SAME container port is fine —
        // that is exactly the multi-zone case (never both live on one host at
        // once, but a spec listing both is legitimate) — no false collision.
        BootSpec::container("f", "img:1")
            .with_port_map(6380, 6379)
            .with_port_map(6381, 6379)
            .validate()
            .unwrap();
        // `ports` (host==container) composes with a distinct map on another port.
        BootSpec::container("f", "img:1")
            .with_port(6379)
            .with_port_map(15003, 15002)
            .validate()
            .unwrap();
    }

    #[test]
    fn container_only_cmd_or_ports_on_a_non_container_backend_is_rejected() {
        // `cmd` and `ports` are container-only knobs; KVM/Redfish silently ignore
        // them, so setting either there is a misconfiguration that would vanish
        // without a trace — reject it (parity with a bmc on a non-Redfish backend).
        // RED-when-broken — drop either guard and its arm passes.

        // A cmd on a KVM spec is rejected.
        let kvm_cmd = BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz")
            .with_cmd(["/bin/init"]);
        assert!(
            matches!(kvm_cmd.validate(), Err(Error::Spec(_))),
            "cmd on KVM rejected"
        );

        // A published port on a KVM spec is ACCEPTED as of 2026-09-05: tunnr's
        // user-mode NIC forwards `(host, guest)` natively, so the pair has a real
        // wire here and is no longer container-only. This assertion is inverted
        // from what it said before, deliberately — see `BootSpec::validate` for
        // why the old rejection was the reason a caller who needed to reach a
        // service in the guest could not use draupnir at all.
        BootSpec::kvm_disk("disky", "/bzImage", "/disk.qcow2")
            .with_port(8080)
            .validate()
            .expect("a KVM boot may forward a host port to the guest");

        // A cmd on a Redfish spec is rejected...
        let redfish_cmd = BootSpec::redfish_iso("node", "/boot.iso", bmc()).with_cmd(["/bin/init"]);
        assert!(
            matches!(redfish_cmd.validate(), Err(Error::Spec(_))),
            "cmd on Redfish rejected"
        );

        // ...and a published port on a Redfish spec is rejected.
        let redfish_port = BootSpec::redfish_iso("node", "/boot.iso", bmc()).with_port(443);
        assert!(
            matches!(redfish_port.validate(), Err(Error::Spec(_))),
            "port on Redfish rejected"
        );

        // The container backend still carries both (no regression).
        BootSpec::container("web", "nginx:latest")
            .with_cmd(["nginx", "-g", "daemon off;"])
            .with_port(8080)
            .with_port(8443)
            .validate()
            .unwrap();
    }

    /// **A forward needs a NIC to arrive on.** `NetMode::None` renders `-nic
    /// none` on the KVM path, so a spec that asks for both is a contradiction —
    /// and the failure mode of resolving it silently is the worst kind: the
    /// guest comes up with no NIC, the host connects to a port nothing is
    /// listening on, and the test hangs to its timeout instead of saying why.
    /// Refused by name, at spec time.
    ///
    /// RED-when-broken: drop the airgap-vs-forward arm in `validate` and this
    /// spec validates clean.
    #[test]
    fn a_kvm_boot_cannot_both_airgap_the_guest_and_forward_a_port_to_it() {
        let both = BootSpec::kvm_disk("contradiction", "/bzImage", "/disk.qcow2")
            .with_net(NetMode::None)
            .with_port_map(2222, 22);
        let err = both.validate().expect_err("airgap + forward is a contradiction");
        let msg = err.to_string();
        assert!(msg.contains("NetMode::None"), "names the airgap: {msg}");
        assert!(msg.contains("forward"), "names the forward: {msg}");

        // Either one ALONE is fine — the refusal is about the pair, not about
        // having reintroduced a rejection of forwards.
        BootSpec::kvm_disk("dark", "/bzImage", "/disk.qcow2")
            .with_net(NetMode::None)
            .validate()
            .expect("airgap alone");
        BootSpec::kvm_disk("open", "/bzImage", "/disk.qcow2")
            .with_port_map(2222, 22)
            .validate()
            .expect("forward alone");
    }

    #[test]
    fn container_resource_knobs_are_container_only_and_must_be_positive() {
        // `cpus` / `mem_limit_mb` are container-only (parity with cmd/ports/net):
        // a CPU/memory cap on a KVM or Redfish spec is rejected up front.
        // RED-when-broken — drop either container-only guard and its arm passes.
        let kvm_cpus =
            BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz").with_cpus(8.0);
        assert!(
            matches!(kvm_cpus.validate(), Err(Error::Spec(_))),
            "cpus on KVM rejected"
        );
        let kvm_mem =
            BootSpec::kvm_disk("disky", "/bzImage", "/disk.qcow2").with_mem_limit_mb(4096);
        assert!(
            matches!(kvm_mem.validate(), Err(Error::Spec(_))),
            "mem limit on KVM rejected"
        );
        let redfish_cpus = BootSpec::redfish_iso("node", "/boot.iso", bmc()).with_cpus(4.0);
        assert!(
            matches!(redfish_cpus.validate(), Err(Error::Spec(_))),
            "cpus on Redfish rejected"
        );

        // A non-positive cap is rejected on the container backend (None = all cores /
        // unconstrained is the way to express "no limit", never 0).
        let zero_cpus = BootSpec::container("cache", "redis:7").with_cpus(0.0);
        assert!(
            matches!(zero_cpus.validate(), Err(Error::Spec(_))),
            "0 cpus rejected"
        );
        let mut neg = BootSpec::container("cache", "redis:7");
        neg.cpus = Some(-1.0);
        assert!(
            matches!(neg.validate(), Err(Error::Spec(_))),
            "negative cpus rejected"
        );
        let zero_mem = BootSpec::container("cache", "redis:7").with_mem_limit_mb(0);
        assert!(
            matches!(zero_mem.validate(), Err(Error::Spec(_))),
            "0 MiB memory rejected"
        );

        // A hot-infra container (all cores, sized memory) validates; and the default
        // (no caps = all host cores, unconstrained memory) validates too (no regression).
        BootSpec::container("falkordb", "docker.io/falkordb/falkordb:v4.20.0")
            .with_port(6379)
            .with_cpus(12.0)
            .with_mem_limit_mb(16384)
            .validate()
            .unwrap();
        BootSpec::container("cache", "redis:7").validate().unwrap();
        assert_eq!(
            BootSpec::container("cache", "redis:7").cpus,
            None,
            "default = all host cores"
        );
    }

    #[test]
    fn the_container_process_identity_is_container_only_and_never_blank() {
        // `user` / `working_dir` are the container process's identity and cwd —
        // container vocabulary, parity with cmd/ports/net/caps. A KVM guest runs
        // its own init and a Redfish node is a board: setting either there would
        // vanish without a trace, so it is refused rather than dropped.
        let kvm = BootSpec::kvm_kernel_rootfs("appliance", "/bzImage", "/rootfs.cpio.gz")
            .with_user("0:0");
        assert!(
            matches!(kvm.validate(), Err(Error::Spec(_))),
            "user on KVM rejected"
        );
        let redfish = BootSpec::redfish_iso("node", "/boot.iso", bmc()).with_working_dir("/src");
        assert!(
            matches!(redfish.validate(), Err(Error::Spec(_))),
            "working_dir on Redfish rejected"
        );

        // Named-but-blank is a typo, not a choice: the daemon would read `""` as
        // "unset" and the caller would believe the process runs as somebody, or
        // starts somewhere, that it does not.
        assert!(matches!(
            BootSpec::container("b", "img:1").with_user("  ").validate(),
            Err(Error::Spec(_))
        ));
        assert!(matches!(
            BootSpec::container("b", "img:1").with_working_dir("").validate(),
            Err(Error::Spec(_))
        ));

        // The shape Njord's appliance build needs, end to end — and the default
        // (neither named) still validates, so nothing that exists today moves.
        let build = BootSpec::container("njord-build-aaa1111-cargo", "njord-toolchain:1.95.0")
            .with_user("0:0")
            .with_working_dir("/src")
            .with_bind("/var/lib/njord-build/src/aaa1111:/src:rw");
        build.validate().unwrap();
        assert_eq!(build.user.as_deref(), Some("0:0"));
        assert_eq!(build.working_dir.as_deref(), Some("/src"));
        let plain = BootSpec::container("cache", "redis:7");
        plain.validate().unwrap();
        assert!(plain.user.is_none() && plain.working_dir.is_none());
    }

    // -- Boot off a medium: the two legs, and the lie between them ---------------

    #[test]
    fn the_install_leg_and_the_boot_installed_leg_are_two_distinct_specs() {
        // Honesty rule 1 in the type system. The install spec carries the medium and
        // boots off it; the boot-installed spec carries NO medium and boots the disk.
        // They are two values, so they can only be two boots.
        let install = BootSpec::kvm_install_from_medium("app", "/gunnar.iso", "/target.qcow2");
        install.validate().unwrap();
        assert_eq!(install.medium_path(), Some("/gunnar.iso"));
        assert_eq!(install.boot_order, BootOrder::Medium);
        assert_eq!(
            install.image,
            ImageSource::BootloaderDisk {
                disk: "/target.qcow2".into()
            },
            "the blank target disk is present to be installed ONTO"
        );
        assert!(
            install.persist_disk,
            "an install whose writes are thrown away installed nothing"
        );

        let installed = BootSpec::kvm_boot_installed_disk("app", "/target.qcow2");
        installed.validate().unwrap();
        assert_eq!(installed.medium_path(), None, "the medium is ABSENT");
        assert_eq!(installed.boot_order, BootOrder::Disk);
        assert!(installed.persist_disk);

        // Same disk, different machine: that is the whole claim.
        assert_ne!(install, installed);
    }

    #[test]
    fn a_boot_installed_spec_that_still_carries_the_medium_is_refused() {
        // THE guard. With the ISO still in the tray, a machine that fails to boot its
        // freshly-installed disk falls through to the installer and looks like a pass.
        // RED-when-broken: delete the `BootOrder::Disk && medium_path().is_some()`
        // arm in validate() and this spec sails through.
        let lie = BootSpec::kvm_boot_installed_disk("app", "/target.qcow2")
            .with_medium("/gunnar.iso");
        let err = lie.validate().unwrap_err();
        let msg = format!("{err}");
        assert!(matches!(err, Error::Spec(_)));
        assert!(msg.contains("/gunnar.iso"), "names the medium: {msg}");
        assert!(msg.contains("ABSENT"), "says what boot-installed MEANS: {msg}");
        assert!(
            msg.contains("second VM lifetime"),
            "points at the fix: {msg}"
        );

        // ...and the same lie spelled the other way (an ISO payload + boot from disk).
        let lie2 = BootSpec::iso_boot("app", "/gunnar.iso").with_boot_order(BootOrder::Disk);
        assert!(
            matches!(lie2.validate(), Err(Error::Spec(_))),
            "an Iso payload counts as an attached medium too"
        );
    }

    #[test]
    fn boot_order_medium_with_no_medium_anywhere_is_refused_by_name() {
        // "Boot off the medium" with nothing in the tray cannot do what it says.
        // RED-when-broken: drop the `Medium && medium_path().is_none()` arm.
        let empty = BootSpec::kvm_bootloader_disk("app", "/d.qcow2")
            .with_boot_order(BootOrder::Medium);
        let err = empty.validate().unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("no medium"), "names the problem: {msg}");
        assert!(msg.contains("medium"), "names the field to set: {msg}");
    }

    #[test]
    fn one_iso_spec_routes_to_kvm_or_to_metal_without_changing_the_boot_claim() {
        // Honesty rule 2: `iso-kvm` and `iso-metal` are distinct RUNS of ONE spec.
        // Everything that describes the boot must survive the routing; only the
        // machine it is aimed at changes.
        let kvm = BootSpec::iso_boot("gunnar-server", "/images/gunnar-server.iso");
        kvm.validate().unwrap();
        assert_eq!(kvm.kind, Kind::Vm);
        assert_eq!(kvm.boot_order, BootOrder::Medium);

        let metal = kvm.clone().on_metal(bmc());
        metal.validate().unwrap();
        assert_eq!(metal.kind, Kind::BareMetal);
        // The boot claim is untouched by the routing.
        assert_eq!(metal.image, kvm.image);
        assert_eq!(metal.medium_path(), kvm.medium_path());
        assert_eq!(metal.boot_order, kvm.boot_order);
        // Each backend reads the SAME field in its own vocabulary — one field, two
        // renderings, so a KVM green and a metal green are the same claim.
        assert_eq!(kvm.boot_order.qemu_value(), Some("d"));
        assert_eq!(metal.boot_order.redfish_target(), Some(BootTarget::Cd));
    }

    #[test]
    fn a_medium_is_rejected_on_the_backends_that_have_no_drive_tray() {
        // RED-when-broken: drop the Kvm|Redfish guard and a container silently
        // accepts an ISO it will never read.
        let ctr = BootSpec::container("cache", "redis:7").with_medium("/boot.iso");
        assert!(matches!(ctr.validate(), Err(Error::Spec(_))), "medium on container rejected");
        let ctr_order = BootSpec::container("cache", "redis:7").with_boot_order(BootOrder::Disk);
        assert!(
            matches!(ctr_order.validate(), Err(Error::Spec(_))),
            "boot order on container rejected"
        );
        // A blank medium path is rejected like every other required path.
        let blank = BootSpec::kvm_bootloader_disk("app", "/d.qcow2").with_medium("  ");
        assert!(matches!(blank.validate(), Err(Error::Spec(_))), "blank medium rejected");
    }

    #[test]
    fn every_pre_existing_constructor_still_names_no_medium_and_no_boot_order() {
        // The additive-parity guard: the two new fields must leave every spec that
        // existed before them exactly as it was, so no caller's launch moves.
        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"),
            BootSpec::container("d", "redis:7"),
            BootSpec::redfish_iso("e", "/boot.iso", bmc()),
        ] {
            assert_eq!(spec.medium, None, "{} names no explicit medium", spec.name);
            assert_eq!(
                spec.boot_order,
                BootOrder::Auto,
                "{} names no boot order",
                spec.name
            );
            assert_eq!(
                spec.boot_order.qemu_value(),
                None,
                "{} emits no -boot",
                spec.name
            );
            spec.validate().unwrap();
        }
        // The one pre-existing spec whose image IS an ISO still resolves a medium
        // through the shared accessor — that is how the Redfish backend keeps working.
        assert_eq!(
            BootSpec::redfish_iso("e", "/boot.iso", bmc()).medium_path(),
            Some("/boot.iso")
        );
    }

    #[test]
    fn the_one_constructor_writer_keeps_the_lean_and_kvm_defaults_apart() {
        // `BootSpec::of` is the single writer; prove it did not flatten the two
        // default shapes into one (KVM is sized, everything else is lean).
        assert_eq!(
            (
                BootSpec::kvm_bootloader_disk("k", "/d.qcow2").mem_mb,
                BootSpec::kvm_bootloader_disk("k", "/d.qcow2").cores
            ),
            (512, 2)
        );
        for lean in [
            BootSpec::container("c", "redis:7"),
            BootSpec::redfish_iso("r", "/b.iso", bmc()),
            BootSpec::exe("x", "/bin/true", Vec::<String>::new()),
        ] {
            assert_eq!((lean.mem_mb, lean.cores), (0, 0), "{} stays lean", lean.name);
        }
    }

    #[test]
    fn image_backend_mismatch_is_rejected() {
        let mut spec = BootSpec::container("bad", "redis:7");
        spec.image = ImageSource::Iso("/boot.iso".into());
        assert!(matches!(spec.validate(), Err(Error::Spec(_))));
    }

    #[test]
    fn redfish_without_bmc_is_rejected() {
        let mut spec = BootSpec::redfish_iso("node", "/boot.iso", bmc());
        spec.bmc = None;
        assert!(matches!(spec.validate(), Err(Error::Spec(_))));
    }

    #[test]
    fn non_redfish_with_bmc_is_rejected() {
        let mut spec = BootSpec::container("cache", "redis:7");
        spec.bmc = Some(bmc());
        assert!(matches!(spec.validate(), Err(Error::Spec(_))));
    }

    #[test]
    fn started_machine_records_the_spec() {
        let spec = BootSpec::container("cache", "redis:7").with_env("PORT", "6379");
        let m = Machine::started("ctr-abc123", &spec);
        assert_eq!(m.spec_name, "cache");
        assert_eq!(m.kind, Kind::Container);
        assert_eq!(m.power, PowerState::On);
        assert_eq!(spec.env.get("PORT").map(String::as_str), Some("6379"));
    }

    /// A fake [`Boot`] that records the specs it was handed and mints a stable id,
    /// so the unifying [`boot`]/[`boot_fleet`] entry points are testable with no
    /// live backend.
    #[derive(Default)]
    struct RecordingBoot {
        seen: std::cell::RefCell<Vec<String>>,
    }

    impl Boot for RecordingBoot {
        fn boot(&self, spec: &BootSpec) -> Result<Machine> {
            self.seen.borrow_mut().push(spec.name.clone());
            Ok(Machine::started(format!("id-{}", spec.name), spec))
        }
    }

    #[test]
    fn boot_validates_then_delegates_to_the_backend() {
        let backend = RecordingBoot::default();
        let spec = BootSpec::container("cache", "redis:7");
        let m = boot(&spec, &backend).unwrap();
        assert_eq!(m.id, "id-cache");
        assert_eq!(m.kind, Kind::Container);
        assert_eq!(backend.seen.borrow().as_slice(), &["cache".to_string()]);
    }

    #[test]
    fn boot_rejects_an_invalid_spec_before_touching_the_backend() {
        let backend = RecordingBoot::default();
        let mut spec = BootSpec::container("bad", "redis:7");
        spec.image = ImageSource::Iso("/boot.iso".into()); // ISO can't boot on Container
        assert!(matches!(boot(&spec, &backend), Err(Error::Spec(_))));
        assert!(
            backend.seen.borrow().is_empty(),
            "backend never touched on an invalid spec"
        );
    }

    #[test]
    fn boot_fleet_drips_and_boots_every_member_through_one_backend() {
        let backend = RecordingBoot::default();
        let one = BootSpec::redfish_iso("node", "/images/installer.iso", bmc());
        let results = boot_fleet(&one, 3, &backend);
        assert_eq!(results.len(), 3);
        let ids: Vec<_> = results.into_iter().map(|r| r.unwrap().id).collect();
        assert_eq!(ids, vec!["id-node-1", "id-node-2", "id-node-3"]);
        assert_eq!(
            backend.seen.borrow().as_slice(),
            &["node-1", "node-2", "node-3"]
        );
    }

    /// A [`Boot`] that mints ids until it has booted `ok_before` members, then
    /// errors — so the documented "a partial fleet is observable" contract of
    /// [`boot_fleet`] can be exercised.
    struct FlakyBoot {
        ok_before: usize,
        booted: std::cell::RefCell<usize>,
    }

    impl Boot for FlakyBoot {
        fn boot(&self, spec: &BootSpec) -> Result<Machine> {
            let mut n = self.booted.borrow_mut();
            if *n >= self.ok_before {
                return Err(Error::Backend(format!(
                    "backend went away booting {}",
                    spec.name
                )));
            }
            *n += 1;
            Ok(Machine::started(format!("id-{}", spec.name), spec))
        }
    }

    #[test]
    fn boot_fleet_reports_a_partial_fleet_when_a_later_member_fails() {
        // The doc promises a partial fleet is observable: the first members boot,
        // a later one errors, and every per-member result is returned in order.
        let backend = FlakyBoot {
            ok_before: 2,
            booted: std::cell::RefCell::new(0),
        };
        let one = BootSpec::redfish_iso("node", "/images/installer.iso", bmc());
        let results = boot_fleet(&one, 4, &backend);
        assert_eq!(results.len(), 4);
        assert_eq!(results[0].as_ref().unwrap().id, "id-node-1");
        assert_eq!(results[1].as_ref().unwrap().id, "id-node-2");
        assert!(
            matches!(results[2], Err(Error::Backend(_))),
            "3rd member fails"
        );
        assert!(
            matches!(results[3], Err(Error::Backend(_))),
            "4th member fails too"
        );
        let ok = results.iter().filter(|r| r.is_ok()).count();
        assert_eq!(ok, 2, "exactly the first two members booted");
    }

    #[test]
    fn plan_fleet_of_zero_is_empty_and_of_one_keeps_a_suffix() {
        assert!(plan_fleet(&BootSpec::container("c", "redis:7"), 0).is_empty());
        // Even n=1 gets the 1-based suffix (a fleet member is always "{name}-{i}").
        let one = plan_fleet(&BootSpec::container("c", "redis:7"), 1);
        assert_eq!(one.len(), 1);
        assert_eq!(one[0].name, "c-1");
    }

    #[test]
    fn cloud_init_user_data_constructor_defaults_meta_data_to_none() {
        let ci = CloudInit::user_data("#cloud-config\n");
        assert_eq!(ci.user_data, "#cloud-config\n");
        assert_eq!(ci.meta_data, None);
        assert_eq!(ci.network_config, None);
    }

    #[test]
    fn container_spec_defaults_are_lean() {
        // A container boot carries no VM sizing and no overrides until asked — the
        // "lean by design" contract the KVM path does not share.
        let spec = BootSpec::container("cache", "docker.io/library/redis:7");
        assert_eq!(spec.mem_mb, 0);
        assert_eq!(spec.cores, 0);
        assert!(spec.cmd.is_empty());
        assert!(spec.ports.is_empty());
        assert!(spec.env.is_empty());
        assert!(spec.bmc.is_none());
        assert!(spec.cloud_init.is_none());
    }

    #[test]
    fn cloud_init_on_a_container_spec_still_validates_and_is_backend_ignored() {
        // cloud_init is a KVM-only provisioning payload; attaching it to a container
        // spec is not an error (the container backend simply ignores it).
        let spec = BootSpec::container("cache", "redis:7")
            .with_cloud_init(CloudInit::user_data("#cloud-config\n"));
        spec.validate().unwrap();
        assert!(spec.cloud_init.is_some());
    }

    #[test]
    fn plan_fleet_drips_n_identical_but_distinctly_named_members() {
        let one = BootSpec::redfish_iso("node", "/images/installer.iso", bmc());
        let fleet = plan_fleet(&one, 8);
        assert_eq!(fleet.len(), 8);
        assert_eq!(fleet[0].name, "node-1");
        assert_eq!(fleet[7].name, "node-8");
        // Identical payload + backend across the whole ring.
        assert!(fleet
            .iter()
            .all(|m| m.image == one.image && m.kind == one.kind));
        // Names are unique.
        let mut names: Vec<_> = fleet.iter().map(|m| m.name.clone()).collect();
        names.sort();
        names.dedup();
        assert_eq!(names.len(), 8);
    }

    /// A backend that is both [`Boot`] and [`Lifecycle`], reporting a scripted
    /// sequence of power states from `status` (then a `fallback` once the sequence
    /// is exhausted) — so the [`await_power_state`] / [`boot_and_await`] readback
    /// seam is testable with no live instance.
    struct ScriptedNode {
        states: std::cell::RefCell<std::collections::VecDeque<PowerState>>,
        fallback: PowerState,
        status_calls: std::cell::Cell<usize>,
    }

    impl ScriptedNode {
        fn new(seq: impl IntoIterator<Item = PowerState>, fallback: PowerState) -> Self {
            Self {
                states: std::cell::RefCell::new(seq.into_iter().collect()),
                fallback,
                status_calls: std::cell::Cell::new(0),
            }
        }
    }

    impl Boot for ScriptedNode {
        fn boot(&self, spec: &BootSpec) -> Result<Machine> {
            Ok(Machine::started(format!("id-{}", spec.name), spec))
        }
    }

    impl Lifecycle for ScriptedNode {
        fn power_on(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn power_off(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn status(&self, _m: &Machine) -> Result<PowerState> {
            self.status_calls.set(self.status_calls.get() + 1);
            Ok(self
                .states
                .borrow_mut()
                .pop_front()
                .unwrap_or(self.fallback))
        }
    }

    fn tiny_bounded() -> WaitOptions {
        WaitOptions::bounded(
            std::time::Duration::from_millis(200),
            std::time::Duration::from_millis(1),
        )
    }

    #[test]
    fn await_power_state_returns_ok_once_the_state_is_reached() {
        // status reports Unknown twice, then On — await must poll past the
        // Unknowns and return Ok the moment it observes On.
        let node = ScriptedNode::new(
            [PowerState::Unknown, PowerState::Unknown, PowerState::On],
            PowerState::On,
        );
        let m = Machine::started("node-1", &BootSpec::container("c", "redis:7"));
        await_power_state(&node, &m, PowerState::On, &tiny_bounded()).unwrap();
        assert!(
            node.status_calls.get() >= 3,
            "polled past the two Unknowns to On"
        );
    }

    #[test]
    fn await_power_state_times_out_when_the_state_is_never_reached() {
        // The node is stuck Off forever; a bounded wait must time out with a
        // Backend error rather than block. RED-when-broken — drop the `== want`
        // gate (return Ok on the first poll) and this Err expectation fails.
        let node = ScriptedNode::new(std::iter::empty(), PowerState::Off);
        let m = Machine::started("node-2", &BootSpec::container("c", "redis:7"));
        let r = await_power_state(
            &node,
            &m,
            PowerState::On,
            &WaitOptions::bounded(
                std::time::Duration::from_millis(20),
                std::time::Duration::from_millis(1),
            ),
        );
        assert!(
            matches!(r, Err(Error::Backend(_))),
            "never-up node times out"
        );
    }

    #[test]
    fn await_power_state_rejects_awaiting_unknown() {
        // Awaiting Unknown is nonsensical (Unknown = "not observed") and is
        // rejected up front as a Spec error, before any poll.
        let node = ScriptedNode::new([PowerState::On], PowerState::On);
        let m = Machine::started("node-3", &BootSpec::container("c", "redis:7"));
        let r = await_power_state(&node, &m, PowerState::Unknown, &tiny_bounded());
        assert!(matches!(r, Err(Error::Spec(_))), "await Unknown rejected");
        assert_eq!(node.status_calls.get(), 0, "rejected before polling");
    }

    #[test]
    fn await_power_state_propagates_a_backend_status_error() {
        // A live backend's status() error is the readback failing — it propagates.
        struct ErrLifecycle;
        impl Lifecycle for ErrLifecycle {
            fn power_on(&self, _m: &Machine) -> Result<()> {
                Ok(())
            }
            fn power_off(&self, _m: &Machine) -> Result<()> {
                Ok(())
            }
            fn status(&self, _m: &Machine) -> Result<PowerState> {
                Err(Error::Backend("BMC unreachable".into()))
            }
        }
        let m = Machine::started("node-4", &BootSpec::container("c", "redis:7"));
        let r = await_power_state(&ErrLifecycle, &m, PowerState::On, &tiny_bounded());
        assert!(
            matches!(r, Err(Error::Backend(_))),
            "status error propagates"
        );
    }

    #[test]
    fn boot_and_await_boots_then_confirms_power_on() {
        // The one-call provision seam: boot, then confirm On. The node reports
        // Unknown once, then On — boot_and_await returns the live Machine only
        // after the On readback.
        let node = ScriptedNode::new([PowerState::Unknown, PowerState::On], PowerState::On);
        let spec = BootSpec::container("cache", "redis:7");
        let m = boot_and_await(&node, &spec, &tiny_bounded()).unwrap();
        assert_eq!(m.id, "id-cache");
        assert_eq!(m.power, PowerState::On);
        assert!(node.status_calls.get() >= 2, "awaited past Unknown to On");
    }

    #[test]
    fn boot_and_await_times_out_when_the_instance_never_comes_up() {
        // A node that never powers on makes boot_and_await a bounded timeout, not a
        // hang — the readback is what distinguishes a booted-but-dead instance.
        let node = ScriptedNode::new(std::iter::empty(), PowerState::Off);
        let spec = BootSpec::container("cache", "redis:7");
        let r = boot_and_await(
            &node,
            &spec,
            &WaitOptions::bounded(
                std::time::Duration::from_millis(20),
                std::time::Duration::from_millis(1),
            ),
        );
        assert!(
            matches!(r, Err(Error::Backend(_))),
            "never-up boot times out"
        );
    }

    #[test]
    fn boot_and_await_rejects_an_invalid_spec_before_booting() {
        // boot_and_await goes through boot(), so an invalid spec is rejected at
        // validate() before the backend (or any wait) is ever touched.
        let node = ScriptedNode::new([PowerState::On], PowerState::On);
        let mut spec = BootSpec::container("bad", "redis:7");
        spec.image = ImageSource::Iso("/boot.iso".into()); // ISO can't boot a container
        let r = boot_and_await(&node, &spec, &tiny_bounded());
        assert!(
            matches!(r, Err(Error::Spec(_))),
            "invalid spec rejected pre-boot"
        );
        assert_eq!(
            node.status_calls.get(),
            0,
            "never awaited an unbooted instance"
        );
    }

    /// A fleet backend where members whose name is in `dead` never power on (their
    /// `status` stays [`PowerState::Off`]) while every other member boots and reports
    /// [`PowerState::On`] — so a *partial-fleet* boot-readback rollup (some up, one
    /// timed out) is testable with no live instances. Keys on `Machine::spec_name`,
    /// which carries the fleet member name.
    struct FleetNode {
        dead: std::collections::HashSet<String>,
    }

    impl FleetNode {
        fn with_dead<'a>(dead: impl IntoIterator<Item = &'a str>) -> Self {
            Self {
                dead: dead.into_iter().map(String::from).collect(),
            }
        }
    }

    impl Boot for FleetNode {
        fn boot(&self, spec: &BootSpec) -> Result<Machine> {
            Ok(Machine::started(format!("id-{}", spec.name), spec))
        }
    }

    impl Lifecycle for FleetNode {
        fn power_on(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn power_off(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn status(&self, m: &Machine) -> Result<PowerState> {
            // A dead member is stuck Off forever (never confirms up); everyone else
            // is On the moment they are polled.
            if self.dead.contains(&m.spec_name) {
                Ok(PowerState::Off)
            } else {
                Ok(PowerState::On)
            }
        }
    }

    #[test]
    fn boot_fleet_and_await_rolls_up_one_dead_member_as_timeout_others_up() {
        // The headline fleet-readback contract: boot a fleet of 3 where the middle
        // member never powers on. The rollup must show node-2 = Timeout while node-1
        // and node-3 = Up, with the aggregate ready/failed tallies correct — a dead
        // node is a per-member verdict, never a hang or an early return.
        //
        // RED-when-broken: this leans on `boot_and_await` confirming On per member
        // AND the Err(Backend)→Timeout classification. Neuter await_power_state's
        // `observed == want` gate (return Ok on the first poll) and node-2 rolls up
        // Up → ready becomes 3, this fails; flip the Timeout classification arm to
        // Error and the `matches!(.., Timeout)` assertion fails.
        let backend = FleetNode::with_dead(["node-2"]);
        let one = BootSpec::redfish_iso("node", "/images/installer.iso", bmc());
        let rollup = boot_fleet_and_await(&one, 3, &backend, &tiny_bounded());

        assert_eq!(
            rollup.members.len(),
            3,
            "one verdict per member, in fleet order"
        );
        assert_eq!(rollup.members[0].name, "node-1");
        assert_eq!(rollup.members[1].name, "node-2");
        assert_eq!(rollup.members[2].name, "node-3");

        // The two healthy members are Up with their live handle...
        match &rollup.members[0].outcome {
            MemberOutcome::Up(m) => assert_eq!(m.id, "id-node-1"),
            other => panic!("node-1 should be Up, was {other:?}"),
        }
        assert!(
            matches!(rollup.members[2].outcome, MemberOutcome::Up(_)),
            "node-3 up"
        );
        // ...and the dead member is a Timeout, not an Up and not a hang.
        assert!(
            matches!(rollup.members[1].outcome, MemberOutcome::Timeout(_)),
            "node-2 never powered on → Timeout, was {:?}",
            rollup.members[1].outcome
        );

        // Aggregate tallies (mutation-verified).
        assert_eq!(
            rollup.ready(),
            2,
            "exactly the two healthy members are ready"
        );
        assert_eq!(rollup.failed(), 1, "exactly the one dead member failed");
        assert!(!rollup.all_ready(), "a partial fleet is not all-ready");
    }

    #[test]
    fn boot_fleet_and_await_reports_a_fully_ready_fleet() {
        // The all-healthy path: every member boots and confirms On, so the rollup is
        // all_ready with ready == n and no failures.
        let backend = FleetNode::with_dead(std::iter::empty());
        let one = BootSpec::container("cache", "redis:7");
        let rollup = boot_fleet_and_await(&one, 4, &backend, &tiny_bounded());
        assert_eq!(rollup.members.len(), 4);
        assert!(rollup
            .members
            .iter()
            .all(|m| matches!(m.outcome, MemberOutcome::Up(_))));
        assert_eq!(rollup.ready(), 4);
        assert_eq!(rollup.failed(), 0);
        assert!(rollup.all_ready(), "a fully-up fleet is all-ready");
    }

    #[test]
    fn boot_fleet_and_await_rolls_up_an_invalid_spec_as_error_not_timeout() {
        // An invalid spec is rejected at validate() inside boot_and_await, before any
        // backend/await — so every member is an Error (hard misconfig), never a
        // Timeout, and the backend is never touched. Distinguishes the two failure
        // buckets: Spec → Error, Backend → Timeout.
        let backend = FleetNode::with_dead(std::iter::empty());
        let mut bad = BootSpec::container("bad", "redis:7");
        bad.image = ImageSource::Iso("/boot.iso".into()); // ISO can't boot a container
        let rollup = boot_fleet_and_await(&bad, 2, &backend, &tiny_bounded());
        assert_eq!(rollup.members.len(), 2);
        assert!(
            rollup
                .members
                .iter()
                .all(|m| matches!(m.outcome, MemberOutcome::Error(_))),
            "an invalid spec rolls up as Error on every member, not Timeout"
        );
        assert_eq!(rollup.ready(), 0);
        assert_eq!(rollup.failed(), 2);
    }

    #[test]
    fn boot_fleet_and_await_of_zero_is_an_empty_not_ready_rollup() {
        // An empty fleet: no members, and all_ready is false (vacuously "nothing up"
        // is not a ready fleet — parity with plan_fleet(0) being empty).
        let backend = FleetNode::with_dead(std::iter::empty());
        let rollup = boot_fleet_and_await(
            &BootSpec::container("c", "redis:7"),
            0,
            &backend,
            &tiny_bounded(),
        );
        assert!(rollup.members.is_empty());
        assert_eq!(rollup.ready(), 0);
        assert_eq!(rollup.failed(), 0);
        assert!(!rollup.all_ready(), "an empty fleet is not all-ready");
    }

    // -- Parallel fleet-boot: identical rollup + proven overlap ------------------

    /// A `Sync` fleet backend that produces a *mixed* rollup with no live instances:
    /// members named in `unbootable` fail at [`Boot::boot`] (`Err(Unsupported)` →
    /// `Error`), members named in `dead` boot but never power on (`status` stays `Off`
    /// → `Timeout`), everyone else comes `Up`. `Sync` (only owned `HashSet`s), so it
    /// drives BOTH the serial and the scoped-thread parallel path — letting the two be
    /// asserted byte-for-byte equal.
    struct MixedNode {
        dead: std::collections::HashSet<String>,
        unbootable: std::collections::HashSet<String>,
    }

    impl Boot for MixedNode {
        fn boot(&self, spec: &BootSpec) -> Result<Machine> {
            if self.unbootable.contains(&spec.name) {
                return Err(Error::Unsupported(format!(
                    "no backend slot for {}",
                    spec.name
                )));
            }
            Ok(Machine::started(format!("id-{}", spec.name), spec))
        }
    }

    impl Lifecycle for MixedNode {
        fn power_on(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn power_off(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn status(&self, m: &Machine) -> Result<PowerState> {
            if self.dead.contains(&m.spec_name) {
                Ok(PowerState::Off)
            } else {
                Ok(PowerState::On)
            }
        }
    }

    #[test]
    fn parallel_fleet_boot_is_identical_to_serial_for_a_mixed_fleet() {
        // The equivalence guard: a mixed fleet (some Up, one Timeout, one Error) MUST
        // roll up to the SAME FleetReadback — same per-member outcomes, same fleet
        // order, same aggregate tallies — whether booted serially or in parallel. The
        // parallel path only changes WHEN members are awaited, never WHAT the rollup
        // says.
        //
        // RED-when-broken: the two share the one `classify_member` step, so any
        // divergence (a reordered join, a different bucket, a dropped member) breaks
        // this `assert_eq!`. Fleet of 5: node-3 dead (→ Timeout), node-5 unbootable
        // (→ Error), node-1/2/4 Up.
        let backend = MixedNode {
            dead: std::collections::HashSet::from(["node-3".to_string()]),
            unbootable: std::collections::HashSet::from(["node-5".to_string()]),
        };
        let spec = BootSpec::container("node", "redis:7");

        let serial = boot_fleet_and_await(&spec, 5, &backend, &tiny_bounded());
        let parallel = boot_fleet_and_await_parallel(&spec, 5, &backend, &tiny_bounded());

        // Byte-for-byte identical rollups (names, order, outcomes, and the carried
        // Machine handles / failure strings all compared by derived PartialEq).
        assert_eq!(
            parallel, serial,
            "parallel rollup must equal the serial rollup"
        );

        // And it is genuinely the mixed shape we intended (not two identical *empty*
        // rollups trivially matching).
        assert_eq!(parallel.members.len(), 5);
        assert!(
            matches!(parallel.members[0].outcome, MemberOutcome::Up(_)),
            "node-1 up"
        );
        assert!(
            matches!(parallel.members[2].outcome, MemberOutcome::Timeout(_)),
            "node-3 timeout"
        );
        assert!(
            matches!(parallel.members[4].outcome, MemberOutcome::Error(_)),
            "node-5 error"
        );
        assert_eq!(parallel.ready(), 3);
        assert_eq!(parallel.failed(), 2);
    }

    /// A `Sync` backend that proves the members are awaited **concurrently**: every
    /// member, on its first `status` poll, bumps a shared "in flight" counter, records
    /// the running peak, then waits (bounded) until all `n` members have entered before
    /// returning `On`. In the parallel path all `n` threads enter together so the peak
    /// reaches `n`; a serial path would only ever have one member in flight (peak 1),
    /// timing out the entry wait instead of hanging.
    struct BarrierNode {
        n: usize,
        in_flight: std::sync::atomic::AtomicUsize,
        peak: std::sync::atomic::AtomicUsize,
    }

    impl BarrierNode {
        fn new(n: usize) -> Self {
            Self {
                n,
                in_flight: std::sync::atomic::AtomicUsize::new(0),
                peak: std::sync::atomic::AtomicUsize::new(0),
            }
        }
    }

    impl Boot for BarrierNode {
        fn boot(&self, spec: &BootSpec) -> Result<Machine> {
            Ok(Machine::started(format!("id-{}", spec.name), spec))
        }
    }

    impl Lifecycle for BarrierNode {
        fn power_on(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn power_off(&self, _m: &Machine) -> Result<()> {
            Ok(())
        }
        fn status(&self, _m: &Machine) -> Result<PowerState> {
            use std::sync::atomic::Ordering::SeqCst;
            let now = self.in_flight.fetch_add(1, SeqCst) + 1;
            self.peak.fetch_max(now, SeqCst);

The file goes on; the console shows all of it.