diff --git a/crates/api-model/src/site_explorer/mod.rs b/crates/api-model/src/site_explorer/mod.rs index 913243eaa9..0d67a314d7 100644 --- a/crates/api-model/src/site_explorer/mod.rs +++ b/crates/api-model/src/site_explorer/mod.rs @@ -929,17 +929,32 @@ impl EndpointExplorationReport { } } + fn machine_id_serial_number(&self) -> Option<&str> { + self.systems + .first() + .and_then(|system| system.serial_number.as_deref().map(str::trim)) + .filter(|sn| !sn.is_empty()) + .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 + // 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()) + })? + }) + } + /// Tries to generate and store a MachineId for the discovered endpoint if /// enough data for generation is available pub fn generate_machine_id( &mut self, force_predicted_host: bool, ) -> ModelResult> { - if let Some(serial_number) = self - .systems - .first() - .and_then(|system| system.serial_number.as_ref()) - { + if let Some(serial_number) = self.machine_id_serial_number() { let vendor = self .systems .first() diff --git a/crates/bmc-explorer/tests/bluefield4_explore.rs b/crates/bmc-explorer/tests/bluefield4_explore.rs new file mode 100644 index 0000000000..9360a4c540 --- /dev/null +++ b/crates/bmc-explorer/tests/bluefield4_explore.rs @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +mod common; + +use bmc_explorer::nv_generate_exploration_report; +use bmc_mock::{DpuMachineInfo, DpuSettings, HostHardwareType, test_support}; +use mac_address::MacAddress; +use model::site_explorer::EndpointType; +use tokio::test; + +#[test] +async fn explore_bluefield4_and_generate_machine_id_from_bluefield_bmc_chassis_serial() { + let h = test_support::dell_poweredge_r760_bluefield4_bmc(DpuMachineInfo { + hw_type: HostHardwareType::DellPowerEdgeR760Bf4, + bmc_mac_address: MacAddress::new([0x02, 0x00, 0x00, 0xbf, 0x04, 0x01]), + host_mac_address: MacAddress::new([0x02, 0x00, 0x00, 0xbf, 0x04, 0x02]), + oob_mac_address: MacAddress::new([0x02, 0x00, 0x00, 0xbf, 0x04, 0x03]), + serial: "MT2610604VN4".to_string(), + settings: DpuSettings::default(), + }) + .await; + let mut report = nv_generate_exploration_report(h.service_root, &common::explorer_config()) + .await + .unwrap(); + + assert_eq!(report.endpoint_type, EndpointType::Bmc); + 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!( + system.serial_number.is_none(), + "BF4 Redfish reports the usable serial on chassis, not system" + ); + + let 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:?}" + ); + let bmc_chassis_serial = report + .chassis + .iter() + .find(|chassis| chassis.id == "Bluefield_BMC") + .and_then(|chassis| chassis.serial_number.as_deref()); + assert_eq!(bmc_chassis_serial, Some("MT2610604VN4")); + + assert!( + report + .service + .iter() + .any(|service| service.id == "FirmwareInventory"), + "firmware inventory service must be present" + ); + + let machine_id = *report + .generate_machine_id(false) + .expect("BF4 report should have enough collected data for machine ID") + .expect("BF4 report should generate a DPU machine ID"); + + assert!(machine_id.machine_type().is_dpu()); + assert_eq!( + machine_id.to_string(), + "fm100dsje1vlqbfpt0vn3hkuijsm07hpd78ctlfhrje2q8ssnj20ke32rdg" + ); + assert_eq!(report.machine_id, Some(machine_id)); +} diff --git a/crates/bmc-mock/src/test_support/mod.rs b/crates/bmc-mock/src/test_support/mod.rs index d5e7471dbe..6d61d972f3 100644 --- a/crates/bmc-mock/src/test_support/mod.rs +++ b/crates/bmc-mock/src/test_support/mod.rs @@ -199,6 +199,17 @@ pub async fn dell_poweredge_r750_bluefield3_bmc(settings: DpuSettings) -> TestBm .await } +pub async fn dell_poweredge_r760_bluefield4_bmc(dpu: DpuMachineInfo) -> TestBmcHandle { + let machine_info = MachineInfo::Dpu(dpu); + test_bmc(machine_router( + &machine_info, + Arc::new(NoopCallbacks), + "test-dpu-id".to_string(), + false, + )) + .await +} + pub async fn generic_ami_bmc() -> TestBmcHandle { test_bmc(machine_router( &host_info(HostHardwareType::GenericAmi),