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

mikronova/draupnir

.nornir/redfish-server.md

draupnir's own BMC — a Rust Redfish server fronting KVM

Feature redfish-server (OFF by default) · src/redfish_server.rs · shared vocabulary in src/redfish/wire.rs · anchored by tests/redfish_server_conformance.rs against tests/fixtures/dmtf/ · live loop in tests/redfish_kvm_loop_proof.rs Declared 2026-08-15.

What it is

A pure-Rust Redfish service that answers the endpoints draupnir's Redfish client drives, and translates them onto a QEMU/KVM guest through draupnir's own Boot/Lifecycle seam. It turns any Linux box with /dev/kvm into a Redfish-controllable machine.

src/redfish.rs was already the client end — it drives a real iDRAC / iLO / OpenBMC. This is the other end of the same protocol, in the same crate, sharing the same wire vocabulary.

Why it exists

Two reasons, and the second is the durable one.

  1. A Redfish burn needs a BMC, and a developer box has none. The iso-metal matrix row can never go green here, because a human with a USB stick is not a matrix row. This gives the wire half of that proof a service to run against — recorded as iso-redfish-sim, on its own row.
  2. It is a real capability. A lab, a CI fleet or a nested-virt pool becomes addressable with the same out-of-band vocabulary as the metal it stands in for. That is squarely draupnir's charter: kvm and container, low level.

It is Rust rather than OpenStack's Python sushy-emulator because putting the metal-path proof on a Python service would put it outside the pure-Rust charter — and because draupnir already owned both ends, so the server is the hinge between them rather than a new dependency.

THE TRAP, and what is done about it

draupnir's client talking to draupnir's server is a MIRROR. Both ends can be wrong in the same way and every test still passes. That is the identity-value failure: a test sitting on 1.0 cannot tell a working transform from a dead one. A loopback green proves self-consistency, not Redfish.

Two answers, neither optional:

The translation — no new machinery

Redfish draupnir
VirtualMedia.InsertMedia BootSpec::medium
Boot.BootSourceOverrideTarget BootOrder, via BootOrder::from_redfish_target
ComputerSystem.Reset {On} Boot::boot → KvmBoot → QEMU/OVMF/KVM
ComputerSystem.Reset {ForceOff} Lifecycle::power_off
ComputerSystem.PowerState Lifecycle::status

BootOrder::from_redfish_target is the inverse of the client's BootOrder::redfish_target, and it lives next to it in src/lib.rs — two mappings in two modules would be two mappings, and the one that mattered would be the one nobody tested.

Each order then selects the constructor that already expresses it:

A target this node cannot honour (Pxe, BiosSetup) is refused by name, never coerced into one it can.

REUSE, not a twin — src/redfish/wire.rs

The paths, property names, action names, enum tokens, the Basic-auth encoder/decoder and the action-body builders/readers are written down once, in redfish::wire, and both ends read them:

The conformance suite then checks those paths against the @odata.id strings in DMTF's own mockup — so "the two ends agree" and "the two ends are right" are separate, separately proven claims.

TLS — why the server needed a certificate at all

draupnir's client pins the BMC certificate (RedfishBoot::pin_cert_pem; verification stays ON with that cert as the sole trusted root). A pinned client needs a real cert to pin, so the server mints a genuine self-signed X.509 with SANs per run and holds the key in memory only — nothing is written to disk, so there is no .key/.pem/.der that could be committed. cert_pem() hands out the public half.

The crate is rcgen 0.13 with default-features = false, features = ["pem","ring"] — the same crate, features and call shape as Skidbladnir's src/pki.rs, which is the constellation's TLS-identity story and is already proven to load into a real rustls::ServerConfig. korp-installer's SigningIdentity (RSA + x509-cert) was read and deliberately not reused: it mints an Authenticode code-signing identity, not a TLS server identity with SANs. Skidbladnir's code cannot simply be called from here either — Skidbladnir path-deps this crate (Skidbladnir/Cargo.toml:87), so a dep back onto its pki module would be a cycle. Same crate, same pattern: one cert story, not two.

Threading

The accept loop runs on a gatling::background::Job — the constellation's sanctioned home for a raw thread (ROOT-LAW #0), the same seam src/exe.rs:246 already uses. No bare std::thread::spawn, no rayon, no async runtime.

The endpoints

Method Path Answer
GET /redfish {"v1": "/redfish/v1/"} — unauthenticated (DSP0266)
GET /redfish/v1/ (and /redfish/v1) ServiceRoot — unauthenticated
GET /redfish/v1/SessionService/Sessions empty SessionCollection
GET /redfish/v1/Systems ComputerSystemCollection
GET /redfish/v1/Systems/{id} ComputerSystem (live PowerState)
PATCH /redfish/v1/Systems/{id} the one-time Boot override → 204
GET …/VirtualMedia and …/VirtualMedia/{slot} the collection / the slot
POST …/VirtualMedia/{slot}/Actions/VirtualMedia.InsertMedia mount → 204
POST …/VirtualMedia/{slot}/Actions/VirtualMedia.EjectMedia unmount → 204
POST …/Systems/{id}/Actions/ComputerSystem.Reset power → 204

Everything below the service root needs HTTP Basic auth (401 + WWW-Authenticate otherwise). A wrong method answers 405 with Allow.

What the anchor checks

  1. No invented property — every key emitted must be one DMTF's schema declares, or a well-formed Redfish annotation (the patternProperties grammar is matched by hand, so ResetType@Redfish.AllowedValues still dies).
  2. Nothing required missing — every name in the schema's required array.
  3. The @odata.type is real — #Ns.vX_Y_Z.Type must be a definition the DMTF schema file for that exact version declares.
  4. Advertised values are DMTF's — ResetType@Redfish.AllowableValues ⊆ Resource.json#/definitions/ResetType; BootSourceOverrideTarget… ⊆ ComputerSystem.json#/definitions/BootSource.
  5. The mockup's URIs are our paths — DMTF's own @odata.id strings and the Reset action's target, compared to wire::*.
  6. The errors are the registry's — every message compared field-for-field with the Base.1.19.0 entry of the same Id, and each live error payload against redfish-error.v1_0_2 + Message.v1_1_2.

The RED controls

control where what it refuses
bad ResetType conformance + loop proof 400 Base.1.19.0.ActionParameterValueNotInList, quoting the value; nothing boots
InsertMedia at a ghost image conformance + loop proof 400 Base.1.19.0.ResourceMissingAtURI, naming the URI; nothing mounts
override applied, not just acknowledged conformance (spec level) + loop proof (machine level) the same ISO left in the tray, only Cd→Hdd: the spec loses its medium AND the guest prints no DVD-ROM line and no appliance banner
unhonourable boot target conformance Pxe/BiosSetup/Usb → 400 PropertyValueNotInList, never coerced to Cd
missing / wrong credential conformance 401 + WWW-Authenticate
wrong-cert pinning conformance the TLS handshake fails — proving pinning is on, not off
tampered DMTF fixture conformance digest mismatch against PROVENANCE.md

Named limitations — read before trusting interop