Gunnar

mikronova/draupnir

.nornir/nocloud-seed.md

<!-- nornir:class: design -->
# 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`](../src/seed.rs)
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 `seed` feature was split out of `backend-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_image` are now thin re-exports of `seed::*`, so the seed lives in
  exactly one place while every existing KVM call site keeps working.

## Flow

![NoCloud seed builder](assets/nocloud-seed.svg)

## Tests

Red-when-broken, a known `meta-data`/`user-data` input → the **exact** expected
seed contents:
- `known_input_renders_exact_seed_contents` — `user-data` verbatim + the exact
  `instance-id`/`local-hostname` in `meta-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` (feature `seed`) — the vfat
  image read back through `fatfs`: label `cidata` + 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](functional-status-emit.md).

## 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.