draupnir 0.1.9

Draupnir — the nordisk boot/provisioning library: fire up a runtime from one BootSpec across three backends (KVM via tunnr · OCI container · Redfish bare-metal virtual-media) and drive its power lifecycle. Odin's ring that drips eight identical copies → boot a fleet of identical machines from one ISO.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
//! **THE LOOP — a real ISO booted THROUGH Redfish, on a real machine.**
//!
//! `tests/redfish_server_conformance.rs` proves the *wire*: every response checked
//! against DMTF's own schema, registry and mockup. This file proves the *machine*:
//! the same `BootSpec` that [`BootSpec::iso_boot`] builds, routed with
//! [`on_metal`](BootSpec::on_metal) through draupnir's Redfish **client**, into
//! draupnir's Redfish **server**, out the other side as a QEMU/KVM guest — which
//! really boots the ISO and says so on its serial console.
//!
//! ```text
//!   BootSpec::iso_boot(..).on_metal(bmc)
//!        └─ RedfishBoot (client, cert PINNED)
//!             ├─ POST  VirtualMedia.InsertMedia     → BootSpec::medium
//!             ├─ PATCH Boot.BootSourceOverrideTarget → BootOrder::Medium
//!             └─ POST  ComputerSystem.Reset {On}    → KvmBoot::boot
//!                                                       └─ QEMU + OVMF + /dev/kvm
//!                                                            └─ ttyS0 says so
//! ```
//!
//! # The form is `iso-redfish-sim`, NEVER `iso-metal`
//!
//! **An emulator proves the wire, never the hardware.** There is no BMC on this box.
//! Recording this green in the cell a real-hardware green would occupy is exactly the
//! false green the form split exists to prevent, so every matrix row this file emits
//! is under `draupnir/iso-redfish-sim`. `iso-metal` stays Absent, visibly, on its own
//! row, until an actual iDRAC/iLO is burned.
//!
//! # The RED that matters
//!
//! A server can answer `204 No Content` to a boot-override PATCH and then boot
//! exactly what it would have booted anyway. Every status code in the conformance
//! suite would still be green. So the last test here flips the override to `Hdd`
//! **with the ISO still in the tray**, boots again for real, and asserts that neither
//! the firmware's DVD-ROM line nor the appliance banner appears — measured on a
//! genuinely blank disk, which is a sabotage that cannot be reverted away.
//!
//! # Running it
//!
//! ```text
//! DRAUPNIR_VM_BOOT_TIMEOUT=200 cargo test \
//!   --features redfish-server,backend-redfish,backend-tunnr \
//!   --test redfish_kvm_loop_proof -- --ignored --nocapture
//! ```
//!
//! `#[ignore]` because it needs `/dev/kvm`, `qemu-system-x86_64`, OVMF and a real
//! ISO. When a prerequisite is missing it **skips LOUDLY, by name**, and never
//! returns a green that stands for a boot that did not happen.
#![cfg(all(
    feature = "redfish-server",
    feature = "backend-redfish",
    feature = "backend-tunnr"
))]

use std::path::{Path, PathBuf};
use std::time::Duration;

use draupnir::redfish::wire;
use draupnir::redfish::RedfishBoot;
use draupnir::redfish_server::{NodeConfig, RedfishKvmServer};
use draupnir::{Boot, BootOrder, BootSpec, BootTarget, Lifecycle, PowerState, Seen, VirtualMedia};

/// Assert on the real value AND record it as a matrix row under the
/// **`iso-redfish-sim`** form. Same `assert_emit!` doctrine as
/// `tests/testmatrix_rows.rs`: the assert is the gate, the emit is the observation.
macro_rules! assert_emit {
    ($check:expr, $ok:expr, $($detail:tt)+) => {{
        let ok: bool = $ok;
        let detail = format!($($detail)+);
        draupnir::functional_status("draupnir/iso-redfish-sim", $check, ok, &detail);
        println!("ROW iso-redfish-sim/{:<28} {:<5} {}", $check, if ok { "GREEN" } else { "RED" }, detail);
        assert!(ok, "iso-redfish-sim::{} — {}", $check, detail);
    }};
}

/// The ISO this proof burns. Overridable: the appliance ISO's path is a property of
/// the box, not of the test.
fn proof_iso() -> String {
    std::env::var("PROOF_ISO")
        .unwrap_or_else(|_| "/home/rickard/Hämtningar/gunnar-appliance-x86_64.iso".to_string())
}

/// The console substring that means **the firmware executed code off the medium** —
/// UEFI naming the CD/DVD device it chose. Generic to QEMU: any bootable ISO produces
/// it and a disk boot never does.
const MEDIUM_MARKER: &str = "UEFI QEMU DVD-ROM";

/// The console substring that means **the system off the medium reached userspace and
/// is serving**. Booting firmware is not the claim; a running appliance is.
fn proof_marker() -> String {
    std::env::var("PROOF_MARKER").unwrap_or_else(|_| "gunnar ssh listener bound".to_string())
}

/// A scratch directory for the blank disk. Deliberately **not** `std::env::temp_dir()`
/// — `/tmp` is a tmpfs on this box, and a 256 MiB raw disk there is 256 MiB of RAM.
fn scratch_dir() -> PathBuf {
    let dir = std::env::var("REDFISH_PROOF_SCRATCH").map(PathBuf::from).unwrap_or_else(|_| {
        PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".into()))
            .join("scratch/draupnir-redfish")
    });
    let _ = std::fs::create_dir_all(&dir);
    dir
}

/// Report a missing prerequisite by name and return `true` (skip). Never silent.
fn missing(what: &str, path: &str) -> bool {
    eprintln!(
        "SKIP redfish_kvm_loop_proof: {what} not present at `{path}` — this proof did NOT run"
    );
    true
}

fn prerequisites_absent(iso: &str) -> bool {
    if !Path::new("/dev/kvm").exists() {
        return missing("/dev/kvm", "/dev/kvm");
    }
    if !Path::new(iso).is_file() {
        return missing("the boot medium (ISO)", iso);
    }
    false
}

const BMC_USER: &str = "redfish-admin";
const BMC_PASS: &str = "loop-proof-secret";

/// A genuinely blank raw disk — the thing an `Hdd` override boots, and the thing that
/// makes the negative leg real. Created with `std::fs`, no `qemu-img` subprocess.
fn blank_disk(tag: &str) -> PathBuf {
    let p = scratch_dir().join(format!("blank-{tag}-{}.raw", std::process::id()));
    let f = std::fs::File::create(&p).expect("create the blank disk");
    f.set_len(256 * 1024 * 1024).expect("size the blank disk");
    drop(f);
    p
}

/// Start the BMC in front of this box's KVM, with a blank local disk as the thing an
/// `Hdd` override would boot.
fn start_bmc(disk: &Path) -> RedfishKvmServer {
    RedfishKvmServer::start(
        NodeConfig::new("System.Embedded.1")
            .credentials(BMC_USER, BMC_PASS)
            .local_disk(disk.to_string_lossy())
            .sized(1024, 2),
    )
    .expect("draupnir's BMC starts")
}

/// The client, with the server's cert PINNED — full TLS verification stays on and
/// that one cert is the sole trusted root. This is the posture draupnir uses against
/// a real BMC, and it is why the server had to grow a real TLS identity at all.
fn pinned_client(server: &RedfishKvmServer) -> RedfishBoot {
    RedfishBoot::new()
        .with_password(BMC_PASS)
        .pin_cert_pem(server.cert_pem().as_bytes().to_vec())
}

fn dump_console(label: &str, console: &str) {
    println!("---------------- {label} ({} bytes) ----------------", console.len());
    println!("{console}");
    println!("---------------- END {label} ----------------");
}

// ===========================================================================
// THE LOOP
// ===========================================================================

/// **Boot a real ISO on a real machine, driven entirely over Redfish.**
#[test]
#[ignore = "REAL BOOT: needs /dev/kvm, qemu-system-x86_64, OVMF and an ISO on disk"]
fn an_iso_booted_through_the_redfish_loop_reaches_its_serving_marker() {
    let iso = proof_iso();
    if prerequisites_absent(&iso) {
        return;
    }
    let disk = blank_disk("loop");
    let server = start_bmc(&disk);
    let bmc = server.bmc_endpoint();
    println!("BMC: {} system={}", server.base_url(), bmc.system_id);

    // ONE spec — the SAME value `iso_boot` builds for a local KVM boot, routed to
    // "metal". Nothing about QEMU, nothing about this server: honesty rule 2, so that
    // "it works in the demo" and "it works on the customer's BMC" are the same claim
    // about the same value.
    let spec = BootSpec::iso_boot("iso-redfish-sim", &iso).on_metal(bmc.clone());
    spec.validate().expect("the medium spec is well formed");
    assert_emit!(
        "one-spec",
        spec.boot_order == BootOrder::Medium && spec.medium_path() == Some(iso.as_str()),
        "the spec routed to Redfish still says boot off `{iso}` (order {:?})",
        spec.boot_order
    );

    // The whole burn, through draupnir's own Redfish CLIENT: InsertMedia, the
    // one-time boot override, ComputerSystem.Reset {On}.
    let client = pinned_client(&server);
    let machine = client
        .boot(&spec)
        .expect("the Redfish client drives the burn against draupnir's BMC");
    println!("BURNED via Redfish: machine id={}", machine.id);

    // The BMC really mounted THIS ISO, and really translated the override.
    assert_emit!(
        "insert-media",
        server.inserted_image().as_deref() == Some(iso.as_str()),
        "VirtualMedia.Image == `{:?}`",
        server.inserted_image()
    );
    let applied = server
        .last_boot_spec()
        .expect("the BMC handed its backend a spec");
    assert_emit!(
        "override-applied",
        applied.boot_order == BootOrder::Medium && applied.medium_path() == Some(iso.as_str()),
        "the BMC booted order={:?} medium={:?}",
        applied.boot_order,
        applied.medium_path()
    );

    // ── the machine ──────────────────────────────────────────────────────────
    // Half 1: the FIRMWARE really chose the CD. Any bootable ISO prints this and a
    // disk boot never does.
    let firmware = server
        .await_serial_marker(MEDIUM_MARKER, Duration::from_secs(60), Duration::from_millis(250))
        .expect("the guest the BMC booted is addressable");
    // Half 2: the system that came off it reached USERSPACE and is serving.
    let marker = proof_marker();
    let serving = server
        .await_serial_marker(&marker, Duration::from_secs(120), Duration::from_millis(250))
        .expect("the guest is still addressable");

    // A thing that keeps running is the NORMAL case: observe it is STILL up, and
    // never wait for it to exit — read the power state back OVER REDFISH, through the
    // client's Lifecycle, so the readback is part of the loop too.
    let bound = RedfishBoot::for_node(bmc.clone(), BMC_PASS)
        .pin_cert_pem(server.cert_pem().as_bytes().to_vec());
    let power = bound.status(&machine);

    let console = server.serial_log().unwrap_or_default();
    dump_console("SERIAL CONSOLE (redfish loop)", &console);
    println!("FIRMWARE: {}", firmware.detail());
    println!("SERVING : {}", serving.detail());
    println!("POWER   : {power:?} (read back over Redfish)");

    // Tear the machine down BEFORE asserting, so a failed assertion never leaks a
    // QEMU process — and do it over Redfish (ComputerSystem.Reset {ForceOff}).
    let _ = bound.power_off(&machine);

    assert_emit!(
        "firmware-off-medium",
        firmware.saw_marker(),
        "{}",
        firmware.detail()
    );
    assert_emit!(
        "appliance-serving",
        serving.saw_marker(),
        "{}",
        serving.detail()
    );
    if let Seen::Marker { line, after } = &serving {
        println!("PROOF: booted off the medium THROUGH REDFISH and served in {after:?}");
        println!("PROOF: `{}`", line.trim());
    }
    assert_emit!(
        "power-readback",
        power.as_ref() == Ok(&PowerState::On),
        "ComputerSystem.PowerState read over Redfish = {power:?}"
    );
    // A real capture, not an empty string that trivially contains nothing — an empty
    // console would make every negative assertion vacuous.
    assert_emit!(
        "console-non-vacuous",
        console.len() > 4096,
        "{} bytes of real serial console",
        console.len()
    );
    let _ = std::fs::remove_file(&disk);
}

/// **RED — the boot override is APPLIED OUTPUT, not a `204` on the wire.**
///
/// The positive proof above is worth exactly what this is worth. Here the ISO stays
/// **in the tray** — `VirtualMedia.Inserted` is still `true` throughout — and only the
/// `BootSourceOverrideTarget` changes, from `Cd` to `Hdd`. If the server merely
/// acknowledged the PATCH and booted what it always boots, the appliance would come
/// up and this would fail.
///
/// The disk it boots instead is genuinely blank, which is a sabotage that cannot be
/// reverted away: there is nothing on it to run.
#[test]
#[ignore = "REAL BOOT: needs /dev/kvm, qemu-system-x86_64 and OVMF"]
fn an_hdd_override_really_stops_the_machine_booting_the_medium_in_its_tray() {
    let iso = proof_iso();
    if prerequisites_absent(&iso) {
        return;
    }
    let disk = blank_disk("hdd-red");
    let server = start_bmc(&disk);
    let bmc = server.bmc_endpoint();
    let client = pinned_client(&server);

    // 1. Insert the SAME appliance ISO that boots green above.
    client
        .insert_media(&bmc, &iso)
        .expect("VirtualMedia.InsertMedia");
    assert_emit!(
        "red-tray-loaded",
        server.inserted_image().as_deref() == Some(iso.as_str()),
        "the appliance ISO is in the tray: {:?}",
        server.inserted_image()
    );

    // 2. Override to Hdd — the ONLY thing that differs from the green leg.
    client
        .set_boot_override(&bmc, BootTarget::Hdd)
        .expect("the Boot override PATCH is accepted");

    // 3. Power on over Redfish.
    let bound = RedfishBoot::for_node(bmc.clone(), BMC_PASS)
        .pin_cert_pem(server.cert_pem().as_bytes().to_vec());
    let machine = draupnir::Machine {
        id: bmc.system_id.clone(),
        spec_name: "iso-redfish-sim-red".into(),
        backend: draupnir::Backend::Redfish,
        power: PowerState::Unknown,
    };
    bound
        .power_on(&machine)
        .expect("ComputerSystem.Reset {On} is accepted");

    // 4. OBSERVE EVERYTHING FIRST, then assert — so a sabotaged server is caught at
    //    BOTH levels (the spec it chose AND what the machine did) instead of the
    //    cheaper spec check hiding the console evidence behind an early panic.
    let applied = server.last_boot_spec().expect("the BMC booted something");
    let tray_still_loaded = server.inserted_image();
    // Wait on a deadline, never on exit.
    let seen = server
        .await_serial_marker(&proof_marker(), Duration::from_secs(30), Duration::from_millis(500))
        .expect("the guest is addressable");
    let console = server.serial_log().unwrap_or_default();
    dump_console("BLANK-DISK CONSOLE (hdd override)", &console);
    println!("SEEN: {}", seen.detail());
    let _ = server.machine().map(|m| bound.power_off(&m));

    // The spec the BMC actually handed KVM: Disk order, and NO medium.
    assert_emit!(
        "red-spec-detached",
        applied.boot_order == BootOrder::Disk && applied.medium_path().is_none(),
        "the Hdd override produced order={:?} medium={:?}",
        applied.boot_order,
        applied.medium_path()
    );
    // ...while the tray is STILL loaded. It is the boot that stopped using it.
    assert_emit!(
        "red-tray-still-loaded",
        tray_still_loaded.as_deref() == Some(iso.as_str()),
        "VirtualMedia.Inserted stayed true: {tray_still_loaded:?}"
    );

    // The detail reads truthfully in BOTH directions: a green row says what was
    // absent, a red row says what appeared. A row whose detail only made sense when
    // it failed would be a row nobody could read when it passed.
    assert_emit!(
        "red-no-appliance",
        !seen.saw_marker(),
        "with the Hdd override the appliance marker `{}` {}",
        proof_marker(),
        if seen.saw_marker() {
            format!("APPEARED — the override was not applied and the green leg proves nothing: {}", seen.detail())
        } else {
            format!("never appeared: {}", seen.detail())
        }
    );
    assert_emit!(
        "red-no-medium-line",
        !console.contains(MEDIUM_MARKER),
        "the firmware {} name `{MEDIUM_MARKER}` — the medium {} detached by the override",
        if console.contains(MEDIUM_MARKER) { "STILL did" } else { "never did" },
        if console.contains(MEDIUM_MARKER) { "was NOT" } else { "was" }
    );
    assert_emit!(
        "red-no-banner",
        !console.contains("GUNNAR APPLIANCE"),
        "the blank disk {} the appliance banner",
        if console.contains("GUNNAR APPLIANCE") { "PRINTED" } else { "never printed" }
    );
    let _ = std::fs::remove_file(&disk);
}

/// **RED — a `ResetType` the BMC does not advertise is refused BY NAME, and no
/// machine starts.** Needs no KVM: the refusal happens before any backend is touched,
/// and that is the point.
#[test]
fn a_bad_reset_type_is_refused_by_name_and_boots_no_machine() {
    let server = RedfishKvmServer::start(
        NodeConfig::new("System.Embedded.1").credentials(BMC_USER, BMC_PASS),
    )
    .expect("the BMC starts");
    let agent = pinned_agent(&server);
    let url = format!("{}{}", server.base_url(), wire::reset_path("System.Embedded.1"));
    let resp = agent
        .post(&url)
        .header("Authorization", &wire::basic_auth_header(BMC_USER, BMC_PASS))
        .send_json(wire::reset_body("Reboot"))
        .expect("the BMC answers");
    let status = resp.status().as_u16();
    let body: serde_json::Value = serde_json::from_str(
        &resp.into_body().read_to_string().unwrap_or_default(),
    )
    .unwrap_or(serde_json::Value::Null);

    assert_emit!(
        "red-bad-reset-type",
        status == 400
            && body["error"]["code"] == "Base.1.19.0.ActionParameterValueNotInList"
            && body["error"]["message"].as_str().unwrap_or_default().contains("'Reboot'"),
        "HTTP {status} {}",
        body["error"]["message"]
    );
    assert_emit!(
        "red-nothing-booted",
        server.machine().is_none() && server.last_boot_spec().is_none(),
        "no machine was started by the refused reset"
    );
}

/// **RED — `InsertMedia` naming an image that is not there is refused BY NAME**,
/// before anything is mounted. Needs no KVM.
#[test]
fn insert_media_for_an_iso_nobody_built_is_refused_by_name() {
    let server = RedfishKvmServer::start(
        NodeConfig::new("System.Embedded.1").credentials(BMC_USER, BMC_PASS),
    )
    .expect("the BMC starts");
    let ghost = "/nonexistent/never-built-by-anyone.iso";
    let err = pinned_client(&server)
        .insert_media(&server.bmc_endpoint(), ghost)
        .expect_err("an ISO that is not on disk cannot be inserted");
    let msg = err.to_string();
    assert_emit!(
        "red-ghost-media",
        msg.contains("400"),
        "the client saw the BMC's refusal: {msg}"
    );
    assert_emit!(
        "red-tray-empty",
        server.inserted_image().is_none(),
        "nothing was mounted by the refused insert"
    );
}

/// A `ureq` agent pinning the server's cert — used only where a raw status code is
/// the subject (draupnir's client maps a 4xx onto an opaque `Error::Backend`).
fn pinned_agent(server: &RedfishKvmServer) -> ureq::Agent {
    use ureq::tls::{Certificate, RootCerts, TlsConfig};
    let cert = Certificate::from_pem(server.cert_pem().as_bytes()).expect("a well-formed PEM");
    ureq::config::Config::builder()
        .tls_config(TlsConfig::builder().root_certs(RootCerts::from([cert])).build())
        .http_status_as_error(false)
        .build()
        .into()
}