Change configuration with pyATS: pyATS is primarily a testing framework, but it can also be used to push configuration to devices. In this section, we show how to combine pyATS with Jinja2 templates and YAML data to apply device-specific configurations dynamically.
pyATS Configuration Management
pyATS was originally designed for network testing rather than day-to-day configuration management. While it does offer a .configure()
method for pushing CLI-based configurations, it is not optimized for large-scale, idempotent, or state-aware configuration tasks in the way that tools like Nornir or Ansible are.
Some key limitations of using pyATS for configuration management include:
Lack of Protocol Abstraction: pyATS does not natively support modern APIs like NETCONF or RESTCONF, which are essential for structured, model-driven configuration.
Not Idempotent or Declarative: Unlike Nornir with NETCONF/RESTCONF or Ansible, pyATS does not support declarative configuration. These tools allow you to define the desired state of the device, and the system ensures the device’s actual state matches it—without repeatedly applying the same changes.
Minimal Support for Configuration Rollback or Validation: pyATS lacks built-in features such as dry-run, configuration diffs, or automatic rollback, which are critical for safe, predictable change management in production environments.
To illustrate this, I will demonstrate a few configuration changes on devices defined in the testbed—first using raw pyATS, then using a combination of pyATS and AEtest, and finally by leveraging Jinja2 templates with YAML-based configuration data.
Change Configuration using pyATS
This Python script uses the pyATS to connect to a network device defined in a testbed YAML file, specifically device R1
, and applies a set of interface configuration commands to GigabitEthernet3
using device.configure()
method
. It establishes a connection, sends the configuration, handles any command execution errors, and then disconnects from the device, logging the progress throughout the process.
from genie.testbed import load # Load your testbed YAML file testbed = load('testbed.yaml') # Get the device from the testbed device = testbed.devices['R1'] # Connect to the device print(f"Connecting to device {device.name}...") device.connect(log_stdout=False) print(f"Successfully connected to {device.name}.") # Configuration commands to send config_commands = """ interface GigabitEthernet3 description Configured by pyATS ip address 192.168.168.1 255.255.255.0 shutdown """ print(f"Starting configuration on device {device.name}...") try: device.configure(config_commands) print(f"Configuration applied successfully on {device.name}.") except SubCommandFailure as e: print(f"Configuration failed on {device.name}: {e}") # Disconnect from the device device.disconnect() print(f"Disconnected from {device.name}.")
(majid) majid@majid-ubuntu:~/devnet/pyats$ python 7.3.change_configuration_via_pyats.py Connecting to device R1... Successfully connected to R1. Starting configuration on device R1... Configuration applied successfully on R1. Disconnected from R1.
Change Configuration using pyATS and AEtest
As you know, AEtest in pyATS is primarily designed for test automation. However, there are cases where making configuration changes is necessary to validate a feature—for example, shutting down an interface to verify redundancy behavior. In such scenarios, modifying the configuration within an AEtest test case can be practical and relevant in real-world environments. Below is a simple example demonstrating how to apply configuration changes inside an AEtest test case.
This AEtest script demonstrates how to make configuration changes during a test case by creating a loopback interface on network devices defined in a testbed file. It connects to all devices in the testbed, then iterates over each one using aetest.loop.mark
, and within the test case, it applies a configuration that creates Loopback100
with a description, IP address, and brings the interface up. After the test execution, it gracefully disconnects from all devices during the cleanup phase.
rom pyats import aetest from pyats.topology import loader testbed = loader.load('testbed.yaml') class common_setup(aetest.CommonSetup): """Common Setup Section""" @aetest.subsection def connect_to_devices(self): testbed.connect(log_stdout=False) @aetest.subsection def loop_mark(self): aetest.loop.mark(MyTestcase, device_name=testbed.devices.keys()) class MyTestcase(aetest.Testcase): @aetest.test def setup(self, device_name): self.device = testbed.devices[device_name] @aetest.test def make_change(self): self.device.configure(''' interface loopback100 description "This is a new loopback interface" ip address 10.10.100.100 255.255.255.0 no shut ''') class common_cleanup(aetest.CommonCleanup): """Common Cleanup Section""" @aetest.subsection def disconnect_from_devices(self): testbed.disconnect() if __name__ == '__main__': # Run the test aetest.main()
... 2025-05-16T09:30:41: %AETEST-INFO: SECTIONS/TESTCASES RESULT 2025-05-16T09:30:41: %AETEST-INFO: -------------------------------------------------------------------------------- 2025-05-16T09:30:41: %AETEST-INFO: . 2025-05-16T09:30:41: %AETEST-INFO: |-- common_setup PASSED 2025-05-16T09:30:41: %AETEST-INFO: | |-- connect_to_devices PASSED 2025-05-16T09:30:41: %AETEST-INFO: | `-- loop_mark PASSED 2025-05-16T09:30:41: %AETEST-INFO: |-- MyTestcase[device_name=R1] PASSED 2025-05-16T09:30:41: %AETEST-INFO: | |-- setup PASSED 2025-05-16T09:30:41: %AETEST-INFO: | `-- make_change PASSED 2025-05-16T09:30:41: %AETEST-INFO: |-- MyTestcase[device_name=R2] PASSED 2025-05-16T09:30:41: %AETEST-INFO: | |-- setup PASSED 2025-05-16T09:30:41: %AETEST-INFO: | `-- make_change PASSED 2025-05-16T09:30:41: %AETEST-INFO: `-- common_cleanup PASSED 2025-05-16T09:30:41: %AETEST-INFO: `-- disconnect_from_devices PASSED ...
Jinja2 Template in pyATS configuration Management
Jinja2 templates were already covered in the course “CLI-Based Network Automation Using Python Nornir”, specifically in the module “Python Jinja2 Template”. As a quick review:
Jinja2 template is used to dynamically generate device configurations by embedding variables and control structures (like loops and conditionals) into a predefined text structure. Instead of hardcoding values like interface names or IP addresses, the template uses placeholders {{
interface }}
that are filled in at runtime with actual data. This makes configurations reusable, scalable, and easy to maintain—especially when applying similar changes across multiple devices with different parameters.
Here’s a brief explanation of the structure of configuration changes using the Jinja2 template and configuration data in your example:
Jinja2 Template
Jinja2 Template (interface_config.j2
)
Defines a generic configuration skeleton for interfaces.
Uses a loop
{% for intf in interfaces %}
to process each interface in the input data.Template variables like
{{ intf.name }}
,{{
, etc., get replaced by actual data values.
intf.description }}
majid@majid-ubuntu:~/devnet/pyats$ pwd /home/majid/devnet/pyats majid@majid-ubuntu:~/devnet/pyats$ ls templates/ interface_config.j2
(majid) majid@majid-ubuntu:~/devnet/pyats$ cat templates/interface_config.j2 {% for intf in interfaces %} interface {{ intf.name }} description {{ intf.description }} ip address {{ intf.ip }} {{ intf.mask }} no shutdown {% endfor %}
Configuration Data
Configuration Data (R1.yaml
, R2.yaml
)
YAML files hold device-specific interface details: name, description, IP, mask.
This data feeds the template for each device, e.g., for
R1.yaml
:
majid@majid-ubuntu:~/devnet/pyats$ pwd /home/majid/devnet/pyats majid@majid-ubuntu:~/devnet/pyats$ ls data/ R1.yaml R2.yaml
(majid) majid@majid-ubuntu:~/devnet/pyats$ cat data/R1.yaml interfaces: - name: GigabitEthernet3 description: Link to LAN ip: 192.168.172.1 mask: 255.255.255.0 - name: Loopback101 description: Loopback ip: 192.168.173.1 mask: 255.255.255.0
(majid) majid@majid-ubuntu:~/devnet/pyats$ cat data/R2.yaml interfaces: - name: GigabitEthernet3 description: Link to LAN ip: 192.168.170.1 mask: 255.255.255.0 - name: Loopback101 description: Loopback ip: 192.168.171.1 mask: 255.255.255.0
Python Script
Python Script
Loads the testbed, which has device definitions.
For each device:
Loads the YAML data for that device.
Renders the Jinja2 template with the device data, producing the actual CLI configuration commands.
Connects to the device using the Genie
device.connect()
.Applies the rendered configuration via
device.configure()
.Disconnects from adevices.
import os import yaml from jinja2 import Environment, FileSystemLoader from genie.testbed import load # Load testbed testbed = load('testbed.yaml') # Set paths template_path = 'templates' data_path = 'data' template_file = 'interface_config.j2' # Set up Jinja2 environment env = Environment(loader=FileSystemLoader(template_path)) # Load Jinja2 template template = env.get_template(template_file) # Iterate over devices in testbed for device_name, device in testbed.devices.items(): print(f"\n[INFO] Processing {device_name}") # Load corresponding YAML data file data_file = os.path.join(data_path, f"{device_name}.yaml") with open(data_file) as f: data = yaml.safe_load(f) # Render configuration from Jinja2 template config = template.render(data) print(f"[INFO] Rendered config:\n{config}") # Connect to the device device.connect(log_stdout=False) print(f"[INFO] Connected to {device_name}") # Send the rendered config device.configure(config) print(f"[INFO] Configuration sent to {device_name}") # Disconnect device.disconnect() print(f"[INFO] Disconnected from {device_name}")