From ba9d361e4c51bce8ea6eb5212d7ab31aad25db8f Mon Sep 17 00:00:00 2001 From: Srikar Date: Tue, 7 Jul 2026 16:34:15 -0700 Subject: [PATCH 1/2] fix: add support to bmc-explorer for Delta powershelves --- crates/bmc-explorer/Cargo.toml | 2 +- crates/bmc-explorer/src/chassis.rs | 221 ++++++++++++++++++++++++++++- crates/bmc-explorer/src/hw/mod.rs | 3 + crates/bmc-explorer/src/lib.rs | 71 +++++++++ 4 files changed, 294 insertions(+), 3 deletions(-) diff --git a/crates/bmc-explorer/Cargo.toml b/crates/bmc-explorer/Cargo.toml index a6eeebee23..ad42003a8a 100644 --- a/crates/bmc-explorer/Cargo.toml +++ b/crates/bmc-explorer/Cargo.toml @@ -64,6 +64,7 @@ nv-redfish = { workspace = true, features = [ ] } regex = { workspace = true } lazy_static = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } @@ -79,7 +80,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 81ee36fd9d..db36c109ef 100644 --- a/crates/bmc-explorer/src/chassis.rs +++ b/crates/bmc-explorer/src/chassis.rs @@ -21,9 +21,11 @@ use std::fmt; use carbide_network::BaseMac; use itertools::Itertools; use mac_address::MacAddress; -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; @@ -110,6 +112,83 @@ 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| { + let id = m.chassis.id().into_inner(); + (id == "chassis" || id == "powershelf") + && m.chassis + .hardware_id() + .manufacturer + .as_ref() + .is_some_and(|mfg| mfg.as_ref().to_lowercase().contains("delta")) + }) + } + + /// 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")) @@ -235,6 +314,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 { @@ -290,11 +370,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, }) } @@ -346,6 +453,53 @@ 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. nv-redfish keeps unrecognized OEM +/// payloads as raw JSON on the resource, so we read it directly rather than +/// through a typed accessor. Returns `None` when the field is absent. +fn delta_psu_power_on(ps: &NvPowerSupply) -> Option { + delta_power_from_oem( + ps.raw() + .base + .base + .oem + .as_ref() + .map(|oem| &oem.additional_properties), + ) +} + +/// Extracts `deltaenergysystems.Power` from a PSU's raw OEM object (the value +/// of the resource's `Oem` property). Split out so the JSON shape can be +/// exercised in unit tests without a live BMC. +fn delta_power_from_oem(oem: Option<&serde_json::Value>) -> Option { + oem?.get("deltaenergysystems")?.get("Power")?.as_bool() +} + +/// 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<'_> { @@ -376,3 +530,66 @@ impl LiteOnSuppliesState<'_> { } } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::{ModelPowerState, delta_power_from_oem, powershelf_power_state}; + + // delta_power_from_oem reads Oem.deltaenergysystems.Power (a bool) off a + // PSU. The JSON mirrors the shape Delta BMCs serve at + // /redfish/v1/Chassis/chassis/PowerSubsystem/PowerSupplies/N. + #[test] + fn delta_power_from_oem_reads_power_flag() { + let on = json!({ + "deltaenergysystems": { + "@odata.type": "#DeltaEnergySystemsPowerSupply.v1_0_0.PowerSupply", + "FanSpeedTarget": 0, + "Power": true + } + }); + let off = json!({ "deltaenergysystems": { "Power": false } }); + + assert_eq!(delta_power_from_oem(Some(&on)), Some(true)); + assert_eq!(delta_power_from_oem(Some(&off)), Some(false)); + // Missing OEM object, missing vendor key, missing/non-bool Power field + // all resolve to None rather than a wrong reading. + assert_eq!(delta_power_from_oem(None), None); + assert_eq!(delta_power_from_oem(Some(&json!({}))), None); + assert_eq!( + delta_power_from_oem(Some(&json!({ "deltaenergysystems": {} }))), + None + ); + assert_eq!( + delta_power_from_oem(Some(&json!({ "deltaenergysystems": { "Power": "true" } }))), + None + ); + // Only the Delta vendor key counts; a foreign OEM payload is ignored. + assert_eq!( + delta_power_from_oem(Some(&json!({ "liteon": { "Power": true } }))), + None + ); + } + + // 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 e20de696c2..87e2058e37 100644 --- a/crates/bmc-explorer/src/lib.rs +++ b/crates/bmc-explorer/src/lib.rs @@ -120,6 +120,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(); } @@ -223,6 +231,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, @@ -288,6 +297,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, @@ -350,6 +415,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( @@ -670,6 +740,7 @@ fn machine_setup_status( } match hw_type { hw::HwType::LiteonPowerShelf => (), + hw::HwType::DeltaPowerShelf => (), hw::HwType::NvSwitch => (), hw::HwType::Viking => { diffs.extend( From 350133bfe7905fc1610c70558a1e91136bc823e2 Mon Sep 17 00:00:00 2001 From: Srikar Date: Mon, 13 Jul 2026 10:21:05 -0700 Subject: [PATCH 2/2] chore: add bmc-mock support for delta powershelves --- Cargo.lock | 20 +-- Cargo.toml | 2 +- crates/bmc-explorer/Cargo.toml | 1 + crates/bmc-explorer/src/chassis.rs | 107 ++++++++-------- .../tests/integration/powershelf_explore.rs | 92 +++++++++++++- crates/bmc-mock/src/bmc_state.rs | 5 + crates/bmc-mock/src/hw/delta_power_shelf.rs | 118 ++++++++++++++++++ crates/bmc-mock/src/hw/mod.rs | 3 + crates/bmc-mock/src/lib.rs | 4 + crates/bmc-mock/src/machine_info.rs | 60 ++++++++- 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 +- 18 files changed, 412 insertions(+), 74 deletions(-) create mode 100644 crates/bmc-mock/src/hw/delta_power_shelf.rs diff --git a/Cargo.lock b/Cargo.lock index ddb05b68e7..28961eab1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7632,9 +7632,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", @@ -7649,9 +7649,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", @@ -7673,9 +7673,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", @@ -7688,9 +7688,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", @@ -7706,9 +7706,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 0aa07001ff..083d030bed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ opentelemetry-semantic-conventions = "0.32.1" tracing-opentelemetry = "0.33.0" # 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 ad42003a8a..113c4846e6 100644 --- a/crates/bmc-explorer/Cargo.toml +++ b/crates/bmc-explorer/Cargo.toml @@ -60,6 +60,7 @@ nv-redfish = { workspace = true, features = [ "oem-hpe", "oem-ami", "oem-liteon", + "oem-delta", "bmc-http", ] } regex = { workspace = true } diff --git a/crates/bmc-explorer/src/chassis.rs b/crates/bmc-explorer/src/chassis.rs index db36c109ef..e67244cc9d 100644 --- a/crates/bmc-explorer/src/chassis.rs +++ b/crates/bmc-explorer/src/chassis.rs @@ -120,13 +120,14 @@ impl ExploredChassisCollection { /// id. pub fn is_delta_powershelf(&self) -> bool { self.members.iter().any(|m| { - let id = m.chassis.id().into_inner(); - (id == "chassis" || id == "powershelf") - && m.chassis + is_delta_powershelf_chassis( + m.chassis.id().into_inner(), + m.chassis .hardware_id() .manufacturer .as_ref() - .is_some_and(|mfg| mfg.as_ref().to_lowercase().contains("delta")) + .map(|mfg| **mfg), + ) }) } @@ -461,25 +462,28 @@ pub struct DeltaPowerSupply { /// 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. nv-redfish keeps unrecognized OEM -/// payloads as raw JSON on the resource, so we read it directly rather than -/// through a typed accessor. Returns `None` when the field is absent. +/// 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 { - delta_power_from_oem( - ps.raw() - .base - .base - .oem - .as_ref() - .map(|oem| &oem.additional_properties), - ) + 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 + } + } } -/// Extracts `deltaenergysystems.Power` from a PSU's raw OEM object (the value -/// of the resource's `Oem` property). Split out so the JSON shape can be -/// exercised in unit tests without a live BMC. -fn delta_power_from_oem(oem: Option<&serde_json::Value>) -> Option { - oem?.get("deltaenergysystems")?.get("Power")?.as_bool() +/// 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. @@ -533,43 +537,36 @@ impl LiteOnSuppliesState<'_> { #[cfg(test)] mod tests { - use serde_json::json; + use super::{ModelPowerState, is_delta_powershelf_chassis, powershelf_power_state}; - use super::{ModelPowerState, delta_power_from_oem, powershelf_power_state}; - - // delta_power_from_oem reads Oem.deltaenergysystems.Power (a bool) off a - // PSU. The JSON mirrors the shape Delta BMCs serve at - // /redfish/v1/Chassis/chassis/PowerSubsystem/PowerSupplies/N. + // 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 delta_power_from_oem_reads_power_flag() { - let on = json!({ - "deltaenergysystems": { - "@odata.type": "#DeltaEnergySystemsPowerSupply.v1_0_0.PowerSupply", - "FanSpeedTarget": 0, - "Power": true - } - }); - let off = json!({ "deltaenergysystems": { "Power": false } }); - - assert_eq!(delta_power_from_oem(Some(&on)), Some(true)); - assert_eq!(delta_power_from_oem(Some(&off)), Some(false)); - // Missing OEM object, missing vendor key, missing/non-bool Power field - // all resolve to None rather than a wrong reading. - assert_eq!(delta_power_from_oem(None), None); - assert_eq!(delta_power_from_oem(Some(&json!({}))), None); - assert_eq!( - delta_power_from_oem(Some(&json!({ "deltaenergysystems": {} }))), - None - ); - assert_eq!( - delta_power_from_oem(Some(&json!({ "deltaenergysystems": { "Power": "true" } }))), - None - ); - // Only the Delta vendor key counts; a foreign OEM payload is ignored. - assert_eq!( - delta_power_from_oem(Some(&json!({ "liteon": { "Power": true } }))), - None - ); + 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 => diff --git a/crates/bmc-explorer/tests/integration/powershelf_explore.rs b/crates/bmc-explorer/tests/integration/powershelf_explore.rs index 97764128cf..6cc8f538b4 100644 --- a/crates/bmc-explorer/tests/integration/powershelf_explore.rs +++ b/crates/bmc-explorer/tests/integration/powershelf_explore.rs @@ -16,7 +16,7 @@ */ 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; use crate::common; @@ -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..297de569f8 --- /dev/null +++ b/crates/bmc-mock/src/hw/delta_power_shelf.rs @@ -0,0 +1,118 @@ +/* + * 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, + serial_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 cc46d767bc..718d04fe3c 100644 --- a/crates/bmc-mock/src/hw/mod.rs +++ b/crates/bmc-mock/src/hw/mod.rs @@ -57,6 +57,9 @@ pub mod dgx_vr_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 23d0fc8e7e..29d57b174c 100644 --- a/crates/bmc-mock/src/lib.rs +++ b/crates/bmc-mock/src/lib.rs @@ -71,6 +71,8 @@ pub enum HostHardwareType { NvidiaDgxVr, #[serde(rename = "liteon_power_shelf")] LiteOnPowerShelf, + #[serde(rename = "delta_power_shelf")] + DeltaPowerShelf, #[serde(rename = "nvidia_switch_nd5200_ld")] NvidiaSwitchNd5200Ld, #[serde(rename = "nvidia_dgx_h100")] @@ -97,6 +99,7 @@ impl fmt::Display for HostHardwareType { Self::SupermicroGb300Nvl => "Supermicro GB300 NVL".fmt(f), Self::NvidiaDgxVr => "NVIDIA DGX VR 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), @@ -120,6 +123,7 @@ impl HostHardwareType { Self::SupermicroGb300Nvl => Some(1), Self::NvidiaDgxVr => 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 1a01bb412e..483421cf69 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)] @@ -129,6 +134,7 @@ impl DpuMachineInfo { | HostHardwareType::NvidiaDgxGb300 | HostHardwareType::SupermicroGb300Nvl => hw::bluefield3::Mode::B3240ColdAisle, HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld | HostHardwareType::NvidiaDgxVr | HostHardwareType::DellPowerEdgeR760Bf4 => { @@ -163,6 +169,7 @@ impl DpuMachineInfo { | HostHardwareType::NvidiaDgxGb300 | HostHardwareType::SupermicroGb300Nvl | HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld => { panic!("Bluefield4 DPU is defined for {}", self.hw_type) } @@ -189,6 +196,7 @@ impl DpuMachineInfo { | HostHardwareType::NvidiaDgxGb300 | HostHardwareType::SupermicroGb300Nvl | HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf | HostHardwareType::NvidiaSwitchNd5200Ld => DpuType::Bluefield3, HostHardwareType::DellPowerEdgeR760Bf4 | HostHardwareType::NvidiaDgxVr => { DpuType::Bluefield4 @@ -280,7 +288,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 { @@ -290,9 +300,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() } @@ -313,6 +333,7 @@ impl HostMachineInfo { | HostHardwareType::NvidiaDgxGb300 | HostHardwareType::NvidiaDgxVr | HostHardwareType::LiteOnPowerShelf + | HostHardwareType::DeltaPowerShelf | HostHardwareType::NvidiaDgxH100 | HostHardwareType::NvidiaSwitchNd5200Ld | HostHardwareType::GenericAmi @@ -339,6 +360,7 @@ impl HostMachineInfo { redfish::oem::BmcVendor::Nvidia(redfish::oem::NvidiaNamestyle::Uppercase) } HostHardwareType::LiteOnPowerShelf => redfish::oem::BmcVendor::LiteOn, + HostHardwareType::DeltaPowerShelf => redfish::oem::BmcVendor::Delta, HostHardwareType::NvidiaSwitchNd5200Ld => { redfish::oem::BmcVendor::Nvidia(redfish::oem::NvidiaNamestyle::Uppercase) } @@ -361,6 +383,7 @@ impl HostMachineInfo { HostHardwareType::SupermicroGb300Nvl => Some("GB NVL"), HostHardwareType::NvidiaDgxVr => Some("VR NVL72"), HostHardwareType::LiteOnPowerShelf => None, + HostHardwareType::DeltaPowerShelf => None, HostHardwareType::NvidiaSwitchNd5200Ld => Some("P3809"), HostHardwareType::NvidiaDgxH100 => Some("AMI Redfish Server"), HostHardwareType::GenericAmi => Some("AMI Redfish Server"), @@ -380,6 +403,7 @@ impl HostMachineInfo { HostHardwareType::SupermicroGb300Nvl => "1.17.0", HostHardwareType::NvidiaDgxVr => "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", @@ -400,6 +424,7 @@ impl HostMachineInfo { HostHardwareType::SupermicroGb300Nvl => self.supermicro_gb300_nvl().manager_config(), HostHardwareType::NvidiaDgxVr => self.dgx_vr_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() } @@ -432,6 +457,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() } @@ -457,6 +483,7 @@ impl HostMachineInfo { HostHardwareType::SupermicroGb300Nvl => self.supermicro_gb300_nvl().chassis_config(), HostHardwareType::NvidiaDgxVr => self.dgx_vr_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() } @@ -486,6 +513,7 @@ impl HostMachineInfo { } HostHardwareType::NvidiaDgxVr => self.dgx_vr_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() } @@ -517,7 +545,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) } } @@ -792,6 +822,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"); @@ -957,6 +1004,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 e8d3d8d88c..bfb9b68d45 100644 --- a/crates/bmc-mock/src/mock_machine_router.rs +++ b/crates/bmc-mock/src/mock_machine_router.rs @@ -124,12 +124,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 15b7c200fd..4d04586e3a 100644 --- a/crates/bmc-mock/src/redfish/computer_system.rs +++ b/crates/bmc-mock/src/redfish/computer_system.rs @@ -314,6 +314,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 99b68b09c7..4d3a2a3f52 100644 --- a/crates/bmc-mock/src/redfish/oem/mod.rs +++ b/crates/bmc-mock/src/redfish/oem/mod.rs @@ -27,6 +27,7 @@ pub enum BmcVendor { Nvidia(NvidiaNamestyle), Wiwynn, LiteOn, + Delta, Ami, Supermicro, Hpe, @@ -46,6 +47,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"), BmcVendor::Hpe => Some("HPE"), @@ -62,6 +66,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 349bf8170b..80790bd8c4 100644 --- a/crates/bmc-mock/src/test_support/mod.rs +++ b/crates/bmc-mock/src/test_support/mod.rs @@ -190,6 +190,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 8abc805c92..37080e1e38 100644 --- a/crates/machine-a-tron/src/host_machine.rs +++ b/crates/machine-a-tron/src/host_machine.rs @@ -107,6 +107,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 895ad21a53..36150ed67a 100644 --- a/crates/machine-a-tron/src/machine_a_tron.rs +++ b/crates/machine-a-tron/src/machine_a_tron.rs @@ -155,7 +155,7 @@ impl MachineATron { .expect("machine was constructed from a configured machine group"); let rack_id = machine_config.rack_id.clone(); let result = match host_info.hw_type { - HostHardwareType::LiteOnPowerShelf => { + HostHardwareType::LiteOnPowerShelf | HostHardwareType::DeltaPowerShelf => { self.app_context .api_client() .add_expected_power_shelf(