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
120 changes: 119 additions & 1 deletion crates/api-core/src/handlers/component_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2641,7 +2641,63 @@ pub(crate) async fn get_component_firmware_status(
return Err(Status::invalid_argument("machine_ids must not be empty"));
}

machine_firmware_statuses(api, &list.machine_ids).await?
// When the component manager is available and not routing through the
// state controller, delegate to cm.compute_tray.get_firmware_status()
// which queries RMS for the tracked job ID. The DB-only
// machine_firmware_statuses() path is kept as a fallback for
// state-controller deployments, where firmware status is derived from
// machine state transitions instead.
if let Some(cm) = api.component_manager.as_ref()
&& !cm.compute_tray_use_state_controller
{
let machines_by_id = load_machines_by_id(api, &list.machine_ids).await?;
let resolved = resolve_compute_tray_endpoints_from_machines(
api.credential_manager.as_ref(),
&machines_by_id,
&list.machine_ids,
)
.await;

let mut statuses: Vec<_> = resolved
.unresolved
.iter()
.map(|u| rpc::FirmwareUpdateStatus {
result: Some(error_result(&u.id.to_string(), u.reason.clone())),
state: rpc::FirmwareUpdateState::FwStateUnknown as i32,
target_version: String::new(),
updated_at: None,
})
.collect();

if !resolved.resolved.endpoints.is_empty() {
let backend_statuses = cm
.compute_tray
.get_firmware_status(&resolved.resolved.endpoints)
.await
.map_err(component_manager_error_to_status)?;
statuses.extend(backend_statuses.into_iter().map(|s| {
let id = resolved
.resolved
.ip_to_machine_id
.get(&s.bmc_ip)
.map(|id| id.to_string())
.unwrap_or_else(|| s.bmc_ip.to_string());
rpc::FirmwareUpdateStatus {
result: Some(if s.error.is_none() {
success_result(&id)
} else {
error_result(&id, s.error.unwrap_or_default())
}),
state: map_fw_state(s.state),
target_version: s.target_version,
updated_at: None,
}
}));
}
statuses
} else {
machine_firmware_statuses(api, &list.machine_ids).await?
}
}
rpc::get_component_firmware_status_request::Target::RackIds(list) => {
if list.rack_ids.is_empty() {
Expand Down Expand Up @@ -4020,4 +4076,66 @@ mod tests {
assert_eq!(resolved.resolved.endpoints[0].bmc_ip, bmc_ip);
assert_eq!(resolved.resolved.ip_to_machine_id.get(&bmc_ip), Some(&id));
}

/// Verify that the result mapping used by the MachineIds firmware-status
/// path converts `bmc_ip` back to the machine ID and propagates the state
/// and target version returned by the CM backend.
#[tokio::test]
async fn compute_tray_firmware_status_result_mapped_to_machine_id() {
use component_manager::mock::MockComputeTrayManager;

let id = host_machine_id();
let machine = standalone_machine();
let bmc_ip = machine.status.bmc_info.ip.expect("fixture has BMC IP");
let machines = HashMap::from([(id, machine)]);
let creds = TestCredentialManager::new(Credentials::UsernamePassword {
username: "root".into(),
password: "secret".into(),
});

let resolved = resolve_compute_tray_endpoints_from_machines(&creds, &machines, &[id]).await;
assert!(resolved.unresolved.is_empty());

let cm = MockComputeTrayManager;
let backend_statuses = cm
.get_firmware_status(&resolved.resolved.endpoints)
.await
.expect("mock CM must not fail");

let statuses: Vec<rpc::FirmwareUpdateStatus> = backend_statuses
.into_iter()
.map(|s| {
let component_id = resolved
.resolved
.ip_to_machine_id
.get(&s.bmc_ip)
.map(|mid| mid.to_string())
.unwrap_or_else(|| s.bmc_ip.to_string());
rpc::FirmwareUpdateStatus {
result: Some(if s.error.is_none() {
success_result(&component_id)
} else {
error_result(&component_id, s.error.unwrap_or_default())
}),
state: map_fw_state(s.state),
target_version: s.target_version,
updated_at: None,
}
})
.collect();

assert_eq!(statuses.len(), 1);
let status = &statuses[0];
// The component ID must be the machine ID, not the raw BMC IP.
assert_eq!(
status.result.as_ref().map(|r| r.component_id.as_str()),
Some(id.to_string().as_str()),
"component_id should be the machine ID, not {bmc_ip}"
);
assert_eq!(
status.state,
rpc::FirmwareUpdateState::FwStateCompleted as i32
);
assert_eq!(status.target_version, "mock-1.0.0");
}
}
Loading