draupnir 0.1.9

Draupnir — the nordisk boot/provisioning library: fire up a runtime from one BootSpec across three backends (KVM via tunnr · OCI container · Redfish bare-metal virtual-media) and drive its power lifecycle. Odin's ring that drips eight identical copies → boot a fleet of identical machines from one ISO.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//! # 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())
}

/// The pure seed-directory write (no emit) — the always-works fallback path.
fn write_seed_dir_inner(dir: &Path, ci: &CloudInit) -> Result<()> {
    std::fs::create_dir_all(dir)
        .map_err(|e| Error::Backend(format!("mkdir {}: {e}", dir.display())))?;
    for (name, contents) in seed_files(ci) {
        let path = dir.join(name);
        std::fs::write(&path, contents.as_bytes())
            .map_err(|e| Error::Backend(format!("write {}: {e}", path.display())))?;
    }
    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() {
        std::fs::create_dir_all(dir)
            .map_err(|e| Error::Backend(format!("mkdir {}: {e}", dir.display())))?;
    }
    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();
        }
    }
    std::fs::write(out, cursor.into_inner())
        .map_err(|e| Error::Backend(format!("write {}: {e}", out.display())))?;
    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")
        );
    }
}