pyATS and Genie: Part 2

Introduction

It’s been awhile since my last post, but I finally found some time to sit down and continue writing this series. Since my last post, I have been spending a lot of time reading through the pyATS docs and the experimenting with its modules. In this post, we will be going over the basics and how to get started with pyATS. My goal is to have you parse your first ‘show’ command and to see how easy it is to traverse the output using a Python module like Dq (which we will jump into later). Now let’s take a look at how pyATS works and begin testing!

Getting Started with pyATS

Before building any scripts or running commands against any devices, let’s talk about the different libraries that coexist with pyATS. I don’t want to spend too much time on the details, but as network engineers, many of us like to know how things work at the most basic level. Here’s a quick illustration of the different libraries that we use alongside pyATS:

I built this visual because it has helped me understand each library and their respective role within the testing infrastructure. I hope it helps you understand them as well.

Installation

The installation process is pretty simple for pyATS and Genie. I recommend using a Python virtual environment, as it isolates your environment from your system-level Python installation. This allows for better dependency management and helps avoid potential conflicts. For my Python virtual environment, I’ll be using Pipenv, but you may also use virtualenv or venv to create your virtual environment. Here’s how to install the necessary packages using virtualenv or venv:

# creates the Python3 virtual environment using virtualenv or venv 
python3 -m virtualenv pyats-venv OR python3 -m venv pyats-venv

source pyats-venv/bin/activate # <--- activates the virtual environment

pip install pyats # <--- installs pyats (includes unicon)
pip install genie # <--- installs genie

Here’s how you would create the same virtual environment using Pipenv:

# install pipenv on the system-level Python3 installation
pip3 install pipenv

pipenv --three # <--- creates a Python3 virtual environment
pipenv shell # <--- activates the virtual environment

pipenv install pyats # <--- install pyats (includes unicon)
pipenv install genie # <--- install genie

I’ve started using Pipenv because of its great benefits. With Pipenv, you no longer need to track your project dependencies using a requirements.txt file. Pipenv automatically tracks the existing packages installed. As packages are installed and uninstalled, a file called the Pipfile is automatically updated. Another file, called Pipfile.lock, is also generated to help rebuild the environment in the future. Another great feature of Pipenv is its ability to identify security vulnerabilities in the packages you’re using in your environment. To learn more about Pipenv, check out this link.

So now we know about the the pyATS and Genie libraries and have them installed, let’s jump into the fun stuff!

Defining the Testbed

If you’ve used Ansible or another automation platform, you will be familiar with building an inventory file. PyATS has a relative concept called a testbed. The testbed file defines the device names, IP addresses, connection types, credentials, OS, and type. The OS is important because that value is used by the Unicon library to determine how to connect to a device and handle the different CLI prompt patterns. Let’s review a sample testbed found in the pyATS docs:

# Example
# -------
#
#   an example testbed file - ios_testbed.yaml

testbed:
    name: IOS_Testbed
    credentials:
        default:
            username: admin
            password: cisco
        enable:
            password: cisco

devices:
    ios-1: # <----- must match to your device hostname in the prompt
        os: ios
        type: ios
        connections:
            a:
                protocol: telnet
                ip: 1.1.1.1
                port: 11023
    ios-2:
        os: ios
        type: ios
        connections:
            a:
                protocol: telnet
                ip: 1.1.1.2
                port: 11024
            vty:
                protocol: ssh
                ip: 5.5.5.5
topology:
    ios-1:
        interfaces:
            GigabitEthernet0/0:
                ipv4: 10.10.10.1/24
                ipv6: '10:10:10::1/64'
                link: link-1
                type: ethernet
            Loopback0:
                ipv4: 192.168.0.1/32
                ipv6: '192::1/128'
                link: ios1_Loopback0
                type: loopback
    ios-2:
        interfaces:
            GigabitEthernet0/0:
                ipv4: 10.10.10.2/24
                ipv6: '10:10:10::2/64'
                link: link-1
                type: ethernet
            Loopback0:
                ipv4: 192.168.0.2/32
                ipv6: '192::2/128'
                link: ios2_Loopback0
                type: loopback

I like this example because it touches on every aspect of a testbed. Starting at the top, you have the testbed defined with a set of credentials. The credentials declared under the testbed section allow them to be used by all devices in the testbed. The devices section is where you define the devices you’ll be testing. For each device, you have to to define the OS, device type, connections (can be more than one), and the credentials (if different from the ones defined under the testbed section). The last section, topology, is the most interesting. You are able to define a logical topology in your testbed file. This allows pyATS to understand how these devices are connected in the real world. Using the example testbed above, you’ll see that ‘link-1’ is used to represent a connection between the ios-1 and ios-2 devices. I like to think of it as translating a Visio diagram to a format that pyATS can understand. By allowing pyATS to understand how these devices are connected, it can provide the foundation for more complex testcases. For example, taking a snapshot of the network before and after losing a link on a specific device to see what’s affected (i.e. link status, routing, etc.).

This brings me to an important point: Everything in pyATS is treated as an object. Take a look at this visual from the pyATS docs:

+--------------------------------------------------------------------------+
| Testbed Object                                                           |
|                                                                          |
| +-----------------------------+          +-----------------------------+ |
| | Device Object - myRouterA   |          | Device Object - myRouterB   | |
| |                             |          |                             | |
| |         device interfaces   |          |          device interfaces  | |
| | +----------+ +----------+   |          |   +----------+ +----------+ | |
| | | intf Obj | | intf Obj |   |          |   |  intf Obj| | intf Obj | | |
| | | Eth1/1   | | Eth1/2 *-----------*----------*  Eth1/1| | Eth1/2   | | |
| | +----------+ + ---------+   |     |    |   +----------+ +----------+ | |
| +-----------------------------+     |    +-----------------------------+ |
|                                     |                                    |
|                               +-----*----+                               |
|                               | Link Obj |                               |
|                               |rtrA-rtrB |                               |
|                               +----------+                               |
+--------------------------------------------------------------------------+

You can see that everything is stored in the Testbed container object. From there, the device objects (myRouterA and myRouterB) both have two interface objects (Eth1/1 and Eth1/2). The link object does not belong to either device, but rather shared between the interface objects on both devices. In our example, we will not be including a topology section in the testbed. I want to keep it easy and simple. However, I wanted to point out the topology section of the testbed, as it can extend the functionality of pyATS once you dive into more advanced use cases. Now let’s move on to creating our first testbed and gathering data from our testbed devices.

Building our Testbed

For our example, we’ll be using the Always-on DevNet ‘IOS XE on CSR’ sandbox. The use case we will be looking at is verifying the IOS software version running on our device(s). It’s common for organizations to define a standard IOS software version for their switches and routers. The problem is that it would be a nightmare for a network admin to login to each device in the network and confirm the IOS version. Besides being a very manual process, it’s also very error-prone. Fortunately, pyATS and Genie provide multiple ways to gather the data AND compare it to our defined standard. Yes, there are a number of off-the-shelf tools that can perform this same function, but I want to show you how easy it is to accomplish with pyATS.

There are two ways to define a testbed: in a YAML file (most common) or directly in a Python dictionary. The YAML file is most common due to its easier readability, but at the end of the day, the testbed is loaded into a Python dictionary. Below is the testbed I’ll be using in our example:

testbed:
  name: DevNet_Testbed

devices:
  csr1000v-1: # <----- must match to your device hostname in the prompt
    os: iosxe
    type: iosxe
    credentials:
      default:
        username: developer
        password: C1sco12345
    connections:
      cli:
        protocol: ssh
        ip: 64.103.37.51 # <----- confirm before testing
        port: 8181

It’s fairly simple to read and understand. I defined a testbed called ‘DevNet_Testbed’ and have one device in it. A few things to note here: The hostname key, which is named ‘csr1000v-1’ in my testbed, must match the hostname shown in the device’s CLI prompt. The reason for this is pyATS is looking for the hostname when logging into the device’s CLI. Another thing to note is that since we are using a public always-on sandbox, the hostname and IP address may change. Before testing, please confirm the hostname (as shown in the CLI prompt) and the public IP address are correct in your testbed. As previously mentioned, we aren’t going to be adding a topology section to our testbed. Now that we have defined our testbed, let’s start writing our first pyATS script!

Learning the Network

Before diving into the code, I want to go over the general flow of the script. First, we will load the testbed file into the script (I just called my testbed ‘testbed.yaml’ – I know, very original). Next, we will pull out only the device we want to test against. You may ask, “Why are we pulling out the only device in the testbed?”. Well, down the road when you have a larger testbed file, there’s a good chance that you will only want to test against a subset of devices. I’m showing you how to do that now – you’ll thank me later. Otherwise, your tests would run against all devices in your testbed. After we identify the device we want to test, we connect to the device, run the necessary command(s) (‘show version’ in our case), and disconnect from the device.

In the next few sections, we are going to look at some Genie modules and device methods that help gather and structure the necessary data from a network device. I’ll provide a code example and the respective output for each method.

Now that we have a general idea of how the script will flow, let’s start writing some code!

genie execute

The execute() method instructs pyATS to connect to the device, run the desired command, and return the raw output. Below is example code that identifies the DevNet sandbox CSR in the testbed and assigns it to the variable named csr. We can then use the csr variable to access other device methods, including connect() and disconnect(). How much easier can it get?!

from pyats.topology import loader

# Load the testbed file
tb = loader.load('testbed.yaml')

# Assign the CSR device to a variable
csr = tb.devices['csr1000v-1']

# Connect to the CSR device
csr.connect()

# Issue 'show version' command and print the output
print(csr.execute('show version'))

# Disconnect from the CSR device
csr.disconnect()
csr1000v-1#
Cisco IOS XE Software, Version 16.09.03
Cisco IOS Software [Fuji], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.9.3, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Wed 20-Mar-19 07:56 by mcpre


Cisco IOS-XE software, Copyright (c) 2005-2019 by cisco Systems, Inc.
All rights reserved.  Certain components of Cisco IOS-XE software are
licensed under the GNU General Public License ("GPL") Version 2.0.  The
software code licensed under GPL Version 2.0 is free software that comes
with ABSOLUTELY NO WARRANTY.  You can redistribute and/or modify such
GPL code under the terms of GPL Version 2.0.  For more details, see the
documentation or "License Notice" file accompanying the IOS-XE software,
or the applicable URL provided on the flyer accompanying the IOS-XE
software.


ROM: IOS-XE ROMMON

csr1000v-1 uptime is 14 minutes
Uptime for this control processor is 15 minutes
System returned to ROM by reload
System image file is "bootflash:packages.conf"
Last reload reason: reload




## Truncated for brevity ##

For the output, you’ll notice that it looks just like it would in an SSH session. The only problem with that is, while it may be readable for a human, it’s not in a great format for a computer. This output is stored as one long string object in Python. This is not ideal when it comes to parsing out the data we need. Maybe there’s a Genie method that can collect and “parse” the data…. sorry, I know it’s a bad one… let’s move on to Genie’s parse method.

genie parse

The parse() method performs exactly the same actions as the execute() method, but provides some additional functionality. Along with sending the command to the device and collecting the output, the output is passed along to one of the thousands of available parsers in Genie’s library. For the complete list of available parsers, check them out here. These parsers will automatically break down the long string into a structured Python dictionary. This is where we can begin programmatically interacting with the data, without the need for complex regex. Let’s take a look at the code and structured output:

from pyats.topology import loader
from pprint import pprint

# Load the testbed file
tb = loader.load('testbed.yaml')

# Assign the CSR device to a variable
csr = tb.devices['csr1000v-1']

# Connect to the CSR device
csr.connect()

# Issue 'show version' command and print the output
pprint(csr.parse('show version'))

# Disconnect from the CSR device
csr.disconnect()
{'version': {'chassis': 'CSR1000V',
             'chassis_sn': '9YUMZ3N5W7V',
             'compiled_by': 'mcpre',
             'compiled_date': 'Wed 20-Mar-19 07:56',
             'curr_config_register': '0x2102',
             'disks': {'bootflash:.': {'disk_size': '7774207',
                                       'type_of_disk': 'virtual hard disk'},
                       'webui:.': {'disk_size': '0',
                                   'type_of_disk': 'WebUI ODM Files'}},
             'hostname': 'csr1000v-1',
             'image_id': 'X86_64_LINUX_IOSD-UNIVERSALK9-M',
             'image_type': 'production image',
             'label': 'RELEASE SOFTWARE (fc2)',
             'last_reload_reason': 'reload',
             'license_level': 'ax',
             'license_type': 'Default. No valid license found.',
             'main_mem': '2392579',
             'mem_size': {'non-volatile configuration': '32768',
                          'physical': '8113280'},
             'next_reload_license_level': 'ax',
             'number_of_intfs': {'Gigabit Ethernet': '3'},
             'os': 'IOS-XE',
             'platform': 'Virtual XE',
             'processor_type': 'VXE',
             'returned_to_rom_by': 'reload',
             'rom': 'IOS-XE ROMMON',
             'rtr_type': 'CSR1000V',
             'system_image': 'bootflash:packages.conf',
             'uptime': '3 minutes',
             'uptime_this_cp': '4 minutes',
             'version': '16.9.3',
             'version_short': '16.9',
             'xe_version': '16.09.03'}}



The only change we made to our code is swapping out the execute() method with the parse() method (see highlighted line of code). Also, we imported pretty print (pprint) in order to make the output look nicer. The biggest difference is the output. In 5 lines of code (minus the comments), we have a structured Python dictionary with datapoints that identify key information you’d find in a ‘show version’ output. You can see the ‘last_reload_reason’, ‘os’, ‘uptime’, and plenty of other valuable datapoints. For our use case, we will be interested in the ‘xe_version’ datapoint.

genie learn

If Genie parse takes care of our use case, why do we need to know another method? Well, what if you want to gather data across multiple network devices with different operating systems that require different ‘show’ commands? With our current knowledge, you would have to parse multiple ‘show’ commands for the devices with different OS types in your testbed. Besides that, the output of these different ‘show’ commands may not include the datapoints you are even looking for. Enter Genie learn…

The Genie learn() method allows you to learn a feature of the device. Features can be protocols running on the device (i.e. ospf, eigrp, lisp, dot1x, etc.) or attributes about the device (i.e. platform, interface). These features are broken up into what Genie calls models. For the complete list of available Genie models, check them out here. These models are used to provide a level of abstraction so that you don’t have to worry about what commands to parse for each OS. This allows you to focus more on the output and finding the datapoints you need. I’m not providing a code snippet for the learn functionality, but I do want to show a small example of how Genie learns routing across the different Cisco NOS platforms.

I chose routing because the commands are so relative across the different platforms, that it can trip up even the most experienced engineer. If you are interested in looking at an active, open-source project that takes pyATS and Genie learn to a whole new level, check out Merlin. This project was started by John Capobianco earlier this year and helps network engineers collect and document information about their network using the power of pyATS.

Querying the Data

We’ve made some good progress thus far. The proper data has been collected, but now it’s time to check it against our defined standards. For the sake of our example, I’m going to declare IOS-XE 16.12.5 as our defined standard.

So how are we going to drill down to the datapoint we are interested in? Normally, we would have to use nested for loops to dig through the dictionaries of data, but not anymore. Genie comes with a suite of helpful libraries including one called Dq (dictionary query). The documentation is tough to find because it’s buried in a submenu within the Genie docs, but I wanted to provide a link for convenience: Dq library. If the link doesn’t take you there, you’ll have to click on ‘User Guide’ along the left side and choose ‘Useful Libraries’ towards the bottom of that submenu. Along with the Dq library, there are some other useful libraries, including Diff, Find, Config, Timeout, and TempResult. These libraries are just added bonuses to the already valuable Genie library. Let’s see how we can use Dq to search and locate our desired datapoint.

from pyats.topology import loader
from pprint import pprint
from genie.utils import Dq

# Load the testbed file
tb = loader.load('testbed.yaml')

# Assign the CSR device to a variable
csr = tb.devices['csr1000v-1']

# Connect to the CSR device
csr.connect()

# Issue 'show version' command and parse the output
parsed_output = csr.parse('show version')
# Store the standard IOS version in a variable for future use
standard_os = '16.12.05'
# Look for the 'xe_version' key and see if it contains the proper IOS version
ios_check = Dq(parsed_output).contains(standard_os).get_values('xe_version')

if ios_check:
    print('IOS Check passed!')
else:
    print('IOS Check failed!')

# Disconnect from the CSR device
csr.disconnect()

I highlighted the two lines that were added to our script in order for us to use the Dq library. The first line imports the library. The second line queries the parsed ‘show version’ output AND performs the comparison for us. Let’s take a minute and look at the magic in that single line of code.

In our code, we convert the parsed ‘show version’ output to a Dq object. This allows us to use all the available methods in the Dq library. In our example, we use the get_values() method to locate the dictionary key we are interested in, and the contains() method to check whether that key “contains” an expected value. As a result, if a value is matched, a Python list with the matched values will be returned. If there are no matches, an empty Python list will be returned. In our example, that returned Python list is stored in the ios_check variable. The last if/else statement just determines whether the list is empty or populated. If the list is populated (meaning there was a match on the IOS versions), then the IOS check passed and we are running the correct version. If the list is empty, the IOS versions did not match and the IOS check failed. Here’s what the returned value would look like if there was a match:

['16.09.03']
IOS Check passed!

You’ll notice that there is one item in the returned Python list, which is the matching string value for the ‘xe_version’ key in the parsed ‘show version’ output.

With one line of code, we were able to query a nested dictionary AND determine whether a certain value existed. Hats off to the pyATS team. There will be so much time (and lines of code) saved from using this extraordinary library. It’s also good to note that this library, along with the rest of the Genie library, can be used independently of pyATS. So whether you’re querying your network or working on a separate project with larger datasets, you can use the power of Dq.

If you struggled to follow along, or would like to review the code we used in our example, check out my Github repo linked in the References section at the end of this post.

Conclusion

That wraps up Part 2 of this pyATS and Genie series. I’ve been enjoying writing these posts and I hope you’ve been able to find value in these phenomenal libraries. There’s so much more to the pyATS and Genie libraries, that I’ve just scratched the surface. Please check out the docs yourself to see the great features included in these libraries. In Part 3, we are going to take a look at the AEtest testing framework and Easypy runtime environment in pyATS. We may even write our first, true testscript.

As always, if you have any questions or just want to chat, hit me up on Twitter (@devnetdan). Thanks for reading!

References

Github repo: dannywade/learning-pyats: Repo for all pyATS code examples

pyATS docs: pyATS Documentation – pyATS – Document – Cisco DevNet
Genie docs: index – Genie Docs – Document – Cisco DevNet
Unicon docs: Documentation – Unicon – Document – Cisco DevNet

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s