diff --git a/wicket/src/cli/command.rs b/wicket/src/cli/command.rs index bae98130b5..899b28971a 100644 --- a/wicket/src/cli/command.rs +++ b/wicket/src/cli/command.rs @@ -10,7 +10,7 @@ use anyhow::Result; use clap::{Args, ColorChoice, Parser, Subcommand}; use super::{ - preflight::PreflightArgs, rack_setup::SetupArgs, + inventory::InventoryArgs, preflight::PreflightArgs, rack_setup::SetupArgs, rack_update::RackUpdateArgs, upload::UploadArgs, }; @@ -49,6 +49,9 @@ impl ShellApp { args.exec(log, wicketd_addr, self.global_opts).await } ShellCommand::Preflight(args) => args.exec(log, wicketd_addr).await, + ShellCommand::Inventory(args) => { + args.exec(log, wicketd_addr, output).await + } } } } @@ -100,4 +103,8 @@ enum ShellCommand { /// Run checks prior to setting up the rack. #[command(subcommand)] Preflight(PreflightArgs), + + /// Enumerate rack components + #[command(subcommand)] + Inventory(InventoryArgs), } diff --git a/wicket/src/cli/inventory.rs b/wicket/src/cli/inventory.rs new file mode 100644 index 0000000000..54bfa304c2 --- /dev/null +++ b/wicket/src/cli/inventory.rs @@ -0,0 +1,133 @@ +// 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/. + +//! Support for inventory checks via wicketd. + +use crate::cli::CommandOutput; +use crate::wicketd::create_wicketd_client; +use anyhow::Context; +use anyhow::Result; +use clap::{Subcommand, ValueEnum}; +use owo_colors::OwoColorize; +use sled_hardware_types::Baseboard; +use slog::Logger; +use std::fmt; +use std::net::SocketAddrV6; +use std::time::Duration; +use wicket_common::rack_setup::BootstrapSledDescription; + +const WICKETD_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Debug, Subcommand)] +pub(crate) enum InventoryArgs { + /// List state of all bootstrap sleds, as configured with rack-setup + ConfiguredBootstrapSleds { + /// Select output format + #[clap(long, default_value_t = OutputFormat::Table)] + format: OutputFormat, + }, +} + +#[derive(Debug, ValueEnum, Clone)] +pub enum OutputFormat { + /// Print output as operator-readable table + Table, + + /// Print output as json + Json, +} + +impl fmt::Display for OutputFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + OutputFormat::Table => write!(f, "table"), + OutputFormat::Json => write!(f, "json"), + } + } +} + +impl InventoryArgs { + pub(crate) async fn exec( + self, + log: Logger, + wicketd_addr: SocketAddrV6, + mut output: CommandOutput<'_>, + ) -> Result<()> { + let client = create_wicketd_client(&log, wicketd_addr, WICKETD_TIMEOUT); + + match self { + InventoryArgs::ConfiguredBootstrapSleds { format } => { + // We don't use the /bootstrap-sleds endpoint, because that + // gets all sleds visible on the bootstrap network. We want + // something subtly different here. + // - We want the status of only sleds we've configured wicket + // to use for setup. /bootstrap-sleds will give us sleds + // we don't want + // - We want the status even if they aren't visible on the + // bootstrap network yet. + // + // In other words, we want the sled information displayed at the + // bottom of the rack setup screen in the TUI, and we get it the + // same way it does. + let conf = client + .get_rss_config() + .await + .context("failed to get rss config")?; + + let bootstrap_sleds = &conf.insensitive.bootstrap_sleds; + match format { + OutputFormat::Json => { + let json_str = + serde_json::to_string_pretty(bootstrap_sleds) + .context("serializing sled data failed")?; + writeln!(output.stdout, "{}", json_str) + .expect("writing to stdout failed"); + } + OutputFormat::Table => { + for sled in bootstrap_sleds { + print_bootstrap_sled_data(sled, &mut output); + } + } + } + + Ok(()) + } + } + } +} + +fn print_bootstrap_sled_data( + desc: &BootstrapSledDescription, + output: &mut CommandOutput<'_>, +) { + let slot = desc.id.slot; + + let identifier = match &desc.baseboard { + Baseboard::Gimlet { identifier, .. } => identifier.clone(), + Baseboard::Pc { identifier, .. } => identifier.clone(), + Baseboard::Unknown => "unknown".to_string(), + }; + + let address = desc.bootstrap_ip; + + // Create status indicators + let status = match address { + None => format!("{}", '⚠'.red()), + Some(_) => format!("{}", '✔'.green()), + }; + + let addr_fmt = match address { + None => "(not available)".to_string(), + Some(addr) => format!("{}", addr), + }; + + // Print out this entry. We say "Cubby" rather than "Slot" here purely + // because the TUI also says "Cubby". + writeln!( + output.stdout, + "{status} Cubby {:02}\t{identifier}\t{addr_fmt}", + slot + ) + .expect("writing to stdout failed"); +} diff --git a/wicket/src/cli/mod.rs b/wicket/src/cli/mod.rs index e63ef467e7..ac406823fe 100644 --- a/wicket/src/cli/mod.rs +++ b/wicket/src/cli/mod.rs @@ -11,6 +11,7 @@ //! support for that. mod command; +mod inventory; mod preflight; mod rack_setup; mod rack_update; diff --git a/wicketd/tests/integration_tests/inventory.rs b/wicketd/tests/integration_tests/inventory.rs index ea696d21c9..ed5ad22d5d 100644 --- a/wicketd/tests/integration_tests/inventory.rs +++ b/wicketd/tests/integration_tests/inventory.rs @@ -9,6 +9,10 @@ use std::time::Duration; use super::setup::WicketdTestContext; use gateway_messages::SpPort; use gateway_test_utils::setup as gateway_setup; +use sled_hardware_types::Baseboard; +use wicket::OutputKind; +use wicket_common::inventory::{SpIdentifier, SpType}; +use wicket_common::rack_setup::BootstrapSledDescription; use wicketd_client::types::{GetInventoryParams, GetInventoryResponse}; #[tokio::test] @@ -45,5 +49,62 @@ async fn test_inventory() { // 4 SPs attached to the inventory. assert_eq!(inventory.sps.len(), 4); + // Test CLI with JSON output + { + let args = + vec!["inventory", "configured-bootstrap-sleds", "--format", "json"]; + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let output = OutputKind::Captured { + log: wicketd_testctx.log().clone(), + stdout: &mut stdout, + stderr: &mut stderr, + }; + + wicket::exec_with_args(wicketd_testctx.wicketd_addr, args, output) + .await + .expect("wicket inventory configured-bootstrap-sleds failed"); + + // stdout should contain a JSON object. + let response: Vec = + serde_json::from_slice(&stdout).expect("stdout is valid JSON"); + + // This only tests the case that we get sleds back with no current + // bootstrap IP. This does provide svalue: it check that the command + // exists, accesses data within wicket, and returns it in the schema we + // expect. But it does not test the case where a sled does have a + // bootstrap IP. + // + // Unfortunately, that's a difficult thing to test today. Wicket gets + // that information by enumerating the IPs on the bootstrap network and + // reaching out to the bootstrap_agent on them directly to ask them who + // they are. Our testing setup does not have a way to provide such an + // IP, or run a bootstrap_agent on an IP to respond. We should update + // this test when we do have that capabilitiy. + assert_eq!( + response, + vec![ + BootstrapSledDescription { + id: SpIdentifier { type_: SpType::Sled, slot: 0 }, + baseboard: Baseboard::Gimlet { + identifier: "SimGimlet00".to_string(), + model: "i86pc".to_string(), + revision: 0 + }, + bootstrap_ip: None + }, + BootstrapSledDescription { + id: SpIdentifier { type_: SpType::Sled, slot: 1 }, + baseboard: Baseboard::Gimlet { + identifier: "SimGimlet01".to_string(), + model: "i86pc".to_string(), + revision: 0 + }, + bootstrap_ip: None + }, + ] + ); + } + wicketd_testctx.teardown().await; }