Skip to content

Commit 29de93f

Browse files
authored
feat: add lenovo GB300 support (#92)
The Lenovo GB300 runs an AMI MegaRAC BMC but uses a distinct GB300 BIOS registry, so it gets its own vendor and AMI-client handling. **Changes** - New `RedfishVendor::LenovoGB300`, routed to `ami::Bmc` and added to the AMI `If-Match` groups. - Detection (`network.rs`): refine `AMI` → `LenovoGB300` when a Lenovo host system is paired with an NVIDIA GB300 baseboard. Systems are fetched individually (the HGX baseboard returns null fields under `$expand`); also prefer `System_0` over the first member. - GB300 behavior (`ami.rs`), verified against the BIOS registry: - Lockdown via `USB000` (prefixed enum) + host interface — no `ConfigBMC`/`KCSACP`. - Serial console, `clear_tpm` (`TCG006TPMClear`), and `machine_setup` use the GB300 prefixed enum values. - Infinite boot via `LEM0003=50` (no `EndlessBoot`); `clear_nvram` → `NotSupported` (no `RECV000`). - Host-interface/ConfigBMC URLs use the resolved manager id (`BMC_0`); shared `lockdown_status_from` helper. **Testing** Validated on a real Lenovo GB300 (detects as `LenovoGB300`, `System_0`/`BMC_0`; lockdown, machine-setup, boot-order functionality correct). --------- Signed-off-by: Krish Dandiwala <kdandiwala@nvidia.com>
1 parent 677c18d commit 29de93f

4 files changed

Lines changed: 233 additions & 82 deletions

File tree

src/ami.rs

Lines changed: 169 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ use crate::{
5757
/// AMI uses BIOS attribute SETUP001 for Administrator Password (UEFI password)
5858
const UEFI_PASSWORD_NAME: &str = "SETUP001";
5959

60+
/// LenovoGB300 has no "EndlessBoot" BIOS attribute; infinite
61+
/// boot is expressed via the LEM0003 boot-retry count, where 50 is the
62+
/// firmware's representation for endless retries.
63+
const GB300_INFINITE_BOOT_RETRY: i64 = 50;
64+
65+
/// Build a lockdown `Status` from the fully-locked / fully-unlocked booleans,
66+
/// defaulting to `Partial` when the readout is neither.
67+
fn lockdown_status_from(message: String, is_locked: bool, is_unlocked: bool) -> Status {
68+
Status {
69+
message,
70+
status: if is_locked {
71+
StatusInternal::Enabled
72+
} else if is_unlocked {
73+
StatusInternal::Disabled
74+
} else {
75+
StatusInternal::Partial
76+
},
77+
}
78+
}
79+
6080
pub struct Bmc {
6181
s: RedfishStandard,
6282
}
@@ -66,6 +86,58 @@ impl Bmc {
6686
Ok(Bmc { s })
6787
}
6888

89+
/// Serial-console BIOS attributes as `(key, enabled_value, disabled_value)`.
90+
/// A `disabled_value` of "any" means any value counts as correctly disabled.
91+
///
92+
/// LenovoGB300's BIOS registry mostly prefixes enum values with the
93+
/// attribute id, but irregularly: TER001/TER010 stay bare, the port is COM0
94+
/// not COM1, and the hyphen in VT-UTF8 is dropped. So both forms are listed
95+
/// explicitly per attribute rather than derived via a prefix rule.
96+
fn serial_console_attrs(&self) -> Vec<(&'static str, &'static str, &'static str)> {
97+
let gb300 = self.s.vendor == Some(RedfishVendor::LenovoGB300);
98+
// (key, generic_enabled, gb300_enabled, disabled)
99+
const ATTRS: &[(&str, &str, &str, &str)] = &[
100+
("TER001", "Enabled", "Enabled", "Disabled"), // Console Redirection
101+
("TER010", "Enabled", "Enabled", "Disabled"), // Console Redirection EMS
102+
("TER06B", "COM1", "TER06BCOM0", "any"), // Out-of-Band Mgmt Port
103+
("TER0021", "115200", "TER0021115200", "any"), // Bits per second
104+
("TER0020", "115200", "TER0020115200", "any"), // Bits per second EMS
105+
("TER012", "VT100Plus", "TER012VT100Plus", "any"), // Terminal Type
106+
("TER011", "VT-UTF8", "TER011VTUTF8", "any"), // Terminal Type EMS
107+
("TER05D", "None", "TER05DNone", "any"), // Flow Control
108+
];
109+
ATTRS
110+
.iter()
111+
.map(|&(key, generic, gb300_val, disabled)| {
112+
(key, if gb300 { gb300_val } else { generic }, disabled)
113+
})
114+
.collect()
115+
}
116+
117+
/// LenovoGB300 lockdown status: USB support (attribute-id prefixed enum,
118+
/// e.g. "USB000Disabled") plus the host interface. There is no KCS BIOS
119+
/// attribute on this platform, so it is not part of the status.
120+
async fn lockdown_status_gb300(&self) -> Result<Status, RedfishError> {
121+
let bios = self.s.bios().await?;
122+
let url = format!("Systems/{}/Bios", self.s.system_id());
123+
let attrs = jsonmap::get_object(&bios, "Attributes", &url)?;
124+
let usb000 = jsonmap::get_str(attrs, "USB000", "Bios Attributes")?;
125+
126+
let hi_url = format!("Managers/{}/HostInterfaces/Self", self.s.manager_id());
127+
let (_status, hi): (_, serde_json::Value) = self.s.client.get(&hi_url).await?;
128+
let hi_enabled = hi
129+
.get("InterfaceEnabled")
130+
.and_then(|v| v.as_bool())
131+
.unwrap_or(true);
132+
133+
let message = format!("usb_support={usb000}, host_interface={hi_enabled}");
134+
135+
let is_locked = usb000 == "USB000Disabled" && !hi_enabled;
136+
let is_unlocked = usb000 == "USB000Enabled" && hi_enabled;
137+
138+
Ok(lockdown_status_from(message, is_locked, is_unlocked))
139+
}
140+
69141
/// LenovoAMI-specific lockdown status via OEM ConfigBMC endpoint.
70142
async fn lockdown_status_lenovo_ami(&self) -> Result<Status, RedfishError> {
71143
const LOCKDOWN_FIELDS: &[&str] = &[
@@ -75,8 +147,8 @@ impl Bmc {
75147
"LockdownBiosUpgradeDowngrade",
76148
];
77149

78-
let (_status, body): (_, serde_json::Value) =
79-
self.s.client.get("Managers/Self/Oem/ConfigBMC").await?;
150+
let config_bmc_url = format!("Managers/{}/Oem/ConfigBMC", self.s.manager_id());
151+
let (_status, body): (_, serde_json::Value) = self.s.client.get(&config_bmc_url).await?;
80152

81153
let values: Vec<&str> = LOCKDOWN_FIELDS
82154
.iter()
@@ -93,16 +165,7 @@ impl Bmc {
93165
let is_locked = values.iter().all(|&v| v == "Enable");
94166
let is_unlocked = values.iter().all(|&v| v == "Disable");
95167

96-
Ok(Status {
97-
message,
98-
status: if is_locked {
99-
StatusInternal::Enabled
100-
} else if is_unlocked {
101-
StatusInternal::Disabled
102-
} else {
103-
StatusInternal::Partial
104-
},
105-
})
168+
Ok(lockdown_status_from(message, is_locked, is_unlocked))
106169
}
107170
}
108171
impl Redfish for Bmc {
@@ -443,47 +506,59 @@ impl Redfish for Bmc {
443506
("LockdownBiosSettingsChange", value),
444507
("LockdownBiosUpgradeDowngrade", value),
445508
]);
446-
return self
447-
.s
448-
.client
449-
.post("Managers/Self/Oem/ConfigBMC", body)
450-
.await
451-
.map(|_| ());
509+
let config_bmc_url = format!("Managers/{}/Oem/ConfigBMC", self.s.manager_id());
510+
return self.s.client.post(&config_bmc_url, body).await.map(|_| ());
452511
}
453512

454-
let (kcsacp, usb, hi_enabled) = match target {
455-
Enabled => ("Deny All", "Disabled", false),
456-
Disabled => ("Allow All", "Enabled", true),
513+
// LenovoGB300 has neither the OEM ConfigBMC endpoint nor
514+
// the generic AMI `KCSACP` BIOS attribute, and its USB enum values
515+
// are attribute-id prefixed (e.g. "USB000Disabled").
516+
let hi_enabled = target == Disabled;
517+
let bios_attrs = if self.s.vendor == Some(RedfishVendor::LenovoGB300) {
518+
let usb = match target {
519+
Enabled => "USB000Disabled",
520+
Disabled => "USB000Enabled",
521+
};
522+
HashMap::from([("USB000".to_string(), usb.into())])
523+
} else {
524+
let (kcsacp, usb) = match target {
525+
Enabled => ("Deny All", "Disabled"),
526+
Disabled => ("Allow All", "Enabled"),
527+
};
528+
HashMap::from([
529+
("KCSACP".to_string(), kcsacp.into()),
530+
("USB000".to_string(), usb.into()),
531+
])
457532
};
458-
self.set_bios(HashMap::from([
459-
("KCSACP".to_string(), kcsacp.into()),
460-
("USB000".to_string(), usb.into()),
461-
]))
462-
.await?;
533+
self.set_bios(bios_attrs).await?;
534+
535+
let hi_url = format!("Managers/{}/HostInterfaces/Self", self.s.manager_id());
463536
let hi_body = HashMap::from([("InterfaceEnabled", hi_enabled)]);
464-
self.s
465-
.client
466-
.patch_with_if_match("Managers/Self/HostInterfaces/Self", hi_body)
467-
.await
537+
self.s.client.patch_with_if_match(&hi_url, hi_body).await
468538
})
469539
}
470540

471541
/// AMI lockdown status - checks KCS access, USB support, and Host Interface.
472-
/// On LenovoAMI, reads the OEM ConfigBMC endpoint instead.
542+
/// On LenovoAMI, reads the OEM ConfigBMC endpoint instead. On LenovoGB300,
543+
/// checks USB support and the host interface (no KCS/ConfigBMC there).
473544
fn lockdown_status<'a>(&'a self) -> crate::RedfishFuture<'a, Result<Status, RedfishError>> {
474545
Box::pin(async move {
475546
if self.s.vendor == Some(RedfishVendor::LenovoAMI) {
476547
return self.lockdown_status_lenovo_ami().await;
477548
}
478549

550+
if self.s.vendor == Some(RedfishVendor::LenovoGB300) {
551+
return self.lockdown_status_gb300().await;
552+
}
553+
479554
let bios = self.s.bios().await?;
480555
let url = format!("Systems/{}/Bios", self.s.system_id());
481556
let attrs = jsonmap::get_object(&bios, "Attributes", &url)?;
482557
let kcsacp = jsonmap::get_str(attrs, "KCSACP", "Bios Attributes")?;
483558
let usb000 = jsonmap::get_str(attrs, "USB000", "Bios Attributes")?;
484559

485-
let hi_url = "Managers/Self/HostInterfaces/Self";
486-
let (_status, hi): (_, serde_json::Value) = self.s.client.get(hi_url).await?;
560+
let hi_url = format!("Managers/{}/HostInterfaces/Self", self.s.manager_id());
561+
let (_status, hi): (_, serde_json::Value) = self.s.client.get(&hi_url).await?;
487562
let hi_enabled = hi
488563
.get("InterfaceEnabled")
489564
.and_then(|v| v.as_bool())
@@ -497,34 +572,18 @@ impl Redfish for Bmc {
497572
let is_locked = kcsacp == "Deny All" && usb000 == "Disabled" && !hi_enabled;
498573
let is_unlocked = kcsacp == "Allow All" && usb000 == "Enabled" && hi_enabled;
499574

500-
Ok(Status {
501-
message,
502-
status: if is_locked {
503-
StatusInternal::Enabled
504-
} else if is_unlocked {
505-
StatusInternal::Disabled
506-
} else {
507-
StatusInternal::Partial
508-
},
509-
})
575+
Ok(lockdown_status_from(message, is_locked, is_unlocked))
510576
})
511577
}
512578

513579
/// Setup serial console for AMI BMC via BIOS attributes.
514580
fn setup_serial_console<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> {
515581
Box::pin(async move {
516-
use serde_json::Value;
517-
518-
let attributes: HashMap<String, Value> = HashMap::from([
519-
("TER001".to_string(), "Enabled".into()), // Console Redirection
520-
("TER010".to_string(), "Enabled".into()), // Console Redirection EMS
521-
("TER06B".to_string(), "COM1".into()), // Out-of-Band Mgmt Port
522-
("TER0021".to_string(), "115200".into()), // Bits per second
523-
("TER0020".to_string(), "115200".into()), // Bits per second EMS
524-
("TER012".to_string(), "VT100Plus".into()), // Terminal Type
525-
("TER011".to_string(), "VT-UTF8".into()), // Terminal Type EMS
526-
("TER05D".to_string(), "None".into()), // Flow Control
527-
]);
582+
let attributes: HashMap<String, serde_json::Value> = self
583+
.serial_console_attrs()
584+
.into_iter()
585+
.map(|(key, enabled, _)| (key.to_string(), enabled.into()))
586+
.collect();
528587

529588
self.set_bios(attributes).await
530589
})
@@ -539,16 +598,7 @@ impl Redfish for Bmc {
539598
let url = format!("Systems/{}/Bios", self.s.system_id());
540599
let attrs = jsonmap::get_object(&bios, "Attributes", &url)?;
541600

542-
let expected = vec![
543-
("TER001", "Enabled", "Disabled"),
544-
("TER010", "Enabled", "Disabled"),
545-
("TER06B", "COM1", "any"),
546-
("TER0021", "115200", "any"),
547-
("TER0020", "115200", "any"),
548-
("TER012", "VT100Plus", "any"),
549-
("TER011", "VT-UTF8", "any"),
550-
("TER05D", "None", "any"),
551-
];
601+
let expected = self.serial_console_attrs();
552602

553603
let mut message = String::new();
554604
let mut enabled = true;
@@ -672,7 +722,15 @@ impl Redfish for Bmc {
672722

673723
fn clear_tpm<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> {
674724
Box::pin(async move {
675-
self.set_bios(HashMap::from([("TCG006".to_string(), "TPM Clear".into())]))
725+
// GB300's Grace BIOS registry prefixes the TCG006 enum value with
726+
// the attribute id ("TCG006TPMClear"); other AMI platforms use the
727+
// bare "TPM Clear".
728+
let clear_value = if self.s.vendor == Some(RedfishVendor::LenovoGB300) {
729+
"TCG006TPMClear"
730+
} else {
731+
"TPM Clear"
732+
};
733+
self.set_bios(HashMap::from([("TCG006".to_string(), clear_value.into())]))
676734
.await
677735
})
678736
}
@@ -1047,8 +1105,8 @@ impl Redfish for Bmc {
10471105
Box::pin(async move {
10481106
let interface_enabled = target == EnabledDisabled::Disabled;
10491107
let hi_body = HashMap::from([("InterfaceEnabled", interface_enabled)]);
1050-
let hi_url = "Managers/Self/HostInterfaces/Self";
1051-
self.s.client.patch_with_if_match(hi_url, hi_body).await
1108+
let hi_url = format!("Managers/{}/HostInterfaces/Self", self.s.manager_id());
1109+
self.s.client.patch_with_if_match(&hi_url, hi_body).await
10521110
})
10531111
}
10541112

@@ -1078,6 +1136,13 @@ impl Redfish for Bmc {
10781136
/// AMI clear_nvram - sets RECV000 (Reset NVRAM) to "Enabled"
10791137
fn clear_nvram<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> {
10801138
Box::pin(async move {
1139+
// The GB300 Grace BIOS registry has no RECV000 (Reset NVRAM)
1140+
// attribute, so there is no equivalent knob to set.
1141+
if self.s.vendor == Some(RedfishVendor::LenovoGB300) {
1142+
return Err(RedfishError::NotSupported(
1143+
"clear_nvram: no RECV000 BIOS attribute on LenovoGB300".to_string(),
1144+
));
1145+
}
10811146
self.set_bios(HashMap::from([("RECV000".to_string(), "Enabled".into())]))
10821147
.await
10831148
})
@@ -1098,6 +1163,14 @@ impl Redfish for Bmc {
10981163

10991164
fn enable_infinite_boot<'a>(&'a self) -> crate::RedfishFuture<'a, Result<(), RedfishError>> {
11001165
Box::pin(async move {
1166+
if self.s.vendor == Some(RedfishVendor::LenovoGB300) {
1167+
return self
1168+
.set_bios(HashMap::from([(
1169+
"LEM0003".to_string(),
1170+
GB300_INFINITE_BOOT_RETRY.into(),
1171+
)]))
1172+
.await;
1173+
}
11011174
self.set_bios(HashMap::from([(
11021175
"EndlessBoot".to_string(),
11031176
"Enabled".into(),
@@ -1113,6 +1186,16 @@ impl Redfish for Bmc {
11131186
let bios = self.s.bios().await?;
11141187
let url = format!("Systems/{}/Bios", self.s.system_id());
11151188
let attrs = jsonmap::get_object(&bios, "Attributes", &url)?;
1189+
if self.s.vendor == Some(RedfishVendor::LenovoGB300) {
1190+
// LEM0003 may be reported as a JSON number or a numeric string.
1191+
let Some(value) = attrs.get("LEM0003") else {
1192+
return Ok(None);
1193+
};
1194+
let retry = value
1195+
.as_i64()
1196+
.or_else(|| value.as_str().and_then(|s| s.parse::<i64>().ok()));
1197+
return Ok(retry.map(|r| r == GB300_INFINITE_BOOT_RETRY));
1198+
}
11161199
let endless_boot = jsonmap::get_str(attrs, "EndlessBoot", "Bios Attributes")?;
11171200
Ok(Some(endless_boot == "Enabled"))
11181201
})
@@ -1348,6 +1431,24 @@ impl Bmc {
13481431

13491432
/// Get the BIOS attributes for machine setup.
13501433
fn machine_setup_attrs(&self) -> HashMap<String, serde_json::Value> {
1434+
// The LenovoGB300 (Grace-based) uses a distinct BIOS registry: enum
1435+
// values are prefixed with the attribute ID (e.g. "PCIS007Enabled"),
1436+
// there is no Intel VMX knob (VMXEN) or boot-mode selector (FBO001),
1437+
// and "Infinite Boot" is expressed via the LEM0003 retry count (50 =
1438+
// endless boot) instead of the "EndlessBoot" attribute.
1439+
if self.s.vendor == Some(RedfishVendor::LenovoGB300) {
1440+
return HashMap::from([
1441+
("PCIS007".to_string(), "PCIS007Enabled".into()), // SR-IOV Support
1442+
("LEM0001".to_string(), 3.into()), // PXE retry count
1443+
("NWSK000".to_string(), "NWSK000Enabled".into()), // Network Stack
1444+
("NWSK001".to_string(), "NWSK001Disabled".into()), // IPv4 PXE Support
1445+
("NWSK006".to_string(), "NWSK006Enabled".into()), // IPv4 HTTP Support
1446+
("NWSK002".to_string(), "NWSK002Disabled".into()), // IPv6 PXE Support
1447+
("NWSK007".to_string(), "NWSK007Disabled".into()), // IPv6 HTTP Support
1448+
("LEM0003".to_string(), GB300_INFINITE_BOOT_RETRY.into()), // Infinite Boot
1449+
]);
1450+
}
1451+
13511452
HashMap::from([
13521453
("VMXEN".to_string(), "Enable".into()), // VMX (Intel Virtualization)
13531454
("PCIS007".to_string(), "Enabled".into()), // SR-IOV Support

src/model/service_root.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub struct ServiceRoot {
6161
pub enum RedfishVendor {
6262
Lenovo,
6363
LenovoAMI,
64+
LenovoGB300,
6465
Dell,
6566
NvidiaDpu,
6667
Supermicro,

0 commit comments

Comments
 (0)