From 2fd00d9e91db2777dac603290d71bb754441a6ed Mon Sep 17 00:00:00 2001 From: spydaNVIDIA Date: Tue, 14 Jul 2026 09:14:44 -0700 Subject: [PATCH] fix: add support to bmc-explorer for Delta powershelves (#3455) fix: add support to bmc-explorer for Delta powershelves - [ ] **Add** - New feature or capability - [ ] **Change** - Changes in existing functionality - [ ] **Fix** - Bug fixes - [ ] **Remove** - Removed features or deprecated functionality - [ ] **Internal** - Internal changes (refactoring, tests, docs, etc.) - [ ] **This PR contains breaking changes** - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] No testing required (docs, internal refactor, etc.) --- Cargo.lock | 20 +- Cargo.toml | 2 +- crates/bmc-explorer/Cargo.toml | 3 +- crates/bmc-explorer/src/chassis.rs | 218 +++++++++++++++++- crates/bmc-explorer/src/hw/mod.rs | 3 + crates/bmc-explorer/src/lib.rs | 71 ++++++ .../bmc-explorer/tests/powershelf_explore.rs | 92 +++++++- crates/bmc-mock/src/bmc_state.rs | 5 + crates/bmc-mock/src/hw/delta_power_shelf.rs | 117 ++++++++++ crates/bmc-mock/src/hw/mod.rs | 3 + crates/bmc-mock/src/lib.rs | 4 + crates/bmc-mock/src/machine_info.rs | 59 ++++- crates/bmc-mock/src/mock_machine_router.rs | 6 +- .../bmc-mock/src/redfish/computer_system.rs | 5 + crates/bmc-mock/src/redfish/oem/mod.rs | 5 + crates/bmc-mock/src/redfish/power_supply.rs | 16 ++ crates/bmc-mock/src/redfish/service_root.rs | 12 +- crates/bmc-mock/src/test_support/mod.rs | 27 +++ crates/machine-a-tron/src/host_machine.rs | 1 + crates/machine-a-tron/src/machine_a_tron.rs | 2 +- .../tests/integration/site_explorer.rs | 1 + 21 files changed, 650 insertions(+), 22 deletions(-) create mode 100644 crates/bmc-mock/src/hw/delta_power_shelf.rs diff --git a/Cargo.lock b/Cargo.lock index 9da4e717c7..fc21c85717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7541,9 +7541,9 @@ dependencies = [ [[package]] name = "nv-redfish" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f4a01b88cbb0e3b30e0bded7f11d9b8f9bd6553df387a1878af975d77ec4d" +checksum = "8f381d59345a3974aacc20e44148621457b5002d32fe5c9850fbce9c11c22f90" dependencies = [ "futures-core", "futures-util", @@ -7558,9 +7558,9 @@ dependencies = [ [[package]] name = "nv-redfish-bmc-http" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c063e5ccfb788c80d2ef292353e867b7c157ec1a7d9358bb576048b9537bcb11" +checksum = "67abd866b1c01bd615cd18afc96305cd436076a16d7a76e53b25bed7fab8422b" dependencies = [ "futures-core", "futures-util", @@ -7582,9 +7582,9 @@ dependencies = [ [[package]] name = "nv-redfish-core" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce8bb8d42d73917156f22350b07dfc8d49aafac8b0a2bb0ec63e874d0c3d715" +checksum = "e5ec054643796ede5865fff5f0e3913ddc2bf7f954975f6f512b40d78be07db7" dependencies = [ "futures-core", "futures-io", @@ -7597,9 +7597,9 @@ dependencies = [ [[package]] name = "nv-redfish-csdl-compiler" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a6f5d4b81f9551289df0700879aa31b5056441aaddf107b280e99b6dba7c77d" +checksum = "34c88dbe8742e28c0e733596661d9c4605ef6cab438cf2c54d1ceb9577053f9a" dependencies = [ "clap", "clap_derive", @@ -7615,9 +7615,9 @@ dependencies = [ [[package]] name = "nv-redfish-schema" -version = "0.10.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d95a042ca3ee9446ee137ff814035501a6934be98a15714ced1f78fcb242fda" +checksum = "bc0a401a80543b6d7c27f28614bc70aa37103cd95b03b0746419bd87e43977f3" dependencies = [ "glob", ] diff --git a/Cargo.toml b/Cargo.toml index e151b93908..02e093e34a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ tracing-appender = "0.2.4" tracing-opentelemetry = "0.32.1" # NV-Redfish -nv-redfish = { version = "0.10.4" } +nv-redfish = { version = "0.11.0" } ######## # MARK: - Pinned packages that we can't upgrade due to conflicts or just bugs diff --git a/crates/bmc-explorer/Cargo.toml b/crates/bmc-explorer/Cargo.toml index 91248eba3b..831cc5cfd3 100644 --- a/crates/bmc-explorer/Cargo.toml +++ b/crates/bmc-explorer/Cargo.toml @@ -55,10 +55,12 @@ nv-redfish = { workspace = true, features = [ "oem-hpe", "oem-ami", "oem-liteon", + "oem-delta", "bmc-http", ] } regex = { workspace = true } lazy_static = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } @@ -74,7 +76,6 @@ bmc-mock = { path = "../bmc-mock" } # Self dev-dependency so the crate's own integration tests get the test-support # helpers; applies only to test/bench targets, never the production library. bmc-explorer = { path = ".", features = ["test-support"] } -serde_json = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [lints] diff --git a/crates/bmc-explorer/src/chassis.rs b/crates/bmc-explorer/src/chassis.rs index 34bd878ffe..917dedfce3 100644 --- a/crates/bmc-explorer/src/chassis.rs +++ b/crates/bmc-explorer/src/chassis.rs @@ -19,9 +19,11 @@ use std::convert::identity; use std::fmt; use itertools::Itertools; -use model::site_explorer::{Chassis, PowerState as ModelPowerState}; +use model::site_explorer::{ + Chassis, ComputerSystem as ModelComputerSystem, PowerState as ModelPowerState, +}; use nv_redfish::assembly::Model as AssemblyModel; -use nv_redfish::chassis::Chassis as NvChassis; +use nv_redfish::chassis::{Chassis as NvChassis, PowerSupply as NvPowerSupply}; use nv_redfish::core::ODataId; use nv_redfish::hardware_id::{Manufacturer, Model}; use nv_redfish::pcie_device::PcieDevice; @@ -107,6 +109,84 @@ impl ExploredChassisCollection { }) } + /// Detects a Delta power shelf. Delta BMCs expose neither a `Vendor` in the + /// service root nor a `/redfish/v1/Systems` collection, so classification + /// relies on a Delta manufacturer on the power-shelf chassis (id "chassis" + /// or "powershelf"). The manufacturer gate is what distinguishes Delta from + /// the Lite-On power shelf, which shares the generic "powershelf" chassis + /// id. + pub fn is_delta_powershelf(&self) -> bool { + self.members.iter().any(|m| { + is_delta_powershelf_chassis( + m.chassis.id().into_inner(), + m.chassis + .hardware_id() + .manufacturer + .as_ref() + .map(|mfg| **mfg), + ) + }) + } + + /// Aggregate power state across all Delta PSUs found on the chassis members. + /// Delta reports commanded PSU on/off state under + /// `Oem.deltaenergysystems.Power`. + pub fn delta_power_state(&self) -> ModelPowerState { + let supplies: Vec<&DeltaPowerSupply> = self + .members + .iter() + .filter_map(|m| m.oem_delta_power_supplies.as_ref()) + .flatten() + .collect(); + + let state = powershelf_power_state(supplies.iter().map(|ps| ps.power_state)); + if state == ModelPowerState::Unknown && !supplies.is_empty() { + let detail = supplies + .iter() + .map(|ps| format!("{}:{:?}", ps.id, ps.power_state)) + .join(", "); + tracing::warn!("Delta power shelf power state is unknown: {detail}"); + } + state + } + + /// Synthesizes a [`ModelComputerSystem`] for a power shelf that does not + /// expose a Redfish `ComputerSystem`. Identity is taken from the primary + /// power-shelf chassis member (mirrors the libredfish Delta path). + pub fn synthesized_powershelf_system(&self) -> ModelComputerSystem { + let member = self + .members + .iter() + .find(|m| { + let id = m.chassis.id().into_inner(); + id == "chassis" || id == "powershelf" + }) + .or_else(|| self.members.first()); + + let (id, manufacturer, model, serial_number) = member + .map(|m| { + let hw_id = m.chassis.hardware_id(); + ( + m.chassis.id().to_string(), + hw_id.manufacturer.map(|v| v.to_string()), + hw_id.model.map(|v| v.to_string()), + hw_id + .serial_number + .map(|v| v.to_string().trim().to_string()), + ) + }) + .unwrap_or_default(); + + ModelComputerSystem { + id, + manufacturer, + model, + serial_number, + power_state: self.delta_power_state(), + ..Default::default() + } + } + pub fn is_gb300(&self) -> bool { self.members.iter().any(|m| { m.chassis.hardware_id().manufacturer == Some(Manufacturer::new("NVIDIA")) @@ -179,6 +259,7 @@ pub struct ExploredChassis { pub network_adapters: ExploredNetworkAdapterCollection, pub assembly_sn: Option, pub oem_liteon_power_supplies: Option>, + pub oem_delta_power_supplies: Option>, } impl ExploredChassis { @@ -234,11 +315,38 @@ impl ExploredChassis { None }; + // Delta power shelves carry the commanded PSU on/off state as an OEM + // extension (`Oem.deltaenergysystems.Power`) on the standard + // PowerSubsystem power supplies. Only fetch it for Delta chassis to + // avoid extra requests on unrelated hardware. + let oem_delta_power_supplies = if chassis + .hardware_id() + .manufacturer + .as_ref() + .is_some_and(|mfg| mfg.as_ref().to_lowercase().contains("delta")) + { + let supplies = chassis + .power_supplies() + .await + .map_err(Error::nv_redfish("Delta power supplies"))?; + let power_supplies = supplies + .iter() + .map(|ps| DeltaPowerSupply { + id: ps.id().to_string(), + power_state: delta_psu_power_on(ps), + }) + .collect(); + Some(power_supplies) + } else { + None + }; + Ok(Self { chassis, network_adapters, assembly_sn, oem_liteon_power_supplies, + oem_delta_power_supplies, }) } @@ -290,6 +398,56 @@ pub struct LiteOnPowerSupply { pub power_state: Option, } +pub struct DeltaPowerSupply { + pub id: String, + pub power_state: Option, +} + +/// Reads a Delta PSU's commanded on/off state from its OEM extension. +/// +/// Delta power shelves report this as `Oem.deltaenergysystems.Power` (a bool) +/// on the standard `PowerSupply` resource, exposed via nv-redfish's typed +/// [`oem_delta`](NvPowerSupply::oem_delta) accessor. Returns `None` when the +/// Delta extension is absent, its `Power` flag is unset, or the OEM payload +/// fails to parse. +fn delta_psu_power_on(ps: &NvPowerSupply) -> Option { + match ps.oem_delta() { + Ok(oem) => oem.and_then(|d| d.power()), + Err(e) => { + tracing::warn!("failed to parse Delta OEM power supply data: {e:?}"); + None + } + } +} + +/// Delta power-shelf identity gate: a power-shelf chassis (id `chassis` or +/// `powershelf`) whose manufacturer identifies as Delta. This is what +/// distinguishes a Delta shelf from the Lite-On shelf, which shares the generic +/// `powershelf` chassis id but reports a different manufacturer. Split out so +/// the gate can be exercised in unit tests without a live BMC. +fn is_delta_powershelf_chassis(chassis_id: &str, manufacturer: Option<&str>) -> bool { + (chassis_id == "chassis" || chassis_id == "powershelf") + && manufacturer.is_some_and(|mfg| mfg.to_lowercase().contains("delta")) +} + +/// Aggregates per-PSU commanded on/off states into a single power-shelf state. +/// +/// All PSUs reporting `Some(true)` means the shelf is on; all `Some(false)` +/// means off. An empty set, a mix, or any `None` yields `Unknown`. +fn powershelf_power_state(states: impl Iterator>) -> ModelPowerState { + let states: Vec> = states.collect(); + if states.is_empty() { + return ModelPowerState::Unknown; + } + if states.iter().all(|v| *v == Some(true)) { + ModelPowerState::On + } else if states.iter().all(|v| *v == Some(false)) { + ModelPowerState::Off + } else { + ModelPowerState::Unknown + } +} + pub struct LiteOnSuppliesState<'a>(&'a [LiteOnPowerSupply]); impl fmt::Display for LiteOnSuppliesState<'_> { @@ -320,3 +478,59 @@ impl LiteOnSuppliesState<'_> { } } } + +#[cfg(test)] +mod tests { + use super::{ModelPowerState, is_delta_powershelf_chassis, powershelf_power_state}; + + // is_delta_powershelf_chassis gates Delta detection: a power-shelf chassis + // id ("chassis"/"powershelf") AND a Delta manufacturer. The manufacturer + // check is case-insensitive and substring-based, and is what separates a + // Delta shelf from a Lite-On shelf sharing the "powershelf" chassis id. + #[test] + fn is_delta_powershelf_chassis_gates_on_id_and_manufacturer() { + let cases: [(&str, Option<&str>, bool); 9] = [ + // Delta manufacturer on either accepted power-shelf chassis id. + ("chassis", Some("DELTA"), true), + ("powershelf", Some("Delta"), true), + // Case-insensitive, substring match on the manufacturer. + ("chassis", Some("delta electronics"), true), + ("powershelf", Some("Delta Energy Systems"), true), + // Right manufacturer but a non-power-shelf chassis id is ignored. + ("Card1", Some("DELTA"), false), + ("Baseboard", Some("delta"), false), + // Power-shelf chassis id but a different (or missing) manufacturer. + ("powershelf", Some("Lite-On"), false), + ("chassis", Some("NVIDIA"), false), + ("chassis", None, false), + ]; + for (id, mfg, expected) in cases { + assert_eq!( + is_delta_powershelf_chassis(id, mfg), + expected, + "id={id:?} manufacturer={mfg:?}" + ); + } + } + + // powershelf_power_state collapses per-PSU flags: all-on => On, all-off => + // Off, and empty / mixed / unknown => Unknown. + #[test] + fn powershelf_power_state_aggregates_psu_flags() { + let cases: [(&[Option], ModelPowerState); 6] = [ + (&[], ModelPowerState::Unknown), + (&[Some(true), Some(true)], ModelPowerState::On), + (&[Some(false), Some(false)], ModelPowerState::Off), + (&[Some(true), Some(false)], ModelPowerState::Unknown), + (&[Some(true), None], ModelPowerState::Unknown), + (&[None], ModelPowerState::Unknown), + ]; + for (states, expected) in cases { + assert_eq!( + powershelf_power_state(states.iter().copied()), + expected, + "states: {states:?}" + ); + } + } +} diff --git a/crates/bmc-explorer/src/hw/mod.rs b/crates/bmc-explorer/src/hw/mod.rs index a02d516a7f..b1d6fb3fb1 100644 --- a/crates/bmc-explorer/src/hw/mod.rs +++ b/crates/bmc-explorer/src/hw/mod.rs @@ -45,6 +45,7 @@ pub enum HwType { Supermicro, Viking, LiteonPowerShelf, + DeltaPowerShelf, NvSwitch, VeraRubin, } @@ -65,6 +66,7 @@ impl HwType { // SMC GB300 runs a Supermicro (OpenBMC) host BMC. Self::SupermicroGb300 => Some(bmc_vendor::BMCVendor::Supermicro), Self::LiteonPowerShelf => Some(bmc_vendor::BMCVendor::Liteon), + Self::DeltaPowerShelf => Some(bmc_vendor::BMCVendor::Delta), Self::NvSwitch => Some(bmc_vendor::BMCVendor::Nvidia), Self::Supermicro => Some(bmc_vendor::BMCVendor::Supermicro), Self::Viking => Some(bmc_vendor::BMCVendor::Nvidia), @@ -90,6 +92,7 @@ impl HwType { // TODO(smc): confirm the SMC GB300 infinite-boot BIOS attribute from the tray BIOS. Self::SupermicroGb300 => None, Self::LiteonPowerShelf => None, + Self::DeltaPowerShelf => None, Self::NvSwitch => None, Self::Supermicro => None, Self::Viking => Some(BiosAttr::new_str("NvidiaInfiniteboot", "Enable")), diff --git a/crates/bmc-explorer/src/lib.rs b/crates/bmc-explorer/src/lib.rs index 4582fd5e3b..e89724db26 100644 --- a/crates/bmc-explorer/src/lib.rs +++ b/crates/bmc-explorer/src/lib.rs @@ -99,6 +99,14 @@ pub async fn nv_generate_exploration_report( ExploredChassisCollection::explore(&root, &chassis_explore_config).await?; let explored_inventories = ExploredInventories::explore(&root).await?; + // Delta power shelves do not expose a `/redfish/v1/Systems` collection (and + // report no vendor in the service root, so nv-redfish fabricates the path + // and gets a 404). Detect them from the chassis and synthesize the report + // from chassis + manager data instead of fetching a ComputerSystem. + if explored_chassis.is_delta_powershelf() { + return build_delta_powershelf_report(&root, explored_chassis, explored_inventories).await; + } + if explored_chassis.is_bluefield2() { root = root.as_ref().clone().restrict_expand().into(); } @@ -205,6 +213,7 @@ pub async fn nv_generate_exploration_report( hw::HwType::Bluefield | hw::HwType::Gb200 | hw::HwType::LiteonPowerShelf + | hw::HwType::DeltaPowerShelf | hw::HwType::NvSwitch, ) => false, None => false, @@ -270,6 +279,62 @@ pub async fn nv_generate_exploration_report( }) } +/// Builds an exploration report for a Delta power shelf. +/// +/// Delta BMCs do not serve `/redfish/v1/Systems`, so the standard flow (which +/// unconditionally fetches a `ComputerSystem`) fails with a 404. Here we skip +/// that fetch and synthesize a `ComputerSystem` from the chassis, matching the +/// behavior of the libredfish Delta power-shelf path. +async fn build_delta_powershelf_report( + root: &ServiceRoot, + explored_chassis: ExploredChassisCollection, + explored_inventories: ExploredInventories, +) -> Result> { + let hw_type = hw::HwType::DeltaPowerShelf; + + let manager = root + .managers() + .await + .map_err(Error::nv_redfish("managers"))? + .ok_or_else(Error::bmc_not_provided("managers"))? + .members() + .await + .map_err(Error::nv_redfish("managers members"))? + .into_iter() + .next() + .ok_or_else(Error::bmc_not_provided("at least one manager"))?; + let explored_manager = ExploredManager::explore(manager, &manager::Config::default()).await?; + + let system = explored_chassis.synthesized_powershelf_system(); + + Ok(EndpointExplorationReport { + endpoint_type: EndpointType::Bmc, + last_exploration_error: None, + last_exploration_latency: None, + machine_id: None, + managers: vec![explored_manager.to_model()?], + systems: vec![system], + chassis: explored_chassis.to_model(), + service: explored_inventories.to_model(Some(hw_type)), + vendor: hw_type.bmc_vendor(), + versions: HashMap::default(), + model: None, + power_shelf_id: None, + switch_id: None, + machine_setup_status: Some(MachineSetupStatus { + is_done: true, + diffs: vec![], + }), + secure_boot_status: None, + lockdown_status: None, + physical_slot_number: None, + compute_tray_index: None, + topology_id: None, + revision_id: None, + remediation_error: None, + }) +} + pub(crate) fn hw_type( root: &nv_redfish::ServiceRoot, explored_system: &ExploredComputerSystem, @@ -332,6 +397,11 @@ pub(crate) fn hw_type( .is_liteon_powershelf() .then_some(hw::HwType::LiteonPowerShelf) }) + .or_else(|| { + explored_chassis + .is_delta_powershelf() + .then_some(hw::HwType::DeltaPowerShelf) + }) } fn lockdown_status( @@ -652,6 +722,7 @@ fn machine_setup_status( } match hw_type { hw::HwType::LiteonPowerShelf => (), + hw::HwType::DeltaPowerShelf => (), hw::HwType::NvSwitch => (), hw::HwType::Viking => { diffs.extend( diff --git a/crates/bmc-explorer/tests/powershelf_explore.rs b/crates/bmc-explorer/tests/powershelf_explore.rs index b7c7f73cd2..13938beac4 100644 --- a/crates/bmc-explorer/tests/powershelf_explore.rs +++ b/crates/bmc-explorer/tests/powershelf_explore.rs @@ -18,7 +18,7 @@ mod common; use bmc_explorer::nv_generate_exploration_report; use bmc_mock::test_support; -use model::site_explorer::EndpointType; +use model::site_explorer::{EndpointType, PowerState}; use tokio::test; #[test] @@ -47,3 +47,93 @@ async fn explore_liteon_power_shelf() { "machine setup status must be present and structurally valid" ); } + +// Delta power shelves expose no `/redfish/v1/Systems` collection (the service +// root omits the `Systems` link and the endpoint 404s). Site-explorer must +// detect the Delta chassis and synthesize a `ComputerSystem` from it instead of +// failing on the missing collection -- this is the ingestion regression the +// Delta support fixes. The mock (`delta_powershelf_bmc`) reproduces the missing +// collection, so a successful report here guards that path. +#[test] +async fn explore_delta_power_shelf() { + let h = test_support::delta_powershelf_bmc().await; + let 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::Delta)); + assert!( + !report.systems.is_empty(), + "a system must be synthesized from the Delta chassis despite no Systems collection" + ); + // The mock reports every Delta PSU as `Oem.deltaenergysystems.Power: true`, + // so the synthesized system's power state must resolve to On. This exercises + // the typed `oem_delta()` accessor end-to-end (extension parse -> per-PSU + // flag -> aggregated shelf state). + assert!( + report + .systems + .iter() + .all(|system| system.power_state == PowerState::On), + "synthesized Delta system power state must be On" + ); + assert!(!report.chassis.is_empty(), "chassis must be present"); + assert!( + report + .service + .iter() + .any(|service| service.id == "FirmwareInventory"), + "firmware inventory service must be present" + ); + assert!( + report + .machine_setup_status + .as_ref() + .is_some_and(|status| !status.diffs.is_empty() || status.is_done), + "machine setup status must be present and structurally valid" + ); +} + +// When every Delta PSU reports `Power: false`, the aggregated shelf state must +// resolve to Off (all-off => Off), exercising the non-On path end-to-end. +#[test] +async fn explore_delta_power_shelf_all_off() { + let h = test_support::delta_powershelf_bmc_with_psu_power(vec![false; 6]).await; + let report = nv_generate_exploration_report(h.service_root, &common::explorer_config()) + .await + .unwrap(); + + assert_eq!(report.vendor, Some(bmc_vendor::BMCVendor::Delta)); + assert!(!report.systems.is_empty(), "a system must be synthesized"); + assert!( + report + .systems + .iter() + .all(|system| system.power_state == PowerState::Off), + "an all-off Delta shelf must resolve to Off" + ); +} + +// A mix of on and off PSUs is not a coherent shelf state, so the aggregation +// must collapse to Unknown rather than guessing On or Off. +#[test] +async fn explore_delta_power_shelf_mixed() { + let h = test_support::delta_powershelf_bmc_with_psu_power(vec![ + true, false, true, false, true, false, + ]) + .await; + let report = nv_generate_exploration_report(h.service_root, &common::explorer_config()) + .await + .unwrap(); + + assert_eq!(report.vendor, Some(bmc_vendor::BMCVendor::Delta)); + assert!(!report.systems.is_empty(), "a system must be synthesized"); + assert!( + report + .systems + .iter() + .all(|system| system.power_state == PowerState::Unknown), + "a mixed Delta shelf must resolve to Unknown" + ); +} diff --git a/crates/bmc-mock/src/bmc_state.rs b/crates/bmc-mock/src/bmc_state.rs index 2554ab7130..3b64e2b8eb 100644 --- a/crates/bmc-mock/src/bmc_state.rs +++ b/crates/bmc-mock/src/bmc_state.rs @@ -39,6 +39,11 @@ pub struct BmcState { pub session_service_state: Arc, pub injection: Arc, pub callbacks: Option>, + /// Whether this BMC advertises and serves the `/redfish/v1/Systems` + /// collection. Delta power shelves expose no `ComputerSystem` collection, + /// so the service root omits the `Systems` link and the collection endpoint + /// returns 404. + pub exposes_computer_systems: bool, } #[derive(Clone, Copy, Debug)] diff --git a/crates/bmc-mock/src/hw/delta_power_shelf.rs b/crates/bmc-mock/src/hw/delta_power_shelf.rs new file mode 100644 index 0000000000..b2b14fdbff --- /dev/null +++ b/crates/bmc-mock/src/hw/delta_power_shelf.rs @@ -0,0 +1,117 @@ +/* + * 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. + */ + +//! Delta Energy Systems power shelf. +//! +//! Modeled on the real Delta scrape under +//! `libredfish/tests/mockups/delta_powershelf/`. Two traits distinguish it +//! from the Lite-On shelf and drive the site-explorer Delta code path: +//! +//! * There is **no `/redfish/v1/Systems` collection** — the service root does +//! not advertise `Systems` and the collection endpoint 404s (see +//! [`crate::HostHardwareType::DeltaPowerShelf`] wiring in +//! `machine_info`/`mock_machine_router`). This reproduces the original +//! ingestion failure that Delta support fixes. +//! * Per-PSU power state is carried under `Oem.deltaenergysystems.Power` +//! rather than the standard `PowerState` field. + +use std::borrow::Cow; + +use mac_address::MacAddress; + +use crate::redfish; + +/// Chassis id reported by a Delta power shelf (matches the scrape). +const CHASSIS_ID: &str = "chassis"; + +/// Default per-PSU power states: the six-bay shelf the real scrape reports, +/// all outputting power. +pub const DEFAULT_PSU_POWER: &[bool] = &[true; 6]; + +pub struct DeltaPowerShelf<'a> { + pub bmc_mac_address: MacAddress, + pub product_serial_number: Cow<'a, str>, + /// Commanded on/off state per PSU bay, reported under + /// `Oem.deltaenergysystems.Power`. One entry per `PowerSupplyUnit`. + pub psu_power: Cow<'a, [bool]>, +} + +impl DeltaPowerShelf<'_> { + pub fn manager_config(&self) -> redfish::manager::Config { + redfish::manager::Config { + managers: vec![redfish::manager::SingleConfig { + id: "SMC", + eth_interfaces: Some(vec![ + redfish::ethernet_interface::builder( + &redfish::ethernet_interface::manager_resource("SMC", "eth0"), + ) + .mac_address(self.bmc_mac_address) + .interface_enabled(true) + .build(), + ]), + host_interfaces: None, + firmware_version: Some("01.04.01.04"), + oem: None, + }], + } + } + + /// Delta power shelves expose no `ComputerSystem`; the collection is empty + /// and (via the `exposes_computer_systems` gate) is not advertised or + /// served. Site-explorer synthesizes a system from the chassis instead. + pub fn system_config(&self) -> redfish::computer_system::Config { + redfish::computer_system::Config { systems: vec![] } + } + + pub fn chassis_config(&self) -> redfish::chassis::ChassisConfig { + redfish::chassis::ChassisConfig { + chassis: vec![redfish::chassis::SingleChassisConfig { + id: CHASSIS_ID.into(), + chassis_type: "RackMount".into(), + manufacturer: Some("DELTA".into()), + part_number: Some("ECD68000048".into()), + model: Some("810".into()), + serial_number: Some(self.product_serial_number.to_string().into()), + sensors: None, + power_supplies: Some( + // The scrape numbers PSU bays "PowerSupplyUnit 1"..; + // power is reported under Oem.deltaenergysystems.Power. + self.psu_power + .iter() + .enumerate() + .map(|(idx, &on)| { + redfish::power_supply::builder(&redfish::power_supply::resource( + CHASSIS_ID, + &format!("PowerSupplyUnit {}", idx + 1), + )) + .oem_delta_power_state(on) + .status(redfish::resource::Status::Ok) + .build() + }) + .collect(), + ), + ..redfish::chassis::SingleChassisConfig::defaults() + }], + } + } + + pub fn update_service_config(&self) -> redfish::update_service::UpdateServiceConfig { + redfish::update_service::UpdateServiceConfig { + firmware_inventory: vec![], + } + } +} diff --git a/crates/bmc-mock/src/hw/mod.rs b/crates/bmc-mock/src/hw/mod.rs index 7339ca7309..4d656174b0 100644 --- a/crates/bmc-mock/src/hw/mod.rs +++ b/crates/bmc-mock/src/hw/mod.rs @@ -51,6 +51,9 @@ pub mod supermicro_gb300_nvl; /// Support of LiteOn Power Shelf. pub mod liteon_power_shelf; +/// Support of Delta Energy Systems Power Shelf. +pub mod delta_power_shelf; + /// Support of NVIDIA Switch ND5200_LD. pub mod nvidia_switch_nd5200_ld; diff --git a/crates/bmc-mock/src/lib.rs b/crates/bmc-mock/src/lib.rs index 8de929750f..59b7e30bc3 100644 --- a/crates/bmc-mock/src/lib.rs +++ b/crates/bmc-mock/src/lib.rs @@ -68,6 +68,8 @@ pub enum HostHardwareType { SupermicroGb300Nvl, #[serde(rename = "liteon_power_shelf")] LiteOnPowerShelf, + #[serde(rename = "delta_power_shelf")] + DeltaPowerShelf, #[serde(rename = "nvidia_switch_nd5200_ld")] NvidiaSwitchNd5200Ld, #[serde(rename = "nvidia_dgx_h100")] @@ -91,6 +93,7 @@ impl fmt::Display for HostHardwareType { Self::NvidiaDgxGb300 => "NVIDIA DGX GB300 NVL".fmt(f), Self::SupermicroGb300Nvl => "Supermicro GB300 NVL".fmt(f), Self::LiteOnPowerShelf => "Lite-On Power Shelf".fmt(f), + Self::DeltaPowerShelf => "Delta Power Shelf".fmt(f), Self::NvidiaSwitchNd5200Ld => "NVIDIA Switch ND5200_LD".fmt(f), Self::NvidiaDgxH100 => "NVIDIA DGX H100".fmt(f), Self::GenericAmi => "Generic AMI Server".fmt(f), @@ -112,6 +115,7 @@ impl HostHardwareType { Self::NvidiaDgxGb300 => Some(1), Self::SupermicroGb300Nvl => Some(1), Self::LiteOnPowerShelf => Some(0), + Self::DeltaPowerShelf => Some(0), Self::NvidiaSwitchNd5200Ld => Some(0), Self::NvidiaDgxH100 => Some(1), Self::GenericAmi => None, diff --git a/crates/bmc-mock/src/machine_info.rs b/crates/bmc-mock/src/machine_info.rs index 2775d0a347..0920303905 100644 --- a/crates/bmc-mock/src/machine_info.rs +++ b/crates/bmc-mock/src/machine_info.rs @@ -46,6 +46,11 @@ pub struct HostMachineInfo { pub nvos_mac_addresses: Vec, pub switch_serial_number: Option, pub hw_mac_addr_pool: MacAddressPoolConfig, + /// Per-PSU commanded on/off states for a Delta power shelf, reported under + /// `Oem.deltaenergysystems.Power`. `None` uses the default all-on shelf; + /// set it (e.g. via [`HostMachineInfo::with_delta_psu_power`]) to model + /// off/mixed shelves. Ignored for non-Delta hardware. + pub delta_psu_power: Option>, } #[derive(Debug, Clone)] @@ -128,6 +133,7 @@ impl DpuMachineInfo { | HostHardwareType::NvidiaDgxGb300 | HostHardwareType::SupermicroGb300Nvl => hw::bluefield3::Mode::B3240ColdAisle, HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld | HostHardwareType::DellPowerEdgeR760Bf4 => { panic!("Bluefield3 DPU is defined for {}", self.hw_type) @@ -168,6 +174,7 @@ impl DpuMachineInfo { | HostHardwareType::NvidiaDgxGb300 | HostHardwareType::SupermicroGb300Nvl | HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld => DpuType::Bluefield3, HostHardwareType::DellPowerEdgeR760Bf4 => DpuType::Bluefield4, } @@ -243,7 +250,9 @@ impl HostMachineInfo { non_dpu_mac_address: if dpus.is_empty() && !matches!( hw_type, - HostHardwareType::LiteOnPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld + HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf + | HostHardwareType::NvidiaSwitchNd5200Ld ) { Some(next_mac()) } else { @@ -253,9 +262,19 @@ impl HostMachineInfo { switch_serial_number, dpus, hw_mac_addr_pool, + delta_psu_power: None, } } + /// Override the Delta power shelf's per-PSU on/off states (one entry per + /// PSU bay). Used by tests to model off/mixed shelves; the default is an + /// all-on six-bay shelf. + #[must_use] + pub fn with_delta_psu_power(mut self, states: Vec) -> Self { + self.delta_psu_power = Some(states); + self + } + pub fn primary_dpu(&self) -> Option<&DpuMachineInfo> { self.dpus.first() } @@ -276,6 +295,7 @@ impl HostMachineInfo { | HostHardwareType::NvidiaDgxGb300 | HostHardwareType::SupermicroGb300Nvl | HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf | HostHardwareType::NvidiaDgxH100 | HostHardwareType::NvidiaSwitchNd5200Ld | HostHardwareType::GenericAmi @@ -295,6 +315,7 @@ impl HostMachineInfo { } HostHardwareType::SupermicroGb300Nvl => redfish::oem::BmcVendor::Supermicro, HostHardwareType::LiteOnPowerShelf => redfish::oem::BmcVendor::LiteOn, + HostHardwareType::DeltaPowerShelf => redfish::oem::BmcVendor::Delta, HostHardwareType::NvidiaSwitchNd5200Ld => { redfish::oem::BmcVendor::Nvidia(redfish::oem::NvidiaNamestyle::Uppercase) } @@ -315,6 +336,7 @@ impl HostMachineInfo { HostHardwareType::NvidiaDgxGb300 => Some("GB BMC"), HostHardwareType::SupermicroGb300Nvl => Some("GB NVL"), HostHardwareType::LiteOnPowerShelf => None, + HostHardwareType::DeltaPowerShelf => None, HostHardwareType::NvidiaSwitchNd5200Ld => Some("P3809"), HostHardwareType::NvidiaDgxH100 => Some("AMI Redfish Server"), HostHardwareType::GenericAmi => Some("AMI Redfish Server"), @@ -332,6 +354,7 @@ impl HostMachineInfo { HostHardwareType::NvidiaDgxGb300 => "1.17.0", HostHardwareType::SupermicroGb300Nvl => "1.17.0", HostHardwareType::LiteOnPowerShelf => "1.9.0", + HostHardwareType::DeltaPowerShelf => "1.9.0", HostHardwareType::NvidiaSwitchNd5200Ld => "1.17.0", HostHardwareType::NvidiaDgxH100 => "1.11.0", HostHardwareType::GenericAmi => "1.17.0", @@ -350,6 +373,7 @@ impl HostMachineInfo { HostHardwareType::NvidiaDgxGb300 => self.dgx_gb300_nvl().manager_config(), HostHardwareType::SupermicroGb300Nvl => self.supermicro_gb300_nvl().manager_config(), HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().manager_config(), + HostHardwareType::DeltaPowerShelf => self.delta_power_shelf().manager_config(), HostHardwareType::NvidiaSwitchNd5200Ld => { self.nvidia_switch_nd5200_ld().manager_config() } @@ -378,6 +402,7 @@ impl HostMachineInfo { self.supermicro_gb300_nvl().system_config(callbacks) } HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().system_config(), + HostHardwareType::DeltaPowerShelf => self.delta_power_shelf().system_config(), HostHardwareType::NvidiaSwitchNd5200Ld => { self.nvidia_switch_nd5200_ld().system_config() } @@ -399,6 +424,7 @@ impl HostMachineInfo { HostHardwareType::NvidiaDgxGb300 => self.dgx_gb300_nvl().chassis_config(), HostHardwareType::SupermicroGb300Nvl => self.supermicro_gb300_nvl().chassis_config(), HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().chassis_config(), + HostHardwareType::DeltaPowerShelf => self.delta_power_shelf().chassis_config(), HostHardwareType::NvidiaSwitchNd5200Ld => { self.nvidia_switch_nd5200_ld().chassis_config() } @@ -424,6 +450,7 @@ impl HostMachineInfo { self.supermicro_gb300_nvl().update_service_config() } HostHardwareType::LiteOnPowerShelf => self.liteon_power_shelf().update_service_config(), + HostHardwareType::DeltaPowerShelf => self.delta_power_shelf().update_service_config(), HostHardwareType::NvidiaSwitchNd5200Ld => { self.nvidia_switch_nd5200_ld().update_service_config() } @@ -448,7 +475,9 @@ impl HostMachineInfo { HostHardwareType::GenericAmi | HostHardwareType::GenericSupermicro => { self.generic_server().discovery_info() } - HostHardwareType::LiteOnPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld => { + HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf + | HostHardwareType::NvidiaSwitchNd5200Ld => { panic!("discovery_info requested for {}", self.hw_type) } } @@ -710,6 +739,23 @@ impl HostMachineInfo { } } + fn delta_power_shelf(&self) -> hw::delta_power_shelf::DeltaPowerShelf<'_> { + hw::delta_power_shelf::DeltaPowerShelf { + bmc_mac_address: self.bmc_mac_address, + product_serial_number: Cow::Borrowed(&self.serial), + psu_power: self.delta_psu_power.as_deref().map_or( + Cow::Borrowed(hw::delta_power_shelf::DEFAULT_PSU_POWER), + Cow::Borrowed, + ), + } + } + + /// Whether this host advertises and serves a `/redfish/v1/Systems` + /// collection. Delta power shelves do not. + pub fn exposes_computer_systems(&self) -> bool { + !matches!(self.hw_type, HostHardwareType::DeltaPowerShelf) + } + fn nvidia_switch_nd5200_ld(&self) -> hw::nvidia_switch_nd5200_ld::NvidiaSwitchNd5200Ld<'_> { let mut pool = MacAddressPool::new_pool(self.hw_mac_addr_pool); let mut next_mac = || pool.allocate().expect("MAC address must be allocated"); @@ -857,6 +903,15 @@ impl MachineInfo { } } + /// Whether this machine advertises and serves a `/redfish/v1/Systems` + /// collection. Only Delta power shelves omit it. + pub fn exposes_computer_systems(&self) -> bool { + match self { + Self::Host(h) => h.exposes_computer_systems(), + Self::Dpu(_) => true, + } + } + pub fn product_serial(&self) -> &String { match self { Self::Host(h) => &h.serial, diff --git a/crates/bmc-mock/src/mock_machine_router.rs b/crates/bmc-mock/src/mock_machine_router.rs index 9272467bc1..ddb7858b8d 100644 --- a/crates/bmc-mock/src/mock_machine_router.rs +++ b/crates/bmc-mock/src/mock_machine_router.rs @@ -122,12 +122,16 @@ pub fn machine_router( session_service_state, injection: injection.clone(), callbacks: Some(callbacks.clone()), + exposes_computer_systems: machine_info.exposes_computer_systems(), }; let account_service_state = state.account_service_state.clone(); let session_service_state = state.session_service_state.clone(); let permit_factory_default_password = matches!( &machine_info, - MachineInfo::Host(h) if h.hw_type == HostHardwareType::LiteOnPowerShelf + MachineInfo::Host(h) if matches!( + h.hw_type, + HostHardwareType::LiteOnPowerShelf | HostHardwareType::DeltaPowerShelf + ) ); let router = ([ Box::new(redfish::expander_router::append), diff --git a/crates/bmc-mock/src/redfish/computer_system.rs b/crates/bmc-mock/src/redfish/computer_system.rs index f599ef85e8..b25b045d01 100644 --- a/crates/bmc-mock/src/redfish/computer_system.rs +++ b/crates/bmc-mock/src/redfish/computer_system.rs @@ -340,6 +340,11 @@ impl SingleSystemState { } async fn get_system_collection(State(state): State) -> Response { + // Delta power shelves serve no `Systems` collection at all (the endpoint + // 404s), which is the condition site-explorer's Delta path handles. + if !state.exposes_computer_systems { + return http::not_found(); + } let members = state .system_state .systems() diff --git a/crates/bmc-mock/src/redfish/oem/mod.rs b/crates/bmc-mock/src/redfish/oem/mod.rs index 17d9db5ac0..8df78c2011 100644 --- a/crates/bmc-mock/src/redfish/oem/mod.rs +++ b/crates/bmc-mock/src/redfish/oem/mod.rs @@ -26,6 +26,7 @@ pub enum BmcVendor { Nvidia(NvidiaNamestyle), Wiwynn, LiteOn, + Delta, Ami, Supermicro, } @@ -44,6 +45,9 @@ impl BmcVendor { BmcVendor::Dell => Some("Dell"), BmcVendor::Wiwynn => Some("WIWYNN"), BmcVendor::LiteOn => None, + // Delta power shelves report no `Vendor` in the service root, which + // is what leads nv-redfish to fall back to its anonymous-BMC quirk. + BmcVendor::Delta => None, BmcVendor::Ami => Some("AMI"), BmcVendor::Supermicro => Some("Supermicro"), } @@ -59,6 +63,7 @@ impl BmcVendor { | BmcVendor::Dell | BmcVendor::Wiwynn | BmcVendor::LiteOn + | BmcVendor::Delta | BmcVendor::Supermicro => { format!("{}/Settings", resource.odata_id) } diff --git a/crates/bmc-mock/src/redfish/power_supply.rs b/crates/bmc-mock/src/redfish/power_supply.rs index 7b3ed0911a..a07e25f2a7 100644 --- a/crates/bmc-mock/src/redfish/power_supply.rs +++ b/crates/bmc-mock/src/redfish/power_supply.rs @@ -85,6 +85,22 @@ impl PowerSupplyBuilder { self.apply_patch(json!({"PowerState": v})) } + /// Delta Energy Systems reports per-PSU power state under + /// `Oem.deltaenergysystems.Power` (not the standard `PowerState` field), + /// alongside a `FanSpeedTarget`. Mirrors the shape served by real Delta + /// power shelves. + pub fn oem_delta_power_state(self, v: bool) -> Self { + self.apply_patch(json!({ + "Oem": { + "deltaenergysystems": { + "@odata.type": "#DeltaEnergySystemsPowerSupply.v1_0_0.PowerSupply", + "Power": v, + "FanSpeedTarget": 0 + } + } + })) + } + pub fn status(self, status: redfish::resource::Status) -> Self { self.apply_patch(json!({ "Status": status.into_json() diff --git a/crates/bmc-mock/src/redfish/service_root.rs b/crates/bmc-mock/src/redfish/service_root.rs index c5bfc3d069..be8720a733 100644 --- a/crates/bmc-mock/src/redfish/service_root.rs +++ b/crates/bmc-mock/src/redfish/service_root.rs @@ -54,7 +54,7 @@ pub fn builder(resource: &redfish::Resource) -> ServiceRootBuilder { } async fn get_service_root(State(state): State) -> Response { - builder(&resource()) + let builder = builder(&resource()) .redfish_version(state.bmc_redfish_version) .maybe_with( ServiceRootBuilder::vendor, @@ -63,8 +63,14 @@ async fn get_service_root(State(state): State) -> Response { .maybe_with(ServiceRootBuilder::product, &state.bmc_product) .account_service(&redfish::account_service::resource()) .session_service(&redfish::session_service::service_resource()) - .chassis_collection(&redfish::chassis::collection()) - .system_collection(&redfish::computer_system::collection()) + .chassis_collection(&redfish::chassis::collection()); + // Delta power shelves advertise no `Systems` collection (see `BmcState`). + let builder = if state.exposes_computer_systems { + builder.system_collection(&redfish::computer_system::collection()) + } else { + builder + }; + builder .manager_collection(&redfish::manager::collection()) .update_service(&redfish::update_service::resource()) .telemetry_service(&redfish::telemetry_service::resource()) diff --git a/crates/bmc-mock/src/test_support/mod.rs b/crates/bmc-mock/src/test_support/mod.rs index dc6bb5a483..00eed6cd28 100644 --- a/crates/bmc-mock/src/test_support/mod.rs +++ b/crates/bmc-mock/src/test_support/mod.rs @@ -175,6 +175,33 @@ pub async fn liteon_powershelf_bmc() -> TestBmcHandle { .await } +pub async fn delta_powershelf_bmc() -> TestBmcHandle { + test_bmc(machine_router( + &host_info(HostHardwareType::DeltaPowerShelf), + Arc::new(NoopCallbacks), + "test-host-id".to_string(), + false, + )) + .await +} + +/// Delta power shelf whose PSUs report the given per-bay on/off states under +/// `Oem.deltaenergysystems.Power`. Lets tests exercise off and mixed shelves +/// (the default [`delta_powershelf_bmc`] is an all-on six-bay shelf). +pub async fn delta_powershelf_bmc_with_psu_power(states: Vec) -> TestBmcHandle { + let machine_info = match host_info(HostHardwareType::DeltaPowerShelf) { + MachineInfo::Host(host) => MachineInfo::Host(host.with_delta_psu_power(states)), + MachineInfo::Dpu(_) => unreachable!("Delta power shelf must be a host"), + }; + test_bmc(machine_router( + &machine_info, + Arc::new(NoopCallbacks), + "test-host-id".to_string(), + false, + )) + .await +} + pub async fn nvidia_switch_nd5200_ld_bmc() -> TestBmcHandle { test_bmc(machine_router( &host_info(HostHardwareType::NvidiaSwitchNd5200Ld), diff --git a/crates/machine-a-tron/src/host_machine.rs b/crates/machine-a-tron/src/host_machine.rs index 0983f08564..8d4c1b39b3 100644 --- a/crates/machine-a-tron/src/host_machine.rs +++ b/crates/machine-a-tron/src/host_machine.rs @@ -106,6 +106,7 @@ impl HostMachine { nvos_mac_addresses: persisted_host_machine.nvos_mac_addresses.clone(), switch_serial_number: persisted_host_machine.switch_serial_number.clone(), hw_mac_addr_pool, + delta_psu_power: None, }; let dpus = dpu_machines .into_iter() diff --git a/crates/machine-a-tron/src/machine_a_tron.rs b/crates/machine-a-tron/src/machine_a_tron.rs index ab42d3079b..b2099b168d 100644 --- a/crates/machine-a-tron/src/machine_a_tron.rs +++ b/crates/machine-a-tron/src/machine_a_tron.rs @@ -139,7 +139,7 @@ impl MachineATron { for machine in &machines { let host_info = machine.host_info(); let result = match host_info.hw_type { - HostHardwareType::LiteOnPowerShelf => { + HostHardwareType::LiteOnPowerShelf | HostHardwareType::DeltaPowerShelf => { self.app_context .api_client() .add_expected_power_shelf( diff --git a/crates/site-explorer/tests/integration/site_explorer.rs b/crates/site-explorer/tests/integration/site_explorer.rs index cd15fff73a..63a5c022a5 100644 --- a/crates/site-explorer/tests/integration/site_explorer.rs +++ b/crates/site-explorer/tests/integration/site_explorer.rs @@ -3232,6 +3232,7 @@ async fn test_site_explorer_pairs_bf4_dpu_from_bluefield_bmc_chassis_serial( 16, ) .unwrap(), + delta_psu_power: None, }; let host_bmc_mac = host_info.bmc_mac_address; let host_serial = host_info.serial.clone();