Skip to content
Open
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
154 changes: 134 additions & 20 deletions dev-tools/reconfigurator-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use nexus_reconfigurator_planning::system::{
RotStateOverrides, SledBuilder, SledInventoryVisibility, SystemDescription,
};
use nexus_reconfigurator_simulation::{
BlueprintId, CollectionId, GraphRenderOptions, SimState,
BlueprintId, CollectionId, DisplayUuidPrefix, GraphRenderOptions, SimState,
};
use nexus_reconfigurator_simulation::{SimStateBuilder, SimTufRepoSource};
use nexus_reconfigurator_simulation::{SimTufRepoDescription, Simulator};
Expand Down Expand Up @@ -61,6 +61,7 @@ use omicron_repl_utils::run_repl_from_file;
use omicron_repl_utils::run_repl_on_stdin;
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::OmicronZoneUuid;
use omicron_uuid_kinds::ReconfiguratorSimOpUuid;
use omicron_uuid_kinds::ReconfiguratorSimStateUuid;
use omicron_uuid_kinds::SledUuid;
use omicron_uuid_kinds::VnicUuid;
Expand Down Expand Up @@ -88,30 +89,23 @@ mod log_capture;
struct ReconfiguratorSim {
// The simulator currently being used.
sim: Simulator,
// The current state.
current: ReconfiguratorSimStateUuid,
// The current system state
log: slog::Logger,
}

impl ReconfiguratorSim {
fn new(log: slog::Logger, seed: Option<String>) -> Self {
Self {
sim: Simulator::new(&log, seed),
current: Simulator::ROOT_ID,
log,
}
Self { sim: Simulator::new(&log, seed), log }
}

fn current_state(&self) -> &SimState {
self.sim
.get_state(self.current)
.get_state(self.sim.current())
.expect("current state should always exist")
}

fn commit_and_bump(&mut self, description: String, state: SimStateBuilder) {
let new_id = state.commit(description, &mut self.sim);
self.current = new_id;
state.commit_and_bump(description, &mut self.sim);
}

fn planning_input(
Expand Down Expand Up @@ -326,6 +320,13 @@ fn process_command(
Commands::Save(args) => cmd_save(sim, args),
Commands::State(StateArgs::Log(args)) => cmd_state_log(sim, args),
Commands::State(StateArgs::Switch(args)) => cmd_state_switch(sim, args),
Commands::Op(OpArgs::Log(args)) => cmd_op_log(sim, args),
Commands::Op(OpArgs::Undo) => cmd_op_undo(sim),
Commands::Op(OpArgs::Redo) => cmd_op_redo(sim),
Commands::Op(OpArgs::Restore(args)) => cmd_op_restore(sim, args),
Commands::Op(OpArgs::Wipe) => cmd_op_wipe(sim),
Commands::Undo => cmd_op_undo(sim),
Commands::Redo => cmd_op_redo(sim),
Commands::Wipe(args) => cmd_wipe(sim, args),
};

Expand Down Expand Up @@ -427,6 +428,13 @@ enum Commands {
/// state-related commands
#[command(flatten)]
State(StateArgs),
/// operation log commands (undo, redo, restore)
#[command(subcommand)]
Op(OpArgs),
/// undo the last operation (alias for `op undo`)
Undo,
/// redo the last undone operation (alias for `op redo`)
Redo,
/// reset the state of the REPL
Wipe(WipeArgs),
}
Expand Down Expand Up @@ -1473,6 +1481,50 @@ struct StateSwitchArgs {
state_id: String,
}

#[derive(Debug, Subcommand)]
enum OpArgs {
/// display the operation log
///
/// Shows the history of operations, similar to `jj op log`.
Log(OpLogArgs),
/// undo the most recent operation
///
/// Creates a new restore operation that goes back to the previous state.
Undo,
/// redo a previously undone operation
///
/// Creates a new restore operation that goes forward to a previously
/// undone state.
Redo,
/// restore to a specific operation
///
/// Creates a new restore operation that sets the heads to match those
/// of the specified operation.
Restore(OpRestoreArgs),
/// wipe the operation log
///
/// Clears all operation history and resets to just the root operation.
/// This is the only operation that violates the append-only principle.
Wipe,
}

#[derive(Debug, Args)]
struct OpLogArgs {
/// Limit number of operations to display
#[clap(long, short = 'n')]
limit: Option<usize>,

/// Verbose mode: show full UUIDs and heads at each operation
#[clap(long, short = 'v')]
verbose: bool,
}

#[derive(Debug, Args)]
struct OpRestoreArgs {
/// The operation ID or unique prefix to restore to
operation_id: String,
}

#[derive(Debug, Args)]
struct WipeArgs {
/// What to wipe
Expand Down Expand Up @@ -2813,7 +2865,7 @@ fn cmd_state_log(
let StateLogArgs { from, limit, verbose } = args;

// Build rendering options.
let options = GraphRenderOptions::new(sim.current)
let options = GraphRenderOptions::new(sim.sim.current())
.with_verbose(verbose)
.with_limit(limit)
.with_from(from);
Expand All @@ -2834,21 +2886,83 @@ fn cmd_state_switch(
Err(_) => sim.sim.get_state_by_prefix(&args.state_id)?,
};

let state = sim
.sim
.get_state(target_id)
.ok_or_else(|| anyhow!("state {} not found", target_id))?;
let (generation, description) = {
let state = sim
.sim
.get_state(target_id)
.ok_or_else(|| anyhow!("state {} not found", target_id))?;
(state.generation(), state.description().to_string())
};

sim.current = target_id;
sim.sim.switch_state(target_id)?;

Ok(Some(format!(
"switched to state {} (generation {}): {}",
target_id,
state.generation(),
state.description()
target_id, generation, description
)))
}

fn cmd_op_log(
sim: &mut ReconfiguratorSim,
args: OpLogArgs,
) -> anyhow::Result<Option<String>> {
let output = sim.sim.render_operation_graph(args.limit, args.verbose);
Ok(Some(output))
}

fn cmd_op_undo(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
sim.sim.operation_undo()?;

let current_op = sim.sim.operation_current();
Ok(Some(format!(
"created operation {}: {}",
DisplayUuidPrefix::new(current_op.id(), false),
current_op.description(false)
)))
}

fn cmd_op_redo(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
sim.sim.operation_redo()?;

let current_op = sim.sim.operation_current();
Ok(Some(format!(
"created operation {}: {}",
DisplayUuidPrefix::new(current_op.id(), false),
current_op.description(false)
)))
}

fn cmd_op_restore(
sim: &mut ReconfiguratorSim,
args: OpRestoreArgs,
) -> anyhow::Result<Option<String>> {
// Try parsing as a full UUID first, then fall back to prefix matching.
let target_id = match args.operation_id.parse::<ReconfiguratorSimOpUuid>() {
Ok(id) => id,
Err(_) => sim.sim.operation_get_by_prefix(&args.operation_id)?,
};

let target_op = sim
.sim
.operation_get(target_id)
.ok_or_else(|| anyhow!("operation {} not found", target_id))?;

let description = target_op.description(false);

sim.sim.operation_restore(target_id)?;

Ok(Some(format!(
"created operation {}: {}",
DisplayUuidPrefix::new(target_id, false),
description
)))
}

fn cmd_op_wipe(sim: &mut ReconfiguratorSim) -> anyhow::Result<Option<String>> {
sim.sim.operation_wipe();
Ok(Some("wiped operation log".to_string()))
}

fn cmd_wipe(
sim: &mut ReconfiguratorSim,
args: WipeArgs,
Expand Down
60 changes: 60 additions & 0 deletions dev-tools/reconfigurator-cli/tests/input/cmds-undo-redo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Test undo/redo functionality for the operation log

load-example --seed test-undo-redo --nsleds 2 --ndisks-per-sled 3

op log

# Make some changes to create an operation history.
sled-add
sled-add
silo-add test-silo
log
op log

# First, undo the silo-add.
undo
log
op log

# Verify we can undo multiple times: undo the sled-add.
undo
op log

# Redo the sled-add we just undid.
redo
op log

# Redo again to restore the silo-add.
redo
op log

# Make a new change.
silo-add another-silo
op log

# Try to undo when we're already at the root. This should fail.
op wipe
op log
undo

# Try to undo twice with one operation available.
load-example --seed test-undo-redo --nsleds 1
op log
undo
undo

# This redo should work.
redo
op log

# We're out of undos, so this redo should fail.
redo

# Do another undo and redo.
undo
redo
op log

# Test op log with the --verbose flag.
op log -n 5 --verbose
log
Empty file.
Loading
Loading