Gunnar

nordisk/Skidbladnir

.nornir/README.md

<p align="center">
  <img src=".nornir/assets/skidbladnir.webp" alt="Skíðblaðnir — the ice-longship hauling the ZNIPPY cargo across the gap, shield-rabbits down the hull, under the aurora" width="440">
</p>

# Skidbladnir

> **Skidbladnir** (Skíðblaðnir) is a 100% Rust airgap install framework: pack a whole
> deployment into one sealed, content-addressed bundle, carry it across the gap, and on the
> far side it creates the service, unpacks the znippy archive, and provisions the data & log
> dirs — starting every component in dependency order with no network and no shell on the target.

⚡ part of the **nordisk** constellation

## Mashup — one sealed bundle onto a zero-network target

Skidbladnir carries the *whole* deployment — infra **and** software — over an airgap in
one content-addressed bundle and brings it up with **no agent, no SSH, no shell** on the
target (it drives systemd straight over D-Bus). The rivals each cover only a slice:

| capability | **Skidbladnir** | Ansible | Packer | cloud-init |
|---|:--:|:--:|:--:|:--:|
| One sealed, content-addressed bundle — infra **+** software + images + config + data, verify-on-receipt | **✅** | ✗ | ◐ (image only) | ✗ |
| **Zero network, zero shell** on the target — drives systemd over D-Bus, no SSH/agent | **✅** | ✗ (SSH + Python) | ✗ | ◐ (first-boot shell) |
| **Provision the machine** (Redfish BMC · Terraform · KVM) **and** install the software, one tool | **✅** | ◐ | ✗ | ✗ |

*The full seven-capability matrix — the `ServiceSpec` → PKI/systemd renderer tree, atomic version-swap + rollback, all-Rust offline — is in [the full overview](.nornir/README-full.md).*

## Benchmarks

<!-- nornir:gen:start:bench_status -->
<!-- nornir:gen:end:bench_status -->

<sub>This block is rendered, not written: the "no numbers" state is a **result**, probed
from this repo on every `nornir docs render Skidbladnir`. It is here so nobody has to
guess whether Skidbladnir was measured and came out slow, or was never measured at all —
an absent table would have read as the former.</sub>

## Which build am I running?

Every Skidbladnir binary answers `--version` / `-V` with `<version> (<git-hash>)`
(build-version-githash-law — a probe whose JSON the matrix trusts as ground truth
must be able to say which commit produced it):

```sh
$ skidbladnir --version           # skidbladnir 0.1.13 (9f73ae0)
$ skidbladnir-probe --version     # skidbladnir-probe 0.1.0 (9f73ae0)
$ skidbladnir-testsvc --version   # skidbladnir-testsvc 0.1.0 (9f73ae0)
```

The hash is stamped at compile time by each crate's `build.rs`, which reads the
on-disk git plumbing with `std::fs` only — no `git`, no `std::process::Command`
(`pure-rust-zero-shell-law`) — and degrades to `unknown` rather than failing the
build when there is no git metadata (a released tarball). `skidbladnir-probe`
answers BEFORE probing and prints no JSON status line, so `--version` can never be
mistaken for a probe report; `skidbladnir-testsvc` answers before binding a port.

## Bifröst — the deploy bridge (folded in)

*Bifröst is the bridge; Skíðblaðnir is the ship that carries what crosses it.*

```sh
skidbladnir bifrost deploy --bundle <file> [--trust-root <ca-pubkey-hex>] [--prefix <dir>] [--root <dir>]
skidbladnir bifrost rollback [--prefix <dir>] [--root <dir>]
```

`deploy` = verify → unfold → migrate → flip `current` → restart → health-probe, and
**auto-rollback to the previous generation if the new one is unhealthy**. A deploy
that returns `health_ok=false rolled_back=true` is a *successful refusal* — production
is still serving the old generation — and exits non-zero so no script reads it as
success. `rollback` is the manual counterpart, for when the new generation passed its
probe but is wrong anyway.

`--trust-root <ca-pubkey-hex>` pins the CA whose ed25519 signature makes the bundle
**authentic**; omitted, the deploy verifies INTEGRITY only (manifest root + per-entry
content hashes) and **says so in one line before it acts**. It used to hardcode no
trust root while its own error text called the argument "the signed payload". A flag a
verb cannot honour is now refused by name (`bifrost rollback --bundle …`) instead of
parsed and dropped. See `tests/bifrost_cli_contract.rs`, which asserts the ON-DISK
difference between a pinned and an unpinned deploy — a test on the printed line alone
stays green for a build that warns and then passes `None`.

**Why a module and not a binary.** It was a standalone `bifrost` crate shipping a
`bifrost` binary, which collided on `$PATH` with knut's `bifrost` (rel2graph). The
name was never the problem — two binaries with it were. knut keeps the name; the deploy
bridge lives here as `skidbladnir::bifrost`.

**The old repo's binary still exists** — as `nornir-deploy`, because the fold is not a
superset. Of the 11 steps its `deploy` performed, 6 have a successor here and 5 do not:
building nornir from a source checkout at a tag, installing the `nornir` operator CLI
onto `$PATH`, and `advisory-db`. The health probe is also weaker here
(`HealthCheck::Tcp(7878)` — a successful connect, not an authenticated `nornir
workspace ls`). Prefer `skidbladnir bifrost` for everything it covers; reach for
`nornir-deploy` only for the rest.

**The fold ported no shelling.** The old tool ran `sudo -n systemctl stop/start`,
`cargo build`, and `cp <bin> <bin>.prev`. Skidbladnir already did all of it, better:

| old shell step | what replaced it |
|---|---|
| `sudo -n systemctl stop/start` | `SystemdRefreshControl` — systemd over **D-Bus**, zero-shell |
| `cp <bin> <bin>.prev` | generation dirs + an atomic `current` flip (`update_product`) |
| restore `.prev` on failure | `refresh_product`'s built-in auto-rollback on a failed probe |
| spawn `nornir workspace ls` | the product's declared `HealthCheck`, via the tested prober |

A copy-to-`.prev` keeps exactly one generation, is not atomic, and a crash mid-copy
leaves neither binary intact — the retained-generation model is strictly better, so
nothing was carried over. `bifrost.rs`'s own test
(`the_bridge_shells_out_for_nothing`) fails if `Command`/`sudo`/`systemctl`/`cargo
build` ever reappears in it.

The old tool's third verb, `advisory-db`, is deliberately **not** here: refreshing a
RustSec mirror is a security-data concern, not a deploy concern (it belongs with
nornir's `security` module or dwarves).

## `--version` does NOT tell you what is running

`--version` reads the binary **on disk**. It says nothing about the process that is
currently serving, and the two drift apart on every deploy. Observed live on oden
(2026-07-31):

```text
$ korp-ui --version          → korp v0.1.0 (fda8c87)   ← the NEW build on disk
$ readlink /proc/<pid>/exe   → …/korp-ui (deleted)     ← the OLD inode, still serving
  process started 09:56:42, binary written 11:01:14    ← an hour stale
```

So "I ran `--version`, it's live" is unsound on its own. `skidbladnir::running`
answers the real question from `/proc` (pure `std::fs`, no shell):

| check | what it catches |
|---|---|
| `/proc/<pid>/exe` ends in `" (deleted)"` | replace-by-unlink: the process holds an inode that no longer exists on disk |
| process start (`/proc/<pid>/stat` field 22 + `/proc/stat` `btime`) < binary mtime | a new generation was deployed and nothing restarted |

`Installer::running_freshness(spec, layout)` bakes it into the product API, and
`SystemdInstaller` implements it for real: unit → systemd `MainPID` → the two checks
above, against the binary the layout's `current` symlink points at *now* (so a
generation swap is caught, not just an unlink). `Freshness::Unknown` is **never** a
pass — an undetermined answer must not read as healthy, which is the whole defect
class this closes.

> Do not use the `/proc/<pid>` **directory mtime** as the start time. It was measured
> here to track the moment you stat it, so every stale process compares equal to a
> freshly-written binary and reads as current — the exact false green this replaces.

## Read the real docs

- 📖 **[The complete manual — `docs/manual.pdf`](docs/manual.pdf)** — every subsystem chapter (pack · archive · move · unfold, PKI, layout, the Redfish/Terraform/KVM backends).
- 📄 **[Full overview — `.nornir/README-full.md`](.nornir/README-full.md)** — the detailed pitch, quickstart, the full capability matrix, packet format, and design.
- 📝 [`docs/book.md`](docs/book.md) — the manual, as Markdown.