1616 */
1717
1818use std:: borrow:: Cow ;
19+ use std:: sync:: { Arc , Mutex } ;
1920
2021use axum:: Router ;
2122use axum:: extract:: State ;
@@ -30,13 +31,46 @@ use crate::{http, redfish};
3031
3132#[ derive( Clone ) ]
3233pub 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+
3746impl 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