Skip to content

Commit f8e78ad

Browse files
committed
feat(bmc-mock): model the BlueField DPU/NIC mode flip
The Bluefield OEM endpoint now accepts the BF-3 `Mode.Set` action and applies it the way real hardware does: the requested DPU/NIC mode is staged and only takes effect on the next power cycle, so a read-back before then still reports the old mode. The staged mode is applied on the `PowerOn` event, alongside the existing Dell BIOS-job completion. Until now the mock could report a Bluefield's mode but not change it, so a test could only stand up a host whose DPU was already in NIC mode -- never watch one get flipped through the same `set_nic_mode` + power-cycle path site-explorer drives. This is the harness piece the end-to-end DPU-to-NIC re-registration test (#2632) builds on. Adds a smoke test: a Bluefield starting in DPU mode reports NIC mode only after a staged `Mode.Set` followed by a power-on, and an unstaged power-on leaves the mode untouched. Signed-off-by: Chet Nichols III <chetn@nvidia.com>
1 parent 47e42bc commit f8e78ad

3 files changed

Lines changed: 142 additions & 3 deletions

File tree

crates/bmc-mock/src/bmc_state.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ impl BmcState {
5252
match event {
5353
BmcEvent::PowerOn => {
5454
self.complete_all_bios_jobs();
55+
self.apply_pending_bluefield_mode();
5556
}
5657
BmcEvent::BootCompleted => {
5758
self.system_state.on_boot_completed();
@@ -64,4 +65,13 @@ impl BmcState {
6465
v.complete_all_bios_jobs()
6566
}
6667
}
68+
69+
/// Apply a BlueField's queued `Mode.Set` (the BF-3 OEM DPU/NIC mode flip),
70+
/// if any. Real hardware picks up the staged mode only after a power cycle,
71+
/// so this runs on `PowerOn`.
72+
fn apply_pending_bluefield_mode(&self) {
73+
if let redfish::oem::State::NvidiaBluefield(v) = &self.oem_state {
74+
v.apply_pending_mode();
75+
}
76+
}
6777
}

crates/bmc-mock/src/http.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ pub(crate) fn not_found() -> Response {
2828
json!("").into_response(StatusCode::NOT_FOUND)
2929
}
3030

31+
pub(crate) fn bad_request(message: &str) -> Response {
32+
json!({ "error": message }).into_response(StatusCode::BAD_REQUEST)
33+
}
34+
3135
pub(crate) fn ok_no_content() -> Response {
3236
StatusCode::NO_CONTENT.into_response()
3337
}

crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
use std::borrow::Cow;
19+
use std::sync::{Arc, Mutex};
1920

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

3132
#[derive(Clone)]
3233
pub struct BluefieldState {
33-
nic_mode: bool,
34+
mode: Arc<Mutex<ModeState>>,
3435
base_mac: MacAddress,
3536
}
3637

38+
struct ModeState {
39+
nic_mode: bool,
40+
/// A `Mode.Set` queues the requested mode here. A real BlueField applies it
41+
/// only after the host power-cycles, so it lands on `nic_mode` on the next
42+
/// `PowerOn` event (see `BmcState::on_event`), not immediately.
43+
pending_nic_mode: Option<bool>,
44+
}
45+
3746
impl BluefieldState {
3847
pub fn new(nic_mode: bool, base_mac: MacAddress) -> Self {
39-
Self { nic_mode, base_mac }
48+
Self {
49+
mode: Arc::new(Mutex::new(ModeState {
50+
nic_mode,
51+
pending_nic_mode: None,
52+
})),
53+
base_mac,
54+
}
55+
}
56+
57+
/// Whether the BlueField currently reports NIC mode.
58+
fn nic_mode(&self) -> bool {
59+
self.mode.lock().unwrap().nic_mode
60+
}
61+
62+
/// Queue a `Mode.Set`; it takes effect on the next power cycle.
63+
fn stage_mode(&self, nic_mode: bool) {
64+
self.mode.lock().unwrap().pending_nic_mode = Some(nic_mode);
65+
}
66+
67+
/// Apply a queued `Mode.Set`, if any -- called on power-on, the point at
68+
/// which a real BlueField picks up a staged mode change.
69+
pub fn apply_pending_mode(&self) {
70+
let mut mode = self.mode.lock().unwrap();
71+
if let Some(pending) = mode.pending_nic_mode.take() {
72+
mode.nic_mode = pending;
73+
}
4074
}
4175
}
4276

@@ -59,6 +93,12 @@ pub fn add_routes(r: Router<BmcState>) -> Router<BmcState> {
5993
&format!("{}/Actions/HostRshim.Set", resource().odata_id),
6094
post(hostrshim_set),
6195
)
96+
.route(
97+
// BF-3 OEM mode flip. Staged here and applied on the next power
98+
// cycle, the same as real hardware.
99+
&format!("{}/Actions/Mode.Set", resource().odata_id),
100+
post(mode_set),
101+
)
62102
.route(
63103
"/redfish/v1/Managers/Bluefield_BMC/Oem/Nvidia",
64104
patch(patch_managers_oem_nvidia),
@@ -73,7 +113,11 @@ async fn get_oem_nvidia(State(state): State<BmcState>) -> Response {
73113
let redfish::oem::State::NvidiaBluefield(state) = state.oem_state else {
74114
return http::not_found();
75115
};
76-
let mode = if state.nic_mode { "NicMode" } else { "DpuMode" };
116+
let mode = if state.nic_mode() {
117+
"NicMode"
118+
} else {
119+
"DpuMode"
120+
};
77121
resource()
78122
.json_patch()
79123
.patch(json!({
@@ -88,3 +132,84 @@ async fn patch_managers_oem_nvidia() -> Response {
88132
// This is used by enable_rshim_bmc() of libredfish client.
89133
json!({}).into_ok_response()
90134
}
135+
136+
/// BF-3 OEM `Mode.Set`: queue a DPU/NIC mode flip. Like real hardware, the
137+
/// change is staged and only takes effect on the next power cycle (applied in
138+
/// `BmcState::on_event` on `PowerOn`), so a read-back before then still shows
139+
/// the old mode.
140+
async fn mode_set(
141+
State(state): State<BmcState>,
142+
axum::Json(body): axum::Json<serde_json::Value>,
143+
) -> Response {
144+
let redfish::oem::State::NvidiaBluefield(bluefield) = state.oem_state else {
145+
return http::not_found();
146+
};
147+
let Some(nic_mode) = parse_requested_mode(&body) else {
148+
return http::bad_request("Mode.Set requires a `Mode` of `NicMode` or `DpuMode`");
149+
};
150+
bluefield.stage_mode(nic_mode);
151+
// No response payload -- 204, per Redfish for an action with nothing to return.
152+
http::ok_no_content()
153+
}
154+
155+
/// Parse a `Mode.Set` body into the requested NIC-mode flag, validating
156+
/// strictly: `Some(true)` for `NicMode`, `Some(false)` for `DpuMode`, `None`
157+
/// for a missing or unrecognized value. A real BF-3 rejects those, and a strict
158+
/// mock turns a drifted client payload into a loud failure rather than a
159+
/// silently wrong flip.
160+
fn parse_requested_mode(body: &serde_json::Value) -> Option<bool> {
161+
match body.get("Mode").and_then(|mode| mode.as_str()) {
162+
Some(mode) if mode.eq_ignore_ascii_case("NicMode") => Some(true),
163+
Some(mode) if mode.eq_ignore_ascii_case("DpuMode") => Some(false),
164+
_ => None,
165+
}
166+
}
167+
168+
#[cfg(test)]
169+
mod tests {
170+
use super::*;
171+
172+
#[test]
173+
fn mode_set_is_staged_and_applied_on_power_on() {
174+
// Starts in DPU mode.
175+
let bf = BluefieldState::new(false, MacAddress::new([0, 0, 0, 0, 0, 1]));
176+
assert!(!bf.nic_mode());
177+
178+
// A `Mode.Set` to NIC mode is staged, not applied immediately -- a
179+
// read-back still reports DPU mode, like a real BF-3 before its power
180+
// cycle.
181+
bf.stage_mode(true);
182+
assert!(!bf.nic_mode());
183+
184+
// The next power-on applies the staged mode.
185+
bf.apply_pending_mode();
186+
assert!(bf.nic_mode());
187+
188+
// A power-on with nothing staged leaves the mode untouched.
189+
bf.apply_pending_mode();
190+
assert!(bf.nic_mode());
191+
}
192+
193+
#[test]
194+
fn parse_requested_mode_validates_strictly() {
195+
assert_eq!(
196+
parse_requested_mode(&serde_json::json!({ "Mode": "NicMode" })),
197+
Some(true)
198+
);
199+
assert_eq!(
200+
parse_requested_mode(&serde_json::json!({ "Mode": "DpuMode" })),
201+
Some(false)
202+
);
203+
// Case-insensitive, matching the handler.
204+
assert_eq!(
205+
parse_requested_mode(&serde_json::json!({ "Mode": "nicmode" })),
206+
Some(true)
207+
);
208+
// Missing or unrecognized -> rejected (the handler returns 400).
209+
assert_eq!(parse_requested_mode(&serde_json::json!({})), None);
210+
assert_eq!(
211+
parse_requested_mode(&serde_json::json!({ "Mode": "bogus" })),
212+
None
213+
);
214+
}
215+
}

0 commit comments

Comments
 (0)