AEtest
pyATS

pyATS AEtest

AEtest (Automation Easy Testing) offers a simple and straight-forward way for users to define, execute and debug testcases and testscripts. AEtest is available as a standard component (aetest) in pyATS in an effort to standardize the definition and execution of testcases & testscripts.

AEtest testscript structure is very modular and straightforward. Each testscript is split into three major container sections which are then further broken down into smaller, method sections. These three containers are Common Setup, Testcase(s), and Common Cleanup. Throughout the steps in this section of the lab, you will examine each of these.


Step 1 - Create Python File for pyATS AEtest Script

In VSCode, using the code keyword in the Terminal window, open a new file for creating a Python pyATS AEtest script:

  
      code -r /home/cisco/Documents/nxapilab/tests/pyats_aetest.py
  

Step 2 - Import pyATS & Logging Libraries

Like you did in the Python requests section for NX-API, you need to import some modules. In this case we need to import pyATS aetest module and the load module from pyATS Genie. The logging import is pretty self-explainatory; for logging. And finally, the unicon import is for catching a ConnectionError in subsequent parts of your code.

    
        from pyats import aetest
        from genie.testbed import load
        import logging
        from unicon.core.errors import ConnectionError


        logger = logging.getLogger(__name__)


        


Step 3 - Add pyATS AEtest CommonSetup Section

CommonSetup is an optional section within each testscript, defined by inheriting the aetest.CommonSetup class, and declaring one or more Subsections inside. CommonSetup always runs first, and is where all the common configurations, prerequisites and initializations shared between the script's testcases should be performed.

This includes but is not limited to the following:

  • check the validity of script inputs (arguments)
  • connect to all testbed devices & check that they are ready, with all the required images/features/hardwares/licenses (including Traffic Generators)
  • configure/bring up the device interface and/or topology
  • setup/load base configuration common/shared between all testcases
  • setup dynamic looping of testcases/sections based on current environment
  • etc.

    
        class CommonSetup(aetest.CommonSetup):
            @aetest.subsection
            def connect(self, testbed):
                """
                establishes connection to all your testbed devices.
                """
                # make sure testbed is provided
                assert testbed, "Testbed is not provided!"

                if len(testbed.devices) == 0: 
                    self.failed('{testbed} is empty'.format(testbed=str(testbed)))
                else:
                    # connect to all testbed devices
                    #   By default ANY error in the CommonSetup will fail the entire test run
                    #   Here we catch common exceptions if a device is unavailable to allow test to continue
                    try:
                        for device in testbed:
                            device.connect(via='rest')
                    except (TimeoutError, ConnectionError):
                        logger.error("Unable to connect to all devices")
        

        


Step 4 - Add pyATS AEtest Testcases Section

Testcase is a collection of smaller tests. Testcases are the actual tests to be performed against the device under test. Each testcase may have its own Setup Section and Cleanup Section, and an arbitrary number of smaller Test Sections.

Each Testcase is defined by inheriting aetest.Testcase class, and defining one or more Test Sections inside. Optionally, each Testcase may also have a single Setup Section and a single Cleanup Section. Testcases run in the order as they are defined in the testscript.

    
        class verify_connected(aetest.Testcase):
            """verify_connected
            Ensure successful connection to all devices in testbed.
            """
            @aetest.test
            def test(self, testbed, steps):
                # Loop over every device in the testbed
                for device_name, device in testbed.devices.items():
                    with steps.start(
                        f"Test Connection Status of {device_name}", continue_=True
                    ) as step:
                        # Test "connected" status
                        if device.connected:
                            logger.info(f"{device_name} connected status: {device.connected}")
                        else:
                            logger.error(f"{device_name} connected status: {device.connected}")
                            step.failed()
                            error_occurred = True


        class verify_ospf_process_id(aetest.Testcase):
            """verify_ospf_process_id
            """
            @aetest.test
            def test(self, testbed, steps):
                command = 'show ip ospf neighbors'
                # Loop over every device in the testbed
                for device_name, device in testbed.devices.items():
                    with steps.start(
                        f"Verify OSPF Process ID is UNDERLAY on {device_name}", continue_=True
                    ) as step:
                        output = device.api.nxapi_method_nxapi_cli('send', command, message_format='json', command_type='cli_show', alias='rest').json()
                        ospf_process_id = output['ins_api']['outputs']['output']['body']['TABLE_ctx']['ROW_ctx']['ptag']
                        if ospf_process_id == 'UNDERLAY':
                            logger.info(f"{device_name} OSPF Process ID: {ospf_process_id}")
                        else:
                            logger.error(f"{device_name} OSPF Process ID: {ospf_process_id}")
                            step.failed()


        class verify_ospf_neighbor_count(aetest.Testcase):
            """verify_ospf_neighbor_count
            """
            @aetest.test
            def test(self, testbed, steps):
                command = 'show ip ospf neighbors'
                # Loop over every device in the testbed
                for device_name, device in testbed.devices.items():
                    with steps.start(
                        f"Verify OSPF Neighbors on {device_name}", continue_=True
                    ) as step:
                        output = device.api.nxapi_method_nxapi_cli('send', command, message_format='json', command_type='cli_show', alias='rest').json()
                        ospf_nbr_count = output['ins_api']['outputs']['output']['body']['TABLE_ctx']['ROW_ctx']['nbrcount']
                        if 'spine' in device.name:
                            expected_ospf_nbr_count = 2
                        elif 'leaf' in device.name:
                            expected_ospf_nbr_count = 1
                        else:
                            logger.info(f"{device_name} not a spine or leaf!")
                            step.failed()
        
                        if ospf_nbr_count == expected_ospf_nbr_count:
                            logger.info(f"{device_name} OSPF Neighbor Count: {ospf_nbr_count}")
                        else:
                            logger.error(f"{device_name} OSPF Neighbor Count: {ospf_nbr_count}")
                            step.failed()
    
    
        class verify_ospf_neighbor_state(aetest.Testcase):
            """verify_ospf_neighbor_state
            """
            @aetest.test
            def test(self, testbed, steps):
                command = 'show ip ospf neighbors'
                # Loop over every device in the testbed
                for device_name, device in testbed.devices.items():
                    with steps.start(
                        f"Verify OSPF Neighbors on {device_name}", continue_=True
                    ) as step:
                        output = device.api.nxapi_method_nxapi_cli('send', command, message_format='json', command_type='cli_show', alias='rest').json()
                        ospf_nbrs = output['ins_api']['outputs']['output']['body']['TABLE_ctx']['ROW_ctx']['TABLE_nbr']['ROW_nbr']
                        if isinstance(ospf_nbrs, list):
                            for ospf_nbr in ospf_nbrs:
                                if ospf_nbr['state'] == 'FULL':
                                    logger.info(f"{device_name} OSPF Neighbor State: {ospf_nbr['state']}")
                                else:
                                    logger.error(f"{device_name} OSPF Neighbor State: {ospf_nbr['state']}")
                                    step.failed()
                        else:
                            if ospf_nbr['state'] == 'FULL':
                                    logger.info(f"{device_name} OSPF Neighbor State: {ospf_nbr['state']}")
                            else:
                                logger.error(f"{device_name} OSPF Neighbor State: {ospf_nbr['state']}")
                                step.failed()


        


Step 5 - Add pyATS AEtest Cleanup Section

CommonCleanup is the last section within each testscript. Any configurations, initializations and environment changes that occured during this script run should be cleaned up or removed here. Eg, the testbed/environment should be returned to the same state as it was before the script run. This includes but is not limited to:

  • removal of all CommonSetup changes in their appropriate, reversed order
  • removal of any lingering changes that were left from previous testcases
  • returning all devices & etc to their initial state
  • etc

    
        class CommonCleanup(aetest.CommonCleanup):
            @aetest.subsection
            def disconnect_from_devices(self, testbed):
                for device in testbed:
                    # Only disconnect if we are connected to the device
                    if device.is_connected() == True:
                        device.disconnect()


        


Step 6 - Main Function for the pyATS AEtest Script

This main function should look similar in comparison to the Python NX-API section, albeit, a bit shorter. The logging level is set first. Next, the AEtest main method is invoked to kickoff the script sections you defined.

    
        if __name__ == '__main__':
            # set logger level
            logger.setLevel(logging.INFO)

            aetest.main()
        

Executing this script by calling the aetest.main() function is considered a standalone execution. It can be done now, however addtional functionality is gained if ran using the EasyPy Execution method as seen in the next step.

After successfully populating pyats_aetest.py with the above connection information, save your pyats_aetest.py file using Ctrl+s on the Windows keyboard or by clicking File then Save.

Warning

Be sure to save your file! Not saving will result in your code not executing.


Step 7 - Create New Python File for pyATS EasyPy Script

In VSCode, using the code keyword in the Terminal window, open a new file for creating a Python pyATS EasyPy script:

  
      code -r /home/cisco/Documents/nxapilab/tests/pyats_easypy.py
  

Step 8 - Populate Python File with pyATS EasyPy Script

Add the small snippet of code below into the pyats_easypy.py file that sets the path lookup for the pyats_aetest.py script file:

    
        import os
        from pyats.easypy import run

        # compute the script path from this location
        SCRIPT_PATH = os.path.dirname(__file__)

        def main(runtime):
            '''job file entrypoint'''

            # run script
            run(testscript=os.path.join(SCRIPT_PATH, 'pyats_aetest.py'), runtime=runtime)
        

After successfully populating pyats_easypy.py with the above connection information, save your pyats_easypy.py file using Ctrl+s on the Windows keyboard or by clicking File then Save.

Warning

Be sure to save your file! Not saving will result in your code not executing.


Step 9 - Execute pyATS AEtest Script using pyATS EasyPy Script

Scripts executed with Easypy - Runtime Environment are called EasyPy Execution. In this mode, all environment handling and control is set by the EasyPy launcher. For example, the following features are available:

  • multiple aetest test scripts can be executed together, aggregated within a job file.
  • initial logging configuration is done by Easypy - Runtime Environment, with user customizations within the job file.
  • TaskLog, result report and archives are generated.
  • uses Reporter for reporting & result tracking, generating result YAML file and result details and summary XML files.
Easypy execution offers a standard, replicable test environment that creates an archive file containing log outputs & environment information for post-mortem debugging.

Execute your AEtest script using EasyPy via the pyATS command line run command, pyats run job to test and verify the expected configuration and state of OSPF. Notice the --testbed-file option that makes use of the testbed file you defined previously for how to connect to your devices.

    
        pyats run job pyats_easypy.py --testbed-file staging-testbed.yaml --archive-dir=${PWD}/results --no-archive-subdir --no-mail
    

You will have to scroll up on the Terminal some, but your test script execution should look like the below. Notice there is a summary, Task Result Summary, for each section, i.e. the sections you built out above in your code. Also notice the detailed results, Task Result Details for each testcase run against each device.

    2024-02-04T11:39:38: %EASYPY-INFO: --------------------------------------------------------------------------------
    2024-02-04T11:39:38: %EASYPY-INFO: Job finished. Wrapping up...
    2024-02-04T11:39:39: %EASYPY-INFO: Creating archive file: /home/cisco/Documents/nxapilab/tests/results/pyats_easypy.2024Feb04_11:39:34.118834.zip
    2024-02-04T11:39:39: %EASYPY-INFO: +------------------------------------------------------------------------------+
    2024-02-04T11:39:39: %EASYPY-INFO: |                                Easypy Report                                 |
    2024-02-04T11:39:39: %EASYPY-INFO: +------------------------------------------------------------------------------+
    2024-02-04T11:39:39: %EASYPY-INFO: pyATS Instance   : /home/cisco/.pyenv/versions/3.11.6/envs/nxapilab2
    2024-02-04T11:39:39: %EASYPY-INFO: Python Version   : cpython-3.11.6 (64bit)
    2024-02-04T11:39:39: %EASYPY-INFO: CLI Arguments    : /home/cisco/.pyenv/versions/nxapilab2/bin/pyats run job pyats_easypy.py --testbed-file staging-testbed.yaml --archive-dir=/home/cisco/Documents/nxapilab/tests/results --no-archive-subdir --no-mail
    2024-02-04T11:39:39: %EASYPY-INFO: User             : cisco
    2024-02-04T11:39:39: %EASYPY-INFO: Host Server      : ubuntu-vm-20
    2024-02-04T11:39:39: %EASYPY-INFO: Host OS Version  : Ubuntu 20.04 focal (x86_64)
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: Job Information
    2024-02-04T11:39:39: %EASYPY-INFO:     Name         : pyats_easypy
    2024-02-04T11:39:39: %EASYPY-INFO:     Start time   : 2024-02-04 11:39:37.433250+00:00
    2024-02-04T11:39:39: %EASYPY-INFO:     Stop time    : 2024-02-04 11:39:38.919767+00:00
    2024-02-04T11:39:39: %EASYPY-INFO:     Elapsed time : 0:00:02
    2024-02-04T11:39:39: %EASYPY-INFO:     Archive      : /home/cisco/Documents/nxapilab/tests/results/pyats_easypy.2024Feb04_11:39:34.118834.zip
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: Total Tasks    : 1 
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: Overall Stats
    2024-02-04T11:39:39: %EASYPY-INFO:     Passed     : 6
    2024-02-04T11:39:39: %EASYPY-INFO:     Passx      : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Failed     : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Aborted    : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Blocked    : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Skipped    : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Errored    : 0
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO:     TOTAL      : 6
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: Success Rate   : 100.00 %
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: Section Stats
    2024-02-04T11:39:39: %EASYPY-INFO:     Passed     : 6
    2024-02-04T11:39:39: %EASYPY-INFO:     Passx      : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Failed     : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Aborted    : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Blocked    : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Skipped    : 0
    2024-02-04T11:39:39: %EASYPY-INFO:     Errored    : 0
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO:     TOTAL      : 6
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: Section Success Rate   : 100.00 %
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: +------------------------------------------------------------------------------+
    2024-02-04T11:39:39: %EASYPY-INFO: |                             Task Result Summary                              |
    2024-02-04T11:39:39: %EASYPY-INFO: +------------------------------------------------------------------------------+
    2024-02-04T11:39:39: %EASYPY-INFO: Task-1: pyats_aetest.common_setup                                         PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: Task-1: pyats_aetest.verify_connected                                     PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: Task-1: pyats_aetest.verify_ospf_process_id                               PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: Task-1: pyats_aetest.verify_ospf_neighbor_count                           PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: Task-1: pyats_aetest.verify_ospf_neighbor_state                           PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: Task-1: pyats_aetest.common_cleanup                                       PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: 
    2024-02-04T11:39:39: %EASYPY-INFO: +------------------------------------------------------------------------------+
    2024-02-04T11:39:39: %EASYPY-INFO: |                             Task Result Details                              |
    2024-02-04T11:39:39: %EASYPY-INFO: +------------------------------------------------------------------------------+
    2024-02-04T11:39:39: %EASYPY-INFO: Task-1: pyats_aetest
    2024-02-04T11:39:39: %EASYPY-INFO: |-- common_setup                                                          PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |   `-- connect                                                           PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |-- verify_connected                                                      PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |   `-- test                                                              PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 1: Test Connection Status of staging-spine1              PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 2: Test Connection Status of staging-leaf1               PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       `-- STEP 3: Test Connection Status of staging-leaf2               PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |-- verify_ospf_process_id                                                PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |   `-- test                                                              PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 1: Verify OSPF Process ID is UNDERLAY on staging-s...    PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 2: Verify OSPF Process ID is UNDERLAY on staging-l...    PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       `-- STEP 3: Verify OSPF Process ID is UNDERLAY on staging-l...    PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |-- verify_ospf_neighbor_count                                            PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |   `-- test                                                              PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 1: Verify OSPF Neighbors on staging-spine1               PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 2: Verify OSPF Neighbors on staging-leaf1                PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       `-- STEP 3: Verify OSPF Neighbors on staging-leaf2                PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |-- verify_ospf_neighbor_state                                            PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |   `-- test                                                              PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 1: Verify OSPF Neighbors on staging-spine1               PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       |-- STEP 2: Verify OSPF Neighbors on staging-leaf1                PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: |       `-- STEP 3: Verify OSPF Neighbors on staging-leaf2                PASSED
    2024-02-04T11:39:39: %EASYPY-INFO: `-- common_cleanup                                                        PASSED
    2024-02-04T11:39:39: %EASYPY-INFO:     `-- disconnect_from_devices                                           PASSED
    2024-02-04T11:39:40: %EASYPY-INFO: Done!

Step 10 - Close pyATS AEtest Python Files

Once executed, close pyats_aetest.py and pyats_easypy.py by clicking the small x (close button) to the right of the file tab:

Once completed, continue to the next section to examine pyATS testing using the RobotFramework.