@@ -222,7 +222,13 @@ pub trait Redfish: Send + Sync + 'static {
222222
223223 /// Sets up a reasonable UEFI configuration.
224224 /// remember to call lockdown() afterwards to secure the server
225- /// - boot_interface_mac: MAC Address of the NIC you wish to boot from
225+ /// - boot_interface: identifies the NIC you wish to boot from. Either a
226+ /// `BootInterfaceRef::Mac` (existing behavior: vendor impl looks up
227+ /// the partition by MAC via its BMC enumeration) or a
228+ /// `BootInterfaceRef::InterfaceId` (vendor-native Redfish
229+ /// `EthernetInterface.Id` — used when we already know the interface
230+ /// partition ID (and don't need to look it up by MAC). One case
231+ /// being if we flip a DPU to NIC mode.
226232 /// If not given we look for a Mellanox Bluefield DPU and use that.
227233 /// Not applicable to Supermicro and the DPU itself.
228234 /// bios_profiles: Map of vendor/model (with spaces replaced by underscores)/profile/type
@@ -234,7 +240,7 @@ pub trait Redfish: Send + Sync + 'static {
234240 /// Ok(None) when no job is created. Caller should wait for job completion before configuring boot order.
235241 fn machine_setup < ' a > (
236242 & ' a self ,
237- boot_interface_mac : Option < & ' a str > ,
243+ boot_interface : Option < BootInterfaceRef < ' a > > ,
238244 bios_profiles : & ' a BiosProfileVendor ,
239245 selected_profile : BiosProfileType ,
240246 oem_manager_profiles : & ' a BiosProfileVendor ,
@@ -243,13 +249,13 @@ pub trait Redfish: Send + Sync + 'static {
243249 /// Is everything that machine_setup does already done?
244250 fn machine_setup_status < ' a > (
245251 & ' a self ,
246- boot_interface_mac : Option < & ' a str > ,
252+ boot_interface : Option < BootInterfaceRef < ' a > > ,
247253 ) -> RedfishFuture < ' a , Result < MachineSetupStatus , RedfishError > > ;
248254
249255 /// Check if only the BIOS/BMC setup is done
250256 fn is_bios_setup < ' a > (
251257 & ' a self ,
252- boot_interface_mac : Option < & ' a str > ,
258+ boot_interface : Option < BootInterfaceRef < ' a > > ,
253259 ) -> RedfishFuture < ' a , Result < bool , RedfishError > > ;
254260
255261 /// Apply a standard BMC password policy. This varies a lot by vendor,
@@ -754,6 +760,80 @@ impl Status {
754760 }
755761}
756762
763+ /// How a caller identifies a boot interface to [`Redfish::machine_setup`]
764+ /// and supporting query methods.
765+ #[ derive( Debug , Clone , Copy ) ]
766+ pub enum BootInterfaceRef < ' a > {
767+ /// MAC address of the NIC to boot from. Vendor impl does its existing
768+ /// BMC-side lookup to derive the native id. Use this whenever the
769+ /// partition is in a state where the BMC publishes its MAC normally,
770+ /// allowing a subsequent lookup of MAC -> InterfaceId.
771+ Mac ( & ' a str ) ,
772+ /// Vendor-native Redfish `EthernetInterface.Id` (e.g. Dell
773+ /// `"NIC.Slot.7-1-1"`). Vendor impl uses it directly. Use this when
774+ /// the interface ID/interface partition ID is already known, and we
775+ /// don't want or need to do a MAC address lookup for it.
776+ InterfaceId ( & ' a str ) ,
777+ }
778+
779+ impl < ' a > BootInterfaceRef < ' a > {
780+ /// Returns the MAC if this is the [`BootInterfaceRef::Mac`] variant.
781+ /// Returns `None` if this is the [`BootInterfaceRef::InterfaceId`]
782+ /// variant.
783+ pub fn mac ( & self ) -> Option < & ' a str > {
784+ match self {
785+ BootInterfaceRef :: Mac ( mac) => Some ( mac) ,
786+ BootInterfaceRef :: InterfaceId ( _) => None ,
787+ }
788+ }
789+ }
790+
791+ /// Returns the current MAC address for a [`BootInterfaceRef`], fetching it
792+ /// from the BMC's `Systems/{}/EthernetInterfaces/{id}` resource when the
793+ /// caller supplied an [`BootInterfaceRef::InterfaceId`].
794+ ///
795+ /// This is the cross-vendor primitive that lets every vendor's existing
796+ /// MAC-based `machine_setup_status` / `is_bios_setup` body keep working
797+ /// for both arms of [`BootInterfaceRef`]: callers that have a MAC pass it
798+ /// straight through, callers that have the BMC's stable id round-trip it
799+ /// to a MAC via the Redfish-standard `EthernetInterface` resource (which
800+ /// every vendor implements).
801+ ///
802+ /// Errors when the resolved interface has no MAC populated, typically
803+ /// because the partition is currently Disabled/inactive (e.g., a
804+ /// NIC-mode-DPU partition that hasn't been activated yet).
805+ pub async fn resolve_boot_interface_mac < R : Redfish + ?Sized > (
806+ redfish : & R ,
807+ boot_interface : BootInterfaceRef < ' _ > ,
808+ ) -> Result < String , RedfishError > {
809+ match boot_interface {
810+ BootInterfaceRef :: Mac ( mac) => Ok ( mac. to_string ( ) ) ,
811+ BootInterfaceRef :: InterfaceId ( id) => {
812+ let eif = redfish. get_system_ethernet_interface ( id) . await ?;
813+ extract_resolved_mac ( eif. mac_address . as_deref ( ) , id)
814+ }
815+ }
816+ }
817+
818+ /// Helper for the interface ID handling side of
819+ /// resolve_boot_interface_mac, and also split out
820+ /// for tests.
821+ fn extract_resolved_mac ( mac : Option < & str > , id : & str ) -> Result < String , RedfishError > {
822+ let mac = mac. unwrap_or ( "" ) ;
823+ if mac. is_empty ( ) {
824+ return Err ( RedfishError :: GenericError {
825+ error : format ! (
826+ "Systems/.../EthernetInterfaces/{id} has no populated \
827+ MACAddress; the partition is likely Disabled or hasn't \
828+ been activated yet. Re-call after the BIOS PATCH \
829+ (machine_setup) + reboot causes the BMC to repopulate \
830+ the MAC."
831+ ) ,
832+ } ) ;
833+ }
834+ Ok ( mac. to_string ( ) )
835+ }
836+
757837#[ derive( Debug ) ]
758838pub struct MachineSetupStatus {
759839 pub is_done : bool ,
@@ -837,3 +917,55 @@ pub type BiosProfileVendor = HashMap<RedfishVendor, BiosProfileModel>;
837917pub fn model_coerce ( original : & str ) -> String {
838918 str:: replace ( original, " " , "_" )
839919}
920+
921+ #[ cfg( test) ]
922+ mod tests {
923+ use super :: * ;
924+
925+ #[ test]
926+ fn boot_interface_ref_mac_returns_inner ( ) {
927+ let mac = "aa:bb:cc:dd:ee:01" ;
928+ let r = BootInterfaceRef :: Mac ( mac) ;
929+ assert_eq ! ( r. mac( ) , Some ( mac) ) ;
930+ }
931+
932+ #[ test]
933+ fn boot_interface_ref_interface_id_mac_is_none ( ) {
934+ let r = BootInterfaceRef :: InterfaceId ( "NIC.Slot.7-1-1" ) ;
935+ assert ! ( r. mac( ) . is_none( ) ) ;
936+ }
937+
938+ #[ test]
939+ fn extract_resolved_mac_passes_through_populated_mac ( ) {
940+ let got = super :: extract_resolved_mac ( Some ( "AA:BB:CC:DD:EE:01" ) , "NIC.Slot.7-1-1" )
941+ . expect ( "populated MAC should be returned as-is" ) ;
942+ assert_eq ! ( got, "AA:BB:CC:DD:EE:01" ) ;
943+ }
944+
945+ #[ test]
946+ fn extract_resolved_mac_errors_on_empty_string_mac ( ) {
947+ // The motivating case: iDRAC publishes `MACAddress: ""` for a
948+ // Disabled partition. We must NOT silently return that empty
949+ // string — vendor verification logic that does
950+ // `display_name.contains(&mac)` would otherwise match every
951+ // boot option.
952+ let err = super :: extract_resolved_mac ( Some ( "" ) , "NIC.Slot.7-1-1" )
953+ . expect_err ( "empty MAC should be an explicit error" ) ;
954+ let msg = err. to_string ( ) ;
955+ assert ! (
956+ msg. contains( "NIC.Slot.7-1-1" ) ,
957+ "error should name the interface id; got: {msg}" ,
958+ ) ;
959+ assert ! (
960+ msg. contains( "Disabled" ) || msg. contains( "activated" ) ,
961+ "error should hint at the partition-state cause; got: {msg}" ,
962+ ) ;
963+ }
964+
965+ #[ test]
966+ fn extract_resolved_mac_errors_on_missing_mac_field ( ) {
967+ let err = super :: extract_resolved_mac ( None , "NIC.Slot.7-1-1" )
968+ . expect_err ( "None MAC should be an explicit error" ) ;
969+ assert ! ( err. to_string( ) . contains( "NIC.Slot.7-1-1" ) ) ;
970+ }
971+ }
0 commit comments