Skip to content

Commit d391e5c

Browse files
authored
wicket cli: add "inventory configured-bootstrap-sleds" (#6218)
## This PR My main motivation for adding: I am working on automating control plane deployment on london and madrid. Before we can start rack init, we need to know if all the sleds we are going to initialize are actually available. This information is something you can see from the rack init TUI, but there was no way to get it from the CLI. I have added an `inventory` command with a `configured-bootstrap-sleds` subcommand, which presents exactly the same information accessible in the TUI's rack init screen. (Note: there is no way to start rack init for the CLI yet, but I will add that in a future PR.) In theory `inventory` could be expanded to provide other information. Note, I am absolutely not attached to the command interface or data format. I only care about making this information accessible so I can use it in my scripts. Meaning, if *any* of this interface should be different from how I've done it in this initial PR, I will make those changes as asked. ### Example Usage ``` artemis@jeeves ~ $ ssh londonwicket inventory configured-bootstrap-sleds ⚠ Cubby 14 BRM42220036 (not available) ✔ Cubby 15 BRM42220062 fdb0:a840:2504:312::1 ✔ Cubby 16 BRM42220030 fdb0:a840:2504:257::1 ✔ Cubby 17 BRM44220007 fdb0:a840:2504:492::1 ``` ``` artemis@jeeves ~ $ ssh londonwicket -- inventory configured-bootstrap-sleds --json | jq [ { "id": { "slot": 14, "type": "sled" }, "baseboard": { "type": "gimlet", "identifier": "BRM42220036", "model": "913-0000019", "revision": 6 }, "bootstrap_ip": "fdb0:a840:2504:212::1" }, { "id": { "slot": 15, "type": "sled" }, "baseboard": { "type": "gimlet", "identifier": "BRM42220062", "model": "913-0000019", "revision": 6 }, "bootstrap_ip": "fdb0:a840:2504:312::1" }, { "id": { "slot": 16, "type": "sled" }, "baseboard": { "type": "gimlet", "identifier": "BRM42220030", "model": "913-0000019", "revision": 6 }, "bootstrap_ip": "fdb0:a840:2504:257::1" }, { "id": { "slot": 17, "type": "sled" }, "baseboard": { "type": "gimlet", "identifier": "BRM44220007", "model": "913-0000019", "revision": 6 }, "bootstrap_ip": "fdb0:a840:2504:492::1" } ] ```
1 parent 04fdbcd commit d391e5c

File tree

4 files changed

+203
-1
lines changed

4 files changed

+203
-1
lines changed

wicket/src/cli/command.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use anyhow::Result;
1010
use clap::{Args, ColorChoice, Parser, Subcommand};
1111

1212
use super::{
13-
preflight::PreflightArgs, rack_setup::SetupArgs,
13+
inventory::InventoryArgs, preflight::PreflightArgs, rack_setup::SetupArgs,
1414
rack_update::RackUpdateArgs, upload::UploadArgs,
1515
};
1616

@@ -49,6 +49,9 @@ impl ShellApp {
4949
args.exec(log, wicketd_addr, self.global_opts).await
5050
}
5151
ShellCommand::Preflight(args) => args.exec(log, wicketd_addr).await,
52+
ShellCommand::Inventory(args) => {
53+
args.exec(log, wicketd_addr, output).await
54+
}
5255
}
5356
}
5457
}
@@ -100,4 +103,8 @@ enum ShellCommand {
100103
/// Run checks prior to setting up the rack.
101104
#[command(subcommand)]
102105
Preflight(PreflightArgs),
106+
107+
/// Enumerate rack components
108+
#[command(subcommand)]
109+
Inventory(InventoryArgs),
103110
}

wicket/src/cli/inventory.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Support for inventory checks via wicketd.
6+
7+
use crate::cli::CommandOutput;
8+
use crate::wicketd::create_wicketd_client;
9+
use anyhow::Context;
10+
use anyhow::Result;
11+
use clap::{Subcommand, ValueEnum};
12+
use owo_colors::OwoColorize;
13+
use sled_hardware_types::Baseboard;
14+
use slog::Logger;
15+
use std::fmt;
16+
use std::net::SocketAddrV6;
17+
use std::time::Duration;
18+
use wicket_common::rack_setup::BootstrapSledDescription;
19+
20+
const WICKETD_TIMEOUT: Duration = Duration::from_secs(5);
21+
22+
#[derive(Debug, Subcommand)]
23+
pub(crate) enum InventoryArgs {
24+
/// List state of all bootstrap sleds, as configured with rack-setup
25+
ConfiguredBootstrapSleds {
26+
/// Select output format
27+
#[clap(long, default_value_t = OutputFormat::Table)]
28+
format: OutputFormat,
29+
},
30+
}
31+
32+
#[derive(Debug, ValueEnum, Clone)]
33+
pub enum OutputFormat {
34+
/// Print output as operator-readable table
35+
Table,
36+
37+
/// Print output as json
38+
Json,
39+
}
40+
41+
impl fmt::Display for OutputFormat {
42+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43+
match self {
44+
OutputFormat::Table => write!(f, "table"),
45+
OutputFormat::Json => write!(f, "json"),
46+
}
47+
}
48+
}
49+
50+
impl InventoryArgs {
51+
pub(crate) async fn exec(
52+
self,
53+
log: Logger,
54+
wicketd_addr: SocketAddrV6,
55+
mut output: CommandOutput<'_>,
56+
) -> Result<()> {
57+
let client = create_wicketd_client(&log, wicketd_addr, WICKETD_TIMEOUT);
58+
59+
match self {
60+
InventoryArgs::ConfiguredBootstrapSleds { format } => {
61+
// We don't use the /bootstrap-sleds endpoint, because that
62+
// gets all sleds visible on the bootstrap network. We want
63+
// something subtly different here.
64+
// - We want the status of only sleds we've configured wicket
65+
// to use for setup. /bootstrap-sleds will give us sleds
66+
// we don't want
67+
// - We want the status even if they aren't visible on the
68+
// bootstrap network yet.
69+
//
70+
// In other words, we want the sled information displayed at the
71+
// bottom of the rack setup screen in the TUI, and we get it the
72+
// same way it does.
73+
let conf = client
74+
.get_rss_config()
75+
.await
76+
.context("failed to get rss config")?;
77+
78+
let bootstrap_sleds = &conf.insensitive.bootstrap_sleds;
79+
match format {
80+
OutputFormat::Json => {
81+
let json_str =
82+
serde_json::to_string_pretty(bootstrap_sleds)
83+
.context("serializing sled data failed")?;
84+
writeln!(output.stdout, "{}", json_str)
85+
.expect("writing to stdout failed");
86+
}
87+
OutputFormat::Table => {
88+
for sled in bootstrap_sleds {
89+
print_bootstrap_sled_data(sled, &mut output);
90+
}
91+
}
92+
}
93+
94+
Ok(())
95+
}
96+
}
97+
}
98+
}
99+
100+
fn print_bootstrap_sled_data(
101+
desc: &BootstrapSledDescription,
102+
output: &mut CommandOutput<'_>,
103+
) {
104+
let slot = desc.id.slot;
105+
106+
let identifier = match &desc.baseboard {
107+
Baseboard::Gimlet { identifier, .. } => identifier.clone(),
108+
Baseboard::Pc { identifier, .. } => identifier.clone(),
109+
Baseboard::Unknown => "unknown".to_string(),
110+
};
111+
112+
let address = desc.bootstrap_ip;
113+
114+
// Create status indicators
115+
let status = match address {
116+
None => format!("{}", '⚠'.red()),
117+
Some(_) => format!("{}", '✔'.green()),
118+
};
119+
120+
let addr_fmt = match address {
121+
None => "(not available)".to_string(),
122+
Some(addr) => format!("{}", addr),
123+
};
124+
125+
// Print out this entry. We say "Cubby" rather than "Slot" here purely
126+
// because the TUI also says "Cubby".
127+
writeln!(
128+
output.stdout,
129+
"{status} Cubby {:02}\t{identifier}\t{addr_fmt}",
130+
slot
131+
)
132+
.expect("writing to stdout failed");
133+
}

wicket/src/cli/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//! support for that.
1212
1313
mod command;
14+
mod inventory;
1415
mod preflight;
1516
mod rack_setup;
1617
mod rack_update;

wicketd/tests/integration_tests/inventory.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ use std::time::Duration;
99
use super::setup::WicketdTestContext;
1010
use gateway_messages::SpPort;
1111
use gateway_test_utils::setup as gateway_setup;
12+
use sled_hardware_types::Baseboard;
13+
use wicket::OutputKind;
14+
use wicket_common::inventory::{SpIdentifier, SpType};
15+
use wicket_common::rack_setup::BootstrapSledDescription;
1216
use wicketd_client::types::{GetInventoryParams, GetInventoryResponse};
1317

1418
#[tokio::test]
@@ -45,5 +49,62 @@ async fn test_inventory() {
4549
// 4 SPs attached to the inventory.
4650
assert_eq!(inventory.sps.len(), 4);
4751

52+
// Test CLI with JSON output
53+
{
54+
let args =
55+
vec!["inventory", "configured-bootstrap-sleds", "--format", "json"];
56+
let mut stdout = Vec::new();
57+
let mut stderr = Vec::new();
58+
let output = OutputKind::Captured {
59+
log: wicketd_testctx.log().clone(),
60+
stdout: &mut stdout,
61+
stderr: &mut stderr,
62+
};
63+
64+
wicket::exec_with_args(wicketd_testctx.wicketd_addr, args, output)
65+
.await
66+
.expect("wicket inventory configured-bootstrap-sleds failed");
67+
68+
// stdout should contain a JSON object.
69+
let response: Vec<BootstrapSledDescription> =
70+
serde_json::from_slice(&stdout).expect("stdout is valid JSON");
71+
72+
// This only tests the case that we get sleds back with no current
73+
// bootstrap IP. This does provide svalue: it check that the command
74+
// exists, accesses data within wicket, and returns it in the schema we
75+
// expect. But it does not test the case where a sled does have a
76+
// bootstrap IP.
77+
//
78+
// Unfortunately, that's a difficult thing to test today. Wicket gets
79+
// that information by enumerating the IPs on the bootstrap network and
80+
// reaching out to the bootstrap_agent on them directly to ask them who
81+
// they are. Our testing setup does not have a way to provide such an
82+
// IP, or run a bootstrap_agent on an IP to respond. We should update
83+
// this test when we do have that capabilitiy.
84+
assert_eq!(
85+
response,
86+
vec![
87+
BootstrapSledDescription {
88+
id: SpIdentifier { type_: SpType::Sled, slot: 0 },
89+
baseboard: Baseboard::Gimlet {
90+
identifier: "SimGimlet00".to_string(),
91+
model: "i86pc".to_string(),
92+
revision: 0
93+
},
94+
bootstrap_ip: None
95+
},
96+
BootstrapSledDescription {
97+
id: SpIdentifier { type_: SpType::Sled, slot: 1 },
98+
baseboard: Baseboard::Gimlet {
99+
identifier: "SimGimlet01".to_string(),
100+
model: "i86pc".to_string(),
101+
revision: 0
102+
},
103+
bootstrap_ip: None
104+
},
105+
]
106+
);
107+
}
108+
48109
wicketd_testctx.teardown().await;
49110
}

0 commit comments

Comments
 (0)