Skip to content

Commit ff5dd1c

Browse files
committed
make the llm code less disgusting, get counts
1 parent 41f0b97 commit ff5dd1c

File tree

5 files changed

+133
-218
lines changed

5 files changed

+133
-218
lines changed

nexus/src/app/update.rs

Lines changed: 71 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@ use dropshot::HttpError;
99
use futures::Stream;
1010
use nexus_auth::authz;
1111
use nexus_db_lookup::LookupPath;
12-
use nexus_db_model::{TufRepoDescription, TufTrustRoot};
12+
use nexus_db_model::{Generation, TufRepoDescription, TufTrustRoot};
1313
use nexus_db_queries::context::OpContext;
1414
use nexus_db_queries::db::{datastore::SQL_BATCH_SIZE, pagination::Paginator};
15+
use nexus_types::deployment::TargetReleaseDescription;
1516
use nexus_types::external_api::shared::TufSignedRootRole;
1617
use nexus_types::external_api::views;
18+
use nexus_types::internal_api::views as internal_views;
19+
use omicron_common::api::external::InternalContext;
1720
use omicron_common::api::external::{
1821
DataPageParams, Error, TufRepoInsertResponse, TufRepoInsertStatus,
1922
};
2023
use omicron_uuid_kinds::{GenericUuid, TufTrustRootUuid};
2124
use semver::Version;
25+
use std::collections::BTreeMap;
2226
use update_common::artifacts::{
2327
ArtifactsWithPlan, ControlPlaneZonesMode, VerificationMode,
2428
};
@@ -61,8 +65,7 @@ impl super::Nexus {
6165
let response = self
6266
.db_datastore
6367
.tuf_repo_insert(opctx, artifacts_with_plan.description())
64-
.await
65-
.map_err(HttpError::from)?;
68+
.await?;
6669

6770
// If we inserted a new repository, move the `ArtifactsWithPlan` (which
6871
// carries with it the `Utf8TempDir`s storing the artifacts) into the
@@ -155,7 +158,7 @@ impl super::Nexus {
155158
pub async fn update_status_external(
156159
&self,
157160
opctx: &OpContext,
158-
) -> Result<views::UpdateStatus, HttpError> {
161+
) -> Result<views::UpdateStatus, Error> {
159162
// Get target release information
160163
let target_release =
161164
match self.datastore().target_release_get_current(opctx).await {
@@ -165,170 +168,97 @@ impl super::Nexus {
165168
Err(_) => None, // No target release set
166169
};
167170

168-
// Get component status using read-only queries
169171
let components_by_release =
170-
self.get_component_status_readonly(opctx).await?;
172+
self.component_version_counts(opctx).await?;
171173

172-
let last_blueprint_time = self
173-
.datastore()
174-
.blueprint_get_latest_time(opctx)
175-
.await
176-
.map_err(HttpError::from)?;
174+
let last_blueprint_time =
175+
self.datastore().blueprint_get_latest_time(opctx).await?;
177176

178-
// Generate blockers (placeholder for now)
179-
let blockers = self.generate_blockers(opctx).await?;
177+
// TODO: Figure out how to list things blocking progress
180178

181179
Ok(views::UpdateStatus {
182180
target_release,
183181
components_by_release,
184182
last_blueprint_time,
185-
blockers,
186183
})
187184
}
188185

189-
/// Generate list of blockers (placeholder implementation)
190-
async fn generate_blockers(
191-
&self,
192-
_opctx: &OpContext,
193-
) -> Result<Vec<views::UpdateBlocker>, HttpError> {
194-
// TODO: Implement actual blocker detection based on planning reports
195-
// For now, return empty list
196-
Ok(Vec::new())
197-
}
198-
199186
/// Get component status using read-only queries to avoid batch operations
200-
async fn get_component_status_readonly(
187+
async fn component_version_counts(
201188
&self,
202189
opctx: &OpContext,
203-
) -> Result<
204-
std::collections::BTreeMap<String, views::ComponentCounts>,
205-
HttpError,
206-
> {
207-
use nexus_sled_agent_shared::inventory::OmicronZoneImageSource;
208-
use std::collections::BTreeMap;
209-
use std::collections::HashMap;
210-
211-
let mut counts = BTreeMap::new();
212-
190+
) -> Result<BTreeMap<String, usize>, Error> {
213191
// Get the latest inventory collection
214-
let Some(inventory) = self
215-
.datastore()
216-
.inventory_get_latest_collection(opctx)
217-
.await
218-
.map_err(HttpError::from)?
192+
let Some(inventory) =
193+
self.datastore().inventory_get_latest_collection(opctx).await?
219194
else {
220195
// No inventory collection available, return empty counts
221-
return Ok(counts);
196+
return Ok(BTreeMap::new());
222197
};
223198

224-
// Build hash-to-version mappings from TUF repos (current and previous)
225-
let mut hash_to_version = HashMap::new();
199+
let target_release =
200+
self.datastore().target_release_get_current(opctx).await?;
226201

227-
// Get current target release
228-
if let Ok(target_release) =
229-
self.datastore().target_release_get_current(opctx).await
230-
{
231-
if let Some(tuf_repo_id) = target_release.tuf_repo_id {
232-
if let Ok(tuf_repo_desc) = self
233-
.datastore()
234-
.tuf_repo_get_by_id(opctx, tuf_repo_id.into())
235-
.await
236-
{
237-
let version_name =
238-
format!("v{}", tuf_repo_desc.repo.system_version);
239-
for artifact in &tuf_repo_desc.artifacts {
240-
hash_to_version
241-
.insert(artifact.sha256, version_name.clone());
242-
}
243-
}
244-
}
245-
}
202+
let Some(target_release_tuf_repo_id) = target_release.tuf_repo_id
203+
else {
204+
return Err(Error::internal_error(
205+
"target release has no TUF repo",
206+
));
207+
};
208+
let target_release_desc = self
209+
.datastore()
210+
.tuf_repo_get_by_id(opctx, target_release_tuf_repo_id.into())
211+
.await?
212+
.into_external();
213+
214+
// TODO: fall back to TargetReleaseDescription::Initial if there's no
215+
// previous release. Might not want to eat *all* errors, though.
246216

247217
// Get previous target release (if exists)
248218
// For simplicity, we'll try to get one generation back
249-
if let Ok(current) =
250-
self.datastore().target_release_get_current(opctx).await
251-
{
252-
if let Some(prev_gen) = current.generation.prev() {
253-
if let Ok(Some(prev_release)) = self
254-
.datastore()
255-
.target_release_get_generation(
256-
opctx,
257-
nexus_db_model::Generation(prev_gen),
258-
)
259-
.await
260-
{
261-
if let Some(prev_tuf_repo_id) = prev_release.tuf_repo_id {
262-
if let Ok(prev_tuf_repo_desc) = self
263-
.datastore()
264-
.tuf_repo_get_by_id(opctx, prev_tuf_repo_id.into())
265-
.await
266-
{
267-
let prev_version_name = format!(
268-
"v{}",
269-
prev_tuf_repo_desc.repo.system_version
270-
);
271-
for artifact in &prev_tuf_repo_desc.artifacts {
272-
// Only add if not already present (current takes priority)
273-
hash_to_version
274-
.entry(artifact.sha256)
275-
.or_insert(prev_version_name.clone());
276-
}
277-
}
278-
}
279-
}
280-
}
281-
}
219+
let Some(prev_gen) = target_release.generation.prev() else {
220+
return Err(Error::internal_error(
221+
"target release has no prev gen",
222+
));
223+
};
224+
225+
let prev_release = self
226+
.datastore()
227+
.target_release_get_generation(opctx, Generation(prev_gen))
228+
.await
229+
.internal_context("fetching previous target release")?;
230+
let Some(prev_release_tuf_repo_id) =
231+
prev_release.and_then(|r| r.tuf_repo_id)
232+
else {
233+
return Err(Error::internal_error("prev release has no TUF repo"));
234+
};
235+
let prev_release_desc = self
236+
.datastore()
237+
.tuf_repo_get_by_id(opctx, prev_release_tuf_repo_id.into())
238+
.await?
239+
.into_external();
282240

283-
// Count zones by version using the hash mappings
284-
for agent in inventory.sled_agents.iter() {
285-
if let Some(reconciliation) = &agent.last_reconciliation {
286-
for (_zone_config, _result) in
287-
reconciliation.reconciled_omicron_zones()
288-
{
289-
let version_key = match &_zone_config.image_source {
290-
OmicronZoneImageSource::InstallDataset => {
291-
"install_dataset".to_string()
292-
}
293-
OmicronZoneImageSource::Artifact { hash } => {
294-
// Convert tufaceous_artifact::ArtifactHash to nexus_db_model::ArtifactHash
295-
let db_hash = nexus_db_model::ArtifactHash(*hash);
296-
// Map hash to actual version if possible
297-
hash_to_version
298-
.get(&db_hash)
299-
.cloned()
300-
.unwrap_or_else(|| {
301-
format!(
302-
"unknown_artifact_{}",
303-
&hash.to_string()[..8]
304-
)
305-
})
306-
}
307-
};
241+
// TODO: It's weird to use the internal view this way. On the other hand
242+
// it feels silly to extract a shared structure that's basically the
243+
// same as this struct.
244+
let status = internal_views::UpdateStatus::new(
245+
&TargetReleaseDescription::TufRepo(prev_release_desc),
246+
&TargetReleaseDescription::TufRepo(target_release_desc),
247+
&inventory,
248+
);
308249

309-
let entry =
310-
counts.entry(version_key).or_insert_with(|| {
311-
views::ComponentCounts {
312-
zone_count: 0,
313-
sp_count: 0,
314-
}
315-
});
316-
entry.zone_count += 1;
317-
}
318-
}
319-
}
250+
let zone_versions = status
251+
.zones
252+
.values()
253+
.flat_map(|zones| zones.iter().map(|zone| zone.version.clone()));
254+
let sp_versions = status.sps.values().flat_map(|sp| {
255+
[sp.slot0_version.clone(), sp.slot1_version.clone()]
256+
});
320257

321-
// Count SPs - for now we can't easily determine SP versions without
322-
// complex operations, so group them all together
323-
let sp_count = inventory.sps.len() as u32;
324-
if sp_count > 0 {
325-
let entry =
326-
counts.entry("sp_firmware".to_string()).or_insert_with(|| {
327-
views::ComponentCounts { zone_count: 0, sp_count: 0 }
328-
});
329-
entry.sp_count = sp_count;
258+
let mut counts = BTreeMap::new();
259+
for version in zone_versions.chain(sp_versions) {
260+
*counts.entry(version.to_string()).or_insert(0) += 1;
330261
}
331-
332262
Ok(counts)
333263
}
334264
}

nexus/tests/integration_tests/target_release.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ async fn get_set_target_release() -> Result<()> {
124124
Ok(())
125125
}
126126

127-
async fn set_target_release(
127+
pub async fn set_target_release(
128128
client: &ClientTestContext,
129129
system_version: Version,
130130
) -> Result<TargetRelease> {

nexus/tests/integration_tests/updates.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ use std::collections::HashSet;
2626
use std::fmt::Debug;
2727
use tough::editor::signed::SignedRole;
2828
use tough::schema::Root;
29+
use tufaceous_artifact::ArtifactVersion;
2930
use tufaceous_artifact::KnownArtifactKind;
3031
use tufaceous_lib::Key;
3132
use tufaceous_lib::assemble::{ArtifactManifest, OmicronRepoAssembler};
3233
use tufaceous_lib::assemble::{DeserializedManifest, ManifestTweak};
3334

35+
use crate::integration_tests::target_release::set_target_release;
36+
3437
const TRUST_ROOTS_URL: &str = "/v1/system/update/trust-roots";
3538

3639
type ControlPlaneTestContext =
@@ -629,12 +632,59 @@ async fn test_trust_root_operations(cptestctx: &ControlPlaneTestContext) {
629632
assert!(response.items.is_empty());
630633
}
631634

632-
#[nexus_test]
633-
async fn test_update_status(cptestctx: &ControlPlaneTestContext) {
635+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
636+
async fn test_update_status() -> Result<()> {
637+
let cptestctx =
638+
test_setup::<omicron_nexus::Server>("test_update_uninitialized", 0)
639+
.await;
634640
let client = &cptestctx.external_client;
641+
let logctx = &cptestctx.logctx;
642+
643+
// Upload a fake TUF repo and set it as the target release
644+
let trust_root = TestTrustRoot::generate().await?;
645+
trust_root.to_upload_request(client, StatusCode::CREATED).execute().await?;
646+
trust_root
647+
.assemble_repo(&logctx.log, &[])
648+
.await?
649+
.to_upload_request(client, StatusCode::OK)
650+
.execute()
651+
.await?;
652+
let v1 = Version::new(1, 0, 0);
653+
set_target_release(client, v1).await?;
654+
655+
// do it again so there are two, so both versions are associated with tuf repos
656+
let v2 = Version::new(2, 0, 0);
657+
let tweaks = &[
658+
ManifestTweak::SystemVersion(v2.clone()),
659+
ManifestTweak::ArtifactVersion {
660+
kind: KnownArtifactKind::SwitchRotBootloader,
661+
version: ArtifactVersion::new("non-semver-2").unwrap(),
662+
},
663+
];
664+
let trust_root = TestTrustRoot::generate().await?;
665+
trust_root.to_upload_request(client, StatusCode::CREATED).execute().await?;
666+
trust_root
667+
.assemble_repo(&logctx.log, tweaks)
668+
.await?
669+
.to_upload_request(client, StatusCode::OK)
670+
.execute()
671+
.await?;
672+
set_target_release(client, v2.clone()).await?;
635673

636674
let status: views::UpdateStatus =
637675
object_get(client, "/v1/system/update/status").await;
638676

639-
dbg!(status);
677+
dbg!(status.clone());
678+
679+
assert_eq!(
680+
status.target_release.unwrap().release_source,
681+
views::TargetReleaseSource::SystemVersion { version: v2 }
682+
);
683+
684+
let counts = status.components_by_release;
685+
assert_eq!(counts.get("install dataset").unwrap(), &7);
686+
assert_eq!(counts.get("unknown").unwrap(), &8);
687+
688+
cptestctx.teardown().await;
689+
Ok(())
640690
}

0 commit comments

Comments
 (0)