From cc33f770787ddf2c03c139165af67a3f3bb5cb41 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Thu, 12 Oct 2023 16:10:56 -0700 Subject: [PATCH] WIP: omdb support for dumping inventory from database --- Cargo.lock | 3 + dev-tools/omdb/src/bin/omdb/db.rs | 530 +++++++++++++++++- nexus/db-model/src/inventory.rs | 66 ++- nexus/db-model/src/schema.rs | 2 + nexus/db-queries/src/db/datastore/mod.rs | 12 +- nexus/src/app/background/common.rs | 6 +- nexus/src/app/background/init.rs | 15 +- .../app/background/inventory_collection.rs | 48 +- schema/crdb/dbinit.sql | 8 +- 9 files changed, 630 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b2772dfc9..87494eeaba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4498,6 +4498,8 @@ dependencies = [ "dns-server", "dns-service-client 0.1.0", "dropshot", + "gateway-messages", + "gateway-test-utils", "headers", "http", "hyper", @@ -5088,6 +5090,7 @@ dependencies = [ "nexus-db-model", "nexus-db-queries", "nexus-defaults", + "nexus-inventory", "nexus-test-interface", "nexus-test-utils", "nexus-test-utils-macros", diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 10e5546b6d..29881df647 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -27,6 +27,7 @@ use clap::ValueEnum; use diesel::expression::SelectableHelper; use diesel::query_dsl::QueryDsl; use diesel::ExpressionMethods; +use nexus_db_model::CabooseWhich; use nexus_db_model::Dataset; use nexus_db_model::Disk; use nexus_db_model::DnsGroup; @@ -34,13 +35,21 @@ use nexus_db_model::DnsName; use nexus_db_model::DnsVersion; use nexus_db_model::DnsZone; use nexus_db_model::ExternalIp; +use nexus_db_model::HwBaseboardId; use nexus_db_model::Instance; +use nexus_db_model::InvCaboose; +use nexus_db_model::InvCollection; +use nexus_db_model::InvCollectionError; +use nexus_db_model::InvRootOfTrust; +use nexus_db_model::InvServiceProcessor; use nexus_db_model::Project; use nexus_db_model::Region; use nexus_db_model::Sled; +use nexus_db_model::SwCaboose; use nexus_db_model::Zpool; use nexus_db_queries::context::OpContext; use nexus_db_queries::db; +use nexus_db_queries::db::datastore::DataStoreConnection; use nexus_db_queries::db::identity::Asset; use nexus_db_queries::db::lookup::LookupPath; use nexus_db_queries::db::model::ServiceKind; @@ -53,6 +62,7 @@ use omicron_common::api::external::Generation; use omicron_common::postgres_config::PostgresConfigWithUrl; use std::cmp::Ordering; use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::collections::HashSet; use std::fmt::Display; use std::num::NonZeroU32; @@ -85,14 +95,16 @@ enum DbCommands { Disks(DiskArgs), /// Print information about internal and external DNS Dns(DnsArgs), - /// Print information about control plane services - Services(ServicesArgs), - /// Print information about sleds - Sleds, /// Print information about customer instances Instances, + /// Print information about collected hardware/software inventory + Inventory(InventoryArgs), /// Print information about the network Network(NetworkArgs), + /// Print information about control plane services + Services(ServicesArgs), + /// Print information about sleds + Sleds, } #[derive(Debug, Args)] @@ -163,6 +175,42 @@ impl CliDnsGroup { } } +#[derive(Debug, Args)] +struct InventoryArgs { + #[command(subcommand)] + command: InventoryCommands, +} + +#[derive(Debug, Subcommand)] +enum InventoryCommands { + /// list all baseboards ever found + BaseboardIds, + /// list all cabooses ever found + Cabooses, + /// list and show details from particular collections + Collections(CollectionsArgs), +} + +#[derive(Debug, Args)] +struct CollectionsArgs { + #[command(subcommand)] + command: CollectionsCommands, +} + +#[derive(Debug, Subcommand)] +enum CollectionsCommands { + /// list collections + List, + /// show what was found in a particular collection + Show(CollectionsShowArgs), +} + +#[derive(Debug, Args)] +struct CollectionsShowArgs { + /// id of the collection + id: Uuid, +} + #[derive(Debug, Args)] struct ServicesArgs { #[command(subcommand)] @@ -266,6 +314,20 @@ impl DbArgs { cmd_db_dns_names(&opctx, &datastore, self.fetch_limit, args) .await } + DbCommands::Instances => { + cmd_db_instances(&datastore, self.fetch_limit).await + } + DbCommands::Inventory(inventory_args) => { + cmd_db_inventory(&datastore, self.fetch_limit, inventory_args) + .await + } + DbCommands::Network(NetworkArgs { + command: NetworkCommands::ListEips, + verbose, + }) => { + cmd_db_eips(&opctx, &datastore, self.fetch_limit, *verbose) + .await + } DbCommands::Services(ServicesArgs { command: ServicesCommands::ListInstances, }) => { @@ -289,16 +351,6 @@ impl DbArgs { DbCommands::Sleds => { cmd_db_sleds(&opctx, &datastore, self.fetch_limit).await } - DbCommands::Instances => { - cmd_db_instances(&datastore, self.fetch_limit).await - } - DbCommands::Network(NetworkArgs { - command: NetworkCommands::ListEips, - verbose, - }) => { - cmd_db_eips(&opctx, &datastore, self.fetch_limit, *verbose) - .await - } } } } @@ -1324,3 +1376,453 @@ fn format_record(record: &DnsRecord) -> impl Display { } } } + +// Inventory + +async fn cmd_db_inventory( + datastore: &DataStore, + limit: NonZeroU32, + inventory_args: &InventoryArgs, +) -> Result<(), anyhow::Error> { + let conn = datastore.pool_connection_for_tests().await?; + match inventory_args.command { + InventoryCommands::BaseboardIds => { + cmd_db_inventory_baseboard_ids(&conn, limit).await + } + InventoryCommands::Cabooses => { + cmd_db_inventory_cabooses(&conn, limit).await + } + InventoryCommands::Collections(CollectionsArgs { + command: CollectionsCommands::List, + }) => cmd_db_inventory_collections_list(&conn, limit).await, + InventoryCommands::Collections(CollectionsArgs { + command: CollectionsCommands::Show(CollectionsShowArgs { id }), + }) => cmd_db_inventory_collections_show(&conn, id, limit).await, + } +} + +async fn cmd_db_inventory_baseboard_ids( + conn: &DataStoreConnection<'_>, + limit: NonZeroU32, +) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct BaseboardRow { + id: Uuid, + part_number: String, + serial_number: String, + } + + use db::schema::hw_baseboard_id::dsl; + let baseboard_ids = dsl::hw_baseboard_id + .order_by((dsl::part_number, dsl::serial_number)) + .limit(i64::from(u32::from(limit))) + .select(HwBaseboardId::as_select()) + .load_async(&**conn) + .await + .context("loading baseboard ids")?; + check_limit(&baseboard_ids, limit, || "loading baseboard ids"); + + let rows = baseboard_ids.into_iter().map(|baseboard_id| BaseboardRow { + id: baseboard_id.id, + part_number: baseboard_id.part_number, + serial_number: baseboard_id.serial_number, + }); + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("{}", table); + + Ok(()) +} + +async fn cmd_db_inventory_cabooses( + conn: &DataStoreConnection<'_>, + limit: NonZeroU32, +) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct CabooseRow { + id: Uuid, + board: String, + git_commit: String, + name: String, + version: String, + } + + use db::schema::sw_caboose::dsl; + let mut cabooses = dsl::sw_caboose + .limit(i64::from(u32::from(limit))) + .select(SwCaboose::as_select()) + .load_async(&**conn) + .await + .context("loading cabooses")?; + check_limit(&cabooses, limit, || "loading cabooses"); + cabooses.sort(); + + let rows = cabooses.into_iter().map(|caboose| CabooseRow { + id: caboose.id, + board: caboose.board, + name: caboose.name, + version: caboose.version, + git_commit: caboose.git_commit, + }); + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("{}", table); + + Ok(()) +} + +async fn cmd_db_inventory_collections_list( + conn: &DataStoreConnection<'_>, + limit: NonZeroU32, +) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct CollectionRow { + id: Uuid, + started: String, + done: String, + } + + use db::schema::inv_collection::dsl; + let collections = dsl::inv_collection + .order_by(dsl::time_started) + .limit(i64::from(u32::from(limit))) + .select(InvCollection::as_select()) + .load_async(&**conn) + .await + .context("loading collections")?; + check_limit(&collections, limit, || "loading collections"); + + let rows = collections.into_iter().map(|collection| CollectionRow { + id: collection.id, + started: humantime::format_rfc3339_seconds( + collection.time_started.into(), + ) + .to_string(), + done: collection + .time_done + .map(|t| humantime::format_rfc3339_seconds(t.into()).to_string()) + .unwrap_or_else(|| String::from("-")), + }); + + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + + println!("{}", table); + + Ok(()) +} + +async fn cmd_db_inventory_collections_show( + conn: &DataStoreConnection<'_>, + id: Uuid, + limit: NonZeroU32, +) -> Result<(), anyhow::Error> { + inv_collection_print(conn, id).await?; + let nerrors = inv_collection_print_errors(conn, id, limit).await?; + + // Load all the baseboards. We could select only the baseboards referenced + // by this collection. But it's simpler to fetch everything. And it's + // uncommon enough at this point to have unreferenced baseboards that it's + // worth calling them out. + let baseboard_ids = { + use db::schema::hw_baseboard_id::dsl; + let baseboard_ids = dsl::hw_baseboard_id + .limit(i64::from(u32::from(limit))) + .select(HwBaseboardId::as_select()) + .load_async(&**conn) + .await + .context("loading baseboard ids")?; + check_limit(&baseboard_ids, limit, || "loading baseboard ids"); + baseboard_ids.into_iter().map(|b| (b.id, b)).collect::>() + }; + let mut unused_baseboard_ids = + baseboard_ids.keys().cloned().collect::>(); + + // Similarly, load cabooses that are referenced by this collection. + let cabooses = { + use db::schema::inv_caboose::dsl as inv_dsl; + use db::schema::sw_caboose::dsl as sw_dsl; + let unique_cabooses = inv_dsl::inv_caboose + .filter(inv_dsl::inv_collection_id.eq(id)) + .select(inv_dsl::sw_caboose_id) + .distinct(); + let cabooses = sw_dsl::sw_caboose + .filter(sw_dsl::id.eq_any(unique_cabooses)) + .limit(i64::from(u32::from(limit))) + .select(SwCaboose::as_select()) + .load_async(&**conn) + .await + .context("loading cabooses")?; + check_limit(&cabooses, limit, || "loading cabooses"); + cabooses.into_iter().map(|c| (c.id, c)).collect::>() + }; + + inv_collection_print_sleds( + conn, + id, + limit, + &baseboard_ids, + &cabooses, + &mut unused_baseboard_ids, + ) + .await?; + + // XXX-dap print unreferenced baseboards + + if nerrors > 0 { + eprintln!( + "warning: {} error{} were reported above", + nerrors, + if nerrors == 1 { "" } else { "s" } + ); + } + + Ok(()) +} + +async fn inv_collection_print( + conn: &DataStoreConnection<'_>, + id: Uuid, +) -> Result<(), anyhow::Error> { + use db::schema::inv_collection::dsl; + let collections = dsl::inv_collection + .filter(dsl::id.eq(id)) + .limit(2) + .select(InvCollection::as_select()) + .load_async(&**conn) + .await + .context("loading collection")?; + anyhow::ensure!( + collections.len() == 1, + "expected exactly one collection with id {}, found {}", + id, + collections.len() + ); + let c = collections.into_iter().next().unwrap(); + println!("collection: {}", c.id); + println!( + "collector: {}{}", + c.collector, + if c.collector.parse::().is_ok() { + " (likely a Nexus instance)" + } else { + "" + } + ); + println!("reason: {}", c.comment); + println!( + "started: {}", + humantime::format_rfc3339_millis(c.time_started.into()) + ); + println!( + "done: {}", + c.time_done + .map(|t| humantime::format_rfc3339_millis(t.into()).to_string()) + .unwrap_or_else(|| String::from("-")) + ); + + Ok(()) +} + +async fn inv_collection_print_errors( + conn: &DataStoreConnection<'_>, + id: Uuid, + limit: NonZeroU32, +) -> Result { + use db::schema::inv_collection_error::dsl; + let errors = dsl::inv_collection_error + .filter(dsl::inv_collection_id.eq(id)) + .limit(i64::from(u32::from(limit))) + .select(InvCollectionError::as_select()) + .load_async(&**conn) + .await + .context("loading collection errors")?; + check_limit(&errors, limit, || "loading collection errors"); + + println!("errors: {}", errors.len()); + for e in &errors { + println!(" error {}: {}", e.idx, e.message); + } + + Ok(errors + .len() + .try_into() + .expect("could not convert error count into u32 (yikes)")) +} + +async fn inv_collection_print_sleds( + conn: &DataStoreConnection<'_>, + id: Uuid, + limit: NonZeroU32, + baseboard_ids: &BTreeMap, + cabooses: &BTreeMap, + unused_baseboard_ids: &mut BTreeSet, +) -> Result<(), anyhow::Error> { + // Load the service processors, grouped by baseboard id. + let sps: BTreeMap = { + use db::schema::inv_service_processor::dsl; + let sps = dsl::inv_service_processor + .filter(dsl::inv_collection_id.eq(id)) + .limit(i64::from(u32::from(limit))) + .select(InvServiceProcessor::as_select()) + .load_async(&**conn) + .await + .context("loading service processors")?; + check_limit(&sps, limit, || "loading service processors"); + sps.into_iter().map(|s| (s.hw_baseboard_id, s)).collect() + }; + + // Load the roots of trust, grouped by baseboard id. + let rots: BTreeMap = { + use db::schema::inv_root_of_trust::dsl; + let rots = dsl::inv_root_of_trust + .filter(dsl::inv_collection_id.eq(id)) + .limit(i64::from(u32::from(limit))) + .select(InvRootOfTrust::as_select()) + .load_async(&**conn) + .await + .context("loading roots of trust")?; + check_limit(&rots, limit, || "loading roots of trust"); + rots.into_iter().map(|s| (s.hw_baseboard_id, s)).collect() + }; + + // Load cabooses found. We group these first by SP/RoT, then by baseboard. + let (sp_cabooses, rot_cabooses) = { + use db::schema::inv_caboose::dsl; + let cabooses_found = dsl::inv_caboose + .filter(dsl::inv_collection_id.eq(id)) + .limit(i64::from(u32::from(limit))) + .select(InvCaboose::as_select()) + .load_async(&**conn) + .await + .context("loading cabooses found")?; + check_limit(&cabooses_found, limit, || "loading cabooses found"); + + let (sp_cabooses, rot_cabooses): (BTreeMap<_, _>, BTreeMap<_, _>) = + cabooses_found + .into_iter() + .map(|c| (c.hw_baseboard_id, c)) + .partition(|(_, ic)| match ic.which { + CabooseWhich::SpSlot0 | CabooseWhich::SpSlot1 => true, + CabooseWhich::RotSlotA | CabooseWhich::RotSlotB => false, + }); + (sp_cabooses, rot_cabooses) + }; + + // Find the list of sled baseboard ids. The canonical way for us to tell if + // something is a sled is by looking at what kind of slot MGS found it in. + let mut sled_baseboard_ids: Vec<_> = + sps.iter() + .filter_map(|(id, sp)| match sp.sp_type { + nexus_db_model::SpType::Sled => Some(id), + nexus_db_model::SpType::Switch + | nexus_db_model::SpType::Power => None, + }) + .collect(); + + // Sort them by part number (which should all the same at this point) and + // then serial number. + sled_baseboard_ids.sort_by(|s1, s2| { + let b1 = baseboard_ids.get(s1); + let b2 = baseboard_ids.get(s2); + match (b1, b2) { + (Some(b1), Some(b2)) => b1 + .part_number + .cmp(&b2.part_number) + .then(b1.serial_number.cmp(&b2.serial_number)), + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }); + + // XXX-dap + // Create sets of SPs and RoTs that we used so that we can tell if any went + // unused. + + // Now print them. + for baseboard_id in sled_baseboard_ids { + // This unwrap should not fail because the collection we're iterating + // over came from the one we're looking into now. + let sp = sps.get(baseboard_id).unwrap(); + let baseboard = baseboard_ids.get(baseboard_id); + unused_baseboard_ids.remove(baseboard_id); + let rot = rots.get(baseboard_id); + + println!(""); + match baseboard { + None => { + // It should be impossible to find an SP whose baseboard + // information we didn't previously fetch. That's either a bug + // in this tool (for failing to fetch or find the right + // baseboard information) or the inventory system (for failing + // to insert a record into the hw_baseboard_id table). + println!("SLED (serial number unknown -- this is a bug)"); + println!(" part number: unknown"); + } + Some(baseboard) => { + println!("SLED {}", baseboard.serial_number); + println!(" part number: {}", baseboard.part_number); + } + }; + + println!(" power: {:?}", sp.power_state); + println!(" revision: {}", sp.baseboard_revision); + println!(" MGS slot: {}", sp.sp_slot); // XXX-dap which cubby? + println!(" found at: {} from {}", sp.time_collected, sp.source); + + if let Some(rot) = rot { + println!(" RoT: active slot: slot {:?}", rot.rot_slot_active); + println!( + " RoT: persistent boot preference: slot {:?}", + rot.rot_slot_active + ); + println!( + " RoT: pending persistent boot preference: {}", + rot.rot_slot_boot_pref_persistent_pending + .map(|s| format!("slot {:?}", s)) + .unwrap_or_else(|| String::from("-")) + ); + println!( + " RoT: transient boot preference: {}", + rot.rot_slot_boot_pref_transient + .map(|s| format!("slot {:?}", s)) + .unwrap_or_else(|| String::from("-")) + ); + + println!( + " RoT: slot A SHA3-256: {}", + rot.rot_slot_a_sha3_256 + .clone() + .unwrap_or_else(|| String::from("-")) + ); + + println!( + " RoT: slot B SHA3-256: {}", + rot.rot_slot_b_sha3_256 + .clone() + .unwrap_or_else(|| String::from("-")) + ); + } else { + println!(" RoT: no information found"); + } + + // XXX-dap cabooses + } + + // XXX-dap switches + // XXX-dap PSCs + + Ok(()) +} diff --git a/nexus/db-model/src/inventory.rs b/nexus/db-model/src/inventory.rs index aaff5b9d63..1fc6a99a40 100644 --- a/nexus/db-model/src/inventory.rs +++ b/nexus/db-model/src/inventory.rs @@ -4,7 +4,8 @@ use crate::impl_enum_type; use crate::schema::{ - hw_baseboard_id, inv_collection, inv_collection_error, sw_caboose, + hw_baseboard_id, inv_caboose, inv_collection, inv_collection_error, + inv_root_of_trust, inv_service_processor, sw_caboose, }; use chrono::DateTime; use chrono::Utc; @@ -127,7 +128,7 @@ impl From for SpType { pub struct InvCollection { pub id: Uuid, pub time_started: DateTime, - pub time_done: DateTime, + pub time_done: Option>, pub collector: String, pub comment: String, } @@ -137,7 +138,7 @@ impl<'a> From<&'a Collection> for InvCollection { InvCollection { id: c.id, time_started: c.time_started, - time_done: c.time_done, + time_done: Some(c.time_done), collector: c.collector.clone(), comment: c.comment.clone(), } @@ -162,7 +163,17 @@ impl<'a> From<&'a BaseboardId> for HwBaseboardId { } } -#[derive(Queryable, Insertable, Clone, Debug, Selectable)] +#[derive( + Queryable, + Insertable, + Clone, + Debug, + Selectable, + Eq, + PartialEq, + Ord, + PartialOrd, +)] #[diesel(table_name = sw_caboose)] pub struct SwCaboose { pub id: Uuid, @@ -197,3 +208,50 @@ impl InvCollectionError { InvCollectionError { inv_collection_id, idx, message } } } + +#[derive(Queryable, Clone, Debug, Selectable)] +#[diesel(table_name = inv_service_processor)] +pub struct InvServiceProcessor { + pub inv_collection_id: Uuid, + pub hw_baseboard_id: Uuid, + pub time_collected: DateTime, + pub source: String, + + pub sp_type: SpType, + // XXX-dap newtype all around + pub sp_slot: i32, + + // XXX-dap newtype all around + // XXX-dap numeric types? + pub baseboard_revision: i64, + pub hubris_archive_id: String, + pub power_state: HwPowerState, +} + +#[derive(Queryable, Clone, Debug, Selectable)] +#[diesel(table_name = inv_root_of_trust)] +pub struct InvRootOfTrust { + pub inv_collection_id: Uuid, + pub hw_baseboard_id: Uuid, + pub time_collected: DateTime, + pub source: String, + + pub rot_slot_active: HwRotSlot, + pub rot_slot_boot_pref_transient: Option, + pub rot_slot_boot_pref_persistent: HwRotSlot, + pub rot_slot_boot_pref_persistent_pending: Option, + pub rot_slot_a_sha3_256: Option, + pub rot_slot_b_sha3_256: Option, +} + +#[derive(Queryable, Clone, Debug, Selectable)] +#[diesel(table_name = inv_caboose)] +pub struct InvCaboose { + pub inv_collection_id: Uuid, + pub hw_baseboard_id: Uuid, + pub time_collected: DateTime, + pub source: String, + + pub which: CabooseWhich, + pub sw_caboose_id: Uuid, +} diff --git a/nexus/db-model/src/schema.rs b/nexus/db-model/src/schema.rs index b14b7ca8c8..2d8c44645a 100644 --- a/nexus/db-model/src/schema.rs +++ b/nexus/db-model/src/schema.rs @@ -1225,6 +1225,8 @@ joinable!(system_update_component_update -> component_update (component_update_i allow_tables_to_appear_in_same_query!(ip_pool_range, ip_pool); joinable!(ip_pool_range -> ip_pool (ip_pool_id)); +allow_tables_to_appear_in_same_query!(sw_caboose, inv_caboose); + allow_tables_to_appear_in_same_query!( dataset, disk, diff --git a/nexus/db-queries/src/db/datastore/mod.rs b/nexus/db-queries/src/db/datastore/mod.rs index 9207c3b7a7..49d5e487b7 100644 --- a/nexus/db-queries/src/db/datastore/mod.rs +++ b/nexus/db-queries/src/db/datastore/mod.rs @@ -134,6 +134,9 @@ impl RunnableQuery for T where { } +pub type DataStoreConnection<'a> = + bb8::PooledConnection<'a, ConnectionManager>; + pub struct DataStore { pool: Arc, virtual_provisioning_collection_producer: crate::provisioning::Producer, @@ -205,8 +208,7 @@ impl DataStore { pub(super) async fn pool_connection_authorized( &self, opctx: &OpContext, - ) -> Result>, Error> - { + ) -> Result { opctx.authorize(authz::Action::Query, &authz::DATABASE).await?; let pool = self.pool.pool(); let connection = pool.get().await.map_err(|err| { @@ -222,8 +224,7 @@ impl DataStore { /// "pool_connection_authorized". pub(super) async fn pool_connection_unauthorized( &self, - ) -> Result>, Error> - { + ) -> Result { let connection = self.pool.pool().get().await.map_err(|err| { Error::unavail(&format!("Failed to access DB connection: {err}")) })?; @@ -234,8 +235,7 @@ impl DataStore { #[doc(hidden)] pub async fn pool_connection_for_tests( &self, - ) -> Result>, Error> - { + ) -> Result { self.pool_connection_unauthorized().await } diff --git a/nexus/src/app/background/common.rs b/nexus/src/app/background/common.rs index 3fcf0483a5..7b05eab61b 100644 --- a/nexus/src/app/background/common.rs +++ b/nexus/src/app/background/common.rs @@ -177,7 +177,7 @@ pub struct Driver { /// /// This is returned by [`Driver::register()`] to identify the corresponding /// background task. It's then accepted by functions like -/// [`Driver::activate()`] and [`Driver::status()`] to identify the task. +/// [`Driver::activate()`] and [`Driver::task_status()`] to identify the task. #[derive(Clone, Debug, Ord, PartialOrd, PartialEq, Eq)] pub struct TaskHandle(String); @@ -277,8 +277,8 @@ impl Driver { /// Enumerate all registered background tasks /// /// This is aimed at callers that want to get the status of all background - /// tasks. You'd call [`Driver::status()`] with each of the items produced - /// by the iterator. + /// tasks. You'd call [`Driver::task_status()`] with each of the items + /// produced by the iterator. pub fn tasks(&self) -> impl Iterator { self.tasks.keys() } diff --git a/nexus/src/app/background/init.rs b/nexus/src/app/background/init.rs index 9881cbaeb3..1ea0fdce9c 100644 --- a/nexus/src/app/background/init.rs +++ b/nexus/src/app/background/init.rs @@ -47,9 +47,6 @@ pub struct BackgroundTasks { /// task handle for the task that collects inventory pub task_inventory_collection: common::TaskHandle, - pub inventory: tokio::sync::watch::Receiver< - Option, - >, } impl BackgroundTasks { @@ -80,8 +77,9 @@ impl BackgroundTasks { // Background task: External endpoints list watcher let (task_external_endpoints, external_endpoints) = { - let watcher = - external_endpoints::ExternalEndpointsWatcher::new(datastore); + let watcher = external_endpoints::ExternalEndpointsWatcher::new( + datastore.clone(), + ); let watcher_channel = watcher.watcher(); let task = driver.register( String::from("external_endpoints"), @@ -99,12 +97,12 @@ impl BackgroundTasks { }; // Background task: inventory collector - let (task_inventory_collection, inventory) = { + let task_inventory_collection = { let watcher = inventory_collection::InventoryCollector::new( + datastore, resolver, &nexus_id.to_string(), ); - let watcher_channel = watcher.watcher(); let task = driver.register( String::from("inventory_collection"), String::from( @@ -117,7 +115,7 @@ impl BackgroundTasks { vec![], ); - (task, watcher_channel) + task }; BackgroundTasks { @@ -129,7 +127,6 @@ impl BackgroundTasks { task_external_endpoints, external_endpoints, task_inventory_collection, - inventory, } } diff --git a/nexus/src/app/background/inventory_collection.rs b/nexus/src/app/background/inventory_collection.rs index b72dafef37..f542cac658 100644 --- a/nexus/src/app/background/inventory_collection.rs +++ b/nexus/src/app/background/inventory_collection.rs @@ -10,34 +10,25 @@ use futures::future::BoxFuture; use futures::FutureExt; use internal_dns::ServiceName; use nexus_db_queries::context::OpContext; +use nexus_db_queries::db::DataStore; use nexus_types::inventory::Collection; use serde_json::json; use std::sync::Arc; -use tokio::sync::watch; /// Background task that reads inventory for the rack pub struct InventoryCollector { + datastore: Arc, resolver: internal_dns::resolver::Resolver, creator: String, - tx: watch::Sender>, - rx: watch::Receiver>, } impl InventoryCollector { pub fn new( + datastore: Arc, resolver: internal_dns::resolver::Resolver, creator: &str, ) -> InventoryCollector { - let (tx, rx) = watch::channel(None); - InventoryCollector { resolver, creator: creator.to_owned(), tx, rx } - } - - /// Exposes the latest inventory collection - /// - /// You can use the returned [`watch::Receiver`] to look at the latest - /// configuration or to be notified when it changes. - pub fn watcher(&self) -> watch::Receiver> { - self.rx.clone() + InventoryCollector { datastore, resolver, creator: creator.to_owned() } } } @@ -51,9 +42,15 @@ impl BackgroundTask for InventoryCollector { 'b: 'c, { async { - match do_collect(&self.resolver, &self.creator, &opctx.log) - .await - .context("failed to collect inventory") + match do_collect( + opctx, + &self.datastore, + &self.resolver, + &self.creator, + &opctx.log, + ) + .await + .context("failed to collect inventory") { Err(error) => { let message = format!("{:#}", error); @@ -66,13 +63,11 @@ impl BackgroundTask for InventoryCollector { "collection_id" => collection.id.to_string(), "time_started" => collection.time_started.to_string(), ); - let result = json!({ + json!({ "collection_id": collection.id.to_string(), "time_started": collection.time_started.to_string(), "time_done": collection.time_done.to_string() - }); - self.tx.send_replace(Some(collection)); - result + }) } } } @@ -81,6 +76,8 @@ impl BackgroundTask for InventoryCollector { } async fn do_collect( + opctx: &OpContext, + datastore: &DataStore, resolver: &internal_dns::resolver::Resolver, creator: &str, log: &slog::Logger, @@ -104,5 +101,14 @@ async fn do_collect( "activation", // TODO-dap useless &mgs_clients, ); - inventory.enumerate().await.context("collecting inventory") + let collection = + inventory.enumerate().await.context("collecting inventory")?; + + // Write it to the database. + datastore + .inventory_insert_collection(opctx, &collection) + .await + .context("saving inventory to database")?; + + Ok(collection) } diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 6956137d31..3c0bf35aa4 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -2592,10 +2592,12 @@ CREATE TABLE IF NOT EXISTS inv_collection ( collector TEXT NOT NULL, comment TEXT NOT NULL ); --- Supports: finding latest collection to use, finding oldest collection to --- clean up -CREATE INDEX IF NOT EXISTS inv_collection_by_time +-- Supports finding latest collection to use +CREATE INDEX IF NOT EXISTS inv_collection_by_time_done ON omicron.public.inv_collection (time_done) WHERE time_done IS NOT NULL; +-- Supports finding the oldest collections (to clean up) +CREATE INDEX IF NOT EXISTS inv_collection_by_time_started + ON omicron.public.inv_collection (time_started); -- list of errors generated during a collection CREATE TABLE IF NOT EXISTS omicron.public.inv_collection_error (