Skip to content

Start implementing upgrade status API #8320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions clients/nexus-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ progenitor::generate_api!(
TypedUuidForUpstairsSessionKind = omicron_uuid_kinds::TypedUuid<omicron_uuid_kinds::UpstairsSessionKind>,
TypedUuidForVolumeKind = omicron_uuid_kinds::TypedUuid<omicron_uuid_kinds::VolumeKind>,
TypedUuidForZpoolKind = omicron_uuid_kinds::TypedUuid<omicron_uuid_kinds::ZpoolKind>,
UpdateStatus = nexus_types::internal_api::views::UpdateStatus,
ZoneStatus = nexus_types::internal_api::views::ZoneStatus,
ZoneStatusVersion = nexus_types::internal_api::views::ZoneStatusVersion
},
patch = {
SledAgentInfo = { derives = [PartialEq, Eq] },
Expand Down
49 changes: 49 additions & 0 deletions dev-tools/omdb/src/bin/omdb/nexus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ enum NexusCommands {
/// interact with support bundles
#[command(visible_alias = "sb")]
SupportBundles(SupportBundleArgs),
/// show running artifact versions
UpdateStatus,
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -778,6 +780,9 @@ impl NexusArgs {
NexusCommands::SupportBundles(SupportBundleArgs {
command: SupportBundleCommands::Inspect(args),
}) => cmd_nexus_support_bundles_inspect(&client, args).await,
NexusCommands::UpdateStatus => {
cmd_nexus_update_status(&client).await
}
}
}
}
Expand Down Expand Up @@ -4044,3 +4049,47 @@ async fn cmd_nexus_support_bundles_inspect(

support_bundle_viewer::run_dashboard(accessor).await
}

/// Runs `omdb nexus upgrade-status`
async fn cmd_nexus_update_status(
client: &nexus_client::Client,
) -> Result<(), anyhow::Error> {
let status = client
.update_status()
.await
.context("retrieving update status")?
.into_inner();

#[derive(Tabled)]
#[tabled(rename_all = "SCREAMING_SNAKE_CASE")]
struct ZoneRow {
sled_id: String,
zone_type: String,
zone_id: String,
version: String,
}

let mut rows = Vec::new();
for (sled_id, mut statuses) in status.zones.into_iter() {
statuses.sort_unstable_by_key(|s| {
(s.zone_type.kind(), s.zone_id, s.version.clone())
});
for status in statuses {
rows.push(ZoneRow {
sled_id: sled_id.to_string(),
zone_type: status.zone_type.kind().name_prefix().into(),
zone_id: status.zone_id.to_string(),
version: status.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!("Running Zones");
println!("{}", table);
Ok(())
}
1 change: 1 addition & 0 deletions dev-tools/omdb/tests/usage_errors.out
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ Commands:
sagas view sagas, create and complete demo sagas
sleds interact with sleds
support-bundles interact with support bundles [aliases: sb]
update-status show running artifact versions
help Print this message or the help of the given subcommand(s)

Options:
Expand Down
16 changes: 16 additions & 0 deletions nexus-sled-agent-shared/src/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ impl ConfigReconcilerInventory {
})
}

/// Iterate over all zones contained in the most-recently-reconciled sled
/// config and report their status as of that reconciliation.
pub fn reconciled_omicron_zones(
&self,
) -> impl Iterator<Item = (&OmicronZoneConfig, &ConfigReconcilerInventoryResult)>
{
// `self.zones` may contain zone IDs that aren't present in
// `last_reconciled_config` at all, if we failed to _shut down_ zones
// that are no longer present in the config. We use `filter_map` to
// strip those out, and only report on the configured zones.
self.zones.iter().filter_map(|(zone_id, result)| {
let config = self.last_reconciled_config.zones.get(zone_id)?;
Some((config, result))
})
}

/// Given a sled config, produce a reconciler result that sled-agent could
/// have emitted if reconciliation succeeded.
///
Expand Down
15 changes: 11 additions & 4 deletions nexus/internal-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ use nexus_types::{
headers::RangeRequest,
params::{self, PhysicalDiskPath, SledSelector, UninitializedSledId},
shared::{self, ProbeInfo, UninitializedSled},
views::Ping,
views::PingStatus,
views::SledPolicy,
views::{Ping, PingStatus, SledPolicy},
},
internal_api::{
params::{
Expand All @@ -30,7 +28,7 @@ use nexus_types::{
},
views::{
BackgroundTask, DemoSaga, Ipv4NatEntryView, MgsUpdateDriverStatus,
Saga,
Saga, UpdateStatus,
},
},
};
Expand Down Expand Up @@ -487,6 +485,15 @@ pub trait NexusInternalApi {
blueprint: TypedBody<Blueprint>,
) -> Result<HttpResponseUpdatedNoContent, HttpError>;

/// Show deployed versions of artifacts
#[endpoint {
method = GET,
path = "/deployment/update-status"
}]
async fn update_status(
rqctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<UpdateStatus>, HttpError>;

/// List uninitialized sleds
#[endpoint {
method = GET,
Expand Down
23 changes: 23 additions & 0 deletions nexus/src/app/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use nexus_types::deployment::BlueprintMetadata;
use nexus_types::deployment::BlueprintTarget;
use nexus_types::deployment::BlueprintTargetSet;
use nexus_types::deployment::PlanningInput;
use nexus_types::internal_api::views::UpdateStatus;
use nexus_types::inventory::Collection;
use omicron_common::api::external::CreateResult;
use omicron_common::api::external::DataPageParams;
Expand Down Expand Up @@ -200,4 +201,26 @@ impl super::Nexus {
let _ = self.blueprint_add(&opctx, &blueprint).await?;
Ok(())
}

pub async fn update_status(
&self,
opctx: &OpContext,
) -> Result<UpdateStatus, Error> {
let planning_context = self.blueprint_planning_context(opctx).await?;
let inventory = planning_context.inventory.ok_or_else(|| {
Error::internal_error("no recent inventory collection found")
})?;
let new = planning_context.planning_input.tuf_repo();
let old = planning_context.planning_input.old_repo();
let status = UpdateStatus::new(
old,
new,
inventory
.sled_agents
.iter()
.map(|(sled_id, agent)| (sled_id, &agent.last_reconciliation)),
);

Ok(status)
}
}
18 changes: 18 additions & 0 deletions nexus/src/internal_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use nexus_types::internal_api::views::DemoSaga;
use nexus_types::internal_api::views::Ipv4NatEntryView;
use nexus_types::internal_api::views::MgsUpdateDriverStatus;
use nexus_types::internal_api::views::Saga;
use nexus_types::internal_api::views::UpdateStatus;
use nexus_types::internal_api::views::to_list;
use omicron_common::api::external::Instance;
use omicron_common::api::external::http_pagination::PaginatedById;
Expand Down Expand Up @@ -857,6 +858,23 @@ impl NexusInternalApi for NexusInternalApiImpl {
.await
}

async fn update_status(
rqctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<UpdateStatus>, HttpError> {
let apictx = &rqctx.context().context;
let handler = async {
let opctx =
crate::context::op_context_for_internal_api(&rqctx).await;
let nexus = &apictx.nexus;
let result = nexus.update_status(&opctx).await?;
Ok(HttpResponseOk(result))
};
apictx
.internal_latencies
.instrument_dropshot_handler(&rqctx, handler)
.await
}

async fn sled_list_uninitialized(
rqctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<ResultsPage<UninitializedSled>>, HttpError> {
Expand Down
125 changes: 125 additions & 0 deletions nexus/types/src/internal_api/views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ use chrono::SecondsFormat;
use chrono::Utc;
use futures::future::ready;
use futures::stream::StreamExt;
use nexus_sled_agent_shared::inventory::ConfigReconcilerInventory;
use nexus_sled_agent_shared::inventory::ConfigReconcilerInventoryResult;
use nexus_sled_agent_shared::inventory::OmicronZoneImageSource;
use nexus_sled_agent_shared::inventory::OmicronZoneType;
use omicron_common::api::external::MacAddr;
use omicron_common::api::external::ObjectStream;
use omicron_common::api::external::TufRepoDescription;
use omicron_common::api::external::Vni;
use omicron_common::snake_case_result;
use omicron_common::snake_case_result::SnakeCaseResult;
use omicron_uuid_kinds::DemoSagaUuid;
use omicron_uuid_kinds::{OmicronZoneUuid, SledUuid};
use schemars::JsonSchema;
use semver::Version;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
Expand Down Expand Up @@ -469,3 +476,121 @@ pub struct WaitingStatus {
pub next_attempt_time: DateTime<Utc>,
pub nattempts_done: u32,
}

#[derive(
Debug,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Serialize,
Deserialize,
JsonSchema,
)]
#[serde(
rename_all = "snake_case",
tag = "zone_status_version",
content = "details"
)]
pub enum ZoneStatusVersion {
Unknown,
InstallDataset,
Version(Version),
Error(String),
}

impl Display for ZoneStatusVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ZoneStatusVersion::Unknown => write!(f, "unknown"),
ZoneStatusVersion::InstallDataset => write!(f, "install dataset"),
ZoneStatusVersion::Version(version) => {
write!(f, "{}", version)
}
ZoneStatusVersion::Error(s) => {
write!(f, "{}", s)
}
}
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ZoneStatus {
pub zone_id: OmicronZoneUuid,
pub zone_type: OmicronZoneType,
pub version: ZoneStatusVersion,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct UpdateStatus {
pub zones: BTreeMap<SledUuid, Vec<ZoneStatus>>,
}

impl UpdateStatus {
pub fn new<'a>(
old: Option<&TufRepoDescription>,
new: Option<&TufRepoDescription>,
sleds: impl Iterator<
Item = (&'a SledUuid, &'a Option<ConfigReconcilerInventory>),
>,
) -> UpdateStatus {
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();
UpdateStatus { zones }
}

pub fn zone_image_source_to_version(
old: Option<&TufRepoDescription>,
new: Option<&TufRepoDescription>,
source: &OmicronZoneImageSource,
res: &ConfigReconcilerInventoryResult,
) -> ZoneStatusVersion {
if let ConfigReconcilerInventoryResult::Err { message } = res {
return ZoneStatusVersion::Error(message.clone());
}

let &OmicronZoneImageSource::Artifact { hash } = source else {
return ZoneStatusVersion::InstallDataset;
};

if let Some(old) = old {
if let Some(_) = old.artifacts.iter().find(|meta| meta.hash == hash)
{
return ZoneStatusVersion::Version(
old.repo.system_version.clone(),
);
}
}

if let Some(new) = new {
if let Some(_) = new.artifacts.iter().find(|meta| meta.hash == hash)
{
return ZoneStatusVersion::Version(
new.repo.system_version.clone(),
);
}
}

ZoneStatusVersion::Unknown
}
}
Loading
Loading