crates/nornir-build-thing/tests/cloud_preset_closure_live.rs
//! **HEAVY, LIVE — a cloud preset's closure is composed, built and BOOTED on
//! the machine that preset IS.** `#[ignore]`d.
//!
//! This is the step that turns a derived Kconfig set into a measured one, and
//! it is the only thing that can. `target::presets` can say AWS Nitro's root
//! device is NVMe and `Target::kconfig_needs` can derive `CONFIG_BLK_DEV_NVME`
//! + `CONFIG_PCI_MSI` from that fact, but neither of them has ever seen an
//! NVMe controller. Until a kernel composed from that closure boots on a
//! machine with one and reports `/dev/nvme0n1`, "the preset is closed" is a
//! claim about a data structure.
//!
//! # What one row proves, and what it does not
//!
//! Three legs, each an APPLIED OUTPUT read off the guest by
//! [`KernelProbe`](nornir_build_thing::kernel::KernelProbe), never a
//! round-trip:
//!
//! * **(a) storage** — the declared boot disk's node exists in `/dev` after
//! `devtmpfs`. The driver bound a real controller.
//! * **(b) network** — an interface other than `lo` is in `/sys/class/net`.
//! Only a BUILT-IN driver that bound its device puts a name there; a `=m`
//! driver in an initramfs puts nothing, which is the measured "an e1000=m
//! guest is dead metal" failure.
//! * **(c) console** — the kernel registered one, read off `/proc/consoles`.
//!
//! And what it does NOT prove is written into the machine it boots
//! ([`ProbeMachine`](nornir_build_thing::kernel::ProbeMachine)): the storage leg is
//! always the real device or the preset is UNPROVABLE HERE, and a NIC QEMU has no
//! model for is rendered as NO NIC — leg (b) is then reported unproven, never green.
//! A green on `aws-nitro` means *the NVMe closure binds and the kernel registers a
//! console*. It does not mean `ena` works, and only a real Nitro instance can say
//! that.
//!
//! Ported onto release-3 on 2026-10-03 from lane AN (`saved/kconfig-solver-2026-08-30`).
//! The lane's own `QemuShape`/`LegProof` model did not land; release-3 judges the
//! machine per facet (`MimicAxes`, 2026-09-05), and this door reads those axes.
//!
//! # LAW 2 — the RED this test is worth nothing without
//!
//! `--red` (env `CLOUD_PRESET_RED=1`) boots the **stock appliance kernel** —
//! the one composed with no `target` group at all — on the same machines. It
//! must FAIL leg (a) on every cloud shape, because that kernel has no NVMe, no
//! SCSI disk and no ethernet driver but virtio's. Measured 2026-08-30 on the
//! shipped `tunnr-kernel` bzImage (2 388 992 B):
//!
//! ```text
//! aws-nitro q35 + -device nvme + e1000e -> nvme ABSENT · /sys/class/net = [lo]
//! gce-gvnic pc + -device nvme + e1000e -> nvme ABSENT · /sys/class/net = [lo]
//! azure-gen1 pc + -device ide-hd + hv_* -> scsi-sd ABSENT
//! metal q35 + nvme + ahci + e1000e -> nvme ABSENT · scsi-sd ABSENT · [lo]
//! ```
//!
//! # How to run
//!
//! ```bash
//! export KERNEL_LIVE_VERSION=6.12.104
//! export KERNEL_LIVE_SHA256=949d166a1263bd0cf0c5aab44eeb57869615099e7c48bf88d0e4adb77379b999
//! export CLOUD_PRESET_ROOT=/home/rickard/scratch/laneAN/root # holds .kernel-out/
//! export CLOUD_PRESET_NAMES=aws-nitro,gce-gvnic,azure-gen1,scaleway-elastic-metal
//! cargo test -p nornir-build-thing --features kernel-build \
//! --test cloud_preset_closure_live -- --ignored --nocapture
//! ```
#![cfg(feature = "kernel-build")]
use std::collections::BTreeMap;
use nornir_build_thing::installer::{InstallerBuild, KernelDonor};
use nornir_build_thing::kernel::{
build_kernel_with, probe_kernel_shaped, DraupnirKernelRunner, KernelBuild, ProbeMachine, UreqFetcher,
};
use nornir_build_thing::kernel_requires::requirements_of_variant;
use nornir_build_thing::target::presets::{preset, TargetPreset};
use nornir_build_thing::target::Target;
use nornir_build_thing::variant::BuildVariantSet;
/// The consumer that DECLARES the machine: one installer whose `target` group
/// names the preset and whose kernel edge is where the closure lands. Nothing
/// here spells a Kconfig symbol — that is the whole point.
fn consumer_for(name: &str) -> BuildVariantSet {
let mut set = BuildVariantSet::default();
set.installers.push(InstallerBuild {
name: "cloud-closure".into(),
target: Target { preset: Some(name.to_string()), ..Target::default() },
kernel_donor: Some(KernelDonor { repo: "tunnr".into(), kernel: "tunnr-kernel".into() }),
..Default::default()
});
set
}
/// The kernel a preset's closure composes to. `slim` and not `nuked`: the
/// `nuked` overlay vetoes `CONFIG_ATA` and `CONFIG_SCSI` BY NAME, so an
/// `ide-disk`, `sata-ahci` or `hv-scsi` closure over it is refused by
/// `apply_requirements` — "a consumer requires what the preset vetoes". That
/// refusal is the mechanism working, not a bug, and it is a REAL finding about
/// the shipped appliance donor: today's appliance kernel cannot be the kernel
/// for Azure or for metal, and the composer says so before anything is built.
fn kernel_for(p: &TargetPreset, version: &str, sha: &str) -> KernelBuild {
let mut k = KernelBuild {
name: format!("closure-{}", p.name),
version: version.into(),
tarball_sha256: sha.into(),
preset: "slim".into(),
base: Some("tinyconfig".into()),
profile: "vanilla".into(),
mem_limit_mb: 8192,
cpus: 16,
timeout_secs: 5400,
probe_timeout_secs: 300,
..KernelBuild::default()
};
// The default image is `debian:trixie`, which has no gcc — measured here
// as `/bin/sh: gcc: not found` on the very first probe. The kernel
// toolchain image is the one carrying gcc, cpio and qemu-system-x86.
if let Ok(img) = std::env::var("CLOUD_PRESET_TOOLCHAIN") {
k.toolchain_image = img;
k.toolchain_packages = Vec::new();
}
if let Ok(d) = std::env::var("CLOUD_PRESET_TOOLCHAIN_SHA256") {
k.toolchain_image_sha256 = d;
}
k
}
#[test]
#[ignore = "HEAVY: composes, compiles and BOOTS one kernel per named cloud preset. Needs \
KERNEL_LIVE_VERSION + KERNEL_LIVE_SHA256, CLOUD_PRESET_ROOT and a podman socket."]
fn a_cloud_presets_closure_boots_on_the_machine_it_was_composed_for() {
let (Ok(version), Ok(sha), Ok(root)) = (
std::env::var("KERNEL_LIVE_VERSION"),
std::env::var("KERNEL_LIVE_SHA256"),
std::env::var("CLOUD_PRESET_ROOT"),
) else {
eprintln!(
"SKIP (not a pass): set KERNEL_LIVE_VERSION, KERNEL_LIVE_SHA256 and \
CLOUD_PRESET_ROOT. This test refuses to fetch an unpinned kernel."
);
return;
};
let root = std::path::PathBuf::from(root);
std::fs::create_dir_all(&root).expect("the build root");
let names: Vec<String> = std::env::var("CLOUD_PRESET_NAMES")
.unwrap_or_else(|_| "aws-nitro,gce-gvnic,azure-gen1,scaleway-elastic-metal".into())
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let runner = DraupnirKernelRunner::new();
let mut verdicts: Vec<String> = Vec::new();
for name in &names {
let p = preset(name).unwrap_or_else(|| panic!("{name}: no such preset"));
let shape = match ProbeMachine::for_preset(p) {
Ok(s) => s,
Err(why) => {
verdicts.push(format!("{name}: UNPROVABLE HERE — {why}"));
continue;
}
};
// ── the closure, derived: the consumer declares the MACHINE, the
// engine derives the symbols. Nothing below names one.
let set = consumer_for(name);
set.installers[0].validate().expect("the declaration");
let reqs = requirements_of_variant(&set, "lane-an", "cloud-closure", &BTreeMap::new())
.expect("the closure")
.unwrap_or_else(|| panic!("{name}: the closure is empty"));
let mut k = kernel_for(p, &version, &sha);
// The composer's own veto check — a preset that cannot grant what the
// machine needs is refused HERE, by name, before a container starts.
if let Err(e) = k.apply_requirements(&reqs) {
verdicts.push(format!("{name}: REFUSED BY THE COMPOSER — {e:#}"));
continue;
}
eprintln!(
"\n══ {name} ══ {} symbols · machine {} · cpu {:?} · disk {} -> {} · nic {:?} · substitutions {:?} · unrendered {:?}",
reqs.rows.len(),
shape.machine,
shape.cpu_arg(),
shape.disk_device,
shape.disk_node,
shape.nic_device,
shape.substitutions,
shape.unrendered,
);
let started = std::time::Instant::now();
let pin = build_kernel_with(&UreqFetcher::default(), &runner, &k, &root)
.unwrap_or_else(|e| panic!("{name}: the build — {e:#}"));
eprintln!("{name}: built in {:?} · bzImage {}", started.elapsed(), pin.iter().map(|a| a.sha256.as_str()).collect::<Vec<_>>().join(" "));
// `CLOUD_PRESET_BUILD_ONLY=1` stops after the compile. It exists for
// one measured reason: the toolchain container has no `/dev/kvm`, and
// a Hyper-V shape's `-cpu host,hv_*` is refused outright by qemu under
// TCG ("CPU model 'host' requires KVM or HVF"). Such a preset can only
// be probed on a KVM host, with the SAME argv `probe_script_for`
// renders — never on a different machine than the one declared.
if std::env::var("CLOUD_PRESET_BUILD_ONLY").is_ok() {
verdicts.push(format!(
"{name}: BUILT ({} symbols), probe deferred to a KVM host",
reqs.rows.len()
));
continue;
}
let probe = probe_kernel_shaped(&runner, &k, &root, Some(&shape))
.unwrap_or_else(|e| panic!("{name}: the probe boot — {e:#}"));
eprintln!(
"{name}: devices {:?} · net {:?} · consoles {:?}",
probe.devices, probe.nets, probe.consoles
);
probe.closes(name, &shape).unwrap_or_else(|e| panic!("{e:#}"));
// The honest half: name every facet a real instance still owes.
let open: Vec<String> = shape
.substitutions
.iter()
.map(|(facet, missing, with)| format!("{facet} ({with} stood in for {missing})"))
.chain(shape.unrendered.iter().map(|f| format!("{f} (no device — needs a real instance)")))
.collect();
verdicts.push(if open.is_empty() {
format!("{name}: CLOSED — every leg the real device")
} else {
format!("{name}: CLOSED under QEMU, still owed by a real instance: {}", open.join(" · "))
});
}
eprintln!("\n── the closure ledger ──");
for v in &verdicts {
eprintln!(" {v}");
}
assert!(!verdicts.is_empty());
}
/// **THE RED.** The stock appliance kernel — composed with NO `target` group,
/// so none of lane AN's declarations are in it — on the same cloud machines.
/// Every one must FAIL leg (a): that is what makes the green above worth
/// something. A closure that would pass with the declaration removed is a
/// hollow guard.
#[test]
#[ignore = "LAW 2: boots the UNDECLARED kernel on each cloud machine and requires it to FAIL. \
Needs CLOUD_PRESET_RED_BZIMAGE + CLOUD_PRESET_ROOT and a podman socket."]
fn the_undeclared_kernel_fails_every_cloud_machine() {
let (Ok(bz), Ok(root)) = (
std::env::var("CLOUD_PRESET_RED_BZIMAGE"),
std::env::var("CLOUD_PRESET_ROOT"),
) else {
eprintln!("SKIP (not a pass): set CLOUD_PRESET_RED_BZIMAGE to a stock appliance bzImage.");
return;
};
let root = std::path::PathBuf::from(root);
let mut k = KernelBuild {
name: "red-undeclared".into(),
version: "0.0.0".into(),
tarball_sha256: "0".repeat(64),
probe_timeout_secs: 300,
..KernelBuild::default()
};
if let Ok(img) = std::env::var("CLOUD_PRESET_TOOLCHAIN") {
k.toolchain_image = img;
k.toolchain_packages = Vec::new();
}
let out = nornir_build_thing::kernel::kernel_output_dir(&root, &k);
std::fs::create_dir_all(&out).expect("the red output dir");
std::fs::copy(&bz, out.join("bzImage")).expect("stage the stock bzImage");
let runner = DraupnirKernelRunner::new();
for name in ["aws-nitro", "gce-gvnic", "azure-gen1", "scaleway-elastic-metal"] {
let p = preset(name).unwrap();
let Ok(shape) = ProbeMachine::for_preset(p) else { continue };
let probe = probe_kernel_shaped(&runner, &k, &root, Some(&shape))
.unwrap_or_else(|e| panic!("{name}: the RED probe did not even boot — {e:#}"));
let verdict = probe.closes(name, &shape);
eprintln!("RED {name}: devices {:?} · net {:?} -> {verdict:?}", probe.devices, probe.nets);
assert!(
verdict.is_err(),
"{name}: the UNDECLARED kernel passed this machine — the declaration is not what \
makes the green, so the green proves nothing"
);
}
}