From 21b1d0b022fab7dc2debf709c6a5f6c9f02dfd1f Mon Sep 17 00:00:00 2001 From: Laura Abbott Date: Tue, 9 Sep 2025 23:02:48 +0000 Subject: [PATCH] Make Host Phase 1 Cosmo aware Cosmo sleds need a separate host phase 1 image. Make the host extraction aware of this. --- artifact/src/kind.rs | 23 ++++-- bin/manifests/fake-non-semver.toml | 6 +- bin/manifests/fake.toml | 6 +- lib/src/artifact.rs | 107 +++++++++++++++++++++------ lib/src/artifact/composite.rs | 16 +++-- lib/src/assemble/manifest.rs | 112 +++++++++++++++++++++-------- 6 files changed, 208 insertions(+), 62 deletions(-) diff --git a/artifact/src/kind.rs b/artifact/src/kind.rs index 4157645..d89415d 100644 --- a/artifact/src/kind.rs +++ b/artifact/src/kind.rs @@ -112,21 +112,34 @@ impl ArtifactKind { pub const SWITCH_ROT_IMAGE_B: Self = Self::from_static("switch_rot_image_b"); - /// Host phase 1 identifier. + /// Gimlet Host phase 1 identifier. /// /// Derived from [`KnownArtifactKind::Host`]. - pub const HOST_PHASE_1: Self = Self::from_static("host_phase_1"); + pub const GIMLET_HOST_PHASE_1: Self = + Self::from_static("gimlet_host_phase_1"); + + /// Cosmo Host phase 1 identifier. + /// + /// Derived from [`KnownArtifactKind::Host`]. + pub const COSMO_HOST_PHASE_1: Self = + Self::from_static("cosmo_host_phase_1"); /// Host phase 2 identifier. /// /// Derived from [`KnownArtifactKind::Host`]. pub const HOST_PHASE_2: Self = Self::from_static("host_phase_2"); - /// Trampoline phase 1 identifier. + /// Gimlet Trampoline phase 1 identifier. + /// + /// Derived from [`KnownArtifactKind::Trampoline`]. + pub const GIMLET_TRAMPOLINE_PHASE_1: Self = + Self::from_static("gimlet_trampoline_phase_1"); + + /// Cosmo Trampoline phase 1 identifier. /// /// Derived from [`KnownArtifactKind::Trampoline`]. - pub const TRAMPOLINE_PHASE_1: Self = - Self::from_static("trampoline_phase_1"); + pub const COSMO_TRAMPOLINE_PHASE_1: Self = + Self::from_static("cosmo_trampoline_phase_1"); /// Trampoline phase 2 identifier. /// diff --git a/bin/manifests/fake-non-semver.toml b/bin/manifests/fake-non-semver.toml index c4d57de..33f5be3 100644 --- a/bin/manifests/fake-non-semver.toml +++ b/bin/manifests/fake-non-semver.toml @@ -24,7 +24,8 @@ name = "fake-host" version = "2.0.0" [artifact.host.source] kind = "composite-host" -phase_1 = { kind = "fake", size = "512KiB" } +gimlet_phase_1 = { kind = "fake", size = "512KiB" } +cosmo_phase_1 = { kind = "fake", size = "512KiB" } phase_2 = { kind = "fake", size = "1MiB" } [[artifact.trampoline]] @@ -32,7 +33,8 @@ name = "fake-trampoline" version = "non-semver" [artifact.trampoline.source] kind = "composite-host" -phase_1 = { kind = "fake", size = "512KiB" } +gimlet_phase_1 = { kind = "fake", size = "512KiB" } +cosmo_phase_1 = { kind = "fake", size = "512KiB" } phase_2 = { kind = "fake", size = "1MiB" } [[artifact.control_plane]] diff --git a/bin/manifests/fake.toml b/bin/manifests/fake.toml index 79c8441..1cffc56 100644 --- a/bin/manifests/fake.toml +++ b/bin/manifests/fake.toml @@ -22,7 +22,8 @@ name = "fake-host" version = "1.0.0" [artifact.host.source] kind = "composite-host" -phase_1 = { kind = "fake", size = "512KiB" } +gimlet_phase_1 = { kind = "fake", size = "512KiB" } +cosmo_phase_1 = { kind = "fake", size = "512KiB" } phase_2 = { kind = "fake", size = "1MiB" } [[artifact.trampoline]] @@ -30,7 +31,8 @@ name = "fake-trampoline" version = "1.0.0" [artifact.trampoline.source] kind = "composite-host" -phase_1 = { kind = "fake", size = "512KiB" } +gimlet_phase_1 = { kind = "fake", size = "512KiB" } +cosmo_phase_1 = { kind = "fake", size = "512KiB" } phase_2 = { kind = "fake", size = "1MiB" } [[artifact.control_plane]] diff --git a/lib/src/artifact.rs b/lib/src/artifact.rs index baf7bfb..5848fb6 100644 --- a/lib/src/artifact.rs +++ b/lib/src/artifact.rs @@ -220,6 +220,30 @@ impl TempWrittenArtifact { } } +pub(crate) fn make_filler_text_with_seed( + // composite artifact. + kind: &str, + version: &ArtifactVersion, + length: usize, + seed: &str, +) -> Vec { + // Add the kind and version to the filler text first. This ensures that + // hashes are unique by kind and version. + let mut out = Vec::with_capacity(length); + out.extend_from_slice(kind.as_bytes()); + out.extend_from_slice(b":"); + out.extend_from_slice(version.as_str().as_bytes()); + out.extend_from_slice(b":"); + out.extend_from_slice(seed.as_bytes()); + out.extend_from_slice(b":"); + let remaining = length.saturating_sub(out.len()); + out.extend( + std::iter::repeat(FILLER_TEXT).flatten().copied().take(remaining), + ); + + out +} + pub(crate) fn make_filler_text( // composite artifact. kind: &str, @@ -249,33 +273,52 @@ pub(crate) fn make_filler_text( /// tarballs. #[derive(Clone, Debug)] pub struct HostPhaseImages { - pub phase_1: Bytes, + pub gimlet_phase_1: Bytes, + pub cosmo_phase_1: Bytes, pub phase_2: Bytes, } +/// File sources for extraction +/// +/// Passing three identical arguments gets confusing and error prone +pub struct HostPhaseImageSource { + pub gimlet_phase_1: W, + pub cosmo_phase_1: W, + pub phase_2: W, +} + impl HostPhaseImages { pub fn extract(reader: R) -> Result { - let mut phase_1 = Vec::new(); + let mut gimlet_phase_1 = Vec::new(); + let mut cosmo_phase_1 = Vec::new(); let mut phase_2 = Vec::new(); - Self::extract_into( - reader, - io::Cursor::<&mut Vec>::new(&mut phase_1), - io::Cursor::<&mut Vec>::new(&mut phase_2), - )?; - Ok(Self { phase_1: phase_1.into(), phase_2: phase_2.into() }) + let source = HostPhaseImageSource { + gimlet_phase_1: io::Cursor::<&mut Vec>::new( + &mut gimlet_phase_1, + ), + cosmo_phase_1: io::Cursor::<&mut Vec>::new(&mut cosmo_phase_1), + phase_2: io::Cursor::<&mut Vec>::new(&mut phase_2), + }; + + Self::extract_into(reader, source)?; + Ok(Self { + gimlet_phase_1: gimlet_phase_1.into(), + cosmo_phase_1: cosmo_phase_1.into(), + phase_2: phase_2.into(), + }) } pub fn extract_into( reader: R, - phase_1: W, - phase_2: W, + source: HostPhaseImageSource, ) -> Result<()> { let uncompressed = flate2::bufread::GzDecoder::new(reader); let mut archive = tar::Archive::new(uncompressed); let mut oxide_json_found = false; - let mut phase_1_writer = Some(phase_1); - let mut phase_2_writer = Some(phase_2); + let mut gimlet_phase_1_writer = Some(source.gimlet_phase_1); + let mut cosmo_phase_1_writer = Some(source.cosmo_phase_1); + let mut phase_2_writer = Some(source.phase_2); for entry in archive .entries() .context("error building list of entries from archive")? @@ -300,9 +343,21 @@ impl HostPhaseImages { ) } oxide_json_found = true; - } else if path == Path::new(HOST_PHASE_1_FILE_NAME) { - if let Some(phase_1) = phase_1_writer.take() { - read_entry_into(entry, HOST_PHASE_1_FILE_NAME, phase_1)?; + } else if path == Path::new(COSMO_HOST_PHASE_1_FILE_NAME) { + if let Some(cosmo_phase_1) = cosmo_phase_1_writer.take() { + read_entry_into( + entry, + COSMO_HOST_PHASE_1_FILE_NAME, + cosmo_phase_1, + )?; + } + } else if path == Path::new(GIMLET_HOST_PHASE_1_FILE_NAME) { + if let Some(gimlet_phase_1) = gimlet_phase_1_writer.take() { + read_entry_into( + entry, + GIMLET_HOST_PHASE_1_FILE_NAME, + gimlet_phase_1, + )?; } } else if path == Path::new(HOST_PHASE_2_FILE_NAME) { if let Some(phase_2) = phase_2_writer.take() { @@ -311,7 +366,8 @@ impl HostPhaseImages { } if oxide_json_found - && phase_1_writer.is_none() + && gimlet_phase_1_writer.is_none() + && cosmo_phase_1_writer.is_none() && phase_2_writer.is_none() { break; @@ -325,8 +381,11 @@ impl HostPhaseImages { // If we didn't `.take()` the writer out of the options, we never saw // the expected phase1/phase2 filenames. - if phase_1_writer.is_some() { - not_found.push(HOST_PHASE_1_FILE_NAME); + if cosmo_phase_1_writer.is_some() { + not_found.push(COSMO_HOST_PHASE_1_FILE_NAME); + } + if gimlet_phase_1_writer.is_some() { + not_found.push(GIMLET_HOST_PHASE_1_FILE_NAME); } if phase_2_writer.is_some() { not_found.push(HOST_PHASE_2_FILE_NAME); @@ -339,8 +398,13 @@ impl HostPhaseImages { Ok(()) } - pub fn phase_1_hash(&self) -> ArtifactHash { - let hash = Sha256::digest(&self.phase_1); + pub fn gimlet_phase_1_hash(&self) -> ArtifactHash { + let hash = Sha256::digest(&self.gimlet_phase_1); + ArtifactHash(hash.into()) + } + + pub fn cosmo_phase_1_hash(&self) -> ArtifactHash { + let hash = Sha256::digest(&self.cosmo_phase_1); ArtifactHash(hash.into()) } @@ -559,7 +623,8 @@ impl ControlPlaneZoneImages { static FILLER_TEXT: &[u8; 16] = b"tufaceousfaketxt"; static OXIDE_JSON_FILE_NAME: &str = "oxide.json"; -pub(crate) static HOST_PHASE_1_FILE_NAME: &str = "image/rom"; +pub(crate) static GIMLET_HOST_PHASE_1_FILE_NAME: &str = "image/gimlet.rom"; +pub(crate) static COSMO_HOST_PHASE_1_FILE_NAME: &str = "image/cosmo.rom"; pub(crate) static HOST_PHASE_2_FILE_NAME: &str = "image/zfs.img"; pub(crate) static ROT_ARCHIVE_A_FILE_NAME: &str = "archive-a.zip"; pub(crate) static ROT_ARCHIVE_B_FILE_NAME: &str = "archive-b.zip"; diff --git a/lib/src/artifact/composite.rs b/lib/src/artifact/composite.rs index 226907d..3a7eab6 100644 --- a/lib/src/artifact/composite.rs +++ b/lib/src/artifact/composite.rs @@ -13,8 +13,9 @@ use tufaceous_artifact::ArtifactHash; use tufaceous_brand_metadata::{ArchiveType, Metadata}; use super::{ - CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY, HOST_PHASE_1_FILE_NAME, - HOST_PHASE_2_FILE_NAME, ROT_ARCHIVE_A_FILE_NAME, ROT_ARCHIVE_B_FILE_NAME, + CONTROL_PLANE_ARCHIVE_ZONE_DIRECTORY, COSMO_HOST_PHASE_1_FILE_NAME, + GIMLET_HOST_PHASE_1_FILE_NAME, HOST_PHASE_2_FILE_NAME, + ROT_ARCHIVE_A_FILE_NAME, ROT_ARCHIVE_B_FILE_NAME, }; /// Represents a single entry in a composite artifact. @@ -104,11 +105,18 @@ impl CompositeHostArchiveBuilder { Ok(Self { inner }) } - pub fn append_phase_1( + pub fn append_gimlet_phase_1( &mut self, entry: CompositeEntry<'_>, ) -> Result { - self.inner.append_file(HOST_PHASE_1_FILE_NAME, entry) + self.inner.append_file(GIMLET_HOST_PHASE_1_FILE_NAME, entry) + } + + pub fn append_cosmo_phase_1( + &mut self, + entry: CompositeEntry<'_>, + ) -> Result { + self.inner.append_file(COSMO_HOST_PHASE_1_FILE_NAME, entry) } pub fn append_phase_2( diff --git a/lib/src/assemble/manifest.rs b/lib/src/assemble/manifest.rs index e7a1f05..39ceb1a 100644 --- a/lib/src/assemble/manifest.rs +++ b/lib/src/assemble/manifest.rs @@ -17,11 +17,12 @@ use tufaceous_artifact::{ArtifactKind, ArtifactVersion, KnownArtifactKind}; use crate::assemble::{DeploymentUnitData, DeploymentUnitScope}; use crate::{ - ArtifactSource, CompositeControlPlaneArchiveBuilder, CompositeEntry, + ArtifactSource, COSMO_HOST_PHASE_1_FILE_NAME, + CompositeControlPlaneArchiveBuilder, CompositeEntry, CompositeHostArchiveBuilder, CompositeRotArchiveBuilder, - HOST_PHASE_1_FILE_NAME, HOST_PHASE_2_FILE_NAME, HostPhaseImages, + GIMLET_HOST_PHASE_1_FILE_NAME, HOST_PHASE_2_FILE_NAME, HostPhaseImages, MtimeSource, ROT_ARCHIVE_A_FILE_NAME, ROT_ARCHIVE_B_FILE_NAME, - make_filler_text, + make_filler_text, make_filler_text_with_seed, }; use super::{ArtifactDeploymentUnits, DeploymentUnitMapBuilder}; @@ -121,12 +122,23 @@ impl ArtifactManifest { ); data_builder .insert(DeploymentUnitData { - name: HOST_PHASE_1_FILE_NAME.to_owned(), + name: COSMO_HOST_PHASE_1_FILE_NAME + .to_owned(), version: artifact_data.version.clone(), - kind: ArtifactKind::HOST_PHASE_1, - hash: images.phase_1_hash(), + kind: ArtifactKind::COSMO_HOST_PHASE_1, + hash: images.cosmo_phase_1_hash(), }) .expect("unique kind"); + data_builder + .insert(DeploymentUnitData { + name: GIMLET_HOST_PHASE_1_FILE_NAME + .to_owned(), + version: artifact_data.version.clone(), + kind: ArtifactKind::GIMLET_HOST_PHASE_1, + hash: images.gimlet_phase_1_hash(), + }) + .expect("unique kind"); + data_builder .insert(DeploymentUnitData { name: HOST_PHASE_2_FILE_NAME.to_owned(), @@ -164,7 +176,8 @@ impl ArtifactManifest { ) } DeserializedArtifactSource::CompositeHost { - phase_1, + gimlet_phase_1, + cosmo_phase_1, phase_2, } => { ensure!( @@ -177,25 +190,37 @@ impl ArtifactManifest { artifact kind {kind:?}" ); - let mtime_source = - if phase_1.is_fake() && phase_2.is_fake() { - // Ensure stability of fake artifacts. - MtimeSource::Zero - } else { - MtimeSource::Now - }; + let mtime_source = if gimlet_phase_1.is_fake() + && cosmo_phase_1.is_fake() + && phase_2.is_fake() + { + // Ensure stability of fake artifacts. + MtimeSource::Zero + } else { + MtimeSource::Now + }; let mut builder = CompositeHostArchiveBuilder::new( Vec::new(), mtime_source, )?; - let phase_1_hash = phase_1.with_entry( - FakeDataAttributes::new( + let cosmo_phase_1_hash = cosmo_phase_1.with_entry( + FakeDataAttributes::new_with_seed( + kind, + &artifact_data.version, + "cosmo".to_string(), + ), + |entry| builder.append_cosmo_phase_1(entry), + )?; + let gimlet_phase_1_hash = gimlet_phase_1.with_entry( + FakeDataAttributes::new_with_seed( kind, &artifact_data.version, + "gimlet".to_string(), ), - |entry| builder.append_phase_1(entry), + |entry| builder.append_gimlet_phase_1(entry), )?; + let phase_2_hash = phase_2.with_entry( FakeDataAttributes::new( kind, @@ -213,10 +238,19 @@ impl ArtifactManifest { ); data_builder .insert(DeploymentUnitData { - name: HOST_PHASE_1_FILE_NAME.to_owned(), + name: GIMLET_HOST_PHASE_1_FILE_NAME.to_owned(), + version: artifact_data.version.clone(), + kind: ArtifactKind::GIMLET_HOST_PHASE_1, + hash: gimlet_phase_1_hash, + }) + .expect("unique kind"); + + data_builder + .insert(DeploymentUnitData { + name: COSMO_HOST_PHASE_1_FILE_NAME.to_owned(), version: artifact_data.version.clone(), - kind: ArtifactKind::HOST_PHASE_1, - hash: phase_1_hash, + kind: ArtifactKind::COSMO_HOST_PHASE_1, + hash: cosmo_phase_1_hash, }) .expect("unique kind"); data_builder @@ -416,22 +450,38 @@ impl ArtifactManifest { struct FakeDataAttributes<'a> { kind: KnownArtifactKind, version: &'a ArtifactVersion, + seed: String, } impl<'a> FakeDataAttributes<'a> { fn new(kind: KnownArtifactKind, version: &'a ArtifactVersion) -> Self { - Self { kind, version } + Self { kind, version, seed: "".to_string() } + } + + fn new_with_seed( + kind: KnownArtifactKind, + version: &'a ArtifactVersion, + seed: String, + ) -> Self { + Self { kind, version, seed } } fn make_data(&self, size: usize) -> Vec { use hubtools::{CabooseBuilder, HubrisArchiveBuilder}; let board = match self.kind { + KnownArtifactKind::Host | KnownArtifactKind::Trampoline => { + // need to use the extra seed to be able to generate different gimlet vs + // cosmo images + return make_filler_text_with_seed( + &self.kind.to_string(), + self.version, + size, + &self.seed, + ); + } // non-Hubris artifacts: just make fake data - KnownArtifactKind::Host - | KnownArtifactKind::Trampoline - | KnownArtifactKind::ControlPlane - | KnownArtifactKind::Zone => { + KnownArtifactKind::ControlPlane | KnownArtifactKind::Zone => { return make_filler_text( &self.kind.to_string(), self.version, @@ -643,7 +693,8 @@ pub enum DeserializedArtifactSource { data_version: Option, }, CompositeHost { - phase_1: DeserializedFileArtifactSource, + gimlet_phase_1: DeserializedFileArtifactSource, + cosmo_phase_1: DeserializedFileArtifactSource, phase_2: DeserializedFileArtifactSource, }, CompositeRot { @@ -665,8 +716,13 @@ impl DeserializedArtifactSource { *size = (*size).saturating_add_signed(size_delta); Ok(()) } - DeserializedArtifactSource::CompositeHost { phase_1, phase_2 } => { - phase_1.apply_size_delta(size_delta)?; + DeserializedArtifactSource::CompositeHost { + gimlet_phase_1, + cosmo_phase_1, + phase_2, + } => { + gimlet_phase_1.apply_size_delta(size_delta)?; + cosmo_phase_1.apply_size_delta(size_delta)?; phase_2.apply_size_delta(size_delta)?; Ok(()) }