diff --git a/nexus/db-model/src/deployment.rs b/nexus/db-model/src/deployment.rs index 8daf9cb9ed..b4ba1b83f8 100644 --- a/nexus/db-model/src/deployment.rs +++ b/nexus/db-model/src/deployment.rs @@ -23,7 +23,8 @@ use nexus_db_schema::schema::{ 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_rot, - bp_pending_mgs_update_sp, bp_sled_metadata, bp_target, + bp_pending_mgs_update_rot_bootloader, bp_pending_mgs_update_sp, + bp_sled_metadata, bp_target, }; use nexus_sled_agent_shared::inventory::OmicronZoneDataset; use nexus_types::deployment::BlueprintHostPhase2DesiredContents; @@ -1311,6 +1312,45 @@ pub trait BpPendingMgsUpdateComponent { fn hw_baseboard_id(&self) -> &Uuid; } +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = bp_pending_mgs_update_rot_bootloader)] +pub struct BpPendingMgsUpdateRotBootloader { + 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_stage0_version: DbArtifactVersion, + pub expected_stage0_next_version: Option, +} + +impl BpPendingMgsUpdateComponent for BpPendingMgsUpdateRotBootloader { + 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::RotBootloader { + expected_stage0_version: (*self.expected_stage0_version) + .clone(), + expected_stage0_next_version: match self + .expected_stage0_next_version + { + Some(v) => ExpectedVersion::Version((*v).clone()), + None => ExpectedVersion::NoValidVersion, + }, + }, + } + } +} + #[derive(Queryable, Clone, Debug, Selectable, Insertable)] #[diesel(table_name = bp_pending_mgs_update_sp)] pub struct BpPendingMgsUpdateSp { diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index 534b432d8c..3d54aea3ef 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(169, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(170, 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(170, "add-pending-mgs-updates-rot-bootloader"), KnownVersion::new(169, "inv-ntp-timesync"), KnownVersion::new(168, "add-inv-host-phase-1-flash-hash"), KnownVersion::new(167, "add-pending-mgs-updates-rot"), diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 905b37c11d..f6ba7e911d 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -55,6 +55,7 @@ use nexus_db_model::BpOmicronZoneNic; use nexus_db_model::BpOximeterReadPolicy; use nexus_db_model::BpPendingMgsUpdateComponent; use nexus_db_model::BpPendingMgsUpdateRot; +use nexus_db_model::BpPendingMgsUpdateRotBootloader; use nexus_db_model::BpPendingMgsUpdateSp; use nexus_db_model::BpSledMetadata; use nexus_db_model::BpTarget; @@ -807,9 +808,121 @@ impl DataStore { _expected_transient_boot_preference, ) = update_dsl::bp_pending_mgs_update_rot::all_columns(); }, - PendingMgsUpdateDetails::RotBootloader { - .. - } => continue, // TODO: Implement. + PendingMgsUpdateDetails::RotBootloader { expected_stage0_version, expected_stage0_next_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_expected_stage0_version = DbArtifactVersion::from( + expected_stage0_version.clone(), + ) + .into_sql::(); + let db_expected_stage0_next_version = + match expected_stage0_next_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_rot_bootloader::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_stage0_version, + db_expected_stage0_next_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_rot_bootloader, + ) + .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_stage0_version, + update_dsl::expected_stage0_next_version, + )) + .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_bootloader (aborting transaction)"; + "count" => count, + &update.baseboard_id, + ); + return Err(TxnError::BadInsertCount { + table_name: "bp_pending_mgs_update_rot_bootloader", + 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_rot_bootloader` 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_bootloader` + // 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_stage0_version, + _expected_stage0_next_version, + ) = update_dsl::bp_pending_mgs_update_rot_bootloader::all_columns(); + } }; } @@ -1413,10 +1526,40 @@ impl DataStore { } }; - // Load all pending RoT updates. + // Load all pending RoT bootloader 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_bootloader = Vec::new(); + { + use nexus_db_schema::schema::bp_pending_mgs_update_rot_bootloader::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_bootloader, + dsl::hw_baseboard_id, + &p.current_pagparams(), + ) + .filter(dsl::blueprint_id.eq(to_db_typed_uuid(blueprint_id))) + .select(BpPendingMgsUpdateRotBootloader::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_bootloader.push(row); + } + } + } + + // Load all pending RoT updates. let mut pending_updates_rot = Vec::new(); { use nexus_db_schema::schema::bp_pending_mgs_update_rot::dsl; @@ -1515,6 +1658,14 @@ 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_bootloader { + process_update_row( + row, + &baseboards_by_id, + &mut pending_mgs_updates, + &blueprint_id, + )?; + } for row in pending_updates_rot { process_update_row( row, @@ -1583,6 +1734,7 @@ impl DataStore { noximeter_policy, npending_mgs_updates_sp, npending_mgs_updates_rot, + npending_mgs_updates_rot_bootloader, ) = self .transaction_retry_wrapper("blueprint_delete") .transaction(&conn, |conn| { @@ -1795,6 +1947,21 @@ impl DataStore { .await? }; + let npending_mgs_updates_rot_bootloader = { + // Skip rustfmt because it bails out on this long line. + #[rustfmt::skip] + use nexus_db_schema::schema:: + bp_pending_mgs_update_rot_bootloader::dsl; + diesel::delete( + dsl::bp_pending_mgs_update_rot_bootloader.filter( + dsl::blueprint_id + .eq(to_db_typed_uuid(blueprint_id)), + ), + ) + .execute_async(&conn) + .await? + }; + Ok(( nblueprints, nsled_metadata, @@ -1808,6 +1975,7 @@ impl DataStore { noximeter_policy, npending_mgs_updates_sp, npending_mgs_updates_rot, + npending_mgs_updates_rot_bootloader, )) } }) @@ -1831,6 +1999,8 @@ impl DataStore { "noximeter_policy" => noximeter_policy, "npending_mgs_updates_sp" => npending_mgs_updates_sp, "npending_mgs_updates_rot" => npending_mgs_updates_rot, + "npending_mgs_updates_rot_bootloader" => + npending_mgs_updates_rot_bootloader, ); Ok(()) @@ -2706,6 +2876,7 @@ mod tests { 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), + query_count!(bp_pending_mgs_update_rot_bootloader, blueprint_id), ] { let count: i64 = result.unwrap(); assert_eq!( @@ -3343,6 +3514,7 @@ mod tests { artifact_version: "2.0.0".parse().unwrap(), }); let blueprint3 = builder.build(); + let authz_blueprint3 = authz_blueprint_from_id(blueprint3.id); datastore .blueprint_insert(&opctx, &blueprint3) .await @@ -3359,6 +3531,48 @@ mod tests { datastore.blueprint_delete(&opctx, &authz_blueprint2).await.unwrap(); ensure_blueprint_fully_deleted(&datastore, blueprint2.id).await; + // We now make sure we can build and insert a blueprint containing an + // RoT bootloader Pending MGS update + let mut builder = BlueprintBuilder::new_based_on( + &logctx.log, + &blueprint3, + &planning_input, + &collection, + "dummy", + ) + .expect("failed to create builder"); + + // Configure an RoT bootloader 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::RotBootloader { + expected_stage0_version: "1.0.0".parse().unwrap(), + expected_stage0_next_version: ExpectedVersion::NoValidVersion, + }, + artifact_hash: ArtifactHash([72; 32]), + artifact_version: "2.0.0".parse().unwrap(), + }); + let blueprint4 = builder.build(); + datastore + .blueprint_insert(&opctx, &blueprint4) + .await + .expect("failed to insert blueprint"); + let bp4_target = BlueprintTarget { + target_id: blueprint4.id, + enabled: true, + time_made_target: now_db_precision(), + }; + datastore + .blueprint_target_set_current(&opctx, bp4_target) + .await + .unwrap(); + datastore.blueprint_delete(&opctx, &authz_blueprint3).await.unwrap(); + ensure_blueprint_fully_deleted(&datastore, blueprint3.id).await; + // Clean up. db.terminate().await; logctx.cleanup_successful(); diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index e11dcc8d60..36826201fa 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -2101,6 +2101,19 @@ table! { } } +table! { + bp_pending_mgs_update_rot_bootloader (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_stage0_version -> Text, + expected_stage0_next_version -> Nullable, + } +} + table! { bp_pending_mgs_update_sp (blueprint_id, hw_baseboard_id) { blueprint_id -> Uuid, diff --git a/schema/crdb/add-pending-mgs-updates-rot-bootloader/up.sql b/schema/crdb/add-pending-mgs-updates-rot-bootloader/up.sql new file mode 100644 index 0000000000..a3faa37410 --- /dev/null +++ b/schema/crdb/add-pending-mgs-updates-rot-bootloader/up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS omicron.public.bp_pending_mgs_update_rot_bootloader ( + 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_stage0_version STRING NOT NULL, + expected_stage0_next_version STRING, + + PRIMARY KEY(blueprint_id, hw_baseboard_id) +); diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 68459e7b3b..e4a08c623a 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -4723,6 +4723,27 @@ CREATE TABLE IF NOT EXISTS omicron.public.bp_oximeter_read_policy ( oximeter_read_mode omicron.public.oximeter_read_mode NOT NULL ); +-- Blueprint information related to pending RoT bootloader upgrades. +CREATE TABLE IF NOT EXISTS omicron.public.bp_pending_mgs_update_rot_bootloader ( + -- 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 bootloader-specific details + expected_stage0_version STRING NOT NULL, + expected_stage0_next_version STRING, -- NULL means invalid (no version expected) + + PRIMARY KEY(blueprint_id, hw_baseboard_id) +); + -- Blueprint information related to pending SP upgrades. CREATE TABLE IF NOT EXISTS omicron.public.bp_pending_mgs_update_sp ( -- Foreign key into the `blueprint` table @@ -6274,7 +6295,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '169.0.0', NULL) + (TRUE, NOW(), NOW(), '170.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT;