From c092aabc69a4522342f4df364c263225241d3795 Mon Sep 17 00:00:00 2001 From: Dmitry Porokh Date: Thu, 2 Jul 2026 19:38:29 -0700 Subject: [PATCH 1/2] fix(redfish): support new BlueField-4 Redfish IDs Handle newer BF4 firmware Redfish IDs for systems, managers, and product chassis while preserving legacy BlueField IDs. Use the product BMC chassis for BF4 identity/pairing serials and update mocks/tests to match newer firmware behavior. Signed-off-by: Dmitry Porokh --- crates/api-model/src/site_explorer/mod.rs | 111 ++++++++++++++---- crates/bmc-explorer/src/computer_system.rs | 13 +- crates/bmc-explorer/src/lib.rs | 13 +- crates/bmc-explorer/src/test_support.rs | 5 +- .../bmc-explorer/tests/bluefield4_explore.rs | 30 +++-- crates/bmc-mock/src/hw/bluefield3.rs | 2 +- crates/bmc-mock/src/hw/bluefield4.rs | 62 +++++----- crates/bmc-mock/src/hw/dell_poweredge_r750.rs | 2 +- .../src/hw/dell_poweredge_r760_bf4.rs | 2 +- crates/bmc-mock/src/hw/dgx_gb300_nvl.rs | 2 +- crates/bmc-mock/src/hw/dgx_vr_nvl.rs | 2 +- crates/bmc-mock/src/hw/generic_ami.rs | 2 +- .../src/hw/hpe_proliant_dl380a_gen11.rs | 2 +- crates/bmc-mock/src/hw/lenovo_gb300_nvl.rs | 2 +- crates/bmc-mock/src/hw/nvidia_dgx_h100.rs | 2 +- .../bmc-mock/src/hw/supermicro_gb300_nvl.rs | 2 +- crates/bmc-mock/src/hw/wiwynn_gb200_nvl.rs | 2 +- .../bmc-mock/src/redfish/computer_system.rs | 111 ++++++------------ .../src/redfish/oem/nvidia/bluefield.rs | 60 ++++++---- crates/site-explorer/src/lib.rs | 7 +- 20 files changed, 241 insertions(+), 193 deletions(-) diff --git a/crates/api-model/src/site_explorer/mod.rs b/crates/api-model/src/site_explorer/mod.rs index d89c6b8d12..02229d50fc 100644 --- a/crates/api-model/src/site_explorer/mod.rs +++ b/crates/api-model/src/site_explorer/mod.rs @@ -641,13 +641,27 @@ impl ExploredDpu { .to_string(), part_number: chassis_map .get("Card1") - .and_then(|value| value.part_number.as_ref()) - .unwrap_or(&"".to_string()) + .and_then(|value| chassis_part_number(value)) + .or_else(|| { + self.report + .chassis + .iter() + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(chassis_part_number) + }) + .unwrap_or("") .to_string(), part_description: chassis_map .get("Card1") - .and_then(|value| value.model.as_ref()) - .unwrap_or(&"".to_string()) + .and_then(|value| chassis_model(value)) + .or_else(|| { + self.report + .chassis + .iter() + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(chassis_model) + }) + .unwrap_or("") .to_string(), firmware_version: inventory_map .get("DPU_NIC") @@ -837,8 +851,8 @@ impl EndpointExplorationReport { // `Bluefield_BMC` on some trays, `BlueField_BMC_0` on others). self.chassis .iter() - .find(|chassis| is_dpu_product_chassis_id(&chassis.id)) - .and_then(chassis_part_number) + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(chassis_part_number) }) } @@ -877,7 +891,7 @@ impl EndpointExplorationReport { if !self .systems .first() - .map(|system| is_bluefield_system_id(&system.id)) + .map(is_bluefield_system) .unwrap_or(false) { return None; @@ -890,8 +904,14 @@ impl EndpointExplorationReport { .collect::>(); let model = chassis_map .get("Card1") - .and_then(|value| value.model.as_ref()) - .unwrap_or(&"".to_string()) + .and_then(|value| chassis_model(value)) + .or_else(|| { + self.chassis + .iter() + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(chassis_model) + }) + .unwrap_or("") .to_string(); match model.to_lowercase() { value if value.contains("bluefield 2") => Some(DpuModel::BlueField2), @@ -942,13 +962,18 @@ impl EndpointExplorationReport { .or_else(|| { self.is_dpu().then(|| { // BF4 reports no system serial in Redfish. The stable product serial is - // on the Bluefield_BMC chassis; use that explicit chassis ID instead of + // on the product BMC chassis; use its known legacy/new IDs instead of // depending on chassis collection order or unrelated component serials. self.chassis .iter() - .find(|chassis| chassis.id == "Bluefield_BMC") - .and_then(|chassis| chassis.serial_number.as_deref().map(str::trim)) - .filter(|serial| !serial.trim().is_empty()) + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(|chassis| { + chassis + .serial_number + .as_deref() + .map(str::trim) + .filter(|serial| !serial.is_empty()) + }) })? }) } @@ -1762,8 +1787,8 @@ fn is_dpu_product_chassis_id(id: &str) -> bool { /// Firmware is inconsistent: older dumps expose `/redfish/v1/Systems/Bluefield` /// while newer BF4 firmware exposes `/redfish/v1/Systems/BlueField_0`. Accept /// both so DPU detection is not silently skipped. -pub fn is_bluefield_system_id(id: &str) -> bool { - matches!(id, "Bluefield" | "BlueField_0") +pub fn is_bluefield_system(system: &ComputerSystem) -> bool { + matches!(system.id.as_str(), "Bluefield" | "BlueField_0") } fn chassis_part_number(chassis: &Chassis) -> Option<&str> { @@ -1774,6 +1799,14 @@ fn chassis_part_number(chassis: &Chassis) -> Option<&str> { .filter(|part_number| !part_number.is_empty()) } +fn chassis_model(chassis: &Chassis) -> Option<&str> { + chassis + .model + .as_deref() + .map(str::trim) + .filter(|model| !model.is_empty()) +} + // returns true if the passed in string is a BlueField part number pub fn is_bluefield_part_number(part_number: &str) -> bool { let normalized_part_number = part_number.trim().to_lowercase(); @@ -1962,14 +1995,18 @@ impl EndpointExplorationReport { .or_else(|| { // BF4 Redfish does not currently expose the product serial or // DPU/NIC mode on the system object. The stable product serial - // lives on the Bluefield_BMC chassis and matches the serial the + // lives on the product BMC chassis and matches the serial the // host BMC reports for the PCIe/network-adapter device. self.chassis .iter() - .find(|chassis| chassis.id == "Bluefield_BMC") - .and_then(|chassis| chassis.serial_number.as_deref()) - .map(str::trim) - .filter(|serial| !serial.is_empty()) + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(|chassis| { + chassis + .serial_number + .as_deref() + .map(str::trim) + .filter(|serial| !serial.is_empty()) + }) }) } } @@ -2082,7 +2119,11 @@ mod explored_mlx_device_tests { ) -> EndpointExplorationReport { EndpointExplorationReport { systems: vec![ComputerSystem { - id: "Bluefield".to_string(), + id: if bmc_chassis_id == "BlueField_BMC_0" { + "BlueField_0".to_string() + } else { + "Bluefield".to_string() + }, ..Default::default() }], chassis: vec![ @@ -2174,6 +2215,34 @@ mod explored_mlx_device_tests { ); } + #[test] + fn recognizes_legacy_and_new_bluefield_system_ids() { + let system = |id: &str| ComputerSystem { + id: id.to_string(), + ..Default::default() + }; + assert!(is_bluefield_system(&system("Bluefield"))); + assert!(is_bluefield_system(&system("BlueField_0"))); + assert!(!is_bluefield_system(&system("Bluefield_0"))); + } + + #[test] + fn new_bf4_ids_use_bmc_chassis_for_identity_and_pairing() { + const SERIAL: &str = "MT2610604VN4"; + let mut report = dpu_report_with_bf4_bmc_chassis("BlueField_BMC_0", "900-9D4A4-00CB-TS4"); + let bmc_chassis = report + .chassis + .iter_mut() + .find(|chassis| chassis.id == "BlueField_BMC_0") + .unwrap(); + bmc_chassis.serial_number = Some(SERIAL.to_string()); + bmc_chassis.model = Some("B4240".to_string()); + + assert_eq!(report.identify_dpu(), Some(DpuModel::Unknown)); + assert_eq!(report.machine_id_serial_number(), Some(SERIAL)); + assert_eq!(report.dpu_pairing_serial_number(), Some(SERIAL)); + } + #[test] fn is_bf4_dpu_part_number_matches_vera_rubin_sku() { assert!(is_bf4_dpu_part_number("900-9D4B4-CWAA-TSA")); diff --git a/crates/bmc-explorer/src/computer_system.rs b/crates/bmc-explorer/src/computer_system.rs index 0e0cefd33c..ecc5f4fbcb 100644 --- a/crates/bmc-explorer/src/computer_system.rs +++ b/crates/bmc-explorer/src/computer_system.rs @@ -56,8 +56,6 @@ pub struct Config<'a, B: Bmc> { // This is expected to be fixed in BMC firmware 24.10-39, which adds // internal retries. pub retry_404_on_eth_interfaces: bool, - // Collect boot options (if set to false then assume that they are empty). - pub need_boot_options: bool, pub explore: &'a ExploreConfig<'a, B>, } @@ -75,11 +73,10 @@ impl ExploredComputerSystem { system: ComputerSystem, config: &Config<'_, B>, ) -> Result> { - let boot_options = if config.need_boot_options - && let Some(collection) = system - .boot_options() - .await - .map_err(Error::nv_redfish("boot options"))? + let boot_options = if let Some(collection) = system + .boot_options() + .await + .map_err(Error::nv_redfish("boot options"))? { collection .members() @@ -190,7 +187,7 @@ impl ExploredComputerSystem { // This part processes dpu case and do two things such as // 1. update system serial_number in case it is empty using chassis serial_number // 2. format serial_number data using the same rules as in fetch_chassis() - if serial_number.is_none() { + if serial_number.is_none() && !chassis.is_bluefield4() { serial_number = chassis.dpu_card1_serial_number()?; } diff --git a/crates/bmc-explorer/src/lib.rs b/crates/bmc-explorer/src/lib.rs index 177eff9aae..b6839c6c62 100644 --- a/crates/bmc-explorer/src/lib.rs +++ b/crates/bmc-explorer/src/lib.rs @@ -50,7 +50,7 @@ use nv_redfish::oem::lenovo::computer_system::{FpMode, PortSwitchingTo}; use nv_redfish::oem::lenovo::manager::KcsState; use nv_redfish::oem::lenovo::security_service::FwRollbackState; use nv_redfish::oem::supermicro::Privilege as SupermicroPrivilege; -use nv_redfish::resource::ResourceNameRef; +use nv_redfish::resource::{ResourceIdRef, ResourceNameRef}; use nv_redfish::service_root::{Product, Vendor}; use nv_redfish::{Bmc, Resource, ServiceRoot}; @@ -62,6 +62,10 @@ pub enum ErrorClass { pub type ErrorClassifier<'a, B> = &'a (dyn Fn(&::Error) -> Option + Sync); +fn is_bluefield_system_id(id: ResourceIdRef<'_>) -> bool { + matches!(id.into_inner(), "Bluefield" | "BlueField_0") +} + pub struct Config<'a, B: Bmc> { pub boot_interface_mac: Option, pub error_classifier: ErrorClassifier<'a, B>, @@ -140,14 +144,11 @@ pub async fn nv_generate_exploration_report( .next() .ok_or_else(Error::bmc_not_provided("at least one manager"))?; - let is_bluefield_system = system.id().into_inner() == "Bluefield"; + let is_bluefield_system = is_bluefield_system_id(system.id()); let system_explore_config = computer_system::Config { need_oem_nvidia_bluefield: is_bluefield_system, ignore_500_on_bios_fetch: is_bluefield_system, retry_404_on_eth_interfaces: is_bluefield_system, - // BlueField-4 returns null in the Members field in - // BootOptions. This is a workaround for this bug. - need_boot_options: !explored_chassis.is_bluefield4(), explore: config, }; let explored_system = ExploredComputerSystem::explore(system, &system_explore_config).await?; @@ -323,7 +324,7 @@ pub(crate) fn hw_type( "Lenovo" if oem_id != Some("Ami") => Some(hw::HwType::Lenovo), "Supermicro" => Some(hw::HwType::Supermicro), "HPE" => Some(hw::HwType::Hpe), - "Nvidia" if system.id().into_inner() == "Bluefield" => Some(hw::HwType::Bluefield), + "Nvidia" if is_bluefield_system_id(system.id()) => Some(hw::HwType::Bluefield), "NVIDIA" if root.product() == Some(Product::new("VR NVL72")) => { Some(hw::HwType::VeraRubin) } diff --git a/crates/bmc-explorer/src/test_support.rs b/crates/bmc-explorer/src/test_support.rs index f39de4717b..94e272f568 100644 --- a/crates/bmc-explorer/src/test_support.rs +++ b/crates/bmc-explorer/src/test_support.rs @@ -24,7 +24,7 @@ use nv_redfish::{Bmc, Resource, ServiceRoot}; use crate::chassis::ExploredChassisCollection; use crate::computer_system::{self, ExploredComputerSystem}; -use crate::{Config, Error, build_chassis_explore_config, hw, hw_type}; +use crate::{Config, Error, build_chassis_explore_config, hw, hw_type, is_bluefield_system_id}; /// Resolve the [`hw::HwType`] for an endpoint, running only the chassis + /// computer-system exploration that detection depends on. @@ -67,12 +67,11 @@ pub async fn detect_hw_type( let other_system_with_bios = systems_iter.find(|system| system.raw().bios.is_some()); let system = other_system_with_bios.unwrap_or(first_system); - let is_bluefield_system = system.id().into_inner() == "Bluefield"; + let is_bluefield_system = is_bluefield_system_id(system.id()); let system_explore_config = computer_system::Config { need_oem_nvidia_bluefield: is_bluefield_system, ignore_500_on_bios_fetch: is_bluefield_system, retry_404_on_eth_interfaces: is_bluefield_system, - need_boot_options: !explored_chassis.is_bluefield4(), explore: config, }; let explored_system = ExploredComputerSystem::explore(system, &system_explore_config).await?; diff --git a/crates/bmc-explorer/tests/bluefield4_explore.rs b/crates/bmc-explorer/tests/bluefield4_explore.rs index d5717a8629..084189b016 100644 --- a/crates/bmc-explorer/tests/bluefield4_explore.rs +++ b/crates/bmc-explorer/tests/bluefield4_explore.rs @@ -41,29 +41,37 @@ async fn explore_bluefield4_and_generate_machine_id_from_bluefield_bmc_chassis_s assert_eq!(report.vendor, Some(bmc_vendor::BMCVendor::Nvidia)); let system = report.systems.first().expect("systems must be present"); - assert_eq!(system.id, "Bluefield"); + assert_eq!(system.id, "BlueField_0"); assert!( system.serial_number.is_none(), "BF4 Redfish reports the usable serial on chassis, not system" ); - let chassis_ids: Vec<&str> = report + assert_eq!( + report.managers.first().map(|manager| manager.id.as_str()), + Some("BlueField_BMC_0") + ); + + let mut chassis_ids: Vec<&str> = report .chassis .iter() .map(|chassis| chassis.id.as_str()) .collect(); - assert!( - chassis_ids.contains(&"Bluefield_BMC"), - "Bluefield_BMC chassis must be present: {chassis_ids:?}" - ); - assert!( - chassis_ids.contains(&"Card1"), - "Card1 chassis must be present: {chassis_ids:?}" + chassis_ids.sort_unstable(); + assert_eq!( + chassis_ids, + [ + "BlueField_0", + "BlueField_BMC_0", + "BlueField_ERoT_BMC_0", + "BlueField_ERoT_CPU_0", + "BlueField_IRoT_NIC_0", + ] ); let bmc_chassis_serial = report .chassis .iter() - .find(|chassis| chassis.id == "Bluefield_BMC") + .find(|chassis| chassis.id == "BlueField_BMC_0") .and_then(|chassis| chassis.serial_number.as_deref()); assert_eq!(bmc_chassis_serial, Some("MT2610604VN4")); @@ -98,7 +106,7 @@ async fn explore_b4240v_and_generate_machine_id() { assert_eq!(report.endpoint_type, EndpointType::Bmc); assert_eq!(report.vendor, Some(bmc_vendor::BMCVendor::Nvidia)); assert!(report.chassis.iter().any(|chassis| { - chassis.id == "Bluefield_BMC" && chassis.model.as_deref() == Some("B4240V") + chassis.id == "BlueField_BMC_0" && chassis.model.as_deref() == Some("B4240V") })); let machine_id = report diff --git a/crates/bmc-mock/src/hw/bluefield3.rs b/crates/bmc-mock/src/hw/bluefield3.rs index 4a884e0c3f..d48a42815a 100644 --- a/crates/bmc-mock/src/hw/bluefield3.rs +++ b/crates/bmc-mock/src/hw/bluefield3.rs @@ -162,7 +162,7 @@ impl Bluefield3<'_> { serial_number: Some(self.product_serial_number.to_string().into()), boot_order_mode: redfish::computer_system::BootOrderMode::ViaSettings, callbacks: Some(callbacks), - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), bios_mode: redfish::computer_system::BiosMode::Generic, oem: redfish::computer_system::Oem::NvidiaBluefield, base_bios: Some( diff --git a/crates/bmc-mock/src/hw/bluefield4.rs b/crates/bmc-mock/src/hw/bluefield4.rs index d3c0aa4eb1..e08b997655 100644 --- a/crates/bmc-mock/src/hw/bluefield4.rs +++ b/crates/bmc-mock/src/hw/bluefield4.rs @@ -41,8 +41,13 @@ pub struct Bluefield4<'a> { } impl Bluefield4<'_> { + const SYSTEM_ID: &'static str = "BlueField_0"; + const MANAGER_ID: &'static str = "BlueField_BMC_0"; + const BMC_CHASSIS_ID: &'static str = "BlueField_BMC_0"; + fn sensor_layout() -> redfish::sensor::Layout { - // BF4 Card1 dump contains 96 sensors total. The generic mock + // The older BF4 layout exposed these sensors below Card1. Newer + // firmware renamed the main card chassis to BlueField_0. The generic mock // layout currently models Temperature, Fan, Power, Current, // and Voltage. Missing BF4 ReadingType counts: Percent=64, // Frequency=2, EnergyJoules=1. @@ -59,7 +64,21 @@ impl Bluefield4<'_> { redfish::chassis::ChassisConfig { chassis: vec![ redfish::chassis::SingleChassisConfig { - id: "Bluefield_BMC".into(), + id: "BlueField_0".into(), + chassis_type: "Component".into(), + manufacturer: Some("NVIDIA".into()), + model: Some("NA".into()), + part_number: Some(self.part_number().into()), + serial_number: Some(self.product_serial_number.to_string().into()), + pcie_devices: Some(vec![]), + sensors: Some(redfish::sensor::generate_chassis_sensors( + "BlueField_0", + Self::sensor_layout(), + )), + ..redfish::chassis::SingleChassisConfig::defaults() + }, + redfish::chassis::SingleChassisConfig { + id: Self::BMC_CHASSIS_ID.into(), chassis_type: "Component".into(), manufacturer: Some("Nvidia".into()), model: Some(self.model().into()), @@ -70,41 +89,24 @@ impl Bluefield4<'_> { ..redfish::chassis::SingleChassisConfig::defaults() }, redfish::chassis::SingleChassisConfig { - id: "Bluefield_BMC_ERoT".into(), + id: "BlueField_ERoT_BMC_0".into(), chassis_type: "Component".into(), manufacturer: Some(Cow::Borrowed("NVIDIA")), serial_number: Some("".into()), ..redfish::chassis::SingleChassisConfig::defaults() }, redfish::chassis::SingleChassisConfig { - id: "Bluefield_CPU_ERoT".into(), + id: "BlueField_ERoT_CPU_0".into(), chassis_type: "Component".into(), manufacturer: Some(Cow::Borrowed("NVIDIA")), serial_number: Some("".into()), ..redfish::chassis::SingleChassisConfig::defaults() }, redfish::chassis::SingleChassisConfig { - id: "Bluefield_NIC".into(), + id: "BlueField_IRoT_NIC_0".into(), chassis_type: "Component".into(), manufacturer: Some(Cow::Borrowed("NVIDIA")), - serial_number: Some("".into()), - ..redfish::chassis::SingleChassisConfig::defaults() - }, - redfish::chassis::SingleChassisConfig { - id: "Card1".into(), - chassis_type: "Card".into(), - pcie_devices: Some(vec![]), - sensors: Some(redfish::sensor::generate_chassis_sensors( - "Card1", - Self::sensor_layout(), - )), - ..redfish::chassis::SingleChassisConfig::defaults() - }, - redfish::chassis::SingleChassisConfig { - id: "MCTP_SPI_DEV".into(), - chassis_type: "".into(), - pcie_devices: Some(vec![]), - sensors: Some(vec![]), + serial_number: Some("0x3BC1ADDC364432C9".into()), ..redfish::chassis::SingleChassisConfig::defaults() }, ], @@ -112,18 +114,18 @@ impl Bluefield4<'_> { } pub fn system_config(&self, callbacks: Arc) -> redfish::computer_system::Config { - let system_id = "Bluefield"; + let system_id = Self::SYSTEM_ID; redfish::computer_system::Config { systems: vec![redfish::computer_system::SingleSystemConfig { - id: Cow::Borrowed("Bluefield"), + id: Cow::Borrowed(system_id), manufacturer: None, model: None, eth_interfaces: Some(vec![]), - chassis: vec!["Bluefield_BMC".into()], + chassis: vec![Self::BMC_CHASSIS_ID.into()], serial_number: None, boot_order_mode: redfish::computer_system::BootOrderMode::ViaSettings, callbacks: Some(callbacks), - boot_options: Some(redfish::computer_system::BootOptionsConfig::NullMembers), + boot_options: Some(vec![]), bios_mode: redfish::computer_system::BiosMode::Generic, oem: redfish::computer_system::Oem::NvidiaBluefield, base_bios: Some( @@ -146,17 +148,17 @@ impl Bluefield4<'_> { pub fn manager_config(&self) -> redfish::manager::Config { redfish::manager::Config { managers: vec![redfish::manager::SingleConfig { - id: "Bluefield_BMC", + id: Self::MANAGER_ID, eth_interfaces: Some(vec![ redfish::ethernet_interface::builder( - &redfish::ethernet_interface::manager_resource("Bluefield_BMC", "eth0"), + &redfish::ethernet_interface::manager_resource(Self::MANAGER_ID, "eth0"), ) .mac_address(self.bmc_mac_address) .interface_enabled(true) .build(), ]), host_interfaces: None, - firmware_version: Some("BF4-26.01-2"), + firmware_version: Some("BF4-26.04-4"), oem: None, }], } diff --git a/crates/bmc-mock/src/hw/dell_poweredge_r750.rs b/crates/bmc-mock/src/hw/dell_poweredge_r750.rs index 73fc821d14..e05d35d5d7 100644 --- a/crates/bmc-mock/src/hw/dell_poweredge_r750.rs +++ b/crates/bmc-mock/src/hw/dell_poweredge_r750.rs @@ -176,7 +176,7 @@ impl DellPowerEdgeR750<'_> { boot_order_mode: redfish::computer_system::BootOrderMode::DellOem, callbacks, chassis: vec!["System.Embedded.1".into()], - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), bios_mode: redfish::computer_system::BiosMode::DellOem, oem: redfish::computer_system::Oem::Generic, log_services: Some(Arc::new(DellLogServices { diff --git a/crates/bmc-mock/src/hw/dell_poweredge_r760_bf4.rs b/crates/bmc-mock/src/hw/dell_poweredge_r760_bf4.rs index e6582064f0..6caebff74a 100644 --- a/crates/bmc-mock/src/hw/dell_poweredge_r760_bf4.rs +++ b/crates/bmc-mock/src/hw/dell_poweredge_r760_bf4.rs @@ -115,7 +115,7 @@ impl DellPowerEdgeR760Bf4<'_> { boot_order_mode: redfish::computer_system::BootOrderMode::DellOem, callbacks, chassis: vec!["System.Embedded.1".into()], - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), bios_mode: redfish::computer_system::BiosMode::DellOem, oem: redfish::computer_system::Oem::Generic, log_services: None, diff --git a/crates/bmc-mock/src/hw/dgx_gb300_nvl.rs b/crates/bmc-mock/src/hw/dgx_gb300_nvl.rs index 0ff35b52cd..25987bddc2 100644 --- a/crates/bmc-mock/src/hw/dgx_gb300_nvl.rs +++ b/crates/bmc-mock/src/hw/dgx_gb300_nvl.rs @@ -177,7 +177,7 @@ impl DgxGB300Nvl<'_> { redfish::computer_system::SingleSystemConfig { base_bios: Some(base_bios(system_id)), bios_mode: redfish::computer_system::BiosMode::Generic, - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), boot_order_mode: redfish::computer_system::BootOrderMode::Generic, chassis: vec!["Chassis_0".into()], eth_interfaces: Some(eth_interfaces), diff --git a/crates/bmc-mock/src/hw/dgx_vr_nvl.rs b/crates/bmc-mock/src/hw/dgx_vr_nvl.rs index 8a687066e0..288c41939d 100644 --- a/crates/bmc-mock/src/hw/dgx_vr_nvl.rs +++ b/crates/bmc-mock/src/hw/dgx_vr_nvl.rs @@ -118,7 +118,7 @@ impl DgxVrNvl<'_> { redfish::computer_system::SingleSystemConfig { base_bios: Some(base_bios(system_id)), bios_mode: redfish::computer_system::BiosMode::Generic, - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), boot_order_mode: redfish::computer_system::BootOrderMode::Generic, chassis: vec!["Chassis_0".into()], eth_interfaces: None, diff --git a/crates/bmc-mock/src/hw/generic_ami.rs b/crates/bmc-mock/src/hw/generic_ami.rs index d8c02f362f..e866067576 100644 --- a/crates/bmc-mock/src/hw/generic_ami.rs +++ b/crates/bmc-mock/src/hw/generic_ami.rs @@ -89,7 +89,7 @@ impl GenericAmi<'_> { boot_order_mode: redfish::computer_system::BootOrderMode::Generic, callbacks: Some(callbacks), chassis: vec!["Self".into()], - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), bios_mode: redfish::computer_system::BiosMode::Generic, oem: redfish::computer_system::Oem::Generic, log_services: None, diff --git a/crates/bmc-mock/src/hw/hpe_proliant_dl380a_gen11.rs b/crates/bmc-mock/src/hw/hpe_proliant_dl380a_gen11.rs index 48a73f42c2..f7bbcceb3d 100644 --- a/crates/bmc-mock/src/hw/hpe_proliant_dl380a_gen11.rs +++ b/crates/bmc-mock/src/hw/hpe_proliant_dl380a_gen11.rs @@ -113,7 +113,7 @@ impl HpeProliantDl380aGen11<'_> { boot_order_mode: redfish::computer_system::BootOrderMode::Generic, callbacks: Some(callbacks), chassis: vec![system_id.into()], - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), bios_mode: redfish::computer_system::BiosMode::Generic, oem: redfish::computer_system::Oem::Generic, log_services: None, diff --git a/crates/bmc-mock/src/hw/lenovo_gb300_nvl.rs b/crates/bmc-mock/src/hw/lenovo_gb300_nvl.rs index f61c449bdd..8a05382027 100644 --- a/crates/bmc-mock/src/hw/lenovo_gb300_nvl.rs +++ b/crates/bmc-mock/src/hw/lenovo_gb300_nvl.rs @@ -181,7 +181,7 @@ impl LenovoGB300Nvl<'_> { redfish::computer_system::SingleSystemConfig { base_bios: Some(base_bios(system_id)), bios_mode: redfish::computer_system::BiosMode::Generic, - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), boot_order_mode: redfish::computer_system::BootOrderMode::Generic, chassis: vec!["Chassis_0".into()], eth_interfaces: Some(eth_interfaces), diff --git a/crates/bmc-mock/src/hw/nvidia_dgx_h100.rs b/crates/bmc-mock/src/hw/nvidia_dgx_h100.rs index 456b2c6300..5f1c687a93 100644 --- a/crates/bmc-mock/src/hw/nvidia_dgx_h100.rs +++ b/crates/bmc-mock/src/hw/nvidia_dgx_h100.rs @@ -179,7 +179,7 @@ impl NvidiaDgxH100<'_> { boot_order_mode: redfish::computer_system::BootOrderMode::ViaSettings, callbacks, chassis: vec!["BMC".into()], - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), bios_mode: redfish::computer_system::BiosMode::Generic, oem: redfish::computer_system::Oem::Generic, base_bios: Some(base_bios(system_id)), diff --git a/crates/bmc-mock/src/hw/supermicro_gb300_nvl.rs b/crates/bmc-mock/src/hw/supermicro_gb300_nvl.rs index 2477faad76..617609a0bc 100644 --- a/crates/bmc-mock/src/hw/supermicro_gb300_nvl.rs +++ b/crates/bmc-mock/src/hw/supermicro_gb300_nvl.rs @@ -178,7 +178,7 @@ impl SupermicroGB300Nvl<'_> { redfish::computer_system::SingleSystemConfig { base_bios: Some(base_bios(system_id)), bios_mode: redfish::computer_system::BiosMode::Generic, - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), boot_order_mode: redfish::computer_system::BootOrderMode::Generic, chassis: vec!["Chassis_0".into()], eth_interfaces: Some(eth_interfaces), diff --git a/crates/bmc-mock/src/hw/wiwynn_gb200_nvl.rs b/crates/bmc-mock/src/hw/wiwynn_gb200_nvl.rs index a0094440fd..5ff3a26f51 100644 --- a/crates/bmc-mock/src/hw/wiwynn_gb200_nvl.rs +++ b/crates/bmc-mock/src/hw/wiwynn_gb200_nvl.rs @@ -107,7 +107,7 @@ impl WiwynnGB200Nvl<'_> { boot_order_mode: redfish::computer_system::BootOrderMode::ViaSettings, callbacks, chassis: vec!["BMC_0".into()], - boot_options: Some(boot_options.into()), + boot_options: Some(boot_options), bios_mode: redfish::computer_system::BiosMode::Generic, oem: redfish::computer_system::Oem::Generic, base_bios: Some( diff --git a/crates/bmc-mock/src/redfish/computer_system.rs b/crates/bmc-mock/src/redfish/computer_system.rs index f599ef85e8..e03d1953a2 100644 --- a/crates/bmc-mock/src/redfish/computer_system.rs +++ b/crates/bmc-mock/src/redfish/computer_system.rs @@ -135,26 +135,6 @@ pub fn add_routes(r: Router, bmc_vendor: redfish::oem::BmcVendor) -> R ) } -pub enum BootOptionsConfig { - Options(Vec), - NullMembers, -} - -impl BootOptionsConfig { - fn as_options(&self) -> Option<&[redfish::boot_option::BootOption]> { - match self { - Self::Options(v) => Some(v), - Self::NullMembers => None, - } - } -} - -impl From> for BootOptionsConfig { - fn from(v: Vec) -> Self { - Self::Options(v) - } -} - pub struct SingleSystemConfig { pub id: Cow<'static, str>, pub eth_interfaces: Option>, @@ -164,7 +144,7 @@ pub struct SingleSystemConfig { pub boot_order_mode: BootOrderMode, pub callbacks: Option>, pub chassis: Vec>, - pub boot_options: Option, + pub boot_options: Option>, pub bios_mode: BiosMode, pub base_bios: Option, pub log_services: Option>, @@ -276,9 +256,7 @@ impl SingleSystemState { pub fn find_boot_option(&self, option_id: &str) -> Option<&redfish::boot_option::BootOption> { self.config .boot_options - .as_ref() - .and_then(|v| v.as_options()) - .into_iter() + .iter() .flatten() .find(|v| v.id == option_id) } @@ -305,9 +283,7 @@ impl SingleSystemState { .filter(|kind| { self.config .boot_options - .as_ref() - .and_then(|v| v.as_options()) - .into_iter() + .iter() .flatten() .any(|opt| opt.kind == *kind) }) @@ -319,9 +295,7 @@ impl SingleSystemState { overrides.first().and_then(|optref| { self.config .boot_options - .as_ref() - .and_then(|v| v.as_options()) - .into_iter() + .iter() .flatten() .find(|v| v.boot_reference() == optref) .map(|opt| opt.kind) @@ -331,8 +305,7 @@ impl SingleSystemState { .or_else(|| { self.config .boot_options - .as_ref() - .and_then(|v| v.as_options())? + .as_ref()? .first() .map(|opt| opt.kind) }) @@ -373,9 +346,7 @@ async fn get_system(State(state): State, Path(system_id): Path b = b.boot_order( &config .boot_options - .as_ref() - .and_then(|v| v.as_options()) - .into_iter() + .iter() .flatten() .map(|v| v.boot_reference()) .collect::>(), @@ -385,7 +356,9 @@ async fn get_system(State(state): State, Path(system_id): Path b = match config.oem { Oem::Generic => b, - Oem::NvidiaBluefield => b.oem_nvidia(&redfish::oem::nvidia::bluefield::resource()), + Oem::NvidiaBluefield => { + b.oem_nvidia(&redfish::oem::nvidia::bluefield::resource(&system_id)) + } }; let pcie_devices = config @@ -644,46 +617,34 @@ async fn get_boot_options_collection( let Some(boot_options) = &system_state.config.boot_options else { return http::not_found(); }; - match boot_options { - BootOptionsConfig::Options(boot_options) => { - let boot_options_order = match system_state.config.boot_order_mode { - BootOrderMode::DellOem => { - // Carbide relies that Dell sorts boot options in according to boot - // order. Code below simulates the same. - if let Some(boot_order) = system_state.boot_order_override() { - let mut indices = (0..boot_options.len()).collect::>(); - indices.sort_by_key(|&i| { - boot_order - .iter() - .enumerate() - .find(|(_, id)| *id == &boot_options[i].id) - .map(|(idx, _)| idx) - .unwrap_or(boot_options.len()) - }); - indices - } else { - (0..boot_options.len()).collect::>() - } - } - BootOrderMode::Generic | BootOrderMode::ViaSettings => { - (0..boot_options.len()).collect() - } - }; - let members = boot_options_order - .into_iter() - .map(|idx| { - redfish::boot_option::resource(&system_id, &boot_options[idx].id).entity_ref() - }) - .collect::>(); - redfish::boot_option::collection(&system_id) - .with_members(&members) - .into_ok_response() + let boot_options_order = match system_state.config.boot_order_mode { + BootOrderMode::DellOem => { + // Carbide relies that Dell sorts boot options in according to boot + // order. Code below simulates the same. + if let Some(boot_order) = system_state.boot_order_override() { + let mut indices = (0..boot_options.len()).collect::>(); + indices.sort_by_key(|&i| { + boot_order + .iter() + .enumerate() + .find(|(_, id)| *id == &boot_options[i].id) + .map(|(idx, _)| idx) + .unwrap_or(boot_options.len()) + }); + indices + } else { + (0..boot_options.len()).collect::>() + } } - BootOptionsConfig::NullMembers => redfish::boot_option::collection(&system_id) - .with_members(&[] as &[String]) - .patch(json!({"Members": null})) - .into_ok_response(), - } + BootOrderMode::Generic | BootOrderMode::ViaSettings => (0..boot_options.len()).collect(), + }; + let members = boot_options_order + .into_iter() + .map(|idx| redfish::boot_option::resource(&system_id, &boot_options[idx].id).entity_ref()) + .collect::>(); + redfish::boot_option::collection(&system_id) + .with_members(&members) + .into_ok_response() } async fn get_boot_option( diff --git a/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs b/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs index 170817bbcc..31d8e2593d 100644 --- a/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs +++ b/crates/bmc-mock/src/redfish/oem/nvidia/bluefield.rs @@ -19,7 +19,7 @@ use std::borrow::Cow; use std::sync::{Arc, Mutex}; use axum::Router; -use axum::extract::State; +use axum::extract::{Path, State}; use axum::response::Response; use axum::routing::{get, patch, post}; use mac_address::MacAddress; @@ -94,9 +94,9 @@ impl BluefieldState { } } -pub fn resource() -> redfish::Resource<'static> { +pub fn resource(system_id: &str) -> redfish::Resource<'static> { redfish::Resource { - odata_id: Cow::Borrowed("/redfish/v1/Systems/Bluefield/Oem/Nvidia"), + odata_id: Cow::Owned(format!("/redfish/v1/Systems/{system_id}/Oem/Nvidia")), odata_type: Cow::Borrowed("#NvidiaComputerSystem.v1_0_0.NvidiaComputerSystem"), // Neither BF2 nor BF-3 provide Id & Name in the resource We // simulate this behavior by removing these fields from final answer. @@ -104,32 +104,39 @@ pub fn resource() -> redfish::Resource<'static> { name: Cow::Borrowed(""), } } + const SYSTEMS_OEM_RESOURCE_DELETE_FIELDS: &[&str] = &["Id", "Name"]; pub fn add_routes(r: Router) -> Router { - r.route(&resource().odata_id, get(get_oem_nvidia)) - .route( - // TODO: This is BF-3 only. - &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), - ) + r.route( + "/redfish/v1/Systems/{system_id}/Oem/Nvidia", + get(get_oem_nvidia), + ) + .route( + // TODO: This is BF-3 only. + "/redfish/v1/Systems/{system_id}/Oem/Nvidia/Actions/HostRshim.Set", + post(hostrshim_set), + ) + .route( + // BF-3 OEM mode flip. Staged here and applied on the next power + // cycle, the same as real hardware. + "/redfish/v1/Systems/{system_id}/Oem/Nvidia/Actions/Mode.Set", + post(mode_set), + ) + .route( + "/redfish/v1/Managers/{manager_id}/Oem/Nvidia", + patch(patch_managers_oem_nvidia), + ) } async fn hostrshim_set() -> Response { json!({}).into_ok_response() } -async fn get_oem_nvidia(State(state): State) -> Response { +async fn get_oem_nvidia( + State(state): State, + axum::extract::Path(system_id): axum::extract::Path, +) -> Response { let redfish::oem::State::NvidiaBluefield(state) = state.oem_state else { return http::not_found(); }; @@ -140,7 +147,7 @@ async fn get_oem_nvidia(State(state): State) -> Response { } else { "DpuMode" }; - resource() + resource(&system_id) .json_patch() .patch(json!({ "Mode": mode, @@ -149,14 +156,21 @@ async fn get_oem_nvidia(State(state): State) -> Response { .delete_fields(SYSTEMS_OEM_RESOURCE_DELETE_FIELDS) .into_ok_response() } - BluefieldState::Bluefield4 => resource() + BluefieldState::Bluefield4 => resource(&system_id) .json_patch() .delete_fields(SYSTEMS_OEM_RESOURCE_DELETE_FIELDS) .into_ok_response(), } } -async fn patch_managers_oem_nvidia() -> Response { +async fn patch_managers_oem_nvidia( + State(state): State, + Path(manager_id): Path, +) -> Response { + if state.manager.find(&manager_id).is_none() { + return http::not_found(); + } + // This is used by enable_rshim_bmc() of libredfish client. json!({}).into_ok_response() } diff --git a/crates/site-explorer/src/lib.rs b/crates/site-explorer/src/lib.rs index a701073ad6..c4c7e9bb11 100644 --- a/crates/site-explorer/src/lib.rs +++ b/crates/site-explorer/src/lib.rs @@ -54,7 +54,7 @@ use model::site_explorer::{ EndpointExplorationError, EndpointExplorationReport, EndpointType, ExploredDpu, ExploredEndpoint, ExploredManagedHost, ExploredManagedSwitch, MachineExpectation, NicMode, PowerState, PreingestionState, Service, SiteExplorerLastRun, is_bf3_dpu_part_number, - is_bf3_supernic_part_number, is_bluefield_part_number, is_bluefield_system_id, + is_bf3_supernic_part_number, is_bluefield_part_number, is_bluefield_system, }; use sqlx::PgPool; use tokio::task::JoinSet; @@ -3571,10 +3571,7 @@ fn find_host_pf_mac_address(dpu_ep: &ExploredEndpoint) -> Result bool { - let has_bluefield_system = report - .systems - .first() - .is_some_and(|system| is_bluefield_system_id(&system.id)); + let has_bluefield_system = report.systems.first().is_some_and(is_bluefield_system); if !has_bluefield_system { return false; } From 253c59253444dafe5de5333845ef1c3ba7dd672c Mon Sep 17 00:00:00 2001 From: Dmitry Porokh Date: Tue, 7 Jul 2026 11:11:35 -0700 Subject: [PATCH 2/2] fix(api-model): recognize Card1 as DPU product chassis ID Signed-off-by: Dmitry Porokh --- crates/api-model/src/site_explorer/mod.rs | 49 ++++++++++------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/crates/api-model/src/site_explorer/mod.rs b/crates/api-model/src/site_explorer/mod.rs index 02229d50fc..2212df9143 100644 --- a/crates/api-model/src/site_explorer/mod.rs +++ b/crates/api-model/src/site_explorer/mod.rs @@ -626,12 +626,6 @@ impl ExploredDpu { .report .create_temporary_dmi_data(serial_number, vendor, model); - let chassis_map = self - .report - .chassis - .iter() - .map(|x| (x.id.as_str(), x)) - .collect::>(); let inventory_map = self.report.get_inventory_map(); let dpu_data = DpuData { @@ -639,28 +633,20 @@ impl ExploredDpu { .host_pf_mac_address .ok_or(ModelError::MissingArgument("Missing base mac"))? .to_string(), - part_number: chassis_map - .get("Card1") - .and_then(|value| chassis_part_number(value)) - .or_else(|| { - self.report - .chassis - .iter() - .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) - .find_map(chassis_part_number) - }) + part_number: self + .report + .chassis + .iter() + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(chassis_part_number) .unwrap_or("") .to_string(), - part_description: chassis_map - .get("Card1") - .and_then(|value| chassis_model(value)) - .or_else(|| { - self.report - .chassis - .iter() - .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) - .find_map(chassis_model) - }) + part_description: self + .report + .chassis + .iter() + .filter(|chassis| is_dpu_product_chassis_id(&chassis.id)) + .find_map(chassis_model) .unwrap_or("") .to_string(), firmware_version: inventory_map @@ -1777,9 +1763,16 @@ pub fn is_bf4_dpu_part_number(part_number: &str) -> bool { || normalized_part_number.starts_with("900-9d4a4") } -/// Whether a DPU BMC chassis member carries the card product identity (part/serial). +/// Whether a DPU BMC chassis member carries the card product identity +/// (part/model/serial). +/// +/// Older Redfish reports publish this identity on `Card1`; newer BF4 firmware may +/// instead publish it on the integrated BMC chassis (`Bluefield_BMC` or +/// `BlueField_BMC_0`). These IDs are expected to be mutually exclusive as product +/// identity sources in real reports, so callers can select the first matching +/// chassis. fn is_dpu_product_chassis_id(id: &str) -> bool { - matches!(id, "Bluefield_BMC" | "BlueField_BMC_0") + matches!(id, "Card1" | "Bluefield_BMC" | "BlueField_BMC_0") } /// Whether a Redfish ComputerSystem id identifies a BlueField DPU system.