@@ -9,16 +9,20 @@ use dropshot::HttpError;
99use futures:: Stream ;
1010use nexus_auth:: authz;
1111use nexus_db_lookup:: LookupPath ;
12- use nexus_db_model:: { TufRepoDescription , TufTrustRoot } ;
12+ use nexus_db_model:: { Generation , TufRepoDescription , TufTrustRoot } ;
1313use nexus_db_queries:: context:: OpContext ;
1414use nexus_db_queries:: db:: { datastore:: SQL_BATCH_SIZE , pagination:: Paginator } ;
15+ use nexus_types:: deployment:: TargetReleaseDescription ;
1516use nexus_types:: external_api:: shared:: TufSignedRootRole ;
1617use nexus_types:: external_api:: views;
18+ use nexus_types:: internal_api:: views as internal_views;
19+ use omicron_common:: api:: external:: InternalContext ;
1720use omicron_common:: api:: external:: {
1821 DataPageParams , Error , TufRepoInsertResponse , TufRepoInsertStatus ,
1922} ;
2023use omicron_uuid_kinds:: { GenericUuid , TufTrustRootUuid } ;
2124use semver:: Version ;
25+ use std:: collections:: BTreeMap ;
2226use 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}
0 commit comments