diff --git a/docs/changelog/2025/december.rst b/docs/changelog/2025/december.rst new file mode 100644 index 00000000..d2b29fab --- /dev/null +++ b/docs/changelog/2025/december.rst @@ -0,0 +1,36 @@ +December 2025 +========== + +December 30 - Unicon v25.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.11 + ``unicon``, v25.11 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon + * adapters/topology.py + * Added support for reverse SSH connections. + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* patterns + * Updated the regex for username prompt for the linux VMs + + diff --git a/docs/changelog/index.rst b/docs/changelog/index.rst index 4050afbb..b21643e6 100644 --- a/docs/changelog/index.rst +++ b/docs/changelog/index.rst @@ -4,6 +4,7 @@ Changelog .. toctree:: :maxdepth: 2 + 2025/december 2025/october 2025/september 2025/august diff --git a/docs/changelog_plugins/2025/december.rst b/docs/changelog_plugins/2025/december.rst new file mode 100644 index 00000000..767c4c85 --- /dev/null +++ b/docs/changelog_plugins/2025/december.rst @@ -0,0 +1,44 @@ +December 2025 +========== + +December 30 - Unicon.Plugins v25.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.11 + ``unicon``, v25.11 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Added cursor position handling in bash ContextMgr to prevent delays during shell initialization. + +* nxos + * Updated the LC bash prompt pattern to include an anchor and improve prompt detection performance. + +* generic + * Updated syslog pattern to handle insecure dynamic warning message for SSH hostkey with insufficient key length. + +* linux + * Updated prompt patterns to better handle ANSI escape sequences in prompts + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* generic/settings.py + * Updated the temporary enable secret to include a special character. + + diff --git a/docs/changelog_plugins/changelog_lsheikal_update_generic_password_ok_20251112073999.rst b/docs/changelog_plugins/changelog_lsheikal_update_generic_password_ok_20251112073999.rst new file mode 100644 index 00000000..6de866d9 --- /dev/null +++ b/docs/changelog_plugins/changelog_lsheikal_update_generic_password_ok_20251112073999.rst @@ -0,0 +1,5 @@ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* generic/statements.py + * Updated the password_ok_stmt to use escape_char_callback instead of sendline. diff --git a/docs/changelog_plugins/index.rst b/docs/changelog_plugins/index.rst index 7ae6ad64..9da5f03f 100644 --- a/docs/changelog_plugins/index.rst +++ b/docs/changelog_plugins/index.rst @@ -4,6 +4,7 @@ Plugins Changelog .. toctree:: :maxdepth: 2 + 2025/december 2025/october 2025/september 2025/august diff --git a/src/unicon/plugins/__init__.py b/src/unicon/plugins/__init__.py index 5811b417..54087297 100644 --- a/src/unicon/plugins/__init__.py +++ b/src/unicon/plugins/__init__.py @@ -1,4 +1,4 @@ -__version__ = "25.10" +__version__ = "25.11" supported_chassis = [ 'single_rp', diff --git a/src/unicon/plugins/generic/patterns.py b/src/unicon/plugins/generic/patterns.py index 475acefa..4ccb1c71 100644 --- a/src/unicon/plugins/generic/patterns.py +++ b/src/unicon/plugins/generic/patterns.py @@ -77,7 +77,7 @@ def __init__(self): r"^.*?(%\w+(-\S+)?-\d+-\w+|" r"yang-infra:|PKI_SSL_IPC:|Guestshell destroyed successfully|" r"%Error opening tftp:\/\/255\.255\.255\.255|Autoinstall trying|" - r"audit: kauditd hold queue overflow|SECURITY WARNING|%RSA key|" + r"audit: kauditd hold queue overflow|SECURITY WARNING|%RSA key|INSECURE DYNAMIC WARNING|" r"(LC|RP)/\d+/\d+/CPU\d+:\w+\s+\d+\s+\d{2}:\d{2}:\d{2}|" r"\[OK\]" r").*\s*$" diff --git a/src/unicon/plugins/generic/settings.py b/src/unicon/plugins/generic/settings.py index bbfc96f9..af691769 100644 --- a/src/unicon/plugins/generic/settings.py +++ b/src/unicon/plugins/generic/settings.py @@ -60,7 +60,7 @@ def __init__(self): # Temporary enable secret used during setup # this is used if no password is available # and would not be saved by default - self.TEMP_ENABLE_SECRET = 'Secret12345' + self.TEMP_ENABLE_SECRET = 'Secret12345!' # Minimum length for enable secret password: # if the password specified is shorter, # use the TEMP_ENABLE_SECRET instead. diff --git a/src/unicon/plugins/generic/statements.py b/src/unicon/plugins/generic/statements.py index 86f95b83..785879c9 100644 --- a/src/unicon/plugins/generic/statements.py +++ b/src/unicon/plugins/generic/statements.py @@ -656,10 +656,11 @@ def __init__(self): loop_continue=True, continue_timer=False) self.password_ok_stmt = Statement(pattern=pat.password_ok, - action=sendline, + action=escape_char_callback, args=None, loop_continue=True, - continue_timer=False) + continue_timer=True, + trim_buffer=False) self.more_prompt_stmt = Statement(pattern=pat.more_prompt, action=more_prompt_handler, args=None, diff --git a/src/unicon/plugins/iosxe/service_implementation.py b/src/unicon/plugins/iosxe/service_implementation.py index 37be8a48..f30c4599 100644 --- a/src/unicon/plugins/iosxe/service_implementation.py +++ b/src/unicon/plugins/iosxe/service_implementation.py @@ -26,7 +26,7 @@ from .service_statements import execute_statement_list, configure_statement_list, confirm -from .statements import grub_prompt_stmt, boot_from_rommon_stmt +from .statements import grub_prompt_stmt, boot_from_rommon_stmt, terminal_position_stmt from unicon.plugins.generic.utils import GenericUtils from unicon.plugins.generic.service_implementation import BashService as GenericBashService @@ -258,6 +258,7 @@ def __init__(self, connection, enable_bash=False, timeout=None, **kwargs): timeout=timeout, **kwargs) + self.terminal_position_dialog = Dialog([terminal_position_stmt]) def __enter__(self): if self.conn.context.get('_disable_selinux'): @@ -272,7 +273,8 @@ def __enter__(self): 'shell', self.conn.spawn, timeout=self.timeout, - context=self.conn.context) + context=self.conn.context, + dialog=self.terminal_position_dialog,) for cmd in self.conn.settings.BASH_INIT_COMMANDS: self.conn.execute( diff --git a/src/unicon/plugins/iosxe/statements.py b/src/unicon/plugins/iosxe/statements.py index b264af1a..1b9a3f67 100644 --- a/src/unicon/plugins/iosxe/statements.py +++ b/src/unicon/plugins/iosxe/statements.py @@ -8,7 +8,10 @@ from unicon.plugins.generic.service_statements import\ admin_password as admin_password_stmt from unicon.plugins.generic.statements import ( - connection_statement_list, boot_timeout_stmt) + connection_statement_list, + boot_timeout_stmt, + terminal_position_handler, +) from .patterns import IosXEReloadPatterns, IosXEPatterns @@ -183,6 +186,13 @@ def boot_image(spawn, context, session): loop_continue=True, continue_timer=False) +terminal_position_stmt = Statement( + pattern=patterns.get_cursor_position, + action=terminal_position_handler, + args=None, + loop_continue=True, + continue_timer=False, +) # Statement covering when a device asks us to reset it. please_reset_stmt = \ @@ -245,4 +255,4 @@ def wrapper(spawn, session, context, **kwargs): boot_from_rommon_statement_list += connection_statement_list.copy() for stmt in boot_from_rommon_statement_list: if stmt.pattern in [reload_patterns.press_return] or stmt.loop_continue is False: - stmt.action = boot_finished_deco(stmt.action) \ No newline at end of file + stmt.action = boot_finished_deco(stmt.action) diff --git a/src/unicon/plugins/linux/patterns.py b/src/unicon/plugins/linux/patterns.py index 9fb3dcd0..04054a78 100644 --- a/src/unicon/plugins/linux/patterns.py +++ b/src/unicon/plugins/linux/patterns.py @@ -12,17 +12,17 @@ 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+)?$'.format(a=ANSI_REGEX) + self.learn_hostname = r'^.*?({a})?(?P[-\w]+)\s?([-\w\]/~:\.\d ]+)?([>\$~%#\]])\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+)?))$' + self.shell_prompt = r'^(.*?(?P((\([-\w]+\) |\x1b(?!\[\?2004).*?)?\S+)?%N\s?([-\w\]/~\s:\.\d]+)?[>\$~%#\]]\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+)?)$' + self.prompt = r'^(.*?([>\$~%\]]|\] # |[^#\s]#|~ #|~/|^admin:|^#|~\s?#\s?)\s?(\x1b\S+\s?)*)$' self.trex_console = r'^(.*?)(?Ptrex>\s*)$' diff --git a/src/unicon/plugins/nxos/patterns.py b/src/unicon/plugins/nxos/patterns.py index 3eccf370..d576f9cc 100644 --- a/src/unicon/plugins/nxos/patterns.py +++ b/src/unicon/plugins/nxos/patterns.py @@ -42,4 +42,4 @@ def __init__(self): self.commit_changes_prompt = r'Uncommitted changes found, commit them before exiting \(yes/no/cancel\)\? \[cancel\]\s*$' self.nxos_module_reload = r'This command will reload module \S+ Proceed\[y\/n]\?' self.l2rib_pycl_prompt = r'^(.*?)L2RIBCLIENT-.+>\s*?' - self.lc_bash_prompt = r'(.*?)root@lc\d+:\S+#\s*?$' + self.lc_bash_prompt = r'^(.*?)root@lc\d+:\S+#\s*?$' \ No newline at end of file diff --git a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_cat9k.yaml b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_cat9k.yaml index 3cfffb3a..a09ef350 100644 --- a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_cat9k.yaml +++ b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_cat9k.yaml @@ -695,6 +695,7 @@ c9k_exec: "term length 0": "" "term width 0": "" "show version | include operating mode" : "" + "show install summary": "" "show version": |2 Cisco IOS XE Software, Version 16.09.02 Cisco IOS Software [Fuji], Catalyst L3 Switch Software (c9k_IOSXE), Version 16.9.2, RELEASE SOFTWARE (fc4) @@ -805,6 +806,7 @@ enable_c9k: "term length 0": "" "term width 0": "" "show version | include operating mode" : "" + "show install summary": "" "show version": |2 Cisco IOS XE Software, Version 16.09.02 Cisco IOS Software [Fuji], Catalyst L3 Switch Software (c9k_IOSXE), Version 16.9.2, RELEASE SOFTWARE (fc4) @@ -1113,3 +1115,93 @@ grub_execute: MANUAL_BOOT=yes "boot": new_state: cat9k_rommon_boot + + +# ================================ +# Login sequence for login_creds + +c9k_login7: + prompt: "Username: " + commands: + "ts_user": # First credential from login_creds: [ts, default] - terminal server auth + new_state: c9k_password7_ts + +c9k_password7_ts: + prompt: "Password: " + commands: + "ts_pass": + response: | + + Password OK + + new_state: c9k_password7_ok # Intermediate state after Password OK + +c9k_password7_ok: + prompt: "" # Empty prompt - requires 'enter' to proceed + commands: + "": + new_state: c9k_login7_device # Now go to device login + +c9k_login7_device: + prompt: "Username: " + commands: + "admin": # Second credential from login_creds: [ts, default] - device credential + new_state: c9k_password7_device + +c9k_password7_device: + prompt: "Password: " + commands: + "cisco": + new_state: c9k_exec + +# ================================ +# Login sequence for login_creds with fallback testing +c9k_login8: + prompt: "Username: " + commands: + "ts_user": # First credential from login_creds: [ts, default] - terminal server auth + new_state: c9k_password8_ts + +c9k_password8_ts: + prompt: "Password: " + commands: + "ts_pass": + response: | + + Password OK + + new_state: c9k_password8_ok # Intermediate state after Password OK + +c9k_password8_ok: + prompt: "" # Empty prompt - requires 'enter' to proceed + commands: + "": + new_state: c9k_login8_device # Now go to device login + +c9k_login8_device: + prompt: "Username: " + commands: + "admin": # Second credential from login_creds: [ts, default] - device credential (will fail) + new_state: c9k_password8_device_fail + +c9k_password8_device_fail: + prompt: "Password: " + commands: + "wrong_password": + response: | + % Login invalid + + new_state: c9k_login8_fallback # Default fails, go to fallback + +c9k_login8_fallback: + prompt: "Username: " + commands: + "fallback_user": # Fallback credential - this will succeed + new_state: c9k_password8_fallback + +c9k_password8_fallback: + prompt: "Password: " + commands: + "fallback_pass": + new_state: c9k_exec + diff --git a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data.yaml b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data.yaml index 5f16bfd7..7927c24b 100644 --- a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data.yaml +++ b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data.yaml @@ -941,14 +941,14 @@ enter_enable_secret: "": "Please enter a secret" "veryverybadpw": response: "%Password validation failed" - "Secret12345": + "Secret12345!": new_state: confirm_enable_secret confirm_enable_secret: prompt: " Confirm enable secret: " commands: "": "Please enter a secret" - "Secret12345": + "Secret12345!": response: |2 The following configuration command script was created: @@ -1002,7 +1002,7 @@ enable_secret_exec: enable_secret_password: prompt: "Password: " commands: - "Secret12345": + "Secret12345!": new_state: general_enable guestshell: @@ -1083,13 +1083,13 @@ management_setup_hostname: management_setup_enable_secret: prompt: "Enter enable secret: " commands: - "Secret12345": + "Secret12345!": new_state: management_setup_enable_secret2 management_setup_enable_secret2: prompt: "Confirm enable secret: " commands: - "Secret12345": + "Secret12345!": new_state: management_setup_enable_password management_setup_enable_password: @@ -1287,13 +1287,13 @@ management_setup_enable_secret1: ------------------------------------------------- prompt: "Enter enable secret: " commands: - "Secret12345": + "Secret12345!": new_state: management_setup_enable_secret_confirm1 management_setup_enable_secret_confirm1: prompt: "Confirm enable secret: " commands: - "Secret12345": + "Secret12345!": new_state: management_setup_confirm_selection @@ -1811,6 +1811,26 @@ RSA_key_message: "": new_state: general_exec +insecure_log_message: + prompt: " INSECURE DYNAMIC WARNING - Module: SSH, Command: crypto key generate rsa modulus label , Reason: An SSH hostkey has been provisioned on the device with insufficient key length, Remediation: Please provision an SSH RSA hostkey with minimum modulus size of 3072 bits for enhanced security, Submode: exec, Parent CLI: Not Applicable" + commands: + "": + new_state: general_exec +enable_reload_insecure_log1: + prompt: "%N#" + commands: + <<: *gen_enable_cmds + "reload": + new_state: enable_reload_insecure_log2 + +enable_reload_insecure_log2: + preface: "Press RETURN to get started!" + prompt: "%N>" + commands: + "": + new_state: insecure_log_message + + enable_reload_RSA_log1: prompt: "%N#" commands: diff --git a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data_vwlc.yaml b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data_vwlc.yaml index 8dd7b83c..cd33f37a 100644 --- a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data_vwlc.yaml +++ b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data_vwlc.yaml @@ -68,13 +68,13 @@ c9k_vwlc_config_dialog: c9k_vwlc_enter_enable_config_secret: prompt: "\n Enter enable secret: " commands: - "Secret12345": + "Secret12345!": new_state: c9k_vwlc_confirm_enable_config_secret c9k_vwlc_confirm_enable_config_secret: prompt: " Confirm enable secret: " commands: - "Secret12345": + "Secret12345!": new_state: c9k_vwlc_enter_enable_secret_selection response: |2 @@ -120,13 +120,13 @@ c9k_vwlc_enter_encryption_config_selection: c9k_vwlc_enter_enable_config_current_secret: prompt: "\nEnter enable secret []: " commands: - "Secret12345": + "Secret12345!": new_state: c9k_vwlc_enter_enable_config_current_secret_again c9k_vwlc_enter_enable_config_current_secret_again: prompt: "\nConfirm enable secret []: " commands: - "Secret12345": + "Secret12345!": new_state: c9k_vwlc_enter_current_secret_selection response: |2 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 30d65e5a..8c0dd9ef 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 @@ -716,6 +716,11 @@ ansi_prompt: commands: *cmds +ansi_prompt2: + prompt: "\x1b[0;32m[user@host:~] >\x1b[m \x1b[m\x0f" + commands: *cmds + + slow_connection_exec: preface: response: "" diff --git a/src/unicon/plugins/tests/test_plugin_iosxe.py b/src/unicon/plugins/tests/test_plugin_iosxe.py index 5b272816..d0e3c42f 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe.py @@ -816,7 +816,7 @@ def setUpClass(cls): os='iosxe', platform='cat9k', credentials=dict(default=dict( - username='cisco', password='cisco', enable_password='Secret12345')), + username='cisco', password='cisco', enable_password='Secret12345!')), log_buffer=True, mit=True ) @@ -1212,7 +1212,7 @@ def test_enable_secret(self): os='iosxe', init_exec_commands=[], init_config_commands=[], - credentials=dict(default=dict(password='Secret12345')), + credentials=dict(default=dict(password='Secret12345!')), log_buffer=True ) c.connect() @@ -1238,7 +1238,7 @@ def test_enable_secret_topology_legacy(self): R1: os: iosxe passwords: - enable: Secret12345 + enable: Secret12345! connections: cli: command: mock_device_cli --os iosxe --state initial_config_dialog --hostname R1 @@ -1258,7 +1258,7 @@ def test_enable_secret_topology(self): os: iosxe credentials: default: - password: Secret12345 + password: Secret12345! connections: cli: command: mock_device_cli --os iosxe --state initial_config_dialog --hostname R1 @@ -1437,6 +1437,7 @@ def test_handler_ddns_pattern(self): d.connect() finally: d.disconnect() + def test_reload_security_log_message(self): d = Connection( @@ -1468,6 +1469,21 @@ def test_reload_RSA_key_log_message(self): finally: d.disconnect() + def test_reload_Insecure_log_message(self): + d = Connection( + hostname='Router', + start=['mock_device_cli --os iosxe --state enable_reload_insecure_log1 --hostname Router'], + os='iosxe', + credentials=dict(default=dict(username='cisco', password='cisco')), + log_buffer=True, + mit=True, + ) + try: + d.connect() + d.reload(post_reload_wait_time=3) + finally: + d.disconnect() + class TestIosxeAsr1k(unittest.TestCase): def test_connect_asr1k_ha(self): diff --git a/src/unicon/plugins/tests/test_plugin_iosxe_cat9k.py b/src/unicon/plugins/tests/test_plugin_iosxe_cat9k.py index 675fb42e..424929f6 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe_cat9k.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe_cat9k.py @@ -87,6 +87,16 @@ def test_boot_from_rommon(self): md.stop() def test_connect_fallback(self): + """ + Test the fallback credentials functionality for IOSXE Cat9k devices. + + This test verifies that when the default credentials fail to authenticate, + the connection process falls back to the configured fallback credentials + (set1) and successfully establishes a connection to the device. + + Expected behavior: Connection should succeed using fallback credentials + when default credentials are not accepted by the device. + """ md = MockDeviceTcpWrapperIOSXE(port=0, state='c9k_login5', hostname='switch') md.start() @@ -118,6 +128,10 @@ def test_connect_fallback(self): try: device.connect() self.assertEqual(device.state_machine.current_state, 'enable') + self.assertIn('current_credentials', device.credentials) + self.assertEqual(device.credentials['current_credentials']['username'], 'cisco') + # Should match the fallback credential, not default + self.assertEqual(device.credentials['current_credentials'], device.credentials['set1']) finally: device.disconnect() md.stop() @@ -158,6 +172,120 @@ def test_connect_fallback_login_handler(self): device.disconnect() md.stop() + def test_connect_login_creds(self): + """ + Test that login_creds stores the last credential as current_credentials. + + This test verifies that when multiple credentials are specified in login_creds, + the connection process correctly stores the last successfully used credential + as the current_credentials in the device's credentials dictionary. + + Expected behavior: + - TS credentials pass initially but are not stored as current_credentials + - Default credentials are used for enable mode and stored as current_credentials + - Device successfully connects and reaches enable state + - current_credentials should reflect the 'default' credential set + + This ensures proper credential tracking for subsequent operations. + """ + md = MockDeviceTcpWrapperIOSXE(port=0, state='c9k_login7', hostname='switch') + md.start() + + testbed = """ + devices: + R1: + os: iosxe + type: cat9k + credentials: + default: + username: admin + password: cisco + ts: + username: ts_user + password: ts_pass + connections: + defaults: + class: unicon.Unicon + a: + protocol: telnet + ip: 127.0.0.1 + port: {} + login_creds: [ts, default] + """.format(md.ports[0]) + + tb = loader.load(testbed) + device = tb.devices.R1 + try: + device.connect() + self.assertEqual(device.state_machine.current_state, 'enable') + self.assertIn('current_credentials', device.credentials) + self.assertEqual(device.credentials['current_credentials']['username'], 'admin') + # Should match the 'default' credential, not 'ts' + self.assertEqual(device.credentials['current_credentials'], device.credentials['default']) + finally: + device.disconnect() + md.stop() + + def test_connect_login_creds_with_fallback(self): + """ + Test login credentials with fallback mechanism for IOSXE Cat9k device connection. + + This test verifies the credential fallback behavior when multiple credential sets + are configured and some fail during authentication: + + Expected behavior: + - TS credentials pass initially + - Default credentials fail (wrong password) + - Fallback credentials are attempted and succeed + - Fallback credentials are stored as current_credentials + - Device successfully connects and reaches enable state + """ + md = MockDeviceTcpWrapperIOSXE(port=0, state='c9k_login8', hostname='switch') + md.start() + + testbed = """ + devices: + R1: + os: iosxe + type: cat9k + credentials: + default: + username: admin + password: wrong_password # This will fail + ts: + username: ts_user + password: ts_pass + fallback_set: + username: fallback_user + password: fallback_pass + connections: + defaults: + class: unicon.Unicon + fallback_credentials: + - fallback_set + a: + protocol: telnet + ip: 127.0.0.1 + port: {} + login_creds: [ts, default] + """.format(md.ports[0]) + + tb = loader.load(testbed) + device = tb.devices.R1 + try: + device.connect() + self.assertEqual(device.state_machine.current_state, 'enable') + # Verify that fallback credential is stored as current_credentials + # Since default failed and fallback succeeded, fallback_set should be stored + self.assertIn('current_credentials', device.credentials) + self.assertEqual(device.credentials['current_credentials']['username'], 'fallback_user') + # Should match the fallback credential, not default or ts + self.assertEqual(device.credentials['current_credentials'], device.credentials['fallback_set']) + + finally: + device.disconnect() + md.stop() + def test_reload_image_from_rommon(self): md = MockDeviceTcpWrapperIOSXE(port=0, state='cat9k_rommon') md.start() diff --git a/src/unicon/plugins/tests/test_plugin_iosxe_cat9k_vwlc.py b/src/unicon/plugins/tests/test_plugin_iosxe_cat9k_vwlc.py index f1bc4968..42e70c64 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe_cat9k_vwlc.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe_cat9k_vwlc.py @@ -21,7 +21,7 @@ def test_reload(self): platform='cat9k', type='vWLC', credentials=dict(default=dict(username='admin', password='cisco'), - enable=dict(password='Secret12345')), + enable=dict(password='Secret12345!')), learn_hostname=True, log_buffer=True, init_exec_commands=[], diff --git a/src/unicon/plugins/tests/test_plugin_iosxe_ha.py b/src/unicon/plugins/tests/test_plugin_iosxe_ha.py index a20e7d4f..fe1796b5 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe_ha.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe_ha.py @@ -172,7 +172,7 @@ def setUpClass(cls): username: cisco password: cisco enable: - password: Secret12345 + password: Secret12345! connections: defaults: class: unicon.Unicon diff --git a/src/unicon/plugins/tests/test_plugin_linux.py b/src/unicon/plugins/tests/test_plugin_linux.py index 28b430e9..c3d8fc98 100644 --- a/src/unicon/plugins/tests/test_plugin_linux.py +++ b/src/unicon/plugins/tests/test_plugin_linux.py @@ -297,7 +297,7 @@ def test_prompt_removal(self): class TestLearnHostname(unittest.TestCase): def test_learn_hostname(self): - states = { + states_to_host = { 'exec': 'Linux', 'exec2': 'Linux', 'exec3': 'Linux', @@ -318,10 +318,11 @@ def test_learn_hostname(self): 'exec18': LinuxSettings().DEFAULT_LEARNED_HOSTNAME, 'exec20': 'Linux', 'exec21': 'mock-server', - 'ansi_prompt': 'apc' + 'ansi_prompt': 'apc', + 'ansi_prompt2': 'host', } - for state in states: + for state in states_to_host: print('\n\n## Testing state %s ##' % state) testbed = """ devices: @@ -341,10 +342,10 @@ def test_learn_hostname(self): tb = loader.load(testbed) c = tb.devices.lnx c.connect(learn_hostname=True) - self.assertEqual(c.learned_hostname, states[state]) + self.assertEqual(c.learned_hostname, states_to_host[state]) # only check for supported prompts - if states[state] != LinuxSettings().DEFAULT_LEARNED_HOSTNAME: + if states_to_host[state] != LinuxSettings().DEFAULT_LEARNED_HOSTNAME: x = c.execute('xml') self.assertEqual(x.replace('\r', ''), mock_data['exec']['commands']['xml']['response'].strip()) x = c.execute('banner1')