From da04ef55f4f2bce6eba2799fefa7e18c8619f3e5 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 2 Apr 2025 11:38:30 -0700 Subject: [PATCH 1/7] add show command that prints SP images --- Cargo.lock | 5 + bin/Cargo.toml | 5 + bin/src/dispatch.rs | 226 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 233 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e04f25..9e3141f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3413,6 +3413,8 @@ version = "0.1.0" dependencies = [ "anyhow", "assert_cmd", + "buf-list", + "bytes", "camino", "chrono", "clap", @@ -3420,6 +3422,8 @@ dependencies = [ "datatest-stable", "dropshot", "fs-err", + "futures", + "hubtools", "humantime", "predicates", "semver", @@ -3429,6 +3433,7 @@ dependencies = [ "slog-term", "tempfile", "tokio", + "tough", "tufaceous-artifact", "tufaceous-lib", ] diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 69c6c03..70aafa8 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -11,10 +11,14 @@ harness = false [dependencies] anyhow = { workspace = true, features = ["backtrace"] } +buf-list.workspace = true +bytes.workspace = true camino.workspace = true chrono.workspace = true clap.workspace = true console.workspace = true +futures.workspace = true +hubtools.workspace = true humantime.workspace = true semver.workspace = true slog.workspace = true @@ -22,6 +26,7 @@ slog-async.workspace = true slog-envlogger.workspace = true slog-term.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tough.workspace = true tufaceous-artifact.workspace = true tufaceous-lib.workspace = true diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs index 6c9bd90..2059426 100644 --- a/bin/src/dispatch.rs +++ b/bin/src/dispatch.rs @@ -2,12 +2,17 @@ // 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::{Context, Result, bail}; -use camino::Utf8PathBuf; +use anyhow::{Context, Result, anyhow, bail}; +use buf_list::BufList; +use camino::{Utf8Path, Utf8PathBuf}; use chrono::{DateTime, Utc}; use clap::{CommandFactory, Parser}; +use futures::TryStreamExt; +use hubtools::RawHubrisArchive; use semver::Version; -use tufaceous_artifact::{ArtifactKind, ArtifactVersion}; +use std::collections::BTreeMap; +use tough::Repository; +use tufaceous_artifact::{ArtifactKind, ArtifactVersion, KnownArtifactKind}; use tufaceous_lib::assemble::{ArtifactManifest, OmicronRepoAssembler}; use tufaceous_lib::{AddArtifact, ArchiveExtractor, Key, OmicronRepo}; @@ -162,6 +167,9 @@ impl Args { Ok(()) } + Command::Show => show(log, &repo_path).await.with_context(|| { + format!("error showing repository at `{repo_path}`") + }), Command::Assemble { manifest_path, output_path, @@ -256,6 +264,8 @@ enum Command { /// The destination to extract the file to. dest: Utf8PathBuf, }, + /// Summarizes the contents of an Omicron TUF repository + Show, /// Assembles a repository from a provided manifest. Assemble { /// Path to artifact manifest. @@ -297,3 +307,213 @@ fn maybe_generate_keys( keys }) } + +struct ArtifactInfo { + artifact_name: String, + artifact_version: ArtifactVersion, + artifact_kind: ArtifactKind, + artifact_target: String, + details: ArtifactInfoDetails, +} + +enum ArtifactInfoDetails { + SpHubrisImage(CabooseInfo), + RotArtifact { a: CabooseInfo, b: CabooseInfo }, + NoDetails, +} + +#[derive(Ord, PartialOrd, Eq, PartialEq)] +enum ArtifactFileKind { + SpHubrisImage, + RotArtifact, + Other, +} + +struct CabooseInfo { + board: String, + git_commit: String, + version: String, + name: String, +} + +async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { + let omicron_repo = + OmicronRepo::load_untrusted_ignore_expiration(log, &repo_path) + .await + .context("loading repository")?; + let tuf_repo = omicron_repo.repo(); + let artifacts_document = omicron_repo + .read_artifacts() + .await + .context("reading artifacts document")?; + println!("system version: {}", artifacts_document.system_version); + + let mut all_artifacts = BTreeMap::new(); + for artifact_metadata in &artifacts_document.artifacts { + eprintln!("loading artifact {}", artifact_metadata.target); + let known_artifact_kind = + artifact_metadata.kind.to_known().ok_or_else(|| { + anyhow!("unknown artifact kind: {}", &artifact_metadata.kind) + })?; + let (details, file_kind) = match known_artifact_kind { + KnownArtifactKind::GimletSp + | KnownArtifactKind::PscSp + | KnownArtifactKind::SwitchSp => ( + ArtifactInfoDetails::SpHubrisImage( + load_caboose(&artifact_metadata.target, tuf_repo).await?, + ), + ArtifactFileKind::SpHubrisImage, + ), + KnownArtifactKind::GimletRot + | KnownArtifactKind::PscRot + | KnownArtifactKind::SwitchRot => { + // XXX-dap + (ArtifactInfoDetails::NoDetails, ArtifactFileKind::Other) + } + KnownArtifactKind::GimletRotBootloader + | KnownArtifactKind::PscRotBootloader + | KnownArtifactKind::SwitchRotBootloader => { + // XXX-dap + (ArtifactInfoDetails::NoDetails, ArtifactFileKind::Other) + } + KnownArtifactKind::Host + | KnownArtifactKind::Trampoline + | KnownArtifactKind::ControlPlane + | KnownArtifactKind::Zone => { + (ArtifactInfoDetails::NoDetails, ArtifactFileKind::Other) + } + }; + + let artifact_info = ArtifactInfo { + artifact_name: artifact_metadata.name.clone(), + artifact_version: artifact_metadata.version.clone(), + artifact_kind: artifact_metadata.kind.clone(), + artifact_target: artifact_metadata.target.clone(), + details, + }; + + all_artifacts + .entry(file_kind) + .or_insert_with(Vec::new) + .push(artifact_info); + } + + println!("SP Hubris Images\n"); + println!(" {:37} {:9} {:13} {:7}", "TARGET", "KIND", "NAME", "VERSION"); + for artifact_info in all_artifacts + .get(&ArtifactFileKind::SpHubrisImage) + .into_iter() + .flatten() + { + let ArtifactInfoDetails::SpHubrisImage(caboose_info) = + &artifact_info.details + else { + panic!("internal type mismatch"); + }; + + // Only print fields that we don't expect are duplicated or otherwise + // uninteresting (like the Git commit). If we're wrong about these + // being duplicated, we'll print a warning below. + println!( + " {:37} {:>9} {:13} {:>7}", + artifact_info.artifact_target, + // XXX-dap Display for ArtifactKind does not honor width + artifact_info.artifact_kind.to_string(), + artifact_info.artifact_name, + artifact_info.artifact_version, + ); + + if caboose_info.version.to_string() + != artifact_info.artifact_version.as_str() + { + eprintln!( + "warning: target {}: caboose version {} does not match \ + artifact version {}", + artifact_info.artifact_target, + caboose_info.version, + artifact_info.artifact_version + ); + } + + // XXX-dap There is a comment on + // `tufaceous_artifact::artifact::Artifact` that says that the `name` + // should match the caboose *board*. That's not true for stuff with + // "lab" in th ename. + if caboose_info.board != artifact_info.artifact_name + && format!("{}-lab", caboose_info.board) + != artifact_info.artifact_name + { + eprintln!( + "warning: target {}: caboose board {} does not match \ + artifact name {}", + artifact_info.artifact_target, + caboose_info.board, + artifact_info.artifact_name, + ); + } + + // See above comment. + if caboose_info.name != artifact_info.artifact_name { + eprintln!( + "warning: target {}: caboose name {} does not match \ + artifact name {}", + artifact_info.artifact_target, + caboose_info.name, + artifact_info.artifact_name, + ); + } + } + + // XXX-dap print out other file kinds + + Ok(()) +} + +async fn load_caboose( + target_name: &str, + tuf_repo: &Repository, +) -> Result { + load_caboose_impl(target_name, tuf_repo).await.with_context(|| { + format!("loading caboose for target {:?}", target_name) + }) +} + +async fn load_caboose_impl( + target_name: &str, + tuf_repo: &Repository, +) -> Result { + let target_name: tough::TargetName = + target_name.parse().context("unsupported target name")?; + let reader = tuf_repo + .read_target(&target_name) + .await + .context("loading target")? + .ok_or_else(|| anyhow!("missing target"))?; + let buf_list = + reader.try_collect::().await.context("reading target")?; + let v: Vec = buf_list.into_iter().flatten().collect(); + let archive = + RawHubrisArchive::from_vec(v).context("loading Hubris archive")?; + let caboose = archive.read_caboose().context("loading caboose")?; + let name = String::from_utf8( + caboose.name().context("reading name from caboose")?.to_vec(), + ) + .context("unexpected non-UTF8 name")?; + let board = String::from_utf8( + caboose.board().context("reading board from caboose")?.to_vec(), + ) + .context("unexpected non-UTF8 board")?; + let git_commit = String::from_utf8( + caboose + .git_commit() + .context("reading git_commit from caboose")? + .to_vec(), + ) + .context("unexpected non-UTF8 git_commit")?; + let version = String::from_utf8( + caboose.version().context("reading version from caboose")?.to_vec(), + ) + .context("unexpected non-UTF8 version")?; + // XXX-dap do something with the signature + Ok(CabooseInfo { board, git_commit, version, name }) +} From 920c99bfb756329593e85c90cff3e08980c71a9d Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 2 Apr 2025 12:47:58 -0700 Subject: [PATCH 2/7] add RoT artifacts --- Cargo.lock | 2 + bin/Cargo.toml | 2 + bin/src/dispatch.rs | 308 ++++++++++++++++++++++++++++++++++++++++---- lib/src/artifact.rs | 4 +- 4 files changed, 291 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e3141f..9a8ae47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3421,6 +3421,7 @@ dependencies = [ "console", "datatest-stable", "dropshot", + "flate2", "fs-err", "futures", "hubtools", @@ -3431,6 +3432,7 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-term", + "tar", "tempfile", "tokio", "tough", diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 70aafa8..d583eca 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -17,6 +17,7 @@ camino.workspace = true chrono.workspace = true clap.workspace = true console.workspace = true +flate2.workspace = true futures.workspace = true hubtools.workspace = true humantime.workspace = true @@ -25,6 +26,7 @@ slog.workspace = true slog-async.workspace = true slog-envlogger.workspace = true slog-term.workspace = true +tar.workspace = true tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tough.workspace = true tufaceous-artifact.workspace = true diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs index 2059426..6c152b8 100644 --- a/bin/src/dispatch.rs +++ b/bin/src/dispatch.rs @@ -7,14 +7,19 @@ use buf_list::BufList; use camino::{Utf8Path, Utf8PathBuf}; use chrono::{DateTime, Utc}; use clap::{CommandFactory, Parser}; +use flate2::bufread::GzDecoder; use futures::TryStreamExt; use hubtools::RawHubrisArchive; use semver::Version; use std::collections::BTreeMap; +use std::io::Read; use tough::Repository; use tufaceous_artifact::{ArtifactKind, ArtifactVersion, KnownArtifactKind}; use tufaceous_lib::assemble::{ArtifactManifest, OmicronRepoAssembler}; -use tufaceous_lib::{AddArtifact, ArchiveExtractor, Key, OmicronRepo}; +use tufaceous_lib::{ + AddArtifact, ArchiveExtractor, Key, OmicronRepo, ROT_ARCHIVE_A_FILE_NAME, + ROT_ARCHIVE_B_FILE_NAME, +}; #[derive(Debug, Parser)] pub struct Args { @@ -317,8 +322,8 @@ struct ArtifactInfo { } enum ArtifactInfoDetails { - SpHubrisImage(CabooseInfo), - RotArtifact { a: CabooseInfo, b: CabooseInfo }, + SpHubrisImage(CommonCabooseInfo), + RotArtifact { a: RotCabooseInfo, b: RotCabooseInfo }, NoDetails, } @@ -329,13 +334,21 @@ enum ArtifactFileKind { Other, } -struct CabooseInfo { +struct CommonCabooseInfo { board: String, git_commit: String, version: String, name: String, } +struct RotCabooseInfo { + board: String, + git_commit: String, + version: String, + name: String, + sign: String, +} + async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { let omicron_repo = OmicronRepo::load_untrusted_ignore_expiration(log, &repo_path) @@ -359,6 +372,7 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { KnownArtifactKind::GimletSp | KnownArtifactKind::PscSp | KnownArtifactKind::SwitchSp => ( + // This target is itself a Hubris archive. ArtifactInfoDetails::SpHubrisImage( load_caboose(&artifact_metadata.target, tuf_repo).await?, ), @@ -367,8 +381,9 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { KnownArtifactKind::GimletRot | KnownArtifactKind::PscRot | KnownArtifactKind::SwitchRot => { - // XXX-dap - (ArtifactInfoDetails::NoDetails, ArtifactFileKind::Other) + let details = + load_rot(&artifact_metadata.target, tuf_repo).await?; + (details, ArtifactFileKind::RotArtifact) } KnownArtifactKind::GimletRotBootloader | KnownArtifactKind::PscRotBootloader @@ -398,7 +413,7 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { .push(artifact_info); } - println!("SP Hubris Images\n"); + println!("SP Artifacts (Hubris archives)\n"); println!(" {:37} {:9} {:13} {:7}", "TARGET", "KIND", "NAME", "VERSION"); for artifact_info in all_artifacts .get(&ArtifactFileKind::SpHubrisImage) @@ -437,8 +452,9 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { // XXX-dap There is a comment on // `tufaceous_artifact::artifact::Artifact` that says that the `name` - // should match the caboose *board*. That's not true for stuff with - // "lab" in th ename. + // should match the caboose *board*. That's not true: SP artifacts + // sometimes (but not always) have a signing key ("lab") in the artifact + // name. if caboose_info.board != artifact_info.artifact_name && format!("{}-lab", caboose_info.board) != artifact_info.artifact_name @@ -464,6 +480,157 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { } } + // XXX-dap + let known_signature_names: BTreeMap<&str, &str> = [ + ( + "84332ef8279df87fbb759dc3866cbc50cd246fbb5a64705a7e60ba86bf01c27d", + "bart", + ), + ( + "11594bb5548a757e918e6fe056e2ad9e084297c9555417a025d8788eacf55daf", + "gimlet-staging-devel", + ), + ( + "5796ee3433f840519c3bcde73e19ee82ccb6af3857eddaabb928b8d9726d93c0", + "gimlet-production-release", + ), + ( + "f592d8f109b81881221eed5af6438abad9b5df8c220b9129c03763e7e10b22c7", + "psc-staging-devel", + ), + ( + "31942f8d53dc908c5cb338bdcecb204785fa87834e8b18f706fc972a42886c8b", + "psc-production-release", + ), + ( + "1432cc4cfe5688c51b55546fe37837c753cfbc89e8c3c6aabcf977fdf0c41e27", + "switch-staging-devel", + ), + ( + "5c69a42ee1f1e6cd5f356d14f81d46f8dbee783bb28777334226c689f169c0eb", + "switch-production-release", + ), + ] + .into_iter() + .collect(); + + println!("\nRoT Artifacts (composite artifacts with two Hubris images)\n"); + println!( + " {:61} {:10} {:36} {:7} {:25}", + "TARGET", "KIND", "NAME", "VERSION", "SIGNING KEY" + ); + for artifact_info in + all_artifacts.get(&ArtifactFileKind::RotArtifact).into_iter().flatten() + { + let ArtifactInfoDetails::RotArtifact { a, b } = &artifact_info.details + else { + panic!("internal type mismatch"); + }; + + let key_name = known_signature_names.get(a.sign.as_str()).map(|s| *s); + + // Only print fields that we don't expect are duplicated or otherwise + // uninteresting (like the Git commit). If we're wrong about these + // being duplicated, we'll print a warning below. + println!( + " {:61} {:>10} {:36} {:>7} {:25}", + artifact_info.artifact_target, + // XXX-dap Display for ArtifactKind does not honor width + artifact_info.artifact_kind.to_string(), + artifact_info.artifact_name, + // XXX-dap Display for ArtifactVersion does not honor width + artifact_info.artifact_version.to_string(), + key_name.unwrap_or("UNKNOWN"), + ); + + if a.board != a.name { + eprintln!( + "warning: archive a: expected caboose \"board\" and \"name\" \ + to be the same, but they're not" + ); + } + + if b.board != b.name { + eprintln!( + "warning: archive a: expected caboose \"board\" and \"name\" \ + to be the same, but they're not" + ); + } + + if a.sign != b.sign { + eprintln!( + "warning: expected archives A and B to have the same \"sign\"" + ); + } + + if key_name.is_none() { + eprintln!("warning: unrecognized \"sign\""); + } + + for caboose_info in [a, b] { + if caboose_info.version.to_string() + != artifact_info.artifact_version.as_str() + { + eprintln!( + "warning: target {}: caboose version {} does not match \ + artifact version {}", + artifact_info.artifact_target, + caboose_info.version, + artifact_info.artifact_version + ); + } + + // XXX-dap + // See the similar comment above. RoT images sometimes have their + // key ("selfsigned-bart", "production-release", or "staging-devel") + // tacked onto the end of the board name in their artifact name. + // XXX-dap verify my assumptions here + // if caboose_info.board != artifact_info.artifact_name + // && format!("{}-lab", caboose_info.board) + // != artifact_info.artifact_name + // { + // eprintln!( + // "warning: target {}: caboose board {} does not match \ + // artifact name {}", + // artifact_info.artifact_target, + // caboose_info.board, + // artifact_info.artifact_name, + // ); + // } + + // XXX-dap + // See above comment. Similarly, the artifact name sometimes has + // keys appended to it. + // if caboose_info.name != artifact_info.artifact_name { + // eprintln!( + // "warning: target {}: caboose name {} does not match \ + // artifact name {}", + // artifact_info.artifact_target, + // caboose_info.name, + // artifact_info.artifact_name, + // ); + // } + } + } + + println!("\nOther artifacts\n"); + println!( + " {:75} {:21} {:40} {:26}", + "TARGET", "KIND", "NAME", "VERSION" + ); + for artifact_info in + all_artifacts.get(&ArtifactFileKind::Other).into_iter().flatten() + { + println!( + " {:75} {:21} {:40} {:26}", + artifact_info.artifact_target, + // XXX-dap Display for ArtifactKind does not honor width + artifact_info.artifact_kind.to_string(), + artifact_info.artifact_name, + artifact_info.artifact_version, + ); + } + // XXX-dap print out other file kinds Ok(()) @@ -472,7 +639,7 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { async fn load_caboose( target_name: &str, tuf_repo: &Repository, -) -> Result { +) -> Result { load_caboose_impl(target_name, tuf_repo).await.with_context(|| { format!("loading caboose for target {:?}", target_name) }) @@ -481,20 +648,23 @@ async fn load_caboose( async fn load_caboose_impl( target_name: &str, tuf_repo: &Repository, -) -> Result { - let target_name: tough::TargetName = - target_name.parse().context("unsupported target name")?; - let reader = tuf_repo - .read_target(&target_name) - .await - .context("loading target")? - .ok_or_else(|| anyhow!("missing target"))?; - let buf_list = - reader.try_collect::().await.context("reading target")?; - let v: Vec = buf_list.into_iter().flatten().collect(); +) -> Result { + let v = load_target_bytes(target_name, tuf_repo).await?; + load_caboose_from_archive_bytes(v).await +} + +async fn load_caboose_from_archive_bytes( + v: Vec, +) -> Result { let archive = RawHubrisArchive::from_vec(v).context("loading Hubris archive")?; let caboose = archive.read_caboose().context("loading caboose")?; + load_caboose_common_fields(&caboose) +} + +fn load_caboose_common_fields( + caboose: &hubtools::Caboose, +) -> Result { let name = String::from_utf8( caboose.name().context("reading name from caboose")?.to_vec(), ) @@ -514,6 +684,98 @@ async fn load_caboose_impl( caboose.version().context("reading version from caboose")?.to_vec(), ) .context("unexpected non-UTF8 version")?; - // XXX-dap do something with the signature - Ok(CabooseInfo { board, git_commit, version, name }) + Ok(CommonCabooseInfo { board, git_commit, version, name }) +} + +async fn load_rot_caboose_from_archive_bytes( + v: Vec, +) -> Result { + let archive = + RawHubrisArchive::from_vec(v).context("loading Hubris archive")?; + let caboose = archive.read_caboose().context("loading caboose")?; + let sign = String::from_utf8( + caboose.sign().context("reading sign from caboose")?.to_vec(), + ) + .context("unexpected non-UTF8 sign")?; + let common = load_caboose_common_fields(&caboose)?; + Ok(RotCabooseInfo { + board: common.board, + git_commit: common.git_commit, + version: common.version, + name: common.name, + sign, + }) +} + +// XXX-dap this could return a reader and then the tar thing wouldn't need to +// load the whole thing into memory at once +async fn load_target_bytes( + target_name: &str, + tuf_repo: &Repository, +) -> Result> { + let target_name: tough::TargetName = + target_name.parse().context("unsupported target name")?; + let reader = tuf_repo + .read_target(&target_name) + .await + .context("loading target")? + .ok_or_else(|| anyhow!("missing target"))?; + let buf_list = + reader.try_collect::().await.context("reading target")?; + let v: Vec = buf_list.into_iter().flatten().collect(); + Ok(v) +} + +async fn load_rot( + target_name: &str, + tuf_repo: &Repository, +) -> Result { + load_rot_impl(target_name, tuf_repo) + .await + .with_context(|| anyhow!("loading RoT target {}", target_name)) +} + +async fn load_rot_impl( + target_name: &str, + tuf_repo: &Repository, +) -> Result { + let v = load_target_bytes(target_name, tuf_repo).await?; + let source = std::io::BufReader::new(std::io::Cursor::new(v)); + let gunzip = GzDecoder::new(source); + let mut tar = tar::Archive::new(gunzip); + let mut caboose_a = None; + let mut caboose_b = None; + + for entry in tar.entries().context("reading tarball")? { + let mut entry = entry.context("reading entry from tarball")?; + let path = + entry.path().context("reading path for entry from tarball")?; + let Some(basename) = path.file_name() else { + continue; + }; + let caboose_which = if basename == ROT_ARCHIVE_A_FILE_NAME { + &mut caboose_a + } else if basename == ROT_ARCHIVE_B_FILE_NAME { + &mut caboose_b + } else { + continue; + }; + + let mut s = Vec::with_capacity( + usize::try_from( + entry + .header() + .size() + .context("corrupted tarball entry size")?, + ) + .context("archive size too large")?, + ); + entry.read_to_end(&mut s).context("reading entry")?; + *caboose_which = Some(load_rot_caboose_from_archive_bytes(s).await?); + } + + match (caboose_a, caboose_b) { + (Some(a), Some(b)) => Ok(ArtifactInfoDetails::RotArtifact { a, b }), + _ => Err(anyhow!("missing expected RoT artifact")), + } } diff --git a/lib/src/artifact.rs b/lib/src/artifact.rs index 09ba379..124b483 100644 --- a/lib/src/artifact.rs +++ b/lib/src/artifact.rs @@ -442,6 +442,6 @@ static FILLER_TEXT: &[u8; 16] = b"tufaceousfaketxt"; static OXIDE_JSON_FILE_NAME: &str = "oxide.json"; static HOST_PHASE_1_FILE_NAME: &str = "image/rom"; static HOST_PHASE_2_FILE_NAME: &str = "image/zfs.img"; -static ROT_ARCHIVE_A_FILE_NAME: &str = "archive-a.zip"; -static ROT_ARCHIVE_B_FILE_NAME: &str = "archive-b.zip"; +pub static ROT_ARCHIVE_A_FILE_NAME: &str = "archive-a.zip"; +pub static ROT_ARCHIVE_B_FILE_NAME: &str = "archive-b.zip"; static CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY: &str = "zones"; From df06d6361ec58c75a48f09266a3dd33ed19f915b Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 2 Apr 2025 14:50:55 -0700 Subject: [PATCH 3/7] preserve formatting in some Display impls --- artifact/src/artifact.rs | 2 +- artifact/src/kind.rs | 2 +- artifact/src/version.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/artifact/src/artifact.rs b/artifact/src/artifact.rs index 6036637..771763f 100644 --- a/artifact/src/artifact.rs +++ b/artifact/src/artifact.rs @@ -82,7 +82,7 @@ impl fmt::Debug for ArtifactHash { impl fmt::Display for ArtifactHash { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&hex::encode(self.0)) + hex::encode(self.0).fmt(f) } } diff --git a/artifact/src/kind.rs b/artifact/src/kind.rs index 6ef13ed..c165611 100644 --- a/artifact/src/kind.rs +++ b/artifact/src/kind.rs @@ -144,7 +144,7 @@ impl From for ArtifactKind { impl fmt::Display for ArtifactKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) + self.0.fmt(f) } } diff --git a/artifact/src/version.rs b/artifact/src/version.rs index 3ae9571..94093e7 100644 --- a/artifact/src/version.rs +++ b/artifact/src/version.rs @@ -106,7 +106,7 @@ impl FromStr for ArtifactVersion { impl fmt::Display for ArtifactVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) + self.as_str().fmt(f) } } From 724cf3e37812b43c2a03cfb6f0d98a615400b419 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 2 Apr 2025 15:23:48 -0700 Subject: [PATCH 4/7] add RoT bootloader artifacts --- bin/src/dispatch.rs | 182 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 140 insertions(+), 42 deletions(-) diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs index 105a9b6..01dc5b5 100644 --- a/bin/src/dispatch.rs +++ b/bin/src/dispatch.rs @@ -175,9 +175,11 @@ impl Args { Ok(()) } - Command::Show => show(log, &repo_path).await.with_context(|| { - format!("error showing repository at `{repo_path}`") - }), + Command::Artifacts => { + show_artifacts(log, &repo_path).await.with_context(|| { + format!("error showing repository at `{repo_path}`") + }) + } Command::Assemble { manifest_path, output_path, @@ -272,8 +274,8 @@ enum Command { /// The destination to extract the file to. dest: Utf8PathBuf, }, - /// Summarizes the contents of an Omicron TUF repository - Show, + /// Summarizes the artifacts of an Omicron TUF repository + Artifacts, /// Assembles a repository from a provided manifest. Assemble { /// Path to artifact manifest. @@ -325,18 +327,12 @@ struct ArtifactInfo { } enum ArtifactInfoDetails { - SpHubrisImage(CommonCabooseInfo), + SpHubrisArchive(CommonCabooseInfo), RotArtifact { a: RotCabooseInfo, b: RotCabooseInfo }, + RotBootloaderArchive(RotCabooseInfo), NoDetails, } -#[derive(Ord, PartialOrd, Eq, PartialEq)] -enum ArtifactFileKind { - SpHubrisImage, - RotArtifact, - Other, -} - struct CommonCabooseInfo { board: String, git_commit: String, @@ -346,13 +342,19 @@ struct CommonCabooseInfo { struct RotCabooseInfo { board: String, + // We don't currently use this but it's here for consistency and in case we + // need it in the future. + #[allow(dead_code)] git_commit: String, version: String, name: String, sign: String, } -async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { +async fn show_artifacts( + log: &slog::Logger, + repo_path: &Utf8Path, +) -> Result<()> { let omicron_repo = OmicronRepo::load_untrusted_ignore_expiration(log, &repo_path) .await @@ -364,41 +366,51 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { .context("reading artifacts document")?; println!("system version: {}", artifacts_document.system_version); - let mut all_artifacts = BTreeMap::new(); + let mut sp_artifacts = Vec::new(); + let mut rot_artifacts = Vec::new(); + let mut other_artifacts = Vec::new(); + let mut rot_bootloader_artifacts = Vec::new(); + for artifact_metadata in &artifacts_document.artifacts { eprintln!("loading artifact {}", artifact_metadata.target); let known_artifact_kind = artifact_metadata.kind.to_known().ok_or_else(|| { anyhow!("unknown artifact kind: {}", &artifact_metadata.kind) })?; - let (details, file_kind) = match known_artifact_kind { + let (details, list) = match known_artifact_kind { KnownArtifactKind::GimletSp | KnownArtifactKind::PscSp | KnownArtifactKind::SwitchSp => ( // This target is itself a Hubris archive. - ArtifactInfoDetails::SpHubrisImage( + ArtifactInfoDetails::SpHubrisArchive( load_caboose(&artifact_metadata.target, tuf_repo).await?, ), - ArtifactFileKind::SpHubrisImage, + &mut sp_artifacts, ), KnownArtifactKind::GimletRot | KnownArtifactKind::PscRot | KnownArtifactKind::SwitchRot => { let details = load_rot(&artifact_metadata.target, tuf_repo).await?; - (details, ArtifactFileKind::RotArtifact) + (details, &mut rot_artifacts) } KnownArtifactKind::GimletRotBootloader | KnownArtifactKind::PscRotBootloader - | KnownArtifactKind::SwitchRotBootloader => { - // XXX-dap - (ArtifactInfoDetails::NoDetails, ArtifactFileKind::Other) - } + | KnownArtifactKind::SwitchRotBootloader => ( + ArtifactInfoDetails::RotBootloaderArchive( + load_rot_bootloader_caboose( + &artifact_metadata.target, + tuf_repo, + ) + .await?, + ), + &mut rot_bootloader_artifacts, + ), KnownArtifactKind::Host | KnownArtifactKind::Trampoline | KnownArtifactKind::ControlPlane | KnownArtifactKind::Zone => { - (ArtifactInfoDetails::NoDetails, ArtifactFileKind::Other) + (ArtifactInfoDetails::NoDetails, &mut other_artifacts) } }; @@ -410,20 +422,13 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { details, }; - all_artifacts - .entry(file_kind) - .or_insert_with(Vec::new) - .push(artifact_info); + list.push(artifact_info); } println!("SP Artifacts (Hubris archives)\n"); println!(" {:37} {:9} {:13} {:7}", "TARGET", "KIND", "NAME", "VERSION"); - for artifact_info in all_artifacts - .get(&ArtifactFileKind::SpHubrisImage) - .into_iter() - .flatten() - { - let ArtifactInfoDetails::SpHubrisImage(caboose_info) = + for artifact_info in &sp_artifacts { + let ArtifactInfoDetails::SpHubrisArchive(caboose_info) = &artifact_info.details else { panic!("internal type mismatch"); @@ -516,14 +521,92 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { .into_iter() .collect(); + println!("\nRoT Bootloader Artifacts\n"); + println!( + " {:75} {:21} {:40} {:7} {:25}", + "TARGET", "KIND", "NAME", "VERSION", "SIGNING KEY" + ); + for artifact_info in &rot_bootloader_artifacts { + let ArtifactInfoDetails::RotBootloaderArchive(rot_caboose) = + &artifact_info.details + else { + panic!("internal type mismatch"); + }; + + let key_name = + known_signature_names.get(rot_caboose.sign.as_str()).map(|s| *s); + + // Only print fields that we don't expect are duplicated or otherwise + // uninteresting (like the Git commit). If we're wrong about these + // being duplicated, we'll print a warning below. + println!( + " {:75} {:>21} {:40} {:>7} {:25}", + artifact_info.artifact_target, + artifact_info.artifact_kind, + artifact_info.artifact_name, + artifact_info.artifact_version, + key_name.unwrap_or("UNKNOWN"), + ); + + if rot_caboose.board != rot_caboose.name { + eprintln!( + "warning: expected caboose \"board\" and \"name\" \ + to be the same, but they're not" + ); + } + + if key_name.is_none() { + eprintln!("warning: unrecognized \"sign\""); + } + + if rot_caboose.version.to_string() + != artifact_info.artifact_version.as_str() + { + eprintln!( + "warning: caboose version {} does not match \ + artifact version {}", + rot_caboose.version, artifact_info.artifact_version + ); + } + + // XXX-dap + // See the similar comment above. RoT images sometimes have their + // key ("selfsigned-bart", "production-release", or "staging-devel") + // tacked onto the end of the board name in their artifact name. + // XXX-dap verify my assumptions here + // if caboose_info.board != artifact_info.artifact_name + // && format!("{}-lab", caboose_info.board) + // != artifact_info.artifact_name + // { + // eprintln!( + // "warning: target {}: caboose board {} does not match \ + // artifact name {}", + // artifact_info.artifact_target, + // caboose_info.board, + // artifact_info.artifact_name, + // ); + // } + + // XXX-dap + // See above comment. Similarly, the artifact name sometimes has + // keys appended to it. + // if caboose_info.name != artifact_info.artifact_name { + // eprintln!( + // "warning: target {}: caboose name {} does not match \ + // artifact name {}", + // artifact_info.artifact_target, + // caboose_info.name, + // artifact_info.artifact_name, + // ); + // } + } + println!("\nRoT Artifacts (composite artifacts with two Hubris images)\n"); println!( " {:61} {:10} {:36} {:7} {:25}", "TARGET", "KIND", "NAME", "VERSION", "SIGNING KEY" ); - for artifact_info in - all_artifacts.get(&ArtifactFileKind::RotArtifact).into_iter().flatten() - { + for artifact_info in &rot_artifacts { let ArtifactInfoDetails::RotArtifact { a, b } = &artifact_info.details else { panic!("internal type mismatch"); @@ -615,14 +698,12 @@ async fn show(log: &slog::Logger, repo_path: &Utf8Path) -> Result<()> { println!("\nOther artifacts\n"); println!( - " {:75} {:21} {:40} {:26}", + " {:61} {:13} {:13} {:26}", "TARGET", "KIND", "NAME", "VERSION" ); - for artifact_info in - all_artifacts.get(&ArtifactFileKind::Other).into_iter().flatten() - { + for artifact_info in &other_artifacts { println!( - " {:75} {:21} {:40} {:26}", + " {:61} {:13} {:13} {:26}", artifact_info.artifact_target, artifact_info.artifact_kind, artifact_info.artifact_name, @@ -686,6 +767,23 @@ fn load_caboose_common_fields( Ok(CommonCabooseInfo { board, git_commit, version, name }) } +async fn load_rot_bootloader_caboose( + target_name: &str, + tuf_repo: &Repository, +) -> Result { + load_rot_bootloader_caboose_impl(target_name, tuf_repo).await.with_context( + || format!("loading caboose for target {:?}", target_name), + ) +} + +async fn load_rot_bootloader_caboose_impl( + target_name: &str, + tuf_repo: &Repository, +) -> Result { + let v = load_target_bytes(target_name, tuf_repo).await?; + load_rot_caboose_from_archive_bytes(v).await +} + async fn load_rot_caboose_from_archive_bytes( v: Vec, ) -> Result { From 9299a0f5d5ad42050ba880e113b501c1f3787d84 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 2 Apr 2025 15:30:24 -0700 Subject: [PATCH 5/7] nit --- bin/src/dispatch.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs index 01dc5b5..a3a3db1 100644 --- a/bin/src/dispatch.rs +++ b/bin/src/dispatch.rs @@ -364,7 +364,6 @@ async fn show_artifacts( .read_artifacts() .await .context("reading artifacts document")?; - println!("system version: {}", artifacts_document.system_version); let mut sp_artifacts = Vec::new(); let mut rot_artifacts = Vec::new(); @@ -425,7 +424,9 @@ async fn show_artifacts( list.push(artifact_info); } - println!("SP Artifacts (Hubris archives)\n"); + println!("System Version: {}", artifacts_document.system_version); + + println!("\nSP Artifacts (Hubris archives)\n"); println!(" {:37} {:9} {:13} {:7}", "TARGET", "KIND", "NAME", "VERSION"); for artifact_info in &sp_artifacts { let ArtifactInfoDetails::SpHubrisArchive(caboose_info) = @@ -601,7 +602,9 @@ async fn show_artifacts( // } } - println!("\nRoT Artifacts (composite artifacts with two Hubris images)\n"); + println!( + "\nRoT Artifacts (composite artifacts with two Hubris archives)\n" + ); println!( " {:61} {:10} {:36} {:7} {:25}", "TARGET", "KIND", "NAME", "VERSION", "SIGNING KEY" From bea040b49ba122336f5094923c0c0156165a0d3d Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 2 Apr 2025 15:34:03 -0700 Subject: [PATCH 6/7] clippy --- bin/src/dispatch.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs index a3a3db1..b3bb2f4 100644 --- a/bin/src/dispatch.rs +++ b/bin/src/dispatch.rs @@ -356,7 +356,7 @@ async fn show_artifacts( repo_path: &Utf8Path, ) -> Result<()> { let omicron_repo = - OmicronRepo::load_untrusted_ignore_expiration(log, &repo_path) + OmicronRepo::load_untrusted_ignore_expiration(log, repo_path) .await .context("loading repository")?; let tuf_repo = omicron_repo.repo(); @@ -446,9 +446,7 @@ async fn show_artifacts( artifact_info.artifact_version, ); - if caboose_info.version.to_string() - != artifact_info.artifact_version.as_str() - { + if caboose_info.version != artifact_info.artifact_version.as_str() { eprintln!( "warning: target {}: caboose version {} does not match \ artifact version {}", @@ -535,7 +533,7 @@ async fn show_artifacts( }; let key_name = - known_signature_names.get(rot_caboose.sign.as_str()).map(|s| *s); + known_signature_names.get(rot_caboose.sign.as_str()).copied(); // Only print fields that we don't expect are duplicated or otherwise // uninteresting (like the Git commit). If we're wrong about these @@ -560,9 +558,7 @@ async fn show_artifacts( eprintln!("warning: unrecognized \"sign\""); } - if rot_caboose.version.to_string() - != artifact_info.artifact_version.as_str() - { + if rot_caboose.version != artifact_info.artifact_version.as_str() { eprintln!( "warning: caboose version {} does not match \ artifact version {}", @@ -615,7 +611,7 @@ async fn show_artifacts( panic!("internal type mismatch"); }; - let key_name = known_signature_names.get(a.sign.as_str()).map(|s| *s); + let key_name = known_signature_names.get(a.sign.as_str()).copied(); // Only print fields that we don't expect are duplicated or otherwise // uninteresting (like the Git commit). If we're wrong about these @@ -654,9 +650,7 @@ async fn show_artifacts( } for caboose_info in [a, b] { - if caboose_info.version.to_string() - != artifact_info.artifact_version.as_str() - { + if caboose_info.version != artifact_info.artifact_version.as_str() { eprintln!( "warning: target {}: caboose version {} does not match \ artifact version {}", From b43764d0bf743e0476d6d6d52ce534b3752d4943 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Wed, 2 Apr 2025 15:50:09 -0700 Subject: [PATCH 7/7] check another inference --- bin/src/dispatch.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bin/src/dispatch.rs b/bin/src/dispatch.rs index b3bb2f4..07ccfe3 100644 --- a/bin/src/dispatch.rs +++ b/bin/src/dispatch.rs @@ -421,6 +421,20 @@ async fn show_artifacts( details, }; + if artifact_metadata.target + != format!( + "{}-{}-{}.tar.gz", + artifact_metadata.kind, + artifact_metadata.name, + artifact_metadata.version + ) + { + eprintln!( + "warning: expected artifact target name to be \ + KIND-NAME-VERSION.tar.gz" + ); + } + list.push(artifact_info); }