Skip to content

Commit e81a7d8

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

3 files changed

Lines changed: 119 additions & 39 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: 104 additions & 39 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,29 +1326,77 @@ 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())]);
1323-
self.s
1324-
.client
1325-
.patch(&url, body)
1326-
.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-
})
1340-
.map(|_status_code| ())
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(version_compare::compare(&fw, MIN_BMC_FW_IPMI_HOST_IFACE)
1350+
.is_ok_and(|c| c != version_compare::Cmp::Lt))
1351+
}
1352+
1353+
/// Disable/enable SSIF in-band access via `IPMIHostInterface` on `Systems/{id}`.
1354+
/// Used for MGX C2 systems that lack the KCSInterface endpoint.
1355+
/// No-op when the BMC firmware is older than 01.05.01.
1356+
async fn set_ipmi_host_interface(&self, enabled: bool) -> Result<(), RedfishError> {
1357+
if !self.bmc_supports_ipmi_host_iface().await? {
1358+
let smc_bmc_ip = self.s.client.host();
1359+
tracing::warn!(
1360+
smc_bmc_ip,
1361+
"MGX C2 BMC firmware is older than {MIN_BMC_FW_IPMI_HOST_IFACE}; \
1362+
skipping IPMIHostInterface write"
1363+
);
1364+
return Ok(());
1365+
}
1366+
1367+
use crate::model::system::IpmiHostInterface;
1368+
let url = format!("Systems/{}", self.s.system_id());
1369+
let body = HashMap::from([(
1370+
"IPMIHostInterface",
1371+
IpmiHostInterface {
1372+
service_enabled: enabled,
1373+
},
1374+
)]);
1375+
self.s.client.patch(&url, body).await.map(|_status_code| ())
1376+
}
1377+
1378+
/// Get whether SSIF in-band access is enabled via `IPMIHostInterface` on `Systems/{id}`.
1379+
/// Used for MGX C2 systems that lack the KCSInterface endpoint.
1380+
/// Returns `false` when the BMC firmware is older than 01.05.01.
1381+
async fn get_ipmi_host_interface_enabled(&self) -> Result<bool, RedfishError> {
1382+
if !self.bmc_supports_ipmi_host_iface().await? {
1383+
let smc_bmc_ip = self.s.client.host();
1384+
tracing::warn!(
1385+
smc_bmc_ip,
1386+
"MGX C2 BMC firmware is older than {MIN_BMC_FW_IPMI_HOST_IFACE}; \
1387+
IPMIHostInterface unavailable, reporting disabled"
1388+
);
1389+
return Ok(false);
1390+
}
1391+
1392+
let system = self.s.get_system().await?;
1393+
let iface = system
1394+
.ipmi_host_interface
1395+
.ok_or_else(|| RedfishError::MissingKey {
1396+
key: "IPMIHostInterface".to_string(),
1397+
url: format!("Systems/{}", self.s.system_id()),
1398+
})?;
1399+
Ok(iface.service_enabled)
13411400
}
13421401

13431402
async fn is_host_interface_enabled(&self) -> Result<bool, RedfishError> {
@@ -1581,7 +1640,13 @@ impl Bmc {
15811640
Ok(by_name)
15821641
}
15831642

1584-
// Check if this is a Grace-Grace SMC (ARS-121L-DNR) that needs host_interface enabled
1643+
/// MGX C2 systems use SSIF instead of x86 KCS, so the KCSInterface
1644+
/// endpoint doesn't exist. Detect them by matching the system model.
1645+
async fn is_mgx_c2(&self) -> Result<bool, RedfishError> {
1646+
let model = self.s.get_system().await?.model.unwrap_or_default();
1647+
Ok(MGX_C2_MODELS.iter().any(|m| model.contains(m)))
1648+
}
1649+
15851650
async fn is_grace_grace_smc(&self) -> Result<bool, RedfishError> {
15861651
Ok(self
15871652
.s

0 commit comments

Comments
 (0)