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
2 changes: 1 addition & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
group: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ BUILD_CMD = $(PYTHON) setup.py bdist_wheel --dist-dir=$(DIST_DIR)
PYPIREPO = pypitest


DEPENDENCIES = robotframework pyyaml dill coverage Sphinx \
DEPENDENCIES = robotframework pyyaml dill coverage Sphinx==7.4.7 \
sphinxcontrib-napoleon sphinxcontrib-mockautodoc \
sphinx-rtd-theme asyncssh PrettyTable "cryptography>=43.0"

Expand Down
67 changes: 67 additions & 0 deletions docs/changelog/2026/march.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
March 2026
==========

March 31 - Unicon v26.3
------------------------



.. csv-table:: Module Versions
:header: "Modules", "Versions"

``unicon.plugins``, v26.3
``unicon``, v26.3




Changelogs
^^^^^^^^^^
--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------

* bases/router/connection_provider
* Added logic to raise a traceback when when HA sync does not complete within POST_BOOT_TIMEOUT

* removed unused `pyats` and `genie` imports


--------------------------------------------------------------------------------
New
--------------------------------------------------------------------------------

* iosxe/ie9k
* Added plugin settings for IE9k platform.

* iosxe/ie3k
* Added plugin settings for IE3k platform.


--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------

* iosxe
* Fixed boot image to support multiple filesystems.
* Fixed encryption selection criteria on boot.

* iosxe/iec3400
* Removed this platform as it must be ie3k.
* Related state machine and test cases were not needed hence weren't moved to ie3k.

* pid_tokens.csv
* Modified PID tokens
* Added IE9k family PID token mappings.
* Added ESS3300/ESS9300 family PID token mappings.

* pid_tokens
* Added PID tokens for IE 3100, 3500 series

* iosxe/settings
* Increased POST_BOOT_TIMEOUT to allow for longer HA sync times.

* iosxe/patterns
* Modified want_continue pattern to match the prompt "Continue? [no]"


1 change: 1 addition & 0 deletions docs/changelog/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
.. toctree::
:maxdepth: 2

2026/march
2026/february
2026/january
2025/december
Expand Down
78 changes: 78 additions & 0 deletions docs/changelog_plugins/2026/march.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
March 2026
==========

March 31 - Unicon.Plugins v26.3
------------------------



.. csv-table:: Module Versions
:header: "Modules", "Versions"

``unicon.plugins``, v26.3
``unicon``, v26.3




Changelogs
^^^^^^^^^^
--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------

* generic
* Configure service
* Refactored banner handling logic to improve maintainability.
* Updated banner processing to send lines sequentially with appropriate delays for device processing.
* HAReloadService
* Fixed command fallback check and added guard to skip sendline when command is empty.
* SwitchoverService
* Fixed command fallback check and added guard to skip sendline when command is empty.

* iosxe
* Configure service
* Updated ACM configlet implementation to use connection context for acm_configlet parameter.
* Ensures proper persistence of acm_configlet during state transitions.
* Fixed multiline banner configuration to support variable delimiters
* HASwitchover
* Changed default command parameter from [] to None.

* iosxe/cat9k
* stackwise_virtual
* Updated the logic to detect current state before during state change
* Updated the logic of designate handles to correctly identify active and standby state after svl configuration.
* 9500x/stackwise_virtual
* Updated the logic to detect current state before during state change

* iosxe/cat4k
* Reload
* Fixed reload_command fallback check and added guard to skip sendline when empty.

* iosxe/cat8k
* SwitchoverService
* Fixed command fallback check and added guard to skip sendline when empty.

* iosxe/stack
* StackSwitchover
* Fixed command fallback check and added guard to skip sendline when empty.
* StackReload
* Fixed reload_command fallback check and added guard to skip sendline when empty.

* iosxe/quad
* QuadSwitchover
* Fixed command fallback check and added guard to skip sendline when empty.
* QuadReload
* Fixed reload_command fallback check and added guard to skip sendline when empty.

* iosxe/cat9k/c9350/stack
* C9350StackReload
* Fixed reload_command fallback check and added guard to skip sendline when empty.

* iosxe/cat9k/c9500x/stackwise_virtual
* SVLStackReload
* Fixed reload_command fallback check, added guard for empty command and improved post-reload recovery and reconnection handling.
* SVLStackSwitchover
* Fixed command fallback check and added guard to skip sendline when empty.


1 change: 1 addition & 0 deletions docs/changelog_plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Plugins Changelog
.. toctree::
:maxdepth: 2

2026/march
2026/february
2026/january
2025/december
Expand Down
2 changes: 1 addition & 1 deletion src/unicon/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "26.2"
__version__ = "26.3"

supported_chassis = [
'single_rp',
Expand Down
80 changes: 56 additions & 24 deletions src/unicon/plugins/generic/service_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -934,9 +934,11 @@ def config_state_change(spawn, from_state, sm):
else:
sm.update_cur_state(from_state)

# Flatten multi-line input into one-command-per-line list.
flat_cmd_list = list(self.utils.flatten_splitlines_command(command) if command else [])

self.result = ''
if command:
flat_cmd = self.utils.flatten_splitlines_command(command)
if flat_cmd_list:
dialog = self.dialog + self.service_dialog(handle=handle, service_dialog=reply)
# Add all known states to detect state changes.
for state in sm.states:
Expand All @@ -957,22 +959,35 @@ def config_state_change(spawn, from_state, sm):
matched_retry_sleep=self.state_change_matched_retry_sleep
))

banner_lines, command_lines, banner_delim = self.get_banner_lines(flat_cmd)
# Use flattened command list
pre_lines, banner_lines, post_lines, banner_delim = self.get_banner_lines(flat_cmd_list)

# Populate context for banner_text_handler only if banner was detected
if banner_lines:
self.connection.log.info('Banner detected, configuring banners without state detection')

for cmd in pre_lines:
handle.spawn.sendline(cmd)
self.update_hostname_if_needed([cmd])
self.process_dialog_on_handle(handle, dialog, timeout)

# Send banner lines
for line in banner_lines:
handle.spawn.sendline(line)
time.sleep(0.1)
handle.spawn.read_update_buffer()

self.process_dialog_on_handle(handle, dialog, timeout)

if bulk:
post_cmds = chain(post_lines, [self.commit_cmd]) if self.commit_cmd else post_lines
for cmd in post_cmds:
handle.spawn.sendline(cmd)
self.update_hostname_if_needed([cmd])
self.process_dialog_on_handle(handle, dialog, timeout)

elif bulk:
indicator = handle.settings.BULK_CONFIG_END_INDICATOR
cmd_lst = list(chain(command_lines, [indicator]))
cmd_lst = list(chain(flat_cmd_list, [indicator]))
if bulk_chunk_lines == 0:
chunks = [cmd_lst]
else:
Expand All @@ -998,8 +1013,8 @@ def config_state_change(spawn, from_state, sm):
handle.spawn.sendline(self.commit_cmd)
self.process_dialog_on_handle(handle, dialog, timeout)
else:
cmds = chain(command_lines, [self.commit_cmd]) \
if self.commit_cmd else command_lines
cmds = chain(flat_cmd_list, [self.commit_cmd]) \
if self.commit_cmd else flat_cmd_list
for cmd in cmds:
handle.spawn.sendline(cmd)
self.update_hostname_if_needed([cmd])
Expand Down Expand Up @@ -1032,34 +1047,49 @@ def config_state_change(spawn, from_state, sm):


def get_banner_lines(self, config_lines):
""" Process lines related to the banner command
""" Process lines related to the banner command.
Handles detection and separation of banner configuration blocks from
regular configuration commands. Supports the first banner block only;
subsequent banners (if any) will be processed sequentially.
Args:
config_lines (list): list of config lines
config_lines (list): Configuration command lines
Returns:
tuple: (banner_lines, command_lines, banner_delim)
tuple: (pre_lines, banner_lines, post_lines, banner_delim)
- pre_lines: Commands before the banner block
- banner_lines: The banner initiation line and content
- post_lines: Commands after the banner block
- banner_delim: The delimiter character used for the banner
"""
banner_lines = []
command_lines = []
pre_lines, banner_lines, post_lines = [], [], []
banner_delim = None
in_banner = False
banner_seen = False

for line in config_lines:

match = re.match(r'^\s*banner\s+(login|motd|exec|incoming)\s+(\S)', line)
if match:
banner_lines.append(line)
banner_delim = match.group(2)
if not in_banner and not banner_seen:
match = re.match(r'^\s*banner\s+(login|motd|exec|incoming)\s+(\S+)', line)
if match:
banner_lines.append(line)
raw_delim = match.group(2)
# Use '^C' token when present, else first character (e.g. '%')
banner_delim = '^C' if raw_delim.startswith('^C') else raw_delim[0]
in_banner = True
banner_seen = True
continue
pre_lines.append(line)
continue

if banner_delim:
if in_banner:
banner_lines.append(line)
# End of banner when delimiter repeats as a full line
if line.strip() == banner_delim:
banner_delim = None
in_banner = False
continue

command_lines.append(line)
post_lines.append(line)

return banner_lines, command_lines, banner_delim
return pre_lines, banner_lines, post_lines, banner_delim

def process_dialog_on_handle(self, handle, dialog, timeout):
try:
Expand Down Expand Up @@ -2175,7 +2205,7 @@ def call_service(self, # noqa: C901
if command and reload_command:
raise SubCommandFailure(
"Please use either 'command' or 'reload_command' parameter")
command = command or reload_command or self.command
command = command if command is not None else (reload_command if reload_command is not None else self.command)

# TODO counter value must be moved to settings
counter = 0
Expand All @@ -2197,7 +2227,8 @@ def call_service(self, # noqa: C901
dialog += Dialog(custom_auth_stmt)

# Issue reload command
con.active.spawn.sendline(command)
if command:
con.active.spawn.sendline(command)
try:
reload_output = dialog.process(con.active.spawn,
context=context,
Expand Down Expand Up @@ -2373,7 +2404,7 @@ def call_service(self, command=None, # noqa: C901
category=DeprecationWarning)

timeout = timeout or self.timeout
command = command or self.command
command = command if command is not None else self.command
switchover_counter = con.settings.SWITCHOVER_COUNTER
con.log.debug("+++ Issuing switchover on %s with "
"switchover_command %s and timeout is %s +++"
Expand Down Expand Up @@ -2403,7 +2434,8 @@ def call_service(self, command=None, # noqa: C901
context = con.standby.context

# Issue switchover command
con.active.spawn.sendline(command)
if command:
con.active.spawn.sendline(command)
try:
dialog.process(con.active.spawn,
timeout=timeout,
Expand Down
13 changes: 12 additions & 1 deletion src/unicon/plugins/generic/statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ def enable_secret_handler(spawn, context, session):
spawn.log.warning('Using enable secret from TEMP_ENABLE_SECRET setting')
enable_secret = spawn.settings.TEMP_ENABLE_SECRET
context['setup_selection'] = 0
context['encryption_selection'] = 2
spawn.sendline(enable_secret)


Expand All @@ -352,6 +353,16 @@ def setup_enter_selection(spawn, context):
spawn.sendline('2')


def setup_enter_encryption_selection(spawn, context):
selection = context.get('encryption_selection', context.get('setup_selection'))
if selection is not None:
if str(selection) == '0':
spawn.log.warning('Not saving setup configuration')
spawn.sendline(f'{selection}')
else:
spawn.sendline('2')


def ssh_tacacs_handler(spawn, context):
result = False
start_cmd = spawn.spawn_command
Expand Down Expand Up @@ -781,7 +792,7 @@ def __init__(self):
continue_timer=False)

self.enter_your_encryption_selection_stmt = Statement(pattern=pat.enter_your_encryption_selection_2,
action=setup_enter_selection,
action=setup_enter_encryption_selection,
args=None,
loop_continue=True,
continue_timer=True)
Expand Down
Loading
Loading