From a0170524e2416b218f0e64d877d16869cf9a004c Mon Sep 17 00:00:00 2001 From: Srikar Date: Tue, 7 Jul 2026 16:34:15 -0700 Subject: [PATCH] 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 91248eba3b..09d2353874 100644 --- a/crates/bmc-explorer/Cargo.toml +++ b/crates/bmc-explorer/Cargo.toml @@ -59,6 +59,7 @@ nv-redfish = { workspace = true, features = [ ] } regex = { workspace = true } lazy_static = { workspace = true } +serde_json = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } @@ -74,7 +75,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 b6839c6c62..1c80e790b8 100644 --- a/crates/bmc-explorer/src/lib.rs +++ b/crates/bmc-explorer/src/lib.rs @@ -112,6 +112,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(); } @@ -215,6 +223,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, @@ -280,6 +289,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, @@ -342,6 +407,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( @@ -662,6 +732,7 @@ fn machine_setup_status( } match hw_type { hw::HwType::LiteonPowerShelf => (), + hw::HwType::DeltaPowerShelf => (), hw::HwType::NvSwitch => (), hw::HwType::Viking => { diffs.extend(