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

nordisk/Skidbladnir

.nornir/ron-schema.md

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 commit 78deafc), verified field-by-field against the serde structs. The companion custom-objects.md covers the SteeringModule custom-code seam; the original design capture is ron-orchestration-design.md.

Skidbladnir has two ways to drive its imperative airgap core (pack · archive · fetch · unfold · install · start):

  1. Declarative RON — a .ron file describes an ordered list of steps; the run_flow interpreter executes it. No new Rust per scenario.
  2. Custom/imperative — call the Rust API or a SteeringModule directly. See custom-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:

from_ron accepts two surface forms and tries them in order:

  1. Bare list (canonical, no-code): [ BootOS(...), InstallApp(...) ] — ron::from_str::<Vec<Step>>(src).
  2. 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 .ron files. Flows are executed through the library API (run_flow / run_ron) — the example at examples/korp-provision.ron is illustrative data, not wired to a binary. NOT YET IMPLEMENTED: a skidbladnir 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, &registry, &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: