Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/bmc-explorer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ path = "tests/integration/main.rs"
# [local-dependencies]
# DO NOT PUT DEPENDENCIES OTHER THAN LOCAL DEPS HERE, THEY SHOULD ALL HAVE 'path =' IN THEM.
bmc-vendor = { path = "../bmc-vendor" }
bmc-mock = { path = "../bmc-mock", optional = true }
carbide-network = { path = "../network", default-features = false }
carbide-api-model = { path = "../api-model", default-features = false }
# DO NOT PUT DEPENDENCIES OTHER THAN LOCAL DEPS HERE, THEY SHOULD ALL HAVE 'path =' IN THEM.
Expand Down Expand Up @@ -70,7 +71,7 @@ tracing = { workspace = true }
default = []
# Exposes src/test_support.rs (test-only helpers) without shipping them in a
# production build.
test-support = []
test-support = ["dep:bmc-mock"]

[dev-dependencies]
axum = { workspace = true }
Expand Down
90 changes: 89 additions & 1 deletion crates/bmc-explorer/src/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@
//! compiles into a production build.

use std::sync::Arc;
use std::time::Duration;

use bmc_mock::test_support::TestBmc;
use bmc_mock::test_support::axum_http_client::Error as TestBmcError;
use bmc_mock::{DpuMachineInfo, DpuSettings, HostHardwareType, HostMachineInfo, MachineInfo};
use model::site_explorer::EndpointExplorationReport;
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, is_bluefield_system_id};
use crate::{
Config, Error, ErrorClass, build_chassis_explore_config, hw, hw_type, is_bluefield_system_id,
nv_generate_exploration_report,
};

/// Resolve the [`hw::HwType`] for an endpoint, running only the chassis +
/// computer-system exploration that detection depends on.
Expand Down Expand Up @@ -78,3 +86,83 @@ pub async fn detect_hw_type<B: Bmc>(

Ok(hw_type(&root, &explored_system, &explored_chassis))
}

pub type MockExplorerError = Error<TestBmc>;

#[derive(Clone, Debug)]
pub struct GeneratedEndpointReport<T> {
pub machine_info: T,
pub report: EndpointExplorationReport,
}

#[derive(Clone, Debug)]
pub struct GeneratedManagedHostReports {
pub host: GeneratedEndpointReport<HostMachineInfo>,
pub dpus: Vec<GeneratedEndpointReport<DpuMachineInfo>>,
}

pub async fn generate_report_for_machine(
machine_info: MachineInfo,
) -> Result<EndpointExplorationReport, MockExplorerError> {
let bmc = bmc_mock::test_support::bmc_for_machine(machine_info).await;
nv_generate_exploration_report(bmc.service_root, &explorer_config()).await
}

pub async fn generate_managed_host_reports(
hw_type: HostHardwareType,
) -> Result<GeneratedManagedHostReports, MockExplorerError> {
generate_managed_host_reports_from_info(host_info_for_hw_type(hw_type)).await
}

pub async fn generate_managed_host_reports_from_info(
host_info: HostMachineInfo,
) -> Result<GeneratedManagedHostReports, MockExplorerError> {
let host_report = generate_report_for_machine(MachineInfo::Host(host_info.clone())).await?;
let mut dpus = Vec::with_capacity(host_info.dpus.len());

for dpu_info in host_info.dpus.iter().cloned() {
let report = generate_report_for_machine(MachineInfo::Dpu(dpu_info.clone())).await?;
dpus.push(GeneratedEndpointReport {
machine_info: dpu_info,
report,
});
}

Ok(GeneratedManagedHostReports {
host: GeneratedEndpointReport {
machine_info: host_info,
report: host_report,
},
dpus,
})
}

fn host_info_for_hw_type(hw_type: HostHardwareType) -> HostMachineInfo {
let dpu_count = hw_type.fixed_number_of_dpu().unwrap_or(0);
let mut pool = bmc_mock::test_support::TEST_MAC_POOL.lock().unwrap();
let hw_mac_addr_pool = pool.allocate_range_config().unwrap();
let dpus = (0..dpu_count)
.map(|_| DpuMachineInfo::new(hw_type, &mut pool, DpuSettings::default()))
.collect();

HostMachineInfo::new(hw_type, dpus, &mut pool, hw_mac_addr_pool)
}

fn explorer_config() -> Config<'static, TestBmc> {
Config {
boot_interface_mac: None,
error_classifier: &error_classifier,
retry_timeout: Duration::from_millis(0),
}
}

fn error_classifier(err: &TestBmcError) -> Option<ErrorClass> {
match err {
TestBmcError::InvalidResponse { status, .. } => match status.as_u16() {
404 => Some(ErrorClass::NotFound),
500 => Some(ErrorClass::InternalServerError),
_ => None,
},
_ => None,
}
}
22 changes: 20 additions & 2 deletions crates/bmc-explorer/tests/integration/bluefield4_explore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/
use bmc_explorer::nv_generate_exploration_report;
use bmc_mock::{DpuMachineInfo, DpuSettings, HostHardwareType, test_support};
use bmc_mock::{DpuMachineInfo, DpuSettings, HostHardwareType, MachineInfo, test_support};
use mac_address::MacAddress;
use model::site_explorer::EndpointType;
use tokio::test;
Expand Down Expand Up @@ -98,7 +98,16 @@ async fn explore_bluefield4_and_generate_machine_id_from_bluefield_bmc_chassis_s

#[test]
async fn explore_b4240v_and_generate_machine_id() {
let h = test_support::nvidia_dgx_vr_bluefield4_dpu_bmc(DpuSettings::default()).await;
let host_mac_address = MacAddress::new([0x02, 0x00, 0x00, 0xbf, 0x14, 0x02]);
let h = test_support::bmc_for_machine(MachineInfo::Dpu(DpuMachineInfo {
hw_type: HostHardwareType::NvidiaDgxVr,
bmc_mac_address: MacAddress::new([0x02, 0x00, 0x00, 0xbf, 0x14, 0x01]),
host_mac_address,
oob_mac_address: MacAddress::new([0x02, 0x00, 0x00, 0xbf, 0x14, 0x03]),
serial: "MT2610604VN5".to_string(),
settings: DpuSettings::default(),
}))
.await;
let mut report = nv_generate_exploration_report(h.service_root, &common::explorer_config())
.await
.expect("B4240V exploration should succeed");
Expand All @@ -108,6 +117,15 @@ async fn explore_b4240v_and_generate_machine_id() {
assert!(report.chassis.iter().any(|chassis| {
chassis.id == "BlueField_BMC_0" && chassis.model.as_deref() == Some("B4240V")
}));
assert_eq!(
report
.systems
.first()
.expect("systems must be present")
.base_mac
.map(|mac| mac.to_mac()),
Some(host_mac_address)
);

let machine_id = report
.generate_machine_id(false)
Expand Down
28 changes: 28 additions & 0 deletions crates/bmc-explorer/tests/integration/vera_rubin_explore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,34 @@ async fn explore_nvidia_dgx_vr_and_generate_machine_id() {
assert_eq!(report.endpoint_type, EndpointType::Bmc);
assert!(!report.systems.is_empty(), "systems must be present");
assert!(!report.chassis.is_empty(), "chassis must be present");
assert!(
report.systems[0].pcie_devices.is_empty(),
"VR host pairing should use the BlueField chassis inventory, not host PCIe devices"
);

let bluefield_chassis = report
.chassis
.iter()
.find(|chassis| chassis.id == "BlueField_0")
.expect("VR host report should expose the attached BF4 as BlueField_0 chassis");
assert_eq!(
bluefield_chassis.part_number.as_deref(),
Some("900-9D4A4-00CB-TS4")
);
assert!(
bluefield_chassis
.serial_number
.as_deref()
.is_some_and(|serial| !serial.is_empty()),
"BlueField_0 chassis should carry the DPU serial for host/DPU pairing"
);
assert!(
bluefield_chassis
.network_adapters
.iter()
.any(|adapter| adapter.id == "BlueField_NIC_0"),
"BlueField_0 chassis should expose the real VR BlueField_NIC_0 adapter path"
);

let machine_id = report
.generate_machine_id(true)
Expand Down
52 changes: 52 additions & 0 deletions crates/bmc-mock/src/hw/bluefield4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ 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";
const NETWORK_ADAPTER_ID: &'static str = "BlueField_NIC_0";
const NETWORK_DEVICE_FUNCTION_ID: &'static str = "0";
const NDF0_TO_BASE_MAC_OFFSET: u64 = 0x10;

fn sensor_layout() -> redfish::sensor::Layout {
// The older BF4 layout exposed these sensors below Card1. Newer
Expand All @@ -70,6 +73,7 @@ impl Bluefield4<'_> {
model: Some("NA".into()),
part_number: Some(self.part_number().into()),
serial_number: Some(self.product_serial_number.to_string().into()),
network_adapters: Some(self.network_adapters()),
pcie_devices: Some(vec![]),
sensors: Some(redfish::sensor::generate_chassis_sensors(
"BlueField_0",
Expand Down Expand Up @@ -113,6 +117,54 @@ impl Bluefield4<'_> {
}
}

fn network_adapters(&self) -> Vec<redfish::network_adapter::NetworkAdapter> {
let function = redfish::network_device_function::builder(
&redfish::network_device_function::chassis_resource(
Self::SYSTEM_ID,
Self::NETWORK_ADAPTER_ID,
Self::NETWORK_DEVICE_FUNCTION_ID,
),
)
.ethernet(json!({
"PermanentMACAddress": Self::ndf0_permanent_mac(self.host_mac_address),
}))
.build();

vec![
redfish::network_adapter::builder_from_nic(
&redfish::network_adapter::chassis_resource(
Self::SYSTEM_ID,
Self::NETWORK_ADAPTER_ID,
),
&self.host_nic(),
)
.network_device_functions(
&redfish::network_device_function::chassis_collection(
Self::SYSTEM_ID,
Self::NETWORK_ADAPTER_ID,
),
vec![function],
)
.status(redfish::resource::Status::Ok)
.build(),
]
}

fn ndf0_permanent_mac(host_mac_address: MacAddress) -> MacAddress {
Self::offset_mac(host_mac_address, Self::NDF0_TO_BASE_MAC_OFFSET)
}

fn offset_mac(mac_address: MacAddress, offset: u64) -> MacAddress {
let bytes = mac_address.bytes();
let value = u64::from_be_bytes([
0, 0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5],
])
.checked_add(offset)
.expect("BF4 NDF0 MAC offset must not overflow");
let bytes = value.to_be_bytes();
MacAddress::new([bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7]])
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub fn system_config(&self, callbacks: Arc<dyn Callbacks>) -> redfish::computer_system::Config {
let system_id = Self::SYSTEM_ID;
redfish::computer_system::Config {
Expand Down
62 changes: 62 additions & 0 deletions crates/bmc-mock/src/hw/dgx_vr_nvl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub struct DgxVrNvl<'a> {
}

impl DgxVrNvl<'_> {
const BLUEFIELD_CHASSIS_ID: &'static str = "BlueField_0";
const BLUEFIELD_NIC_ID: &'static str = "BlueField_NIC_0";
const BLUEFIELD_PCIE_DEVICE_ID: &'static str = "BlueField_0";

pub fn manager_config(&self) -> redfish::manager::Config {
let bmc_manager_id = "BMC_0";
let bmc_eth_builder = |eth| {
Expand Down Expand Up @@ -154,6 +158,7 @@ impl DgxVrNvl<'_> {
leak_detectors: None,
..redfish::chassis::SingleChassisConfig::defaults()
},
self.bluefield_chassis_config(),
redfish::chassis::SingleChassisConfig {
id: "HGX_Chassis_0".into(),
chassis_type: "Zone".into(),
Expand All @@ -169,6 +174,63 @@ impl DgxVrNvl<'_> {
}
}

fn bluefield_chassis_config(&self) -> redfish::chassis::SingleChassisConfig {
let bf4 = self.dpu.host_nic();
redfish::chassis::SingleChassisConfig {
id: Self::BLUEFIELD_CHASSIS_ID.into(),
chassis_type: "Component".into(),
manufacturer: Some("NVIDIA".into()),
model: bf4.model.clone(),
part_number: bf4.part_number.clone(),
serial_number: bf4.serial_number.clone(),
network_adapters: Some(vec![self.bluefield_network_adapter()]),
pcie_devices: Some(vec![
redfish::pcie_device::builder_from_nic(
&redfish::pcie_device::chassis_resource(
Self::BLUEFIELD_CHASSIS_ID,
Self::BLUEFIELD_PCIE_DEVICE_ID,
),
&bf4,
)
.status(redfish::resource::Status::Ok)
.build(),
]),
sensors: None,
leak_detectors: None,
..redfish::chassis::SingleChassisConfig::defaults()
}
}

fn bluefield_network_adapter(&self) -> redfish::network_adapter::NetworkAdapter {
let network_device_functions = ["0", "1"]
.into_iter()
.map(|id| {
redfish::network_device_function::builder(
&redfish::network_device_function::chassis_resource(
Self::BLUEFIELD_CHASSIS_ID,
Self::BLUEFIELD_NIC_ID,
id,
),
)
.build()
})
.collect();

redfish::network_adapter::builder(&redfish::network_adapter::chassis_resource(
Self::BLUEFIELD_CHASSIS_ID,
Self::BLUEFIELD_NIC_ID,
))
.network_device_functions(
&redfish::network_device_function::chassis_collection(
Self::BLUEFIELD_CHASSIS_ID,
Self::BLUEFIELD_NIC_ID,
),
network_device_functions,
)
.status(redfish::resource::Status::Ok)
.build()
}

pub fn update_service_config(&self) -> redfish::update_service::UpdateServiceConfig {
redfish::update_service::UpdateServiceConfig {
firmware_inventory: vec![],
Expand Down
1 change: 1 addition & 0 deletions crates/site-explorer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ thiserror = { workspace = true }
version-compare = { workspace = true }

[dev-dependencies]
bmc-explorer = { path = "../bmc-explorer", features = ["test-support"] }
bmc-mock = { path = "../bmc-mock" }
opentelemetry-prometheus = { workspace = true }
prometheus = { workspace = true }
Expand Down
Loading
Loading