Skip to content

Commit 3a3eb52

Browse files
authored
[6/n] [reconfigurator-cli] allow loading up an example system (#6788)
Add a new command to the reconfigurator CLI, `load-example`, that conjures up an example system. This allows for really easy setup, both interactively and in automated tests. Plus, having everything be seeded is really nice for reproducibility.
1 parent 1f3de04 commit 3a3eb52

File tree

9 files changed

+759
-21
lines changed

9 files changed

+759
-21
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dev-tools/reconfigurator-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ omicron-rpaths.workspace = true
1414
anyhow.workspace = true
1515
assert_matches.workspace = true
1616
camino.workspace = true
17+
chrono.workspace = true
1718
clap.workspace = true
1819
dropshot.workspace = true
1920
humantime.workspace = true
@@ -33,6 +34,7 @@ slog-error-chain.workspace = true
3334
slog.workspace = true
3435
swrite.workspace = true
3536
tabled.workspace = true
37+
typed-rng.workspace = true
3638
uuid.workspace = true
3739
omicron-workspace-hack.workspace = true
3840

dev-tools/reconfigurator-cli/src/main.rs

Lines changed: 174 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
77
use anyhow::{anyhow, bail, Context};
88
use camino::Utf8PathBuf;
9+
use chrono::Utc;
910
use clap::CommandFactory;
1011
use clap::FromArgMatches;
1112
use clap::ValueEnum;
@@ -15,6 +16,7 @@ use internal_dns_types::diff::DnsDiff;
1516
use nexus_inventory::CollectionBuilder;
1617
use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder;
1718
use nexus_reconfigurator_planning::blueprint_builder::EnsureMultiple;
19+
use nexus_reconfigurator_planning::example::ExampleSystemBuilder;
1820
use nexus_reconfigurator_planning::planner::Planner;
1921
use nexus_reconfigurator_planning::system::{
2022
SledBuilder, SledHwInventory, SystemDescription,
@@ -34,6 +36,8 @@ use nexus_types::internal_api::params::DnsConfigParams;
3436
use nexus_types::inventory::Collection;
3537
use omicron_common::api::external::Generation;
3638
use omicron_common::api::external::Name;
39+
use omicron_common::policy::NEXUS_REDUNDANCY;
40+
use omicron_uuid_kinds::CollectionKind;
3741
use omicron_uuid_kinds::CollectionUuid;
3842
use omicron_uuid_kinds::GenericUuid;
3943
use omicron_uuid_kinds::OmicronZoneUuid;
@@ -45,6 +49,7 @@ use std::collections::BTreeMap;
4549
use std::io::BufRead;
4650
use swrite::{swriteln, SWrite};
4751
use tabled::Tabled;
52+
use typed_rng::TypedUuidRng;
4853
use uuid::Uuid;
4954

5055
/// REPL state
@@ -76,13 +81,67 @@ struct ReconfiguratorSim {
7681
/// External DNS zone name configured
7782
external_dns_zone_name: String,
7883

84+
/// RNG for collection IDs
85+
collection_id_rng: TypedUuidRng<CollectionKind>,
86+
7987
/// Policy overrides
8088
num_nexus: Option<u16>,
8189

8290
log: slog::Logger,
8391
}
8492

8593
impl ReconfiguratorSim {
94+
fn new(log: slog::Logger) -> Self {
95+
Self {
96+
system: SystemDescription::new(),
97+
collections: IndexMap::new(),
98+
blueprints: IndexMap::new(),
99+
internal_dns: BTreeMap::new(),
100+
external_dns: BTreeMap::new(),
101+
silo_names: vec!["example-silo".parse().unwrap()],
102+
external_dns_zone_name: String::from("oxide.example"),
103+
collection_id_rng: TypedUuidRng::from_entropy(),
104+
num_nexus: None,
105+
log,
106+
}
107+
}
108+
109+
/// Returns true if the user has made local changes to the simulated
110+
/// system.
111+
///
112+
/// This is used when the user asks to load an example system. Doing that
113+
/// basically requires a clean slate.
114+
fn user_made_system_changes(&self) -> bool {
115+
// Use this pattern to ensure that if a new field is added to
116+
// ReconfiguratorSim, it will fail to compile until it's added here.
117+
let Self {
118+
system,
119+
collections,
120+
blueprints,
121+
internal_dns,
122+
external_dns,
123+
// For purposes of this method, we let these policy parameters be
124+
// set to any arbitrary value. This lets example systems be
125+
// generated using these values.
126+
silo_names: _,
127+
external_dns_zone_name: _,
128+
collection_id_rng: _,
129+
num_nexus: _,
130+
log: _,
131+
} = self;
132+
133+
system.has_sleds()
134+
|| !collections.is_empty()
135+
|| !blueprints.is_empty()
136+
|| !internal_dns.is_empty()
137+
|| !external_dns.is_empty()
138+
}
139+
140+
// Reset the state of the REPL.
141+
fn wipe(&mut self) {
142+
*self = Self::new(self.log.clone());
143+
}
144+
86145
fn blueprint_lookup(&self, id: Uuid) -> Result<&Blueprint, anyhow::Error> {
87146
self.blueprints
88147
.get(&id)
@@ -181,22 +240,12 @@ fn main() -> anyhow::Result<()> {
181240
let cmd = CmdReconfiguratorSim::parse();
182241

183242
let log = dropshot::ConfigLogging::StderrTerminal {
184-
level: dropshot::ConfigLoggingLevel::Debug,
243+
level: dropshot::ConfigLoggingLevel::Info,
185244
}
186245
.to_logger("reconfigurator-sim")
187246
.context("creating logger")?;
188247

189-
let mut sim = ReconfiguratorSim {
190-
system: SystemDescription::new(),
191-
collections: IndexMap::new(),
192-
blueprints: IndexMap::new(),
193-
internal_dns: BTreeMap::new(),
194-
external_dns: BTreeMap::new(),
195-
log,
196-
silo_names: vec!["example-silo".parse().unwrap()],
197-
external_dns_zone_name: String::from("oxide.example"),
198-
num_nexus: None,
199-
};
248+
let mut sim = ReconfiguratorSim::new(log);
200249

201250
if let Some(input_file) = cmd.input_file {
202251
let file = std::fs::File::open(&input_file)
@@ -310,8 +359,10 @@ fn process_entry(sim: &mut ReconfiguratorSim, entry: String) -> LoopResult {
310359
Commands::Show => cmd_show(sim),
311360
Commands::Set(args) => cmd_set(sim, args),
312361
Commands::Load(args) => cmd_load(sim, args),
362+
Commands::LoadExample(args) => cmd_load_example(sim, args),
313363
Commands::FileContents(args) => cmd_file_contents(args),
314364
Commands::Save(args) => cmd_save(sim, args),
365+
Commands::Wipe => cmd_wipe(sim),
315366
};
316367

317368
match cmd_result {
@@ -380,8 +431,12 @@ enum Commands {
380431
Save(SaveArgs),
381432
/// load state from a file
382433
Load(LoadArgs),
434+
/// generate and load an example system
435+
LoadExample(LoadExampleArgs),
383436
/// show information about what's in a saved file
384437
FileContents(FileContentsArgs),
438+
/// reset the state of the REPL
439+
Wipe,
385440
}
386441

387442
#[derive(Debug, Args)]
@@ -511,6 +566,33 @@ struct LoadArgs {
511566
collection_id: Option<CollectionUuid>,
512567
}
513568

569+
#[derive(Debug, Args)]
570+
struct LoadExampleArgs {
571+
/// Seed for the RNG that's used to generate the example system.
572+
///
573+
/// Setting this makes it possible for callers to get deterministic
574+
/// results. In automated tests, the seed is typically the name of the
575+
/// test.
576+
#[clap(long, default_value = "reconfigurator_cli_example")]
577+
seed: String,
578+
579+
/// The number of sleds in the example system.
580+
#[clap(short = 's', long, default_value_t = ExampleSystemBuilder::DEFAULT_N_SLEDS)]
581+
nsleds: usize,
582+
583+
/// The number of disks per sled in the example system.
584+
#[clap(short = 'd', long, default_value_t = SledBuilder::DEFAULT_NPOOLS)]
585+
ndisks_per_sled: u8,
586+
587+
/// Do not create zones in the example system.
588+
#[clap(short = 'Z', long)]
589+
no_zones: bool,
590+
591+
/// Do not create entries for disks in the blueprint.
592+
#[clap(long)]
593+
no_disks_in_blueprint: bool,
594+
}
595+
514596
#[derive(Debug, Args)]
515597
struct FileContentsArgs {
516598
/// input file
@@ -675,7 +757,12 @@ fn cmd_inventory_generate(
675757
)
676758
.context("recording Omicron zones")?;
677759
}
678-
let inventory = builder.build();
760+
761+
let mut inventory = builder.build();
762+
// Assign collection IDs from the RNG. This enables consistent results when
763+
// callers have explicitly seeded the RNG (e.g., in tests).
764+
inventory.id = sim.collection_id_rng.next();
765+
679766
let rv = format!(
680767
"generated inventory collection {} from configured sleds",
681768
inventory.id
@@ -848,7 +935,7 @@ fn cmd_blueprint_diff(
848935
// Diff'ing DNS is a little trickier. First, compute what DNS should be for
849936
// each blueprint. To do that we need to construct a list of sleds suitable
850937
// for the executor.
851-
let sleds_by_id = make_sleds_by_id(&sim)?;
938+
let sleds_by_id = make_sleds_by_id(&sim.system)?;
852939
let internal_dns_config1 = blueprint_internal_dns_config(
853940
&blueprint1,
854941
&sleds_by_id,
@@ -881,10 +968,9 @@ fn cmd_blueprint_diff(
881968
}
882969

883970
fn make_sleds_by_id(
884-
sim: &ReconfiguratorSim,
971+
system: &SystemDescription,
885972
) -> Result<BTreeMap<SledUuid, execution::Sled>, anyhow::Error> {
886-
let collection = sim
887-
.system
973+
let collection = system
888974
.to_collection_builder()
889975
.context(
890976
"unexpectedly failed to create collection for current set of sleds",
@@ -924,7 +1010,7 @@ fn cmd_blueprint_diff_dns(
9241010

9251011
let blueprint_dns_zone = match dns_group {
9261012
CliDnsGroup::Internal => {
927-
let sleds_by_id = make_sleds_by_id(sim)?;
1013+
let sleds_by_id = make_sleds_by_id(&sim.system)?;
9281014
blueprint_internal_dns_config(
9291015
blueprint,
9301016
&sleds_by_id,
@@ -1003,6 +1089,11 @@ fn cmd_save(
10031089
)))
10041090
}
10051091

1092+
fn cmd_wipe(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
1093+
sim.wipe();
1094+
Ok(Some("wiped reconfigurator-sim state".to_string()))
1095+
}
1096+
10061097
fn cmd_show(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
10071098
let mut s = String::new();
10081099
do_print_properties(&mut s, sim);
@@ -1275,6 +1366,71 @@ fn cmd_load(
12751366
Ok(Some(s))
12761367
}
12771368

1369+
fn cmd_load_example(
1370+
sim: &mut ReconfiguratorSim,
1371+
args: LoadExampleArgs,
1372+
) -> anyhow::Result<Option<String>> {
1373+
if sim.user_made_system_changes() {
1374+
bail!(
1375+
"changes made to simulated system: run `wipe system` before \
1376+
loading an example system"
1377+
);
1378+
}
1379+
1380+
// Generate the example system.
1381+
let (example, blueprint) = ExampleSystemBuilder::new(&sim.log, &args.seed)
1382+
.nsleds(args.nsleds)
1383+
.ndisks_per_sled(args.ndisks_per_sled)
1384+
.nexus_count(sim.num_nexus.map_or(NEXUS_REDUNDANCY, |n| n.into()))
1385+
.create_zones(!args.no_zones)
1386+
.create_disks_in_blueprint(!args.no_disks_in_blueprint)
1387+
.build();
1388+
1389+
// Generate the internal and external DNS configs based on the blueprint.
1390+
let sleds_by_id = make_sleds_by_id(&example.system)?;
1391+
let internal_dns = blueprint_internal_dns_config(
1392+
&blueprint,
1393+
&sleds_by_id,
1394+
&Default::default(),
1395+
)?;
1396+
let external_dns = blueprint_external_dns_config(
1397+
&blueprint,
1398+
&sim.silo_names,
1399+
sim.external_dns_zone_name.clone(),
1400+
);
1401+
1402+
// No more fallible operations from here on out: set the system state.
1403+
let collection_id = example.collection.id;
1404+
let blueprint_id = blueprint.id;
1405+
sim.system = example.system;
1406+
sim.collections.insert(collection_id, example.collection);
1407+
sim.internal_dns.insert(
1408+
blueprint.internal_dns_version,
1409+
DnsConfigParams {
1410+
generation: blueprint.internal_dns_version.into(),
1411+
time_created: Utc::now(),
1412+
zones: vec![internal_dns],
1413+
},
1414+
);
1415+
sim.external_dns.insert(
1416+
blueprint.external_dns_version,
1417+
DnsConfigParams {
1418+
generation: blueprint.external_dns_version.into(),
1419+
time_created: Utc::now(),
1420+
zones: vec![external_dns],
1421+
},
1422+
);
1423+
sim.blueprints.insert(blueprint.id, blueprint);
1424+
sim.collection_id_rng =
1425+
TypedUuidRng::from_seed(&args.seed, "reconfigurator-cli");
1426+
1427+
Ok(Some(format!(
1428+
"loaded example system with:\n\
1429+
- collection: {collection_id}\n\
1430+
- blueprint: {blueprint_id}",
1431+
)))
1432+
}
1433+
12781434
fn cmd_file_contents(args: FileContentsArgs) -> anyhow::Result<Option<String>> {
12791435
let loaded = read_file(&args.filename)?;
12801436

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
load-example --seed test-basic
2+
load-example --seed test-basic
3+
4+
show
5+
6+
sled-list
7+
inventory-list
8+
blueprint-list
9+
10+
sled-show 2eb69596-f081-4e2d-9425-9994926e0832
11+
blueprint-show ade5749d-bdf3-4fab-a8ae-00bea01b3a5a
12+
13+
blueprint-diff-inventory 9e187896-7809-46d0-9210-d75be1b3c4d4 ade5749d-bdf3-4fab-a8ae-00bea01b3a5a
14+
15+
inventory-generate
16+
blueprint-diff-inventory b32394d8-7d79-486f-8657-fd5219508181 ade5749d-bdf3-4fab-a8ae-00bea01b3a5a
17+
18+
wipe
19+
load-example --seed test-basic --nsleds 1 --ndisks-per-sled 4 --no-zones
20+
21+
sled-list
22+
inventory-list
23+
blueprint-list
24+
25+
sled-show 89d02b1b-478c-401a-8e28-7a26f74fa41b
26+
blueprint-show ade5749d-bdf3-4fab-a8ae-00bea01b3a5a

dev-tools/reconfigurator-cli/tests/output/cmd-example-stderr

Whitespace-only changes.

0 commit comments

Comments
 (0)