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.

üyATS Configuration Management
üyATS Configuration Management

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 }}, {{
      intf.description }}
      , etc., get replaced by actual data values.

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}")
Back to: Network Automation with pyATS & Genie (in Progress) > Automating Network Testcases with AEtest

Leave a Reply

Your email address will not be published. Required fields are marked *


Post comment