Getting Started with Python Network Libraries for Network Engineers – Part 3

Blog Detail

This blog post is the third in a series covering common Python libraries that can be used to interact with network devices. In this post we will cover the Scrapli Python library by Carl Montanari. Per its documentation, Scrapli is the words “scrape” and “cli” (i.e. screen scrape) squished together. Its goal is to provide a thoroughly tested, well typed, well documented, simple API that supports both synchronous and asynchronous interaction with network devices.

Differentiation from Other Libraries

Scrapli is different from other libraries in the following ways:

  1. Scrapli provides multiple forms of transport. It defaults to using a system’s local SSH binary. This is in contrast to Netmiko, which uses Paramiko for transport. The forms of transport it supports can be found here along with a justification for the decision to allow for multiple transports and default to a system’s local SSH binary.
  2. Scrapli supports the same platforms as NAPALM out of the box. This is a subset of the platforms supported by Netmiko. The scrapli community project provides the ability for the scrapli community to contribute to and use drivers beyond those included in the base project. The platforms supported out of the box are:
    • Cisco IOS-XE
    • Cisco NX-OS
    • Juniper JunOS
    • Cisco IOS-XR
    • Arista EOS
  3. If you’re looking to write unit/integration tests for your code, scrapli has an adjacent library called scrapli-replay that can be used to do so.

Installation

You can install Scrapli via pip install scrapli. Or, if you are using Poetry, you can use poetry add scrapli. Other methods for installing scrapli are documented here.

Getting Connected

Note: We will only cover connecting via SSH in this blog post.

To start, you’ll want to import and instantiate one of the scrapli “core” drivers from scrapli.driver.core.

Scrapli requires a hostname (IP or DNS name) and an authentication method (generally username and password) to get connected. In addition to this, you will also want to specify whether or not to use SSH strict host key checking (we pass in False below).

>>> from scrapli.driver.core import IOSXEDriver
>>>
>>> conn = IOSXEDriver(
...     host="192.0.2.3",
...     auth_username="cisco",
...     auth_password="cisco",
...     auth_strict_key=False,
... )
>>> type(conn)
scrapli.driver.core.cisco_iosxe.sync_driver.IOSXEDriver
>>> conn.open()

Note: If host strict key checking is enabled (the default), an SSH session will be permitted only to hosts that have a key defined inside of your system’s “known_hosts” file.

You can also use the Scrapli class to dynamically select and instantiate a driver (much like ConnectHandler in Netmiko) in the following way:

>>> from scrapli import Scrapli
>>>
>>> conn = Scrapli(
...     host="192.0.2.3",
...     auth_username="cisco",
...     auth_password="cisco",
...     auth_strict_key=False,
...     platform="cisco_iosxe",
...  )
>>> type(conn)
scrapli.driver.core.cisco_iosxe.sync_driver.IOSXEDriver
>>> conn.open()

A list of the basic driver arguments can be found here.

Sending Commands

Once you have instantiated your Driver object, you can send single show commands via the .send_command() method. You will use the same command syntax you would type in if you were directly connected to the device via SSH:

>>> response = conn.send_command("show ip interface brief")
>>> print(response.result)
Interface              IP-Address      OK? Method Status                Protocol
FastEthernet0          unassigned      YES NVRAM  down                  down
GigabitEthernet1/0/1   192.0.2.3       YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  up                    up
>>> print(response.failed)
False

Likewise, you can send multiple commands using the .send_commands() method.

>>> response = conn.send_commands(["show ip interface brief", "show running-config | include hostname"])
>>> print(response.result[0])
Interface              IP-Address      OK? Method Status                Protocol
FastEthernet0          unassigned      YES NVRAM  down                  down
GigabitEthernet1/0/1   192.0.2.3       YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  up                    up
>>> print(response.result[1])
hostname cisco

You can also send commands from a file using the .send_commands_from_file() method.

# commands.txt
show ip int br
show run | i hostname
response = conn.send_commands_from_file("commands.txt")
>>> print(response.result[0])
Interface              IP-Address      OK? Method Status                Protocol
FastEthernet0          unassigned      YES NVRAM  down                  down
GigabitEthernet1/0/1   192.0.2.3       YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  up                    up
>>> print(response.result[1])
hostname cisco

If you want to run a command to edit the configuration, you would use the .send_configs() method instead. This method takes care of entering configuration mode for you, and it requires the commands be in a list or set:

>>> response = conn.send_configs(["interface Gi1/0/3", "no description"])
>>> print(response.result)
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
cisco(config)#interface Gi1/0/3
cisco(config-if)#no description

Note: The send_configs() method doesn’t exit config mode like Netmiko does. It, instead, relies on the send_command() method to acquire the user exec mode prompt.

As is the case with .send_commands(), you can also send configurations from a file using the send_configs_from_file() method.

# config_changes.txt
interface Gi1/0/3
 no description
>>> response = conn.send_configs_from_file("config_changes.txt")
>>> print(response.result)
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
cisco(config)#interface Gi1/0/3
cisco(config-if)#no description

Finally, there is a send_config() method that will parse the configuration provided as input and split it into lines, each line being sent to the device. This is useful for copying full configs over to the device.

Command Output Parsing

We’ve covered parsing strategies here on this blog before. Just as with Netmiko, Scrapli supports TextFSMTTP, and Genie. Let’s take a quick look on how to use them with Scrapli.

TextFSM

Scrapli defines TextFSM as an “extra”. As such, the first step to using TextFSM templates with scrapli is to install the TextFSM library. You can use pip or Poetry to do so.

pip install 'scrapli[textfsm]'
poetry add 'scrapli[textfsm]'

Once TextFSM is installed, you can use the .textfsm_parse_output() method on a Scrapli response object to return the output as parsed by the NTC template. This method uses the platform inferred by the driver to set the textfsm-platform. It combines this with the command sent to the device to guess as to which template it should use to parse the returned data.

>>> response = conn.send_command("show interfaces")
>>> print(response.textfsm_parse_output())
[{'abort': '',
  'address': '381c.1ae6.cd81',
  'bandwidth': '100000 Kbit',
  'bia': '381c.1ae6.cd81',
  'crc': '0',
  'delay': '100 usec',
...

You can see the template used for this command here.

Scrapli also supports defining the template that should be used to parse the response manually. You can import and use the textfsm_parse function from scrapli.helper to do so.

>>> from scrapli.helper import textfsm_parse
>>>
>>> response = conn.send_command("show interfaces")
>>> structured_result = textfsm_parse("/path/to/template", response.result)

TTP

Scrapli also supports the TTP parsing library. As with TextFSM, you will need to install it via pip install ttp or poetry add ttp before it can be used. The TTP installation does not currently include any templates, so you will need to find or create your own and provide the path to those templates.

>>> response = conn.send_command("show interfaces")
>>> structured_result = response.ttp_parse_output(template="show_interfaces_template.ttp")
>>> pprint(structured_result)
[[[{'description': 'CAM1',
    'interface': 'GigabitEthernet1/0/1',
    'link_status': 'up',
    'protocol_status': 'up'},
   {'description': 'CAM2',
    'interface': 'GigabitEthernet1/0/2',
    'link_status': 'up',
    'protocol_status': 'up'},
...

Genie

The last parser that Scrapli currently supports is Genie. As with TextFSM and TTP, scrapli does not install Genie nor its required library, pyATS, by default, so you will need to install them separately via pip install 'pyats[library]' or poetry add 'pyats[library]'. Athough Genie parsers exist for non-Cisco platforms, In Scrapli, Genie parsers can be used only for Cisco devices.

>>> response = conn.send_command("show interfaces")
>>> structured_result = response.genie_parse_output()
>>> pprint(structured_result)
{'GigabitEthernet1/0/1': {'arp_timeout': '04:00:00',
                          'arp_type': 'arpa',
                          'bandwidth': 100000,
                          'connected': True,
                          'counters': {'in_broadcast_pkts': 41240,
...

Note: Genie does not support custom templates.

Next Steps

  • Perhaps you’d like to use asyncio for interactions with network devices, or learn more advanced concepts? The documentation is excellent, and a great place to start.
  • If you’d like more examples of common interactions using scrapli, Carl has created a directory with such examples here.
  • Scrapli has an ecosystem including a few adjacent tools that are worth checking out (scrapli_netconf, scrapli_nornir…etc.).

Conclusion

I hope you enjoy Scrapli. It’s an excellent tool and is my personal go-to tool for new projects in which I need to automate interactions with devices for which it has a driver.

-Phillip

New to Python libraries? NTC’s Training Academy is holding a 3-day course Automating Networks with Python I on September 26-28, 2022 with 50% labs to get you up to speed.
Visit our 2022 public course schedule to see our full list.



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Getting Started with Python Network Libraries for Network Engineers – Part 1

Blog Detail

This blog post will be the first in a series covering common Python libraries that can be used to interact with network devices. In this post we will cover the Netmiko Python library by Kirk Byers. Netmiko is based on the Paramiko Python library, but whereas Paramiko was designed to interact with standard OpenSSH devices (like Linux), Netmiko was designed to interact with network devices. It has a large number of supported platforms included for connecting via SSH, and it can also accommodate limited Telnet or serial connections as well as Secure Copy (SCP) for file transfers.

Installation

You can install Netmiko via pip install netmiko. Or, if you are using Poetry, you can use poetry add netmiko.

Getting Connected

Note: We will only be covering connecting via SSH in this blog post.

Like all SSH connections, Netmiko requires a hostname (IP or DNS name) and an authentication method (generally username and password) to get connected. In addition to this, you will also need to specify the device type you will be connecting to.

>>> from netmiko import ConnectHandler
>>> 
>>> conn = ConnectHandler(
...     host="192.0.2.3",
...     username="cisco",
...     password="cisco",
...     device_type="cisco_ios"
... )

There are two ways of determining the device type: looking it up in a list or having Netmiko try to detect the device type automatically. You can see the list of current device types by digging into the code on GitHub, specifically the CLASS_MAPPER_BASE dictionary in the ssh_dispatcher.py file. If, however, you aren’t exactly sure which device type you need to choose, you can use the SSHDetect class to have Netmiko help:

>>> from netmiko import ConnectHandler, SSHDetect
>>> 
>>> detect = SSHDetect(
...     host="192.0.2.3",
...     username="cisco",
...     password="cisco",
...     device_type="autodetect"  # Note specifically passing 'autodetect' here is required
... )
>>> detect.autodetect()  # This method returns the most likely device type
'cisco_ios'
>>> detect.potential_matches  # You can also see all the potential device types and their corresponding accuracy rating
{'cisco_ios': 99}
>>> conn = ConnectHandler(
...     host="192.0.2.3",
...     username="cisco",
...     password="cisco",
...     device_type=detect.autodetect()
... )

Common Methods

Once you have instantiated your ConnectHandler object, you can send single show commands via the .send_command() method. You will use the same command syntax you would type in if you were directly connected to the device via SSH:

>>> output = conn.send_command("show ip int br")
>>> print(output)
Interface              IP-Address      OK? Method Status                Protocol
FastEthernet0          unassigned      YES NVRAM  down                  down
GigabitEthernet1/0/1   unassigned      YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  up                    up
...

Note: You can send multiple show commands back-to-back with the .send_multiline(["command1", "command2"]).

If you want to run a command to edit the configuration, you would use the .send_config_set() method instead. This method takes care of entering and exiting configuration mode for you, and it requires the commands be in a list or set:

>>> output = conn.send_config_set(("interface Gi1/0/3", "no description"))
>>> print(output)
configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
cisco(config)#interface Gi1/0/3
cisco(config-if)#no description
cisco(config-if)#end
cisco#

And since we are good network engineers, we know we should always save our configuration after making changes with the .save_config() method:

>>> output = conn.save_config()
>>> print(output)
write mem
Building configuration...
[OK]
cisco#

Command Output Parsing

We’ve covered parsing strategies here on this blog before, including the three currently supported parsers Netmiko supports which are TextFSMTTP, and Genie. Let’s take a quick look on how to use them with Netmiko.

TextFSM

If you are just starting out, the easiest way to get structured output data would be to use the included TextFSM parser. By default, Netmiko includes the TextFSM library for the parsing as well as NTC Templates to use as the default templates. To get structured output, simply add use_textfsm=True to the parameters of the .send_command() method:

>>> output = conn.send_command("show interfaces", use_textfsm=True)
>>> pprint(output)
[{'abort': '',
  'address': '381c.1ae6.cd81',
  'bandwidth': '100000 Kbit',
  'bia': '381c.1ae6.cd81',
  'crc': '0',
  'delay': '100 usec',
...

You can see the template used for this command here.

TTP

Netmiko also supports the TTP parsing library, but you will need to install it via pip install ttp or poetry add ttp first. It also does not currently include any templates, so you will need to find or create your own and then provide the path to those templates when you send your command.

Creating TTP templates yourself is definitely more of a manual process, but it gives you the freedom to pare down to only the information that you need. For example, if you just need the interface name, status, and description you can have a template like so:

{{ interface }} is {{ link_status }}, line protocol is {{ protocol_status }} {{ ignore }}
  Description: {{ description }}

And then you would reference the template path using ttp_template:

>>> output = conn.send_command("show interfaces", use_ttp=True, ttp_template="templates/show_interfaces.ttp")
>>> pprint(output)
[[[{'description': 'CAM1',
    'interface': 'GigabitEthernet1/0/1',
    'link_status': 'up',
    'protocol_status': 'up'},
   {'description': 'CAM2',
    'interface': 'GigabitEthernet1/0/2',
    'link_status': 'up',
    'protocol_status': 'up'},
...

Genie

The last parser that Netmiko currently supports is Genie. Netmiko, however, does not install Genie nor its required library, PyATS, by default, so you will need to install them separately via pip install 'pyats[library]' or poetry add 'pyats[library]'. Once they are installed, it is again very similar to enable the Genie parsing:

>>> output = conn.send_command("show interfaces", use_genie=True)
>>> pprint(output)
{'GigabitEthernet1/0/1': {'arp_timeout': '04:00:00',
                          'arp_type': 'arpa',
                          'bandwidth': 100000,
                          'connected': True,
                          'counters': {'in_broadcast_pkts': 41240,
...

Note: Genie does not support custom templates.


Conclusion

As you can see, it doesn’t take much to get started with Netmiko. If you’d like to learn more advanced interactions with Netmiko, such as transferring files via SCP, connecting via SSH keys, or even handling commands that prompt for additional input, the best place to start would be the Netmiko Examples page in the GitHub repository. Another great resource is the #netmiko channel in the Network to Code Slack.

-Joe

New to Python libraries? NTC’s Training Academy is holding a 3-day course Automating Networks with Python I on September 26-28, 2022 with 50% labs to get you up to speed.
Visit our 2022 public course schedule to see our full list.



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Parsing Strategies – TTP Parser

Blog Detail

The Parsing Strategies blog post series continues with the introduction of TTP Parser. TTP is a relatively new Python library that has gained some adoption within the Network Automation community. It provides a simple way to parse text into structured data with an approach similar to TextFSM but, in my opinion, has a lot more to offer such as output modifiers, macros, built-in functions, results formatting, and many other features that will be discussed throughout the blog. Join the TTP (#ttp-template-text-parser) Slack channel in our Networktocode Slack if you are interested in joining the conversation.

This blog post will provide an overview of TTP. If you are looking for some basic instructions on how to get started using parsers with Ansible (NTC-Templates, TTP, etc.), start here: Ansible Parsers

What is TTP?

TTP is a Python library which uses implied RegEx patterns to parse data, but it is also incredibly flexible. This parsing strategy provides a way to process data at runtime, as opposed to post-processing by using built-in functions, custom macros, and output formatters while parsing. We will dive deeper into this in a later section. The use of macros (Python functions) to manipulate data and generate desired output is one of my favorite features of the library, thus being my go to parsing strategy of choice when it comes to any hierarchical configuration output. TTP provides a series of output formatters to transform data into YAML, JSON, Table, CSV, and more. TTP can be used as a CLI utility, Python library or available with Netmiko and Ansible.

Groups

Capture groups are declared by using XML group tags, which allows nesting of other groups to generate a hierarchy. Any match inside a group is appended to a list of results. Groups have several attributes that can be set, but only the ‘name’ attribute is required. An important attribute to highlight is the “method” value, which can be set to “group” or “table.” When parsing CLI output, it’s recommended to set the method to table. This tells the parser to consider every line as the start of capturing for the group. Otherwise, setting the “start” indicator per match will be required if you have a variation of regular expressions to capture in the group. Although groups use XML group tags, TTP Templates have a deeper resemblance to Jinja templates and share similar characteristics.

Group Example:

<group name="some_group" method="table">
data to parse
    <group name="nested">
    more data to parse
    </group>
</group>

RegEx Indicator Patterns

TTP offers the ability to specify RegEx patterns to capture within a match variable. If we take a look at the source code, we can review the exact RegEx patterns that are being applied to capture a data. It’s important to understand what the regular expression pattern is before applying it, to ensure you properly capture variables.

Patterns

PHRASE = r"(\S+ {1})+?\S+"
ROW = r"(\S+ +)+?\S+"
ORPHRASE = r"\S+|(\S+ {1})+?\S+"
DIGIT = r"\d+"
IP = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}"
PREFIX = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}"
IPV6 = r"(?:[a-fA-F0-9]{1,4}:|:){1,7}(?:[a-fA-F0-9]{1,4}|:?)"
PREFIXV6 = r"(?:[a-fA-F0-9]{1,4}:|:){1,7}(?:[a-fA-F0-9]{1,4}|:?)/[0-9]{1,3}"
_line_ = r".+"
WORD = r"\S+"
MAC = r"(?:[0-9a-fA-F]{2}(:|\.|\-)){5}([0-9a-fA-F]{2})|(?:[0-9a-fA-F]{4}(:|\.|\-)){2}([0-9a-fA-F]{4})"

Macros

As the documentation states, “Macros are python code within a macro tag. This code can contain a number of function definitions, these functions can be referenced within TTP templates.” This allows us to process data during parsing by sending the captured output from our template into a function and return a processed result. This helps eliminate the need for post-processing of the values after the data has been parsed. TTP offers the ability to use these macro functions within match variables, groups, output, and input data. Below, we will review a couple examples of using macros on matched variables.

Macro Example:

<macro>
def subscription_level(data):
    data = data.replace('"','').split()
    if len(data) >= 3:
        return {"card-type": data[0], "subscription-level": data[2]}
    return data[0]
</macro>

Structuring Output (Data Modeling)

Another awesome feature of TTP is the ability to manipulate how the data is structured and represented back to us. I won’t go into all the details and capabilities, but you can see from our example below that we have a list of neighbors under our BGP results for the IPV4 Address Family. However, under our peering neighbors, we have generated a dictionary with the key of the neighbor that was found in the subsequent result. Can you find the differences in the way the parser was structured to accomplish the different data structures? There is an ‘Awesome’ hint under the Dynamic Path example!

Simple List:

<group name="neighbor">
neighbor {{ neighbor }} {{ activate | macro("to_bool") }}
neighbor 10.1.0.1 send-community {{ send-community }}
neighbor 10.1.0.1 route-map {{ route-map }} {{ route-map-direction }}
</group>

Result Snippet:

"neighbor": [
{
    "activate": true,
    "neighbor": "10.1.0.1",
    "route-map": "PL-EBGP-PE1-OUT",
    "route-map-direction": "out",
    "send-community": "both"
},
{
    "activate": true,
    "neighbor": "10.1.0.5",
    "route-map": "PL-EBGP-PE2-OUT",
    "route-map-direction": "out",
    "send-community": "both"
}

Dynamic Path:

<group name="neighbor.{{ neighbor }}">  <------ Awesome!
neighbor {{ neighbor }} remote-as {{ remote-as }}
neighbor 10.1.0.1 update-source {{ update-source }}
</group>

Result Snippet:

"neighbor": {
    "10.1.0.1": {
        "remote-as": "65000",
        "update-source": "GigabitEthernet2.1001"
    },
    "10.1.0.5": {
        "remote-as": "65000",
        "update-source": "GigabitEthernet3.1001"
    }

There are several other techniques to format structure, and I highly encourage you to review the documentation to get the most out of the Forming Results Structure feature.

Parsing Hierarchical Configuration

Let’s get right to it! I know you can’t wait for the good stuff. Here is an example of using groups, macros, specified RegEx indicators ( ORPHRASE & DIGIT ), path formatters, and match variable indicators (‘start’, ‘end’, ‘ignore’) to parse a Nokia 7750 card configuration.

#--------------------------------------------------
echo "Card Configuration"
#--------------------------------------------------
    card 1
        card-type "iom-1" level cr
        fail-on-error
        mda 1
            mda-type "me6-100gb-qsfp28"
            ingress-xpl
                window 10
            exit
            egress-xpl
                window 10
            exit
            fail-on-error
            no shutdown
        exit
        no shutdown
    exit

TTP Template:

<macro>
def subscription_level(data):
    data = data.replace('"','').split()
    return {"card-type": data[0], "subscription-level": data[2]}
</macro>

#-------------------------------------------------- {{ ignore }}
echo "Card Configuration" {{ _start_ }}
#-------------------------------------------------- {{ ignore }}
    <group name="configure.card">
    card {{ slot-number | DIGIT }}
        card-type {{ card-type | ORPHRASE  | macro('subscription_level') }}
        fail-on-error {{fail-on-error | set(true) }}
        <group name="mda">
        mda {{ mda-slot }}
            shutdown {{ admin-state | set(false) }}
            mda-type {{ mda-type | replace('"', '') }}
            <group name="ingress-xpl">
            ingress-xpl {{ _start_ }}
                window {{ window }}
            exit {{ _end_ }}
            </group>
            <group name="egress-xpl">
            egress-xpl {{ _start_ }}
                window {{ window }}
            exit {{ _end_ }}
            </group>
            fail-on-error {{ fail-on-error | set(true) }}
            no shutdown {{ admin-state | set(true) }}
        </group>
        exit {{ ignore }}
    </group>
#-------------------------------------------------- {{ _end_ }}

Result:

[
   {
      "configure":{
         "card":{
            "card-type":{
               "card-type":"iom-1",
               "subscription-level":"cr"
            },
            "fail-on-error":true,
            "mda":{
               "admin-state":true,
               "egress-xpl":{
                  "window":"10"
               },
               "fail-on-error":true,
               "ingress-xpl":{
                  "window":"10"
               },
               "mda-slot":"1",
               "mda-type":"me6-100gb-qsfp28"
            },
            "slot-number":"1"
         }
      }
   }
]

Awesome! But what exactly is happening with our “subscription_level” macro? Our template includes the following match variable:

"card-type {{ card-type | ORPHRASE  | macro('subscription_level') }}".

This is using “ORPHRASE” to capture a single word or a phrase and the matched text (‘“iom-1” level cr”’) is then sent into the “subscription_level” function for processing. The function is taking this captured string and manipulating the text by splitting it to produce the following list: “[‘iom-1’, ‘level’, ‘cr’]”. Finally, it’s returning a dictionary with the keys for ‘card-type’ and ‘subscription-level’.

{
   "card-type":{
      "card-type":"iom-1",
      "subscription-level":"cr"
   }
}

Let’s take a look at another example template to parse a Cisco IOS BGP configuration that’s also taking advantage of TTP Built-In Functions. The goal is to convert values that would be better represented as booleans in our data model, specifically the following lines: “log-neighbor-changes” and “activate”. Although we can use the ‘set’ function like our previous example to accomplish something similar, I really want to drive home the fact that we can use python functions to accomplish our desired state of a matched variable. Also, macros have unique behavior when returning data, which we will review in more detail. We will also use ‘is_ip’ to validate our neighbor address is in fact an IP Address and DIGIT as a RegEx indicator to match a number.

Here is the raw output of the running configuration for BGP:

router bgp 65001
 bgp router-id 192.168.10.1
 bgp log-neighbor-changes
 neighbor 10.1.0.1 remote-as 65000
 neighbor 10.1.0.1 update-source GigabitEthernet2.1001
 neighbor 10.1.0.5 remote-as 65000
 neighbor 10.1.0.5 update-source GigabitEthernet3.1001
 !
 address-family ipv4
  redistribute connected
  neighbor 10.1.0.1 activate
  neighbor 10.1.0.1 send-community both
  neighbor 10.1.0.1 route-map PL-EBGP-PE1-OUT out
  neighbor 10.1.0.5 activate
  neighbor 10.1.0.5 send-community both
  neighbor 10.1.0.5 route-map PL-EBGP-PE2-OUT out
 exit-address-family

Template:

<macro>
def to_bool(captured_data):
    represent_as_bools = ["activate", "log-neighbor-changes"]
    if captured_data in represent_as_bools:
      return captured_data, {captured_data: True}
</macro>

<group name="bgp">
router bgp {{ asn | DIGIT }}
 bgp router-id {{ router-id }}
 bgp {{ log-neighbor-changes | macro("to_bool") }}
 <group name="neighbor.{{ neighbor }}">
 neighbor {{ neighbor | is_ip }} remote-as {{ remote-as }}
 neighbor 10.1.0.1 update-source {{ update-source }}
 </group>
 ! {{ ignore }}
 <group name="afi.{{ afi }}">
 address-family {{ afi }}
  redistribute {{ redistribute }}
  <group name="neighbor">
  neighbor {{ neighbor | is_ip }} {{ activate | macro("to_bool") }}
  neighbor 10.1.0.1 send-community {{ send-community }}
  neighbor 10.1.0.1 route-map {{ route-map }} {{ route-map-direction }}
  </group>
 exit-address-family {{ ignore }}
 </group>
</group>

The output of our Ansible task:

TASK [DEBUG] *******************************************************************
ok: [AS65001_CE1] => {
    "msg": [
        [
            {
                "bgp": {
                    "afi": {
                        "ipv4": {
                            "neighbor": [
                                {
                                    "activate": true,
                                    "neighbor": "10.1.0.1",
                                    "route-map": "PL-EBGP-PE1-OUT",
                                    "route-map-direction": "out",
                                    "send-community": "both"
                                },
                                {
                                    "activate": true,
                                    "neighbor": "10.1.0.5",
                                    "route-map": "PL-EBGP-PE2-OUT",
                                    "route-map-direction": "out",
                                    "send-community": "both"
                                }
                            ],
                            "redistribute": "connected"
                        }
                    },
                    "asn": "65001",
                    "log-neighbor-changes": true,
                    "neighbor": {
                        "10.1.0.1": {
                            "remote-as": "65000",
                            "update-source": "GigabitEthernet2.1001"
                        },
                        "10.1.0.5": {
                            "remote-as": "65000",
                            "update-source": "GigabitEthernet3.1001"
                        }
                    },
                    "router-id": "192.168.10.1"
                }
            }
        ]
    ]
}

Well, that was easy. All we had to do was replace the values that are of interest with jinja-like syntax and define several groups with XML group tags to properly structure our results. The macro function “to_bool” was used to process the captured data and return a boolean. You may have noticed that we returned the captured_data and a dictionary in our macro, as opposed to our earlier example only returning a simple dictionary. This is because macros will behave differently according to the data that’s being returned. Here is an explanation from the Documentation:

“If macro returns True or False – original data unchanged, macro handled as condition functions, invalidating result on False and keeps processing result on True If macro returns None – data processing continues, no additional logic associated If macro returns single item – that item replaces original data supplied to macro and processed further If macro return tuple of two elements – fist element must be string – match result, second – dictionary of additional fields to add to results”

Parsing Show Commands

Let’s continue the pattern of the series and parse the output of a simple “show lldp neighbors” command output for IOS.

Topology

Raw Output:

Capability codes:
    (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
    (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other

Device ID           Local Intf     Hold-time  Capability      Port ID
R3.admin-save.com   Gi0/1          120        R               Gi0/0
R2.admin-save.com   Gi0/0          120        R               Gi0/0

Total entries displayed: 2

Now, let’s see how simple the TTP Template is to parse operational show output commands:

<group name="LLDP_NEIGHBORS" method="table">
Device ID           Local Intf     Hold-time  Capability      Port ID {{ignore}}
{{DEVICE_ID}} {{LOCAL_INT}} {{HOLD_TIME | DIGIT}} {{CAPABILITY}} {{ PORT_ID }}
</group>
<group name="TOTAL_ENTRIES">
Total entries displayed: {{ COUNT | DIGIT}}
</group>

That’s it! Let’s review some important pieces to make this a successful template.

  • Method
    • “method=’table’” is applied to the group “LLDP_NEIGHBORS” as we are parsing operational show commands in table format.
  • Ignore
    • ”” is used to tell the parser to discard the lines that we don’t care about inside of our capture group. Any lines outside of the group are simply ignored and discarded by default.

Example Playbook:

---
- name: "EXAMPLE TTP PLAYBOOK"
  hosts: R1
  connection: network_cli

  tasks:

    - name: "10. PARSE LLDP NEIGHBORS WITH TTP"
      ansible.netcommon.cli_parse:
        command: "show lldp neighbors"
        parser:
          name: ansible.netcommon.ttp
        set_fact: lldp

    - name: DEBUG
      debug:
        msg: "{{ lldp }}"

The above playbook is referencing the template at the following relative location: “templates/ios_show_lldp_neighbors.ttp”. The templates directory contains the template starting with the ansible_network_os followed by the command.

Parsed Output:

ok: [R1] => {
    "msg": [
        [
            {
                "LLDP_NEIGHBORS": [
                    {
                        "CAPABILITY": "R",
                        "DEVICE_ID": "R3.admin-save.com",
                        "HOLD_TIME": "120",
                        "LOCAL_INT": "Gi0/1",
                        "PORT_ID": "Gi0/0"
                    },
                    {
                        "CAPABILITY": "R",
                        "DEVICE_ID": "R2.admin-save.com",
                        "HOLD_TIME": "120",
                        "LOCAL_INT": "Gi0/0",
                        "PORT_ID": "Gi0/0"
                    }
                ],
                "TOTAL_ENTRIES": {
                    "COUNT": "2"
                }
            }
        ]

Finally, one thing to keep in mind is the several nested lists that were produced. It’s as simple as ensuring you are accessing the correct list when evaluating the results.

Example Ansible debug task:

- name: DEBUG
  debug:
    msg: "{{ lldp[0][0]['TOTAL_ENTRIES'] }}"

Output:

TASK [DEBUG] *******************************************************************
ok: [R1] => {
    "msg": {
        "COUNT": "2"
    }
}

Conclusion

Although we have barely scratched the surface, you can see that TTP offers many great features. I find it to be very accommodating when parsing full hierarchical running configuration outputs, more so than other available parsers. The library is constantly evolving and implementing new features. Take a second to join the Slack channel to keep up with development and ask any questions you may have!

-Hugo



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!