Skip to content

Commit 408f043

Browse files
authored
[3/n] [reconfigurator-sim] add an operation log (#9365)
Add an operation log to the simulator, as well as ways to undo, redo and restore to the last operation. Modeled pretty directly after how Jujutsu's operation log works. The biggest change here is that the current pointer is now managed by the simulator rather than reconfigurator-cli, so that it can be versioned as part of the operation.
1 parent be2bc89 commit 408f043

File tree

12 files changed

+1202
-52
lines changed

12 files changed

+1202
-52
lines changed

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

Lines changed: 163 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ use nexus_reconfigurator_planning::system::{
2929
RotStateOverrides, SledBuilder, SledInventoryVisibility, SystemDescription,
3030
};
3131
use nexus_reconfigurator_simulation::{
32-
BlueprintId, CollectionId, GraphRenderOptions, GraphStartingState,
33-
ReconfiguratorSimId, SimState,
32+
BlueprintId, CollectionId, DisplayUuidPrefix, GraphRenderOptions,
33+
GraphStartingState, ReconfiguratorSimId, ReconfiguratorSimOpId, SimState,
3434
};
3535
use nexus_reconfigurator_simulation::{SimStateBuilder, SimTufRepoSource};
3636
use nexus_reconfigurator_simulation::{SimTufRepoDescription, Simulator};
@@ -62,6 +62,7 @@ use omicron_repl_utils::run_repl_from_file;
6262
use omicron_repl_utils::run_repl_on_stdin;
6363
use omicron_uuid_kinds::GenericUuid;
6464
use omicron_uuid_kinds::OmicronZoneUuid;
65+
use omicron_uuid_kinds::ReconfiguratorSimOpUuid;
6566
use omicron_uuid_kinds::ReconfiguratorSimStateUuid;
6667
use omicron_uuid_kinds::SledUuid;
6768
use omicron_uuid_kinds::VnicUuid;
@@ -93,30 +94,23 @@ pub mod test_utils;
9394
struct ReconfiguratorSim {
9495
// The simulator currently being used.
9596
sim: Simulator,
96-
// The current state.
97-
current: ReconfiguratorSimStateUuid,
9897
// The current system state
9998
log: slog::Logger,
10099
}
101100

102101
impl ReconfiguratorSim {
103102
fn new(log: slog::Logger, seed: Option<String>) -> Self {
104-
Self {
105-
sim: Simulator::new(&log, seed),
106-
current: Simulator::ROOT_ID,
107-
log,
108-
}
103+
Self { sim: Simulator::new(&log, seed), log }
109104
}
110105

111106
fn current_state(&self) -> &SimState {
112107
self.sim
113-
.get_state(self.current)
108+
.get_state(self.sim.current())
114109
.expect("current state should always exist")
115110
}
116111

117112
fn commit_and_bump(&mut self, description: String, state: SimStateBuilder) {
118-
let new_id = state.commit(description, &mut self.sim);
119-
self.current = new_id;
113+
state.commit_and_bump(description, &mut self.sim);
120114
}
121115

122116
fn planning_input(
@@ -483,6 +477,13 @@ fn process_command(
483477
Commands::Save(args) => cmd_save(sim, args),
484478
Commands::State(StateArgs::Log(args)) => cmd_state_log(sim, args),
485479
Commands::State(StateArgs::Switch(args)) => cmd_state_switch(sim, args),
480+
Commands::Op(OpArgs::Log(args)) => cmd_op_log(sim, args),
481+
Commands::Op(OpArgs::Undo) => cmd_op_undo(sim),
482+
Commands::Op(OpArgs::Redo) => cmd_op_redo(sim),
483+
Commands::Op(OpArgs::Restore(args)) => cmd_op_restore(sim, args),
484+
Commands::Op(OpArgs::Wipe) => cmd_op_wipe(sim),
485+
Commands::Undo => cmd_op_undo(sim),
486+
Commands::Redo => cmd_op_redo(sim),
486487
Commands::Wipe(args) => cmd_wipe(sim, args),
487488
};
488489

@@ -584,6 +585,13 @@ enum Commands {
584585
/// state-related commands
585586
#[command(flatten)]
586587
State(StateArgs),
588+
/// operation log commands (undo, redo, restore)
589+
#[command(subcommand)]
590+
Op(OpArgs),
591+
/// undo the last operation (alias for `op undo`)
592+
Undo,
593+
/// redo the last undone operation (alias for `op redo`)
594+
Redo,
587595
/// reset the state of the REPL
588596
Wipe(WipeArgs),
589597
}
@@ -1195,6 +1203,45 @@ impl From<ReconfiguratorSimStateIdOpt> for ReconfiguratorSimId {
11951203
}
11961204
}
11971205

1206+
#[derive(Clone, Debug)]
1207+
enum ReconfiguratorSimOpIdOpt {
1208+
/// use a specific reconfigurator sim operation by full UUID
1209+
Id(ReconfiguratorSimOpUuid),
1210+
/// use a reconfigurator sim operation by UUID prefix
1211+
Prefix(String),
1212+
}
1213+
1214+
impl FromStr for ReconfiguratorSimOpIdOpt {
1215+
type Err = Infallible;
1216+
1217+
fn from_str(s: &str) -> Result<Self, Self::Err> {
1218+
match s.parse::<ReconfiguratorSimOpUuid>() {
1219+
Ok(id) => Ok(ReconfiguratorSimOpIdOpt::Id(id)),
1220+
Err(_) => Ok(ReconfiguratorSimOpIdOpt::Prefix(s.to_owned())),
1221+
}
1222+
}
1223+
}
1224+
1225+
impl fmt::Display for ReconfiguratorSimOpIdOpt {
1226+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1227+
match self {
1228+
ReconfiguratorSimOpIdOpt::Id(id) => id.fmt(f),
1229+
ReconfiguratorSimOpIdOpt::Prefix(prefix) => prefix.fmt(f),
1230+
}
1231+
}
1232+
}
1233+
1234+
impl From<ReconfiguratorSimOpIdOpt> for ReconfiguratorSimOpId {
1235+
fn from(value: ReconfiguratorSimOpIdOpt) -> Self {
1236+
match value {
1237+
ReconfiguratorSimOpIdOpt::Id(id) => ReconfiguratorSimOpId::Id(id),
1238+
ReconfiguratorSimOpIdOpt::Prefix(prefix) => {
1239+
ReconfiguratorSimOpId::Prefix(prefix)
1240+
}
1241+
}
1242+
}
1243+
}
1244+
11981245
/// Clap field for an optional mupdate override UUID.
11991246
///
12001247
/// This structure is similar to `Option`, but is specified separately to:
@@ -1669,6 +1716,50 @@ struct StateSwitchArgs {
16691716
state_id: ReconfiguratorSimStateIdOpt,
16701717
}
16711718

1719+
#[derive(Debug, Subcommand)]
1720+
enum OpArgs {
1721+
/// display the operation log
1722+
///
1723+
/// Shows the history of operations, similar to `jj op log`.
1724+
Log(OpLogArgs),
1725+
/// undo the most recent operation
1726+
///
1727+
/// Creates a new restore operation that goes back to the previous state.
1728+
Undo,
1729+
/// redo a previously undone operation
1730+
///
1731+
/// Creates a new restore operation that goes forward to a previously
1732+
/// undone state.
1733+
Redo,
1734+
/// restore to a specific operation
1735+
///
1736+
/// Creates a new restore operation that sets the heads to match those
1737+
/// of the specified operation.
1738+
Restore(OpRestoreArgs),
1739+
/// wipe the operation log
1740+
///
1741+
/// Clears all operation history and resets to just the root operation.
1742+
/// This is the only operation that violates the append-only principle.
1743+
Wipe,
1744+
}
1745+
1746+
#[derive(Debug, Args)]
1747+
struct OpLogArgs {
1748+
/// Limit number of operations to display
1749+
#[clap(long, short = 'n')]
1750+
limit: Option<usize>,
1751+
1752+
/// Verbose mode: show full UUIDs and heads at each operation
1753+
#[clap(long, short = 'v')]
1754+
verbose: bool,
1755+
}
1756+
1757+
#[derive(Debug, Args)]
1758+
struct OpRestoreArgs {
1759+
/// The operation ID or unique prefix to restore to
1760+
operation_id: ReconfiguratorSimOpIdOpt,
1761+
}
1762+
16721763
#[derive(Debug, Args)]
16731764
struct WipeArgs {
16741765
/// What to wipe
@@ -3005,7 +3096,7 @@ fn cmd_state_log(
30053096
GraphStartingState::None
30063097
};
30073098

3008-
let options = GraphRenderOptions::new(sim.current)
3099+
let options = GraphRenderOptions::new(sim.sim.current())
30093100
.with_verbose(verbose)
30103101
.with_starting_state(starting_state);
30113102

@@ -3020,17 +3111,72 @@ fn cmd_state_switch(
30203111
) -> anyhow::Result<Option<String>> {
30213112
let state = sim.sim.resolve_and_get_state(args.state_id.into())?;
30223113
let target_id = state.id();
3114+
// Need to grab the generation and description here because switch_state
3115+
// below requires mutable access.
3116+
let generation = state.generation();
3117+
let description = state.description().to_owned();
30233118

3024-
sim.current = target_id;
3119+
sim.sim.switch_state(target_id)?;
30253120

30263121
Ok(Some(format!(
30273122
"switched to state {} (generation {}): {}",
3028-
target_id,
3029-
state.generation(),
3030-
state.description()
3123+
target_id, generation, description,
3124+
)))
3125+
}
3126+
3127+
fn cmd_op_log(
3128+
sim: &mut ReconfiguratorSim,
3129+
args: OpLogArgs,
3130+
) -> anyhow::Result<Option<String>> {
3131+
let output = sim.sim.render_operation_graph(args.limit, args.verbose);
3132+
Ok(Some(output))
3133+
}
3134+
3135+
fn cmd_op_undo(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
3136+
sim.sim.operation_undo()?;
3137+
3138+
let current_op = sim.sim.operation_current();
3139+
Ok(Some(format!(
3140+
"created operation {}: {}",
3141+
DisplayUuidPrefix::new(current_op.id(), false),
3142+
current_op.description(false)
30313143
)))
30323144
}
30333145

3146+
fn cmd_op_redo(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
3147+
sim.sim.operation_redo()?;
3148+
3149+
let current_op = sim.sim.operation_current();
3150+
Ok(Some(format!(
3151+
"created operation {}: {}",
3152+
DisplayUuidPrefix::new(current_op.id(), false),
3153+
current_op.description(false)
3154+
)))
3155+
}
3156+
3157+
fn cmd_op_restore(
3158+
sim: &mut ReconfiguratorSim,
3159+
args: OpRestoreArgs,
3160+
) -> anyhow::Result<Option<String>> {
3161+
let target_op =
3162+
sim.sim.resolve_and_get_operation(args.operation_id.into())?;
3163+
let target_id = target_op.id();
3164+
let description = target_op.description(false);
3165+
3166+
sim.sim.operation_restore(target_id)?;
3167+
3168+
Ok(Some(format!(
3169+
"created operation {}: {}",
3170+
DisplayUuidPrefix::new(target_id, false),
3171+
description
3172+
)))
3173+
}
3174+
3175+
fn cmd_op_wipe(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
3176+
sim.sim.operation_wipe();
3177+
Ok(Some("wiped operation log".to_string()))
3178+
}
3179+
30343180
fn cmd_wipe(
30353181
sim: &mut ReconfiguratorSim,
30363182
args: WipeArgs,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Test undo/redo functionality for the operation log
2+
3+
load-example --seed test-undo-redo --nsleds 2 --ndisks-per-sled 3
4+
5+
op log
6+
7+
# Make some changes to create an operation history.
8+
sled-add
9+
sled-add
10+
silo-add test-silo
11+
log
12+
op log
13+
14+
# First, undo the silo-add.
15+
undo
16+
log
17+
op log
18+
19+
# Verify we can undo multiple times: undo the sled-add.
20+
undo
21+
op log
22+
23+
# Redo the sled-add we just undid.
24+
redo
25+
op log
26+
27+
# Redo again to restore the silo-add.
28+
redo
29+
op log
30+
31+
# Make a new change.
32+
silo-add another-silo
33+
op log
34+
35+
# Try to undo when we're already at the root. This should fail.
36+
op wipe
37+
op log
38+
undo
39+
40+
# Try to undo twice with one operation available.
41+
load-example --seed test-undo-redo --nsleds 1
42+
op log
43+
undo
44+
undo
45+
46+
# This redo should work.
47+
redo
48+
op log
49+
50+
# We're out of undos, so this redo should fail.
51+
redo
52+
53+
# Do another undo and redo.
54+
undo
55+
redo
56+
op log
57+
58+
# Test op log with the --verbose flag.
59+
op log -n 5 --verbose
60+
log

dev-tools/reconfigurator-cli/tests/output/cmds-undo-redo-stderr

Whitespace-only changes.

0 commit comments

Comments
 (0)