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

nordisk/Skidbladnir

.nornir/socket-deployment-guide.md

Sockets — guide

Design: socket-deployment.md. Feature: sockets (off by default).

skidbladnir = { version = "0.1", features = ["sockets"] }

The whole lifecycle

use skidbladnir::socket::{socket_for, BuildThingType, Mode, FixtureKind, FixtureHealth};

// 1. Type in, socket out. No per-cell config — this is the registry the pane calls.
let ty = BuildThingType::from_kind("tunnr-appliance").unwrap();   // from build-variants.toml
let socket = socket_for(ty);

// 2. The plan + topology render BEFORE anything boots (so the pane is never empty).
let plan = socket.plan(Mode::Test);
for row in plan.topology() {
    println!("{:<16} {:<28} {}", row.kind.as_str(), row.detail,
             row.endpoint.as_deref().unwrap_or("-"));
}

// 3. Light it. Fixtures are realized; health is Ready / NotApplicable / Down / NotImplemented.
let mut dep = socket.light(Mode::Test);
let netdev = dep.qemu_netdev_arg().expect("this socket runs on a shared net");
// -> "user,id=n0,net=10.9.0.0/24,host=10.9.0.1,restrict=off,dhcpstart=10.9.0.100,dns=10.9.0.1"
// Prepend `-netdev ` and pair it with `-device virtio-net` on the guest.

// 4. The operator adds a WireGuard key (Test mode only).
dep.add_wg_peer(peer_pubkey_b64, Some("10.9.0.1:51820".into()), vec!["10.9.0.0/24".into()])?;

// 5. Feed the captured boot console back in. THIS is what turns things green.
dep.confirm(&console);
assert_eq!(dep.fixture(FixtureKind::Dhcp).unwrap().health, FixtureHealth::Serving);

// 6. Assert against the node.
for probe in socket.probes() {
    let v = probe.run(&console);
    println!("{}: {} — {}", v.probe, if v.passed { "PASS" } else { "FAIL" }, v.detail);
}

// 7. Render, then tear down (consumes it; the operator's keys go with it).
let json = dep.json();
let order = dep.teardown();

Reading the health of a fixture

Health ok() What it means
Ready ✔ provisioned, will serve — not yet observed serving
Serving ✔ observed serving, from real console evidence
NotApplicable ✔ off in this mode by design (Prod DHCP). Serves nothing, fakes nothing
Down(why) ✘ it failed, or Prod refused it. why() quotes the proof
NotImplemented(why) ✘ nothing provisions this yet. why() names the missing seam

dep.all_ok() is true only if every fixture is in the top three. dep.refusals() gives you (kind, why) for the bottom two — render that, not a spinner. Every why carries a TO FIX: clause (socket::REMEDY_MARKER); split on it to show the remedy as its own line.

Reading the evidence

confirm() classifies the console per fixture into an Evidence:

Evidence meaning
Served proof it worked — a bound lease in the pool, a completed handshake
Failed(line) proof it did not, quoting the line. Wins over success evidence anywhere else, and downgrades even Serving
Unknown(why) does not settle it, and says why ("a DHCP attempt but no bound lease in the pool…")

The readers are public and independently testable: NetSpec::dhcp_evidence, wg_evidence, dns_evidence, plus NetSpec::leased_address / in_dhcp_pool and parse_handshake_age_secs.

WireGuard peer states

Handshake is_up() when
NeverConnected ✘ added, nothing observed
Handshaking ✘ an initiation was seen, no completion
Established { last_handshake_secs } ✔ if fresh a completed handshake
Established past 180 s ✘ (as_str() = "stale") older than WireGuard's REJECT_AFTER_TIME
Lost ✘ it was up, and the tunnel is now proven dead

Handshake::STALE_AFTER_SECS is 180 — WireGuard's own REJECT_AFTER_TIME. Staleness can only be detected when the console carries a wg show capture (latest handshake: … ago); a plain boot console has no age signal, so confirm records the observation as fresh.

Adding things

A fixture: impl Fixture (at minimum kind(); override runtime() and light_health() if it has preconditions or its own runtime) → add one arm to fixture_for_kind. If a socket should compose it, add the kind to that socket's arm in SocketPlan::for_type. Nothing else — the plan and the deployment stay in lockstep automatically.

A socket: add an arm to SocketPlan::for_type (its fixture list, console count) → impl Socket (usually just accepts + plan; override probes() if it has cluster-level assertions) → add an arm to socket_for. Zero pane changes.

A probe: impl Probe { fn name(&self) -> &str; fn run(&self, console: &str) -> ProbeVerdict } → return it from the relevant socket's probes().

A network backend: impl NetworkBackend. Override is_test_grade() -> false only if it really is production-fit — the default is true, and Mode::Prod refuses every fixture a test-grade backend realizes.

Gotchas