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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion crates/bmc-explorer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -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]
Expand Down
218 changes: 216 additions & 2 deletions crates/bmc-explorer/src/chassis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -107,6 +109,84 @@ impl<B: Bmc> ExploredChassisCollection<B> {
})
}

/// 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"))
Expand Down Expand Up @@ -179,6 +259,7 @@ pub struct ExploredChassis<B: Bmc> {
pub network_adapters: ExploredNetworkAdapterCollection<B>,
pub assembly_sn: Option<String>,
pub oem_liteon_power_supplies: Option<Vec<LiteOnPowerSupply>>,
pub oem_delta_power_supplies: Option<Vec<DeltaPowerSupply>>,
}

impl<B: Bmc> ExploredChassis<B> {
Expand Down Expand Up @@ -234,11 +315,38 @@ impl<B: Bmc> ExploredChassis<B> {
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,
})
}

Expand Down Expand Up @@ -290,6 +398,56 @@ pub struct LiteOnPowerSupply {
pub power_state: Option<bool>,
}

pub struct DeltaPowerSupply {
pub id: String,
pub power_state: Option<bool>,
}

/// 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<B: Bmc>(ps: &NvPowerSupply<B>) -> Option<bool> {
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<Item = Option<bool>>) -> ModelPowerState {
let states: Vec<Option<bool>> = 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<'_> {
Expand Down Expand Up @@ -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<bool>], 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:?}"
);
}
}
}
3 changes: 3 additions & 0 deletions crates/bmc-explorer/src/hw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub enum HwType {
Supermicro,
Viking,
LiteonPowerShelf,
DeltaPowerShelf,
NvSwitch,
VeraRubin,
}
Expand All @@ -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),
Expand All @@ -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")),
Expand Down
Loading
Loading