@@ -29,8 +29,8 @@ use nexus_reconfigurator_planning::system::{
2929 RotStateOverrides , SledBuilder , SledInventoryVisibility , SystemDescription ,
3030} ;
3131use nexus_reconfigurator_simulation:: {
32- BlueprintId , CollectionId , GraphRenderOptions , GraphStartingState ,
33- ReconfiguratorSimId , SimState ,
32+ BlueprintId , CollectionId , DisplayUuidPrefix , GraphRenderOptions ,
33+ GraphStartingState , ReconfiguratorSimId , ReconfiguratorSimOpId , SimState ,
3434} ;
3535use nexus_reconfigurator_simulation:: { SimStateBuilder , SimTufRepoSource } ;
3636use nexus_reconfigurator_simulation:: { SimTufRepoDescription , Simulator } ;
@@ -62,6 +62,7 @@ use omicron_repl_utils::run_repl_from_file;
6262use omicron_repl_utils:: run_repl_on_stdin;
6363use omicron_uuid_kinds:: GenericUuid ;
6464use omicron_uuid_kinds:: OmicronZoneUuid ;
65+ use omicron_uuid_kinds:: ReconfiguratorSimOpUuid ;
6566use omicron_uuid_kinds:: ReconfiguratorSimStateUuid ;
6667use omicron_uuid_kinds:: SledUuid ;
6768use omicron_uuid_kinds:: VnicUuid ;
@@ -93,30 +94,23 @@ pub mod test_utils;
9394struct 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
102101impl 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 ) ]
16731764struct 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+
30343180fn cmd_wipe (
30353181 sim : & mut ReconfiguratorSim ,
30363182 args : WipeArgs ,
0 commit comments