Skidbladnir RON schema — the declarative provisioning object model
Class:
design· Source of truth:src/orchestration.rs. This documents the RON object model as implemented (nornir commit78deafc), verified field-by-field against the serde structs. The companioncustom-objects.mdcovers theSteeringModulecustom-code seam; the original design capture isron-orchestration-design.md.
Skidbladnir has two ways to drive its imperative airgap core (pack · archive · fetch · unfold · install · start):
- Declarative RON — a
.ronfile describes an ordered list of steps; therun_flowinterpreter executes it. No new Rust per scenario. - Custom/imperative — call the Rust API or a
SteeringModuledirectly. Seecustom-objects.md.
This doc is the RON object model: every top-level type, its fields, how it deserializes, and an annotated end-to-end example.
The top-level objects
Everything lives in src/orchestration.rs. The RON-facing types (the ones you write in a .ron) are Flow, Step, and BootBackend. The rest of the object model (RunContext, StepOutcome, Dispatch, Registry, SteeringModule) is the runtime side — produced/consumed by the interpreter, never written in RON.
| Object | Kind | File:line | Written in RON? |
Flow |
struct { steps: Vec<Step> } |
orchestration.rs:104 |
yes (optional wrapper) |
Step |
enum (5 variants) | orchestration.rs:68 |
yes — the list elements |
BootBackend |
enum (4 variants) | orchestration.rs:49 |
yes (a BootOS field) |
RunContext |
struct (threaded state) | orchestration.rs:139 |
no (runtime) |
Dispatch |
struct (one trace row) | orchestration.rs:153 |
no (runtime) |
StepOutcome |
struct (a step's result) | orchestration.rs:165 |
no (runtime) |
SteeringModule |
trait (the seam) | orchestration.rs:186 |
no (Rust) |
Registry |
struct (name→module) | orchestration.rs:198 |
no (runtime) |
Flow — the whole scenario
pub struct Flow { pub steps: Vec<Step> } // orchestration.rs:104
A flow is just an ordered Vec<Step>. Flow derives Serialize, Deserialize, Default, Clone, PartialEq, Eq.
Step — the typed vocabulary (5 variants)
pub enum Step { // orchestration.rs:68
BootOS { backend: BootBackend, os: String },
Seed { config: String },
InstallApp{ spec: String },
Configure { target: String, params: BTreeMap<String, String> },
Federate { registry: String },
}
| Variant | Fields | What it does | Module it routes to | REAL? |
BootOS |
backend: BootBackend, os: String |
Boot/provision an OS on a backend | per-backend (below) | Kvm real, rest mock |
Seed |
config: String |
First-boot NoCloud (cloud-init) config | Seed |
mock |
InstallApp |
spec: String |
Install a registered application | InstallApp |
real |
Configure |
target: String, params: map |
Configure a target subsystem | Configure |
mock |
Federate |
registry: String |
Federate with an upstream registry | Federate |
mock |
The "REAL?" column is verified against the native module implementations — see custom-objects.md for the honest-mock rationale per module.
Each Step knows its own module name through Step::module_kind() (orchestration.rs:85) — that string is the registry key the interpreter dispatches on.
BootBackend — the boot/provision target (4 variants)
pub enum BootBackend { Redfish, Kvm, Oci, Terraform } // orchestration.rs:49
module_kind() maps each variant to a distinct boot module name:
BootBackend |
Module kind | Status |
Kvm |
KvmBoot |
REAL — wraps unfold::kvm::KvmTarget, builds the tested qemu argv |
Redfish |
RedfishBoot |
honest mock — needs the redfish feature + a reachable BMC |
Oci |
OciDeploy |
honest mock — needs draupnir's OCI engine (pack-oci/msi-oci) |
Terraform |
TerraformApply |
honest mock — needs the terraform feature + the tf binary |
Deserialization — how a .ron becomes a Flow
Two entry points, both in orchestration.rs:
Flow::from_ron(src: &str)(:112)Flow::from_ron_file(path: &Path)(:126)
from_ron accepts two surface forms and tries them in order:
- Bare list (canonical, no-code):
[ BootOS(...), InstallApp(...) ]—ron::from_str::<Vec<Step>>(src). - Explicit wrapper (fallback):
Flow(steps: [ ... ])—ron::from_str::<Flow>(src).
If both fail, the error names both parse failures. Because Step is a serde enum with struct variants, RON writes them as Variant(field: value, …) — e.g. BootOS(backend: Kvm, os: "ubuntu-24.04"). BootBackend variants are bare identifiers (Kvm, Redfish, …). Configure's params is a RON map: {"KEY": "value"}.
There is no CLI runner and no auto-discovery of
.ronfiles. Flows are executed through the library API (run_flow/run_ron) — the example atexamples/korp-provision.ronis illustrative data, not wired to a binary. NOT YET IMPLEMENTED: askidbladnir run <flow.ron>command.
Annotated end-to-end example
The real, tested scenario (mirrors examples/korp-provision.ron and the ron_drives_infra_then_installs_the_app test, orchestration.rs:727):
// A declarative provisioning flow: boot infra → seed → configure → install the app.
// Run it with skidbladnir::orchestration::run_flow(&flow, ®istry, &mut ctx).
[
// 1. INFRA — boot ubuntu on a local KVM VM. Routes to KvmBoot (REAL: builds the
// qemu argv from unfold::kvm::KvmTarget; the actual spawn is gated on `boot`).
BootOS(backend: Kvm, os: "ubuntu-24.04"),
// 2. INFRA — first-boot cloud-init config. Routes to Seed (records + validates the
// NoCloud config; authoring the CIDATA image needs draupnir/seed → honest mock).
Seed(config: "#cloud-config\npackages: [curl, ca-certificates]\n"),
// 3. INFRA — set a target param. Routes to Configure (records params → honest mock;
// real needs the target-specific driver).
Configure(target: "korp", params: {"KORP_DATA": "/var/lib/korp"}),
// 4. APP — install the application. Routes to InstallApp (REAL: reuses the
// install::install_product lifecycle when a bundle is registered, else renders
// the real ServiceSpec unit).
InstallApp(spec: "korp"),
]
Executed, this produces (verified by the test at orchestration.rs:727): a RunContext.trace of ["KvmBoot", "Seed", "Configure", "InstallApp"], a real qemu argv in outcome 0's data, and — with a real korp bundle — a full install: software/data/log tree, minted identity, rendered unit, and ordered enable/start verbs.
Serialization / round-trip
Flow and Step derive Serialize too, so a flow round-trips through serde (RON in, JSON out, RON out). The ron_wrapper_form_and_roundtrip test (orchestration.rs:715) asserts the JSON round-trip. RunContext, Dispatch, and StepOutcome are not serde types — they are inspected in-process (the inject-assert law: a test asserts exactly what each step did from the plain-data StepOutcome).
Verified against implementation
Everything above was checked field-by-field against src/orchestration.rs at nornir 78deafc. Gaps that the original design capture describes but the code does not yet implement are called out explicitly here and in custom-objects.md:
- NOT YET IMPLEMENTED: a CLI runner (
skidbladnir run flow.ron). - NOT YET IMPLEMENTED: znippy-packaged modules and the WASM module-kind (the design's Phase-2 — Phase 1 is native, in-tree only).
- The design lists a
TalosConfigmodule kind; the code ships a genericConfigurestep/module instead (honest mock).