diff --git a/Cargo.lock b/Cargo.lock index 4a4684ef720..388ffe4e2c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3512,6 +3512,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "chrono", + "daft", "gateway-messages", "omicron-workspace-hack", "progenitor 0.9.1", diff --git a/clients/gateway-client/Cargo.toml b/clients/gateway-client/Cargo.toml index 96f6484122c..56ae822a249 100644 --- a/clients/gateway-client/Cargo.toml +++ b/clients/gateway-client/Cargo.toml @@ -10,6 +10,7 @@ workspace = true [dependencies] base64.workspace = true chrono.workspace = true +daft.workspace = true gateway-messages.workspace = true progenitor.workspace = true rand.workspace = true diff --git a/clients/gateway-client/src/lib.rs b/clients/gateway-client/src/lib.rs index a78204d731b..0daa4817c8a 100644 --- a/clients/gateway-client/src/lib.rs +++ b/clients/gateway-client/src/lib.rs @@ -69,6 +69,7 @@ progenitor::generate_api!( SpIgnition = { derives = [PartialEq, Eq, PartialOrd, Ord] }, SpIgnitionSystemType = { derives = [Copy, PartialEq, Eq, PartialOrd, Ord] }, SpState = { derives = [PartialEq, Eq, PartialOrd, Ord] }, + SpType = { derives = [daft::Diffable] }, }, ); diff --git a/common/src/update.rs b/common/src/update.rs index 14f0c1adba9..0ee322d75ba 100644 --- a/common/src/update.rs +++ b/common/src/update.rs @@ -4,6 +4,7 @@ use std::{fmt, str::FromStr}; +use daft::Diffable; use hex::FromHexError; use schemars::{ JsonSchema, @@ -63,6 +64,7 @@ impl From for ArtifactId { /// by name and version. This type indicates that. #[derive( Debug, + Diffable, Clone, PartialEq, Eq, @@ -85,6 +87,7 @@ pub struct ArtifactHashId { #[derive( Copy, Clone, + Diffable, Eq, PartialEq, Ord, @@ -94,6 +97,7 @@ pub struct ArtifactHashId { Deserialize, JsonSchema, )] +#[daft(leaf)] #[serde(transparent)] #[cfg_attr(feature = "testing", derive(test_strategy::Arbitrary))] pub struct ArtifactHash( 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 92cdc66465f..6e8de1e845d 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 @@ -466,6 +468,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 142371feb0a..0baec5c7c99 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -872,6 +872,8 @@ impl DataStore { Ok(Blueprint { id: blueprint_id, + // TODO these need to be serialized to the database. + pending_mgs_updates: BTreeMap::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 861ff538e69..2b432e8f826 100644 --- a/nexus/db-queries/src/db/datastore/rack.rs +++ b/nexus/db-queries/src/db/datastore/rack.rs @@ -1054,6 +1054,7 @@ mod test { blueprint: Blueprint { id: BlueprintUuid::new_v4(), sleds: BTreeMap::new(), + pending_mgs_updates: BTreeMap::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -1539,6 +1540,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: BTreeMap::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -1796,6 +1798,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: BTreeMap::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -2002,6 +2005,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: BTreeMap::new(), cockroachdb_setting_preserve_downgrade: CockroachDbPreserveDowngrade::DoNotModify, parent_blueprint_id: None, @@ -2138,6 +2142,7 @@ mod test { let blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: make_sled_config_only_zones(blueprint_zones), + pending_mgs_updates: BTreeMap::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 c101c1cade8..00682b5caad 100644 --- a/nexus/reconfigurator/execution/src/dns.rs +++ b/nexus/reconfigurator/execution/src/dns.rs @@ -685,6 +685,7 @@ mod test { let mut blueprint = Blueprint { id: BlueprintUuid::new_v4(), sleds: blueprint_sleds, + pending_mgs_updates: BTreeMap::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 8ed3862bda0..104f0f60b57 100644 --- a/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs +++ b/nexus/reconfigurator/planning/src/blueprint_builder/builder.rs @@ -454,6 +454,7 @@ impl<'a> BlueprintBuilder<'a> { Blueprint { id: rng.next_blueprint(), sleds, + pending_mgs_updates: BTreeMap::new(), parent_blueprint_id: None, internal_dns_version: Generation::new(), external_dns_version: Generation::new(), @@ -688,6 +689,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/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs index dcd1e6b4fca..cbc79d718ab 100644 --- a/nexus/src/app/background/tasks/blueprint_execution.rs +++ b/nexus/src/app/background/tasks/blueprint_execution.rs @@ -252,6 +252,7 @@ mod test { let blueprint = Blueprint { id, sleds: blueprint_sleds, + pending_mgs_updates: BTreeMap::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..31638b00f1d 100644 --- a/nexus/src/app/background/tasks/blueprint_load.rs +++ b/nexus/src/app/background/tasks/blueprint_load.rs @@ -217,6 +217,7 @@ mod test { Blueprint { id, sleds: BTreeMap::new(), + pending_mgs_updates: BTreeMap::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 8f5bd9a1b81..8a758a4b238 100644 --- a/nexus/test-utils/src/lib.rs +++ b/nexus/test-utils/src/lib.rs @@ -925,6 +925,7 @@ impl<'a, N: NexusServer> ControlPlaneTestContextBuilder<'a, N> { Blueprint { id: BlueprintUuid::new_v4(), sleds: blueprint_sleds, + pending_mgs_updates: BTreeMap::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 fa27efa0f57..711db005bf1 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -101,7 +101,11 @@ use blueprint_display::{ }; use id_map::{IdMap, IdMappable}; +use crate::inventory::BaseboardId; pub use blueprint_diff::BlueprintDiffSummary; +use blueprint_display::BpPendingMgsUpdates; +use gateway_client::types::SpType; +use omicron_common::update::ArtifactHashId; /// 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 +155,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: BTreeMap, + /// which blueprint this blueprint is based on pub parent_blueprint_id: Option, @@ -488,6 +495,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 +577,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 + .values() + .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(()) } } @@ -962,6 +1002,21 @@ impl fmt::Display for BlueprintZoneImageSource { } } +#[derive( + Clone, Debug, Eq, PartialEq, JsonSchema, Deserialize, Serialize, Diffable, +)] +pub struct PendingMgsUpdate { + /// id of the baseboard that we're going to update + baseboard_id: BaseboardId, + /// what type of baseboard this is + sp_type: SpType, + /// last known MGS slot (cubby number) of the baseboard + slot_id: u16, + /// which artifact to apply to this device + /// (implies which component is being updated) + artifact_hash_id: ArtifactHashId, +} + /// 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..807e525c6bd 100644 --- a/nexus/types/src/deployment/blueprint_diff.rs +++ b/nexus/types/src/deployment/blueprint_diff.rs @@ -58,6 +58,9 @@ impl<'a> BlueprintDiffSummary<'a> { let BlueprintDiff { // Fields in which changes are meaningful. sleds, + // TODO Will need to diff these when we can actually create + // blueprints with pending MGS updates. + pending_mgs_updates: _, clickhouse_cluster_config, // Metadata fields for which changes don't reflect semantic // changes from one blueprint to the next. diff --git a/nexus/types/src/deployment/blueprint_display.rs b/nexus/types/src/deployment/blueprint_display.rs index ee11559cb35..bfdcbef98bc 100644 --- a/nexus/types/src/deployment/blueprint_display.rs +++ b/nexus/types/src/deployment/blueprint_display.rs @@ -100,23 +100,23 @@ impl fmt::Display for BpGeneration { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BpGeneration::Value(generation) => { - write!(f, "at generation {generation}") + write!(f, " at generation {generation}") } BpGeneration::Diff { before: None, after: Some(after) } => { - write!(f, "at generation {after}") + write!(f, " at generation {after}") } BpGeneration::Diff { before: Some(before), after: None } => { - write!(f, "from generation {before}") + write!(f, " from generation {before}") } BpGeneration::Diff { before: Some(before), after: Some(after) } => { if before == after { - write!(f, "at generation {after}") + write!(f, " at generation {after}") } else { - write!(f, "generation {before} -> {after}") + write!(f, " generation {before} -> {after}") } } BpGeneration::Diff { before: None, after: None } => { - write!(f, "unknown generation") + write!(f, " unknown generation") } } } @@ -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/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index 95c4f80e05f..c2aeba993fa 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -14,6 +14,7 @@ use crate::external_api::params::UninitializedSledId; use chrono::DateTime; use chrono::Utc; use clickhouse_admin_types::ClickhouseKeeperClusterMembership; +use daft::Diffable; pub use gateway_client::types::PowerState; pub use gateway_client::types::RotImageError; pub use gateway_client::types::RotSlot; @@ -34,6 +35,7 @@ use omicron_uuid_kinds::CollectionUuid; use omicron_uuid_kinds::DatasetUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use std::collections::BTreeMap; @@ -205,7 +207,16 @@ impl Collection { /// 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. #[derive( - Clone, Debug, Ord, Eq, PartialOrd, PartialEq, Deserialize, Serialize, + Clone, + Debug, + Diffable, + Ord, + Eq, + PartialOrd, + PartialEq, + Deserialize, + Serialize, + JsonSchema, )] pub struct BaseboardId { /// Oxide Part Number diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index a09ebce4053..0941f959b53 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -1571,6 +1571,25 @@ } ] }, + "ArtifactHashId": { + "description": "A hash-based identifier for an artifact.\n\nSome places, e.g. the installinator, request artifacts by hash rather than by name and version. This type indicates that.", + "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,13 @@ } ] }, + "pending_mgs_updates": { + "description": "List of pending MGS-mediated updates", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PendingMgsUpdate" + } + }, "sleds": { "description": "A map of sled id -> desired configuration of the sled.", "type": "object", @@ -1968,6 +2012,7 @@ "external_dns_version", "id", "internal_dns_version", + "pending_mgs_updates", "sleds", "time_created" ] @@ -4625,6 +4670,47 @@ "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" + } + ] + }, + "baseboard_id": { + "description": "id of the baseboard that we're going to update", + "allOf": [ + { + "$ref": "#/components/schemas/BaseboardId" + } + ] + }, + "slot_id": { + "description": "last known MGS slot (cubby number) of the baseboard", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "sp_type": { + "description": "what type of baseboard this is", + "allOf": [ + { + "$ref": "#/components/schemas/SpType" + } + ] + } + }, + "required": [ + "artifact_hash_id", + "baseboard_id", + "slot_id", + "sp_type" + ] + }, "PhysicalDiskKind": { "description": "Describes the form factor of physical disks.", "type": "string", @@ -5768,6 +5854,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..c1de8fe0fe4 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -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: BTreeMap::new(), parent_blueprint_id: None, internal_dns_version, // We don't configure external DNS during RSS, so set it to an initial