Skip to content

Commit 7cdf76c

Browse files
committed
feat: support complete boot-interface targets
A caller can know both a boot interface's MAC address and its vendor-native Redfish interface ID, but `BootInterfaceRef` could only accept one at a time. If the first call changed the BMC before returning an error, retrying the complete mutation with the other identifier can repeat that work. So, `BootInterfaceRef::Pair` keeps both identifiers together for one operation. `resolve_boot_interface_mac` uses the supplied MAC without another Redfish lookup, while Dell uses the interface ID for `NetworkDeviceFunction` matching and slot selection without falling back to the MAC. The existing `Mac` and `InterfaceId` variants keep their behavior. Since `BootInterfaceRef` is a public exhaustive enum, downstream exhaustive matches need a `Pair` arm when they update to this version. Tests added! This supports #109 Signed-off-by: Chet Nichols III <chetn@nvidia.com>
1 parent e2855bc commit 7cdf76c

2 files changed

Lines changed: 133 additions & 33 deletions

File tree

src/dell.rs

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ const MAX_ACCOUNT_ID: u8 = 16;
7272
/// matches partition `NIC.Slot.7-1-1`. Equality also matches. This lets us
7373
/// locate the NDF for partitions whose MAC has been stripped (the
7474
/// NicMode-Disabled case).
75+
/// - [`BootInterfaceRef::Pair`] uses the same interface-ID match as
76+
/// [`BootInterfaceRef::InterfaceId`]. Its MAC is not a fallback.
7577
fn nw_dev_func_matches(
7678
nw_dev_func: &NetworkDeviceFunction,
7779
boot_interface: crate::BootInterfaceRef<'_>,
@@ -82,7 +84,11 @@ fn nw_dev_func_matches(
8284
.as_ref()
8385
.and_then(|e| e.mac_address.as_ref())
8486
.is_some_and(|m| m.eq_ignore_ascii_case(&target.to_string())),
85-
crate::BootInterfaceRef::InterfaceId(target) => nw_dev_func
87+
crate::BootInterfaceRef::InterfaceId(target)
88+
| crate::BootInterfaceRef::Pair {
89+
interface_id: target,
90+
..
91+
} => nw_dev_func
8692
.id
8793
.as_deref()
8894
.is_some_and(|ndf_id| target == ndf_id || target.starts_with(&format!("{ndf_id}-"))),
@@ -2496,18 +2502,21 @@ impl Bmc {
24962502
/// `HttpDev1Interface` BIOS attribute and the first-boot-option check key on
24972503
/// for a boot interface.
24982504
///
2499-
/// A [`crate::BootInterfaceRef::InterfaceId`] already *is* the slot id, so it
2500-
/// is used directly -- this is the stable identifier that survives a NicMode
2501-
/// flip (or any case where the `NetworkDeviceFunction`'s `Ethernet.MACAddress`
2502-
/// is empty): a by-MAC lookup can't find the NDF, but the partition id still
2503-
/// resolves it. A [`crate::BootInterfaceRef::Mac`] is resolved to the slot via
2504-
/// the `NetworkDeviceFunction`, matched by MAC. `None` is the zero-DPU case.
2505+
/// [`crate::BootInterfaceRef::InterfaceId`] and
2506+
/// [`crate::BootInterfaceRef::Pair`] already contain the slot id, so it is
2507+
/// used directly. This stable identifier survives a NicMode flip or any case
2508+
/// where the `NetworkDeviceFunction`'s `Ethernet.MACAddress` is empty.
2509+
/// [`crate::BootInterfaceRef::Mac`] is resolved to the slot through the
2510+
/// `NetworkDeviceFunction`, matched by MAC. `None` is the zero-DPU case.
25052511
async fn nic_slot_for(
25062512
&self,
25072513
boot_interface: Option<crate::BootInterfaceRef<'_>>,
25082514
) -> Result<String, RedfishError> {
25092515
Ok(match boot_interface {
2510-
Some(crate::BootInterfaceRef::InterfaceId(id)) => id.to_string(),
2516+
Some(crate::BootInterfaceRef::InterfaceId(id))
2517+
| Some(crate::BootInterfaceRef::Pair {
2518+
interface_id: id, ..
2519+
}) => id.to_string(),
25112520
Some(crate::BootInterfaceRef::Mac(mac)) => self.dpu_nic_slot(&mac.to_string()).await?,
25122521
None => String::new(),
25132522
})
@@ -2778,9 +2787,9 @@ impl std::fmt::Display for XmlPcdata<'_> {
27782787

27792788
#[cfg(test)]
27802789
mod tests {
2781-
use super::{boot_option_name_matches, nw_dev_func_matches, XmlPcdata};
2790+
use super::{boot_option_name_matches, nw_dev_func_matches, Bmc, XmlPcdata};
27822791
use crate::model::network_device_function::{Ethernet, NetworkDeviceFunction};
2783-
use crate::BootInterfaceRef;
2792+
use crate::{BootInterfaceRef, Endpoint, RedfishClientPool};
27842793
use std::collections::HashMap;
27852794

27862795
fn ndf_with(id: Option<&str>, mac: Option<&str>) -> NetworkDeviceFunction {
@@ -2840,6 +2849,56 @@ mod tests {
28402849
assert!(nw_dev_func_matches(&populated, BootInterfaceRef::Mac(mac)));
28412850
}
28422851

2852+
#[test]
2853+
fn nw_dev_func_pair_matches_by_interface_id() {
2854+
let mac: mac_address::MacAddress = "C4:70:BD:2C:3C:0A".parse().unwrap();
2855+
let pair = BootInterfaceRef::Pair {
2856+
mac_address: mac,
2857+
interface_id: "NIC.Slot.40-1-1",
2858+
};
2859+
2860+
assert!(nw_dev_func_matches(
2861+
&ndf_with(Some("NIC.Slot.40-1-1"), None),
2862+
pair,
2863+
));
2864+
assert!(nw_dev_func_matches(
2865+
&ndf_with(Some("NIC.Slot.40-1"), None),
2866+
pair,
2867+
));
2868+
}
2869+
2870+
#[test]
2871+
fn nw_dev_func_pair_does_not_fall_back_to_mac() {
2872+
let mac: mac_address::MacAddress = "C4:70:BD:2C:3C:0A".parse().unwrap();
2873+
let pair = BootInterfaceRef::Pair {
2874+
mac_address: mac,
2875+
interface_id: "NIC.Slot.40-1-1",
2876+
};
2877+
let matching_mac_wrong_id = ndf_with(Some("NIC.Slot.7-1-1"), Some("c4:70:bd:2c:3c:0a"));
2878+
2879+
assert!(!nw_dev_func_matches(&matching_mac_wrong_id, pair));
2880+
}
2881+
2882+
#[tokio::test]
2883+
async fn nic_slot_for_pair_uses_interface_id_directly() {
2884+
let pool = RedfishClientPool::builder().build().unwrap();
2885+
let standard = pool
2886+
.create_standard_client(Endpoint::default())
2887+
.expect("test Redfish client should be constructed without a request");
2888+
let bmc = Bmc::new(*standard).unwrap();
2889+
let mac: mac_address::MacAddress = "C4:70:BD:2C:3C:0A".parse().unwrap();
2890+
2891+
let got = bmc
2892+
.nic_slot_for(Some(BootInterfaceRef::Pair {
2893+
mac_address: mac,
2894+
interface_id: "NIC.Slot.40-1-1",
2895+
}))
2896+
.await
2897+
.expect("pair should use its interface ID without querying the empty endpoint");
2898+
2899+
assert_eq!(got, "NIC.Slot.40-1-1");
2900+
}
2901+
28432902
#[test]
28442903
fn boot_option_name_matches_legacy_and_extended_names() {
28452904
let expected = "HTTP Device 1: NIC in Slot 4 Port 1 Partition 1";

src/lib.rs

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,12 @@ pub trait Redfish: Send + Sync + 'static {
224224

225225
/// Sets up a reasonable UEFI configuration.
226226
/// remember to call lockdown() afterwards to secure the server
227-
/// - boot_interface: identifies the NIC you wish to boot from. Either a
228-
/// `BootInterfaceRef::Mac` (existing behavior: vendor impl looks up
229-
/// the partition by MAC via its BMC enumeration) or a
230-
/// `BootInterfaceRef::InterfaceId` (vendor-native Redfish
231-
/// `EthernetInterface.Id` — used when we already know the interface
232-
/// partition ID (and don't need to look it up by MAC). One case
233-
/// being if we flip a DPU to NIC mode.
227+
/// - boot_interface: identifies the NIC you wish to boot from. A
228+
/// `BootInterfaceRef::Mac` uses the existing vendor lookup by MAC, while
229+
/// `BootInterfaceRef::InterfaceId` uses a vendor-native Redfish
230+
/// `EthernetInterface.Id`. `BootInterfaceRef::Pair` supplies both so each
231+
/// vendor can use its native identifier without resolving one from the
232+
/// other.
234233
/// If not given we look for a Mellanox Bluefield DPU and use that.
235234
/// Not applicable to Supermicro and the DPU itself.
236235
/// bios_profiles: Map of vendor/model (with spaces replaced by underscores)/profile/type
@@ -537,11 +536,11 @@ pub trait Redfish: Send + Sync + 'static {
537536

538537
/// Change the boot order so the system will boot from the chosen NIC first.
539538
///
540-
/// `boot_interface` selects the target NIC. A `BootInterfaceRef::Mac` is the
541-
/// classic path: the vendor impl looks the NIC up via its BMC enumeration
542-
/// by MAC. A `BootInterfaceRef::InterfaceId` is the vendor-native Redfish
543-
/// `EthernetInterface.Id` -- used when the caller already knows the
544-
/// partition ID.
539+
/// `boot_interface` selects the target NIC. A `BootInterfaceRef::Mac` uses
540+
/// the existing vendor lookup by MAC, while `BootInterfaceRef::InterfaceId`
541+
/// uses a vendor-native Redfish `EthernetInterface.Id`.
542+
/// `BootInterfaceRef::Pair` supplies both so the vendor can use its native
543+
/// identifier directly.
545544
fn set_boot_order_dpu_first<'a>(
546545
&'a self,
547546
boot_interface: BootInterfaceRef<'a>,
@@ -772,8 +771,7 @@ impl Status {
772771
}
773772

774773
/// How a caller identifies a boot interface to [`Redfish::machine_setup`]
775-
/// and supporting query methods. Callers can pass either form; vendor
776-
/// impls handle both.
774+
/// and supporting query methods.
777775
#[derive(Debug, Clone, Copy)]
778776
pub enum BootInterfaceRef<'a> {
779777
/// MAC address of the boot interface. Vendor impl translates it into
@@ -783,25 +781,34 @@ pub enum BootInterfaceRef<'a> {
783781
/// interface (e.g. `"NIC.Slot.7-1-1"`). Vendor impl uses it
784782
/// directly.
785783
InterfaceId(&'a str),
784+
/// Complete identity for one boot interface. Both fields must identify the
785+
/// same interface; this is one target, not an instruction to try both.
786+
/// MAC-oriented vendor paths use `mac_address`, while interface-ID-oriented
787+
/// paths use `interface_id`.
788+
Pair {
789+
mac_address: mac_address::MacAddress,
790+
interface_id: &'a str,
791+
},
786792
}
787793

788794
impl BootInterfaceRef<'_> {
789-
/// Returns the MAC if this is the [`BootInterfaceRef::Mac`] variant.
790-
/// Returns `None` if this is the [`BootInterfaceRef::InterfaceId`]
791-
/// variant.
795+
/// Returns the supplied MAC when this selector contains one.
792796
pub fn mac(&self) -> Option<mac_address::MacAddress> {
793797
match self {
794-
BootInterfaceRef::Mac(mac) => Some(*mac),
798+
BootInterfaceRef::Mac(mac)
799+
| BootInterfaceRef::Pair {
800+
mac_address: mac, ..
801+
} => Some(*mac),
795802
BootInterfaceRef::InterfaceId(_) => None,
796803
}
797804
}
798805
}
799806

800807
/// Returns the MAC address for a [`BootInterfaceRef`].
801-
/// [`BootInterfaceRef::Mac`] is a pass-through; [`BootInterfaceRef::InterfaceId`]
802-
/// is resolved by fetching `Systems/{}/EthernetInterfaces/{id}` via the
803-
/// Redfish-standard `EthernetInterface` resource (every vendor
804-
/// implements it).
808+
/// [`BootInterfaceRef::Mac`] and [`BootInterfaceRef::Pair`] pass through their
809+
/// supplied MAC. [`BootInterfaceRef::InterfaceId`] is resolved by fetching
810+
/// `Systems/{}/EthernetInterfaces/{id}` via the Redfish-standard
811+
/// `EthernetInterface` resource (every vendor implements it).
805812
///
806813
/// Used by methods that compare against a MAC (verification paths that
807814
/// walk boot options by MAC substring, etc.) so the caller can pass
@@ -811,7 +818,10 @@ pub async fn resolve_boot_interface_mac<R: Redfish + ?Sized>(
811818
boot_interface: BootInterfaceRef<'_>,
812819
) -> Result<String, RedfishError> {
813820
match boot_interface {
814-
BootInterfaceRef::Mac(mac) => Ok(mac.to_string()),
821+
BootInterfaceRef::Mac(mac)
822+
| BootInterfaceRef::Pair {
823+
mac_address: mac, ..
824+
} => Ok(mac.to_string()),
815825
BootInterfaceRef::InterfaceId(id) => {
816826
let eif = redfish.get_system_ethernet_interface(id).await?;
817827
extract_resolved_mac(eif.mac_address.as_deref(), id)
@@ -945,6 +955,37 @@ mod tests {
945955
assert!(r.mac().is_none());
946956
}
947957

958+
#[test]
959+
fn boot_interface_ref_pair_mac_returns_inner() {
960+
let mac: mac_address::MacAddress = "AA:BB:CC:DD:EE:01".parse().unwrap();
961+
let r = BootInterfaceRef::Pair {
962+
mac_address: mac,
963+
interface_id: "NIC.Slot.7-1-1",
964+
};
965+
assert_eq!(r.mac(), Some(mac));
966+
}
967+
968+
#[tokio::test]
969+
async fn resolve_boot_interface_mac_uses_pair_mac_without_lookup() {
970+
let pool = RedfishClientPool::builder().build().unwrap();
971+
let redfish = pool
972+
.create_standard_client(Endpoint::default())
973+
.expect("test Redfish client should be constructed without a request");
974+
let mac: mac_address::MacAddress = "AA:BB:CC:DD:EE:01".parse().unwrap();
975+
976+
let got = resolve_boot_interface_mac(
977+
redfish.as_ref(),
978+
BootInterfaceRef::Pair {
979+
mac_address: mac,
980+
interface_id: "NIC.Slot.7-1-1",
981+
},
982+
)
983+
.await
984+
.expect("pair should use its MAC without querying the empty endpoint");
985+
986+
assert_eq!(got, mac.to_string());
987+
}
988+
948989
#[test]
949990
fn extract_resolved_mac_passes_through_populated_mac() {
950991
let got = super::extract_resolved_mac(Some("AA:BB:CC:DD:EE:01"), "NIC.Slot.7-1-1")

0 commit comments

Comments
 (0)