xtask/tests/installer_iso_fleet_kvm.rs
//! **The FLEET proof — ONE per-site ISO → N machines, each minting its OWN
//! identity** (appliance brief §4's closing sentence, run literally: "each
//! machine installs, mints its OWN key on P3, and prints its public key for
//! the hub").
//!
//! draupnir is the PLANNER: `plan_fleet` fans one `BootSpec` into N members —
//! the same spec shape whose Redfish arm burns real metal (the fleet
//! wire-shape proof in draupnir's redfish.rs pins those requests). The BOOT
//! leg rides korp-installer's `kvmproof` harness, because draupnir's KVM
//! backend cannot boot an ISO (`ImageSource::suits`: Iso → Redfish only) —
//! the wall is named in draupnir/.nornir/iso-to-fleet.md, per §4's own
//! don't-break-working-proofs clause.
//!
//! Per member: blank disk → install boot (INSTALL-OK) → **FIRST BOOT of the
//! installed disk** (bare-metal semantics: no cdrom, the firmware walks to
//! the ESP's BOOTX64.EFI) → korp-start mounts P3, MINTS, prints the canonical
//! line. The fleet-level assert is the one a single-machine proof cannot
//! make: the N public keys are DISTINCT — an ISO that accidentally baked an
//! identity would hand the whole fleet one key, and this is the test that
//! catches it.
//!
//! Heavy, opt-in:
//! ```text
//! KORP_TEST_KERNEL=/path/to/bzImage \
//! cargo test -p xtask --test installer_iso_fleet_kvm -- --ignored --nocapture
//! ```
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use draupnir::{plan_fleet, BmcEndpoint, BootSpec};
use korp_installer::isoproof::{
attach_slirp_nic, free_port, identity_line_count, minted_pubkey, FirstBootProof,
IsoInstallProof, KvmPreflight, INSTALL_OK,
};
use korp_installer::kvmproof::{boot_disk_until_verdict, gpt_entry, read_fat_file};
use wg_appliance_core::hubfixture::HubFixture;
/// THIS product, everywhere the shared harness needs to know whose ceremony it
/// is running: the rollup row's product, and the `product=` field on the
/// canonical identity line korp-start prints (from the installed cmdline's
/// `korp.entry`). The line's FORMAT is wg-appliance-core's, read back through
/// the harness — never a private prefix constant that can drift from it.
const PRODUCT: &str = "holger";
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.to_path_buf()
}
/// **Pack a FRESH `app.znippy` from the tree being tested** — build the static
/// musl `holger-mvp`, then `appliance-pack` it. Called by every proof in this
/// file BEFORE `installer-iso`.
///
/// Exists because of a measured lie (2026-08-15→17): `installer-iso` READS
/// `.znippy-out/holger-app.znippy` and reuses it silently, with no freshness
/// check. The durability fix in `raw.rs::write_relpath` was committed 17:12;
/// the znippy on disk had been packed 16:41 — so the "re-run after the fix"
/// power-cycle proof drove a PRE-FIX binary and reported the fix insufficient.
/// A proof that masters its ISO from a payload it did not pack is measuring
/// whatever happened to be in an output directory, not the tree — the same
/// disease gunnar's ISO cell recorded on 2026-08-05 ("a build input declared
/// inside the output directory"), one level up.
/// ⚠ **AND THE ARGV IS THE DECLARATION'S, NOT THIS FILE'S** (2026-09-21, lane H2).
///
/// This function used to hand-spell `--no-default-features --features
/// mvp-server,appliance`, while the declared `mvp-server-musl` row carries
/// `mvp-server, appliance, auth-passkey, sign, wasm-plugins, meter-guests,
/// fmt-raw, fmt-rust, jobs`. A second writer of a build argv, drifted from the
/// one declaration — the disease the doc above names, one level further in.
///
/// What it cost, MEASURED by running this proof: the payload served **no
/// formats at all**, so every appliance installed from it died at first boot,
///
/// ```text
/// Error: [holger-mvp-1] repository 'bundles' is `ron_repo_type: "raw"` but this
/// server was built without the `fmt-raw` feature; the formats this binary
/// serves are [] — rebuild with `--features fmt-raw` (or `fmt-all`)
/// Kernel panic - not syncing: Attempted to kill init!
/// ```
///
/// — "boots but does not serve", inside the proof that exists to catch exactly
/// that. `Row::cargo_args()` is the same call `gate::rows_check` makes, so the
/// features, profile and target are whatever the declaration says and this file
/// names none of them.
fn pack_fresh_app_znippy(root: &Path) {
let set = holger_build_space::build_variant_set().expect("build space loads");
let row = set
.variants
.iter()
.find(|v| v.name == "mvp-server-musl")
.expect("the declared static appliance payload");
let build = Command::new(env!("CARGO"))
.args(row.cargo_args())
.current_dir(root)
.output()
.expect("spawn cargo build (musl holger-mvp)");
assert!(
build.status.success(),
"the musl holger-mvp must build from THIS tree:\n{}",
String::from_utf8_lossy(&build.stderr)
);
let target_dir = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| root.join("target"));
let bin = target_dir.join("x86_64-unknown-linux-musl/release/holger-mvp");
assert!(
bin.is_file(),
"the musl build left no binary at {}",
bin.display()
);
let pack = Command::new(env!("CARGO"))
.args([
"run",
"--quiet",
"--manifest-path",
"xtask/Cargo.toml",
"--",
"appliance-pack",
"--bin",
])
.arg(&bin)
.current_dir(root)
.output()
.expect("spawn cargo xtask appliance-pack");
assert!(
pack.status.success(),
"appliance-pack must pack the binary this tree just built:\n{}",
String::from_utf8_lossy(&pack.stderr)
);
}
#[test]
#[ignore = "heavy: masters one per-site ISO, then per fleet member installs under KVM and FIRST-BOOTS the installed disk — asserts N distinct minted identities"]
fn one_iso_fans_to_n_machines_each_minting_its_own_identity() {
let root = workspace_root();
let Some(preflight) = KvmPreflight::detect("holger fleet, N distinct identities (iso-kvm)")
else {
return;
};
let scratch = root.join("target/fleet-proof");
let _ = std::fs::remove_dir_all(&scratch);
std::fs::create_dir_all(&scratch).unwrap();
// ── 1. ONE per-site ISO through THE VERB (the declared cell) ──
// THE HUB'S IDENTITY EXISTS BEFORE THE FLEET IS BURNED — reserve keypair
// + port now, bake hub.conf from the ONE renderer, activate the pump per
// appliance key after its first boot. Endpoint 10.0.2.2 = QEMU slirp's
// host alias (guest→host); the fixture binds 0.0.0.0:<port> on the host.
let hub = HubFixture::reserve().expect("reserve the hub identity");
let hub_conf = hub.hub_conf(
std::net::Ipv4Addr::new(10, 0, 2, 2),
"10.77.0.0/24",
5,
Some((std::net::Ipv4Addr::new(10, 77, 0, 7), 24)),
);
let hub_path = scratch.join("fleet-hub.conf");
std::fs::write(&hub_path, &hub_conf).unwrap();
let ron_path = root.join("xtask/tests/fixtures/site-holger.ron");
pack_fresh_app_znippy(&root);
let out = Command::new(env!("CARGO"))
.args([
"run",
"--quiet",
"--manifest-path",
"xtask/Cargo.toml",
"--",
"installer-iso",
"--param",
"site=fleet-site-1",
"--param",
])
.arg(format!("hub_conf=@{}", hub_path.display()))
.arg("--param")
.arg(format!("holger_ron=@{}", ron_path.display()))
.args(["--param", "underlay=10.0.2.15/24@eth0"])
.args(["--root-key", "dev"])
.current_dir(&root)
.output()
.expect("spawn installer-iso");
assert!(
out.status.success(),
"the verb must master the fleet ISO:\n{}\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let iso = root.join(".iso-out/holger-installer.iso");
assert!(iso.is_file());
// The declared cell (disk size + attachment shape — the one writer).
let set = holger_build_space::build_variant_set().expect("build space loads");
let cell = set
.installers
.iter()
.find(|i| i.name == "holger-installer")
.expect("the declared holger-installer cell")
.clone();
// ── 2. draupnir PLANS the fleet: one spec, N members ──
// The Redfish-shaped spec is the SAME one whose request shapes draupnir's
// fleet wire-shape test pins against three distinct BMCs; here its
// members name the KVM stand-ins for those machines. The URL is what a
// depot would serve — the KVM leg boots the local file the URL names.
let template = BootSpec::redfish_iso(
"fleet-site-1-holger",
"https://depot.fleet-site-1.example/holger-installer.iso",
BmcEndpoint {
host: "https://bmc-template.example".into(),
username: "admin".into(),
system_id: "System.Embedded.1".into(),
},
);
let fleet = plan_fleet(&template, 2);
assert_eq!(fleet.len(), 2);
// ── 3. per member: install, then FIRST BOOT off the installed disk ──
let mut pubkeys: Vec<String> = Vec::new();
for member in &fleet {
let mdir = scratch.join(&member.name);
std::fs::create_dir_all(&mdir).unwrap();
let disk = mdir.join("disk.raw");
let install_dir = mdir.join("install");
// ── the SHARED install ceremony, with holger's values as inputs ──
// Gap #15's install-time leg rides `expect_serial`: the enrolled root
// verifies at INSTALL time — PASS, not NOT-ENROLLED (the measured
// pre-enrollment state), and the policy token is on the installer's own
// cmdline so a NOT-ENROLLED install would have REFUSED here instead of
// shipping integrity-only.
IsoInstallProof {
product: PRODUCT,
check: "install_ok",
iso: &iso,
disk: &disk,
disk_bytes: cell.target_disk_bytes(),
disk_args: cell.kvm_disk_args(&disk.display().to_string()),
workdir: &install_dir,
marker: INSTALL_OK,
timeout: Duration::from_secs(180),
expect_serial: &["signature-over-root=PASS"],
expect_cmdline: &["korp.entry=holger", "korp.site=fleet-site-1"],
expect_data_files: &[],
detail: "fleet member: per-site ISO → KVM boot → INSTALL-OK with the enrolled root verified",
}
.run(&preflight);
// ── FIRST BOOT: the disk alone, bare-metal semantics — the firmware
// walks to \EFI\BOOT\BOOTX64.EFI on the ESP the installer wrote ──
// The marker is the SERVING line, not the mint: a mint with no app
// behind it is the exact "boots but does not serve" the argv gap
// measured (help text -> PID 1 died -> kernel panic). The harness
// returns the WHOLE capture, so the identity asserts below read it.
let boot_dir = mdir.join("firstboot");
let booted = FirstBootProof {
product: PRODUCT,
disk: &disk,
disk_args: cell.kvm_disk_args(&disk.display().to_string()),
workdir: &boot_dir,
// A real bind (TcpListener::bind fails loud before this prints),
// never a startup banner.
marker: "HTTP/OCI gateway listening on 0.0.0.0:8443",
hostfwd: None,
timeout: Duration::from_secs(120),
expect_serial: &[
// the first boot MINTS (never a pre-baked key)
"MINTED",
// the DECLARED argv reached the exec — the cell's cmd, via korp.args
r#"exec argv: ["holger", "start", "--config", "/data/holger.ron"]"#,
// Gap #15 on the BOOT side: every boot re-verifies the
// enrollment (korp-start's signature leg against the ESP's
// root.pub) — the require_signed policy rode the carry to the
// installed cmdline.
"signature-over-root=PASS",
// ENV SEAM + UNDERLAY: the declared environment reached the
// exec, the underlay address applied, and the tunnel INITIATED
// toward the baked hub endpoint.
"wg-appliance underlay: 10.0.2.15/24 on eth0",
"wg-appliance address: 10.77.0.7/24 on wg0",
"wg-appliance tunnel: initiating interface=wg0",
// Gap #12: the boot narration is DURABLE — the log rides P3,
// named on serial by the boot itself.
"log: boot narration teed to /data/log/boot.log",
],
refuse_serial: &[
// PID 1 must not die (the literal shape of the argv gap).
"Kernel panic",
// The APP must actually take over — minting an identity on a
// machine that then fails to run the product is not an
// installed appliance. Proven RED by measurement (2026-08-13):
// with a glibc-linked payload this failed with `fexecve …
// ENOENT` (the absent PT_INTERP), AFTER a green mint. A static
// payload is what the appliance shape requires and this is the
// refusal that enforces it.
"FAILED exec",
],
}
.run(&preflight);
let serial = booted.serial;
// holger prints the canonical line ITSELF (the ceremony's load half):
// the SAME key korp-start minted — the line appears (at least) twice in
// one boot and both spell the same identity.
assert!(
identity_line_count(&serial, PRODUCT) >= 2,
"member {}: korp-start AND holger both print the canonical line:\n{serial}",
member.name
);
let (p3_first_log, p3_last_log) = gpt_entry(&disk, 2);
let logged = read_fat_file(&disk, p3_first_log, p3_last_log, "log/boot.log")
.unwrap_or_else(|| panic!("member {} keeps log/boot.log on P3", member.name));
let logged = String::from_utf8_lossy(&logged);
assert!(
logged.contains("wg-appliance identity: product=holger"),
"member {}: the durable log carries the canonical line (RAM-copy replay):\n{logged}",
member.name
);
let key = booted.pubkey.clone().unwrap_or_else(|| {
panic!(
"member {} prints the canonical identity line:\n{serial}",
member.name
)
});
// #16 pubkey-identity: what the machine PRINTED must be what it
// KEPT — read identity.pub straight off the installed P3 and compare.
// A machine that prints one key and stores another would hand the hub
// a credential that never authenticates.
let (p3_first, p3_last) = gpt_entry(&disk, 2);
let stored = read_fat_file(&disk, p3_first, p3_last, "wg/identity.pub")
.unwrap_or_else(|| panic!("member {} keeps identity.pub on P3", member.name));
assert_eq!(
String::from_utf8_lossy(&stored).trim(),
key,
"member {}: the stored identity must be the printed one",
member.name
);
// #18 second reboot: boot the SAME disk again — the key must survive
// and MINTED must NOT reappear (a second mint would silently replace
// the identity the hub already registered).
let reboot_dir = mdir.join("reboot2");
std::fs::create_dir_all(&reboot_dir).unwrap();
// THE TUNNEL BOOT (#17): the hub pump activates on the RESERVED port
// with THIS member's minted key, the guest boots and initiates, the
// hub responds, traffic seals, and the hub pings the appliance's
// tunnel address — the reply crosses back through the tunnel. The
// boot is HELD past the serving marker until the echo lands.
let key_bytes = wg_appliance_core::decode_key(&key).expect("the minted key decodes");
let fixture = HubFixture::spawn_on(hub.port, hub.hub_private, key_bytes)
.expect("activate the hub pump on the reserved port");
// The ping is sent AFTER the handshake responds (the hub can neither
// seal into a session that does not exist nor knows the appliance's
// NAT address until it hears it) — the hold closure watches for the
// response and fires the ping exactly once.
let pinged = std::cell::Cell::new(false);
// The SAME attachment shape the first boot used — the declared cell's
// disk argv plus the harness's virtio NIC (one writer for both).
let mut disk_only = cell.kvm_disk_args(&disk.display().to_string());
attach_slirp_nic(&mut disk_only, None);
let serial2 = boot_disk_until_verdict(
&preflight.qemu,
&preflight.ovmf_code,
&preflight.ovmf_vars,
&disk_only,
&reboot_dir,
"HTTP/OCI gateway listening on",
Duration::from_secs(120),
|| {
let v = fixture.verdict();
if v.responses > 0 && !pinged.get() {
fixture.send_ping(
std::net::Ipv4Addr::new(10, 77, 0, 1),
std::net::Ipv4Addr::new(10, 77, 0, 7),
7,
);
pinged.set(true);
}
v.echo_replies > 0
},
Duration::from_secs(60),
);
let verdict = fixture.verdict();
drop(fixture); // release the port for the next member's activation
assert!(
verdict.initiations > 0 && verdict.responses > 0,
"member {}: the installed machine HANDSHOOK the hub: {verdict:?}",
member.name
);
assert!(
verdict.sealed_packets > 0,
"member {}: traffic crossed the tunnel: {verdict:?}",
member.name
);
assert!(
verdict.echo_replies > 0,
"member {}: the hub's ping came back THROUGH the tunnel from the \
installed machine — tunnel e2e on metal: {verdict:?}",
member.name
);
assert!(
!serial2.contains("MINTED"),
"member {}'s SECOND boot must load, never re-mint:\n{serial2}",
member.name
);
assert_eq!(
minted_pubkey(&serial2, PRODUCT).as_deref(),
Some(key.as_str()),
"member {}: the identity survives reboot #2",
member.name
);
// Gap #12, generation two: the SECOND boot rotated — the first
// boot's narration is now prev.log (containing the MINTED line this
// boot must not print), and boot.log is the new boot's.
let prev = read_fat_file(&disk, p3_first_log, p3_last_log, "log/prev.log")
.unwrap_or_else(|| panic!("member {} rotated to log/prev.log", member.name));
assert!(
String::from_utf8_lossy(&prev).contains("MINTED"),
"member {}: the previous generation is the FIRST boot's narration",
member.name
);
pubkeys.push(key);
}
// ── 4. the fleet-level truth: every machine minted its OWN identity ──
assert_eq!(pubkeys.len(), 2);
assert_ne!(
pubkeys[0], pubkeys[1],
"two machines from ONE ISO must hold DISTINCT identities — a shared \
key means the ISO carries a baked-in identity, the exact defect the \
per-machine mint exists to prevent"
);
// The rollup's fleet cell — an extra proof column (floor-not-cap), through
// the harness's ONE component writer so it lands on `iso-kvm`, never the
// form-less `iso` a KVM green could hide behind.
korp_installer::isoproof::emit_kvm_row(
PRODUCT,
"fleet_distinct_identities",
true,
&format!(
"one ISO -> {} machines, distinct minted pubkeys",
pubkeys.len()
),
);
}
// ─────────────────────────────────────────────────────────────────────────
// The ISO bar's last leg: "…reboot AGAIN → still the same key AND THE REPO
// IS STILL THERE" (henriks_prompt.txt §PROOFS REQUIRED).
//
// The identity half was already proven above. What was NOT proven is the
// half that costs a customer their data: that the SERVED CONTENT written
// through the gateway survives a power cycle. A P3 that remounts empty, or
// a filesystem that loses writes on an unclean stop, passes every identity
// assertion in this file while quietly discarding the customer's artifacts.
//
// WHY THIS RIDES A HOSTFWD AND NOT THE TUNNEL, stated because the brief asks
// for the round trip "over the tunnel": the host-side hub is a raw-packet
// fixture — `HubFixture` seals datagrams and crafts ICMP echo requests
// (`craft_icmp_echo_request`), and it deliberately owns no tun and no root.
// There is no TCP stack behind it, so an HTTP round trip cannot cross it
// from the test host without writing one, which is precisely the twin the
// brief forbids. The two halves are therefore proven separately and neither
// is faked: `one_iso_fans_to_n_machines_each_minting_its_own_identity` proves
// traffic CROSSES the tunnel with NO hostfwd at all (the only path in is the
// tunnel), and this proves content SURVIVES the reboot. Read together they
// cover step 7; read alone neither claims the other.
// ─────────────────────────────────────────────────────────────────────────
/// One HTTP/1.1 request over a fresh connection, hand-rolled on `std::net`
/// so this proof adds no dependency. Returns `(status, body)`; `None` when
/// the guest is not listening yet — the caller RETRIES rather than failing,
/// because "the port is not up in this poll" is not the same as "the server
/// refused", and conflating them is how a flaky proof gets written.
fn http(port: u16, method: &str, path: &str, body: Option<&[u8]>) -> Option<(u16, Vec<u8>)> {
use std::io::{Read, Write};
let mut s = std::net::TcpStream::connect(("127.0.0.1", port)).ok()?;
s.set_read_timeout(Some(Duration::from_secs(10))).ok()?;
let mut req = format!("{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n");
if let Some(b) = body {
req.push_str(&format!("Content-Length: {}\r\n", b.len()));
req.push_str("Content-Type: application/octet-stream\r\n");
}
req.push_str("\r\n");
s.write_all(req.as_bytes()).ok()?;
if let Some(b) = body {
s.write_all(b).ok()?;
}
s.flush().ok()?;
let mut raw = Vec::new();
s.read_to_end(&mut raw).ok()?;
let split = raw.windows(4).position(|w| w == b"\r\n\r\n")?;
let head = String::from_utf8_lossy(&raw[..split]).to_string();
let status: u16 = head.split_whitespace().nth(1)?.parse().ok()?;
Some((status, raw[split + 4..].to_vec()))
}
#[test]
#[ignore = "heavy: installs a machine, PUTs a blob through the gateway, power-cycles it, and GETs the same bytes back off the installed disk"]
fn served_content_survives_a_power_cycle() {
let root = workspace_root();
let Some(preflight) =
KvmPreflight::detect("holger served content survives a power cycle (iso-kvm)")
else {
return;
};
let scratch = root.join("target/content-persist-proof");
let _ = std::fs::remove_dir_all(&scratch);
std::fs::create_dir_all(&scratch).unwrap();
// The cell REQUIRES a hub declaration, so reserve one; this proof never
// activates the pump — the tunnel is the other test's subject.
let hub = HubFixture::reserve().expect("reserve the hub identity");
let hub_conf = hub.hub_conf(
std::net::Ipv4Addr::new(10, 0, 2, 2),
"10.77.0.0/24",
5,
Some((std::net::Ipv4Addr::new(10, 77, 0, 7), 24)),
);
let hub_path = scratch.join("persist-hub.conf");
std::fs::write(&hub_path, &hub_conf).unwrap();
let ron_path = root.join("xtask/tests/fixtures/site-holger.ron");
pack_fresh_app_znippy(&root);
let out = Command::new(env!("CARGO"))
.args([
"run",
"--quiet",
"--manifest-path",
"xtask/Cargo.toml",
"--",
"installer-iso",
"--param",
"site=persist-site",
"--param",
])
.arg(format!("hub_conf=@{}", hub_path.display()))
.arg("--param")
.arg(format!("holger_ron=@{}", ron_path.display()))
.args(["--param", "underlay=10.0.2.15/24@eth0"])
.args(["--root-key", "dev"])
.current_dir(&root)
.output()
.expect("spawn installer-iso");
assert!(
out.status.success(),
"the verb must master the ISO:\n{}",
String::from_utf8_lossy(&out.stderr)
);
let iso = root.join(".iso-out/holger-installer.iso");
assert!(iso.is_file());
let set = holger_build_space::build_variant_set().expect("build space loads");
let cell = set
.installers
.iter()
.find(|i| i.name == "holger-installer")
.expect("the declared cell")
.clone();
// ── install, through the SHARED ceremony ──
let disk = scratch.join("disk.raw");
let idir = scratch.join("install");
IsoInstallProof {
product: PRODUCT,
check: "install_ok",
iso: &iso,
disk: &disk,
disk_bytes: cell.target_disk_bytes(),
disk_args: cell.kvm_disk_args(&disk.display().to_string()),
workdir: &idir,
marker: INSTALL_OK,
timeout: Duration::from_secs(180),
expect_serial: &[],
expect_cmdline: &["korp.entry=holger", "korp.site=persist-site"],
expect_data_files: &[],
detail: "content-persistence arm: per-site ISO → KVM boot → INSTALL-OK",
}
.run(&preflight);
// The blob: path-addressed into the declared `bundles` raw repo, whose
// data dir is /data/store/bundles — ON P3, which is the whole point.
const BLOB_PATH: &str = "/bundles/persist/proof.bin";
let blob: Vec<u8> = (0u16..4096).map(|i| (i % 251) as u8).collect();
// Boot args WITH a host forward: the gateway must be reachable as a real
// TCP peer, not merely announced on serial. The serving assertion in the
// fleet proof reads a serial LINE; this one reads bytes off a socket.
let port = free_port();
let boot_args = |disk: &std::path::Path| {
let mut a = cell.kvm_disk_args(&disk.display().to_string());
attach_slirp_nic(&mut a, Some((port, 8443)));
a
};
// ── boot 1: PUT the blob while the guest is held alive ──
let b1 = scratch.join("boot1");
std::fs::create_dir_all(&b1).unwrap();
let put_ok = std::cell::Cell::new(false);
let serial1 = boot_disk_until_verdict(
&preflight.qemu,
&preflight.ovmf_code,
&preflight.ovmf_vars,
&boot_args(&disk),
&b1,
"HTTP/OCI gateway listening on",
Duration::from_secs(180),
|| {
if put_ok.get() {
return true;
}
match http(port, "PUT", BLOB_PATH, Some(&blob)) {
Some((s, _)) if (200..300).contains(&s) => {
put_ok.set(true);
true
}
_ => false,
}
},
Duration::from_secs(90),
);
assert!(serial1.contains("MINTED"), "first boot mints:\n{serial1}");
assert!(
put_ok.get(),
"the blob was STORED through the gateway (no 2xx seen):\n{serial1}"
);
let key1 =
minted_pubkey(&serial1, PRODUCT).expect("first boot prints the canonical identity line");
// ── boot 2: the SAME disk, power-cycled. GET it back. ──
let b2 = scratch.join("boot2");
std::fs::create_dir_all(&b2).unwrap();
let got = std::cell::RefCell::new(None::<Vec<u8>>);
let serial2 = boot_disk_until_verdict(
&preflight.qemu,
&preflight.ovmf_code,
&preflight.ovmf_vars,
&boot_args(&disk),
&b2,
"HTTP/OCI gateway listening on",
Duration::from_secs(180),
|| {
if got.borrow().is_some() {
return true;
}
match http(port, "GET", BLOB_PATH, None) {
Some((200, body)) => {
*got.borrow_mut() = Some(body);
true
}
_ => false,
}
},
Duration::from_secs(90),
);
// THE ASSERTION THIS TEST EXISTS FOR: same bytes, off the installed disk,
// after a power cycle. Length first so a truncation reports as a length.
let body = got.borrow().clone().unwrap_or_else(|| {
panic!(
"the blob was NOT served after the power cycle — the repo did not survive:\n{serial2}"
)
});
assert_eq!(
body.len(),
blob.len(),
"the stored blob came back TRUNCATED after the reboot"
);
assert_eq!(
body, blob,
"the stored blob came back CORRUPTED after the reboot"
);
// …and the identity half still holds on the same boot, so a data home
// that survived by being REFORMATTED cannot pass: a wipe would re-mint.
assert!(
!serial2.contains("MINTED"),
"the second boot must LOAD, never re-mint:\n{serial2}"
);
assert_eq!(
minted_pubkey(&serial2, PRODUCT).as_deref(),
Some(key1.as_str()),
"the identity survives the same power cycle the content did"
);
korp_installer::isoproof::emit_kvm_row(
PRODUCT,
"installed_content_survives_reboot",
true,
&format!(
"{} bytes PUT through the gateway, byte-identical after a power cycle",
blob.len()
),
);
}
/// The instrument, checked against a real socket. `http` is what decides
/// whether the blob came back, so a bug in IT would report as "the repo did
/// not survive" — a false red on the product for a defect in the test. This
/// drives it against a listener that answers a known body and asserts the
/// status, the length and the bytes are read back exactly.
#[test]
fn the_http_client_reads_status_and_body_off_a_real_socket() {
use std::io::{Read, Write};
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
let body: Vec<u8> = (0u16..300).map(|i| (i % 251) as u8).collect();
let expect = body.clone();
let h = std::thread::spawn(move || {
let (mut s, _) = l.accept().unwrap();
// Read the request head so the client's write side is exercised too.
let mut buf = [0u8; 2048];
let n = s.read(&mut buf).unwrap();
let head = String::from_utf8_lossy(&buf[..n]).to_string();
s.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n",
expect.len()
)
.as_bytes(),
)
.unwrap();
s.write_all(&expect).unwrap();
head
});
let (status, got) = http(port, "GET", "/bundles/persist/proof.bin", None).expect("a reply");
let head = h.join().unwrap();
assert_eq!(status, 200, "the status line is parsed, not guessed");
assert_eq!(
got.len(),
body.len(),
"the whole body is read, not one packet"
);
assert_eq!(got, body, "the bytes survive the read");
assert!(
head.starts_with("GET /bundles/persist/proof.bin HTTP/1.1"),
"{head}"
);
// A closed port is None, NOT a panic — the caller retries on it, and
// conflating "not up yet" with "refused" is how a flaky proof is written.
assert!(
http(free_port(), "GET", "/x", None).is_none(),
"a dead port yields None"
);
}