6
6
7
7
use anyhow:: { anyhow, bail, Context } ;
8
8
use camino:: Utf8PathBuf ;
9
+ use chrono:: Utc ;
9
10
use clap:: CommandFactory ;
10
11
use clap:: FromArgMatches ;
11
12
use clap:: ValueEnum ;
@@ -15,6 +16,7 @@ use internal_dns_types::diff::DnsDiff;
15
16
use nexus_inventory:: CollectionBuilder ;
16
17
use nexus_reconfigurator_planning:: blueprint_builder:: BlueprintBuilder ;
17
18
use nexus_reconfigurator_planning:: blueprint_builder:: EnsureMultiple ;
19
+ use nexus_reconfigurator_planning:: example:: ExampleSystemBuilder ;
18
20
use nexus_reconfigurator_planning:: planner:: Planner ;
19
21
use nexus_reconfigurator_planning:: system:: {
20
22
SledBuilder , SledHwInventory , SystemDescription ,
@@ -34,6 +36,8 @@ use nexus_types::internal_api::params::DnsConfigParams;
34
36
use nexus_types:: inventory:: Collection ;
35
37
use omicron_common:: api:: external:: Generation ;
36
38
use omicron_common:: api:: external:: Name ;
39
+ use omicron_common:: policy:: NEXUS_REDUNDANCY ;
40
+ use omicron_uuid_kinds:: CollectionKind ;
37
41
use omicron_uuid_kinds:: CollectionUuid ;
38
42
use omicron_uuid_kinds:: GenericUuid ;
39
43
use omicron_uuid_kinds:: OmicronZoneUuid ;
@@ -45,6 +49,7 @@ use std::collections::BTreeMap;
45
49
use std:: io:: BufRead ;
46
50
use swrite:: { swriteln, SWrite } ;
47
51
use tabled:: Tabled ;
52
+ use typed_rng:: TypedUuidRng ;
48
53
use uuid:: Uuid ;
49
54
50
55
/// REPL state
@@ -76,13 +81,67 @@ struct ReconfiguratorSim {
76
81
/// External DNS zone name configured
77
82
external_dns_zone_name : String ,
78
83
84
+ /// RNG for collection IDs
85
+ collection_id_rng : TypedUuidRng < CollectionKind > ,
86
+
79
87
/// Policy overrides
80
88
num_nexus : Option < u16 > ,
81
89
82
90
log : slog:: Logger ,
83
91
}
84
92
85
93
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
+
86
145
fn blueprint_lookup ( & self , id : Uuid ) -> Result < & Blueprint , anyhow:: Error > {
87
146
self . blueprints
88
147
. get ( & id)
@@ -181,22 +240,12 @@ fn main() -> anyhow::Result<()> {
181
240
let cmd = CmdReconfiguratorSim :: parse ( ) ;
182
241
183
242
let log = dropshot:: ConfigLogging :: StderrTerminal {
184
- level : dropshot:: ConfigLoggingLevel :: Debug ,
243
+ level : dropshot:: ConfigLoggingLevel :: Info ,
185
244
}
186
245
. to_logger ( "reconfigurator-sim" )
187
246
. context ( "creating logger" ) ?;
188
247
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) ;
200
249
201
250
if let Some ( input_file) = cmd. input_file {
202
251
let file = std:: fs:: File :: open ( & input_file)
@@ -310,8 +359,10 @@ fn process_entry(sim: &mut ReconfiguratorSim, entry: String) -> LoopResult {
310
359
Commands :: Show => cmd_show ( sim) ,
311
360
Commands :: Set ( args) => cmd_set ( sim, args) ,
312
361
Commands :: Load ( args) => cmd_load ( sim, args) ,
362
+ Commands :: LoadExample ( args) => cmd_load_example ( sim, args) ,
313
363
Commands :: FileContents ( args) => cmd_file_contents ( args) ,
314
364
Commands :: Save ( args) => cmd_save ( sim, args) ,
365
+ Commands :: Wipe => cmd_wipe ( sim) ,
315
366
} ;
316
367
317
368
match cmd_result {
@@ -380,8 +431,12 @@ enum Commands {
380
431
Save ( SaveArgs ) ,
381
432
/// load state from a file
382
433
Load ( LoadArgs ) ,
434
+ /// generate and load an example system
435
+ LoadExample ( LoadExampleArgs ) ,
383
436
/// show information about what's in a saved file
384
437
FileContents ( FileContentsArgs ) ,
438
+ /// reset the state of the REPL
439
+ Wipe ,
385
440
}
386
441
387
442
#[ derive( Debug , Args ) ]
@@ -511,6 +566,33 @@ struct LoadArgs {
511
566
collection_id : Option < CollectionUuid > ,
512
567
}
513
568
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
+
514
596
#[ derive( Debug , Args ) ]
515
597
struct FileContentsArgs {
516
598
/// input file
@@ -675,7 +757,12 @@ fn cmd_inventory_generate(
675
757
)
676
758
. context ( "recording Omicron zones" ) ?;
677
759
}
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
+
679
766
let rv = format ! (
680
767
"generated inventory collection {} from configured sleds" ,
681
768
inventory. id
@@ -848,7 +935,7 @@ fn cmd_blueprint_diff(
848
935
// Diff'ing DNS is a little trickier. First, compute what DNS should be for
849
936
// each blueprint. To do that we need to construct a list of sleds suitable
850
937
// for the executor.
851
- let sleds_by_id = make_sleds_by_id ( & sim) ?;
938
+ let sleds_by_id = make_sleds_by_id ( & sim. system ) ?;
852
939
let internal_dns_config1 = blueprint_internal_dns_config (
853
940
& blueprint1,
854
941
& sleds_by_id,
@@ -881,10 +968,9 @@ fn cmd_blueprint_diff(
881
968
}
882
969
883
970
fn make_sleds_by_id (
884
- sim : & ReconfiguratorSim ,
971
+ system : & SystemDescription ,
885
972
) -> Result < BTreeMap < SledUuid , execution:: Sled > , anyhow:: Error > {
886
- let collection = sim
887
- . system
973
+ let collection = system
888
974
. to_collection_builder ( )
889
975
. context (
890
976
"unexpectedly failed to create collection for current set of sleds" ,
@@ -924,7 +1010,7 @@ fn cmd_blueprint_diff_dns(
924
1010
925
1011
let blueprint_dns_zone = match dns_group {
926
1012
CliDnsGroup :: Internal => {
927
- let sleds_by_id = make_sleds_by_id ( sim) ?;
1013
+ let sleds_by_id = make_sleds_by_id ( & sim. system ) ?;
928
1014
blueprint_internal_dns_config (
929
1015
blueprint,
930
1016
& sleds_by_id,
@@ -1003,6 +1089,11 @@ fn cmd_save(
1003
1089
) ) )
1004
1090
}
1005
1091
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
+
1006
1097
fn cmd_show ( sim : & mut ReconfiguratorSim ) -> anyhow:: Result < Option < String > > {
1007
1098
let mut s = String :: new ( ) ;
1008
1099
do_print_properties ( & mut s, sim) ;
@@ -1275,6 +1366,71 @@ fn cmd_load(
1275
1366
Ok ( Some ( s) )
1276
1367
}
1277
1368
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
+
1278
1434
fn cmd_file_contents ( args : FileContentsArgs ) -> anyhow:: Result < Option < String > > {
1279
1435
let loaded = read_file ( & args. filename ) ?;
1280
1436
0 commit comments