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

nordisk/edda

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"
        );
    }
}