diff --git a/Cargo.lock b/Cargo.lock index 27a165c307..aad3a8782a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4951,6 +4951,8 @@ dependencies = [ "dropshot", "expectorate", "futures", + "gateway-messages", + "gateway-test-utils", "libc", "nexus-test-interface", "nexus-test-utils", @@ -5140,6 +5142,9 @@ dependencies = [ "dropshot", "expectorate", "futures", + "gateway-client", + "gateway-messages", + "gateway-test-utils", "humantime", "internal-dns 0.1.0", "ipnetwork", diff --git a/clients/gateway-client/src/lib.rs b/clients/gateway-client/src/lib.rs index 800254b197..b071d34975 100644 --- a/clients/gateway-client/src/lib.rs +++ b/clients/gateway-client/src/lib.rs @@ -48,7 +48,7 @@ progenitor::generate_api!( }), derives = [schemars::JsonSchema], patch = { - SpIdentifier = { derives = [Copy, PartialEq, Hash, Eq, PartialOrd, Ord, Serialize, Deserialize] }, + SpIdentifier = { derives = [Copy, PartialEq, Hash, Eq, Serialize, Deserialize] }, SpIgnition = { derives = [PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize] }, SpIgnitionSystemType = { derives = [Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize] }, SpState = { derives = [ PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize] }, @@ -59,3 +59,17 @@ progenitor::generate_api!( HostPhase2RecoveryImageId = { derives = [ PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize] }, }, ); + +// Override the impl of Ord for SpIdentifier because the default one orders the +// fields in a different order than people are likely to want. +impl Ord for crate::types::SpIdentifier { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.type_.cmp(&other.type_).then(self.slot.cmp(&other.slot)) + } +} + +impl PartialOrd for crate::types::SpIdentifier { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index cd4af6e947..ff3c650d6d 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -14,9 +14,12 @@ chrono.workspace = true clap.workspace = true diesel.workspace = true dropshot.workspace = true +futures.workspace = true +gateway-client.workspace = true +gateway-messages.workspace = true +gateway-test-utils.workspace = true humantime.workspace = true internal-dns.workspace = true -futures.workspace = true nexus-client.workspace = true nexus-db-model.workspace = true nexus-db-queries.workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/main.rs b/dev-tools/omdb/src/bin/omdb/main.rs index d1a56e1d80..32141d2809 100644 --- a/dev-tools/omdb/src/bin/omdb/main.rs +++ b/dev-tools/omdb/src/bin/omdb/main.rs @@ -41,6 +41,7 @@ use std::net::SocketAddr; use std::net::SocketAddrV6; mod db; +mod mgs; mod nexus; mod oximeter; mod sled_agent; @@ -57,6 +58,7 @@ async fn main() -> Result<(), anyhow::Error> { match &args.command { OmdbCommands::Db(db) => db.run_cmd(&args, &log).await, + OmdbCommands::Mgs(mgs) => mgs.run_cmd(&args, &log).await, OmdbCommands::Nexus(nexus) => nexus.run_cmd(&args, &log).await, OmdbCommands::Oximeter(oximeter) => oximeter.run_cmd(&log).await, OmdbCommands::SledAgent(sled) => sled.run_cmd(&args, &log).await, @@ -155,6 +157,8 @@ impl Omdb { enum OmdbCommands { /// Query the control plane database (CockroachDB) Db(db::DbArgs), + /// Debug a specific Management Gateway Service instance + Mgs(mgs::MgsArgs), /// Debug a specific Nexus instance Nexus(nexus::NexusArgs), /// Query oximeter collector state diff --git a/dev-tools/omdb/src/bin/omdb/mgs.rs b/dev-tools/omdb/src/bin/omdb/mgs.rs new file mode 100644 index 0000000000..d2938418e1 --- /dev/null +++ b/dev-tools/omdb/src/bin/omdb/mgs.rs @@ -0,0 +1,488 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Prototype code for collecting information from systems in the rack + +use crate::Omdb; +use anyhow::Context; +use clap::Args; +use clap::Subcommand; +use futures::StreamExt; +use gateway_client::types::PowerState; +use gateway_client::types::RotSlot; +use gateway_client::types::RotState; +use gateway_client::types::SpComponentCaboose; +use gateway_client::types::SpComponentInfo; +use gateway_client::types::SpIdentifier; +use gateway_client::types::SpIgnition; +use gateway_client::types::SpIgnitionInfo; +use gateway_client::types::SpIgnitionSystemType; +use gateway_client::types::SpState; +use gateway_client::types::SpType; +use tabled::Tabled; + +/// Arguments to the "omdb mgs" subcommand +#[derive(Debug, Args)] +pub struct MgsArgs { + /// URL of an MGS instance to query + #[clap(long, env("OMDB_MGS_URL"))] + mgs_url: Option, + + #[command(subcommand)] + command: MgsCommands, +} + +#[derive(Debug, Subcommand)] +enum MgsCommands { + /// Show information about devices and components visible to MGS + Inventory(InventoryArgs), +} + +#[derive(Debug, Args)] +struct InventoryArgs {} + +impl MgsArgs { + pub(crate) async fn run_cmd( + &self, + omdb: &Omdb, + log: &slog::Logger, + ) -> Result<(), anyhow::Error> { + let mgs_url = match &self.mgs_url { + Some(cli_or_env_url) => cli_or_env_url.clone(), + None => { + eprintln!( + "note: MGS URL not specified. Will pick one from DNS." + ); + let addrs = omdb + .dns_lookup_all( + log.clone(), + internal_dns::ServiceName::ManagementGatewayService, + ) + .await?; + let addr = addrs.into_iter().next().expect( + "expected at least one MGS address from \ + successful DNS lookup", + ); + format!("http://{}", addr) + } + }; + eprintln!("note: using MGS URL {}", &mgs_url); + let mgs_client = gateway_client::Client::new(&mgs_url, log.clone()); + + match &self.command { + MgsCommands::Inventory(inventory_args) => { + cmd_mgs_inventory(&mgs_client, inventory_args).await + } + } + } +} + +/// Runs `omdb mgs inventory` +/// +/// Shows devices and components that are visible to an MGS instance. +async fn cmd_mgs_inventory( + mgs_client: &gateway_client::Client, + _args: &InventoryArgs, +) -> Result<(), anyhow::Error> { + // Report all the SP identifiers that MGS is configured to talk to. + println!("ALL CONFIGURED SPs\n"); + let mut sp_ids = mgs_client + .sp_all_ids() + .await + .context("listing SP identifiers")? + .into_inner(); + sp_ids.sort(); + show_sp_ids(&sp_ids)?; + println!(""); + + // Report which SPs are visible via Ignition. + println!("SPs FOUND THROUGH IGNITION\n"); + let mut sp_list_ignition = mgs_client + .ignition_list() + .await + .context("listing ignition")? + .into_inner(); + sp_list_ignition.sort_by(|a, b| a.id.cmp(&b.id)); + show_sps_from_ignition(&sp_list_ignition)?; + println!(""); + + // Print basic state about each SP that's visible to ignition. + println!("SERVICE PROCESSOR STATES\n"); + let mgs_client = std::sync::Arc::new(mgs_client); + let c = &mgs_client; + let mut sp_infos = + futures::stream::iter(sp_list_ignition.iter().filter_map(|ignition| { + if matches!(ignition.details, SpIgnition::Yes { .. }) { + Some(ignition.id) + } else { + None + } + })) + .then(|sp_id| async move { + c.sp_get(sp_id.type_, sp_id.slot) + .await + .with_context(|| format!("fetching info about SP {:?}", sp_id)) + .map(|s| (sp_id, s)) + }) + .collect::>>() + .await + .into_iter() + .filter_map(|r| match r { + Ok((sp_id, v)) => Some((sp_id, v.into_inner())), + Err(error) => { + eprintln!("error: {:?}", error); + None + } + }) + .collect::>(); + sp_infos.sort(); + show_sp_states(&sp_infos)?; + println!(""); + + // Print detailed information about each SP that we've found so far. + for (sp_id, sp_state) in &sp_infos { + show_sp_details(&mgs_client, sp_id, sp_state).await?; + } + + Ok(()) +} + +fn sp_type_to_str(s: &SpType) -> &'static str { + match s { + SpType::Sled => "Sled", + SpType::Power => "Power", + SpType::Switch => "Switch", + } +} + +fn show_sp_ids(sp_ids: &[SpIdentifier]) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct SpIdRow { + #[tabled(rename = "TYPE")] + type_: &'static str, + slot: u32, + } + + impl<'a> From<&'a SpIdentifier> for SpIdRow { + fn from(id: &SpIdentifier) -> Self { + SpIdRow { type_: sp_type_to_str(&id.type_), slot: id.slot } + } + } + + let table_rows = sp_ids.iter().map(SpIdRow::from); + let table = tabled::Table::new(table_rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + println!("{}", textwrap::indent(&table.to_string(), " ")); + Ok(()) +} + +fn show_sps_from_ignition( + sp_list_ignition: &[SpIgnitionInfo], +) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct IgnitionRow { + #[tabled(rename = "TYPE")] + type_: &'static str, + slot: u32, + system_type: String, + } + + impl<'a> From<&'a SpIgnitionInfo> for IgnitionRow { + fn from(value: &SpIgnitionInfo) -> Self { + IgnitionRow { + type_: sp_type_to_str(&value.id.type_), + slot: value.id.slot, + system_type: match value.details { + SpIgnition::No => "-".to_string(), + SpIgnition::Yes { + id: SpIgnitionSystemType::Gimlet, + .. + } => "Gimlet".to_string(), + SpIgnition::Yes { + id: SpIgnitionSystemType::Sidecar, + .. + } => "Sidecar".to_string(), + SpIgnition::Yes { + id: SpIgnitionSystemType::Psc, .. + } => "PSC".to_string(), + SpIgnition::Yes { + id: SpIgnitionSystemType::Unknown(v), + .. + } => format!("unknown: type {}", v), + }, + } + } + } + + let table_rows = sp_list_ignition.iter().map(IgnitionRow::from); + let table = tabled::Table::new(table_rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + println!("{}", textwrap::indent(&table.to_string(), " ")); + Ok(()) +} + +fn show_sp_states( + sp_states: &[(SpIdentifier, SpState)], +) -> Result<(), anyhow::Error> { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct SpStateRow<'a> { + #[tabled(rename = "TYPE")] + type_: &'static str, + slot: u32, + model: String, + serial: String, + rev: u32, + hubris: &'a str, + pwr: &'static str, + rot_active: String, + } + + impl<'a> From<&'a (SpIdentifier, SpState)> for SpStateRow<'a> { + fn from((id, v): &'a (SpIdentifier, SpState)) -> Self { + SpStateRow { + type_: sp_type_to_str(&id.type_), + slot: id.slot, + model: v.model.clone(), + serial: v.serial_number.clone(), + rev: v.revision, + hubris: &v.hubris_archive_id, + pwr: match v.power_state { + PowerState::A0 => "A0", + PowerState::A1 => "A1", + PowerState::A2 => "A2", + }, + rot_active: match &v.rot { + RotState::CommunicationFailed { message } => { + format!("error: {}", message) + } + RotState::Enabled { active: RotSlot::A, .. } => { + "slot A".to_string() + } + RotState::Enabled { active: RotSlot::B, .. } => { + "slot B".to_string() + } + }, + } + } + } + + let table_rows = sp_states.iter().map(SpStateRow::from); + let table = tabled::Table::new(table_rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + println!("{}", textwrap::indent(&table.to_string(), " ")); + Ok(()) +} + +const COMPONENTS_WITH_CABOOSES: &'static [&'static str] = &["sp", "rot"]; + +async fn show_sp_details( + mgs_client: &gateway_client::Client, + sp_id: &SpIdentifier, + sp_state: &SpState, +) -> Result<(), anyhow::Error> { + println!( + "SP DETAILS: type {:?} slot {}\n", + sp_type_to_str(&sp_id.type_), + sp_id.slot + ); + + println!(" ROOT OF TRUST\n"); + match &sp_state.rot { + RotState::CommunicationFailed { message } => { + println!(" error: {}", message); + } + RotState::Enabled { + active, + pending_persistent_boot_preference, + persistent_boot_preference, + slot_a_sha3_256_digest, + slot_b_sha3_256_digest, + transient_boot_preference, + } => { + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct Row { + name: &'static str, + value: String, + } + + let rows = vec![ + Row { + name: "active slot", + value: format!("slot {:?}", active), + }, + Row { + name: "persistent boot preference", + value: format!("slot {:?}", persistent_boot_preference), + }, + Row { + name: "pending persistent boot preference", + value: pending_persistent_boot_preference + .map(|s| format!("slot {:?}", s)) + .unwrap_or_else(|| "-".to_string()), + }, + Row { + name: "transient boot preference", + value: transient_boot_preference + .map(|s| format!("slot {:?}", s)) + .unwrap_or_else(|| "-".to_string()), + }, + Row { + name: "slot A SHA3 256 digest", + value: slot_a_sha3_256_digest + .clone() + .unwrap_or_else(|| "-".to_string()), + }, + Row { + name: "slot B SHA3 256 digest", + value: slot_b_sha3_256_digest + .clone() + .unwrap_or_else(|| "-".to_string()), + }, + ]; + + 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(), " ")); + println!(""); + } + } + + let component_list = mgs_client + .sp_component_list(sp_id.type_, sp_id.slot) + .await + .with_context(|| format!("fetching components for SP {:?}", sp_id)); + let list = match component_list { + Ok(l) => l.into_inner(), + Err(e) => { + eprintln!("error: {:#}", e); + return Ok(()); + } + }; + + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct SpComponentRow<'a> { + name: &'a str, + description: &'a str, + device: &'a str, + presence: String, + serial: String, + } + + impl<'a> From<&'a SpComponentInfo> for SpComponentRow<'a> { + fn from(v: &'a SpComponentInfo) -> Self { + SpComponentRow { + name: &v.component, + description: &v.description, + device: &v.device, + presence: format!("{:?}", v.presence), + serial: format!("{:?}", v.serial_number), + } + } + } + + if list.components.is_empty() { + println!(" COMPONENTS: none found\n"); + return Ok(()); + } + + let table_rows = list.components.iter().map(SpComponentRow::from); + let table = tabled::Table::new(table_rows) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + println!(" COMPONENTS\n"); + println!("{}", textwrap::indent(&table.to_string(), " ")); + println!(""); + + #[derive(Tabled)] + #[tabled(rename_all = "SCREAMING_SNAKE_CASE")] + struct CabooseRow { + component: String, + board: String, + git_commit: String, + name: String, + version: String, + } + + impl<'a> From<(&'a SpIdentifier, &'a SpComponentInfo, SpComponentCaboose)> + for CabooseRow + { + fn from( + (_sp_id, component, caboose): ( + &'a SpIdentifier, + &'a SpComponentInfo, + SpComponentCaboose, + ), + ) -> Self { + CabooseRow { + component: component.component.clone(), + board: caboose.board, + git_commit: caboose.git_commit, + name: caboose.name, + version: caboose.version.unwrap_or_else(|| "-".to_string()), + } + } + } + + let mut cabooses = Vec::new(); + for c in &list.components { + if !COMPONENTS_WITH_CABOOSES.contains(&c.component.as_str()) { + continue; + } + + for i in 0..1 { + let r = mgs_client + .sp_component_caboose_get( + sp_id.type_, + sp_id.slot, + &c.component, + i, + ) + .await + .with_context(|| { + format!( + "get caboose for sp type {:?} sp slot {} \ + component {:?} slot {}", + sp_id.type_, sp_id.slot, &c.component, i + ) + }); + match r { + Ok(v) => { + cabooses.push(CabooseRow::from((sp_id, c, v.into_inner()))) + } + Err(error) => { + eprintln!("warn: {:#}", error); + } + } + } + } + + if cabooses.is_empty() { + println!(" CABOOSES: none found\n"); + return Ok(()); + } + + let table = tabled::Table::new(cabooses) + .with(tabled::settings::Style::empty()) + .with(tabled::settings::Padding::new(0, 1, 0, 0)) + .to_string(); + println!(" COMPONENT CABOOSES\n"); + println!("{}", textwrap::indent(&table.to_string(), " ")); + println!(""); + + Ok(()) +} diff --git a/dev-tools/omdb/tests/successes.out b/dev-tools/omdb/tests/successes.out index b1464cb824..eb075a84ea 100644 --- a/dev-tools/omdb/tests/successes.out +++ b/dev-tools/omdb/tests/successes.out @@ -84,6 +84,111 @@ stderr: note: using database URL postgresql://root@[::1]:REDACTED_PORT/omicron?sslmode=disable note: database schema version matches expected (5.0.0) ============================================= +EXECUTING COMMAND: omdb ["mgs", "inventory"] +termination: Exited(0) +--------------------------------------------- +stdout: +ALL CONFIGURED SPs + + TYPE SLOT + Sled 0 + Sled 1 + Switch 0 + Switch 1 + +SPs FOUND THROUGH IGNITION + + TYPE SLOT SYSTEM_TYPE + Sled 0 Gimlet + Sled 1 Gimlet + Switch 0 Sidecar + Switch 1 Sidecar + +SERVICE PROCESSOR STATES + + TYPE SLOT MODEL SERIAL REV HUBRIS PWR ROT_ACTIVE + Sled 0 FAKE_SIM_GIMLET SimGimlet00 0 0000000000000000 A2 slot A + Sled 1 FAKE_SIM_GIMLET SimGimlet01 0 0000000000000000 A2 slot A + Switch 0 FAKE_SIM_SIDECAR SimSidecar0 0 0000000000000000 A2 slot A + Switch 1 FAKE_SIM_SIDECAR SimSidecar1 0 0000000000000000 A2 slot A + +SP DETAILS: type "Sled" slot 0 + + ROOT OF TRUST + + NAME VALUE + active slot slot A + persistent boot preference slot A + pending persistent boot preference - + transient boot preference - + slot A SHA3 256 digest - + slot B SHA3 256 digest - + + COMPONENTS + + NAME DESCRIPTION DEVICE PRESENCE SERIAL + sp3-host-cpu FAKE host cpu sp3-host-cpu Present None + dev-0 FAKE temperature sensor fake-tmp-sensor Failed None + + CABOOSES: none found + +SP DETAILS: type "Sled" slot 1 + + ROOT OF TRUST + + NAME VALUE + active slot slot A + persistent boot preference slot A + pending persistent boot preference - + transient boot preference - + slot A SHA3 256 digest - + slot B SHA3 256 digest - + + COMPONENTS + + NAME DESCRIPTION DEVICE PRESENCE SERIAL + sp3-host-cpu FAKE host cpu sp3-host-cpu Present None + + CABOOSES: none found + +SP DETAILS: type "Switch" slot 0 + + ROOT OF TRUST + + NAME VALUE + active slot slot A + persistent boot preference slot A + pending persistent boot preference - + transient boot preference - + slot A SHA3 256 digest - + slot B SHA3 256 digest - + + COMPONENTS + + NAME DESCRIPTION DEVICE PRESENCE SERIAL + dev-0 FAKE temperature sensor 1 fake-tmp-sensor Present None + dev-1 FAKE temperature sensor 2 fake-tmp-sensor Failed None + + CABOOSES: none found + +SP DETAILS: type "Switch" slot 1 + + ROOT OF TRUST + + NAME VALUE + active slot slot A + persistent boot preference slot A + pending persistent boot preference - + transient boot preference - + slot A SHA3 256 digest - + slot B SHA3 256 digest - + + COMPONENTS: none found + +--------------------------------------------- +stderr: +note: using MGS URL http://[::1]:REDACTED_PORT/ +============================================= EXECUTING COMMAND: omdb ["nexus", "background-tasks", "doc"] termination: Exited(0) --------------------------------------------- diff --git a/dev-tools/omdb/tests/test_all_output.rs b/dev-tools/omdb/tests/test_all_output.rs index d757369ead..90e93ee429 100644 --- a/dev-tools/omdb/tests/test_all_output.rs +++ b/dev-tools/omdb/tests/test_all_output.rs @@ -42,6 +42,7 @@ async fn test_omdb_usage_errors() { &["db", "dns", "names"], &["db", "services"], &["db", "network"], + &["mgs"], &["nexus"], &["nexus", "background-tasks"], &["sled-agent"], @@ -58,10 +59,16 @@ async fn test_omdb_usage_errors() { #[nexus_test] async fn test_omdb_success_cases(cptestctx: &ControlPlaneTestContext) { + let gwtestctx = gateway_test_utils::setup::test_setup( + "test_omdb_success_case", + gateway_messages::SpPort::One, + ) + .await; let cmd_path = path_to_executable(CMD_OMDB); let postgres_url = cptestctx.database.listen_url(); let nexus_internal_url = format!("http://{}/", cptestctx.internal_client.bind_address); + let mgs_url = format!("http://{}/", gwtestctx.client.bind_address); let mut output = String::new(); let invocations: &[&[&'static str]] = &[ &["db", "dns", "show"], @@ -70,6 +77,7 @@ async fn test_omdb_success_cases(cptestctx: &ControlPlaneTestContext) { &["db", "services", "list-instances"], &["db", "services", "list-by-sled"], &["db", "sleds"], + &["mgs", "inventory"], &["nexus", "background-tasks", "doc"], &["nexus", "background-tasks", "show"], // We can't easily test the sled agent output because that's only @@ -81,9 +89,14 @@ async fn test_omdb_success_cases(cptestctx: &ControlPlaneTestContext) { println!("running commands with args: {:?}", args); let p = postgres_url.to_string(); let u = nexus_internal_url.clone(); + let g = mgs_url.clone(); do_run( &mut output, - move |exec| exec.env("OMDB_DB_URL", &p).env("OMDB_NEXUS_URL", &u), + move |exec| { + exec.env("OMDB_DB_URL", &p) + .env("OMDB_NEXUS_URL", &u) + .env("OMDB_MGS_URL", &g) + }, &cmd_path, args, ) @@ -91,6 +104,7 @@ async fn test_omdb_success_cases(cptestctx: &ControlPlaneTestContext) { } assert_contents("tests/successes.out", &output); + gwtestctx.teardown().await; } /// Verify that we properly deal with cases where: diff --git a/dev-tools/omdb/tests/usage_errors.out b/dev-tools/omdb/tests/usage_errors.out index dc2a16bc47..7bedc3ecbc 100644 --- a/dev-tools/omdb/tests/usage_errors.out +++ b/dev-tools/omdb/tests/usage_errors.out @@ -10,6 +10,7 @@ Usage: omdb [OPTIONS] Commands: db Query the control plane database (CockroachDB) + mgs Debug a specific Management Gateway Service instance nexus Debug a specific Nexus instance oximeter Query oximeter collector state sled-agent Debug a specific Sled @@ -33,6 +34,7 @@ Usage: omdb [OPTIONS] Commands: db Query the control plane database (CockroachDB) + mgs Debug a specific Management Gateway Service instance nexus Debug a specific Nexus instance oximeter Query oximeter collector state sled-agent Debug a specific Sled @@ -208,6 +210,24 @@ Options: --verbose Print out raw data structures from the data store -h, --help Print help ============================================= +EXECUTING COMMAND: omdb ["mgs"] +termination: Exited(2) +--------------------------------------------- +stdout: +--------------------------------------------- +stderr: +Debug a specific Management Gateway Service instance + +Usage: omdb mgs [OPTIONS] + +Commands: + inventory Show information about devices and components visible to MGS + help Print this message or the help of the given subcommand(s) + +Options: + --mgs-url URL of an MGS instance to query [env: OMDB_MGS_URL=] + -h, --help Print help +============================================= EXECUTING COMMAND: omdb ["nexus"] termination: Exited(2) --------------------------------------------- diff --git a/dev-tools/omicron-dev/Cargo.toml b/dev-tools/omicron-dev/Cargo.toml index 5439b69c76..251ee16c01 100644 --- a/dev-tools/omicron-dev/Cargo.toml +++ b/dev-tools/omicron-dev/Cargo.toml @@ -13,6 +13,8 @@ camino.workspace = true clap.workspace = true dropshot.workspace = true futures.workspace = true +gateway-messages.workspace = true +gateway-test-utils.workspace = true libc.workspace = true nexus-test-utils.workspace = true nexus-test-interface.workspace = true diff --git a/dev-tools/omicron-dev/src/bin/omicron-dev.rs b/dev-tools/omicron-dev/src/bin/omicron-dev.rs index 14617d6ba4..9107766d8a 100644 --- a/dev-tools/omicron-dev/src/bin/omicron-dev.rs +++ b/dev-tools/omicron-dev/src/bin/omicron-dev.rs @@ -30,6 +30,7 @@ async fn main() -> Result<(), anyhow::Error> { OmicronDb::DbPopulate { ref args } => cmd_db_populate(args).await, OmicronDb::DbWipe { ref args } => cmd_db_wipe(args).await, OmicronDb::ChRun { ref args } => cmd_clickhouse_run(args).await, + OmicronDb::MgsRun { ref args } => cmd_mgs_run(args).await, OmicronDb::RunAll { ref args } => cmd_run_all(args).await, OmicronDb::CertCreate { ref args } => cmd_cert_create(args).await, }; @@ -68,6 +69,12 @@ enum OmicronDb { args: ChRunArgs, }, + /// Run a simulated Management Gateway Service for development + MgsRun { + #[clap(flatten)] + args: MgsRunArgs, + }, + /// Run a full simulated control plane RunAll { #[clap(flatten)] @@ -465,3 +472,34 @@ fn write_private_file( .with_context(|| format!("open {:?} for writing", path))?; file.write_all(contents).with_context(|| format!("write to {:?}", path)) } + +#[derive(Clone, Debug, Args)] +struct MgsRunArgs {} + +async fn cmd_mgs_run(_args: &MgsRunArgs) -> Result<(), anyhow::Error> { + // Start a stream listening for SIGINT + let signals = Signals::new(&[SIGINT]).expect("failed to wait for SIGINT"); + let mut signal_stream = signals.fuse(); + + println!("omicron-dev: setting up MGS ... "); + let gwtestctx = gateway_test_utils::setup::test_setup( + "omicron-dev", + gateway_messages::SpPort::One, + ) + .await; + println!("omicron-dev: MGS is running."); + + let addr = gwtestctx.client.bind_address; + println!("omicron-dev: MGS API: http://{:?}", addr); + + // Wait for a signal. + let caught_signal = signal_stream.next().await; + assert_eq!(caught_signal.unwrap(), SIGINT); + eprintln!( + "omicron-dev: caught signal, shutting down and removing \ + temporary directory" + ); + + gwtestctx.teardown().await; + Ok(()) +} diff --git a/dev-tools/omicron-dev/tests/output/cmd-omicron-dev-noargs-stderr b/dev-tools/omicron-dev/tests/output/cmd-omicron-dev-noargs-stderr index f3c28e1ab9..ac1c87e165 100644 --- a/dev-tools/omicron-dev/tests/output/cmd-omicron-dev-noargs-stderr +++ b/dev-tools/omicron-dev/tests/output/cmd-omicron-dev-noargs-stderr @@ -7,6 +7,7 @@ Commands: db-populate Populate an existing CockroachDB cluster with the Omicron schema db-wipe Wipe the Omicron schema (and all data) from an existing CockroachDB cluster ch-run Run a ClickHouse database server for development + mgs-run Run a simulated Management Gateway Service for development run-all Run a full simulated control plane cert-create Create a self-signed certificate for use with Omicron help Print this message or the help of the given subcommand(s)