Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/Skidbladnir

.nornir/README.md

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.

Benchmarks

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.

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):

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

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):

$ 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