<!-- nornir:class: idiot -->
# edda — for beginners
> No jargon. What this repo is and why it exists.
## The one-sentence version
edda is a **box of reusable tools** that used to live inside the big nornir program
but were taken out so *other* programs can use them too — without having to drag the
whole of nornir along.
## The name
In Norse mythology the *Edda* is the collected book of stories. Here it is the
collected book of **shared library crates**: small, self-contained pieces of code
that do one job well.
## Why take them out of nornir?
Imagine nornir is a huge Swiss-army knife. korp and holger only wanted the
*scissors* and the *screwdriver*, but to get them they had to carry the whole knife
— every blade, the corkscrew, all of it. That is wasteful and tangles everything
together.
So each genuinely-reusable tool was lifted into its own little crate in edda. Now a
program adds just the scissors. nornir still works exactly as before, because it
picks the tools back up under their old names — nothing it called changed.
## What is in the box
- **autonom** — asks "is every part of the code actually tested?" and prints a list
of the parts that are not.
- **jera** — runs long jobs and remembers them; can start a container or a virtual
machine without ever typing a shell command.
- **nornir-testmatrix** — runs all the tests and builds a pass/fail grid.
- **nornir-robotui** — a robot that clicks around a graphical app to test it.
- **nornir-saga** — records what a user did, then replays it as an automatic test.
- **nornir-bench** — measures how fast things are and keeps the history.
- **nornir-coverage** — measures how many lines of code the tests actually ran, and can compare today's run against a saved baseline to shout if coverage **dropped** (a "did we get lazier?" alarm for CI).
- **nornir-git** — talks to git repositories directly, in pure Rust.
- **nornir-cli-outcome** — gives every command the same tidy shape of answer.
- **nornir-depgraph** — works out which repo depends on which (who to rebuild when
one changes), and can flatten that picture into simple table rows to save, then
rebuild the exact same picture from those rows later.
- **nornir-claude** — the one shared way to chat with Claude (Anthropic's AI). It
comes with a **pretend** version that needs no internet and no password (for
tests and demos) and a **real** version that finds your key the same way the
official tools do — it never writes a key into the code. Two apps used to each
carry their own copy of this; now they share this one.
- **nornir-hash** — the one shared way to compute a file or blob's **fingerprint**
(a SHA-256 "content id"). Give it some bytes or a file, get back a short text code
that is the same every time for the same content and different for anything else —
which is how the storage systems tell "have I seen this exact thing before?". Six
programs had each written this same little routine by hand; now they share this one.
- **nornir-xtask-support** + **nornir-pages** — the one shared way to **build and
publish a repo's little in-browser demo**. Most repos have a "look, it runs in your
browser" page (a tiny web app compiled to WebAssembly). Building it and pushing it
live involves the same fiddly steps every time — find the tools, compile, and ship
the files to the web-page branch. `nornir-xtask-support` holds the small reusable
helpers (find a program, run a command, copy folders, install the right build tool);
`nornir-pages` holds the whole recipe. A repo just fills in a little form (its name,
colours, web address — eleven blanks) and the two shared crates do the rest, the
same way for all four repos. Before, each repo carried its own ~450-line copy of
this. (Both are "internal only" — never shipped to the public crate registry.)
- **nornir-humanize** — the one shared way to turn a **big raw number into a short
readable label**. A file that is `1572864` bytes should read `1.5 MiB`; a job that
took `123000` milliseconds should read `2m 03s`; three-and-a-bit million rows
should read `3.4M`. Simple — except more than eight different little routines had
each been written by hand, and they disagreed: one wrote `KiB`, another `KB`,
another just `K`; one wrote `3.4M`, another `3.4m`. Now there is one honest set of
rules everything uses, so every screen and report reads the same. (Internal only.)
- **nornir-escape** — the one shared way to make a label **safe to put inside a
picture or document**. When a tool draws an SVG chart or writes an XML file, a
stray `&`, `<`, `>` or `"` in a repo name would corrupt the file, so those few
characters must be swapped for safe codes (`&` becomes `&`, and so on). This
tiny swap had been copy-pasted **seven times, letter for letter**, across four
different projects — one copy even had a note apologising for being a copy. Now
there is one shared version, and if the label has none of those characters it
hands the text straight back without doing any work. (Internal only.)
- **nornir-service::resolve_token** — the one shared way a server **finds its
password**. Each of the nordisk servers needs a secret so only trusted callers
can talk to it, and each looked for that secret in the same three places, in the
same order: a value typed on the command line, then a setting in the
environment, then a small hidden file in your home folder — and if none of those
is filled in, it just runs **open** (no password, fine for a laptop). All three
servers had written that little "look here, then here, then here" search by hand;
now they share one copy, and each just says which environment setting and which
file are *its* own. A blank in any spot is treated as "not set" and it keeps
looking, so an empty value can never accidentally become the password.
(Internal only.)
- **the `nornir-auth-*` family** — the one shared way a server **checks who you
are**. There are many ways to prove your identity — a simple secret token, a
company single-sign-on login (the "log in with Microsoft/Google" button), a
client certificate, a passkey/fingerprint, an API key — and each is its own
small crate: `nornir-auth-oidc`, `-bearer`, `-oauth2`, `-apikey`, `-mtls`,
`-saml`, `-webauthn`. A server switches on only the ones it wants and ignores
the rest, so a tiny server that only needs a token doesn't have to carry the
heavy machinery for enterprise SSO. They all plug into the same socket (the
"AuthProvider" seam), so the server code that asks "is this caller allowed?"
never changes when you add or remove a method. And **`nornir-auth-client`** is
the other side of the same coin — the shared `login` command a person runs to
sign in, which pops open the browser SSO flow and safely remembers the token.
## "The test just sat there for seven minutes" (2026-08-30)
The boot proofs run a real machine and wait. If the machine never says anything,
the proof waits out its whole budget — 420 seconds — and then reports a failure
built out of guesses ("no boot signature on the disk"), which is a description of
what the *waiting* found, not of what went wrong.
Two things changed so that stops happening.
**The appliance is allowed to end the test.** When our own software gives up it
says so out loud, on the console: `INSTALLER: FAILED hwdetect: no usable target
disk`. The proof now reads that line the moment it appears, stops, and puts the
appliance's own sentence in the report. Same machine, same medium: **420.1 s → 3.8 s**.
**A machine we cannot possibly install on is refused before anything starts.**
Some cloud shapes take a disk *image*, not a bootable CD; some put their disk on
a wire our medium has no driver for. Both are readable from what is already
written down, so the door says no in **0.03 s** and tells you which fact stopped
it — instead of booting a machine that was never going to work.
**And "it timed out" is now its own answer**, spelled `[TIMEOUT]`, with its own
exit code. It is not a failure: a failure means we measured something and it was
bad. A timeout means we measured *nothing*, and pretending those are the same
thing is how a test that proves nothing gets mistaken for a test that passed.
## "It boots on my laptop but the cloud shows a blank screen" (2026-08-30)
An `.iso` file is shaped like a **CD**. Every machine knows how to start a CD.
But most clouds do not offer you a CD drive. You upload the file and they turn it
into the machine's **hard disk**. And a PC that starts from a hard disk looks at
one place only — the very first 512 bytes — for two magic bytes, `55 AA`. A plain
`.iso` has zeros there. So the machine looks, finds nothing, and stops. No error,
no output, nothing: we measured seven minutes of a completely empty serial log.
The fix has a name — a **hybrid** image. We put a tiny bit of boot code in the
first 512 bytes (which an `.iso` leaves empty anyway, so the file does not get any
bigger), and that code loads the same boot loader the CD path uses. One file, both
ways in: burn it to a CD, write it to a USB stick, or upload it to a cloud as a
disk image.
In the recipe it is one word, `medium_boot = "hybrid"`. To check a file you were
handed:
```bash
dd if=whatever.iso bs=1 skip=510 count=2 2>/dev/null | od -An -tx1
# 55 aa → it can boot as a disk
# 00 00 → CD only; a cloud that imports it will show you a blank screen
```
## The one rule to remember
**Moving a tool never breaks anything.** A tool can move into edda, get published,
and the programs that used it keep working — they just point at the new home. Add
alongside, never rip out. That is the whole philosophy: additive, no duplication,
no surprises.
## "Why is CONFIG_XXX in my kernel?" — for idiots
A kernel we build has about **820 switches turned on**. Our recipes name **84** of them.
So for nine switches out of ten the honest answer used to be *"the kernel put it there,
we don't know why"* — 764 of 849 lines in `kernel.why.toml` said exactly that.
Kconfig, the kernel's own configuration language, has three ways for one switch to turn on
another:
* **`select`** — "if I am on, THAT one is on too, and you may not say no."
* **`depends on`** — "I cannot be on unless THAT one is."
* **`default y`** — "unless somebody says otherwise, I am on."
Follow those backwards far enough and you land on something you can actually point at. That
is what the solver does, and it writes the walk into `kernel.why.toml` as a **chain**:
```
CONFIG_VT [declared] --select--> CONFIG_INPUT
```
Read it right to left: *"`CONFIG_INPUT` is in your kernel because `CONFIG_VT` selects it,
and `CONFIG_VT` is there because WE asked for it — the appliance's only console on a cloud
VNC is the VGA text console."* Take that one line out of the recipe and `CONFIG_INPUT` goes
with it. That is not a guess; the test proves it by actually taking the line out and
rebuilding the config.
Some chains are five links long:
```
CONFIG_BLK_DEV_INITRD --default--> CONFIG_RD_ZSTD --select--> CONFIG_DECOMPRESS_ZSTD
--select--> CONFIG_ZSTD_DECOMPRESS --select--> CONFIG_ZSTD_COMMON --select--> CONFIG_XXHASH
```
*"There is a zstd hash function in your kernel because your PID 1 rides an initramfs."*
**Four things the chain can end at**, and they mean different things:
| it ends at | plain English |
|---|---|
| `declared` | **we** asked for it. Ours to change. |
| `base-config` | the kernel preset we started from already had it. Change the preset. |
| `kconfig-default` | Linux gives it to everybody. Nothing to change. |
| `arch` | it follows from building for x86_64. |
**43 of 765 have no chain, and we say so rather than making one up.** 41 of them are
questions the compiler answers, not the config file — `CONFIG_CC_HAS_INT128` means "does
your gcc understand `__int128`", and the only way to find out is to try it. That is why the
same recipe gives `CONFIG_GCC_VERSION=150200` on oden and `140200` inside the build
container. The other two are one `choice` menu that declares no default and one empty string.
**The practical payoff:** we can now tell, before starting a 48-second container, that a
recipe contradicts itself. Turn the serial port off for a machine that has none, and the
"print the boot log to the serial port" line underneath it can no longer quietly go with it
— the build refuses and names both lines.