Skip to content
Draft
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: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(181, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(182, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(182, "blueprint-time-created-index"),
KnownVersion::new(181, "rename-nat-table"),
KnownVersion::new(180, "sled-cpu-family"),
KnownVersion::new(179, "add-pending-mgs-updates-host-phase-1"),
Expand Down
20 changes: 20 additions & 0 deletions nexus/db-queries/src/db/datastore/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,26 @@ impl DataStore {
Ok(blueprints.into_iter().map(BlueprintMetadata::from).collect())
}

/// Get the time of the most recently created blueprint
pub async fn blueprint_get_latest_time(
&self,
opctx: &OpContext,
) -> Result<Option<DateTime<Utc>>, Error> {
use nexus_db_schema::schema::blueprint;

opctx
.authorize(authz::Action::ListChildren, &authz::BLUEPRINT_CONFIG)
.await?;

let latest_time = blueprint::table
.select(diesel::dsl::max(blueprint::time_created))
.get_result_async(&*self.pool_connection_authorized(opctx).await?)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(latest_time)
}

/// Store a complete blueprint into the database
pub async fn blueprint_insert(
&self,
Expand Down
20 changes: 12 additions & 8 deletions nexus/external-api/output/nexus_tags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,6 @@ support_bundle_index GET /experimental/v1/system/suppor
support_bundle_list GET /experimental/v1/system/support-bundles
support_bundle_update PUT /experimental/v1/system/support-bundles/{bundle_id}
support_bundle_view GET /experimental/v1/system/support-bundles/{bundle_id}
system_update_get_repository GET /v1/system/update/repository/{system_version}
system_update_put_repository PUT /v1/system/update/repository
system_update_trust_root_create POST /v1/system/update/trust-roots
system_update_trust_root_delete DELETE /v1/system/update/trust-roots/{trust_root_id}
system_update_trust_root_list GET /v1/system/update/trust-roots
system_update_trust_root_view GET /v1/system/update/trust-roots/{trust_root_id}
target_release_update PUT /v1/system/update/target-release
target_release_view GET /v1/system/update/target-release
timeseries_query POST /v1/timeseries/query

API operations found with tag "floating-ips"
Expand Down Expand Up @@ -302,6 +294,18 @@ API operations found with tag "system/status"
OPERATION ID METHOD URL PATH
ping GET /v1/ping

API operations found with tag "system/update"
OPERATION ID METHOD URL PATH
system_update_get_repository GET /v1/system/update/repository/{system_version}
system_update_put_repository PUT /v1/system/update/repository
system_update_trust_root_create POST /v1/system/update/trust-roots
system_update_trust_root_delete DELETE /v1/system/update/trust-roots/{trust_root_id}
system_update_trust_root_list GET /v1/system/update/trust-roots
system_update_trust_root_view GET /v1/system/update/trust-roots/{trust_root_id}
target_release_update PUT /v1/system/update/target-release
target_release_view GET /v1/system/update/target-release
update_status GET /v1/system/update/status

API operations found with tag "tokens"
OPERATION ID METHOD URL PATH
current_user_access_token_delete DELETE /v1/me/access-tokens/{token_id}
Expand Down
33 changes: 24 additions & 9 deletions nexus/external-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,13 @@ const PUT_UPDATE_REPOSITORY_MAX_BYTES: usize = 4 * GIB;
external_docs = {
url = "http://docs.oxide.computer/api/system-silos"
}
}
},
"system/update" = {
description = "Endpoints for uploading and managing system updates",
external_docs = {
url = "http://docs.oxide.computer/api/system-update"
}
},
}
}
}]
Expand Down Expand Up @@ -2958,7 +2964,7 @@ pub trait NexusExternalApi {
#[endpoint {
method = PUT,
path = "/v1/system/update/repository",
tags = ["experimental"], // ["system/update"],
tags = ["system/update"],
request_body_max_bytes = PUT_UPDATE_REPOSITORY_MAX_BYTES,
}]
async fn system_update_put_repository(
Expand All @@ -2971,7 +2977,7 @@ pub trait NexusExternalApi {
#[endpoint {
method = GET,
path = "/v1/system/update/repository/{system_version}",
tags = ["experimental"], // ["system/update"],
tags = ["system/update"],
}]
async fn system_update_get_repository(
rqctx: RequestContext<Self::Context>,
Expand All @@ -2987,7 +2993,7 @@ pub trait NexusExternalApi {
#[endpoint {
method = GET,
path = "/v1/system/update/trust-roots",
tags = ["experimental"], // ["system/update"],
tags = ["system/update"],
}]
async fn system_update_trust_root_list(
rqctx: RequestContext<Self::Context>,
Expand All @@ -2998,7 +3004,7 @@ pub trait NexusExternalApi {
#[endpoint {
method = POST,
path = "/v1/system/update/trust-roots",
tags = ["experimental"], // ["system/update"],
tags = ["system/update"],
}]
async fn system_update_trust_root_create(
rqctx: RequestContext<Self::Context>,
Expand All @@ -3009,7 +3015,7 @@ pub trait NexusExternalApi {
#[endpoint {
method = GET,
path = "/v1/system/update/trust-roots/{trust_root_id}",
tags = ["experimental"], // ["system/update"],
tags = ["system/update"],
}]
async fn system_update_trust_root_view(
rqctx: RequestContext<Self::Context>,
Expand All @@ -3024,7 +3030,7 @@ pub trait NexusExternalApi {
#[endpoint {
method = DELETE,
path = "/v1/system/update/trust-roots/{trust_root_id}",
tags = ["experimental"], // ["system/update"],
tags = ["system/update"],
}]
async fn system_update_trust_root_delete(
rqctx: RequestContext<Self::Context>,
Expand All @@ -3041,7 +3047,7 @@ pub trait NexusExternalApi {
#[endpoint {
method = GET,
path = "/v1/system/update/target-release",
tags = ["experimental"], // "system/update"
tags = ["system/update"],
}]
async fn target_release_view(
rqctx: RequestContext<Self::Context>,
Expand All @@ -3055,13 +3061,22 @@ pub trait NexusExternalApi {
#[endpoint {
method = PUT,
path = "/v1/system/update/target-release",
tags = ["experimental"], // "system/update"
tags = ["system/update"],
}]
async fn target_release_update(
rqctx: RequestContext<Self::Context>,
params: TypedBody<params::SetTargetReleaseParams>,
) -> Result<HttpResponseCreated<views::TargetRelease>, HttpError>;

#[endpoint {
method = GET,
path = "/v1/system/update/status",
tags = ["system/update"],
}]
async fn update_status(
rqctx: RequestContext<Self::Context>,
) -> Result<HttpResponseOk<views::UpdateStatus>, HttpError>;

// Silo users

/// List users
Expand Down
118 changes: 115 additions & 3 deletions nexus/src/app/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ use dropshot::HttpError;
use futures::Stream;
use nexus_auth::authz;
use nexus_db_lookup::LookupPath;
use nexus_db_model::{TufRepoDescription, TufTrustRoot};
use nexus_db_model::{Generation, TufRepoDescription, TufTrustRoot};
use nexus_db_queries::context::OpContext;
use nexus_db_queries::db::{datastore::SQL_BATCH_SIZE, pagination::Paginator};
use nexus_types::deployment::TargetReleaseDescription;
use nexus_types::external_api::shared::TufSignedRootRole;
use nexus_types::external_api::views;
use nexus_types::internal_api::views as internal_views;
use omicron_common::api::external::InternalContext;
use omicron_common::api::external::{
DataPageParams, Error, TufRepoInsertResponse, TufRepoInsertStatus,
};
use omicron_uuid_kinds::{GenericUuid, TufTrustRootUuid};
use semver::Version;
use std::collections::BTreeMap;
use update_common::artifacts::{
ArtifactsWithPlan, ControlPlaneZonesMode, VerificationMode,
};
Expand Down Expand Up @@ -60,8 +65,7 @@ impl super::Nexus {
let response = self
.db_datastore
.tuf_repo_insert(opctx, artifacts_with_plan.description())
.await
.map_err(HttpError::from)?;
.await?;

// If we inserted a new repository, move the `ArtifactsWithPlan` (which
// carries with it the `Utf8TempDir`s storing the artifacts) into the
Expand Down Expand Up @@ -149,4 +153,112 @@ impl super::Nexus {
.await
.map_err(HttpError::from)
}

/// Get external update status with aggregated component counts and blockers
pub async fn update_status_external(
&self,
opctx: &OpContext,
) -> Result<views::UpdateStatus, Error> {
// Get target release information
let target_release =
match self.datastore().target_release_get_current(opctx).await {
Ok(tr) => Some(
self.datastore().target_release_view(opctx, &tr).await?,
),
Err(_) => None, // No target release set
};

let components_by_release =
self.component_version_counts(opctx).await?;

let last_blueprint_time =
self.datastore().blueprint_get_latest_time(opctx).await?;

// TODO: Figure out how to list things blocking progress

Ok(views::UpdateStatus {
target_release,
components_by_release,
last_blueprint_time,
})
}

/// Get component status using read-only queries to avoid batch operations
async fn component_version_counts(
&self,
opctx: &OpContext,
) -> Result<BTreeMap<String, usize>, Error> {
// Get the latest inventory collection
let Some(inventory) =
self.datastore().inventory_get_latest_collection(opctx).await?
else {
// No inventory collection available, return empty counts
return Ok(BTreeMap::new());
};

let target_release =
self.datastore().target_release_get_current(opctx).await?;

let Some(target_release_tuf_repo_id) = target_release.tuf_repo_id
else {
return Err(Error::internal_error(
"target release has no TUF repo",
));
};
let target_release_desc = self
.datastore()
.tuf_repo_get_by_id(opctx, target_release_tuf_repo_id.into())
.await?
.into_external();

// TODO: fall back to TargetReleaseDescription::Initial if there's no
// previous release. Might not want to eat *all* errors, though.

// Get previous target release (if exists)
// For simplicity, we'll try to get one generation back
let Some(prev_gen) = target_release.generation.prev() else {
return Err(Error::internal_error(
"target release has no prev gen",
));
};

let prev_release = self
.datastore()
.target_release_get_generation(opctx, Generation(prev_gen))
.await
.internal_context("fetching previous target release")?;
let Some(prev_release_tuf_repo_id) =
prev_release.and_then(|r| r.tuf_repo_id)
else {
return Err(Error::internal_error("prev release has no TUF repo"));
};
let prev_release_desc = self
.datastore()
.tuf_repo_get_by_id(opctx, prev_release_tuf_repo_id.into())
.await?
.into_external();

// TODO: It's weird to use the internal view this way. On the other hand
// it feels silly to extract a shared structure that's basically the
// same as this struct.
let status = internal_views::UpdateStatus::new(
&TargetReleaseDescription::TufRepo(prev_release_desc),
&TargetReleaseDescription::TufRepo(target_release_desc),
&inventory,
);

let zone_versions = status
.zones
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI #8901 changes the status struct to add information about other components. I think git may not realize it conflicts, but it definitely does - sorry! (Fundamentally it's still the same "list of components and their versions", but some of the other components are slightly more complicated to report, and there are weird failure cases; e.g., we could know which disk a sled booted from but not what's on it, or know what's on both disks but not which it booted from, etc.)

.values()
.flat_map(|zones| zones.iter().map(|zone| zone.version.clone()));
let sp_versions = status.sps.values().flat_map(|sp| {
[sp.slot0_version.clone(), sp.slot1_version.clone()]
});

let mut counts = BTreeMap::new();
for version in zone_versions.chain(sp_versions) {
*counts.entry(version.to_string()).or_insert(0) += 1;
}
Ok(counts)
}
}
18 changes: 18 additions & 0 deletions nexus/src/external_api/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6872,6 +6872,24 @@ impl NexusExternalApi for NexusExternalApiImpl {
.await
}

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

// Silo users

async fn user_list(
Expand Down
2 changes: 1 addition & 1 deletion nexus/tests/integration_tests/target_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ async fn get_set_target_release() -> Result<()> {
Ok(())
}

async fn set_target_release(
pub async fn set_target_release(
client: &ClientTestContext,
system_version: Version,
) -> Result<TargetRelease> {
Expand Down
Loading
Loading