Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/bmc-mock/src/bmc_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
}
}
}
4 changes: 4 additions & 0 deletions crates/bmc-mock/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
131 changes: 128 additions & 3 deletions crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

use std::borrow::Cow;
use std::sync::{Arc, Mutex};

use axum::Router;
use axum::extract::State;
Expand All @@ -30,13 +31,46 @@ use crate::{http, redfish};

#[derive(Clone)]
pub struct BluefieldState {
nic_mode: bool,
mode: Arc<Mutex<ModeState>>,
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<bool>,
}

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;
}
}
}

Expand All @@ -59,6 +93,12 @@ pub fn add_routes(r: Router<BmcState>) -> Router<BmcState> {
&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),
Expand All @@ -73,7 +113,11 @@ async fn get_oem_nvidia(State(state): State<BmcState>) -> 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!({
Expand All @@ -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<BmcState>,
axum::Json(body): axum::Json<serde_json::Value>,
) -> 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<bool> {
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
);
}
}
Loading