NoCloud cloud-init seed — draupnir owns the seed builder
Additive capability so draupnir (the one boot engine) owns the NoCloud cloud-init seed its boot backends attach at first boot — ported from Skidbladnir/src/machine.rs (KvmController::nocloud_seed_files / build_seed_image / build_fat_image) into the new seed module as the single source of truth. Skidbladnir/jera consume it rather than each re-rolling their own seed authoring.
What NoCloud is
cloud-init's NoCloud datasource 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 user-data, meta-data, and an optional network-config, and applies it. draupnir's builder emits exactly that.
What was added (additive — every existing backend UNCHANGED)
The seed module, the pure builder, with one file set and two outputs:
| fn | output | deps / feature |
seed_files(&CloudInit) -> Vec<(&str, String)> |
the pure file set: user-data, meta-data (a default instance-id/local-hostname when omitted), and network-config when present |
pure std, always compiled |
write_seed_dir(dir, &CloudInit) -> PathBuf |
the file set written into a plain seed directory (a NoCloud seedfrom dir / bind-mount) |
pure std, always compiled |
build_seed_image(out, &CloudInit) -> PathBuf |
a vfat seed image, volume label cidata, pure-Rust fatfs (no genisoimage/mkisofs) |
feature seed (pulls fatfs) |
- Only the seed image costs a dependency; the file-set and 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.
- The
seedfeature was split out ofbackend-tunnr(which now pulls it), so a consumer can author a seed image without dragging the whole KVM boot path. - KVM backend unchanged in behaviour:
KvmBoot::nocloud_seed_files/build_seed_imageare now thin re-exports ofseed::*, so the seed lives in exactly one place while every existing KVM call site keeps working.
Flow
Tests
Red-when-broken, a known meta-data/user-data input → the exact expected seed contents:
known_input_renders_exact_seed_contents—user-dataverbatim + the exactinstance-id/local-hostnameinmeta-data.omitted_meta_data_gets_the_default_instance_id_and_hostname,network_config_is_the_optional_third_file.write_seed_dir_writes_each_file_verbatim— the seed-dir output on disk.build_seed_image_roundtrips_label_and_contents(featureseed) — the vfat image read back throughfatfs: labelcidata+ every file's bytes survive.
Plus tests/testmatrix_rows.rs — the same surfaces as functional_status verdict rows for nornir's introspection-coverage gate (feature testmatrix; a no-op otherwise). Green: lean 79, seed/testmatrix 81.
The seed builders now also self-report at runtime: write_seed_dir and build_seed_image each emit a draupnir/seed functional_status row (GREEN when the seed landed / the cidata image was authored, RED when a mkdir/write/format failed), so a boot whose cloud-init seed could not be authored is a RED matrix row — see functional-status-emit.
Follow-up (logged, not this pass)
Dedup Skidbladnir onto draupnir's seed. Skidbladnir/src/machine.rs still carries its own nocloud_seed_files/build_seed_image/build_fat_image. Once Skidbladnir depends on draupnir's seed feature (staged in design.md's extraction plan, step 5), delete Skidbladnir's copy and call draupnir::seed::* — one seed builder in the constellation, not two.