Skip to content

Commit 2957371

Browse files
committed
feat: add Dell set_boot_override impl via HttpDev1Uri BIOS attribute
Implements `set_boot_override` for Dell iDRAC by PATCHing the `HttpDev1Uri`-related BIOS attributes via `/Systems/{id}/Bios/Settings`. Returns the BIOS config job ID; callers can DELETE it to cancel before the BIOS reboot if needed. Worth noting Dell doesn't expose the standard Redfish `Boot.HttpBootUri` property, and also rejects PATCHing `BootSourceOverrideTarget/Enabled` via `/Systems/{id}/Settings` (only `Boot.BootOrder` and `BootSourceOverrideMode` are accepted there). The `HttpDev1Uri` attribute is the Dell-documented path for pinning UEFI HTTP boot URLs. Verified end-to-end: - iDRAC9: R760 (BIOS 2.5.4), R760xd2 (BIOS 1.7.5), XE9680 (some). - iDRAC10: R670 (BIOS 1.7.5). Integration tests cover both paths: - dell mockup (`HttpDev1Uri.ReadOnly=false`) exercises the `PATCH` path. - dell_multi_dpu mockup (`HttpDev1Uri.ReadOnly=true`) exercises the locked path. Note that there seemed to be some machines that were reporting as being locked/read-only, even though they didn't appear to be in lockdown. Not really sure what was going on there, but if for some reason the PATCHing fails, we'll just return the error (and it will be up to the caller to decide what they want to do). Signed-off-by: Chet Nichols III <chetn@nvidia.com>
1 parent 6da004b commit 2957371

5 files changed

Lines changed: 153 additions & 11 deletions

File tree

src/dell.rs

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -604,18 +604,85 @@ impl Redfish for Bmc {
604604
})
605605
}
606606

607-
/// Not yet implemented on Dell iDRAC. Dell does not expose the standard
608-
/// Redfish `Boot.HttpBootUri` property; setting an HTTP boot URI on Dell
609-
/// requires PATCHing the `HttpDev1Uri` and related BIOS attributes via
610-
/// `/Systems/{id}/Bios/Settings`. Planned as a follow-up PR.
607+
/// Dell iDRAC does not expose the standard Redfish `Boot.HttpBootUri`
608+
/// property, and rejects PATCHes to `/Systems/{id}/Settings` that include
609+
/// `BootSourceOverrideTarget` or `BootSourceOverrideEnabled` (only
610+
/// `Boot.BootOrder` and `Boot.BootSourceOverrideMode` are accepted via
611+
/// that endpoint). The Dell-specific path for pinning a UEFI HTTP boot URL
612+
/// is via the `HttpDev1Uri` BIOS attribute (plus its `HttpDev1EnDis`,
613+
/// `HttpDev1DhcpEnDis`, `HttpDev1Protocol` siblings) PATCH'd to
614+
/// `/Systems/{id}/Bios/Settings` with `@Redfish.SettingsApplyTime: OnReset`.
615+
///
616+
/// That creates a BIOS config job that applies on next reboot; the job ID
617+
/// is returned so callers can `DELETE` it to cancel before reboot.
618+
///
619+
/// This has been tested (and verified) on:
620+
/// - iDRAC9: R760 (BIOS 2.5.4), R760xd2 (BIOS 1.7.5), XE9680.
621+
/// - iDRAC10: R670 (BIOS 1.7.5).
622+
///
623+
/// HOWEVER, there seems to be some behavior in OTHER machines that I can't
624+
/// quite narrow down, where `HttpDev1Uri.ReadOnly: true` in the BIOS Attribute
625+
/// Registry, despite `HttpDev1EnDis: Enabled`. Systems were not in lockdown,
626+
/// at least it didn't look like it, so I'm not sure what put those systems
627+
/// into that state, and I couldn't actually figure out how to get them
628+
/// unlocked (this was as part of running across an entire development fleet).
629+
///
630+
/// On these locked hosts, it returns HTTP 400 with a Dell-specific
631+
/// MessageId of the form `IDRAC.<ver>.SYS410` ("Unable to modify the
632+
/// attribute because the attribute is read-only and depends on other
633+
/// attributes"). We translate that specific error into `NotSupported` so
634+
/// the caller can fall back to DHCP option 67 for the URL. Any other 400
635+
/// or error propagates unchanged.
636+
///
637+
/// Once we figure out the weird locked state, callers can opt machines
638+
/// into the BMC-pinning path more aggressively.
611639
fn set_boot_override<'a>(
612640
&'a self,
613-
_settings: BootOverride,
641+
settings: BootOverride,
614642
) -> crate::RedfishFuture<'a, Result<Option<String>, RedfishError>> {
615643
Box::pin(async move {
616-
Err(RedfishError::NotSupported(
617-
"No Dell set_boot_override implementation".to_string(),
618-
))
644+
let Some(uri) = settings.http_boot_uri else {
645+
// Dell does not accept BootSourceOverrideTarget/Enabled PATCHes
646+
// via /Systems/{id}/Settings. Without an http_boot_uri to set
647+
// via the BIOS attribute path, there's no Dell-specific
648+
// operation for this method to perform.
649+
return Err(RedfishError::NotSupported(
650+
"Dell set_boot_override requires http_boot_uri; BootSourceOverrideTarget/Enabled are not settable via Redfish on iDRAC".to_string(),
651+
));
652+
};
653+
654+
let url = format!("Systems/{}/Bios/Settings", self.s.system_id());
655+
let body = serde_json::json!({
656+
"@Redfish.SettingsApplyTime": {"ApplyTime": "OnReset"},
657+
"Attributes": {
658+
"HttpDev1Uri": uri,
659+
"HttpDev1EnDis": "Enabled",
660+
"HttpDev1DhcpEnDis": "Disabled",
661+
"HttpDev1Protocol": "IPv4",
662+
}
663+
});
664+
665+
match self.s.client.patch(&url, body).await {
666+
Ok((_, Some(headers))) => {
667+
let job_id = self
668+
.parse_job_id_from_response_headers(&url, headers)
669+
.await?;
670+
Ok(Some(job_id))
671+
}
672+
Ok((_, None)) => Err(RedfishError::NoHeader),
673+
Err(RedfishError::HTTPErrorCode {
674+
status_code,
675+
response_body,
676+
..
677+
}) if status_code == StatusCode::BAD_REQUEST
678+
&& response_body.contains("SYS410") =>
679+
{
680+
Err(RedfishError::NotSupported(format!(
681+
"Dell iDRAC rejected HttpDev1Uri PATCH as ReadOnly (MessageId SYS410). Response: {response_body}"
682+
)))
683+
}
684+
Err(e) => Err(e),
685+
}
619686
})
620687
}
621688

tests/integration_test.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -494,9 +494,41 @@ async fn run_integration_test(
494494
.await?;
495495
}
496496

497-
// Dell and Lenovo return NotSupported until follow-up PRs implement the
498-
// BIOS-attribute path (HttpDev1Uri etc. on Dell, equivalent on Lenovo XCC).
499-
if vendor_dir == "dell" || vendor_dir == "dell_multi_dpu" || vendor_dir == "lenovo" {
497+
// Dell mockups use the patch_response.json side-file mechanism in the
498+
// Python mockup server to simulate real iDRAC responses to PATCH
499+
// /Bios/Settings:
500+
// - `dell` mockup: returns 202 + Location header → impl parses the
501+
// job ID out of Location and returns Ok(Some(job_id)). Exercises
502+
// the success path.
503+
// - `dell_multi_dpu` mockup: returns 400 with the Dell-specific SYS410
504+
// MessageId in the body → impl translates that to NotSupported.
505+
// Exercises the read-only-attribute failure path.
506+
if vendor_dir == "dell" {
507+
match redfish
508+
.set_boot_override(libredfish::BootOverride {
509+
target: libredfish::BootSourceOverrideTarget::UefiHttp,
510+
enabled: libredfish::BootSourceOverrideEnabled::Continuous,
511+
mode: Some(libredfish::BootSourceOverrideMode::UEFI),
512+
http_boot_uri: Some("http://example.invalid/ipxe.efi".to_string()),
513+
})
514+
.await
515+
{
516+
Ok(Some(job_id)) => {
517+
assert_eq!(
518+
job_id, "JID_900000000001",
519+
"Expected job ID parsed from mockup's Location header"
520+
);
521+
}
522+
other => panic!(
523+
"Expected Ok(Some(job_id)) for {vendor_dir}, got {:?}",
524+
other.map(Some)
525+
),
526+
}
527+
}
528+
529+
// dell_multi_dpu (mockup-simulated SYS410 lock) and lenovo (no impl)
530+
// both return NotSupported.
531+
if vendor_dir == "dell_multi_dpu" || vendor_dir == "lenovo" {
500532
match redfish
501533
.set_boot_override(libredfish::BootOverride {
502534
target: libredfish::BootSourceOverrideTarget::UefiHttp,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"match_request_body_contains": "HttpDev1Uri",
3+
"status": 202,
4+
"headers": {
5+
"Content-Type": "application/json",
6+
"Location": "/redfish/v1/TaskService/Tasks/JID_900000000001"
7+
},
8+
"body": "{\"@Message.ExtendedInfo\":[{\"Message\":\"Successfully scheduled the job.\",\"MessageArgs\":[],\"MessageId\":\"IDRAC.2.9.JCP001\",\"Severity\":\"OK\"}]}"
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"match_request_body_contains": "HttpDev1Uri",
3+
"status": 400,
4+
"headers": {
5+
"Content-Type": "application/json"
6+
},
7+
"body": "{\"error\":{\"@Message.ExtendedInfo\":[{\"Message\":\"Unable to modify the attribute because the attribute is read-only and depends on other attributes.\",\"MessageArgs\":[\"HttpDev1Uri\"],\"MessageId\":\"IDRAC.2.16.SYS410\",\"RelatedProperties\":[\"#/Attributes/HttpDev1Uri\"],\"Severity\":\"Warning\",\"Resolution\":\"Verify if the attribute has dependency on other attributes and retry the operation.\"}],\"code\":\"Base.1.18.GeneralError\",\"message\":\"A general error has occurred. See ExtendedInfo for more information.\"}}"
8+
}

tests/redfishMockupServer.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,32 @@ def do_PATCH(self):
711711
if data_received:
712712
logger.info(" PATCH: Data: {}".format(data_received))
713713

714+
# If a canned PATCH response file exists at the resource path
715+
# (patch_response.json), use it instead of the default merge+204.
716+
# Lets vendor mockups simulate real-BMC responses like
717+
# "202 + Location header" (success) or "400 + vendor-specific
718+
# error MessageId" (failure).
719+
#
720+
# The canned response file can optionally include a top-level
721+
# `match_request_body_contains` (string or list of strings); when
722+
# present, the canned response is only used if the request body
723+
# contains any of those substrings. This lets us target a
724+
# specific PATCH operation on a shared endpoint without
725+
# intercepting other PATCHes to the same resource.
726+
patch_response_fpath = self.construct_path(self.path, "patch_response.json")
727+
if os.path.isfile(patch_response_fpath):
728+
use_canned = True
729+
with open(patch_response_fpath) as f:
730+
canned = json.load(f)
731+
match = canned.get("match_request_body_contains")
732+
if match is not None:
733+
needles = match if isinstance(match, list) else [match]
734+
raw_body = json.dumps(data_received)
735+
use_canned = any(n in raw_body for n in needles)
736+
if use_canned:
737+
self.send_response_file(patch_response_fpath)
738+
return
739+
714740
# construct path "mockdir/path/to/resource/<filename>"
715741
fpath = self.construct_path(self.path, "index.json")
716742
success, payload = self.get_cached_link(fpath)

0 commit comments

Comments
 (0)