diff --git a/crates/api-core/src/handlers/component_manager.rs b/crates/api-core/src/handlers/component_manager.rs index 91d6594213..a634dd9871 100644 --- a/crates/api-core/src/handlers/component_manager.rs +++ b/crates/api-core/src/handlers/component_manager.rs @@ -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() { @@ -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 = 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"); + } }