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

nordisk/gunnar

deploy/upcloud/Cargo.toml

# gunnar's **UpCloud re-image**: the live appliance at `gunnar.rs` taken from an
# installer ISO on this disk to a serving box, over the UpCloud API 1.3, in Rust.
#
# ## Why this is a crate and not xtask code
#
# `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 there could switch a feature on inside the
# appliance build. This work needs HTTPS and JSON. Both cannot live in xtask.
#
# Three options were weighed before this one was taken:
#
#  a. **A normal crate that xtask RUNS.** What this is. `xtask/src/reinstall.rs`
#     spawns `cargo run --manifest-path deploy/upcloud/Cargo.toml`, exactly the
#     way `cargo xtask install-test` reaches `installtest/` and `cargo xtask
#     iso-installer` reaches dwarves. Spawning cargo is not the shell-out the
#     rule forbids — cargo is the build tool, and the door it opens is a Rust
#     `main`, not a script. `isoinstaller.rs`'s header carries that argument.
#  b. **Hand-roll HTTP and JSON inside xtask.** Rejected: it means a TLS stack in
#     a crate whose whole purpose is to have no dependencies, and a hand-rolled
#     TLS client is the one thing in this procedure that MUST not be homemade.
#     Without TLS there is no talking to `api.upcloud.com` at all.
#  c. **Put it in an existing crate.** Rejected: every `crates/*` member is in
#     the appliance's resolved graph, so `ureq` + `rustls` + `serde_json` would
#     be measured by `cargo xtask guard` as new appliance cost for a tool the
#     appliance never runs. A detached workspace costs the graph nothing.
#
# `[workspace]` below makes this a workspace ROOT, so the parent's resolution and
# the parent `Cargo.lock` are untouched — the same mechanism `installtest/` and
# `<ops>/sitetest/` use, and for the same reason.
#
# ## The dependencies, and why each one
#
# * `upcloud-api` (edda) — **the constellation's ONE UpCloud surface**, since
#   2026-09-21. This crate carried its own `ureq` transport, its own URL
#   builder and a hardcoded `api.upcloud.com` with no override, so a
#   `private-gunnar-ops` `mock reimage --apply` chained into this binary and
#   would have re-imaged the LIVE appliance while its banner said FAKE. Now
#   every call goes through the trait, over ONE wire that builds the request
#   for the account and for a `mock-upcloud` by the same code, and the cloud is
#   chosen once in `main` from `--mock-api`. `ureq` (3, rustls) arrives through
#   it; there is no second HTTP client here.
# * `serde_json` — the API speaks JSON and nothing else. Used through `Value`
#   rather than derived structs: the bodies are five keys wide, and `Value` is
#   what lets the scripted world in `tests/` hand the procedure a canned reply
#   without a parallel set of wire types to keep in step with the real ones.
# * `nornir-hash` — `sha256_file`, streaming, 64 KiB at a time. The upload
#   verification of step 3 is the one gate that cannot be skipped, and the
#   constellation already owns exactly one SHA-256 primitive (edda). A local
#   `Sha256::new(); hex(...)` here would be the seventh copy that leaf was
#   written to delete.
[package]
name = "gunnar-upcloud"
description = "Re-image the live gunnar appliance on UpCloud from an installer ISO, over API 1.3, in pure Rust. Driven by `cargo xtask reinstall-appliance`."
version = "0.0.0"
edition = "2021"
publish = false
# MEASURED 2026-09-13: the moment `src/bin/vnc-capture.rs` gave this crate a
# SECOND binary, `cargo xtask reinstall-appliance` stopped working — it spawns
# `cargo run --release --manifest-path deploy/upcloud/Cargo.toml --` (see
# `xtask/src/reinstall.rs`), and cargo answered `could not determine which
# binary to run` and exited 101. The re-image door was dead, for a diagnostic
# tool that was only ever meant to watch. The verb has no `--bin` to give, so
# the manifest has to say which binary IS the crate: the procedure, never the
# observer.
default-run = "gunnar-upcloud"

[workspace]

[dependencies]
upcloud-api = { version = "0.1.5" }
serde_json = "1"
# ── THE FOUR edda LEAVES BELOW ARE crates.io VERSIONS, NOT SIBLING PATHS ─────
#
# They were `path = "../../../edda/crates/…"` until 2026-09-12, and the reason
# each one gave for it was the same sentence: *"this deploy door already
# REQUIRES the sibling `edda` checkout, so a second registry spelling would buy
# nothing."* That premise stopped being true one file at a time —
# `installtest/` deleted its `[patch.crates-io]` on 2026-09-10 when
# `korp-installer 0.1.2` published, `bench/` took its last sibling path to a
# version on 2026-09-09, and the root workspace took the six gunnar leaves to
# versions the same day. This manifest was the LAST tree in the repository
# requiring `../edda` on disk, and a requirement held by exactly one detached
# workspace is not a shared premise, it is a straggler.
#
# `cargo metadata` reads a path dependency's manifest at RESOLVE time, so while
# these four lines said `path`, `cargo xtask deploy-check` — and anything else
# that so much as asks this tree what it contains — was a hard error in a clone
# with nothing beside it. `xtask/tests/deploy_check.rs` is the guard.
#
# All four are published and the requirement here is the version this door was
# written against; cargo takes the newest compatible one.
nornir-hash = { version = "0.1.0" }
# `expand_home` — the ONE implementation, re-exported by `src/site.rs`. It was a
# copy there until 2026-09-10, one of FOUR in this repository that
# `nornir mimir analyze duplicates` clustered.
nornir-xtask-support = { version = "0.1.1" }
# * `nornir-build-thing` — THE ROSTER, and the reason this dependency is not
#   optional. `target::presets` is the one table of named machines in the
#   constellation, and each row says how its platform takes a medium
#   (`MediumBring`). `src/machine.rs` derives this door's attach strategy from
#   that word instead of remembering it: `upcloud` is `IsoCdrom`, so the ISO
#   rides twice; `scaleway-instance` is `ImportQcow2`, so this verb refuses BY
#   NAME rather than attempting an ISO attach a platform has no door for.
#
#   Before this line the pair of attaches was a constant in `plan.rs` — the
#   SIXTH copy of a fact draupnir, installtest, mother_kvm, this crate's header
#   and the roster itself already carried, and the only one that was
#   load-bearing. Reusing the declaration instead of restating it is the LAW
#   ("reuse the nordisk repos"), and here it is also the bug fix.
#
#   `default-features = false`: the default feature set is already dep-light,
#   but naming it keeps `plugin-host`'s wasmtime out of this tree by
#   declaration rather than by luck. A registry version, like `nornir-hash`
#   above: `MediumBring` and `target::presets` are both in the published
#   0.1.17, and `Cargo.lock` beside this file resolves exactly that.
#
#   **ONE version of the build system per repository**, and this line is the
#   one that got away. efb6c29 took the root workspace, `installtest/` and this
#   manifest to `0.1.17` together — but it committed the root and `installtest/`
#   LOCKS and not this detached workspace's, leaving a lock pinned at `0.1.16`
#   under a requirement of `^0.1.17`. An unsatisfiable lock is not a lock:
#   cargo re-resolved on every `cargo xtask reimage`, took the newest compatible
#   release (0.1.18 by 2026-09-17) and WROTE it here, and the next re-image
#   refused with `[gunnar-dirty]` because the tree was modified. Three hand
#   reverts on 2026-09-17 each restored the unsatisfiable state and bought one
#   run. `xtask/src/reinstall.rs` now spawns with `--locked` so a resolve that
#   would move this file is an error instead of a write, and
#   `xtask/tests/lock_current.rs` keeps the file current so that flag never
#   turns into a dead door.
#
#   If this version moves, the root `Cargo.toml`'s workspace pin, `installtest/`
#   and this line move TOGETHER, and every affected `Cargo.lock` goes in the
#   SAME commit. Two versions of `nornir-build-thing` in one repository is how a
#   `MediumBring` variant lands here that the appliance build never saw.
nornir-build-thing = { version = "0.2.6", default-features = false }
# * `nornir-git`, `default-features = false` — **THE SSH TRANSPORT**, for step 11:
#   restarting the front's three services after the appliance underneath them was
#   replaced. `nornir_git::ssh::connect_exec_with` is the russh dialler
#   `gunnar-client`'s send-pack uses every day, with the connect / authenticate /
#   `exec` / blocking-I/O-bridge / in-runtime teardown already written and tested
#   against a real russh server on loopback. That teardown is not trivia: getting
#   its `Drop` wrong panics with "there is no reactor running", a bug that repo
#   found and fixed once.
#
#   The alternative was spawning `ssh`, and that is the one dependency decision
#   in this file that is a LAW question rather than a taste one. `cargo` is
#   spawned by `xtask` because it is a BUILD TOOL opening a door onto a Rust
#   `main`; `ssh` is not a build tool, it IS the work — transport,
#   authentication and remote invocation are the whole of what step 11 does, and
#   doing that in another program is exactly what "pure Rust, no shelling"
#   refuses. `ssh2`/libssh2 would be FFI to C while a Rust library exists.
#
#   `default-features = false` drops `porcelain`, i.e. gix and its ~57 `gix-*`
#   crates plus the reqwest/rustls/icu tail, leaving russh + ssh-key + tokio: the
#   transport half and nothing else. This is a DETACHED workspace, so none of it
#   reaches the appliance's resolved graph or the parent `Cargo.lock`.
#
#   `monetize-impl/deploy/front/src/remote.rs` made the same call the same night
#   for the same box. What crosses the fence is this library, not that crate: a
#   path dependency from the OPEN product into a private repository would make
#   every gunnar build need a checkout nobody outside Vetra has. `src/front.rs`
#   names the twin.
nornir-git = { version = "0.2.4", default-features = false }
# * `russh` + `tokio` — **the host key in the receipt**, and neither line adds a
#   crate to this tree. `nornir-git` above already resolves both (its transport
#   IS russh on tokio; `deploy/upcloud/Cargo.lock` has carried russh 0.61.2 until OSSF-NEXT (2026-10-30) moved it to the root's nornir-git 0.2 and russh 0.63,
#   ssh-key 0.7.0-rc.10 and tokio 1.53.1 since that dependency landed). What
#   these two lines buy is the right to NAME them: `src/banner.rs` opens one
#   key exchange against the re-imaged box and records the host key it
#   presented, which `nornir-git`'s API cannot hand back — its `check_server_key`
#   prints a fingerprint and keeps it (`ssh.rs:353`).
#
#   The feature sets are deliberately a SUBSET of what `nornir-git` already
#   asks for ("macros", "rt-multi-thread", "io-std", "io-util", "sync", "fs",
#   "time"), so resolver-2 unification changes nothing: no new crate, no new
#   feature, no movement in the lock beyond this crate's own dependency list.
#
#   Why it is worth a dependency line at all: `known_hosts` records no
#   provenance. A key taken off the wire by `ssh-keyscan` and a key read off an
#   install console are the same bytes once written, so the only thing that
#   makes a forge push mean anything is that the fingerprint came from a record
#   written BY the install. This crate IS that record's author.
russh = { version = "0.65" }
tokio = { version = "1", features = ["rt-multi-thread", "time"] }

[dev-dependencies]
# **UpCloud, for the order proofs** — the published fake of API 1.3, its
# MEASURED behaviours included (`BEHAVIOURS-LEDGER.md`). The flow tests ran
# on a scripted world this crate wrote until 2026-09-21; the owner's order was
# to run them on the mock. `test-inject` is its test-only door for a refusal
# the provider CAN give but no behaviour produces on demand; it claims nothing
# about UpCloud. A dev-dependency only: nothing of it reaches the binary.
mock-upcloud = { version = "0.1.3", features = ["test-inject"] }
# `tokio/net` for the TESTS only. `src/banner.rs`'s host-key proof starts a real
# russh server on loopback and reads its key over a real key exchange, which
# needs a listener; the binary itself only ever dials, so `net` is not widened
# on the normal dependency above. russh already pulls `net` into the graph on
# every non-wasm target, so this adds no crate and no build — it is the
# DECLARATION that this crate's tests use it.
tokio = { version = "1", features = ["net"] }

[lib]
name = "gunnar_upcloud"
path = "src/lib.rs"

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

# A SECOND bin, not a verb on the first. `gunnar-upcloud` is `reinstall`: a
# destructive procedure gated behind `--confirm-destroy`, whose parser refuses
# unknown flags precisely so nothing is dropped silently on a run that wipes a
# disk. Capturing the console is READ-ONLY observation and must stay reachable
# without touching that door — the day it was written (2026-09-13) the standing
# instruction on the lane was "observe only".
#
# `src/vnc.rs` behind it adds NO dependency: it asks the server for the Raw
# encoding and nothing else, which makes the wire format a rectangle of pixels,
# and it writes PNG with stored deflate blocks. See that module's header.
[[bin]]
name = "gunnar-vnc-capture"
path = "src/bin/vnc-capture.rs"

[lints.rust]
unsafe_code = "forbid"