Skip to content

Commit f1b2160

Browse files
committed
feat: count firmware update outcomes across every target
Firmware updates run for three targets -- host BIOS/BMC, DPU NICs, and SuperNICs -- and none of their outcomes are countable today: starts, completions, and failures live only in log lines, including the "wrong version after attempted update" case that means an update silently didn't take. This makes all of it countable, with each event replacing its site's log line at the historical level (info for progress, warn for failures) and the same information as structured fields. Two counters from `carbide-instrument`: - `carbide_firmware_updates_total{target, phase}` -- started and completed, per target. For hosts both phases fire, so the started-to-completed gap is the in-flight backlog; the DPU-NIC and SuperNIC targets emit started only, with completion visible in their own state flows. - `carbide_firmware_update_failures_total{target, cause}` -- `no_update_match` (a DPU left ready between snapshot and trigger) and `wrong_version_after_update` (the update ran and the firmware version still doesn't match -- the silent-failure case). Notable details: - Started counts only committed triggers: the DPU-NIC emit fires after `txn.commit()`, so an update whose trigger rolled back is a `no_update_match` failure, never a started update, and the started-minus-completed math stays truthful. - Host completion emits once per machine as its update markers clear, replacing one Vec-debug log line with per-machine lines. - A SuperNIC re-counts one started per scout pass while a device stays mismatched -- each pass issues a real apply command, and the sustained rate is the stuck-device signal. Tests added! This supports #3175 Signed-off-by: Chet Nichols III <chetn@nvidia.com>
1 parent c5ede1e commit f1b2160

7 files changed

Lines changed: 352 additions & 36 deletions

File tree

crates/api-core/src/handlers/host_reprovisioning.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,17 @@ pub(crate) async fn trigger_host_reprovisioning(
7979
&machine_id,
8080
)
8181
.await?;
82+
// Manual initiations pair with the same completion emit the
83+
// update manager's automatic path gets, keeping the
84+
// started-to-completed gap truthful for every initiator.
85+
carbide_instrument::emit(
86+
crate::machine_update_manager::metrics::FirmwareUpdateProgress {
87+
target: crate::machine_update_manager::metrics::FirmwareUpdateTarget::Host,
88+
phase: crate::machine_update_manager::metrics::FirmwareUpdatePhase::Started,
89+
machine_id,
90+
detail: initiator.to_string(),
91+
},
92+
);
8293
}
8394
Mode::Clear => {
8495
db::host_machine_update::clear_host_reprovisioning_request(&mut txn, &machine_id)

crates/api-core/src/handlers/svpc.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ use rpc::protos::mlx_device::MlxDeviceInfo;
3434
use tonic::{Request, Response, Status};
3535

3636
use crate::api::{Api, log_request_data};
37+
use crate::machine_update_manager::metrics::{
38+
FirmwareUpdatePhase, FirmwareUpdateProgress, FirmwareUpdateTarget,
39+
};
3740
use crate::{CarbideError, CarbideResult};
3841

3942
// Code to handle SVPC specific information.
@@ -197,12 +200,16 @@ fn build_apply_firmware_command<'a>(
197200
return None;
198201
}
199202

200-
tracing::info!(
201-
%machine_id, %pci_name, %part_number, %psid,
202-
observed_fw_version = ?device_info.fw_version_current,
203-
expected_fw_version = %fw_profile.firmware_spec.version,
204-
"firmware version mismatch, applying firmware"
205-
);
203+
carbide_instrument::emit(FirmwareUpdateProgress {
204+
target: FirmwareUpdateTarget::SuperNic,
205+
phase: FirmwareUpdatePhase::Started,
206+
machine_id,
207+
detail: format!(
208+
"pci_name={pci_name} part_number={part_number} psid={psid} \
209+
observed_fw_version={:?} expected_fw_version={}",
210+
device_info.fw_version_current, fw_profile.firmware_spec.version
211+
),
212+
});
206213
Some(Cow::Borrowed(fw_profile))
207214
})();
208215

crates/api-core/src/machine_update_manager/dpu_nic_firmware.rs

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ use sqlx::PgConnection;
2929

3030
use super::dpu_nic_firmware_metrics::DpuNicFirmwareUpdateMetrics;
3131
use super::machine_update_module::MachineUpdateModule;
32+
use super::metrics::{
33+
FirmwareUpdateFailed, FirmwareUpdateFailureCause, FirmwareUpdatePhase, FirmwareUpdateProgress,
34+
FirmwareUpdateTarget,
35+
};
3236
use crate::cfg::file::CarbideConfig;
3337
use crate::machine_update_manager::MachineUpdateManager;
3438
use crate::{CarbideResult, DatabaseError};
@@ -46,6 +50,14 @@ pub struct DpuNicFirmwareUpdate {
4650
/// DPF handle for discovering outdated DPF-managed DPUs. `None` when DPF
4751
/// is disabled in config; in that case `find_outdated_dpus_dpf` is not called.
4852
pub dpf: Option<Arc<dyn DpfOperations>>,
53+
/// Wrong-version outcomes already counted, keyed by DPU and the version it
54+
/// landed on: the condition persists across manager passes (the update
55+
/// marker stays until an operator intervenes or a new update runs), and a
56+
/// counter must record the outcome once, not once per poll. An entry
57+
/// clears when the DPU's markers clear, so a later attempt that lands
58+
/// wrong again is a new outcome. In-memory: a restart re-reports at most
59+
/// once per stuck DPU.
60+
pub reported_wrong_versions: std::sync::Mutex<HashSet<(MachineId, String)>>,
4961
}
5062

5163
#[async_trait]
@@ -105,11 +117,6 @@ impl MachineUpdateModule for DpuNicFirmwareUpdate {
105117
output + format!("{} ({}) ", dpu.dpu_machine_id, dpu.firmware_version).as_str()
106118
});
107119

108-
tracing::info!(
109-
"Starting DPU updates for host {}: {}",
110-
host_machine_id,
111-
dpu_update_string
112-
);
113120
// If the reprovisioning failed to update the database for a
114121
// given {dpu,host}_machine_id, log it as a warning and don't
115122
// add it to updates_started.
@@ -124,11 +131,13 @@ impl MachineUpdateModule for DpuNicFirmwareUpdate {
124131
{
125132
match reprovisioning_err {
126133
DatabaseError::NotFoundError { id, .. } => {
127-
tracing::warn!(
128-
"failed to trigger reprovisioning for managed host : {} - no update match for id: {}",
129-
host_machine_id,
130-
id
131-
);
134+
carbide_instrument::emit(FirmwareUpdateFailed {
135+
target: FirmwareUpdateTarget::DpuNic,
136+
cause: FirmwareUpdateFailureCause::NoUpdateMatch,
137+
machine_id: host_machine_id,
138+
unmatched_dpu_machine_id: id,
139+
firmware_version: String::new(),
140+
});
132141
continue;
133142
}
134143
_ => {
@@ -139,6 +148,15 @@ impl MachineUpdateModule for DpuNicFirmwareUpdate {
139148

140149
txn.commit().await?;
141150

151+
// Counted only once the trigger is committed: a DPU that left
152+
// ready between snapshot and trigger is a NoUpdateMatch failure,
153+
// not a started update.
154+
carbide_instrument::emit(FirmwareUpdateProgress {
155+
target: FirmwareUpdateTarget::DpuNic,
156+
phase: FirmwareUpdatePhase::Started,
157+
machine_id: host_machine_id,
158+
detail: dpu_update_string,
159+
});
142160
updates_started.insert(host_machine_id);
143161
}
144162

@@ -163,13 +181,26 @@ impl MachineUpdateModule for DpuNicFirmwareUpdate {
163181
machine_id = %updated_machine.dpu_machine_id,
164182
"Failed to remove machine update markers: {}", e
165183
);
184+
} else if let Ok(mut reported) = self.reported_wrong_versions.lock() {
185+
reported.retain(|(dpu, _)| *dpu != updated_machine.dpu_machine_id);
166186
}
167-
} else {
168-
tracing::warn!(
169-
machine_id = %updated_machine.dpu_machine_id,
170-
firmware_version = %updated_machine.firmware_version,
171-
"Incorrect firmware version after attempted update"
172-
);
187+
} else if self
188+
.reported_wrong_versions
189+
.lock()
190+
.is_ok_and(|mut reported| {
191+
reported.insert((
192+
updated_machine.dpu_machine_id,
193+
updated_machine.firmware_version.clone(),
194+
))
195+
})
196+
{
197+
carbide_instrument::emit(FirmwareUpdateFailed {
198+
target: FirmwareUpdateTarget::DpuNic,
199+
cause: FirmwareUpdateFailureCause::WrongVersionAfterUpdate,
200+
machine_id: updated_machine.dpu_machine_id,
201+
unmatched_dpu_machine_id: String::new(),
202+
firmware_version: updated_machine.firmware_version,
203+
});
173204
}
174205
}
175206
Ok(())
@@ -243,6 +274,7 @@ impl DpuNicFirmwareUpdate {
243274
let mut metrics = DpuNicFirmwareUpdateMetrics::new();
244275
metrics.register_callbacks(&meter);
245276
Some(DpuNicFirmwareUpdate {
277+
reported_wrong_versions: std::sync::Mutex::new(HashSet::new()),
246278
metrics: Some(metrics),
247279
config,
248280
dpf,

crates/api-core/src/machine_update_manager/host_firmware.rs

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use sqlx::PgConnection;
3232
use tokio::sync::Mutex;
3333

3434
use super::machine_update_module::MachineUpdateModule;
35+
use super::metrics::{FirmwareUpdatePhase, FirmwareUpdateProgress, FirmwareUpdateTarget};
3536
use crate::CarbideResult;
3637
use crate::cfg::file::CarbideConfig;
3738

@@ -90,15 +91,22 @@ impl MachineUpdateModule for HostFirmwareUpdate {
9091
continue;
9192
}
9293

93-
tracing::info!("Moving {} to host reprovision", machine_update);
94-
9594
db::host_machine_update::trigger_host_reprovisioning_request(
9695
&mut txn,
9796
"Automated",
9897
machine_update,
9998
)
10099
.await?;
101100

101+
// Counted after the trigger succeeds; the commit below spans the
102+
// whole batch, so a later DB error can re-count machines on the
103+
// next pass -- the same repeat-on-retry the old log line had.
104+
carbide_instrument::emit(FirmwareUpdateProgress {
105+
target: FirmwareUpdateTarget::Host,
106+
phase: FirmwareUpdatePhase::Started,
107+
machine_id: *machine_update,
108+
detail: String::new(),
109+
});
102110
updates_started.insert(*machine_update);
103111
}
104112

@@ -109,18 +117,21 @@ impl MachineUpdateModule for HostFirmwareUpdate {
109117
async fn clear_completed_updates(&self, txn: &mut PgConnection) -> CarbideResult<()> {
110118
let completed = db::host_machine_update::find_completed_updates(txn).await?;
111119

112-
if !completed.is_empty() {
113-
tracing::info!("Completed host firmware updates: {completed:?}");
114-
for machine in completed {
115-
db::machine::remove_health_report(
116-
txn,
117-
&machine,
118-
health_report::HealthReportApplyMode::Merge,
119-
HOST_FW_UPDATE_HEALTH_REPORT_SOURCE,
120-
)
121-
.await?;
122-
db::machine::update_update_complete(&machine, true, txn).await?;
123-
}
120+
for machine in completed {
121+
db::machine::remove_health_report(
122+
txn,
123+
&machine,
124+
health_report::HealthReportApplyMode::Merge,
125+
HOST_FW_UPDATE_HEALTH_REPORT_SOURCE,
126+
)
127+
.await?;
128+
db::machine::update_update_complete(&machine, true, txn).await?;
129+
carbide_instrument::emit(FirmwareUpdateProgress {
130+
target: FirmwareUpdateTarget::Host,
131+
phase: FirmwareUpdatePhase::Completed,
132+
machine_id: machine,
133+
detail: String::new(),
134+
});
124135
}
125136
Ok(())
126137
}

0 commit comments

Comments
 (0)