installtest/src/firmware.rs
//! **The firmware door**: the installer medium under the firmware the CUSTOMER's
//! machine has — SeaBIOS or UEFI — installed, booted, power-cut, booted again.
//!
//! ```text
//! gunnar-installtest installer --target upcloud --iso PATH
//! gunnar-installtest installer --target qemu-local-bios --iso PATH
//! gunnar-installtest installer --firmware seabios --serial none --iso PATH
//! gunnar-installtest installer --firmware uefi --iso PATH
//! ```
//!
//! # `--target <preset>` (2026-08-29)
//!
//! The machine is RENDERED from edda's one roster
//! (`nornir_build_thing::target::presets::PRESETS`, 13 shapes) through
//! [`TargetPreset::qemu_shape`]: firmware, `-machine`, `-cpu`, the disk and NIC
//! device models, the NIC count, the COM-port map (no ttyS0 = `-serial none`
//! in that slot) and whether the medium rides `-cdrom`. A preset whose
//! [`TargetPreset::status`] is `needs-real-cloud` (ENA, gVNIC, Hyper-V, Xen,
//! bare metal) is REFUSED BY NAME with the preset's own reason and the
//! mimicable roster — never booted as a wrong default. `--firmware` and
//! `--serial` stay as explicit overrides ON TOP of the preset. When VGA is the
//! only console the door takes a QMP `screendump` of the guest as the proof
//! medium beside the port probe. Every leg that ran emits its
//! `appliance-boot/<preset>` row (`install_ok` / `serves` / `ready`) through
//! `isoproof::emit_boot_row`, the same writer `emit_kvm_row` is, so
//! `nornir_testmatrix::appliance::ApplianceBootGrid` folds it onto the roster
//! the code hands in. The proof names are the grid's own constants.
//!
//! # The four outcomes, and why a hang is not one of them (measured 2026-08-30)
//!
//! A leg ends `PASS`, `FAIL`, `REFUSED` or `TIMEOUT` ([`Verdict`]), and the
//! door's exit code says which ([`exit_code`]). It used to end `PASS` or `FAIL`,
//! and a hang was a `FAIL` you could only recognise by its 420 000 ms.
//!
//! Two presets HUNG this door, measured that day, 420.10 s and 420.29 s of
//! nothing each:
//!
//! * **`gce-virtio`** booted and the appliance refused CORRECTLY on the UART —
//! `INSTALLER: FAILED hwdetect: no usable target disk` — because the preset's
//! boot disk rides `virtio-scsi` and gunnar's recipe declares
//! `buses = [ide-cdrom, virtio-blk]`. The door watched for one line, `Kernel
//! panic`, so it sat out the whole budget and then blamed the CONSEQUENCES
//! ("no protective MBR signature on the target") of an installer that had
//! refused to touch the disk.
//! * **`ovh-public-cloud`** left **zero bytes** of serial: its roster row brings
//! the medium as an imported qcow2, so no cdrom is attached; the firmware is
//! SeaBIOS, which boots an MBR; and the medium's LBA 0 is all zeros. Nothing
//! on that machine could hand over.
//!
//! Both are answered twice over, and neither by widening a match:
//!
//! 1. [`medium_cannot_install_here`] refuses both from the DECLARATIONS before
//! a byte boots — 0.03 s, naming the bus or the attachment. It is asked of
//! every preset and lets the four the deploy gate runs straight through.
//! 2. `korp_installer::serial::terminal_verdict` (inside `kvmproof::boot_hold`,
//! so no door can forget it) ends a leg the instant the GUEST prints a
//! terminal line — the installer's `FAILED`, either first stage's refusal, a
//! panic — and the row quotes it. Proven live on the machine that hung:
//! **420.10 s → 3.81 s**, `[REFUSED] B1 install 3753 ms the installer ended
//! the boot by name: INSTALLER: FAILED hwdetect: …`.
//!
//! And whatever else happens, a budget that runs out is `[TIMEOUT]` with its own
//! exit code ([`EXIT_TIMED_OUT`]), because nothing was measured and that is a
//! different fact from a measured failure.
//!
//! # Why this exists
//!
//! Every boot proof in this constellation ran under OVMF: `iso.rs` and `kvm.rs`
//! through tunnr's q35+OVMF launch, `installer_ssh.rs` through
//! `korp_installer::kvmproof`'s q35+OVMF doors. UpCloud boots **SeaBIOS only**,
//! attaches the ISO as an **IDE cdrom**, gives the guest **no UART** (VGA is the
//! only console) and **two NICs**. None of those four facts was declared
//! anywhere, and each one cost a blind lane against the real host (2026-08-28:
//! *"noll utdata, installeraren startar aldrig"*; 2026-08-29: the appliance went
//! dark after the kernel because `console=ttyS0` was `/dev/console` on a machine
//! with no ttyS0). The SeaBIOS boot path has existed in code since edda
//! `39cb702`; what did not exist was an automated proof of it. This is that
//! proof, and the hand-rolled `seabios-proof.sh` it replaces is the measurement
//! it reproduces.
//!
//! # The three legs
//!
//! * **B1 install** — the medium as IDE cdrom AND as a read-only virtio drive
//! (the nuked kernel vetoes ATA; the PID-1 finds the volume by `CD001` on
//! virtio), the blank target disk attached FIRST (`vda`), `-boot d`,
//! `-no-reboot`. The installer's own reboot after `INSTALL-OK` exits QEMU.
//! Gate: QEMU exited, no panic, and the DISK carries what an install lays —
//! protective MBR + `korp/cmdline` on the ESP. With a UART, `INSTALL-OK` on
//! serial too.
//! * **B2 serve** — the installed disk alone, no medium, two NICs, `:2222` and
//! `:50051` forwarded. Gate: gunnar's own `SSH-2.0-gunnar` identification on
//! the first and a TCP accept on the second. Then SIGKILL — a power cut.
//! * **B3 power-cut reboot** — B2 again, fresh ports. Same identity, serves
//! again, from a disk that was never flushed.
//!
//! The declaration is the writer for the target disk size and its attachment
//! (`InstallerBuild::target_disk_bytes` / `kvm_disk_args`), the machine argv is
//! `kvmproof::Machine`'s — the SAME writer the UEFI installer proof uses — and
//! the port probe is this crate's own `ssh_banner`. Nothing here is a second
//! spelling of something another module already spells.
use std::io::Read as _;
use std::net::{SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use korp_installer::isoproof::{self, ESP_CMDLINE_PATH, ESP_PARTITION, INSTALL_OK};
use korp_installer::kvmproof::{self, Firmware, Machine};
use nornir_build_thing::installer::InstallerBuild;
use nornir_build_thing::target::presets::{self, PresetStatus, TargetPreset};
use nornir_build_thing::target::Firmware as DeclaredFirmware;
use nornir_build_thing::variant::BuildVariantSet;
use nornir_testmatrix::appliance::APPLIANCE_BOOT_PROOFS;
/// gunnar's SSH listener inside the appliance.
const GUEST_SSH: u16 = 2222;
/// gunnar's control plane inside the appliance. A sealed medium declares
/// `--no-grpc` and this probe stays closed on every interface; an open one
/// binds the PRIVATE interface by name (`--grpc-bind eth1:50051`) and it must
/// then accept on the private NIC and REFUSE on the public one — see
/// [`Door::expect_grpc`] and `boot_installed`.
const GUEST_GRPC: u16 = 50051;
/// The cloud shape: a public and a utility interface (the historical door).
const NICS: usize = 2;
/// The slirp network the SECOND NIC gets. QEMU's default is `10.0.2.0/24` for
/// every `-netdev user`, so two NICs on the default network are two names for
/// one address — and Linux's weak host model then answers a packet arriving on
/// either from a socket bound to the other. On one network the public/private
/// asymmetry below passes whatever the listener did; on two it is real.
const PRIVATE_NET: &str = "10.0.3.0/24";
/// The virtio-blk device word — the one bus `InstallerBuild::kvm_disk_args`
/// (the declaration's writer for the target disk) spells. Any other bus is
/// rendered here as `-drive if=none` + the preset's device.
const VIRTIO_BLK: &str = "virtio-blk-pci";
/// What the medium's own SSH server says first.
const IDENT_PREFIX: &str = "SSH-2.0-gunnar";
/// Which firmware the door boots under. The word on the command line.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FirmwareChoice {
Uefi,
SeaBios,
}
impl FirmwareChoice {
pub fn parse(s: &str) -> Result<Self, String> {
match s.trim().to_ascii_lowercase().as_str() {
"uefi" | "ovmf" => Ok(FirmwareChoice::Uefi),
"seabios" | "bios" | "pc" => Ok(FirmwareChoice::SeaBios),
other => Err(format!("unknown --firmware `{other}`: uefi | seabios")),
}
}
pub fn label(self) -> &'static str {
match self {
FirmwareChoice::Uefi => "uefi",
FirmwareChoice::SeaBios => "seabios",
}
}
}
/// **The machine the door boots**, resolved before anything runs: either the
/// historical flags (`--firmware` + `--serial`, 2 NICs, virtio everywhere,
/// medium as cdrom + virtio) or a roster preset rendered through
/// [`TargetPreset::qemu_shape`], with the flags as overrides on top.
#[derive(Debug, Clone)]
pub struct Shape {
/// The `appliance-boot/<preset>` word when a preset was named; `None` =
/// the flag door, which emits no roster row (it names no shape).
pub preset: Option<&'static str>,
pub firmware: FirmwareChoice,
/// COM1..COM4 — `kvmproof::Machine::uarts`. A port may EXIST without any
/// `console=` naming it (UpCloud's ttyS1/ttyS2): see `serial_console`.
pub uarts: [bool; 4],
/// A `console=` names a UART — the guest WRITES to one, so the door may
/// gate on the serial log (`INSTALL-OK`). False = gate on applied output.
pub serial_console: bool,
pub cpu: Option<String>,
/// `-machine` type when the preset's chipset is not the firmware's default.
pub chipset: Option<String>,
pub disk_device: &'static str,
pub nic_device: &'static str,
pub nics: usize,
/// The medium rides `-cdrom` (IDE) beside its virtio face.
pub medium_as_cdrom: bool,
/// A VGA adapter is a declared console; with no UART it is THE console
/// and the door screendumps it.
pub vga: bool,
}
impl Shape {
/// The pre-preset door, byte-for-byte: `--firmware` + `--serial`.
pub fn flags(firmware: FirmwareChoice, serial: bool) -> Self {
Shape {
preset: None,
firmware,
uarts: if serial {
[true, false, false, false]
} else {
[false; 4]
},
serial_console: serial,
cpu: None,
chipset: None,
disk_device: VIRTIO_BLK,
nic_device: "virtio-net-pci",
nics: NICS,
medium_as_cdrom: true,
vga: false,
}
}
/// A roster preset, rendered — refused by name when QEMU cannot mimic it.
/// `firmware` / `serial` are the explicit overrides (`None` = the preset's).
pub fn from_preset(
name: &str,
firmware: Option<FirmwareChoice>,
serial: Option<bool>,
) -> Result<Self, String> {
let mimicable = || {
presets::PRESETS
.iter()
.filter(|p| p.status() != PresetStatus::NeedsRealCloud)
.map(|p| p.name)
.collect::<Vec<_>>()
.join(" ")
};
let p: &'static TargetPreset = presets::preset(name).ok_or_else(|| {
format!(
"unknown --target `{name}`; the roster: {}",
presets::names().join(" ")
)
})?;
if p.status() == PresetStatus::NeedsRealCloud {
return Err(format!(
"--target {name} needs a real cloud ({}): {} — QEMU cannot mimic it; mimicable presets: {}",
p.cloud,
p.gap().unwrap_or("no QEMU model"),
mimicable()
));
}
let q = p.qemu_shape().ok_or_else(|| {
format!(
"--target {name}: status {} but no QEMU shape renders",
p.status().as_str()
)
})?;
// `Both` = the medium carries both paths; the chipset picks the boot
// firmware (q35 → OVMF, pc → SeaBIOS) unless `--firmware` says.
let firmware = firmware.unwrap_or(match q.firmware {
DeclaredFirmware::Uefi => FirmwareChoice::Uefi,
DeclaredFirmware::SeaBios => FirmwareChoice::SeaBios,
DeclaredFirmware::Both if q.machine == "q35" => FirmwareChoice::Uefi,
DeclaredFirmware::Both => FirmwareChoice::SeaBios,
});
// `--serial uart` adds a ttyS0 AND a console on it (the kernel's
// `console=ttyS0` from the medium's cmdline lands); `--serial none`
// removes every port. Otherwise the preset's map and its `console=`.
let (uarts, serial_console) = match serial {
None => (q.uarts, q.serial),
Some(true) => {
let mut u = q.uarts;
u[0] = true;
(u, true)
}
Some(false) => ([false; 4], false),
};
let default_machine = match firmware {
FirmwareChoice::Uefi => "q35",
FirmwareChoice::SeaBios => "pc",
};
Ok(Shape {
preset: Some(p.name),
firmware,
uarts,
serial_console,
cpu: q.cpu.map(str::to_string),
chipset: (q.machine != default_machine).then(|| q.machine.to_string()),
disk_device: q.disk_device,
nic_device: q.nic_device,
nics: usize::from(q.nics),
medium_as_cdrom: q.medium_as_cdrom,
vga: q.vga,
})
}
pub fn has_uart(&self) -> bool {
self.uarts.iter().any(|p| *p)
}
/// VGA is the only console: the screendump is the proof medium.
pub fn vga_only(&self) -> bool {
self.vga && !self.serial_console
}
/// The one-line description on every report.
pub fn label(&self) -> String {
let uarts: Vec<String> = self
.uarts
.iter()
.enumerate()
.filter(|(_, p)| **p)
.map(|(i, _)| format!("ttyS{i}"))
.collect();
format!(
"{}firmware={} machine={} cpu={} disk={} nic={}×{} uarts=[{}]{} medium={}",
self.preset
.map(|p| format!("target={p} · "))
.unwrap_or_default(),
self.firmware.label(),
self.chipset.as_deref().unwrap_or(match self.firmware {
FirmwareChoice::Uefi => "q35",
FirmwareChoice::SeaBios => "pc",
}),
self.cpu.as_deref().unwrap_or("qemu-default"),
self.disk_device,
self.nic_device,
self.nics,
if uarts.is_empty() {
"none".to_string()
} else {
uarts.join(",")
},
if self.vga_only() {
" console=vga(screendump)"
} else if self.serial_console {
" console=serial"
} else {
" console=none"
},
if self.medium_as_cdrom {
"cdrom+virtio"
} else {
"virtio"
},
)
}
/// The target disk's attachment: the declaration's writer for virtio-blk,
/// else `-drive if=none` + the preset's device model (+ the cell's GPU).
fn disk_args(&self, cell: &InstallerBuild, disk: &str) -> Vec<String> {
if self.disk_device == VIRTIO_BLK {
return cell.kvm_disk_args(disk);
}
let fmt = if disk.ends_with(".qcow2") {
"qcow2"
} else {
"raw"
};
let mut a = vec![
"-drive".to_string(),
format!("file={disk},if=none,id=target,format={fmt}"),
];
match self.disk_device {
"virtio-scsi-pci" => a.extend(
[
"-device",
"virtio-scsi-pci,id=scsi0",
"-device",
"scsi-hd,drive=target,bus=scsi0.0",
]
.map(String::from),
),
"nvme" => a.extend(["-device", "nvme,drive=target,serial=korp0"].map(String::from)),
other => a.extend(["-device".to_string(), format!("{other},drive=target")]),
}
a.extend(cell.kvm_gpu_args());
a
}
}
impl Shape {
/// The proof script's machine: 2 GiB, 2 vCPUs by default — gunnar serving
/// with a git store wants more than the 512 MiB install-only doors give. The
/// COM map, `-cpu` and `-machine` are the shape's (a preset's, or the flags').
///
/// **Sizing overrides** (appliance-rig, 2026-08-31): the real
/// git.example.test appliance is 4 vCPU / 16 GiB, and a proof that a number
/// "runs on appliance hardware" must boot on THAT shape, not the 2/2048
/// install default. `GUNNAR_INSTALLTEST_SMP` and `GUNNAR_INSTALLTEST_MEM_MB`
/// drive the vCPU count and RAM without moving the default — every earlier
/// proof keeps 2/2048, and the rig sets `SMP=4 MEM_MB=16384`. A value that
/// does not parse is ignored (the default stands), never a silent 0.
pub fn machine(&self) -> Result<Machine, String> {
let firmware = match self.firmware {
FirmwareChoice::Uefi => {
let (code, vars) = kvmproof::locate_ovmf()
.ok_or_else(|| "no OVMF CODE+VARS pair (install ovmf/edk2)".to_string())?;
Firmware::Uefi {
code,
vars_template: vars,
}
}
FirmwareChoice::SeaBios => Firmware::SeaBios,
};
let env_u32 = |name: &str, default: u32| -> u32 {
std::env::var(name)
.ok()
.and_then(|v| v.trim().parse::<u32>().ok())
.filter(|&n| n > 0)
.unwrap_or(default)
};
Ok(Machine {
firmware,
uarts: self.uarts,
mem_mb: env_u32("GUNNAR_INSTALLTEST_MEM_MB", 2048),
smp: env_u32("GUNNAR_INSTALLTEST_SMP", 2),
cpu: self.cpu.clone(),
chipset: self.chipset.clone(),
})
}
}
/// One run of the door.
pub struct Door {
pub iso: PathBuf,
pub shape: Shape,
/// Scratch on DISK — a sparse 64 GiB target has no business in `/tmp`.
pub work: PathBuf,
/// Whether `:50051` must accept. `true` for a medium whose declared
/// `korp.args=` binds the control plane off the loopback, else `false`.
pub expect_grpc: bool,
/// **`--grpc-only`**: `Some(role)` proves B2/B3 by `Bootstrap.Status` on the
/// private interface — a build stamp and this declared role — instead of
/// the SSH banner, for a medium that serves no git (`--role twin`). A banner
/// on `:2222` then FAILS the leg. `None` = the historical banner door.
pub grpc_only: Option<String>,
/// How the medium's virtio face is attached — see [`Twin`]. Until edda's
/// twin rule the installer ranked a target-sized twin above the blank disk
/// (a coin toss the enumeration order decided) and a larger one ALWAYS,
/// and wiped it (2026-08-29).
pub twin: Twin,
/// **Extra fixed disks beside the target** (`--disks 75,75,75,75`, GiB),
/// attached after it as `vdb, vdc, …` in BOTH the install and the boot
/// legs — the multi-disk /data rule measured at the door: the
/// smallest disk boots, the largest equal group becomes ONE striped xfs,
/// an odd disk is left untouched. Empty = the single-disk door as before.
pub disks: Vec<u64>,
/// **The appliance-rig hook** (`--drive-cmd PATH`, 2026-08-31): a host
/// program run ONCE in B2 the instant the guest's ports answer — invoked
/// as `PATH <ssh_port> <grpc_private_port> <grpc_public_port>` with the
/// forwarded ports as argv. A non-zero exit FAILS B2 (its stderr is the
/// detail); a zero exit's stdout goes in the leg detail. This is how the
/// full sitetest / bench / torture battery is driven against a live,
/// appliance-shaped guest without hand-rolling a second qemu: the
/// door owns the KVM/OVMF/disks/boot-proof, the script owns the battery.
/// `None` = the historical B2 (probe the ports, power-cut, done).
pub drive_cmd: Option<PathBuf>,
}
/// How the medium's virtio face is attached in B1.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Twin {
/// The ISO file, read-only, after the target — the historical shape.
Iso,
/// A writable sparse image seeded with the ISO bytes and padded to the
/// target's size, attached BEFORE the target — UpCloud's shape: the ISO
/// imported into a 10 G storage beside a 10 G target. Same class, same
/// capacity: class→capacity ranking is decided by enumeration order.
Target,
/// As `Target`, padded to the target's size + 1 GiB: capacity ranking
/// picks it EVERY time — the deterministic negative control for the rule.
Larger,
}
impl Twin {
pub fn parse(s: &str) -> Result<Self, String> {
match s {
"iso" => Ok(Twin::Iso),
"target" => Ok(Twin::Target),
"larger" => Ok(Twin::Larger),
other => Err(format!("unknown --twin `{other}`: iso | target | larger")),
}
}
}
/// **How a leg ended.** Four outcomes, never three: the row and the exit code
/// both have to tell a hang from a failure from a pass.
///
/// The third and fourth were split apart on 2026-08-30, having been one
/// word. `--target gce-virtio` and `--target ovh-public-cloud` each cost
/// **420 s** and then printed `[FAIL] … QEMU never exited … no protective MBR …`
/// — four sentences about consequences, none of them the one the appliance
/// itself had said 400 seconds earlier, and nothing anywhere saying the budget
/// had simply run out. A gate that hangs burns the whole budget, blames the
/// wrong thing, and to anything holding only an exit code it is a pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
/// Every gate this leg has formed.
Pass,
/// A gate did not form, for a reason the DOOR computed (no MBR on the
/// target, nothing answering on `:2222`, the twin wiped).
Failed,
/// **The GUEST ended it, by name** — `korp_installer::serial::terminal_verdict`
/// found a line the appliance, a first stage or the kernel printed and
/// stopped on. Its words are in the detail, verbatim.
Refused,
/// **The budget ran out** with no verdict at all. Never a failure OF the
/// appliance and never, ever a pass: nothing was measured.
TimedOut,
}
impl Verdict {
/// The word in the row. `<ops>/deploy.sh`'s gate greps for these.
pub fn label(self) -> &'static str {
match self {
Verdict::Pass => "PASS",
Verdict::Failed => "FAIL",
Verdict::Refused => "REFUSED",
Verdict::TimedOut => "TIMEOUT",
}
}
}
/// One leg's verdict, with its measurement.
#[derive(Debug, Clone)]
pub struct Leg {
pub name: &'static str,
pub verdict: Verdict,
pub ms: u128,
pub detail: String,
}
impl Leg {
pub fn pass(&self) -> bool {
self.verdict == Verdict::Pass
}
/// Downgrade a leg a later check found wanting. It only ever moves AWAY
/// from `Pass`: a `TimedOut` leg that also has no MBR on its target is
/// still a leg that timed out, and letting a second check overwrite the
/// first would put the hang back where it cannot be seen.
pub fn demote(&mut self, to: Verdict) {
if self.verdict == Verdict::Pass {
self.verdict = to;
}
}
fn line(&self) -> String {
format!(
" [{}] {:<18} {:>8} ms {}",
self.verdict.label(),
self.name,
self.ms,
self.detail
)
}
}
/// The whole ladder. Stops at the first failed leg — B2 on a disk B1 did not
/// write would measure nothing — and hands back every leg that ran.
/// **The red switch for the console gate.** Set it and a machine the
/// declarations already refuse is booted anyway, so the appliance's own refusal
/// can be watched ending the leg. A gate nobody can see fire is not a gate
/// — and after [`medium_cannot_install_here`] landed, `gce-virtio`
/// stops being the live demonstration it was.
pub const BOOT_ANYWAY: &str = "GUNNAR_INSTALLTEST_BOOT_ANYWAY";
/// **Can this medium install on this machine at all?** Asked of the two
/// DECLARATIONS and of the medium's own first sector, before a byte boots.
/// `Some(why)` names every reason, and the door refuses instead of hanging.
///
/// Both reasons were measured on 2026-08-30, each costing **420 s**
/// of nothing per run:
///
/// * **the bus.** `--target gce-virtio` boots its disk on `virtio-scsi`;
/// gunnar's recipe declares `buses = [ide-cdrom, virtio-blk]`, so the
/// appliance kernel carries no SCSI and the target disk is not there. The
/// appliance said exactly that on the UART — `FAILED hwdetect: no usable
/// target disk — every fixed disk carries the installer medium` — and it was
/// RIGHT: a machine that declares a disk on a bus its own medium cannot
/// drive is not a door, it is a contradiction, and it can be read off the
/// two declarations without booting anything.
/// * **the attachment.** `--target ovh-public-cloud` brings its medium as an
/// imported qcow2, so the door attaches no cdrom; the firmware is SeaBIOS,
/// which boots an MBR; and the medium is a pure El Torito ISO whose LBA 0 is
/// **all zeros** (measured: `[510..512] = 00 00`, not `55 AA`). Nothing on
/// that machine is bootable, which is why the run left **zero bytes** of
/// serial. `gce-virtio` shares the no-ISO-attach property but boots anyway:
/// OVMF reads El Torito off any block device. The firmware is the difference,
/// so the check names it rather than refusing every no-cdrom preset.
///
/// Neither refusal is a widened match that makes something pass. Both say the
/// same thing the roster already says — this platform has no installer path,
/// or has one the medium cannot reach — in seconds instead of minutes.
fn medium_cannot_install_here(iso: &Path, shape: &Shape, cell: &InstallerBuild) -> Option<String> {
let name = shape.preset?;
let p = presets::preset(name)?;
let mut why: Vec<String> = Vec::new();
// 1. the bus the machine's boot disk rides, against the buses the MEDIUM
// was built to drive.
if let Some(bus) = p.boot_disk.target_bus() {
if !cell.target.has_bus(bus) {
why.push(format!(
"THE BUS: `{name}` boots its disk on `{}`, and this medium's recipe declares \
`[build.installer.target] buses = [{}]` — so its kernel carries no driver for \
that bus and the target disk is not there to install on. The appliance refuses \
this correctly on its own console (`FAILED hwdetect: no usable target disk`), \
420 s later; the two declarations say it now",
bus.as_str(),
cell.target
.buses
.iter()
.map(|b| b.as_str())
.collect::<Vec<_>>()
.join(", "),
));
}
}
// 2. whether the medium can be REACHED on this machine at all.
if !shape.medium_as_cdrom && shape.firmware == FirmwareChoice::SeaBios {
let lba0 = kvmproof::read_at(iso, 510, 2);
let bootable_mbr = lba0.len() == 2 && (lba0[0], lba0[1]) == (0x55, 0xAA);
if !bootable_mbr {
why.push(format!(
"THE ATTACHMENT: `{name}` takes {}, so the door attaches no cdrom — and this \
medium is a pure El Torito ISO ({} at LBA 0[510..512], not 55 AA), which SeaBIOS \
cannot boot off a hard-disk device. Nothing here hands over: the run leaves ZERO \
bytes of serial and burns its whole budget. This preset has no INSTALLER path; a \
KVM proof of it could only ever prove the INSTALLED disk boots on its device set",
p.medium.describe(),
lba0.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" "),
));
}
}
if why.is_empty() {
return None;
}
Some(format!(
"--target {name} cannot be installed by this medium, and it is the DECLARATIONS that say so, \
not a boot:\n · {}\nSet {BOOT_ANYWAY}=1 to boot it anyway and watch the guest refuse.",
why.join("\n · ")
))
}
pub fn run(door: &Door) -> Result<Vec<Leg>, String> {
// ── preflight, by name ──────────────────────────────────────────────────
if !door.iso.is_file() {
return Err(format!("no medium at {}", door.iso.display()));
}
let qemu = kvmproof::which("qemu-system-x86_64")
.ok_or_else(|| "no qemu-system-x86_64 on PATH".to_string())?;
// `accel=kvm:tcg` falls back to TCG silently, and every number below would
// then be a statement about an emulator. Refuse, like the kvm arm does.
if !Path::new("/dev/kvm").exists() {
return Err("this host has no /dev/kvm; a TCG boot would time every leg wrong".into());
}
let shape = &door.shape;
let machine = shape.machine()?;
// ── the declaration is the writer for the target disk ───────────────────
let cell = declared_installer(&door.iso)?;
// ── the medium against the machine, BEFORE anything boots ───────────────
if let Some(why) = medium_cannot_install_here(&door.iso, shape, &cell) {
if std::env::var_os(BOOT_ANYWAY).is_some() {
eprintln!("installer door · {BOOT_ANYWAY} is set — booting a machine the declarations already refuse:\n{why}");
} else {
return Err(why);
}
}
let _ = std::fs::remove_dir_all(&door.work);
std::fs::create_dir_all(&door.work).map_err(|e| format!("{}: {e}", door.work.display()))?;
let disk = door.work.join("target.img");
kvmproof::create_blank_disk(&disk, cell.target_disk_bytes());
let extras = create_extra_disks(&door.work, &door.disks);
eprintln!(
"installer door · {} · {} GiB target{} · medium {}",
shape.label(),
cell.target_disk_gib,
describe_extras(&door.disks),
door.iso.display()
);
let mut legs = Vec::new();
// ── B1 install ──────────────────────────────────────────────────────────
let (mut b1, run) = install_leg(
&qemu,
&machine,
shape,
&cell,
&door.iso,
&disk,
&extras,
&door.work.join("b1-install"),
door.twin,
"B1 install",
);
match data_set_verdict(&disk, cell.target_disk_bytes(), &extras) {
Ok(d) => b1.detail.push_str(&format!("; {d}")),
Err(e) => {
b1.demote(Verdict::Failed);
b1.detail.push_str(&format!("; DATA SET: {e}"));
}
}
eprintln!("{}", b1.line());
emit_row(shape, 0, &b1);
legs.push(b1);
if !legs[0].pass() {
eprintln!("── B1 serial tail ──\n{}", tail(&run.serial, 30));
return Ok(legs);
}
// ── B2 serve, B3 power-cut reboot ───────────────────────────────────────
for (i, name) in [(2usize, "B2 serve"), (3, "B3 power-cut reboot")] {
let (boot_disk, others) = boot_disk_of(&disk, cell.target_disk_bytes(), &extras);
let boot_arg = boot_disk.display().to_string();
// The rig hook runs in B2 only, and only when `--drive-cmd` was given;
// B3 always probes-then-power-cuts so the power-cut leg keeps its exact
// historical meaning (a fresh boot from a disk that was never flushed).
let grpc_public = isoproof::free_port();
let mut drive = drive_closure(&door.drive_cmd, grpc_public, i == 2);
let leg = boot_installed(
&qemu,
&machine,
shape,
&cell,
&boot_arg,
&others,
&door.work.join(format!("b{i}-boot")),
name,
door.expect_grpc,
door.grpc_only.as_deref(),
drive
.as_mut()
.map(|d| d as &mut dyn FnMut(u16, u16) -> Result<String, String>),
);
eprintln!("{}", leg.line());
emit_row(shape, i - 1, &leg);
let pass = leg.pass();
legs.push(leg);
if !pass {
break;
}
}
Ok(legs)
}
/// The B2 rig hook as a `boot_installed` drive closure: run `cmd` once, with
/// the two forwarded ports (ssh, grpc-private) plus a third the door forwards
/// on the PUBLIC NIC, as argv. `None` when there is no `--drive-cmd` or this is
/// not the B2 leg — the historical probe-only path.
fn drive_closure(
cmd: &Option<PathBuf>,
_grpc_public: u16,
is_b2: bool,
) -> Option<impl FnMut(u16, u16) -> Result<String, String>> {
let cmd = cmd.clone().filter(|_| is_b2)?;
Some(
move |ssh_port: u16, grpc_port: u16| -> Result<String, String> {
eprintln!(
" rig: driving {} against ssh :{ssh_port} grpc :{grpc_port} …",
cmd.display()
);
let out = std::process::Command::new(&cmd)
.arg(ssh_port.to_string())
.arg(grpc_port.to_string())
.output()
.map_err(|e| format!("{}: {e}", cmd.display()))?;
// The battery's own stdout/stderr is the record — stream it up so a
// failing run is legible in the door's log, not just a red leg.
let so = String::from_utf8_lossy(&out.stdout);
let se = String::from_utf8_lossy(&out.stderr);
if !so.trim().is_empty() {
eprintln!("── rig stdout ──\n{}", so.trim_end());
}
if !se.trim().is_empty() {
eprintln!("── rig stderr ──\n{}", se.trim_end());
}
if out.status.success() {
Ok(format!(
"rig {} PASSED ({} B stdout)",
cmd.display(),
out.stdout.len()
))
} else {
Err(format!(
"rig {} exited {}: {}",
cmd.display(),
out.status,
se.trim()
))
}
},
)
}
/// **One run of the installer medium onto `disk`** — B1's body, so the
/// upgrade door (`run_upgrade`) runs it twice on the SAME image with two
/// media. `twin` attaches the medium's virtio face per [`Twin`]; the leg
/// gates on `install_verdict` (UART marker where there is one, the disk
/// always) and on the twin's PVD surviving.
#[allow(clippy::too_many_arguments)]
fn install_leg(
qemu: &Path,
machine: &Machine,
shape: &Shape,
cell: &InstallerBuild,
iso: &Path,
disk: &Path,
extras: &[(PathBuf, u64)],
workdir: &Path,
twin: Twin,
name: &'static str,
) -> (Leg, kvmproof::BootRun) {
let serial = shape.serial_console;
let disk_arg = disk.display().to_string();
// `--twin target`: the medium's virtio face is a writable image of the
// TARGET's size, seeded with the ISO bytes, attached FIRST — so it is `vda`,
// same class and capacity as the blank target `vdb`. Class→capacity ranking
// alone is a coin toss the enumeration order decides in the twin's favour;
// the installer must refuse it by its bytes and pick `vdb`.
let twin_bytes = match twin {
Twin::Iso => None,
Twin::Target => Some(cell.target_disk_bytes()),
Twin::Larger => Some(cell.target_disk_bytes() + (1 << 30)),
};
let twin_img = twin_bytes.map(|bytes| {
let twin = workdir.parent().unwrap_or(workdir).join("twin.img");
std::fs::copy(iso, &twin).expect("seed the twin with the ISO bytes");
std::fs::OpenOptions::new()
.write(true)
.open(&twin)
.and_then(|f| f.set_len(bytes))
.expect("pad the twin");
twin
});
let mut args = Vec::new();
if let Some(twin) = &twin_img {
args.push("-drive".into());
args.push(format!("file={},if=virtio,format=raw", twin.display()));
}
// **On a machine that takes NO cdrom, the MEDIUM IS THE BOOT DISK.** That is
// not a convenience, it is what the platform does: OVH/Scaleway import a
// glance image, GCE a `disk.raw`, AWS a VM Import — you CREATE the instance
// FROM the image and attach the install target as a second volume. Rendering
// it the other way round is a machine no customer has.
//
// MEASURED 2026-08-30 on the ovh-public-cloud shape with a medium
// that DOES carry a hybrid boot sector: with the blank target first, SeaBIOS
// said `Booting from Hard Disk... Boot failed: not a bootable disk` and then
// `No bootable device` — it tries the FIRST disk and does not walk on to the
// second. Same medium, same argv, medium first: `korp-boot-bios: legacy BIOS
// stage`, the kernel off 512-byte LBAs, `INSTALLER: vda: carries the
// installer medium … excluded`, `INSTALLER: INSTALL-OK`.
//
// With `--twin` the medium's face is already the first drive (that is what a
// twin IS), so this only moves the plain case.
let medium_first = twin_img.is_none() && !shape.medium_as_cdrom;
if medium_first {
isoproof::attach_iso_as_virtio(&mut args, iso);
}
args.extend(shape.disk_args(cell, &disk_arg));
attach_extra_disks(&mut args, extras);
if twin_img.is_none() && !medium_first {
isoproof::attach_iso_as_virtio(&mut args, iso);
}
if shape.medium_as_cdrom {
isoproof::attach_cdrom(&mut args, iso);
}
isoproof::attach_slirp_nics_model(&mut args, &[], shape.nics, shape.nic_device);
let qmp = shape.vga_only().then(|| Qmp::attach(&mut args, workdir));
let mut polls = 0u32;
let run = kvmproof::boot_hold(
qemu,
machine,
&args,
workdir,
Duration::from_secs(420),
// End early only on a panic; otherwise the installer's own reboot
// (turned into an exit by `-no-reboot`) ends the leg, so the FAT on P3
// is whatever the guest flushed before IT chose to reboot, not whatever
// QEMU had at the microsecond a marker landed. On a VGA-only shape the
// screen is dumped at 1 s and then every 5 s so the last frame before
// the reboot is kept — the installer's own console, which no UART
// carries.
|_serial| {
polls += 1;
if let Some(q) = &qmp {
if polls == 4 || polls % 20 == 0 {
let _ = q.screendump();
}
}
// No gate of our own: `boot_hold` ends the run on the guest's OWN
// terminal line (a panic among them), so this closure only has to
// keep the screen dumps coming.
false
},
);
let (boot_disk, _) = boot_disk_of(disk, cell.target_disk_bytes(), extras);
let mut leg = install_verdict(&run, &boot_disk, serial);
leg.name = name;
if let Some(q) = &qmp {
leg.detail.push_str(&format!("; {}", q.describe()));
}
if let Some(twin) = &twin_img {
// The twin's PVD (`CD001` at sector 16) must survive B1 untouched: a
// wiped twin is THE bug, whatever the target says.
let magic = kvmproof::read_at(twin, 16 * 2048 + 1, 5);
if magic == b"CD001" {
leg.detail
.push_str("; twin vda intact (CD001 at sector 16), target vdb installed");
} else {
leg.demote(Verdict::Failed);
leg.detail
.push_str("; the medium's TWIN was WIPED (no CD001 at sector 16 on vda)");
}
}
(leg, run)
}
/// The ESP record korp-installer writes (`korp/install`): `mode=upgrade` +
/// the kept `korp-data` span, or `mode=fresh`.
const ESP_INSTALL_RECORD: &str = "korp/install";
/// The `korp-data` partition's slot in the GPT (p3).
const DATA_PARTITION: usize = 2;
/// blake3 over the whole `korp-data` span of `disk` — the measurement that
/// says "kept byte for byte". Sparse holes read fast; a 48 GiB P3 costs
/// seconds, not minutes.
fn hash_data_partition(
disk: &Path,
extras: &[(PathBuf, u64)],
) -> Result<(String, u64, u64), String> {
// A multi-disk box keeps /data on the data disks (`korp-data-<i>`, one
// partition each): the hash is over every member's partition, in order,
// and "moved" means any member's span moved. A single-disk box: p3.
let members: Vec<&(PathBuf, u64)> = extras
.iter()
.filter(|(p, _)| gpt_entry_name(p, 0).starts_with("korp-data-"))
.collect();
if !members.is_empty() {
let mut joined = String::new();
let (mut first0, mut total) = (0u64, 0u64);
for (i, (p, _)) in members.iter().enumerate() {
let (first, last) = kvmproof::gpt_entry(p, 0);
let len = (last - first + 1) * 512;
let mut f = std::fs::File::open(p).map_err(|e| format!("{}: {e}", p.display()))?;
let part = korp_installer::selftest::hash_installed_region(&mut f, first * 512, len)
.map_err(|e| format!("hash {}: {e:#}", p.display()))?;
joined.push_str(&part.to_hex());
joined.push_str(&format!("@{first}+"));
if i == 0 {
first0 = first;
}
total += len;
}
return Ok((joined, first0, total));
}
let (first, last) = kvmproof::gpt_entry(disk, DATA_PARTITION);
if last < first || first == 0 {
return Err(format!(
"no korp-data entry (p{}) on {}",
DATA_PARTITION + 1,
disk.display()
));
}
let mut f = std::fs::File::open(disk).map_err(|e| format!("{}: {e}", disk.display()))?;
let len = (last - first + 1) * 512;
let hash = korp_installer::selftest::hash_installed_region(&mut f, first * 512, len)
.map_err(|e| format!("hash p{}: {e:#}", DATA_PARTITION + 1))?;
Ok((hash.to_hex().to_string(), first, len))
}
/// The extra disks of a door: `data<i>.img`, sparse, beside the target.
fn create_extra_disks(work: &Path, gib: &[u64]) -> Vec<(PathBuf, u64)> {
gib.iter()
.enumerate()
.map(|(i, g)| {
let p = work.join(format!("data{i}.img"));
let bytes = g << 30;
kvmproof::create_blank_disk(&p, bytes);
(p, bytes)
})
.collect()
}
fn describe_extras(gib: &[u64]) -> String {
if gib.is_empty() {
String::new()
} else {
format!(
" + {} extra disk(s) [{}] GiB",
gib.len(),
gib.iter()
.map(|g| g.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
/// `-drive file=…,if=virtio,format=raw` per extra disk, after the target:
/// `vdb, vdc, …` — the same face on the install and the boot legs.
fn attach_extra_disks(args: &mut Vec<String>, extras: &[(PathBuf, u64)]) {
for (p, _) in extras {
args.push("-drive".into());
args.push(format!("file={},if=virtio,format=raw", p.display()));
}
}
/// The UTF-16LE name of GPT entry `index` on `disk` (empty when the slot is unused).
fn gpt_entry_name(disk: &Path, index: usize) -> String {
let e = kvmproof::read_at(disk, 2 * 512 + index as u64 * 128, 128);
if e[..16].iter().all(|&b| b == 0) {
return String::new();
}
let units: Vec<u16> = e[56..128]
.chunks(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.take_while(|&u| u != 0)
.collect();
String::from_utf16_lossy(&units)
}
const MD_SB_MAGIC: u32 = 0xa92b_4efc;
/// **The multi-disk rule, measured on the images after B1.** Expected from
/// the sizes alone (the installer's own rule, restated here so a drift shows
/// as red): the smallest disk (ties → the first attached) carries the ESP;
/// with no other disk it also carries `korp-data` (p3); otherwise the
/// largest equal-size group (1 %) of the rest carries one `korp-data-<i>`
/// each — an md superblock at sector 8 naming the group's size when ≥ 2,
/// the xfs straight on it when 1 — and the boot disk has NO p3; every other
/// disk is untouched (its first sector still zero). Returns the band's fact.
/// **The disk the installer made bootable** — the smallest one, ties by
/// attach order (the installer's own rule, `korp-installer::dataset`). B1's
/// disk gate and B2/B3's boot device follow THIS disk, never `target.img` by
/// habit: measured 2026-08-29 (`--disks 75,75,75,40`), the installer laid the
/// ESP on vde (40 GiB) and the door read vda for the MBR and called it red.
fn boot_disk_of(
target: &Path,
target_bytes: u64,
extras: &[(PathBuf, u64)],
) -> (PathBuf, Vec<(PathBuf, u64)>) {
let mut all: Vec<(PathBuf, u64)> = vec![(target.to_path_buf(), target_bytes)];
all.extend(extras.iter().cloned());
let boot = all
.iter()
.enumerate()
.min_by_key(|(i, d)| (d.1, *i))
.map(|(i, _)| i)
.unwrap();
let boot_path = all[boot].0.clone();
let others: Vec<(PathBuf, u64)> = all
.into_iter()
.enumerate()
.filter(|(i, _)| *i != boot)
.map(|(_, d)| d)
.collect();
(boot_path, others)
}
fn data_set_verdict(
target: &Path,
target_bytes: u64,
extras: &[(PathBuf, u64)],
) -> Result<String, String> {
let mut all: Vec<(PathBuf, u64, String)> =
vec![(target.to_path_buf(), target_bytes, "vda".into())];
for (i, (p, b)) in extras.iter().enumerate() {
all.push((p.clone(), *b, format!("vd{}", (b'b' + i as u8) as char)));
}
let boot = all
.iter()
.enumerate()
.min_by_key(|(i, d)| (d.1, *i))
.map(|(i, _)| i)
.unwrap();
let rest: Vec<usize> = (0..all.len()).filter(|&i| i != boot).collect();
let equal = |a: u64, b: u64| {
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
(hi - lo) * 100 <= hi
};
let mut data: Vec<usize> = Vec::new();
for &pivot in &rest {
let g: Vec<usize> = rest
.iter()
.copied()
.filter(|&i| equal(all[i].1, all[pivot].1))
.collect();
if g.len() > data.len()
|| (g.len() == data.len() && !data.is_empty() && all[pivot].1 > all[data[0]].1)
{
data = g;
}
}
let gib = |b: u64| format!("{}GiB", b >> 30);
let mut why = Vec::new();
let (bp, bb, bn) = &all[boot];
if gpt_entry_name(bp, 0) != "EFI System" {
why.push(format!(
"{bn} ({}) should be the boot disk (smallest) but carries no ESP",
gib(*bb)
));
}
let p3 = gpt_entry_name(bp, DATA_PARTITION);
if data.is_empty() && p3 != "korp-data" {
why.push(format!(
"single disk: {bn} should carry korp-data (p3), has {p3:?}"
));
}
if !data.is_empty() && !p3.is_empty() {
why.push(format!(
"{bn} carries a p3 ({p3}) although /data is on the data disks"
));
}
for (role, &i) in data.iter().enumerate() {
let (p, _, n) = &all[i];
let name = gpt_entry_name(p, 0);
if name != format!("korp-data-{role}") {
why.push(format!("{n}: expected korp-data-{role}, found {name:?}"));
continue;
}
let (first, _) = kvmproof::gpt_entry(p, 0);
if data.len() >= 2 {
let sb = kvmproof::read_at(p, first * 512 + 4096, 256);
let magic = u32::from_le_bytes(sb[0..4].try_into().unwrap());
let raid_disks = u32::from_le_bytes(sb[0x5C..0x60].try_into().unwrap());
let dev = u32::from_le_bytes(sb[0xA0..0xA4].try_into().unwrap());
if magic != MD_SB_MAGIC || raid_disks != data.len() as u32 || dev != role as u32 {
why.push(format!("{n}: md superblock magic {magic:#x} raid_disks {raid_disks} dev {dev} — expected the {}-member set, role {role}", data.len()));
}
} else if kvmproof::read_at(p, first * 512, 4) != b"XFSB" {
why.push(format!(
"{n}: a lone data disk should carry xfs straight on korp-data-0"
));
}
}
let mut odd = Vec::new();
for &i in &rest {
if !data.contains(&i) {
let (p, b, n) = &all[i];
if kvmproof::read_at(p, 0, 512).iter().any(|&x| x != 0) {
why.push(format!(
"{n} ({}) is unequal and should be untouched, but its first sector was written",
gib(*b)
));
} else {
odd.push(format!("{n} {} untouched", gib(*b)));
}
}
}
if !why.is_empty() {
return Err(why.join("; "));
}
let data_fact = if data.is_empty() {
format!("data on {bn} p3")
} else {
format!(
"data {}×{} {} xfs [{}]",
data.len(),
gib(all[data[0]].1),
if data.len() >= 2 { "raid0" } else { "plain" },
data.iter()
.map(|&i| all[i].2.clone())
.collect::<Vec<_>>()
.join(", ")
)
};
Ok(format!(
"boot {bn} {} · {data_fact}{}",
gib(*bb),
if odd.is_empty() {
String::new()
} else {
format!(" · {}", odd.join(", "))
}
))
}
/// Read `korp/install` off the installed ESP.
fn esp_install_record(disk: &Path) -> Option<String> {
let (first, last) = kvmproof::gpt_entry(disk, ESP_PARTITION);
kvmproof::read_fat_file(disk, first, last, ESP_INSTALL_RECORD)
.map(|b| String::from_utf8_lossy(&b).trim().to_string())
}
/// **The upgrade door** — the data-survives rule (edda c09dd54, decided
/// 2026-08-29: data must survive an upgrade) measured end to end:
///
/// * **U1 install (old)** — `old_iso` onto a blank target, exactly B1.
/// * **U2 serve + bootstrap + push** — the installed disk alone; when the ports
/// answer the door bootstraps the admin key over gRPC (`gunnar-cli control
/// bootstrap`), pushes a repository over ssh (create-on-push, a nonce in its
/// README) and reads `control status` = closed. Then a power cut.
/// * **U3 reinstall (new)** — `door.iso` onto the SAME image, no blank. Gates:
/// B1's own verdict, the ESP record says `mode=upgrade`, and blake3 over the
/// whole `korp-data` span is IDENTICAL to the hash taken after U2 (the UART,
/// where there is one, must say `KEPT`).
/// * **U4 serve after upgrade** — `control status` must say closed (never
/// OPEN: a reinstall that re-opened bootstrap is the defect), and the
/// repository pushed in U2 must clone with its nonce intact.
///
/// `cli` is the `gunnar-cli` that drives the guest from the host.
pub fn run_upgrade(door: &Door, old_iso: &Path, cli: &Path) -> Result<Vec<Leg>, String> {
if !door.iso.is_file() {
return Err(format!("no medium at {}", door.iso.display()));
}
if !old_iso.is_file() {
return Err(format!("no OLD medium at {}", old_iso.display()));
}
if !cli.is_file() {
return Err(format!(
"no gunnar-cli at {} (build it: cargo build --release -p gunnar-cli)",
cli.display()
));
}
let qemu = kvmproof::which("qemu-system-x86_64")
.ok_or_else(|| "no qemu-system-x86_64 on PATH".to_string())?;
if !Path::new("/dev/kvm").exists() {
return Err("no /dev/kvm".into());
}
let shape = &door.shape;
let machine = shape.machine()?;
let cell = declared_installer(&door.iso)?;
let _ = std::fs::remove_dir_all(&door.work);
std::fs::create_dir_all(&door.work).map_err(|e| format!("{}: {e}", door.work.display()))?;
let disk = door.work.join("target.img");
kvmproof::create_blank_disk(&disk, cell.target_disk_bytes());
let disk_arg = disk.display().to_string();
let extras = create_extra_disks(&door.work, &door.disks);
eprintln!(
"upgrade door · {} · {} GiB target{} · old medium {} · new medium {}",
shape.label(),
cell.target_disk_gib,
describe_extras(&door.disks),
old_iso.display(),
door.iso.display()
);
let mut legs = Vec::new();
// ── U1 ──
let (mut u1, run) = install_leg(
&qemu,
&machine,
shape,
&cell,
old_iso,
&disk,
&extras,
&door.work.join("u1-install-old"),
door.twin,
"U1 install (old)",
);
match esp_install_record(&disk) {
Some(r) if r.starts_with("mode=fresh") => {
u1.detail.push_str("; ESP korp/install: mode=fresh")
}
Some(r) => u1.detail.push_str(&format!(
"; ESP korp/install: {}",
r.lines().next().unwrap_or_default()
)),
None => u1
.detail
.push_str("; (old medium wrote no korp/install record — pre-rule installer)"),
}
eprintln!("{}", u1.line());
legs.push(u1);
if !legs[0].pass() {
eprintln!("── U1 serial tail ──\n{}", tail(&run.serial, 30));
return Ok(legs);
}
// ── U2: serve, bootstrap, push, status ──
// Absolute: ssh runs from inside the push tree, and a relative `--work`
// would name a key that is not there (measured: `Identity file … not
// accessible`).
let keys = std::path::absolute(door.work.join("keys")).map_err(|e| e.to_string())?;
std::fs::create_dir_all(&keys).map_err(|e| e.to_string())?;
let key = keys.join("admin");
crate::keygen(&key)?;
let ssh = format!(
"ssh -i {} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR",
key.display()
);
let nonce = format!(
"data-survives {} {}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
);
let principal = "upg";
let repo = "survives";
let mut drive_u2 = |ssh_port: u16, grpc_port: u16| -> Result<String, String> {
let at = format!("127.0.0.1:{grpc_port}");
let status = cli_out(cli, &["control", "status", "--at", &at])?;
if !status.contains("bootstrap OPEN") {
return Err(format!("before bootstrap, status must say OPEN:\n{status}"));
}
let key_s = key.display().to_string();
cli_out(
cli,
&["control", "bootstrap", principal, "-i", &key_s, "--at", &at],
)?;
let status = cli_out(cli, &["control", "status", "--at", &at])?;
if !status.contains("bootstrap closed") {
return Err(format!(
"after bootstrap, status must say closed:\n{status}"
));
}
let tree = door.work.join("u2-push");
let _ = std::fs::remove_dir_all(&tree);
std::fs::create_dir_all(&tree).map_err(|e| e.to_string())?;
// sha256: what a create-on-push repository is (server default; post-install.sh pushes the same way).
crate::git(
&tree,
&ssh,
&["init", "-q", "--object-format=sha256", "-b", "main"],
)?;
std::fs::write(tree.join("README"), format!("{nonce}\n")).map_err(|e| e.to_string())?;
crate::git(&tree, &ssh, &["add", "README"])?;
crate::git(
&tree,
&ssh,
&[
"-c",
"user.name=installtest",
"-c",
"user.email=installtest@example.invalid",
"commit",
"-qm",
"data survives",
],
)?;
let url = format!("ssh://{principal}@127.0.0.1:{ssh_port}/{principal}/{repo}");
crate::git(&tree, &ssh, &["push", "-q", &url, "main"])?;
Ok(format!(
"bootstrapped `{principal}` (status closed), pushed {principal}/{repo} with nonce"
))
};
let u2 = boot_installed(
&qemu,
&machine,
shape,
&cell,
&disk_arg,
&extras,
&door.work.join("u2-serve"),
"U2 bootstrap+push",
door.expect_grpc,
None,
Some(&mut drive_u2),
);
eprintln!("{}", u2.line());
let pass = u2.pass();
legs.push(u2);
if !pass {
return Ok(legs);
}
let (p3_before, p3_first, p3_len) = hash_data_partition(&disk, &extras)?;
// ── U3: the NEW medium on the SAME disk ──
let (mut u3, run) = install_leg(
&qemu,
&machine,
shape,
&cell,
&door.iso,
&disk,
&extras,
&door.work.join("u3-reinstall-new"),
door.twin,
"U3 reinstall (new)",
);
let mut why = Vec::new();
match esp_install_record(&disk) {
Some(r) if r.starts_with("mode=upgrade") => {}
Some(r) => why.push(format!(
"ESP korp/install says `{}`, not mode=upgrade",
r.lines().next().unwrap_or_default()
)),
None => why.push("no korp/install record on the ESP after the reinstall".into()),
}
if shape.serial_console
&& !run
.serial
.contains("KEPT; GPT untouched; ESP + rootfs rewritten (mode upgrade")
{
why.push("the UART never said `/data … KEPT … (mode upgrade)`".into());
}
let (p3_after, first_after, len_after) = hash_data_partition(&disk, &extras)?;
if (first_after, len_after) != (p3_first, p3_len) {
why.push(format!(
"korp-data moved: LBA {p3_first}+{p3_len} B → LBA {first_after}+{len_after} B"
));
}
if p3_after != p3_before {
why.push(format!(
"korp-data CHANGED: blake3 {}… → {}…",
&p3_before[..12],
&p3_after[..12]
));
}
if !why.is_empty() {
u3.demote(Verdict::Failed);
u3.detail = format!("{}; {}", why.join("; "), u3.detail);
} else {
u3.detail.push_str(&format!(
"; ESP korp/install: mode=upgrade; korp-data p{} ({} GiB at LBA {}) blake3 {}… IDENTICAL before/after",
DATA_PARTITION + 1,
p3_len >> 30,
p3_first,
&p3_after[..12]
));
}
eprintln!("{}", u3.line());
let pass = u3.pass();
legs.push(u3);
if !pass {
eprintln!("── U3 serial tail ──\n{}", tail(&run.serial, 40));
return Ok(legs);
}
// ── U4: serve after the upgrade — closed, and the repo is there ──
let mut drive_u4 = |ssh_port: u16, grpc_port: u16| -> Result<String, String> {
let at = format!("127.0.0.1:{grpc_port}");
let status = cli_out(cli, &["control", "status", "--at", &at])?;
if status.contains("bootstrap OPEN") {
return Err(format!(
"bootstrap is OPEN after the upgrade — /data did not survive:\n{status}"
));
}
if !status.contains("bootstrap closed") {
return Err(format!("status after the upgrade:\n{status}"));
}
let tree = door.work.join("u4-clone");
let _ = std::fs::remove_dir_all(&tree);
std::fs::create_dir_all(&tree).map_err(|e| e.to_string())?;
let url = format!("ssh://{principal}@127.0.0.1:{ssh_port}/{principal}/{repo}");
crate::git(&tree, &ssh, &["clone", "-q", &url, "c"])?;
let readme = std::fs::read_to_string(tree.join("c").join("README"))
.map_err(|e| format!("README after clone: {e}"))?;
if readme.trim() != nonce {
return Err(format!(
"the cloned README is not the pushed one: {readme:?} vs {nonce:?}"
));
}
Ok(format!("status closed (enrolled `{principal}` from U2); {principal}/{repo} cloned, nonce intact"))
};
let u4 = boot_installed(
&qemu,
&machine,
shape,
&cell,
&disk_arg,
&extras,
&door.work.join("u4-serve"),
"U4 serve after upgrade",
door.expect_grpc,
None,
Some(&mut drive_u4),
);
eprintln!("{}", u4.line());
legs.push(u4);
Ok(legs)
}
/// Run `gunnar-cli` on the host and hand back stdout; a non-zero exit is an
/// error carrying stderr.
fn cli_out(cli: &Path, args: &[&str]) -> Result<String, String> {
let out = std::process::Command::new(cli)
.args(args)
.output()
.map_err(|e| format!("{} {}: {e}", cli.display(), args.join(" ")))?;
if !out.status.success() {
return Err(format!(
"gunnar-cli {} failed ({}): {} {}",
args.join(" "),
out.status,
String::from_utf8_lossy(&out.stdout).trim(),
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
/// The `appliance-boot/<preset>` row for one leg — only when a preset was
/// named (the flag door names no shape). Proof names are the grid's own.
fn emit_row(shape: &Shape, leg: usize, l: &Leg) {
if let Some(preset) = shape.preset {
let detail = format!(
"{} ms; {}",
l.ms,
l.detail.lines().next().unwrap_or_default()
);
isoproof::emit_boot_row(preset, APPLIANCE_BOOT_PROOFS[leg], l.pass(), &detail);
}
}
// ── QMP screendump: the proof medium when VGA is the only console ───────────
/// A QMP control socket on the guest, for `screendump`. Pure std: a unix
/// stream, the capabilities handshake, one command. Nothing here asserts —
/// the dump is EVIDENCE beside the port probe, not the gate.
struct Qmp {
sock: PathBuf,
ppm: PathBuf,
}
impl Qmp {
fn attach(args: &mut Vec<String>, workdir: &Path) -> Self {
std::fs::create_dir_all(workdir).expect("create the boot workdir");
let sock = workdir.join("qmp.sock");
let _ = std::fs::remove_file(&sock);
args.push("-qmp".into());
args.push(format!("unix:{},server,nowait", sock.display()));
Qmp {
sock,
ppm: workdir.join("vga.ppm"),
}
}
/// One `screendump` to `vga.ppm` (overwritten). `Err` when the socket is
/// not up yet or QEMU refused — the caller just tries again later.
fn screendump(&self) -> Result<(), String> {
use std::io::{BufRead, BufReader, Write};
let mut s =
std::os::unix::net::UnixStream::connect(&self.sock).map_err(|e| e.to_string())?;
s.set_read_timeout(Some(Duration::from_secs(5)))
.map_err(|e| e.to_string())?;
let mut r = BufReader::new(s.try_clone().map_err(|e| e.to_string())?);
let mut line = String::new();
r.read_line(&mut line).map_err(|e| e.to_string())?; // the greeting
s.write_all(b"{\"execute\":\"qmp_capabilities\"}\n")
.map_err(|e| e.to_string())?;
Self::reply(&mut r)?;
let cmd = format!(
"{{\"execute\":\"screendump\",\"arguments\":{{\"filename\":\"{}\"}}}}\n",
self.ppm.display()
);
s.write_all(cmd.as_bytes()).map_err(|e| e.to_string())?;
Self::reply(&mut r)
}
/// Read lines until a `return` or an `error` (events in between are skipped).
fn reply(r: &mut impl std::io::BufRead) -> Result<(), String> {
loop {
let mut line = String::new();
if r.read_line(&mut line).map_err(|e| e.to_string())? == 0 {
return Err("QMP closed".into());
}
if line.contains("\"return\"") {
return Ok(());
}
if line.contains("\"error\"") {
return Err(line.trim().to_string());
}
}
}
/// What the last dump shows: its size and whether anything was drawn.
/// A PPM is `P6\n<w> <h>\n255\n` + RGB bytes; "blank" = every pixel black.
fn describe(&self) -> String {
let Ok(bytes) = std::fs::read(&self.ppm) else {
return "VGA screendump: none taken".into();
};
let mut fields = 0;
let mut at = 0;
while fields < 4 && at < bytes.len() {
while at < bytes.len() && !bytes[at].is_ascii_whitespace() {
at += 1;
}
fields += 1;
at += 1;
}
let lit = bytes
.get(at..)
.map(|px| px.iter().filter(|b| **b > 0x20).count())
.unwrap_or(0);
format!(
"VGA screendump {} ({} B, {} lit bytes{})",
self.ppm.display(),
bytes.len(),
lit,
if lit == 0 { " — BLANK" } else { "" }
)
}
}
/// **What the medium says about itself** — read off the ISO, not off its name.
///
/// Two facts, both already on every composed medium and neither of them a new
/// thing to write:
///
/// * the **ISO-9660 volume identifier** in the primary volume descriptor
/// (`GUNNAR_INSTALL` for an installer, `GUNNAR_APPLIANCE` for a live
/// appliance) — one 2 KiB read at LBA 16;
/// * the **`korp/cmdline` on the ESP**, the exact kernel command line this
/// medium boots, which is what tells one installer row from another:
/// `korp.args=serve,--no-grpc,…` on the sealed medium against
/// `korp.args=serve,--grpc-bind,eth1:50051` on the open one.
///
/// `None` when the path is not a readable ISO-9660 image at all. A partial
/// answer is normal and useful: an image whose ESP cannot be read still names
/// its volume, and the caller narrows on what it has.
struct MediumIdentity {
volume_id: String,
cmdline: Option<String>,
}
/// The ESP image the EFI half of the medium boots from, by the name the ISO
/// writer gives it (`nornir_build_thing::image_build`: `w.set_efi_boot(
/// "efiboot.img")`). Matched case-insensitively and without the ISO-9660
/// `;1` version suffix, because that is what the directory record holds.
pub(crate) const ESP_IMAGE_NAME: &str = "efiboot.img";
/// One 2 048-byte ISO block.
pub(crate) const ISO_BLOCK: u64 = 2048;
pub(crate) fn read_range(iso: &Path, offset: u64, len: usize) -> Option<Vec<u8>> {
use std::io::{Read, Seek, SeekFrom};
let mut f = std::fs::File::open(iso).ok()?;
f.seek(SeekFrom::Start(offset)).ok()?;
let mut buf = vec![0u8; len];
f.read_exact(&mut buf).ok()?;
Some(buf)
}
/// `(extent_lba, data_len)` of a file in the ISO's ROOT directory, by name.
///
/// One level deep on purpose: the ESP image is a root-level file on every
/// medium this door drives, and a recursive walk would be code with no caller.
pub(crate) fn iso_root_file(iso: &Path, want: &str) -> Option<(u64, u64)> {
let pvd = read_range(iso, 16 * ISO_BLOCK, ISO_BLOCK as usize)?;
if pvd.first() != Some(&1) || &pvd[1..6] != b"CD001" {
return None;
}
// The root directory record is embedded in the PVD at byte 156.
let root_lba = u32::from_le_bytes(pvd[158..162].try_into().ok()?) as u64;
let root_len = u32::from_le_bytes(pvd[166..170].try_into().ok()?) as usize;
let dir = read_range(iso, root_lba * ISO_BLOCK, root_len)?;
let mut at = 0usize;
while at < dir.len() {
let rec_len = dir[at] as usize;
if rec_len == 0 {
// Padding to the end of this block; records never straddle one.
at = (at / ISO_BLOCK as usize + 1) * ISO_BLOCK as usize;
continue;
}
let rec = dir.get(at..at + rec_len)?;
let name_len = *rec.get(32)? as usize;
let name = rec.get(33..33 + name_len)?;
// `.` and `..` are the one-byte names 0x00 and 0x01.
let name = String::from_utf8_lossy(name);
let name = name.split(';').next().unwrap_or_default();
if name.eq_ignore_ascii_case(want) {
let lba = u32::from_le_bytes(rec.get(2..6)?.try_into().ok()?) as u64;
let len = u32::from_le_bytes(rec.get(10..14)?.try_into().ok()?) as u64;
return Some((lba, len));
}
at += rec_len;
}
None
}
fn medium_identity(iso: &Path) -> Option<MediumIdentity> {
let pvd = read_range(iso, 16 * ISO_BLOCK, ISO_BLOCK as usize)?;
if pvd.first() != Some(&1) || &pvd[1..6] != b"CD001" {
return None;
}
let volume_id = String::from_utf8_lossy(&pvd[40..72]).trim().to_string();
// The ESP, and the command line inside it. Both halves are allowed to
// fail: a medium that names its volume and hides its ESP is still a
// medium that named its volume.
let cmdline = iso_root_file(iso, ESP_IMAGE_NAME)
.and_then(|(lba, len)| read_range(iso, lba * ISO_BLOCK, usize::try_from(len).ok()?))
.and_then(|esp| kvmproof::read_esp_image_file(&esp, ESP_CMDLINE_PATH))
.map(|b| String::from_utf8_lossy(&b).trim().to_string());
Some(MediumIdentity { volume_id, cmdline })
}
/// Does this declared cell's command line **fit** the one the medium boots?
///
/// Every token the cell declares must be a token of the medium's cmdline. Not
/// equality: the composer adds tokens of its own (`initrd=`,
/// `korp.installer_root=`, the payload's `korp.znippy=`), and a cell that
/// declares a subset of what was composed is still the cell it was composed
/// from. What it does discriminate is exactly what differs between two rows —
/// `korp.args=serve,--no-grpc,…` is not a token of a medium that boots
/// `korp.args=serve,--grpc-bind,eth1:50051`.
fn cell_fits_cmdline(cell: &InstallerBuild, cmdline: &str) -> bool {
let on_medium: Vec<&str> = cmdline.split_whitespace().collect();
cell.effective_boot_params()
.iter()
.all(|p| on_medium.iter().any(|t| *t == p))
}
/// The `[[build.installer]]` this medium was composed from — **by the medium,
/// then by the file stem**.
///
/// # Why not the stem alone
///
/// It used to be the stem alone (`.iso-out/<name>.iso`), and a renamed file
/// was therefore a refused run: *"no `[[build.installer]]` named X"*. That has
/// cost two lanes a run, and it is the wrong question to ask — a copy of a
/// medium under another name is the same medium, and the identity is **on** it:
/// the ISO-9660 volume id and the `korp/cmdline` on its ESP
/// ([`medium_identity`]).
///
/// The order is: narrow the declared rows to the ones whose `volume_id` the
/// medium carries, narrow again to the ones whose declared boot parameters the
/// medium's own command line contains, and take a **unique** survivor. Anything
/// less than unique falls back to the stem with a named warning, because a
/// guess between two rows is worse than the old refusal — and a stem that
/// matches is still the composer's own convention and is right almost always.
///
/// Refused by name when neither answers, and the refusal says **both** what
/// the medium said and which cells are declared.
fn declared_installer(iso: &Path) -> Result<InstallerBuild, String> {
let root = crate::workspace_root();
let set = BuildVariantSet::discover(&root)
.map_err(|e| format!("reading gunnar's declaration under {}: {e}", root.display()))?
.ok_or_else(|| format!("gunnar declares no build space under {}", root.display()))?;
let stem = iso
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
// ── (1) the medium ──────────────────────────────────────────────────────
let identity = medium_identity(iso);
let mut medium_said = "the medium could not be read as an ISO-9660 image".to_string();
if let Some(id) = &identity {
let by_volume: Vec<&InstallerBuild> = set
.installers
.iter()
.filter(|i| i.volume_id == id.volume_id)
.collect();
let narrowed: Vec<&InstallerBuild> = match &id.cmdline {
Some(c) => by_volume
.iter()
.copied()
.filter(|i| cell_fits_cmdline(i, c))
.collect(),
None => by_volume.clone(),
};
medium_said = format!(
"the medium says volume_id={:?}{}, which fits {}",
id.volume_id,
match &id.cmdline {
Some(_) => " and carries a korp/cmdline",
None => " and no readable korp/cmdline",
},
if narrowed.is_empty() {
"no declared installer".to_string()
} else {
narrowed
.iter()
.map(|i| i.name.as_str())
.collect::<Vec<_>>()
.join(", ")
}
);
if let [only] = narrowed.as_slice() {
if only.name != stem {
eprintln!(
" note: the medium {} is named `{stem}` but IS `{}` — resolved off the \
volume id and its korp/cmdline, not off the filename.",
iso.display(),
only.name
);
}
return Ok((*only).clone());
}
eprintln!(
" warning: {medium_said}; falling back to the file stem `{stem}`. A medium that \
cannot name exactly one declared installer is resolved by its filename, which is \
the composer's convention and not a fact about the bytes."
);
} else if iso.exists() {
eprintln!(
" warning: {} is not a readable ISO-9660 image, so its `[[build.installer]]` is \
resolved by the file stem `{stem}` instead of by the medium.",
iso.display()
);
}
// ── (2) the stem ────────────────────────────────────────────────────────
set.installers
.iter()
.find(|i| i.name == stem)
.cloned()
.ok_or_else(|| {
format!(
"no `[[build.installer]]` named `{stem}` (the medium's file stem), and {medium_said} \
— declared: {}. Either name the ISO after the cell it was composed from, or \
compose it from a cell whose volume_id and boot parameters this medium carries.",
set.installers.iter().map(|i| i.name.as_str()).collect::<Vec<_>>().join(", ")
)
})
}
/// B1's gate, read off the DISK (and the UART when there is one).
fn install_verdict(run: &kvmproof::BootRun, disk: &Path, serial: bool) -> Leg {
// The guest's own terminal line is the WHOLE answer and it arrives first:
// every gate below is downstream of a boot that stopped. Report it verbatim
// and stop — a row that instead says "no protective MBR signature" about an
// installer that refused to touch the disk blames the consequence.
if let Some(t) = &run.terminal {
return Leg {
name: "B1 install",
verdict: Verdict::Refused,
ms: run.elapsed.as_millis(),
detail: t.say(),
};
}
if run.timed_out {
return Leg {
name: "B1 install",
verdict: Verdict::TimedOut,
ms: run.elapsed.as_millis(),
detail: format!(
"NO VERDICT: the budget ran out with the guest still running and nothing terminal on \
the console ({} B of serial). Nothing was measured — this is not a failed install, \
it is an unmeasured one.{}",
run.serial.len(),
if run.serial.is_empty() {
" ZERO serial bytes: the medium never handed over — firmware, not appliance."
} else {
""
}
),
};
}
let mut why = Vec::new();
match run.exited {
Some(_) => {}
None => {
why.push("QEMU never exited: the installer did not reboot within the budget".into())
}
}
if serial && !run.serial.contains(INSTALL_OK) {
why.push(format!("no `{INSTALL_OK}` on the UART"));
}
let mbr = kvmproof::read_at(disk, 510, 2);
if mbr.len() != 2 || (mbr[0], mbr[1]) != (0x55, 0xAA) {
why.push("no protective MBR signature on the target".into());
}
let cmdline = {
let (first, last) = kvmproof::gpt_entry(disk, ESP_PARTITION);
kvmproof::read_fat_file(disk, first, last, ESP_CMDLINE_PATH)
.map(|b| String::from_utf8_lossy(&b).trim().to_string())
};
match &cmdline {
Some(c) if c.contains("korp.entry=") => {}
Some(c) => why.push(format!("the installed cmdline names no product: {c}")),
None => why.push(format!("no {ESP_CMDLINE_PATH} on the installed ESP")),
}
let detail = if why.is_empty() {
format!(
"{}; installed cmdline: {}",
if serial {
"INSTALL-OK on the UART, then the installer rebooted"
} else {
"no UART — gated on the disk: MBR + korp/cmdline"
},
cmdline.unwrap_or_default()
)
} else {
why.join("; ")
};
Leg {
name: "B1 install",
verdict: if why.is_empty() {
Verdict::Pass
} else {
Verdict::Failed
},
ms: run.elapsed.as_millis(),
detail,
}
}
/// **Is a gRPC server BOUND behind this forwarded port?**
///
/// ★ A bare `TcpStream::connect` cannot answer this and never could. QEMU's
/// `hostfwd` is a real listening socket ON THE HOST: `connect()` to it
/// succeeds whatever is or is not bound inside the guest, and the failure only
/// shows up afterwards as an immediate EOF. So `:50051 accepts` was true for
/// every medium this door has ever driven — a green that could not go red.
/// MEASURED 2026-08-31: with the appliance's listener provably on
/// `10.0.3.15:50051` (its own log line), the forward on the OTHER NIC still
/// accepted.
///
/// The applied output instead: an h2 server sends its SETTINGS frame as the
/// server connection preface, unprompted, the moment the connection is up
/// (RFC 9113 §3.4). Measured against the live appliance, that is 27 bytes
/// arriving with nothing sent first. A forward to nothing yields EOF.
///
/// Bytes read = a server is bound. Zero bytes = there is not one.
fn grpc_speaks(port: u16) -> bool {
let addr: SocketAddr = ([127, 0, 0, 1], port).into();
let Ok(mut sock) = TcpStream::connect_timeout(&addr, Duration::from_millis(500)) else {
return false;
};
if sock
.set_read_timeout(Some(Duration::from_millis(1200)))
.is_err()
{
return false;
}
let mut buf = [0u8; 64];
matches!(sock.read(&mut buf), Ok(n) if n > 0)
}
/// **How many interfaces this medium brings up**, delegated to
/// [`gunnar_recipe::declared_interface_count`] — the one reader of
/// `korp.env.GUNNAR_NET`, which is the declaration that owns the fact.
///
/// The rig carried its own copy of this parse until 2026-09-06. Its own doc
/// had already argued that a second place for the number would drift; a second
/// place for the PARSE is the same defect one level down, and the gate in
/// `gunnar-recipe` that refuses a door on an unconfigured interface must agree
/// with the rig that boots one, exactly.
///
/// **Not** the option's `bakes.when_on` (which is the value that actually
/// reaches the medium): this reads the CELL as declared, which is what a door
/// is handed. They are held equal by `recipe.rs`, which spells both from
/// `CLOUD_GUNNAR_NET`.
fn declared_interface_count(cell: &InstallerBuild) -> usize {
usize::from(gunnar_recipe::declared_interface_count(&cell.boot_params))
}
/// B2/B3: the installed disk alone, probed over the forwarded ports, then a
/// power cut (the door's kill IS the cut — no ACPI, no flush).
fn boot_installed(
qemu: &Path,
machine: &Machine,
shape: &Shape,
cell: &InstallerBuild,
disk_arg: &str,
extras: &[(PathBuf, u64)],
workdir: &Path,
name: &'static str,
expect_grpc: bool,
// `--grpc-only`: the role the medium declares. The leg is then proved by
// `Bootstrap.Status` on the private interface (build + role), not a banner.
grpc_only: Option<&str>,
// The upgrade door's hands on the guest: run ONCE when the ports answer,
// its `Err` fails the leg, its `Ok` text goes in the detail.
mut drive: Option<&mut dyn FnMut(u16, u16) -> Result<String, String>>,
) -> Leg {
let ssh_port = isoproof::free_port();
// TWO probes of :50051, one per interface. The public one is the whole
// point: `gunnar control repo list --at <public>:50051` answered an
// uncredentialled caller from the desktop on 2026-08-31, and the only way a door
// can say that is closed is to ask on the public side and be REFUSED.
let grpc_public_port = isoproof::free_port();
let grpc_private_port = isoproof::free_port();
let mut args = shape.disk_args(cell, disk_arg);
attach_extra_disks(&mut args, extras);
assert!(
!args.iter().any(|a| a.ends_with(".iso") || a == "-cdrom"),
"the boot leg must carry NO medium: {args:?}"
);
// **The machine must be at least what the MEDIUM configures.** A preset's
// `nics` is what a cloud hands out; the medium's `korp.env.GUNNAR_NET`
// names the interfaces it brings up, and a medium that configures two and
// binds its control plane on the second cannot serve on a machine with
// one. Proving it on such a machine is not a test of the product — it goes
// red for a reason that has nothing to do with the appliance (MEASURED
// 2026-08-31 on a one-NIC qemu-local door: `no interface named "eth1"`,
// then the init panic).
let declared_nics = declared_interface_count(cell);
let nics = shape.nics.max(declared_nics);
if nics > shape.nics {
eprintln!(
" machine: {nics} NIC(s) — the preset offers {}, and the medium's GUNNAR_NET names \
{declared_nics}",
shape.nics
);
}
let specs: Vec<isoproof::SlirpNic> = (0..nics)
.map(|n| isoproof::SlirpNic {
// n0 keeps QEMU's default network so every existing forward and
// every existing expectation is byte-identical; each later NIC gets
// its own /24 so an address is one interface's and not both's.
net: (n > 0).then(|| format!("10.0.{}.0/24", n + 2)),
hostfwds: match n {
0 => vec![(ssh_port, GUEST_SSH), (grpc_public_port, GUEST_GRPC)],
1 => vec![(grpc_private_port, GUEST_GRPC)],
_ => Vec::new(),
},
})
.collect();
debug_assert_eq!(specs[0].net, None, "n0 must stay on QEMU's default network");
isoproof::attach_slirp_nic_specs(&mut args, &specs, shape.nic_device);
// With one NIC there is no private side to ask, so the private probe is the
// public one and the asymmetry below is not asserted.
let has_private_nic = nics > 1;
let qmp = shape.vga_only().then(|| Qmp::attach(&mut args, workdir));
let mut banner: Option<String> = None;
let mut grpc_open = false;
let mut grpc_public_open = false;
// `--grpc-only`: the verdict on the first `Bootstrap.Status` answer.
let mut control: Option<Result<String, String>> = None;
let mut ready_ms: Option<u128> = None;
let mut driven: Option<Result<String, String>> = None;
let mut polls = 0u32;
let started = Instant::now();
let run = kvmproof::boot_hold(
qemu,
machine,
&args,
workdir,
Duration::from_secs(180),
|_serial| {
polls += 1;
// Every 5 s on a VGA-only shape: a boot that never answers leaves its
// LAST frame behind — the dark-boot evidence a UART-less cloud's VNC
// is the only other way to see.
if let Some(q) = &qmp {
if polls == 4 || polls % 20 == 0 {
let _ = q.screendump();
}
}
if banner.is_none() {
let addr: SocketAddr = ([127, 0, 0, 1], ssh_port).into();
if let Ok(b) = crate::ssh_banner(&addr) {
if b.starts_with("SSH-") {
banner = Some(b);
}
}
}
let formed = if let Some(want) = grpc_only {
// No banner to wait for: a twin binds its forge listener to
// loopback. The control plane names who answers (src/control.rs).
if control.is_none() {
let private: SocketAddr = ([127, 0, 0, 1], grpc_private_port).into();
if let Ok(a) =
crate::control::bootstrap_status(private, Duration::from_millis(1500))
{
grpc_open = true;
control = Some(crate::control::grpc_only_verdict(&a, want));
}
}
if control.is_some() && !grpc_public_open {
grpc_public_open = grpc_speaks(grpc_public_port);
}
control.is_some()
} else {
if banner.is_some() && !grpc_open {
grpc_open = grpc_speaks(grpc_private_port);
}
// The PUBLIC side, asked every poll and never gated on the private one:
// "closed" must be an answer this loop can form on its own, or a public
// listener that came up first would be recorded as absent.
if banner.is_some() && !grpc_public_open {
grpc_public_open = grpc_speaks(grpc_public_port);
}
banner.is_some() && (grpc_open || !expect_grpc)
};
if formed && ready_ms.is_none() {
ready_ms = Some(started.elapsed().as_millis());
// The console as a human on the cloud's VNC would see it, at the
// moment the ports answered.
if let Some(q) = &qmp {
let _ = q.screendump();
}
if let Some(d) = drive.as_mut() {
driven = Some(d(ssh_port, grpc_private_port));
}
}
formed
},
);
// The guest refused or died by name, and it said why — quote it, and do not
// bury it under "nothing answered on :2222", which is merely what follows.
if let Some(t) = &run.terminal {
return Leg {
name,
verdict: Verdict::Refused,
ms: ready_ms.unwrap_or(run.elapsed.as_millis()),
detail: format!(
"{}{}\n── serial tail ──\n{}",
t.say(),
qmp.as_ref()
.map(|q| format!("; {}", q.describe()))
.unwrap_or_default(),
tail(&run.serial, 20)
),
};
}
if run.timed_out && banner.is_none() && control.is_none() {
return Leg {
name,
verdict: Verdict::TimedOut,
ms: run.elapsed.as_millis(),
detail: format!(
"NO VERDICT: the budget ran out — nothing ever answered on :{GUEST_SSH} and the \
console printed nothing terminal ({} B of serial){}\n── serial tail ──\n{}",
run.serial.len(),
qmp.as_ref()
.map(|q| format!("; {}", q.describe()))
.unwrap_or_default(),
tail(&run.serial, 20)
),
};
}
let mut why = Vec::new();
if let Some(want) = grpc_only {
match &control {
Some(Ok(_)) => {}
Some(Err(e)) => why.push(e.clone()),
None => why.push(format!(
":{GUEST_GRPC} never answered Bootstrap.Status on the PRIVATE interface within the budget"
)),
}
if let Some(b) = &banner {
why.push(format!(
"a --grpc-only medium (role {want}) serves git on :{GUEST_SSH}: {b:?} — a twin binds \
its forge listener to loopback"
));
}
} else {
match &banner {
Some(b) if b.starts_with(IDENT_PREFIX) => {}
Some(b) => why.push(format!(
"the listener on :{GUEST_SSH} is not gunnar's: {b:?}"
)),
None => why.push(format!(
"nothing answered on :{GUEST_SSH} within the budget"
)),
}
}
if grpc_only.is_none() && expect_grpc && !grpc_open {
why.push(format!(
":{GUEST_GRPC} never accepted on the PRIVATE interface — the medium declares \
`--grpc-bind eth1:50051` and either eth1 never took a lease or the listener \
refused to bind (the serial says which)"
));
}
// **THE ASYMMETRY.** A control plane on the interface that faces the
// internet is the defect this door exists to catch, and the reason is
// `Bootstrap`: it is the ONE service mounted outside the auth gate (a gated
// `Bootstrap` on a server with an empty key table could be reached by
// nobody), so on a keyless medium `Bootstrap.Enroll` is open to whoever
// reaches the port first, and that caller becomes gunnar-admin. A wildcard
// bind passes every other assertion in this function — it accepts on the
// private side too — so this is the only line that can tell `eth1:50051`
// from `0.0.0.0:50051`.
//
// ⚠ This comment used to justify itself with "on git.example.test, `repo
// list`, `user list` and `repo create` all answered a caller from the desktop with
// NO credential (2026-08-31)". That is FALSE and the correction is recorded
// in `gunnar-build-thing/src/recipe.rs::OPEN_KORP_ARGS`: `gunnar control`
// signs with `~/.ssh/id_ed25519` when `-i` is absent, and the desktop's key is
// enrolled on that box as gunnar-admin. Re-measured with a raw gRPC request
// carrying no metadata, every gated service answered `grpc-status: 16`.
// The leg below is still right; it was resting on the wrong fact.
if has_private_nic && grpc_public_open {
why.push(format!(
":{GUEST_GRPC} ACCEPTED on the PUBLIC interface. `Bootstrap` is mounted outside \
the auth gate by design, so on a medium that declares no `--admin-key` the \
enrolment window is open to anyone who can route to the box, and the first \
caller to take it becomes gunnar-admin. Bind the private interface by name \
(`--grpc-bind eth1:50051`), never a wildcard"
));
}
if run.exited.is_some() && banner.is_none() && control.is_none() {
why.push("QEMU exited on its own before anything served".into());
}
let driven_note = match &driven {
Some(Ok(text)) => format!("; {text}"),
Some(Err(e)) => {
why.push(format!("driving the guest failed: {e}"));
String::new()
}
None => String::new(),
};
let detail = if why.is_empty() {
format!(
"{} on :{GUEST_SSH}{}{}{driven_note}; then SIGKILL (power cut)",
match &control {
Some(Ok(c)) => format!("{c} on the PRIVATE interface, no git"),
_ => banner.clone().unwrap_or_default(),
},
if grpc_open {
format!(
", :{GUEST_GRPC} accepts on the PRIVATE interface and is {} on the PUBLIC one",
if grpc_public_open { "OPEN" } else { "closed" }
)
} else if has_private_nic {
format!(
", :{GUEST_GRPC} closed on both interfaces (public probe: {})",
if grpc_public_open { "OPEN" } else { "closed" }
)
} else {
String::new()
},
qmp.as_ref()
.map(|q| format!("; {}", q.describe()))
.unwrap_or_default()
)
} else {
format!(
"{}{}\n── serial tail ──\n{}",
why.join("; "),
qmp.as_ref()
.map(|q| format!("; {}", q.describe()))
.unwrap_or_default(),
tail(&run.serial, 20)
)
};
Leg {
name,
verdict: if why.is_empty() {
Verdict::Pass
} else {
Verdict::Failed
},
ms: ready_ms.unwrap_or(run.elapsed.as_millis()),
detail,
}
}
/// The role the medium's `korp.args=` declares (`primary` when none) — what
/// `--grpc-only` holds the booted box's `Bootstrap.Status` to.
pub fn declared_role(iso: &Path) -> Result<String, String> {
declared_installer(iso).map(|c| crate::control::declared_role(&c.effective_boot_params()))
}
/// Whether the declared `korp.args=` binds the control plane off the loopback.
/// **Does this medium's declaration bring a control plane up off the loopback?**
///
/// It matched `--grpc-bind,0.0.0.0` and nothing else until 2026-08-31, which
/// was true of every medium that had one, because a wildcard was the only
/// spelling `--grpc-bind` accepted for "reachable from another host". It is no
/// longer: the keyless installer names the PRIVATE interface
/// (`--grpc-bind,eth1:50051`), and a probe expectation still keyed on the old
/// literal would read the fixed medium as having no control plane at all — the
/// door would then pass without ever asking whether :50051 answers.
///
/// So: any `--grpc-bind` value whose host is neither a loopback literal nor
/// absent. The same question `gunnar_build_thing::reachability::
/// binds_control_plane_offbox` answers about the same argv; this crate is
/// detached and cannot depend on that one, and the alternative — a literal
/// that goes stale silently — is what this comment is about.
pub fn declared_grpc_open(iso: &Path) -> bool {
declared_installer(iso)
.map(|c| {
c.effective_boot_params()
.iter()
.filter_map(|p| p.strip_prefix("korp.args="))
.flat_map(|tail| tail.split(','))
.scan(false, |after_flag, tok| {
let take = *after_flag;
*after_flag = tok == "--grpc-bind";
Some(if take {
Some(tok.to_string())
} else {
tok.strip_prefix("--grpc-bind=").map(str::to_owned)
})
})
.flatten()
.any(|bind| {
let host = bind.rsplit_once(':').map(|(h, _)| h).unwrap_or(&bind);
let host = host.trim_start_matches('[').trim_end_matches(']');
!(host.is_empty()
|| host.eq_ignore_ascii_case("localhost")
|| host == "::1"
|| host.starts_with("127."))
})
})
.unwrap_or(false)
}
pub fn render(door: &Door, legs: &[Leg]) -> String {
let mut s = format!(
"\n══ gunnar installer door · {} ══\n medium: {}{}\n",
door.shape.label(),
door.iso.display(),
match door.twin {
Twin::Iso => "",
Twin::Target => " (virtio twin padded to the target's size, attached first)",
Twin::Larger => " (virtio twin padded to the target's size + 1 GiB, attached first)",
}
);
for l in legs {
s.push_str(&l.line());
s.push('\n');
}
// The ladder stops at the first failed leg, so "every leg that ran passed" is
// "the ladder completed" — 3 legs on the plain door, 4 on the upgrade door.
let all = !legs.is_empty() && legs.iter().all(|l| l.pass());
let timed_out = legs.iter().any(|l| l.verdict == Verdict::TimedOut);
let refused = legs.iter().any(|l| l.verdict == Verdict::Refused);
s.push_str(match (all, timed_out, refused) {
(true, _, _) => " ALL PASS\n",
// Named first and separately: a timeout measured NOTHING, and a report
// that folds it into "not proven" invites the reader to treat the two
// the same. They are not the same. One is evidence; the other is its
// absence.
(_, true, _) => " NOT PROVEN — TIMED OUT (no verdict formed; nothing was measured)\n",
(_, _, true) => {
" NOT PROVEN — REFUSED BY THE GUEST (its own words are in the row above)\n"
}
_ => " NOT PROVEN\n",
});
s
}
/// **The door's own exit code**, so a caller holding nothing else can still
/// tell the three apart: `0` proven, [`EXIT_NOT_PROVEN`] a real failure or a
/// refusal, [`EXIT_TIMED_OUT`] a budget that ran out.
pub fn exit_code(legs: &[Leg], want: usize) -> u8 {
if legs.iter().any(|l| l.verdict == Verdict::TimedOut) {
EXIT_TIMED_OUT
} else if legs.len() == want && legs.iter().all(|l| l.pass()) {
0
} else {
EXIT_NOT_PROVEN
}
}
/// A leg failed or the guest refused — something was measured and it was bad.
pub const EXIT_NOT_PROVEN: u8 = 1;
/// **A leg timed out** — nothing was measured. Distinct from
/// [`EXIT_NOT_PROVEN`] on purpose: `2` is the shell's usage-error convention
/// and `124` is `timeout(1)`'s own, so neither could be told from this door's.
pub const EXIT_TIMED_OUT: u8 = 3;
fn tail(text: &str, n: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
lines[lines.len().saturating_sub(n)..].join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_firmware_word_is_read_and_refused_by_name() {
assert_eq!(
FirmwareChoice::parse("seabios").unwrap(),
FirmwareChoice::SeaBios
);
assert_eq!(FirmwareChoice::parse("UEFI").unwrap(), FirmwareChoice::Uefi);
let err = FirmwareChoice::parse("coreboot").unwrap_err();
assert!(err.contains("coreboot") && err.contains("seabios"), "{err}");
}
/// The roster renders: UpCloud is SeaBIOS + pc + EPYC-Rome, nothing at
/// 0x3f8 (ttyS1/ttyS2 exist), 2 NICs, VGA the only console, medium as
/// cdrom; qemu-local-bios has a UART; the flag door is unchanged.
#[test]
fn the_preset_renders_the_machine_and_the_flags_override_it() {
let up = Shape::from_preset("upcloud", None, None).unwrap();
assert_eq!(up.preset, Some("upcloud"));
assert_eq!(up.firmware, FirmwareChoice::SeaBios);
assert_eq!(up.uarts, [false, true, true, false]);
assert_eq!(up.cpu.as_deref(), Some("EPYC-Rome"));
assert_eq!(up.chipset, None, "pc IS SeaBIOS's default");
assert_eq!(
(up.disk_device, up.nic_device, up.nics),
(VIRTIO_BLK, "virtio-net-pci", 2)
);
assert!(up.medium_as_cdrom && up.vga_only(), "{up:?}");
let label = up.label();
assert!(
label.contains("target=upcloud")
&& label.contains("ttyS1,ttyS2")
&& label.contains("screendump"),
"{label}"
);
let bios = Shape::from_preset("qemu-local-bios", None, None).unwrap();
assert!(bios.has_uart() && bios.serial_console && !bios.vga_only());
let local = Shape::from_preset("qemu-local", None, None).unwrap();
assert_eq!(
local.firmware,
FirmwareChoice::Uefi,
"Both on q35 boots OVMF"
);
let gce = Shape::from_preset("gce-virtio", None, None).unwrap();
assert_eq!(
gce.chipset.as_deref(),
Some("pc"),
"pc under UEFI is the override case"
);
// Overrides on top of the preset.
let forced = Shape::from_preset("upcloud", Some(FirmwareChoice::Uefi), Some(true)).unwrap();
assert_eq!(forced.firmware, FirmwareChoice::Uefi);
assert_eq!(forced.uarts, [true, true, true, false]);
assert!(forced.serial_console && !forced.vga_only());
assert_eq!(
Shape::from_preset("qemu-local-bios", None, Some(false))
.unwrap()
.uarts,
[false; 4]
);
// The flag door is byte-identical to before.
let f = Shape::flags(FirmwareChoice::SeaBios, false);
assert_eq!(
(f.preset, f.uarts, f.nics, f.cpu, f.serial_console),
(None, [false; 4], NICS, None, false)
);
}
/// **The medium says how many NICs it needs, and the door listens.**
///
/// `GUNNAR_NET=eth0:dhcp,eth1:dhcp` plus `--grpc-bind eth1:50051` is not
/// satisfiable on a one-NIC machine, and the `qemu-local` preset offers
/// one. Read it wrong and the door boots a guest with no eth1, `gunnar
/// serve` refuses by name, and the row blames the appliance for the
/// harness (MEASURED under KVM 2026-08-31, before this existed).
#[test]
fn the_interface_count_comes_from_the_mediums_own_gunnar_net() {
let cell = |net: &str| InstallerBuild {
boot_params: vec![
"console=tty0".to_string(),
format!("korp.env.GUNNAR_NET={net}"),
"korp.entry=gunnar".to_string(),
],
..InstallerBuild::default()
};
assert_eq!(declared_interface_count(&cell("eth0:dhcp,eth1:dhcp")), 2);
assert_eq!(declared_interface_count(&cell("eth0:dhcp")), 1);
assert_eq!(
declared_interface_count(&cell("eth0:10.0.2.15/24:10.0.2.2,eth1:dhcp")),
2,
"a static public address with a DHCP private one is still two interfaces"
);
assert_eq!(
declared_interface_count(&cell("eth0:dhcp,eth1:dhcp,eth2:dhcp")),
3
);
// A medium that names no network still gets a machine with a NIC —
// never zero, which would be a guest that cannot be reached at all.
assert_eq!(
declared_interface_count(&InstallerBuild::default()),
1,
"an absent GUNNAR_NET reads as one interface, never none"
);
}
/// The REAL declaration, by name: `gunnar-installer-open` is the medium
/// git.example.test is installed from, and it must ask for two.
#[test]
fn the_shipped_keyless_installer_asks_for_two_interfaces() {
let root = crate::workspace_root();
let Ok(Some(set)) = BuildVariantSet::discover(&root) else {
eprintln!("no build space at {}; nothing to read", root.display());
return;
};
let Some(cell) = set
.installers
.iter()
.find(|i| i.name == "gunnar-installer-open")
.cloned()
else {
panic!("gunnar declares a `[[build.installer]]` named gunnar-installer-open");
};
assert_eq!(
declared_interface_count(&cell),
2,
"the open installer binds --grpc-bind eth1:50051; a one-NIC machine cannot serve it. \
boot_params: {:?}",
cell.boot_params
);
}
/// needs-real-cloud is refused BY NAME with the preset's reason; unknown
/// names get the roster.
#[test]
fn a_real_cloud_preset_is_refused_by_name() {
for name in ["aws-nitro", "azure-gen2", "scaleway-elastic-metal"] {
let err = Shape::from_preset(name, None, None).unwrap_err();
assert!(
err.contains(name) && err.contains("needs a real cloud") && err.contains("upcloud"),
"{err}"
);
}
let err = Shape::from_preset("digitalocean", None, None).unwrap_err();
assert!(
err.contains("digitalocean") && err.contains("qemu-local-bios"),
"{err}"
);
// Every mimicable preset renders.
for p in presets::PRESETS
.iter()
.filter(|p| p.status() != PresetStatus::NeedsRealCloud)
{
Shape::from_preset(p.name, None, None).unwrap_or_else(|e| panic!("{}: {e}", p.name));
}
}
/// The scsi/nvme buses render outside the declaration's virtio-blk writer.
#[test]
fn a_non_virtio_blk_bus_renders_its_own_device() {
let cell = declared_installer(Path::new("/x/gunnar-installer-open.iso")).unwrap();
let mut gce = Shape::from_preset("gce-virtio", None, None).unwrap();
assert_eq!(gce.disk_device, "virtio-scsi-pci");
let a = gce.disk_args(&cell, "/t/target.img");
assert!(
a.iter().any(|x| x == "scsi-hd,drive=target,bus=scsi0.0"),
"{a:?}"
);
gce.disk_device = "nvme";
assert!(gce
.disk_args(&cell, "/t/target.img")
.iter()
.any(|x| x.starts_with("nvme,drive=target")));
let up = Shape::from_preset("upcloud", None, None).unwrap();
assert_eq!(
up.disk_args(&cell, "/t/target.img"),
cell.kvm_disk_args("/t/target.img")
);
}
/// A door on the gunnar checkout resolves its cell from the ISO's stem when
/// there is no medium to read, and says which installers exist when the
/// stem names none.
#[test]
fn the_medium_names_its_cell_or_is_refused_with_the_roster() {
let open = declared_installer(Path::new("/x/gunnar-installer-open.iso")).unwrap();
assert_eq!(open.name, "gunnar-installer-open");
assert!(open.target_disk_bytes() >= 1 << 30);
assert!(declared_grpc_open(Path::new(
"/x/gunnar-installer-open.iso"
)));
assert!(
!declared_grpc_open(Path::new("/x/gunnar-installer.iso")),
"the sealed medium is --no-grpc"
);
let err = declared_installer(Path::new("/x/nothing-declares-this.iso")).unwrap_err();
assert!(
err.contains("gunnar-installer-open") && err.contains("nothing-declares-this"),
"{err}"
);
// …and the refusal now says what the MEDIUM said too, not only what is
// declared, so a run that failed here can tell "wrong name" from
// "wrong bytes" without opening the ISO by hand.
assert!(
err.contains("could not be read as an ISO-9660 image"),
"{err}"
);
}
/// ★ **The command line discriminates the two installer rows.**
///
/// This is the fact the whole medium-first lookup rests on: the sealed and
/// the open installer carry the SAME `volume_id` (`GUNNAR_INSTALL`), so the
/// volume alone can never tell them apart — `korp.args=` can, and does.
///
/// The cmdline below is the real one, read out of
/// `.iso-out/gunnar-installer-open.iso`'s ESP with `mcopy` on 2026-08-30.
#[test]
fn the_command_line_on_the_esp_tells_the_sealed_cell_from_the_open_one() {
let root = crate::workspace_root();
let set = BuildVariantSet::discover(&root).unwrap().unwrap();
let open_medium = "console=tty0 console=ttyS0,115200 panic=10 \
korp.env.GUNNAR_DATA_ROOT=/data korp.env.GUNNAR_APPLIANCE=1 \
korp.args=serve,--grpc-bind,utility:50051 korp.entry=gunnar \
korp.znippy=/app.znippy korp.env.GUNNAR_NET=eth0:dhcp,eth1:dhcp korp.data.fs=xfs \
korp.data.io=uring korp.appliance=lean korp.require_signed=0 \
initrd=\\initrd korp.installer_root=/run/installer \
KORP_INSTALLER_ROOT=/run/installer";
let fits: Vec<&str> = set
.installers
.iter()
.filter(|i| i.volume_id == "GUNNAR_INSTALL" && cell_fits_cmdline(i, open_medium))
.map(|i| i.name.as_str())
.collect();
assert_eq!(
fits,
["gunnar-installer-open"],
"exactly one declared installer boots this command line; the volume id alone \
fits both, which is why the cmdline is read at all"
);
// The tokens the composer added and the cell never declared (initrd=,
// korp.znippy=, korp.installer_root=) do not disqualify it: the rule is
// "every declared token is on the medium", not equality.
let open = set
.installers
.iter()
.find(|i| i.name == "gunnar-installer-open")
.unwrap();
assert!(open
.effective_boot_params()
.iter()
.all(|p| open_medium.split_whitespace().any(|t| t == p)));
assert!(!open_medium.contains("--no-grpc"));
}
/// ★ **A renamed medium still resolves — off its bytes.**
///
/// The regression this closes: `gunnar-installtest` looked its cell up by
/// the ISO's file STEM, so `cp gunnar-installer-open.iso run-7.iso` refused
/// the run with *"no `[[build.installer]]` named run-7"*. It cost two lanes
/// a run. A copy of a medium under another name is the same medium.
///
/// Driven against a REAL composed ISO, hard-linked (not copied — the medium
/// is 38 MB and `/home` is tight) under a stem that names no declared cell.
/// Skipped, loudly, when no medium has been composed on this box: this
/// asserts about bytes and there is nothing honest to assert without them.
#[test]
fn a_renamed_medium_resolves_off_its_volume_and_its_cmdline() {
let composed = crate::workspace_root().join(".iso-out/gunnar-installer-open.iso");
if !composed.exists() {
eprintln!(
"SKIP: no {} on this box — compose one to drive this",
composed.display()
);
return;
}
// The identity is on the medium…
let id = medium_identity(&composed).expect("a composed ISO names its volume");
assert_eq!(id.volume_id, "GUNNAR_INSTALL");
let cmdline = id.cmdline.expect("korp/cmdline on the ESP");
assert!(cmdline.contains("korp.args="), "{cmdline}");
// …and it survives a rename that the old stem lookup refused outright.
let dir = std::env::temp_dir().join(format!("laneAH-rename-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let renamed = dir.join("nothing-declares-this-stem.iso");
std::fs::hard_link(&composed, &renamed)
.or_else(|_| std::fs::copy(&composed, &renamed).map(|_| ()))
.expect("stage the renamed medium");
let cell = declared_installer(&renamed).expect("the medium names its own cell");
assert_eq!(cell.name, "gunnar-installer-open");
assert!(
declared_grpc_open(&renamed),
"and the door reads its control plane off it"
);
let _ = std::fs::remove_dir_all(&dir);
}
// ── the doors that hang (2026-08-30) ────────────────────────────────────
/// A medium file at a stem `declared_installer` resolves, with LBA 0 laid
/// as asked: `None` = a pure El Torito ISO (all zeros, what the composer
/// actually writes), `Some((0x55, 0xAA))` = a hybrid one SeaBIOS could boot.
fn medium_file(tag: &str, mbr: Option<(u8, u8)>) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("aq-medium-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let p = dir.join("gunnar-installer-open.iso");
let mut bytes = vec![0u8; 4096];
if let Some((a, b)) = mbr {
bytes[510] = a;
bytes[511] = b;
}
std::fs::write(&p, bytes).unwrap();
p
}
/// **gce-virtio, in seconds instead of 420 of them.** MEASURED before this
/// check existed: the door booted, the appliance printed `FAILED hwdetect:
/// no usable target disk` at once and the door still sat for 420.10 s. The
/// contradiction is between two DECLARATIONS and needs no boot at all.
#[test]
fn a_machine_whose_disk_bus_the_medium_cannot_drive_is_refused_by_name() {
let iso = medium_file("bus", None);
let cell = declared_installer(&iso).unwrap();
// The premise, stated: this is the medium gunnar actually ships.
assert_eq!(
cell.target
.buses
.iter()
.map(|b| b.as_str())
.collect::<Vec<_>>(),
["ide-cdrom", "virtio-blk"],
"gunnar's recipe stopped declaring the buses this test is about"
);
let shape = Shape::from_preset("gce-virtio", None, None).unwrap();
let why = medium_cannot_install_here(&iso, &shape, &cell).expect("refused, not booted");
assert!(why.contains("THE BUS"), "{why}");
assert!(
why.contains("virtio-scsi"),
"names the bus the machine wants: {why}"
);
assert!(
why.contains("ide-cdrom, virtio-blk"),
"names the buses the medium has: {why}"
);
assert!(
why.contains(BOOT_ANYWAY),
"says how to see the guest refuse: {why}"
);
}
/// **ovh-public-cloud: zero serial bytes, and why.** SeaBIOS boots an MBR;
/// this preset attaches no cdrom; the medium has no MBR. Measured: LBA 0
/// `[510..512] = 00 00` on the real 38 680 576 B ISO, and a 420.29 s run
/// with a `serial.log` of **0 bytes**.
#[test]
fn a_seabios_machine_that_attaches_no_cdrom_is_refused_by_name() {
let iso = medium_file("attach", None);
let cell = declared_installer(&iso).unwrap();
let shape = Shape::from_preset("ovh-public-cloud", None, None).unwrap();
assert!(
!shape.medium_as_cdrom,
"the preset's own roster row brings no cdrom"
);
assert_eq!(shape.firmware, FirmwareChoice::SeaBios);
let why = medium_cannot_install_here(&iso, &shape, &cell).expect("refused, not booted");
assert!(why.contains("THE ATTACHMENT"), "{why}");
assert!(
why.contains("00 00"),
"quotes the bytes it read, not a flag: {why}"
);
assert!(
why.contains("qcow2"),
"names how this platform DOES take a medium: {why}"
);
// It reads the medium, not the preset: the same machine with a hybrid
// ISO has a boot path and is not refused for the attachment.
let hybrid = medium_file("hybrid", Some((0x55, 0xAA)));
let cell2 = declared_installer(&hybrid).unwrap();
assert_eq!(
medium_cannot_install_here(&hybrid, &shape, &cell2),
None,
"an MBR at LBA 0 is a boot path SeaBIOS can take"
);
// And it is the FIRMWARE that decides: gce-virtio brings no cdrom
// either, and OVMF reads El Torito off any block device — so it is
// refused for the BUS and never for the attachment.
let gce = Shape::from_preset("gce-virtio", None, None).unwrap();
let gce_why = medium_cannot_install_here(&iso, &gce, &cell).unwrap();
assert!(
!gce_why.contains("THE ATTACHMENT"),
"UEFI boots this medium: {gce_why}"
);
}
/// The negative control that keeps the two refusals from being a way to
/// make anything go quiet: every preset the deploy pipeline gates on must
/// still boot.
#[test]
fn the_presets_the_deploy_gate_runs_are_not_refused() {
let iso = medium_file("gate", None);
let cell = declared_installer(&iso).unwrap();
for name in ["qemu-local", "qemu-local-bios", "upcloud", "hetzner-cloud"] {
let shape = Shape::from_preset(name, None, None).unwrap();
assert_eq!(
medium_cannot_install_here(&iso, &shape, &cell),
None,
"{name} must still run"
);
}
// The flag door names no preset and is never refused by this check.
let flags = Shape::flags(FirmwareChoice::Uefi, true);
assert_eq!(medium_cannot_install_here(&iso, &flags, &cell), None);
}
/// **The two media gunnar ships answer two different machines** — the
/// declaration side of the 2026-08-30 measurement, asserted from the recipe and
/// not from a boot, so it is red the moment either row loses its reason.
///
/// The RED it replaces was measured on the shared medium: `--target
/// gce-virtio` **420.10 s → refused, THE BUS**, and `--target
/// ovh-public-cloud` **420.29 s → refused, THE ATTACHMENT**. Both refusals
/// are still exactly right about THAT medium — the appliance's kernel has
/// no SCSI and never should — so neither was widened. What changed is that
/// gunnar now declares a medium each machine can take:
///
/// * every medium is `medium_boot = "hybrid"`, so it carries a `55 AA` boot
/// sector and SeaBIOS can start it off an imported disk image;
/// * `gunnar-installer-gce` declares `buses ∋ virtio-scsi` and a donor whose
/// preset does not veto `CONFIG_SCSI`.
///
/// Both media were then booted: ovh-public-cloud 3/3 legs on the shared one
/// (7 s), gce-virtio 3/3 on the scsi one (15 s).
#[test]
fn the_two_media_gunnar_ships_answer_two_different_machines() {
use nornir_build_thing::installer::MediumBoot;
use nornir_build_thing::target::{Bus, Firmware};
let root = crate::workspace_root();
let set = BuildVariantSet::discover(&root).unwrap().unwrap();
let by = |n: &str| {
set.installers
.iter()
.find(|i| i.name == n)
.unwrap_or_else(|| panic!("{n}"))
};
// 1. EVERY medium is bootable off a disk it was imported as. A medium
// that quietly went back to `el-torito` would be the 0-byte-serial
// run again.
//
// Asserted as the RULE and not as a constant. This said
// `== MediumBoot::Hybrid` until 2026-09-05, which was the same claim
// only while `hybrid` was the ONLY way to be disk-bootable. It is
// not any more: edda grew `MediumBoot::Gpt` (a GPT ESP reachable in
// the same reserved system area, no boot code at LBA 0) for exactly
// the machine `hybrid` cannot serve. Naming one spelling made the
// guard fire on `gunnar-installer-azure` — a medium that IS disk-
// bootable, by the other word.
//
// The two words are not interchangeable and the firmware decides
// which is legal, so that is what is checked:
//
// * `hybrid` puts real boot code at LBA 0 and edda's composer
// REFUSES it unless `target.firmware` offers a legacy stage — "a
// `55 AA` that chainloads nothing is worse than none, because
// firmware stops looking once it finds one";
// * `gpt` is the UEFI-only answer (Azure Gen2: no legacy devices at
// all, so there is no firmware to run a boot sector).
//
// Checking the pair catches both halves: a `gpt` medium on a machine
// that needs a BIOS sector, and a `hybrid` one on a machine with no
// BIOS to read it — the second of which edda would refuse at compose
// time, i.e. after the bytes were written.
for i in &set.installers {
// The EFFECTIVE firmware, not the raw field: a row declared by
// `preset` (azure-gen2, aws-nitro) carries the field at its default
// `both`, and the machine it names is what the composer consumes.
let uefi_only = i.target.effective().firmware == Firmware::Uefi;
assert_ne!(
i.medium_boot,
MediumBoot::ElTorito,
"'{}' must stay bootable off a disk it was imported as — `el-torito` alone \
parks on a zeroed LBA 0 (measured: 420 s of no serial at all)",
i.name
);
if uefi_only {
assert_eq!(
i.medium_boot,
MediumBoot::Gpt,
"'{}' declares firmware = uefi, which has nothing that reads a `55 AA` \
boot sector; `gpt` is the word for that machine and edda's composer \
refuses `hybrid` there by name",
i.name
);
} else {
assert_eq!(
i.medium_boot,
MediumBoot::Hybrid,
"'{}' can be started by a legacy BIOS, so its medium carries the boot \
sector that starts it",
i.name
);
}
}
// 2. The appliance media are still the default machine — no SCSI, no
// bytes for a bus their customers do not have.
let open = by("gunnar-installer-open");
assert!(open.target.is_default());
assert!(!open.target.buses.contains(&Bus::VirtioScsi));
// 3. The GCE medium is the one that answers a virtio-scsi disk, and it
// takes a DIFFERENT donor — the shared one's `nuked` preset vetoes
// CONFIG_SCSI by name, so widening the shared row would not build.
let gce = by("gunnar-installer-gce");
assert!(gce.target.buses.contains(&Bus::VirtioScsi));
assert_ne!(
gce.kernel_donor, open.kernel_donor,
"a different bus is a different kernel"
);
assert_ne!(
gce.volume_id, open.volume_id,
"a medium must be able to say which cell it is"
);
// 4. And the preflight agrees, both ways round, on a medium with a boot
// sector: the shared row is still refused for THE BUS on gce-virtio
// (which is the truth about its kernel), the GCE row is not, and
// neither is refused for THE ATTACHMENT any more.
let iso = medium_file("as-hybrid", Some((0x55, 0xAA)));
let gce_shape = Shape::from_preset("gce-virtio", None, None).unwrap();
let ovh_shape = Shape::from_preset("ovh-public-cloud", None, None).unwrap();
let why = medium_cannot_install_here(&iso, &gce_shape, open)
.expect("the shared medium still cannot");
assert!(
why.contains("THE BUS") && !why.contains("THE ATTACHMENT"),
"{why}"
);
assert_eq!(
medium_cannot_install_here(&iso, &gce_shape, gce),
None,
"the scsi medium can"
);
assert_eq!(
medium_cannot_install_here(&iso, &ovh_shape, open),
None,
"a boot sector is a boot path"
);
assert_eq!(medium_cannot_install_here(&iso, &ovh_shape, gce), None);
}
/// **A timeout is not a failure and above all not a pass** — in the row and
/// in the exit code. Before this, `[FAIL]` after 420 s of nothing was the
/// only thing either of them said.
#[test]
fn the_four_outcomes_are_told_apart_in_the_row_and_in_the_exit_code() {
let leg = |v: Verdict| Leg {
name: "B1 install",
verdict: v,
ms: 1,
detail: String::new(),
};
assert!(leg(Verdict::Pass).line().contains("[PASS]"));
assert!(leg(Verdict::Failed).line().contains("[FAIL]"));
assert!(leg(Verdict::Refused).line().contains("[REFUSED]"));
assert!(leg(Verdict::TimedOut).line().contains("[TIMEOUT]"));
assert_eq!(
exit_code(
&[leg(Verdict::Pass), leg(Verdict::Pass), leg(Verdict::Pass)],
3
),
0
);
assert_eq!(exit_code(&[leg(Verdict::Failed)], 3), EXIT_NOT_PROVEN);
assert_eq!(exit_code(&[leg(Verdict::Refused)], 3), EXIT_NOT_PROVEN);
assert_eq!(exit_code(&[leg(Verdict::TimedOut)], 3), EXIT_TIMED_OUT);
// A ladder that stopped early is not a pass even if every leg that ran
// passed — the historical rule, kept.
assert_eq!(exit_code(&[leg(Verdict::Pass)], 3), EXIT_NOT_PROVEN);
// Every non-zero code reaches a shell as red, and none of them collides
// with `timeout(1)`'s 124 or the usage-error 2.
assert!(EXIT_NOT_PROVEN != 0 && EXIT_TIMED_OUT != 0);
assert!(EXIT_TIMED_OUT != 2 && EXIT_TIMED_OUT != 124 && EXIT_TIMED_OUT != EXIT_NOT_PROVEN);
}
/// A later check may downgrade a leg but must never launder one: a leg that
/// timed out and also has no MBR on its target still TIMED OUT.
#[test]
fn a_second_check_cannot_hide_a_timeout_behind_a_failure() {
let mut l = Leg {
name: "B1 install",
verdict: Verdict::TimedOut,
ms: 1,
detail: String::new(),
};
l.demote(Verdict::Failed);
assert_eq!(l.verdict, Verdict::TimedOut);
let mut ok = Leg {
name: "B1 install",
verdict: Verdict::Pass,
ms: 1,
detail: String::new(),
};
ok.demote(Verdict::Failed);
assert_eq!(ok.verdict, Verdict::Failed);
}
/// The summary line says WHICH kind of "not proven" happened — a report
/// that folded a hang into the same three words as a measured failure is
/// what let two doors hang unnoticed for a day.
#[test]
fn the_summary_names_a_timeout_apart_from_a_failure() {
let door = Door {
iso: PathBuf::from("/x/gunnar-installer-open.iso"),
shape: Shape::flags(FirmwareChoice::Uefi, true),
work: PathBuf::from("/x/work"),
expect_grpc: false,
grpc_only: None,
twin: Twin::Iso,
disks: Vec::new(),
// `None` = the historical B2 this test was written against
// (probe the ports, power-cut, done). Added 2026-09-05: the
// `--drive-cmd` field landed without this initializer, so the
// whole `--lib` test target stopped COMPILING — every unit test
// in this crate, not only this one, has been unrunnable since,
// which is the quietest possible way for a guard to stop being
// able to say red.
drive_cmd: None,
};
let leg = |v: Verdict| Leg {
name: "B1 install",
verdict: v,
ms: 1,
detail: String::new(),
};
assert!(render(&door, &[leg(Verdict::Pass)]).contains("ALL PASS"));
assert!(render(&door, &[leg(Verdict::TimedOut)]).contains("TIMED OUT"));
assert!(render(&door, &[leg(Verdict::Refused)]).contains("REFUSED BY THE GUEST"));
let plain = render(&door, &[leg(Verdict::Failed)]);
assert!(
plain.contains("NOT PROVEN") && !plain.contains("TIMED OUT"),
"{plain}"
);
}
}