@@ -8,17 +8,28 @@ use crate::Omdb;
8
8
use crate :: check_allow_destructive:: DestructiveOperationToken ;
9
9
use crate :: db:: DbUrlOptions ;
10
10
use anyhow:: Context as _;
11
+ use async_bb8_diesel:: AsyncRunQueryDsl ;
11
12
use camino:: Utf8PathBuf ;
12
13
use clap:: Args ;
13
14
use clap:: Subcommand ;
15
+ use diesel:: ExpressionMethods ;
16
+ use diesel:: QueryDsl ;
17
+ use diesel:: SelectableHelper ;
18
+ use nexus_db_model:: BpTarget ;
14
19
use nexus_db_queries:: authz;
15
20
use nexus_db_queries:: context:: OpContext ;
16
21
use nexus_db_queries:: db:: DataStore ;
22
+ use nexus_db_queries:: db:: datastore:: SQL_BATCH_SIZE ;
23
+ use nexus_db_queries:: db:: pagination:: Paginator ;
24
+ use nexus_types:: deployment:: Blueprint ;
25
+ use nexus_types:: deployment:: BlueprintMetadata ;
17
26
use nexus_types:: deployment:: UnstableReconfiguratorState ;
18
27
use omicron_common:: api:: external:: Error ;
19
28
use omicron_common:: api:: external:: LookupType ;
29
+ use omicron_uuid_kinds:: BlueprintUuid ;
20
30
use omicron_uuid_kinds:: GenericUuid ;
21
31
use slog:: Logger ;
32
+ use std:: collections:: BTreeMap ;
22
33
23
34
/// Arguments to the "omdb reconfigurator" subcommand
24
35
#[ derive( Debug , Args ) ]
@@ -40,6 +51,8 @@ enum ReconfiguratorCommands {
40
51
/// Save the current Reconfigurator state to a file and remove historical
41
52
/// artifacts from the live system (e.g., non-target blueprints)
42
53
Archive ( ExportArgs ) ,
54
+ /// Show recent history of blueprints
55
+ History ( HistoryArgs ) ,
43
56
}
44
57
45
58
#[ derive( Debug , Args , Clone ) ]
@@ -48,6 +61,17 @@ struct ExportArgs {
48
61
output_file : Utf8PathBuf ,
49
62
}
50
63
64
+ #[ derive( Debug , Args , Clone ) ]
65
+ struct HistoryArgs {
66
+ /// how far back in the history to show (number of targets)
67
+ #[ clap( long, default_value_t = 128 ) ]
68
+ limit : u32 ,
69
+
70
+ /// also attempt to diff blueprints
71
+ #[ clap( long, default_value_t = false ) ]
72
+ diff : bool ,
73
+ }
74
+
51
75
impl ReconfiguratorArgs {
52
76
/// Run a `omdb reconfigurator` subcommand.
53
77
pub ( crate ) async fn run_cmd (
@@ -77,6 +101,14 @@ impl ReconfiguratorArgs {
77
101
)
78
102
. await
79
103
}
104
+ ReconfiguratorCommands :: History ( history_args) => {
105
+ cmd_reconfigurator_history (
106
+ & opctx,
107
+ & datastore,
108
+ history_args,
109
+ )
110
+ . await
111
+ }
80
112
}
81
113
} )
82
114
. await
@@ -192,3 +224,142 @@ async fn cmd_reconfigurator_archive(
192
224
193
225
Ok ( ( ) )
194
226
}
227
+
228
+ /// Show recent history of blueprints
229
+ async fn cmd_reconfigurator_history (
230
+ opctx : & OpContext ,
231
+ datastore : & DataStore ,
232
+ history_args : & HistoryArgs ,
233
+ ) -> anyhow:: Result < ( ) > {
234
+ // Select recent targets.
235
+ let limit = history_args. limit ;
236
+ let mut targets: Vec < _ > = {
237
+ use nexus_db_queries:: db:: schema:: bp_target:: dsl;
238
+ let conn = datastore
239
+ . pool_connection_for_tests ( )
240
+ . await
241
+ . context ( "obtaining connection" ) ?;
242
+ dsl:: bp_target
243
+ . select ( BpTarget :: as_select ( ) )
244
+ . order_by ( dsl:: version. desc ( ) )
245
+ . limit ( i64:: from ( limit) )
246
+ . get_results_async ( & * conn)
247
+ . await
248
+ . context ( "listing targets" ) ?
249
+ } ;
250
+
251
+ // Select everything from the blueprint table.
252
+ // This shouldn't be very large.
253
+ let mut all_blueprints: BTreeMap < BlueprintUuid , BlueprintMetadata > =
254
+ BTreeMap :: new ( ) ;
255
+ let mut paginator = Paginator :: new ( SQL_BATCH_SIZE ) ;
256
+ while let Some ( p) = paginator. next ( ) {
257
+ let records_batch = datastore
258
+ . blueprints_list ( opctx, & p. current_pagparams ( ) )
259
+ . await
260
+ . context ( "batch of blueprints" ) ?;
261
+ paginator = p. found_batch ( & records_batch, & |b| * b. id . as_untyped_uuid ( ) ) ;
262
+ all_blueprints. extend ( records_batch. into_iter ( ) . map ( |b| ( b. id , b) ) ) ;
263
+ }
264
+
265
+ // Sort the target list in increasing order.
266
+ // (This should be the same as reversing it.)
267
+ targets. sort_by_key ( |b| b. version ) ;
268
+
269
+ // Now, print the history.
270
+ println ! ( "{:>5} {:24} {:36}" , "VERSN" , "TIME" , "BLUEPRINT" ) ;
271
+ if targets. len ( ) == usize:: try_from ( limit) . unwrap ( ) {
272
+ println ! ( "... (earlier history omitted)" ) ;
273
+ }
274
+
275
+ // prev_blueprint_id is `None` only during the first iteration.
276
+ let mut prev_blueprint_id: Option < BlueprintUuid > = None ;
277
+
278
+ // prev_blueprint is `None` if any of these is true:
279
+ // - if we're not printing diffs
280
+ // - if this is the first iteration of the loop
281
+ // - if the previous blueprint was missing from the database or we didn't
282
+ // load it because _it_ was the first iteration of the loop or _its_
283
+ // parent was missing from the database
284
+ let mut prev_blueprint: Option < Blueprint > = None ;
285
+
286
+ for t in targets {
287
+ let target_id = BlueprintUuid :: from ( t. blueprint_id ) ;
288
+
289
+ print ! (
290
+ "{:>5} {} {} {:>8}" ,
291
+ t. version,
292
+ humantime:: format_rfc3339_millis( t. time_made_target. into( ) ) ,
293
+ target_id,
294
+ if t. enabled { "enabled" } else { "disabled" } ,
295
+ ) ;
296
+
297
+ if prev_blueprint_id == Some ( target_id) {
298
+ // The only change here could be to the enable/disable bit.
299
+ // There's nothing else to say.
300
+ println ! ( ) ;
301
+ } else {
302
+ // The blueprint id changed.
303
+ let comment = match all_blueprints. get ( & target_id) {
304
+ Some ( b) => & b. comment ,
305
+ None => "blueprint details no longer available" ,
306
+ } ;
307
+ println ! ( ": {}" , comment) ;
308
+
309
+ match (
310
+ // are we printing diffs?
311
+ history_args. diff ,
312
+ // was the previous blueprint (if any) in the database?
313
+ prev_blueprint_id
314
+ . and_then ( |prev_id| all_blueprints. get ( & prev_id) ) ,
315
+ // is this blueprint in the database?
316
+ all_blueprints. get ( & target_id) ,
317
+ ) {
318
+ ( true , Some ( previous) , Some ( _) ) => {
319
+ // In this case, we are printing diffs and both the previous
320
+ // and current blueprints are in the database.
321
+ //
322
+ // We might already have loaded the full previous blueprint,
323
+ // if we took this branch on the last iteration. But we
324
+ // might not have, if that blueprint's parent was absent
325
+ // from the database.
326
+ let previous_blueprint = match prev_blueprint {
327
+ Some ( p) => p,
328
+ None => {
329
+ blueprint_load ( opctx, datastore, previous. id )
330
+ . await ?
331
+ }
332
+ } ;
333
+ assert_eq ! ( previous_blueprint. id, previous. id) ;
334
+ let current_blueprint =
335
+ blueprint_load ( opctx, datastore, target_id) . await ?;
336
+ let diff = current_blueprint
337
+ . diff_since_blueprint ( & previous_blueprint) ;
338
+ println ! ( "{}" , diff. display( ) ) ;
339
+ prev_blueprint = Some ( current_blueprint) ;
340
+ }
341
+ _ => {
342
+ prev_blueprint = None ;
343
+ }
344
+ } ;
345
+ }
346
+
347
+ prev_blueprint_id = Some ( target_id) ;
348
+ }
349
+
350
+ Ok ( ( ) )
351
+ }
352
+
353
+ async fn blueprint_load (
354
+ opctx : & OpContext ,
355
+ datastore : & DataStore ,
356
+ id : BlueprintUuid ,
357
+ ) -> anyhow:: Result < Blueprint > {
358
+ let id = * id. as_untyped_uuid ( ) ;
359
+ let authz_blueprint =
360
+ authz:: Blueprint :: new ( authz:: FLEET , id, LookupType :: ById ( id) ) ;
361
+ datastore
362
+ . blueprint_read ( opctx, & authz_blueprint)
363
+ . await
364
+ . with_context ( || format ! ( "read blueprint {}" , id) )
365
+ }
0 commit comments