Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions wicket-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ omicron-common.workspace = true
omicron-workspace-hack.workspace = true
owo-colors.workspace = true
oxnet.workspace = true
semver.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand Down
177 changes: 177 additions & 0 deletions wicket-common/src/rack_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

use std::{collections::BTreeSet, time::Duration};

use semver::Version;

use dropshot::HttpError;
use omicron_common::update::ArtifactId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -133,3 +136,177 @@ impl UpdateTestError {
}
}
}

/// Counts of component updates by state.
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct UpdateStateCounts {
pub completed: usize,
pub failed: usize,
pub aborted: usize,
pub in_progress: usize,
pub not_started: usize,
}

impl UpdateStateCounts {
pub fn from_components(components: &[ComponentUpdateStatus]) -> Self {
let mut counts = Self::default();
for c in components {
match c.state {
UpdateState::Completed => counts.completed += 1,
UpdateState::Failed => counts.failed += 1,
UpdateState::Aborted => counts.aborted += 1,
UpdateState::InProgress => counts.in_progress += 1,
UpdateState::NotStarted => counts.not_started += 1,
}
}
counts
}
}

/// The status of a rack update.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct RackUpdateStatus {
Comment on lines +166 to +168
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if you can extend one of the tests in https://github.com/oxidecomputer/omicron/blob/main/wicketd/tests/integration_tests/updates.rs to get a basic smoke test of the rack update status going.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, added here! aa374c3

/// The overall update state, rolled up across all components.
pub state: UpdateState,
/// The version of the top-level TUF archive.
pub system_version: Option<Version>,
/// The artifacts included in the TUF archive.
pub artifacts: Vec<ArtifactId>,
/// The update status of each of the target components.
pub components: Vec<ComponentUpdateStatus>,
/// Counts of component updates by state.
pub state_counts: UpdateStateCounts,
}

/// The message from a failed or aborted update step.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ExitMessage {
pub message: String,
pub causes: Vec<String>,
}

/// The status of an update for a component within a rack.
/// Here, a component means a Sled, Switch, or PSC.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ComponentUpdateStatus {
Comment thread
adamlouis marked this conversation as resolved.
/// The ID of the component.
pub id: SpIdentifier,
/// The state of the component update.
pub state: UpdateState,
/// The index of the current step (if in progress) or the last
/// step (if terminal).
pub step_index: Option<usize>,
/// The total number of steps in the update.
pub total_steps: Option<usize>,
/// The time elapsed since starting the update.
pub elapsed_secs: Option<f64>,
/// The failure or abort message, if the update reached a terminal failed
/// or aborted state.
pub exit_message: Option<ExitMessage>,
}

/// The state of a rack or component update.
#[derive(
Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum UpdateState {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest calling this ComponentUpdateState to avoid confusion with the existing UpdateState, but that doesn't quite work here because we also use the same enum to represent the overall update state. I'm not quite sure what to call this.

NotStarted,
InProgress,
Completed,
Failed,
Aborted,
}

pub fn rollup_update_state(states: &[UpdateState]) -> UpdateState {
if states.is_empty() {
// An empty list is treated as "not started".
UpdateState::NotStarted
} else if states.iter().any(|s| matches!(s, UpdateState::Failed)) {
// If *any* component failed, the update failed.
UpdateState::Failed
} else if states.iter().any(|s| matches!(s, UpdateState::Aborted)) {
// If *any* component was aborted (and none failed),
// the update is aborted.
UpdateState::Aborted
} else if states.iter().all(|s| matches!(s, UpdateState::Completed)) {
// If *all* components are completed, the update is completed.
UpdateState::Completed
} else if states.iter().all(|s| matches!(s, UpdateState::NotStarted)) {
// If *all* components are not started, the update is not started.
UpdateState::NotStarted
} else {
// Here, some components have started, none have failed or been aborted,
// and not all are completed. Therefore, the update is in progress.
UpdateState::InProgress
}
}
Comment on lines +221 to +243
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. What is this used for?
  2. Is this correct? I feel like in-progress is more important than completed?

Copy link
Copy Markdown
Contributor Author

@adamlouis adamlouis Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In manufacturing, this is the field we poll to determine update completion (example snippet below). In our initial wicket client code, this is the only field that's used.

I believe this is correct - though may be overlooking something. Is there a test case you have in mind?

The Completed branch here is using all - so InProgress does take precedence.

The structure as written handles the less obvious InProgress case where the components are NotStarted or Completed, with none individually in InProgress. For example in the test:

assert_eq!(rollup_update_state(&[NotStarted, Completed]), InProgress);

Let me know if there's some more clear way to write this. Or, if the rules here are not universal to foreseeable wicket users / callers, we can move this function into the manufacturing client.

(Thanks for all the comments! Will address the remaining ones when we square this away)


async fn poll_mupdate_status(
    wicket: &WicketSh,
    timeout: Duration,
) -> Result<RackUpdateStatus> {
    let deadline = Instant::now() + timeout;
    loop {
        let status = wicket.rack_update_status()?;
        match status.state {
            UpdateState::Completed
            | UpdateState::Failed
            | UpdateState::Aborted => return Ok(status),
            UpdateState::NotStarted | UpdateState::InProgress => {}
        }
        if Instant::now() >= deadline {
            bail!("timed out waiting for mupdate to complete");
        }
        tokio::time::sleep(Duration::from_secs(5)).await;
    }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if there's some more clear way to write this. Or, if the rules here are not universal to foreseeable wicket users / callers, we can move this function into the manufacturing client.

To be honest, maybe comments calling out that some of these are any and others are all? They're so visually similar that it was kinda hard to tell they were any different.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, I completely missed the any/all distinction!


impl UpdateState {
/// The exit code corresponding to this state.
///
/// State-specific codes start at 4: exit code 1 tends to be used for
/// generic errors, and 2 and 3 tend to be reserved for things like
/// incorrect CLI args.
///
/// - 0: Completed
/// - 4: NotStarted
/// - 5: InProgress
/// - 6: Failed
/// - 7: Aborted
pub fn exit_code(self) -> u8 {
match self {
UpdateState::Completed => 0,
UpdateState::NotStarted => 4,
UpdateState::InProgress => 5,
UpdateState::Failed => 6,
UpdateState::Aborted => 7,
}
}
}

impl std::fmt::Display for UpdateState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UpdateState::NotStarted => write!(f, "not started"),
UpdateState::InProgress => write!(f, "in progress"),
UpdateState::Completed => write!(f, "completed"),
UpdateState::Failed => write!(f, "failed"),
UpdateState::Aborted => write!(f, "aborted"),
}
Comment thread
adamlouis marked this conversation as resolved.
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_rollup_update_state() {
use UpdateState::*;

// Empty is treated as NotStarted.
assert_eq!(rollup_update_state(&[]), NotStarted);

// Single states roll up to themselves.
assert_eq!(rollup_update_state(&[NotStarted]), NotStarted);
assert_eq!(rollup_update_state(&[InProgress]), InProgress);
assert_eq!(rollup_update_state(&[Completed]), Completed);
assert_eq!(rollup_update_state(&[Failed]), Failed);
assert_eq!(rollup_update_state(&[Aborted]), Aborted);

// Failed / Aborted take priority
assert_eq!(rollup_update_state(&[Completed, Failed]), Failed);
assert_eq!(rollup_update_state(&[InProgress, Failed]), Failed);
assert_eq!(rollup_update_state(&[Aborted, Completed]), Aborted);
assert_eq!(rollup_update_state(&[Aborted, Failed]), Failed);

// Complete if all Completed.
assert_eq!(rollup_update_state(&[Completed, Completed]), Completed);

// Otherwise ... InProgress
assert_eq!(rollup_update_state(&[Completed, InProgress]), InProgress);
assert_eq!(rollup_update_state(&[NotStarted, InProgress]), InProgress);
assert_eq!(rollup_update_state(&[NotStarted, Completed]), InProgress);
}
}
1 change: 1 addition & 0 deletions wicket/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ slog-async.workspace = true
slog-envlogger.workspace = true
slog-term.workspace = true
supports-color.workspace = true
tabled.workspace = true
textwrap.workspace = true
tokio = { workspace = true, features = ["full"] }
tokio-util.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion wicket/src/bin/wicket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@

use anyhow::Result;

fn main() -> Result<()> {
fn main() -> Result<std::process::ExitCode> {
wicket::exec()
}
17 changes: 12 additions & 5 deletions wicket/src/cli/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! Code that manages command dispatch from a shell for wicket.

use std::net::SocketAddrV6;
use std::process::ExitCode;

use anyhow::Result;
use clap::{Args, ColorChoice, Parser, Subcommand};
Expand Down Expand Up @@ -37,20 +38,26 @@ impl ShellApp {
log: slog::Logger,
wicketd_addr: SocketAddrV6,
output: CommandOutput<'_>,
) -> Result<()> {
) -> Result<ExitCode> {
match self.command {
ShellCommand::UploadRepo(args) => {
args.exec(log, wicketd_addr).await
args.exec(log, wicketd_addr).await?;
Ok(ExitCode::SUCCESS)
}
ShellCommand::RackUpdate(args) => {
args.exec(log, wicketd_addr, self.global_opts, output).await
}
ShellCommand::Setup(args) => {
args.exec(log, wicketd_addr, self.global_opts).await
args.exec(log, wicketd_addr, self.global_opts).await?;
Ok(ExitCode::SUCCESS)
}
ShellCommand::Preflight(args) => {
args.exec(log, wicketd_addr).await?;
Ok(ExitCode::SUCCESS)
}
ShellCommand::Preflight(args) => args.exec(log, wicketd_addr).await,
ShellCommand::Inventory(args) => {
args.exec(log, wicketd_addr, output).await
args.exec(log, wicketd_addr, output).await?;
Ok(ExitCode::SUCCESS)
}
}
}
Expand Down
Loading
Loading