Skip to content

Commit 1e12c0e

Browse files
authored
[reconfigurator-cli] disallow loading state into non-empty systems (#6891)
Currently, the `cmd_load` command doesn't require an empty system and lets you effectively merge two states. However, one of the things it does is overwrite the internal and external DNS maps, which means that existing blueprints can end up pointing to DNS generations that don't have any correspondence. The easiest way to address this issue is to change the `load` function to require an empty system, similar to `load-example`. Make that change here, and add some tests for it. The load function also becomes somewhat simpler, since a bunch of checks that currently exist can go away.
1 parent 14edbf3 commit 1e12c0e

File tree

5 files changed

+112
-77
lines changed

5 files changed

+112
-77
lines changed

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

+31-72
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ use nexus_types::deployment::BlueprintZoneFilter;
2929
use nexus_types::deployment::OmicronZoneNic;
3030
use nexus_types::deployment::PlanningInput;
3131
use nexus_types::deployment::SledFilter;
32-
use nexus_types::deployment::SledLookupErrorKind;
3332
use nexus_types::deployment::{Blueprint, UnstableReconfiguratorState};
3433
use nexus_types::internal_api::params::DnsConfigParams;
3534
use nexus_types::inventory::Collection;
@@ -152,18 +151,6 @@ impl ReconfiguratorSim {
152151
assert!(previous.is_none());
153152
}
154153

155-
fn blueprint_insert_loaded(
156-
&mut self,
157-
blueprint: Blueprint,
158-
) -> Result<(), anyhow::Error> {
159-
let entry = self.blueprints.entry(blueprint.id);
160-
if let indexmap::map::Entry::Occupied(_) = &entry {
161-
return Err(anyhow!("blueprint already exists: {}", blueprint.id));
162-
}
163-
let _ = entry.or_insert(blueprint);
164-
Ok(())
165-
}
166-
167154
fn planning_input(
168155
&self,
169156
parent_blueprint: &Blueprint,
@@ -1162,6 +1149,10 @@ fn cmd_load(
11621149
sim: &mut ReconfiguratorSim,
11631150
args: LoadArgs,
11641151
) -> anyhow::Result<Option<String>> {
1152+
if sim.user_made_system_changes() {
1153+
bail!("changes made to simulated system: run `wipe` before loading");
1154+
}
1155+
11651156
let input_path = args.filename;
11661157
let collection_id = args.collection_id;
11671158
let loaded = read_file(&input_path)?;
@@ -1205,44 +1196,9 @@ fn cmd_load(
12051196
},
12061197
)?;
12071198

1208-
let current_planning_input = sim
1209-
.system
1210-
.to_planning_input_builder()
1211-
.context("generating planning input")?
1212-
.build();
12131199
for (sled_id, sled_details) in
12141200
loaded.planning_input.all_sleds(SledFilter::Commissioned)
12151201
{
1216-
match current_planning_input
1217-
.sled_lookup(SledFilter::Commissioned, sled_id)
1218-
{
1219-
Ok(_) => {
1220-
swriteln!(
1221-
s,
1222-
"sled {}: skipped (one with \
1223-
the same id is already loaded)",
1224-
sled_id
1225-
);
1226-
continue;
1227-
}
1228-
Err(error) => match error.kind() {
1229-
SledLookupErrorKind::Filtered { .. } => {
1230-
swriteln!(
1231-
s,
1232-
"error: load sled {}: turning a decommissioned sled \
1233-
into a commissioned one is not supported",
1234-
sled_id
1235-
);
1236-
continue;
1237-
}
1238-
SledLookupErrorKind::Missing => {
1239-
// A sled being missing from the input is the only case in
1240-
// which we decide to load new sleds. The logic to do that
1241-
// is below.
1242-
}
1243-
},
1244-
}
1245-
12461202
let Some(inventory_sled_agent) =
12471203
primary_collection.sled_agents.get(&sled_id)
12481204
else {
@@ -1290,32 +1246,38 @@ fn cmd_load(
12901246
}
12911247

12921248
for collection in loaded.collections {
1293-
if sim.collections.contains_key(&collection.id) {
1294-
swriteln!(
1295-
s,
1296-
"collection {}: skipped (one with the \
1297-
same id is already loaded)",
1298-
collection.id
1299-
);
1300-
} else {
1301-
swriteln!(s, "collection {} loaded", collection.id);
1302-
sim.collections.insert(collection.id, collection);
1249+
match sim.collections.entry(collection.id) {
1250+
indexmap::map::Entry::Occupied(_) => {
1251+
// We started with an empty system, so the only way we can hit
1252+
// this is if the serialized state contains a duplicate
1253+
// collection ID.
1254+
swriteln!(
1255+
s,
1256+
"error: collection {} skipped (duplicate found)",
1257+
collection.id
1258+
)
1259+
}
1260+
indexmap::map::Entry::Vacant(entry) => {
1261+
swriteln!(s, "collection {} loaded", collection.id);
1262+
entry.insert(collection);
1263+
}
13031264
}
13041265
}
13051266

13061267
for blueprint in loaded.blueprints {
1307-
let blueprint_id = blueprint.id;
1308-
match sim.blueprint_insert_loaded(blueprint) {
1309-
Ok(_) => {
1310-
swriteln!(s, "blueprint {} loaded", blueprint_id);
1311-
}
1312-
Err(error) => {
1268+
match sim.blueprints.entry(blueprint.id) {
1269+
// We started with an empty system, so the only way we can hit this
1270+
// is if the serialized state contains a duplicate blueprint ID.
1271+
indexmap::map::Entry::Occupied(_) => {
13131272
swriteln!(
13141273
s,
1315-
"blueprint {}: skipped ({:#})",
1316-
blueprint_id,
1317-
error
1318-
);
1274+
"error: blueprint {} skipped (duplicate found)",
1275+
blueprint.id
1276+
)
1277+
}
1278+
indexmap::map::Entry::Vacant(entry) => {
1279+
swriteln!(s, "blueprint {} loaded", blueprint.id);
1280+
entry.insert(blueprint);
13191281
}
13201282
}
13211283
}
@@ -1356,10 +1318,7 @@ fn cmd_load_example(
13561318
args: LoadExampleArgs,
13571319
) -> anyhow::Result<Option<String>> {
13581320
if sim.user_made_system_changes() {
1359-
bail!(
1360-
"changes made to simulated system: run `wipe system` before \
1361-
loading an example system"
1362-
);
1321+
bail!("changes made to simulated system: run `wipe` before loading");
13631322
}
13641323

13651324
// Generate the example system.

dev-tools/reconfigurator-cli/tests/input/cmds.txt

+7
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ sled-list
1212

1313
inventory-generate
1414
inventory-list
15+
16+
save state.json
17+
load state.json
18+
19+
wipe
20+
load state.json
21+
sled-show dde1c0e2-b10d-4621-b420-f179f7a7a00a

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ loaded example system with:
44
- blueprint: ade5749d-bdf3-4fab-a8ae-00bea01b3a5a
55

66
> load-example --seed test-basic
7-
error: changes made to simulated system: run `wipe system` before loading an example system
7+
error: changes made to simulated system: run `wipe` before loading
88

99
>
1010

dev-tools/reconfigurator-cli/tests/output/cmd-stdout

+53
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,56 @@ generated inventory collection ..........<REDACTED_UUID>........... from configu
6666
ID NERRORS TIME_DONE
6767
..........<REDACTED_UUID>........... 0 <REDACTED_TIMESTAMP>
6868

69+
>
70+
71+
> save state.json
72+
saved planning input, collections, and blueprints to "state.json"
73+
74+
> load state.json
75+
error: changes made to simulated system: run `wipe` before loading
76+
77+
>
78+
79+
> wipe
80+
wiped reconfigurator-sim state
81+
82+
> load state.json
83+
using collection ..........<REDACTED_UUID>........... as source of sled inventory data
84+
sled ..........<REDACTED_UUID>........... loaded
85+
sled ..........<REDACTED_UUID>........... loaded
86+
sled ..........<REDACTED_UUID>........... loaded
87+
collection ..........<REDACTED_UUID>........... loaded
88+
loaded service IP pool ranges: [V4(Ipv4Range { first: 192.0.2.2, last: 192.0.2.20 })]
89+
configured external DNS zone name: oxide.example
90+
configured silo names: example-silo
91+
internal DNS generations:
92+
external DNS generations:
93+
loaded data from "state.json"
94+
95+
96+
> sled-show ..........<REDACTED_UUID>...........
97+
sled ..........<REDACTED_UUID>...........
98+
subnet fd00:1122:3344:101::/64
99+
zpools (10):
100+
..........<REDACTED_UUID>........... (zpool)
101+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
102+
..........<REDACTED_UUID>........... (zpool)
103+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
104+
..........<REDACTED_UUID>........... (zpool)
105+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
106+
..........<REDACTED_UUID>........... (zpool)
107+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
108+
..........<REDACTED_UUID>........... (zpool)
109+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
110+
..........<REDACTED_UUID>........... (zpool)
111+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
112+
..........<REDACTED_UUID>........... (zpool)
113+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
114+
..........<REDACTED_UUID>........... (zpool)
115+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
116+
..........<REDACTED_UUID>........... (zpool)
117+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
118+
..........<REDACTED_UUID>........... (zpool)
119+
↳ SledDisk { disk_identity: DiskIdentity { vendor: "fake-vendor", model: "fake-model", serial: "serial-..........<REDACTED_UUID>..........." }, disk_id: ..........<REDACTED_UUID>........... (physical_disk), policy: InService, state: Active }
120+
121+

dev-tools/reconfigurator-cli/tests/test_basic.rs

+20-4
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,34 @@ use std::path::PathBuf;
3030
use std::sync::Arc;
3131
use std::time::Duration;
3232
use subprocess::Exec;
33+
use subprocess::ExitStatus;
3334
use swrite::swriteln;
3435
use swrite::SWrite;
3536

3637
fn path_to_cli() -> PathBuf {
3738
path_to_executable(env!("CARGO_BIN_EXE_reconfigurator-cli"))
3839
}
3940

41+
fn run_cli(file: impl AsRef<Utf8Path>) -> (ExitStatus, String, String) {
42+
let file = file.as_ref();
43+
44+
// Turn the path into an absolute one, because we're going to set a custom
45+
// cwd for the subprocess.
46+
let file = file.canonicalize_utf8().expect("file canonicalized");
47+
eprintln!("using file: {file}");
48+
49+
// Create a temporary directory for the CLI to use -- that will let it
50+
// read and write files in its own sandbox.
51+
let tmpdir = camino_tempfile::tempdir().expect("failed to create tmpdir");
52+
let exec = Exec::cmd(path_to_cli()).arg(file).cwd(tmpdir.path());
53+
run_command(exec)
54+
}
55+
4056
// Run a battery of simple commands and make sure things basically seem to work.
4157
#[test]
4258
fn test_basic() {
43-
let exec = Exec::cmd(path_to_cli()).arg("tests/input/cmds.txt");
44-
let (exit_status, stdout_text, stderr_text) = run_command(exec);
59+
let (exit_status, stdout_text, stderr_text) =
60+
run_cli("tests/input/cmds.txt");
4561
assert_exit_code(exit_status, EXIT_SUCCESS, &stderr_text);
4662
let stdout_text = Redactor::default().do_redact(&stdout_text);
4763
assert_contents("tests/output/cmd-stdout", &stdout_text);
@@ -51,8 +67,8 @@ fn test_basic() {
5167
// Run tests against a loaded example system.
5268
#[test]
5369
fn test_example() {
54-
let exec = Exec::cmd(path_to_cli()).arg("tests/input/cmds-example.txt");
55-
let (exit_status, stdout_text, stderr_text) = run_command(exec);
70+
let (exit_status, stdout_text, stderr_text) =
71+
run_cli("tests/input/cmds-example.txt");
5672
assert_exit_code(exit_status, EXIT_SUCCESS, &stderr_text);
5773

5874
// The example system uses a fixed seed, which means that UUIDs are

0 commit comments

Comments
 (0)