Gunnar

mikronova/draupnir

.nornir/functional-status-emit.md

<!-- nornir:class: design -->
# draupnir functional-status emit — lifecycle surfaces into nornir's matrix

Added draupnir 0.1.9 (additive). Draupnir's live **lifecycle surfaces** self-report
whether they *actually worked* by recording a `functional_status(component, check,
ok, detail)` row — GREEN when the surface succeeded, **RED when a boot / probe /
teardown / seed authoring failed**. The nornir `Aspect::Functional` runner drains
those rows into the constellation-wide introspection-coverage matrix, so a failed
boot shows up as a **RED matrix row automatically**, without anyone watching a log.

This reuses the shared `nornir-testmatrix` sink verbatim (the `functional_status`
shim in `src/lib.rs` behind the optional `testmatrix` feature) — no twin. With the
feature OFF the emit is a compiled-out `#[inline]` no-op and the lean default build
pulls no nornir dependency.

## The emitting surfaces

| Surface | `component` / `check` | GREEN | RED |
|---|---|---|---|
| `boot()` (`src/lib.rs`) | `draupnir/boot` · `boot` | backend brought the instance up | `Boot::boot` failed (no slot, backend error) |
| `rollback()` (`src/lib.rs`) | `draupnir/lifecycle` · `rollback` | backend confirmed power-off | `Lifecycle::power_off` failed |
| `await_power_state()` (`src/lib.rs`) | `draupnir/lifecycle` · `await_power_state` | instance reached the wanted state | bounded wait timed out |
| `boot_fleet_and_await[_parallel]()` | `draupnir/lifecycle` · `boot_fleet_and_await[_parallel]` | every member ready | ≥1 member timed out / errored |
| `ContainerBoot::wait_ready()` (`src/container.rs`) | `draupnir/container` · `wait_ready` | container reached its `Readiness` | probe timed out / backend error |
| `write_seed_dir()` (`src/seed.rs`) | `draupnir/seed` · `write_seed_dir` | NoCloud file set landed on disk | mkdir/write failed |
| `build_seed_image()` (`src/seed.rs`) | `draupnir/seed` · `build_seed_image` | `cidata` vfat image authored | fatfs format/author failed |

Each emit wraps the surface's own `Result` (`outcome.is_ok()`) and then returns that
same `Result` unchanged — the verdict is an *observation*, never a control-flow
change. A failure is always **both** a RED row **and** a propagated `Err`: a boot /
teardown / seed that could not complete must be visible, never swallowed.

## `rollback()` — the undo seam (draupnir 0.1.9, additive)

```rust
// Tear a booted instance down by powering it off through the backend's Lifecycle,
// recording the teardown verdict. GREEN on power-off; a backend failure to power it
// down is a RED row AND a propagated Err.
pub fn rollback<L: Lifecycle>(lifecycle: &L, machine: &Machine) -> Result<()>;
```

The *undo* half of `boot_and_await`: jera / Skidbladnir call it when a provision must
be reverted — a boot that never reached `PowerState::On`, a `wait_ready` that timed
out, or a fleet member being reclaimed. Written against the plain `Lifecycle` trait so
it rolls back **any** backend (KVM / container / Redfish) and a mock alike — one shared
teardown, never a per-backend twin.

## Tests (RED-when-broken, no live instance)

`tests/testmatrix_rows.rs`:
- `failed_boot_and_rollback_emit_red_rows` — a **fake/failed instance** (a backend
  whose `boot` and `power_off` both error) drives `boot()` and `rollback()`; each call
  (a) propagates the `Err` and (b) records a **RED** row, while a healthy backend
  records the GREEN control. **RED-when-broken**: drop the emit or invert the verdict
  (a broken boot reported GREEN) and the drain assertions fail. The drain-and-assert
  half uses `draupnir::drain_status_rows()` (a thin re-drain of the nornir buffer,
  feature `testmatrix`); the default build still asserts the propagated `Err`.
- The existing `await_power_state_*` / `boot_fleet_*` / `seed_*` rows cover the other
  surfaces in the table above.