diff --git a/crates/bmc-mock/src/bmc_state.rs b/crates/bmc-mock/src/bmc_state.rs index e35c7d86d4..7861b1c680 100644 --- a/crates/bmc-mock/src/bmc_state.rs +++ b/crates/bmc-mock/src/bmc_state.rs @@ -52,6 +52,7 @@ impl BmcState { match event { BmcEvent::PowerOn => { self.complete_all_bios_jobs(); + self.apply_pending_bluefield_mode(); } BmcEvent::BootCompleted => { self.system_state.on_boot_completed(); @@ -64,4 +65,13 @@ impl BmcState { v.complete_all_bios_jobs() } } + + /// Apply a BlueField's queued `Mode.Set` (the BF-3 OEM DPU/NIC mode flip), + /// if any. Real hardware picks up the staged mode only after a power cycle, + /// so this runs on `PowerOn`. + fn apply_pending_bluefield_mode(&self) { + if let redfish::oem::State::NvidiaBluefield(v) = &self.oem_state { + v.apply_pending_mode(); + } + } } diff --git a/crates/bmc-mock/src/http.rs b/crates/bmc-mock/src/http.rs index 65f1f6f852..d5b41efd35 100644 --- a/crates/bmc-mock/src/http.rs +++ b/crates/bmc-mock/src/http.rs @@ -28,6 +28,10 @@ pub(crate) fn not_found() -> Response { json!("").into_response(StatusCode::NOT_FOUND) } +pub(crate) fn bad_request(message: &str) -> Response { + json!({ "error": message }).into_response(StatusCode::BAD_REQUEST) +} + pub(crate) fn ok_no_content() -> Response { StatusCode::NO_CONTENT.into_response() } diff --git a/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs b/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs index 865b363c04..3dd5fd4239 100644 --- a/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs +++ b/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs @@ -16,6 +16,7 @@ */ use std::borrow::Cow; +use std::sync::{Arc, Mutex}; use axum::Router; use axum::extract::State; @@ -30,13 +31,46 @@ use crate::{http, redfish}; #[derive(Clone)] pub struct BluefieldState { - nic_mode: bool, + mode: Arc>, base_mac: MacAddress, } +struct ModeState { + nic_mode: bool, + /// A `Mode.Set` queues the requested mode here. A real BlueField applies it + /// only after the host power-cycles, so it lands on `nic_mode` on the next + /// `PowerOn` event (see `BmcState::on_event`), not immediately. + pending_nic_mode: Option, +} + impl BluefieldState { pub fn new(nic_mode: bool, base_mac: MacAddress) -> Self { - Self { nic_mode, base_mac } + Self { + mode: Arc::new(Mutex::new(ModeState { + nic_mode, + pending_nic_mode: None, + })), + base_mac, + } + } + + /// Whether the BlueField currently reports NIC mode. + fn nic_mode(&self) -> bool { + self.mode.lock().unwrap().nic_mode + } + + /// Queue a `Mode.Set`; it takes effect on the next power cycle. + fn stage_mode(&self, nic_mode: bool) { + self.mode.lock().unwrap().pending_nic_mode = Some(nic_mode); + } + + /// Apply a queued `Mode.Set`, if any -- called on power-on, the point at + /// which a real BlueField picks up a staged mode change. + pub fn apply_pending_mode(&self) { + let mut mode = self.mode.lock().unwrap(); + if let Some(pending) = mode.pending_nic_mode.take() { + mode.nic_mode = pending; + } } } @@ -59,6 +93,12 @@ pub fn add_routes(r: Router) -> Router { &format!("{}/Actions/HostRshim.Set", resource().odata_id), post(hostrshim_set), ) + .route( + // BF-3 OEM mode flip. Staged here and applied on the next power + // cycle, the same as real hardware. + &format!("{}/Actions/Mode.Set", resource().odata_id), + post(mode_set), + ) .route( "/redfish/v1/Managers/Bluefield_BMC/Oem/Nvidia", patch(patch_managers_oem_nvidia), @@ -73,7 +113,11 @@ async fn get_oem_nvidia(State(state): State) -> Response { let redfish::oem::State::NvidiaBluefield(state) = state.oem_state else { return http::not_found(); }; - let mode = if state.nic_mode { "NicMode" } else { "DpuMode" }; + let mode = if state.nic_mode() { + "NicMode" + } else { + "DpuMode" + }; resource() .json_patch() .patch(json!({ @@ -88,3 +132,84 @@ async fn patch_managers_oem_nvidia() -> Response { // This is used by enable_rshim_bmc() of libredfish client. json!({}).into_ok_response() } + +/// BF-3 OEM `Mode.Set`: queue a DPU/NIC mode flip. Like real hardware, the +/// change is staged and only takes effect on the next power cycle (applied in +/// `BmcState::on_event` on `PowerOn`), so a read-back before then still shows +/// the old mode. +async fn mode_set( + State(state): State, + axum::Json(body): axum::Json, +) -> Response { + let redfish::oem::State::NvidiaBluefield(bluefield) = state.oem_state else { + return http::not_found(); + }; + let Some(nic_mode) = parse_requested_mode(&body) else { + return http::bad_request("Mode.Set requires a `Mode` of `NicMode` or `DpuMode`"); + }; + bluefield.stage_mode(nic_mode); + // No response payload -- 204, per Redfish for an action with nothing to return. + http::ok_no_content() +} + +/// Parse a `Mode.Set` body into the requested NIC-mode flag, validating +/// strictly: `Some(true)` for `NicMode`, `Some(false)` for `DpuMode`, `None` +/// for a missing or unrecognized value. A real BF-3 rejects those, and a strict +/// mock turns a drifted client payload into a loud failure rather than a +/// silently wrong flip. +fn parse_requested_mode(body: &serde_json::Value) -> Option { + match body.get("Mode").and_then(|mode| mode.as_str()) { + Some(mode) if mode.eq_ignore_ascii_case("NicMode") => Some(true), + Some(mode) if mode.eq_ignore_ascii_case("DpuMode") => Some(false), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mode_set_is_staged_and_applied_on_power_on() { + // Starts in DPU mode. + let bf = BluefieldState::new(false, MacAddress::new([0, 0, 0, 0, 0, 1])); + assert!(!bf.nic_mode()); + + // A `Mode.Set` to NIC mode is staged, not applied immediately -- a + // read-back still reports DPU mode, like a real BF-3 before its power + // cycle. + bf.stage_mode(true); + assert!(!bf.nic_mode()); + + // The next power-on applies the staged mode. + bf.apply_pending_mode(); + assert!(bf.nic_mode()); + + // A power-on with nothing staged leaves the mode untouched. + bf.apply_pending_mode(); + assert!(bf.nic_mode()); + } + + #[test] + fn parse_requested_mode_validates_strictly() { + assert_eq!( + parse_requested_mode(&serde_json::json!({ "Mode": "NicMode" })), + Some(true) + ); + assert_eq!( + parse_requested_mode(&serde_json::json!({ "Mode": "DpuMode" })), + Some(false) + ); + // Case-insensitive, matching the handler. + assert_eq!( + parse_requested_mode(&serde_json::json!({ "Mode": "nicmode" })), + Some(true) + ); + // Missing or unrecognized -> rejected (the handler returns 400). + assert_eq!(parse_requested_mode(&serde_json::json!({})), None); + assert_eq!( + parse_requested_mode(&serde_json::json!({ "Mode": "bogus" })), + None + ); + } +}