#![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};
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);
}};
}
fn proof_iso() -> String {
std::env::var("PROOF_ISO")
.unwrap_or_else(|_| "/home/rickard/Hämtningar/gunnar-appliance-x86_64.iso".to_string())
}
const MEDIUM_MARKER: &str = "UEFI QEMU DVD-ROM";
fn proof_marker() -> String {
std::env::var("PROOF_MARKER").unwrap_or_else(|_| "gunnar ssh listener bound".to_string())
}
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
}
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";
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
}
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")
}
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} ----------------");
}
#[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);
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
);
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);
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()
);
let firmware = server
.await_serial_marker(MEDIUM_MARKER, Duration::from_secs(60), Duration::from_millis(250))
.expect("the guest the BMC booted is addressable");
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");
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)");
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:?}"
);
assert_emit!(
"console-non-vacuous",
console.len() > 4096,
"{} bytes of real serial console",
console.len()
);
let _ = std::fs::remove_file(&disk);
}
#[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);
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()
);
client
.set_boot_override(&bmc, BootTarget::Hdd)
.expect("the Boot override PATCH is accepted");
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");
let applied = server.last_boot_spec().expect("the BMC booted something");
let tray_still_loaded = server.inserted_image();
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));
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()
);
assert_emit!(
"red-tray-still-loaded",
tray_still_loaded.as_deref() == Some(iso.as_str()),
"VirtualMedia.Inserted stayed true: {tray_still_loaded:?}"
);
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);
}
#[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"
);
}
#[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"
);
}
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()
}