²©ñRŠÊ˜·³Ç

pyATS Triggers and Verifications

Date: Aug 5, 2024 By and . Sample Chapter is provided courtesy of .

The pyATS library (Genie) provides the Genie Harness to execute network tests with dynamic and robust capabilities. The Genie Harness can be daunting for new users of the pyATS library (Genie), as there are many configuration options and parameters. By the end of the chapter, you'll understand the powerful capabilities of Genie Harness and how to write and execute triggers and verifications.

Automated network tests face challenges in achieving futureproofing due to the dynamic nature of networks and their evolving requirements. The pyATS library (Genie) provides the Genie Harness to execute network tests with dynamic and robust capabilities. The Genie Harness is part of the pyATS library (Genie), which is built on the foundation of pyATS. It introduces the ability for engineers to create dynamic, event-driven tests with the use of processors, triggers, and verifications. In this chapter, the following topics are going to be covered:

  • Genie objects

  • Genie Harness

  • Triggers

  • Verifications

The Genie Harness can be daunting for new users of the pyATS library (Genie), as there are many configuration options and parameters. The focus of this chapter will be to provide an overview of Genie Harness and its features and wrap up with a focus on triggers and verifications. By the end of the chapter, you’ll understand the powerful capabilities of Genie Harness and how to write and execute triggers and verifications.

Genie Objects

One of the hardest parts of network automation is figuring out how to normalize and structure the data returned from multiple network device types. For example, running commands to gather operational data (CPU, memory, interface statistics, routing state, and so on) from a Cisco IOS XE switch, IOS XR router, and an ASA firewall may have different, but similar, output. How do we account for the miniscule differences across the different outputs? The pyATS library (Genie) abstracts the details of the parsed data returned from devices by creating network OS-agnostic data models. Two types of Genie objects represent these models: Ops and Conf. In the following sections, you’ll dive into the details of each object.

Genie Ops

As you may be able to guess from the name, the Genie Ops object learns everything about the operational state of a device. The operational state data is represented as a structured data model, or feature. A feature is a network OS-agnostic data model that is created when pyATS “learns” about a device. Multiple commands are executed on a device and the output is parsed and normalized to create the structure of the feature. You can access the learned feature data by accessing <feature>.info. To learn more information about the available models, check out the Genie models documentation (). Example 9-1 shows how to instantiate an Ops object for BGP and learn the BGP feature on a Cat8000v device. Figure 9-1 shows the associated output.

Example 9-1 Genie Ops Object

from genie import testbed
from genie.libs.ops.bgp.iosxe.bgp import Bgp
import pprint

# Load Genie testbed
testbed = testbed.load(testbed="testbed.yaml")

# Find the device using hostname or alias
uut = testbed.devices["cat8k-rt2"]
uut.connect()

# Instantiate the Ops object
bgp_obj = Bgp(device=uut)

# This will send many show commands to learn the operational state
# of BGP on this device
bgp_obj.learn()

pprint.pprint(bgp_obj.info)
FIGURE 9.1

Figure 9.1 Genie Ops Object Output

Genie Conf

The Genie Conf object allows you to take advantage of Python’s object-oriented programming (OOP) approach by building logical representations of devices and generating configurations based on the logical device and its attributes. Just like Genie Ops, the structure of Genie Conf objects is based on the feature models. The best way to describe the Conf object is by example. Example 9-2 shows how to build a simple network interface object for an IOS XE device. You’ll notice that the code is very comprehensive, specifically for a network engineer, and will even generate the configuration!

Example 9-2 Genie Conf Object

from genie import testbed
from genie.libs.conf.interface.iosxe.interface import Interface
import pprint


# Load Genie testbed
testbed = testbed.load(testbed="testbed.yaml")

# Find the device using hostname or alias
uut = testbed.devices["cat8k-rt2"]

# Instantiate the Conf object
interface_obj = Interface(device=uut, name="Loopback100")

# Add attributes to the Interface object
interface_obj.description = "Managed by pyATS"
interface_obj.ipv4 = "1.1.1.1"
interface_obj.ipv4.netmask = "255.255.255.255"
interface_obj.shutdown = False


# Build the configuration for the interface
print(interface_obj.build_config(apply=False))



! Code Execution Output
interface Loopback100
 description Managed by pyATS
 ip address 1.1.1.1 255.255.255.255
 no shutdown
 exit

You may notice the last line of code actually builds the necessary configuration for you! To go one step further, you can add the interface object to a testbed device and push the configuration to a device. The Conf object allows you to drive the configuration of network devices with Python, which takes you one step further into the world of automation.

Genie Harness

The Genie Harness is structured much like a pyATS testscript. There are three main sections: Common Setup, Triggers and Verifications, and Common Cleanup. The Common Setup section will connect to your devices, take a snapshot of current system state, and optionally configure the devices, if necessary. The Triggers and Verifications section, which you will see later in the chapter, will execute the triggers and verifications to perform tests on the devices. This is where the action happens! The Common Cleanup section confirms that the state of the devices is the same as their state in the Common Setup section by taking another snapshot of the current system state and comparing it with the one captured in the Common Setup section.

gRun

The first step to running jobs with Genie Harness is creating a job file. Job files are set up much like a pyATS job file. Within the job file there’s a main function that is used as an entry point to run the job(s). However, instead of using run() within the main function to run the job, you must use gRun(), or the “Genie Run” function. This function is used to execute pyATS testscripts with additional arguments that provide robust and dynamic testing. Datafiles are passed in as arguments, which allows you to run specific triggers and verifications. Example 9-3 shows a Genie job file from the documentation that runs one trigger and one verification.

Example 9-3 Genie Harness – Job file

from genie.harness.main import gRun

def main():
         # Using built-in triggers and verifications
         gRun(trigger_uids=["TriggerSleep"],
         verification_uids=["Verify_IpInterfaceBrief"])

The trigger and verification names in Example 9-3 are self-explanatory: sleep for a specified amount of time and parse and verify the output of show ip interface brief command. Each trigger and verification is part of the pyATS library. If these were custom-built triggers or if a testbed device did not have the name or alias of “uut” in your testbed file, you would need to create a mapping datafile. A mapping datafile is used to create a relationship, or mapping, between the devices in a testbed file and Genie. It’s required if you want to control multiple connections and connection types (CLI, XML, YANG) to testbed devices. By default, Genie Harness will only connect to the device with the name or alias of “uut,” which represents “unit under testing.” The uut name/alias requirement allows Genie Harness to properly load the correct default trigger and verification datafiles. Otherwise, you must include a list of testbed devices to the triggers/verifications in the respective datafile. If this doesn’t make sense, don’t worry; we will touch on datafiles and provide examples further in the chapter that should help provide clarity.

To wrap up the Genie Harness example, the Genie job is run with the same Easypy runtime environment used to run pyATS jobs. To run the Genie job from the command line, enter the following:

(.venv)dan@linux-pc# pyats run job {job file}.py --testbed-file
{/path/to/testbed}

Remember to have your Python virtual environment activated! In the next section, we will jump into datafiles and how they are defined.

Datafiles

The purpose of a datafile is to provide additional information about the Genie features (triggers, verifications, and so on) you would like Genie Harness to run during a job. For example, the trigger datafile may specify which testbed devices a trigger should run on in the job. There are many datafiles available in Genie Harness. However, many of them are optional and are only needed if you’re planning to modify the default datafiles provided. The default datafiles can be found at the following path:

$VIRTUAL_ENV/lib/python<version>/site-packages/genie/libs/sdk/genie_
yamls

Within the genie_yamls directory, you’ll find default datafiles that apply to all operating systems (OSs) and others that are OS-specific. These default datafiles are only implicitly activated when a testbed device has either a name or alias of uut. If there isn’t a testbed device with that name or alias, the default datafile will not be implicitly passed to that job. I would highly recommend checking out (not editing) the default datafiles. If you’d like to edit one, you may create a new datafile in your local directory and extend the default one—but don’t jump too far ahead yet! This topic will be covered later in the chapter. Here’s a list of the different datafiles that can be passed to gRun:

  • Testing datafile

  • Mapping datafile

  • Verification datafile

  • Trigger datafile

  • Subsection datafile

  • Configuration datafile

  • PTS datafile

Each datafile serves a purpose to a specific Genie Harness feature, but one only needs to be specified if you are deviating from the provided default datafile. For example, the Profile the System (PTS) default datafile only specifies to run on the testbed device with the alias uut. If you would like it to run on more devices, you’ll need to create a pts_datafile.yaml file that maps devices to the device features you want profiled by PTS and include the pts_datafile argument to gRun.

Device Configuration

Applying device configuration is always a hot topic when it comes to network automation. In the context of pyATS and the pyATS library (Genie), the focus is on applying (and reverting) the configuration during testing. In many cases, you’ll want to test a feature by configuring it on a device, testing its functionality, and removing the configuration before the end of testing. The pyATS library (Genie) provides many ways to apply configuration to devices. Here are some of the options you have to configure a network device during testing:

  • Manual configuration before testing begins (not recommended).

  • Automatically apply the configuration to the device in the Common Setup and Common Cleanup sections with TFTP/SCP/FTP. A config.yaml file can be provided to the config_datafile argument of gRun, which specifies the configuration to apply.

  • Automatically apply the configuration to the device in the Common Setup and Common Cleanup sections using Jinja2 template rendering. The Jinja2 template filename will be passed to gRun using the jinja2_config argument and the device variables will be passed as key-value pairs using the jinja2_arguments argument.

You will dive into Jinja2 templates further and how to use them to generate configurations in Chapter 10, “Automated Configuration Management.” For now, just understand that you can standardize the configuration being pushed to the network devices under testing using configuration templates and a template rendering engine (Jinja2) to render the templates with device variables, resulting in complete configuration files.

All configurations should be built using the show running style, which means you create your configuration files how they would appear when you view a device’s configuration using the show running-config command. This differs from how you would configure a device interactively via SSH using the configure terminal approach.

After the devices under testing have been configured, the pyATS library learns the configuration of the devices via the check_config subsection. The check_config subsection runs twice: once during Common Setup and another time during Common Cleanup. It collects the running-config of each device in the topology and compares the two configuration “snapshots” to ensure the configuration remains the same before and after testing.

PTS (Profile the System)

Earlier in this chapter, you saw examples of device features that can be learned (for example, BGP) during testing. This is made possible by the network OS-agnostic models built into pyATS. These models create the foundation for building reliable data structures and provide the ability to parse data from the network.

PTS provides the ability to “profile” the network during testing. PTS creates profile snapshots of each feature in the Common Setup and Common Cleanup sections. PTS can learn about all the device features, or a specific list of device features can be provided as a list to the pts_features argument of gRun. Example 9-4 shows how gRun is called in a job file with a list of features passed to the pts_features argument.

Example 9-4 PTS Feature

from genie.harness.main import gRun

def main():
         # Profiling built-in features (models) w/o the PTS datafile
         gRun(pts_features=["bgp", "interface"])

Along with having the ability to profile a subset of features/device commands, PTS, by default, will only run on the device with the device alias uut. To have more devices profiled by PTS, you’ll need to supply a pts_datafile.yaml file. The datafile can provide a list of devices to profile and describe specific attributes to ignore in the output when comparing snapshots (such as timers, uptime, and dates). Example 9-5 shows a PTS datafile, and Example 9-6 shows the updated gRun call, with the pts_datafile argument included.

Example 9-5 PTS Datafile – ex0906_pts_datafile.yaml

extends: "%CALLABLE{genie.libs.sdk.genie_yamls.datafile(pts)}"

bgp:
  devices: ["cat8k-rt1", "cat8k-rt2"]
  exclude:
    - up_time

interface:
  devices: ["cat8k-rt1", "cat8k-rt2"]

Example 9-6 PTS Datafile Argument

from genie.harness.main import gRun

def main():
         # Profiling built-in features (models) w/ the PTS datafile
         gRun(pts_features=["bgp", "interface"],
         pts_datafile="ex0906_pts_datafile.yaml")

PTS Golden Config

PTS profiles the operational state of the network devices under testing, but how do we know the state is what we expect? PTS provides a “golden config” snapshot feature that compares the profiles learned by PTS to what is considered the golden snapshot. Each job run generates a file named pts that is saved to the pyATS archive directory of the job. Any PTS file can be moved to a fixed location and used as the golden snapshot. Like the pts_datafile argument, the pts_golden_config argument can be passed to gRun, which points to the golden PTS snapshot used to compare against the current test run snapshots.

There’s a lot to digest with Genie Harness as well as a lot of different options, and an understanding of these features and when to use them is critical. In the following sections, we will turn our attention to triggers and verifications. Let’s take a look at verifications first, as triggers rely on them to perform properly.

Verifications

A verification runs one or multiple commands to retrieve the current state of the device. The main purpose of a verification is to capture and compare the operational state before and after a trigger, or set of triggers, performs an action on a device. The state of the device can be retrieved via the multiple connection types offered by the pyATS library (CLI, YANG, and so on). Verifications typically run in conjunction with triggers to verify the trigger action did what it was supposed to do and to check for unexpected results, such as changes to a feature you didn’t initiate.

Verification Types

Verifications can be broken down into two types: global and local. The difference between the two is related to scoping and when each verification type runs within a script.

  • Global verifications: Global verifications are used to capture a snapshot of a device before a trigger is executed. Global verifications run immediately after the Common Setup section and before a trigger in a script. If more than one trigger is executed, subsequent snapshots are captured before and after each trigger using the same set of verifications.

  • Local verifications: Local verifications are independent of global verifications and run as subsections within a trigger. More specifically, a set of snapshots is taken before a trigger action and a subsequent set is taken after to compare to the first one. Local verifications confirm the trigger action did what it was supposed to do (configure/unconfigure, shut/no shut, and so on).

Figure 9-2 shows where and when the different verification types run in a Genie job.

FIGURE 9.2

Figure 9.2 Verification Execution

Verification Datafile

A verification datafile is used to customize the execution of built-in or custom verifications. Like other datafiles, verification_datafile.yaml must be provided to gRun using the verification_datafile argument. Example 9-7 shows a verification datafile that extends the default verification datafile (via the extends: key) and overrides the default setting of connecting to only the “uut” device (via the devices: key) for the Verify_Interfaces verification. If you wanted to change the list of devices to connect to for another verification, you would need to add that verification in the datafile.

Example 9-7 Verification Datafile – ex0908_verification_datafile.yaml

# Extend default verification datafile to inherit the required keys
# (class, source, etc.) per the verification datafile schema
extends: "%CALLABLE{genie.libs.sdk.genie_yamls.datafile(verification)}"

Verify_Interfaces:
  devices: ["cat8k-rt1", "cat8k-rt2"]

In order to run the verifications listed in the datafile, or any other built-in verifications, you’ll need to include them in the verification_uids argument to gRun. Example 9-8 shows how to run the Verify_Interfaces verification with the verification datafile from Example 9-7.

Example 9-8 gRun – Verifications

from genie.harness.main import gRun


def main():
         gRun(
         verification_uids=["Verify_Interfaces"],
         verification_datafile="ex0908_verification_datafile.yaml"
         )

Writing a Verification

The process in which a feature is verified during testing may be different for different use cases. If a built-in verification does not suffice, the pyATS library (Genie) allows you to create your own verification.

There are several ways to create your own verification. You can use a Genie Ops feature (model), a parser, or callable. For the Genie Ops feature and parser options, you can use an existing model or parser, or you can create your own. The last option, using a callable, is discouraged, as it isn’t OS-agnostic and does not provide extensibility to use different management interfaces (CLI, YANG, and so on). Example 9-9 shows a custom verification built with the show bgp all parser. Take note of the list of excluded values from the parsed data (found under the exclude: key). The reason is because many of these values are dynamic (such as timers, counters, and so on) and are almost guaranteed to be different between snapshots. Remember, if a parsed value is different between snapshots, the verification will fail.

Example 9-9 Custom Verification – ex0910_verification_datafile.yaml

# Local verification datafile that already extends the default datafile
extends: verification_datafile.yaml

Verify_Bgp:
    cmd:
        class: show_bgp.ShowBgpAll
        pkg: genie.libs.parser
    context: cli
    source:
        class: genie.harness.base.Template
    devices: ["cat8k-rt1", "cat8k-rt2"]
    iteration:
        attempt: 5
        interval: 10
    exclude:
    - if_handle
    - keepalives
    - last_reset
    - reset_reason
    - foreign_port
    - local_port
    - msg_rcvd
    - msg_sent
    - up_down
    - bgp_table_version
    - routing_table_version
    - tbl_ver
    - table_version
    - memory_usage
    - updates
    - mss
    - total
    - total_bytes
    - up_time

    - bgp_negotiated_keepalive_timers
    - hold_time
    - keepalive_interval
    - sent
    - received
    - status_codes
    - holdtime
    - router_id
    - connections_dropped
    - connections_established
    - advertised
    - prefixes
    - routes
    - state_pfxrcd

To run the custom verification, you follow the same process as running any other verification. Pass the verification datafile with the custom verification to gRun via the verification_datafile argument and add the custom verification name to the list of verifications in the verification_uids argument. Example 9-10 shows the updated gRun call with the custom verification name and datafile. Remember, the custom verification datafile (Example 9-9) extends the original verification datafile (Example 9-7), which essentially inherits all the built-in verifications from the pyATS library (Genie).

Example 9-10 gRun – Custom Verification

from genie.harness.main import gRun


def main():
         gRun(
         verification_uids=["Verify_Interfaces", "Verify_Bgp"],
         verification_datafile="ex0910_verification_datafile.yaml"
         )

Triggers

Triggers perform a specific action, or a sequence of actions, on a device to alter its state and/or configuration. As examples, actions may include adding/removing parts of a configuration, flapping protocols/interfaces, or performing high availability (HA) events such as rebooting a device. The important part to understand is that triggers are what alter the device during testing.

The pyATS library (Genie) has many prebuilt triggers available for Cisco IOS/IOS XE, NX-OS, and IOS XR. All prebuilt triggers are documented, describing what happens when the trigger is initiated and what keys/values to include in the trigger datafile specifically for that trigger. For example, the TriggerShutNoShutBgpNeighbors trigger performs the following workflow:

  1. Learn BGP Ops object and verify it has “established” neighbors. If there aren’t any “established” neighbors, skip the trigger.

  2. Shut the BGP neighbor that was learned from step 1 with the BGP Conf object.

  3. Verify the state of the learned neighbor(s) in step 2 is “down.”

  4. Unshut the BGP neighbor(s).

  5. Learn BGP Ops again and verify it is the same as the BGP Ops snapshot in step 1.

As you might recall from earlier in this chapter, the Genie Ops object represents a device/feature’s operational state via a Python object, and the Genie Conf object represents a feature, as a Python object, that can be configured on a device. The focus of the Conf object is what feature you want to apply on the device, not how to apply it per device (OS) platform. This allows a network engineer to focus on the network features being tested and not on the low-level details of how the configuration is applied.

Now that there’s a general understanding of what triggers do, let’s check out how they can be configured using a trigger datafile.

Trigger Datafile

As with other features of the pyATS library (Genie), to run triggers, there needs to be a datafile—more specifically, a trigger datafile (trigger_datafile.yaml). The pyATS library provides a default trigger datafile found in the same location as all the other default datafiles, discussed earlier in the chapter. However, if you want to customize any specific trigger settings, such as what devices or group of devices to run on during testing (any device besides uut), or to run a custom trigger, you’ll need to create your own trigger datafile. A complete example can be found at the end of the chapter that includes both triggers and verifications, but let’s focus now on just triggers in a brief example. Example 9-11 shows a custom trigger file that flaps the OSPF process on the targeted devices (iosv-0 and iosv01). Example 9-12 shows how to include the appropriate trigger and trigger datafile in the list of arguments to gRun.

Example 9-11 Trigger Datafile – ex0912_trigger_datafile.yaml

extends: "%CALLABLE{genie.libs.sdk.genie_yamls.datafile(trigger)}"

# Custom trigger - created in Example 9-14
TriggerShutNoShutOspf:
# source imports the custom trigger, just as you would any other Python class
  source:
    class: ex0915_custom_trigger.ShutNoShutOspf
  devices: ["iosv-0", "iosv-1"]

Example 9-12 gRun – Triggers and Trigger Datafile

from genie.harness.main import gRun


def main():
         gRun(
         trigger_uids=["TriggerShutNoShutOspf"],
         trigger_datafile="ex0912_trigger_datafile.yaml"
         )

Trigger Cluster

The last neat trigger feature to cover is the ability to execute a group of multiple triggers and verifications in one cluster trigger. First, a trigger datafile must be created with the list of triggers and verifications, the order in which to run them, and a list of testbed devices to run them against. Example 9-13 shows a trigger datafile configured for a trigger cluster and the accompanying test results if it was run.

Example 9-13 Trigger Cluster

TriggerCombined:
    sub_verifications: ['Verify_BgpVrfAllAll']
    sub_triggers: [ 'TriggerSleep', 'TriggerShutNoShutBgp']
    sub_order: ['TriggerSleep', 'Verify_BgpVrfAllAll',
    'TriggerSleep','TriggerShutNoShutBgp','Verify_BgpVrfAllAll']
    devices: ['uut']

-- TriggerCombined.uut                                                 PASSED
   |-- TriggerSleep_sleep.1                                            PASSED
   |-- TestcaseVerificationOps_verify.2                                PASSED
   |-- TriggerSleep_sleep.3                                            PASSED
   |-- TriggerShutNoShutBgp_verify_prerequisite.4                      PASSED
   |   |-- Step 1: Learning 'Bgp' Ops                                  PASSED
   |   |-- Step 2: Verifying requirements                              PASSED
   |   '-- Step 3: Merge requirements                                  PASSED
   |-- TriggerShutNoShutBgp_shut.5                                     PASSED
   |   '-- Step 1: Configuring 'Bgp'                                   PASSED
   |-- TriggerShutNoShutBgp_verify_shut.6                              PASSED
   |   '-- Step 1: Verifying 'Bgp' state with ops.bgp.bgp.Bgp          PASSED
   |-- TriggerShutNoShutBgp_unshut.7                                   PASSED
   |   '-- Step 1: Unconfiguring 'Bgp'                                 PASSED
   |-- TriggerShutNoShutBgp_verify_initial_state.8                     PASSED
   |   '-- Step 1: Verifying ops 'Bgp' is back to original state       PASSED
   '-- TestcaseVerificationOps_verify.9                                PASSED

You may notice that the triggers have accompanying local verifications that run before and after the trigger is run to ensure the action was actually taken against the device. This is the true power of triggers. One of the biggest reasons people are skeptical about network automation is due to the lack of trust. Did this automation script/test really do what it’s supposed to do? Triggers provide that verification out of the box through global and local verifications.

What if we wanted to build our own trigger with verifications? In the next section, you’ll see how to do just that!

Writing a Trigger

The pyATS library (Genie) provides the ability to write your own triggers. A trigger is simply a Python class that has multiple tests in it that either configure, verify, or unconfigure the configuration or device feature you’re trying to test.

To begin, your custom trigger must inherit from a base Trigger class. This base class contains common setup and cleanup tasks that help identify any unexpected changes to testbed devices not currently under testing (for example, a device rebooting). For our custom trigger, we are going to shut and unshut OSPF. Yes, this trigger already exists in the library, but it serves as a great example when you’re beginning to create custom triggers. The workflow is going to look like this:

  1. Check that OSPF is configured and running.

  2. Shut down the OSPF process.

  3. Verify that OSPF is shut down.

  4. Unshut the OSPF process.

  5. Verify OSPF is up and running.

In Examples 9-14 and 9-15, you’ll see the code to create the custom OSPF trigger and the associated job file, running it with gRun. To run the job file, you’ll need the following files:

  • ex0915_custom_trigger.py

  • ex0916_custom_trigger_job.py

  • ex0915_custom_trigger_datafile.yaml

  • testbed2.yaml

The testbed2.yaml file has two IOSv routers, named “iosv-0” and “iosv-1,” running OSPF. The file ex0915_custom_trigger_datafile.yaml is used to map the custom OSPF triggers and the testbed devices:

# ex0915_custom_trigger_datafile.yaml

extends: "%CALLABLE{genie.libs.sdk.genie_yamls.datafile(trigger)}"

# Custom trigger
TriggerMyShutNoShutOspf:
    # source imports the custom trigger
    source:
        class: ex0915_custom_trigger.MyShutNoShutOspf
    devices: ["iosv-0", "iosv-1"]

Example 9-14 Custom Trigger and Job File

import time
import logging
from pyats import aetest

from genie.harness.base import Trigger
from genie.metaparser.util.exceptions import SchemaEmptyParserError

log = logging.getLogger()

class MyShutNoShutOspf(Trigger):
"""Shut and unshut OSPF process. Verify both actions."""

                 @aetest.setup
                 def prerequisites(self, uut):
                 """Check whether OSPF is configured and running."""

                 # Checks if OSPF is configured. If not, skip this trigger
                 try:
                                  output = uut.parse("show ip ospf")
                 except SchemaEmptyParserError:
                     self.failed(f"OSPF is not configured on device {uut.name}")

                 # Extract the OSPF process ID
                 self.ospf_id = list(output["vrf"]["default"]["address_family"]                  ["ipv4"]["instance"].keys())[0]


                 # Checks if the OSPF process is enabled
                 ospf_enabled = output["vrf"]["default"]["address_family"]                  ["ipv4"]["instance"][self.ospf_id]["enable"]

                 if not ospf_enabled:
                       self.skipped(f"OSPF is not enabled on device {uut.name}")


                 @aetest.test
                 def ShutOspf(self, uut):
                 """Shutdown the OSPF process"""

                 uut.configure(f"router ospf {self.ospf_id}\n shutdown")
                 time.sleep(5)


                 @aetest.test
                 def verify_ShutOspf(self, uut):
                 """Verify ShutOspf worked"""

                 output = uut.parse("show ip ospf")

                 ospf_enabled = output["vrf"]["default"]["address_family"]                  ["ipv4"]["instance"][self.ospf_id]["enable"]

                 if ospf_enabled:
                           self.failed(f"OSPF is enabled on device {uut.name}")

                 @aetest.test
                 def NoShutOspf(self, uut):
                 """Unshut the OSPF process"""

                 uut.configure(f"router ospf {self.ospf_id}\n no shutdown")

                 @aetest.test
                 def verify_NoShutOspf(self, uut):
                 """Verify NoShutOspf worked"""

                 output = uut.parse("show ip ospf")

                 ospf_enabled = output["vrf"]["default"]["address_family"]                  ["ipv4"]["instance"][self.ospf_id]["enable"]

                 if not ospf_enabled:
                            self.failed(f"OSPF is enabled on device {uut.name}")

Example 9-15 Running a Custom Trigger – ex0915_custom_trigger_job.py

from genie.harness.main import gRun

def main():
         gRun(
         trigger_uids=["TriggerMyShutNoShutOspf"],
         trigger_datafile="ex0915_custom_trigger_datafile.yaml",
         )



# Running the job using 'pyats run job' command
# pyats run job ex0916_custom_trigger_job.py --testbed-file testbed2.yaml

Figure 9-3 shows some sample job output.

x
FIGURE 9.3

Figure 9.3 Job Results

Trigger and Verification Example

Now it’s time to combine the triggers and verifications into one complete example. In the example, we will build on previous examples where we flap (shut/unshut) the OSPF process on two IOSv routers. The trigger has local verifications that will confirm OSPF is indeed shut down and confirm that it comes up after being unshut. In addition to the local verifications defined in the trigger, the job will also introduce a global verification to check the router link states (LSA Type 1) in the OSPF LSDB (link state database). This global verification will run before and after the trigger. This allows us to confirm that LSA Type 1 packets are being exchanged before and after testing and that the router link types have not changed during testing.

Let’s see how this example can be implemented and executed using Genie Harness and also within a pyATS testscript.

Genie Harness (gRun)

This whole chapter has been focused on Genie Harness, so let’s not rehash the details. Example 9-16 shows an example of the job file, trigger datafile, and verification datafile used to execute the job. Figure 9-4 shows the associated test results.

Example 9-16 Trigger and Verification Example – ex0917_complete_example.py

# Job file - ex0917_complete_example.py
from genie.harness.main import gRun

def main():
gRun(
         trigger_uids=["TriggerShutNoShutOspf"],
         trigger_datafile="ex0917_trigger_datafile.yaml",
         verification_uids=["Verify_IpOspfDatabaseRouter"],
         verification_datafile="ex0917_verification_datafile.yaml"
         )



# Trigger datafile – ex0917_trigger_datafile.yaml
extends: "%CALLABLE{genie.libs.sdk.genie_yamls.datafile(trigger)}"

# Custom trigger - created before Example 9-14
TriggerShutNoShutOspf:
# source imports the custom trigger, just as you would any other Python class
  source:
    class: ex0915_custom_trigger.MyShutNoShutOspf
  devices: ["iosv-0", "iosv-1"]





# Verification datafile - ex0917_verification_datafile.yaml
extends: "%CALLABLE{genie.libs.sdk.genie_yamls.datafile(verification)}"


Verify_IpOspfDatabaseRouter:
  devices: ["iosv-0", "iosv-1"]
FIGURE 9.4

Figure 9.4 Trigger and Verification Example Results

pyATS

Triggers and verifications can be run in a pyATS testscript as a testcase or within a test section. Custom trigger and verification datafiles can be provided using the --trigger-datafile and --verification-datafile arguments when calling a pyATS job.

To run it as its own testcase, you’ll just need to create a class that inherits from GenieStandalone, which inherits from the pyATS Testcase class. The inherited class you create will provide a list of triggers and verifications. The same trigger and verification datafiles will be included as options when running the pyATS job via the command line.

The second way to include triggers and verifications in a pyATS testscript is by including them in an individual test section, as part of a pyATS testcase. The run_genie_sdk function allows you to run triggers or verifications as steps within a section.

Example 9-17 shows how to include triggers and verifications as their own testcase and within an existing subsection in a pyATS testscript. To run the testscript, you’ll need to add the --trigger-datafile and --verification-datafile arguments with the appropriate datafiles to map the custom trigger and verifications to the additional devices. These datafiles are not included in the example. Figure 9-5 shows the testscript results.

Example 9-17 Triggers and Verifications in pyATS Testscript

from pyats import aetest
import genie
from genie.harness.standalone import GenieStandalone, run_genie_sdk


class CommonSetup(aetest.CommonSetup):
""" Common Setup section """

       @aetest.subsection
       def connect(self, testbed):
       """Connect to each device in the testbed."""

               genie_testbed = genie.testbed.load(testbed)
               self.parent.parameters["testbed"] = genie_testbed
               genie_testbed.connect()


# Call Triggers and Verifications as independent pyATS testcase
class GenieOspfTriggerVerification(GenieStandalone):
"""Shut/unshut the OSPF process and verify LSA Type 1 packets are still being
exchanged before and after testing."""


         # Must specify 'uut'
         # If other devices are included in the datafile(s), they will be tested
         uut = "iosv-0"
         triggers = ["TriggerShutNoShutOspf"]
         verifications = ["Verify_IpOspfDatabaseRouter"]




# Calling Triggers and Verifications within a pyATS section
class tc_pyats_genie(aetest.Testcase):
         """Testcase with triggers and verifications."""
         # First test section
         @aetest.test
         def simple_test_1(self, steps):
         """Sample test section."""

         # Run Genie triggers and verifications
         # Note that you must specify the order of each trigger and verification
         run_genie_sdk(self,
                          steps,
                          ["Verify_IpOspfDatabaseRouter",                            "TriggerShutNoShutOspf",                            "Verify_IpOspfDatabaseRouter"],
                          uut="iosv-0"
                          )


class CommonCleanup(aetest.CommonCleanup):
"""Common Cleanup section"""

@aetest.subsection
def disconnect_from_devices(self, testbed):
         """Disconnect from each device in the testbed."""
         testbed.disconnect()
FIGURE 9.5

Figure 9.5 Triggers and Verifications in pyATS Testscript Results

Summary

This chapter covered a lot of information about the pyATS library (Genie), the Genie Harness, with its many different features, along with triggers and verifications. The Genie Harness allows you to take advantage of the pyATS infrastructure without having to dive-deep into code. The goal of the pyATS library (Genie) is to be modular and robust. Triggers and verifications are a perfect example. They make it easy to quickly build dynamic testcases that change with your network requirements. I highly recommend taking a closer at the code examples in this chapter and trying them out for yourself. Within minutes, you’ll see how quickly you can test a network feature with speed and accuracy!