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-supportholds the small reusable helpers (find a program, run a command, copy folders, install the right build tool);nornir-pagesholds 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
1572864bytes should read1.5 MiB; a job that took123000milliseconds should read2m 03s; three-and-a-bit million rows should read3.4M. Simple — except more than eight different little routines had each been written by hand, and they disagreed: one wroteKiB, anotherKB, another justK; one wrote3.4M, another3.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. Andnornir-auth-clientis the other side of the same coin — the sharedlogincommand 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:
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.