Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6/n] [reconfigurator-cli] allow loading up an example system #6788

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions dev-tools/reconfigurator-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ omicron-rpaths.workspace = true
anyhow.workspace = true
assert_matches.workspace = true
camino.workspace = true
chrono.workspace = true
clap.workspace = true
dropshot.workspace = true
humantime.workspace = true
Expand All @@ -33,6 +34,7 @@ slog-error-chain.workspace = true
slog.workspace = true
swrite.workspace = true
tabled.workspace = true
typed-rng.workspace = true
uuid.workspace = true
omicron-workspace-hack.workspace = true

Expand Down
192 changes: 174 additions & 18 deletions dev-tools/reconfigurator-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use anyhow::{anyhow, bail, Context};
use camino::Utf8PathBuf;
use chrono::Utc;
use clap::CommandFactory;
use clap::FromArgMatches;
use clap::ValueEnum;
Expand All @@ -15,6 +16,7 @@ use internal_dns_types::diff::DnsDiff;
use nexus_inventory::CollectionBuilder;
use nexus_reconfigurator_planning::blueprint_builder::BlueprintBuilder;
use nexus_reconfigurator_planning::blueprint_builder::EnsureMultiple;
use nexus_reconfigurator_planning::example::ExampleSystemBuilder;
use nexus_reconfigurator_planning::planner::Planner;
use nexus_reconfigurator_planning::system::{
SledBuilder, SledHwInventory, SystemDescription,
Expand All @@ -34,6 +36,8 @@ use nexus_types::internal_api::params::DnsConfigParams;
use nexus_types::inventory::Collection;
use omicron_common::api::external::Generation;
use omicron_common::api::external::Name;
use omicron_common::policy::NEXUS_REDUNDANCY;
use omicron_uuid_kinds::CollectionKind;
use omicron_uuid_kinds::CollectionUuid;
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::OmicronZoneUuid;
Expand All @@ -45,6 +49,7 @@ use std::collections::BTreeMap;
use std::io::BufRead;
use swrite::{swriteln, SWrite};
use tabled::Tabled;
use typed_rng::TypedUuidRng;
use uuid::Uuid;

/// REPL state
Expand Down Expand Up @@ -76,13 +81,67 @@ struct ReconfiguratorSim {
/// External DNS zone name configured
external_dns_zone_name: String,

/// RNG for collection IDs
collection_id_rng: TypedUuidRng<CollectionKind>,

/// Policy overrides
num_nexus: Option<u16>,

log: slog::Logger,
}

impl ReconfiguratorSim {
fn new(log: slog::Logger) -> Self {
Self {
system: SystemDescription::new(),
collections: IndexMap::new(),
blueprints: IndexMap::new(),
internal_dns: BTreeMap::new(),
external_dns: BTreeMap::new(),
silo_names: vec!["example-silo".parse().unwrap()],
external_dns_zone_name: String::from("oxide.example"),
collection_id_rng: TypedUuidRng::from_entropy(),
num_nexus: None,
log,
}
}

/// Returns true if the user has made local changes to the simulated
/// system.
///
/// This is used when the user asks to load an example system. Doing that
/// basically requires a clean slate.
fn user_made_system_changes(&self) -> bool {
// Use this pattern to ensure that if a new field is added to
// ReconfiguratorSim, it will fail to compile until it's added here.
let Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense but I wonder if we can give more explicit instruction to somebody that's adding a new field? How do they know which category it's in?

All of this has me wondering if we want to separate out these pieces into a separate object that we replace wholesale when needed...but I don't have a concrete suggestion at this point. We can go with this and see how it goes.

Copy link
Contributor Author

@sunshowers sunshowers Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah thinking about this, maybe we can introduce notions of a "system reset" (only system, not policy), a "policy reset", and a "full reset" (system and policy). Then to load an example, you'd need to do a system reset but not a policy reset.

Copy link
Contributor Author

@sunshowers sunshowers Oct 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started pulling this thread and it got out of hand a bit:

Cargo.lock                                                   |   16 +
Cargo.toml                                                   |    1 +
dev-tools/reconfigurator-cli/Cargo.toml                      |    3 +
dev-tools/reconfigurator-cli/src/dispatch.rs                 |   74 +++
dev-tools/reconfigurator-cli/src/lib.rs                      |   12 +
dev-tools/reconfigurator-cli/src/main.rs                     | 1310 +-------------------------------------------------
dev-tools/reconfigurator-cli/src/repl.rs                     | 1246 +++++++++++++++++++++++++++++++++++++++++++++++
dev-tools/reconfigurator-cli/src/rng.rs                      |  108 +++++
dev-tools/reconfigurator-cli/src/sim.rs                      |  346 +++++++++++++
dev-tools/reconfigurator-cli/tests/input/cmds-example.txt    |   26 +
dev-tools/reconfigurator-cli/tests/output/cmd-example-stderr |    0
dev-tools/reconfigurator-cli/tests/output/cmd-example-stdout |  526 ++++++++++++++++++++
dev-tools/reconfigurator-cli/tests/test_basic.rs             |   15 +
nexus/reconfigurator/planning/src/example.rs                 |    2 +-
nexus/reconfigurator/planning/src/system.rs                  |   12 +-
nexus/types/src/deployment/execution/dns.rs                  |    4 +-
16 files changed, 2390 insertions(+), 1311 deletions(-)

I'm going to tackle this in a separate followup PR.

system,
collections,
blueprints,
internal_dns,
external_dns,
// For purposes of this method, we let these policy parameters be
// set to any arbitrary value. This lets example systems be
// generated using these values.
silo_names: _,
external_dns_zone_name: _,
collection_id_rng: _,
num_nexus: _,
log: _,
} = self;

system.has_sleds()
|| !collections.is_empty()
|| !blueprints.is_empty()
|| !internal_dns.is_empty()
|| !external_dns.is_empty()
}

// Reset the state of the REPL.
fn wipe(&mut self) {
*self = Self::new(self.log.clone());
}

fn blueprint_lookup(&self, id: Uuid) -> Result<&Blueprint, anyhow::Error> {
self.blueprints
.get(&id)
Expand Down Expand Up @@ -181,22 +240,12 @@ fn main() -> anyhow::Result<()> {
let cmd = CmdReconfiguratorSim::parse();

let log = dropshot::ConfigLogging::StderrTerminal {
level: dropshot::ConfigLoggingLevel::Debug,
level: dropshot::ConfigLoggingLevel::Info,
}
.to_logger("reconfigurator-sim")
.context("creating logger")?;

let mut sim = ReconfiguratorSim {
system: SystemDescription::new(),
collections: IndexMap::new(),
blueprints: IndexMap::new(),
internal_dns: BTreeMap::new(),
external_dns: BTreeMap::new(),
log,
silo_names: vec!["example-silo".parse().unwrap()],
external_dns_zone_name: String::from("oxide.example"),
num_nexus: None,
};
let mut sim = ReconfiguratorSim::new(log);

if let Some(input_file) = cmd.input_file {
let file = std::fs::File::open(&input_file)
Expand Down Expand Up @@ -310,8 +359,10 @@ fn process_entry(sim: &mut ReconfiguratorSim, entry: String) -> LoopResult {
Commands::Show => cmd_show(sim),
Commands::Set(args) => cmd_set(sim, args),
Commands::Load(args) => cmd_load(sim, args),
Commands::LoadExample(args) => cmd_load_example(sim, args),
Commands::FileContents(args) => cmd_file_contents(args),
Commands::Save(args) => cmd_save(sim, args),
Commands::Wipe => cmd_wipe(sim),
};

match cmd_result {
Expand Down Expand Up @@ -380,8 +431,12 @@ enum Commands {
Save(SaveArgs),
/// load state from a file
Load(LoadArgs),
/// generate and load an example system
LoadExample(LoadExampleArgs),
/// show information about what's in a saved file
FileContents(FileContentsArgs),
/// reset the state of the REPL
Wipe,
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -511,6 +566,33 @@ struct LoadArgs {
collection_id: Option<CollectionUuid>,
}

#[derive(Debug, Args)]
struct LoadExampleArgs {
/// Seed for the RNG that's used to generate the example system.
///
/// Setting this makes it possible for callers to get deterministic
/// results. In automated tests, the seed is typically the name of the
/// test.
#[clap(long, default_value = "reconfigurator_cli_example")]
seed: String,

/// The number of sleds in the example system.
#[clap(short = 's', long, default_value_t = ExampleSystemBuilder::DEFAULT_N_SLEDS)]
nsleds: usize,

/// The number of disks per sled in the example system.
#[clap(short = 'd', long, default_value_t = SledBuilder::DEFAULT_NPOOLS)]
ndisks_per_sled: u8,

/// Do not create zones in the example system.
#[clap(short = 'Z', long)]
no_zones: bool,

/// Do not create entries for disks in the blueprint.
#[clap(long)]
no_disks_in_blueprint: bool,
}

#[derive(Debug, Args)]
struct FileContentsArgs {
/// input file
Expand Down Expand Up @@ -675,7 +757,12 @@ fn cmd_inventory_generate(
)
.context("recording Omicron zones")?;
}
let inventory = builder.build();

let mut inventory = builder.build();
// Assign collection IDs from the RNG. This enables consistent results when
// callers have explicitly seeded the RNG (e.g., in tests).
inventory.id = sim.collection_id_rng.next();

let rv = format!(
"generated inventory collection {} from configured sleds",
inventory.id
Expand Down Expand Up @@ -848,7 +935,7 @@ fn cmd_blueprint_diff(
// Diff'ing DNS is a little trickier. First, compute what DNS should be for
// each blueprint. To do that we need to construct a list of sleds suitable
// for the executor.
let sleds_by_id = make_sleds_by_id(&sim)?;
let sleds_by_id = make_sleds_by_id(&sim.system)?;
let internal_dns_config1 = blueprint_internal_dns_config(
&blueprint1,
&sleds_by_id,
Expand Down Expand Up @@ -881,10 +968,9 @@ fn cmd_blueprint_diff(
}

fn make_sleds_by_id(
sim: &ReconfiguratorSim,
system: &SystemDescription,
) -> Result<BTreeMap<SledUuid, execution::Sled>, anyhow::Error> {
let collection = sim
.system
let collection = system
.to_collection_builder()
.context(
"unexpectedly failed to create collection for current set of sleds",
Expand Down Expand Up @@ -924,7 +1010,7 @@ fn cmd_blueprint_diff_dns(

let blueprint_dns_zone = match dns_group {
CliDnsGroup::Internal => {
let sleds_by_id = make_sleds_by_id(sim)?;
let sleds_by_id = make_sleds_by_id(&sim.system)?;
blueprint_internal_dns_config(
blueprint,
&sleds_by_id,
Expand Down Expand Up @@ -1003,6 +1089,11 @@ fn cmd_save(
)))
}

fn cmd_wipe(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
sim.wipe();
Ok(Some("wiped reconfigurator-sim state".to_string()))
}

fn cmd_show(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
let mut s = String::new();
do_print_properties(&mut s, sim);
Expand Down Expand Up @@ -1275,6 +1366,71 @@ fn cmd_load(
Ok(Some(s))
}

fn cmd_load_example(
sim: &mut ReconfiguratorSim,
args: LoadExampleArgs,
) -> anyhow::Result<Option<String>> {
if sim.user_made_system_changes() {
bail!(
"changes made to simulated system: run `wipe system` before \
loading an example system"
);
}

// Generate the example system.
let (example, blueprint) = ExampleSystemBuilder::new(&sim.log, &args.seed)
.nsleds(args.nsleds)
.ndisks_per_sled(args.ndisks_per_sled)
.nexus_count(sim.num_nexus.map_or(NEXUS_REDUNDANCY, |n| n.into()))
.create_zones(!args.no_zones)
.create_disks_in_blueprint(!args.no_disks_in_blueprint)
.build();

// Generate the internal and external DNS configs based on the blueprint.
let sleds_by_id = make_sleds_by_id(&example.system)?;
let internal_dns = blueprint_internal_dns_config(
&blueprint,
&sleds_by_id,
&Default::default(),
)?;
let external_dns = blueprint_external_dns_config(
&blueprint,
&sim.silo_names,
sim.external_dns_zone_name.clone(),
);

// No more fallible operations from here on out: set the system state.
let collection_id = example.collection.id;
let blueprint_id = blueprint.id;
sim.system = example.system;
sim.collections.insert(collection_id, example.collection);
sim.internal_dns.insert(
blueprint.internal_dns_version,
DnsConfigParams {
generation: blueprint.internal_dns_version.into(),
time_created: Utc::now(),
zones: vec![internal_dns],
},
);
sim.external_dns.insert(
blueprint.external_dns_version,
DnsConfigParams {
generation: blueprint.external_dns_version.into(),
time_created: Utc::now(),
zones: vec![external_dns],
},
);
sim.blueprints.insert(blueprint.id, blueprint);
sim.collection_id_rng =
TypedUuidRng::from_seed(&args.seed, "reconfigurator-cli");

Ok(Some(format!(
"loaded example system with:\n\
- collection: {collection_id}\n\
- blueprint: {blueprint_id}",
)))
}

fn cmd_file_contents(args: FileContentsArgs) -> anyhow::Result<Option<String>> {
let loaded = read_file(&args.filename)?;

Expand Down
26 changes: 26 additions & 0 deletions dev-tools/reconfigurator-cli/tests/input/cmds-example.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
load-example --seed test-basic
load-example --seed test-basic

show

sled-list
inventory-list
blueprint-list

sled-show 2eb69596-f081-4e2d-9425-9994926e0832
blueprint-show ade5749d-bdf3-4fab-a8ae-00bea01b3a5a

blueprint-diff-inventory 9e187896-7809-46d0-9210-d75be1b3c4d4 ade5749d-bdf3-4fab-a8ae-00bea01b3a5a

inventory-generate
blueprint-diff-inventory b32394d8-7d79-486f-8657-fd5219508181 ade5749d-bdf3-4fab-a8ae-00bea01b3a5a

wipe
load-example --seed test-basic --nsleds 1 --ndisks-per-sled 4 --no-zones

sled-list
inventory-list
blueprint-list

sled-show 89d02b1b-478c-401a-8e28-7a26f74fa41b
blueprint-show ade5749d-bdf3-4fab-a8ae-00bea01b3a5a
Empty file.
Loading
Loading