pyATS Testing Tutorial
pyATS (Python Automated Test System) is a powerful, open-source Python framework originally developed by Cisco for network testing and validation. With the Genie library, pyATS can parse, learn, and compare network device states, making it a must-have tool for network engineers and automation professionals. In this tutorial, you'll learn how to install pyATS, create a testbed, run your first test, and use Genie for network state validation.
What is pyATS and Genie?
- pyATS stands for Python Automated Test System. It is a modular, Python-based test automation framework used for network validation, health checks, and regression testing. While it was developed by Cisco, it works with many network platforms.
- Genie is a library that extends pyATS with network knowledge—parsers, "learn" features, and diffing capabilities.
- Together, they allow you to automate network testing, parse CLI output, and compare network states before and after changes.
pyATS is used by Cisco internally for millions of tests per month and is now open source for everyone.
Installation
It's best to use a Python virtual environment for pyATS projects:
# Create and activate a virtual environment
mkdir pyats-demo
cd pyats-demo
python3 -m venv .
source bin/activate
# Install pyATS and Genie (full extras)
pip install "pyats[full]"
Check your installation:
Creating a Testbed File
A testbed file describes your network devices for pyATS. You can create one interactively:
You'll be prompted for device names, IPs, credentials, and connection details. The result is a YAML file like:
devices:
csr1:
os: iosxe
type: router
connections:
cli:
protocol: ssh
ip: 192.168.1.10
credentials:
default:
username: admin
password: mypassword
You can add more devices by editing the YAML file.
Running Your First pyATS/Genie Test
Let's use Genie to "learn" OSPF state from your devices:
- This command connects to all devices in your testbed and collects OSPF state, saving the results in the
ospf1
folder. - You can repeat this after making changes (e.g., disabling an interface) and save to a new folder:
Comparing Network States with pyATS Diff
To see what changed between two states:
- This will show you exactly what changed in OSPF between the two runs—great for troubleshooting and validation!
Use Cases for pyATS
- Network Health Checks: Automate regular checks for interface status, routing, CPU/memory, etc.
- Pre/Post Change Validation: Capture network state before and after upgrades or config changes, and compare.
- Automated Testing: Integrate with CI/CD pipelines for continuous network validation.
- Parsing CLI Output: Use Genie parsers to turn CLI output into structured data for further analysis.
Example: Health Check Automation
You can schedule pyATS/Genie scripts to run at intervals, collecting and comparing network state, and alerting on changes or anomalies.
Key Resources
- Roger Perkin: pyATS Genie Tutorial
- Cisco pyATS Getting Started Guide
- DevNet Academy pyATS Start Guide
- Dave Brown: pyATS Getting Started
- Cisco pyATS Documentation
- Genie Parsers List
Conclusion
pyATS and Genie are essential tools for network engineers looking to automate testing, validation, and troubleshooting. With simple YAML testbeds, powerful "learn" and diff features, and a Pythonic workflow, you can bring your network automation to the next level.
For more network automation tutorials, check out the NetDevOps blog and tools index.
Bonus: Generate a pyATS Testbed from Nautobot
You can automate the creation of your pyATS testbed file by extracting device data directly from Nautobot. The script below connects to Nautobot using environment variables for the URL, API token, and site name, and outputs a testbed YAML file for use with pyATS.
Note: For Nautobot 2.x and later, "sites" are now locations of type
site
. The script below supports both Nautobot 1.x (using thesite
field) and Nautobot 2.x (using thelocation
field with typesite
).
Usage:
export NAUTOBOT_URL=https://nautobot.example.com/api/
export NAUTOBOT_TOKEN=yourtoken
export NAUTOBOT_SITE=ams-dc1
python3 nautobot_to_pyats_testbed.py > testbed.yml
Script:
#!/usr/bin/env python3
"""
Script to extract devices from a Nautobot site/location and generate a pyATS testbed YAML file.
Supports both Nautobot 1.x (site field) and Nautobot 2.x (locations of type 'site').
Environment variables required:
- NAUTOBOT_URL: Nautobot API base URL (e.g., https://nautobot.example.com/api/)
- NAUTOBOT_TOKEN: Nautobot API token
- NAUTOBOT_SITE: Name or slug of the site/location to extract devices from
Usage:
export NAUTOBOT_URL=https://nautobot.example.com/api/
export NAUTOBOT_TOKEN=yourtoken
export NAUTOBOT_SITE=ams-dc1
python3 nautobot_to_pyats_testbed.py > testbed.yml
"""
import os
import sys
import requests
import yaml
NAUTOBOT_URL = os.environ.get("NAUTOBOT_URL")
NAUTOBOT_TOKEN = os.environ.get("NAUTOBOT_TOKEN")
NAUTOBOT_SITE = os.environ.get("NAUTOBOT_SITE")
if not (NAUTOBOT_URL and NAUTOBOT_TOKEN and NAUTOBOT_SITE):
print("Error: Please set NAUTOBOT_URL, NAUTOBOT_TOKEN, and NAUTOBOT_SITE environment variables.", file=sys.stderr)
sys.exit(1)
HEADERS = {
"Authorization": f"Token {NAUTOBOT_TOKEN}",
"Accept": "application/json",
}
def get_location_id(site_name):
# For Nautobot 2.x: get location with type 'site'
url = f"{NAUTOBOT_URL.rstrip('/')}/dcim/locations/?location_type=site&name={site_name}"
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status()
results = resp.json().get("results", [])
if results:
return results[0]["id"]
return None
def get_devices(site):
# Try Nautobot 2.x locations first
location_id = get_location_id(site)
if location_id:
url = f"{NAUTOBOT_URL.rstrip('/')}/dcim/devices/?location_id={location_id}&limit=1000"
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status()
return resp.json().get("results", [])
# Fallback to Nautobot 1.x site field
url = f"{NAUTOBOT_URL.rstrip('/')}/dcim/devices/?site={site}&limit=1000"
resp = requests.get(url, headers=HEADERS)
resp.raise_for_status()
return resp.json().get("results", [])
def get_primary_ip(device):
ip = device.get("primary_ip4") or device.get("primary_ip")
if ip and ip.get("address"):
return ip["address"].split("/")[0]
return None
def build_testbed(devices):
testbed = {"devices": {}}
for dev in devices:
name = dev["name"]
os_type = dev.get("platform", {}).get("slug", "iosxe")
mgmt_ip = get_primary_ip(dev)
if not mgmt_ip:
continue # skip devices without management IP
testbed["devices"][name] = {
"os": os_type,
"type": dev.get("device_type", {}).get("model", "router"),
"connections": {
"cli": {
"protocol": "ssh",
"ip": mgmt_ip
}
},
"credentials": {
"default": {
"username": "<username>",
"password": "<password>"
}
}
}
return testbed
if __name__ == "__main__":
devices = get_devices(NAUTOBOT_SITE)
if not devices:
print(f"No devices found for site/location '{NAUTOBOT_SITE}'.", file=sys.stderr)
sys.exit(1)
testbed = build_testbed(devices)
yaml.dump(testbed, sys.stdout, default_flow_style=False, sort_keys=False)