Skip to content
Draft
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
10 changes: 10 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/bmc-explorer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ path = "tests/integration/main.rs"
# [local-dependencies]
# DO NOT PUT DEPENDENCIES OTHER THAN LOCAL DEPS HERE, THEY SHOULD ALL HAVE 'path =' IN THEM.
bmc-vendor = { path = "../bmc-vendor" }
hw-platform = { path = "../hw-platform" }
bmc-mock = { path = "../bmc-mock", optional = true }
carbide-network = { path = "../network", default-features = false }
carbide-api-model = { path = "../api-model", default-features = false }
Expand Down
103 changes: 20 additions & 83 deletions crates/bmc-explorer/src/chassis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,19 @@ impl<B: Bmc> ExploredChassisCollection<B> {
}
}

pub(crate) fn is_liteon_powershelf(&self) -> bool {
self.members.iter().any(|m| {
m.chassis.id().into_inner() == "powershelf"
|| (m.chassis.id().into_inner() == "chassis"
&& m.chassis
.hardware_id()
.manufacturer
.as_ref()
.is_some_and(|mfg| mfg.as_ref().to_lowercase().contains("lite-on")))
})
/// Projects the members onto the identity fields `hw_platform` classifies on.
pub(crate) fn identities(&self) -> Vec<hw_platform::ChassisIdentity<'_>> {
self.members
.iter()
.map(|m| {
let hardware_id = m.chassis.hardware_id();
hw_platform::ChassisIdentity {
id: m.chassis.id().into_inner(),
manufacturer: hardware_id.manufacturer.map(|v| v.into_inner()),
model: hardware_id.model.map(|v| v.into_inner()),
}
})
.collect()
}

pub(crate) fn liteon_power_state(&self) -> Option<LiteOnSuppliesState<'_>> {
Expand All @@ -129,23 +132,13 @@ 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.
/// Detects a Delta power shelf; see [`hw_platform::is_delta_powershelf`].
///
/// Delta detection is needed before classification runs -- it selects the
/// exploration path for a BMC that exposes no `Systems` collection -- so it
/// is reachable on its own as well as through `hw_platform::classify`.
pub(crate) 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),
)
})
hw_platform::is_delta_powershelf(&self.identities())
}

/// Aggregate power state across all Delta PSUs found on the chassis members.
Expand Down Expand Up @@ -210,13 +203,6 @@ impl<B: Bmc> ExploredChassisCollection<B> {
}
}

pub(crate) fn is_gb300(&self) -> bool {
self.members.iter().any(|m| {
m.chassis.hardware_id().manufacturer == Some(Manufacturer::new("NVIDIA"))
&& m.chassis.hardware_id().model == Some(Model::new("NVIDIA GB300"))
})
}

pub(crate) fn is_mgx_c2(&self) -> bool {
self.members.iter().any(|m| {
let hardware_id = m.chassis.hardware_id();
Expand All @@ -228,12 +214,6 @@ impl<B: Bmc> ExploredChassisCollection<B> {
})
}

pub(crate) fn is_lenovo(&self) -> bool {
self.members
.iter()
.any(|m| m.chassis.hardware_id().manufacturer == Some(Manufacturer::new("Lenovo")))
}

pub(crate) fn is_bluefield2(&self) -> bool {
self.members
.iter()
Expand Down Expand Up @@ -503,16 +483,6 @@ fn delta_psu_power_on<B: Bmc>(ps: &NvPowerSupply<B>) -> Option<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"))
}

fn is_mgx_c2_processor_module(
manufacturer: Option<&str>,
model: Option<&str>,
Expand Down Expand Up @@ -577,10 +547,7 @@ impl LiteOnSuppliesState<'_> {

#[cfg(test)]
mod tests {
use super::{
ModelPowerState, is_delta_powershelf_chassis, is_mgx_c2_processor_module,
powershelf_power_state,
};
use super::{ModelPowerState, is_mgx_c2_processor_module, powershelf_power_state};

#[test]
fn identifies_mgx_c2_processor_modules() {
Expand Down Expand Up @@ -618,36 +585,6 @@ mod tests {
}
}

// 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]
Expand Down
171 changes: 7 additions & 164 deletions crates/bmc-explorer/src/hw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
* limitations under the License.
*/

use std::fmt;
//! Per-platform BMC exploration helpers.
//!
//! The [`HwType`] taxonomy and the rules that resolve one from Redfish live in
//! the `hw-platform` crate, so `carbide-health` can classify the same way
//! without depending on this crate's exploration types. They are re-exported
//! here because every caller in this crate reaches them through `hw::`.

use itertools::Itertools;
pub use hw_platform::{BiosAttr, BiosAttrValue, HwType};

pub mod bluefield;
pub mod dell;
Expand All @@ -30,165 +35,3 @@ pub mod supermicro;
pub mod supermicro_gb300;
pub mod vera_rubin;
pub mod viking;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HwType {
Ami,
Bluefield,
Dell,
Gb200,
DgxGb300,
Hpe,
Lenovo,
LenovoAmi,
LenovoGb300,
SupermicroGb300,
Supermicro,
Viking,
LiteonPowerShelf,
DeltaPowerShelf,
NvSwitch,
VeraRubin,
}

impl HwType {
pub const fn bmc_vendor(&self) -> Option<bmc_vendor::BMCVendor> {
match self {
Self::Ami => None,
Self::Bluefield => Some(bmc_vendor::BMCVendor::Nvidia),
Self::Dell => Some(bmc_vendor::BMCVendor::Dell),
Self::Gb200 => Some(bmc_vendor::BMCVendor::Nvidia),
// DGX GB300 uses the NVIDIA "GB BMC" (same BMC family as GB200).
Self::DgxGb300 => Some(bmc_vendor::BMCVendor::Nvidia),
Self::Hpe => Some(bmc_vendor::BMCVendor::Hpe),
Self::Lenovo => Some(bmc_vendor::BMCVendor::Lenovo),
Self::LenovoAmi => Some(bmc_vendor::BMCVendor::LenovoAMI),
Self::LenovoGb300 => Some(bmc_vendor::BMCVendor::LenovoAMI),
// 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),
Self::VeraRubin => Some(bmc_vendor::BMCVendor::Nvidia),
}
}

pub const fn infinite_boot_enabled_attr(&self) -> Option<BiosAttr<'static>> {
match self {
Self::Ami => Some(BiosAttr::new_str("EndlessBoot", "Enabled")),
Self::Bluefield => None,
Self::Dell => Some(BiosAttr::new_str("BootSeqRetry", "Enabled")),
Self::Gb200 => Some(BiosAttr::new_str("EmbeddedUefiShell", "Disabled")),
// The DGX GB300 BIOS exposes EmbeddedUefiShell, but the value that means
// infinite-boot-enabled is not yet characterized on hardware (GB200's polarity
// is not assumed to carry over). Left None until confirmed on a tray.
// TODO(dgx-gb300): set the infinite-boot attribute from the DGX GB300 BIOS.
Self::DgxGb300 => None,
Self::Hpe => None,
Self::Lenovo => Some(BiosAttr::new_str("BootModes_InfiniteBootRetry", "Enabled")),
Self::LenovoAmi => Some(BiosAttr::new_str("EndlessBoot", "Enabled")),
Self::LenovoGb300 => Some(BiosAttr::new_int("LEM0003", 50)),
// 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")),
// Same EmbeddedUefiShell polarity as GB200 / libredfish NvidiaGBx00.
Self::VeraRubin => Some(BiosAttr::new_str("EmbeddedUefiShell", "Disabled")),
}
}
}

#[derive(Clone, Copy)]
pub struct BiosAttr<'a> {
pub key: &'a str,
pub value: BiosAttrValue<'a>,
}

impl BiosAttr<'_> {
pub const fn new_bool(key: &'static str, value: bool) -> BiosAttr<'static> {
BiosAttr {
key,
value: BiosAttrValue::Bool(value),
}
}
pub const fn new_str(key: &'static str, value: &'static str) -> BiosAttr<'static> {
BiosAttr {
key,
value: BiosAttrValue::Str(value),
}
}
pub const fn new_any_str(
key: &'static str,
value: &'static [&'static str],
) -> BiosAttr<'static> {
BiosAttr {
key,
value: BiosAttrValue::AnyStr(value),
}
}
pub const fn new_int(key: &'static str, value: i64) -> BiosAttr<'static> {
BiosAttr {
key,
value: BiosAttrValue::Int(value),
}
}
}

#[derive(Clone, Copy)]
pub enum BiosAttrValue<'a> {
Str(&'a str),
AnyStr(&'a [&'a str]),
Bool(bool),
Int(i64),
}

impl fmt::Display for BiosAttrValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BiosAttrValue::Str(v) => v.fmt(f),
BiosAttrValue::Bool(v) => v.fmt(f),
BiosAttrValue::Int(v) => v.fmt(f),
BiosAttrValue::AnyStr(v) => write!(f, "any({})", v.iter().join(",")),
}
}
}

#[cfg(test)]
mod tests {
use bmc_vendor::BMCVendor;
use carbide_test_support::value_scenarios;

use super::*;

#[test]
fn hw_type_bmc_vendor_maps_each_variant() {
value_scenarios!(run = |hardware_type: HwType| hardware_type.bmc_vendor();
"generic AMI has no canonical vendor" {
HwType::Ami => None,
}

"hardware types map to canonical vendors" {
HwType::Bluefield => Some(BMCVendor::Nvidia),
HwType::Dell => Some(BMCVendor::Dell),
HwType::Gb200 => Some(BMCVendor::Nvidia),
HwType::DgxGb300 => Some(BMCVendor::Nvidia),
HwType::Hpe => Some(BMCVendor::Hpe),
HwType::Lenovo => Some(BMCVendor::Lenovo),
HwType::LenovoAmi => Some(BMCVendor::LenovoAMI),
HwType::LenovoGb300 => Some(BMCVendor::LenovoAMI),
HwType::SupermicroGb300 => Some(BMCVendor::Supermicro),
HwType::Supermicro => Some(BMCVendor::Supermicro),
HwType::Viking => Some(BMCVendor::Nvidia),
HwType::LiteonPowerShelf => Some(BMCVendor::Liteon),
HwType::DeltaPowerShelf => Some(BMCVendor::Delta),
HwType::NvSwitch => Some(BMCVendor::Nvidia),
HwType::VeraRubin => Some(BMCVendor::Nvidia),
}
);
}
}
Loading
Loading