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

nordisk/gunnar

installtest/Cargo.toml

# gunnar's **install tests**: a detached crate with its own `[workspace]`.
#
# ## What it is
#
# Two permanent, re-runnable proofs that a *freshly installed* gunnar serves real
# git:
#
#   * **container**, the appliance shape. gunnar is PID 1 in an OCI container,
#     fired up through **draupnir**'s `backend-oci` (bollard over the podman
#     socket). Stock `git` pushes over SSH, stock `git` clones back, and the two
#     **object graphs** are compared object for object.
#   * **kvm**, the machine shape. A real KVM guest booted by **tunnr** (QEMU +
#     OVMF + KVM), gunnar installed into it **as a systemd unit through
#     Skidbladnir**, and then the same push/clone/object-graph assertion driven
#     from the host over a `hostfwd` port.
#
# ## Why draupnir and not podman(1), and not dwarves
#
# `xtask/src/images.rs` already drives `podman` as a **subprocess**, and that is
# fine for what it measures (image size, the exported filesystem, whether a shell
# is in the image; questions that are about the *tarball*). This crate asks a
# different question, "does an installed gunnar serve?", and the constellation
# already owns one boot seam for that: draupnir, whose container backend is
# bollard over the podman REST socket. Reusing it beats growing a second seam.
#
# The KVM arm goes to tunnr directly rather than through draupnir's KVM backend,
# and the reason is a measured limitation rather than a preference. See the
# `backend-tunnr` note below and `src/kvm.rs`'s header.
#
# **dwarves is deliberately NOT used.** `dwarves-demo/src/build_thing.rs` defers
# to a demo stub that returns `ok("would run …")`, so a dwarves-driven install
# test would report success having executed nothing: the exact blind green that
# measurement exists to catch. draupnir has zero `Command::new` and zero `unimplemented!` on its
# run path; every operation is a real REST round trip.
#
# ## Why detached, and why not in `xtask`
#
# Identical to the reasoning recorded in `bench/Cargo.toml`, and for the same two
# reasons:
#
#  1. `xtask/Cargo.toml` declares **zero dependencies, deliberately**. It is the
#     dependency guard, and a workspace member takes part in resolver-2 feature
#     unification, so a dependency here could switch a feature on inside the
#     appliance build. `xtask` reaches this crate the way it reaches every other
#     tool: by running it (`cargo xtask install-test`).
#  2. A workspace member's dev-dependencies are built by `cargo test --workspace`,
#     which would drag bollard, tokio and QEMU wiring into gunnar's ordinary test
#     run.
#
# Its own `[workspace]` table makes it a separate workspace root, so the parent
# workspace cannot absorb it and the parent `Cargo.lock` is never touched.
[package]
name = "gunnar-installtest"
version = "0.0.0"
edition = "2021"
publish = false

[workspace]

# ── korp-installer: THE REGISTRY COPY, AND NO PATCH ──────────────────────────
#
# There WAS a `[patch.crates-io]` block here, and it is gone because the reason
# for it is gone.
#
# MEASURED 2026-09-10, `cargo check --manifest-path installtest/Cargo.toml
# --all-targets` against the registry: **four errors, all in `src/medium.rs`**,
# and every one of them the same shape —
#
#   error[E0425]: cannot find type `SignerName` in module `korp_installer::secureboot`
#   error[E0425]: cannot find value `FIXTURE_SEED_MARK` in module `korp_installer::forge`
#   error[E0425]: cannot find function `signer_names` in module `korp_installer::secureboot`
#   error[E0425]: cannot find function `initramfs_entries` in module `korp_installer::forge`
#
# All four existed in the `edda` checkout and none of them existed in the
# published `korp-installer 0.1.1`: the crate had drifted without a version
# bump, so this whole crate — every arm, not only the medium reader — was RED
# against a fresh resolve from the day `src/medium.rs` was written. The patch
# made it green HERE and nowhere else, which is the same thing as green nowhere:
# a clone without an `edda` beside it could not build this crate at all.
#
# `korp-installer 0.1.2` publishes all four. The requirement below names it, the
# patch is deleted, and this crate now resolves from crates.io alone — proven by
# checking it in a clone with no sibling `edda` on disk.

[dependencies]
libc = "0.2"
# The engine for EVERY arm that boots something: the container arm's OCI engine
# and — since 2026-09-05 — the KVM and ISO arms' guests too. Path dep, like
# `bench/`'s `nornir-bench`. draupnir 0.1.9 is not on crates.io and the sibling
# checkout is what jera and Skidbladnir use too.
#
# `backend-tunnr` is ON, and turning it on is what stopped this crate being a
# SECOND KVM path in the constellation. Two draupnir gaps had kept it off. Both
# were real, both were measured here, and both are now fixed upstream:
#
#  1. **No host→guest port forward.** `KvmBoot::to_tunnr_spec` only ever mapped
#     `NetMode::None`, and `BootSpec::validate` REJECTED `ports`/`port_maps` on
#     any non-container backend — so the one mechanism by which these arms reach
#     the gunnar they installed could not be expressed at all. draupnir
#     `60ffb06` honours both forms on `Backend::Kvm` and folds them onto
#     `tunnr_vm::NetConfig::User { hostfwd }` (`hostfwd=tcp::<host>-:<guest>`),
#     and an airgap (`NetMode::None`) combined with a forward is now refused BY
#     NAME rather than dropped in silence.
#  2. **A medium could only be an IDE `-cdrom`.** The appliance kernel is the
#     `nuked` preset and vetoes ATA, so the medium the firmware booted from was
#     invisible to the PID 1 it started. MEASURED 2026-08-15 on this box:
#     `INSTALLER: volume: no block device carries the ISO-9660 magic (CD001) …
#     (probed: loop1 … vda)` — no `sr0` anywhere. draupnir `519afcf` adds
#     `MediumAttach::{Cdrom, Virtio, Both}`, the virtio argv copied verbatim
#     from `korp_installer::isoproof::attach_iso_as_virtio`; `Cdrom` stays the
#     default, so nothing written before it moved a byte.
#
# `seed` rides along (`backend-tunnr = ["dep:tunnr-vm", "seed"]`) and is still
# the pure-Rust `fatfs` NoCloud image author — but it is now reached through
# `BootSpec::with_cloud_init` rather than called beside a boot this crate wired
# itself, so draupnir owns the seed AND the drive it arrives on, in one place.
draupnir = { version = "0.2.0", default-features = false, features = ["backend-oci", "backend-tunnr", "seed"] }

# The KVM primitive itself: QEMU + OVMF (UEFI) + KVM, `hostfwd`, a live serial
# capture and an ACPI power-down. The BOOTS go through draupnir now; what is
# still reached for here is `tunnr_vm::host_capabilities()` — the /dev/kvm +
# QEMU preflight, which draupnir does not re-export. Without it QEMU falls back
# to TCG and every timing this crate reports becomes a statement about an
# emulator, silently — the failure mode measurement guards against. draupnir pulls
# the same path dep for `backend-tunnr`, so this is one crate named twice, not
# two copies.
tunnr-vm = { version = "0.2.1" }

# ── PROMOTED FROM dev-dependencies 2026-08-26, for `src/installer_ssh.rs` ────
#
# These two were dev-only while the only caller of the installer path was a
# test. `bench/examples/nornir-bench.rs`'s `gunnar.installer_ssh` arm is a
# second caller of the SAME six operations, and a bench arm links the LIBRARY —
# so the mechanism has to live in `src/` and the crates it reaches for have to
# be ordinary dependencies. The alternative was a second copy of "compose,
# install, boot, authenticate" inside the bench example, which is precisely the
# twin `iso.rs`/`kvm.rs`/`container.rs` exist to prevent.
#
# The features are the dev block's, unchanged and for its reasons:
# `default-features = false` drops korp-installer's OpenZL C toolchain and the
# splash image codecs; `forge` is `isocompose::ForgeComposer`, the ONE composer
# `dwarves buildthing --installer` reaches; `secure-boot` is NOT optional
# alongside it, because `isocompose.rs` REFUSES to compose when the signing env
# is set and the feature is absent, and dwarves shipping `["forge"]` alone
# silently mastered unsigned installers for a fortnight (measured 2026-08-14);
# `testmatrix` is what makes `isoproof::emit_kvm_row` a row rather than a no-op.
#
# Nothing a SHIPPED gunnar links: this whole crate is a detached workspace of
# test tooling, its `[workspace]` table keeps it out of the parent's resolution,
# and the parent `Cargo.lock` is untouched.
korp-installer = { version = "0.2.0", default-features = false, features = ["secure-boot", "forge", "testmatrix"] }
# The DECLARATION, read through the type that owns it — `[[build.installer]]`,
# its params, `target_disk_bytes()` and `kvm_disk_args()`. A caller that spells
# `--release --target x86_64-unknown-linux-musl` or `64` itself is a SECOND
# declaration, and the two drift the first time the cell's diet changes.
nornir-build-thing = { version = "0.2.0", default-features = false, features = ["provenance"] }
# For `reachability::declared_interface_count` — the ONE reader of
# `korp.env.GUNNAR_NET`. The rig used to carry its own copy of that parse, and a
# rule that decides whether a medium's door opens must not live in two places
# (its own doc said so). Featherlight: nornir-build-thing, which is already
# here, plus anyhow.
gunnar-recipe = { path = "../crates/gunnar-recipe" }
# ── `--grpc-only` (src/control.rs), 2026-09-15 ──────────────────────────────
# A twin medium serves no git, so the door asks `Bootstrap.Status` for the
# build and the role instead of reading a banner. gunnar's own generated
# client, the same tonic 0.14 / tokio 1 already in this lockfile through
# gunnar-server — no new package, only three direct edges.
gunnar-proto = { path = "../crates/gunnar-proto" }
tonic = { version = "0.14", default-features = false, features = ["transport"] }
tokio = { version = "1", features = ["rt", "time"] }
# The `appliance-boot/<preset>` grid's OWN proof names (`install_ok` /
# `serves` / `ready`) for the `--target` door's rows — read from the constant,
# never respelled here. The rows themselves go through
# `korp_installer::isoproof::emit_boot_row` (feature `testmatrix` above). Was
# dev-only (the exe_drive rows); the library door is what emits now.
nornir-testmatrix = { version = "0.2", features = ["testmatrix"] }

[[bin]]
name = "gunnar-installtest"
path = "src/main.rs"

# ── SECURE BOOT ARMING (tests/secure_boot.rs) ────────────────────────────────
#
# A DEV-dependency, and only ever that. `korp-installer` carries the pure-Rust
# key ceremony, the Authenticode signer and the UEFI varstore enroller — the
# machinery that is already PROVEN in its own repository — and gunnar's job is
# to be ARMED with it, not to grow a second copy. Nothing in a shipped
# gunnar links it: the appliance only ever RUNS what the firmware already
# verified, so the RSA/CMS/X.509 stack belongs on the build box that signs a
# release and nowhere else.
#
# It is here rather than in `xtask/` (zero dependencies, deliberately — it is the
# dependency guard) or in the parent workspace (a member's dev-dependencies
# are built by `cargo test --workspace`, which would drag the signing stack into
# gunnar's ordinary test run). This crate is a detached workspace, so its
# resolution never touches the parent `Cargo.lock` — the same reasoning that put
# draupnir and tunnr-vm above.
#
# `default-features = false` drops the `znippy` OpenZL C toolchain and the
# `splash` image codecs: the proof installs a raw block payload and needs
# neither. `secure-boot` is the only feature the arming wants.
[dev-dependencies]
# `installer_options` renders the net plan the PID-1 parses (`NetConfig`, `NetPlan`).
appliance-pid1 = { path = "../../edda/crates/appliance-pid1" }
tempfile = "3"
#
# `forge` added 2026-08-15 for `tests/installer_drive.rs`: it is what compiles
# `isocompose::ForgeComposer` (and `payload::pack_znippy`), the ONE composer
# `dwarves buildthing --installer` and holger's xtask both reach. gunnar
# declares a `[[build.installer]]` cell and had no caller for it in this
# repository, so `gunnar-installer` was DECLARED and never once composed.
#
# `secure-boot` stays alongside it deliberately and is not an accident of
# history: `isocompose.rs` REFUSES to compose when the signing env is set and
# the feature is absent, precisely because dwarves shipped `["forge"]` alone and
# silently mastered unsigned installers for a fortnight (measured 2026-08-14).
# MOVED to [dependencies] 2026-08-26 — see the block there for why.
# NOTE on draupnir and `backend-tunnr`: ON since 2026-09-05 (the [dependencies]
# block above says what changed upstream), and `installer_drive` still does not
# boot through it. That is now a statement about THIS test rather than about
# draupnir, and the distinction matters:
#
#  * what it takes from draupnir is the two-lifetime REFUSAL
#    (`BootSpec::kvm_boot_installed_disk(..).with_medium(..)` must fail
#    `validate()`) — pure std, no backend, unchanged by the feature;
#  * its BOOTS go through `korp_installer::isoproof`, which also owns the
#    multi-disk attach order, the GPT/FAT readback, the ESP `korp/install`
#    record and the appliance-boot rollup rows this drive is assembled out of.
#    Moving those onto draupnir is a job of its own, not a feature flag.
#
# The two things that used to make this a draupnir LIMITATION are gone.
# MEASURED 2026-08-15: driving the install through draupnir's KVM backend died
# with `INSTALLER: volume: no block device carries the ISO-9660 magic (CD001)`
# because `medium_qemu_args` emitted `-cdrom` alone and the appliance kernel has
# no ATA/AHCI/SCSI at all — that is now `MediumAttach::Both`, whose virtio argv
# is copied verbatim from the `isoproof::attach_iso_as_virtio` that fixed it.
# And the forwarding NIC `isoproof::attach_slirp_nic` adds is now
# `BootSpec::with_port_map`. Keep the measurement; it is why both exist.
# The scratch directories the three disk images and two UEFI target dirs live in.

# ── THE DECLARATION, READ RATHER THAN RESPELLED (tests/exe_drive.rs) ─────────
#
# `exe_drive` runs the DECLARED `gunnar-server` cell, and the only honest way to
# know which binary that is — package, bin, features, profile, target triple —
# is to read `.nornir/build-variants.toml` through the type that owns it.
# A test that spells `--release --target x86_64-unknown-linux-musl` itself is a
# SECOND declaration, and the two drift the first time the cell's diet changes
# (measured precedent: the ISO test's hard-coded `.iso-out/appliance-root`,
# fixed 2026-08-05 by reading the cell's own `staging`).
#
# DEV-only and path-based: `edda` is already a mandatory sibling checkout for
# this crate (`korp-installer` above), so this adds no new resolve-time
# requirement, and nothing a shipped gunnar links.
# MOVED to [dependencies] 2026-08-26 — see the block there for why. The reason
# recorded above (read the declaration through the type that owns it) is
# unchanged; only the section it lives in moved, because the bench arm links
# the library and a dev-dependency is invisible from there.
# The fixture hub's vocabulary (encode_key for the learned-peer compare) — the
# hub itself is reached through korp-installer's `wgfixture` (one writer).
wg-appliance-core = { version = "0.1.4" }
# The rows themselves. `gunnar-appliance/exe` cells were `Absent` in the
# appliance rollup until `exe_drive` existed — there was no exe-form emitter in
# this repository at all. Feature ON here because a drive that records nothing
# leaves the grid exactly as empty as no drive.
nornir-testmatrix = { version = "0.2", features = ["testmatrix"] }
# **The appliance's OWN parser**, so the DECLARATION and the CODE meet in one
# assertion (`tests/installer_options.rs`).
#
# The `dhcp` option bakes `korp.env.GUNNAR_NET=eth0:{static_ip}:{gateway}` when
# an operator unticks DHCP. Whether that renders a spelling the appliance
# ACCEPTS is a fact about `gunnar_server::pid1::NetConfig::parse` and about
# nothing else — asserting a string literal here would be a second opinion
# about gunnar's own grammar, which is exactly how a declaration and a binary
# drift. `--no-default-features` with the appliance's own engine and no
# `dhcp-client`: the OFF build is the one the static bake is for.
#
# DEV-only, same as everything else in this block: nothing a shipped gunnar or
# a composed medium links.
gunnar-server = { path = "../crates/gunnar-server", default-features = false, features = ["server", "engine-znippy"] }

# TEMPORARY (release-3 merge, 2026-10-03), the same table the root carries: this
# detached workspace reaches gunnar's crates by path, and they ride edda's
# gix-0.88 / russh-0.63 crates until edda publishes them. Delete with the root's.
[patch.crates-io]
gunnar-bitmap = { path = "../../edda/crates/gunnar-bitmap" }
gunnar-sendpack = { path = "../../edda/crates/gunnar-sendpack" }
gunnar-pktline = { path = "../../edda/crates/gunnar-pktline" }
nornir-git = { path = "../../edda/crates/nornir-git" }
nornir-build-thing = { path = "../../edda/crates/nornir-build-thing" }
git-storage-trait = { path = "../../znippy/git-storage-trait" }
# The estate's sibling patches this workspace's lock takes that the root's old
# `.cargo/siblings.toml` include used to supply (G-B7, 2026-10-26: the root takes
# none now, so every estate workspace names its own; xtask/tests/lone_clone.rs).
estate-clock = { path = "../../edda/crates/estate-clock" }
disk-snapshot = { path = "../../edda/crates/disk-snapshot" }
appliance-pid1 = { path = "../../edda/crates/appliance-pid1" }
wg-appliance-core = { path = "../../edda/crates/wg-appliance-core" }
edda-admit = { path = "../../edda/crates/admit" }
nornir-auth-oidc = { path = "../../edda/crates/nornir-auth-oidc" }
nornir-auth-saml = { path = "../../edda/crates/nornir-auth-saml" }
nornir-auth-webauthn = { path = "../../edda/crates/nornir-auth-webauthn" }
nornir-identity = { path = "../../edda/crates/nornir-identity" }
nornir-identity-session = { path = "../../edda/crates/nornir-identity-session" }
nornir-identity-sso = { path = "../../edda/crates/nornir-identity-sso" }
nornir-identity-directory = { path = "../../edda/crates/nornir-identity-directory" }
nornir-auth-sshsig = { path = "../../edda/crates/nornir-auth-sshsig" }
nornir-service = { path = "../../edda/crates/nornir-service" }
nordisk-pairing = { path = "../../Skidbladnir/crates/installer/nordisk-pairing" }