diff --git a/dev-tools/omdb/src/bin/omdb/reconfigurator.rs b/dev-tools/omdb/src/bin/omdb/reconfigurator.rs index e9cf41d909d..9a361d794e6 100644 --- a/dev-tools/omdb/src/bin/omdb/reconfigurator.rs +++ b/dev-tools/omdb/src/bin/omdb/reconfigurator.rs @@ -65,6 +65,10 @@ enum ReconfiguratorCommands { #[derive(Debug, Args, Clone)] struct ExportArgs { + /// maximum number of blueprints to save + #[clap(long, default_value_t = 1000)] + nmax_blueprints: usize, + /// where to save the output output_file: Utf8PathBuf, } @@ -147,13 +151,22 @@ async fn cmd_reconfigurator_export( ) -> anyhow::Result { // See Nexus::blueprint_planning_context(). eprint!("assembling reconfigurator state ... "); + let limit = export_args.nmax_blueprints; let state = nexus_reconfigurator_preparation::reconfigurator_state_load( - opctx, datastore, + opctx, datastore, limit, ) .await?; eprintln!("done"); + if state.blueprints.len() >= limit { + eprintln!( + "warning: reached limit of {limit} while fetching blueprints" + ); + eprintln!("warning: saving only the most recent {limit}"); + } + let output_path = &export_args.output_file; + eprint!("saving to {} ... ", output_path); let file = std::fs::OpenOptions::new() .create_new(true) .write(true) @@ -161,7 +174,7 @@ async fn cmd_reconfigurator_export( .with_context(|| format!("open {:?}", output_path))?; serde_json::to_writer_pretty(&file, &state) .with_context(|| format!("write {:?}", output_path))?; - eprintln!("wrote {}", output_path); + eprintln!("done"); Ok(state) } @@ -199,7 +212,7 @@ async fn cmd_reconfigurator_archive( let mut ndeleted = 0; - eprintln!("removing non-target blueprints ..."); + eprintln!("removing saved, non-target blueprints ..."); for blueprint in &saved_state.blueprints { if blueprint.id == target_blueprint_id { continue; @@ -245,6 +258,15 @@ async fn cmd_reconfigurator_archive( eprintln!("done ({ndeleted} blueprint{plural} deleted)",); } + if saved_state.blueprints.len() >= archive_args.nmax_blueprints { + eprintln!( + "warning: Only tried deleting the most recent {} blueprints\n\ + warning: because that's all that was fetched and saved.\n\ + warning: You may want to run this tool again to archive more.", + saved_state.blueprints.len(), + ); + } + Ok(()) } diff --git a/nexus/db-queries/src/db/datastore/inventory.rs b/nexus/db-queries/src/db/datastore/inventory.rs index 5899cbedecd..451e9885589 100644 --- a/nexus/db-queries/src/db/datastore/inventory.rs +++ b/nexus/db-queries/src/db/datastore/inventory.rs @@ -4078,6 +4078,29 @@ impl DataStore { internal_dns_generation_status, }) } + + pub async fn inventory_collections_latest( + &self, + opctx: &OpContext, + count: u8, + ) -> anyhow::Result> { + let limit: i64 = i64::from(count); + let conn = self + .pool_connection_authorized(opctx) + .await + .context("getting connection")?; + + use nexus_db_schema::schema::inv_collection::dsl; + let collections = dsl::inv_collection + .select(InvCollection::as_select()) + .order_by(dsl::time_started.desc()) + .limit(limit) + .load_async(&*conn) + .await + .context("failed to list collections")?; + + Ok(collections) + } } #[derive(Debug)] diff --git a/nexus/reconfigurator/cli-integration-tests/tests/integration/blueprint_edit.rs b/nexus/reconfigurator/cli-integration-tests/tests/integration/blueprint_edit.rs index 0ac5b596670..b905818652d 100644 --- a/nexus/reconfigurator/cli-integration-tests/tests/integration/blueprint_edit.rs +++ b/nexus/reconfigurator/cli-integration-tests/tests/integration/blueprint_edit.rs @@ -116,7 +116,7 @@ async fn test_blueprint_edit(cptestctx: &ControlPlaneTestContext) { // Assemble state that we can load into reconfigurator-cli. let state1 = nexus_reconfigurator_preparation::reconfigurator_state_load( - &opctx, datastore, + &opctx, datastore, 20, ) .await .expect("failed to assemble reconfigurator state"); diff --git a/nexus/reconfigurator/preparation/src/lib.rs b/nexus/reconfigurator/preparation/src/lib.rs index d400bb3e9b1..d3c0bf51184 100644 --- a/nexus/reconfigurator/preparation/src/lib.rs +++ b/nexus/reconfigurator/preparation/src/lib.rs @@ -12,7 +12,6 @@ use nexus_db_model::Generation; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::DataStore; use nexus_db_queries::db::datastore::DataStoreDnsTest; -use nexus_db_queries::db::datastore::DataStoreInventoryTest; use nexus_db_queries::db::datastore::Discoverability; use nexus_db_queries::db::datastore::SQL_BATCH_SIZE; use nexus_db_queries::db::pagination::Paginator; @@ -57,6 +56,7 @@ use omicron_uuid_kinds::OmicronZoneUuid; use slog::Logger; use slog::error; use slog_error_chain::InlineErrorChain; +use std::cmp::Reverse; use std::collections::BTreeMap; use std::collections::BTreeSet; @@ -406,12 +406,13 @@ async fn fetch_all_service_ip_pool_ranges( Ok(ranges) } -/// Loads state for import into `reconfigurator-cli` +/// Loads state for debugging or import into `reconfigurator-cli` /// -/// This is only to be used in omdb or tests. +/// This is used in omdb, tests, and in Nexus to collect support bundles pub async fn reconfigurator_state_load( opctx: &OpContext, datastore: &DataStore, + nmax_blueprints: usize, ) -> Result { opctx.check_complex_operations_allowed()?; let planner_config = datastore @@ -420,8 +421,11 @@ pub async fn reconfigurator_state_load( .map_or_else(PlannerConfig::default, |c| c.config.planner_config); let planning_input = PlanningInputFromDb::assemble(opctx, datastore, planner_config).await?; + + // We'll grab the most recent several inventory collections. + const NCOLLECTIONS: u8 = 5; let collection_ids = datastore - .inventory_collections() + .inventory_collections_latest(opctx, NCOLLECTIONS) .await .context("listing collections")? .into_iter() @@ -439,11 +443,13 @@ pub async fn reconfigurator_state_load( .collect::>() .await; + // Grab the latest target blueprint. let target_blueprint = datastore .blueprint_target_get_current(opctx) .await .context("failed to read current target blueprint")?; + // Paginate through the list of all blueprints. let mut blueprint_ids = Vec::new(); let mut paginator = Paginator::new( SQL_BATCH_SIZE, @@ -460,6 +466,13 @@ pub async fn reconfigurator_state_load( blueprint_ids.extend(batch.into_iter()); } + // We'll only grab the most recent blueprints that fit within the limit that + // we were given. This is a heuristic intended to grab what's most likely + // to be useful even when the system has a large number of blueprints. But + // the intent is that callers provide a limit that should be large enough to + // cover everything. + blueprint_ids.sort_by_key(|bpm| Reverse(bpm.time_created)); + let blueprint_ids = blueprint_ids.into_iter().take(nmax_blueprints); let blueprints = futures::stream::iter(blueprint_ids) .filter_map(|bpm| async move { let blueprint_id = bpm.id.into_untyped_uuid();