diff --git a/Cargo.lock b/Cargo.lock index 508a58f2cd..1b63efeb98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6336,6 +6336,7 @@ dependencies = [ "expectorate", "futures", "gateway-client", + "gateway-types", "hyper-rustls", "id-map", "iddqd", diff --git a/nexus/db-model/src/deployment.rs b/nexus/db-model/src/deployment.rs index 2c0b859d57..8daf9cb9ed 100644 --- a/nexus/db-model/src/deployment.rs +++ b/nexus/db-model/src/deployment.rs @@ -5,7 +5,7 @@ //! Types for representing the deployed software and configuration in the //! database -use crate::inventory::{SpMgsSlot, SpType, ZoneType}; +use crate::inventory::{HwRotSlot, SpMgsSlot, SpType, ZoneType}; use crate::omicron_zone_config::{self, OmicronZoneNic}; use crate::typed_uuid::DbTypedUuid; use crate::{ @@ -22,8 +22,8 @@ use nexus_db_schema::schema::{ bp_clickhouse_keeper_zone_id_to_node_id, bp_clickhouse_server_zone_id_to_node_id, bp_omicron_dataset, bp_omicron_physical_disk, bp_omicron_zone, bp_omicron_zone_nic, - bp_oximeter_read_policy, bp_pending_mgs_update_sp, bp_sled_metadata, - bp_target, + bp_oximeter_read_policy, bp_pending_mgs_update_rot, + bp_pending_mgs_update_sp, bp_sled_metadata, bp_target, }; use nexus_sled_agent_shared::inventory::OmicronZoneDataset; use nexus_types::deployment::BlueprintHostPhase2DesiredContents; @@ -36,6 +36,7 @@ use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneType; use nexus_types::deployment::ClickhouseClusterConfig; use nexus_types::deployment::CockroachDbPreserveDowngrade; +use nexus_types::deployment::ExpectedActiveRotSlot; use nexus_types::deployment::PendingMgsUpdate; use nexus_types::deployment::PendingMgsUpdateDetails; use nexus_types::deployment::{ @@ -1302,6 +1303,14 @@ impl BpOximeterReadPolicy { } } +pub trait BpPendingMgsUpdateComponent { + /// Converts a BpMgsUpdate into a PendingMgsUpdate + fn into_generic(self, baseboard_id: Arc) -> PendingMgsUpdate; + + /// Retrieves the baseboard ID + fn hw_baseboard_id(&self) -> &Uuid; +} + #[derive(Queryable, Clone, Debug, Selectable, Insertable)] #[diesel(table_name = bp_pending_mgs_update_sp)] pub struct BpPendingMgsUpdateSp { @@ -1315,11 +1324,12 @@ pub struct BpPendingMgsUpdateSp { pub expected_inactive_version: Option, } -impl BpPendingMgsUpdateSp { - pub fn into_generic( - self, - baseboard_id: Arc, - ) -> PendingMgsUpdate { +impl BpPendingMgsUpdateComponent for BpPendingMgsUpdateSp { + fn hw_baseboard_id(&self) -> &Uuid { + &self.hw_baseboard_id + } + + fn into_generic(self, baseboard_id: Arc) -> PendingMgsUpdate { PendingMgsUpdate { baseboard_id, sp_type: self.sp_type.into(), @@ -1338,3 +1348,55 @@ impl BpPendingMgsUpdateSp { } } } + +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = bp_pending_mgs_update_rot)] +pub struct BpPendingMgsUpdateRot { + pub blueprint_id: DbTypedUuid, + pub hw_baseboard_id: Uuid, + pub sp_type: SpType, + pub sp_slot: SpMgsSlot, + pub artifact_sha256: ArtifactHash, + pub artifact_version: DbArtifactVersion, + pub expected_active_slot: HwRotSlot, + pub expected_active_version: DbArtifactVersion, + pub expected_inactive_version: Option, + pub expected_persistent_boot_preference: HwRotSlot, + pub expected_pending_persistent_boot_preference: Option, + pub expected_transient_boot_preference: Option, +} + +impl BpPendingMgsUpdateComponent for BpPendingMgsUpdateRot { + fn hw_baseboard_id(&self) -> &Uuid { + &self.hw_baseboard_id + } + + fn into_generic(self, baseboard_id: Arc) -> PendingMgsUpdate { + PendingMgsUpdate { + baseboard_id, + sp_type: self.sp_type.into(), + slot_id: **self.sp_slot, + artifact_hash: self.artifact_sha256.into(), + artifact_version: (*self.artifact_version).clone(), + details: PendingMgsUpdateDetails::Rot { + expected_active_slot: ExpectedActiveRotSlot { + slot: self.expected_active_slot.into(), + version: (*self.expected_active_version).clone(), + }, + expected_inactive_version: self + .expected_inactive_version + .map(|v| ExpectedVersion::Version(v.into())) + .unwrap_or(ExpectedVersion::NoValidVersion), + expected_persistent_boot_preference: self + .expected_persistent_boot_preference + .into(), + expected_pending_persistent_boot_preference: self + .expected_pending_persistent_boot_preference + .map(|s| s.into()), + expected_transient_boot_preference: self + .expected_transient_boot_preference + .map(|s| s.into()), + }, + } + } +} diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index f7c94edc3a..9918f69189 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(166, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(167, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(167, "add-pending-mgs-updates-rot"), KnownVersion::new(166, "bundle-user-comment"), KnownVersion::new(165, "route-config-rib-priority"), KnownVersion::new(164, "fix-leaked-bp-oximeter-read-policy-rows"), diff --git a/nexus/db-queries/Cargo.toml b/nexus/db-queries/Cargo.toml index 471ab2351b..b9c3e5098a 100644 --- a/nexus/db-queries/Cargo.toml +++ b/nexus/db-queries/Cargo.toml @@ -88,6 +88,7 @@ criterion.workspace = true expectorate.workspace = true hyper-rustls.workspace = true gateway-client.workspace = true +gateway-types.workspace = true illumos-utils.workspace = true internal-dns-resolver.workspace = true itertools.workspace = true diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 19889503a6..905b37c11d 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -53,17 +53,21 @@ use nexus_db_model::BpOmicronPhysicalDisk; use nexus_db_model::BpOmicronZone; use nexus_db_model::BpOmicronZoneNic; use nexus_db_model::BpOximeterReadPolicy; +use nexus_db_model::BpPendingMgsUpdateComponent; +use nexus_db_model::BpPendingMgsUpdateRot; use nexus_db_model::BpPendingMgsUpdateSp; use nexus_db_model::BpSledMetadata; use nexus_db_model::BpTarget; use nexus_db_model::DbArtifactVersion; use nexus_db_model::DbTypedUuid; use nexus_db_model::HwBaseboardId; +use nexus_db_model::HwRotSlot; use nexus_db_model::SpMgsSlot; use nexus_db_model::SpType; use nexus_db_model::SqlU16; use nexus_db_model::TufArtifact; use nexus_db_model::to_db_typed_uuid; +use nexus_db_schema::enums::HwRotSlotEnum; use nexus_db_schema::enums::SpTypeEnum; use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintMetadata; @@ -83,10 +87,12 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupType; use omicron_common::api::external::ResourceType; use omicron_common::bail_unless; +use omicron_uuid_kinds::BlueprintKind; use omicron_uuid_kinds::BlueprintUuid; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::OmicronZoneUuid; use omicron_uuid_kinds::SledUuid; +use omicron_uuid_kinds::TypedUuid; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::sync::Arc; @@ -509,150 +515,302 @@ impl DataStore { // // This way, we don't need to know the id. The database looks // it up for us as it does the INSERT. + // + // For each SP component (SP, RoT, RoT bootloader) we will be + // inserting to a different table: bp_pending_mgs_update_sp, + // bp_pending_mgs_update_rot, or + // bp_pending_mgs_update_rot_bootloader. for update in &blueprint.pending_mgs_updates { // Right now, we only implement support for storing SP // updates. - let (expected_active_version, expected_inactive_version) = - match &update.details { - PendingMgsUpdateDetails::Sp { - expected_active_version, - expected_inactive_version, - } => ( - expected_active_version, - expected_inactive_version, - ), - PendingMgsUpdateDetails::Rot { .. } - | PendingMgsUpdateDetails::RotBootloader { - .. - } => continue, - }; - - let db_blueprint_id = DbTypedUuid::from(blueprint_id) - .into_sql::( - ); - let db_sp_type = - SpType::from(update.sp_type).into_sql::(); - let db_slot_id = - SpMgsSlot::from(SqlU16::from(update.slot_id)) - .into_sql::(); - let db_artifact_hash = - ArtifactHash::from(update.artifact_hash) + match &update.details { + PendingMgsUpdateDetails::Sp { + expected_active_version, + expected_inactive_version, + } => { + let db_blueprint_id = DbTypedUuid::from( + blueprint_id + ).into_sql::(); + let db_sp_type = + SpType::from(update.sp_type).into_sql::(); + let db_slot_id = + SpMgsSlot::from(SqlU16::from(update.slot_id)) + .into_sql::(); + let db_artifact_hash = + ArtifactHash::from(update.artifact_hash) + .into_sql::(); + let db_artifact_version = DbArtifactVersion::from( + update.artifact_version.clone(), + ) .into_sql::(); - let db_artifact_version = DbArtifactVersion::from( - update.artifact_version.clone(), - ) - .into_sql::(); - let db_expected_version = DbArtifactVersion::from( - expected_active_version.clone(), - ) - .into_sql::(); - let db_expected_inactive_version = - match expected_inactive_version { - ExpectedVersion::NoValidVersion => None, - ExpectedVersion::Version(v) => { - Some(DbArtifactVersion::from(v.clone())) + let db_expected_version = DbArtifactVersion::from( + expected_active_version.clone(), + ) + .into_sql::(); + let db_expected_inactive_version = + match expected_inactive_version { + ExpectedVersion::NoValidVersion => None, + ExpectedVersion::Version(v) => { + Some(DbArtifactVersion::from(v.clone())) + } + } + .into_sql::>(); + + // Skip formatting several lines to prevent rustfmt bailing + // out. + #[rustfmt::skip] + use nexus_db_schema::schema::hw_baseboard_id::dsl + as baseboard_dsl; + #[rustfmt::skip] + use nexus_db_schema::schema::bp_pending_mgs_update_sp::dsl + as update_dsl; + let selection = + nexus_db_schema::schema::hw_baseboard_id::table + .select(( + db_blueprint_id, + baseboard_dsl::id, + db_sp_type, + db_slot_id, + db_artifact_hash, + db_artifact_version, + db_expected_version, + db_expected_inactive_version, + )) + .filter( + baseboard_dsl::part_number.eq(update + .baseboard_id + .part_number + .clone()), + ) + .filter( + baseboard_dsl::serial_number.eq(update + .baseboard_id + .serial_number + .clone()), + ); + let count = diesel::insert_into( + update_dsl::bp_pending_mgs_update_sp, + ) + .values(selection) + .into_columns(( + update_dsl::blueprint_id, + update_dsl::hw_baseboard_id, + update_dsl::sp_type, + update_dsl::sp_slot, + update_dsl::artifact_sha256, + update_dsl::artifact_version, + update_dsl::expected_active_version, + update_dsl::expected_inactive_version, + )) + .execute_async(&conn) + .await?; + if count != 1 { + // This should be impossible in practice. We + // will insert however many rows matched the + // `baseboard_id` parts of the query above. It + // can't be more than one 1 because we've + // filtered on a pair of columns that are unique + // together. It could only be 0 if the baseboard + // id had never been seen before in an inventory + // collection. But in that case, how did we + // manage to construct a blueprint with it? + // + // This could happen in the test suite or with + // `reconfigurator-cli`, which both let you + // create any blueprint you like. In the test + // suite, the test just has to deal with this + // behaviour (e.g., by inserting an inventory + // collection containing this SP). With + // `reconfigurator-cli`, this amounts to user + // error. + error!(&opctx.log, + "blueprint insertion: unexpectedly tried to \ + insert wrong number of rows into \ + bp_pending_mgs_update_sp (aborting transaction)"; + "count" => count, + &update.baseboard_id, + ); + return Err(TxnError::BadInsertCount { + table_name: "bp_pending_mgs_update_sp", + count, + baseboard_id: update.baseboard_id.clone(), + }); } - } - .into_sql::>(); - // Skip formatting several lines to prevent rustfmt bailing - // out. - #[rustfmt::skip] - use nexus_db_schema::schema::hw_baseboard_id::dsl - as baseboard_dsl; - #[rustfmt::skip] - use nexus_db_schema::schema::bp_pending_mgs_update_sp::dsl - as update_dsl; - let selection = - nexus_db_schema::schema::hw_baseboard_id::table - .select(( - db_blueprint_id, - baseboard_dsl::id, - db_sp_type, - db_slot_id, - db_artifact_hash, - db_artifact_version, - db_expected_version, - db_expected_inactive_version, - )) - .filter( - baseboard_dsl::part_number.eq(update - .baseboard_id - .part_number - .clone()), + // This statement is just here to force a compilation + // error if the set of columns in + // `bp_pending_mgs_update_sp` changes because that + // will affect the correctness of the above + // statement. + // + // If you're here because of a compile error, you + // might be changing the `bp_pending_mgs_update_sp` + // table. Update the statement below and be sure to + // update the code above, too! + let ( + _blueprint_id, + _hw_baseboard_id, + _sp_type, + _sp_slot, + _artifact_sha256, + _artifact_version, + _expected_active_version, + _expected_inactive_version, + ) = update_dsl::bp_pending_mgs_update_sp::all_columns(); + }, + PendingMgsUpdateDetails::Rot { + expected_active_slot, + expected_inactive_version, + expected_persistent_boot_preference, + expected_pending_persistent_boot_preference, + expected_transient_boot_preference, + } => { + let db_blueprint_id = DbTypedUuid::from( + blueprint_id + ).into_sql::(); + let db_sp_type = + SpType::from(update.sp_type).into_sql::(); + let db_slot_id = + SpMgsSlot::from(SqlU16::from(update.slot_id)) + .into_sql::(); + let db_artifact_hash = + ArtifactHash::from(update.artifact_hash) + .into_sql::(); + let db_artifact_version = DbArtifactVersion::from( + update.artifact_version.clone(), ) - .filter( - baseboard_dsl::serial_number.eq(update - .baseboard_id - .serial_number - .clone()), - ); - let count = diesel::insert_into( - update_dsl::bp_pending_mgs_update_sp, - ) - .values(selection) - .into_columns(( - update_dsl::blueprint_id, - update_dsl::hw_baseboard_id, - update_dsl::sp_type, - update_dsl::sp_slot, - update_dsl::artifact_sha256, - update_dsl::artifact_version, - update_dsl::expected_active_version, - update_dsl::expected_inactive_version, - )) - .execute_async(&conn) - .await?; - if count != 1 { - // This should be impossible in practice. We will - // insert however many rows matched the `baseboard_id` - // parts of the query above. It can't be more than one - // 1 because we've filtered on a pair of columns that - // are unique together. It could only be 0 if the - // baseboard id had never been seen before in an - // inventory collection. But in that case, how did we - // manage to construct a blueprint with it? - // - // This could happen in the test suite or with - // `reconfigurator-cli`, which both let you create any - // blueprint you like. In the test suite, the test just - // has to deal with this behavior (e.g., by inserting an - // inventory collection containing this SP). With - // `reconfigurator-cli`, this amounts to user error. - error!(&opctx.log, - "blueprint insertion: unexpectedly tried to insert \ - wrong number of rows into \ - bp_pending_mgs_update_sp (aborting transaction)"; - "count" => count, - &update.baseboard_id, - ); - return Err(TxnError::BadInsertCount { - table_name: "bp_pending_mgs_update_sp", - count, - baseboard_id: update.baseboard_id.clone(), - }); - } + .into_sql::(); + let db_expected_active_slot = HwRotSlot::from( + *expected_active_slot.slot(), + ) + .into_sql::(); + let db_expected_active_version = DbArtifactVersion::from( + expected_active_slot.version(), + ) + .into_sql::(); + let db_expected_inactive_version = + match expected_inactive_version { + ExpectedVersion::NoValidVersion => None, + ExpectedVersion::Version(v) => { + Some(DbArtifactVersion::from(v.clone())) + } + } + .into_sql::>(); + let db_expected_persistent_boot_preference = HwRotSlot::from( + *expected_persistent_boot_preference + ).into_sql::(); + let db_expected_pending_persistent_boot_preference = + expected_pending_persistent_boot_preference.map( + |p| HwRotSlot::from(p) + ).into_sql::>(); + let db_expected_transient_boot_preference = + expected_transient_boot_preference.map( + |p| HwRotSlot::from(p) + ).into_sql::>(); + + // Skip formatting several lines to prevent rustfmt bailing + // out. + #[rustfmt::skip] + use nexus_db_schema::schema::hw_baseboard_id::dsl + as baseboard_dsl; + #[rustfmt::skip] + use nexus_db_schema::schema::bp_pending_mgs_update_rot::dsl + as update_dsl; + let selection = + nexus_db_schema::schema::hw_baseboard_id::table + .select(( + db_blueprint_id, + baseboard_dsl::id, + db_sp_type, + db_slot_id, + db_artifact_hash, + db_artifact_version, + db_expected_active_slot, + db_expected_active_version, + db_expected_inactive_version, + db_expected_persistent_boot_preference, + db_expected_pending_persistent_boot_preference, + db_expected_transient_boot_preference, + )) + .filter( + baseboard_dsl::part_number.eq(update + .baseboard_id + .part_number + .clone()), + ) + .filter( + baseboard_dsl::serial_number.eq(update + .baseboard_id + .serial_number + .clone()), + ); + let count = diesel::insert_into( + update_dsl::bp_pending_mgs_update_rot, + ) + .values(selection) + .into_columns(( + update_dsl::blueprint_id, + update_dsl::hw_baseboard_id, + update_dsl::sp_type, + update_dsl::sp_slot, + update_dsl::artifact_sha256, + update_dsl::artifact_version, + update_dsl::expected_active_slot, + update_dsl::expected_active_version, + update_dsl::expected_inactive_version, + update_dsl::expected_persistent_boot_preference, + update_dsl::expected_pending_persistent_boot_preference, + update_dsl::expected_transient_boot_preference, + )) + .execute_async(&conn) + .await?; + if count != 1 { + // As with `PendingMgsUpdateDetails::Sp`, this + // should be impossible in practice. + error!(&opctx.log, + "blueprint insertion: unexpectedly tried to \ + insert wrong number of rows into \ + bp_pending_mgs_update_rot (aborting transaction)"; + "count" => count, + &update.baseboard_id, + ); + return Err(TxnError::BadInsertCount { + table_name: "bp_pending_mgs_update_rot", + count, + baseboard_id: update.baseboard_id.clone(), + }); + } - // This statement is just here to force a compilation error - // if the set of columns in `bp_pending_mgs_update_sp` - // changes because that will affect the correctness of the - // above statement. - // - // If you're here because of a compile error, you might be - // changing the `bp_pending_mgs_update_sp` table. Update - // the statement below and be sure to update the code above, - // too! - let ( - _blueprint_id, - _hw_baseboard_id, - _sp_type, - _sp_slot, - _artifact_sha256, - _artifact_version, - _expected_active_version, - _expected_inactive_version, - ) = update_dsl::bp_pending_mgs_update_sp::all_columns(); + // This statement is just here to force a compilation + // error if the set of columns in + // `bp_pending_mgs_update_rot` changes because that + // will affect the correctness of the above + // statement. + // + // If you're here because of a compile error, you + // might be changing the `bp_pending_mgs_update_rot` + // table. Update the statement below and be sure to + // update the code above, too! + let ( + _blueprint_id, + _hw_baseboard_id, + _sp_type, + _sp_slot, + _artifact_sha256, + _artifact_version, + _expected_active_slot, + _expected_active_version, + _expected_inactive_version, + _expected_persistent_boot_preference, + _expected_pending_persistent_boot_preference, + _expected_transient_boot_preference, + ) = update_dsl::bp_pending_mgs_update_rot::all_columns(); + }, + PendingMgsUpdateDetails::RotBootloader { + .. + } => continue, // TODO: Implement. + }; } Ok(()) @@ -1255,10 +1413,40 @@ impl DataStore { } }; - // Load all pending SP updates. + // Load all pending RoT updates. // // Pagination is a little silly here because we will only allow one at a // time in practice for a while, but it's easy enough to do. + let mut pending_updates_rot = Vec::new(); + { + use nexus_db_schema::schema::bp_pending_mgs_update_rot::dsl; + + let mut paginator = Paginator::new( + SQL_BATCH_SIZE, + dropshot::PaginationOrder::Ascending, + ); + while let Some(p) = paginator.next() { + let batch = paginated( + dsl::bp_pending_mgs_update_rot, + dsl::hw_baseboard_id, + &p.current_pagparams(), + ) + .filter(dsl::blueprint_id.eq(to_db_typed_uuid(blueprint_id))) + .select(BpPendingMgsUpdateRot::as_select()) + .load_async(&*conn) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?; + + paginator = p.found_batch(&batch, &|d| d.hw_baseboard_id); + for row in batch { + pending_updates_rot.push(row); + } + } + } + + // Load all pending SP updates. let mut pending_updates_sp = Vec::new(); { use nexus_db_schema::schema::bp_pending_mgs_update_sp::dsl; @@ -1327,26 +1515,21 @@ impl DataStore { // Combine this information to assemble the set of pending MGS updates. let mut pending_mgs_updates = PendingMgsUpdates::new(); + for row in pending_updates_rot { + process_update_row( + row, + &baseboards_by_id, + &mut pending_mgs_updates, + &blueprint_id, + )?; + } for row in pending_updates_sp { - let Some(baseboard) = baseboards_by_id.get(&row.hw_baseboard_id) - else { - // This should be impossible. - return Err(Error::internal_error(&format!( - "loading blueprint {}: missing baseboard that we should \ - have fetched: {}", - blueprint_id, row.hw_baseboard_id - ))); - }; - - let update = row.into_generic(baseboard.clone()); - if let Some(previous) = pending_mgs_updates.insert(update) { - // This should be impossible. - return Err(Error::internal_error(&format!( - "blueprint {}: found multiple pending updates for \ - baseboard {:?}", - blueprint_id, previous.baseboard_id - ))); - } + process_update_row( + row, + &baseboards_by_id, + &mut pending_mgs_updates, + &blueprint_id, + )?; } Ok(Blueprint { @@ -1399,6 +1582,7 @@ impl DataStore { nclickhouse_servers, noximeter_policy, npending_mgs_updates_sp, + npending_mgs_updates_rot, ) = self .transaction_retry_wrapper("blueprint_delete") .transaction(&conn, |conn| { @@ -1596,6 +1780,21 @@ impl DataStore { .await? }; + let npending_mgs_updates_rot = { + // Skip rustfmt because it bails out on this long line. + #[rustfmt::skip] + use nexus_db_schema::schema:: + bp_pending_mgs_update_rot::dsl; + diesel::delete( + dsl::bp_pending_mgs_update_rot.filter( + dsl::blueprint_id + .eq(to_db_typed_uuid(blueprint_id)), + ), + ) + .execute_async(&conn) + .await? + }; + Ok(( nblueprints, nsled_metadata, @@ -1608,6 +1807,7 @@ impl DataStore { nclickhouse_servers, noximeter_policy, npending_mgs_updates_sp, + npending_mgs_updates_rot, )) } }) @@ -1630,6 +1830,7 @@ impl DataStore { "nclickhouse_servers" => nclickhouse_servers, "noximeter_policy" => noximeter_policy, "npending_mgs_updates_sp" => npending_mgs_updates_sp, + "npending_mgs_updates_rot" => npending_mgs_updates_rot, ); Ok(()) @@ -1973,6 +2174,38 @@ impl DataStore { } } +// Helper to process BpPendingMgsUpdateComponent rows +fn process_update_row( + row: T, + baseboards_by_id: &BTreeMap>, + pending_mgs_updates: &mut PendingMgsUpdates, + blueprint_id: &TypedUuid, +) -> Result<(), Error> +where + T: BpPendingMgsUpdateComponent, +{ + let Some(baseboard) = baseboards_by_id.get(row.hw_baseboard_id()) else { + // This should be impossible. + return Err(Error::internal_error(&format!( + "loading blueprint {}: missing baseboard that we should \ + have fetched: {}", + blueprint_id, + row.hw_baseboard_id() + ))); + }; + + let update = row.into_generic(baseboard.clone()); + if let Some(previous) = pending_mgs_updates.insert(update) { + // This should be impossible. + return Err(Error::internal_error(&format!( + "blueprint {}: found multiple pending updates for \ + baseboard {:?}", + blueprint_id, previous.baseboard_id + ))); + } + Ok(()) +} + // Helper to create an `authz::Blueprint` for a specific blueprint ID fn authz_blueprint_from_id(blueprint_id: BlueprintUuid) -> authz::Blueprint { let blueprint_id = blueprint_id.into_untyped_uuid(); @@ -2347,6 +2580,7 @@ mod tests { use crate::db::pub_test_utils::TestDatabase; use crate::db::raw_query_builder::QueryBuilder; + use gateway_types::rot::RotSlot; use nexus_inventory::CollectionBuilder; use nexus_inventory::now_db_precision; use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder; @@ -2362,6 +2596,7 @@ mod tests { use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneImageSource; use nexus_types::deployment::BlueprintZoneType; + use nexus_types::deployment::ExpectedActiveRotSlot; use nexus_types::deployment::OmicronZoneExternalFloatingIp; use nexus_types::deployment::PendingMgsUpdate; use nexus_types::deployment::PlanningInput; @@ -2470,6 +2705,7 @@ mod tests { query_count!(bp_clickhouse_server_zone_id_to_node_id, blueprint_id), query_count!(bp_oximeter_read_policy, blueprint_id), query_count!(bp_pending_mgs_update_sp, blueprint_id), + query_count!(bp_pending_mgs_update_rot, blueprint_id), ] { let count: i64 = result.unwrap(); assert_eq!( @@ -3077,15 +3313,36 @@ mod tests { // blueprint2 is more interesting in terms of containing a variety of // different blueprint structures. We want to try deleting that. To do // that, we have to create a new blueprint and make that one the target. - let blueprint3 = BlueprintBuilder::new_based_on( + let mut builder = BlueprintBuilder::new_based_on( &logctx.log, &blueprint2, &planning_input, &collection, "dummy", ) - .expect("failed to create builder") - .build(); + .expect("failed to create builder"); + + // Configure an RoT update + let (baseboard_id, sp) = + collection.sps.iter().next().expect("at least one SP"); + builder.pending_mgs_update_insert(PendingMgsUpdate { + baseboard_id: baseboard_id.clone(), + sp_type: sp.sp_type, + slot_id: sp.sp_slot, + details: PendingMgsUpdateDetails::Rot { + expected_active_slot: ExpectedActiveRotSlot { + slot: RotSlot::A, + version: "1.0.0".parse().unwrap(), + }, + expected_inactive_version: ExpectedVersion::NoValidVersion, + expected_persistent_boot_preference: RotSlot::A, + expected_pending_persistent_boot_preference: None, + expected_transient_boot_preference: None, + }, + artifact_hash: ArtifactHash([72; 32]), + artifact_version: "2.0.0".parse().unwrap(), + }); + let blueprint3 = builder.build(); datastore .blueprint_insert(&opctx, &blueprint3) .await diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index 51dd5434c6..d256b19fd3 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -2094,6 +2094,23 @@ table! { } } +table! { + bp_pending_mgs_update_rot (blueprint_id, hw_baseboard_id) { + blueprint_id -> Uuid, + hw_baseboard_id -> Uuid, + sp_type -> crate::enums::SpTypeEnum, + sp_slot -> Int4, + artifact_sha256 -> Text, + artifact_version -> Text, + expected_active_slot -> crate::enums::HwRotSlotEnum, + expected_active_version -> Text, + expected_inactive_version -> Nullable, + expected_persistent_boot_preference -> crate::enums::HwRotSlotEnum, + expected_pending_persistent_boot_preference -> Nullable, + expected_transient_boot_preference -> Nullable, + } +} + table! { bootstore_keys (key, generation) { key -> Text, diff --git a/schema/crdb/add-pending-mgs-updates-rot/up.sql b/schema/crdb/add-pending-mgs-updates-rot/up.sql new file mode 100644 index 0000000000..dba08cf152 --- /dev/null +++ b/schema/crdb/add-pending-mgs-updates-rot/up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS omicron.public.bp_pending_mgs_update_rot ( + blueprint_id UUID, + hw_baseboard_id UUID NOT NULL, + sp_type omicron.public.sp_type NOT NULL, + sp_slot INT4 NOT NULL, + artifact_sha256 STRING(64) NOT NULL, + artifact_version STRING(64) NOT NULL, + + expected_active_slot omicron.public.hw_rot_slot NOT NULL, + expected_active_version STRING NOT NULL, + expected_inactive_version STRING, + expected_persistent_boot_preference omicron.public.hw_rot_slot NOT NULL, + expected_pending_persistent_boot_preference omicron.public.hw_rot_slot, + expected_transient_boot_preference omicron.public.hw_rot_slot, + + PRIMARY KEY(blueprint_id, hw_baseboard_id) +); diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 21414557de..5ba503fc8a 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -4707,6 +4707,31 @@ CREATE TABLE IF NOT EXISTS omicron.public.bp_pending_mgs_update_sp ( PRIMARY KEY(blueprint_id, hw_baseboard_id) ); +-- Blueprint information related to pending RoT upgrades. +CREATE TABLE IF NOT EXISTS omicron.public.bp_pending_mgs_update_rot ( + -- Foreign key into the `blueprint` table + blueprint_id UUID, + -- identify of the device to be updated + -- (foreign key into the `hw_baseboard_id` table) + hw_baseboard_id UUID NOT NULL, + -- location of this device according to MGS + sp_type omicron.public.sp_type NOT NULL, + sp_slot INT4 NOT NULL, + -- artifact to be deployed to this device + artifact_sha256 STRING(64) NOT NULL, + artifact_version STRING(64) NOT NULL, + + -- RoT-specific details + expected_active_slot omicron.public.hw_rot_slot NOT NULL, + expected_active_version STRING NOT NULL, + expected_inactive_version STRING, -- NULL means invalid (no version expected) + expected_persistent_boot_preference omicron.public.hw_rot_slot NOT NULL, + expected_pending_persistent_boot_preference omicron.public.hw_rot_slot, + expected_transient_boot_preference omicron.public.hw_rot_slot, + + PRIMARY KEY(blueprint_id, hw_baseboard_id) +); + -- Mapping of Omicron zone ID to CockroachDB node ID. This isn't directly used -- by the blueprint tables above, but is used by the more general Reconfigurator -- system along with them (e.g., to decommission expunged CRDB nodes). @@ -6212,7 +6237,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '166.0.0', NULL) + (TRUE, NOW(), NOW(), '167.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT;