Skip to content

Commit 17aa841

Browse files
committed
feat: support disabling interface between host and BMC for SMC MGX C2 servers
1 parent 6ab9486 commit 17aa841

3 files changed

Lines changed: 123 additions & 34 deletions

File tree

src/model/system.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ pub struct ComputerSystem {
201201
pub serial_console: Option<SerialConsole>, // Newer Redfish impls, inc Supermicro
202202
pub links: Option<ComputerSystemLinks>,
203203
pub boot_progress: Option<BootProgress>,
204+
#[serde(rename = "IPMIHostInterface")]
205+
pub ipmi_host_interface: Option<IpmiHostInterface>, // MGX C2 Supermicro (SSIF, not KCS); BMC firmware 01.05.01+
206+
}
207+
208+
/// SSIF in-band host-to-BMC interface on MGX C2 Supermicro systems.
209+
/// Present at `Systems/{id}` when the BMC firmware is 01.05.01+.
210+
#[derive(Debug, Serialize, Deserialize, Clone)]
211+
#[serde(rename_all = "PascalCase")]
212+
pub struct IpmiHostInterface {
213+
pub service_enabled: bool,
204214
}
205215

206216
#[derive(Debug, Serialize, Deserialize, Default, Clone)]

src/network.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ impl RedfishHttpClient {
279279
}
280280
}
281281

282+
/// Returns the hostname or IP address of the BMC this client connects to.
283+
pub fn host(&self) -> &str {
284+
&self.endpoint.host
285+
}
286+
282287
/// Returns `true` if this client has no credentials (i.e. anonymous/unauthenticated).
283288
pub fn is_anonymous(&self) -> bool {
284289
self.endpoint.user.is_none()

src/supermicro.rs

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,20 @@ use crate::{
5858

5959
const MELLANOX_UEFI_HTTP_IPV4: &str = "UEFI HTTP IPv4 Mellanox Network Adapter";
6060
const NVIDIA_UEFI_HTTP_IPV4: &str = "UEFI HTTP IPv4 Nvidia Network Adapter";
61+
62+
/// MGX C2 systems use SSIF instead of x86 KCS for in-band BMC communication,
63+
/// so the KCSInterface endpoint doesn't exist. These models require the
64+
/// IPMIHostInterface fallback on Systems/{id}.
65+
const MGX_C2_MODELS: [&str; 4] = [
66+
"ARS-121L-DNR",
67+
"ARS-221GL-NR",
68+
"SYS-221H-TNR",
69+
"SYS-221H-TN24R",
70+
];
71+
72+
/// Minimum BMC firmware version that exposes `IPMIHostInterface` on
73+
/// `Systems/{id}` for MGX C2 systems.
74+
const MIN_BMC_FW_IPMI_HOST_IFACE: &str = "01.05.01";
6175
const HARD_DISK: &str = "UEFI Hard Disk";
6276
const NETWORK: &str = "UEFI Network";
6377

@@ -325,19 +339,7 @@ impl Redfish for Bmc {
325339
fn lockdown_status<'a>(&'a self) -> crate::RedfishFuture<'a, Result<Status, RedfishError>> {
326340
Box::pin(async move {
327341
let is_hi_on = self.is_host_interface_enabled().await?;
328-
let kcs_privilege = match self.get_kcs_privilege().await {
329-
Ok(priviledge) => Ok(Some(priviledge)),
330-
Err(e) => {
331-
// The Grace-Grace Supermicros in our GB200 lab do not seem to support
332-
// querying KCS access from the host to its BMC. Use this workaround to
333-
// temporarily enable ingesting these servers.
334-
if e.not_found() {
335-
Ok(None)
336-
} else {
337-
Err(e)
338-
}
339-
}
340-
}?;
342+
let kcs_privilege = self.get_kcs_privilege().await?;
341343

342344
let is_syslockdown = self.get_syslockdown().await?;
343345
let message = format!("SysLockdownEnabled={is_syslockdown}, kcs_privilege={kcs_privilege:#?}, host_interface_enabled={is_hi_on}");
@@ -346,14 +348,10 @@ impl Redfish for Bmc {
346348
let is_grace_grace = self.is_grace_grace_smc().await?;
347349

348350
let is_locked = is_syslockdown
349-
&& kcs_privilege
350-
.clone()
351-
.unwrap_or(supermicro::Privilege::Callback)
352-
== supermicro::Privilege::Callback
351+
&& kcs_privilege == supermicro::Privilege::Callback
353352
&& (is_grace_grace || !is_hi_on);
354353
let is_unlocked = !is_syslockdown
355-
&& kcs_privilege.unwrap_or(supermicro::Privilege::Administrator)
356-
== supermicro::Privilege::Administrator
354+
&& kcs_privilege == supermicro::Privilege::Administrator
357355
&& is_hi_on;
358356
Ok(Status {
359357
message,
@@ -1286,11 +1284,24 @@ impl Bmc {
12861284
}
12871285

12881286
async fn get_kcs_privilege(&self) -> Result<supermicro::Privilege, RedfishError> {
1287+
if self.is_mgx_c2().await? {
1288+
let enabled = self.get_ipmi_host_interface_enabled().await?;
1289+
return if enabled {
1290+
Ok(supermicro::Privilege::Administrator)
1291+
} else {
1292+
Ok(supermicro::Privilege::Callback)
1293+
};
1294+
}
1295+
12891296
let url = format!(
12901297
"Managers/{}/Oem/Supermicro/KCSInterface",
12911298
self.s.manager_id()
12921299
);
1293-
let (_, body): (_, HashMap<String, serde_json::Value>) = self.s.client.get(&url).await?;
1300+
let (_, body) = self
1301+
.s
1302+
.client
1303+
.get::<HashMap<String, serde_json::Value>>(&url)
1304+
.await?;
12941305
let key = "Privilege";
12951306
let p_str = body
12961307
.get(key)
@@ -1315,31 +1326,88 @@ impl Bmc {
13151326
&self,
13161327
privilege: supermicro::Privilege,
13171328
) -> Result<(), RedfishError> {
1329+
if self.is_mgx_c2().await? {
1330+
let enabled = privilege == supermicro::Privilege::Administrator;
1331+
return self.set_ipmi_host_interface(enabled).await;
1332+
}
1333+
13181334
let url = format!(
13191335
"Managers/{}/Oem/Supermicro/KCSInterface",
13201336
self.s.manager_id()
13211337
);
13221338
let body = HashMap::from([("Privilege", privilege.to_string())]);
1339+
self.s.client.patch(&url, body).await?;
1340+
Ok(())
1341+
}
1342+
1343+
/// Returns `true` when the BMC firmware version is at least
1344+
/// [`MIN_BMC_FW_IPMI_HOST_IFACE`] (`01.05.01`), which is the first
1345+
/// version to expose `IPMIHostInterface` on `Systems/{id}`.
1346+
async fn bmc_supports_ipmi_host_iface(&self) -> Result<bool, RedfishError> {
1347+
let manager = self.s.get_manager().await?;
1348+
let fw = manager.firmware_version.unwrap_or_default();
1349+
Ok(
1350+
version_compare::compare(&fw, MIN_BMC_FW_IPMI_HOST_IFACE)
1351+
.is_ok_and(|c| c != version_compare::Cmp::Lt),
1352+
)
1353+
}
1354+
1355+
/// Disable/enable SSIF in-band access via `IPMIHostInterface` on `Systems/{id}`.
1356+
/// Used for MGX C2 systems that lack the KCSInterface endpoint.
1357+
/// No-op when the BMC firmware is older than 01.05.01.
1358+
async fn set_ipmi_host_interface(
1359+
&self,
1360+
enabled: bool,
1361+
) -> Result<(), RedfishError> {
1362+
if !self.bmc_supports_ipmi_host_iface().await? {
1363+
let smc_bmc_ip = self.s.client.host();
1364+
tracing::warn!(
1365+
smc_bmc_ip,
1366+
"MGX C2 BMC firmware is older than {MIN_BMC_FW_IPMI_HOST_IFACE}; \
1367+
skipping IPMIHostInterface write"
1368+
);
1369+
return Ok(());
1370+
}
1371+
1372+
use crate::model::system::IpmiHostInterface;
1373+
let url = format!("Systems/{}", self.s.system_id());
1374+
let body = HashMap::from([(
1375+
"IPMIHostInterface",
1376+
IpmiHostInterface {
1377+
service_enabled: enabled,
1378+
},
1379+
)]);
13231380
self.s
13241381
.client
13251382
.patch(&url, body)
13261383
.await
1327-
.or_else(|err| {
1328-
// The Grace-Grace Supermicros in our GB200 lab do not seem to support
1329-
// disabling KCS access from the host to its BMC. Use this workaround to
1330-
// temporarily enable ingesting these servers.
1331-
if err.not_found() {
1332-
tracing::warn!(
1333-
"Supermicro was uanble to find {url}: {err}; not returning error to caller"
1334-
);
1335-
Ok((StatusCode::OK, None))
1336-
} else {
1337-
Err(err)
1338-
}
1339-
})
13401384
.map(|_status_code| ())
13411385
}
13421386

1387+
/// Get whether SSIF in-band access is enabled via `IPMIHostInterface` on `Systems/{id}`.
1388+
/// Used for MGX C2 systems that lack the KCSInterface endpoint.
1389+
/// Returns `false` when the BMC firmware is older than 01.05.01.
1390+
async fn get_ipmi_host_interface_enabled(&self) -> Result<bool, RedfishError> {
1391+
if !self.bmc_supports_ipmi_host_iface().await? {
1392+
let smc_bmc_ip = self.s.client.host();
1393+
tracing::warn!(
1394+
smc_bmc_ip,
1395+
"MGX C2 BMC firmware is older than {MIN_BMC_FW_IPMI_HOST_IFACE}; \
1396+
IPMIHostInterface unavailable, reporting disabled"
1397+
);
1398+
return Ok(false);
1399+
}
1400+
1401+
let system = self.s.get_system().await?;
1402+
let iface = system.ipmi_host_interface.ok_or_else(|| {
1403+
RedfishError::MissingKey {
1404+
key: "IPMIHostInterface".to_string(),
1405+
url: format!("Systems/{}", self.s.system_id()),
1406+
}
1407+
})?;
1408+
Ok(iface.service_enabled)
1409+
}
1410+
13431411
async fn is_host_interface_enabled(&self) -> Result<bool, RedfishError> {
13441412
let url = format!("Managers/{}/HostInterfaces", self.s.manager_id());
13451413
let host_interface_ids = self.s.get_members(&url).await?;
@@ -1581,7 +1649,13 @@ impl Bmc {
15811649
Ok(by_name)
15821650
}
15831651

1584-
// Check if this is a Grace-Grace SMC (ARS-121L-DNR) that needs host_interface enabled
1652+
/// MGX C2 systems use SSIF instead of x86 KCS, so the KCSInterface
1653+
/// endpoint doesn't exist. Detect them by matching the system model.
1654+
async fn is_mgx_c2(&self) -> Result<bool, RedfishError> {
1655+
let model = self.s.get_system().await?.model.unwrap_or_default();
1656+
Ok(MGX_C2_MODELS.iter().any(|m| model.contains(m)))
1657+
}
1658+
15851659
async fn is_grace_grace_smc(&self) -> Result<bool, RedfishError> {
15861660
Ok(self
15871661
.s

0 commit comments

Comments
 (0)