diff --git a/docs/changelog/2025/may.rst b/docs/changelog/2025/may.rst new file mode 100644 index 00000000..a7e23904 --- /dev/null +++ b/docs/changelog/2025/may.rst @@ -0,0 +1,41 @@ +May 2025 +========== + + - Unicon v25.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.5 + ``unicon``, v25.5 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* connection provider + * moved the logic of boot_device to a separate function before designating handles + * added the init_active to handle the learn_hostname instead of having it in designate handles + * Store "current_credentials" under device.credentials when credentials are used + +* connection + * Added logging per subconnection for DualRp, Stack and Quad connection + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * service implementation + * Update the state for debug mode in attach service. + + diff --git a/docs/changelog/index.rst b/docs/changelog/index.rst index 29318f11..aa6b2dc6 100644 --- a/docs/changelog/index.rst +++ b/docs/changelog/index.rst @@ -4,6 +4,7 @@ Changelog .. toctree:: :maxdepth: 2 + 2025/may 2025/april 2025/march 2025/february diff --git a/docs/changelog_plugins/2025/may.rst b/docs/changelog_plugins/2025/may.rst new file mode 100644 index 00000000..696fff0f --- /dev/null +++ b/docs/changelog_plugins/2025/may.rst @@ -0,0 +1,64 @@ +May 2025 +========== + + - Unicon.Plugins v25.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.5 + ``unicon``, v25.5 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* iosxe/cat9k/stackwise_virtual + * Added support for SVL + +* iosxe/cat9k/c9500x/stackwise_virtual + * Added support for SVL + +* generic + * Add loghandler for subconnections to capture the buffer output + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Made it so incorrect login errors will attempt to use fallback credentials + +* nxos + * Add support for bash_console with module argument. + * Make l2rib_dt_prompt pattern more strict + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* nxos + * Added support for configure session + * When using device.configure() you can now pass a session name with session="session_name" + * IE device.configure("...", session="my_session") + +* iosxe + * IosXEPatterns + * Updated the recovery-mode regex to match prompt for both mode + * Added the rp-rec-mode regex to match prompt + * Added acm state and transition support to IOS-XE plugin. + * Enhanced Configure and HAConfigure services to support ACM CLI via acm_configlet argument using context-driven state transitions. + * Added context-based transition function to enter ACM mode using acm configlet create . + * Added post-service transition to gracefully return to enable mode after configuration. + + diff --git a/docs/changelog_plugins/index.rst b/docs/changelog_plugins/index.rst index 7d379485..6df7e7aa 100644 --- a/docs/changelog_plugins/index.rst +++ b/docs/changelog_plugins/index.rst @@ -4,6 +4,7 @@ Plugins Changelog .. toctree:: :maxdepth: 2 + 2025/may 2025/april 2025/march 2025/february diff --git a/docs/user_guide/services/nxos.rst b/docs/user_guide/services/nxos.rst index 350eed9f..9ba86832 100644 --- a/docs/user_guide/services/nxos.rst +++ b/docs/user_guide/services/nxos.rst @@ -13,11 +13,55 @@ For list of all the other service please refer this: * mandatory arguments are marked with `*`. +bash_console +------------ + +Service to execute commands in the router Bash. ``bash_console`` +gives you a router-like object to execute commands on using python context +managers. + +=========== ====================== =========================================== +Argument Type Description +=========== ====================== =========================================== +timeout int (default 60 sec) timeout in sec for executing commands +target str 'standby' to bring standby console to bash. +enable_bash bool (default: True) enable bash service on device. +module str module to connect to (optional) +command str command to use with `run bash {command}` + (optional) +=========== ====================== =========================================== + +Bash service will be enabled by default on devices that require the service to +be configured. Bash configuration will be done on first invocation of the +bash_console service. + +You can specify the module name to use rlogin from the bash (root) shell to +connect to the module shell. The command will default to `sudo su`. + +.. code-block:: python + + with device.bash_console() as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + + with device.bash_console(module='lc1') as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + +To run commands in the root shell, use `command="sudo su"`. + +.. code-block:: python + + with device.bash_console(command='sudo su') as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + + shellexec --------- -Service to execute commands on the Bourne-Again SHell (Bash). - +Service to execute commands on the Bourne-Again SHell (Bash). Similar to +``bash_console``. ========== ====================== ======================================== Argument Type Description diff --git a/docs/user_guide/supported_platforms.rst b/docs/user_guide/supported_platforms.rst index 7e46fd97..aeae35e7 100644 --- a/docs/user_guide/supported_platforms.rst +++ b/docs/user_guide/supported_platforms.rst @@ -14,7 +14,7 @@ the iosxe table, it will fallback to use the generic ``iosxe`` plugin. If .. tip:: - The priority to pick up which plugin is: chassis_type > os > platform > model. + The priority to pick up which plugin is: chassis_type > os > platform > model > submodel. .. important:: @@ -30,8 +30,8 @@ the iosxe table, it will fallback to use the generic ``iosxe`` plugin. If .. csv-table:: Unicon Supported Platforms :align: center - :widths: 20, 20, 20, 40 - :header: "os", "platform", "model", "Comments" + :widths: 20, 20, 20, 20, 40 + :header: "os", "platform", "model", "submodel", "Comments" ``apic`` ``aireos`` @@ -46,21 +46,22 @@ the iosxe table, it will fallback to use the generic ``iosxe`` plugin. If ``confd``, ``nfvis`` ``dnos6`` ``dnos10`` - ``fxos``,,,"Tested with FP2K." + ``fxos``,,,,"Tested with FP2K." ``fxos``, ``fp4k`` ``fxos``, ``fp9k`` - ``fxos``, ``ftd``,,"Deprecated, please use one of the other fxos plugins." - ``gaia``, , , "Check Point Gaia OS" + ``fxos``, ``ftd``,,,"Deprecated, please use one of the other fxos plugins." + ``gaia``, , , , "Check Point Gaia OS" ``hvrp`` ``ios``, ``ap`` ``ios``, ``iol`` ``ios``, ``iosv`` - ``ios``, ``pagent``,,"See example below." + ``ios``, ``pagent``,,,"See example below." ``iosxe`` ``iosxe``, ``cat3k`` ``iosxe``, ``cat3k``, ``ewlc`` ``iosxe``, ``cat8k`` - ``iosxe``, ``cat9k`` + ``iosxe``, ``cat9k``, + ``iosxe``, ``cat9k``, ``c9500``, ``c9500x``, "See example below." ``iosxe``, ``c9800`` ``iosxe``, ``c9800``, ``ewc_ap`` ``iosxe``, ``csr1000v`` @@ -76,8 +77,8 @@ the iosxe table, it will fallback to use the generic ``iosxe`` plugin. If ``iosxr``, ``spitfire`` ``ironware`` ``ise`` - ``linux``, , , "Generic Linux server with bash prompts" - ``nd``, , , "Nexus Dashboard (ND) Linux server. identical to os: linux" + ``linux``, , , , "Generic Linux server with bash prompts" + ``nd``, , , , "Nexus Dashboard (ND) Linux server. identical to os: linux" ``nxos`` ``nxos``, ``mds`` ``nxos``, ``n5k`` @@ -85,17 +86,17 @@ the iosxe table, it will fallback to use the generic ``iosxe`` plugin. If ``nxos``, ``n9k`` ``nxos``, ``nxosv`` ``nxos``, ``aci`` - ``nso``,,, "Network Service Orchestrator" - ``ons``,,, "Optical Networking System" - ``sdwan``, ``viptela``,,"Identical to os=viptela." + ``nso``,,,, "Network Service Orchestrator" + ``ons``,,,, "Optical Networking System" + ``sdwan``, ``viptela``,,,"Identical to os=viptela." ``sros`` ``staros`` ``vos`` ``junos`` ``eos`` ``sros`` - ``viptela``,,,"Identical to os=sdwan, platform=viptela." - ``windows``,,,"Only command shell (cmd) is supported. Powershell is not supported" + ``viptela``,,,,"Identical to os=sdwan, platform=viptela." + ``windows``,,,,"Only command shell (cmd) is supported. Powershell is not supported" To use this table - locate your device's os/platform/model information, and fill your pyATS testbed YAML with it: @@ -222,6 +223,35 @@ Example: Stack router port: 2003 member: 3 <<< peer rp id +Example: Stackwise Virtual Router +--------------------------------- + +.. code-block:: yaml + + devices: + router_hostname: + os: iosxe + platform: cat9k + model: c9500 + submodel: c9500x + chassis_type: stackwise_virtual <<< define the chassis_type as 'stackwise_virtual' + credentials: + default: + username: xxx + password: yyy + enable: + password: zzz + connections: + defaults: + class: unicon.Unicon + a: + protocol: telnet + ip: 1.1.1.1 + port: 2001 + b: + protocol: telnet + ip: 1.1.1.1 + port: 2002 Example: Quad Sup router ------------------------ diff --git a/src/unicon/plugins/__init__.py b/src/unicon/plugins/__init__.py index e91989cc..ef06013d 100644 --- a/src/unicon/plugins/__init__.py +++ b/src/unicon/plugins/__init__.py @@ -1,10 +1,11 @@ -__version__ = '25.4' +__version__ = "25.5" supported_chassis = [ 'single_rp', 'dual_rp', 'stack', 'quad', + 'stackwise_virtual' ] supported_os = [ diff --git a/src/unicon/plugins/apic/patterns.py b/src/unicon/plugins/apic/patterns.py index 8d4949fd..4605c0c1 100644 --- a/src/unicon/plugins/apic/patterns.py +++ b/src/unicon/plugins/apic/patterns.py @@ -1,11 +1,13 @@ __author__ = "dwapstra" +from unicon.utils import ANSI_REGEX from unicon.plugins.generic.patterns import GenericPatterns class ApicPatterns(GenericPatterns): def __init__(self): super().__init__() + self.learn_hostname = r'^.*?({a})?(?P[-\w]+)\s?([-\w\]/~:\.\d ]+)?([>\$~%#\]])\s*(\x1b\S+)?$'.format(a=ANSI_REGEX) self.enable_prompt = r'^(.*?)((\x1b\S+)?\x00)*(%N)#\s*(\x1b\S+)?$' self.config_prompt = r'^(.*?)((\x1b\S+)?\x00)*(%N)\(config.*\)#\s*(\x1b\S+)?$' self.shell_prompt = r'^(.*?)((\x1b\S+)?\x00)*\[[-\.\w]+@((%N)\s+.*?\]#)\s*(\x1b\S+)?$' diff --git a/src/unicon/plugins/apic/statemachine.py b/src/unicon/plugins/apic/statemachine.py index 0ded1891..5972d548 100644 --- a/src/unicon/plugins/apic/statemachine.py +++ b/src/unicon/plugins/apic/statemachine.py @@ -22,13 +22,16 @@ def create(self): enable = State('enable', patterns.enable_prompt) config = State('config', patterns.config_prompt) shell = State('shell', patterns.shell_prompt) + learn_hostname = State('learn_hostname', patterns.learn_hostname) setup = State('setup', list(setup_patterns.__dict__.values())) self.add_state(enable) self.add_state(config) + self.add_state(learn_hostname) self.add_state(setup) self.add_state(shell) + self.add_path(Path(learn_hostname, enable, None, None)) enable_to_config = Path(enable, config, 'configure', None) config_to_enable = Path(config, enable, 'end', None) diff --git a/src/unicon/plugins/generic/service_implementation.py b/src/unicon/plugins/generic/service_implementation.py index fb1c7df6..354fdb34 100644 --- a/src/unicon/plugins/generic/service_implementation.py +++ b/src/unicon/plugins/generic/service_implementation.py @@ -2093,6 +2093,10 @@ def call_service(self, # noqa: C901 lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) self.connection.log.addHandler(lb) + # logging the output to subconnections + for subcon in con.subconnections: + subcon.log.addHandler(lb) + # Clear log buffer self.log_buffer.seek(0) self.log_buffer.truncate() @@ -2250,6 +2254,8 @@ def call_service(self, # noqa: C901 self.log_buffer.truncate() self.connection.log.removeHandler(lb) + for subcon in con.subconnections: + subcon.log.removeHandler(lb) self.result = True if return_output: @@ -2681,7 +2687,7 @@ class AttachModuleService(BaseService): with rtr.attach(1) as m: m.execute('show interface') m.execute(['show interface 1', 'show interface 2']) - # if we want to go to module_debug state + # if we want to go to lc_shell state with rtr.attach(1, debug=True) as m: m.execute('show interface') m.execute(['show interface 1', 'show interface 2']) @@ -2742,7 +2748,7 @@ def __enter__(self): raise NotImplementedError('Attach module state not implemented') self.conn.log.debug('+++ attaching module +++') - conn.state_machine.go_to('module_debug' if self.debug else 'module', + conn.state_machine.go_to('lc_shell' if self.debug else 'module', conn.spawn, context=self.context, timeout=self.timeout) diff --git a/src/unicon/plugins/generic/statements.py b/src/unicon/plugins/generic/statements.py index 5c3f1b55..31fd4623 100644 --- a/src/unicon/plugins/generic/statements.py +++ b/src/unicon/plugins/generic/statements.py @@ -451,6 +451,11 @@ def incorrect_login_handler(spawn, context, session): # If credentials have been supplied, there are no login retries. # The user must supply appropriate credentials to ensure login # does not fail. Skip it for the first attempt + + # Attempt fallback credentials if available + if session['current_credential']: + return + raise UniconAuthenticationError( 'Login failure, either wrong username or password') if 'incorrect_login_attempts' not in session: diff --git a/src/unicon/plugins/iosxe/cat9k/c9500x/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9500x/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/__init__.py new file mode 100644 index 00000000..ded18442 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/__init__.py @@ -0,0 +1,25 @@ +""" A Stackwise-virtual C9500X IOS-XE connection implementation. +""" + +from unicon.plugins.iosxe.stack import StackIosXEServiceList +from unicon.plugins.iosxe.stack import IosXEStackRPConnection +from unicon.plugins.iosxe.cat9k.stackwise_virtual.connection_provider import StackwiseVirtualConnectionProvider + +from . import service_implementation as svc + + +class IosXEC9500xStackwiseVirtualServiceList(StackIosXEServiceList): + + def __init__(self): + super().__init__() + self.reload = svc.SVLStackReload + + +class IosXEC9500xStackwiseVirtualRPConnection(IosXEStackRPConnection): + os = 'iosxe' + platform = 'cat9k' + model = 'c9500' + submodel = 'c9500x' + chassis_type = 'stackwise_virtual' + connection_provider_class = StackwiseVirtualConnectionProvider + subcommand_list = IosXEC9500xStackwiseVirtualServiceList diff --git a/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/service_implementation.py b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/service_implementation.py new file mode 100644 index 00000000..5b7b04c3 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/service_implementation.py @@ -0,0 +1,249 @@ +""" Stack based IOS-XE/cat9k/c9500X service implementations. """ +from time import sleep +from collections import namedtuple +from datetime import timedelta +from concurrent.futures import ThreadPoolExecutor, wait as wait_futures, ALL_COMPLETED + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.bases.routers.services import BaseService + +from unicon.plugins.iosxe.stack.utils import StackUtils +from unicon.plugins.generic.statements import custom_auth_statements +from unicon.plugins.iosxe.stack.service_statements import (switch_prompt, + stack_reload_stmt_list) + +utils = StackUtils() + + +class SVLStackReload(BaseService): + """ Service to reload the SVL stack device. + + Arguments: + reload_command: reload command to be used. default "redundancy reload shelf" + reply: Additional Dialog( i.e patterns) to be handled + timeout: Timeout value in sec, Default Value is 900 sec + image_to_boot: image to boot from rommon state + return_output: if True, return namedtuple with result and reload output + + Returns: + console True on Success, raises SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'redundancy reload shelf' + rtr.reload(reload_command="reload location all", timeout=700) + """ + + def __init__(self, connection, context, *args, **kwargs): + super().__init__(connection, context, *args, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.STACK_RELOAD_TIMEOUT + self.reload_command = "redundancy reload shelf" + self.dialog = Dialog(stack_reload_stmt_list) + + def call_service(self, + reload_command=None, + reply=Dialog([]), + timeout=None, + image_to_boot=None, + return_output=False, + member=None, + error_pattern = None, + append_error_pattern= None, + post_reload_wait_time=None, + *args, + **kwargs): + + self.result = False + if member: + reload_command = f'reload slot {member}' + reload_cmd = reload_command or self.reload_command + timeout = timeout or self.timeout + conn = self.connection.active + + if error_pattern is None: + self.error_pattern = self.connection.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if post_reload_wait_time is None: + self.post_reload_wait_time = self.connection.settings.POST_RELOAD_WAIT + else: + self.post_reload_wait_time = post_reload_wait_time + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + # update all subconnection context with image_to_boot + if image_to_boot: + for subconn in self.connection.subconnections: + subconn.context.image_to_boot = image_to_boot + reload_dialog = self.dialog + if reply: + reload_dialog = reply + reload_dialog + + custom_auth_stmt = custom_auth_statements(conn.settings.LOGIN_PROMPT, + conn.settings.PASSWORD_PROMPT) + if custom_auth_stmt: + reload_dialog += Dialog(custom_auth_stmt) + + reload_dialog += Dialog([switch_prompt]) + + conn.context['post_reload_wait_time'] = timedelta(seconds= self.post_reload_wait_time) + + conn.log.info('Processing on active rp %s-%s with timeout %s' % (conn.hostname, conn.alias, timeout)) + conn.sendline(reload_cmd) + + conn_list = self.connection.subconnections + reload_cmd_output = None + + def task(con): + + # The following multithreading logic is designed to manage + # all the subconnections within the stack. + # A loop has been implemented to handle the + # "Press RETURN to get started" prompt twice. Based on extensive + # testing during SVL reloads on 9500x devices, it was observed + # that the device is not fully ready after the first prompt. + # As a result, the logic accounts for this behavior by waiting for + # the second occurrence of the message, which is assumed to be the + # default behavior for these devices. + + for _ in range(2): + reload_cmd_output = reload_dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + self.result = reload_cmd_output.match_output + self.get_service_result() + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(task, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + + if 'state' in conn.context and conn.context.state == 'rommon': + conn.log.info(f"Waiting {self.connection.settings.STACK_ROMMON_SLEEP} seconds for all peers to come to boot state") + # If manual boot enabled wait for all peers to come to boot state. + sleep(self.connection.settings.STACK_ROMMON_SLEEP) + + conn.context.pop('state') + + def boot(con): + + # send boot command for each subconnection + utils.send_boot_cmd(con, timeout, self.prompt_recovery, reply) + + self.connection.log.info('Processing on rp %s-%s' % (con.hostname, con.alias)) + con.context['post_reload_timeout'] = timedelta(seconds= self.post_reload_wait_time) + + # process boot up for each subconnection + # The following multithreading logic is designed to manage + # all the subconnections within the stack. + # A loop has been implemented to handle the + # "Press RETURN to get started" prompt twice. Based on extensive + # testing during SVL reloads on 9500x devices, it was observed + # that the device is not fully ready after the first prompt. + # As a result, the logic accounts for this behavior by waiting for + # the second occurrence of the message, which is assumed to be the + # default behavior for these devices. + for _ in range(2): + utils.boot_process(con, timeout, self.prompt_recovery, reload_dialog) + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(boot, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + else: + try: + # bring device to enable mode + conn.sendline() + conn.log.info("Bringing device to any state") + conn.state_machine.go_to('any', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + + except Exception as e: + raise SubCommandFailure('Failed to bring device to disable mode.', e) from e + + # check active and standby rp is ready + self.connection.log.info('Wait for Standby RP to be ready.') + interval = self.connection.settings.RELOAD_POSTCHECK_INTERVAL + if utils.is_active_standby_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('Active and Standby RPs are ready.') + else: + self.connection.log.info('Timeout in %s secs. ' + 'Standby RP is not in Ready state. Reload failed' % timeout) + self.result = False + return + + if member: + if utils.is_all_member_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('All Members are ready.') + else: + self.connection.log.info(f'Timeout in {timeout} secs. ' + f'Member{member} is not in Ready state. Reload failed') + self.result = False + return + + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.STACK_POST_RELOAD_SLEEP) + sleep(self.connection.settings.STACK_POST_RELOAD_SLEEP) + + self.connection.log.info('Initialize the connection after reload') + self.connection.connection_provider.init_connection() + + self.connection.log.info("+++ Reload Completed Successfully +++") + self.result = True + + if return_output: + Result = namedtuple('Result', ['result', 'output']) + self.result = Result(self.result, reload_cmd_output.match_output.replace(reload_cmd, '', 1)) diff --git a/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/__init__.py b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/__init__.py new file mode 100644 index 00000000..62c6e2a7 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/__init__.py @@ -0,0 +1,23 @@ +""" A Stackwise-virtual IOS-XE connection implementation. +""" + +from unicon.plugins.iosxe.stack import StackIosXEServiceList +from unicon.plugins.iosxe.stack import IosXEStackRPConnection +from .connection_provider import StackwiseVirtualConnectionProvider + +from unicon.plugins.iosxe.stack.service_implementation import StackReload + + +class IosXECat9kStackwiseVirtualServiceList(StackIosXEServiceList): + + def __init__(self): + super().__init__() + self.reload = StackReload + + +class IosXECat9kStackwiseVirtualRPConnection(IosXEStackRPConnection): + os = 'iosxe' + platform = 'cat9k' + chassis_type = 'stackwise_virtual' + connection_provider_class = StackwiseVirtualConnectionProvider + subcommand_list = IosXECat9kStackwiseVirtualServiceList diff --git a/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py new file mode 100644 index 00000000..432bc746 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py @@ -0,0 +1,91 @@ +""" +Authors: + pyATS TEAM (pyats-support@cisco.com, pyats-support-ext@cisco.com) +""" + +from unicon.eal.dialogs import Dialog +from unicon.bases.routers.connection_provider import BaseStackRpConnectionProvider + +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 + additional dialogs and steps required for + connecting to stack device + """ + def __init__(self, *args, **kwargs): + + """ Initializes the base connection provider + """ + super().__init__(*args, **kwargs) + + def designate_handles(self): + """ Identifies the Role of each handle and designates if + it is active or standby + """ + + con = self.connection + + con.log.info('+++ designating handles for SVL stack +++') + + subcons = list(con._subconnections.items()) + subcon1_alias, subcon1 = subcons[0] + subcon2_alias, subcon2 = subcons[1] + target_alias = None + other_alias = None + + # Try to go to enable mode on both connections + for subcon in [subcon1, subcon2]: + try: + subcon.state_machine.go_to( + 'enable', + subcon.spawn, + context=subcon.context, + ) + except Exception: + pass + con.log.debug('{} in state: {}'.format(subcon.alias, subcon.state_machine.current_state)) + + if subcon1.state_machine.current_state == 'enable': + target_alias = subcon1_alias + other_alias = subcon2_alias + elif subcon2.state_machine.current_state == 'enable': + target_alias = subcon2_alias + other_alias = subcon1_alias + + con._set_active_alias(target_alias) + con._set_standby_alias(other_alias) + con._handles_designated = True + + device = con.device + + try: + # To check if the device is in SVL state + output = device.parse("show switch") + stack_info = output.get("switch", {}).get("stack", {}) + roles = [switch_info.get("role") for switch_info in stack_info.values()] + + if "active" in roles and "standby" in roles: + # Only designate handle when in SVL state + # There are case when in non-SVL the device connection + # becomes active for both connection and there isn't a standby state + # it would have either active and member state or just active state + super().designate_handles() + + except Exception: + con.log.exception("Failed to designate handle for SVL stack") + + def get_connection_dialog(self): + """ creates and returns a Dialog to handle all device prompts + appearing during initial connection to the device. + See generic/statements.py for connnection statement lists + """ + con = self.connection + custom_auth_stmt = custom_auth_statements( + self.connection.settings.LOGIN_PROMPT, + self.connection.settings.PASSWORD_PROMPT) + return con.connect_reply + \ + Dialog(custom_auth_stmt + connection_statement_list + if custom_auth_stmt else connection_statement_list) diff --git a/src/unicon/plugins/iosxe/connection_provider.py b/src/unicon/plugins/iosxe/connection_provider.py index fd2afbf2..80180e6b 100644 --- a/src/unicon/plugins/iosxe/connection_provider.py +++ b/src/unicon/plugins/iosxe/connection_provider.py @@ -32,6 +32,12 @@ def learn_tokens(self): con.spawn, context=con.context, prompt_recovery=con.prompt_recovery) + if con.state_machine.current_state in ['acm', 'config']: + con.state_machine.go_to('enable', + con.spawn, + context=con.context, + timeout=con.connection_timeout, + prompt_recovery=con.prompt_recovery) # If the learn token is not enabled we need to see if the device is in Controller-Managed mode # or it's in autonomous mode. If the device is in Controller-Managed mode, enable token discovery. if get_device_mode(con) == 'Controller-Managed': diff --git a/src/unicon/plugins/iosxe/patterns.py b/src/unicon/plugins/iosxe/patterns.py index afbcb566..a8e2334e 100644 --- a/src/unicon/plugins/iosxe/patterns.py +++ b/src/unicon/plugins/iosxe/patterns.py @@ -23,9 +23,9 @@ def __init__(self): self.want_continue_confirm = r'.*Do you want to continue\?\s*\[confirm]\s*$' self.want_continue_yes = r'.*Do you want to continue\?\s*\[y/n]\?\s*\[yes]:\s*$' self.disable_prompt = \ - r'^(.*?)(\(unlicensed\))?(WLC|Router|RouterRP|Switch|ios|switch|%N)([0-9])?(\(standby\))?(-stby)?(-standby)?(\(boot\))?(\(recovery-mode\))?>\s?$' + r'^(.*?)(\(unlicensed\))?(WLC|Router|RouterRP|Switch|ios|switch|%N)([0-9])?(\(recovery-mode\))?(\(rp-rec-mode\))?(\(standby\))?(-stby)?(-standby)?(\(boot\))?>\s?$' self.enable_prompt = \ - r'^(.*?)(\(unlicensed\))?(WLC|Router|RouterRP|Switch|ios|switch|%N)([0-9])?(\(recovery-mode\))?(\(standby\))?(-stby)?(-standby)?(\(boot\))?#[\s\x07]*$' + r'^(.*?)(\(unlicensed\))?(WLC|Router|RouterRP|Switch|ios|switch|%N)([0-9])?(\(recovery-mode\))?(\(rp-rec-mode\))?(\(standby\))?(-stby)?(-standby)?(\(boot\))?#[\s\x07]*$' self.maintenance_mode_prompt = \ r'^(.*?)(\(unlicensed\))?(WLC|Router|RouterRP|Switch|ios|switch|%N)([0-9])?(\(standby\))?(-stby)?(-standby)?(\(boot\))?\(maint-mode\)#[\s\x07]*$' self.press_enter = ReloadPatterns().press_enter @@ -41,6 +41,7 @@ def __init__(self): self.tclsh_prompt = r'^(.*?)\(tcl.*?\)#[\s\x07]*$' self.macro_prompt = r'^(.*?)(\{\.\.\}|then.else.fi)\s*>\s*$' self.unable_to_create = r'^(.*?)Unable to create.*$' + self.acm_prompt = r'^(.*?)\(acm.*?\)#[\s\x07]*$' class IosXEReloadPatterns(ReloadPatterns): diff --git a/src/unicon/plugins/iosxe/service_implementation.py b/src/unicon/plugins/iosxe/service_implementation.py index 8258ff4f..7a319659 100644 --- a/src/unicon/plugins/iosxe/service_implementation.py +++ b/src/unicon/plugins/iosxe/service_implementation.py @@ -55,6 +55,23 @@ def truncate_trailing_prompt(self, con_state, self.utils = ConfigUtils() + def pre_service(self, *args, **kwargs): + self.acm_configlet = kwargs.pop('acm_configlet', None) + self.prompt_recovery = kwargs.get('prompt_recovery', True) + + if self.acm_configlet: + self.connection.state_machine.go_to('acm', self.connection.spawn,context={'acm_configlet': self.acm_configlet}) + self.start_state = 'acm' + self.end_state = 'acm' + else: + super().pre_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if self.acm_configlet: + self.connection.state_machine.go_to('enable', self.connection.spawn) + else: + super().post_service(*args, **kwargs) + class Config(Configure): pass @@ -96,6 +113,23 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.dialog += Dialog(configure_statement_list) + def pre_service(self, *args, **kwargs): + self.acm_configlet = kwargs.pop('acm_configlet', None) + self.prompt_recovery = kwargs.get('prompt_recovery', True) + + if self.acm_configlet: + self.connection.state_machine.go_to('acm', self.connection.spawn,context={'acm_configlet': self.acm_configlet}) + self.start_state = 'acm' + self.end_state = 'acm' + else: + super().pre_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if self.acm_configlet: + self.connection.state_machine.go_to('enable', self.connection.spawn) + else: + super().post_service(*args, **kwargs) + class HAConfig(HAConfigure): pass @@ -397,6 +431,7 @@ def __init__(self, connection, context, **kwargs): self.service_name = 'tclsh' self.__dict__.update(kwargs) + class MaintenanceMode(ContextMgrBaseService): def __init__(self, connection, context, **kwargs): diff --git a/src/unicon/plugins/iosxe/stack/exception.py b/src/unicon/plugins/iosxe/stack/exception.py index 6e305b93..7f837873 100644 --- a/src/unicon/plugins/iosxe/stack/exception.py +++ b/src/unicon/plugins/iosxe/stack/exception.py @@ -1,10 +1,3 @@ class StackException(Exception): ''' base class ''' pass - -class StackMemberReadyException(StackException): - """ - Exception for when all the member of stack device is configured - """ - pass - diff --git a/src/unicon/plugins/iosxe/stack/service_implementation.py b/src/unicon/plugins/iosxe/stack/service_implementation.py index a6c5d08f..a0d301f5 100644 --- a/src/unicon/plugins/iosxe/stack/service_implementation.py +++ b/src/unicon/plugins/iosxe/stack/service_implementation.py @@ -3,16 +3,18 @@ from collections import namedtuple from datetime import datetime, timedelta import re +from concurrent.futures import ThreadPoolExecutor, wait as wait_futures, ALL_COMPLETED + from unicon.eal.dialogs import Dialog from unicon.core.errors import SubCommandFailure from unicon.bases.routers.services import BaseService -from .exception import StackMemberReadyException from .utils import StackUtils from unicon.plugins.generic.statements import custom_auth_statements, buffer_settled from unicon.plugins.generic.service_statements import standby_reset_rp_statement_list from .service_statements import (switch_prompt, stack_reload_stmt_list, + stack_reload_stmt_list_1, stack_switchover_stmt_list, stack_factory_reset_stmt_list) from unicon.plugins.generic.service_implementation import Enable as GenericEnable, Execute as GenericExecute @@ -216,12 +218,12 @@ def call_service(self, conn = self.connection.active if error_pattern is None: - self.error_pattern = conn.settings.ERROR_PATTERN + self.error_pattern = self.connection.settings.ERROR_PATTERN else: self.error_pattern = error_pattern if post_reload_wait_time is None: - self.post_reload_wait_time = conn.settings.POST_RELOAD_WAIT + self.post_reload_wait_time = self.connection.settings.POST_RELOAD_WAIT else: self.post_reload_wait_time = post_reload_wait_time @@ -246,45 +248,119 @@ def call_service(self, reload_dialog += Dialog([switch_prompt]) - conn.context['post_reload_timeout'] = timedelta(seconds= self.post_reload_wait_time) + conn.context['post_reload_wait_time'] = timedelta(seconds= self.post_reload_wait_time) - conn.log.info('Processing on active rp %s-%s' % (conn.hostname, conn.alias)) + conn.log.info('Processing on active rp %s-%s with timeout %s' % (conn.hostname, conn.alias, timeout)) conn.sendline(reload_cmd) - try: - reload_cmd_output = reload_dialog.process(conn.spawn, - timeout=timeout, - prompt_recovery=self.prompt_recovery, - context=conn.context) - self.result=reload_cmd_output.match_output + + conn_list = self.connection.subconnections + + reload_cmd_output = None + + reload_dialog2 = Dialog(stack_reload_stmt_list_1) + + def task(con): + + # The purpose of this dialog is to manage the initial interaction + # with the device during the reload process. The dialog handles + # the startup sequence until the device either displays the ROMMON + # prompt or "All switches in the stack have been discovered. + # Accelerating discovery" message indicating readiness. At this + # point, the dialog exits to proceed with subsequent operations. + reload_cmd_output = reload_dialog2.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + + # A sendline command is necessary when the device is configured + # for manual boot or when device is in enable/disable state + # during the member reload to ensure the subsequent dialog can + # proceed seamlessly. + con.sendline() + + # The dialog process outlined below manages the + # "Press RETURN to get started" prompt for each subconnection. + # This ensures that the reload process is completed successfully + # across all connections. + reload_cmd_output2 = reload_dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + + self.result = reload_cmd_output.match_output + reload_cmd_output2.match_output self.get_service_result() - except StackMemberReadyException as e: - conn.log.debug('This is an expected exception for getting out of the dialog process') - pass - except Exception as e: - raise SubCommandFailure('Error during reload', e) from e + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(task, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e if 'state' in conn.context and conn.context.state == 'rommon': + conn.log.info(f"Waiting {self.connection.settings.STACK_ROMMON_SLEEP} seconds for all peers to come to boot state ") # If manual boot enabled wait for all peers to come to boot state. sleep(self.connection.settings.STACK_ROMMON_SLEEP) conn.context.pop('state') - try: + + def boot(con): + # send boot command for each subconnection - for subconn in self.connection.subconnections: - utils.send_boot_cmd(subconn, timeout, self.prompt_recovery, reply) + utils.send_boot_cmd(con, timeout, self.prompt_recovery, reply) + self.connection.log.info('Processing on rp %s-%s' % (con.hostname, con.alias)) + con.context['post_reload_timeout'] = timedelta(seconds= self.post_reload_wait_time) # process boot up for each subconnection - for subconn in self.connection.subconnections: - self.connection.log.info('Processing on rp ' - '%s-%s' % (conn.hostname, subconn.alias)) - subconn.context['post_reload_timeout'] = timedelta(seconds= self.post_reload_wait_time) - utils.boot_process(subconn, timeout, self.prompt_recovery, reload_dialog) + utils.boot_process(con, timeout, self.prompt_recovery, reload_dialog) - except Exception as e: - self.connection.log.error(e) - raise SubCommandFailure('Reload failed.', e) from e + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(boot, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e else: try: + conn.log.info("Bring device to any state") # bring device to enable mode conn.state_machine.go_to('any', conn.spawn, timeout=timeout, prompt_recovery=self.prompt_recovery, diff --git a/src/unicon/plugins/iosxe/stack/service_patterns.py b/src/unicon/plugins/iosxe/stack/service_patterns.py index bae0caad..af0c5068 100644 --- a/src/unicon/plugins/iosxe/stack/service_patterns.py +++ b/src/unicon/plugins/iosxe/stack/service_patterns.py @@ -24,4 +24,4 @@ def __init__(self): super().__init__() self.reload_entire_shelf = r'^.*?Reload the entire shelf \[confirm\]' self.reload_fast = r'^.*Proceed with reload fast\? \[confirm\]' - self.apply_config = r'.*All switches in the stack have been discovered. Accelerating discovery.*' + self.accelarating_discovery = r'^.*All switches in the stack have been discovered. Accelerating discovery' diff --git a/src/unicon/plugins/iosxe/stack/service_statements.py b/src/unicon/plugins/iosxe/stack/service_statements.py index f3a6bb05..92046cfa 100644 --- a/src/unicon/plugins/iosxe/stack/service_statements.py +++ b/src/unicon/plugins/iosxe/stack/service_statements.py @@ -1,59 +1,39 @@ """ Generic IOS-XE Stack Service Statements """ -from time import time, sleep -from datetime import datetime, timedelta from unicon.eal.dialogs import Statement -from unicon.plugins.generic.service_statements import reload_statement_list -from unicon.plugins.generic.statements import buffer_settled -from unicon.plugins.iosxe.service_statements import factory_reset_confirm, are_you_sure_confirm -from .service_patterns import StackIosXESwitchoverPatterns, StackIosXEReloadPatterns -from .exception import StackMemberReadyException +from unicon.plugins.generic.service_statements import (reload_statement_list, + save_env, + reload_confirm_ios, + reload_confirm_iosxe, + reload_entire_shelf, + reload_this_shelf, + send_response) + +from unicon.plugins.iosxe.service_statements import (factory_reset_confirm, + are_you_sure_confirm) +from .service_patterns import (StackIosXESwitchoverPatterns, + StackIosXEReloadPatterns) def update_curr_state(spawn, context, state): context['state'] = state + def switchover_failed(spawn, context): context['switchover_failed'] = True + def boot_from_rommon(sm, spawn, context): cmd = "boot {}".format(context['image_to_boot']) \ if "image_to_boot" in context else "boot" spawn.sendline(cmd) + def send_boot_cmd(spawn, context): cmd = "boot {}".format(context['image_to_boot']) \ if "image_to_boot" in context else "boot" spawn.sendline(cmd) -def stack_press_return(spawn, context, session): - # for stack devices if we reload from a member console we will see 2 press return to continue. - # to make sure that we get out of the process dialog when all the members are ready we - # make sure first we match "All switches in the stack have been discovered. Accelerating discovery" in the - # buffer then we raise the StackMemberReadyException to end the process. - if session.get('apply_config_on_all_members'): - spawn.log.info('Waiting for buffer to settle') - timeout_time = context.get('post_reload_wait_time', 60) - if not isinstance(timeout_time, timedelta): - timeout_time = timedelta(seconds=timeout_time) - start_time = current_time = datetime.now() - while (current_time - start_time) < timeout_time: - if buffer_settled(spawn, wait_time=15): - spawn.log.info('Buffer settled, accessing device..') - break - current_time = datetime.now() - if (current_time - start_time) > timeout_time: - spawn.log.info('Time out, trying to acces device..') - break - spawn.sendline() - raise StackMemberReadyException - -def apply_config_on_all_switch(spawn, session): - # we need to match theis pattern to make sure all the members are ready and we can access the device - """ Handles the number of apply configure message seen after install image """ - session["apply_config_on_all_members"] = True - - # switchover service statements switchover_pat = StackIosXESwitchoverPatterns() @@ -61,28 +41,34 @@ def apply_config_on_all_switch(spawn, session): save_config = Statement(pattern=switchover_pat.save_config, action='sendline(yes)', loop_continue=True, continue_timer=False) + proceed_sw = Statement(pattern=switchover_pat.switchover_proceed, - action='sendline()', - loop_continue=True, continue_timer=False) + action='sendline()', + loop_continue=True, continue_timer=False) + commit_changes = Statement(pattern=switchover_pat.cisco_commit_changes_prompt, - action='sendline(yes)', - loop_continue=True, continue_timer=False) + action='sendline(yes)', + loop_continue=True, continue_timer=False) + term_state = Statement(pattern=switchover_pat.terminal_state, - action='sendline(\r)', - loop_continue=True, continue_timer=False) + action='sendline(\r)', + loop_continue=True, continue_timer=False) + gen_rsh_key = Statement(pattern=switchover_pat.gen_rsh_key, action='sendline()', loop_continue=True, continue_timer=False) + auto_pro = Statement(pattern=switchover_pat.auto_provision, - action='sendline(yes)', - loop_continue=True, continue_timer=False) + action='sendline(yes)', + loop_continue=True, continue_timer=False) + secure_passwd = Statement(pattern=switchover_pat.secure_passwd_std, - action='sendline(no)', - loop_continue=True, continue_timer=False) + action='sendline(no)', + loop_continue=True, continue_timer=False) build_config = Statement(pattern=switchover_pat.build_config, - action=None, - loop_continue=True, continue_timer=False) + action=None, + loop_continue=True, continue_timer=False) sw_init = Statement(pattern=switchover_pat.switchover_init, action=None, @@ -113,12 +99,6 @@ def apply_config_on_all_switch(spawn, session): loop_continue=False, continue_timer=False) -press_return_stack = Statement(pattern=switchover_pat.press_return, - action=stack_press_return, - args=None, - loop_continue=True, - continue_timer=False) - found_return = Statement(pattern=switchover_pat.press_return, args=None, loop_continue=False, @@ -148,23 +128,32 @@ def apply_config_on_all_switch(spawn, session): continue_timer=False) reload_fast = Statement(pattern=reload_pat.reload_fast, - action='sendline()', - loop_continue=True, - continue_timer=False) - -apply_config = Statement(pattern=reload_pat.apply_config, - action=apply_config_on_all_switch, - loop_continue=True, - continue_timer=False) - + action='sendline()', + loop_continue=True, + continue_timer=False) + +accelarating_discovery = Statement(pattern=reload_pat.accelarating_discovery, + action=send_response, + args=None, + loop_continue=False, + continue_timer=False) + +stack_reload_stmt_list_1 = [save_env, reload_confirm_ios, reload_confirm_iosxe, + reload_entire_shelf, reload_this_shelf, + # Below statements have loop_continue=False + # enable and disable state is needed by dialog + # processor during member reload to process the + # device state during reload + en_state, dis_state, + switch_prompt, + accelarating_discovery] stack_reload_stmt_list = list(reload_statement_list) +# The enable and disable states are needed when using `reload slot N` stack_reload_stmt_list.extend([en_state, dis_state]) -stack_reload_stmt_list.insert(0, press_return_stack) stack_reload_stmt_list.insert(0, reload_shelf) stack_reload_stmt_list.insert(0, reload_fast) -stack_reload_stmt_list.insert(0, apply_config) stack_factory_reset_stmt_list = [factory_reset_confirm, are_you_sure_confirm] diff --git a/src/unicon/plugins/iosxe/stack/utils.py b/src/unicon/plugins/iosxe/stack/utils.py index 7f656bb1..3c935cf9 100644 --- a/src/unicon/plugins/iosxe/stack/utils.py +++ b/src/unicon/plugins/iosxe/stack/utils.py @@ -7,7 +7,6 @@ from unicon.eal.dialogs import Dialog from unicon.utils import Utils, AttributeDict -from .exception import StackMemberReadyException from .service_statements import send_boot logger = logging.getLogger(__name__) @@ -87,13 +86,12 @@ def boot_process(self, connection, timeout, prompt_recovery, dialog=Dialog([])): None """ connection.spawn.sendline() - try: - dialog.process(connection.spawn, timeout=timeout, - prompt_recovery=prompt_recovery, - context=connection.context) - except StackMemberReadyException as e: - logger.debug('This is an expected exception for getting out of the dialog proceess') - pass + + # reload dialog is expected to passed here + dialog.process(connection.spawn, timeout=timeout, + prompt_recovery=prompt_recovery, + context=connection.context) + connection.state_machine.go_to('any', connection.spawn, timeout=timeout, prompt_recovery=prompt_recovery, context=connection.context) diff --git a/src/unicon/plugins/iosxe/statemachine.py b/src/unicon/plugins/iosxe/statemachine.py index 09c23d20..cf6fcdaf 100644 --- a/src/unicon/plugins/iosxe/statemachine.py +++ b/src/unicon/plugins/iosxe/statemachine.py @@ -63,6 +63,10 @@ def enable_to_maintenance_transition(statemachine, spawn, context): spawn.sendline() +def enable_to_acm_transition(state_machine, spawn, context): + configlet_name = context.get('acm_configlet', '') + spawn.sendline(f'acm configlet create {configlet_name}') + def maintenance_to_enable_transition(statemachine, spawn, context): @@ -121,6 +125,7 @@ def create(self): guestshell = State('guestshell', patterns.guestshell_prompt) rommon = State('rommon', patterns.rommon_prompt) tclsh = State('tclsh', patterns.tclsh_prompt) + acm = State('acm', patterns.acm_prompt) macro = State('macro', patterns.macro_prompt) maintenance = State('maintenance', patterns.maintenance_mode_prompt) config_pki_hexmode = State('config_pki_hexmode', patterns.config_pki_prompt) @@ -143,6 +148,9 @@ def create(self): enable_to_tclsh = Path(enable, tclsh, 'tclsh', None) tclsh_to_enable = Path(tclsh, enable, 'exit', None) + enable_to_acm = Path(enable, acm, enable_to_acm_transition, None) + acm_to_enable = Path(acm, enable, 'end', None) + macro_to_config = Path(macro, config, send_break, None) enable_to_maintanance = Path(enable, maintenance, enable_to_maintenance_transition, None) @@ -155,6 +163,7 @@ def create(self): self.add_state(config) self.add_state(guestshell) self.add_state(tclsh) + self.add_state(acm) self.add_state(macro) self.add_state(maintenance) self.add_state(config_pki_hexmode) @@ -167,6 +176,8 @@ def create(self): self.add_path(guestshell_to_enable) self.add_path(enable_to_tclsh) self.add_path(tclsh_to_enable) + self.add_path(enable_to_acm) + self.add_path(acm_to_enable) self.add_path(macro_to_config) self.add_path(enable_to_maintanance) self.add_path(maintenance_to_enable) @@ -206,6 +217,7 @@ def create(self): rommon = State('rommon', patterns.rommon_prompt) shell = State('shell', patterns.shell_prompt) tclsh = State('tclsh', patterns.tclsh_prompt) + acm = State('acm', patterns.acm_prompt) macro = State('macro', patterns.macro_prompt) def update_cur_state(sm, state): @@ -242,6 +254,9 @@ def update_cur_state(sm, state): enable_to_tclsh = Path(enable, tclsh, 'tclsh', None) tclsh_to_enable = Path(tclsh, enable, 'exit', None) + enable_to_acm = Path(enable, acm, enable_to_acm_transition, None) + acm_to_enable = Path(acm, enable, 'end', None) + macro_to_config = Path(macro, config, send_break, None) self.add_state(disable) @@ -249,6 +264,7 @@ def update_cur_state(sm, state): self.add_state(config) self.add_state(rommon) self.add_state(tclsh) + self.add_state(acm) self.add_state(macro) # Ensure that a locked standby is properly detected. @@ -264,6 +280,8 @@ def update_cur_state(sm, state): self.add_path(rommon_to_disable) self.add_path(enable_to_tclsh) self.add_path(tclsh_to_enable) + self.add_path(enable_to_acm) + self.add_path(acm_to_enable) self.add_path(macro_to_config) # Adding SHELL state to IOSXE platform. diff --git a/src/unicon/plugins/nxos/patterns.py b/src/unicon/plugins/nxos/patterns.py index 9de1725f..dc517259 100644 --- a/src/unicon/plugins/nxos/patterns.py +++ b/src/unicon/plugins/nxos/patterns.py @@ -41,4 +41,5 @@ def __init__(self): self.module_elam_insel_prompt = r'^(.*?)module-\d+(\(\w+-elam-insel\d+\))?#\s*?$' 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_dt_prompt = r'^(.*?)L2RIBCLIENT-\d+>' + self.l2rib_dt_prompt = r'^(.*?)L2RIBCLIENT-.+>\s*?$' + self.lc_bash_prompt = r'(.*?)root@lc\d+:\S+#\s*?$' diff --git a/src/unicon/plugins/nxos/service_implementation.py b/src/unicon/plugins/nxos/service_implementation.py index c6dc2897..8821e5a9 100644 --- a/src/unicon/plugins/nxos/service_implementation.py +++ b/src/unicon/plugins/nxos/service_implementation.py @@ -1614,8 +1614,8 @@ def pre_service(self, *args, **kwargs): self.end_state = 'boot' super().pre_service(*args, **kwargs) - def call_service(self, enable_bash=True, **kwargs): - super().call_service(enable_bash=enable_bash, **kwargs) + def call_service(self, enable_bash=True, module=None, command=None, **kwargs): + super().call_service(enable_bash=enable_bash, module=module, command=command, **kwargs) def post_service(self, *args, **kwargs): self.start_state = self.orig_state_state @@ -1623,6 +1623,14 @@ def post_service(self, *args, **kwargs): class ContextMgr(GenericBashService.ContextMgr): + def __init__(self, connection, module=None, command=None, **kwargs): + super().__init__(connection, **kwargs) + self.module = module + self.conn.context['_module'] = module + if self.module: + command = command or 'sudo su' + self.conn.context['_bash_command'] = command + def __enter__(self): self.conn.log.debug('+++ attaching bash shell +++') # overwrite the command to go into the shell @@ -1635,10 +1643,12 @@ def __enter__(self): self.conn.configure('feature bash', timeout=self.timeout) sm = self.conn.state_machine - sm.go_to('shell', self.conn.spawn) + sm.go_to('shell', self.conn.spawn, context=self.conn.context) - return self + if self.module: + sm.go_to('lc_shell', self.conn.spawn, context=self.conn.context) + return self class L2ribDtService(BaseService): diff --git a/src/unicon/plugins/nxos/statemachine.py b/src/unicon/plugins/nxos/statemachine.py index b2d0a3fa..caa2faa9 100644 --- a/src/unicon/plugins/nxos/statemachine.py +++ b/src/unicon/plugins/nxos/statemachine.py @@ -42,6 +42,16 @@ def shell_to_l2rib_dt_transition(state_machine, spawn, context): spawn.sendline('/isan/bin/l2rib_dt %s' % context.get('_client_id', '-r')) +def shell_to_lc_shell_transition(state_machine, spawn, context): + spawn.sendline('rlogin %s' % context.get('_module', '')) + + +def enable_to_shell_transition(state_machine, spawn, context): + command = context.get('_bash_command') + run_command = 'run bash' + (f' {command}' if command else '') + spawn.sendline(run_command) + + class NxosSingleRpStateMachine(GenericSingleRpStateMachine): def create(self): @@ -58,6 +68,7 @@ def create(self): l2rib_dt = State('l2rib_dt', patterns.l2rib_dt_prompt) boot = State('boot', patterns.boot_prompt) boot_config = State('boot_config', patterns.boot_config_prompt) + lc_shell = State('lc_shell', patterns.lc_bash_prompt) loader_to_enable = Path(loader, enable, boot_from_rommon, Dialog( boot_statement_list)) @@ -69,7 +80,7 @@ def create(self): loop_continue=True) ])) - enable_to_shell = Path(enable, shell, 'run bash', None) + enable_to_shell = Path(enable, shell, enable_to_shell_transition, None) shell_to_enable = Path(shell, enable, 'exit', None) enable_to_guestshell = Path(enable, guestshell, 'guestshell', None) @@ -94,6 +105,8 @@ def create(self): boot_to_shell = Path(boot, shell, 'start', None) shell_to_boot = Path(shell, boot, 'exit', None) + shell_to_lc_shell = Path(shell, lc_shell, shell_to_lc_shell_transition, None) + lc_shell_to_shell = Path(lc_shell, shell, 'exit', None) # Add State and Path to State Machine self.add_state(enable) @@ -109,7 +122,7 @@ def create(self): self.add_state(l2rib_dt) self.add_state(boot) self.add_state(boot_config) - + self.add_state(lc_shell) self.add_path(loader_to_enable) self.add_path(enable_to_config) @@ -131,7 +144,8 @@ def create(self): self.add_path(boot_to_enable) self.add_path(boot_to_shell) self.add_path(shell_to_boot) - + self.add_path(shell_to_lc_shell) + self.add_path(lc_shell_to_shell) self.add_default_statements(default_statement_list) diff --git a/src/unicon/plugins/tests/mock/mock_device_iosxe.py b/src/unicon/plugins/tests/mock/mock_device_iosxe.py index f08923cb..fa74ab61 100644 --- a/src/unicon/plugins/tests/mock/mock_device_iosxe.py +++ b/src/unicon/plugins/tests/mock/mock_device_iosxe.py @@ -227,6 +227,16 @@ def stack_enable(self, transport, cmd): if len(ports): for port in ports: self.set_state(port, 'stack_rommon') + for other in self.transport_handles: + prompt = self.get_prompt(other) + self._write('\n{}'.format(prompt), other) + if cmd == "reload slot 1": + ports = [p for p in self.transport_ports.keys() \ + if p != self.transport_handles[transport]] + if len(ports): + for other in self.transport_handles: + prompt = self.get_prompt(other) + self._write('\n{}'.format(prompt), other) def update_show_switch(self, transport): port = self.transport_handles[transport] @@ -286,6 +296,7 @@ def main(args=None): parser.add_argument('--state', help='initial state') parser.add_argument('--ha', action='store_true', help='HA mode') parser.add_argument('--hostname', help='Device hostname (default: Switch') + parser.add_argument('--stack', action='store_true', help='Stack mode') parser.add_argument('-d', action='store_true', help='Debug') args = parser.parse_args() @@ -304,7 +315,7 @@ def main(args=None): hostname = 'Switch' if args.ha: - md = MockDeviceTcpWrapperIOSXE(hostname=hostname, state=state) + md = MockDeviceTcpWrapperIOSXE(hostname=hostname, state=state, stack=args.stack) md.run() else: md = MockDeviceIOSXE(hostname=hostname, state=state) 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 40286fd7..c04f7e75 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 @@ -232,6 +232,12 @@ general_enable: "write memory": "" "tclsh": new_state: tclsh + "acm configlet create my_config": + response: + - | + advanced configuration mode started. + enter configuration commands, one per line. end with cntl/z. + new_state: acm_configlet "long_hostname": new_state: general_enable_long_hostname "delete /force flash:CRFT_*": "" @@ -1534,6 +1540,19 @@ tclsh: "exit": new_state: general_enable +acm_configlet: + prompt: "%N(acm)#" + commands: + "interface loopback 1": + new_state: acm_if + +acm_if: + prompt: "%N(acm-if)#" + commands: + "description test": "" + "end": + new_state: general_enable + tclsh_long_hostname: prompt: "very-very-long-hostn(tcl)#" commands: diff --git a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data_cat9k_acm_ha.yaml b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data_cat9k_acm_ha.yaml new file mode 100644 index 00000000..22a1b0e5 --- /dev/null +++ b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_data_cat9k_acm_ha.yaml @@ -0,0 +1,169 @@ +ha_cat9k_exec: + prompt: "%N>" + commands: + "show version | include operating mode": "" + "enable": + new_state: ha_cat9k_enable_password + +ha_cat9k_enable_password: + prompt: "Password:" + commands: + "lab": + new_state: ha_cat9k_enable + +ha_cat9k_enable: + prompt: "%N#" + commands: + "term length 0": "" + "term width 0": "" + "show version | include operating mode" : "" + "show version": | + Cisco IOS XE Software, Version BLD_POLARIS_DEV_LATEST_20250520_001713 + Cisco IOS Software [IOSXE], C9800 Software (C9800_IOSXE-K9), Experimental Version 17.19.20250520:011900 [BLD_POLARIS_DEV_LATEST_20250519_135055:/nobackup/mcpre/s2c-build-ws 101] + Copyright (c) 1986-2025 by Cisco Systems, Inc. + Compiled Mon 19-May-25 18:19 by mcpre + + + Cisco IOS-XE software, Copyright (c) 2005-2025 by cisco Systems, Inc. + All rights reserved. Certain components of Cisco IOS-XE software are + licensed under the GNU General Public License ("GPL") Version 2.0. The + software code licensed under GPL Version 2.0 is free software that comes + with ABSOLUTELY NO WARRANTY. You can redistribute and/or modify such + GPL code under the terms of GPL Version 2.0. For more details, see the + documentation or "License Notice" file accompanying the IOS-XE software, + or the applicable URL provided on the flyer accompanying the IOS-XE + software. + + + ROM: 16.12(3r) + + Y8-9800-2042-2043 uptime is 19 hours, 43 minutes + Uptime for this control processor is 19 hours, 45 minutes + System returned to ROM by Image Install + System image file is "bootflash:packages.conf" + Last reload reason: Image Install + + + + This product contains cryptographic features and is subject to United + States and local country laws governing import, export, transfer and + use. Delivery of Cisco cryptographic products does not imply + third-party authority to import, export, distribute or use encryption. + Importers, exporters, distributors and users are responsible for + compliance with U.S. and local country laws. By using this product you + agree to comply with applicable laws and regulations. If you are unable + to comply with U.S. and local laws, return this product immediately. + + A summary of U.S. laws governing Cisco cryptographic products may be found at: + http://www.cisco.com/wwl/export/crypto/tool/stqrg.html + + If you require further assistance please contact us by sending email to + export@cisco.com. + + License Type: Smart License is permanent + License Level: adventerprise + Next reload license Level: adventerprise + AIR License Level: AIR Network Advantage addon AIR DNA Advantage + Next reload AIR License Level: AIR Network Advantage addon AIR DNA Advantage + Cisco Wireless License Level: Cisco Wireless Advantage + Next reload Cisco Wireless License Level: Cisco Wireless Advantage + Cisco Wireless License enforcement: DISABLE + + The current crypto throughput level is 0 kbps + + + Smart Licensing Status: Smart Licensing Using Policy + + cisco C9800-L-C-K9 (KATAR) processor (revision KATAR) with 1609943K/6147K bytes of memory. + Processor board ID FCL24180011 + Router operating mode: Autonomous + 2 Virtual Ethernet interfaces + 4 2.5 Gigabit Ethernet interfaces + 2 Ten Gigabit Ethernet interfaces + 32768K bytes of non-volatile configuration memory. + 16777216K bytes of physical memory. + 26251263K bytes of eUSB flash at bootflash:. + 26251263K bytes of eUSB flash at bootflash-2:. + 0K bytes of Cloud S3 Storage at cloudfs:. + + Base Ethernet MAC Address : F4:BD:9E:58:48:80 + + Installation mode is INSTALL + + Configuration register is 0x2102 + "sh redundancy stat | inc my state": |2 + my state = 13 -ACTIVE + "config term": + new_state: ha_cat9k_config + "sh redundancy state": |2 + my state = 13 -ACTIVE + peer state = 8 -STANDBY HOT + Mode = Duplex + Unit = Primary + Unit ID = 48 + + Redundancy Mode (Operational) = sso + Redundancy Mode (Configured) = sso + Redundancy State = sso + Maintenance Mode = Disabled + Manual Swact = enabled + Communications = Up + + client count = 84 + client_notification_TMR = 30000 milliseconds + RF debug mask = 0x0 + "acm configlet create my_config": + response: + - | + advanced configuration mode started. + enter configuration commands, one per line. end with cntl/z. + new_state: acm_configlet1 + +ha_cat9k_config: + prompt: "%N(conf)#" + commands: &config_cmds + "no logging console": "" + "line vty 0 4": "" + "line console 0": "" + "exec-timeout 0": "" + "redundancy": "" + "main-cpu": "" + "standby console enable": "" + "config-register 0x0": "" + "end": + new_state: ha_cat9k_enable + +ha_cat9k_stby_exec: + prompt: "%N-stby>" + commands: + "show version | include operating mode": "" + "enable": + new_state: "ha_cat9k_stby_enable_password" + +ha_cat9k_stby_enable_password: + prompt: "Password: " + commands: + "lab": + new_state: ha_cat9k_stby_enable + +ha_cat9k_stby_enable: + prompt: "%N-stby#" + commands: + "term length 0": "" + "term width 0": "" + "show version": | + Cisco IOS XE Software, Version BLD_V177_THROTTLE_LATEST_20210903_031009_V17_7_0_94 + "show version | include operating mode" : "" + +acm_configlet1: + prompt: "%N(acm)#" + commands: + "interface loopback 1": + new_state: acm_if1 + +acm_if1: + prompt: "%N(acm-if)#" + commands: + "description test": "" + "end": + new_state: general_enable diff --git a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_stack.yaml b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_stack.yaml index 5761675a..630114e2 100644 --- a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_stack.yaml +++ b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_mock_stack.yaml @@ -369,14 +369,14 @@ stack_rommon_1: "boot": response: | Booting...(use SKIP_POST)Up 1000 Mbps Full duplex (port 0) (SGMII) - + The system is not configured to boot automatically. The following command will finish loading the operating system software: - + boot - - + + switch: boot Reading full image into memory.............................................................................................................................................................................................................................................................................................................................................................................................................................................................................done Bundle Image @@ -412,21 +412,9 @@ stack_rommon_1: ##### Switch number is 2 All switches in the stack have been discovered. Accelerating discovery + This software version supports only Smart Licensing as the software licensing mechanism. + + Press RETURN to get started! timing: - 0:,0,0.005 - new_state: stack_press_return - - - -stack_press_return: - prompt: "This software version supports only Smart Licensing as the software licensing mechanism." - commands: - "": - new_state: stack_press_return_1 - - -stack_press_return_1: - prompt: "Press RETURN to get started!" - commands: - "": - new_state: stack_exec \ No newline at end of file + new_state: stack_exec diff --git a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_stack_reload.txt b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_stack_reload.txt index 0624a042..c5d3cf74 100644 --- a/src/unicon/plugins/tests/mock_data/iosxe/iosxe_stack_reload.txt +++ b/src/unicon/plugins/tests/mock_data/iosxe/iosxe_stack_reload.txt @@ -1,17 +1,3 @@ -redundancy reload shelf - -System configuration has been modified. Save? [yes/no]: n -Reload the entire shelf [confirm] -Preparing to reload this shelf -reload fp action requested -process exit with reload stack code - - -watchdog: watchdog0: watchdog did not stop! -reboot: Restarting system - - - Booting...(use SKIP_POST)Up 1000 Mbps Full duplex (port 0) (SGMII) The system is not configured to boot automatically. The diff --git a/src/unicon/plugins/tests/mock_data/nxos/nxos_mock_data.yaml b/src/unicon/plugins/tests/mock_data/nxos/nxos_mock_data.yaml index b19c5516..c364845c 100644 --- a/src/unicon/plugins/tests/mock_data/nxos/nxos_mock_data.yaml +++ b/src/unicon/plugins/tests/mock_data/nxos/nxos_mock_data.yaml @@ -371,7 +371,7 @@ config_dual: bash: prompt: "bash-4.2$ " - commands: + commands: &bash_commands "ls": | bootflash system bin "sudo yum list installed | grep n9000": | @@ -383,6 +383,26 @@ bash: new_state: nxos_l2rib_dt "exit": new_state: exec + "rlogin lc1": + new_state: lc_shell + "sudo su": + new_state: bash_root + +bash_root: + prompt: "bash-4.2# " + commands: + <<: *bash_commands + "exit": + new_state: bash + + +lc_shell: + prompt: "root@lc1:~# " + commands: + "ls": | + bootflash system bin + exit: + new_state: bash guestshell: prompt: "[admin@guestshell ~]$" diff --git a/src/unicon/plugins/tests/test_plugin_iosxe.py b/src/unicon/plugins/tests/test_plugin_iosxe.py index f8cc75bc..a392fd67 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe.py @@ -1479,6 +1479,51 @@ def test_tclsh_long_hostname(self): c.enable() c.disconnect() +class TestIosxeAcmConfigure(unittest.TestCase): + + def test_acm_configure(self): + c = Connection( + hostname='PE1', + start=['mock_device_cli --os iosxe --state general_enable --hostname PE1'], + os='iosxe', + mit=True + ) + c.connect() + config_txt = [ + 'interface loopback 1', + 'description test' + ] + c.configure(config_txt, acm_configlet='my_config') + c.disconnect() + + def test_ha_acm_configure(self): + md = MockDeviceTcpWrapperIOSXE(port=0, state='ha_cat9k_exec,ha_cat9k_stby_exec', hostname='R1') + md.start() + + c = Connection( + hostname='R1', + start=[ + 'telnet 127.0.0.1 {}'.format(md.ports[0]), + 'telnet 127.0.0.1 {}'.format(md.ports[1]) + ], + os='iosxe', + connection_timeout=10, + credentials=dict(default=dict(password='lab')) + ) + try: + c.connect() + config_txt = [ + 'interface loopback 1', + 'description test' + ] + c.configure(config_txt, acm_configlet='my_config') + except Exception: + raise + finally: + c.disconnect() + md.stop() + + class TestConfigTransition(unittest.TestCase): def test_config_transition_setting(self): diff --git a/src/unicon/plugins/tests/test_plugin_iosxe_stack.py b/src/unicon/plugins/tests/test_plugin_iosxe_stack.py index 88b7a156..5c3af3d6 100644 --- a/src/unicon/plugins/tests/test_plugin_iosxe_stack.py +++ b/src/unicon/plugins/tests/test_plugin_iosxe_stack.py @@ -231,10 +231,12 @@ def test_reload(self): tacacs_password='cisco', enable_password='cisco') d.settings.STACK_POST_RELOAD_SLEEP = 0 + d.settings.STACK_ROMMON_SLEEP = 1 + d.settings.POST_RELOAD_WAIT = 1 d.connect() self.assertTrue(d.active.alias == 'peer_1') - d.reload() + d.reload(timeout=10) d.disconnect() md.stop() @@ -250,10 +252,12 @@ def test_reload_member(self): tacacs_password='cisco', enable_password='cisco') d.settings.STACK_POST_RELOAD_SLEEP = 0 + d.settings.STACK_ROMMON_SLEEP = 1 + d.settings.POST_RELOAD_WAIT = 1 d.connect() self.assertTrue(d.active.alias == 'peer_1') - d.reload(member=1) + d.reload(member=1, timeout=10) d.disconnect() md.stop() @@ -279,10 +283,13 @@ def test_reload_with_error_pattern(self): try: d.connect() d.settings.STACK_POST_RELOAD_SLEEP = 0 + d.settings.STACK_ROMMON_SLEEP = 1 + d.settings.POST_RELOAD_WAIT = 1 with self.assertRaises(SubCommandFailure): d.reload('active_install_add', reply=install_add_one_shot_dialog, - error_pattern = error_pattern) + error_pattern = error_pattern, + timeout=10) self.assertEqual(d.reload.error_pattern, error_pattern) finally: d.disconnect() @@ -301,10 +308,12 @@ def test_reload_member_with_post_reload_wait_time(self): enable_password='cisco', post_reload_wait_time='120') d.settings.STACK_POST_RELOAD_SLEEP = 0 + d.settings.STACK_ROMMON_SLEEP = 1 + d.settings.POST_RELOAD_WAIT = 1 d.connect() self.assertTrue(d.active.alias == 'peer_1') - d.reload(member=1) + d.reload(member=1, timeout=10) d.disconnect() md.stop() diff --git a/src/unicon/plugins/tests/test_plugin_nxos.py b/src/unicon/plugins/tests/test_plugin_nxos.py index a8dda123..78a36731 100644 --- a/src/unicon/plugins/tests/test_plugin_nxos.py +++ b/src/unicon/plugins/tests/test_plugin_nxos.py @@ -144,6 +144,23 @@ def test_bash_ha_standby(self): finally: ha.stop() + def test_bash_module(self): + c = Connection(hostname='switch', + start=['mock_device_cli --os nxos --state exec'], + os='nxos', + username='cisco', + tacacs_password='cisco') + + try: + c.connect() + with c.bash_console(module='lc1') as console: + output = console.execute('ls') + self.assertEqual(output, 'bootflash system bin') + self.assertIn('exit', c.spawn.match.match_output) + self.assertIn('switch#', c.spawn.match.match_output) + finally: + c.disconnect() + class TestNxosPluginGuestshellService(unittest.TestCase):