diff --git a/docs/changelog/2026/february.rst b/docs/changelog/2026/february.rst new file mode 100644 index 00000000..4b84d82b --- /dev/null +++ b/docs/changelog/2026/february.rst @@ -0,0 +1,59 @@ +February 2026 +========== + +February 24 - Unicon v26.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v26.2 + ``unicon``, v26.2 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* mock_device + * Fix asyncio deprecation warning for python 3.14 + +* routers.connection_provider + * Added logic to merge settings dict instead of replacing Settings object + * When settings dict is passed to connect(), it now properly updates existing Settings object using update() method + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* stackwisevirtualconnectionprovider + * Avoid traceback on empty 'show switch' output + +* unicon.plugin/cat8k + * Modified the Switchover implementaion of cat8k to connect post switchover + * This is to avoid any prompt mismatch issues post switchover + +* generic/statements + * Modified terminal_position_handler + * Changed terminal position response to \x1b[0;0R + +* generic/service_pattern + * Modified ping verbose regex patterns for verbose prompts to correctly match the prompt. + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* linux + * Modified LinuxPatterns + * Add support for linux prompt (server.cisco.com)~ + + diff --git a/docs/changelog/index.rst b/docs/changelog/index.rst index 2c5afd63..c0c7357e 100644 --- a/docs/changelog/index.rst +++ b/docs/changelog/index.rst @@ -4,6 +4,7 @@ Changelog .. toctree:: :maxdepth: 2 + 2026/february 2026/january 2025/december 2025/october diff --git a/docs/changelog_plugins/2026/february.rst b/docs/changelog_plugins/2026/february.rst new file mode 100644 index 00000000..e8189ef4 --- /dev/null +++ b/docs/changelog_plugins/2026/february.rst @@ -0,0 +1,32 @@ +February 2026 +========== + +February 24 - Unicon.Plugins v26.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v26.2 + ``unicon``, v26.2 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * fix for 3.14 runtime emits extra terminal/argparse warnings + * Update PID tokens for C8000V + +* iosxe/cat9k/stackwise_virtual + * Added support to handle standby unlocked in designate handles + * Fix the designate handle to wait for the boot process to complete before designating handles. + + diff --git a/docs/changelog_plugins/index.rst b/docs/changelog_plugins/index.rst index 3096e8c1..8e5f4bb3 100644 --- a/docs/changelog_plugins/index.rst +++ b/docs/changelog_plugins/index.rst @@ -4,6 +4,7 @@ Plugins Changelog .. toctree:: :maxdepth: 2 + 2026/february 2026/january 2025/december 2025/october diff --git a/src/unicon/plugins/__init__.py b/src/unicon/plugins/__init__.py index 0555e40a..735a174a 100644 --- a/src/unicon/plugins/__init__.py +++ b/src/unicon/plugins/__init__.py @@ -1,4 +1,4 @@ -__version__ = "26.1" +__version__ = "26.2" supported_chassis = [ 'single_rp', diff --git a/src/unicon/plugins/generic/service_patterns.py b/src/unicon/plugins/generic/service_patterns.py index a3b077df..2b57b261 100644 --- a/src/unicon/plugins/generic/service_patterns.py +++ b/src/unicon/plugins/generic/service_patterns.py @@ -64,7 +64,7 @@ def __init__(self): self.tunnel = r'^.*Tunnel interface number \[.+\]\s?: $' self.repeat = r'^.*Repeat count \[.+\]\s?: $' self.size = r'^.*Datagram size \[.+\]\s?: $' - self.verbose = r'^.*Verbose \[.+\]\s?: $' + self.verbose = r'^.*Verbose(\?)? \[.+\]\s?: $' self.interval = r'^.*Interval in milliseconds \[.+\]: $' self.packet_timeout = r'^.*Timeout in seconds \[.+\]\s?: $' self.sending_interval = r'^.*Sending interval in seconds \[.+\]\s?: $' diff --git a/src/unicon/plugins/generic/statements.py b/src/unicon/plugins/generic/statements.py index f800505a..28ae7da2 100644 --- a/src/unicon/plugins/generic/statements.py +++ b/src/unicon/plugins/generic/statements.py @@ -40,7 +40,7 @@ def terminal_position_handler(spawn, session, context): """ send terminal position (VT100) """ - spawn.send('\x1b[0;200R') + spawn.send('\x1b[0;0R') def connection_refused_handler(spawn, context): diff --git a/src/unicon/plugins/iosxe/cat8k/service_implementation.py b/src/unicon/plugins/iosxe/cat8k/service_implementation.py index e32aace2..6e8c7af0 100644 --- a/src/unicon/plugins/iosxe/cat8k/service_implementation.py +++ b/src/unicon/plugins/iosxe/cat8k/service_implementation.py @@ -108,24 +108,8 @@ def call_service(self, command=None, sleep(con.settings.POST_SWITCHOVER_WAIT) con.spawn.sendline() - con.state_machine.go_to( - 'any', - con.spawn, - prompt_recovery=self.prompt_recovery, - timeout=con.connection_timeout, - context=self.context - ) - - con.log.info(f'Waiting {con.settings.POST_SWITCHOVER_WAIT} seconds before going to enable mode') - sleep(con.settings.POST_SWITCHOVER_WAIT) - - con.spawn.sendline() - con.state_machine.go_to( - 'enable', - con.spawn, - prompt_recovery=self.prompt_recovery, - context=self.context - ) + + con.connection_provider.connect() self.result = True if not sync_standby: diff --git a/src/unicon/plugins/iosxe/cat8k/service_statements.py b/src/unicon/plugins/iosxe/cat8k/service_statements.py index 6afa84ac..d132566d 100644 --- a/src/unicon/plugins/iosxe/cat8k/service_statements.py +++ b/src/unicon/plugins/iosxe/cat8k/service_statements.py @@ -29,7 +29,7 @@ continue_timer=True) switchover_complete = Statement(pattern=pat.switchover_complete, - action='sendline()', + action=None, loop_continue=False, continue_timer=False) diff --git a/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py index b47239ac..0a4dcd98 100644 --- a/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py +++ b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py @@ -3,11 +3,15 @@ pyATS TEAM (pyats-support@cisco.com, pyats-support-ext@cisco.com) """ import re -from unicon.eal.dialogs import Dialog + +from unicon.eal.dialogs import Dialog, Statement from unicon.bases.routers.connection_provider import BaseStackRpConnectionProvider +from genie.metaparser.util.exceptions import SchemaEmptyParserError + from unicon.plugins.generic.statements import connection_statement_list, custom_auth_statements + class StackwiseVirtualConnectionProvider(BaseStackRpConnectionProvider): """ Implements Stack Connection Provider, This class overrides the base class with the @@ -36,12 +40,23 @@ def designate_handles(self): other_alias = None # Try to go to enable mode on both connections + standby_locked_dialog = Dialog([ + Statement( + pattern=r'.*Standby console disabled.*', + action=None, + loop_continue=False, + continue_timer=False, + ) + ]) + for subcon in [subcon1, subcon2]: try: subcon.state_machine.go_to( 'enable', subcon.spawn, context=subcon.context, + timeout=con.settings.BOOT_TIMEOUT, + dialog=standby_locked_dialog, ) except Exception: pass @@ -63,7 +78,11 @@ def designate_handles(self): device = con.device try: # To check if the device is in SVL state - output = device.parse("show switch") + try: + output = device.parse("show switch") + except SchemaEmptyParserError: + con.log.debug("show switch returned empty output") + output = {} stack_info = output.get("switch", {}).get("stack", {}) roles = [switch_info.get("role") for switch_info in stack_info.values()] diff --git a/src/unicon/plugins/linux/patterns.py b/src/unicon/plugins/linux/patterns.py index 04054a78..4375dac5 100644 --- a/src/unicon/plugins/linux/patterns.py +++ b/src/unicon/plugins/linux/patterns.py @@ -12,17 +12,78 @@ def __init__(self): # The reason for using the learn_hostname pattern instead of the shell_prompt pattern # to learn the hostname, is that the regex in the router implementation matches \S # which is not exact enough for the known linux prompts. - self.learn_hostname = r'^.*?({a})?(?P[-\w]+)\s?([-\w\]/~:\.\d ]+)?([>\$~%#\]])\s*(\x1b\S+\s?)*$'.format(a=ANSI_REGEX) + # Supported prompt formats. + # Linux# + # Linux> + # user@host ~$ + # [user@host ~]$ + # agent-lab9-pm:~:2017> + # root@agent-lab11-pm:~# + # root@localhost ~% + # vm-7:3> + # \x1b]0;cisco@dev-server:~^Gcisco@dev-server:3> + # (dev) user@dev-1-name dir$ + # [user@new-host dir]$ + # host ~ # + # host:~ # + # \x1b]0;rally@rally: /workspace\x07rally@rally:/workspace$ \x1b[K + # root@sj21-pxe-03.cisco.com:~/ + # [Linux] # + # cxta@mock-server:~$ + # (server.cisco.com)~ : + # sma03:testuser 1] + # pod-esa01.cisco.com:testuser 1] + # \x1b[37mapc> + # \x1b[0;32m[user@host:~] >\x1b[m \x1b[m\x0f + self.learn_hostname = r'^.*?({a})?\(?(?P[-\w\.]+)\)?\s?([-\w\]/~:\.\d ]+)?([>\$~%#\]]|~/|~\s?:)\s*(\x1b\S+\s?)*$'.format(a=ANSI_REGEX) # shell_prompt pattern will be used by the 'shell' state after lean_hostname matches # a known hostname pattern this pattern is set for the shell state at transition # from learn_hostname to shell, see statemachine for more details. - self.shell_prompt = r'^(.*?(?P((\([-\w]+\) |\x1b(?!\[\?2004).*?)?\S+)?%N\s?([-\w\]/~\s:\.\d]+)?[>\$~%#\]]\s?(\x1b\S+\s?)*))$' + # Supported shell prompt formats, %N is replaced with learned hostname. + # Linux$ + # Linux# + # Linux> + # user@host ~$ + # [user@host ~]$ + # agent-lab9-pm:~:2017> + # root@agent-lab11-pm:~# + # root@localhost ~% + # vm-7:3> + # \x1b]0;cisco@dev-server:~^Gcisco@dev-server:3> + # (dev) user@dev-1-name dir$ + # [user@new-host dir]$ + # host ~ # + # host:~ # + # \x1b]0;rally@rally: /workspace\x07rally@rally:/workspace$ \x1b[K + # root@sj21-pxe-03.cisco.com:~/ + # [Linux] # + # cxta@mock-server:~$ + # (server.cisco.com)~ : + # sma03:testuser 1] + # pod-esa01.cisco.com:testuser 1] + # \x1b[37mapc> + # \x1b[0;32m[user@host:~] >\x1b[m \x1b[m\x0f + self.shell_prompt = r'^(.*?(?P((\([-\w\.]+\) |\x1b(?!\[\?2004).*?)?\S+)?\(?%N\)?\s?([-\w\]/~\s:\.\d]+)?([>\$~%#\]]|~/|~\s?:)\s?(\x1b\S+\s?)*))$' # default linux prompt with loose matching of the prompt # this can result in false prompt matching when output has # one of the prompt characters at the end of the line, # e.g. XML output or a banner - self.prompt = r'^(.*?([>\$~%\]]|\] # |[^#\s]#|~ #|~/|^admin:|^#|~\s?#\s?)\s?(\x1b\S+\s?)*)$' + # Supported fallback prompt formats. + # > + # $ + # ~ + # % + # ] + # ] # + # user# + # ~ # + # ~/ + # admin: + # # + # ~# + # ~ : + self.prompt = r'^(.*?([>\$~%\]]|\] # |[^#\s]#|~ #|~/|^admin:|^#|~\s?#\s?|~\s?:)\s?(\x1b\S+\s?)*)$' self.trex_console = r'^(.*?)(?Ptrex>\s*)$' diff --git a/src/unicon/plugins/pid_tokens.csv b/src/unicon/plugins/pid_tokens.csv index 3913f949..bf068896 100644 --- a/src/unicon/plugins/pid_tokens.csv +++ b/src/unicon/plugins/pid_tokens.csv @@ -263,7 +263,7 @@ C6832-X-LE,iosxe,cat6k,c6800, C6840-X-LE-40G,iosxe,cat6k,c6800, C6880-X,iosxe,cat6k,c6800, C6880-X-LE,iosxe,cat6k,c6800, -C8000V,iosxe,cat8k,c8000v, +C8000V,iosxe,c8kv,c8000v, C8200-1N-4T,iosxe,cat8k,c8200, C8200-UCPE-1N8,iosxe,cat8k,c8200, C8500-12X,iosxe,cat8k,c8500, diff --git a/src/unicon/plugins/tests/mock_data/linux/linux_mock_data.yaml b/src/unicon/plugins/tests/mock_data/linux/linux_mock_data.yaml index 60cf1251..ca313d94 100644 --- a/src/unicon/plugins/tests/mock_data/linux/linux_mock_data.yaml +++ b/src/unicon/plugins/tests/mock_data/linux/linux_mock_data.yaml @@ -166,6 +166,8 @@ exec: new_state: exec20 "prompt21": new_state: exec21 + "prompt22": + new_state: exec22 "ls": | /tmp /var @@ -401,6 +403,10 @@ exec21: prompt: "cxta@mock-server:~$ " commands: *cmds +exec22: + prompt: "(server.cisco.com)~ : " + commands: *cmds + sma_prompt: prompt: "sma03:testuser 1] " diff --git a/src/unicon/plugins/tests/test_plugin_generic.py b/src/unicon/plugins/tests/test_plugin_generic.py index 3f5ea078..33310c2f 100644 --- a/src/unicon/plugins/tests/test_plugin_generic.py +++ b/src/unicon/plugins/tests/test_plugin_generic.py @@ -1020,7 +1020,7 @@ class TestEscapeHandler(unittest.TestCase): def setUp(self): self.old_term_setting = os.environ.get('TERM') - os.environ['TERM'] = 'VT100' + os.environ['TERM'] = 'xterm' def test_escape_handler_uav(self): c = Connection(hostname='Router', diff --git a/src/unicon/plugins/tests/test_plugin_iosxe.py b/src/unicon/plugins/tests/test_plugin_iosxe.py index 6f2ce7f6..ed562305 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe.py @@ -11,11 +11,11 @@ import os import time import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock, MagicMock from pyats.topology import loader -from unittest.mock import Mock import unicon +from unicon.plugins.generic.statements import terminal_position_handler from unicon import Connection from unicon.eal.dialogs import Dialog, Statement from unicon.eal.utils import ExpectMatch, MatchMode @@ -593,6 +593,18 @@ def test_traceroute_vrf(self): class TestIosXEluginBashService(unittest.TestCase): + def test_terminal_position_handler(self): + """Test that terminal_position_handler sends correct VT100 cursor + position response ESC[0;0R without any additional cleanup.""" + mock_spawn = MagicMock() + mock_session = {} + mock_context = {} + + terminal_position_handler(mock_spawn, mock_session, mock_context) + + # Verify the handler sent only the cursor position response + mock_spawn.send.assert_called_once_with('\x1b[0;0R') + def test_bash(self): c = Connection(hostname='Router', start=['mock_device_cli --os iosxe --state isr_exec --hostname Router'], diff --git a/src/unicon/plugins/tests/test_plugin_iosxe_cat8k.py b/src/unicon/plugins/tests/test_plugin_iosxe_cat8k.py index 13d853ec..cde1b4d1 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe_cat8k.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe_cat8k.py @@ -166,6 +166,38 @@ def test_switchover_failure_standby_sync_timeout(self): c.disconnect() md.stop() + def test_switchover_verify_reconnect(self): + """Test that switchover properly reconnects after standby becomes active.""" + md = MockDeviceTcpWrapperIOSXECat8k(port=0, state='c8k_login') + md.start() + + c = Connection( + hostname='Switch', + start=['telnet 127.0.0.1 {}'.format(md.ports[0])], + os='iosxe', + platform='cat8k', + settings=dict( + POST_DISCONNECT_WAIT_SEC=0, + GRACEFUL_DISCONNECT_WAIT_SEC=0.2, + POST_HA_RELOAD_CONFIG_SYNC_WAIT=1, + POST_SWITCHOVER_WAIT=1, + ), + credentials=dict(default=dict(username='admin', password='cisco')), + mit=True, + ) + try: + c.connect() + initial_state = c.state_machine.current_state + c.switchover() + # After switchover, should be back in enable state + self.assertEqual(c.state_machine.current_state, 'enable') + # Should be able to execute commands + output = c.execute('show version') + self.assertIn('Cisco IOS', output) + finally: + c.disconnect() + md.stop() + @unittest.skip("Skipping until test is fixed") class TestIosXECat8kPluginReload(unittest.TestCase): diff --git a/src/unicon/plugins/tests/test_plugin_iosxe_iec3400.py b/src/unicon/plugins/tests/test_plugin_iosxe_iec3400.py index 570e561d..1b881cc8 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe_iec3400.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe_iec3400.py @@ -21,7 +21,8 @@ def test_terminal_position_handler(self): ) c.connect() c.execute('get terminal position') - self.assertEqual(c.spawn.match.match_output, '^[[0;200RPE1#') + self.assertIn('^[[0;0R', c.spawn.match.match_output) + self.assertTrue(c.spawn.match.match_output.endswith('PE1#')) c.disconnect() def test_reload_with_error_pattern(self): diff --git a/src/unicon/plugins/tests/test_plugin_linux.py b/src/unicon/plugins/tests/test_plugin_linux.py index c3d8fc98..bd0f0f8c 100644 --- a/src/unicon/plugins/tests/test_plugin_linux.py +++ b/src/unicon/plugins/tests/test_plugin_linux.py @@ -54,7 +54,7 @@ def test_connect_sma(self): os='linux', username='admin', password='cisco') - c1 = Connection(hostname='pod-esa01', + c1 = Connection(hostname='pod-esa01.cisco.com', start=['mock_device_cli --os linux --state connect_sma'], os='linux', username='admin', @@ -269,7 +269,9 @@ class TestLinuxPluginPrompts(unittest.TestCase): 'prompt17', 'prompt18', 'prompt19', - 'prompt20' + 'prompt20', + 'prompt21', + 'prompt22' ] @classmethod @@ -314,10 +316,11 @@ def test_learn_hostname(self): 'exec14': 'rally', 'exec15': LinuxSettings().DEFAULT_LEARNED_HOSTNAME, 'sma_prompt' : 'sma03', - 'sma_prompt_1' : 'pod-esa01', + 'sma_prompt_1' : 'pod-esa01.cisco.com', 'exec18': LinuxSettings().DEFAULT_LEARNED_HOSTNAME, 'exec20': 'Linux', 'exec21': 'mock-server', + 'exec22': 'server.cisco.com', 'ansi_prompt': 'apc', 'ansi_prompt2': 'host', } diff --git a/src/unicon/plugins/tests/test_plugin_nd.py b/src/unicon/plugins/tests/test_plugin_nd.py index 5d340760..92afa2ab 100644 --- a/src/unicon/plugins/tests/test_plugin_nd.py +++ b/src/unicon/plugins/tests/test_plugin_nd.py @@ -304,7 +304,7 @@ def test_learn_hostname(self): 'exec14': 'rally', 'exec15': LinuxSettings().DEFAULT_LEARNED_HOSTNAME, 'sma_prompt' : 'sma03', - 'sma_prompt_1' : 'pod-esa01', + 'sma_prompt_1' : 'pod-esa01.cisco.com', 'exec18': LinuxSettings().DEFAULT_LEARNED_HOSTNAME, }