diff --git a/Cargo.lock b/Cargo.lock index 453647cf5e9..73fa8567617 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..40a7808c3d5 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,39 @@ 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 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-common/src/progress.rs b/installinator-common/src/progress.rs index 0c311580c63..effcfe6bbfb 100644 --- a/installinator-common/src/progress.rs +++ b/installinator-common/src/progress.rs @@ -15,6 +15,7 @@ use schemars::{ use serde::{Deserialize, Serialize}; use serde_with::rust::deserialize_ignore_any; use thiserror::Error; +use tokio::task::JoinError; use update_engine::{AsError, StepSpec, errors::NestedEngineError}; // --- @@ -239,6 +240,8 @@ pub enum WriteError { ChecksumValidationError(#[source] anyhow::Error), #[error("error removing files from {path}: {error}")] RemoveFilesError { path: Utf8PathBuf, error: std::io::Error }, + #[error("error computing control plane hashes")] + ControlPlaneHashComputeError(#[source] JoinError), #[error("error fsyncing output directory: {error}")] SyncOutputDirError { error: std::io::Error }, #[error("error interacting with zpool: {error}")] 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 96b7c0aa49e..a615d5d4ee2 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::{JoinError, JoinSet}, }; use tufaceous_artifact::{ArtifactHash, ArtifactHashId}; use tufaceous_lib::ControlPlaneZoneImages; @@ -677,8 +682,12 @@ impl ControlPlaneZoneWriteContext<'_> { "Writing MUPdate override file", async move |cx| { let transport = transport.into_value(cx.token()).await; - let mupdate_json = - self.mupdate_override_artifact(mupdate_uuid); + let mupdate_json = self + .mupdate_override_artifact(mupdate_uuid) + .await + .map_err(|error| { + WriteError::ControlPlaneHashComputeError(error) + })?; let out_path = self .output_directory @@ -759,21 +768,51 @@ 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. + ) -> Result { 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) + Ok(BufList::from(json_bytes)) + } +} + +/// Computes the zone hash IDs. +/// +/// Hash computation is done in parallel on blocking tasks. If the runtime shuts +/// down causing a task abort, or a task panics (should not happen in normal +/// use), a `JoinError` is returned. +async fn compute_zone_hashes( + images: &ControlPlaneZoneImages, +) -> Result, JoinError> { + 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, hash: ArtifactHash(hash.into()) } + }); + } + + let mut output = IdMap::new(); + while let Some(res) = tasks.join_next().await { + output.insert(res?); } + Ok(output) } fn remove_contents_of(path: &Utf8Path) -> io::Result<()> { 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;