From eae82d4125a356496cad77f5a5840df284ff20b6 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 20 May 2025 03:13:43 +0000 Subject: [PATCH] [spr] changes to main this commit is based on Created using spr 1.3.6-beta.1 [skip ci] --- Cargo.lock | 1 + common/src/update/mupdate_override.rs | 35 +++++++++++- installinator/Cargo.toml | 1 + installinator/src/write.rs | 57 ++++++++++++++++--- .../zone-images/src/mupdate_override.rs | 2 + wicketd/tests/integration_tests/updates.rs | 12 ++++ 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e560f69f8d..5f2bf894c0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4796,6 +4796,7 @@ dependencies = [ "hex", "hex-literal", "http", + "id-map", "illumos-utils", "installinator-client", "installinator-common", diff --git a/common/src/update/mupdate_override.rs b/common/src/update/mupdate_override.rs index e657320e0c7..2a679f5b5ee 100644 --- a/common/src/update/mupdate_override.rs +++ b/common/src/update/mupdate_override.rs @@ -6,9 +6,10 @@ use std::collections::BTreeSet; +use id_map::{IdMap, IdMappable}; use omicron_uuid_kinds::MupdateOverrideUuid; use serde::{Deserialize, Serialize}; -use tufaceous_artifact::ArtifactHashId; +use tufaceous_artifact::{ArtifactHash, ArtifactHashId}; /// MUPdate override information, typically serialized as JSON (RFD 556). /// @@ -20,10 +21,42 @@ pub struct MupdateOverrideInfo { pub mupdate_uuid: MupdateOverrideUuid, /// Artifact hashes written out to the install dataset. + /// + /// Currently includes the host phase 2 and composite control plane + /// artifacts. Information about individual zones is included in + /// [`Self::zones`]. pub hash_ids: BTreeSet, + + /// Control plane zone file names and hashes. + pub zones: IdMap, } impl MupdateOverrideInfo { /// The name of the file on the install dataset. pub const FILE_NAME: &'static str = "mupdate-override.json"; } + +/// Control plane zone information written out to the install dataset. +/// +/// Part of [`MupdateOverrideInfo`]. +#[derive( + Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize, +)] +pub struct MupdateOverrideZone { + /// The file name. + pub file_name: String, + + /// The file size. + pub file_size: u64, + + /// The hash of the file. + pub hash: ArtifactHash, +} + +impl IdMappable for MupdateOverrideZone { + type Id = String; + + fn id(&self) -> Self::Id { + self.file_name.clone() + } +} diff --git a/installinator/Cargo.toml b/installinator/Cargo.toml index 68bf11b584d..f9b57abbd45 100644 --- a/installinator/Cargo.toml +++ b/installinator/Cargo.toml @@ -21,6 +21,7 @@ display-error-chain.workspace = true futures.workspace = true hex.workspace = true http.workspace = true +id-map.workspace = true illumos-utils.workspace = true installinator-client.workspace = true installinator-common.workspace = true diff --git a/installinator/src/write.rs b/installinator/src/write.rs index 84b88a5cb8a..da5a0a49cd7 100644 --- a/installinator/src/write.rs +++ b/installinator/src/write.rs @@ -12,21 +12,26 @@ use std::{ use anyhow::{Context, Result, anyhow, ensure}; use async_trait::async_trait; use buf_list::BufList; -use bytes::Buf; +use bytes::{Buf, Bytes}; use camino::{Utf8Path, Utf8PathBuf}; +use id_map::IdMap; use illumos_utils::zpool::{Zpool, ZpoolName}; use installinator_common::{ ControlPlaneZonesSpec, ControlPlaneZonesStepId, RawDiskWriter, StepContext, StepProgress, StepResult, StepSuccess, UpdateEngine, WriteComponent, WriteError, WriteOutput, WriteSpec, WriteStepId, }; -use omicron_common::{disk::M2Slot, update::MupdateOverrideInfo}; +use omicron_common::{ + disk::M2Slot, + update::{MupdateOverrideInfo, MupdateOverrideZone}, +}; use omicron_uuid_kinds::MupdateOverrideUuid; use sha2::{Digest, Sha256}; use slog::{Logger, info, warn}; use tokio::{ fs::File, io::{AsyncWrite, AsyncWriteExt}, + task::JoinSet, }; use tufaceous_artifact::{ArtifactHash, ArtifactHashId}; use tufaceous_lib::ControlPlaneZoneImages; @@ -678,7 +683,7 @@ impl ControlPlaneZoneWriteContext<'_> { async move |cx| { let transport = transport.into_value(cx.token()).await; let mupdate_json = - self.mupdate_override_artifact(mupdate_uuid); + self.mupdate_override_artifact(mupdate_uuid).await; let out_path = self .output_directory @@ -759,23 +764,61 @@ impl ControlPlaneZoneWriteContext<'_> { .register(); } - fn mupdate_override_artifact( + async fn mupdate_override_artifact( &self, mupdate_uuid: MupdateOverrideUuid, ) -> BufList { - // Might be worth writing out individual hash IDs for each zone in the - // future. let hash_ids = [self.host_phase_2_id.clone(), self.control_plane_id.clone()] .into_iter() .collect(); - let mupdate_override = MupdateOverrideInfo { mupdate_uuid, hash_ids }; + let zones = compute_zone_hashes(&self.zones).await; + + let mupdate_override = + MupdateOverrideInfo { mupdate_uuid, hash_ids, zones }; let json_bytes = serde_json::to_vec(&mupdate_override) .expect("this serialization is infallible"); BufList::from(json_bytes) } } +/// Computes the zone hash IDs. +/// +/// Hash computation is done in parallel on blocking tasks. +/// +/// # Panics +/// +/// Panics if the runtime shuts down causing a task abort, or a task panics. +async fn compute_zone_hashes( + images: &ControlPlaneZoneImages, +) -> IdMap { + let mut tasks = JoinSet::new(); + for (file_name, data) in &images.zones { + let file_name = file_name.clone(); + // data is a Bytes so is cheap to clone. + let data: Bytes = data.clone(); + // Compute hashes in parallel. + tasks.spawn_blocking(move || { + let mut hasher = Sha256::new(); + hasher.update(&data); + let hash = hasher.finalize(); + MupdateOverrideZone { + file_name, + file_size: u64::try_from(data.len()).unwrap(), + hash: ArtifactHash(hash.into()), + } + }); + } + + let mut output = IdMap::new(); + while let Some(res) = tasks.join_next().await { + // Propagate panics across tasks—this is the standard pattern we follow + // in installinator. + output.insert(res.expect("task panicked")); + } + output +} + fn remove_contents_of(path: &Utf8Path) -> io::Result<()> { use std::fs; diff --git a/sled-agent/zone-images/src/mupdate_override.rs b/sled-agent/zone-images/src/mupdate_override.rs index 0b2337f3110..4dd2a2f4d8e 100644 --- a/sled-agent/zone-images/src/mupdate_override.rs +++ b/sled-agent/zone-images/src/mupdate_override.rs @@ -970,6 +970,7 @@ mod tests { MupdateOverrideInfo { mupdate_uuid: OVERRIDE_UUID, hash_ids: BTreeSet::new(), + zones: IdMap::new(), } } @@ -977,6 +978,7 @@ mod tests { MupdateOverrideInfo { mupdate_uuid: OVERRIDE_2_UUID, hash_ids: BTreeSet::new(), + zones: IdMap::new(), } } diff --git a/wicketd/tests/integration_tests/updates.rs b/wicketd/tests/integration_tests/updates.rs index 16bf3927f7b..b8ee985f50f 100644 --- a/wicketd/tests/integration_tests/updates.rs +++ b/wicketd/tests/integration_tests/updates.rs @@ -447,6 +447,18 @@ async fn test_installinator_fetch() { "mupdate override info matches across A and B drives", ); + // Check that the zone1 and zone2 images are present in the zone set. (The + // names come from fake-non-semver.toml, under + // [artifact.control-plane.source]). + assert!( + a_override_info.zones.contains_key("zone1.tar.gz"), + "zone1 is present in the zone set" + ); + assert!( + a_override_info.zones.contains_key("zone2.tar.gz"), + "zone2 is present in the zone set" + ); + recv_handle.await.expect("recv_handle succeeded"); wicketd_testctx.teardown().await;