Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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']
commands:['configure', 'set invalid config', 'commit', 'rollback', 'exit']
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
error:Exception: Configuration failed and was aborted
state:ready
commands:['configure terminal', 'service-policy output FOOBAR', 'commit', 'abort', 'end']
23 changes: 19 additions & 4 deletions src/drivers.act
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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 = ""

Expand All @@ -250,23 +254,32 @@ 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 = ""

elif self._state == DRIVER_STATE_COMMITTING:
# 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
if config_callback is not None:
self._config_callback = None
config_callback(None, self._session_log)

self._pending_commit_commands = []
self._transition_to_state(DRIVER_STATE_READY)
self._output_buffer = ""

Expand Down Expand Up @@ -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 = ""

Expand All @@ -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
Expand Down
49 changes: 49 additions & 0 deletions src/test_drivers.act
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand Down