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

nordisk/nornir

tests/appliance_ladder.rs

//! **nornir on the appliance ladder** — the eight rungs, answered by nornir, for
//! nornir.
//!
//! The rungs are the suite's, not this file's: `nornir_testmatrix::appliance`
//! declares `APPLIANCE_PRODUCTS × APPLIANCE_PROOFS`, and nornir is one of the
//! five products. Until now nornir emitted **nothing at all** on that grid — 0
//! of 8 — which the 2026-09-10 inventory reported as eight holes. A hole says
//! "somebody has not done the work yet". For five of these rungs that is simply
//! untrue: nornir has no ISO, no image and no tunnel, and never claimed one.
//!
//! So this file answers all eight, and the answer is one of exactly two kinds:
//!
//! * **a proof** — nornir's own binary is driven and the row carries what it
//!   did (`identity_ready`, `identity_minted`); or
//! * **a declared `n/a`** — the rung is not a stage nornir has, and the reason
//!   is MEASURED here rather than asserted in prose: the manifest and the build
//!   declaration are read, and the row only goes out when the file really says
//!   what the reason claims it says.
//!
//! The third rung with a proof, `identity_survives_replacement`, is emitted
//! where it was already proven — `src/bin/nornir.rs`'s
//! `uninstall_with_no_flags_kills_data_and_keeps_the_key`, which has asserted
//! exactly that since long before this file existed. A second test re-proving it
//! would be a second implementation of the same claim.
//!
//! ## Why an n/a and not an `#[ignore]`
//!
//! An `#[ignore]`d test is a proof somebody could run on the right box. There is
//! no box on which nornir boots off an installer ISO, because nornir declares no
//! bootable target. `functional_na` says that out loud, with the reason, and the
//! rollup renders it `·` (no such stage) rather than `—` (no evidence).

use std::path::{Path, PathBuf};
use std::process::Command;

use nornir_testmatrix::{functional_na, functional_status};

/// The host form of nornir's appliance component. nornir has exactly one form —
/// it is a systemd service, not a container and not an image — so the bare
/// product name is the whole vocabulary here.
///
/// It is spelled OUT at every call site rather than passed as this constant,
/// and the constant exists to be asserted against. A computed or `const`
/// coordinate is invisible to `cargo`-free discovery — the 2026-09-10 inventory
/// measured exactly that in holger and korp, where the component lived inside a
/// helper and six proven rungs read as "nothing emits this".
const HOST: &str = "nornir-appliance";

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn read(rel: &str) -> String {
    let p = repo_root().join(rel);
    std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
}

/// Does this TOML declare a section of the given kind? Textual on purpose — the
/// question is line-shaped (`[[build.iso]]` alone on a line) and a parser would
/// be a second reader of a file the build thing already owns.
fn declares_section(toml: &str, section: &str) -> bool {
    toml.lines().any(|l| l.trim() == section)
}

/// **Rungs 1 and 2 — the host identity ceremony, driven through the real binary.**
///
/// Two runs of `nornir key generate` against a scratch key directory:
///
/// 1. the first MINTS (`identity_minted`) — the directory is empty before and
///    holds a private + public ed25519 pair after;
/// 2. the second LOADS the same identity (`identity_ready`) — the ceremony is
///    idempotent and the public half is byte-identical, which is the property
///    that makes an identity an identity rather than a fresh key per boot.
///
/// ## Isolating the ceremony from the operator's own key — and why the env
/// override alone is NOT enough
///
/// `DWARVES_SSH_DIR` and `NORNIR_SSH_DIR` are both pointed at the scratch dir,
/// which is the first rung of `nornir_git::deploykey`'s ladder. That is
/// necessary and it is not sufficient, because `Resolution::resolve()` picks the
/// first rung that **actually has a key**, not the first rung that is
/// configured. An empty scratch dir is skipped, the ladder walks on, and it
/// arrives at `$HOME/.nornir/ssh` — the developer's real identity. MEASURED:
/// with the two env vars set and nothing else, the first `key generate` printed
/// `• already present: ~/.nornir/ssh/id_ed25519.pub` and the mint
/// assertion failed on somebody's actual private key.
///
/// So `HOME` is redirected too, into a scratch home beside the key dir — and
/// `DWARVES_SSH_CONFINE=1` removes what `HOME` cannot reach. Rungs 4 and 6 of the
/// ladder are `/home/dwarves/.ssh` and `/home/nornir/.ssh`: ABSOLUTE paths that
/// no env var neutralises. Neither user exists on this box, so the test passed
/// without the confine — by luck about this machine, not by any property of the
/// fixture. On a box with a real dwarves/nornir service install it would have
/// reached a real SERVICE key. Confined, the two explicit overrides ARE the whole
/// ladder, so the isolation holds anywhere.
///
/// A test that had to touch the operator's key to pass would be proving nothing
/// about nornir and quite a lot about the box it ran on.
#[test]
fn the_host_identity_ceremony_mints_once_and_then_loads_the_same_identity() {
    let base = std::env::temp_dir().join(format!("nornir-appliance-ladder-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&base);
    let dir = base.join("ssh");
    let home = base.join("home");
    std::fs::create_dir_all(&dir).expect("scratch key dir");
    std::fs::create_dir_all(&home).expect("scratch home");
    let priv_path = dir.join("id_ed25519");
    let pub_path = dir.join("id_ed25519.pub");
    assert!(!pub_path.exists(), "the scratch dir starts with no identity");

    let run = |what: &str| -> String {
        let out = Command::new(env!("CARGO_BIN_EXE_nornir"))
            .args(["key", "generate"])
            .env("DWARVES_SSH_DIR", &dir)
            .env("NORNIR_SSH_DIR", &dir)
            // The rung below the env overrides — without this the ladder reaches
            // the real one. See the note above.
            .env("HOME", &home)
            // …and the rungs $HOME CANNOT reach: /home/dwarves/.ssh and
            // /home/nornir/.ssh are ABSOLUTE. On this box neither user exists, so
            // the test passed without this — but on a box with a real service
            // install the ladder would walk into a real SERVICE key, and no env
            // var could stop it. DWARVES_SSH_CONFINE deletes those rungs
            // outright (nornir-git, edda 1b70392), which is what makes the
            // isolation a property of the FIXTURE rather than of this machine.
            .env("DWARVES_SSH_CONFINE", "1")
            .output()
            .unwrap_or_else(|e| panic!("run `nornir key generate` ({what}): {e}"));
        assert!(out.status.success(), "`nornir key generate` ({what}) exited {}", out.status);
        let stdout = String::from_utf8_lossy(&out.stdout).to_string();
        // The ceremony must have happened in the SCRATCH dir. If the ladder ever
        // walks past it again, this says so in one line instead of leaving a
        // confusing assertion failure three lines down.
        assert!(
            !stdout.contains("/.nornir/ssh") || stdout.contains(&*dir.to_string_lossy()),
            "`nornir key generate` ({what}) reached an identity outside the scratch dir {} — \
             the deploykey ladder walked past the override: {stdout}",
            dir.display()
        );
        stdout
    };

    let first = run("mint");
    let minted = priv_path.is_file() && pub_path.is_file();
    let pub1 = std::fs::read_to_string(&pub_path).unwrap_or_default();
    let shaped = pub1.starts_with("ssh-ed25519 ") && pub1.contains("nornir@");
    functional_status(
        "nornir-appliance",
        "identity_minted",
        minted && shaped && first.contains("generated"),
        &format!(
            "host form (nornir declares no container image): `nornir key generate` minted \
             {} + {} — {:?}",
            priv_path.display(),
            pub_path.display(),
            pub1.split_whitespace().next_back().unwrap_or("")
        ),
    );
    assert!(minted, "the mint wrote both halves: {first}");
    assert!(shaped, "the public half is an ed25519 OpenSSH line: {pub1:?}");

    let second = run("load");
    let pub2 = std::fs::read_to_string(&pub_path).unwrap_or_default();
    let stable = pub2 == pub1 && !pub1.is_empty();
    functional_status(
        "nornir-appliance",
        "identity_ready",
        stable && second.contains("already present"),
        &format!(
            "host form: the second ceremony LOADED the same identity (public half byte-identical, \
             {} bytes) instead of minting a new one",
            pub1.len()
        ),
    );
    assert!(stable, "a re-run must not mint a second identity:\n{first}\n{second}");
    assert!(second.contains("already present"), "and it must say so: {second}");

    let _ = std::fs::remove_dir_all(&base);
}

/// **Rungs 5 and 6 — the tunnel, which nornir has not got.**
///
/// The ceremony that proves `tunnel_up_traffic_crossed` and
/// `ping_through_tunnel` lives in one crate, `wg-appliance-core`; a product that
/// does not depend on it has no tunnel to bring up. That is the measurement:
/// nornir's own manifest is read and the absence is checked, so the day nornir
/// grows a tunnel this test goes RED and the two rows stop being `n/a`.
#[test]
fn nornir_has_no_tunnel_so_the_two_tunnel_rungs_are_declared_not_applicable() {
    let manifest = read("Cargo.toml");
    let live_dep = |needle: &str| {
        manifest
            .lines()
            .map(str::trim)
            .filter(|l| !l.starts_with('#'))
            .any(|l| l.starts_with(needle))
    };
    let has_ceremony = live_dep("wg-appliance-core");
    let has_tunnr_vm = live_dep("tunnr-vm");
    assert!(
        !has_ceremony && !has_tunnr_vm,
        "nornir now declares a tunnel dependency — these two rows are no longer n/a, \
         they are proofs somebody owes (wg-appliance-core={has_ceremony}, tunnr-vm={has_tunnr_vm})"
    );

    functional_na(
        "nornir-appliance",
        "tunnel_up_traffic_crossed",
        "nornir declares no wg-appliance-core (the crate that owns the ceremony) and no tunnr-vm — \
         it is a warehouse daemon on the host network, with no tunnel to bring up",
    );
    functional_na(
        "nornir-appliance",
        "ping_through_tunnel",
        "nornir declares no wg-appliance-core (the crate that owns the ceremony) and no tunnr-vm — \
         there is no tunnel for an echo to cross",
    );
}

/// **Rungs 3, 7 and 8 — the image and the disk, which nornir also has not got.**
///
/// `identity_survives_upgrade` is an IMAGE upgrade, `install_ok` is an installer
/// ISO laying the product onto a disk, and `secure_boot_armed` is enforcing
/// firmware accepting that ISO. All three presuppose a bootable artefact, and
/// nornir's build declaration has none: `.nornir/build-variants.toml` declares
/// `[[build.variant]]` rows and nothing else. Measured here, so the day nornir
/// declares an ISO the test goes red and three `n/a`s become three holes to fill.
#[test]
fn nornir_declares_no_bootable_artefact_so_three_rungs_are_declared_not_applicable() {
    let variants = read(".nornir/build-variants.toml");
    let image = declares_section(&variants, "[[build.image]]");
    let iso = declares_section(&variants, "[[build.iso]]");
    let installer = declares_section(&variants, "[[build.installer]]");
    let vm = declares_section(&variants, "[[build.vm]]");
    assert!(
        declares_section(&variants, "[[build.variant]]"),
        "the file was read and it really is nornir's build declaration"
    );
    assert!(
        !image && !iso && !installer && !vm,
        "nornir now declares a bootable artefact — these rows are proofs somebody owes, not n/a \
         (image={image}, iso={iso}, installer={installer}, vm={vm})"
    );

    functional_na(
        "nornir-appliance",
        "identity_survives_upgrade",
        "nornir declares no [[build.image]] — there is no image to upgrade; the service form's \
         upgrade is `nornir install update`, and the key store it must not touch is proven by \
         identity_survives_replacement",
    );
    functional_na(
        "nornir-appliance",
        "install_ok",
        "nornir declares no [[build.iso]] and no [[build.installer]] in \
         .nornir/build-variants.toml — it installs as a systemd unit through skidbladnir, it \
         never lays itself onto a disk",
    );
    functional_na(
        "nornir-appliance",
        "secure_boot_armed",
        "nornir signs no boot artefact because it declares none — see install_ok; enforcing \
         firmware has nothing of nornir's to accept or refuse",
    );
}

/// The file has to answer ALL EIGHT — a ladder with a quietly missing rung is
/// the same failure as the grid this whole exercise came out of.
///
/// So the roster is read from the crate that owns it and every rung is checked
/// off against what this repo emits: the two proved here, the one proved in
/// `src/bin/nornir.rs`, and the five declared `n/a`.
#[test]
fn every_rung_of_the_ladder_is_answered_by_this_repo() {
    let here = Path::new(file!()).file_name().and_then(|n| n.to_str()).unwrap_or("");
    assert!(!here.is_empty(), "this test knows its own file");

    let answered: [(&str, &str); 8] = [
        ("identity_ready", "proved here"),
        ("identity_minted", "proved here"),
        ("identity_survives_replacement", "proved in src/bin/nornir.rs"),
        ("identity_survives_upgrade", "n/a here"),
        ("tunnel_up_traffic_crossed", "n/a here"),
        ("ping_through_tunnel", "n/a here"),
        ("install_ok", "n/a here"),
        ("secure_boot_armed", "n/a here"),
    ];
    for proof in nornir_testmatrix::APPLIANCE_PROOFS {
        assert!(
            answered.iter().any(|(p, _)| *p == proof),
            "the ladder grew the rung `{proof}` and nornir answers it nowhere — \
             a silent hole is exactly what this file exists to prevent"
        );
    }

    // …and the claims about OTHER files are true, not decoration.
    let ladder = read("tests/appliance_ladder.rs");
    for proof in ["identity_ready", "identity_minted"] {
        assert!(ladder.contains(&format!("\"{proof}\"")), "{proof} is emitted in this file");
    }
    // The component is spelled out at the call sites, and THIS is the spelling.
    assert!(
        ladder.contains(&format!("\"{HOST}\"")),
        "every row here carries the host component `{HOST}`"
    );
    let bin = read("src/bin/nornir.rs");
    assert!(
        bin.contains("\"identity_survives_replacement\""),
        "src/bin/nornir.rs really is where the replacement rung is emitted"
    );
}