diff --git a/dev-tools/omdb/src/bin/omdb/nexus.rs b/dev-tools/omdb/src/bin/omdb/nexus.rs index d45865b4a7..5af75fac8f 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus.rs @@ -1556,6 +1556,62 @@ fn print_task_details(bgtask: &BackgroundTask, details: &serde_json::Value) { } } } + } else if name == "blueprint_loader" { + #[derive(Deserialize)] + struct BlueprintLoaderStatus { + target_id: Uuid, + time_created: DateTime, + status: String, + enabled: bool, + } + + match serde_json::from_value::(details.clone()) { + Err(error) => eprintln!( + "warning: failed to interpret task details: {:?}: {:?}", + error, details + ), + Ok(status) => { + println!(" target blueprint: {}", status.target_id); + println!( + " execution: {}", + if status.enabled { "enabled" } else { "disabled" } + ); + println!( + " created at: {}", + humantime::format_rfc3339_millis( + status.time_created.into() + ) + ); + println!(" status: {}", status.status); + } + } + } else if name == "blueprint_executor" { + #[derive(Deserialize)] + struct BlueprintExecutorStatus { + target_id: Uuid, + enabled: bool, + errors: Option>, + } + + match serde_json::from_value::(details.clone()) + { + Err(error) => eprintln!( + "warning: failed to interpret task details: {:?}: {:?}", + error, details + ), + Ok(status) => { + println!(" target blueprint: {}", status.target_id); + println!( + " execution: {}", + if status.enabled { "enabled" } else { "disabled" } + ); + let errors = status.errors.as_deref().unwrap_or(&[]); + println!(" errors: {}", errors.len()); + for (i, e) in errors.iter().enumerate() { + println!(" error {}: {}", i, e); + } + } + } } else { println!( "warning: unknown background task: {:?} \ diff --git a/nexus/reconfigurator/execution/src/dns.rs b/nexus/reconfigurator/execution/src/dns.rs index 9531843259..9ab84e15ff 100644 --- a/nexus/reconfigurator/execution/src/dns.rs +++ b/nexus/reconfigurator/execution/src/dns.rs @@ -1665,7 +1665,11 @@ mod test { // If we execute it again, we should see no more changes. _ = realize_blueprint_and_expect( - &opctx, datastore, resolver, &blueprint, &overrides, + &opctx, + datastore, + resolver, + &blueprint2, + &overrides, ) .await; verify_dns_unchanged( diff --git a/nexus/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs index dbbfcb3b14..2b1e3eedca 100644 --- a/nexus/src/app/background/tasks/blueprint_execution.rs +++ b/nexus/src/app/background/tasks/blueprint_execution.rs @@ -83,7 +83,7 @@ impl BlueprintExecutor { "target_id" => %blueprint.id); return json!({ "target_id": blueprint.id.to_string(), - "error": "blueprint disabled" + "enabled": false, }); } @@ -111,6 +111,7 @@ impl BlueprintExecutor { json!({ "target_id": blueprint.id.to_string(), + "enabled": true, "needs_saga_recovery": needs_saga_recovery, }) } @@ -119,6 +120,7 @@ impl BlueprintExecutor { errors.into_iter().map(|e| format!("{:#}", e)).collect(); json!({ "target_id": blueprint.id.to_string(), + "enabled": true, "errors": errors }) } @@ -316,6 +318,7 @@ mod test { value, json!({ "target_id": blueprint_id, + "enabled": true, "needs_saga_recovery": false, }) ); @@ -410,6 +413,7 @@ mod test { value, json!({ "target_id": blueprint.1.id.to_string(), + "enabled": true, "needs_saga_recovery": false, }) ); @@ -427,7 +431,7 @@ mod test { assert_eq!( value, json!({ - "error": "blueprint disabled", + "enabled": false, "target_id": blueprint.1.id.to_string() }) ); diff --git a/nexus/src/app/background/tasks/blueprint_load.rs b/nexus/src/app/background/tasks/blueprint_load.rs index 31bc00441d..70fcf713bc 100644 --- a/nexus/src/app/background/tasks/blueprint_load.rs +++ b/nexus/src/app/background/tasks/blueprint_load.rs @@ -78,6 +78,7 @@ impl BackgroundTask for TargetBlueprintLoader { }; // Decide what to do with the new blueprint + let enabled = new_bp_target.enabled; let Some((old_bp_target, old_blueprint)) = self.last.as_deref() else { // We've found a target blueprint for the first time. @@ -97,6 +98,7 @@ impl BackgroundTask for TargetBlueprintLoader { "time_created": time_created, "time_found": chrono::Utc::now(), "status": "first target blueprint", + "enabled": enabled, }); }; @@ -116,7 +118,8 @@ impl BackgroundTask for TargetBlueprintLoader { "target_id": target_id, "time_created": time_created, "time_found": chrono::Utc::now(), - "status": "target blueprint updated" + "status": "target blueprint updated", + "enabled": enabled, }) } else { // The new target id matches the old target id @@ -159,6 +162,7 @@ impl BackgroundTask for TargetBlueprintLoader { "time_created": time_created, "time_found": chrono::Utc::now(), "status": format!("target blueprint {status}"), + "enabled": enabled, }) } else { // We found a new target blueprint that exactly @@ -173,7 +177,8 @@ impl BackgroundTask for TargetBlueprintLoader { json!({ "target_id": target_id, "time_created": time_created, - "status": "target blueprint unchanged" + "status": "target blueprint unchanged", + "enabled": enabled, }) } } diff --git a/update-engine/src/display/line_display_shared.rs b/update-engine/src/display/line_display_shared.rs index 99b03b13f7..99d66bd06f 100644 --- a/update-engine/src/display/line_display_shared.rs +++ b/update-engine/src/display/line_display_shared.rs @@ -16,6 +16,7 @@ use owo_colors::OwoColorize; use swrite::{swrite, SWrite as _}; use crate::{ + display::StepIndexDisplay, events::{ ProgressCounter, ProgressEvent, ProgressEventKind, StepEvent, StepEventKind, StepInfo, StepOutcome, @@ -716,17 +717,16 @@ impl LineDisplayFormatter { ) { ld_step_info.nest_data.add_prefix(line); - // Print out "/)". Leave space such that we - // print out e.g. "1/8)" and " 3/14)". - // Add 1 to the index to make it 1-based. - let step_index = ld_step_info.step_info.index + 1; - let step_index_width = ld_step_info.total_steps.to_string().len(); + // Print out "(/)" in a padded way, so that successive + // steps are vertically aligned. swrite!( line, - "{:width$}/{:width$}) ", - step_index, - ld_step_info.total_steps, - width = step_index_width + "({}) ", + StepIndexDisplay::new( + ld_step_info.step_info.index, + ld_step_info.total_steps + ) + .padded(true), ); swrite!( diff --git a/update-engine/src/display/mod.rs b/update-engine/src/display/mod.rs index c58a4535a0..f6775dd37b 100644 --- a/update-engine/src/display/mod.rs +++ b/update-engine/src/display/mod.rs @@ -11,11 +11,14 @@ //! * [`LineDisplay`]: a line-oriented display suitable for the command line. //! * [`GroupDisplay`]: manages state and shows the results of several //! [`LineDisplay`]s at once. +//! * Some utility displayers which can be used to build custom displayers. mod group_display; mod line_display; mod line_display_shared; +mod utils; pub use group_display::GroupDisplay; pub use line_display::{LineDisplay, LineDisplayStyles}; use line_display_shared::*; +pub use utils::*; diff --git a/update-engine/src/display/utils.rs b/update-engine/src/display/utils.rs new file mode 100644 index 0000000000..0a03a09fa1 --- /dev/null +++ b/update-engine/src/display/utils.rs @@ -0,0 +1,58 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Utility displayers. + +use std::fmt; + +/// Given an index and a count of total steps, displays `{current}/{total}`. +/// +/// Here: +/// +/// * `current` is `index + 1`. +/// * If `padded` is `true`, `current` is right-aligned and padded with spaces +/// to the width of `total`. +/// +/// # Examples +/// +/// ``` +/// use update_engine::display::StepIndexDisplay; +/// +/// let display = StepIndexDisplay::new(0, 8); +/// assert_eq!(display.to_string(), "1/8"); +/// let display = StepIndexDisplay::new(82, 230); +/// assert_eq!(display.to_string(), "83/230"); +/// let display = display.padded(true); +/// assert_eq!(display.to_string(), " 83/230"); +/// ``` +#[derive(Debug)] +pub struct StepIndexDisplay { + index: usize, + total: usize, + padded: bool, +} + +impl StepIndexDisplay { + /// Create a new `StepIndexDisplay`. + /// + /// The index is 0-based (i.e. 1 is added to it when it is displayed). + pub fn new(index: usize, total: usize) -> Self { + Self { index, total, padded: false } + } + + pub fn padded(self, padded: bool) -> Self { + Self { padded, ..self } + } +} + +impl fmt::Display for StepIndexDisplay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.padded { + let width = self.total.to_string().len(); + write!(f, "{:>width$}/{}", self.index + 1, self.total) + } else { + write!(f, "{}/{}", self.index + 1, self.total) + } + } +} diff --git a/wicket/src/ui/panes/update.rs b/wicket/src/ui/panes/update.rs index 3a61e25a3a..96a55667fe 100644 --- a/wicket/src/ui/panes/update.rs +++ b/wicket/src/ui/panes/update.rs @@ -29,6 +29,7 @@ use ratatui::widgets::{ use ratatui::Frame; use slog::{info, o, Logger}; use tui_tree_widget::{Tree, TreeItem, TreeState}; +use update_engine::display::StepIndexDisplay; use update_engine::{ AbortReason, CompletionReason, ExecutionStatus, FailureReason, StepKey, TerminalKind, WillNotBeRunReason, @@ -1984,9 +1985,11 @@ impl ComponentUpdateListState { )); status_text.push(Span::styled( format!( - " (step {}/{})", - step_key.index + 1, - summary.total_steps, + " (step {})", + StepIndexDisplay::new( + step_key.index, + summary.total_steps, + ) ), style::plain_text(), )); @@ -2015,9 +2018,11 @@ impl ComponentUpdateListState { )); status_text.push(Span::styled( format!( - " at step {}/{}", - info.step_key.index + 1, - summary.total_steps, + " at step {}", + StepIndexDisplay::new( + info.step_key.index, + summary.total_steps, + ) ), style::plain_text(), )); @@ -2033,9 +2038,11 @@ impl ComponentUpdateListState { )); status_text.push(Span::styled( format!( - " at step {}/{}", - info.step_key.index + 1, - summary.total_steps, + " at step {}", + StepIndexDisplay::new( + info.step_key.index, + summary.total_steps, + ) ), style::plain_text(), ));