diff --git a/snapshots/expected/test_drivers/commit_failure_automatic_rollback b/snapshots/expected/test_drivers/commit_failure_automatic_rollback index 55ed388..d607b17 100644 --- a/snapshots/expected/test_drivers/commit_failure_automatic_rollback +++ b/snapshots/expected/test_drivers/commit_failure_automatic_rollback @@ -5,4 +5,4 @@ user@device# commit error: commit failed - invalid configuration user@device# rollback user@device# -commands:['configure', 'set invalid config', 'commit', 'exit', 'rollback', 'exit'] \ No newline at end of file +commands:['configure', 'set invalid config', 'commit', 'rollback', 'exit'] \ No newline at end of file diff --git a/snapshots/expected/test_drivers/configuration_and_commit_flow b/snapshots/expected/test_drivers/configuration_and_commit_flow index ee60a30..32066e9 100644 --- a/snapshots/expected/test_drivers/configuration_and_commit_flow +++ b/snapshots/expected/test_drivers/configuration_and_commit_flow @@ -2,6 +2,7 @@ session_log:configure user@device# set interfaces ge-0/0/0 description test user@device# commit commit complete -user@device# +user@device# exit +user@device> state:ready commands:['configure', 'set interfaces ge-0/0/0 description test', 'commit', 'exit'] \ No newline at end of file diff --git a/snapshots/expected/test_drivers/iosxr_commit_failure_staged_commit_flow b/snapshots/expected/test_drivers/iosxr_commit_failure_staged_commit_flow new file mode 100644 index 0000000..56897ac --- /dev/null +++ b/snapshots/expected/test_drivers/iosxr_commit_failure_staged_commit_flow @@ -0,0 +1,3 @@ +error:Exception: Configuration failed and was aborted +state:ready +commands:['configure terminal', 'service-policy output FOOBAR', 'commit', 'abort', 'end'] \ No newline at end of file diff --git a/src/drivers.act b/src/drivers.act index 873081e..61293c1 100644 --- a/src/drivers.act +++ b/src/drivers.act @@ -91,6 +91,7 @@ class BaseDriver(Driver): _command_callback: ?action(err: ?Exception, response: ?str) -> None _config_callback: ?action(err: ?Exception, session_log: str) -> None _config_commands: list[str] + _pending_commit_commands: list[str] _rollback_commits: int def __init__(self, device_type: str, ssh_client: SSHClientWrapper, log: logging.Logger): @@ -104,6 +105,7 @@ class BaseDriver(Driver): self._command_callback = None self._config_callback = None self._config_commands = [] + self._pending_commit_commands = [] self._rollback_commits = 0 def initialize(self) -> None: @@ -164,6 +166,7 @@ class BaseDriver(Driver): if config_callback is not None: self._config_callback = None self._config_commands = [] + self._pending_commit_commands = [] config_callback(error, self._session_log) @@ -231,8 +234,9 @@ class BaseDriver(Driver): else: # No config commands, go straight to commit self._transition_to_state(DRIVER_STATE_COMMITTING) - commit_cmds = self._get_commit_commands() - for cmd in commit_cmds: + self._pending_commit_commands = self._get_commit_commands().copy() + if len(self._pending_commit_commands) > 0: + cmd = self._pending_commit_commands.pop(0) self.ssh_client.send_str(cmd + "\n") self._output_buffer = "" @@ -250,8 +254,9 @@ class BaseDriver(Driver): else: # All config commands sent, proceed to commit self._transition_to_state(DRIVER_STATE_COMMITTING) - commit_cmds = self._get_commit_commands() - for cmd in commit_cmds: + self._pending_commit_commands = self._get_commit_commands().copy() + if len(self._pending_commit_commands) > 0: + cmd = self._pending_commit_commands.pop(0) self.ssh_client.send_str(cmd + "\n") self._output_buffer = "" @@ -259,7 +264,14 @@ class BaseDriver(Driver): # Check if commit succeeded or failed if self._commit_failed(self._output_buffer): # Commit failed - trigger abort/rollback + self._pending_commit_commands = [] self._handle_configuration_failure() + self._output_buffer = "" + elif len(self._pending_commit_commands) > 0: + # Send next commit-phase command (e.g. exit/end/write memory) + cmd = self._pending_commit_commands.pop(0) + self.ssh_client.send_str(cmd + "\n") + self._output_buffer = "" else: # Configuration committed successfully config_callback = self._config_callback @@ -267,6 +279,7 @@ class BaseDriver(Driver): self._config_callback = None config_callback(None, self._session_log) + self._pending_commit_commands = [] self._transition_to_state(DRIVER_STATE_READY) self._output_buffer = "" @@ -360,6 +373,7 @@ class BaseDriver(Driver): self._config_callback = cb self._config_commands = config.copy() + self._pending_commit_commands = [] self._output_buffer = "" self._session_log = "" @@ -383,6 +397,7 @@ class BaseDriver(Driver): def _handle_configuration_failure(self) -> None: """Handle configuration failure - platform specific behavior""" self.log.warning("Configuration failed, aborting configuration") + self._pending_commit_commands = [] self._transition_to_state(DRIVER_STATE_ABORTING_CONFIG) # Send device-specific abort commands diff --git a/src/test_drivers.act b/src/test_drivers.act index dc2161c..ac83eba 100644 --- a/src/test_drivers.act +++ b/src/test_drivers.act @@ -206,6 +206,55 @@ actor _test_commit_failure_automatic_rollback(t: testing.AsyncT): after 0: run_test() +actor _test_iosxr_commit_failure_staged_commit_flow(t: testing.AsyncT): + """Test IOS XR commit failure detection with staged commit commands""" + + var test_ssh: drivers.TestSSHWrapper = drivers.TestSSHWrapper() + var log: logging.Logger = logging.Logger(t.log_handler) + var driver: drivers.CiscoIOSXRDriver = drivers.CiscoIOSXRDriver(drivers.DEVICE_TYPE_CISCO_IOSXR, test_ssh, log) + + def run_test() -> None: + driver.initialize() + test_ssh.clear_sent_commands() + + config_commands = ["service-policy output FOOBAR"] + driver.configure_and_commit(config_callback, config_commands) + + # Enter config mode and apply command + driver.handle_data(b"configure terminal\r\n\rRP/0/RP0/CPU0:ios(config)#") + driver.handle_data(b"service-policy output FOOBAR\r\n\rRP/0/RP0/CPU0:ios(config)#") + + # IOS XR commit failure - should trigger abort without sending "end" from commit phase + driver.handle_data(b"commit\r\n% Failed to commit one or more configuration items during a pseudo-atomic operation. All changes made have been reverted. Please issue 'show configuration failed [inheritance]' from this session to view the errors\r\nRP/0/RP0/CPU0:ios(config)#") + + # Simulate abort completion prompt + driver.handle_data(b"abort\r\nRP/0/RP0/CPU0:ios(config)#") + + def config_callback(err: ?Exception, session_log: str) -> None: + if err is None: + t.failure(Exception("Expected error due to commit failure")) + return + + error_msg = str(err) + if "Configuration failed and was aborted" not in error_msg: + t.failure(Exception("Expected abort error message, got: " + error_msg)) + return + + if driver.get_state() != drivers.DRIVER_STATE_READY: + t.failure(Exception("Expected READY state after abort, got: " + driver.get_state())) + return + + commands = test_ssh.get_sent_commands() + expected_commands = ["configure terminal", "service-policy output FOOBAR", "commit", "abort", "end"] + if commands != expected_commands: + t.failure(Exception("Expected IOS XR abort sequence after commit failure, got: " + str(commands))) + return + + result = "error:" + error_msg + "\nstate:" + driver.get_state() + "\ncommands:" + str(commands) + t.success(result) + + after 0: run_test() + actor _test_driver_state_transition_validation(t: testing.AsyncT): """Test state transition validation logic"""