//! **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(|_| {
format!(
"{}/Hämtningar/gunnar-appliance-x86_64.iso",
std::env::var("HOME").unwrap_or_default()
)
})
}
/// 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(),
kind: draupnir::Kind::BareMetal,
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()
}