diff --git a/dev-tools/omdb/tests/successes.out b/dev-tools/omdb/tests/successes.out index d1c2108b44a..e8638aa2aa6 100644 --- a/dev-tools/omdb/tests/successes.out +++ b/dev-tools/omdb/tests/successes.out @@ -1423,6 +1423,7 @@ parent: internal DNS version: 1 external DNS version: 2 + PENDING MGS-MANAGED UPDATES: 0 --------------------------------------------- stderr: @@ -1519,6 +1520,7 @@ parent: internal DNS version: 1 external DNS version: 2 + PENDING MGS-MANAGED UPDATES: 0 --------------------------------------------- stderr: diff --git a/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout b/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout index a2639cd2db7..20cc5fee234 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout @@ -358,6 +358,7 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 > @@ -453,5 +454,6 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 diff --git a/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout b/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout index d7791c3baa4..3dc26155c76 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmd-expunge-newly-added-stdout @@ -302,6 +302,7 @@ parent: 06c88262-f435-410e-ba98-101bed41ec27 internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 > blueprint-edit 3f00b694-1b16-4aaa-8f78-e6b3a527b434 expunge-zone 9995de32-dd52-4eb1-b0eb-141eb84bc739 @@ -605,6 +606,7 @@ parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434 internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 > blueprint-plan 366b0b68-d80e-4bc1-abd3-dc69837847e0 @@ -922,6 +924,7 @@ parent: 366b0b68-d80e-4bc1-abd3-dc69837847e0 internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 > blueprint-edit 9c998c1d-1a7b-440a-ae0c-40f781dea6e2 expunge-zone d786ef4a-5acb-4f5d-a732-a00addf986b5 diff --git a/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout b/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout index 737ac8bcdba..efb997be6d5 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmd-set-zone-images-stdout @@ -94,6 +94,7 @@ parent: 1b013011-2062-4b48-b544-a32b23bce83a internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 > @@ -198,6 +199,7 @@ parent: 9766ca20-38d4-4380-b005-e7c43c797e7c internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 > blueprint-diff 971eeb12-1830-4fa0-a699-98ea0164505c f714e6ea-e85a-4d7d-93c2-a018744fe176 @@ -470,6 +472,7 @@ parent: bb128f06-a2e1-44c1-8874-4f789d0ff896 internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 > blueprint-diff f714e6ea-e85a-4d7d-93c2-a018744fe176 d9c572a1-a68c-4945-b1ec-5389bd588fe9 diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 122852e6f48..88d46a13570 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -59,6 +59,7 @@ use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::ClickhouseClusterConfig; use nexus_types::deployment::CockroachDbPreserveDowngrade; +use nexus_types::deployment::PendingMgsUpdates; use omicron_common::api::external::DataPageParams; use omicron_common::api::external::Error; use omicron_common::api::external::ListResultVec; @@ -892,6 +893,9 @@ impl DataStore { Ok(Blueprint { id: blueprint_id, + // TODO these need to be serialized to the database. + // See oxidecomputer/omicron#7981. + pending_mgs_updates: PendingMgsUpdates::new(), sleds: sled_configs, parent_blueprint_id, internal_dns_version, diff --git a/nexus/db-queries/src/db/datastore/rack.rs b/nexus/db-queries/src/db/datastore/rack.rs index 23e6e3a3fd6..c3138f94eda 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1012,6 +1012,7 @@ mod test { use nexus_sled_agent_shared::inventory::OmicronZoneDataset; use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::CockroachDbPreserveDowngrade; + use nexus_types::deployment::PendingMgsUpdates; use nexus_types::deployment::{ BlueprintZoneConfig, OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingIp, @@ -1054,6 +1055,7 @@ mod test { blueprint: Blueprint { id: BlueprintUuid::new_v4(), sleds: BTreeMap::new(), + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -1539,6 +1541,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -1796,6 +1799,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -2002,6 +2006,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -2138,6 +2143,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, diff --git a/nexus/reconfigurator/execution/src/dns.rs b/nexus/reconfigurator/execution/src/dns.rs index 35e72328ae8..c704b65a27b 100644 --- a/nexus/reconfigurator/execution/src/dns.rs +++ b/nexus/reconfigurator/execution/src/dns.rs @@ -339,6 +339,7 @@ mod test { pub use nexus_types::deployment::OmicronZoneExternalFloatingAddr; pub use nexus_types::deployment::OmicronZoneExternalFloatingIp; pub use nexus_types::deployment::OmicronZoneExternalSnatIp; + use nexus_types::deployment::PendingMgsUpdates; use nexus_types::deployment::SledFilter; use nexus_types::deployment::blueprint_zone_type; use nexus_types::external_api::params; @@ -688,6 +689,7 @@ mod test { let mut blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: blueprint_sleds, + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, diff --git a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs index 710da6ab5c2..4780a2aef4c 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -83,6 +83,7 @@ use thiserror::Error; use super::ClickhouseZonesThatShouldBeRunning; use super::clickhouse::ClickhouseAllocator; +use nexus_types::deployment::PendingMgsUpdates; /// Errors encountered while assembling blueprints #[derive(Debug, Error)] @@ -454,6 +455,7 @@ impl<'a> BlueprintBuilder<'a> { Blueprint { id: rng.next_blueprint(), sleds, + pending_mgs_updates: PendingMgsUpdates::new(), parent_blueprint_id: None, internal_dns_version: Generation::new(), external_dns_version: Generation::new(), @@ -688,6 +690,10 @@ impl<'a> BlueprintBuilder<'a> { Blueprint { id: blueprint_id, sleds, + pending_mgs_updates: self + .parent_blueprint + .pending_mgs_updates + .clone(), parent_blueprint_id: Some(self.parent_blueprint.id), internal_dns_version: self.input.internal_dns_version(), external_dns_version: self.input.external_dns_version(), diff --git a/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt b/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt index 66d230fca31..414972e916b 100644 --- a/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt +++ b/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt @@ -487,3 +487,4 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 diff --git a/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt b/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt index 9e995b8a5fe..44c6265ab0e 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt @@ -294,3 +294,4 @@ parent: 516e80a3-b362-4fac-bd3c-4559717120dd internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 diff --git a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt index ced3527bbb4..e00b1ed5064 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt @@ -466,3 +466,4 @@ parent: 4d4e6c38-cd95-4c4e-8f45-6af4d686964b internal DNS version: 1 external DNS version: 1 + PENDING MGS-MANAGED UPDATES: 0 diff --git a/nexus/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs index fccba57a0bf..edabb29744c 100644 --- a/nexus/src/app/background/tasks/blueprint_execution.rs +++ b/nexus/src/app/background/tasks/blueprint_execution.rs @@ -190,7 +190,7 @@ mod test { use nexus_types::deployment::{ Blueprint, BlueprintSledConfig, BlueprintTarget, BlueprintZoneConfig, BlueprintZoneDisposition, BlueprintZoneImageSource, BlueprintZoneType, - CockroachDbPreserveDowngrade, blueprint_zone_type, + CockroachDbPreserveDowngrade, PendingMgsUpdates, blueprint_zone_type, }; use nexus_types::external_api::views::SledState; use omicron_common::api::external; @@ -253,6 +253,7 @@ mod test { let blueprint = Blueprint { id, sleds: blueprint_sleds, + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: Some(current_target.target_id), diff --git a/nexus/src/app/background/tasks/blueprint_load.rs b/nexus/src/app/background/tasks/blueprint_load.rs index da2bc6dee2b..ba0dc248cd7 100644 --- a/nexus/src/app/background/tasks/blueprint_load.rs +++ b/nexus/src/app/background/tasks/blueprint_load.rs @@ -195,6 +195,7 @@ mod test { use nexus_test_utils_macros::nexus_test; use nexus_types::deployment::{ Blueprint, BlueprintTarget, CockroachDbPreserveDowngrade, + PendingMgsUpdates, }; use omicron_common::api::external::Generation; use omicron_uuid_kinds::BlueprintUuid; @@ -217,6 +218,7 @@ mod test { Blueprint { id, sleds: BTreeMap::new(), + pending_mgs_updates: PendingMgsUpdates::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: Some(parent_blueprint_id), diff --git a/nexus/test-utils/src/lib.rs b/nexus/test-utils/src/lib.rs index ac6d98506bd..bb33ab5f0be 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -98,6 +98,7 @@ use std::sync::Arc; use std::time::Duration; use uuid::Uuid; +use nexus_types::deployment::PendingMgsUpdates; pub use sim::TEST_HARDWARE_THREADS; pub use sim::TEST_RESERVOIR_RAM; @@ -940,6 +941,7 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> { Blueprint { id: BlueprintUuid::new_v4(), sleds: blueprint_sleds, + pending_mgs_updates: PendingMgsUpdates::new(), parent_blueprint_id: None, internal_dns_version: dns_config.generation, external_dns_version: Generation::new(), diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs index c84b88fe862..7017f392e59 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -45,6 +45,7 @@ use omicron_uuid_kinds::ZpoolUuid; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; +use slog::Key; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fmt; @@ -52,7 +53,9 @@ use std::net::Ipv6Addr; use std::net::SocketAddrV6; use strum::EnumIter; use tufaceous_artifact::ArtifactHash; +use tufaceous_artifact::ArtifactHashId; use tufaceous_artifact::ArtifactVersion; +use tufaceous_artifact::ArtifactVersionError; mod blueprint_diff; mod blueprint_display; @@ -63,7 +66,11 @@ mod planning_input; mod tri_map; mod zone_type; +use crate::inventory::BaseboardId; +pub use blueprint_diff::BlueprintDiffSummary; +use blueprint_display::BpPendingMgsUpdates; pub use clickhouse::ClickhouseClusterConfig; +use gateway_client::types::SpType; pub use network_resources::AddNetworkResourceError; pub use network_resources::OmicronZoneExternalFloatingAddr; pub use network_resources::OmicronZoneExternalFloatingIp; @@ -91,6 +98,7 @@ pub use planning_input::SledLookupError; pub use planning_input::SledLookupErrorKind; pub use planning_input::SledResources; pub use planning_input::ZpoolFilter; +use std::sync::Arc; pub use zone_type::BlueprintZoneType; pub use zone_type::DurableDataset; pub use zone_type::blueprint_zone_type; @@ -100,8 +108,7 @@ use blueprint_display::{ BpTable, BpTableData, BpTableRow, KvListWithHeading, constants::*, }; use id_map::{IdMap, IdMappable}; - -pub use blueprint_diff::BlueprintDiffSummary; +use std::str::FromStr; /// Describes a complete set of software and configuration for the system // Blueprints are a fundamental part of how the system modifies itself. Each @@ -151,6 +158,9 @@ pub struct Blueprint { /// A map of sled id -> desired configuration of the sled. pub sleds: BTreeMap, + /// List of pending MGS-mediated updates + pub pending_mgs_updates: PendingMgsUpdates, + /// which blueprint this blueprint is based on pub parent_blueprint_id: Option, @@ -488,6 +498,7 @@ impl fmt::Display for BlueprintDisplay<'_> { let Blueprint { id, sleds, + pending_mgs_updates, parent_blueprint_id, // These two cockroachdb_* fields are handled by // `make_cockroachdb_table()`, called below. @@ -569,6 +580,38 @@ impl fmt::Display for BlueprintDisplay<'_> { writeln!(f, "{}", self.make_cockroachdb_table())?; writeln!(f, "{}", self.make_metadata_table())?; + writeln!( + f, + " PENDING MGS-MANAGED UPDATES: {}", + pending_mgs_updates.len() + )?; + if !pending_mgs_updates.is_empty() { + writeln!( + f, + "{}", + BpTable::new( + BpPendingMgsUpdates {}, + None, + pending_mgs_updates + .iter() + .map(|pu| { + BpTableRow::from_strings( + BpDiffState::Unchanged, + vec![ + pu.sp_type.to_string(), + pu.slot_id.to_string(), + pu.baseboard_id.part_number.clone(), + pu.baseboard_id.serial_number.clone(), + pu.artifact_hash_id.kind.to_string(), + pu.artifact_hash_id.hash.to_string(), + ], + ) + }) + .collect() + ) + )?; + } + Ok(()) } } @@ -1000,6 +1043,198 @@ impl fmt::Display for BlueprintZoneImageVersion { } } +#[derive( + Clone, Debug, Eq, PartialEq, JsonSchema, Deserialize, Serialize, Diffable, +)] +pub struct PendingMgsUpdates { + // The IdMap key is the baseboard_id. Only one outstanding MGS-managed + // update is allowed for a given baseboard. + by_baseboard: IdMap, +} + +impl PendingMgsUpdates { + pub fn new() -> PendingMgsUpdates { + PendingMgsUpdates { by_baseboard: IdMap::new() } + } + + pub fn iter(&self) -> impl Iterator { + self.into_iter() + } + + pub fn len(&self) -> usize { + self.by_baseboard.len() + } + + pub fn is_empty(&self) -> bool { + self.by_baseboard.is_empty() + } + + pub fn contains_key(&self, key: &Arc) -> bool { + self.by_baseboard.contains_key(key) + } + + pub fn get( + &self, + baseboard_id: &Arc, + ) -> Option<&PendingMgsUpdate> { + self.by_baseboard.get(baseboard_id) + } + + pub fn remove( + &mut self, + baseboard_id: &Arc, + ) -> Option { + self.by_baseboard.remove(baseboard_id) + } + + pub fn insert( + &mut self, + update: PendingMgsUpdate, + ) -> Option { + self.by_baseboard.insert(update) + } +} + +impl<'a> IntoIterator for &'a PendingMgsUpdates { + type Item = &'a PendingMgsUpdate; + type IntoIter = std::collections::btree_map::Values< + 'a, + Arc, + PendingMgsUpdate, + >; + fn into_iter(self) -> Self::IntoIter { + self.by_baseboard.iter() + } +} + +#[derive( + Clone, Debug, Eq, PartialEq, JsonSchema, Deserialize, Serialize, Diffable, +)] +pub struct PendingMgsUpdate { + // identity of the baseboard + /// id of the baseboard that we're going to update + pub baseboard_id: Arc, + + // location of the baseboard (that we'd pass to MGS) + /// what type of baseboard this is + pub sp_type: SpType, + /// last known MGS slot (cubby number) of the baseboard + pub slot_id: u32, + + /// component-specific details of the pending update + pub details: PendingMgsUpdateDetails, + + /// which artifact to apply to this device + /// (implies which component is being updated) + pub artifact_hash_id: ArtifactHashId, + pub artifact_version: ArtifactVersion, +} + +impl slog::KV for PendingMgsUpdate { + fn serialize( + &self, + record: &slog::Record, + serializer: &mut dyn slog::Serializer, + ) -> slog::Result { + slog::KV::serialize(&self.baseboard_id, record, serializer)?; + serializer + .emit_str(Key::from("sp_type"), &format!("{:?}", self.sp_type))?; + serializer.emit_u32(Key::from("sp_slot"), self.slot_id)?; + slog::KV::serialize(&self.details, record, serializer)?; + serializer.emit_str( + Key::from("artifact_kind"), + &self.artifact_hash_id.kind.as_str(), + )?; + serializer.emit_str( + Key::from("artifact_hash"), + &self.artifact_hash_id.hash.to_string(), + ) + } +} + +impl IdMappable for PendingMgsUpdate { + type Id = Arc; + fn id(&self) -> Self::Id { + self.baseboard_id.clone() + } +} + +/// Describes the component-specific details of a PendingMgsUpdate +// This needs to specify: +// +// - the "component" (that we provide to the SP) +// - the slot that needs to be updated +// - any preconditions we expect to be true. These generally specify what we +// think is in each slot. This is intended to reduce the chance that an +// update operation using outdated configuration winds up rolling back the +// deployed version. +// +// Much of this may be implicit. See comments below. +#[derive( + Clone, Debug, Eq, PartialEq, JsonSchema, Deserialize, Serialize, Diffable, +)] +#[serde(tag = "component", rename_all = "snake_case")] +pub enum PendingMgsUpdateDetails { + /// the SP itself is being updated + Sp { + // implicit: component = SP_ITSELF + // implicit: firmware slot id = 0 (always 0 for SP itself) + /// expected contents of the active slot + expected_active_version: ArtifactVersion, + /// expected contents of the inactive slot + expected_inactive_version: ExpectedVersion, + }, +} + +impl slog::KV for PendingMgsUpdateDetails { + fn serialize( + &self, + _record: &slog::Record, + serializer: &mut dyn slog::Serializer, + ) -> slog::Result { + match self { + PendingMgsUpdateDetails::Sp { + expected_active_version, + expected_inactive_version, + } => { + serializer.emit_str(Key::from("component"), "sp")?; + serializer.emit_str( + Key::from("expected_active_version"), + &expected_active_version.to_string(), + )?; + serializer.emit_str( + Key::from("expected_inactive_version"), + &format!("{:?}", expected_inactive_version), + ) + } + } + } +} + +/// Describes the version that we expect to find in some firmware slot +#[derive( + Clone, Debug, Eq, PartialEq, JsonSchema, Deserialize, Serialize, Diffable, +)] +#[serde(tag = "kind", content = "version", rename_all = "snake_case")] +pub enum ExpectedVersion { + /// We expect to find _no_ valid caboose in this slot + NoValidVersion, + /// We expect to find the specified version in this slot + Version(ArtifactVersion), +} + +impl FromStr for ExpectedVersion { + type Err = ArtifactVersionError; + + fn from_str(s: &str) -> Result { + if s == "invalid" { + Ok(ExpectedVersion::NoValidVersion) + } else { + Ok(ExpectedVersion::Version(s.parse()?)) + } + } +} + /// The desired state of an Omicron-managed physical disk in a blueprint. #[derive( Debug, diff --git a/nexus/types/src/deployment/blueprint_diff.rs b/nexus/types/src/deployment/blueprint_diff.rs index ae3541f5a38..3d7df967d1d 100644 --- a/nexus/types/src/deployment/blueprint_diff.rs +++ b/nexus/types/src/deployment/blueprint_diff.rs @@ -58,6 +58,7 @@ impl<'a> BlueprintDiffSummary<'a> { let BlueprintDiff { // Fields in which changes are meaningful. sleds, + pending_mgs_updates, clickhouse_cluster_config, // Metadata fields for which changes don't reflect semantic // changes from one blueprint to the next. @@ -79,6 +80,14 @@ impl<'a> BlueprintDiffSummary<'a> { return true; } + // Did we modify, add, or remove any pending MGS updates? + if pending_mgs_updates.by_baseboard.modified().next().is_some() + || !pending_mgs_updates.by_baseboard.added.is_empty() + || !pending_mgs_updates.by_baseboard.removed.is_empty() + { + return true; + } + // Did the clickhouse config change? if clickhouse_cluster_config.before != clickhouse_cluster_config.after { return true; diff --git a/nexus/types/src/deployment/blueprint_display.rs b/nexus/types/src/deployment/blueprint_display.rs index ee11559cb35..77754ae7342 100644 --- a/nexus/types/src/deployment/blueprint_display.rs +++ b/nexus/types/src/deployment/blueprint_display.rs @@ -400,6 +400,25 @@ impl BpTableSchema for BpClickhouseServersTableSchema { } } +/// The [`BpTable`] schema for pending MGS updates +pub struct BpPendingMgsUpdates {} +impl BpTableSchema for BpPendingMgsUpdates { + fn table_name(&self) -> &'static str { + "Pending MGS-managed updates" + } + + fn column_names(&self) -> &'static [&'static str] { + &[ + "sp_type", + "slot", + "part_number", + "serial_number", + "artifact_kind", + "artifact_hash", + ] + } +} + // An entry in a [`KvListWithHeading`] #[derive(Debug)] pub struct KvPair { diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index 40d2c88c942..f1f63c2ffc0 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -1571,6 +1571,25 @@ } ] }, + "ArtifactHashId": { + "description": "A hash-based identifier for an artifact or deployment unit: the kind and hash.", + "type": "object", + "properties": { + "hash": { + "description": "The hash of the artifact.", + "type": "string", + "format": "hex string (32 bytes)" + }, + "kind": { + "description": "The kind of artifact this is.", + "type": "string" + } + }, + "required": [ + "hash", + "kind" + ] + }, "ArtifactVersion": { "description": "An artifact version.\n\nThis is a freeform identifier with some basic validation. It may be the serialized form of a semver version, or a custom identifier that uses the same character set as a semver, plus `_`.\n\nThe exact pattern accepted is `^[a-zA-Z0-9._+-]{1,63}$`.\n\n# Ord implementation\n\n`ArtifactVersion`s are not intended to be sorted, just compared for equality. `ArtifactVersion` implements `Ord` only for storage within sorted collections.", "type": "string", @@ -1659,6 +1678,24 @@ "serial" ] }, + "BaseboardId": { + "description": "A unique baseboard id found during a collection\n\nBaseboard ids are the keys used to link up information from disparate sources (like a service processor and a sled agent).\n\nThese are normalized in the database. Each distinct baseboard id is assigned a uuid and shared across the many possible collections that reference it.\n\nUsually, the part number and serial number are combined with a revision number. We do not include that here. If we ever did find a baseboard with the same part number and serial number but a new revision number, we'd want to treat that as the same baseboard as one with a different revision number.", + "type": "object", + "properties": { + "part_number": { + "description": "Oxide Part Number", + "type": "string" + }, + "serial_number": { + "description": "Serial number (unique for a given part number)", + "type": "string" + } + }, + "required": [ + "part_number", + "serial_number" + ] + }, "BfdMode": { "description": "BFD connection mode.", "type": "string", @@ -1947,6 +1984,14 @@ } ] }, + "pending_mgs_updates": { + "description": "List of pending MGS-mediated updates", + "allOf": [ + { + "$ref": "#/components/schemas/PendingMgsUpdates" + } + ] + }, "sleds": { "description": "A map of sled id -> desired configuration of the sled.", "type": "object", @@ -1968,6 +2013,7 @@ "external_dns_version", "id", "internal_dns_version", + "pending_mgs_updates", "sleds", "time_created" ] @@ -3747,6 +3793,45 @@ "request_id" ] }, + "ExpectedVersion": { + "description": "Describes the version that we expect to find in some firmware slot", + "oneOf": [ + { + "description": "We expect to find _no_ valid caboose in this slot", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "no_valid_version" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "description": "We expect to find the specified version in this slot", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "version" + ] + }, + "version": { + "$ref": "#/components/schemas/ArtifactVersion" + } + }, + "required": [ + "kind", + "version" + ] + } + ] + }, "ExternalPortDiscovery": { "oneOf": [ { @@ -3814,6 +3899,12 @@ "$ref": "#/components/schemas/BlueprintZoneConfig" } }, + "IdMapPendingMgsUpdate": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PendingMgsUpdate" + } + }, "ImportExportPolicy": { "description": "Define policy relating to the import and export of prefixes from a BGP peer.", "oneOf": [ @@ -4664,6 +4755,109 @@ "collector_id" ] }, + "PendingMgsUpdate": { + "type": "object", + "properties": { + "artifact_hash_id": { + "description": "which artifact to apply to this device (implies which component is being updated)", + "allOf": [ + { + "$ref": "#/components/schemas/ArtifactHashId" + } + ] + }, + "artifact_version": { + "$ref": "#/components/schemas/ArtifactVersion" + }, + "baseboard_id": { + "description": "id of the baseboard that we're going to update", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "details": { + "description": "component-specific details of the pending update", + "allOf": [ + { + "$ref": "#/components/schemas/PendingMgsUpdateDetails" + } + ] + }, + "slot_id": { + "description": "last known MGS slot (cubby number) of the baseboard", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "sp_type": { + "description": "what type of baseboard this is", + "allOf": [ + { + "$ref": "#/components/schemas/SpType" + } + ] + } + }, + "required": [ + "artifact_hash_id", + "artifact_version", + "baseboard_id", + "details", + "slot_id", + "sp_type" + ] + }, + "PendingMgsUpdateDetails": { + "description": "Describes the component-specific details of a PendingMgsUpdate", + "oneOf": [ + { + "description": "the SP itself is being updated", + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "sp" + ] + }, + "expected_active_version": { + "description": "expected contents of the active slot", + "allOf": [ + { + "$ref": "#/components/schemas/ArtifactVersion" + } + ] + }, + "expected_inactive_version": { + "description": "expected contents of the inactive slot", + "allOf": [ + { + "$ref": "#/components/schemas/ExpectedVersion" + } + ] + } + }, + "required": [ + "component", + "expected_active_version", + "expected_inactive_version" + ] + } + ] + }, + "PendingMgsUpdates": { + "type": "object", + "properties": { + "by_baseboard": { + "$ref": "#/components/schemas/IdMapPendingMgsUpdate" + } + }, + "required": [ + "by_baseboard" + ] + }, "PhysicalDiskKind": { "description": "Describes the form factor of physical disks.", "type": "string", @@ -5807,6 +6001,15 @@ "last_port" ] }, + "SpType": { + "description": "SpType\n\n
JSON schema\n\n```json { \"type\": \"string\", \"enum\": [ \"sled\", \"power\", \"switch\" ] } ```
", + "type": "string", + "enum": [ + "sled", + "power", + "switch" + ] + }, "Srv": { "type": "object", "properties": { diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index b0ec33d001f..b1d4d550e2e 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -91,11 +91,11 @@ use nexus_client::{ use nexus_sled_agent_shared::inventory::{ OmicronSledConfig, OmicronZoneConfig, OmicronZoneType, OmicronZonesConfig, }; -use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::{ Blueprint, BlueprintDatasetConfig, BlueprintDatasetDisposition, BlueprintZoneType, CockroachDbPreserveDowngrade, blueprint_zone_type, }; +use nexus_types::deployment::{BlueprintSledConfig, PendingMgsUpdates}; use nexus_types::external_api::views::SledState; use omicron_common::address::get_sled_address; use omicron_common::api::external::Generation; @@ -1540,6 +1540,7 @@ pub(crate) fn build_initial_blueprint_from_sled_configs( Ok(Blueprint { id: BlueprintUuid::new_v4(), sleds: blueprint_sleds, + pending_mgs_updates: PendingMgsUpdates::new(), parent_blueprint_id: None, internal_dns_version, // We don't configure external DNS during RSS, so set it to an initial