[python]Use TextFSM to easily get objects you need from unstructured data.

Introduction

Netmiko has support of textfsm, however it does not have every template to help the matching, to learn how to use textfsm is useful in this situation which you can create your own template without overly rely on module’s limited template. TextFSM is created by google, it provides easier and more structured way of extracting information from unstructured data, the return value after parsing textfsm template is a list of lists.

There are two tutorials or posts:

  1. https://codingpackets.com/blog/textfsm-getting-started/
  2. https://codingnetworker.com/2015/08/parse-cli-outputs-textfsm/

Two years ago I was intimidated by the above posts and pushed myself to learn regex, two years later I could fully understood them as I need textFSM now.

Advantage of TextFSM

Without textFSM you will have to design regex that matches all possible output of cisco’s “show access-list”, this is time consuming and messy, to understand how complicated and messy you need to read this post which I created, and the regex which I posted was not perfect.

This is the perfect one which I created after I posted the post, using the below will group the “interesting” data into dictionary.

'''
Regex for matching all types of access-list patterns in Cisco ASA,
tested and constructed by Cyrus Lok.
(?P) is used to make the matched data into dictionary format,
re.match.groupdict() will group the matched data into value, and the tag_name as
dictionary key.
'''
# changed from (?P\w+) to (?P\S+, this is because
# some acl names have hypens which are not matched by \w+.
# \w matches only these [0-9a-zA-Z]
patterns = [
    # Match specific source and specific destination address of any protocol and with or without specific ports or range of ports
    r'^\s+access-list\s(?P\S+)\sline\s(?P\d+)\sextended\s(?P(permit|deny))\s(?P\w+)\shost\s(?P\d+\.\d+\.\d+\.\d+)\shost\s(?P\d+\.\d+\.\d+\.\d+)\s?(eq\s(?P\w+)|range?\s(?P\w+)\s(?P\w+))*.*$',
    # Match specific source and range destination address of any protocol and with or without specific ports or range of ports
    r'^\s+access-list\s(?P\S+)\sline\s(?P\d+)\sextended\s(?P(permit|deny))\s(?P\w+)\shost\s(?P\d+\.\d+\.\d+\.\d+)\srange\s(?P\d+\.\d+\.\d+\.\d+)\s(?P\d+\.\d+\.\d+\.\d+)\s?(eq\s(?P\w+)|range?\s(?P\w+)\s(?P\w+))?.*$',
    # Match specific source address and specific destination network or any address of any protocol and with or without specific ports or range of ports
    r'^\s+access-list\s(?P\S+)\sline\s(?P\d+)\sextended\s(?P(permit|deny))\s(?P\w+)\shost\s(?P\d+\.\d+\.\d+\.\d+)\s((?Pany)|(?P\d+\.\d+\.\d+\.\d+)\s(?P\d+\.\d+\.\d+\.\d+))\s?(eq\s(?P\w+)?|range?\s(?P\w+)\s(?P\w+))?.*$',
    # Match specific source network or any and specific host address of any protocol and with or without specific ports or range of ports
    r'^\s+access-list\s(?P\S+)\sline\s(?P\d+)\sextended\s(?P(permit|deny))\s(?P\w+)\s((?Pany)|(?P\d+\.\d+\.\d+\.\d+)\s(?P\d+\.\d+\.\d+\.\d+))\shost\s(?P\d+\.\d+\.\d+\.\d+)\s?(eq\s(?P\w+)?|range?\s(?P\w+)\s(?P\w+))?.*$',
    # Match specific source network or source address range and destination network or destination address range with or without specific ports or range of ports
    r'^\s+access-list\s(?P\S+)\sline\s(?P\d+)\sextended\s(?P(permit|deny))\s(?P\w+)\s((?Pany)|(?P\d+\.\d+\.\d+\.\d+)\s(?P\d+\.\d+\.\d+\.\d+)|range\s(?P\d+\.\d+\.\d+\.\d+)\s(?P\d+\.\d+\.\d+\.\d+))\s((?P\d+\.\d+\.\d+\.\d+)\s(?P\d+\.\d+\.\d+\.\d+)|range\s(?P\d+\.\d+\.\d+\.\d+)\s(?P\d+\.\d+\.\d+\.\d+))\s?(eq\s(?P\w+)?|range?\s(?P\w+)\s(?P\w+))?.*$',
    # Match allow or deny any source and any destination
    r'^\s*access-list\s(?P\S+)\sline\s(?P\d+)\sextended\s(?P(permit|deny))\s(?Pip)\s(?Pany)\s(?Pany).*$'
    ]

With TextFSM, you assign “variables” with a regex, then place these “variables” into expected output, this method uses divide and conquer to reduce the complexity significantly into manageable pieces, this is how a sample TextFSM sample looks like:

Value acl_name (\S+)
Value line_number (\d+)
Value action (permit|deny)
Value protocol (tcp|udp|icmp)
# Only match numbers for src_address and dst_address
Value src_address (\d+\.\d+\.\d+\.\d+)
Value dst_address (\d+\.\d+\.\d+\.\d+)
Value port_number ([0-9]{1,5}|\S+)

Start
 ^access-list ${acl_name} line ${line_number} extended ${action} ${protocol} host ${src_address} host ${dst_address} eq ${port_number} -> Record
 ^access-list ${acl_name} line ${line_number} extended ${action} ${protocol} host ${src_address} host ${dst_address} -> Record

Demonstration

I am using fw02, which is a Cisco ASAv, there are two ACLs – abc and abcd – abc has one line, and abcd has 81 lines. The script converts the “show access-list {acl_name}” into dictionary.

from network.ciscoasa import read_acl, connect_asa_host, get_asa_credential this is my own script for inserting acl and getting acl from cisco asa.

The below is the code for the testing:

from network.ciscoasa import read_acl, connect_asa_host, get_asa_credential
from textfsm import TextFSM
from pprint import pprint

# get the credential of fw02
credential = get_asa_credential(hostname="fw02")

# creates a nornir object that is specifically for fw02.
fw02 = connect_asa_host(hostname="fw02", username=credential["username"], password=credential["password"])

# returns an AggregatedResult object as the response.
response = fw02.run(task=read_acl, acl_name="abc")

# This gives the output of "show access-list" of fw02.
result = response["fw02"][1].result

# Get failure status, if True = failed.
failure = response["fw02"][0].failed

# Get the textFSM template
with open("../network/templates/show_access_list.textfsm") as template_file:
    template = TextFSM(template_file)

# the output of textfsm parsing is a nested list object.
result_list = template.ParseText(result)

# collect the acl_name which is at index 0 for every list within the super list.
acl_names_list = [result[0] for result in result_list]
# remove the duplicated acl_name with set.
acl_names_set = {acl_name for acl_name in acl_names_list}

# Keys to collect acl values.
acl_descriptions = ["line", "action", "protocol", "src_addr", "dst_addr", "service"]

# a list of acl dictionary for one acl_name, initialize to empty list.
tmp_collect_acl_dict = []
# a list to collect all acls of all acl_names, initialize to empty list.
all_acl = []

# use to temporary collect the list of acl associated with one acl_name_set.
tmp_list = list()

for acl_name_set in acl_names_set:
    """
    There are two levels, first level acl_names_set,
    Second level collects the acl list that has the name of acl_name_set.
    """
    for r in result_list:
        """
        Collect the acls associated with acl_name_set
        """
        if r[0] == acl_name_set:
            tmp_list.append(r[1:])
    for t in tmp_list:
        """
        Collect the list of dictionaries that belongs to one acl_name_set.
        """
        tmp_collect_acl_dict.append(dict(zip(acl_descriptions, t)))

    # Aggregate all the acl under one acl_name_set
    all_acl.append(
        {
            acl_name_set: tmp_collect_acl_dict
        }
    )

    """
    Must clear the tmp_list and tmp_collect_acl_dict,
    in order to collect a fresh list of acl of the next acl_name_set.
    """
    tmp_list = []
    tmp_collect_acl_dict = []

response_to_user = {
    "status": "success" if not failure else "failed",
    "request": all_acl
}
# Easier to read response_to_user
pprint(response_to_user)

Response output – show access-list abc

nor24

Response output – show access-list abcd

This access-list has 81 lines, hence only a portion of the output is shown
The code has to change slightly to this response = fw02.run(task=read_acl, acl_name="abcd"), acl_name is the argument of read_acl.

nor25

Response output – show access-list

This will show all acls, the code has to change slightly response = fw02.run(task=read_acl)

Since the output is excessively long, only a portion is will be shown here.
nor26

8 thoughts on “[python]Use TextFSM to easily get objects you need from unstructured data.

    1. I just tried on cisco’s access-list with the template: access-list {{acl_name}} extended {{action}} {{protocol}} {{src_type}} {{src_object}} {{dst_type}} {{dst_object}} {{svc_obj_type}} {{service}} log
      but does not seem to work… my output list is empty..

    2. It worked! looks like it has problem doing format and structure… I was using the parser.result(structure=”dictionary”) and parser.result(format=”json”), after i remove all options just using parser.result() i got the list of list of dictionary.

  1. I haven’t used it a lot and don’t know its weaknesses but this works:
    “`
    In [21]: %cpaste
    Pasting code; enter ‘–‘ alone on the line to stop or use Ctrl-D.
    :data = ”’
    :access-list abc extended permit tcp host 192.168.31.1 host 1.1.1.1 eq ssh log
    :”’
    :
    :template = ”’access-list {{acl_name}} extended {{action}} {{protocol}} {{src_type}} {{src_object}} {{dst_type}} {{dst_object}} {{svc_obj_type}} {{service}} log”’
    :

    In [22]: parser = ttp(data=data, template=template)

    In [23]: parser.parse()

    In [24]: parser.result()
    Out[24]:
    [[{‘acl_name’: ‘abc’,
    ‘action’: ‘permit’,
    ‘protocol’: ‘tcp’,
    ‘src_type’: ‘host’,
    ‘src_object’: ‘192.168.31.1’,
    ‘dst_type’: ‘host’,
    ‘dst_object’: ‘1.1.1.1’,
    ‘svc_obj_type’: ‘eq’,
    ‘service’: ‘ssh’}]]
    “`

    Since you do a lot of automation I just wanted to let you know about it =)

  2. thanks. that was what i was doing, but i realize i could not parse the entire long of strings… but this ttp has very high potential…getting data from unstructured data without the need to know regex is really amazing 😀

Leave a comment