diff --git a/Cargo.lock b/Cargo.lock index 8d9f54be73..52b9d68097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5007,6 +5007,7 @@ dependencies = [ "nexus-client 0.1.0", "nexus-db-model", "nexus-db-queries", + "nexus-types", "omicron-common 0.1.0", "omicron-rpaths", "pq-sys", diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 488ee7c268..1d7e6884d1 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -652,6 +652,12 @@ impl From<&Generation> for i64 { } } +impl From for Generation { + fn from(value: u32) -> Self { + Generation(u64::from(value)) + } +} + impl TryFrom for Generation { type Error = anyhow::Error; diff --git a/nexus/db-queries/src/db/datastore/dns.rs b/nexus/db-queries/src/db/datastore/dns.rs index 5cdc031a1b..79eb517c47 100644 --- a/nexus/db-queries/src/db/datastore/dns.rs +++ b/nexus/db-queries/src/db/datastore/dns.rs @@ -149,7 +149,7 @@ impl DataStore { /// List all DNS names in the given DNS zone at the given group version /// (paginated) - async fn dns_names_list( + pub async fn dns_names_list( &self, opctx: &OpContext, dns_zone_id: Uuid, diff --git a/nexus/src/app/background/common.rs b/nexus/src/app/background/common.rs index bcb05b0bec..7fc9f0327a 100644 --- a/nexus/src/app/background/common.rs +++ b/nexus/src/app/background/common.rs @@ -283,6 +283,8 @@ impl Driver { self.tasks.keys() } + // XXX-dap helper function + /// Returns a summary of what this task does (for developers) pub fn task_description(&self, task: &TaskHandle) -> &str { // It should be hard to hit this in practice, since you'd have to have diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index 2165f25a5e..b1ee6bc452 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -161,7 +161,7 @@ impl super::Nexus { .collect(); let mut dns_update = DnsVersionUpdateBuilder::new( DnsGroup::External, - format!("create silo: {:?}", silo_name), + format!("create silo: {:?}", silo_name.as_str()), self.id.to_string(), ); dns_update.add_name(silo_dns_name(silo_name), dns_records)?; diff --git a/nexus/src/app/silo.rs b/nexus/src/app/silo.rs index b07764b299..fd0414aad3 100644 --- a/nexus/src/app/silo.rs +++ b/nexus/src/app/silo.rs @@ -92,7 +92,7 @@ impl super::Nexus { let silo_name = &new_silo_params.identity.name; let mut dns_update = DnsVersionUpdateBuilder::new( DnsGroup::External, - format!("create silo: {:?}", silo_name), + format!("create silo: {:?}", silo_name.as_str()), self.id.to_string(), ); dns_update.add_name(silo_dns_name(silo_name), dns_records)?; diff --git a/omdb/Cargo.toml b/omdb/Cargo.toml index 0b00bb603d..211a82ec15 100644 --- a/omdb/Cargo.toml +++ b/omdb/Cargo.toml @@ -16,6 +16,7 @@ humantime.workspace = true nexus-client.workspace = true nexus-db-model.workspace = true nexus-db-queries.workspace = true +nexus-types.workspace = true omicron-common.workspace = true # See omicron-rpaths for more about the "pq-sys" dependency. pq-sys = "*" diff --git a/omdb/src/bin/omdb/db.rs b/omdb/src/bin/omdb/db.rs index 6f3b76a275..797df89c93 100644 --- a/omdb/src/bin/omdb/db.rs +++ b/omdb/src/bin/omdb/db.rs @@ -6,17 +6,25 @@ use anyhow::anyhow; use anyhow::Context; +use chrono::SecondsFormat; use clap::Args; use clap::Subcommand; +use clap::ValueEnum; +use nexus_db_model::DnsGroup; use nexus_db_model::Sled; use nexus_db_queries::context::OpContext; use nexus_db_queries::db; use nexus_db_queries::db::identity::Asset; use nexus_db_queries::db::model::ServiceKind; use nexus_db_queries::db::DataStore; +use nexus_types::internal_api::params::DnsRecord; +use nexus_types::internal_api::params::Srv; use omicron_common::api::external::DataPageParams; +use omicron_common::api::external::Generation; use omicron_common::postgres_config::PostgresConfigWithUrl; +use std::cmp::Ordering; use std::collections::BTreeMap; +use std::fmt::Display; use std::num::NonZeroU32; use std::sync::Arc; use strum::IntoEnumIterator; @@ -42,12 +50,54 @@ pub struct DbArgs { /// Subcommands that query or update the database #[derive(Debug, Subcommand)] enum DbCommands { + /// Print information about internal and external DNS + Dns(DnsArgs), /// Print information about control plane services Services(ServicesArgs), /// Print information about sleds Sleds, } +#[derive(Debug, Args)] +struct DnsArgs { + #[command(subcommand)] + command: DnsCommands, +} + +#[derive(Debug, Subcommand)] +enum DnsCommands { + /// Summarize current version of all DNS zones + Show, + /// Show what changed in a given DNS version + Diff(DnsVersionArgs), + /// Show the full contents of a given DNS zone and version + Names(DnsVersionArgs), +} + +#[derive(Debug, Args)] +struct DnsVersionArgs { + /// name of a DNS group + #[arg(value_enum)] + group: CliDnsGroup, + /// version of the group's data + version: u32, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +enum CliDnsGroup { + Internal, + External, +} + +impl CliDnsGroup { + fn dns_group(&self) -> DnsGroup { + match self { + CliDnsGroup::Internal => DnsGroup::Internal, + CliDnsGroup::External => DnsGroup::External, + } + } +} + #[derive(Debug, Args)] struct ServicesArgs { #[command(subcommand)] @@ -83,6 +133,17 @@ impl DbArgs { let opctx = OpContext::for_tests(log.clone(), datastore.clone()); match &self.command { + DbCommands::Dns(DnsArgs { command: DnsCommands::Show }) => { + cmd_db_dns_show(&opctx, &datastore, self.fetch_limit).await + } + DbCommands::Dns(DnsArgs { command: DnsCommands::Diff(args) }) => { + cmd_db_dns_diff(&opctx, &datastore, self.fetch_limit, args) + .await + } + DbCommands::Dns(DnsArgs { command: DnsCommands::Names(args) }) => { + cmd_db_dns_names(&opctx, &datastore, self.fetch_limit, args) + .await + } DbCommands::Services(ServicesArgs { command: ServicesCommands::ListInstances, }) => { @@ -169,6 +230,18 @@ where } } +/// Returns pagination parameters to fetch the first page of results for a +/// paginated endpoint +fn first_page<'a, T>(limit: NonZeroU32) -> DataPageParams<'a, T> { + DataPageParams { + marker: None, + direction: dropshot::PaginationOrder::Ascending, + limit, + } +} + +// SERVICES + #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct ServiceInstanceRow { @@ -185,13 +258,8 @@ async fn cmd_db_services_list_instances( datastore: &DataStore, limit: NonZeroU32, ) -> Result<(), anyhow::Error> { - let pagparams: DataPageParams<'_, Uuid> = DataPageParams { - marker: None, - direction: dropshot::PaginationOrder::Ascending, - limit, - }; let sled_list = datastore - .sled_list(&opctx, &pagparams) + .sled_list(&opctx, &first_page(limit)) .await .context("listing sleds")?; check_limit(&sled_list, limit, || String::from("listing sleds")); @@ -202,16 +270,10 @@ async fn cmd_db_services_list_instances( let mut rows = vec![]; for service_kind in ServiceKind::iter() { - let pagparams: DataPageParams<'_, Uuid> = DataPageParams { - marker: None, - direction: dropshot::PaginationOrder::Ascending, - limit, - }; - let context = || format!("listing instances of kind {:?}", service_kind); let instances = datastore - .services_list_kind(&opctx, service_kind, &pagparams) + .services_list_kind(&opctx, service_kind, &first_page(limit)) .await .with_context(&context)?; check_limit(&instances, limit, &context); @@ -244,6 +306,8 @@ async fn cmd_db_services_list_instances( Ok(()) } +// SLEDS + #[derive(Tabled)] #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct ServiceInstanceSledRow { @@ -259,13 +323,8 @@ async fn cmd_db_services_list_by_sled( datastore: &DataStore, limit: NonZeroU32, ) -> Result<(), anyhow::Error> { - let pagparams: DataPageParams<'_, Uuid> = DataPageParams { - marker: None, - direction: dropshot::PaginationOrder::Ascending, - limit, - }; let sled_list = datastore - .sled_list(&opctx, &pagparams) + .sled_list(&opctx, &first_page(limit)) .await .context("listing sleds")?; check_limit(&sled_list, limit, || String::from("listing sleds")); @@ -276,16 +335,10 @@ async fn cmd_db_services_list_by_sled( BTreeMap::new(); for service_kind in ServiceKind::iter() { - let pagparams: DataPageParams<'_, Uuid> = DataPageParams { - marker: None, - direction: dropshot::PaginationOrder::Ascending, - limit, - }; - let context = || format!("listing instances of kind {:?}", service_kind); let instances = datastore - .services_list_kind(&opctx, service_kind, &pagparams) + .services_list_kind(&opctx, service_kind, &first_page(limit)) .await .with_context(&context)?; check_limit(&instances, limit, &context); @@ -346,14 +399,8 @@ async fn cmd_db_sleds( datastore: &DataStore, limit: NonZeroU32, ) -> Result<(), anyhow::Error> { - let pagparams: DataPageParams<'_, Uuid> = DataPageParams { - marker: None, - direction: dropshot::PaginationOrder::Ascending, - limit, - }; - let sleds = datastore - .sled_list(&opctx, &pagparams) + .sled_list(&opctx, &first_page(limit)) .await .context("listing sleds")?; check_limit(&sleds, limit, || String::from("listing sleds")); @@ -368,3 +415,151 @@ async fn cmd_db_sleds( Ok(()) } + +// DNS +// XXX-dap add "history" command? + +/// Run `omdb db dns show`. +async fn cmd_db_dns_show( + opctx: &OpContext, + datastore: &DataStore, + limit: NonZeroU32, +) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct ZoneRow { + group: String, + zone: String, + #[tabled(rename = "ver")] + version: String, + updated: String, + reason: String, + } + + let mut rows = Vec::with_capacity(2); + for group in [DnsGroup::Internal, DnsGroup::External] { + let ctx = || format!("listing DNS zones for DNS group {:?}", group); + let group_zones = datastore + .dns_zones_list(opctx, group, &first_page(limit)) + .await + .with_context(ctx)?; + check_limit(&group_zones, limit, ctx); + + let version = datastore + .dns_group_latest_version(opctx, group) + .await + .with_context(|| { + format!("fetching latest version for DNS group {:?}", group) + })?; + + rows.extend(group_zones.into_iter().map(|zone| ZoneRow { + group: group.to_string(), + zone: zone.zone_name, + version: version.version.0.to_string(), + updated: + version.time_created.to_rfc3339_opts(SecondsFormat::Secs, true), + reason: version.comment.clone(), + })); + } + + 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(()) +} + +/// Run `omdb db dns diff`. +async fn cmd_db_dns_diff( + _opctx: &OpContext, + _datastore: &DataStore, + _limit: NonZeroU32, + _args: &DnsVersionArgs, +) -> Result<(), anyhow::Error> { + // XXX-dap + todo!(); +} + +/// Run `omdb db dns names`. +async fn cmd_db_dns_names( + opctx: &OpContext, + datastore: &DataStore, + limit: NonZeroU32, + args: &DnsVersionArgs, +) -> Result<(), anyhow::Error> { + // The caller gave us a DNS group. First we need to find the zones. + let group = args.group.dns_group(); + let ctx = || format!("listing DNS zones for DNS group {:?}", group); + let group_zones = datastore + .dns_zones_list(opctx, group, &first_page(limit)) + .await + .with_context(ctx)?; + check_limit(&group_zones, limit, ctx); + + if group_zones.is_empty() { + println!("no DNS zones found for group {:?}", group.to_string()); + return Ok(()); + } + + // There will almost never be more than one zone. But just in case, we'll + // iterate over whatever we find and print all the names in each one. + let version = Generation::try_from(i64::from(args.version)).unwrap(); + for zone in group_zones { + println!("{} zone: {}", group, zone.zone_name); + println!(" {:50} {}", "NAME", "RECORDS"); + let ctx = || format!("listing names for zone {:?}", zone.zone_name); + let mut names = datastore + .dns_names_list(opctx, zone.id, version.into(), &first_page(limit)) + .await + .with_context(ctx)?; + check_limit(&names, limit, ctx); + names.sort_by(|(n1, _), (n2, _)| { + // A natural sort by name puts records starting with numbers first + // (which will be some of the uuids), then underscores (the SRV + // names), and then the letters (the rest of the uuids). This is + // ugly. Put the SRV records last (based on the underscore). (We + // could look at the record type instead, but that's just as cheesy: + // names can in principle have multiple different kinds of records, + // and we'd still want records of the same type to be sorted by + // name.) + match (n1.chars().next(), n2.chars().next()) { + (Some('_'), Some(c)) if c != '_' => Ordering::Greater, + (Some(c), Some('_')) if c != '_' => Ordering::Less, + _ => n1.cmp(n2), + } + }); + for (name, records) in names { + if records.len() == 1 { + match &records[0] { + DnsRecord::Srv(_) => (), + DnsRecord::Aaaa(_) | DnsRecord::A(_) => { + println!( + " {:50} {}", + name, + format_record(&records[0]) + ); + continue; + } + } + } + + println!(" {:50} (records: {})", name, records.len()); + for r in &records { + println!(" {}", format_record(r)); + } + } + } + + Ok(()) +} + +fn format_record(record: &DnsRecord) -> impl Display { + match record { + DnsRecord::A(addr) => format!("A {}", addr), + DnsRecord::Aaaa(addr) => format!("AAAA {}", addr), + DnsRecord::Srv(Srv { port, target, .. }) => { + format!("SRV port {:5} {}", port, target) + } + } +} diff --git a/omdb/src/bin/omdb/nexus.rs b/omdb/src/bin/omdb/nexus.rs index 5b509ad2e5..53c936cb95 100644 --- a/omdb/src/bin/omdb/nexus.rs +++ b/omdb/src/bin/omdb/nexus.rs @@ -48,7 +48,7 @@ enum BackgroundTaskCommands { /// Print a summary of the status of all background tasks List, /// Print human-readable summary of the status of each background task - Details, + Show, } impl NexusArgs { @@ -68,8 +68,8 @@ impl NexusArgs { command: BackgroundTaskCommands::List, }) => cmd_nexus_background_task_list(&client).await, NexusCommands::BackgroundTask(BackgroundTaskArgs { - command: BackgroundTaskCommands::Details, - }) => cmd_nexus_background_task_details(&client).await, + command: BackgroundTaskCommands::Show, + }) => cmd_nexus_background_task_show(&client).await, } } } @@ -115,8 +115,8 @@ async fn cmd_nexus_background_task_list( Ok(()) } -/// Runs `omdb nexus background-task details` -async fn cmd_nexus_background_task_details( +/// Runs `omdb nexus background-task show` +async fn cmd_nexus_background_task_show( client: &nexus_client::Client, ) -> Result<(), anyhow::Error> { let response = @@ -126,12 +126,12 @@ async fn cmd_nexus_background_task_details( // We want to pick the order that we print some tasks intentionally. Then // we want to print anything else that we find. for name in [ - "dns_config_external", - "dns_servers_external", - "dns_propagation_external", "dns_config_internal", "dns_servers_internal", "dns_propagation_internal", + "dns_config_external", + "dns_servers_external", + "dns_propagation_external", ] { if let Some(bgtask) = tasks.remove(name) { print_task(&bgtask); @@ -265,22 +265,27 @@ fn print_task_details(bgtask: &BackgroundTask, details: &serde_json::Value) { found_dns_servers.addresses.len(), ); - #[derive(Tabled)] - #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] - struct ServerRow<'a> { - dns_server_addr: &'a str, + if !found_dns_servers.addresses.is_empty() { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct ServerRow<'a> { + dns_server_addr: &'a str, + } + + let mut addrs = found_dns_servers.addresses; + addrs.sort(); + let rows = addrs + .iter() + .map(|dns_server_addr| ServerRow { dns_server_addr }); + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + println!( + "{}", + textwrap::indent(&table.to_string(), " ") + ); } - - let mut addrs = found_dns_servers.addresses; - addrs.sort(); - let rows = addrs - .iter() - .map(|dns_server_addr| ServerRow { dns_server_addr }); - let table = tabled::Table::new(rows) - .with(tabled::settings::Style::empty()) - .with(tabled::settings::Padding::new(0, 1, 0, 0)) - .to_string(); - println!("{}", textwrap::indent(&table.to_string(), " ")); } } } else if name == "dns_propagation_internal" @@ -299,7 +304,7 @@ fn print_task_details(bgtask: &BackgroundTask, details: &serde_json::Value) { #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] struct DnsPropRow<'a> { dns_server_addr: &'a str, - last_result: String, + last_result: &'static str, } match serde_json::from_value::(details.clone()) { @@ -319,8 +324,8 @@ fn print_task_details(bgtask: &BackgroundTask, details: &serde_json::Value) { DnsPropRow { dns_server_addr: addr, last_result: match result { - Ok(_) => "success".to_string(), - Err(_) => format!("error (see below)"), + Ok(_) => "success", + Err(_) => "error (see below)", }, } }); @@ -423,13 +428,12 @@ fn print_task_details(bgtask: &BackgroundTask, details: &serde_json::Value) { let tls_cert_rows: Vec = details .by_dns_name .iter() - .map(|(dns_name, endpoint)| { + .flat_map(|(dns_name, endpoint)| { endpoint .tls_certs .iter() .map(|digest| TlsCertRow { dns_name, digest }) }) - .flatten() .collect(); println!(" warnings: {}", details.warnings.len());