Gunnar

mikronova/draupnir

src/seed.rs

//! # NoCloud cloud-init seed — draupnir's owned, pure seed builder
//!
//! The **NoCloud** datasource is how cloud-init provisions a machine at first boot
//! from a **local** source (no metadata server): it looks for a volume — a vfat
//! image or a directory — labelled `cidata` carrying a small file set
//! (`user-data`, `meta-data`, and an optional `network-config`) and applies it.
//!
//! This module is the **pure builder** that emits that seed. It is the *source of
//! truth* for the NoCloud seed across the constellation — ported from
//! `Skidbladnir/src/machine.rs`'s `KvmController::nocloud_seed_files` /
//! `build_seed_image` / `build_fat_image` so draupnir (the one boot engine) owns
//! it, and Skidbladnir/jera consume it rather than re-rolling their own.
//!
//! ## Two outputs, one file set
//!
//! - [`seed_files`] — the **pure** render of the NoCloud file set from a
//!   [`CloudInit`]. Zero dependencies, always compiled. Everything else is built
//!   on top of it, so the *contents* of a seed are identical whichever container
//!   carries them.
//! - [`write_seed_dir`] — write the file set into a plain **seed directory** (a
//!   NoCloud `seedfrom` dir / a bind-mount / a `-fw_cfg` source). Pure std, always
//!   compiled — the always-works fallback that needs no image tooling.
//! - [`build_seed_image`] — author a **vfat seed image** (volume label `cidata`)
//!   in **pure Rust** via `fatfs` — no `genisoimage`/`mkisofs` subprocess. Behind
//!   the `seed` feature (the only part that pulls a dependency); this is what the
//!   KVM backend attaches to a VM as a read-only virtio drive.
//!
//! Only the seed *image* costs a dependency; the file-set and the seed *directory*
//! are pure std, so any boot path (KVM today; a container init that reads a NoCloud
//! dir tomorrow) can attach a seed with no extra crates.

use crate::{CloudInit, Error, Result};
use std::path::{Path, PathBuf};

/// The canonical NoCloud **volume label**. cloud-init matches it
/// case-insensitively; `cidata` is the canonical spelling. It labels both the vfat
/// [`build_seed_image`] output and (conventionally) a [`write_seed_dir`] mount.
pub const NOCLOUD_LABEL: &str = "cidata";

/// The default `meta-data` supplied when a [`CloudInit`] omits one — a minimal
/// document carrying an `instance-id` and `local-hostname` so cloud-init always
/// finds the two files a NoCloud datasource requires.
pub const DEFAULT_META_DATA: &str = "instance-id: draupnir\nlocal-hostname: draupnir\n";

/// **Pure** render of the NoCloud seed file set — `[("user-data", …),
/// ("meta-data", …)]`, plus `("network-config", …)` when the [`CloudInit`] carries
/// one. When `meta-data` is omitted, [`DEFAULT_META_DATA`] is supplied so both
/// required files are always present. This is the testable core every other output
/// in this module is built from (zero deps, always compiled).
pub fn seed_files(ci: &CloudInit) -> Vec<(&'static str, String)> {
    let md = ci
        .meta_data
        .clone()
        .unwrap_or_else(|| DEFAULT_META_DATA.to_string());
    let mut files = vec![("user-data", ci.user_data.clone()), ("meta-data", md)];
    // cloud-init reads an optional `network-config` from the same NoCloud volume.
    if let Some(nc) = &ci.network_config {
        files.push(("network-config", nc.clone()));
    }
    files
}

/// Write the NoCloud [`seed_files`] into `dir` as a plain **seed directory** (the
/// pure-std, no-image-tooling output): `dir/user-data`, `dir/meta-data`, and
/// `dir/network-config` when present. `dir` is created if it does not exist.
/// Returns the directory path. Always compiled (no feature, no dependency).
pub fn write_seed_dir(dir: impl AsRef<Path>, ci: &CloudInit) -> Result<PathBuf> {
    let dir = dir.as_ref();
    let outcome = write_seed_dir_inner(dir, ci);
    // Record the NoCloud-seed verdict: GREEN when the file set landed on disk, RED
    // when a mkdir/write failed — the seed surface nornir's matrix reads back (a boot
    // whose cloud-init seed could not be authored must be visible, never swallowed).
    crate::functional_status(
        "draupnir/seed",
        "write_seed_dir",
        outcome.is_ok(),
        &match &outcome {
            Ok(()) => format!("NoCloud seed dir written to {}", dir.display()),
            Err(e) => format!("NoCloud seed dir {} failed: {e}", dir.display()),
        },
    );
    outcome.map(|()| dir.to_path_buf())
}

/// Create `dir` (and any missing parents) with the leaf **private to its owner**
/// — mode 0700 on unix, applied by `mkdir(2)` itself so there is never an
/// instant at which the directory is group- or world-readable.
///
/// A directory that ALREADY exists is left exactly as it is, deliberately: the
/// parent of a seed may be `/tmp` or a caller-owned tree, and silently
/// tightening someone else's directory is a worse bug than the one this fixes.
/// The mode that protects the key material is the FILE mode below; this is
/// defence in depth for the directory this function itself creates.
///
/// Public because a CONSUMER that pre-creates the workdir it later hands to
/// `build_seed_image*` must create it with the same discipline — draupnir owns
/// the seed writer, so it owns this too rather than every caller re-rolling it.
pub fn create_private_dir(dir: &Path) -> Result<()> {
    #[cfg(unix)]
    {
        use std::os::unix::fs::DirBuilderExt;
        std::fs::DirBuilder::new()
            .recursive(true)
            .mode(0o700)
            .create(dir)
            .map_err(|e| Error::Backend(format!("mkdir {}: {e}", dir.display())))
    }
    #[cfg(not(unix))]
    {
        std::fs::create_dir_all(dir)
            .map_err(|e| Error::Backend(format!("mkdir {}: {e}", dir.display())))
    }
}

/// Write `bytes` to `path` in a file **created** mode 0600 (unix).
///
/// The mode is handed to `open(2)` as the creation mode, not applied afterwards
/// with `set_permissions`: a `write`-then-`chmod` leaves a window in which the
/// key material is already on disk and still world-readable, and that window IS
/// the defect (F61). Any pre-existing file is removed first and the create is
/// `O_EXCL`, so the mode always applies (an `open` of an existing file ignores
/// it) and a symlink planted at `path` in a shared directory makes this FAIL
/// rather than write key bytes through it.
fn write_private(path: &Path, bytes: &[u8]) -> Result<()> {
    use std::io::Write;
    let _ = std::fs::remove_file(path);
    let mut opts = std::fs::OpenOptions::new();
    opts.write(true).create_new(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt;
        opts.mode(0o600);
    }
    let mut f = opts
        .open(path)
        .map_err(|e| Error::Backend(format!("create {}: {e}", path.display())))?;
    f.write_all(bytes)
        .map_err(|e| Error::Backend(format!("write {}: {e}", path.display())))?;
    f.flush()
        .map_err(|e| Error::Backend(format!("flush {}: {e}", path.display())))
}

/// The pure seed-directory write (no emit) — the always-works fallback path.
fn write_seed_dir_inner(dir: &Path, ci: &CloudInit) -> Result<()> {
    create_private_dir(dir)?;
    for (name, contents) in seed_files(ci) {
        write_private(&dir.join(name), contents.as_bytes())?;
    }
    Ok(())
}

/// Materialize a [`CloudInit`] into a NoCloud **seed image** at `out`: a FAT12/16
/// image, volume-labelled [`NOCLOUD_LABEL`] (`cidata`), holding [`seed_files`] at
/// its root. **Pure Rust** via `fatfs` — no `genisoimage`/`mkisofs` subprocess
/// (zero-shell), the same authoring Skidbladnir uses. Returns the image path. The
/// KVM backend attaches this as a read-only virtio drive; the guest's cloud-init
/// finds it by its `cidata` vfat label at first boot. (feature `seed`)
#[cfg(feature = "seed")]
pub fn build_seed_image(out: impl AsRef<Path>, ci: &CloudInit) -> Result<PathBuf> {
    let out = out.as_ref();
    let blobs: Vec<(String, Vec<u8>)> = seed_files(ci)
        .into_iter()
        .map(|(n, c)| (n.to_string(), c.into_bytes()))
        .collect();
    let outcome = build_fat_image(out, NOCLOUD_LABEL, &blobs);
    // Record the seed-image verdict: GREEN when the `cidata` vfat image was authored,
    // RED when fatfs formatting/authoring failed — the seed surface nornir's matrix
    // reads back (the KVM backend attaches this image; a failed author must be visible).
    crate::functional_status(
        "draupnir/seed",
        "build_seed_image",
        outcome.is_ok(),
        &match &outcome {
            Ok(()) => format!("NoCloud `cidata` seed image authored at {}", out.display()),
            Err(e) => format!("NoCloud seed image {} failed: {e}", out.display()),
        },
    );
    outcome.map(|()| out.to_path_buf())
}

/// Materialize a [`CloudInit`] into a NoCloud **seed image** at `out` **plus a set
/// of extra, consumer-native files** at the same volume root — the knob a
/// distroless appliance needs (L5: add the knob here, never fork the builder).
///
/// The standard [`seed_files`] (`user-data`, `meta-data`, optional
/// `network-config`) are authored exactly as [`build_seed_image`] does, so a
/// full-OS guest's **cloud-init still provisions from this very image**; `extra`
/// rides alongside for guests that have no cloud-init and read their own file
/// (tunnr's distroless PID-1 reads `tunnr-appliance.conf`). An empty `extra`
/// produces a byte-identical image to [`build_seed_image`]. (feature `seed`)
#[cfg(feature = "seed")]
pub fn build_seed_image_with(
    out: impl AsRef<Path>,
    ci: &CloudInit,
    extra: &[(String, Vec<u8>)],
) -> Result<PathBuf> {
    let out = out.as_ref();
    let mut blobs: Vec<(String, Vec<u8>)> = seed_files(ci)
        .into_iter()
        .map(|(n, c)| (n.to_string(), c.into_bytes()))
        .collect();
    blobs.extend(extra.iter().cloned());
    let outcome = build_fat_image(out, NOCLOUD_LABEL, &blobs);
    crate::functional_status(
        "draupnir/seed",
        "build_seed_image_with",
        outcome.is_ok(),
        &match &outcome {
            Ok(()) => format!(
                "NoCloud `cidata` seed image (+{} extra file(s)) authored at {}",
                extra.len(),
                out.display()
            ),
            Err(e) => format!("NoCloud seed image {} failed: {e}", out.display()),
        },
    );
    outcome.map(|()| out.to_path_buf())
}

/// Read every root-level file out of an in-memory **FAT seed image** as
/// `(name, bytes)` — the GUEST side of the seam. The image is taken as a byte
/// slice (not a path) deliberately: a guest reads its seed off a **read-only**
/// virtio block device, which cannot be opened `O_RDWR` as `fatfs` requires, so
/// the caller slurps `/dev/vdX` with `std::fs::read` and hands the bytes here.
/// Pure Rust, no mount, no loop device, no `mtools`. (feature `seed`)
#[cfg(feature = "seed")]
pub fn read_seed_files(image: &[u8]) -> Result<Vec<(String, Vec<u8>)>> {
    use std::io::Read;
    let cursor = std::io::Cursor::new(image.to_vec());
    let fs = fatfs::FileSystem::new(cursor, fatfs::FsOptions::new())
        .map_err(|e| Error::Backend(format!("open FAT seed image: {e}")))?;
    let mut out = Vec::new();
    for entry in fs.root_dir().iter() {
        let entry = entry.map_err(|e| Error::Backend(format!("read FAT dir: {e}")))?;
        if entry.is_dir() {
            continue;
        }
        let name = entry.file_name();
        let mut buf = Vec::new();
        entry
            .to_file()
            .read_to_end(&mut buf)
            .map_err(|e| Error::Backend(format!("read {name} from FAT seed: {e}")))?;
        out.push((name, buf));
    }
    Ok(out)
}

/// The FAT **volume label** of an in-memory seed image — a guest uses this to
/// confirm it found the NoCloud volume ([`NOCLOUD_LABEL`], case-insensitive)
/// rather than some other attached disk. (feature `seed`)
#[cfg(feature = "seed")]
pub fn seed_volume_label(image: &[u8]) -> Result<String> {
    let cursor = std::io::Cursor::new(image.to_vec());
    let fs = fatfs::FileSystem::new(cursor, fatfs::FsOptions::new())
        .map_err(|e| Error::Backend(format!("open FAT seed image: {e}")))?;
    Ok(fs.volume_label())
}

/// Whether an in-memory image is a NoCloud seed: it parses as FAT **and** its
/// volume label is [`NOCLOUD_LABEL`] (matched case-insensitively, exactly as
/// cloud-init matches it). The guest's device-probe predicate. (feature `seed`)
#[cfg(feature = "seed")]
pub fn is_nocloud_seed(image: &[u8]) -> bool {
    matches!(seed_volume_label(image), Ok(l) if l.trim().eq_ignore_ascii_case(NOCLOUD_LABEL))
}

/// Read an in-memory seed image back into the [`CloudInit`] it was authored from
/// (`user-data` + `meta-data` + optional `network-config`). Round-trips
/// [`build_seed_image`]. Extra, non-standard files are ignored here — read those
/// with [`read_seed_files`]. (feature `seed`)
#[cfg(feature = "seed")]
pub fn read_seed_image(image: &[u8]) -> Result<CloudInit> {
    let files = read_seed_files(image)?;
    let pick = |want: &str| {
        files
            .iter()
            .find(|(n, _)| n.eq_ignore_ascii_case(want))
            .map(|(_, b)| String::from_utf8_lossy(b).into_owned())
    };
    let user_data =
        pick("user-data").ok_or_else(|| Error::Backend("seed image has no `user-data`".into()))?;
    Ok(CloudInit {
        user_data,
        meta_data: pick("meta-data"),
        network_config: pick("network-config"),
    })
}

/// Pull one **flat scalar** out of a NoCloud `meta-data` document — the
/// `key: value` lines cloud-init's NoCloud `meta-data` is made of
/// (`instance-id`, `local-hostname`). Deliberately NOT a YAML parser: a
/// distroless PID-1 must read its identity without dragging serde/yaml into a
/// static init binary, and NoCloud `meta-data` is flat by construction. Comments
/// (`#`) and surrounding quotes are stripped; the first match wins. Pure std,
/// always compiled (no feature, no dependency).
pub fn meta_data_value(meta_data: &str, key: &str) -> Option<String> {
    for line in meta_data.lines() {
        let line = line.split('#').next().unwrap_or("").trim();
        let Some((k, v)) = line.split_once(':') else {
            continue;
        };
        if !k.trim().eq_ignore_ascii_case(key) {
            continue;
        }
        let v = v.trim().trim_matches(|c| c == '"' || c == '\'').trim();
        if v.is_empty() {
            return None;
        }
        return Some(v.to_string());
    }
    None
}

/// Author a **FAT12/16 image** at `out`, volume-labelled `label` (≤11 chars),
/// holding `files` (name → bytes) at the root — pure Rust via `fatfs`, into an
/// in-memory buffer written out in one shot. Sized to the payload + 1 MiB slack,
/// min 2 MiB, rounded up to 512 KiB. (feature `seed`)
#[cfg(feature = "seed")]
pub fn build_fat_image(out: &Path, label: &str, files: &[(String, Vec<u8>)]) -> Result<()> {
    use std::io::Write;
    if let Some(dir) = out.parent() {
        create_private_dir(dir)?;
    }
    let payload: u64 = files.iter().map(|(_, b)| b.len() as u64).sum();
    let needed = (payload + 1024 * 1024).max(2 * 1024 * 1024);
    let size = needed.div_ceil(512 * 1024) * 512 * 1024;
    let mut cursor = std::io::Cursor::new(vec![0u8; size as usize]);
    let mut lbl = [b' '; 11];
    for (i, b) in label.bytes().take(11).enumerate() {
        lbl[i] = b;
    }
    fatfs::format_volume(
        &mut cursor,
        fatfs::FormatVolumeOptions::new().volume_label(lbl),
    )
    .map_err(|e| Error::Backend(format!("format FAT {}: {e}", out.display())))?;
    {
        let fs = fatfs::FileSystem::new(&mut cursor, fatfs::FsOptions::new())
            .map_err(|e| Error::Backend(format!("open FAT {}: {e}", out.display())))?;
        let root = fs.root_dir();
        for (name, bytes) in files {
            let mut file = root
                .create_file(name)
                .map_err(|e| Error::Backend(format!("create {name} in FAT: {e}")))?;
            file.truncate().ok();
            file.write_all(bytes)
                .map_err(|e| Error::Backend(format!("write {name} in FAT: {e}")))?;
            file.flush().ok();
        }
    }
    // A seed image can carry PRIVATE KEY material (tunnr writes `PrivateKey =`
    // straight into its appliance conf), so the bytes land in a file CREATED
    // 0600 — never written first and chmod-ed after (F61).
    write_private(out, &cursor.into_inner())?;
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A unique scratch path under the system temp dir (no `tempfile` dev-dep,
    /// matching the repo's existing convention).
    fn scratch(tag: &str) -> PathBuf {
        std::env::temp_dir().join(format!("draupnir-seed-{tag}-{}", std::process::id()))
    }

    /// RED-WHEN-BROKEN: a known `meta-data` + `user-data` input renders the EXACT
    /// expected seed contents — the instance-id/hostname in `meta-data` and the
    /// verbatim `user-data` YAML. If the render drifts, this fails on the byte.
    #[test]
    fn known_input_renders_exact_seed_contents() {
        let user_data =
            "#cloud-config\nhostname: web-01\nruncmd:\n  - [systemctl, start, holger]\n";
        let ci = CloudInit {
            user_data: user_data.into(),
            meta_data: Some("instance-id: iid-web-01\nlocal-hostname: web-01\n".into()),
            network_config: None,
        };
        let files = seed_files(&ci);

        assert_eq!(
            files.len(),
            2,
            "just user-data + meta-data when no network-config"
        );
        // user-data is the verbatim YAML.
        assert_eq!(files[0].0, "user-data");
        assert_eq!(files[0].1, user_data);
        // meta-data carries the exact instance-id + hostname.
        assert_eq!(files[1].0, "meta-data");
        assert_eq!(
            files[1].1,
            "instance-id: iid-web-01\nlocal-hostname: web-01\n"
        );
    }

    /// An omitted `meta-data` is filled with [`DEFAULT_META_DATA`] carrying both an
    /// `instance-id` and a `local-hostname` (cloud-init needs both files).
    #[test]
    fn omitted_meta_data_gets_the_default_instance_id_and_hostname() {
        let ci = CloudInit::user_data("#cloud-config\n");
        let files = seed_files(&ci);
        assert_eq!(files[1].0, "meta-data");
        assert_eq!(files[1].1, DEFAULT_META_DATA);
        assert!(files[1].1.contains("instance-id:"));
        assert!(files[1].1.contains("local-hostname:"));
    }

    /// A `network-config` is emitted as the third file only when present.
    #[test]
    fn network_config_is_the_optional_third_file() {
        let plain = CloudInit::user_data("#cloud-config\n");
        assert!(!seed_files(&plain)
            .iter()
            .any(|(n, _)| *n == "network-config"));

        let net = CloudInit::user_data("#cloud-config\n")
            .with_network_config("version: 2\nethernets:\n  eth0:\n    dhcp4: true\n");
        let files = seed_files(&net);
        assert_eq!(files.len(), 3);
        let nc = files
            .iter()
            .find(|(n, _)| *n == "network-config")
            .expect("network-config present");
        assert_eq!(nc.1, "version: 2\nethernets:\n  eth0:\n    dhcp4: true\n");
    }

    /// The **seed directory** output writes each file with its exact contents on
    /// disk (the pure-std, no-image-tooling path).
    #[test]
    fn write_seed_dir_writes_each_file_verbatim() {
        let dir = scratch("dir");
        let _ = std::fs::remove_dir_all(&dir);
        let ci = CloudInit {
            user_data: "#cloud-config\npackages: [curl]\n".into(),
            meta_data: Some("instance-id: iid-42\nlocal-hostname: node-42\n".into()),
            network_config: Some("version: 2\n".into()),
        };
        let out = write_seed_dir(&dir, &ci).unwrap();
        assert_eq!(out, dir);

        let read = |name: &str| std::fs::read_to_string(dir.join(name)).unwrap();
        assert_eq!(read("user-data"), "#cloud-config\npackages: [curl]\n");
        assert_eq!(
            read("meta-data"),
            "instance-id: iid-42\nlocal-hostname: node-42\n"
        );
        assert_eq!(read("network-config"), "version: 2\n");

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

    /// FUNCTIONAL: author the vfat seed image, read it back through `fatfs`, and
    /// confirm the volume label is `cidata` and every file's contents survive the
    /// round-trip (so a guest's cloud-init will find and read them). (feature `seed`)
    #[cfg(feature = "seed")]
    #[test]
    fn build_seed_image_roundtrips_label_and_contents() {
        use std::io::Read;
        let out = scratch("img").with_extension("img");
        let _ = std::fs::remove_file(&out);
        let ci = CloudInit {
            user_data: "#cloud-config\nruncmd:\n  - [echo, hi]\n".into(),
            meta_data: Some("instance-id: iid-img\nlocal-hostname: img-host\n".into()),
            network_config: Some("version: 2\nethernets:\n  eth0:\n    dhcp4: true\n".into()),
        };
        let path = build_seed_image(&out, &ci).unwrap();
        assert_eq!(path, out);

        let img = std::fs::File::options()
            .read(true)
            .write(true)
            .open(&out)
            .unwrap();
        let fs = fatfs::FileSystem::new(img, fatfs::FsOptions::new()).unwrap();
        assert_eq!(fs.volume_label().to_ascii_lowercase(), NOCLOUD_LABEL);

        let read = |name: &str| {
            let mut buf = String::new();
            fs.root_dir()
                .open_file(name)
                .unwrap()
                .read_to_string(&mut buf)
                .unwrap();
            buf
        };
        assert_eq!(
            read("user-data"),
            "#cloud-config\nruncmd:\n  - [echo, hi]\n"
        );
        assert_eq!(
            read("meta-data"),
            "instance-id: iid-img\nlocal-hostname: img-host\n"
        );
        assert_eq!(
            read("network-config"),
            "version: 2\nethernets:\n  eth0:\n    dhcp4: true\n"
        );

        drop(fs);
        let _ = std::fs::remove_file(&out);
    }

    /// RED-WHEN-BROKEN, the GUEST side of the seam: an image authored by
    /// [`build_seed_image`] must read back — through the byte-slice reader a
    /// guest uses on a read-only virtio device — as the EXACT [`CloudInit`] it
    /// was built from, and be recognised as a `cidata` NoCloud volume. If the
    /// reader drifts (wrong file, truncated read, label mismatch) this fails.
    /// (feature `seed`)
    #[cfg(feature = "seed")]
    #[test]
    fn seed_image_roundtrips_through_the_guest_side_reader() {
        let out = scratch("guestread").with_extension("img");
        let _ = std::fs::remove_file(&out);
        let ci = CloudInit {
            user_data: "#cloud-config\nhostname: node-7\n".into(),
            meta_data: Some("instance-id: iid-7\nlocal-hostname: node-7\n".into()),
            network_config: Some("version: 2\n".into()),
        };
        build_seed_image(&out, &ci).unwrap();
        let bytes = std::fs::read(&out).unwrap();

        assert!(
            is_nocloud_seed(&bytes),
            "the authored image IS a `cidata` NoCloud volume"
        );
        assert_eq!(
            seed_volume_label(&bytes).unwrap().to_ascii_lowercase(),
            NOCLOUD_LABEL
        );

        let back = read_seed_image(&bytes).unwrap();
        assert_eq!(back.user_data, ci.user_data);
        assert_eq!(back.meta_data, ci.meta_data);
        assert_eq!(back.network_config, ci.network_config);

        let _ = std::fs::remove_file(&out);
    }

    /// A non-FAT blob (and a FAT volume with some *other* label) must NOT be
    /// mistaken for a NoCloud seed — the guest probes several block devices and
    /// must reject the ones that are not its seed. (feature `seed`)
    #[cfg(feature = "seed")]
    #[test]
    fn a_non_cidata_image_is_not_mistaken_for_a_seed() {
        assert!(
            !is_nocloud_seed(&[0u8; 4096]),
            "zeroes are not a NoCloud seed"
        );
        assert!(
            !is_nocloud_seed(b"not a filesystem at all"),
            "garbage is not a seed"
        );

        let other = scratch("otherlabel").with_extension("img");
        let _ = std::fs::remove_file(&other);
        build_fat_image(&other, "SOMEDISK", &[("a.txt".to_string(), b"x".to_vec())]).unwrap();
        let bytes = std::fs::read(&other).unwrap();
        assert!(
            !is_nocloud_seed(&bytes),
            "a FAT volume labelled SOMEDISK is not the `cidata` seed"
        );
        let _ = std::fs::remove_file(&other);
    }

    /// The extra-files knob: the standard NoCloud trio is still authored (a
    /// full-OS guest's cloud-init still provisions) AND the consumer-native file
    /// rides on the same volume for a guest that has no cloud-init.
    /// (feature `seed`)
    #[cfg(feature = "seed")]
    #[test]
    fn extra_files_ride_alongside_the_standard_nocloud_set() {
        let out = scratch("extra").with_extension("img");
        let _ = std::fs::remove_file(&out);
        let ci = CloudInit::user_data("#cloud-config\n");
        let appliance = "[Interface]\nAddress = 10.9.0.1/24\n";
        build_seed_image_with(
            &out,
            &ci,
            &[(
                "tunnr-appliance.conf".to_string(),
                appliance.as_bytes().to_vec(),
            )],
        )
        .unwrap();

        let bytes = std::fs::read(&out).unwrap();
        let files = read_seed_files(&bytes).unwrap();
        let name_of = |want: &str| files.iter().find(|(n, _)| n.eq_ignore_ascii_case(want));
        assert!(
            name_of("user-data").is_some(),
            "cloud-init still finds user-data"
        );
        assert!(
            name_of("meta-data").is_some(),
            "cloud-init still finds meta-data"
        );
        let extra = name_of("tunnr-appliance.conf").expect("the consumer-native file is present");
        assert_eq!(String::from_utf8_lossy(&extra.1), appliance);
        // The standard CloudInit still round-trips past the extra file.
        assert_eq!(
            read_seed_image(&bytes).unwrap().user_data,
            "#cloud-config\n"
        );

        let _ = std::fs::remove_file(&out);
    }

    /// RED-WHEN-BROKEN: the flat `meta-data` scalar reader pulls the exact
    /// identity a distroless guest boots with — and returns `None` (never a
    /// wrong-key value) for an absent or empty key.
    #[test]
    fn meta_data_value_reads_the_flat_nocloud_scalars() {
        let meta = "instance-id: iid-demo-server\nlocal-hostname: tunnr-server  # the name\n";
        assert_eq!(
            meta_data_value(meta, "instance-id").as_deref(),
            Some("iid-demo-server")
        );
        assert_eq!(
            meta_data_value(meta, "local-hostname").as_deref(),
            Some("tunnr-server")
        );
        // case-insensitive key match (cloud-init docs use lowercase; be liberal).
        assert_eq!(
            meta_data_value(meta, "Local-Hostname").as_deref(),
            Some("tunnr-server")
        );
        // absent / empty keys are None, never a neighbouring value.
        assert_eq!(meta_data_value(meta, "public-keys"), None);
        assert_eq!(meta_data_value("local-hostname:\n", "local-hostname"), None);
        // quoted values are unwrapped.
        assert_eq!(
            meta_data_value("local-hostname: \"quoted-host\"\n", "local-hostname").as_deref(),
            Some("quoted-host")
        );
    }

    /// RED-WHEN-BROKEN (F61): a seed image carries the guest's **private key
    /// material**, so its bytes must never be readable by another local user.
    /// This asserts the mode of the file the builder CREATES — not one fixed up
    /// afterwards, because the window between `write` and `chmod` is the bug.
    #[cfg(all(unix, feature = "seed"))]
    #[test]
    fn seed_image_is_created_private_to_its_owner() {
        use std::os::unix::fs::PermissionsExt;
        let dir = scratch("mode");
        let _ = std::fs::remove_dir_all(&dir);
        let out = dir.join("seed.img");
        build_fat_image(
            &out,
            NOCLOUD_LABEL,
            &[("secret".into(), b"PrivateKey = x".to_vec())],
        )
        .unwrap();
        let mode = std::fs::metadata(&out).unwrap().permissions().mode() & 0o777;
        assert_eq!(
            mode,
            0o600,
            "seed image {} is mode {mode:o}, readable off-owner; a seed carries \
             private keys and must be CREATED 0600",
            out.display()
        );
        // The directory the builder had to create holds key-bearing images: no
        // group/other bits, so a listing cannot even enumerate the fleet.
        let dmode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
        assert_eq!(
            dmode,
            0o700,
            "seed directory {} is mode {dmode:o}",
            dir.display()
        );
        let _ = std::fs::remove_dir_all(&dir);
    }

    /// RED-WHEN-BROKEN (F61): the plain seed-DIRECTORY writer has the same shape
    /// as the image writer — cloud-init `user-data` routinely carries passwords
    /// and keys — so its files are created 0600 in a 0700 directory.
    #[cfg(unix)]
    #[test]
    fn seed_dir_files_are_created_private_to_their_owner() {
        use std::os::unix::fs::PermissionsExt;
        let dir = scratch("dirmode");
        let _ = std::fs::remove_dir_all(&dir);
        let ci = CloudInit {
            user_data: "#cloud-config\npassword: hunter2\n".into(),
            meta_data: Some("instance-id: iid\nlocal-hostname: h\n".into()),
            network_config: Some("version: 2\n".into()),
        };
        write_seed_dir(&dir, &ci).unwrap();
        let dmode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
        assert_eq!(
            dmode,
            0o700,
            "seed directory {} is mode {dmode:o}",
            dir.display()
        );
        for name in ["user-data", "meta-data", "network-config"] {
            let p = dir.join(name);
            let mode = std::fs::metadata(&p).unwrap().permissions().mode() & 0o777;
            assert_eq!(mode, 0o600, "{} is mode {mode:o}", p.display());
        }
        let _ = std::fs::remove_dir_all(&dir);
    }
}