From db313651029d34897b6e0c1e1cea46f7f1f4a478 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 14:30:00 -0400 Subject: [PATCH 01/21] rework update status types to add spots for remaining components --- nexus/types/src/internal_api/views.rs | 133 ++++++++++++++++++-------- 1 file changed, 92 insertions(+), 41 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index f7db6d86612..a245f2615be 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -31,7 +31,6 @@ use schemars::JsonSchema; use semver::Version; use serde::Deserialize; use serde::Serialize; -use std::collections::BTreeMap; use std::collections::VecDeque; use std::fmt::Display; use std::net::IpAddr; @@ -560,17 +559,65 @@ 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 SpStatus { - pub sled_id: Option, pub slot0_version: TufRepoVersion, pub slot1_version: TufRepoVersion, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct SledAgentUpdateStatus { + pub sled_id: SledUuid, + pub zones: IdOrdMap, + pub host_phase_2: (), +} + +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 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 sled_id: Option, + pub rot_bootloader: (), + pub rot: (), + pub sp: SpStatus, + pub host_os_phase_1: (), +} + +impl IdOrdItem for MgsDrivenUpdateStatus { + type Key<'a> = &'a str; + + fn key(&self) -> Self::Key<'_> { + &self.baseboard_description + } + + id_upcast!(); +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct UpdateStatus { - pub zones: BTreeMap>, - pub sps: BTreeMap, + pub mgs_driven: IdOrdMap, + pub sleds: IdOrdMap, } impl UpdateStatus { @@ -582,14 +629,14 @@ impl 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 { + .map(|sa| SledAgentUpdateStatus { + sled_id: sa.sled_id, + zones: sa + .last_reconciliation + .iter() + .flat_map(|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( @@ -598,47 +645,51 @@ impl UpdateStatus { &conf.image_source, res, ), - }) - .collect() - }), - ) + } + }) + }) + .collect(), + host_phase_2: (), }) .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 }, - ) + + let mut mgs_driven = inventory + .sps + .keys() + .map(|baseboard_id| MgsDrivenUpdateStatus { + baseboard_description: baseboard_id.to_string(), + // We'll fill this in below. + sled_id: None, + rot_bootloader: (), + rot: (), + sp: { + 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) + }); + SpStatus { slot0_version, slot1_version } + }, + host_os_phase_1: (), }) - .collect(); + .collect::>(); // 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) { + let baseboard_id = baseboard_id.to_string(); + if let Some(mut sp) = mgs_driven.get_mut(baseboard_id.as_str()) { sp.sled_id = Some(sa.sled_id); } } } - let sps = sps.into_iter().map(|(k, v)| (k.to_string(), v)).collect(); - - UpdateStatus { zones, sps } + UpdateStatus { sleds, mgs_driven } } fn caboose_to_version( From 76ddd8a2968a306486f60d8c319a90d5f768cc25 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 14:40:38 -0400 Subject: [PATCH 02/21] refactor caboose_to_version into MgsDrivenUpdateStatusBuilder --- nexus/types/src/internal_api/views.rs | 126 ++++++++++++++------------ 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index a245f2615be..85dcee72169 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; @@ -614,6 +613,54 @@ impl IdOrdItem for MgsDrivenUpdateStatus { id_upcast!(); } +struct MgsDrivenUpdateStatusBuilder<'a> { + inventory: &'a Collection, + baseboard_id: &'a BaseboardId, + old: &'a TargetReleaseDescription, + new: &'a TargetReleaseDescription, +} + +impl MgsDrivenUpdateStatusBuilder<'_> { + fn version_for(&self, which: CabooseWhich) -> TufRepoVersion { + let Some(caboose) = self + .inventory + .caboose_for(which, self.baseboard_id) + .map(|c| &c.caboose) + else { + return TufRepoVersion::Unknown; + }; + + let matching_caboose = |a: &TufArtifactMeta| { + caboose.board == a.id.name + && matches!( + a.id.kind.to_known(), + Some( + KnownArtifactKind::GimletSp + | KnownArtifactKind::PscSp + | KnownArtifactKind::SwitchSp + ) + ) + && caboose.version == a.id.version.to_string() + }; + 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) = self.old.tuf_repo() { + if old.artifacts.iter().any(matching_caboose) { + return TufRepoVersion::Version( + old.repo.system_version.clone(), + ); + } + } + + TufRepoVersion::Unknown + } +} + #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct UpdateStatus { pub mgs_driven: IdOrdMap, @@ -656,26 +703,25 @@ impl UpdateStatus { let mut mgs_driven = inventory .sps .keys() - .map(|baseboard_id| MgsDrivenUpdateStatus { - baseboard_description: baseboard_id.to_string(), - // We'll fill this in below. - sled_id: None, - rot_bootloader: (), - rot: (), - sp: { - 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) - }); - SpStatus { slot0_version, slot1_version } - }, - host_os_phase_1: (), + .map(|baseboard_id| { + let b = MgsDrivenUpdateStatusBuilder { + inventory, + baseboard_id, + old, + new, + }; + MgsDrivenUpdateStatus { + baseboard_description: baseboard_id.to_string(), + // We'll fill this in below. + sled_id: None, + rot_bootloader: (), + rot: (), + sp: SpStatus { + slot0_version: b.version_for(CabooseWhich::SpSlot0), + slot1_version: b.version_for(CabooseWhich::SpSlot1), + }, + host_os_phase_1: (), + } }) .collect::>(); @@ -683,7 +729,8 @@ impl UpdateStatus { for sa in inventory.sled_agents.iter() { if let Some(baseboard_id) = &sa.baseboard_id { let baseboard_id = baseboard_id.to_string(); - if let Some(mut sp) = mgs_driven.get_mut(baseboard_id.as_str()) { + if let Some(mut sp) = mgs_driven.get_mut(baseboard_id.as_str()) + { sp.sled_id = Some(sa.sled_id); } } @@ -692,41 +739,6 @@ impl UpdateStatus { UpdateStatus { sleds, mgs_driven } } - fn caboose_to_version( - old: &TargetReleaseDescription, - new: &TargetReleaseDescription, - caboose: &Caboose, - ) -> TufRepoVersion { - let matching_caboose = |a: &TufArtifactMeta| { - caboose.board == a.id.name - && matches!( - a.id.kind.to_known(), - Some( - KnownArtifactKind::GimletSp - | KnownArtifactKind::PscSp - | KnownArtifactKind::SwitchSp - ) - ) - && caboose.version == a.id.version.to_string() - }; - if let Some(new) = 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 old.artifacts.iter().any(matching_caboose) { - return TufRepoVersion::Version( - old.repo.system_version.clone(), - ); - } - } - - TufRepoVersion::Unknown - } - fn zone_image_source_to_version( old: &TargetReleaseDescription, new: &TargetReleaseDescription, From eaedf9bc10ba5399c36b954459a1f3409d887d8c Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 14:51:00 -0400 Subject: [PATCH 03/21] move more to MgsUpdateStatusBuilder --- nexus/types/src/internal_api/views.rs | 82 ++++++++++++++++++--------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 85dcee72169..918e21e9931 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -30,6 +30,7 @@ use schemars::JsonSchema; use semver::Version; use serde::Deserialize; use serde::Serialize; +use std::collections::BTreeMap; use std::collections::VecDeque; use std::fmt::Display; use std::net::IpAddr; @@ -603,6 +604,25 @@ pub struct MgsDrivenUpdateStatus { pub host_os_phase_1: (), } +impl MgsDrivenUpdateStatus { + fn new( + inventory: &Collection, + baseboard_id: &BaseboardId, + old: &TargetReleaseDescription, + new: &TargetReleaseDescription, + sled_ids: &BTreeMap<&BaseboardId, SledUuid>, + ) -> Self { + MgsDrivenUpdateStatusBuilder { + inventory, + baseboard_id, + old, + new, + sled_ids, + } + .build() + } +} + impl IdOrdItem for MgsDrivenUpdateStatus { type Key<'a> = &'a str; @@ -618,9 +638,29 @@ struct MgsDrivenUpdateStatusBuilder<'a> { baseboard_id: &'a BaseboardId, old: &'a TargetReleaseDescription, new: &'a TargetReleaseDescription, + sled_ids: &'a BTreeMap<&'a BaseboardId, SledUuid>, } impl MgsDrivenUpdateStatusBuilder<'_> { + fn build(&self) -> MgsDrivenUpdateStatus { + let sled_id = self.sled_ids.get(&self.baseboard_id).copied(); + MgsDrivenUpdateStatus { + baseboard_description: self.baseboard_id.to_string(), + sled_id, + rot_bootloader: (), + rot: (), + sp: self.sp(), + host_os_phase_1: (), + } + } + + fn sp(&self) -> SpStatus { + SpStatus { + slot0_version: self.version_for(CabooseWhich::SpSlot0), + slot1_version: self.version_for(CabooseWhich::SpSlot1), + } + } + fn version_for(&self, which: CabooseWhich) -> TufRepoVersion { let Some(caboose) = self .inventory @@ -700,42 +740,32 @@ impl UpdateStatus { }) .collect(); - let mut mgs_driven = inventory + // 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 .keys() .map(|baseboard_id| { - let b = MgsDrivenUpdateStatusBuilder { + MgsDrivenUpdateStatus::new( inventory, baseboard_id, old, new, - }; - MgsDrivenUpdateStatus { - baseboard_description: baseboard_id.to_string(), - // We'll fill this in below. - sled_id: None, - rot_bootloader: (), - rot: (), - sp: SpStatus { - slot0_version: b.version_for(CabooseWhich::SpSlot0), - slot1_version: b.version_for(CabooseWhich::SpSlot1), - }, - host_os_phase_1: (), - } + &sled_ids_by_baseboard, + ) }) .collect::>(); - // 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 { - let baseboard_id = baseboard_id.to_string(); - if let Some(mut sp) = mgs_driven.get_mut(baseboard_id.as_str()) - { - sp.sled_id = Some(sa.sled_id); - } - } - } - UpdateStatus { sleds, mgs_driven } } From cd82b0ca8b609bf6613adf0c7cb91982614c2e9d Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 14:54:45 -0400 Subject: [PATCH 04/21] add rot status --- nexus/types/src/internal_api/views.rs | 34 +++++++++++++++++---------- nexus/types/src/inventory.rs | 7 ++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 918e21e9931..5483b47f6c1 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -12,6 +12,7 @@ use chrono::SecondsFormat; use chrono::Utc; use futures::future::ready; use futures::stream::StreamExt; +use gateway_types::rot::RotSlot; use iddqd::IdOrdItem; use iddqd::IdOrdMap; use iddqd::id_upcast; @@ -569,6 +570,13 @@ impl IdOrdItem for ZoneStatus { id_upcast!(); } +#[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 slot0_version: TufRepoVersion, @@ -599,7 +607,7 @@ pub struct MgsDrivenUpdateStatus { pub baseboard_description: String, pub sled_id: Option, pub rot_bootloader: (), - pub rot: (), + pub rot: RotStatus, pub sp: SpStatus, pub host_os_phase_1: (), } @@ -643,24 +651,26 @@ struct MgsDrivenUpdateStatusBuilder<'a> { impl MgsDrivenUpdateStatusBuilder<'_> { fn build(&self) -> MgsDrivenUpdateStatus { - let sled_id = self.sled_ids.get(&self.baseboard_id).copied(); MgsDrivenUpdateStatus { baseboard_description: self.baseboard_id.to_string(), - sled_id, + sled_id: self.sled_ids.get(self.baseboard_id).copied(), rot_bootloader: (), - rot: (), - sp: self.sp(), + rot: RotStatus { + active_slot: self + .inventory + .rot_state_for(self.baseboard_id) + .map(|state| state.active_slot), + slot_a_version: self.version_for(CabooseWhich::RotSlotA), + slot_b_version: self.version_for(CabooseWhich::RotSlotB), + }, + sp: SpStatus { + slot0_version: self.version_for(CabooseWhich::SpSlot0), + slot1_version: self.version_for(CabooseWhich::SpSlot1), + }, host_os_phase_1: (), } } - fn sp(&self) -> SpStatus { - SpStatus { - slot0_version: self.version_for(CabooseWhich::SpSlot0), - slot1_version: self.version_for(CabooseWhich::SpSlot1), - } - } - fn version_for(&self, which: CabooseWhich) -> TufRepoVersion { let Some(caboose) = self .inventory 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, From 6038fcca40a9c4d2d9f6d903d7633374504923b1 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 14:56:57 -0400 Subject: [PATCH 05/21] add rot bootloader status --- nexus/types/src/internal_api/views.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 5483b47f6c1..90d11fcd608 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -570,6 +570,12 @@ impl IdOrdItem for ZoneStatus { id_upcast!(); } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct RotBootloaderStatus { + pub stage_0_version: TufRepoVersion, + pub stage_0_next_version: TufRepoVersion, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct RotStatus { pub active_slot: Option, @@ -606,7 +612,7 @@ pub struct MgsDrivenUpdateStatus { // key in JSON maps, so we squish it into a string. pub baseboard_description: String, pub sled_id: Option, - pub rot_bootloader: (), + pub rot_bootloader: RotBootloaderStatus, pub rot: RotStatus, pub sp: SpStatus, pub host_os_phase_1: (), @@ -654,7 +660,11 @@ impl MgsDrivenUpdateStatusBuilder<'_> { MgsDrivenUpdateStatus { baseboard_description: self.baseboard_id.to_string(), sled_id: self.sled_ids.get(self.baseboard_id).copied(), - rot_bootloader: (), + rot_bootloader: RotBootloaderStatus { + stage_0_version: self.version_for(CabooseWhich::Stage0), + stage_0_next_version: self + .version_for(CabooseWhich::Stage0Next), + }, rot: RotStatus { active_slot: self .inventory From a04feda16eb987f16b444ce63cd9776c8ffb2cba Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 15:13:28 -0400 Subject: [PATCH 06/21] fix board and kind checks in version_for() --- nexus/types/src/internal_api/views.rs | 41 ++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 90d11fcd608..55c44547148 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -41,6 +41,7 @@ use std::time::Duration; use std::time::Instant; use steno::SagaResultErr; use steno::UndoActionError; +use tufaceous_artifact::ArtifactKind; use tufaceous_artifact::KnownArtifactKind; use uuid::Uuid; @@ -690,17 +691,37 @@ impl MgsDrivenUpdateStatusBuilder<'_> { 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 - ) - ) - && caboose.version == a.id.version.to_string() + Some(&caboose.board) == a.board.as_ref() + && caboose.version != a.id.version.to_string() + && matching_kinds.contains(&a.id.kind) }; if let Some(new) = self.new.tuf_repo() { if new.artifacts.iter().any(matching_caboose) { From 2763c6123402322550457957656a8dd7844121ee Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 15:20:29 -0400 Subject: [PATCH 07/21] add host phase 1 --- nexus/types/src/internal_api/views.rs | 95 ++++++++++++++++++++------- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 55c44547148..659a3b7b751 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -23,6 +23,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; @@ -571,6 +572,23 @@ impl IdOrdItem for ZoneStatus { id_upcast!(); } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct SledAgentUpdateStatus { + pub sled_id: SledUuid, + pub zones: IdOrdMap, + pub host_phase_2: (), +} + +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 stage_0_version: TufRepoVersion, @@ -591,20 +609,10 @@ pub struct SpStatus { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct SledAgentUpdateStatus { - pub sled_id: SledUuid, - pub zones: IdOrdMap, - pub host_phase_2: (), -} - -impl IdOrdItem for SledAgentUpdateStatus { - type Key<'a> = SledUuid; - - fn key(&self) -> Self::Key<'_> { - self.sled_id - } - - id_upcast!(); +pub struct HostPhase1Status { + pub active_slot: Option, + pub slot_a_version: TufRepoVersion, + pub slot_b_version: TufRepoVersion, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -616,7 +624,7 @@ pub struct MgsDrivenUpdateStatus { pub rot_bootloader: RotBootloaderStatus, pub rot: RotStatus, pub sp: SpStatus, - pub host_os_phase_1: (), + pub host_os_phase_1: HostPhase1Status, } impl MgsDrivenUpdateStatus { @@ -662,27 +670,68 @@ impl MgsDrivenUpdateStatusBuilder<'_> { baseboard_description: self.baseboard_id.to_string(), sled_id: self.sled_ids.get(self.baseboard_id).copied(), rot_bootloader: RotBootloaderStatus { - stage_0_version: self.version_for(CabooseWhich::Stage0), + stage_0_version: self.version_for_caboose(CabooseWhich::Stage0), stage_0_next_version: self - .version_for(CabooseWhich::Stage0Next), + .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(CabooseWhich::RotSlotA), - slot_b_version: self.version_for(CabooseWhich::RotSlotB), + 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(CabooseWhich::SpSlot0), - slot1_version: self.version_for(CabooseWhich::SpSlot1), + slot0_version: self.version_for_caboose(CabooseWhich::SpSlot0), + slot1_version: self.version_for_caboose(CabooseWhich::SpSlot1), + }, + host_os_phase_1: HostPhase1Status { + 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), }, - host_os_phase_1: (), } } - fn version_for(&self, which: CabooseWhich) -> TufRepoVersion { + 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; + }; + + // We could also filter down on a.id.kind, but we don't really need to: + // we're checking a sha256 hash from inventory, so we only expect one + // match anyway. + let matching_artifact = |a: &TufArtifactMeta| a.hash == artifact_hash; + + if let Some(new) = self.new.tuf_repo() { + if new.artifacts.iter().any(matching_artifact) { + return TufRepoVersion::Version( + new.repo.system_version.clone(), + ); + } + } + if let Some(old) = self.old.tuf_repo() { + if old.artifacts.iter().any(matching_artifact) { + return TufRepoVersion::Version( + old.repo.system_version.clone(), + ); + } + } + + TufRepoVersion::Unknown + } + + fn version_for_caboose(&self, which: CabooseWhich) -> TufRepoVersion { let Some(caboose) = self .inventory .caboose_for(which, self.baseboard_id) From 23f755e3dcbd0daf81836b20378dc35cbcf11b67 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 15:32:50 -0400 Subject: [PATCH 08/21] add host phase 2 --- nexus/types/src/internal_api/views.rs | 140 ++++++++++++++++++-------- 1 file changed, 98 insertions(+), 42 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 659a3b7b751..3d9f023de20 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -16,6 +16,8 @@ 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; @@ -42,6 +44,7 @@ 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; @@ -538,6 +541,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 { @@ -572,11 +602,44 @@ impl IdOrdItem for ZoneStatus { id_upcast!(); } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct HostPhase2Status { + pub boot_disk: Option>, + pub slot_a_version: TufRepoVersion, + pub slot_b_version: TufRepoVersion, +} + +impl HostPhase2Status { + fn new( + inv: &BootPartitionContents, + old: &TargetReleaseDescription, + new: &TargetReleaseDescription, + ) -> Self { + Self { + boot_disk: Some(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: (), + pub host_phase_2: HostPhase2Status, } impl IdOrdItem for SledAgentUpdateStatus { @@ -708,27 +771,7 @@ impl MgsDrivenUpdateStatusBuilder<'_> { return TufRepoVersion::Unknown; }; - // We could also filter down on a.id.kind, but we don't really need to: - // we're checking a sha256 hash from inventory, so we only expect one - // match anyway. - let matching_artifact = |a: &TufArtifactMeta| a.hash == artifact_hash; - - if let Some(new) = self.new.tuf_repo() { - if new.artifacts.iter().any(matching_artifact) { - return TufRepoVersion::Version( - new.repo.system_version.clone(), - ); - } - } - if let Some(old) = self.old.tuf_repo() { - if old.artifacts.iter().any(matching_artifact) { - return TufRepoVersion::Version( - old.repo.system_version.clone(), - ); - } - } - - TufRepoVersion::Unknown + TufRepoVersion::for_artifact(self.old, self.new, artifact_hash) } fn version_for_caboose(&self, which: CabooseWhich) -> TufRepoVersion { @@ -806,27 +849,40 @@ impl UpdateStatus { let sleds = inventory .sled_agents .iter() - .map(|sa| SledAgentUpdateStatus { - sled_id: sa.sled_id, - zones: sa - .last_reconciliation - .iter() - .flat_map(|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, - ), - } + .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: None, + 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: (), + .collect(), + host_phase_2: HostPhase2Status::new( + &inv.boot_partitions, + old, + new, + ), + } }) .collect(); From 27591884f9c95a791934cd89fe1b4163c8f46cc0 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 15:50:18 -0400 Subject: [PATCH 09/21] update omdb --- .../omdb/src/bin/omdb/nexus/update_status.rs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) 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..e778644d375 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs @@ -19,8 +19,18 @@ 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_zones( + status + .sleds + .iter() + .map(|s| (s.sled_id, s.zones.iter().cloned().collect())), + ); + print_sps( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), s.sled_id, &s.sp)), + ); Ok(()) } @@ -59,7 +69,9 @@ fn print_zones(zones: impl Iterator)>) { println!("{}", table); } -fn print_sps(sps: impl Iterator) { +fn print_sps<'a>( + sps: impl Iterator, &'a SpStatus)>, +) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct SpRow { @@ -70,8 +82,8 @@ fn print_sps(sps: impl Iterator) { } let mut rows = Vec::new(); - for (baseboard_id, status) in sps { - let SpStatus { sled_id, slot0_version, slot1_version } = status; + for (baseboard_id, sled_id, status) in sps { + let SpStatus { slot0_version, slot1_version } = status; rows.push(SpRow { baseboard_id, sled_id: sled_id.map_or("".to_string(), |id| id.to_string()), From 323d957336eb0596eea44977485f37f96ead5f16 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 15:56:02 -0400 Subject: [PATCH 10/21] openapi --- nexus/types/src/internal_api/views.rs | 8 +- openapi/nexus-internal.json | 244 +++++++++++++++++++++++--- 2 files changed, 226 insertions(+), 26 deletions(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 3d9f023de20..f53f0a7b29c 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -604,7 +604,9 @@ impl IdOrdItem for ZoneStatus { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct HostPhase2Status { - pub boot_disk: Option>, + #[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, } @@ -616,7 +618,7 @@ impl HostPhase2Status { new: &TargetReleaseDescription, ) -> Self { Self { - boot_disk: Some(inv.boot_disk.clone()), + 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), } @@ -855,7 +857,7 @@ impl UpdateStatus { sled_id: sa.sled_id, zones: IdOrdMap::new(), host_phase_2: HostPhase2Status { - boot_disk: None, + boot_disk: Err("unknown".to_string()), slot_a_version: TufRepoVersion::Unknown, slot_b_version: TufRepoVersion::Unknown, }, diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index 5fc544306fc..538f47832c0 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -4814,6 +4814,84 @@ "id" ] }, + "HostPhase1Status": { + "type": "object", + "properties": { + "active_slot": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/M2Slot" + } + ] + }, + "slot_a_version": { + "$ref": "#/components/schemas/TufRepoVersion" + }, + "slot_b_version": { + "$ref": "#/components/schemas/TufRepoVersion" + } + }, + "required": [ + "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 +5471,41 @@ "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" + }, + "sled_id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TypedUuidForSledKind" + } + ] + }, + "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", @@ -7856,6 +7969,21 @@ "time" ] }, + "RotBootloaderStatus": { + "type": "object", + "properties": { + "stage_0_next_version": { + "$ref": "#/components/schemas/TufRepoVersion" + }, + "stage_0_version": { + "$ref": "#/components/schemas/TufRepoVersion" + } + }, + "required": [ + "stage_0_next_version", + "stage_0_version" + ] + }, "RotSlot": { "oneOf": [ { @@ -7888,6 +8016,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": { @@ -8230,6 +8381,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": [ @@ -8454,14 +8639,6 @@ "SpStatus": { "type": "object", "properties": { - "sled_id": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/TypedUuidForSledKind" - } - ] - }, "slot0_version": { "$ref": "#/components/schemas/TufRepoVersion" }, @@ -8898,25 +9075,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": { From 4d630bacea72059bf1655e06f4679e9122e257ff Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 16:02:38 -0400 Subject: [PATCH 11/21] add rot bootloader output to omdb --- .../omdb/src/bin/omdb/nexus/update_status.rs | 40 ++++++++++++++++++- nexus/types/src/internal_api/views.rs | 8 ++-- 2 files changed, 43 insertions(+), 5 deletions(-) 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 e778644d375..928b5c243ad 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,9 @@ //! omdb commands related to update status use anyhow::Context; -use nexus_types::internal_api::views::{SpStatus, ZoneStatus}; +use nexus_types::internal_api::views::{ + RotBootloaderStatus, SpStatus, ZoneStatus, +}; use omicron_uuid_kinds::SledUuid; use tabled::Tabled; @@ -25,6 +27,9 @@ pub async fn cmd_nexus_update_status( .iter() .map(|s| (s.sled_id, s.zones.iter().cloned().collect())), ); + print_rot_bootloaders(status.mgs_driven.iter().map(|s| { + (s.baseboard_description.clone(), s.sled_id, &s.rot_bootloader) + })); print_sps( status .mgs_driven @@ -69,6 +74,39 @@ fn print_zones(zones: impl Iterator)>) { println!("{}", table); } +fn print_rot_bootloaders<'a>( + sps: impl Iterator, &'a RotBootloaderStatus)>, +) { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct BootloaderRow { + baseboard_id: String, + sled_id: String, + stage0_version: String, + stage0_next_version: String, + } + + let mut rows = Vec::new(); + for (baseboard_id, sled_id, status) in sps { + let RotBootloaderStatus { stage0_version, stage0_next_version } = + status; + rows.push(BootloaderRow { + baseboard_id, + sled_id: sled_id.map_or("".to_string(), |id| id.to_string()), + 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_sps<'a>( sps: impl Iterator, &'a SpStatus)>, ) { diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index f53f0a7b29c..8a001d454e3 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -656,8 +656,8 @@ impl IdOrdItem for SledAgentUpdateStatus { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct RotBootloaderStatus { - pub stage_0_version: TufRepoVersion, - pub stage_0_next_version: TufRepoVersion, + pub stage0_version: TufRepoVersion, + pub stage0_next_version: TufRepoVersion, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -735,8 +735,8 @@ impl MgsDrivenUpdateStatusBuilder<'_> { baseboard_description: self.baseboard_id.to_string(), sled_id: self.sled_ids.get(self.baseboard_id).copied(), rot_bootloader: RotBootloaderStatus { - stage_0_version: self.version_for_caboose(CabooseWhich::Stage0), - stage_0_next_version: self + stage0_version: self.version_for_caboose(CabooseWhich::Stage0), + stage0_next_version: self .version_for_caboose(CabooseWhich::Stage0Next), }, rot: RotStatus { From bb619986baa0c151ef706914b66541b922ffdcec Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 16:09:03 -0400 Subject: [PATCH 12/21] add rot output to omdb --- .../omdb/src/bin/omdb/nexus/update_status.rs | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) 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 928b5c243ad..407a2324563 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs @@ -5,8 +5,9 @@ //! omdb commands related to update status use anyhow::Context; +use gateway_types::rot::RotSlot; use nexus_types::internal_api::views::{ - RotBootloaderStatus, SpStatus, ZoneStatus, + RotBootloaderStatus, RotStatus, SpStatus, ZoneStatus, }; use omicron_uuid_kinds::SledUuid; use tabled::Tabled; @@ -30,6 +31,12 @@ pub async fn cmd_nexus_update_status( print_rot_bootloaders(status.mgs_driven.iter().map(|s| { (s.baseboard_description.clone(), s.sled_id, &s.rot_bootloader) })); + print_rots( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), s.sled_id, &s.rot)), + ); print_sps( status .mgs_driven @@ -107,6 +114,44 @@ fn print_rot_bootloaders<'a>( println!("{}", table); } +fn print_rots<'a>( + sps: impl Iterator, &'a RotStatus)>, +) { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct RotRow { + baseboard_id: String, + sled_id: String, + slot_a_version: String, + slot_b_version: String, + } + + let mut rows = Vec::new(); + for (baseboard_id, sled_id, status) in sps { + 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, + 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 RoT Software"); + println!("{}", table); +} + fn print_sps<'a>( sps: impl Iterator, &'a SpStatus)>, ) { From e9a25f5934f1f31cb0ee4c731beff0e412646317 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 16:11:55 -0400 Subject: [PATCH 13/21] add host phase 1 output to omdb --- .../omdb/src/bin/omdb/nexus/update_status.rs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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 407a2324563..90721f13f8c 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs @@ -7,8 +7,9 @@ use anyhow::Context; use gateway_types::rot::RotSlot; use nexus_types::internal_api::views::{ - RotBootloaderStatus, RotStatus, SpStatus, ZoneStatus, + HostPhase1Status, RotBootloaderStatus, RotStatus, SpStatus, ZoneStatus, }; +use omicron_common::disk::M2Slot; use omicron_uuid_kinds::SledUuid; use tabled::Tabled; @@ -43,6 +44,9 @@ pub async fn cmd_nexus_update_status( .iter() .map(|s| (s.baseboard_description.clone(), s.sled_id, &s.sp)), ); + print_host_phase_1s(status.mgs_driven.iter().map(|s| { + (s.baseboard_description.clone(), s.sled_id, &s.host_os_phase_1) + })); Ok(()) } @@ -183,3 +187,42 @@ fn print_sps<'a>( println!("Installed SP Software"); println!("{}", table); } + +fn print_host_phase_1s<'a>( + sps: impl Iterator, &'a HostPhase1Status)>, +) { + #[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, sled_id, status) in sps { + let HostPhase1Status { active_slot, slot_a_version, slot_b_version } = + status; + 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); +} From cfaf73430aa3f9fc3a753f0bab85aafa73d4aa71 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 25 Aug 2025 16:26:08 -0400 Subject: [PATCH 14/21] add host phase 2 output to omdb --- .../omdb/src/bin/omdb/nexus/update_status.rs | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) 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 90721f13f8c..03ef9cfcb80 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs @@ -7,7 +7,8 @@ use anyhow::Context; use gateway_types::rot::RotSlot; use nexus_types::internal_api::views::{ - HostPhase1Status, RotBootloaderStatus, RotStatus, SpStatus, ZoneStatus, + HostPhase1Status, HostPhase2Status, RotBootloaderStatus, RotStatus, + SpStatus, ZoneStatus, }; use omicron_common::disk::M2Slot; use omicron_uuid_kinds::SledUuid; @@ -23,12 +24,6 @@ pub async fn cmd_nexus_update_status( .context("retrieving update status")? .into_inner(); - print_zones( - status - .sleds - .iter() - .map(|s| (s.sled_id, s.zones.iter().cloned().collect())), - ); print_rot_bootloaders(status.mgs_driven.iter().map(|s| { (s.baseboard_description.clone(), s.sled_id, &s.rot_bootloader) })); @@ -47,6 +42,15 @@ pub async fn cmd_nexus_update_status( print_host_phase_1s(status.mgs_driven.iter().map(|s| { (s.baseboard_description.clone(), s.sled_id, &s.host_os_phase_1) })); + print_host_phase_2s( + status.sleds.iter().map(|s| (s.sled_id, &s.host_phase_2)), + ); + print_zones( + status + .sleds + .iter() + .map(|s| (s.sled_id, s.zones.iter().cloned().collect())), + ); Ok(()) } @@ -86,7 +90,9 @@ fn print_zones(zones: impl Iterator)>) { } fn print_rot_bootloaders<'a>( - sps: impl Iterator, &'a RotBootloaderStatus)>, + bootloaders: impl Iterator< + Item = (String, Option, &'a RotBootloaderStatus), + >, ) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] @@ -98,7 +104,7 @@ fn print_rot_bootloaders<'a>( } let mut rows = Vec::new(); - for (baseboard_id, sled_id, status) in sps { + for (baseboard_id, sled_id, status) in bootloaders { let RotBootloaderStatus { stage0_version, stage0_next_version } = status; rows.push(BootloaderRow { @@ -119,7 +125,7 @@ fn print_rot_bootloaders<'a>( } fn print_rots<'a>( - sps: impl Iterator, &'a RotStatus)>, + rots: impl Iterator, &'a RotStatus)>, ) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] @@ -131,7 +137,7 @@ fn print_rots<'a>( } let mut rows = Vec::new(); - for (baseboard_id, sled_id, status) in sps { + for (baseboard_id, sled_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)", ""), @@ -189,7 +195,7 @@ fn print_sps<'a>( } fn print_host_phase_1s<'a>( - sps: impl Iterator, &'a HostPhase1Status)>, + phase_1s: impl Iterator, &'a HostPhase1Status)>, ) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] @@ -201,7 +207,7 @@ fn print_host_phase_1s<'a>( } let mut rows = Vec::new(); - for (baseboard_id, sled_id, status) in sps { + for (baseboard_id, sled_id, status) in phase_1s { let HostPhase1Status { active_slot, slot_a_version, slot_b_version } = status; let (slot_a_suffix, slot_b_suffix) = match active_slot { @@ -226,3 +232,40 @@ fn print_host_phase_1s<'a>( 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); +} From 4e20c7d25f8b331ef2c7d7a18f9e20b8234e03f1 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 26 Aug 2025 11:45:39 -0400 Subject: [PATCH 15/21] openapi --- openapi/nexus-internal.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index 13fc098bed7..86c024acbd7 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -8019,16 +8019,16 @@ "RotBootloaderStatus": { "type": "object", "properties": { - "stage_0_next_version": { + "stage0_next_version": { "$ref": "#/components/schemas/TufRepoVersion" }, - "stage_0_version": { + "stage0_version": { "$ref": "#/components/schemas/TufRepoVersion" } }, "required": [ - "stage_0_next_version", - "stage_0_version" + "stage0_next_version", + "stage0_version" ] }, "RotSlot": { From f88a532320f28768df6f7912dd12877b16f8d2ea Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 26 Aug 2025 13:30:05 -0400 Subject: [PATCH 16/21] empty commit for new TUF repo From b02aeceac7f18e427de859372fab21b5240b2db5 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 26 Aug 2025 14:34:03 -0400 Subject: [PATCH 17/21] don't try to report host phase 1 status for non-sled SPs --- .../omdb/src/bin/omdb/nexus/update_status.rs | 87 ++++++++++--------- nexus/types/src/internal_api/views.rs | 48 ++++++---- 2 files changed, 77 insertions(+), 58 deletions(-) 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 03ef9cfcb80..ec5eea10ffb 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs @@ -24,24 +24,30 @@ pub async fn cmd_nexus_update_status( .context("retrieving update status")? .into_inner(); - print_rot_bootloaders(status.mgs_driven.iter().map(|s| { - (s.baseboard_description.clone(), s.sled_id, &s.rot_bootloader) - })); + print_rot_bootloaders( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), &s.rot_bootloader)), + ); print_rots( status .mgs_driven .iter() - .map(|s| (s.baseboard_description.clone(), s.sled_id, &s.rot)), + .map(|s| (s.baseboard_description.clone(), &s.rot)), ); print_sps( status .mgs_driven .iter() - .map(|s| (s.baseboard_description.clone(), s.sled_id, &s.sp)), + .map(|s| (s.baseboard_description.clone(), &s.sp)), + ); + print_host_phase_1s( + status + .mgs_driven + .iter() + .map(|s| (s.baseboard_description.clone(), &s.host_os_phase_1)), ); - print_host_phase_1s(status.mgs_driven.iter().map(|s| { - (s.baseboard_description.clone(), s.sled_id, &s.host_os_phase_1) - })); print_host_phase_2s( status.sleds.iter().map(|s| (s.sled_id, &s.host_phase_2)), ); @@ -90,26 +96,22 @@ fn print_zones(zones: impl Iterator)>) { } fn print_rot_bootloaders<'a>( - bootloaders: impl Iterator< - Item = (String, Option, &'a RotBootloaderStatus), - >, + bootloaders: impl Iterator, ) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct BootloaderRow { baseboard_id: String, - sled_id: String, stage0_version: String, stage0_next_version: String, } let mut rows = Vec::new(); - for (baseboard_id, sled_id, status) in bootloaders { + for (baseboard_id, status) in bootloaders { let RotBootloaderStatus { stage0_version, stage0_next_version } = status; rows.push(BootloaderRow { baseboard_id, - sled_id: sled_id.map_or("".to_string(), |id| id.to_string()), stage0_version: stage0_version.to_string(), stage0_next_version: stage0_next_version.to_string(), }); @@ -124,20 +126,17 @@ fn print_rot_bootloaders<'a>( println!("{}", table); } -fn print_rots<'a>( - rots: impl Iterator, &'a RotStatus)>, -) { +fn print_rots<'a>(rots: impl Iterator) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct RotRow { baseboard_id: String, - sled_id: String, slot_a_version: String, slot_b_version: String, } let mut rows = Vec::new(); - for (baseboard_id, sled_id, status) in rots { + 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)", ""), @@ -147,7 +146,6 @@ fn print_rots<'a>( }; rows.push(RotRow { 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}"), }); @@ -162,24 +160,20 @@ fn print_rots<'a>( println!("{}", table); } -fn print_sps<'a>( - sps: impl Iterator, &'a SpStatus)>, -) { +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, sled_id, status) in sps { + for (baseboard_id, status) in sps { 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(), }); @@ -195,7 +189,7 @@ fn print_sps<'a>( } fn print_host_phase_1s<'a>( - phase_1s: impl Iterator, &'a HostPhase1Status)>, + phase_1s: impl Iterator, ) { #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] @@ -207,21 +201,30 @@ fn print_host_phase_1s<'a>( } let mut rows = Vec::new(); - for (baseboard_id, sled_id, status) in phase_1s { - let HostPhase1Status { active_slot, slot_a_version, slot_b_version } = - status; - 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}"), - }); + 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) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 8a001d454e3..2ba17a0e040 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -12,6 +12,7 @@ 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; @@ -674,10 +675,17 @@ pub struct SpStatus { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct HostPhase1Status { - pub active_slot: Option, - pub slot_a_version: TufRepoVersion, - pub slot_b_version: TufRepoVersion, +#[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, + }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -685,7 +693,6 @@ 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 sled_id: Option, pub rot_bootloader: RotBootloaderStatus, pub rot: RotStatus, pub sp: SpStatus, @@ -696,6 +703,7 @@ impl MgsDrivenUpdateStatus { fn new( inventory: &Collection, baseboard_id: &BaseboardId, + sp_type: SpType, old: &TargetReleaseDescription, new: &TargetReleaseDescription, sled_ids: &BTreeMap<&BaseboardId, SledUuid>, @@ -703,6 +711,7 @@ impl MgsDrivenUpdateStatus { MgsDrivenUpdateStatusBuilder { inventory, baseboard_id, + sp_type, old, new, sled_ids, @@ -724,6 +733,7 @@ impl IdOrdItem for MgsDrivenUpdateStatus { 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>, @@ -731,9 +741,21 @@ struct MgsDrivenUpdateStatusBuilder<'a> { 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(), - sled_id: self.sled_ids.get(self.baseboard_id).copied(), rot_bootloader: RotBootloaderStatus { stage0_version: self.version_for_caboose(CabooseWhich::Stage0), stage0_next_version: self @@ -753,14 +775,7 @@ impl MgsDrivenUpdateStatusBuilder<'_> { slot0_version: self.version_for_caboose(CabooseWhich::SpSlot0), slot1_version: self.version_for_caboose(CabooseWhich::SpSlot1), }, - host_os_phase_1: HostPhase1Status { - 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), - }, + host_os_phase_1, } } @@ -902,11 +917,12 @@ impl UpdateStatus { let mgs_driven = inventory .sps - .keys() - .map(|baseboard_id| { + .iter() + .map(|(baseboard_id, sp)| { MgsDrivenUpdateStatus::new( inventory, baseboard_id, + sp.sp_type, old, new, &sled_ids_by_baseboard, From 940040cd1838dc8121d75d2f530a7dc48ce5d27b Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 26 Aug 2025 14:38:47 -0400 Subject: [PATCH 18/21] openapi --- openapi/nexus-internal.json | 74 +++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index 86c024acbd7..9954f1cfa77 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -4815,26 +4815,60 @@ ] }, "HostPhase1Status": { - "type": "object", - "properties": { - "active_slot": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/M2Slot" + "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" ] }, - "slot_a_version": { - "$ref": "#/components/schemas/TufRepoVersion" - }, - "slot_b_version": { - "$ref": "#/components/schemas/TufRepoVersion" + { + "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" + ] } - }, - "required": [ - "slot_a_version", - "slot_b_version" ] }, "HostPhase2Status": { @@ -5486,14 +5520,6 @@ "rot_bootloader": { "$ref": "#/components/schemas/RotBootloaderStatus" }, - "sled_id": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/TypedUuidForSledKind" - } - ] - }, "sp": { "$ref": "#/components/schemas/SpStatus" } From 6e62b4c9894636f82830a6c0919da25b39aa4993 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 26 Aug 2025 15:07:36 -0400 Subject: [PATCH 19/21] awful typo --- nexus/types/src/internal_api/views.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nexus/types/src/internal_api/views.rs b/nexus/types/src/internal_api/views.rs index 2ba17a0e040..49d744fc715 100644 --- a/nexus/types/src/internal_api/views.rs +++ b/nexus/types/src/internal_api/views.rs @@ -829,7 +829,7 @@ impl MgsDrivenUpdateStatusBuilder<'_> { }; let matching_caboose = |a: &TufArtifactMeta| { Some(&caboose.board) == a.board.as_ref() - && caboose.version != a.id.version.to_string() + && caboose.version == a.id.version.to_string() && matching_kinds.contains(&a.id.kind) }; if let Some(new) = self.new.tuf_repo() { From cf4583d2187f813413721f8ecd844c2accd37ec9 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 27 Aug 2025 10:14:30 -0400 Subject: [PATCH 20/21] add linebreaks between sections --- dev-tools/omdb/src/bin/omdb/nexus/update_status.rs | 5 +++++ 1 file changed, 5 insertions(+) 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 ec5eea10ffb..8cc6a8252a3 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus/update_status.rs @@ -30,27 +30,32 @@ pub async fn cmd_nexus_update_status( .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 From 81e10225690fc0db7c0e9c15484e70319b88951c Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Wed, 27 Aug 2025 10:26:06 -0400 Subject: [PATCH 21/21] empty commit for new TUF repo