diff --git a/Cargo.lock b/Cargo.lock index 3a45dcb381..c4385bf694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5127,6 +5127,7 @@ dependencies = [ "expectorate", "humantime", "internal-dns 0.1.0", + "ipnetwork", "nexus-client 0.1.0", "nexus-db-model", "nexus-db-queries", diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index 5b2adde1b2..5a05e93db9 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -33,6 +33,7 @@ textwrap.workspace = true tokio = { workspace = true, features = [ "full" ] } uuid.workspace = true omicron-workspace-hack = { version = "0.1", path = "../../workspace-hack" } +ipnetwork.workspace = true [dev-dependencies] expectorate.workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/db.rs b/dev-tools/omdb/src/bin/omdb/db.rs index 93e5ef4301..10e5546b6d 100644 --- a/dev-tools/omdb/src/bin/omdb/db.rs +++ b/dev-tools/omdb/src/bin/omdb/db.rs @@ -12,6 +12,9 @@ //! would be the only consumer -- and in that case it's okay to query the //! database directly. +// NOTE: eminates from Tabled macros +#![allow(clippy::useless_vec)] + use crate::Omdb; use anyhow::anyhow; use anyhow::bail; @@ -30,7 +33,9 @@ use nexus_db_model::DnsGroup; use nexus_db_model::DnsName; use nexus_db_model::DnsVersion; use nexus_db_model::DnsZone; +use nexus_db_model::ExternalIp; use nexus_db_model::Instance; +use nexus_db_model::Project; use nexus_db_model::Region; use nexus_db_model::Sled; use nexus_db_model::Zpool; @@ -86,6 +91,8 @@ enum DbCommands { Sleds, /// Print information about customer instances Instances, + /// Print information about the network + Network(NetworkArgs), } #[derive(Debug, Args)] @@ -170,6 +177,22 @@ enum ServicesCommands { ListBySled, } +#[derive(Debug, Args)] +struct NetworkArgs { + #[command(subcommand)] + command: NetworkCommands, + + /// Print out raw data structures from the data store. + #[clap(long)] + verbose: bool, +} + +#[derive(Debug, Subcommand)] +enum NetworkCommands { + /// List external IPs + ListEips, +} + impl DbArgs { /// Run a `omdb db` subcommand. pub(crate) async fn run_cmd( @@ -269,6 +292,13 @@ impl DbArgs { 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 + } } } } @@ -1098,6 +1128,156 @@ async fn cmd_db_dns_names( Ok(()) } +async fn cmd_db_eips( + opctx: &OpContext, + datastore: &DataStore, + limit: NonZeroU32, + verbose: bool, +) -> Result<(), anyhow::Error> { + use db::schema::external_ip::dsl; + let ips: Vec = dsl::external_ip + .filter(dsl::time_deleted.is_null()) + .select(ExternalIp::as_select()) + .get_results_async(&*datastore.pool_connection_for_tests().await?) + .await?; + + check_limit(&ips, limit, || String::from("listing external ips")); + + struct PortRange { + first: u16, + last: u16, + } + + impl Display for PortRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.first, self.last) + } + } + + #[derive(Tabled)] + enum Owner { + Instance { project: String, name: String }, + Service { kind: String }, + None, + } + + impl Display for Owner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Instance { project, name } => { + write!(f, "Instance {project}/{name}") + } + Self::Service { kind } => write!(f, "Service {kind}"), + Self::None => write!(f, "None"), + } + } + } + + #[derive(Tabled)] + struct IpRow { + ip: ipnetwork::IpNetwork, + ports: PortRange, + kind: String, + owner: Owner, + } + + if verbose { + for ip in &ips { + if verbose { + println!("{ip:#?}"); + } + } + return Ok(()); + } + + let mut rows = Vec::new(); + + for ip in &ips { + let owner = if let Some(owner_id) = ip.parent_id { + if ip.is_service { + let service = match LookupPath::new(opctx, datastore) + .service_id(owner_id) + .fetch() + .await + { + Ok(instance) => instance, + Err(e) => { + eprintln!( + "error looking up service with id {owner_id}: {e}" + ); + continue; + } + }; + Owner::Service { kind: format!("{:?}", service.1.kind) } + } else { + use db::schema::instance::dsl as instance_dsl; + let instance = match instance_dsl::instance + .filter(instance_dsl::id.eq(owner_id)) + .limit(1) + .select(Instance::as_select()) + .load_async(&*datastore.pool_connection_for_tests().await?) + .await + .context("loading requested instance")? + .pop() + { + Some(instance) => instance, + None => { + eprintln!("instance with id {owner_id} not found"); + continue; + } + }; + + use db::schema::project::dsl as project_dsl; + let project = match project_dsl::project + .filter(project_dsl::id.eq(instance.project_id)) + .limit(1) + .select(Project::as_select()) + .load_async(&*datastore.pool_connection_for_tests().await?) + .await + .context("loading requested project")? + .pop() + { + Some(instance) => instance, + None => { + eprintln!( + "project with id {} not found", + instance.project_id + ); + continue; + } + }; + + Owner::Instance { + project: project.name().to_string(), + name: instance.name().to_string(), + } + } + } else { + Owner::None + }; + + let row = IpRow { + ip: ip.ip, + ports: PortRange { + first: ip.first_port.into(), + last: ip.last_port.into(), + }, + kind: format!("{:?}", ip.kind), + owner, + }; + rows.push(row); + } + + rows.sort_by(|a, b| a.ip.cmp(&b.ip)); + let table = tabled::Table::new(rows) + .with(tabled::settings::Style::empty()) + .to_string(); + + println!("{}", table); + + Ok(()) +} + fn print_name( prefix: &str, name: &str, diff --git a/dev-tools/omdb/tests/test_all_output.rs b/dev-tools/omdb/tests/test_all_output.rs index 0eddcb492c..d757369ead 100644 --- a/dev-tools/omdb/tests/test_all_output.rs +++ b/dev-tools/omdb/tests/test_all_output.rs @@ -41,6 +41,7 @@ async fn test_omdb_usage_errors() { &["db", "dns", "diff"], &["db", "dns", "names"], &["db", "services"], + &["db", "network"], &["nexus"], &["nexus", "background-tasks"], &["sled-agent"], diff --git a/dev-tools/omdb/tests/usage_errors.out b/dev-tools/omdb/tests/usage_errors.out index 136a631e80..b5421b76af 100644 --- a/dev-tools/omdb/tests/usage_errors.out +++ b/dev-tools/omdb/tests/usage_errors.out @@ -91,6 +91,7 @@ Commands: services Print information about control plane services sleds Print information about sleds instances Print information about customer instances + network Print information about the network help Print this message or the help of the given subcommand(s) Options: @@ -112,6 +113,7 @@ Commands: services Print information about control plane services sleds Print information about sleds instances Print information about customer instances + network Print information about the network help Print this message or the help of the given subcommand(s) Options: @@ -186,6 +188,24 @@ Commands: Options: -h, --help Print help ============================================= +EXECUTING COMMAND: omdb ["db", "network"] +termination: Exited(2) +--------------------------------------------- +stdout: +--------------------------------------------- +stderr: +Print information about the network + +Usage: omdb db network [OPTIONS] + +Commands: + list-eips List external IPs + help Print this message or the help of the given subcommand(s) + +Options: + --verbose Print out raw data structures from the data store + -h, --help Print help +============================================= EXECUTING COMMAND: omdb ["nexus"] termination: Exited(2) ---------------------------------------------