Skip to content
Open
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
168 changes: 113 additions & 55 deletions src/supermicro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,8 @@ const NVIDIA_UEFI_HTTP_IPV4: &str = "UEFI HTTP IPv4 Nvidia Network Adapter";
/// MGX C2 systems use SSIF instead of x86 KCS for in-band BMC communication,
/// so the KCSInterface endpoint doesn't exist. These models require the
/// IPMIHostInterface fallback on Systems/{id}.
const MGX_C2_MODELS: [&str; 4] = [
"ARS-121L-DNR",
"ARS-221GL-NR",
"SYS-221H-TNR",
"SYS-221H-TN24R",
];
const MGX_C2_PROCESSOR_MODULE_MODEL_PREFIX: &str = "PG535";
const MGX_C2_PROCESSOR_MODULE_PART_NUMBER_FRAGMENT: &str = "2G535";

/// Minimum BMC firmware version that exposes `IPMIHostInterface` on
/// `Systems/{id}` for MGX C2 systems.
Expand Down Expand Up @@ -328,8 +324,8 @@ impl Redfish for Bmc {
use EnabledDisabled::*;
match target {
Enabled => {
// Grace-Grace SMCs can't PXE boot if host interface is disabled
if !self.is_grace_grace_smc().await? {
// ARS-121L-DNR can't PXE boot if the host interface is disabled.
if !self.is_ars_121l_dnr().await? {
self.set_host_interfaces(Disabled).await?;
}
self.set_kcs_privilege(supermicro::Privilege::Callback)
Expand All @@ -355,15 +351,15 @@ impl Redfish for Bmc {
let is_syslockdown = self.get_syslockdown().await?;
let message = format!("SysLockdownEnabled={is_syslockdown}, kcs_privilege={kcs_privilege:#?}, host_interface_enabled={is_hi_on}");

// Grace-Grace SMCs (ARS-121L-DNR) need host_interface enabled even with lockdown
let is_grace_grace = self.is_grace_grace_smc().await?;
let must_keep_host_interface_enabled = self.is_ars_121l_dnr().await?;
let kcs_locked =
kcs_privilege.is_none() || kcs_privilege == Some(supermicro::Privilege::Callback);
let kcs_unlocked = kcs_privilege.is_none()
|| kcs_privilege == Some(supermicro::Privilege::Administrator);

let is_locked = is_syslockdown
&& kcs_privilege == supermicro::Privilege::Callback
&& (is_grace_grace || !is_hi_on);
let is_unlocked = !is_syslockdown
&& kcs_privilege == supermicro::Privilege::Administrator
&& is_hi_on;
let is_locked =
is_syslockdown && kcs_locked && (must_keep_host_interface_enabled || !is_hi_on);
let is_unlocked = !is_syslockdown && kcs_unlocked && is_hi_on;
Ok(Status {
message,
status: if is_locked {
Expand Down Expand Up @@ -1307,7 +1303,7 @@ impl Bmc {

macro_rules! add_keys {
($name:literal, $value:expr) => {
for real_key in bios_keys.remove($name).unwrap_or(vec![]) {
for real_key in bios_keys.remove($name).unwrap_or_default() {
bios_attrs.push((real_key, $value.into()));
}
};
Expand All @@ -1328,6 +1324,7 @@ impl Bmc {
add_keys!("IntelVTforDirectedI/O(VT-d)", EnableDisable::Enable);
add_keys!("IntelVirtualizationTechnology", EnableDisable::Enable);
add_keys!("SR-IOVSupport", EnabledDisabled::Enabled);
add_keys!("SR_IOVSupport", EnabledDisabled::Enabled);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the difference between this and the existing line 1326?


// UEFI NIC boot
add_keys!("IPv4HTTPSupport", EnabledDisabled::Enabled);
Expand Down Expand Up @@ -1372,13 +1369,16 @@ impl Bmc {
Ok(bios_attrs)
}

async fn get_kcs_privilege(&self) -> Result<supermicro::Privilege, RedfishError> {
async fn get_kcs_privilege(&self) -> Result<Option<supermicro::Privilege>, RedfishError> {
if self.is_mgx_c2().await? {
if !self.bmc_supports_ipmi_host_iface().await? {
return Ok(None);
}
let enabled = self.get_ipmi_host_interface_enabled().await?;
return if enabled {
Ok(supermicro::Privilege::Administrator)
Ok(Some(supermicro::Privilege::Administrator))
} else {
Ok(supermicro::Privilege::Callback)
Ok(Some(supermicro::Privilege::Callback))
};
}

Expand All @@ -1404,18 +1404,29 @@ impl Bmc {
expected_type: "&str".to_string(),
url: url.to_string(),
})?;
p_str.parse().map_err(|_| RedfishError::InvalidKeyType {
key: key.to_string(),
expected_type: "oem::supermicro::Privilege".to_string(),
url: url.to_string(),
})
p_str
.parse()
.map(Some)
.map_err(|_| RedfishError::InvalidKeyType {
key: key.to_string(),
expected_type: "oem::supermicro::Privilege".to_string(),
url: url.to_string(),
})
}

async fn set_kcs_privilege(
&self,
privilege: supermicro::Privilege,
) -> Result<(), RedfishError> {
if self.is_mgx_c2().await? {
if !self.bmc_supports_ipmi_host_iface().await? {
tracing::warn!(
smc_bmc_ip = self.s.client.host(),
"MGX C2 BMC firmware is older than {MIN_BMC_FW_IPMI_HOST_IFACE}; \
skipping IPMIHostInterface write"
);
return Ok(());
}
let enabled = privilege == supermicro::Privilege::Administrator;
return self.set_ipmi_host_interface(enabled).await;
}
Expand All @@ -1441,18 +1452,7 @@ impl Bmc {

/// Disable/enable SSIF in-band access via `IPMIHostInterface` on `Systems/{id}`.
/// Used for MGX C2 systems that lack the KCSInterface endpoint.
/// No-op when the BMC firmware is older than 01.05.01.
async fn set_ipmi_host_interface(&self, enabled: bool) -> Result<(), RedfishError> {
if !self.bmc_supports_ipmi_host_iface().await? {
let smc_bmc_ip = self.s.client.host();
tracing::warn!(
smc_bmc_ip,
"MGX C2 BMC firmware is older than {MIN_BMC_FW_IPMI_HOST_IFACE}; \
skipping IPMIHostInterface write"
);
return Ok(());
}

use crate::model::system::IpmiHostInterface;
let url = format!("Systems/{}", self.s.system_id());
let body = HashMap::from([(
Expand All @@ -1466,18 +1466,7 @@ impl Bmc {

/// Get whether SSIF in-band access is enabled via `IPMIHostInterface` on `Systems/{id}`.
/// Used for MGX C2 systems that lack the KCSInterface endpoint.
/// Returns `false` when the BMC firmware is older than 01.05.01.
async fn get_ipmi_host_interface_enabled(&self) -> Result<bool, RedfishError> {
if !self.bmc_supports_ipmi_host_iface().await? {
let smc_bmc_ip = self.s.client.host();
tracing::warn!(
smc_bmc_ip,
"MGX C2 BMC firmware is older than {MIN_BMC_FW_IPMI_HOST_IFACE}; \
IPMIHostInterface unavailable, reporting disabled"
);
return Ok(false);
}

let system = self.s.get_system().await?;
let iface = system
.ipmi_host_interface
Expand Down Expand Up @@ -1681,9 +1670,9 @@ impl Bmc {
self.change_boot_order(ordered).await
}

// BIOS attribute names by their clean name.
// e.g.{ QuietBoot -> [QuietBoot#002E]
// TXTSupport -> [TXTSupport#0062, TXTSupport#0072] }
// BIOS attribute names by their canonical name.
// e.g. QuietBoot -> [QuietBoot_002E]
// TXTSupport -> [TXTSupport_0062, TXTSupport_0072]
async fn bios_attributes_name_map(&self) -> Result<HashMap<String, Vec<String>>, RedfishError> {
let bios_attrs = self.s.bios_attributes().await?;

Expand All @@ -1696,7 +1685,10 @@ impl Bmc {
};
let mut by_name: HashMap<String, Vec<String>> = HashMap::with_capacity(attrs_map.len());
for k in attrs_map.keys() {
let clean_key = k.split('_').next().unwrap().to_string();
let clean_key = k
.rsplit_once('_')
.map_or(k.as_str(), |(name, _suffix)| name.trim_end_matches('_'))
.to_string();
by_name
.entry(clean_key)
.and_modify(|e| e.push(k.clone()))
Expand All @@ -1706,13 +1698,22 @@ impl Bmc {
}

/// MGX C2 systems use SSIF instead of x86 KCS, so the KCSInterface
/// endpoint doesn't exist. Detect them by matching the system model.
/// endpoint doesn't exist. Detect them by the NVIDIA PG535
/// processor-module identity.
/// See: https://docs.nvidia.com/dccpu/grace-perf-tuning-guide/system.html
async fn is_mgx_c2(&self) -> Result<bool, RedfishError> {
let model = self.s.get_system().await?.model.unwrap_or_default();
Ok(MGX_C2_MODELS.iter().any(|m| model.contains(m)))
let chassis = self
.s
.get_collection(ODataId {
odata_id: "/redfish/v1/Chassis".to_string(),
})
.await?
.try_get::<Chassis>()?;

Ok(chassis.members.iter().any(is_mgx_c2_processor_module))
}

async fn is_grace_grace_smc(&self) -> Result<bool, RedfishError> {
async fn is_ars_121l_dnr(&self) -> Result<bool, RedfishError> {
Ok(self
.s
.get_system()
Expand All @@ -1723,6 +1724,20 @@ impl Bmc {
}
}

fn is_mgx_c2_processor_module(chassis: &Chassis) -> bool {
chassis
.manufacturer
.as_deref()
.is_some_and(|manufacturer| manufacturer.eq_ignore_ascii_case("NVIDIA"))
&& (chassis
.model
.as_deref()
.is_some_and(|model| model.starts_with(MGX_C2_PROCESSOR_MODULE_MODEL_PREFIX))
|| chassis.part_number.as_deref().is_some_and(|part_number| {
part_number.contains(MGX_C2_PROCESSOR_MODULE_PART_NUMBER_FRAGMENT)
}))
}

// UpdateParameters is what is sent for a multipart firmware upload's metadata.
#[allow(clippy::type_complexity)]
#[derive(Serialize)]
Expand Down Expand Up @@ -1782,3 +1797,46 @@ impl UpdateParameters {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn identifies_mgx_c2_processor_modules() {
let cases = [
("PG535 model", Some("NVIDIA"), Some("PG535-B02"), None, true),
(
"2G535 part number",
Some("Nvidia"),
None,
Some("699-2G535-0200-310"),
true,
),
(
"unrelated NVIDIA module",
Some("NVIDIA"),
Some("B4240"),
Some("900-9D3B4-00CC-EA0"),
false,
),
(
"non-NVIDIA PG535",
Some("Supermicro"),
Some("PG535-B02"),
Some("699-2G535-0200-310"),
false,
),
];

for (case, manufacturer, model, part_number, expected) in cases {
let chassis = Chassis {
manufacturer: manufacturer.map(str::to_string),
model: model.map(str::to_string),
part_number: part_number.map(str::to_string),
..Default::default()
};
assert_eq!(is_mgx_c2_processor_module(&chassis), expected, "{case}");
}
}
}
Loading