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.
In VSCode, using the code
keyword in the Terminal window, open a new file for creating a Python pyATS AEtest script:
touch /home/pod14/workspace/nxapilab/tests/pyats_aetest.py
code-server -r /home/pod14/workspace/nxapilab/tests/pyats_aetest.py
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__)
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:
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")
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()
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:
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()
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.
Be sure to save your file! Not saving will result in your code not executing.
In VSCode, using the code
keyword in the Terminal window, open a new file for creating a Python pyATS EasyPy script:
touch /home/pod14/workspace/nxapilab/tests/pyats_easypy.py
code-server -r /home/pod14/workspace/nxapilab/tests/pyats_easypy.py
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)
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:
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.
2025-06-04T17:07:00: %EASYPY-INFO: -------------------------------------------------------------------------------- 2025-06-04T17:07:00: %EASYPY-INFO: Job finished. Wrapping up... 2025-06-04T17:07:01: %EASYPY-INFO: Creating archive file: /home/pod14/workspace/nxapilab/tests/results/pyats_easypy.2025Jun04_17:06:19.179774.zip 2025-06-04T17:07:01: %EASYPY-INFO: +------------------------------------------------------------------------------+ 2025-06-04T17:07:01: %EASYPY-INFO: | Easypy Report | 2025-06-04T17:07:01: %EASYPY-INFO: +------------------------------------------------------------------------------+ 2025-06-04T17:07:01: %EASYPY-INFO: pyATS Instance : /home/pod14/.pyenv/versions/3.11.9/envs/nxapilab 2025-06-04T17:07:01: %EASYPY-INFO: Python Version : cpython-3.11.9 (64bit) 2025-06-04T17:07:01: %EASYPY-INFO: CLI Arguments : /home/pod14/.pyenv/versions/nxapilab/bin/pyats run job pyats_easypy.py --testbed-file staging-testbed.yaml --archive-dir=/home/pod14/workspace/nxapilab/tests/results --no-archive-subdir --no-mail 2025-06-04T17:07:01: %EASYPY-INFO: User : pod01 2025-06-04T17:07:01: %EASYPY-INFO: Host Server : ubuntu-code-server-template 2025-06-04T17:07:01: %EASYPY-INFO: Host OS Version : Ubuntu 22.04 jammy (x86_64) 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: Job Information 2025-06-04T17:07:01: %EASYPY-INFO: Name : pyats_easypy 2025-06-04T17:07:01: %EASYPY-INFO: Result : PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Start time : 2025-06-04 17:06:24.296488+00:00 2025-06-04T17:07:01: %EASYPY-INFO: Stop time : 2025-06-04 17:07:00.448946+00:00 2025-06-04T17:07:01: %EASYPY-INFO: Elapsed time : 0:00:37 2025-06-04T17:07:01: %EASYPY-INFO: Archive : /home/pod14/workspace/nxapilab/tests/results/pyats_easypy.2025Jun04_17:06:19.179774.zip 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: Total Tasks : 1 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: Overall Stats 2025-06-04T17:07:01: %EASYPY-INFO: Passed : 6 2025-06-04T17:07:01: %EASYPY-INFO: Passx : 0 2025-06-04T17:07:01: %EASYPY-INFO: Failed : 0 2025-06-04T17:07:01: %EASYPY-INFO: Aborted : 0 2025-06-04T17:07:01: %EASYPY-INFO: Blocked : 0 2025-06-04T17:07:01: %EASYPY-INFO: Skipped : 0 2025-06-04T17:07:01: %EASYPY-INFO: Errored : 0 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: TOTAL : 6 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: Success Rate : 100.00 % 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: Section Stats 2025-06-04T17:07:01: %EASYPY-INFO: Passed : 6 2025-06-04T17:07:01: %EASYPY-INFO: Passx : 0 2025-06-04T17:07:01: %EASYPY-INFO: Failed : 0 2025-06-04T17:07:01: %EASYPY-INFO: Aborted : 0 2025-06-04T17:07:01: %EASYPY-INFO: Blocked : 0 2025-06-04T17:07:01: %EASYPY-INFO: Skipped : 0 2025-06-04T17:07:01: %EASYPY-INFO: Errored : 0 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: TOTAL : 6 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: Section Success Rate : 100.00 % 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: +------------------------------------------------------------------------------+ 2025-06-04T17:07:01: %EASYPY-INFO: | Task Result Summary | 2025-06-04T17:07:01: %EASYPY-INFO: +------------------------------------------------------------------------------+ 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest.common_setup PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest.verify_connected PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest.verify_ospf_process_id PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest.verify_ospf_neighbor_count PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest.verify_ospf_neighbor_state PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest.common_cleanup PASSED 2025-06-04T17:07:01: %EASYPY-INFO: 2025-06-04T17:07:01: %EASYPY-INFO: +------------------------------------------------------------------------------+ 2025-06-04T17:07:01: %EASYPY-INFO: | Task Result Details | 2025-06-04T17:07:01: %EASYPY-INFO: +------------------------------------------------------------------------------+ 2025-06-04T17:07:01: %EASYPY-INFO: Task-1: pyats_aetest PASSED 2025-06-04T17:07:01: %EASYPY-INFO: |-- common_setup PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- connect PASSED 2025-06-04T17:07:01: %EASYPY-INFO: |-- verify_connected PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- test PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 1: Test Connection Status of staging-spine1 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 2: Test Connection Status of staging-leaf1 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- STEP 3: Test Connection Status of staging-leaf2 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: |-- verify_ospf_process_id PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- test PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 1: Verify OSPF Process ID is UNDERLAY on staging-s... PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 2: Verify OSPF Process ID is UNDERLAY on staging-l... PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- STEP 3: Verify OSPF Process ID is UNDERLAY on staging-l... PASSED 2025-06-04T17:07:01: %EASYPY-INFO: |-- verify_ospf_neighbor_count PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- test PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 1: Verify OSPF Neighbors on staging-spine1 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 2: Verify OSPF Neighbors on staging-leaf1 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- STEP 3: Verify OSPF Neighbors on staging-leaf2 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: |-- verify_ospf_neighbor_state PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- test PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 1: Verify OSPF Neighbors on staging-spine1 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | |-- STEP 2: Verify OSPF Neighbors on staging-leaf1 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: | `-- STEP 3: Verify OSPF Neighbors on staging-leaf2 PASSED 2025-06-04T17:07:01: %EASYPY-INFO: `-- common_cleanup PASSED 2025-06-04T17:07:01: %EASYPY-INFO: `-- disconnect_from_devices PASSED 2025-06-04T17:07:01: %EASYPY-INFO: Done!
On the keyword press Ctrl + K + W
. This should close all open tabs to clear your workspace for the next section.
Once completed, continue to the next section to examine pyATS testing using the RobotFramework.