Skip to content

Commit 4a7556c

Browse files
authored
add omdb reconfigurator history (#7600)
1 parent 63f3dfc commit 4a7556c

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed

dev-tools/omdb/src/bin/omdb/reconfigurator.rs

+171
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,28 @@ use crate::Omdb;
88
use crate::check_allow_destructive::DestructiveOperationToken;
99
use crate::db::DbUrlOptions;
1010
use anyhow::Context as _;
11+
use async_bb8_diesel::AsyncRunQueryDsl;
1112
use camino::Utf8PathBuf;
1213
use clap::Args;
1314
use clap::Subcommand;
15+
use diesel::ExpressionMethods;
16+
use diesel::QueryDsl;
17+
use diesel::SelectableHelper;
18+
use nexus_db_model::BpTarget;
1419
use nexus_db_queries::authz;
1520
use nexus_db_queries::context::OpContext;
1621
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;
1726
use nexus_types::deployment::UnstableReconfiguratorState;
1827
use omicron_common::api::external::Error;
1928
use omicron_common::api::external::LookupType;
29+
use omicron_uuid_kinds::BlueprintUuid;
2030
use omicron_uuid_kinds::GenericUuid;
2131
use slog::Logger;
32+
use std::collections::BTreeMap;
2233

2334
/// Arguments to the "omdb reconfigurator" subcommand
2435
#[derive(Debug, Args)]
@@ -40,6 +51,8 @@ enum ReconfiguratorCommands {
4051
/// Save the current Reconfigurator state to a file and remove historical
4152
/// artifacts from the live system (e.g., non-target blueprints)
4253
Archive(ExportArgs),
54+
/// Show recent history of blueprints
55+
History(HistoryArgs),
4356
}
4457

4558
#[derive(Debug, Args, Clone)]
@@ -48,6 +61,17 @@ struct ExportArgs {
4861
output_file: Utf8PathBuf,
4962
}
5063

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+
5175
impl ReconfiguratorArgs {
5276
/// Run a `omdb reconfigurator` subcommand.
5377
pub(crate) async fn run_cmd(
@@ -77,6 +101,14 @@ impl ReconfiguratorArgs {
77101
)
78102
.await
79103
}
104+
ReconfiguratorCommands::History(history_args) => {
105+
cmd_reconfigurator_history(
106+
&opctx,
107+
&datastore,
108+
history_args,
109+
)
110+
.await
111+
}
80112
}
81113
})
82114
.await
@@ -192,3 +224,142 @@ async fn cmd_reconfigurator_archive(
192224

193225
Ok(())
194226
}
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+
}

dev-tools/omdb/tests/usage_errors.out

+1
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ Commands:
842842
export Save the current Reconfigurator state to a file
843843
archive Save the current Reconfigurator state to a file and remove historical artifacts from the
844844
live system (e.g., non-target blueprints)
845+
history Show recent history of blueprints
845846
help Print this message or the help of the given subcommand(s)
846847

847848
Options:

0 commit comments

Comments
 (0)