build-thing launch axis + the machine spine — design
Class:
design· Home: draupnir, because draupnir is the hub the whole machine spine hangs from. The how-to is inbuild-thing-launch-guide.md. This doc is the map for the deep-wiring that connects a built artifact to a running, asserted machine, all Rust, no shell.
1. The thesis: two axes, not one list
A build-thing produces an artifact and then (optionally) launches it. Those are two different things, and conflating them is what made exe / container / kvm / iso read as one list when it is really a matrix:
| BUILD (what comes out) | LAUNCH (how you run it) | verdict channel |
exe — cargo build |
run — spawn the binary | exit code + stdout |
container — ContainerBuild (bollard) |
deploy — podman run (bollard) + logs pane |
container stdout stream |
vm — VmBuild (kernel+rootfs) |
boot console — tunnr_vm::boot_test + serial pane |
contract markers in serial |
iso — bootloader-in-disk (new) |
boot console · or → USB → bare metal | same serial contract |
kvm is not an artifact — it is a launch target. An artifact declares which targets it supports; the UI renders the launch control that fits. This is the whole correction: build-thing already has ArtifactKind ∈ {Exe,Lib,Wasm,Container,Vm,Service} (edda nornir-build-thing/src/variant.rs); what it lacks is a first-class launch axis beside the build one, and a kind-aware rendering of it.
2. The machine spine — who owns what
The launch axis only works because there is exactly one machine spine underneath, and every layer has a single job:
- tunnr — the muscle. Owns the QEMU/OVMF/KVM primitive (
boot_test,src/boot_primitive.rs) and the OS-less appliance semantics (boot contract, markers, PID-1 payloads). It already boots both models: direct-kernel when a kernel is set, firmware/bootloader-in-disk when it is empty. - draupnir — the thin seam. The boot/provisioning library: one
BootSpec,ImageSource,Boot/Lifecycle, fleet fan-out, the seed builder, three backend adapters (kvm→tunnr,oci,redfish). It is the ruler from tunnr's thinnest appliance to the fattest full distro — the same axis as direct-kernel ↔ bootloader-in-disk. - Skidbladnir — airgap-install application. Should delete its own qemu argv builder (
unfold::kvm/full_argv/Command::new("qemu-system-x86_64")) and route throughdraupnir::kvm::KvmBoot. It already consumesdraupnir::seed; the launcher is the last thing it twins. - dwarves — the CI/CD forge. Pure orchestrator; it never instantiates a VM itself. It calls the ready-made tool (jera → draupnir → tunnr). Its
run_vmtoday is a TODO stub — the wiring turns it into a real call.
No cycle: draupnir → tunnr-vm (feature backend-tunnr) is one-directional; the tunnr → draupnir edge lives only in leaf crates (tunnr-demo, payloads/*), never in tunnr-vm. Clean DAG, verified against the Cargo.tomls.
3. The keystone change — draupnir grows a fat-dist boot model
Today draupnir::ImageSource (src/lib.rs:124) can express direct-kernel (KernelRootfs) and kernel+disk (Disk) — but validate() requires a kernel on both (src/lib.rs:622-624), so it cannot express the kernel-less, bootloader-in-disk boot that a fat distro (and Skidbladnir's airgap install) needs. That single gap is why jera_vm_routable() is always false.
The five focused edits (all in draupnir; zero in tunnr — the engine already does it):
src/lib.rs:124— addImageSource::BootloaderDisk { disk: String }(self-booting disk, no-kernel).src/lib.rs:154—suits():BootloaderDisksuitsBackend::Kvm.src/lib.rs:622—validate(): require only the disk path, not a kernel.src/lib.rs:~463— abootloader_diskconstructor peer.src/kvm.rs:74—to_tunnr_spec: mapBootloaderDiskto an empty-kerneltunnr_vm::BootSpec(tunnr firmware-boots the disk). And, while here, fix the seed drift (kvm.rs:104): use tunnr's nativeBootSpec::with_seedinstead of hand-rolling the seed-driveintoextra_qemu_args, so the seed argv is authored in one place.
This unlocks both downstream moves at once: the fat-dist launch, and Skidbladnir routing onto draupnir.
4. The launch axis — declared in the model, rendered elsewhere
The wiring adds an explicit launch action, distinct from build, to the build-thing data model (edda/crates/nornir-build-thing/src/surface.rs, state_json) — kind-aware:
- Container → DEPLOY (a future bollard
podman run). - Vm / iso → BOOT CONSOLE (a future
tunnr_vm::boot_test, serial + contract markers). - Exe → RUN (spawn + exit code).
This is a declaration of what launch a cell supports, carried in state_json for a consumer (dwarves, or a future UI) to render. Two deliberate scope lines:
- nornir is out of scope. nornir is huge and its viz (
src/viz/build_tab.rs,test_boot.rs) is not touched by this wiring. The model declares the axis; where it renders is a separate decision. - logs get their own framework. The surface exposes a typed seam (
LaunchOutput { Logs, Serial }) as the hand-off point — it does not implement log/serial streaming. That is a separate framework, wired later.
No shell — when the executor is built, container work is the bollard Rust API and VM work is boot_test; neither lives in this model phase.
5. Caching & sealing
- holger is the artifact cache/store: built container/VM artifacts are content- addressed and stored via dwarves' existing
HolgerArtifactStoreseam, so a launch can pull a prior build instead of rebuilding. - znippy can seal/package an artifact set, but is not required for the launch axis today — noted as optional, not wired in this pass.
6. The shell island — ported to Rust
tunnr's outer Buildroot orchestration was variants/buildroot/build.sh + board/tunnr/post-build.sh (shell). Both are now deleted and ported to pure Rust:
tunnr-buildroot(tunnr-vm::buildroot) — the orchestrator: reusesbuild_rust_initfor the inner PID-1, clones Buildroot via gix (behind thebuildroot-fetchfeature — no shelledgit, law L9), and runs Buildrootmakevia a directCommand::new("make")(a sanctioned build-tool exec, not shell glue).tunnr-postbuild— theBR2_ROOTFS_POST_BUILD_SCRIPThook as a compiled Rust binary. Because it is compiled it is executable by construction, so thepost-build.shmode-100644 / Error-126 bug is gone rather than patched.
BuildShVmBuilder calls the Rust path; the defconfigs point at the compiled hook. No shell remains in the Buildroot path — the last island is drained.
7. What the wiring touches (the deep pass)
| Repo | Change | Gate |
| draupnir | ImageSource::BootloaderDisk + validate + to_tunnr_spec + seed-drift fix |
cargo test --features backend-tunnr,seed |
| Skidbladnir | route machine::kvm through draupnir::kvm; enable draupnir/backend-tunnr; delete own launcher |
cargo build --features kvm,matrix |
| tunnr | tunnr-demo adopts draupnir fleet fan-out for the two-node OS-less boot |
cargo build -p tunnr-demo |
| dwarves | run_vm real spec via jera→draupnir; new vm-boot feature enabling jera/vm-tunnr; VmBoot step carries a real spec |
cargo build + cargo build --features vm-boot |
| edda | launch axis declared in build-thing surface (kind-aware) + LaunchOutput logs seam |
cargo build -p nornir-build-thing |
| out of scope — huge; viz rendering + logs are a separate framework | — |
Every row is compile-gated; nothing is reported wired that does not build.