diff --git a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs index d5acced5d7a..8cc6a8252a3 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs @@ -5,7 +5,12 @@ //! omdb commands related to update status use anyhow::Context; -use nexus_types::internal_api::views::{SpStatus, ZoneStatus}; +use gateway_types::rot::RotSlot; +use nexus_types::internal_api::views::{ + HostPhase1Status, HostPhase2Status, RotBootloaderStatus, RotStatus, + SpStatus, ZoneStatus, +}; +use omicron_common::disk::M2Slot; use omicron_uuid_kinds::SledUuid; use tabled::Tabled; @@ -19,8 +24,44 @@ pub async fn cmd_nexus_update_status( .context("retrieving update status")? .into_inner(); - print_zones(status.zones.into_iter()); - print_sps(status.sps.into_iter()); + print_rot_bootloaders( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), &s.rot_bootloader)), + ); + println!(); + print_rots( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), &s.rot)), + ); + println!(); + print_sps( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), &s.sp)), + ); + println!(); + print_host_phase_1s( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), &s.host_os_phase_1)), + ); + println!(); + print_host_phase_2s( + status.sleds.iter().map(|s| (s.sled_id, &s.host_phase_2)), + ); + println!(); + print_zones( + status + .sleds + .iter() + .map(|s| (s.sled_id, s.zones.iter().cloned().collect())), + ); Ok(()) } @@ -59,22 +100,85 @@ fn print_zones(zones: impl Iterator)>) { println!("{}", table); } -fn print_sps(sps: impl Iterator) { +fn print_rot_bootloaders<'a>( + bootloaders: impl Iterator, +) { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct BootloaderRow { + baseboard_id: String, + stage0_version: String, + stage0_next_version: String, + } + + let mut rows = Vec::new(); + for (baseboard_id, status) in bootloaders { + let RotBootloaderStatus { stage0_version, stage0_next_version } = + status; + rows.push(BootloaderRow { + baseboard_id, + stage0_version: stage0_version.to_string(), + stage0_next_version: stage0_next_version.to_string(), + }); + } + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("Installed RoT Bootloader Software"); + println!("{}", table); +} + +fn print_rots<'a>(rots: impl Iterator) { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct RotRow { + baseboard_id: String, + slot_a_version: String, + slot_b_version: String, + } + + let mut rows = Vec::new(); + for (baseboard_id, status) in rots { + let RotStatus { active_slot, slot_a_version, slot_b_version } = status; + let (slot_a_suffix, slot_b_suffix) = match active_slot { + Some(RotSlot::A) => (" (active)", ""), + Some(RotSlot::B) => ("", " (active)"), + // This is not expected! Be louder. + None => ("", " (ACTIVE SLOT UNKNOWN)"), + }; + rows.push(RotRow { + baseboard_id, + slot_a_version: format!("{slot_a_version}{slot_a_suffix}"), + slot_b_version: format!("{slot_b_version}{slot_b_suffix}"), + }); + } + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("Installed RoT Software"); + println!("{}", table); +} + +fn print_sps<'a>(sps: impl Iterator) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct SpRow { baseboard_id: String, - sled_id: String, slot0_version: String, slot1_version: String, } let mut rows = Vec::new(); for (baseboard_id, status) in sps { - let SpStatus { sled_id, slot0_version, slot1_version } = status; + let SpStatus { slot0_version, slot1_version } = status; rows.push(SpRow { baseboard_id, - sled_id: sled_id.map_or("".to_string(), |id| id.to_string()), slot0_version: slot0_version.to_string(), slot1_version: slot1_version.to_string(), }); @@ -88,3 +192,88 @@ fn print_sps(sps: impl Iterator) { println!("Installed SP Software"); println!("{}", table); } + +fn print_host_phase_1s<'a>( + phase_1s: impl Iterator, +) { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct HostPhase1Row { + baseboard_id: String, + sled_id: String, + slot_a_version: String, + slot_b_version: String, + } + + let mut rows = Vec::new(); + for (baseboard_id, status) in phase_1s { + match status { + HostPhase1Status::NotASled => continue, + HostPhase1Status::Sled { + sled_id, + active_slot, + slot_a_version, + slot_b_version, + } => { + let (slot_a_suffix, slot_b_suffix) = match active_slot { + Some(M2Slot::A) => (" (active)", ""), + Some(M2Slot::B) => ("", " (active)"), + // This is not expected! Be louder. + None => ("", " (ACTIVE SLOT UNKNOWN)"), + }; + rows.push(HostPhase1Row { + baseboard_id, + sled_id: sled_id + .map_or("".to_string(), |id| id.to_string()), + slot_a_version: format!("{slot_a_version}{slot_a_suffix}"), + slot_b_version: format!("{slot_b_version}{slot_b_suffix}"), + }); + } + } + } + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("Installed Host Phase 1 Software"); + println!("{}", table); +} + +fn print_host_phase_2s<'a>( + sleds: impl Iterator, +) { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct HostPhase2Row { + sled_id: String, + slot_a_version: String, + slot_b_version: String, + } + + let mut rows = Vec::new(); + for (sled_id, status) in sleds { + let HostPhase2Status { boot_disk, slot_a_version, slot_b_version } = + status; + let (slot_a_suffix, slot_b_suffix) = match boot_disk { + Ok(M2Slot::A) => (" (boot disk)", "".to_string()), + Ok(M2Slot::B) => ("", " (boot disk)".to_string()), + // This is not expected! Be louder. + Err(err) => ("", format!(" (BOOT DISK UNKNOWN: {err})")), + }; + rows.push(HostPhase2Row { + sled_id: sled_id.to_string(), + slot_a_version: format!("{slot_a_version}{slot_a_suffix}"), + slot_b_version: format!("{slot_b_version}{slot_b_suffix}"), + }); + } + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("Installed Host Phase 2 Software"); + println!("{}", table); +} diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index f7db6d86612..49d744fc715 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -5,7 +5,6 @@ use crate::deployment::PendingMgsUpdate; use crate::deployment::TargetReleaseDescription; use crate::inventory::BaseboardId; -use crate::inventory::Caboose; use crate::inventory::CabooseWhich; use crate::inventory::Collection; use chrono::DateTime; @@ -13,9 +12,13 @@ use chrono::SecondsFormat; use chrono::Utc; use futures::future::ready; use futures::stream::StreamExt; +use gateway_client::types::SpType; +use gateway_types::rot::RotSlot; use iddqd::IdOrdItem; use iddqd::IdOrdMap; use iddqd::id_upcast; +use nexus_sled_agent_shared::inventory::BootPartitionContents; +use nexus_sled_agent_shared::inventory::BootPartitionDetails; use nexus_sled_agent_shared::inventory::ConfigReconcilerInventoryResult; use nexus_sled_agent_shared::inventory::OmicronZoneImageSource; use nexus_sled_agent_shared::inventory::OmicronZoneType; @@ -23,6 +26,7 @@ use omicron_common::api::external::MacAddr; use omicron_common::api::external::ObjectStream; use omicron_common::api::external::TufArtifactMeta; use omicron_common::api::external::Vni; +use omicron_common::disk::M2Slot; use omicron_common::snake_case_result; use omicron_common::snake_case_result::SnakeCaseResult; use omicron_uuid_kinds::DemoSagaUuid; @@ -41,6 +45,8 @@ use std::time::Duration; use std::time::Instant; use steno::SagaResultErr; use steno::UndoActionError; +use tufaceous_artifact::ArtifactHash; +use tufaceous_artifact::ArtifactKind; use tufaceous_artifact::KnownArtifactKind; use uuid::Uuid; @@ -536,6 +542,33 @@ pub enum TufRepoVersion { Error(String), } +impl TufRepoVersion { + fn for_artifact( + old: &TargetReleaseDescription, + new: &TargetReleaseDescription, + artifact_hash: ArtifactHash, + ) -> TufRepoVersion { + let matching_artifact = |a: &TufArtifactMeta| a.hash == artifact_hash; + + if let Some(new) = new.tuf_repo() { + if new.artifacts.iter().any(matching_artifact) { + return TufRepoVersion::Version( + new.repo.system_version.clone(), + ); + } + } + if let Some(old) = old.tuf_repo() { + if old.artifacts.iter().any(matching_artifact) { + return TufRepoVersion::Version( + old.repo.system_version.clone(), + ); + } + } + + TufRepoVersion::Unknown + } +} + impl Display for TufRepoVersion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -560,112 +593,253 @@ pub struct ZoneStatus { pub version: TufRepoVersion, } +impl IdOrdItem for ZoneStatus { + type Key<'a> = OmicronZoneUuid; + + fn key(&self) -> Self::Key<'_> { + self.zone_id + } + + id_upcast!(); +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct HostPhase2Status { + #[serde(with = "snake_case_result")] + #[schemars(schema_with = "SnakeCaseResult::::json_schema")] + pub boot_disk: Result, + pub slot_a_version: TufRepoVersion, + pub slot_b_version: TufRepoVersion, +} + +impl HostPhase2Status { + fn new( + inv: &BootPartitionContents, + old: &TargetReleaseDescription, + new: &TargetReleaseDescription, + ) -> Self { + Self { + boot_disk: inv.boot_disk.clone(), + slot_a_version: Self::slot_version(old, new, &inv.slot_a), + slot_b_version: Self::slot_version(old, new, &inv.slot_b), + } + } + + fn slot_version( + old: &TargetReleaseDescription, + new: &TargetReleaseDescription, + details: &Result, + ) -> TufRepoVersion { + let artifact_hash = match details.as_ref() { + Ok(details) => details.artifact_hash, + Err(err) => return TufRepoVersion::Error(err.clone()), + }; + TufRepoVersion::for_artifact(old, new, artifact_hash) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct SledAgentUpdateStatus { + pub sled_id: SledUuid, + pub zones: IdOrdMap, + pub host_phase_2: HostPhase2Status, +} + +impl IdOrdItem for SledAgentUpdateStatus { + type Key<'a> = SledUuid; + + fn key(&self) -> Self::Key<'_> { + self.sled_id + } + + id_upcast!(); +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct RotBootloaderStatus { + pub stage0_version: TufRepoVersion, + pub stage0_next_version: TufRepoVersion, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct RotStatus { + pub active_slot: Option, + pub slot_a_version: TufRepoVersion, + pub slot_b_version: TufRepoVersion, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct SpStatus { - pub sled_id: Option, pub slot0_version: TufRepoVersion, pub slot1_version: TufRepoVersion, } -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct UpdateStatus { - pub zones: BTreeMap>, - pub sps: BTreeMap, +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case", tag = "kind")] +pub enum HostPhase1Status { + /// This device has no host phase 1 status because it is not a sled (e.g., + /// it's a PSC or switch). + NotASled, + Sled { + sled_id: Option, + active_slot: Option, + slot_a_version: TufRepoVersion, + slot_b_version: TufRepoVersion, + }, } -impl UpdateStatus { - pub fn new( +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct MgsDrivenUpdateStatus { + // This is a stringified [`BaseboardId`]. We can't use `BaseboardId` as a + // key in JSON maps, so we squish it into a string. + pub baseboard_description: String, + pub rot_bootloader: RotBootloaderStatus, + pub rot: RotStatus, + pub sp: SpStatus, + pub host_os_phase_1: HostPhase1Status, +} + +impl MgsDrivenUpdateStatus { + fn new( + inventory: &Collection, + baseboard_id: &BaseboardId, + sp_type: SpType, old: &TargetReleaseDescription, new: &TargetReleaseDescription, - inventory: &Collection, - ) -> UpdateStatus { - let sleds = inventory - .sled_agents - .iter() - .map(|agent| (&agent.sled_id, &agent.last_reconciliation)); - let zones = sleds - .map(|(sled_id, inv)| { - ( - *sled_id, - inv.as_ref().map_or(vec![], |inv| { - inv.reconciled_omicron_zones() - .map(|(conf, res)| ZoneStatus { - zone_id: conf.id, - zone_type: conf.zone_type.clone(), - version: Self::zone_image_source_to_version( - old, - new, - &conf.image_source, - res, - ), - }) - .collect() - }), - ) - }) - .collect(); - let baseboard_ids: Vec<_> = inventory.sps.keys().cloned().collect(); - - // Find all SP versions and git commits via cabooses - let mut sps: BTreeMap = baseboard_ids - .into_iter() - .map(|baseboard_id| { - let slot0_version = inventory - .caboose_for(CabooseWhich::SpSlot0, &baseboard_id) - .map_or(TufRepoVersion::Unknown, |c| { - Self::caboose_to_version(old, new, &c.caboose) - }); - let slot1_version = inventory - .caboose_for(CabooseWhich::SpSlot1, &baseboard_id) - .map_or(TufRepoVersion::Unknown, |c| { - Self::caboose_to_version(old, new, &c.caboose) - }); - ( - (*baseboard_id).clone(), - SpStatus { sled_id: None, slot0_version, slot1_version }, - ) - }) - .collect(); + sled_ids: &BTreeMap<&BaseboardId, SledUuid>, + ) -> Self { + MgsDrivenUpdateStatusBuilder { + inventory, + baseboard_id, + sp_type, + old, + new, + sled_ids, + } + .build() + } +} - // Fill in the sled_id for the sp if known - for sa in inventory.sled_agents.iter() { - if let Some(baseboard_id) = &sa.baseboard_id { - if let Some(sp) = sps.get_mut(baseboard_id) { - sp.sled_id = Some(sa.sled_id); - } - } +impl IdOrdItem for MgsDrivenUpdateStatus { + type Key<'a> = &'a str; + + fn key(&self) -> Self::Key<'_> { + &self.baseboard_description + } + + id_upcast!(); +} + +struct MgsDrivenUpdateStatusBuilder<'a> { + inventory: &'a Collection, + baseboard_id: &'a BaseboardId, + sp_type: SpType, + old: &'a TargetReleaseDescription, + new: &'a TargetReleaseDescription, + sled_ids: &'a BTreeMap<&'a BaseboardId, SledUuid>, +} + +impl MgsDrivenUpdateStatusBuilder<'_> { + fn build(&self) -> MgsDrivenUpdateStatus { + let host_os_phase_1 = match self.sp_type { + SpType::Power | SpType::Switch => HostPhase1Status::NotASled, + SpType::Sled => HostPhase1Status::Sled { + sled_id: self.sled_ids.get(self.baseboard_id).copied(), + active_slot: self + .inventory + .host_phase_1_active_slot_for(self.baseboard_id) + .map(|s| s.slot), + slot_a_version: self.version_for_host_phase_1(M2Slot::A), + slot_b_version: self.version_for_host_phase_1(M2Slot::B), + }, + }; + + MgsDrivenUpdateStatus { + baseboard_description: self.baseboard_id.to_string(), + rot_bootloader: RotBootloaderStatus { + stage0_version: self.version_for_caboose(CabooseWhich::Stage0), + stage0_next_version: self + .version_for_caboose(CabooseWhich::Stage0Next), + }, + rot: RotStatus { + active_slot: self + .inventory + .rot_state_for(self.baseboard_id) + .map(|state| state.active_slot), + slot_a_version: self + .version_for_caboose(CabooseWhich::RotSlotA), + slot_b_version: self + .version_for_caboose(CabooseWhich::RotSlotB), + }, + sp: SpStatus { + slot0_version: self.version_for_caboose(CabooseWhich::SpSlot0), + slot1_version: self.version_for_caboose(CabooseWhich::SpSlot1), + }, + host_os_phase_1, } + } - let sps = sps.into_iter().map(|(k, v)| (k.to_string(), v)).collect(); + fn version_for_host_phase_1(&self, slot: M2Slot) -> TufRepoVersion { + let Some(artifact_hash) = self + .inventory + .host_phase_1_flash_hash_for(slot, self.baseboard_id) + .map(|h| h.hash) + else { + return TufRepoVersion::Unknown; + }; - UpdateStatus { zones, sps } + TufRepoVersion::for_artifact(self.old, self.new, artifact_hash) } - fn caboose_to_version( - old: &TargetReleaseDescription, - new: &TargetReleaseDescription, - caboose: &Caboose, - ) -> TufRepoVersion { + fn version_for_caboose(&self, which: CabooseWhich) -> TufRepoVersion { + let Some(caboose) = self + .inventory + .caboose_for(which, self.baseboard_id) + .map(|c| &c.caboose) + else { + return TufRepoVersion::Unknown; + }; + + // TODO-cleanup This is really fragile! The RoT and bootloader kinds + // here aren't `KnownArtifactKind`s, so if we add more + // `ArtifactKind` constants we have to remember to update these + // lists. Maybe we fix this as a part of + // https://github.com/oxidecomputer/tufaceous/issues/37? + let matching_kinds = match which { + CabooseWhich::SpSlot0 | CabooseWhich::SpSlot1 => [ + ArtifactKind::from_known(KnownArtifactKind::GimletSp), + ArtifactKind::from_known(KnownArtifactKind::PscSp), + ArtifactKind::from_known(KnownArtifactKind::SwitchSp), + ], + CabooseWhich::RotSlotA => [ + ArtifactKind::GIMLET_ROT_IMAGE_A, + ArtifactKind::PSC_ROT_IMAGE_A, + ArtifactKind::SWITCH_ROT_IMAGE_A, + ], + CabooseWhich::RotSlotB => [ + ArtifactKind::GIMLET_ROT_IMAGE_B, + ArtifactKind::PSC_ROT_IMAGE_B, + ArtifactKind::SWITCH_ROT_IMAGE_B, + ], + CabooseWhich::Stage0 | CabooseWhich::Stage0Next => [ + ArtifactKind::GIMLET_ROT_STAGE0, + ArtifactKind::PSC_ROT_STAGE0, + ArtifactKind::SWITCH_ROT_STAGE0, + ], + }; let matching_caboose = |a: &TufArtifactMeta| { - caboose.board == a.id.name - && matches!( - a.id.kind.to_known(), - Some( - KnownArtifactKind::GimletSp - | KnownArtifactKind::PscSp - | KnownArtifactKind::SwitchSp - ) - ) + Some(&caboose.board) == a.board.as_ref() && caboose.version == a.id.version.to_string() + && matching_kinds.contains(&a.id.kind) }; - if let Some(new) = new.tuf_repo() { + if let Some(new) = self.new.tuf_repo() { if new.artifacts.iter().any(matching_caboose) { return TufRepoVersion::Version( new.repo.system_version.clone(), ); } } - if let Some(old) = old.tuf_repo() { + if let Some(old) = self.old.tuf_repo() { if old.artifacts.iter().any(matching_caboose) { return TufRepoVersion::Version( old.repo.system_version.clone(), @@ -675,6 +849,89 @@ impl UpdateStatus { TufRepoVersion::Unknown } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct UpdateStatus { + pub mgs_driven: IdOrdMap, + pub sleds: IdOrdMap, +} + +impl UpdateStatus { + pub fn new( + old: &TargetReleaseDescription, + new: &TargetReleaseDescription, + inventory: &Collection, + ) -> UpdateStatus { + let sleds = inventory + .sled_agents + .iter() + .map(|sa| { + let Some(inv) = sa.last_reconciliation.as_ref() else { + return SledAgentUpdateStatus { + sled_id: sa.sled_id, + zones: IdOrdMap::new(), + host_phase_2: HostPhase2Status { + boot_disk: Err("unknown".to_string()), + slot_a_version: TufRepoVersion::Unknown, + slot_b_version: TufRepoVersion::Unknown, + }, + }; + }; + + SledAgentUpdateStatus { + sled_id: sa.sled_id, + zones: inv + .reconciled_omicron_zones() + .map(|(conf, res)| ZoneStatus { + zone_id: conf.id, + zone_type: conf.zone_type.clone(), + version: Self::zone_image_source_to_version( + old, + new, + &conf.image_source, + res, + ), + }) + .collect(), + host_phase_2: HostPhase2Status::new( + &inv.boot_partitions, + old, + new, + ), + } + }) + .collect(); + + // Build a map so we can look up the sled ID for a given baseboard (when + // collecting the MGS-driven update status below, all we have is the + // baseboard). + let sled_ids_by_baseboard: BTreeMap<&BaseboardId, SledUuid> = inventory + .sled_agents + .iter() + .filter_map(|sa| { + let baseboard_id = sa.baseboard_id.as_deref()?; + Some((baseboard_id, sa.sled_id)) + }) + .collect(); + + let mgs_driven = inventory + .sps + .iter() + .map(|(baseboard_id, sp)| { + MgsDrivenUpdateStatus::new( + inventory, + baseboard_id, + sp.sp_type, + old, + new, + &sled_ids_by_baseboard, + ) + }) + .collect::>(); + + UpdateStatus { sleds, mgs_driven } + } fn zone_image_source_to_version( old: &TargetReleaseDescription, diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index 603085d9fde..7ca78f007ea 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -211,6 +211,13 @@ impl Collection { .and_then(|by_bb| by_bb.get(baseboard_id)) } + pub fn rot_state_for( + &self, + baseboard_id: &BaseboardId, + ) -> Option<&RotState> { + self.rots.get(baseboard_id) + } + pub fn rot_page_for( &self, which: RotPageWhich, diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index 6915ad92c21..9954f1cfa77 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -4814,6 +4814,118 @@ "id" ] }, + "HostPhase1Status": { + "oneOf": [ + { + "description": "This device has no host phase 1 status because it is not a sled (e.g., it's a PSC or switch).", + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": [ + "not_a_sled" + ] + } + }, + "required": [ + "kind" + ] + }, + { + "type": "object", + "properties": { + "active_slot": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/M2Slot" + } + ] + }, + "kind": { + "type": "string", + "enum": [ + "sled" + ] + }, + "sled_id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TypedUuidForSledKind" + } + ] + }, + "slot_a_version": { + "$ref": "#/components/schemas/TufRepoVersion" + }, + "slot_b_version": { + "$ref": "#/components/schemas/TufRepoVersion" + } + }, + "required": [ + "kind", + "slot_a_version", + "slot_b_version" + ] + } + ] + }, + "HostPhase2Status": { + "type": "object", + "properties": { + "boot_disk": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/M2Slot" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/M2Slot" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "slot_a_version": { + "$ref": "#/components/schemas/TufRepoVersion" + }, + "slot_b_version": { + "$ref": "#/components/schemas/TufRepoVersion" + } + }, + "required": [ + "boot_disk", + "slot_a_version", + "slot_b_version" + ] + }, "IdMapBlueprintDatasetConfig": { "type": "object", "additionalProperties": { @@ -5393,6 +5505,33 @@ "minLength": 5, "maxLength": 17 }, + "MgsDrivenUpdateStatus": { + "type": "object", + "properties": { + "baseboard_description": { + "type": "string" + }, + "host_os_phase_1": { + "$ref": "#/components/schemas/HostPhase1Status" + }, + "rot": { + "$ref": "#/components/schemas/RotStatus" + }, + "rot_bootloader": { + "$ref": "#/components/schemas/RotBootloaderStatus" + }, + "sp": { + "$ref": "#/components/schemas/SpStatus" + } + }, + "required": [ + "baseboard_description", + "host_os_phase_1", + "rot", + "rot_bootloader", + "sp" + ] + }, "MgsUpdateDriverStatus": { "description": "Status of ongoing update attempts, recently completed attempts, and update requests that are waiting for retry.", "type": "object", @@ -7903,6 +8042,21 @@ "time" ] }, + "RotBootloaderStatus": { + "type": "object", + "properties": { + "stage0_next_version": { + "$ref": "#/components/schemas/TufRepoVersion" + }, + "stage0_version": { + "$ref": "#/components/schemas/TufRepoVersion" + } + }, + "required": [ + "stage0_next_version", + "stage0_version" + ] + }, "RotSlot": { "oneOf": [ { @@ -7935,6 +8089,29 @@ } ] }, + "RotStatus": { + "type": "object", + "properties": { + "active_slot": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RotSlot" + } + ] + }, + "slot_a_version": { + "$ref": "#/components/schemas/TufRepoVersion" + }, + "slot_b_version": { + "$ref": "#/components/schemas/TufRepoVersion" + } + }, + "required": [ + "slot_a_version", + "slot_b_version" + ] + }, "RouteConfig": { "type": "object", "properties": { @@ -8277,6 +8454,40 @@ "usable_physical_ram" ] }, + "SledAgentUpdateStatus": { + "type": "object", + "properties": { + "host_phase_2": { + "$ref": "#/components/schemas/HostPhase2Status" + }, + "sled_id": { + "$ref": "#/components/schemas/TypedUuidForSledKind" + }, + "zones": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ZoneStatus" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneStatus" + }, + "uniqueItems": true + } + }, + "required": [ + "host_phase_2", + "sled_id", + "zones" + ] + }, "SledCpuFamily": { "description": "Identifies the kind of CPU present on a sled, determined by reading CPUID.\n\nThis is intended to broadly support the control plane answering the question \"can I run this instance on that sled?\" given an instance with either no or some CPU platform requirement. It is not enough information for more precise placement questions - for example, is a CPU a high-frequency part or many-core part? We don't include Genoa here, but in that CPU family there are high frequency parts, many-core parts, and large-cache parts. To support those questions (or satisfactorily answer #8730) we would need to collect additional information and send it along.", "oneOf": [ @@ -8501,14 +8712,6 @@ "SpStatus": { "type": "object", "properties": { - "sled_id": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/TypedUuidForSledKind" - } - ] - }, "slot0_version": { "$ref": "#/components/schemas/TufRepoVersion" }, @@ -8945,25 +9148,46 @@ "UpdateStatus": { "type": "object", "properties": { - "sps": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/SpStatus" - } + "mgs_driven": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/MgsDrivenUpdateStatus" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/MgsDrivenUpdateStatus" + }, + "uniqueItems": true }, - "zones": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ZoneStatus" - } - } + "sleds": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/SledAgentUpdateStatus" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/SledAgentUpdateStatus" + }, + "uniqueItems": true } }, "required": [ - "sps", - "zones" + "mgs_driven", + "sleds" ] }, "UplinkAddressConfig": {