In the Beginning…

Often when making their first steps into Network Automation, people may have an idea of what they want to do, but not exactly how to get there. For example, an engineer may want to simplify a single time-intensive task “Gather the SW version, serial numbers, and uptime from all of my Cisco ASAs.” However, upon getting the information, they’re unsure what to do with it or how to parse it and use it for something else. I’ve seen this question come up a few different times, and thought a series of blog posts was in order. Each of the posts (including this one) outline some sample parsing options. However, it is worth noting that Regex is not better or worse than using a parsing library, for example, they may just have different use cases.

  • First, in this post, I will outline the basics of gathering our information and parsing it directly with our own Regular Expressions (Regex).
  • Next, I will demonstrate parsing this information with the popular CiscoConfParse and HeirConfig libraries.
  • Third, I will demonstrate simple parsing with Google’s TextFSM and the awesome library of templates provided by Network To Code, ntc-templates.
  • Finally, I will demonstrate simple parsing with Cisco’s Genie/PyATS libraries.

Note that I am going to skip over a lot of the minutia of what Netmiko is, and how to use it to gather network device information, as there are many wonderful resources out there already on this. We’re going to focus primarily on what to do with the output once you have it.

Image of a Person sitting at a computer connecting to two firewall devices.
Example Network

In this series of posts, we’ll just use the simple network shown here. There is a Management Station with Python 3.8 installed, and two Cisco ASAv firewalls (ASAv1 and ASAv2) in a tiered setup.

Code samples and “requirements.txt” for this post can be found on my Github.

Environment Preparation

For this entire series of posts, I will be working the examples in a python virtual environment setup with the following dependencies installed. Note that we’re installing iPython, which is a much better alternative to the built in Python interactive shell. I highly recommend checking it out by executing ipython in this virtual environment. iPython allows us to do some more interactive introspection on objects and method/object doc_strings in real time, which we’ll use in later posts.

  • iPython
  • Netmiko
  • CiscoConfParse
  • HeirConfig
  • NTC Templates
  • Genie/PyATS

First, I create a virtual environment with whatever name you wish, and then activate that environment:

python3.8 -m venv nn_examples
cd nn_examples;source bin/activate

Next, I install the dependencies as discussed above:

pip install ciscoconfparse genie hier-config ipython netmiko ntc_templates pyats 

Gathering our Output

We need to instantiate a connection to our ASAs, and then execute the command “show version | inc So|Serial| up” against them to gather the needed output. Simple enough with Netmiko as shown below (or on Github):


#!/usr/bin/env python3
 
"""
Example code for Network-Notes.com entry on Parsing Network Device Output
"""
 
import getpass
import re
import sys
 
 
from netmiko import ConnectHandler
 
 
"""
Test parsing network device configuration
"""
 
# Gather the needed credentials
net_device_username = input("Username: ")
net_device_password = getpass.getpass()
# Set enable/secret = password for now
net_device_secret = net_device_password
 
# Setup a dict with our ASAvs in it, in real world this could be read
# from a CSV or the CLI or any other source
firewalls = {
    "ASAv1": {"ip": "10.10.10.50", "platform": "cisco_asa"},
    "ASAv2": {"ip": "10.10.60.2", "platform": "cisco_asa"},
}
 
# Setup an empty dict for our results:
results = {}
 
# Instantiate netmiko connection objects and gather the output of
# `show version | inc So|Serial| up` on these two firewalls
for fw_name, fw_data in firewalls.items():
    print(f"Connecting to {fw_name}...")
    fw_connection = ConnectHandler(
        ip=fw_data["ip"],
        device_type=fw_data["platform"],
        username=net_device_username,
        password=net_device_password,
        secret=net_device_secret,
    )
    results[fw_name] = fw_connection.send_command(
        command_string="show version | inc So|Serial| up"
    )
 
# Print our results
for fw_name, result in results.items():
    print(f"{fw_name} information:\n")
    print(result)
 
sys.exit()

Successfully executed with “python3.8 raw_gather.py”, this will output:

 Username: cisco
 Password: 
 Connecting to ASAv1…
 Connecting to ASAv2…
 ASAv1 information:

 Cisco Adaptive Security Appliance Software Version 9.12(1)2 
 ASAv1 up 23 mins 50 secs
 Serial Number: 123456789AB

 ASAv2 information:

 Cisco Adaptive Security Appliance Software Version 9.13(1)2 
 ASAv2 up 24 mins 5 secs
 Serial Number: 123456789AC

Now that we have an example of gathering the information, let’s parse it to python variables via Regex.

Parsing with Regex

Much like above, I will not exhaustively cover what Regex (Regular Expressions) are, as there are boundless resources on the internet (and good old O’Reilly books) that do this. Suffice to say for our purposes, that it is a way to match patterns in a string of text and gather output based on that, and that I strongly recommend finding a good test bed like Regex101 to help you along the way. You can get extremely complicated with Regex, and it can also bite you in the behind, just ask Cloudflare… Here is the three Regular Expressions I am using to match relevant output from the ASA with links to Regex101 examples demonstrating them.

Now here is what the program looks like utilizing the python re Regex module. The only difference is the addition of a section for parsing the output we’ve received, so that is all I’ll list below. The complete file is on Github as parse_with_regex.py:


# Parse our results:
print("Parsing Results...")
parsed_results = {}
for fw_name, result in results.items():
    parsed_results[fw_name] = {}
    parsed_results[fw_name]["version"] = re.search(
        r"Software Version (\d.\d{1,2}(?:\(?\d{1,2}?\)?\d{1,2}?)?)\W*$",
        result,
        re.MULTILINE,
    )
    parsed_results[fw_name]["uptime"] = re.search(
        r"up (.*)$", result, re.MULTILINE
    )
    parsed_results[fw_name]["serial"] = re.search(
        r"Serial Number: (\S*)$", result, re.MULTILINE
    )
 
# Print our results
for fw_name in results.keys():
    print(f"{fw_name} information:\n")
    print(f"\tVersion: {parsed_results[fw_name]['version'].group(1)}")
    print(f"\tUptime: {parsed_results[fw_name]['uptime'].group(1)}")
    print(f"\tSerial Number: {parsed_results[fw_name]['serial'].group(1)}")

This will give us the results of:

Username: cisco
 Password: 
 Connecting to ASAv1…
 Connecting to ASAv2…
 Parsing Results…
 ASAv1 information:
     Version: 9.12(1)2
     Uptime: 1 hour 25 mins
     Serial Number: 123456789AB
 ASAv2 information:
     Version: 9.13(1)2
     Uptime: 1 hour 25 mins
     Serial Number: 123456789AC

As you can see, this is a much more structured and useful output. We could also similarly output this to a CSV or other data source, now that we have parsed the data into something of more value than a raw string of all the data.

Next Up, Parsing Libraries

In the next post I publish, I will utilize this same environment, but make use of some common configuration parsing libraries (CiscoConfParse and HeirConfig) to show how they could be useful for certain use cases.