diff --git a/pkgs/clean-pkg/changelog/2023/september.rst b/pkgs/clean-pkg/changelog/2023/september.rst index 07ea30f68..275449448 100644 --- a/pkgs/clean-pkg/changelog/2023/september.rst +++ b/pkgs/clean-pkg/changelog/2023/september.rst @@ -6,9 +6,6 @@ * Added * Class ConfigureReplace in clean stages -* stages/iosxe - * Updated connect stage to support rommon boot - -------------------------------------------------------------------------------- Fix diff --git a/pkgs/clean-pkg/sdk_generator/output/github_clean.json b/pkgs/clean-pkg/sdk_generator/output/github_clean.json index 1abe7294b..05a08e8d5 100644 --- a/pkgs/clean-pkg/sdk_generator/output/github_clean.json +++ b/pkgs/clean-pkg/sdk_generator/output/github_clean.json @@ -166,18 +166,13 @@ "url": "https://github.com/CiscoTestAutomation/genielibs/tree/master/pkgs/clean-pkg/src/genie/libs/clean/stages/stages.py#L33" }, "iosxe": { - "doc": "This stage connects to the device that is being cleaned.\nStage Schema\n------------\nconnect:\n via (str, optional): Which connection to use from the testbed file. Uses the\n default connection if not specified.\n alias (str, optional): Which connection alias to use from the testbed file.\n timeout (int, optional): The timeout for the connection to complete in seconds.\n Defaults to 200.\n retry_timeout (int, optional): Overall timeout for retry mechanism in seconds.\n Defaults to 0 which means no retry.\n retry_interval (int, optional): Interval for retry mechanism in seconds. Defaults\n to 0 which means no retry.\nExample\n-------\nconnect:\n timeout: 60\n", - "module_name": "stages.stages", - "package": "genie.libs.clean", "sdwan": { "doc": "This stage connects to the device that is being cleaned.\n\nStage Schema\n------------\nconnect:\n\n via (str, optional): Which connection to use from the testbed file. Uses the\n default connection if not specified.\n\n timeout (int, optional): The timeout for the connection to complete in seconds.\n Defaults to 200.\n\n retry_timeout (int, optional): Overall timeout for retry mechanism in seconds.\n Defaults to 0 which means no retry.\n\n retry_interval (int, optional): Interval for retry mechanism in seconds. Defaults\n to 0 which means no retry.\n\nExample\n-------\nconnect:\n timeout: 60\n", "module_name": "stages.stages", "package": "genie.libs.clean", "uid": "Connect", "url": "https://github.com/CiscoTestAutomation/genielibs/tree/master/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/sdwan/stages.py#L31" - }, - "uid": "Connect", - "url": "https://github.com/CiscoTestAutomation/genielibs/tree/master/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/stages.py#L2039" + } }, "linux": { "wsim": { diff --git a/pkgs/clean-pkg/src/genie/libs/clean/recovery/recovery.py b/pkgs/clean-pkg/src/genie/libs/clean/recovery/recovery.py index 7fdd31050..396e7caac 100644 --- a/pkgs/clean-pkg/src/genie/libs/clean/recovery/recovery.py +++ b/pkgs/clean-pkg/src/genie/libs/clean/recovery/recovery.py @@ -213,8 +213,7 @@ def _connectivity(device, console_activity_pattern=None, console_breakboot_char= 'ip_address': list, 'subnet_mask': str, 'gateway': str, - 'tftp_server': str, - Optional('ether_port'): int, + 'tftp_server': str }, Optional('recovery_password'): str, Optional('clear_line'): bool, diff --git a/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/stages.py b/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/stages.py index b87089be8..5bb34d3ac 100644 --- a/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/stages.py +++ b/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/stages.py @@ -7,7 +7,7 @@ import time import os.path import logging -from ipaddress import IPv4Address, IPv6Address, IPv4Interface, IPv6Interface +from ipaddress import IPv4Interface, IPv6Interface # pyATS from pyats.async_ import pcall @@ -18,7 +18,7 @@ from genie.abstract import Lookup from genie.libs import clean from genie.libs.clean.recovery.recovery import _disconnect_reconnect -from genie.metaparser.util.schemaengine import Optional, Required, Any, Or, ListOf +from genie.metaparser.util.schemaengine import Optional, Required, Any from genie.utils import Dq from genie.metaparser.util.schemaengine import Optional, Required, Any, Or, ListOf from genie.utils import Dq @@ -2034,154 +2034,3 @@ def configure_replace(self, steps, device, path, file, config_replace_options=CO except Exception as e: step.failed("Configure replace failed", from_exception=e) - - -class Connect(BaseStage): - """This stage connects to the device that is being cleaned. -Stage Schema ------------- -connect: - via (str, optional): Which connection to use from the testbed file. Uses the - default connection if not specified. - alias (str, optional): Which connection alias to use from the testbed file. - timeout (int, optional): The timeout for the connection to complete in seconds. - Defaults to 200. - retry_timeout (int, optional): Overall timeout for retry mechanism in seconds. - Defaults to 0 which means no retry. - retry_interval (int, optional): Interval for retry mechanism in seconds. Defaults - to 0 which means no retry. -Example -------- -connect: - timeout: 60 -""" - - # ================= - # Argument Defaults - # ================= - VIA = None - ALIAS = None - TIMEOUT = 200 - RETRY_TIMEOUT = 0 - RETRY_INTERVAL = 0 - - - # ============ - # Stage Schema - # ============ - schema = { - Optional('via', description="Which connection to use from the testbed file. Uses the default connection if not specified."): str, - Optional('alias', description="Which connection alias to use."): str, - Optional('timeout', description=f"The timeout for the connection to complete in seconds. Defaults to {TIMEOUT}.", default=TIMEOUT): Or(str, int), - Optional('retry_timeout', description=f"Overall timeout for retry mechanism in seconds. Defaults to {RETRY_TIMEOUT} which means no retry.", default=RETRY_TIMEOUT): Or(str, int, float), - Optional('retry_interval', description=f"Interval for retry mechanism in seconds. Defaults to {RETRY_INTERVAL} which means no retry.", default=RETRY_INTERVAL): Or(str, int, float), - } - - # ============================== - # Execution order of Stage steps - # ============================== - exec_order = [ - 'connect' - ] - - def connect(self, steps, device, via=VIA, alias=ALIAS, timeout=TIMEOUT, - retry_timeout=RETRY_TIMEOUT, retry_interval=RETRY_INTERVAL): - - with steps.start("Connecting to the device") as step: - - log.info('Checking connection to device: %s' % device.name) - - # Create a timeout that will loop - retry_timeout = Timeout(float(retry_timeout), float(retry_interval)) - retry_timeout.one_more_time = True - # Without this we see 'Performing the last attempt' even if retry - # is not being used. - retry_timeout.disable_log = True - - while retry_timeout.iterate(): - retry_timeout.disable_log = False - - # mit=True is used to make sure we do not initialize the connection - # and we can check if the device is in rommon. - device.instantiate(connection_timeout=timeout, - learn_hostname=True, - prompt_recovery=True, - via=via, - alias=alias, - mit=True) - try: - if alias: - getattr(device, alias).connect() - else: - device.connect() - except Exception: - log.error("Connection to the device failed", exc_info=True) - device.destroy_all() - # Loop - else: - step.passed("Successfully connected".format(device.name)) - # Don't loop - - retry_timeout.sleep() - - step.failed("Could not connect. Scroll up for tracebacks.") - - with steps.start(f'Checking the current state of the device: {device.name}') as step: - - log.info(f'Checking the current state of the device: {device.name}') - - state = "" - try: - # To check the state for HA devices - if device.is_ha and hasattr(device, 'subconnections'): - if isinstance(device.subconnections, list): - states = list(set([con.state_machine.current_state for con in device.subconnections])) - if states == ['rommon']: - state = 'rommon' - # Remove the one rp connection for Recovery worker API device_recovery_boot - # to boot a single RP as HA recovery is not yet implemented fully. - device.subconnections = [device.subconnections[0]] - elif 'rommon' in states: - step.failed(f'One of the device connection is in rommon state, need to recover device.') - else: - state = device.state_machine.current_state - except Exception as e: - log.warning(f'There is no connection in device.subconnections: {e}') - - if state == "rommon": - - with steps.start("Setting the rommon variables") as step: - - log.info('Setting the rommon variables for TFTP boot') - - try: - device.api.configure_rommon_tftp() - except Exception as e: - step.failed(f'Failed to set rommon variables. {e}') - else: - log.info("Successfully set the rommon variables") - - with steps.start("Booting the device from rommon") as step: - - log.info('Booting the device from rommon') - - try: - # Gets the recovery details from clean yaml - device.api.device_recovery_boot() - except Exception as e: - step.failed(f'Failed to device boot device from rommon. {e}') - else: - log.info("Successfully booted the device from rommon.") - - - with steps.start("Disconnect and reconnect to the device") as step: - - try: - _disconnect_reconnect(device) - except Exception as e: - step.failed(f'Failed to initialize the connection. {e}') - else: - step.passed("Successfully connected to the device") - - - diff --git a/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/tests/test_connect.py b/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/tests/test_connect.py deleted file mode 100644 index d5170825a..000000000 --- a/pkgs/clean-pkg/src/genie/libs/clean/stages/iosxe/tests/test_connect.py +++ /dev/null @@ -1,69 +0,0 @@ -import unittest -from unittest.mock import MagicMock, Mock, call, patch -import logging - -from pyats.aetest.steps import Steps -from pyats.results import Passed, Failed -from pyats.aetest.signals import TerminateStepSignal -from pyats.topology import loader -from genie.libs.clean.stages.iosxe.stages import Connect -from unicon.plugins.tests.mock.mock_device_iosxe import MockDeviceTcpWrapperIOSXE -import unicon - -unicon.settings.Settings.POST_DISCONNECT_WAIT_SEC = 0 -unicon.settings.Settings.GRACEFUL_DISCONNECT_WAIT_SEC = 0.2 - -logger = logging.getLogger(__name__) - -class TestIosXEConnect(unittest.TestCase): - """ Run unit testing on a mocked IOSXE ASR HA device """ - - @classmethod - def setUpClass(self): - self.md = MockDeviceTcpWrapperIOSXE(hostname='R1', port=0, state='general_enable') - self.md.start() - testbed = """ - testbed: - servers: - http: - dynamic: true - protocol: http - ftp: - dynamic: true - protocol: ftp - tftp: - dynamic: true - protocol: tftp - devices: - R1: - os: iosxe - type: router - tacacs: - username: cisco - passwords: - tacacs: cisco - connections: - defaults: - class: unicon.Unicon - a: - protocol: telnet - ip: 127.0.0.1 - port: {} - """.format(self.md.ports[0]) - - self.testbed = loader.load(testbed) - self.device = self.testbed.devices['R1'] - self.device.connect(mit=True) - - @classmethod - def tearDownClass(self): - self.device.disconnect() - self.md.stop() - - def test_connect(self): - steps = Steps() - self.connect = Connect() - self.connect(steps=steps, device=self.device) - self.assertEqual(Passed, steps.details[0].result) - - diff --git a/pkgs/sdk-pkg/sdk_generator/output/github_apis.json b/pkgs/sdk-pkg/sdk_generator/output/github_apis.json index 03e0f6f65..687a3430f 100644 --- a/pkgs/sdk-pkg/sdk_generator/output/github_apis.json +++ b/pkgs/sdk-pkg/sdk_generator/output/github_apis.json @@ -13698,7 +13698,7 @@ }, "device_recovery_boot": { "com": { - "doc": "Boot device using golden image or using tftp server\n Args:\n device: device object\n break_count: \n console_activity_pattern: \n console_breakboot_char: \n console_breakboot_telnet_break: Use telnet `send break` to interrupt device boot\n grub_activity_pattern: \n grub_breakboot_char: \n timeout: \n recovery_password: \n golden_image: \n kickstart: \n system: \n tftp_boot:\n image: (Mandatory)\n ip_address: (Mandatory)\n subnet_mask: (Mandatory)\n gateway: (Mandatory)\n tftp_server: (Mandatory)\n ether_port: (Optional) (Default to 0)\n Return:\n None\n Raise:\n Exception\n ", + "doc": "Boot device using golden image or using tftp server\n Args:\n device: device object\n break_count: \n console_activity_pattern: \n console_breakboot_char: \n console_breakboot_telnet_break: Use telnet `send break` to interrupt device boot\n grub_activity_pattern: \n grub_breakboot_char: \n timeout: \n recovery_password: \n golden_image: \n kickstart: \n system: \n tftp_boot:\n image: (Mandatory)\n ip_address: (Mandatory)\n subnet_mask: (Mandatory)\n gateway: (Mandatory)\n tftp_server: (Mandatory)\n Return:\n None\n Raise:\n Exception\n ", "module_name": "utils", "package": "genie.libs.sdk.apis", "uid": "device_recovery_boot", diff --git a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/iosxe/rommon/configure.py b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/iosxe/rommon/configure.py index 837ddf976..baf0f4c80 100644 --- a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/iosxe/rommon/configure.py +++ b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/iosxe/rommon/configure.py @@ -21,22 +21,6 @@ def configure_rommon_tftp(device, ipv6_address=False): """ tftp = {} - # Get the current device state from the device - if device.is_ha and hasattr(device, 'subconnections'): - if isinstance(device.subconnections, list): - states = list(set([con.state_machine.current_state for con in device.subconnections])) - if states == ['rommon']: - state = 'rommon' - elif 'rommon' in states: - raise Exception(f'One of the device connection is in rommon state, need to recover device.') - else: - state = device.state_machine.current_state - - # check the device is in rommon - if state != 'rommon': - raise Exception(f'The device is not in rommon state') - - # Check if management attribute in device object, if not set to empty dict if not hasattr(device, 'management'): setattr(device, "management", {}) @@ -75,15 +59,10 @@ def configure_rommon_tftp(device, ipv6_address=False): log.warning(f"Some TFTP information is missing: {tftp}") for set_command, value in tftp.items(): - # To set rommon variables cmd = f'{set_command}={value}' try: - # Configure tftp rommon variables in active rp - if device.is_ha and hasattr(device, 'subconnections'): - device.subconnections[0].execute(cmd) - else: - device.execute(cmd) + device.execute(cmd) except Exception as e: raise SubCommandFailure( f"Failed to set the rommon variable {set_command}. Error:\n{e}") diff --git a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/mock_data/iosxe/mock_data.yaml b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/mock_data/iosxe/mock_data.yaml index ac221bd0e..d246da1f7 100644 --- a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/mock_data/iosxe/mock_data.yaml +++ b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/mock_data/iosxe/mock_data.yaml @@ -1,3 +1,17 @@ +configure: + commands: + end: + new_state: execute + line console 0: + new_state: configure_line + no logging console: '' + prompt: ott-c9300-63(config)# +configure_line: + commands: + end: + new_state: execute + exec-timeout 0: '' + prompt: ott-c9300-63(config-line)# connect: commands: ? '' @@ -26,4 +40,11 @@ execute: response: - '' response_type: circular - prompt: 'switch:' + config term: + new_state: configure + config-transaction: + new_state: configure + show version: '' + term length 0: '' + term width 0: '' + prompt: ott-c9300-63# diff --git a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/test_api_configure_rommon_tftp.py b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/test_api_configure_rommon_tftp.py index f928a39ff..bd44e95a0 100644 --- a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/test_api_configure_rommon_tftp.py +++ b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/tests/iosxe/rommon/configure/configure_rommon_tftp/test_api_configure_rommon_tftp.py @@ -41,7 +41,9 @@ def setUpClass(self): self.testbed = loader.load(testbed) self.device = self.testbed.devices['ott-c9300-63'] self.device.connect( - mit=True + learn_hostname=True, + init_config_commands=[], + init_exec_commands=[] ) def test_configure_rommon_tftp(self): diff --git a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/utils.py b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/utils.py index 5c5795199..5c0ad8a8c 100644 --- a/pkgs/sdk-pkg/src/genie/libs/sdk/apis/utils.py +++ b/pkgs/sdk-pkg/src/genie/libs/sdk/apis/utils.py @@ -4315,7 +4315,6 @@ def device_recovery_boot(device, console_activity_pattern=None, console_breakboo subnet_mask: (Mandatory) gateway: (Mandatory) tftp_server: (Mandatory) - ether_port: (Optional) (Default to 0) Return: None Raise: @@ -4340,8 +4339,6 @@ def device_recovery_boot(device, console_activity_pattern=None, console_breakboo grub_breakboot_char = grub_breakboot_char or recovery_info.get('grub_breakboot_char') or 'c' break_count = break_count or recovery_info.get('break_count') or 15 timeout = timeout or recovery_info.get('timeout') or 750 - ether_port = 0 - tftp_boot = tftp_boot or recovery_info.get('tftp_boot', {}) log.info('Destroy device connection.') try: @@ -4355,9 +4352,7 @@ def device_recovery_boot(device, console_activity_pattern=None, console_breakboo format(device.name))) log.info("Golden image information found:\n{}".format(golden_image)) - elif tftp_boot: - # set ether_port to default value 0 - tftp_boot.setdefault('ether_port', ether_port) + elif tftp_boot or recovery_info.get('tftp_boot'): log.info(banner("Booting device '{}' with the Tftp images".\ format(device.name))) log.info("Tftp boot information found:\n{}".format(tftp_boot)) @@ -4396,7 +4391,7 @@ def device_recovery_boot(device, console_activity_pattern=None, console_breakboo 'break_count': break_count, 'timeout': timeout, 'golden_image': golden_image or recovery_info.get('golden_image'), - 'tftp_boot': tftp_boot, + 'tftp_boot': tftp_boot or recovery_info.get('tftp_boot'), 'recovery_password': recovery_password or recovery_info.get('recovery_password')} ) except Exception as e: