Skip to content

Commit f41bd73

Browse files
authored
[nexus] Update status API (#8897)
Closes #8869 * Add `update_status` endpoint at `/v1/system/update/status` that includes: * Current target release (same as existing `target_release_view`) * `time_last_progress`, representing the `time_made_target` of the latest `bp_target` — meant to indicate the last time the update system Did Something * `components_by_release_version`, a map where the keys are to_string'd `TufRepoVersion`s and the values are counts of components on that version * Remove `target_release_view` endpoint, which is fully redundant with update status * Add a line to the doc comment on `target_release_update` that you can use update status to check the current target release * Rework the `TargetRelease` struct that was previously being returned from `target_release_view` and is now part of the update status response * Remove `generation` from `TargetRelease` because it is not meaningful to the end user * Other changes that are easier to explain in the inline comments Listing blockers or problems preventing further update progress is mentioned as a goal of #8869, but they are currently stored as JSON blobs in the DB for debug purposes. They will soon be stored in a more regimented way that should make it easy to stick a list of blockers in the response body. If this PR gets merged without that, I'll make a dedicated issue for it. ### Example status response ```json { "target_release": { "time_requested": "2025-09-24T22:37:43.266338Z", "version": "2.0.0" }, "components_by_release_version": { "install dataset": 7, "unknown": 15 }, "time_last_step_planned": "2025-09-24T22:37:41.556717Z", "suspended": false } ```
1 parent 76ec1fa commit f41bd73

File tree

14 files changed

+523
-258
lines changed

14 files changed

+523
-258
lines changed

common/src/api/external/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3572,7 +3572,7 @@ pub enum ImportExportPolicy {
35723572
/// will fail to parse if the key is not present. The JSON Schema in the
35733573
/// OpenAPI definition will also reflect that the field is required. See
35743574
/// <https://github.com/serde-rs/serde/issues/2753>.
3575-
#[derive(Clone, Debug, Serialize)]
3575+
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
35763576
pub struct Nullable<T>(pub Option<T>);
35773577

35783578
impl<T> From<Option<T>> for Nullable<T> {

nexus/db-model/src/target_release.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use super::{Generation, impl_enum_type};
66
use crate::typed_uuid::DbTypedUuid;
77
use chrono::{DateTime, Utc};
88
use nexus_db_schema::schema::target_release;
9-
use nexus_types::external_api::views;
109
use omicron_uuid_kinds::TufRepoKind;
1110

1211
impl_enum_type!(
@@ -60,15 +59,4 @@ impl TargetRelease {
6059
tuf_repo_id: Some(tuf_repo_id),
6160
}
6261
}
63-
64-
pub fn into_external(
65-
&self,
66-
release_source: views::TargetReleaseSource,
67-
) -> views::TargetRelease {
68-
views::TargetRelease {
69-
generation: (&self.generation.0).into(),
70-
time_requested: self.time_requested,
71-
release_source,
72-
}
73-
}
7462
}

nexus/db-queries/src/db/datastore/target_release.rs

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,14 @@
77
use super::DataStore;
88
use crate::authz;
99
use crate::context::OpContext;
10-
use crate::db::model::{
11-
Generation, SemverVersion, TargetRelease, TargetReleaseSource,
12-
};
10+
use crate::db::model::{Generation, TargetRelease};
1311
use async_bb8_diesel::AsyncRunQueryDsl as _;
1412
use diesel::insert_into;
1513
use diesel::prelude::*;
1614
use diesel::sql_types;
1715
use nexus_db_errors::{ErrorHandler, public_error_from_diesel};
1816
use nexus_db_schema::enums::TargetReleaseSourceEnum;
1917
use nexus_db_schema::schema::target_release::dsl;
20-
use nexus_types::external_api::views;
2118
use omicron_common::api::external::{CreateResult, Error, LookupResult};
2219
use omicron_uuid_kinds::TufRepoUuid;
2320
use std::collections::BTreeSet;
@@ -181,49 +178,6 @@ impl DataStore {
181178
}
182179
}
183180

184-
/// Convert a model-level target release to an external view.
185-
/// This method lives here because we have to look up the version
186-
/// corresponding to the TUF repo.
187-
pub async fn target_release_view(
188-
&self,
189-
opctx: &OpContext,
190-
target_release: &TargetRelease,
191-
) -> LookupResult<views::TargetRelease> {
192-
opctx
193-
.authorize(authz::Action::Read, &authz::TARGET_RELEASE_CONFIG)
194-
.await?;
195-
let conn = self.pool_connection_authorized(opctx).await?;
196-
let release_source = match target_release.release_source {
197-
TargetReleaseSource::Unspecified => {
198-
views::TargetReleaseSource::Unspecified
199-
}
200-
TargetReleaseSource::SystemVersion => {
201-
use nexus_db_schema::schema::tuf_repo;
202-
if let Some(tuf_repo_id) = target_release.tuf_repo_id {
203-
views::TargetReleaseSource::SystemVersion {
204-
version: tuf_repo::table
205-
.select(tuf_repo::system_version)
206-
.filter(tuf_repo::id.eq(tuf_repo_id))
207-
.first_async::<SemverVersion>(&*conn)
208-
.await
209-
.map_err(|e| {
210-
public_error_from_diesel(
211-
e,
212-
ErrorHandler::Server,
213-
)
214-
})?
215-
.into(),
216-
}
217-
} else {
218-
return Err(Error::internal_error(
219-
"missing TUF repo ID for specified system version",
220-
));
221-
}
222-
}
223-
};
224-
Ok(target_release.into_external(release_source))
225-
}
226-
227181
/// Lists the most recent N distinct target releases
228182
pub async fn target_release_fetch_recent_distinct(
229183
&self,

nexus/db-queries/src/db/datastore/update.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ use nexus_db_errors::OptionalError;
2121
use nexus_db_errors::{ErrorHandler, public_error_from_diesel};
2222
use nexus_db_lookup::DbConnection;
2323
use nexus_db_model::{
24-
ArtifactHash, TargetRelease, TufArtifact, TufRepo, TufRepoDescription,
25-
TufTrustRoot, to_db_typed_uuid,
24+
ArtifactHash, DbTypedUuid, TargetRelease, TufArtifact, TufRepo,
25+
TufRepoDescription, TufTrustRoot, to_db_typed_uuid,
2626
};
2727
use omicron_common::api::external::{
2828
self, CreateResult, DataPageParams, DeleteResult, Generation,
@@ -180,6 +180,33 @@ impl DataStore {
180180
Ok(TufRepoDescription { repo, artifacts })
181181
}
182182

183+
/// Given a TUF repo ID, get its version. We could use `tuf_repo_get_by_id`,
184+
/// but that makes an additional query for the artifacts that we don't need
185+
/// in the code that uses this method.
186+
pub async fn tuf_repo_get_version(
187+
&self,
188+
opctx: &OpContext,
189+
tuf_repo_id: &DbTypedUuid<TufRepoKind>,
190+
) -> LookupResult<semver::Version> {
191+
opctx
192+
.authorize(authz::Action::Read, &authz::TARGET_RELEASE_CONFIG)
193+
.await?;
194+
let conn = self.pool_connection_authorized(opctx).await?;
195+
use nexus_db_schema::schema::tuf_repo;
196+
tuf_repo::table
197+
.select(tuf_repo::system_version)
198+
.filter(tuf_repo::id.eq(tuf_repo_id.into_untyped_uuid()))
199+
.first_async::<SemverVersion>(&*conn)
200+
.await
201+
.map(|v| v.0)
202+
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))
203+
// looking up a non-existent ID will 500, but it doesn't
204+
// automatically include the bad ID
205+
.with_internal_context(|| {
206+
format!("tuf_repo_get_version {tuf_repo_id}")
207+
})
208+
}
209+
183210
/// Returns the list of all TUF repo artifacts known to the system.
184211
pub async fn tuf_list_repos(
185212
&self,

nexus/external-api/output/nexus_tags.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,12 +303,12 @@ API operations found with tag "system/update"
303303
OPERATION ID METHOD URL PATH
304304
system_update_get_repository GET /v1/system/update/repository/{system_version}
305305
system_update_put_repository PUT /v1/system/update/repository
306+
system_update_status GET /v1/system/update/status
306307
system_update_trust_root_create POST /v1/system/update/trust-roots
307308
system_update_trust_root_delete DELETE /v1/system/update/trust-roots/{trust_root_id}
308309
system_update_trust_root_list GET /v1/system/update/trust-roots
309310
system_update_trust_root_view GET /v1/system/update/trust-roots/{trust_root_id}
310311
target_release_update PUT /v1/system/update/target-release
311-
target_release_view GET /v1/system/update/target-release
312312

313313
API operations found with tag "tokens"
314314
OPERATION ID METHOD URL PATH

nexus/external-api/src/lib.rs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3257,36 +3257,35 @@ pub trait NexusExternalApi {
32573257
path_params: Path<params::TufTrustRootPath>,
32583258
) -> Result<HttpResponseDeleted, HttpError>;
32593259

3260-
/// Get the current target release of the rack's system software
3260+
/// Set target release
32613261
///
3262-
/// This may not correspond to the actual software running on the rack
3263-
/// at the time of request; it is instead the release that the rack
3264-
/// reconfigurator should be moving towards as a goal state. After some
3265-
/// number of planning and execution phases, the software running on the
3266-
/// rack should eventually correspond to the release described here.
3262+
/// Set the current target release of the rack's system software. The rack
3263+
/// reconfigurator will treat the software specified here as a goal state
3264+
/// for the rack's software, and attempt to asynchronously update to that
3265+
/// release. Use the update status endpoint to view the current target
3266+
/// release.
32673267
#[endpoint {
3268-
method = GET,
3268+
method = PUT,
32693269
path = "/v1/system/update/target-release",
32703270
tags = ["system/update"],
32713271
}]
3272-
async fn target_release_view(
3272+
async fn target_release_update(
32733273
rqctx: RequestContext<Self::Context>,
3274-
) -> Result<HttpResponseOk<views::TargetRelease>, HttpError>;
3274+
params: TypedBody<params::SetTargetReleaseParams>,
3275+
) -> Result<HttpResponseUpdatedNoContent, HttpError>;
32753276

3276-
/// Set the current target release of the rack's system software
3277+
/// Fetch system update status
32773278
///
3278-
/// The rack reconfigurator will treat the software specified here as
3279-
/// a goal state for the rack's software, and attempt to asynchronously
3280-
/// update to that release.
3279+
/// Returns information about the current target release and the
3280+
/// progress of system software updates.
32813281
#[endpoint {
3282-
method = PUT,
3283-
path = "/v1/system/update/target-release",
3282+
method = GET,
3283+
path = "/v1/system/update/status",
32843284
tags = ["system/update"],
32853285
}]
3286-
async fn target_release_update(
3286+
async fn system_update_status(
32873287
rqctx: RequestContext<Self::Context>,
3288-
params: TypedBody<params::SetTargetReleaseParams>,
3289-
) -> Result<HttpResponseCreated<views::TargetRelease>, HttpError>;
3288+
) -> Result<HttpResponseOk<views::UpdateStatus>, HttpError>;
32903289

32913290
// Silo users
32923291

nexus/src/app/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use self::saga::SagaExecutor;
99
use crate::DropshotServer;
1010
use crate::app::background::BackgroundTasksData;
1111
use crate::app::background::SagaRecoveryHelpers;
12+
use crate::app::update::UpdateStatusHandle;
1213
use crate::populate::PopulateArgs;
1314
use crate::populate::PopulateStatus;
1415
use crate::populate::populate_start;
@@ -285,6 +286,9 @@ pub struct Nexus {
285286
#[allow(dead_code)]
286287
repo_depot_resolver: Box<dyn qorb::resolver::Resolver>,
287288

289+
/// handle to pull update status data
290+
update_status: UpdateStatusHandle,
291+
288292
/// state of overall Nexus quiesce activity
289293
quiesce: NexusQuiesceHandle,
290294
}
@@ -351,7 +355,7 @@ impl Nexus {
351355
let quiesce = NexusQuiesceHandle::new(
352356
db_datastore.clone(),
353357
config.deployment.id,
354-
blueprint_load_rx,
358+
blueprint_load_rx.clone(),
355359
quiesce_opctx,
356360
);
357361

@@ -534,6 +538,7 @@ impl Nexus {
534538
mgs_update_status_rx,
535539
mgs_resolver,
536540
repo_depot_resolver,
541+
update_status: UpdateStatusHandle::new(blueprint_load_rx),
537542
quiesce,
538543
};
539544

0 commit comments

Comments
 (0)