Skip to content

Commit 46db5fb

Browse files
authored
Merge pull request #121 from CiscoTestAutomation/release_26.1
Release 26.1
2 parents db910fc + dcf6279 commit 46db5fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1380
-80
lines changed

docs/changelog/2026/january.rst

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
January 2026
2+
==========
3+
4+
January 27 - Unicon v26.1
5+
------------------------
6+
7+
8+
9+
.. csv-table:: Module Versions
10+
:header: "Modules", "Versions"
11+
12+
``unicon.plugins``, v26.1
13+
``unicon``, v26.1
14+
15+
16+
17+
18+
Changelogs
19+
^^^^^^^^^^
20+
--------------------------------------------------------------------------------
21+
Fix
22+
--------------------------------------------------------------------------------
23+
24+
* unicon/patterns
25+
* Update connection refused pattern to include "Requested line is busy!"
26+
27+
* routers/connection_providers
28+
* connect
29+
* Unwrap connection kwargs and assign to device object for the arguments to be used by underlying connection providers.
30+
31+
* bases/router/connection_provider
32+
* Use enable service to transition to enable mode
33+
34+
35+
--------------------------------------------------------------------------------
36+
Add
37+
--------------------------------------------------------------------------------
38+
39+
* nxos/n9kv
40+
* Added AttachModuleConsoleN9k service to attach to module console of N9K devices.
41+
42+
43+
--------------------------------------------------------------------------------
44+
New
45+
--------------------------------------------------------------------------------
46+
47+
* iosxe/c8kv/statemachine
48+
* Added IosXEC8kvSingleRpStateMachine and IosXEC8kvDualRpStateMachine
49+
* Added new state machine for C8KV devices to support boot statement
50+
51+
* iosxe/cat9k/c9350/stack
52+
* Added the support for stack for c9350 devices
53+
* Added C9350StackReload service
54+
55+
56+
--------------------------------------------------------------------------------
57+
Recovery.
58+
--------------------------------------------------------------------------------
59+
60+
* iosxe/c8kv/statements
61+
* Added boot_image statement for C8KV devices
62+
* Modified the statement to support C8KV grub> mode by adding send(cmd)
63+
64+
65+
--------------------------------------------------------------------------------
66+
Fix
67+
--------------------------------------------------------------------------------
68+
69+
* pid_tokens
70+
* Updated proper platform/model for IR1101 devices.
71+
72+
* generic/service_pattern
73+
* Modified ping validate pattern to match the "Validate reply data? [no]" prompt correctly in generic patterns.
74+
75+
* generic/service_implementation
76+
* enable
77+
* Updated UniconAuthenticationError and CredentialsExhaustedError as exceptions as they were wrapped inside the subcommand failure for a failing UT.
78+
79+
* iosxe/patterns
80+
* Updated enable_prompt regex patterns to include 'eWLC' and allow alphanumeric characters in the device identifier section.
81+
82+
* generic/statemachine
83+
* Fixed config transition retry handling to avoid resending configure terminal when configuration mode is already entered.
84+
85+

docs/changelog/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Changelog
44
.. toctree::
55
:maxdepth: 2
66

7+
2026/january
78
2025/december
89
2025/october
910
2025/september
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
January 2026
2+
==========
3+
4+
January 27 - Unicon v26.1
5+
------------------------
6+
7+
8+
9+
.. csv-table:: Module Versions
10+
:header: "Modules", "Versions"
11+
12+
``unicon.plugins``, v26.1
13+
``unicon``, v26.1
14+
15+
16+
17+
18+
Changelogs
19+
^^^^^^^^^^
20+
--------------------------------------------------------------------------------
21+
Fix
22+
--------------------------------------------------------------------------------
23+
24+
* generic
25+
* Update enable service to user transition dialog
26+
* Update escape_char_stmt to handle 2 check for buffer for connection refuse
27+
28+
* iosxe
29+
* Add Enable service to explicitly add "enable" command
30+
31+
* unicon.plugins
32+
* IOSXE/C9500/SVL_STACK
33+
* Add dis_state prompt statement to stack_switchover_stmt_list preventing timeout when the standby comes up at disable mode.
34+
* IOSXE
35+
* updated the configure service logic to support the multiline banner
36+
37+
* iosxr
38+
* Updated the run_prompt regex to avoid mixing standalone # in execution output with enable prompt.
39+
40+

docs/changelog_plugins/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Plugins Changelog
44
.. toctree::
55
:maxdepth: 2
66

7+
2026/january
78
2025/december
89
2025/october
910
2025/september

src/unicon/plugins/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "25.11"
1+
__version__ = "26.1"
22

33
supported_chassis = [
44
'single_rp',

src/unicon/plugins/generic/service_implementation.py

Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828

2929
from unicon.bases.routers.services import BaseService
3030
from unicon.core.errors import SubCommandFailure, StateMachineError, \
31-
CopyBadNetworkError, TimeoutError, UniconBackendDecodeError
31+
CopyBadNetworkError, TimeoutError, UniconBackendDecodeError, \
32+
UniconAuthenticationError, CredentialsExhaustedError
3233
from unicon.eal.dialogs import Dialog
3334
from unicon.eal.dialogs import Statement
3435
from unicon.plugins.generic.statements import (
@@ -47,6 +48,7 @@
4748

4849
from unicon.plugins.generic.utils import GenericUtils
4950
from .service_statements import execution_statement_list, configure_statement_list
51+
from .statements import disable_enable_transition_statements
5052
from unicon.plugins.generic.statemachine import config_transition
5153

5254
utils = GenericUtils()
@@ -480,6 +482,7 @@ def __init__(self, connection, context, **kwargs):
480482
super().__init__(connection, context, **kwargs)
481483
self.start_state = 'enable'
482484
self.end_state = 'enable'
485+
self.dialog = Dialog(disable_enable_transition_statements)
483486
self.__dict__.update(kwargs)
484487

485488
def pre_service(self, *args, **kwargs):
@@ -491,7 +494,7 @@ def call_service(self, target=None, command='', *args, **kwargs):
491494
handle = self.get_handle(target)
492495
spawn = self.get_spawn(target)
493496
sm = self.get_sm(target)
494-
timeout = kwargs.get('timeout', None)
497+
timeout = kwargs.get('timeout', None) or handle.settings.ENABLE_TIMEOUT
495498

496499
# If the device is in rommon, enable() will use the
497500
# image_to_boot info to boot the image specified
@@ -511,9 +514,13 @@ def call_service(self, target=None, command='', *args, **kwargs):
511514
spawn,
512515
context=handle.context,
513516
timeout=timeout)
517+
except (UniconAuthenticationError, CredentialsExhaustedError):
518+
# Don't wrap auth errors - re-raise them directly
519+
raise
514520
except Exception as err:
515521
raise SubCommandFailure("Failed to Bring device to Enable State",
516522
err) from err
523+
517524
self.result = True
518525

519526

@@ -604,6 +611,7 @@ def __init__(self, connection, context, **kwargs):
604611
self.matched_retry_sleep = connection.settings.EXECUTE_MATCHED_RETRY_SLEEP
605612
self.state_change_matched_retries = connection.settings.EXECUTE_STATE_CHANGE_MATCH_RETRIES
606613
self.state_change_matched_retry_sleep = connection.settings.EXECUTE_STATE_CHANGE_MATCH_RETRY_SLEEP
614+
self.detect_state = True
607615

608616
def log_service_call(self):
609617
pass
@@ -620,12 +628,15 @@ def call_service(self, command=[], # noqa: C901
620628
allow_state_change=None,
621629
matched_retries=None,
622630
matched_retry_sleep=None,
631+
detect_state=None,
623632
*args, **kwargs):
624633
con = self.connection
625634
sm = self.get_sm()
626635
if allow_state_change is None:
627636
allow_state_change = con.settings.EXEC_ALLOW_STATE_CHANGE
628637

638+
self.detect_state = detect_state if detect_state is not None else self.detect_state
639+
629640
timeout = timeout or self.timeout
630641

631642
if error_pattern is None:
@@ -682,24 +693,25 @@ def call_service(self, command=[], # noqa: C901
682693
if custom_auth_stmt:
683694
dialog += Dialog(custom_auth_stmt)
684695

685-
# Add all known states to detect state changes.
686-
for state in sm.states:
687-
# The current state is already added by the service_dialog method
688-
if state.name != sm.current_state:
689-
if allow_state_change:
690-
dialog.append(Statement(
691-
pattern=state.pattern,
692-
matched_retries=self.state_change_matched_retries,
693-
matched_retry_sleep=self.state_change_matched_retry_sleep
694-
))
695-
else:
696-
dialog.append(Statement(
697-
pattern=state.pattern,
698-
action=invalid_state_change_action,
699-
args={'err_state': state, 'sm': sm},
700-
matched_retries=self.state_change_matched_retries,
701-
matched_retry_sleep=self.state_change_matched_retry_sleep
702-
))
696+
if self.detect_state:
697+
# Add all known states to detect state changes.
698+
for state in sm.states:
699+
# The current state is already added by the service_dialog method
700+
if state.name != sm.current_state:
701+
if allow_state_change:
702+
dialog.append(Statement(
703+
pattern=state.pattern,
704+
matched_retries=self.state_change_matched_retries,
705+
matched_retry_sleep=self.state_change_matched_retry_sleep
706+
))
707+
else:
708+
dialog.append(Statement(
709+
pattern=state.pattern,
710+
action=invalid_state_change_action,
711+
args={'err_state': state, 'sm': sm},
712+
matched_retries=self.state_change_matched_retries,
713+
matched_retry_sleep=self.state_change_matched_retry_sleep
714+
))
703715

704716
# store the last used dialog, used by unittest
705717
self._last_dialog = dialog
@@ -926,7 +938,6 @@ def config_state_change(spawn, from_state, sm):
926938
if command:
927939
flat_cmd = self.utils.flatten_splitlines_command(command)
928940
dialog = self.dialog + self.service_dialog(handle=handle, service_dialog=reply)
929-
sp = handle.spawn
930941
# Add all known states to detect state changes.
931942
for state in sm.states:
932943
# The current state is already added by the service_dialog method
@@ -945,38 +956,52 @@ def config_state_change(spawn, from_state, sm):
945956
matched_retries=self.state_change_matched_retries,
946957
matched_retry_sleep=self.state_change_matched_retry_sleep
947958
))
959+
960+
banner_lines, command_lines, banner_delim = self.get_banner_lines(flat_cmd)
961+
962+
# Populate context for banner_text_handler only if banner was detected
963+
if banner_lines:
964+
self.connection.log.info('Banner detected, configuring banners without state detection')
965+
966+
# Send banner lines
967+
for line in banner_lines:
968+
handle.spawn.sendline(line)
969+
time.sleep(0.1)
970+
handle.spawn.read_update_buffer()
971+
972+
948973
if bulk:
949974
indicator = handle.settings.BULK_CONFIG_END_INDICATOR
950-
cmd_lst = list(chain(flat_cmd, [indicator]))
975+
cmd_lst = list(chain(command_lines, [indicator]))
951976
if bulk_chunk_lines == 0:
952977
chunks = [cmd_lst]
953978
else:
954979
chunks = [cmd_lst[i:i + bulk_chunk_lines]
955980
for i in range(0, len(cmd_lst), bulk_chunk_lines)]
956981
for idx, chunk in enumerate(chunks, 1):
957982
chunk_cmd = '\n'.join(chunk)
958-
sp.sendline(chunk_cmd)
983+
handle.spawn.sendline(chunk_cmd)
959984
if idx != len(chunks):
960985
sleep(bulk_chunk_sleep)
961-
sp.read_update_buffer()
986+
handle.spawn.read_update_buffer()
962987
else:
963988
try:
964-
sp.expect([indicator], timeout=timeout,
989+
handle.spawn.expect([indicator], timeout=timeout,
965990
trim_buffer=False)
966-
self.result, _, sp.buffer = \
967-
sp.buffer.rpartition(indicator)
991+
self.result, _, handle.spawn.buffer = \
992+
handle.spawn.buffer.rpartition(indicator)
968993
except Exception as err:
969994
raise SubCommandFailure('Configuration failed',
970995
err) from err
971996
self.process_dialog_on_handle(handle, dialog, timeout)
972997
if self.commit_cmd:
973-
sp.sendline(self.commit_cmd)
998+
handle.spawn.sendline(self.commit_cmd)
974999
self.process_dialog_on_handle(handle, dialog, timeout)
9751000
else:
976-
cmds = chain(flat_cmd, [self.commit_cmd]) \
977-
if self.commit_cmd else flat_cmd
1001+
cmds = chain(command_lines, [self.commit_cmd]) \
1002+
if self.commit_cmd else command_lines
9781003
for cmd in cmds:
979-
sp.sendline(cmd)
1004+
handle.spawn.sendline(cmd)
9801005
self.update_hostname_if_needed([cmd])
9811006
self.process_dialog_on_handle(handle, dialog, timeout)
9821007
# To handle the session
@@ -986,7 +1011,7 @@ def config_state_change(spawn, from_state, sm):
9861011
sleep(self.connection.settings.CONFIG_LOCK_RETRY_SLEEP)
9871012
config_transition(handle.state_machine, handle.spawn, handle.context)
9881013
handle.context['config_session_locked'] = False
989-
sp.sendline(cmd)
1014+
handle.spawn.sendline(cmd)
9901015
self.process_dialog_on_handle(handle, dialog, timeout)
9911016

9921017
# store config_result so it can be returned to the user later
@@ -1005,6 +1030,37 @@ def config_state_change(spawn, from_state, sm):
10051030
# return the config_result to the user via self.result
10061031
self.result = config_result
10071032

1033+
1034+
def get_banner_lines(self, config_lines):
1035+
""" Process lines related to the banner command
1036+
Args:
1037+
config_lines (list): list of config lines
1038+
Returns:
1039+
tuple: (banner_lines, command_lines, banner_delim)
1040+
"""
1041+
banner_lines = []
1042+
command_lines = []
1043+
banner_delim = None
1044+
1045+
for line in config_lines:
1046+
1047+
match = re.match(r'^\s*banner\s+(login|motd|exec|incoming)\s+(\S)', line)
1048+
if match:
1049+
banner_lines.append(line)
1050+
banner_delim = match.group(2)
1051+
continue
1052+
1053+
if banner_delim:
1054+
banner_lines.append(line)
1055+
# End of banner when delimiter repeats as a full line
1056+
if line.strip() == banner_delim:
1057+
banner_delim = None
1058+
continue
1059+
1060+
command_lines.append(line)
1061+
1062+
return banner_lines, command_lines, banner_delim
1063+
10081064
def process_dialog_on_handle(self, handle, dialog, timeout):
10091065
try:
10101066
cmd_result = dialog.process(

src/unicon/plugins/generic/service_patterns.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def __init__(self):
9999
self.verbomode = r'^.*Verbose mode\? \[.+\]\s?: $'
100100
self.ext_cmds_source = r'^.*Source .*address( or interface)?\s?: $'
101101
self.tos = r'^.*Type of service \[.+\]\s?: $'
102-
self.validate = r'^.*Validate reply data\? \[.+\]\s?: $'
102+
self.validate = r'^.*Validate reply data\?\s*\[.+\]:\s*$'
103103
self.data_pattern = r'^.*Data pattern \[.+\]\s?: $'
104104
self.dfbit_header = r'^.*Set DF bit in IP header(\?)? \[.+\]\s?: $'
105105
self.dscp = r'^.*DSCP .*\[.+\]\s?: $'

0 commit comments

Comments
 (0)