From dd58876f4048108911506baef56f350ffa8050c1 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 1 Nov 2023 12:57:40 -0700 Subject: [PATCH 1/3] =?UTF-8?q?[=F0=9D=98=80=F0=9D=97=BD=F0=9D=97=BF]=20in?= =?UTF-8?q?itial=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created using spr 1.3.4 --- tufaceous-lib/src/assemble/manifest.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tufaceous-lib/src/assemble/manifest.rs b/tufaceous-lib/src/assemble/manifest.rs index 409c85808c..437b84e7b0 100644 --- a/tufaceous-lib/src/assemble/manifest.rs +++ b/tufaceous-lib/src/assemble/manifest.rs @@ -261,9 +261,10 @@ impl<'a> FakeDataAttributes<'a> { | KnownArtifactKind::Trampoline | KnownArtifactKind::ControlPlane => return make_filler_text(size), - // hubris artifacts: build a fake archive - KnownArtifactKind::GimletSp => "fake-gimlet-sp", - KnownArtifactKind::GimletRot => "fake-gimlet-rot", + // hubris artifacts: build a fake archive (SimGimletSp and + // SimGimletRot are used by sp-sim) + KnownArtifactKind::GimletSp => "SimGimletSp", + KnownArtifactKind::GimletRot => "SimGimletRot", KnownArtifactKind::PscSp => "fake-psc-sp", KnownArtifactKind::PscRot => "fake-psc-rot", KnownArtifactKind::SwitchSp => "fake-sidecar-sp", From e53f7537372443b57cf72edbea20196349d41f48 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 1 Nov 2023 14:00:54 -0700 Subject: [PATCH 2/3] =?UTF-8?q?[=F0=9D=98=80=F0=9D=97=BD=F0=9D=97=BF]=20ch?= =?UTF-8?q?anges=20introduced=20through=20rebase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created using spr 1.3.4 [skip ci] --- Cargo.lock | 29 + Cargo.toml | 2 + update-engine/Cargo.toml | 5 + .../examples/update-engine-basic/display.rs | 123 ++- .../examples/update-engine-basic/main.rs | 194 ++-- update-engine/src/buffer.rs | 85 +- update-engine/src/display/group_display.rs | 467 +++++++++ update-engine/src/display/line_display.rs | 134 +++ .../src/display/line_display_shared.rs | 959 ++++++++++++++++++ update-engine/src/display/mod.rs | 21 + update-engine/src/errors.rs | 14 + update-engine/src/events.rs | 146 +++ update-engine/src/lib.rs | 1 + wicket/Cargo.toml | 3 + wicket/src/dispatch.rs | 82 +- wicket/src/helpers.rs | 70 ++ wicket/src/lib.rs | 2 + wicket/src/rack_update.rs | 377 +++++++ wicket/src/runner.rs | 105 +- wicket/src/state/inventory.rs | 88 +- wicket/src/state/update.rs | 39 +- wicket/src/wicketd.rs | 2 +- 22 files changed, 2721 insertions(+), 227 deletions(-) create mode 100644 update-engine/src/display/group_display.rs create mode 100644 update-engine/src/display/line_display.rs create mode 100644 update-engine/src/display/line_display_shared.rs create mode 100644 update-engine/src/display/mod.rs create mode 100644 wicket/src/helpers.rs create mode 100644 wicket/src/rack_update.rs diff --git a/Cargo.lock b/Cargo.lock index 2df98809a1..87a3900104 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3722,6 +3722,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is_ci" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" + [[package]] name = "itertools" version = "0.10.5" @@ -8637,6 +8643,22 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "supports-color" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "swrite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3fece30b2dc06d65ecbca97b602db15bf75f932711d60cc604534f1f8b7a03" + [[package]] name = "syn" version = "1.0.109" @@ -9650,6 +9672,7 @@ dependencies = [ "camino", "camino-tempfile", "cancel-safe-futures", + "clap 4.4.3", "debug-ignore", "derive-where", "either", @@ -9666,8 +9689,11 @@ dependencies = [ "serde_json", "serde_with", "slog", + "supports-color", + "swrite", "tokio", "tokio-stream", + "unicode-width", "uuid", ] @@ -10027,6 +10053,7 @@ dependencies = [ "ciborium", "clap 4.4.3", "crossterm 0.27.0", + "debug-ignore", "futures", "hex", "humantime", @@ -10050,6 +10077,7 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-term", + "supports-color", "tempfile", "textwrap 0.16.0", "tokio", @@ -10059,6 +10087,7 @@ dependencies = [ "tui-tree-widget", "unicode-width", "update-engine", + "uuid", "wicket-common", "wicketd-client", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 999fc680a5..0a4cdf368c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -342,6 +342,8 @@ static_assertions = "1.1.0" steno = "0.4.0" strum = { version = "0.25", features = [ "derive" ] } subprocess = "0.2.9" +supports-color = "2.1.0" +swrite = "0.1.0" libsw = { version = "3.3.0", features = ["tokio"] } syn = { version = "2.0" } tabled = "0.14" diff --git a/update-engine/Cargo.toml b/update-engine/Cargo.toml index af988bf091..12e718e902 100644 --- a/update-engine/Cargo.toml +++ b/update-engine/Cargo.toml @@ -13,13 +13,16 @@ either.workspace = true futures.workspace = true indexmap.workspace = true linear-map.workspace = true +owo-colors.workspace = true petgraph.workspace = true serde.workspace = true serde_json.workspace = true serde_with.workspace = true schemars = { workspace = true, features = ["uuid1"] } slog.workspace = true +swrite.workspace = true tokio = { workspace = true, features = ["macros", "sync", "time", "rt-multi-thread"] } +unicode-width.workspace = true uuid.workspace = true omicron-workspace-hack.workspace = true @@ -28,8 +31,10 @@ buf-list.workspace = true bytes.workspace = true camino.workspace = true camino-tempfile.workspace = true +clap.workspace = true indicatif.workspace = true omicron-test-utils.workspace = true owo-colors.workspace = true +supports-color.workspace = true tokio = { workspace = true, features = ["io-util"] } tokio-stream.workspace = true diff --git a/update-engine/examples/update-engine-basic/display.rs b/update-engine/examples/update-engine-basic/display.rs index e6b80e3637..122777211b 100644 --- a/update-engine/examples/update-engine-basic/display.rs +++ b/update-engine/examples/update-engine-basic/display.rs @@ -12,28 +12,135 @@ use indexmap::{map::Entry, IndexMap}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use owo_colors::OwoColorize; use tokio::{sync::mpsc, task::JoinHandle}; -use update_engine::events::ProgressCounter; +use update_engine::{ + display::{GroupDisplay, LineDisplay, LineDisplayStyles}, + events::ProgressCounter, +}; -use crate::spec::{ - Event, ExampleComponent, ExampleStepId, ExampleStepMetadata, ProgressEvent, - ProgressEventKind, StepEventKind, StepInfoWithMetadata, StepOutcome, +use crate::{ + spec::{ + Event, EventBuffer, ExampleComponent, ExampleStepId, + ExampleStepMetadata, ProgressEvent, ProgressEventKind, StepEventKind, + StepInfoWithMetadata, StepOutcome, + }, + DisplayStyle, }; /// An example that displays an event stream on the command line. pub(crate) fn make_displayer( log: &slog::Logger, + display_style: DisplayStyle, + prefix: Option, ) -> (JoinHandle>, mpsc::Sender) { let (sender, receiver) = mpsc::channel(512); let log = log.clone(); let join_handle = - tokio::task::spawn( - async move { display_messages(&log, receiver).await }, - ); + match display_style { + DisplayStyle::ProgressBar => tokio::task::spawn(async move { + display_progress_bar(&log, receiver).await + }), + DisplayStyle::Line => tokio::task::spawn(async move { + display_line(&log, receiver, prefix).await + }), + DisplayStyle::Group => tokio::task::spawn(async move { + display_group(&log, receiver).await + }), + }; (join_handle, sender) } -async fn display_messages( +async fn display_line( + log: &slog::Logger, + mut receiver: mpsc::Receiver, + prefix: Option, +) -> Result<()> { + slog::info!(log, "setting up display"); + let mut buffer = EventBuffer::new(8); + let mut display = LineDisplay::new(std::io::stdout()); + // For now, always colorize. TODO: figure out whether colorization should be + // done based on always/auto/never etc. + if supports_color::on(supports_color::Stream::Stdout).is_some() { + display.set_styles(LineDisplayStyles::colorized()); + } + if let Some(prefix) = prefix { + display.set_prefix(prefix); + } + display.set_progress_interval(Duration::from_millis(50)); + while let Some(event) = receiver.recv().await { + buffer.add_event(event); + display.write_event_buffer(&buffer)?; + } + + Ok(()) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +enum GroupDisplayKey { + Example, + Other, +} + +async fn display_group( + log: &slog::Logger, + mut receiver: mpsc::Receiver, +) -> Result<()> { + slog::info!(log, "setting up display"); + + let mut display = GroupDisplay::new( + [ + (GroupDisplayKey::Example, "example"), + (GroupDisplayKey::Other, "other"), + ], + std::io::stdout(), + ); + // For now, always colorize. TODO: figure out whether colorization should be + // done based on always/auto/never etc. + if supports_color::on(supports_color::Stream::Stdout).is_some() { + display.set_styles(LineDisplayStyles::colorized()); + } + + display.set_progress_interval(Duration::from_millis(50)); + + let mut example_buffer = EventBuffer::default(); + let mut example_buffer_last_seen = None; + let mut other_buffer = EventBuffer::default(); + let mut other_buffer_last_seen = None; + + let mut interval = tokio::time::interval(Duration::from_secs(2)); + interval.tick().await; + + loop { + tokio::select! { + _ = interval.tick() => { + // Print out status lines every 2 seconds. + display.write_stats("Status")?; + } + event = receiver.recv() => { + let Some(event) = event else { break }; + example_buffer.add_event(event.clone()); + other_buffer.add_event(event); + + display.add_event_report( + &GroupDisplayKey::Example, + example_buffer.generate_report_since(&mut example_buffer_last_seen), + )?; + display.add_event_report( + &GroupDisplayKey::Other, + other_buffer.generate_report_since(&mut other_buffer_last_seen), + )?; + display.write_events()?; + } + } + } + + // Print status at the end. + display.write_stats("Summary")?; + + Ok(()) +} + +async fn display_progress_bar( log: &slog::Logger, mut receiver: mpsc::Receiver, ) -> Result<()> { diff --git a/update-engine/examples/update-engine-basic/main.rs b/update-engine/examples/update-engine-basic/main.rs index 260473edde..075e4ed253 100644 --- a/update-engine/examples/update-engine-basic/main.rs +++ b/update-engine/examples/update-engine-basic/main.rs @@ -4,13 +4,14 @@ // Copyright 2023 Oxide Computer Company -use std::time::Duration; +use std::{io::IsTerminal, time::Duration}; -use anyhow::{bail, Context}; +use anyhow::{bail, Context, Result}; use buf_list::BufList; use bytes::Buf; use camino::Utf8PathBuf; use camino_tempfile::Utf8TempDir; +use clap::{Parser, ValueEnum}; use display::make_displayer; use omicron_test_utils::dev::test_setup_log; use spec::{ @@ -26,63 +27,113 @@ mod display; mod spec; #[tokio::main(worker_threads = 2)] -async fn main() { - let logctx = test_setup_log("update_engine_basic_example"); - - let context = ExampleContext::new(&logctx.log); - let (display_handle, sender) = make_displayer(&logctx.log); - - let engine = UpdateEngine::new(&logctx.log, sender); - - // Download component 1. - let component_1 = engine.for_component(ExampleComponent::Component1); - let download_handle_1 = context.register_download_step( - &component_1, - "https://www.example.org".to_owned(), - 1_048_576, - ); - - // An example of a skipped step for component 1. - context.register_skipped_step(&component_1); - - // Create temporary directories for component 1. - let temp_dirs_handle_1 = - context.register_create_temp_dirs_step(&component_1, 2); - - // Write component 1 out to disk. - context.register_write_step( - &component_1, - download_handle_1, - temp_dirs_handle_1, - None, - ); - - // Download component 2. - let component_2 = engine.for_component(ExampleComponent::Component2); - let download_handle_2 = context.register_download_step( - &component_2, - "https://www.example.com".to_owned(), - 1_048_576 * 8, - ); - - // Create temporary directories for component 2. - let temp_dirs_handle_2 = - context.register_create_temp_dirs_step(&component_2, 3); - - // Now write component 2 out to disk. - context.register_write_step( - &component_2, - download_handle_2, - temp_dirs_handle_2, - Some(1), - ); - - _ = engine.execute().await; - - // Wait until all messages have been received by the displayer. - _ = display_handle.await; - - // Do not clean up the log file so people can inspect it. +async fn main() -> Result<()> { + let app = App::parse(); + app.exec().await +} + +#[derive(Debug, Parser)] +struct App { + /// Display style to use. + #[clap(long, short = 's', default_value_t, value_enum)] + display_style: DisplayStyleOpt, + + /// Prefix to set on all log messages with display-style=line. + #[clap(long, short = 'p')] + prefix: Option, +} + +impl App { + async fn exec(self) -> Result<()> { + let logctx = test_setup_log("update_engine_basic_example"); + + let display_style = match self.display_style { + DisplayStyleOpt::ProgressBar => DisplayStyle::ProgressBar, + DisplayStyleOpt::Line => DisplayStyle::Line, + DisplayStyleOpt::Group => DisplayStyle::Group, + DisplayStyleOpt::Auto => { + if std::io::stdout().is_terminal() { + DisplayStyle::ProgressBar + } else { + DisplayStyle::Line + } + } + }; + + let context = ExampleContext::new(&logctx.log); + let (display_handle, sender) = + make_displayer(&logctx.log, display_style, self.prefix); + + let engine = UpdateEngine::new(&logctx.log, sender); + + // Download component 1. + let component_1 = engine.for_component(ExampleComponent::Component1); + let download_handle_1 = context.register_download_step( + &component_1, + "https://www.example.org".to_owned(), + 1_048_576, + ); + + // An example of a skipped step for component 1. + context.register_skipped_step(&component_1); + + // Create temporary directories for component 1. + let temp_dirs_handle_1 = + context.register_create_temp_dirs_step(&component_1, 2); + + // Write component 1 out to disk. + context.register_write_step( + &component_1, + download_handle_1, + temp_dirs_handle_1, + None, + ); + + // Download component 2. + let component_2 = engine.for_component(ExampleComponent::Component2); + let download_handle_2 = context.register_download_step( + &component_2, + "https://www.example.com".to_owned(), + 1_048_576 * 8, + ); + + // Create temporary directories for component 2. + let temp_dirs_handle_2 = + context.register_create_temp_dirs_step(&component_2, 3); + + // Now write component 2 out to disk. + context.register_write_step( + &component_2, + download_handle_2, + temp_dirs_handle_2, + Some(1), + ); + + _ = engine.execute().await; + + // Wait until all messages have been received by the displayer. + _ = display_handle.await; + + // Do not clean up the log file so people can inspect it. + + Ok(()) + } +} + +#[derive(Copy, Clone, Debug, Default, ValueEnum)] +enum DisplayStyleOpt { + ProgressBar, + Line, + Group, + #[default] + Auto, +} + +#[derive(Copy, Clone, Debug)] +enum DisplayStyle { + ProgressBar, + Line, + Group, } /// Context shared across steps. This forms the lifetime "'a" defined by the @@ -146,9 +197,30 @@ impl ExampleContext { ({num_bytes} bytes)", ); - // Try a second time, and this time go all the way to 100%. + // Try a second time, and this time go to 80%. + let mut buf_list = BufList::new(); + for i in 0..8 { + tokio::time::sleep(Duration::from_millis(100)).await; + cx.send_progress(StepProgress::with_current_and_total( + num_bytes * i / 10, + num_bytes, + ProgressUnits::BYTES, + serde_json::Value::Null, + )) + .await; + buf_list.push_chunk(&b"downloaded-data"[..]); + } + + // Now indicate a progress reset. + cx.send_progress(StepProgress::reset( + serde_json::Value::Null, + "Progress reset", + )) + .await; + + // Try again. let mut buf_list = BufList::new(); - for i in 0..10 { + for i in 0..8 { tokio::time::sleep(Duration::from_millis(100)).await; cx.send_progress(StepProgress::with_current_and_total( num_bytes * i / 10, diff --git a/update-engine/src/buffer.rs b/update-engine/src/buffer.rs index 2426814444..8cdef8e02e 100644 --- a/update-engine/src/buffer.rs +++ b/update-engine/src/buffer.rs @@ -106,6 +106,23 @@ impl EventBuffer { self.event_store.root_execution_id } + /// Returns information about terminal status for this buffer's root + /// execution ID, or None if the execution has not started or is currently + /// running. + pub fn root_terminal_info(&self) -> Option { + let Some(root_execution_id) = self.root_execution_id() else { + return None; + }; + + let summary = self.steps().summarize(); + summary + .get(&root_execution_id) + .expect("root execution ID must always be present in summary") + .execution_status + .terminal_info() + .cloned() + } + /// Returns information about each step, as currently tracked by the buffer, /// in order of when the events were first defined. pub fn steps(&self) -> EventBufferSteps<'_, S> { @@ -248,6 +265,7 @@ impl EventStore { &event, 0, None, + None, root_event_index, event.total_elapsed, ); @@ -255,6 +273,29 @@ impl EventStore { if new_execution.nest_level == 0 { self.root_execution_id = Some(new_execution.execution_id); } + // If there's a parent key, then what's the child index? + let parent_key_and_child_index = + if let Some(parent_key) = new_execution.parent_key { + match self.map.get_mut(&parent_key) { + Some(parent_data) => { + let child_index = parent_data.child_executions_seen; + parent_data.child_executions_seen += 1; + Some((parent_key, child_index)) + } + None => { + // This should never happen -- it indicates that the + // parent key was unknown. This can happen if we + // didn't receive an event regarding a parent + // execution being started. + // + // TODO: This should probably be an error that gets + // bubbled up to callers. + None + } + } + } else { + None + }; let total_steps = new_execution.steps_to_add.len(); for (new_step_key, new_step, sort_key) in new_execution.steps_to_add { @@ -263,6 +304,7 @@ impl EventStore { self.map.entry(new_step_key).or_insert_with(|| { EventBufferStepData::new( new_step, + parent_key_and_child_index, sort_key, new_execution.nest_level, total_steps, @@ -319,6 +361,7 @@ impl EventStore { &mut self, event: &StepEvent, nest_level: usize, + parent_key: Option, parent_sort_key: Option<&StepSortKey>, root_event_index: RootEventIndex, root_total_elapsed: Duration, @@ -347,6 +390,7 @@ impl EventStore { } new_execution = Some(NewExecutionAction { execution_id: event.execution_id, + parent_key, nest_level, steps_to_add, }); @@ -497,6 +541,7 @@ impl EventStore { let actions = self.recurse_for_step_event( nested_event, nest_level + 1, + Some(parent_key), parent_sort_key.as_ref(), root_event_index, root_total_elapsed, @@ -741,6 +786,9 @@ struct NewExecutionAction { // An execution ID corresponding to a new run, if seen. execution_id: ExecutionId, + // The parent key for this execution, if this is a nested step. + parent_key: Option, + // The nest level for this execution. nest_level: usize, @@ -802,12 +850,18 @@ impl<'buf, S: StepSpec> EventBufferSteps<'buf, S> { #[derive_where(Clone, Debug)] pub struct EventBufferStepData { step_info: StepInfo, + parent_key_and_child_index: Option<(StepKey, usize)>, + sort_key: StepSortKey, // XXX: nest_level and total_steps are common to each execution, but are - // stored separately here. Should we store them in a separate map - // indexed by execution ID? + // stored separately here. These should likely move into + // EventBufferExecutionData. nest_level: usize, total_steps: usize, + + // The number of child executions seen so far. + child_executions_seen: usize, + // Invariant: stored in order sorted by leaf event index. high_priority: Vec>, step_status: StepStatus, @@ -819,6 +873,7 @@ pub struct EventBufferStepData { impl EventBufferStepData { fn new( step_info: StepInfo, + parent_key_and_child_index: Option<(StepKey, usize)>, sort_key: StepSortKey, nest_level: usize, total_steps: usize, @@ -826,9 +881,11 @@ impl EventBufferStepData { ) -> Self { Self { step_info, + parent_key_and_child_index, sort_key, nest_level, total_steps, + child_executions_seen: 0, high_priority: Vec::new(), step_status: StepStatus::NotStarted, last_root_event_index: root_event_index, @@ -840,6 +897,11 @@ impl EventBufferStepData { &self.step_info } + #[inline] + pub fn parent_key_and_child_index(&self) -> Option<(StepKey, usize)> { + self.parent_key_and_child_index + } + #[inline] pub fn nest_level(&self) -> usize { self.nest_level @@ -850,6 +912,11 @@ impl EventBufferStepData { self.total_steps } + #[inline] + pub fn child_executions_seen(&self) -> usize { + self.child_executions_seen + } + #[inline] pub fn step_status(&self) -> &StepStatus { &self.step_status @@ -1462,6 +1529,20 @@ pub enum TerminalKind { Aborted, } +impl ExecutionStatus { + /// Returns the terminal status and the total amount of time elapsed, or + /// None if the execution has not reached a terminal state. + /// + /// The time elapsed might be None if the execution was interrupted and + /// completion information wasn't available. + pub fn terminal_info(&self) -> Option<&ExecutionTerminalInfo> { + match self { + Self::NotStarted | Self::Running { .. } => None, + Self::Terminal(info) => Some(info), + } + } +} + /// Keys for the event tree. #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] enum EventTreeNode { diff --git a/update-engine/src/display/group_display.rs b/update-engine/src/display/group_display.rs new file mode 100644 index 0000000000..a4e8418334 --- /dev/null +++ b/update-engine/src/display/group_display.rs @@ -0,0 +1,467 @@ +// 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/. + +// Copyright 2023 Oxide Computer Company + +use std::{borrow::Borrow, collections::BTreeMap, fmt, time::Duration}; + +use owo_colors::OwoColorize; +use swrite::{swrite, SWrite}; +use unicode_width::UnicodeWidthStr; + +use crate::{ + errors::UnknownReportKey, events::EventReport, EventBuffer, + ExecutionTerminalInfo, StepSpec, TerminalKind, +}; + +use super::{ + line_display_shared::LineDisplayFormatter, LineDisplayShared, + LineDisplayStyles, HEADER_WIDTH, +}; + +/// A displayer that simultaneously manages and shows line-based output for +/// several event buffers. +/// +/// `K` is the key type for each element in the group. Its [`fmt::Display`] impl +/// is called to obtain the prefix, and `Eq + Ord` is used for keys. +#[derive(Debug)] +pub struct GroupDisplay { + // We don't need to add any buffering here because we already write data to + // the writer in a line-buffered fashion (see Self::write_events). + writer: W, + max_width: usize, + single_states: BTreeMap>, + formatter: LineDisplayFormatter, + stats: GroupDisplayStats, +} + +impl GroupDisplay { + /// Creates a new `GroupDisplay` with the provided report keys and + /// prefixes. + /// + /// The function passed in is expected to create a writer. + pub fn new( + keys_and_prefixes: impl IntoIterator, + writer: W, + ) -> Self + where + Str: Into, + { + // Right-align prefixes to their maximum width -- this helps keep the + // output organized. + let mut max_width = 0; + let keys_and_prefixes: Vec<_> = keys_and_prefixes + .into_iter() + .map(|(k, prefix)| { + let prefix = prefix.into(); + max_width = + max_width.max(UnicodeWidthStr::width(prefix.as_str())); + (k, prefix) + }) + .collect(); + let single_states: BTreeMap<_, _> = keys_and_prefixes + .into_iter() + .map(|(k, prefix)| (k, SingleState::new(prefix, max_width))) + .collect(); + + let not_started = single_states.len(); + Self { + writer, + max_width, + single_states, + formatter: LineDisplayFormatter::new(), + stats: GroupDisplayStats::new(not_started), + } + } + + /// Creates a new `GroupDisplay` with the provided report keys, using the + /// `Display` impl to obtain the respective prefixes. + pub fn new_with_display( + keys: impl IntoIterator, + writer: W, + ) -> Self + where + K: fmt::Display, + { + Self::new( + keys.into_iter().map(|k| { + let prefix = k.to_string(); + (k, prefix) + }), + writer, + ) + } + + /// Sets the styles for all future lines. + #[inline] + pub fn set_styles(&mut self, styles: LineDisplayStyles) { + self.formatter.set_styles(styles); + } + + /// Sets the amount of time before new progress events are shown. + #[inline] + pub fn set_progress_interval(&mut self, interval: Duration) { + self.formatter.set_progress_interval(interval); + } + + /// Returns true if this `GroupDisplay` is producing reports corresponding + /// to the given key. + pub fn contains_key(&self, key: &Q) -> bool + where + K: Borrow, + Q: Ord, + { + self.single_states.contains_key(key) + } + + /// Adds an event report to the display, keyed by the index, and updates + /// internal state. + /// + /// Returns `Ok(())` if the report was accepted because the key was + /// known to this `GroupDisplay`, and an error if it was not. + pub fn add_event_report( + &mut self, + key: &Q, + event_report: EventReport, + ) -> Result<(), UnknownReportKey> + where + K: Borrow, + Q: Ord, + { + if let Some(state) = self.single_states.get_mut(key) { + let result = state.add_event_report(event_report); + self.stats.apply_result(result); + Ok(()) + } else { + Err(UnknownReportKey {}) + } + } + + /// Writes a "Status" or "Summary" line to the writer with statistics. + pub fn write_stats(&mut self, header: &str) -> std::io::Result<()> { + // Add a prefix which is equal to the maximum width of the prefixes. + // [prefix 00:00:00] takes up self.max_width + 9 characters inside the + // brackets. + let total_width = self.max_width + 9; + let mut line = format!("[{:total_width$}] ", ""); + self.stats.format_line(&mut line, header, &self.formatter); + writeln!(self.writer, "{line}") + } + + /// Writes all pending events to the writer. + pub fn write_events(&mut self) -> std::io::Result<()> { + let mut lines = Vec::new(); + for state in self.single_states.values_mut() { + state.format_events(&self.formatter, &mut lines); + } + for line in lines { + writeln!(self.writer, "{line}")?; + } + Ok(()) + } + + /// Returns the current statistics for this `GroupDisplay`. + pub fn stats(&self) -> &GroupDisplayStats { + &self.stats + } +} + +#[derive(Clone, Copy, Debug)] +pub struct GroupDisplayStats { + /// The total number of reports. + pub total: usize, + + /// The number of reports that have not yet started. + pub not_started: usize, + + /// The number of reports that are currently running. + pub running: usize, + + /// The number of reports that indicate successful completion. + pub completed: usize, + + /// The number of reports that indicate failure. + pub failed: usize, + + /// The number of reports that indicate being aborted. + pub aborted: usize, + + /// The number of reports where we didn't receive a final state and it got + /// overwritten by another report. + /// + /// Overwritten reports are considered failures since we don't know what + /// happened. + pub overwritten: usize, +} + +impl GroupDisplayStats { + fn new(total: usize) -> Self { + Self { + total, + not_started: total, + completed: 0, + failed: 0, + aborted: 0, + overwritten: 0, + running: 0, + } + } + + /// Returns the number of terminal reports. + pub fn terminal_count(&self) -> usize { + self.completed + self.failed + self.aborted + self.overwritten + } + + /// Returns true if all reports have reached a terminal state. + pub fn is_terminal(&self) -> bool { + self.not_started == 0 && self.running == 0 + } + + /// Returns true if there are any failures. + pub fn has_failures(&self) -> bool { + self.failed > 0 || self.aborted > 0 || self.overwritten > 0 + } + + fn apply_result(&mut self, result: AddEventReportResult) { + // Process result.after first to avoid integer underflow. + match result.after { + SingleStateTag::NotStarted => self.not_started += 1, + SingleStateTag::Running => self.running += 1, + SingleStateTag::Terminal(TerminalKind::Completed) => { + self.completed += 1 + } + SingleStateTag::Terminal(TerminalKind::Failed) => self.failed += 1, + SingleStateTag::Terminal(TerminalKind::Aborted) => { + self.aborted += 1 + } + SingleStateTag::Overwritten => self.overwritten += 1, + } + + match result.before { + SingleStateTag::NotStarted => self.not_started -= 1, + SingleStateTag::Running => self.running -= 1, + SingleStateTag::Terminal(TerminalKind::Completed) => { + self.completed -= 1 + } + SingleStateTag::Terminal(TerminalKind::Failed) => self.failed -= 1, + SingleStateTag::Terminal(TerminalKind::Aborted) => { + self.aborted -= 1 + } + SingleStateTag::Overwritten => self.overwritten -= 1, + } + } + + fn format_line( + &self, + line: &mut String, + header: &str, + formatter: &LineDisplayFormatter, + ) { + let header_style = if self.has_failures() { + formatter.styles().error_style + } else { + formatter.styles().progress_style + }; + + swrite!(line, "{:>HEADER_WIDTH$} ", header.style(header_style)); + let terminal_count = self.terminal_count(); + swrite!( + line, + "{terminal_count}/{}: {} running, {} {}", + self.total, + self.running.style(formatter.styles().meta_style), + self.completed.style(formatter.styles().meta_style), + "completed".style(formatter.styles().progress_style), + ); + if self.failed > 0 { + swrite!( + line, + ", {} {}", + self.failed.style(formatter.styles().meta_style), + "failed".style(formatter.styles().error_style), + ); + } + if self.aborted > 0 { + swrite!( + line, + ", {} {}", + self.aborted.style(formatter.styles().meta_style), + "aborted".style(formatter.styles().error_style), + ); + } + if self.overwritten > 0 { + swrite!( + line, + ", {} {}", + self.overwritten.style(formatter.styles().meta_style), + "overwritten".style(formatter.styles().error_style), + ); + } + } +} + +#[derive(Debug)] +struct SingleState { + shared: LineDisplayShared, + kind: SingleStateKind, + prefix: String, +} + +impl SingleState { + fn new(prefix: String, max_width: usize) -> Self { + // Right-align the prefix to the maximum width. + let prefix = format!("{:>max_width$}", prefix); + Self { + shared: LineDisplayShared::new(), + kind: SingleStateKind::NotStarted { displayed: false }, + prefix, + } + } + + /// Adds an event report and updates the internal state. + fn add_event_report( + &mut self, + event_report: EventReport, + ) -> AddEventReportResult { + let before = match &self.kind { + SingleStateKind::NotStarted { .. } => { + self.kind = SingleStateKind::Running { + event_buffer: EventBuffer::new(8), + }; + SingleStateTag::NotStarted + } + SingleStateKind::Running { .. } => SingleStateTag::Running, + + SingleStateKind::Terminal { info, .. } => { + // Once we've reached a terminal state, we don't record any more + // events. + return AddEventReportResult::unchanged( + SingleStateTag::Terminal(info.kind), + ); + } + SingleStateKind::Overwritten { .. } => { + // This update has already completed -- assume that the event + // buffer is for a new update, which we don't show. + return AddEventReportResult::unchanged( + SingleStateTag::Overwritten, + ); + } + }; + + let SingleStateKind::Running { event_buffer } = &mut self.kind else { + unreachable!("other branches were handled above"); + }; + + if let Some(root_execution_id) = event_buffer.root_execution_id() { + if event_report.root_execution_id != Some(root_execution_id) { + // The report is for a different execution ID -- assume that + // this event is completed and mark our current execution as + // completed. + self.kind = SingleStateKind::Overwritten { displayed: false }; + return AddEventReportResult { + before, + after: SingleStateTag::Overwritten, + }; + } + } + + event_buffer.add_event_report(event_report); + let after = if let Some(info) = event_buffer.root_terminal_info() { + // Grab the event buffer to store it in the terminal state. + let event_buffer = + std::mem::replace(event_buffer, EventBuffer::new(0)); + let terminal_kind = info.kind; + self.kind = SingleStateKind::Terminal { + info, + pending_event_buffer: Some(event_buffer), + }; + SingleStateTag::Terminal(terminal_kind) + } else { + SingleStateTag::Running + }; + + AddEventReportResult { before, after } + } + + pub(super) fn format_events( + &mut self, + formatter: &LineDisplayFormatter, + out: &mut Vec, + ) { + let mut cx = self.shared.with_context(&self.prefix, formatter); + match &mut self.kind { + SingleStateKind::NotStarted { displayed } => { + if !*displayed { + let line = + cx.format_generic("Update not started, waiting..."); + out.push(line); + *displayed = true; + } + } + SingleStateKind::Running { event_buffer } => { + cx.format_event_buffer(event_buffer, out); + } + SingleStateKind::Terminal { info, pending_event_buffer } => { + // Are any remaining events left? This also sets pending_event_buffer + // to None after displaying remaining events. + if let Some(event_buffer) = pending_event_buffer.take() { + cx.format_event_buffer(&event_buffer, out); + // Also show a line to wrap up the terminal status. + let line = cx.format_terminal_info(info); + out.push(line); + } + + // Nothing to do, the terminal status was already printed above. + } + SingleStateKind::Overwritten { displayed } => { + if !*displayed { + let line = cx.format_generic( + "Update overwritten (a different update was started)\ + assuming failure", + ); + out.push(line); + *displayed = true; + } + } + } + } +} + +#[derive(Debug)] +enum SingleStateKind { + NotStarted { + displayed: bool, + }, + Running { + event_buffer: EventBuffer, + }, + Terminal { + info: ExecutionTerminalInfo, + // The event buffer is kept around so that we can display any remaining + // lines. + pending_event_buffer: Option>, + }, + Overwritten { + displayed: bool, + }, +} + +struct AddEventReportResult { + before: SingleStateTag, + after: SingleStateTag, +} + +impl AddEventReportResult { + fn unchanged(tag: SingleStateTag) -> Self { + Self { before: tag, after: tag } + } +} + +#[derive(Copy, Clone, Debug)] +enum SingleStateTag { + NotStarted, + Running, + Terminal(TerminalKind), + Overwritten, +} diff --git a/update-engine/src/display/line_display.rs b/update-engine/src/display/line_display.rs new file mode 100644 index 0000000000..786e689e9e --- /dev/null +++ b/update-engine/src/display/line_display.rs @@ -0,0 +1,134 @@ +// 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/. + +// Copyright 2023 Oxide Computer Company + +use debug_ignore::DebugIgnore; +use derive_where::derive_where; +use owo_colors::Style; +use std::time::Duration; + +use crate::{EventBuffer, ExecutionTerminalInfo, StepSpec}; + +use super::{LineDisplayFormatter, LineDisplayShared}; + +/// A line-oriented display. +/// +/// This display produces output to the provided writer. +#[derive_where(Debug)] +pub struct LineDisplay { + writer: DebugIgnore, + shared: LineDisplayShared, + formatter: LineDisplayFormatter, + prefix: String, +} + +impl LineDisplay { + /// Creates a new LineDisplay. + pub fn new(writer: W) -> Self { + Self { + writer: DebugIgnore(writer), + shared: LineDisplayShared::new(), + formatter: LineDisplayFormatter::new(), + prefix: String::new(), + } + } + + /// Sets the prefix for all future lines. + #[inline] + pub fn set_prefix(&mut self, prefix: impl Into) { + self.prefix = prefix.into(); + } + + /// Sets the styles for all future lines. + #[inline] + pub fn set_styles(&mut self, styles: LineDisplayStyles) { + self.formatter.set_styles(styles); + } + + /// Sets the amount of time before the next progress event is shown. + #[inline] + pub fn set_progress_interval(&mut self, interval: Duration) { + self.formatter.set_progress_interval(interval); + } + + /// Writes an event buffer to the writer, incrementally. + /// + /// This is a stateful method that will only display events that have not + /// been displayed before. + pub fn write_event_buffer( + &mut self, + buffer: &EventBuffer, + ) -> std::io::Result<()> { + let mut lines = Vec::new(); + self.shared + .with_context(&self.prefix, &self.formatter) + .format_event_buffer(buffer, &mut lines); + for line in lines { + writeln!(self.writer, "{line}")?; + } + + Ok(()) + } + + /// Writes terminal information to the writer. + pub fn write_terminal_info( + &mut self, + info: &ExecutionTerminalInfo, + ) -> std::io::Result<()> { + let line = self + .shared + .with_context(&self.prefix, &self.formatter) + .format_terminal_info(info); + writeln!(self.writer, "{line}") + } + + /// Writes a generic line to the writer, with prefix attached if provided. + pub fn write_generic(&mut self, message: &str) -> std::io::Result<()> { + let line = self + .shared + .with_context(&self.prefix, &self.formatter) + .format_generic(message); + writeln!(self.writer, "{line}") + } +} + +/// Styles for [`LineDisplay`]. +/// +/// By default this isn't colorized, but it can be if so chosen. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct LineDisplayStyles { + pub prefix_style: Style, + pub meta_style: Style, + pub step_name_style: Style, + pub progress_style: Style, + pub progress_message_style: Style, + pub warning_style: Style, + pub warning_message_style: Style, + pub error_style: Style, + pub error_message_style: Style, + pub skipped_style: Style, + pub retry_style: Style, +} + +impl LineDisplayStyles { + /// Returns a default set of colorized styles with ANSI colors. + pub fn colorized() -> Self { + let mut ret = Self::default(); + ret.prefix_style = Style::new().bold(); + ret.meta_style = Style::new().bold(); + ret.step_name_style = Style::new().cyan(); + ret.progress_style = Style::new().bold().green(); + ret.progress_message_style = Style::new().green(); + ret.warning_style = Style::new().bold().yellow(); + ret.warning_message_style = Style::new().yellow(); + ret.error_style = Style::new().bold().red(); + ret.error_message_style = Style::new().red(); + ret.skipped_style = Style::new().bold().yellow(); + ret.retry_style = Style::new().bold().yellow(); + + ret + } +} diff --git a/update-engine/src/display/line_display_shared.rs b/update-engine/src/display/line_display_shared.rs new file mode 100644 index 0000000000..4f37f89d7d --- /dev/null +++ b/update-engine/src/display/line_display_shared.rs @@ -0,0 +1,959 @@ +// 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/. + +// Copyright 2023 Oxide Computer Company + +//! Types and code shared between `LineDisplay` and `GroupDisplay`. + +use std::{ + collections::{hash_map::Entry, HashMap}, + fmt::{self, Write as _}, + time::Duration, +}; + +use owo_colors::OwoColorize; +use swrite::{swrite, SWrite as _}; + +use crate::{ + events::{ + LowPriorityStepEventKind, ProgressCounter, ProgressEvent, StepEvent, + StepInfo, StepOutcome, + }, + AbortInfo, AbortReason, CompletionInfo, EventBuffer, EventBufferStepData, + ExecutionTerminalInfo, FailureInfo, FailureReason, NestedSpec, + RootEventIndex, StepKey, StepSpec, StepStatus, TerminalKind, +}; + +use super::LineDisplayStyles; + +// This is chosen to leave enough room for all possible headers: "Completed" at +// 9 characters is the longest. +pub(super) const HEADER_WIDTH: usize = 9; + +#[derive(Debug)] +pub(super) struct LineDisplayShared { + step_data: HashMap, +} + +impl LineDisplayShared { + pub(super) fn new() -> Self { + Self { step_data: HashMap::new() } + } + + pub(super) fn with_context<'a>( + &'a mut self, + prefix: &'a str, + formatter: &'a LineDisplayFormatter, + ) -> LineDisplaySharedContext<'_> { + LineDisplaySharedContext { shared: self, prefix, formatter } + } +} + +#[derive(Debug)] +pub(super) struct LineDisplaySharedContext<'a> { + shared: &'a mut LineDisplayShared, + prefix: &'a str, + formatter: &'a LineDisplayFormatter, +} + +impl<'a> LineDisplaySharedContext<'a> { + /// Produces a generic line from the prefix and message. + /// + /// This line does not have a trailing newline; adding one is the caller's + /// responsibility. + pub(super) fn format_generic(&self, message: &str) -> String { + let mut line = self.formatter.start_println(self.prefix); + line.push_str(message); + line + } + + /// Produces lines for this event buffer, and advances internal state. + /// + /// Returned lines do not have a trailing newline; adding them is the + /// caller's responsibility. + pub(super) fn format_event_buffer( + &mut self, + buffer: &EventBuffer, + out: &mut Vec, + ) { + let steps = buffer.steps(); + for (step_key, data) in steps.as_slice() { + self.format_step_and_update(buffer, *step_key, data, out); + } + } + + /// Produces lines corresponding to this step, and advances internal state. + /// + /// Returned lines do not have a trailing newline; adding them is the + /// caller's responsibility. + pub(super) fn format_step_and_update( + &mut self, + buffer: &EventBuffer, + step_key: StepKey, + data: &EventBufferStepData, + out: &mut Vec, + ) { + let ld_step_info = LineDisplayStepInfo { + step_info: data.step_info(), + parent_key_and_child_index: data.parent_key_and_child_index(), + total_steps: data.total_steps(), + }; + let nest_level = data.nest_level(); + let step_status = data.step_status(); + + match step_status { + StepStatus::NotStarted => {} + StepStatus::Running { progress_event, low_priority } => { + for event in low_priority { + self.format_low_priority_event( + event, + step_key, + ld_step_info, + nest_level, + out, + ); + } + + self.format_progress_event( + progress_event, + step_key, + data, + ld_step_info, + nest_level, + out, + ); + + self.insert_or_update_index( + step_key, + data.last_root_event_index(), + ); + } + StepStatus::Completed { info } => { + if let Some(sd) = self.shared.step_data.get(&step_key) { + if sd.last_root_event_index >= data.last_root_event_index() + { + // We've already displayed this event. + return; + } + } + + match info { + Some(info) => { + let mut line = self.formatter.start_line( + self.prefix, + Some(info.root_total_elapsed), + ); + self.formatter.add_completion_and_step_info( + &mut line, + NestLevel::Regular(nest_level), + ld_step_info, + info, + ); + out.push(line); + } + None => { + // This means that we don't know what happened to the step + // but it did complete. + let mut line = + self.formatter.start_line(self.prefix, None); + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Completed" + .style(self.formatter.styles.progress_style), + ); + self.formatter.add_step_info( + &mut line, + ld_step_info, + NestLevel::Regular(nest_level), + ); + swrite!( + line, + ": with {}", + "unknown outcome" + .style(self.formatter.styles.meta_style), + ); + out.push(line); + } + } + + self.insert_or_update_index( + step_key, + data.last_root_event_index(), + ); + } + StepStatus::Failed { reason } => { + if let Some(ld) = self.shared.step_data.get(&step_key) { + if ld.last_root_event_index >= data.last_root_event_index() + { + // We've already displayed this event. + return; + } + } + + match reason { + FailureReason::StepFailed(info) => { + let mut line = self.formatter.start_line( + self.prefix, + Some(info.root_total_elapsed), + ); + let nest_level = NestLevel::Regular(nest_level); + + // The prefix is used for "Caused by" lines below. Add + // the requisite amount of spacing here. + let mut caused_by_prefix = line.clone(); + swrite!(caused_by_prefix, "{:>HEADER_WIDTH$} ", ""); + nest_level.add_prefix(&mut caused_by_prefix); + + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Failed".style(self.formatter.styles.error_style) + ); + self.formatter.add_step_info( + &mut line, + ld_step_info, + nest_level, + ); + line.push_str(": "); + self.formatter.add_failure_info( + &mut line, + &caused_by_prefix, + info, + ); + out.push(line); + } + FailureReason::ParentFailed { parent_step } => { + let parent_step_info = buffer + .get(&parent_step) + .expect("parent step must exist"); + let mut line = + self.formatter.start_line(self.prefix, None); + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Failed".style(self.formatter.styles.error_style) + ); + self.formatter.add_step_info( + &mut line, + ld_step_info, + NestLevel::Regular(nest_level), + ); + swrite!( + line, + ": because parent step {} failed", + parent_step_info + .step_info() + .description + .style(self.formatter.styles.step_name_style) + ); + out.push(line); + } + } + + self.insert_or_update_index( + step_key, + data.last_root_event_index(), + ); + } + StepStatus::Aborted { reason, .. } => { + if let Some(ld) = self.shared.step_data.get(&step_key) { + if ld.last_root_event_index >= data.last_root_event_index() + { + // We've already displayed this event. + return; + } + } + + match reason { + AbortReason::StepAborted(info) => { + let mut line = self.formatter.start_line( + self.prefix, + Some(info.root_total_elapsed), + ); + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Aborted".style(self.formatter.styles.error_style) + ); + self.formatter.add_step_info( + &mut line, + ld_step_info, + NestLevel::Regular(nest_level), + ); + line.push_str(": "); + self.formatter.add_abort_info(&mut line, info); + out.push(line); + } + AbortReason::ParentAborted { parent_step } => { + let parent_step_info = buffer + .get(&parent_step) + .expect("parent step must exist"); + let mut line = + self.formatter.start_line(self.prefix, None); + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Aborted".style(self.formatter.styles.error_style) + ); + self.formatter.add_step_info( + &mut line, + ld_step_info, + NestLevel::Regular(nest_level), + ); + swrite!( + line, + ": because parent step {} aborted", + parent_step_info + .step_info() + .description + .style(self.formatter.styles.step_name_style) + ); + out.push(line); + } + } + + self.insert_or_update_index( + step_key, + data.last_root_event_index(), + ); + } + StepStatus::WillNotBeRun { .. } => { + // We don't print "will not be run". (TODO: maybe add an + // extended mode which does do so?) + } + } + } + + /// Formats this terminal information. + /// + /// This line does not have a trailing newline; adding one is the caller's + /// responsibility. + pub(super) fn format_terminal_info( + &self, + info: &ExecutionTerminalInfo, + ) -> String { + let mut line = + self.formatter.start_line(self.prefix, info.leaf_total_elapsed); + match info.kind { + TerminalKind::Completed => { + swrite!( + line, + "{:>HEADER_WIDTH$} Execution {}", + "Terminal".style(self.formatter.styles.progress_style), + "completed".style(self.formatter.styles.progress_style), + ); + } + TerminalKind::Failed => { + swrite!( + line, + "{:>HEADER_WIDTH$} Execution {}", + "Terminal".style(self.formatter.styles.error_style), + "failed".style(self.formatter.styles.error_style), + ); + } + TerminalKind::Aborted => { + swrite!( + line, + "{:>HEADER_WIDTH$} Execution {}", + "Terminal".style(self.formatter.styles.error_style), + "aborted".style(self.formatter.styles.error_style), + ); + } + } + line + } + + fn format_low_priority_event( + &mut self, + event: &StepEvent, + step_key: StepKey, + ld_step_info: LineDisplayStepInfo<'_>, + nest_level: usize, + out: &mut Vec, + ) { + if let Some(sd) = self.shared.step_data.get(&step_key) { + if sd.last_root_event_index >= RootEventIndex(event.event_index) { + // We've already displayed this event. + return; + } + } + + let Some(lowpri_event) = event.to_low_priority() else { + // Can't show anything for unknown events. + return; + }; + + let mut line = + self.formatter.start_line(self.prefix, Some(event.total_elapsed)); + + match lowpri_event.kind { + LowPriorityStepEventKind::ProgressReset { + attempt, + attempt_elapsed, + message, + .. + } => { + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Reset".style(self.formatter.styles.warning_style) + ); + self.formatter.add_step_info( + &mut line, + ld_step_info, + NestLevel::Regular(nest_level), + ); + swrite!( + line, + ": after {:.2?}", + attempt_elapsed.style(self.formatter.styles.meta_style), + ); + if attempt > 1 { + swrite!( + line, + " (at attempt {})", + attempt.style(self.formatter.styles.meta_style), + ); + } + swrite!( + line, + " with message: {}", + message.style(self.formatter.styles.warning_message_style) + ); + } + LowPriorityStepEventKind::AttemptRetry { + next_attempt, + attempt_elapsed, + message, + .. + } => { + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Retry".style(self.formatter.styles.warning_style) + ); + self.formatter.add_step_info( + &mut line, + ld_step_info, + NestLevel::Regular(nest_level), + ); + swrite!( + line, + ": after {:.2?}", + attempt_elapsed.style(self.formatter.styles.meta_style), + ); + swrite!( + line, + " (at attempt {})", + next_attempt + .saturating_sub(1) + .style(self.formatter.styles.meta_style), + ); + swrite!( + line, + " with message: {}", + message.style(self.formatter.styles.warning_message_style) + ); + } + } + + out.push(line); + } + + fn format_progress_event( + &mut self, + progress_event: &ProgressEvent, + step_key: StepKey, + data: &EventBufferStepData, + ld_step_info: LineDisplayStepInfo<'_>, + nest_level: usize, + out: &mut Vec, + ) { + let Some(leaf_step_elapsed) = progress_event.kind.leaf_step_elapsed() + else { + // Can't show anything for unknown events. + return; + }; + let leaf_attempt = progress_event + .kind + .leaf_attempt() + .expect("if leaf_step_elapsed is Some, leaf_attempt must be Some"); + let leaf_attempt_elapsed = progress_event + .kind + .leaf_attempt_elapsed() + .expect( + "if leaf_step_elapsed is Some, leaf_attempt_elapsed must be Some", + ); + + let sd = self + .shared + .step_data + .entry(step_key) + .or_insert_with(|| StepData::new(data.last_root_event_index())); + + let (is_first_event, should_display) = match sd.last_progress_event_at { + Some(last_progress_event_at) => { + // Don't show events with zero attempt elapsed time unless + // they're the first (the others will be shown as part of + // low-priority step events). + let should_display = if leaf_attempt > 1 { + leaf_attempt_elapsed > Duration::ZERO + } else { + true + }; + // Show further progress events only after the progress interval + // has elapsed. + let should_display = should_display + && leaf_step_elapsed + > last_progress_event_at + + self.formatter.progress_interval; + (false, should_display) + } + None => (true, true), + }; + + if should_display { + let mut line = self + .formatter + .start_line(self.prefix, Some(progress_event.total_elapsed)); + let nest_level = if is_first_event { + NestLevel::Regular(nest_level) + } else { + // Add an extra half-indent for non-first progress events. + NestLevel::ExtraHalf(nest_level) + }; + + let (before, after) = match progress_event.kind.progress_counter() { + Some(counter) => { + let progress_str = format_progress_counter(counter); + ( + format!( + "{:>HEADER_WIDTH$} ", + "Progress" + .style(self.formatter.styles.progress_style) + ), + format!( + "{progress_str} after {:.2?}", + leaf_attempt_elapsed + .style(self.formatter.styles.meta_style), + ), + ) + } + None => { + let before = format!( + "{:>HEADER_WIDTH$} ", + "Running".style(self.formatter.styles.progress_style), + ); + + // If the leaf attempt elapsed is non-zero, show it. + let after = if leaf_attempt_elapsed > Duration::ZERO { + format!( + "after {:.2?}", + leaf_attempt_elapsed + .style(self.formatter.styles.meta_style), + ) + } else { + String::new() + }; + + (before, after) + } + }; + + swrite!(line, "{}", before); + self.formatter.add_step_info(&mut line, ld_step_info, nest_level); + if !after.is_empty() { + swrite!(line, ": {}", after); + } + + out.push(line); + + sd.update_progress_event(leaf_step_elapsed); + } + } + + fn insert_or_update_index( + &mut self, + step_key: StepKey, + last_root_index: RootEventIndex, + ) { + match self.shared.step_data.entry(step_key) { + Entry::Occupied(mut entry) => { + entry.get_mut().update_last_root_event_index(last_root_index); + } + Entry::Vacant(entry) => { + entry.insert(StepData::new(last_root_index)); + } + } + } +} + +fn format_progress_counter(counter: &ProgressCounter) -> String { + match counter.total { + Some(total) => { + // Show a percentage value. Correct alignment requires converting to + // a string in the middle like this. + let percent = (counter.current as f64 / total as f64) * 100.0; + // <12.34> is 5 characters wide. + let percent_width = 5; + let counter_width = total.to_string().len(); + format!( + "{:>percent_width$.2}% ({:>counter_width$}/{} {})", + percent, counter.current, total, counter.units, + ) + } + None => format!("{} {}", counter.current, counter.units), + } +} + +/// State that tracks line display formatting. +/// +/// Each `LineDisplay` and `GroupDisplay` has one of these. +#[derive(Debug)] +pub(super) struct LineDisplayFormatter { + styles: LineDisplayStyles, + progress_interval: Duration, +} + +impl LineDisplayFormatter { + pub(super) fn new() -> Self { + Self { + styles: LineDisplayStyles::default(), + progress_interval: Duration::from_secs(1), + } + } + + #[inline] + pub(super) fn styles(&self) -> &LineDisplayStyles { + &self.styles + } + + #[inline] + pub(super) fn set_styles(&mut self, styles: LineDisplayStyles) { + self.styles = styles; + } + + #[inline] + pub(super) fn set_progress_interval(&mut self, interval: Duration) { + self.progress_interval = interval; + } + + // --- + // Internal helpers + // --- + + fn start_println(&self, prefix: &str) -> String { + if !prefix.is_empty() { + format!("[{}] ", prefix.style(self.styles.prefix_style)) + } else { + String::new() + } + } + + fn start_line( + &self, + prefix: &str, + total_elapsed: Option, + ) -> String { + let mut line = format!("[{}", prefix.style(self.styles.prefix_style)); + + if !prefix.is_empty() { + line.push(' '); + } + + // Show total elapsed time in an hh:mm:ss format. + if let Some(total_elapsed) = total_elapsed { + let total_elapsed = total_elapsed.as_secs(); + let hours = total_elapsed / 3600; + let minutes = (total_elapsed % 3600) / 60; + let seconds = total_elapsed % 60; + swrite!(&mut line, "{:02}:{:02}:{:02}", hours, minutes, seconds); + } else { + // Add 8 spaces to align with hh:mm:ss. + line.push_str(" "); + } + + line.push_str("] "); + + line + } + + fn add_step_info( + &self, + line: &mut String, + ld_step_info: LineDisplayStepInfo<'_>, + nest_level: NestLevel, + ) { + nest_level.add_prefix(line); + + match ld_step_info.parent_key_and_child_index { + Some((parent_key, child_index)) => { + // Print e.g. (6a . + swrite!( + line, + "({}{} ", + // Add 1 to the index to make it 1-based. + parent_key.index + 1, + AsLetters(child_index) + ); + } + None => { + swrite!(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(); + swrite!( + line, + "{:width$}/{:width$}) ", + step_index, + ld_step_info.total_steps, + width = step_index_width + ); + + swrite!( + line, + "{}", + ld_step_info + .step_info + .description + .style(self.styles.step_name_style) + ); + } + + pub(super) fn add_completion_and_step_info( + &self, + line: &mut String, + nest_level: NestLevel, + ld_step_info: LineDisplayStepInfo<'_>, + info: &CompletionInfo, + ) { + let mut meta = format!( + "after {:.2?}", + info.step_elapsed.style(self.styles.meta_style) + ); + if info.attempt > 1 { + swrite!( + meta, + " (at attempt {})", + info.attempt.style(self.styles.meta_style) + ); + } + + match &info.outcome { + StepOutcome::Success { message, .. } => { + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Completed".style(self.styles.progress_style), + ); + self.add_step_info(line, ld_step_info, nest_level); + match message { + Some(message) => { + swrite!( + line, + ": {meta} with message: {}", + message.style(self.styles.progress_message_style) + ); + } + None => { + swrite!(line, ": {meta}"); + } + } + } + StepOutcome::Warning { message, .. } => { + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Completed".style(self.styles.warning_style), + ); + self.add_step_info(line, ld_step_info, nest_level); + swrite!( + line, + ": {meta} with warning: {}", + message.style(self.styles.warning_message_style) + ); + } + StepOutcome::Skipped { message, .. } => { + swrite!( + line, + "{:>HEADER_WIDTH$} ", + "Skipped".style(self.styles.skipped_style), + ); + self.add_step_info(line, ld_step_info, nest_level); + swrite!( + line, + ": {}", + message.style(self.styles.warning_message_style) + ); + } + }; + } + + pub(super) fn add_failure_info( + &self, + line: &mut String, + line_prefix: &str, + info: &FailureInfo, + ) { + let mut meta = format!( + "after {:.2?}", + info.step_elapsed.style(self.styles.meta_style) + ); + if info.total_attempts > 1 { + swrite!( + meta, + " (after {} attempts)", + info.total_attempts.style(self.styles.meta_style) + ); + } + + swrite!( + line, + "{meta}: {}", + info.message.style(self.styles.error_message_style) + ); + if !info.causes.is_empty() { + swrite!( + line, + "\n{line_prefix}{}", + " Caused by:".style(self.styles.meta_style) + ); + for cause in &info.causes { + swrite!(line, "\n{line_prefix} - {}", cause); + } + } + + // The last newline is added by the caller. + } + + pub(super) fn add_abort_info(&self, line: &mut String, info: &AbortInfo) { + let mut meta = format!( + "after {:.2?}", + info.step_elapsed.style(self.styles.meta_style) + ); + if info.attempt > 1 { + swrite!( + meta, + " (at attempt {})", + info.attempt.style(self.styles.meta_style) + ); + } + + swrite!(line, "{meta} with message \"{}\"", info.message,); + } +} + +#[derive(Clone, Copy, Debug)] +pub(super) struct LineDisplayStepInfo<'a> { + pub(super) step_info: &'a StepInfo, + pub(super) parent_key_and_child_index: Option<(StepKey, usize)>, + pub(super) total_steps: usize, +} + +/// Per-step stateful data tracked by the line displayer. +#[derive(Debug)] +struct StepData { + /// The last root event index that was displayed for this step. + /// + /// This is used to avoid displaying the same event twice. + last_root_event_index: RootEventIndex, + + /// The last `leaf_step_elapsed` at which a progress event was displayed for + /// this step. + last_progress_event_at: Option, +} + +impl StepData { + fn new(last_root_event_index: RootEventIndex) -> Self { + Self { last_root_event_index, last_progress_event_at: None } + } + + fn update_progress_event(&mut self, leaf_step_elapsed: Duration) { + self.last_progress_event_at = Some(leaf_step_elapsed); + } + + fn update_last_root_event_index( + &mut self, + root_event_index: RootEventIndex, + ) { + self.last_root_event_index = root_event_index; + } +} + +#[derive(Copy, Clone, Debug)] +pub(super) enum NestLevel { + /// Regular nest level. + Regular(usize), + + /// These many nest levels, except also add an extra half indent. + ExtraHalf(usize), +} + +impl NestLevel { + fn add_prefix(self, line: &mut String) { + match self { + NestLevel::Regular(0) => {} + NestLevel::Regular(nest_level) => { + line.push_str(&"....".repeat(nest_level)); + line.push_str(" "); + } + NestLevel::ExtraHalf(nest_level) => { + line.push_str(&"....".repeat(nest_level)); + line.push_str(".. "); + } + } + } +} + +/// A display impl that converts a 0-based index into a letter or a series of +/// letters. +/// +/// This is effectively a conversion to base 26. +struct AsLetters(usize); + +impl fmt::Display for AsLetters { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut index = self.0; + loop { + let letter = (b'a' + (index % 26) as u8) as char; + f.write_char(letter)?; + index /= 26; + if index == 0 { + break; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_progress_counter() { + let tests = vec![ + (ProgressCounter::new(5, 20, "units"), "25.00% ( 5/20 units)"), + (ProgressCounter::new(0, 20, "bytes"), " 0.00% ( 0/20 bytes)"), + (ProgressCounter::new(20, 20, "cubes"), "100.00% (20/20 cubes)"), + // NaN is a weird case that is a buggy update engine impl in practice + (ProgressCounter::new(0, 0, "units"), " NaN% (0/0 units)"), + (ProgressCounter::current(5, "units"), "5 units"), + ]; + for (input, output) in tests { + assert_eq!( + format_progress_counter(&input), + output, + "format matches for input: {:?}", + input + ); + } + } +} diff --git a/update-engine/src/display/mod.rs b/update-engine/src/display/mod.rs new file mode 100644 index 0000000000..c58a4535a0 --- /dev/null +++ b/update-engine/src/display/mod.rs @@ -0,0 +1,21 @@ +// 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/. + +// Copyright 2023 Oxide Computer Company + +//! Displayers for the update engine. +//! +//! Currently implemented are: +//! +//! * [`LineDisplay`]: a line-oriented display suitable for the command line. +//! * [`GroupDisplay`]: manages state and shows the results of several +//! [`LineDisplay`]s at once. + +mod group_display; +mod line_display; +mod line_display_shared; + +pub use group_display::GroupDisplay; +pub use line_display::{LineDisplay, LineDisplayStyles}; +use line_display_shared::*; diff --git a/update-engine/src/errors.rs b/update-engine/src/errors.rs index f40ce096d3..055460a447 100644 --- a/update-engine/src/errors.rs +++ b/update-engine/src/errors.rs @@ -185,3 +185,17 @@ pub enum ConvertGenericPathElement { Path(&'static str), ArrayIndex(&'static str, usize), } + +/// The +/// [`GroupDisplay::add_event_report`](crate::display::GroupDisplay::add_event_report) +/// method was called with an unknown key. +#[derive(Clone, Debug)] +pub struct UnknownReportKey {} + +impl fmt::Display for UnknownReportKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("unknown report key") + } +} + +impl error::Error for UnknownReportKey {} diff --git a/update-engine/src/events.rs b/update-engine/src/events.rs index 3816157d0d..21eb05f0ee 100644 --- a/update-engine/src/events.rs +++ b/update-engine/src/events.rs @@ -194,6 +194,68 @@ impl StepEvent { } } + /// Converts this event into the corresponding low-priority (leaf) step + /// event, recursing into nested events as necessary. + /// + /// Returns None if this is not a low-priority event. + pub fn to_low_priority(&self) -> Option { + let step_and_kind = match &self.kind { + StepEventKind::NoStepsDefined + | StepEventKind::ExecutionStarted { .. } + | StepEventKind::StepCompleted { .. } + | StepEventKind::ExecutionCompleted { .. } + | StepEventKind::ExecutionFailed { .. } + | StepEventKind::ExecutionAborted { .. } + | StepEventKind::Unknown => None, + StepEventKind::ProgressReset { + step, + attempt, + metadata, + step_elapsed, + attempt_elapsed, + message, + } => { + let step = step.clone().into_generic(); + let kind = LowPriorityStepEventKind::ProgressReset { + attempt: *attempt, + metadata: serde_json::to_value(metadata) + .unwrap_or_else(|_| serde_json::Value::Null), + step_elapsed: *step_elapsed, + attempt_elapsed: *attempt_elapsed, + message: message.clone(), + }; + Some((step, kind)) + } + StepEventKind::AttemptRetry { + step, + next_attempt, + step_elapsed, + attempt_elapsed, + message, + } => { + let step = step.clone().into_generic(); + let kind = LowPriorityStepEventKind::AttemptRetry { + next_attempt: *next_attempt, + step_elapsed: *step_elapsed, + attempt_elapsed: *attempt_elapsed, + message: message.clone(), + }; + Some((step, kind)) + } + StepEventKind::Nested { event, .. } => { + return event.to_low_priority(); + } + }; + + step_and_kind.map(|(step, kind)| LowPriorityStepEvent { + leaf_execution_id: self.execution_id, + leaf_event_index: self.event_index, + leaf_total_elapsed: self.total_elapsed, + step, + kind, + }) + } + /// Returns the execution ID for the leaf event, recursing into nested /// events if necessary. pub fn leaf_execution_id(&self) -> ExecutionId { @@ -820,6 +882,69 @@ pub enum StepEventPriority { High, } +/// A low-priority step event. +/// +/// Returned by [`StepEventKind::to_low_priority`]. +#[derive(Clone, Debug)] +pub struct LowPriorityStepEvent { + /// The leaf execution ID. + pub leaf_execution_id: ExecutionId, + + /// The leaf event index. + pub leaf_event_index: usize, + + /// The total time elapsed for the leaf event. + pub leaf_total_elapsed: Duration, + + /// Information about the leaf step. + pub step: StepInfoWithMetadata, + + /// The kind of low-priority event this is. + pub kind: LowPriorityStepEventKind, +} + +/// A kind of low-priority step event. +/// +/// Part of [`LowPriorityStepEvent::kind`]. +#[derive(Clone, Debug)] +pub enum LowPriorityStepEventKind { + /// Progress was reset along an attempt, and this attempt is going down a + /// different path. + ProgressReset { + /// The current attempt number. + attempt: usize, + + /// Progress-related metadata associated with this attempt. + metadata: serde_json::Value, + + /// Total time elapsed since the start of the step. Includes prior + /// attempts. + step_elapsed: Duration, + + /// The amount of time this attempt has taken so far. + attempt_elapsed: Duration, + + /// A message assocaited with the reset. + message: Cow<'static, str>, + }, + + /// An attempt failed and this step is being retried. + AttemptRetry { + /// The attempt number for the next attempt. + next_attempt: usize, + + /// Total time elapsed since the start of the step. Includes prior + /// attempts. + step_elapsed: Duration, + + /// The amount of time the previous attempt took. + attempt_elapsed: Duration, + + /// A message associated with the retry. + message: Cow<'static, str>, + }, +} + #[derive(Deserialize, Serialize, JsonSchema)] #[derive_where(Clone, Debug, Eq, PartialEq)] #[serde(bound = "", rename_all = "snake_case", tag = "kind")] @@ -1143,6 +1268,8 @@ impl ProgressEventKind { /// Returns `step_elapsed` for the leaf event, recursing into nested events /// as necessary. + /// + /// Returns None for unknown events. pub fn leaf_step_elapsed(&self) -> Option { match self { ProgressEventKind::WaitingForProgress { step_elapsed, .. } @@ -1156,6 +1283,25 @@ impl ProgressEventKind { } } + /// Returns `attempt_elapsed` for the leaf event, recursing into nested + /// events as necessary. + /// + /// Returns None for unknown events. + pub fn leaf_attempt_elapsed(&self) -> Option { + match self { + ProgressEventKind::WaitingForProgress { + attempt_elapsed, .. + } + | ProgressEventKind::Progress { attempt_elapsed, .. } => { + Some(*attempt_elapsed) + } + ProgressEventKind::Nested { event, .. } => { + event.kind.leaf_attempt_elapsed() + } + ProgressEventKind::Unknown => None, + } + } + /// Converts a generic version into self. /// /// This version can be used to convert a generic type into a more concrete diff --git a/update-engine/src/lib.rs b/update-engine/src/lib.rs index f753fa738a..fea92d3b73 100644 --- a/update-engine/src/lib.rs +++ b/update-engine/src/lib.rs @@ -57,6 +57,7 @@ mod buffer; mod context; +pub mod display; mod engine; pub mod errors; pub mod events; diff --git a/wicket/Cargo.toml b/wicket/Cargo.toml index 5392e72e9f..11f476d98c 100644 --- a/wicket/Cargo.toml +++ b/wicket/Cargo.toml @@ -13,6 +13,7 @@ camino.workspace = true ciborium.workspace = true clap.workspace = true crossterm.workspace = true +debug-ignore.workspace = true futures.workspace = true hex = { workspace = true, features = ["serde"] } humantime.workspace = true @@ -33,6 +34,7 @@ slog.workspace = true slog-async.workspace = true slog-envlogger.workspace = true slog-term.workspace = true +supports-color.workspace = true textwrap.workspace = true tokio = { workspace = true, features = ["full"] } tokio-util.workspace = true @@ -40,6 +42,7 @@ toml.workspace = true toml_edit.workspace = true tui-tree-widget = "0.13.0" unicode-width.workspace = true +uuid.workspace = true zeroize.workspace = true omicron-passwords.workspace = true diff --git a/wicket/src/dispatch.rs b/wicket/src/dispatch.rs index e8191f59cb..8784a0cb22 100644 --- a/wicket/src/dispatch.rs +++ b/wicket/src/dispatch.rs @@ -8,12 +8,13 @@ use std::net::{Ipv6Addr, SocketAddrV6}; use anyhow::{bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; -use clap::Parser; +use clap::{Args, ColorChoice, Parser, Subcommand}; use omicron_common::{address::WICKETD_PORT, FileKv}; use slog::Drain; use crate::{ - preflight::PreflightArgs, rack_setup::SetupArgs, upload::UploadArgs, Runner, + preflight::PreflightArgs, rack_setup::SetupArgs, + rack_update::RackUpdateArgs, upload::UploadArgs, Runner, }; pub fn exec() -> Result<()> { @@ -27,14 +28,22 @@ pub fn exec() -> Result<()> { format!("could not parse shell arguments from input {ssh_args}") })?; - let log = setup_log(&log_path()?, WithStderr::Yes)?; // parse_from uses the the first argument as the command name. Insert "wicket" as // the command name. - let args = ShellCommand::parse_from( + let app = ShellApp::parse_from( std::iter::once("wicket".to_owned()).chain(args), ); - match args { + + let log = setup_log( + &log_path()?, + WithStderr::Yes { use_color: app.global_opts.use_color() }, + )?; + + match app.command { ShellCommand::UploadRepo(args) => args.exec(log, wicketd_addr), + ShellCommand::RackUpdate(args) => { + args.exec(log, wicketd_addr, app.global_opts) + } ShellCommand::Setup(args) => args.exec(log, wicketd_addr), ShellCommand::Preflight(args) => args.exec(log, wicketd_addr), } @@ -46,20 +55,62 @@ pub fn exec() -> Result<()> { } } +/// An app that represents wicket started with arguments over ssh. +#[derive(Debug, Parser)] +struct ShellApp { + /// Global options. + #[clap(flatten)] + global_opts: GlobalOpts, + + /// The command to run. + #[clap(subcommand)] + command: ShellCommand, +} + +#[derive(Debug, Args)] +#[clap(next_help_heading = "Global options")] +pub(crate) struct GlobalOpts { + /// Color output (auto, always, never) + /// + /// This may not work everywhere at the moment. + #[clap(long, value_enum, global = true, default_value_t)] + pub(crate) color: ColorChoice, +} + +impl GlobalOpts { + /// Returns true if color should be used on standard error. + pub(crate) fn use_color(&self) -> bool { + match self.color { + ColorChoice::Auto => { + supports_color::on_cached(supports_color::Stream::Stderr) + .is_some() + } + ColorChoice::Always => true, + ColorChoice::Never => false, + } + } +} + /// Arguments passed to wicket. /// /// Wicket is designed to be used as a captive shell, set up via sshd /// ForceCommand. If no arguments are specified, wicket behaves like a TUI. /// However, if arguments are specified via SSH_ORIGINAL_COMMAND, wicketd /// accepts an upload command. -#[derive(Debug, Parser)] +#[derive(Debug, Subcommand)] enum ShellCommand { /// Upload a TUF repository to wicketd. #[command(visible_alias = "upload")] UploadRepo(UploadArgs), + + /// Perform a rack update. + #[command(subcommand)] + RackUpdate(RackUpdateArgs), + /// Interact with rack setup configuration. #[command(subcommand)] Setup(SetupArgs), + /// Run checks prior to setting up the rack. #[command(subcommand)] Preflight(PreflightArgs), @@ -80,8 +131,8 @@ fn setup_log( let drain = slog_term::FullFormat::new(decorator).build().fuse(); let drain = match with_stderr { - WithStderr::Yes => { - let stderr_drain = stderr_env_drain("RUST_LOG"); + WithStderr::Yes { use_color } => { + let stderr_drain = stderr_env_drain("RUST_LOG", use_color); let drain = slog::Duplicate::new(drain, stderr_drain).fuse(); slog_async::Async::new(drain).build().fuse() } @@ -93,7 +144,7 @@ fn setup_log( #[derive(Copy, Clone, Debug)] enum WithStderr { - Yes, + Yes { use_color: bool }, No, } @@ -107,8 +158,17 @@ fn log_path() -> Result { } } -fn stderr_env_drain(env_var: &str) -> impl Drain { - let stderr_decorator = slog_term::TermDecorator::new().build(); +fn stderr_env_drain( + env_var: &str, + use_color: bool, +) -> impl Drain { + let mut builder = slog_term::TermDecorator::new(); + if use_color { + builder = builder.force_color(); + } else { + builder = builder.force_plain(); + } + let stderr_decorator = builder.build(); let stderr_drain = slog_term::FullFormat::new(stderr_decorator).build().fuse(); let mut builder = slog_envlogger::LogBuilder::new(stderr_drain); diff --git a/wicket/src/helpers.rs b/wicket/src/helpers.rs new file mode 100644 index 0000000000..564b7e9348 --- /dev/null +++ b/wicket/src/helpers.rs @@ -0,0 +1,70 @@ +// 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 functions. + +use std::env::VarError; + +use anyhow::{bail, Context}; +use wicketd_client::types::{UpdateSimulatedResult, UpdateTestError}; + +pub(crate) fn get_update_test_error( + env_var: &str, +) -> Result, anyhow::Error> { + // 30 seconds should always be enough to cause a timeout. (The default + // timeout for progenitor is 15 seconds, and in wicket we set an even + // shorter timeout.) + const DEFAULT_TEST_TIMEOUT_SECS: u64 = 30; + + let test_error = match std::env::var(env_var) { + Ok(v) if v == "fail" => Some(UpdateTestError::Fail), + Ok(v) if v == "timeout" => { + Some(UpdateTestError::Timeout { secs: DEFAULT_TEST_TIMEOUT_SECS }) + } + Ok(v) if v.starts_with("timeout:") => { + // Extended start_timeout syntax with a custom + // number of seconds. + let suffix = v.strip_prefix("timeout:").unwrap(); + match suffix.parse::() { + Ok(secs) => Some(UpdateTestError::Timeout { secs }), + Err(error) => { + return Err(error).with_context(|| { + format!( + "could not parse {env_var} \ + in the form `timeout:`: {v}" + ) + }); + } + } + } + Ok(value) => { + bail!("unrecognized value for {env_var}: {value}"); + } + Err(VarError::NotPresent) => None, + Err(VarError::NotUnicode(value)) => { + bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); + } + }; + Ok(test_error) +} + +pub(crate) fn get_update_simulated_result( + env_var: &str, +) -> Result, anyhow::Error> { + let result = match std::env::var(env_var) { + Ok(v) if v == "success" => Some(UpdateSimulatedResult::Success), + Ok(v) if v == "warning" => Some(UpdateSimulatedResult::Warning), + Ok(v) if v == "skipped" => Some(UpdateSimulatedResult::Skipped), + Ok(v) if v == "failure" => Some(UpdateSimulatedResult::Failure), + Ok(value) => { + bail!("unrecognized value for {env_var}: {value}"); + } + Err(VarError::NotPresent) => None, + Err(VarError::NotUnicode(value)) => { + bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); + } + }; + + Ok(result) +} diff --git a/wicket/src/lib.rs b/wicket/src/lib.rs index a16ef2a3e1..eef42ab706 100644 --- a/wicket/src/lib.rs +++ b/wicket/src/lib.rs @@ -9,9 +9,11 @@ use std::time::Duration; mod dispatch; mod events; +mod helpers; mod keymap; mod preflight; mod rack_setup; +mod rack_update; mod runner; mod state; mod ui; diff --git a/wicket/src/rack_update.rs b/wicket/src/rack_update.rs new file mode 100644 index 0000000000..a57c04806d --- /dev/null +++ b/wicket/src/rack_update.rs @@ -0,0 +1,377 @@ +// 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/. + +//! Command-line driven rack update. +//! +//! This is an alternative to using the Wicket UI to perform a rack update. + +use std::{ + collections::{BTreeMap, BTreeSet}, + net::SocketAddrV6, + time::Duration, +}; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::{Args, Subcommand}; +use slog::Logger; +use tokio::{sync::watch, task::JoinHandle}; +use update_engine::display::{GroupDisplay, LineDisplayStyles}; +use wicket_common::update_events::EventReport; +use wicketd_client::types::{StartUpdateOptions, StartUpdateParams}; + +use crate::{ + events::EventReportMap, + helpers::{get_update_simulated_result, get_update_test_error}, + state::{ComponentId, ParsableComponentId}, + wicketd::{create_wicketd_client, WICKETD_TIMEOUT}, + GlobalOpts, +}; + +#[derive(Debug, Subcommand)] +pub(crate) enum RackUpdateArgs { + /// Start a rack update. + Start(StartRackUpdateArgs), + /// Attach to a running update. + Attach(AttachArgs), +} + +impl RackUpdateArgs { + pub(crate) fn exec( + self, + log: Logger, + wicketd_addr: SocketAddrV6, + global_opts: GlobalOpts, + ) -> Result<()> { + let runtime = + tokio::runtime::Runtime::new().context("creating tokio runtime")?; + runtime.block_on(self.exec_impl(log, wicketd_addr, global_opts)) + } + + async fn exec_impl( + self, + log: Logger, + wicketd_addr: SocketAddrV6, + global_opts: GlobalOpts, + ) -> Result<()> { + match self { + RackUpdateArgs::Start(args) => { + args.exec(log, wicketd_addr, global_opts).await + } + RackUpdateArgs::Attach(args) => { + args.exec(log, wicketd_addr, global_opts).await + } + } + } +} + +#[derive(Debug, Args)] +pub(crate) struct StartRackUpdateArgs { + #[clap(flatten)] + component_ids: ComponentIdSelector, + + /// Force update the RoT even if the version is the same. + #[clap(long)] + force_update_rot: bool, + + /// Force update the SP even if the version is the same. + #[clap(long)] + force_update_sp: bool, + + /// Detach after starting the update. + /// + /// The `attach` command can be used to reattach to the running update. + #[clap(short, long)] + detach: bool, +} + +impl StartRackUpdateArgs { + async fn exec( + self, + log: Logger, + wicketd_addr: SocketAddrV6, + global_opts: GlobalOpts, + ) -> Result<()> { + // NOTE: This process is not idempotent. Doing so would require using a + // UUID, and storing that UUID in wicket. + let client = create_wicketd_client(&log, wicketd_addr, WICKETD_TIMEOUT); + + let update_ids = self.component_ids.to_component_ids()?; + let options = CreateStartUpdateOptions { + force_update_rot: self.force_update_rot, + force_update_sp: self.force_update_sp, + } + .to_start_update_options()?; + + let num_update_ids = update_ids.len(); + + let params = StartUpdateParams { + targets: update_ids.iter().copied().map(Into::into).collect(), + options, + }; + + slog::debug!(log, "Sending post_start_update"; "num_update_ids" => num_update_ids); + match client.post_start_update(¶ms).await { + Ok(_) => { + slog::info!(log, "Update started for {num_update_ids} targets"); + } + Err(error) => { + // Error responses can be printed out more clearly. + if let wicketd_client::Error::ErrorResponse(rv) = &error { + slog::error!( + log, + "Error response from wicketd: {}", + rv.message + ); + bail!("Received error from wicketd while starting update"); + } else { + bail!(error); + } + } + } + + if self.detach { + return Ok(()); + } + + // Now, attach to the update by printing out update logs. + do_attach_to_updates(log, client, update_ids, global_opts).await?; + + Ok(()) + } +} + +#[derive(Debug, Args)] +pub(crate) struct AttachArgs { + #[clap(flatten)] + component_ids: ComponentIdSelector, +} + +impl AttachArgs { + async fn exec( + self, + log: Logger, + wicketd_addr: SocketAddrV6, + global_opts: GlobalOpts, + ) -> Result<()> { + let client = create_wicketd_client(&log, wicketd_addr, WICKETD_TIMEOUT); + + let update_ids = self.component_ids.to_component_ids()?; + do_attach_to_updates(log, client, update_ids, global_opts).await + } +} + +async fn do_attach_to_updates( + log: Logger, + client: wicketd_client::Client, + update_ids: BTreeSet, + global_opts: GlobalOpts, +) -> Result<()> { + let mut display = GroupDisplay::new_with_display( + update_ids.iter().copied(), + std::io::stderr(), + ); + if global_opts.use_color() { + display.set_styles(LineDisplayStyles::colorized()); + } + + let (mut rx, handle) = start_fetch_reports_task(&log, client.clone()).await; + let mut status_timer = tokio::time::interval(Duration::from_secs(5)); + status_timer.tick().await; + + while !display.stats().is_terminal() { + tokio::select! { + res = rx.changed() => { + if res.is_err() { + // The sending end is closed, which means that the task + // created by start_fetch_reports_task died... this can + // happen either due to a panic or due to an error. + match handle.await { + Ok(Ok(())) => { + // The task exited normally, which means that the + // sending end was closed normally. This cannot + // happen. + bail!("fetch_reports task exited with Ok(()) \ + -- this should never happen here"); + } + Ok(Err(error)) => { + // The task exited with an error. + return Err(error).context("fetch_reports task errored out"); + } + Err(error) => { + // The task panicked. + return Err(anyhow!(error)).context("fetch_reports task panicked"); + } + } + } + + let event_reports = rx.borrow_and_update(); + // TODO: parallelize this computation? + for (id, event_report) in &*event_reports { + // If display.add_event_report errors out, it's for a report for a + // component we weren't interested in. Ignore it. + _ = display.add_event_report(&id, event_report.clone()); + } + + // Print out status for each component ID at the end -- do it here so + // that we also consider components for which we haven't seen status + // yet. + display.write_events()?; + } + _ = status_timer.tick() => { + display.write_stats("Status")?; + } + } + } + + // Show any remaining events. + display.write_events()?; + // And also show a summary. + display.write_stats("Summary")?; + + std::mem::drop(rx); + // This produces + handle + .await + .context("fetch_reports task panicked after rx dropped")? + .context("fetch_reports task errored out after rx dropped")?; + + if display.stats().has_failures() { + bail!("one or more failures occurred"); + } + + Ok(()) +} + +async fn start_fetch_reports_task( + log: &Logger, + client: wicketd_client::Client, +) -> (watch::Receiver>, JoinHandle>) +{ + // Since reports are always cumulative, we can use a watch receiver here + // rather than an mpsc receiver. If we start using incremental reports at + // some point this would need to be changed to be an mpsc receiver. + let (tx, rx) = watch::channel(BTreeMap::new()); + let log = log.new(slog::o!("task" => "fetch_reports")); + + let handle = tokio::spawn(async move { + loop { + let response = client.get_artifacts_and_event_reports().await?; + let reports = response.into_inner().event_reports; + let reports = parse_event_report_map(&log, reports); + if tx.send(reports).is_err() { + // The receiving end is closed, exit. + break; + } + tokio::time::sleep(Duration::from_secs(1)).await; + } + + Ok(()) + }); + (rx, handle) +} + +/// Command-line arguments for selecting component IDs. +#[derive(Debug, Args)] +#[clap(next_help_heading = "COMPONENT SELECTORS")] +struct ComponentIdSelector { + /// The sleds to operate on. + #[clap(long, value_delimiter = ',')] + sled: Vec, + + /// The switches to operate on. + #[clap(long, value_delimiter = ',')] + switch: Vec, + + /// The PSCs to operate on. + #[clap(long, value_delimiter = ',')] + psc: Vec, +} + +impl ComponentIdSelector { + /// Validates that all the sleds, switches, and PSCs are reasonable (though + /// they might not exist on the actual hardware), then return the set of + /// selected component IDs. + fn to_component_ids(&self) -> Result> { + let mut component_ids = BTreeSet::new(); + for sled in &self.sled { + component_ids.insert(ComponentId::new_sled(*sled)?); + } + for switch in &self.switch { + component_ids.insert(ComponentId::new_switch(*switch)?); + } + for psc in &self.psc { + component_ids.insert(ComponentId::new_psc(*psc)?); + } + if component_ids.is_empty() { + bail!("at least one component ID must be selected via --sled, --switch or --psc"); + } + + Ok(component_ids) + } +} + +pub(crate) struct CreateStartUpdateOptions { + pub(crate) force_update_rot: bool, + pub(crate) force_update_sp: bool, +} + +impl CreateStartUpdateOptions { + pub(crate) fn to_start_update_options(&self) -> Result { + let test_error = + get_update_test_error("WICKET_TEST_START_UPDATE_ERROR")?; + + // This is a debug environment variable used to + // add a test step. + let test_step_seconds = + std::env::var("WICKET_UPDATE_TEST_STEP_SECONDS").ok().map(|v| { + v.parse().expect( + "parsed WICKET_UPDATE_TEST_STEP_SECONDS \ + as a u64", + ) + }); + + let test_simulate_rot_result = get_update_simulated_result( + "WICKET_UPDATE_TEST_SIMULATE_ROT_RESULT", + )?; + let test_simulate_sp_result = get_update_simulated_result( + "WICKET_UPDATE_TEST_SIMULATE_SP_RESULT", + )?; + + Ok(StartUpdateOptions { + test_error, + test_step_seconds, + test_simulate_rot_result, + test_simulate_sp_result, + skip_rot_version_check: self.force_update_rot, + skip_sp_version_check: self.force_update_sp, + }) + } +} + +/// Converts an `EventReportMap` to a map by component ID. +pub(crate) fn parse_event_report_map( + log: &Logger, + reports: EventReportMap, +) -> BTreeMap { + let mut component_id_map = BTreeMap::new(); + for (sp_type, logs) in reports { + for (i, event_report) in logs { + let Ok(id) = ComponentId::try_from(ParsableComponentId { + sp_type: &sp_type, + i: &i, + }) else { + slog::warn!( + log, + "Invalid ComponentId in EventReportMap: {} {}", + &sp_type, + &i + ); + continue; + }; + component_id_map.insert(id, event_report); + } + } + + component_id_map +} diff --git a/wicket/src/runner.rs b/wicket/src/runner.rs index c37b16d5d9..573bbd254c 100644 --- a/wicket/src/runner.rs +++ b/wicket/src/runner.rs @@ -2,8 +2,6 @@ // 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/. -use anyhow::bail; -use anyhow::Context; use crossterm::event::Event as TermEvent; use crossterm::event::EventStream; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -17,7 +15,6 @@ use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use slog::Logger; use slog::{debug, error, info}; -use std::env::VarError; use std::io::{stdout, Stdout}; use std::net::SocketAddrV6; use std::time::Instant; @@ -27,11 +24,10 @@ use tokio::sync::mpsc::{ use tokio::time::{interval, Duration}; use wicketd_client::types::AbortUpdateOptions; use wicketd_client::types::ClearUpdateStateOptions; -use wicketd_client::types::StartUpdateOptions; -use wicketd_client::types::UpdateSimulatedResult; -use wicketd_client::types::UpdateTestError; use crate::events::EventReportMap; +use crate::helpers::get_update_test_error; +use crate::rack_update::CreateStartUpdateOptions; use crate::ui::Screen; use crate::wicketd::{self, WicketdHandle, WicketdManager}; use crate::{Action, Cmd, Event, KeyHandler, Recorder, State, TICK_INTERVAL}; @@ -180,43 +176,18 @@ impl RunnerCore { } Action::StartUpdate(component_id) => { if let Some(wicketd) = wicketd { - let test_error = get_update_test_error( - "WICKET_TEST_START_UPDATE_ERROR", - )?; - - // This is a debug environment variable used to - // add a test step. - let test_step_seconds = - std::env::var("WICKET_UPDATE_TEST_STEP_SECONDS") - .ok() - .map(|v| { - v.parse().expect( - "parsed WICKET_UPDATE_TEST_STEP_SECONDS \ - as a u64", - ) - }); - - let test_simulate_rot_result = get_update_simulated_result( - "WICKET_UPDATE_TEST_SIMULATE_ROT_RESULT", - )?; - let test_simulate_sp_result = get_update_simulated_result( - "WICKET_UPDATE_TEST_SIMULATE_SP_RESULT", - )?; - - let options = StartUpdateOptions { - test_error, - test_step_seconds, - test_simulate_rot_result, - test_simulate_sp_result, - skip_rot_version_check: self + let options = CreateStartUpdateOptions { + force_update_rot: self .state .force_update_state .force_update_rot, - skip_sp_version_check: self + force_update_sp: self .state .force_update_state .force_update_sp, - }; + } + .to_start_update_options()?; + wicketd.tx.blocking_send( wicketd::Request::StartUpdate { component_id, options }, )?; @@ -281,66 +252,6 @@ impl RunnerCore { } } -fn get_update_test_error( - env_var: &str, -) -> Result, anyhow::Error> { - // 30 seconds should always be enough to cause a timeout. (The default - // timeout for progenitor is 15 seconds, and in wicket we set an even - // shorter timeout.) - const DEFAULT_TEST_TIMEOUT_SECS: u64 = 30; - - let test_error = match std::env::var(env_var) { - Ok(v) if v == "fail" => Some(UpdateTestError::Fail), - Ok(v) if v == "timeout" => { - Some(UpdateTestError::Timeout { secs: DEFAULT_TEST_TIMEOUT_SECS }) - } - Ok(v) if v.starts_with("timeout:") => { - // Extended start_timeout syntax with a custom - // number of seconds. - let suffix = v.strip_prefix("timeout:").unwrap(); - match suffix.parse::() { - Ok(secs) => Some(UpdateTestError::Timeout { secs }), - Err(error) => { - return Err(error).with_context(|| { - format!( - "could not parse {env_var} \ - in the form `timeout:`: {v}" - ) - }); - } - } - } - Ok(value) => { - bail!("unrecognized value for {env_var}: {value}"); - } - Err(VarError::NotPresent) => None, - Err(VarError::NotUnicode(value)) => { - bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); - } - }; - Ok(test_error) -} - -fn get_update_simulated_result( - env_var: &str, -) -> Result, anyhow::Error> { - let result = match std::env::var(env_var) { - Ok(v) if v == "success" => Some(UpdateSimulatedResult::Success), - Ok(v) if v == "warning" => Some(UpdateSimulatedResult::Warning), - Ok(v) if v == "skipped" => Some(UpdateSimulatedResult::Skipped), - Ok(v) if v == "failure" => Some(UpdateSimulatedResult::Failure), - Ok(value) => { - bail!("unrecognized value for {env_var}: {value}"); - } - Err(VarError::NotPresent) => None, - Err(VarError::NotUnicode(value)) => { - bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); - } - }; - - Ok(result) -} - /// The `Runner` owns the main UI thread, and starts a tokio runtime /// for interaction with downstream services. pub struct Runner { diff --git a/wicket/src/state/inventory.rs b/wicket/src/state/inventory.rs index 3a561167b1..e5c1803695 100644 --- a/wicket/src/state/inventory.rs +++ b/wicket/src/state/inventory.rs @@ -4,7 +4,8 @@ //! Information about all top-level Oxide components (sleds, switches, PSCs) -use anyhow::anyhow; +use anyhow::{bail, Result}; +use omicron_common::api::internal::nexus::KnownArtifactKind; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -64,26 +65,13 @@ impl Inventory { }; // Validate and get a ComponentId - let (id, component) = match type_ { - SpType::Sled => { - if i > 31 { - return Err(anyhow!("Invalid sled slot: {}", i)); - } - (ComponentId::Sled(i as u8), Component::Sled(sp)) - } - SpType::Switch => { - if i > 1 { - return Err(anyhow!("Invalid switch slot: {}", i)); - } - (ComponentId::Switch(i as u8), Component::Switch(sp)) - } - SpType::Power => { - if i > 1 { - return Err(anyhow!("Invalid power shelf slot: {}", i)); - } - (ComponentId::Psc(i as u8), Component::Psc(sp)) - } + let id = ComponentId::from_sp_type_and_slot(type_, i as u8)?; + let component = match type_ { + SpType::Sled => Component::Sled(sp), + SpType::Switch => Component::Switch(sp), + SpType::Power => Component::Psc(sp), }; + new_inventory.inventory.insert(id, component); // TODO: Plumb through real power state @@ -204,6 +192,66 @@ pub enum ComponentId { } impl ComponentId { + /// The maximum possible sled ID. + pub const MAX_SLED_ID: u8 = 31; + + /// The maximum possible switch ID. + pub const MAX_SWITCH_ID: u8 = 1; + + /// The maximum possible power shelf ID. + /// + /// Currently shipping racks don't have PSC 1. + pub const MAX_PSC_ID: u8 = 0; + + pub fn new_sled(slot: u8) -> Result { + if slot > Self::MAX_SLED_ID { + bail!("Invalid sled slot: {}", slot); + } + Ok(Self::Sled(slot)) + } + + pub fn new_switch(slot: u8) -> Result { + if slot > Self::MAX_SWITCH_ID { + bail!("Invalid switch slot: {}", slot); + } + Ok(Self::Switch(slot)) + } + + pub fn new_psc(slot: u8) -> Result { + if slot > Self::MAX_PSC_ID { + bail!("Invalid power shelf slot: {}", slot); + } + Ok(Self::Psc(slot)) + } + + pub fn from_sp_type_and_slot(sp_type: SpType, slot: u8) -> Result { + match sp_type { + SpType::Sled => Self::new_sled(slot), + SpType::Switch => Self::new_switch(slot), + SpType::Power => Self::new_psc(slot), + } + } + + pub fn name(&self) -> String { + self.to_string() + } + + pub fn sp_known_artifact_kind(&self) -> KnownArtifactKind { + match self { + ComponentId::Sled(_) => KnownArtifactKind::GimletSp, + ComponentId::Switch(_) => KnownArtifactKind::SwitchSp, + ComponentId::Psc(_) => KnownArtifactKind::PscSp, + } + } + + pub fn rot_known_artifact_kind(&self) -> KnownArtifactKind { + match self { + ComponentId::Sled(_) => KnownArtifactKind::GimletRot, + ComponentId::Switch(_) => KnownArtifactKind::SwitchRot, + ComponentId::Psc(_) => KnownArtifactKind::PscRot, + } + } + pub fn to_string_uppercase(&self) -> String { let mut s = self.to_string(); s.make_ascii_uppercase(); diff --git a/wicket/src/state/update.rs b/wicket/src/state/update.rs index 1a0aafb9cf..8aade4c9cb 100644 --- a/wicket/src/state/update.rs +++ b/wicket/src/state/update.rs @@ -8,13 +8,14 @@ use wicket_common::update_events::{ UpdateStepId, }; +use crate::rack_update::parse_event_report_map; use crate::{events::EventReportMap, ui::defaults::style}; -use super::{ComponentId, ParsableComponentId, ALL_COMPONENT_IDS}; +use super::{ComponentId, ALL_COMPONENT_IDS}; use omicron_common::api::internal::nexus::KnownArtifactKind; use serde::{Deserialize, Serialize}; -use slog::{warn, Logger}; -use std::collections::{BTreeMap, HashSet}; +use slog::Logger; +use std::collections::BTreeMap; use std::fmt::Display; use wicketd_client::types::{ArtifactId, SemverVersion}; @@ -102,34 +103,18 @@ impl RackUpdateState { } } - let mut updated_component_ids = HashSet::new(); - - for (sp_type, logs) in reports { - for (i, log) in logs { - let Ok(id) = ComponentId::try_from(ParsableComponentId { - sp_type: &sp_type, - i: &i, - }) else { - warn!( - logger, - "Invalid ComponentId in EventReport: {} {}", - &sp_type, - &i - ); - continue; - }; - let item_state = self.items.get_mut(&id).unwrap(); - item_state.update(log); - updated_component_ids.insert(id); - } - } - - // Reset all component IDs that weren't updated. + let reports = parse_event_report_map(logger, reports); + // Reset all component IDs that aren't in the event report map. for (id, item) in &mut self.items { - if !updated_component_ids.contains(id) { + if !reports.contains_key(id) { item.reset(); } } + + for (id, report) in reports { + let item_state = self.items.get_mut(&id).unwrap(); + item_state.update(report); + } } } diff --git a/wicket/src/wicketd.rs b/wicket/src/wicketd.rs index 2411542429..ef630b9936 100644 --- a/wicket/src/wicketd.rs +++ b/wicket/src/wicketd.rs @@ -40,7 +40,7 @@ const WICKETD_POLL_INTERVAL: Duration = Duration::from_millis(500); // WICKETD_TIMEOUT used to be 1 second, but that might be too short (and in // particular might be responsible for // https://github.com/oxidecomputer/omicron/issues/3103). -const WICKETD_TIMEOUT: Duration = Duration::from_secs(5); +pub(crate) const WICKETD_TIMEOUT: Duration = Duration::from_secs(5); // Assume that these requests are periodic on the order of seconds or the // result of human interaction. In either case, this buffer should be plenty From d0fa787958508d602e6a5836241660877d455317 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 1 Nov 2023 14:01:26 -0700 Subject: [PATCH 3/3] Fix test Created using spr 1.3.4 --- Cargo.lock | 29 - Cargo.toml | 2 - update-engine/Cargo.toml | 5 - .../examples/update-engine-basic/display.rs | 123 +-- .../examples/update-engine-basic/main.rs | 194 ++-- update-engine/src/buffer.rs | 85 +- update-engine/src/display/group_display.rs | 467 --------- update-engine/src/display/line_display.rs | 134 --- .../src/display/line_display_shared.rs | 959 ------------------ update-engine/src/display/mod.rs | 21 - update-engine/src/errors.rs | 14 - update-engine/src/events.rs | 146 --- update-engine/src/lib.rs | 1 - wicket/Cargo.toml | 3 - wicket/src/dispatch.rs | 82 +- wicket/src/helpers.rs | 70 -- wicket/src/lib.rs | 2 - wicket/src/rack_update.rs | 377 ------- wicket/src/runner.rs | 105 +- wicket/src/state/inventory.rs | 88 +- wicket/src/state/update.rs | 39 +- wicket/src/wicketd.rs | 2 +- 22 files changed, 227 insertions(+), 2721 deletions(-) delete mode 100644 update-engine/src/display/group_display.rs delete mode 100644 update-engine/src/display/line_display.rs delete mode 100644 update-engine/src/display/line_display_shared.rs delete mode 100644 update-engine/src/display/mod.rs delete mode 100644 wicket/src/helpers.rs delete mode 100644 wicket/src/rack_update.rs diff --git a/Cargo.lock b/Cargo.lock index 87a3900104..2df98809a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3722,12 +3722,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "is_ci" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" - [[package]] name = "itertools" version = "0.10.5" @@ -8643,22 +8637,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" -[[package]] -name = "supports-color" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" -dependencies = [ - "is-terminal", - "is_ci", -] - -[[package]] -name = "swrite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3fece30b2dc06d65ecbca97b602db15bf75f932711d60cc604534f1f8b7a03" - [[package]] name = "syn" version = "1.0.109" @@ -9672,7 +9650,6 @@ dependencies = [ "camino", "camino-tempfile", "cancel-safe-futures", - "clap 4.4.3", "debug-ignore", "derive-where", "either", @@ -9689,11 +9666,8 @@ dependencies = [ "serde_json", "serde_with", "slog", - "supports-color", - "swrite", "tokio", "tokio-stream", - "unicode-width", "uuid", ] @@ -10053,7 +10027,6 @@ dependencies = [ "ciborium", "clap 4.4.3", "crossterm 0.27.0", - "debug-ignore", "futures", "hex", "humantime", @@ -10077,7 +10050,6 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-term", - "supports-color", "tempfile", "textwrap 0.16.0", "tokio", @@ -10087,7 +10059,6 @@ dependencies = [ "tui-tree-widget", "unicode-width", "update-engine", - "uuid", "wicket-common", "wicketd-client", "zeroize", diff --git a/Cargo.toml b/Cargo.toml index 0a4cdf368c..999fc680a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -342,8 +342,6 @@ static_assertions = "1.1.0" steno = "0.4.0" strum = { version = "0.25", features = [ "derive" ] } subprocess = "0.2.9" -supports-color = "2.1.0" -swrite = "0.1.0" libsw = { version = "3.3.0", features = ["tokio"] } syn = { version = "2.0" } tabled = "0.14" diff --git a/update-engine/Cargo.toml b/update-engine/Cargo.toml index 12e718e902..af988bf091 100644 --- a/update-engine/Cargo.toml +++ b/update-engine/Cargo.toml @@ -13,16 +13,13 @@ either.workspace = true futures.workspace = true indexmap.workspace = true linear-map.workspace = true -owo-colors.workspace = true petgraph.workspace = true serde.workspace = true serde_json.workspace = true serde_with.workspace = true schemars = { workspace = true, features = ["uuid1"] } slog.workspace = true -swrite.workspace = true tokio = { workspace = true, features = ["macros", "sync", "time", "rt-multi-thread"] } -unicode-width.workspace = true uuid.workspace = true omicron-workspace-hack.workspace = true @@ -31,10 +28,8 @@ buf-list.workspace = true bytes.workspace = true camino.workspace = true camino-tempfile.workspace = true -clap.workspace = true indicatif.workspace = true omicron-test-utils.workspace = true owo-colors.workspace = true -supports-color.workspace = true tokio = { workspace = true, features = ["io-util"] } tokio-stream.workspace = true diff --git a/update-engine/examples/update-engine-basic/display.rs b/update-engine/examples/update-engine-basic/display.rs index 122777211b..e6b80e3637 100644 --- a/update-engine/examples/update-engine-basic/display.rs +++ b/update-engine/examples/update-engine-basic/display.rs @@ -12,135 +12,28 @@ use indexmap::{map::Entry, IndexMap}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use owo_colors::OwoColorize; use tokio::{sync::mpsc, task::JoinHandle}; -use update_engine::{ - display::{GroupDisplay, LineDisplay, LineDisplayStyles}, - events::ProgressCounter, -}; +use update_engine::events::ProgressCounter; -use crate::{ - spec::{ - Event, EventBuffer, ExampleComponent, ExampleStepId, - ExampleStepMetadata, ProgressEvent, ProgressEventKind, StepEventKind, - StepInfoWithMetadata, StepOutcome, - }, - DisplayStyle, +use crate::spec::{ + Event, ExampleComponent, ExampleStepId, ExampleStepMetadata, ProgressEvent, + ProgressEventKind, StepEventKind, StepInfoWithMetadata, StepOutcome, }; /// An example that displays an event stream on the command line. pub(crate) fn make_displayer( log: &slog::Logger, - display_style: DisplayStyle, - prefix: Option, ) -> (JoinHandle>, mpsc::Sender) { let (sender, receiver) = mpsc::channel(512); let log = log.clone(); let join_handle = - match display_style { - DisplayStyle::ProgressBar => tokio::task::spawn(async move { - display_progress_bar(&log, receiver).await - }), - DisplayStyle::Line => tokio::task::spawn(async move { - display_line(&log, receiver, prefix).await - }), - DisplayStyle::Group => tokio::task::spawn(async move { - display_group(&log, receiver).await - }), - }; + tokio::task::spawn( + async move { display_messages(&log, receiver).await }, + ); (join_handle, sender) } -async fn display_line( - log: &slog::Logger, - mut receiver: mpsc::Receiver, - prefix: Option, -) -> Result<()> { - slog::info!(log, "setting up display"); - let mut buffer = EventBuffer::new(8); - let mut display = LineDisplay::new(std::io::stdout()); - // For now, always colorize. TODO: figure out whether colorization should be - // done based on always/auto/never etc. - if supports_color::on(supports_color::Stream::Stdout).is_some() { - display.set_styles(LineDisplayStyles::colorized()); - } - if let Some(prefix) = prefix { - display.set_prefix(prefix); - } - display.set_progress_interval(Duration::from_millis(50)); - while let Some(event) = receiver.recv().await { - buffer.add_event(event); - display.write_event_buffer(&buffer)?; - } - - Ok(()) -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] -enum GroupDisplayKey { - Example, - Other, -} - -async fn display_group( - log: &slog::Logger, - mut receiver: mpsc::Receiver, -) -> Result<()> { - slog::info!(log, "setting up display"); - - let mut display = GroupDisplay::new( - [ - (GroupDisplayKey::Example, "example"), - (GroupDisplayKey::Other, "other"), - ], - std::io::stdout(), - ); - // For now, always colorize. TODO: figure out whether colorization should be - // done based on always/auto/never etc. - if supports_color::on(supports_color::Stream::Stdout).is_some() { - display.set_styles(LineDisplayStyles::colorized()); - } - - display.set_progress_interval(Duration::from_millis(50)); - - let mut example_buffer = EventBuffer::default(); - let mut example_buffer_last_seen = None; - let mut other_buffer = EventBuffer::default(); - let mut other_buffer_last_seen = None; - - let mut interval = tokio::time::interval(Duration::from_secs(2)); - interval.tick().await; - - loop { - tokio::select! { - _ = interval.tick() => { - // Print out status lines every 2 seconds. - display.write_stats("Status")?; - } - event = receiver.recv() => { - let Some(event) = event else { break }; - example_buffer.add_event(event.clone()); - other_buffer.add_event(event); - - display.add_event_report( - &GroupDisplayKey::Example, - example_buffer.generate_report_since(&mut example_buffer_last_seen), - )?; - display.add_event_report( - &GroupDisplayKey::Other, - other_buffer.generate_report_since(&mut other_buffer_last_seen), - )?; - display.write_events()?; - } - } - } - - // Print status at the end. - display.write_stats("Summary")?; - - Ok(()) -} - -async fn display_progress_bar( +async fn display_messages( log: &slog::Logger, mut receiver: mpsc::Receiver, ) -> Result<()> { diff --git a/update-engine/examples/update-engine-basic/main.rs b/update-engine/examples/update-engine-basic/main.rs index 075e4ed253..260473edde 100644 --- a/update-engine/examples/update-engine-basic/main.rs +++ b/update-engine/examples/update-engine-basic/main.rs @@ -4,14 +4,13 @@ // Copyright 2023 Oxide Computer Company -use std::{io::IsTerminal, time::Duration}; +use std::time::Duration; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Context}; use buf_list::BufList; use bytes::Buf; use camino::Utf8PathBuf; use camino_tempfile::Utf8TempDir; -use clap::{Parser, ValueEnum}; use display::make_displayer; use omicron_test_utils::dev::test_setup_log; use spec::{ @@ -27,113 +26,63 @@ mod display; mod spec; #[tokio::main(worker_threads = 2)] -async fn main() -> Result<()> { - let app = App::parse(); - app.exec().await -} - -#[derive(Debug, Parser)] -struct App { - /// Display style to use. - #[clap(long, short = 's', default_value_t, value_enum)] - display_style: DisplayStyleOpt, - - /// Prefix to set on all log messages with display-style=line. - #[clap(long, short = 'p')] - prefix: Option, -} - -impl App { - async fn exec(self) -> Result<()> { - let logctx = test_setup_log("update_engine_basic_example"); - - let display_style = match self.display_style { - DisplayStyleOpt::ProgressBar => DisplayStyle::ProgressBar, - DisplayStyleOpt::Line => DisplayStyle::Line, - DisplayStyleOpt::Group => DisplayStyle::Group, - DisplayStyleOpt::Auto => { - if std::io::stdout().is_terminal() { - DisplayStyle::ProgressBar - } else { - DisplayStyle::Line - } - } - }; - - let context = ExampleContext::new(&logctx.log); - let (display_handle, sender) = - make_displayer(&logctx.log, display_style, self.prefix); - - let engine = UpdateEngine::new(&logctx.log, sender); - - // Download component 1. - let component_1 = engine.for_component(ExampleComponent::Component1); - let download_handle_1 = context.register_download_step( - &component_1, - "https://www.example.org".to_owned(), - 1_048_576, - ); - - // An example of a skipped step for component 1. - context.register_skipped_step(&component_1); - - // Create temporary directories for component 1. - let temp_dirs_handle_1 = - context.register_create_temp_dirs_step(&component_1, 2); - - // Write component 1 out to disk. - context.register_write_step( - &component_1, - download_handle_1, - temp_dirs_handle_1, - None, - ); - - // Download component 2. - let component_2 = engine.for_component(ExampleComponent::Component2); - let download_handle_2 = context.register_download_step( - &component_2, - "https://www.example.com".to_owned(), - 1_048_576 * 8, - ); - - // Create temporary directories for component 2. - let temp_dirs_handle_2 = - context.register_create_temp_dirs_step(&component_2, 3); - - // Now write component 2 out to disk. - context.register_write_step( - &component_2, - download_handle_2, - temp_dirs_handle_2, - Some(1), - ); - - _ = engine.execute().await; - - // Wait until all messages have been received by the displayer. - _ = display_handle.await; - - // Do not clean up the log file so people can inspect it. - - Ok(()) - } -} - -#[derive(Copy, Clone, Debug, Default, ValueEnum)] -enum DisplayStyleOpt { - ProgressBar, - Line, - Group, - #[default] - Auto, -} - -#[derive(Copy, Clone, Debug)] -enum DisplayStyle { - ProgressBar, - Line, - Group, +async fn main() { + let logctx = test_setup_log("update_engine_basic_example"); + + let context = ExampleContext::new(&logctx.log); + let (display_handle, sender) = make_displayer(&logctx.log); + + let engine = UpdateEngine::new(&logctx.log, sender); + + // Download component 1. + let component_1 = engine.for_component(ExampleComponent::Component1); + let download_handle_1 = context.register_download_step( + &component_1, + "https://www.example.org".to_owned(), + 1_048_576, + ); + + // An example of a skipped step for component 1. + context.register_skipped_step(&component_1); + + // Create temporary directories for component 1. + let temp_dirs_handle_1 = + context.register_create_temp_dirs_step(&component_1, 2); + + // Write component 1 out to disk. + context.register_write_step( + &component_1, + download_handle_1, + temp_dirs_handle_1, + None, + ); + + // Download component 2. + let component_2 = engine.for_component(ExampleComponent::Component2); + let download_handle_2 = context.register_download_step( + &component_2, + "https://www.example.com".to_owned(), + 1_048_576 * 8, + ); + + // Create temporary directories for component 2. + let temp_dirs_handle_2 = + context.register_create_temp_dirs_step(&component_2, 3); + + // Now write component 2 out to disk. + context.register_write_step( + &component_2, + download_handle_2, + temp_dirs_handle_2, + Some(1), + ); + + _ = engine.execute().await; + + // Wait until all messages have been received by the displayer. + _ = display_handle.await; + + // Do not clean up the log file so people can inspect it. } /// Context shared across steps. This forms the lifetime "'a" defined by the @@ -197,30 +146,9 @@ impl ExampleContext { ({num_bytes} bytes)", ); - // Try a second time, and this time go to 80%. - let mut buf_list = BufList::new(); - for i in 0..8 { - tokio::time::sleep(Duration::from_millis(100)).await; - cx.send_progress(StepProgress::with_current_and_total( - num_bytes * i / 10, - num_bytes, - ProgressUnits::BYTES, - serde_json::Value::Null, - )) - .await; - buf_list.push_chunk(&b"downloaded-data"[..]); - } - - // Now indicate a progress reset. - cx.send_progress(StepProgress::reset( - serde_json::Value::Null, - "Progress reset", - )) - .await; - - // Try again. + // Try a second time, and this time go all the way to 100%. let mut buf_list = BufList::new(); - for i in 0..8 { + for i in 0..10 { tokio::time::sleep(Duration::from_millis(100)).await; cx.send_progress(StepProgress::with_current_and_total( num_bytes * i / 10, diff --git a/update-engine/src/buffer.rs b/update-engine/src/buffer.rs index 8cdef8e02e..2426814444 100644 --- a/update-engine/src/buffer.rs +++ b/update-engine/src/buffer.rs @@ -106,23 +106,6 @@ impl EventBuffer { self.event_store.root_execution_id } - /// Returns information about terminal status for this buffer's root - /// execution ID, or None if the execution has not started or is currently - /// running. - pub fn root_terminal_info(&self) -> Option { - let Some(root_execution_id) = self.root_execution_id() else { - return None; - }; - - let summary = self.steps().summarize(); - summary - .get(&root_execution_id) - .expect("root execution ID must always be present in summary") - .execution_status - .terminal_info() - .cloned() - } - /// Returns information about each step, as currently tracked by the buffer, /// in order of when the events were first defined. pub fn steps(&self) -> EventBufferSteps<'_, S> { @@ -265,7 +248,6 @@ impl EventStore { &event, 0, None, - None, root_event_index, event.total_elapsed, ); @@ -273,29 +255,6 @@ impl EventStore { if new_execution.nest_level == 0 { self.root_execution_id = Some(new_execution.execution_id); } - // If there's a parent key, then what's the child index? - let parent_key_and_child_index = - if let Some(parent_key) = new_execution.parent_key { - match self.map.get_mut(&parent_key) { - Some(parent_data) => { - let child_index = parent_data.child_executions_seen; - parent_data.child_executions_seen += 1; - Some((parent_key, child_index)) - } - None => { - // This should never happen -- it indicates that the - // parent key was unknown. This can happen if we - // didn't receive an event regarding a parent - // execution being started. - // - // TODO: This should probably be an error that gets - // bubbled up to callers. - None - } - } - } else { - None - }; let total_steps = new_execution.steps_to_add.len(); for (new_step_key, new_step, sort_key) in new_execution.steps_to_add { @@ -304,7 +263,6 @@ impl EventStore { self.map.entry(new_step_key).or_insert_with(|| { EventBufferStepData::new( new_step, - parent_key_and_child_index, sort_key, new_execution.nest_level, total_steps, @@ -361,7 +319,6 @@ impl EventStore { &mut self, event: &StepEvent, nest_level: usize, - parent_key: Option, parent_sort_key: Option<&StepSortKey>, root_event_index: RootEventIndex, root_total_elapsed: Duration, @@ -390,7 +347,6 @@ impl EventStore { } new_execution = Some(NewExecutionAction { execution_id: event.execution_id, - parent_key, nest_level, steps_to_add, }); @@ -541,7 +497,6 @@ impl EventStore { let actions = self.recurse_for_step_event( nested_event, nest_level + 1, - Some(parent_key), parent_sort_key.as_ref(), root_event_index, root_total_elapsed, @@ -786,9 +741,6 @@ struct NewExecutionAction { // An execution ID corresponding to a new run, if seen. execution_id: ExecutionId, - // The parent key for this execution, if this is a nested step. - parent_key: Option, - // The nest level for this execution. nest_level: usize, @@ -850,18 +802,12 @@ impl<'buf, S: StepSpec> EventBufferSteps<'buf, S> { #[derive_where(Clone, Debug)] pub struct EventBufferStepData { step_info: StepInfo, - parent_key_and_child_index: Option<(StepKey, usize)>, - sort_key: StepSortKey, // XXX: nest_level and total_steps are common to each execution, but are - // stored separately here. These should likely move into - // EventBufferExecutionData. + // stored separately here. Should we store them in a separate map + // indexed by execution ID? nest_level: usize, total_steps: usize, - - // The number of child executions seen so far. - child_executions_seen: usize, - // Invariant: stored in order sorted by leaf event index. high_priority: Vec>, step_status: StepStatus, @@ -873,7 +819,6 @@ pub struct EventBufferStepData { impl EventBufferStepData { fn new( step_info: StepInfo, - parent_key_and_child_index: Option<(StepKey, usize)>, sort_key: StepSortKey, nest_level: usize, total_steps: usize, @@ -881,11 +826,9 @@ impl EventBufferStepData { ) -> Self { Self { step_info, - parent_key_and_child_index, sort_key, nest_level, total_steps, - child_executions_seen: 0, high_priority: Vec::new(), step_status: StepStatus::NotStarted, last_root_event_index: root_event_index, @@ -897,11 +840,6 @@ impl EventBufferStepData { &self.step_info } - #[inline] - pub fn parent_key_and_child_index(&self) -> Option<(StepKey, usize)> { - self.parent_key_and_child_index - } - #[inline] pub fn nest_level(&self) -> usize { self.nest_level @@ -912,11 +850,6 @@ impl EventBufferStepData { self.total_steps } - #[inline] - pub fn child_executions_seen(&self) -> usize { - self.child_executions_seen - } - #[inline] pub fn step_status(&self) -> &StepStatus { &self.step_status @@ -1529,20 +1462,6 @@ pub enum TerminalKind { Aborted, } -impl ExecutionStatus { - /// Returns the terminal status and the total amount of time elapsed, or - /// None if the execution has not reached a terminal state. - /// - /// The time elapsed might be None if the execution was interrupted and - /// completion information wasn't available. - pub fn terminal_info(&self) -> Option<&ExecutionTerminalInfo> { - match self { - Self::NotStarted | Self::Running { .. } => None, - Self::Terminal(info) => Some(info), - } - } -} - /// Keys for the event tree. #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] enum EventTreeNode { diff --git a/update-engine/src/display/group_display.rs b/update-engine/src/display/group_display.rs deleted file mode 100644 index a4e8418334..0000000000 --- a/update-engine/src/display/group_display.rs +++ /dev/null @@ -1,467 +0,0 @@ -// 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/. - -// Copyright 2023 Oxide Computer Company - -use std::{borrow::Borrow, collections::BTreeMap, fmt, time::Duration}; - -use owo_colors::OwoColorize; -use swrite::{swrite, SWrite}; -use unicode_width::UnicodeWidthStr; - -use crate::{ - errors::UnknownReportKey, events::EventReport, EventBuffer, - ExecutionTerminalInfo, StepSpec, TerminalKind, -}; - -use super::{ - line_display_shared::LineDisplayFormatter, LineDisplayShared, - LineDisplayStyles, HEADER_WIDTH, -}; - -/// A displayer that simultaneously manages and shows line-based output for -/// several event buffers. -/// -/// `K` is the key type for each element in the group. Its [`fmt::Display`] impl -/// is called to obtain the prefix, and `Eq + Ord` is used for keys. -#[derive(Debug)] -pub struct GroupDisplay { - // We don't need to add any buffering here because we already write data to - // the writer in a line-buffered fashion (see Self::write_events). - writer: W, - max_width: usize, - single_states: BTreeMap>, - formatter: LineDisplayFormatter, - stats: GroupDisplayStats, -} - -impl GroupDisplay { - /// Creates a new `GroupDisplay` with the provided report keys and - /// prefixes. - /// - /// The function passed in is expected to create a writer. - pub fn new( - keys_and_prefixes: impl IntoIterator, - writer: W, - ) -> Self - where - Str: Into, - { - // Right-align prefixes to their maximum width -- this helps keep the - // output organized. - let mut max_width = 0; - let keys_and_prefixes: Vec<_> = keys_and_prefixes - .into_iter() - .map(|(k, prefix)| { - let prefix = prefix.into(); - max_width = - max_width.max(UnicodeWidthStr::width(prefix.as_str())); - (k, prefix) - }) - .collect(); - let single_states: BTreeMap<_, _> = keys_and_prefixes - .into_iter() - .map(|(k, prefix)| (k, SingleState::new(prefix, max_width))) - .collect(); - - let not_started = single_states.len(); - Self { - writer, - max_width, - single_states, - formatter: LineDisplayFormatter::new(), - stats: GroupDisplayStats::new(not_started), - } - } - - /// Creates a new `GroupDisplay` with the provided report keys, using the - /// `Display` impl to obtain the respective prefixes. - pub fn new_with_display( - keys: impl IntoIterator, - writer: W, - ) -> Self - where - K: fmt::Display, - { - Self::new( - keys.into_iter().map(|k| { - let prefix = k.to_string(); - (k, prefix) - }), - writer, - ) - } - - /// Sets the styles for all future lines. - #[inline] - pub fn set_styles(&mut self, styles: LineDisplayStyles) { - self.formatter.set_styles(styles); - } - - /// Sets the amount of time before new progress events are shown. - #[inline] - pub fn set_progress_interval(&mut self, interval: Duration) { - self.formatter.set_progress_interval(interval); - } - - /// Returns true if this `GroupDisplay` is producing reports corresponding - /// to the given key. - pub fn contains_key(&self, key: &Q) -> bool - where - K: Borrow, - Q: Ord, - { - self.single_states.contains_key(key) - } - - /// Adds an event report to the display, keyed by the index, and updates - /// internal state. - /// - /// Returns `Ok(())` if the report was accepted because the key was - /// known to this `GroupDisplay`, and an error if it was not. - pub fn add_event_report( - &mut self, - key: &Q, - event_report: EventReport, - ) -> Result<(), UnknownReportKey> - where - K: Borrow, - Q: Ord, - { - if let Some(state) = self.single_states.get_mut(key) { - let result = state.add_event_report(event_report); - self.stats.apply_result(result); - Ok(()) - } else { - Err(UnknownReportKey {}) - } - } - - /// Writes a "Status" or "Summary" line to the writer with statistics. - pub fn write_stats(&mut self, header: &str) -> std::io::Result<()> { - // Add a prefix which is equal to the maximum width of the prefixes. - // [prefix 00:00:00] takes up self.max_width + 9 characters inside the - // brackets. - let total_width = self.max_width + 9; - let mut line = format!("[{:total_width$}] ", ""); - self.stats.format_line(&mut line, header, &self.formatter); - writeln!(self.writer, "{line}") - } - - /// Writes all pending events to the writer. - pub fn write_events(&mut self) -> std::io::Result<()> { - let mut lines = Vec::new(); - for state in self.single_states.values_mut() { - state.format_events(&self.formatter, &mut lines); - } - for line in lines { - writeln!(self.writer, "{line}")?; - } - Ok(()) - } - - /// Returns the current statistics for this `GroupDisplay`. - pub fn stats(&self) -> &GroupDisplayStats { - &self.stats - } -} - -#[derive(Clone, Copy, Debug)] -pub struct GroupDisplayStats { - /// The total number of reports. - pub total: usize, - - /// The number of reports that have not yet started. - pub not_started: usize, - - /// The number of reports that are currently running. - pub running: usize, - - /// The number of reports that indicate successful completion. - pub completed: usize, - - /// The number of reports that indicate failure. - pub failed: usize, - - /// The number of reports that indicate being aborted. - pub aborted: usize, - - /// The number of reports where we didn't receive a final state and it got - /// overwritten by another report. - /// - /// Overwritten reports are considered failures since we don't know what - /// happened. - pub overwritten: usize, -} - -impl GroupDisplayStats { - fn new(total: usize) -> Self { - Self { - total, - not_started: total, - completed: 0, - failed: 0, - aborted: 0, - overwritten: 0, - running: 0, - } - } - - /// Returns the number of terminal reports. - pub fn terminal_count(&self) -> usize { - self.completed + self.failed + self.aborted + self.overwritten - } - - /// Returns true if all reports have reached a terminal state. - pub fn is_terminal(&self) -> bool { - self.not_started == 0 && self.running == 0 - } - - /// Returns true if there are any failures. - pub fn has_failures(&self) -> bool { - self.failed > 0 || self.aborted > 0 || self.overwritten > 0 - } - - fn apply_result(&mut self, result: AddEventReportResult) { - // Process result.after first to avoid integer underflow. - match result.after { - SingleStateTag::NotStarted => self.not_started += 1, - SingleStateTag::Running => self.running += 1, - SingleStateTag::Terminal(TerminalKind::Completed) => { - self.completed += 1 - } - SingleStateTag::Terminal(TerminalKind::Failed) => self.failed += 1, - SingleStateTag::Terminal(TerminalKind::Aborted) => { - self.aborted += 1 - } - SingleStateTag::Overwritten => self.overwritten += 1, - } - - match result.before { - SingleStateTag::NotStarted => self.not_started -= 1, - SingleStateTag::Running => self.running -= 1, - SingleStateTag::Terminal(TerminalKind::Completed) => { - self.completed -= 1 - } - SingleStateTag::Terminal(TerminalKind::Failed) => self.failed -= 1, - SingleStateTag::Terminal(TerminalKind::Aborted) => { - self.aborted -= 1 - } - SingleStateTag::Overwritten => self.overwritten -= 1, - } - } - - fn format_line( - &self, - line: &mut String, - header: &str, - formatter: &LineDisplayFormatter, - ) { - let header_style = if self.has_failures() { - formatter.styles().error_style - } else { - formatter.styles().progress_style - }; - - swrite!(line, "{:>HEADER_WIDTH$} ", header.style(header_style)); - let terminal_count = self.terminal_count(); - swrite!( - line, - "{terminal_count}/{}: {} running, {} {}", - self.total, - self.running.style(formatter.styles().meta_style), - self.completed.style(formatter.styles().meta_style), - "completed".style(formatter.styles().progress_style), - ); - if self.failed > 0 { - swrite!( - line, - ", {} {}", - self.failed.style(formatter.styles().meta_style), - "failed".style(formatter.styles().error_style), - ); - } - if self.aborted > 0 { - swrite!( - line, - ", {} {}", - self.aborted.style(formatter.styles().meta_style), - "aborted".style(formatter.styles().error_style), - ); - } - if self.overwritten > 0 { - swrite!( - line, - ", {} {}", - self.overwritten.style(formatter.styles().meta_style), - "overwritten".style(formatter.styles().error_style), - ); - } - } -} - -#[derive(Debug)] -struct SingleState { - shared: LineDisplayShared, - kind: SingleStateKind, - prefix: String, -} - -impl SingleState { - fn new(prefix: String, max_width: usize) -> Self { - // Right-align the prefix to the maximum width. - let prefix = format!("{:>max_width$}", prefix); - Self { - shared: LineDisplayShared::new(), - kind: SingleStateKind::NotStarted { displayed: false }, - prefix, - } - } - - /// Adds an event report and updates the internal state. - fn add_event_report( - &mut self, - event_report: EventReport, - ) -> AddEventReportResult { - let before = match &self.kind { - SingleStateKind::NotStarted { .. } => { - self.kind = SingleStateKind::Running { - event_buffer: EventBuffer::new(8), - }; - SingleStateTag::NotStarted - } - SingleStateKind::Running { .. } => SingleStateTag::Running, - - SingleStateKind::Terminal { info, .. } => { - // Once we've reached a terminal state, we don't record any more - // events. - return AddEventReportResult::unchanged( - SingleStateTag::Terminal(info.kind), - ); - } - SingleStateKind::Overwritten { .. } => { - // This update has already completed -- assume that the event - // buffer is for a new update, which we don't show. - return AddEventReportResult::unchanged( - SingleStateTag::Overwritten, - ); - } - }; - - let SingleStateKind::Running { event_buffer } = &mut self.kind else { - unreachable!("other branches were handled above"); - }; - - if let Some(root_execution_id) = event_buffer.root_execution_id() { - if event_report.root_execution_id != Some(root_execution_id) { - // The report is for a different execution ID -- assume that - // this event is completed and mark our current execution as - // completed. - self.kind = SingleStateKind::Overwritten { displayed: false }; - return AddEventReportResult { - before, - after: SingleStateTag::Overwritten, - }; - } - } - - event_buffer.add_event_report(event_report); - let after = if let Some(info) = event_buffer.root_terminal_info() { - // Grab the event buffer to store it in the terminal state. - let event_buffer = - std::mem::replace(event_buffer, EventBuffer::new(0)); - let terminal_kind = info.kind; - self.kind = SingleStateKind::Terminal { - info, - pending_event_buffer: Some(event_buffer), - }; - SingleStateTag::Terminal(terminal_kind) - } else { - SingleStateTag::Running - }; - - AddEventReportResult { before, after } - } - - pub(super) fn format_events( - &mut self, - formatter: &LineDisplayFormatter, - out: &mut Vec, - ) { - let mut cx = self.shared.with_context(&self.prefix, formatter); - match &mut self.kind { - SingleStateKind::NotStarted { displayed } => { - if !*displayed { - let line = - cx.format_generic("Update not started, waiting..."); - out.push(line); - *displayed = true; - } - } - SingleStateKind::Running { event_buffer } => { - cx.format_event_buffer(event_buffer, out); - } - SingleStateKind::Terminal { info, pending_event_buffer } => { - // Are any remaining events left? This also sets pending_event_buffer - // to None after displaying remaining events. - if let Some(event_buffer) = pending_event_buffer.take() { - cx.format_event_buffer(&event_buffer, out); - // Also show a line to wrap up the terminal status. - let line = cx.format_terminal_info(info); - out.push(line); - } - - // Nothing to do, the terminal status was already printed above. - } - SingleStateKind::Overwritten { displayed } => { - if !*displayed { - let line = cx.format_generic( - "Update overwritten (a different update was started)\ - assuming failure", - ); - out.push(line); - *displayed = true; - } - } - } - } -} - -#[derive(Debug)] -enum SingleStateKind { - NotStarted { - displayed: bool, - }, - Running { - event_buffer: EventBuffer, - }, - Terminal { - info: ExecutionTerminalInfo, - // The event buffer is kept around so that we can display any remaining - // lines. - pending_event_buffer: Option>, - }, - Overwritten { - displayed: bool, - }, -} - -struct AddEventReportResult { - before: SingleStateTag, - after: SingleStateTag, -} - -impl AddEventReportResult { - fn unchanged(tag: SingleStateTag) -> Self { - Self { before: tag, after: tag } - } -} - -#[derive(Copy, Clone, Debug)] -enum SingleStateTag { - NotStarted, - Running, - Terminal(TerminalKind), - Overwritten, -} diff --git a/update-engine/src/display/line_display.rs b/update-engine/src/display/line_display.rs deleted file mode 100644 index 786e689e9e..0000000000 --- a/update-engine/src/display/line_display.rs +++ /dev/null @@ -1,134 +0,0 @@ -// 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/. - -// Copyright 2023 Oxide Computer Company - -use debug_ignore::DebugIgnore; -use derive_where::derive_where; -use owo_colors::Style; -use std::time::Duration; - -use crate::{EventBuffer, ExecutionTerminalInfo, StepSpec}; - -use super::{LineDisplayFormatter, LineDisplayShared}; - -/// A line-oriented display. -/// -/// This display produces output to the provided writer. -#[derive_where(Debug)] -pub struct LineDisplay { - writer: DebugIgnore, - shared: LineDisplayShared, - formatter: LineDisplayFormatter, - prefix: String, -} - -impl LineDisplay { - /// Creates a new LineDisplay. - pub fn new(writer: W) -> Self { - Self { - writer: DebugIgnore(writer), - shared: LineDisplayShared::new(), - formatter: LineDisplayFormatter::new(), - prefix: String::new(), - } - } - - /// Sets the prefix for all future lines. - #[inline] - pub fn set_prefix(&mut self, prefix: impl Into) { - self.prefix = prefix.into(); - } - - /// Sets the styles for all future lines. - #[inline] - pub fn set_styles(&mut self, styles: LineDisplayStyles) { - self.formatter.set_styles(styles); - } - - /// Sets the amount of time before the next progress event is shown. - #[inline] - pub fn set_progress_interval(&mut self, interval: Duration) { - self.formatter.set_progress_interval(interval); - } - - /// Writes an event buffer to the writer, incrementally. - /// - /// This is a stateful method that will only display events that have not - /// been displayed before. - pub fn write_event_buffer( - &mut self, - buffer: &EventBuffer, - ) -> std::io::Result<()> { - let mut lines = Vec::new(); - self.shared - .with_context(&self.prefix, &self.formatter) - .format_event_buffer(buffer, &mut lines); - for line in lines { - writeln!(self.writer, "{line}")?; - } - - Ok(()) - } - - /// Writes terminal information to the writer. - pub fn write_terminal_info( - &mut self, - info: &ExecutionTerminalInfo, - ) -> std::io::Result<()> { - let line = self - .shared - .with_context(&self.prefix, &self.formatter) - .format_terminal_info(info); - writeln!(self.writer, "{line}") - } - - /// Writes a generic line to the writer, with prefix attached if provided. - pub fn write_generic(&mut self, message: &str) -> std::io::Result<()> { - let line = self - .shared - .with_context(&self.prefix, &self.formatter) - .format_generic(message); - writeln!(self.writer, "{line}") - } -} - -/// Styles for [`LineDisplay`]. -/// -/// By default this isn't colorized, but it can be if so chosen. -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct LineDisplayStyles { - pub prefix_style: Style, - pub meta_style: Style, - pub step_name_style: Style, - pub progress_style: Style, - pub progress_message_style: Style, - pub warning_style: Style, - pub warning_message_style: Style, - pub error_style: Style, - pub error_message_style: Style, - pub skipped_style: Style, - pub retry_style: Style, -} - -impl LineDisplayStyles { - /// Returns a default set of colorized styles with ANSI colors. - pub fn colorized() -> Self { - let mut ret = Self::default(); - ret.prefix_style = Style::new().bold(); - ret.meta_style = Style::new().bold(); - ret.step_name_style = Style::new().cyan(); - ret.progress_style = Style::new().bold().green(); - ret.progress_message_style = Style::new().green(); - ret.warning_style = Style::new().bold().yellow(); - ret.warning_message_style = Style::new().yellow(); - ret.error_style = Style::new().bold().red(); - ret.error_message_style = Style::new().red(); - ret.skipped_style = Style::new().bold().yellow(); - ret.retry_style = Style::new().bold().yellow(); - - ret - } -} diff --git a/update-engine/src/display/line_display_shared.rs b/update-engine/src/display/line_display_shared.rs deleted file mode 100644 index 4f37f89d7d..0000000000 --- a/update-engine/src/display/line_display_shared.rs +++ /dev/null @@ -1,959 +0,0 @@ -// 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/. - -// Copyright 2023 Oxide Computer Company - -//! Types and code shared between `LineDisplay` and `GroupDisplay`. - -use std::{ - collections::{hash_map::Entry, HashMap}, - fmt::{self, Write as _}, - time::Duration, -}; - -use owo_colors::OwoColorize; -use swrite::{swrite, SWrite as _}; - -use crate::{ - events::{ - LowPriorityStepEventKind, ProgressCounter, ProgressEvent, StepEvent, - StepInfo, StepOutcome, - }, - AbortInfo, AbortReason, CompletionInfo, EventBuffer, EventBufferStepData, - ExecutionTerminalInfo, FailureInfo, FailureReason, NestedSpec, - RootEventIndex, StepKey, StepSpec, StepStatus, TerminalKind, -}; - -use super::LineDisplayStyles; - -// This is chosen to leave enough room for all possible headers: "Completed" at -// 9 characters is the longest. -pub(super) const HEADER_WIDTH: usize = 9; - -#[derive(Debug)] -pub(super) struct LineDisplayShared { - step_data: HashMap, -} - -impl LineDisplayShared { - pub(super) fn new() -> Self { - Self { step_data: HashMap::new() } - } - - pub(super) fn with_context<'a>( - &'a mut self, - prefix: &'a str, - formatter: &'a LineDisplayFormatter, - ) -> LineDisplaySharedContext<'_> { - LineDisplaySharedContext { shared: self, prefix, formatter } - } -} - -#[derive(Debug)] -pub(super) struct LineDisplaySharedContext<'a> { - shared: &'a mut LineDisplayShared, - prefix: &'a str, - formatter: &'a LineDisplayFormatter, -} - -impl<'a> LineDisplaySharedContext<'a> { - /// Produces a generic line from the prefix and message. - /// - /// This line does not have a trailing newline; adding one is the caller's - /// responsibility. - pub(super) fn format_generic(&self, message: &str) -> String { - let mut line = self.formatter.start_println(self.prefix); - line.push_str(message); - line - } - - /// Produces lines for this event buffer, and advances internal state. - /// - /// Returned lines do not have a trailing newline; adding them is the - /// caller's responsibility. - pub(super) fn format_event_buffer( - &mut self, - buffer: &EventBuffer, - out: &mut Vec, - ) { - let steps = buffer.steps(); - for (step_key, data) in steps.as_slice() { - self.format_step_and_update(buffer, *step_key, data, out); - } - } - - /// Produces lines corresponding to this step, and advances internal state. - /// - /// Returned lines do not have a trailing newline; adding them is the - /// caller's responsibility. - pub(super) fn format_step_and_update( - &mut self, - buffer: &EventBuffer, - step_key: StepKey, - data: &EventBufferStepData, - out: &mut Vec, - ) { - let ld_step_info = LineDisplayStepInfo { - step_info: data.step_info(), - parent_key_and_child_index: data.parent_key_and_child_index(), - total_steps: data.total_steps(), - }; - let nest_level = data.nest_level(); - let step_status = data.step_status(); - - match step_status { - StepStatus::NotStarted => {} - StepStatus::Running { progress_event, low_priority } => { - for event in low_priority { - self.format_low_priority_event( - event, - step_key, - ld_step_info, - nest_level, - out, - ); - } - - self.format_progress_event( - progress_event, - step_key, - data, - ld_step_info, - nest_level, - out, - ); - - self.insert_or_update_index( - step_key, - data.last_root_event_index(), - ); - } - StepStatus::Completed { info } => { - if let Some(sd) = self.shared.step_data.get(&step_key) { - if sd.last_root_event_index >= data.last_root_event_index() - { - // We've already displayed this event. - return; - } - } - - match info { - Some(info) => { - let mut line = self.formatter.start_line( - self.prefix, - Some(info.root_total_elapsed), - ); - self.formatter.add_completion_and_step_info( - &mut line, - NestLevel::Regular(nest_level), - ld_step_info, - info, - ); - out.push(line); - } - None => { - // This means that we don't know what happened to the step - // but it did complete. - let mut line = - self.formatter.start_line(self.prefix, None); - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Completed" - .style(self.formatter.styles.progress_style), - ); - self.formatter.add_step_info( - &mut line, - ld_step_info, - NestLevel::Regular(nest_level), - ); - swrite!( - line, - ": with {}", - "unknown outcome" - .style(self.formatter.styles.meta_style), - ); - out.push(line); - } - } - - self.insert_or_update_index( - step_key, - data.last_root_event_index(), - ); - } - StepStatus::Failed { reason } => { - if let Some(ld) = self.shared.step_data.get(&step_key) { - if ld.last_root_event_index >= data.last_root_event_index() - { - // We've already displayed this event. - return; - } - } - - match reason { - FailureReason::StepFailed(info) => { - let mut line = self.formatter.start_line( - self.prefix, - Some(info.root_total_elapsed), - ); - let nest_level = NestLevel::Regular(nest_level); - - // The prefix is used for "Caused by" lines below. Add - // the requisite amount of spacing here. - let mut caused_by_prefix = line.clone(); - swrite!(caused_by_prefix, "{:>HEADER_WIDTH$} ", ""); - nest_level.add_prefix(&mut caused_by_prefix); - - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Failed".style(self.formatter.styles.error_style) - ); - self.formatter.add_step_info( - &mut line, - ld_step_info, - nest_level, - ); - line.push_str(": "); - self.formatter.add_failure_info( - &mut line, - &caused_by_prefix, - info, - ); - out.push(line); - } - FailureReason::ParentFailed { parent_step } => { - let parent_step_info = buffer - .get(&parent_step) - .expect("parent step must exist"); - let mut line = - self.formatter.start_line(self.prefix, None); - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Failed".style(self.formatter.styles.error_style) - ); - self.formatter.add_step_info( - &mut line, - ld_step_info, - NestLevel::Regular(nest_level), - ); - swrite!( - line, - ": because parent step {} failed", - parent_step_info - .step_info() - .description - .style(self.formatter.styles.step_name_style) - ); - out.push(line); - } - } - - self.insert_or_update_index( - step_key, - data.last_root_event_index(), - ); - } - StepStatus::Aborted { reason, .. } => { - if let Some(ld) = self.shared.step_data.get(&step_key) { - if ld.last_root_event_index >= data.last_root_event_index() - { - // We've already displayed this event. - return; - } - } - - match reason { - AbortReason::StepAborted(info) => { - let mut line = self.formatter.start_line( - self.prefix, - Some(info.root_total_elapsed), - ); - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Aborted".style(self.formatter.styles.error_style) - ); - self.formatter.add_step_info( - &mut line, - ld_step_info, - NestLevel::Regular(nest_level), - ); - line.push_str(": "); - self.formatter.add_abort_info(&mut line, info); - out.push(line); - } - AbortReason::ParentAborted { parent_step } => { - let parent_step_info = buffer - .get(&parent_step) - .expect("parent step must exist"); - let mut line = - self.formatter.start_line(self.prefix, None); - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Aborted".style(self.formatter.styles.error_style) - ); - self.formatter.add_step_info( - &mut line, - ld_step_info, - NestLevel::Regular(nest_level), - ); - swrite!( - line, - ": because parent step {} aborted", - parent_step_info - .step_info() - .description - .style(self.formatter.styles.step_name_style) - ); - out.push(line); - } - } - - self.insert_or_update_index( - step_key, - data.last_root_event_index(), - ); - } - StepStatus::WillNotBeRun { .. } => { - // We don't print "will not be run". (TODO: maybe add an - // extended mode which does do so?) - } - } - } - - /// Formats this terminal information. - /// - /// This line does not have a trailing newline; adding one is the caller's - /// responsibility. - pub(super) fn format_terminal_info( - &self, - info: &ExecutionTerminalInfo, - ) -> String { - let mut line = - self.formatter.start_line(self.prefix, info.leaf_total_elapsed); - match info.kind { - TerminalKind::Completed => { - swrite!( - line, - "{:>HEADER_WIDTH$} Execution {}", - "Terminal".style(self.formatter.styles.progress_style), - "completed".style(self.formatter.styles.progress_style), - ); - } - TerminalKind::Failed => { - swrite!( - line, - "{:>HEADER_WIDTH$} Execution {}", - "Terminal".style(self.formatter.styles.error_style), - "failed".style(self.formatter.styles.error_style), - ); - } - TerminalKind::Aborted => { - swrite!( - line, - "{:>HEADER_WIDTH$} Execution {}", - "Terminal".style(self.formatter.styles.error_style), - "aborted".style(self.formatter.styles.error_style), - ); - } - } - line - } - - fn format_low_priority_event( - &mut self, - event: &StepEvent, - step_key: StepKey, - ld_step_info: LineDisplayStepInfo<'_>, - nest_level: usize, - out: &mut Vec, - ) { - if let Some(sd) = self.shared.step_data.get(&step_key) { - if sd.last_root_event_index >= RootEventIndex(event.event_index) { - // We've already displayed this event. - return; - } - } - - let Some(lowpri_event) = event.to_low_priority() else { - // Can't show anything for unknown events. - return; - }; - - let mut line = - self.formatter.start_line(self.prefix, Some(event.total_elapsed)); - - match lowpri_event.kind { - LowPriorityStepEventKind::ProgressReset { - attempt, - attempt_elapsed, - message, - .. - } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Reset".style(self.formatter.styles.warning_style) - ); - self.formatter.add_step_info( - &mut line, - ld_step_info, - NestLevel::Regular(nest_level), - ); - swrite!( - line, - ": after {:.2?}", - attempt_elapsed.style(self.formatter.styles.meta_style), - ); - if attempt > 1 { - swrite!( - line, - " (at attempt {})", - attempt.style(self.formatter.styles.meta_style), - ); - } - swrite!( - line, - " with message: {}", - message.style(self.formatter.styles.warning_message_style) - ); - } - LowPriorityStepEventKind::AttemptRetry { - next_attempt, - attempt_elapsed, - message, - .. - } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Retry".style(self.formatter.styles.warning_style) - ); - self.formatter.add_step_info( - &mut line, - ld_step_info, - NestLevel::Regular(nest_level), - ); - swrite!( - line, - ": after {:.2?}", - attempt_elapsed.style(self.formatter.styles.meta_style), - ); - swrite!( - line, - " (at attempt {})", - next_attempt - .saturating_sub(1) - .style(self.formatter.styles.meta_style), - ); - swrite!( - line, - " with message: {}", - message.style(self.formatter.styles.warning_message_style) - ); - } - } - - out.push(line); - } - - fn format_progress_event( - &mut self, - progress_event: &ProgressEvent, - step_key: StepKey, - data: &EventBufferStepData, - ld_step_info: LineDisplayStepInfo<'_>, - nest_level: usize, - out: &mut Vec, - ) { - let Some(leaf_step_elapsed) = progress_event.kind.leaf_step_elapsed() - else { - // Can't show anything for unknown events. - return; - }; - let leaf_attempt = progress_event - .kind - .leaf_attempt() - .expect("if leaf_step_elapsed is Some, leaf_attempt must be Some"); - let leaf_attempt_elapsed = progress_event - .kind - .leaf_attempt_elapsed() - .expect( - "if leaf_step_elapsed is Some, leaf_attempt_elapsed must be Some", - ); - - let sd = self - .shared - .step_data - .entry(step_key) - .or_insert_with(|| StepData::new(data.last_root_event_index())); - - let (is_first_event, should_display) = match sd.last_progress_event_at { - Some(last_progress_event_at) => { - // Don't show events with zero attempt elapsed time unless - // they're the first (the others will be shown as part of - // low-priority step events). - let should_display = if leaf_attempt > 1 { - leaf_attempt_elapsed > Duration::ZERO - } else { - true - }; - // Show further progress events only after the progress interval - // has elapsed. - let should_display = should_display - && leaf_step_elapsed - > last_progress_event_at - + self.formatter.progress_interval; - (false, should_display) - } - None => (true, true), - }; - - if should_display { - let mut line = self - .formatter - .start_line(self.prefix, Some(progress_event.total_elapsed)); - let nest_level = if is_first_event { - NestLevel::Regular(nest_level) - } else { - // Add an extra half-indent for non-first progress events. - NestLevel::ExtraHalf(nest_level) - }; - - let (before, after) = match progress_event.kind.progress_counter() { - Some(counter) => { - let progress_str = format_progress_counter(counter); - ( - format!( - "{:>HEADER_WIDTH$} ", - "Progress" - .style(self.formatter.styles.progress_style) - ), - format!( - "{progress_str} after {:.2?}", - leaf_attempt_elapsed - .style(self.formatter.styles.meta_style), - ), - ) - } - None => { - let before = format!( - "{:>HEADER_WIDTH$} ", - "Running".style(self.formatter.styles.progress_style), - ); - - // If the leaf attempt elapsed is non-zero, show it. - let after = if leaf_attempt_elapsed > Duration::ZERO { - format!( - "after {:.2?}", - leaf_attempt_elapsed - .style(self.formatter.styles.meta_style), - ) - } else { - String::new() - }; - - (before, after) - } - }; - - swrite!(line, "{}", before); - self.formatter.add_step_info(&mut line, ld_step_info, nest_level); - if !after.is_empty() { - swrite!(line, ": {}", after); - } - - out.push(line); - - sd.update_progress_event(leaf_step_elapsed); - } - } - - fn insert_or_update_index( - &mut self, - step_key: StepKey, - last_root_index: RootEventIndex, - ) { - match self.shared.step_data.entry(step_key) { - Entry::Occupied(mut entry) => { - entry.get_mut().update_last_root_event_index(last_root_index); - } - Entry::Vacant(entry) => { - entry.insert(StepData::new(last_root_index)); - } - } - } -} - -fn format_progress_counter(counter: &ProgressCounter) -> String { - match counter.total { - Some(total) => { - // Show a percentage value. Correct alignment requires converting to - // a string in the middle like this. - let percent = (counter.current as f64 / total as f64) * 100.0; - // <12.34> is 5 characters wide. - let percent_width = 5; - let counter_width = total.to_string().len(); - format!( - "{:>percent_width$.2}% ({:>counter_width$}/{} {})", - percent, counter.current, total, counter.units, - ) - } - None => format!("{} {}", counter.current, counter.units), - } -} - -/// State that tracks line display formatting. -/// -/// Each `LineDisplay` and `GroupDisplay` has one of these. -#[derive(Debug)] -pub(super) struct LineDisplayFormatter { - styles: LineDisplayStyles, - progress_interval: Duration, -} - -impl LineDisplayFormatter { - pub(super) fn new() -> Self { - Self { - styles: LineDisplayStyles::default(), - progress_interval: Duration::from_secs(1), - } - } - - #[inline] - pub(super) fn styles(&self) -> &LineDisplayStyles { - &self.styles - } - - #[inline] - pub(super) fn set_styles(&mut self, styles: LineDisplayStyles) { - self.styles = styles; - } - - #[inline] - pub(super) fn set_progress_interval(&mut self, interval: Duration) { - self.progress_interval = interval; - } - - // --- - // Internal helpers - // --- - - fn start_println(&self, prefix: &str) -> String { - if !prefix.is_empty() { - format!("[{}] ", prefix.style(self.styles.prefix_style)) - } else { - String::new() - } - } - - fn start_line( - &self, - prefix: &str, - total_elapsed: Option, - ) -> String { - let mut line = format!("[{}", prefix.style(self.styles.prefix_style)); - - if !prefix.is_empty() { - line.push(' '); - } - - // Show total elapsed time in an hh:mm:ss format. - if let Some(total_elapsed) = total_elapsed { - let total_elapsed = total_elapsed.as_secs(); - let hours = total_elapsed / 3600; - let minutes = (total_elapsed % 3600) / 60; - let seconds = total_elapsed % 60; - swrite!(&mut line, "{:02}:{:02}:{:02}", hours, minutes, seconds); - } else { - // Add 8 spaces to align with hh:mm:ss. - line.push_str(" "); - } - - line.push_str("] "); - - line - } - - fn add_step_info( - &self, - line: &mut String, - ld_step_info: LineDisplayStepInfo<'_>, - nest_level: NestLevel, - ) { - nest_level.add_prefix(line); - - match ld_step_info.parent_key_and_child_index { - Some((parent_key, child_index)) => { - // Print e.g. (6a . - swrite!( - line, - "({}{} ", - // Add 1 to the index to make it 1-based. - parent_key.index + 1, - AsLetters(child_index) - ); - } - None => { - swrite!(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(); - swrite!( - line, - "{:width$}/{:width$}) ", - step_index, - ld_step_info.total_steps, - width = step_index_width - ); - - swrite!( - line, - "{}", - ld_step_info - .step_info - .description - .style(self.styles.step_name_style) - ); - } - - pub(super) fn add_completion_and_step_info( - &self, - line: &mut String, - nest_level: NestLevel, - ld_step_info: LineDisplayStepInfo<'_>, - info: &CompletionInfo, - ) { - let mut meta = format!( - "after {:.2?}", - info.step_elapsed.style(self.styles.meta_style) - ); - if info.attempt > 1 { - swrite!( - meta, - " (at attempt {})", - info.attempt.style(self.styles.meta_style) - ); - } - - match &info.outcome { - StepOutcome::Success { message, .. } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Completed".style(self.styles.progress_style), - ); - self.add_step_info(line, ld_step_info, nest_level); - match message { - Some(message) => { - swrite!( - line, - ": {meta} with message: {}", - message.style(self.styles.progress_message_style) - ); - } - None => { - swrite!(line, ": {meta}"); - } - } - } - StepOutcome::Warning { message, .. } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Completed".style(self.styles.warning_style), - ); - self.add_step_info(line, ld_step_info, nest_level); - swrite!( - line, - ": {meta} with warning: {}", - message.style(self.styles.warning_message_style) - ); - } - StepOutcome::Skipped { message, .. } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Skipped".style(self.styles.skipped_style), - ); - self.add_step_info(line, ld_step_info, nest_level); - swrite!( - line, - ": {}", - message.style(self.styles.warning_message_style) - ); - } - }; - } - - pub(super) fn add_failure_info( - &self, - line: &mut String, - line_prefix: &str, - info: &FailureInfo, - ) { - let mut meta = format!( - "after {:.2?}", - info.step_elapsed.style(self.styles.meta_style) - ); - if info.total_attempts > 1 { - swrite!( - meta, - " (after {} attempts)", - info.total_attempts.style(self.styles.meta_style) - ); - } - - swrite!( - line, - "{meta}: {}", - info.message.style(self.styles.error_message_style) - ); - if !info.causes.is_empty() { - swrite!( - line, - "\n{line_prefix}{}", - " Caused by:".style(self.styles.meta_style) - ); - for cause in &info.causes { - swrite!(line, "\n{line_prefix} - {}", cause); - } - } - - // The last newline is added by the caller. - } - - pub(super) fn add_abort_info(&self, line: &mut String, info: &AbortInfo) { - let mut meta = format!( - "after {:.2?}", - info.step_elapsed.style(self.styles.meta_style) - ); - if info.attempt > 1 { - swrite!( - meta, - " (at attempt {})", - info.attempt.style(self.styles.meta_style) - ); - } - - swrite!(line, "{meta} with message \"{}\"", info.message,); - } -} - -#[derive(Clone, Copy, Debug)] -pub(super) struct LineDisplayStepInfo<'a> { - pub(super) step_info: &'a StepInfo, - pub(super) parent_key_and_child_index: Option<(StepKey, usize)>, - pub(super) total_steps: usize, -} - -/// Per-step stateful data tracked by the line displayer. -#[derive(Debug)] -struct StepData { - /// The last root event index that was displayed for this step. - /// - /// This is used to avoid displaying the same event twice. - last_root_event_index: RootEventIndex, - - /// The last `leaf_step_elapsed` at which a progress event was displayed for - /// this step. - last_progress_event_at: Option, -} - -impl StepData { - fn new(last_root_event_index: RootEventIndex) -> Self { - Self { last_root_event_index, last_progress_event_at: None } - } - - fn update_progress_event(&mut self, leaf_step_elapsed: Duration) { - self.last_progress_event_at = Some(leaf_step_elapsed); - } - - fn update_last_root_event_index( - &mut self, - root_event_index: RootEventIndex, - ) { - self.last_root_event_index = root_event_index; - } -} - -#[derive(Copy, Clone, Debug)] -pub(super) enum NestLevel { - /// Regular nest level. - Regular(usize), - - /// These many nest levels, except also add an extra half indent. - ExtraHalf(usize), -} - -impl NestLevel { - fn add_prefix(self, line: &mut String) { - match self { - NestLevel::Regular(0) => {} - NestLevel::Regular(nest_level) => { - line.push_str(&"....".repeat(nest_level)); - line.push_str(" "); - } - NestLevel::ExtraHalf(nest_level) => { - line.push_str(&"....".repeat(nest_level)); - line.push_str(".. "); - } - } - } -} - -/// A display impl that converts a 0-based index into a letter or a series of -/// letters. -/// -/// This is effectively a conversion to base 26. -struct AsLetters(usize); - -impl fmt::Display for AsLetters { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut index = self.0; - loop { - let letter = (b'a' + (index % 26) as u8) as char; - f.write_char(letter)?; - index /= 26; - if index == 0 { - break; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_progress_counter() { - let tests = vec![ - (ProgressCounter::new(5, 20, "units"), "25.00% ( 5/20 units)"), - (ProgressCounter::new(0, 20, "bytes"), " 0.00% ( 0/20 bytes)"), - (ProgressCounter::new(20, 20, "cubes"), "100.00% (20/20 cubes)"), - // NaN is a weird case that is a buggy update engine impl in practice - (ProgressCounter::new(0, 0, "units"), " NaN% (0/0 units)"), - (ProgressCounter::current(5, "units"), "5 units"), - ]; - for (input, output) in tests { - assert_eq!( - format_progress_counter(&input), - output, - "format matches for input: {:?}", - input - ); - } - } -} diff --git a/update-engine/src/display/mod.rs b/update-engine/src/display/mod.rs deleted file mode 100644 index c58a4535a0..0000000000 --- a/update-engine/src/display/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -// 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/. - -// Copyright 2023 Oxide Computer Company - -//! Displayers for the update engine. -//! -//! Currently implemented are: -//! -//! * [`LineDisplay`]: a line-oriented display suitable for the command line. -//! * [`GroupDisplay`]: manages state and shows the results of several -//! [`LineDisplay`]s at once. - -mod group_display; -mod line_display; -mod line_display_shared; - -pub use group_display::GroupDisplay; -pub use line_display::{LineDisplay, LineDisplayStyles}; -use line_display_shared::*; diff --git a/update-engine/src/errors.rs b/update-engine/src/errors.rs index 055460a447..f40ce096d3 100644 --- a/update-engine/src/errors.rs +++ b/update-engine/src/errors.rs @@ -185,17 +185,3 @@ pub enum ConvertGenericPathElement { Path(&'static str), ArrayIndex(&'static str, usize), } - -/// The -/// [`GroupDisplay::add_event_report`](crate::display::GroupDisplay::add_event_report) -/// method was called with an unknown key. -#[derive(Clone, Debug)] -pub struct UnknownReportKey {} - -impl fmt::Display for UnknownReportKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("unknown report key") - } -} - -impl error::Error for UnknownReportKey {} diff --git a/update-engine/src/events.rs b/update-engine/src/events.rs index 21eb05f0ee..3816157d0d 100644 --- a/update-engine/src/events.rs +++ b/update-engine/src/events.rs @@ -194,68 +194,6 @@ impl StepEvent { } } - /// Converts this event into the corresponding low-priority (leaf) step - /// event, recursing into nested events as necessary. - /// - /// Returns None if this is not a low-priority event. - pub fn to_low_priority(&self) -> Option { - let step_and_kind = match &self.kind { - StepEventKind::NoStepsDefined - | StepEventKind::ExecutionStarted { .. } - | StepEventKind::StepCompleted { .. } - | StepEventKind::ExecutionCompleted { .. } - | StepEventKind::ExecutionFailed { .. } - | StepEventKind::ExecutionAborted { .. } - | StepEventKind::Unknown => None, - StepEventKind::ProgressReset { - step, - attempt, - metadata, - step_elapsed, - attempt_elapsed, - message, - } => { - let step = step.clone().into_generic(); - let kind = LowPriorityStepEventKind::ProgressReset { - attempt: *attempt, - metadata: serde_json::to_value(metadata) - .unwrap_or_else(|_| serde_json::Value::Null), - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - message: message.clone(), - }; - Some((step, kind)) - } - StepEventKind::AttemptRetry { - step, - next_attempt, - step_elapsed, - attempt_elapsed, - message, - } => { - let step = step.clone().into_generic(); - let kind = LowPriorityStepEventKind::AttemptRetry { - next_attempt: *next_attempt, - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - message: message.clone(), - }; - Some((step, kind)) - } - StepEventKind::Nested { event, .. } => { - return event.to_low_priority(); - } - }; - - step_and_kind.map(|(step, kind)| LowPriorityStepEvent { - leaf_execution_id: self.execution_id, - leaf_event_index: self.event_index, - leaf_total_elapsed: self.total_elapsed, - step, - kind, - }) - } - /// Returns the execution ID for the leaf event, recursing into nested /// events if necessary. pub fn leaf_execution_id(&self) -> ExecutionId { @@ -882,69 +820,6 @@ pub enum StepEventPriority { High, } -/// A low-priority step event. -/// -/// Returned by [`StepEventKind::to_low_priority`]. -#[derive(Clone, Debug)] -pub struct LowPriorityStepEvent { - /// The leaf execution ID. - pub leaf_execution_id: ExecutionId, - - /// The leaf event index. - pub leaf_event_index: usize, - - /// The total time elapsed for the leaf event. - pub leaf_total_elapsed: Duration, - - /// Information about the leaf step. - pub step: StepInfoWithMetadata, - - /// The kind of low-priority event this is. - pub kind: LowPriorityStepEventKind, -} - -/// A kind of low-priority step event. -/// -/// Part of [`LowPriorityStepEvent::kind`]. -#[derive(Clone, Debug)] -pub enum LowPriorityStepEventKind { - /// Progress was reset along an attempt, and this attempt is going down a - /// different path. - ProgressReset { - /// The current attempt number. - attempt: usize, - - /// Progress-related metadata associated with this attempt. - metadata: serde_json::Value, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The amount of time this attempt has taken so far. - attempt_elapsed: Duration, - - /// A message assocaited with the reset. - message: Cow<'static, str>, - }, - - /// An attempt failed and this step is being retried. - AttemptRetry { - /// The attempt number for the next attempt. - next_attempt: usize, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The amount of time the previous attempt took. - attempt_elapsed: Duration, - - /// A message associated with the retry. - message: Cow<'static, str>, - }, -} - #[derive(Deserialize, Serialize, JsonSchema)] #[derive_where(Clone, Debug, Eq, PartialEq)] #[serde(bound = "", rename_all = "snake_case", tag = "kind")] @@ -1268,8 +1143,6 @@ impl ProgressEventKind { /// Returns `step_elapsed` for the leaf event, recursing into nested events /// as necessary. - /// - /// Returns None for unknown events. pub fn leaf_step_elapsed(&self) -> Option { match self { ProgressEventKind::WaitingForProgress { step_elapsed, .. } @@ -1283,25 +1156,6 @@ impl ProgressEventKind { } } - /// Returns `attempt_elapsed` for the leaf event, recursing into nested - /// events as necessary. - /// - /// Returns None for unknown events. - pub fn leaf_attempt_elapsed(&self) -> Option { - match self { - ProgressEventKind::WaitingForProgress { - attempt_elapsed, .. - } - | ProgressEventKind::Progress { attempt_elapsed, .. } => { - Some(*attempt_elapsed) - } - ProgressEventKind::Nested { event, .. } => { - event.kind.leaf_attempt_elapsed() - } - ProgressEventKind::Unknown => None, - } - } - /// Converts a generic version into self. /// /// This version can be used to convert a generic type into a more concrete diff --git a/update-engine/src/lib.rs b/update-engine/src/lib.rs index fea92d3b73..f753fa738a 100644 --- a/update-engine/src/lib.rs +++ b/update-engine/src/lib.rs @@ -57,7 +57,6 @@ mod buffer; mod context; -pub mod display; mod engine; pub mod errors; pub mod events; diff --git a/wicket/Cargo.toml b/wicket/Cargo.toml index 11f476d98c..5392e72e9f 100644 --- a/wicket/Cargo.toml +++ b/wicket/Cargo.toml @@ -13,7 +13,6 @@ camino.workspace = true ciborium.workspace = true clap.workspace = true crossterm.workspace = true -debug-ignore.workspace = true futures.workspace = true hex = { workspace = true, features = ["serde"] } humantime.workspace = true @@ -34,7 +33,6 @@ slog.workspace = true slog-async.workspace = true slog-envlogger.workspace = true slog-term.workspace = true -supports-color.workspace = true textwrap.workspace = true tokio = { workspace = true, features = ["full"] } tokio-util.workspace = true @@ -42,7 +40,6 @@ toml.workspace = true toml_edit.workspace = true tui-tree-widget = "0.13.0" unicode-width.workspace = true -uuid.workspace = true zeroize.workspace = true omicron-passwords.workspace = true diff --git a/wicket/src/dispatch.rs b/wicket/src/dispatch.rs index 8784a0cb22..e8191f59cb 100644 --- a/wicket/src/dispatch.rs +++ b/wicket/src/dispatch.rs @@ -8,13 +8,12 @@ use std::net::{Ipv6Addr, SocketAddrV6}; use anyhow::{bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; -use clap::{Args, ColorChoice, Parser, Subcommand}; +use clap::Parser; use omicron_common::{address::WICKETD_PORT, FileKv}; use slog::Drain; use crate::{ - preflight::PreflightArgs, rack_setup::SetupArgs, - rack_update::RackUpdateArgs, upload::UploadArgs, Runner, + preflight::PreflightArgs, rack_setup::SetupArgs, upload::UploadArgs, Runner, }; pub fn exec() -> Result<()> { @@ -28,22 +27,14 @@ pub fn exec() -> Result<()> { format!("could not parse shell arguments from input {ssh_args}") })?; + let log = setup_log(&log_path()?, WithStderr::Yes)?; // parse_from uses the the first argument as the command name. Insert "wicket" as // the command name. - let app = ShellApp::parse_from( + let args = ShellCommand::parse_from( std::iter::once("wicket".to_owned()).chain(args), ); - - let log = setup_log( - &log_path()?, - WithStderr::Yes { use_color: app.global_opts.use_color() }, - )?; - - match app.command { + match args { ShellCommand::UploadRepo(args) => args.exec(log, wicketd_addr), - ShellCommand::RackUpdate(args) => { - args.exec(log, wicketd_addr, app.global_opts) - } ShellCommand::Setup(args) => args.exec(log, wicketd_addr), ShellCommand::Preflight(args) => args.exec(log, wicketd_addr), } @@ -55,62 +46,20 @@ pub fn exec() -> Result<()> { } } -/// An app that represents wicket started with arguments over ssh. -#[derive(Debug, Parser)] -struct ShellApp { - /// Global options. - #[clap(flatten)] - global_opts: GlobalOpts, - - /// The command to run. - #[clap(subcommand)] - command: ShellCommand, -} - -#[derive(Debug, Args)] -#[clap(next_help_heading = "Global options")] -pub(crate) struct GlobalOpts { - /// Color output (auto, always, never) - /// - /// This may not work everywhere at the moment. - #[clap(long, value_enum, global = true, default_value_t)] - pub(crate) color: ColorChoice, -} - -impl GlobalOpts { - /// Returns true if color should be used on standard error. - pub(crate) fn use_color(&self) -> bool { - match self.color { - ColorChoice::Auto => { - supports_color::on_cached(supports_color::Stream::Stderr) - .is_some() - } - ColorChoice::Always => true, - ColorChoice::Never => false, - } - } -} - /// Arguments passed to wicket. /// /// Wicket is designed to be used as a captive shell, set up via sshd /// ForceCommand. If no arguments are specified, wicket behaves like a TUI. /// However, if arguments are specified via SSH_ORIGINAL_COMMAND, wicketd /// accepts an upload command. -#[derive(Debug, Subcommand)] +#[derive(Debug, Parser)] enum ShellCommand { /// Upload a TUF repository to wicketd. #[command(visible_alias = "upload")] UploadRepo(UploadArgs), - - /// Perform a rack update. - #[command(subcommand)] - RackUpdate(RackUpdateArgs), - /// Interact with rack setup configuration. #[command(subcommand)] Setup(SetupArgs), - /// Run checks prior to setting up the rack. #[command(subcommand)] Preflight(PreflightArgs), @@ -131,8 +80,8 @@ fn setup_log( let drain = slog_term::FullFormat::new(decorator).build().fuse(); let drain = match with_stderr { - WithStderr::Yes { use_color } => { - let stderr_drain = stderr_env_drain("RUST_LOG", use_color); + WithStderr::Yes => { + let stderr_drain = stderr_env_drain("RUST_LOG"); let drain = slog::Duplicate::new(drain, stderr_drain).fuse(); slog_async::Async::new(drain).build().fuse() } @@ -144,7 +93,7 @@ fn setup_log( #[derive(Copy, Clone, Debug)] enum WithStderr { - Yes { use_color: bool }, + Yes, No, } @@ -158,17 +107,8 @@ fn log_path() -> Result { } } -fn stderr_env_drain( - env_var: &str, - use_color: bool, -) -> impl Drain { - let mut builder = slog_term::TermDecorator::new(); - if use_color { - builder = builder.force_color(); - } else { - builder = builder.force_plain(); - } - let stderr_decorator = builder.build(); +fn stderr_env_drain(env_var: &str) -> impl Drain { + let stderr_decorator = slog_term::TermDecorator::new().build(); let stderr_drain = slog_term::FullFormat::new(stderr_decorator).build().fuse(); let mut builder = slog_envlogger::LogBuilder::new(stderr_drain); diff --git a/wicket/src/helpers.rs b/wicket/src/helpers.rs deleted file mode 100644 index 564b7e9348..0000000000 --- a/wicket/src/helpers.rs +++ /dev/null @@ -1,70 +0,0 @@ -// 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 functions. - -use std::env::VarError; - -use anyhow::{bail, Context}; -use wicketd_client::types::{UpdateSimulatedResult, UpdateTestError}; - -pub(crate) fn get_update_test_error( - env_var: &str, -) -> Result, anyhow::Error> { - // 30 seconds should always be enough to cause a timeout. (The default - // timeout for progenitor is 15 seconds, and in wicket we set an even - // shorter timeout.) - const DEFAULT_TEST_TIMEOUT_SECS: u64 = 30; - - let test_error = match std::env::var(env_var) { - Ok(v) if v == "fail" => Some(UpdateTestError::Fail), - Ok(v) if v == "timeout" => { - Some(UpdateTestError::Timeout { secs: DEFAULT_TEST_TIMEOUT_SECS }) - } - Ok(v) if v.starts_with("timeout:") => { - // Extended start_timeout syntax with a custom - // number of seconds. - let suffix = v.strip_prefix("timeout:").unwrap(); - match suffix.parse::() { - Ok(secs) => Some(UpdateTestError::Timeout { secs }), - Err(error) => { - return Err(error).with_context(|| { - format!( - "could not parse {env_var} \ - in the form `timeout:`: {v}" - ) - }); - } - } - } - Ok(value) => { - bail!("unrecognized value for {env_var}: {value}"); - } - Err(VarError::NotPresent) => None, - Err(VarError::NotUnicode(value)) => { - bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); - } - }; - Ok(test_error) -} - -pub(crate) fn get_update_simulated_result( - env_var: &str, -) -> Result, anyhow::Error> { - let result = match std::env::var(env_var) { - Ok(v) if v == "success" => Some(UpdateSimulatedResult::Success), - Ok(v) if v == "warning" => Some(UpdateSimulatedResult::Warning), - Ok(v) if v == "skipped" => Some(UpdateSimulatedResult::Skipped), - Ok(v) if v == "failure" => Some(UpdateSimulatedResult::Failure), - Ok(value) => { - bail!("unrecognized value for {env_var}: {value}"); - } - Err(VarError::NotPresent) => None, - Err(VarError::NotUnicode(value)) => { - bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); - } - }; - - Ok(result) -} diff --git a/wicket/src/lib.rs b/wicket/src/lib.rs index eef42ab706..a16ef2a3e1 100644 --- a/wicket/src/lib.rs +++ b/wicket/src/lib.rs @@ -9,11 +9,9 @@ use std::time::Duration; mod dispatch; mod events; -mod helpers; mod keymap; mod preflight; mod rack_setup; -mod rack_update; mod runner; mod state; mod ui; diff --git a/wicket/src/rack_update.rs b/wicket/src/rack_update.rs deleted file mode 100644 index a57c04806d..0000000000 --- a/wicket/src/rack_update.rs +++ /dev/null @@ -1,377 +0,0 @@ -// 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/. - -//! Command-line driven rack update. -//! -//! This is an alternative to using the Wicket UI to perform a rack update. - -use std::{ - collections::{BTreeMap, BTreeSet}, - net::SocketAddrV6, - time::Duration, -}; - -use anyhow::{anyhow, bail, Context, Result}; -use clap::{Args, Subcommand}; -use slog::Logger; -use tokio::{sync::watch, task::JoinHandle}; -use update_engine::display::{GroupDisplay, LineDisplayStyles}; -use wicket_common::update_events::EventReport; -use wicketd_client::types::{StartUpdateOptions, StartUpdateParams}; - -use crate::{ - events::EventReportMap, - helpers::{get_update_simulated_result, get_update_test_error}, - state::{ComponentId, ParsableComponentId}, - wicketd::{create_wicketd_client, WICKETD_TIMEOUT}, - GlobalOpts, -}; - -#[derive(Debug, Subcommand)] -pub(crate) enum RackUpdateArgs { - /// Start a rack update. - Start(StartRackUpdateArgs), - /// Attach to a running update. - Attach(AttachArgs), -} - -impl RackUpdateArgs { - pub(crate) fn exec( - self, - log: Logger, - wicketd_addr: SocketAddrV6, - global_opts: GlobalOpts, - ) -> Result<()> { - let runtime = - tokio::runtime::Runtime::new().context("creating tokio runtime")?; - runtime.block_on(self.exec_impl(log, wicketd_addr, global_opts)) - } - - async fn exec_impl( - self, - log: Logger, - wicketd_addr: SocketAddrV6, - global_opts: GlobalOpts, - ) -> Result<()> { - match self { - RackUpdateArgs::Start(args) => { - args.exec(log, wicketd_addr, global_opts).await - } - RackUpdateArgs::Attach(args) => { - args.exec(log, wicketd_addr, global_opts).await - } - } - } -} - -#[derive(Debug, Args)] -pub(crate) struct StartRackUpdateArgs { - #[clap(flatten)] - component_ids: ComponentIdSelector, - - /// Force update the RoT even if the version is the same. - #[clap(long)] - force_update_rot: bool, - - /// Force update the SP even if the version is the same. - #[clap(long)] - force_update_sp: bool, - - /// Detach after starting the update. - /// - /// The `attach` command can be used to reattach to the running update. - #[clap(short, long)] - detach: bool, -} - -impl StartRackUpdateArgs { - async fn exec( - self, - log: Logger, - wicketd_addr: SocketAddrV6, - global_opts: GlobalOpts, - ) -> Result<()> { - // NOTE: This process is not idempotent. Doing so would require using a - // UUID, and storing that UUID in wicket. - let client = create_wicketd_client(&log, wicketd_addr, WICKETD_TIMEOUT); - - let update_ids = self.component_ids.to_component_ids()?; - let options = CreateStartUpdateOptions { - force_update_rot: self.force_update_rot, - force_update_sp: self.force_update_sp, - } - .to_start_update_options()?; - - let num_update_ids = update_ids.len(); - - let params = StartUpdateParams { - targets: update_ids.iter().copied().map(Into::into).collect(), - options, - }; - - slog::debug!(log, "Sending post_start_update"; "num_update_ids" => num_update_ids); - match client.post_start_update(¶ms).await { - Ok(_) => { - slog::info!(log, "Update started for {num_update_ids} targets"); - } - Err(error) => { - // Error responses can be printed out more clearly. - if let wicketd_client::Error::ErrorResponse(rv) = &error { - slog::error!( - log, - "Error response from wicketd: {}", - rv.message - ); - bail!("Received error from wicketd while starting update"); - } else { - bail!(error); - } - } - } - - if self.detach { - return Ok(()); - } - - // Now, attach to the update by printing out update logs. - do_attach_to_updates(log, client, update_ids, global_opts).await?; - - Ok(()) - } -} - -#[derive(Debug, Args)] -pub(crate) struct AttachArgs { - #[clap(flatten)] - component_ids: ComponentIdSelector, -} - -impl AttachArgs { - async fn exec( - self, - log: Logger, - wicketd_addr: SocketAddrV6, - global_opts: GlobalOpts, - ) -> Result<()> { - let client = create_wicketd_client(&log, wicketd_addr, WICKETD_TIMEOUT); - - let update_ids = self.component_ids.to_component_ids()?; - do_attach_to_updates(log, client, update_ids, global_opts).await - } -} - -async fn do_attach_to_updates( - log: Logger, - client: wicketd_client::Client, - update_ids: BTreeSet, - global_opts: GlobalOpts, -) -> Result<()> { - let mut display = GroupDisplay::new_with_display( - update_ids.iter().copied(), - std::io::stderr(), - ); - if global_opts.use_color() { - display.set_styles(LineDisplayStyles::colorized()); - } - - let (mut rx, handle) = start_fetch_reports_task(&log, client.clone()).await; - let mut status_timer = tokio::time::interval(Duration::from_secs(5)); - status_timer.tick().await; - - while !display.stats().is_terminal() { - tokio::select! { - res = rx.changed() => { - if res.is_err() { - // The sending end is closed, which means that the task - // created by start_fetch_reports_task died... this can - // happen either due to a panic or due to an error. - match handle.await { - Ok(Ok(())) => { - // The task exited normally, which means that the - // sending end was closed normally. This cannot - // happen. - bail!("fetch_reports task exited with Ok(()) \ - -- this should never happen here"); - } - Ok(Err(error)) => { - // The task exited with an error. - return Err(error).context("fetch_reports task errored out"); - } - Err(error) => { - // The task panicked. - return Err(anyhow!(error)).context("fetch_reports task panicked"); - } - } - } - - let event_reports = rx.borrow_and_update(); - // TODO: parallelize this computation? - for (id, event_report) in &*event_reports { - // If display.add_event_report errors out, it's for a report for a - // component we weren't interested in. Ignore it. - _ = display.add_event_report(&id, event_report.clone()); - } - - // Print out status for each component ID at the end -- do it here so - // that we also consider components for which we haven't seen status - // yet. - display.write_events()?; - } - _ = status_timer.tick() => { - display.write_stats("Status")?; - } - } - } - - // Show any remaining events. - display.write_events()?; - // And also show a summary. - display.write_stats("Summary")?; - - std::mem::drop(rx); - // This produces - handle - .await - .context("fetch_reports task panicked after rx dropped")? - .context("fetch_reports task errored out after rx dropped")?; - - if display.stats().has_failures() { - bail!("one or more failures occurred"); - } - - Ok(()) -} - -async fn start_fetch_reports_task( - log: &Logger, - client: wicketd_client::Client, -) -> (watch::Receiver>, JoinHandle>) -{ - // Since reports are always cumulative, we can use a watch receiver here - // rather than an mpsc receiver. If we start using incremental reports at - // some point this would need to be changed to be an mpsc receiver. - let (tx, rx) = watch::channel(BTreeMap::new()); - let log = log.new(slog::o!("task" => "fetch_reports")); - - let handle = tokio::spawn(async move { - loop { - let response = client.get_artifacts_and_event_reports().await?; - let reports = response.into_inner().event_reports; - let reports = parse_event_report_map(&log, reports); - if tx.send(reports).is_err() { - // The receiving end is closed, exit. - break; - } - tokio::time::sleep(Duration::from_secs(1)).await; - } - - Ok(()) - }); - (rx, handle) -} - -/// Command-line arguments for selecting component IDs. -#[derive(Debug, Args)] -#[clap(next_help_heading = "COMPONENT SELECTORS")] -struct ComponentIdSelector { - /// The sleds to operate on. - #[clap(long, value_delimiter = ',')] - sled: Vec, - - /// The switches to operate on. - #[clap(long, value_delimiter = ',')] - switch: Vec, - - /// The PSCs to operate on. - #[clap(long, value_delimiter = ',')] - psc: Vec, -} - -impl ComponentIdSelector { - /// Validates that all the sleds, switches, and PSCs are reasonable (though - /// they might not exist on the actual hardware), then return the set of - /// selected component IDs. - fn to_component_ids(&self) -> Result> { - let mut component_ids = BTreeSet::new(); - for sled in &self.sled { - component_ids.insert(ComponentId::new_sled(*sled)?); - } - for switch in &self.switch { - component_ids.insert(ComponentId::new_switch(*switch)?); - } - for psc in &self.psc { - component_ids.insert(ComponentId::new_psc(*psc)?); - } - if component_ids.is_empty() { - bail!("at least one component ID must be selected via --sled, --switch or --psc"); - } - - Ok(component_ids) - } -} - -pub(crate) struct CreateStartUpdateOptions { - pub(crate) force_update_rot: bool, - pub(crate) force_update_sp: bool, -} - -impl CreateStartUpdateOptions { - pub(crate) fn to_start_update_options(&self) -> Result { - let test_error = - get_update_test_error("WICKET_TEST_START_UPDATE_ERROR")?; - - // This is a debug environment variable used to - // add a test step. - let test_step_seconds = - std::env::var("WICKET_UPDATE_TEST_STEP_SECONDS").ok().map(|v| { - v.parse().expect( - "parsed WICKET_UPDATE_TEST_STEP_SECONDS \ - as a u64", - ) - }); - - let test_simulate_rot_result = get_update_simulated_result( - "WICKET_UPDATE_TEST_SIMULATE_ROT_RESULT", - )?; - let test_simulate_sp_result = get_update_simulated_result( - "WICKET_UPDATE_TEST_SIMULATE_SP_RESULT", - )?; - - Ok(StartUpdateOptions { - test_error, - test_step_seconds, - test_simulate_rot_result, - test_simulate_sp_result, - skip_rot_version_check: self.force_update_rot, - skip_sp_version_check: self.force_update_sp, - }) - } -} - -/// Converts an `EventReportMap` to a map by component ID. -pub(crate) fn parse_event_report_map( - log: &Logger, - reports: EventReportMap, -) -> BTreeMap { - let mut component_id_map = BTreeMap::new(); - for (sp_type, logs) in reports { - for (i, event_report) in logs { - let Ok(id) = ComponentId::try_from(ParsableComponentId { - sp_type: &sp_type, - i: &i, - }) else { - slog::warn!( - log, - "Invalid ComponentId in EventReportMap: {} {}", - &sp_type, - &i - ); - continue; - }; - component_id_map.insert(id, event_report); - } - } - - component_id_map -} diff --git a/wicket/src/runner.rs b/wicket/src/runner.rs index 573bbd254c..c37b16d5d9 100644 --- a/wicket/src/runner.rs +++ b/wicket/src/runner.rs @@ -2,6 +2,8 @@ // 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/. +use anyhow::bail; +use anyhow::Context; use crossterm::event::Event as TermEvent; use crossterm::event::EventStream; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -15,6 +17,7 @@ use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use slog::Logger; use slog::{debug, error, info}; +use std::env::VarError; use std::io::{stdout, Stdout}; use std::net::SocketAddrV6; use std::time::Instant; @@ -24,10 +27,11 @@ use tokio::sync::mpsc::{ use tokio::time::{interval, Duration}; use wicketd_client::types::AbortUpdateOptions; use wicketd_client::types::ClearUpdateStateOptions; +use wicketd_client::types::StartUpdateOptions; +use wicketd_client::types::UpdateSimulatedResult; +use wicketd_client::types::UpdateTestError; use crate::events::EventReportMap; -use crate::helpers::get_update_test_error; -use crate::rack_update::CreateStartUpdateOptions; use crate::ui::Screen; use crate::wicketd::{self, WicketdHandle, WicketdManager}; use crate::{Action, Cmd, Event, KeyHandler, Recorder, State, TICK_INTERVAL}; @@ -176,18 +180,43 @@ impl RunnerCore { } Action::StartUpdate(component_id) => { if let Some(wicketd) = wicketd { - let options = CreateStartUpdateOptions { - force_update_rot: self + let test_error = get_update_test_error( + "WICKET_TEST_START_UPDATE_ERROR", + )?; + + // This is a debug environment variable used to + // add a test step. + let test_step_seconds = + std::env::var("WICKET_UPDATE_TEST_STEP_SECONDS") + .ok() + .map(|v| { + v.parse().expect( + "parsed WICKET_UPDATE_TEST_STEP_SECONDS \ + as a u64", + ) + }); + + let test_simulate_rot_result = get_update_simulated_result( + "WICKET_UPDATE_TEST_SIMULATE_ROT_RESULT", + )?; + let test_simulate_sp_result = get_update_simulated_result( + "WICKET_UPDATE_TEST_SIMULATE_SP_RESULT", + )?; + + let options = StartUpdateOptions { + test_error, + test_step_seconds, + test_simulate_rot_result, + test_simulate_sp_result, + skip_rot_version_check: self .state .force_update_state .force_update_rot, - force_update_sp: self + skip_sp_version_check: self .state .force_update_state .force_update_sp, - } - .to_start_update_options()?; - + }; wicketd.tx.blocking_send( wicketd::Request::StartUpdate { component_id, options }, )?; @@ -252,6 +281,66 @@ impl RunnerCore { } } +fn get_update_test_error( + env_var: &str, +) -> Result, anyhow::Error> { + // 30 seconds should always be enough to cause a timeout. (The default + // timeout for progenitor is 15 seconds, and in wicket we set an even + // shorter timeout.) + const DEFAULT_TEST_TIMEOUT_SECS: u64 = 30; + + let test_error = match std::env::var(env_var) { + Ok(v) if v == "fail" => Some(UpdateTestError::Fail), + Ok(v) if v == "timeout" => { + Some(UpdateTestError::Timeout { secs: DEFAULT_TEST_TIMEOUT_SECS }) + } + Ok(v) if v.starts_with("timeout:") => { + // Extended start_timeout syntax with a custom + // number of seconds. + let suffix = v.strip_prefix("timeout:").unwrap(); + match suffix.parse::() { + Ok(secs) => Some(UpdateTestError::Timeout { secs }), + Err(error) => { + return Err(error).with_context(|| { + format!( + "could not parse {env_var} \ + in the form `timeout:`: {v}" + ) + }); + } + } + } + Ok(value) => { + bail!("unrecognized value for {env_var}: {value}"); + } + Err(VarError::NotPresent) => None, + Err(VarError::NotUnicode(value)) => { + bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); + } + }; + Ok(test_error) +} + +fn get_update_simulated_result( + env_var: &str, +) -> Result, anyhow::Error> { + let result = match std::env::var(env_var) { + Ok(v) if v == "success" => Some(UpdateSimulatedResult::Success), + Ok(v) if v == "warning" => Some(UpdateSimulatedResult::Warning), + Ok(v) if v == "skipped" => Some(UpdateSimulatedResult::Skipped), + Ok(v) if v == "failure" => Some(UpdateSimulatedResult::Failure), + Ok(value) => { + bail!("unrecognized value for {env_var}: {value}"); + } + Err(VarError::NotPresent) => None, + Err(VarError::NotUnicode(value)) => { + bail!("invalid Unicode for {env_var}: {}", value.to_string_lossy()); + } + }; + + Ok(result) +} + /// The `Runner` owns the main UI thread, and starts a tokio runtime /// for interaction with downstream services. pub struct Runner { diff --git a/wicket/src/state/inventory.rs b/wicket/src/state/inventory.rs index e5c1803695..3a561167b1 100644 --- a/wicket/src/state/inventory.rs +++ b/wicket/src/state/inventory.rs @@ -4,8 +4,7 @@ //! Information about all top-level Oxide components (sleds, switches, PSCs) -use anyhow::{bail, Result}; -use omicron_common::api::internal::nexus::KnownArtifactKind; +use anyhow::anyhow; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -65,13 +64,26 @@ impl Inventory { }; // Validate and get a ComponentId - let id = ComponentId::from_sp_type_and_slot(type_, i as u8)?; - let component = match type_ { - SpType::Sled => Component::Sled(sp), - SpType::Switch => Component::Switch(sp), - SpType::Power => Component::Psc(sp), + let (id, component) = match type_ { + SpType::Sled => { + if i > 31 { + return Err(anyhow!("Invalid sled slot: {}", i)); + } + (ComponentId::Sled(i as u8), Component::Sled(sp)) + } + SpType::Switch => { + if i > 1 { + return Err(anyhow!("Invalid switch slot: {}", i)); + } + (ComponentId::Switch(i as u8), Component::Switch(sp)) + } + SpType::Power => { + if i > 1 { + return Err(anyhow!("Invalid power shelf slot: {}", i)); + } + (ComponentId::Psc(i as u8), Component::Psc(sp)) + } }; - new_inventory.inventory.insert(id, component); // TODO: Plumb through real power state @@ -192,66 +204,6 @@ pub enum ComponentId { } impl ComponentId { - /// The maximum possible sled ID. - pub const MAX_SLED_ID: u8 = 31; - - /// The maximum possible switch ID. - pub const MAX_SWITCH_ID: u8 = 1; - - /// The maximum possible power shelf ID. - /// - /// Currently shipping racks don't have PSC 1. - pub const MAX_PSC_ID: u8 = 0; - - pub fn new_sled(slot: u8) -> Result { - if slot > Self::MAX_SLED_ID { - bail!("Invalid sled slot: {}", slot); - } - Ok(Self::Sled(slot)) - } - - pub fn new_switch(slot: u8) -> Result { - if slot > Self::MAX_SWITCH_ID { - bail!("Invalid switch slot: {}", slot); - } - Ok(Self::Switch(slot)) - } - - pub fn new_psc(slot: u8) -> Result { - if slot > Self::MAX_PSC_ID { - bail!("Invalid power shelf slot: {}", slot); - } - Ok(Self::Psc(slot)) - } - - pub fn from_sp_type_and_slot(sp_type: SpType, slot: u8) -> Result { - match sp_type { - SpType::Sled => Self::new_sled(slot), - SpType::Switch => Self::new_switch(slot), - SpType::Power => Self::new_psc(slot), - } - } - - pub fn name(&self) -> String { - self.to_string() - } - - pub fn sp_known_artifact_kind(&self) -> KnownArtifactKind { - match self { - ComponentId::Sled(_) => KnownArtifactKind::GimletSp, - ComponentId::Switch(_) => KnownArtifactKind::SwitchSp, - ComponentId::Psc(_) => KnownArtifactKind::PscSp, - } - } - - pub fn rot_known_artifact_kind(&self) -> KnownArtifactKind { - match self { - ComponentId::Sled(_) => KnownArtifactKind::GimletRot, - ComponentId::Switch(_) => KnownArtifactKind::SwitchRot, - ComponentId::Psc(_) => KnownArtifactKind::PscRot, - } - } - pub fn to_string_uppercase(&self) -> String { let mut s = self.to_string(); s.make_ascii_uppercase(); diff --git a/wicket/src/state/update.rs b/wicket/src/state/update.rs index 8aade4c9cb..1a0aafb9cf 100644 --- a/wicket/src/state/update.rs +++ b/wicket/src/state/update.rs @@ -8,14 +8,13 @@ use wicket_common::update_events::{ UpdateStepId, }; -use crate::rack_update::parse_event_report_map; use crate::{events::EventReportMap, ui::defaults::style}; -use super::{ComponentId, ALL_COMPONENT_IDS}; +use super::{ComponentId, ParsableComponentId, ALL_COMPONENT_IDS}; use omicron_common::api::internal::nexus::KnownArtifactKind; use serde::{Deserialize, Serialize}; -use slog::Logger; -use std::collections::BTreeMap; +use slog::{warn, Logger}; +use std::collections::{BTreeMap, HashSet}; use std::fmt::Display; use wicketd_client::types::{ArtifactId, SemverVersion}; @@ -103,17 +102,33 @@ impl RackUpdateState { } } - let reports = parse_event_report_map(logger, reports); - // Reset all component IDs that aren't in the event report map. - for (id, item) in &mut self.items { - if !reports.contains_key(id) { - item.reset(); + let mut updated_component_ids = HashSet::new(); + + for (sp_type, logs) in reports { + for (i, log) in logs { + let Ok(id) = ComponentId::try_from(ParsableComponentId { + sp_type: &sp_type, + i: &i, + }) else { + warn!( + logger, + "Invalid ComponentId in EventReport: {} {}", + &sp_type, + &i + ); + continue; + }; + let item_state = self.items.get_mut(&id).unwrap(); + item_state.update(log); + updated_component_ids.insert(id); } } - for (id, report) in reports { - let item_state = self.items.get_mut(&id).unwrap(); - item_state.update(report); + // Reset all component IDs that weren't updated. + for (id, item) in &mut self.items { + if !updated_component_ids.contains(id) { + item.reset(); + } } } } diff --git a/wicket/src/wicketd.rs b/wicket/src/wicketd.rs index ef630b9936..2411542429 100644 --- a/wicket/src/wicketd.rs +++ b/wicket/src/wicketd.rs @@ -40,7 +40,7 @@ const WICKETD_POLL_INTERVAL: Duration = Duration::from_millis(500); // WICKETD_TIMEOUT used to be 1 second, but that might be too short (and in // particular might be responsible for // https://github.com/oxidecomputer/omicron/issues/3103). -pub(crate) const WICKETD_TIMEOUT: Duration = Duration::from_secs(5); +const WICKETD_TIMEOUT: Duration = Duration::from_secs(5); // Assume that these requests are periodic on the order of seconds or the // result of human interaction. In either case, this buffer should be plenty