From d9d561a97b31f426be78c8ae8a96e4aefaaa9edb Mon Sep 17 00:00:00 2001 From: Caleb Carlson Date: Sat, 15 Aug 2026 00:13:55 +0000 Subject: [PATCH 1/3] fix(api-core): fix stuck compute-tray fw status `GetComponentFirmwareStatus` for `MachineIds` unconditionally used a DB-only path that derives state from machine table fields. Direct CM-driven updates never enter the `HostReprovision` state, so the status was always `FwStateQueued` indefinitely. When the CM is present and `compute_tray_use_state_controller` is false, the `MachineIds` arm now resolves machine IDs to `ComputeTrayEndpoints` and delegates to `cm.compute_tray.get_firmware_status()`, the same RMS-querying path already used by `SwitchIds` and `PowerShelfIds`. The DB-only path is kept as a fallback for state-controller deployments. Signed-off-by: Caleb Carlson --- .../src/handlers/component_manager.rs | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/api-core/src/handlers/component_manager.rs b/crates/api-core/src/handlers/component_manager.rs index 91d6594213..ccefba6358 100644 --- a/crates/api-core/src/handlers/component_manager.rs +++ b/crates/api-core/src/handlers/component_manager.rs @@ -2641,7 +2641,61 @@ 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(); + + 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() { From 204f930209960ecd37b6c55cb014db6ba0cf9d45 Mon Sep 17 00:00:00 2001 From: Caleb Carlson Date: Tue, 18 Aug 2026 17:50:56 +0000 Subject: [PATCH 2/3] fix(api-core): fix stuck compute-tray fw status - MachineIds arm of GetComponentFirmwareStatus unconditionally used a DB-only path, causing status to remain FwStateQueued indefinitely for direct CM-driven firmware updates - When CM is present and compute_tray_use_state_controller is false, delegate to cm.compute_tray.get_firmware_status(), the same RMS-querying path used by SwitchIds/PowerShelfIds - Keep DB-only machine_firmware_statuses() as fallback for state-controller deployments - Add unit test covering endpoint resolution + CM result mapping back to machine ID Signed-off-by: Caleb Carlson --- .../src/handlers/component_manager.rs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/crates/api-core/src/handlers/component_manager.rs b/crates/api-core/src/handlers/component_manager.rs index ccefba6358..99102a080a 100644 --- a/crates/api-core/src/handlers/component_manager.rs +++ b/crates/api-core/src/handlers/component_manager.rs @@ -4074,4 +4074,67 @@ 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"); + } } From 9595893ec5335f1ce00dbaf478aa2e839aab54a0 Mon Sep 17 00:00:00 2001 From: Caleb Carlson Date: Tue, 18 Aug 2026 19:29:55 +0000 Subject: [PATCH 3/3] fix(api-core): skip CM call when no endpoints resolve - Guard cm.compute_tray.get_firmware_status() behind a non-empty endpoints check - When all machine IDs fail to resolve (missing BMC MAC/IP or credentials), return the per-machine error statuses directly without calling the backend - Fix nightly rustfmt line-break in new test Signed-off-by: Caleb Carlson --- .../src/handlers/component_manager.rs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/crates/api-core/src/handlers/component_manager.rs b/crates/api-core/src/handlers/component_manager.rs index 99102a080a..a634dd9871 100644 --- a/crates/api-core/src/handlers/component_manager.rs +++ b/crates/api-core/src/handlers/component_manager.rs @@ -2669,29 +2669,31 @@ pub(crate) async fn get_component_firmware_status( }) .collect(); - 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, - } - })); + 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? @@ -4091,8 +4093,7 @@ mod tests { password: "secret".into(), }); - let resolved = - resolve_compute_tray_endpoints_from_machines(&creds, &machines, &[id]).await; + let resolved = resolve_compute_tray_endpoints_from_machines(&creds, &machines, &[id]).await; assert!(resolved.unresolved.is_empty()); let cm = MockComputeTrayManager;