# Draupnir — the low-level BOOT / PROVISIONING library of the nordisk
# constellation. One crate, three boot backends: KVM (drives tunnr), OCI
# containers, and Redfish bare-metal virtual-media boot.
#
# Standalone workspace whose root IS the `draupnir` crate (the tunnr /
# Skidbladnir house pattern): this pins it as its own workspace root so a
# parent directory's workspace cannot accidentally absorb it, and so the two
# high-level consumers (jera, Skidbladnir) can path/version-dep it as a leaf
# without dragging a wider tree.
[workspace]
resolver = "2"
# The recipe crate — draupnir's `impl BuildThing` (2026-08-30, lane AG). It is a
# workspace MEMBER and not a dependency of anything here: nothing draupnir ships
# links it, and it exists so `.nornir/build-variants.toml` can be EMITTED from
# code instead of hand-written. dwarves' forge finds it by package name.
members = ["crates/draupnir-build-thing"]
[package]
name = "draupnir"
# 0.1.10: DRIFTED from the published 0.1.9 — `BootSpec` grew `success_marker` +
# `boot_timeout` and KvmBoot grew `booted_ok`/`exit_reason`/`shutdown(grace)` when
# dwarves' demo boots were routed through the shared seam (draupnir c0fa2f1,
# 2026-08-22). crates.io bytes are immutable, so consumers only see it as 0.1.10.
# 0.1.13: run-to-completion no longer hands back a PREFIX of a container's
# output as if it were the whole. `ContainerControl::logs_ended` (defaulted
# true, so every mock is unaffected) + `RunOptions::log_settle` + `LOG_SETTLE`:
# after the container exits, `drive_to_completion` waits for the engine's log
# stream to CLOSE before its final drain, and refuses by name if it never does.
# MEASURED on t14s 2026-09-12 over the live podman engine: 20 runs of a
# container that printed 901 lines and exited returned `exit_code = Some(0)`
# with 0, 0, 0 and 749 lines in 4 of them. nornir-build-thing reads cargo's
# `--message-format=json` off exactly this stream, so those runs were builds
# that reported success and no binary — the gunnar appliance compose's step-3
# failure.
# 0.1.14: an interrupted run no longer wedges the box for ever. Container names
# are deterministic (`draupnir-<job>`), so the leftover of a killed run holds the
# next run's name — and the blind `remove_container(force)` that stood before the
# create threw its own result away, so when it failed the create went ahead and
# came back as the daemon's bare 500. MEASURED on the gunnar appliance bring-up
# 2026-09-20: one ladder run killed mid-build left
# `draupnir-cargo-build-pid1-korp-start` behind and EVERY ISO compose on that box
# then died 381 s in with `the container name "…" is already in use by
# 01f3042cab74…`, four layers under `vm 'gunnar-installer-metered' FAILED and
# produced nothing`, until a human ran `podman rm -f`. Now every create runs a
# preflight FIRST (`ensure_name_free_over`): a leftover that is not running is
# REAPED and handed back as a `Reap` for the consumer to announce
# (`ContainerBoot::drain_reaps`), a RUNNING holder is a named refusal
# (`REFUSED [container-name-in-use]`) and is never killed, and a name still stuck
# in the engine's storage layer says so and gives the one command that clears it.
# Recovery is on the NEXT start, because a `Drop` does not survive a `pkill`.
# BEHAVIOUR CHANGE: booting over a container of the same name that is RUNNING now
# refuses instead of silently destroying it.
version = "0.2.0"
edition = "2021"
rust-version = "1.85"
description = "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."
license = "MIT OR Apache-2.0"
repository = "https://git.gunnar.rs/mikronova/draupnir"
keywords = ["boot", "provisioning", "redfish", "kvm", "container"]
categories = ["virtualization", "os"]
# Development notes, agent instructions and machine-local tooling stay in the
# repository; they are not part of the published crate.
exclude = [
".nornir/",
"docs/",
"brokkr.toml",
"brokkr-software-criteria.md",
]
[lib]
name = "draupnir"
path = "src/lib.rs"
[features]
# Lean by design: the default build is PURE std — the trait surface, the
# BootSpec/Machine types, validation and the fleet fan-out all compile with no
# dependency at all. Each live backend sits behind a feature so a lean consumer
# pulls only what it drives.
default = []
# The REAL KVM backend over tunnr's `tunnr_vm::boot_test(BootSpec) -> BootHandle`
# primitive. The `kvm` module + its `Boot`/`Lifecycle` seam always compile (an
# honest, un-feature-gated stub); this feature adds the live tunnr wire. tunnr-vm
# is not yet on crates.io, so a PATH dep is the correct interim (exactly how
# `jera` wires it) — switch to a version once tunnr publishes. Pulls `seed` so the
# KVM adapter can author the cloud-init NoCloud seed *image* it attaches to a VM.
backend-tunnr = ["dep:tunnr-vm", "seed"]
# The **NoCloud cloud-init seed** builder ([`seed`]) — draupnir owns it (ported
# from Skidbladnir). The file-set render + the plain **seed directory** output are
# pure std (always compiled, no dep); THIS feature adds the pure-Rust `fatfs`
# **vfat seed image** authoring (volume label `cidata`, no `genisoimage`/`mkisofs`
# subprocess). Split out from `backend-tunnr` so a consumer can author a seed
# image without dragging the KVM boot path.
seed = ["dep:fatfs"]
# Route this crate's `functional_status` emit markers into real nornir test-matrix
# rows (the constellation-wide introspection-coverage gate). OFF by default so the
# lean core pulls no nornir dep — then `functional_status` is a compiled-out no-op.
testmatrix = ["dep:nornir-testmatrix", "nornir-testmatrix/testmatrix"]
# The REAL OCI container backend over the podman/Docker REST API (`bollard`) — the
# same zero-shell container engine `jera` drives, reused rather than re-rolled. A
# dedicated tokio runtime drives the async API from the synchronous `Boot`/
# `Lifecycle` seam. OFF by default so a plain `cargo build` stays pure-std.
#
# Also carries the image ARCHIVE path (`export_image`/`import_image`): `bytes` for
# bollard's streaming load body, and `serde_json` — already in this manifest for
# Redfish — to read the `manifest.json` at the root of a docker-archive, which is
# how an exported tar names the tags and config digest it actually contains.
backend-oci = ["dep:bollard", "dep:tokio", "dep:futures", "dep:tar", "dep:bytes", "dep:serde_json", "dep:hyper", "dep:hyper-util", "dep:http-body-util"]
# The DAEMON-LESS OCI runtime backend over **youki-core** (`libcontainer`): a
# pure-Rust runc alternative that runs containers directly on Linux namespaces +
# cgroups-v2, NO daemon, NO CLI (the in-appliance engine korp-server links). OFF by
# default so a plain `cargo build` stays pure-std and the `bollard` socket path is
# the default container engine. Pulls `tar` (shared with `backend-oci`) for the
# layer→rootfs unpack. Alias-worthy as `oci-native`.
backend-youki = ["dep:libcontainer", "dep:rustix", "dep:flate2", "dep:tar"]
# The REAL bare-metal Redfish backend: a pure-Rust `ureq` (rustls, no
# openssl/native-tls) client over the handful of DMTF Redfish endpoints we drive
# (VirtualMedia insert/eject, the ComputerSystem `Boot` override, and
# ComputerSystem.Reset). OFF by default so a plain `cargo build` stays pure-std
# and offline-green; the request shaping is pure + fixture-tested with no BMC.
backend-redfish = ["dep:ureq", "dep:serde_json"]
# ── draupnir's OWN qemu (feature `backend-qemu`) ──
# `src/qemu.rs` renders the argv and spawns the guest itself, with NO tunnr edge:
# the machine shape (SeaBIOS / no serial / framebuffer / skewed RTC) is a
# property of the hypervisor door, not of the kernel harness behind it. The argv
# builder and the qcow2 authoring are unconditional (std only); this feature adds
# the QMP control plane — screendump and the ACPI power button — which needs a
# JSON parser. serde_json is ALREADY an optional dependency here (backend-oci,
# backend-redfish, redfish-server all pull it), so turning this on adds NO
# package to the lockfile, and it does not pull tunnr-vm at all.
backend-qemu = ["dep:serde_json"]
# The Redfish **SERVER** — draupnir's own BMC, fronting KVM ([`redfish_server`]).
#
# The other end of `backend-redfish`: a pure-Rust Redfish service that answers the
# six endpoints draupnir's client drives and translates them onto the vocabulary
# that already exists — `InsertMedia` → [`BootSpec::medium`], the `Boot` override →
# [`BootOrder`], `ComputerSystem.Reset` → the [`Boot`]/[`Lifecycle`] seam. That
# turns any Linux box with `/dev/kvm` into a Redfish-controllable machine, which is
# squarely this crate's charter, and it gives the `iso-redfish-sim` matrix form a
# BMC to burn against on a box that has none.
#
# OFF by default and NOT implied by `backend-redfish`: a client-only build must not
# grow a listening socket. Pulls `rustls` (pure Rust, `ring` — the same 0.23 line
# `ureq` already resolves to, so no second TLS stack enters the graph) and `rcgen`
# for the server's TLS identity, because draupnir's own client PINS the BMC
# certificate and a pinned client needs a real cert to pin. `rcgen` — not
# korp-installer's RSA/`x509-cert` `SigningIdentity`, which mints an *Authenticode
# code-signing* identity — is the constellation's TLS-identity crate: it is exactly
# what Skidbladnir's `src/pki.rs` uses (`rcgen 0.13`, `default-features = false`,
# `["pem","ring"]`, proven to load into a real `rustls::ServerConfig`). That code
# cannot simply be called from here: Skidbladnir path-deps *this* crate
# (`Skidbladnir/Cargo.toml:87`), so a dep back onto its `pki` module would be a
# cycle. Same crate, same features, same pattern — one cert story, not two.
redfish-server = ["dep:rustls", "dep:rcgen", "dep:serde_json"]
[dependencies]
# ── threading (always): the ONE sanctioned home for concurrency ──
# ROOT-LAW #0 ("rayon-free" / core-saturation): every thread in the constellation
# originates in gatling, never a bare `std::thread::spawn`. The parallel fleet
# fan-out (`boot_fleet_and_await_parallel`) routes each member's blocking boot
# through `gatling::gatling_forkjoin::gatling_for_each` (one worker per member).
# Published as `rotaryengine`; the library identity stays `gatling`. REGISTRY,
# not path (2026-10-22, holger stinky 16/28): a path dep here put a second
# rotaryengine 0.1.13 into every downstream lock beside the crates.io copy that
# jera, znippy-zoomies and the published draupnir already pull. The crates.io
# 0.1.13 source is byte-identical to the sibling checkout, so one copy is enough.
gatling = { version = "0.1.13", package = "rotaryengine" }
# ── real KVM backend (feature `backend-tunnr`) ──
# The path resolves to the sibling tunnr workspace root whose root crate is
# `tunnr-vm` (lib `tunnr_vm`); OFF by default so a plain `cargo build` has no
# dep and the KVM `boot()` is a compiled-out honest stub.
tunnr-vm = { path = "../tunnr", version = "0.2.0", optional = true }
# Pure-Rust FAT12/16 authoring for the cloud-init NoCloud seed image (volume label
# `cidata`) the KVM guest's cloud-init reads at first boot — the same `fatfs` crate
# Skidbladnir uses, so there is no `genisoimage`/`mkisofs` subprocess (zero-shell).
# Rides the `seed` feature (which `backend-tunnr` pulls).
fatfs = { version = "0.3", optional = true }
# ── the nornir test-matrix emit sink (feature `testmatrix`) ──
# Lean serde-only crate: turns `functional_status` markers into real matrix rows.
# PATH added 2026-08-01 for `tests/silenced_tests_guard.rs`, which needs the
# `gatedtests` module — that module exists only in edda's unpublished 0.2.8, and
# cargo demands ONE canonical source path for a package across [dependencies] and
# [dev-dependencies]. `version` is kept so `cargo publish` still resolves it from
# the registry for external consumers; locally the sibling path wins (the same
# path+version shape; the `gatling` dep above is registry-only since 2026-10-22).
nornir-testmatrix = { path = "../edda/crates/nornir-testmatrix", version = "0.2", default-features = false, optional = true }
# ── real OCI container backend (feature `backend-oci`) ──
# bollard is the async podman/Docker REST client; pinned to the same 0.20 line
# jera uses so there is one container engine across the constellation. tokio drives
# its async API from draupnir's synchronous seam; futures for the pull/log streams.
bollard = { version = "0.20", optional = true }
# `net` + `io-util` are for `daemon_get`'s own socket (below); `rt-multi-thread` is
# the runtime that drives bollard's async API from the synchronous seam.
tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util"], optional = true }
futures = { version = "0.3", optional = true }
# ── the raw daemon read (`daemon_get`) ──
# The two calls whose ANSWER IS A STATE WORD (`/containers/json`,
# `/containers/{id}/json`) cannot go through bollard: its models are generated from
# DOCKER's OpenAPI document, and podman's own vocabulary (`stopped`, `configured`,
# `stopping`) fails the whole response — one stopped container on oden took out the
# entire container listing and korp refused to adopt its own Spark. bollard exposes
# no raw-body escape hatch, so those two read the JSON themselves.
# NOT new crates in the build graph: hyper 1.10.1, hyper-util 0.1.20 and
# http-body-util 0.1.4 are already there as bollard's OWN transport (see
# `Cargo.lock`) — naming them here only makes the client constructible, exactly as
# `bytes` above.
hyper = { version = "1", features = ["client", "http1"], optional = true }
hyper-util = { version = "0.1", features = ["tokio"], optional = true }
http-body-util = { version = "0.1", optional = true }
# Pure-Rust tar for the OCI image-build context (tar the build dir → `build_image`)
# and the `create`+`cp` artifact extract (unpack `download_from_container`'s tar) —
# so Skidbladnir's `pack.rs` `podman build` + `podman cp` twin routes through the
# ONE OCI engine. Rides `backend-oci`; the lean default pulls nothing.
tar = { version = "0.4", optional = true }
# `bytes::Bytes` is the chunk type bollard's STREAMING import (`import_image_stream`)
# demands (`Stream<Item = Result<Bytes, E>>`); the non-streaming `import_image` wants
# the whole archive as one `Bytes`, which for a 1.4 GB Spark image is exactly the
# gigabyte-in-RAM we refuse. Not a new crate in the build graph — bollard already
# pulls `bytes` (1.12.1 in `Cargo.lock`); naming it here only makes the type
# constructible. Pinned to bollard's own 1.x line so the types unify.
bytes = { version = "1", optional = true }
# ── the DAEMON-LESS OCI runtime backend (feature `backend-youki`, aka oci-native) ──
# youki-core (`libcontainer`) is the pure-Rust LOW-LEVEL OCI runtime — a runc
# alternative that runs a container DIRECTLY over Linux namespaces + cgroups-v2 with
# NO daemon and NO CLI. This is the in-appliance container engine korp-server (static
# PID-1) links, per Gemini's revised design: mount cgroups, set up namespaces, unpack
# an OCI image's layers into a bundle rootfs, generate `config.json`, and drive the
# create/start/kill/delete lifecycle — all in-process. Zero-shell: single-line rootless
# uid/gid maps are written straight to `/proc/self/uid_map` (no `newuidmap` exec), and
# any boot-path shell-out into a `scratch` bundle is an instant ENOENT.
# - `v2` → cgroups-v2 (Unified) only; no cgroup-v1.
# - `systemd` → the pure-Rust **native-dbus** systemd cgroup manager. libcontainer
# FORCES the systemd cgroup path whenever a user namespace is present (rootless),
# so this is required for a rootless run: it creates a transient scope under the
# caller's delegated `user@UID.service` slice over the session bus — still zero
# external process (a hand-rolled dbus client, not the `systemctl`/`busctl` CLI).
libcontainer = { version = "0.7", default-features = false, features = ["v2", "systemd"], optional = true }
# geteuid/getegid for the single-line rootless id-map (map container-root → the
# caller's own uid, so youki writes uid_map directly and never execs `newuidmap`).
rustix = { version = "1", default-features = false, features = ["process"], optional = true }
# gzip decode for the OCI-image layer tarballs when unpacking them into a bundle rootfs.
flate2 = { version = "1", optional = true }
# ── real Redfish backend (feature `backend-redfish`) ──
# Pure-Rust HTTP: ureq over rustls (ring) — NO openssl/native-tls (pure-Rust
# charter). `disable_verification` handles the self-signed certs BMCs ship with.
# serde_json shapes the Redfish action bodies.
ureq = { version = "3", default-features = false, features = ["rustls", "json", "gzip"], optional = true }
serde_json = { version = "1", optional = true }
# ── the Redfish SERVER (feature `redfish-server`) ──
# TLS for draupnir's own BMC. rustls 0.23 over **ring** — the exact provider `ureq`
# already resolves to in this tree (`Cargo.lock`: rustls 0.23.42 → ring 0.17.14), so
# the server side adds no second crypto backend and client + server share one stack.
# `std` for the blocking `StreamOwned` seam the accept loop reads/writes through.
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"], optional = true }
# The server's TLS identity — a genuine self-signed X.509 with SANs, minted per run
# and held **in memory only** (never written to disk, so no `.key`/`.pem`/`.der` can
# be committed). Same crate, same feature set and same call shape as Skidbladnir's
# `pki::issue_leaf` (`Skidbladnir/Cargo.toml:116`); see the `redfish-server` feature
# note for why the code is mirrored in crate choice rather than imported.
rcgen = { version = "0.14", default-features = false, features = ["pem", "ring"], optional = true }
# ── dev-only: the fleet-boot bench (does NOT affect the lib's runtime deps) ──
# criterion drives the LIGHT serial-vs-parallel fleet-boot arm (`benches/fleet_boot.rs`);
# it is a dev-dependency only, so a consumer's `cargo build` still pulls the pure-std
# lib with no criterion in its tree. The Loki bencher runs the authoritative HEAVY pass.
[dev-dependencies]
criterion = { version = "0.8", default-features = false }
# The SILENCED-TEST guard (`tests/silenced_tests_guard.rs`): edda's
# `nornir_testmatrix::gatedtests::audit_repo`, which fails this repo's own
# `cargo test` the moment a `tests/*.rs` goes dark behind an off-by-default
# feature. Deliberately NO `version` — crates.io's newest nornir-testmatrix
# (0.2.7) has no `gatedtests` module, so a version requirement would be a claim
# the registry cannot honour. A path-only dev-dep is stripped by `cargo publish`,
# so no consumer ever sees this edge.
nornir-testmatrix = { path = "../edda/crates/nornir-testmatrix", default-features = false }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
# `tests/redfish_server_conformance.rs` re-hashes every vendored DMTF fixture against
# the digests recorded in `tests/fixtures/dmtf/PROVENANCE.md` before it reads them.
# Without that, a fixture quietly edited to make a conformance assertion pass would
# be indistinguishable from conformance — the anchor has to be tamper-evident or it
# is not an anchor. Pure-Rust `sha2` (no `cc`, no openssl); dev-only, so no consumer
# sees it.
sha2 = "0.11"
# The conformance suite's HTTP transport. Deliberately a THIRD-PARTY client rather
# than anything draupnir wrote: if the server emits malformed HTTP, an independent
# implementation is what notices. Same crate/features the `backend-redfish` optional
# dep resolves to, so no second HTTP stack enters the graph.
ureq = { version = "3", default-features = false, features = ["rustls", "json", "gzip"] }
serde_json = "1"
# Per-test scratch dirs for the firmware-resolution proofs (`firmware_tests`): the
# OVMF search has to be provable against a KNOWN directory, not against whatever
# the build host happens to have in /usr/share/OVMF. Pure-Rust, dev-only.
tempfile = "3"
# The fleet-boot bench: serial `boot_fleet_and_await` vs the concurrent
# `boot_fleet_and_await_parallel` over a fake fast backend with a small per-member
# status latency (models a BMC/QEMU/container readback round-trip). `harness = false`
# → criterion owns `main`.
[[bench]]
name = "fleet_boot"
harness = false