Adding Cisco IOS support to NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support)

If  you’re a networking passionate I’m pretty sure you’ve already heard about NAPALM (no, I’m not talking about the flammable liquid used in warfare 🙂 ). Anyway, if you’ve not yet, you’re going to discover a very nice project for network automation.


 

NAPALM

What is it? Let’s quote its documentation page:

NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) is a Python library that implements a set of functions to interact with different network device Operating Systems using a unified API.

It’s a project developed by David Barroso and Elisa Jasinska (thank you guys 😀 ), owned by Spotify and, as the quote says itself, it is used to interact with different hardware networking vendors. Basically, it works like an API on top of other APIs, adding another level of abstraction.

Lately, many vendors have developed APIs to making it easier to interact with their equipments. For example, most of the JunOS devices support Juniper PyEZ, and so do Cisco’s Nexus with its NX-API.

This way, if I want to interact with a Juniper device I can use PyEZ, whereas I’d use NX-API if I wish to talk with a Nexus switch, and the example continues with other specialized APIs.

What NAPALM does is hiding this layer unifying the way we access a networking device, regardless who built it.

napalm

Back to few days ago, NAPALM supported the following network OS:

  • Arista EOS
  • Juniper JunOS
  • Cisco IOS-XR
  • Cisco NX-OS
  • Fortinet FortiOS
  • IBM OS

This is possible thanks to the introduction of the NetworkDriver concept. Every time we want to interact with a device, we can only specify what OS we are going to talk to and NAPALM will select the correct NetworkDriver (basically, a library with all the functions related to that OS).

>>> from napalm import get_network_driver
>>> get_network_driver('eos')
>>>
>>> get_network_driver('iosxr')
>>>
>>> get_network_driver('junos')
>>>
>>> get_network_driver('fortios')

NAPALM will still use third party APIs but this will be trasparent to the user.

Cisco IOS support

Unlike NX-OS, Cisco IOS have no API support. Therefore, it’s not that straightforward to obtain structured data from it and at first NAPALM didn’t support it. So I thought this could be a nice spot to play 🙂

I forked the main repository and started to code.

IOSDriver

Since no native API exists, I had to use something more general: netmiko. This is a pretty sweet Python library making it super easy to connect and interact with networking devices. Once a command is sent, netmiko can give me back the output and then I can start to filter and parse it.

The module is composed by 12 methods:

  • open(): opens the connection with the remote device. It is the first method to be used.
  • close(): closes the connection with the remote device.
  • load_merge_candidate(filename, config): loads a candidate configuration from a textfile or a configuration string. If both are passed, filename is picked. At this point no configuration is pushed to the device yet.
  • compare_config(): simply shows the list of commands proposed by the load_merge_candidate method and ready to be executed if commited.
  • discard_config(): what if we notice some errors after the compare_config? We can discard the proposed changes using this method.
  • commit_commit(): pushes the configuration from load_merge_candidate and saves the configuration.
  • rollback(): we can rollback the commited changes using this method. This simply adds the no keyword to commands (anyway it’s smart enough to recognize parent/child commands)
  • get_lldp_neighbors(): extracts lldp neighbors information from the device.
  • get_facts(): extracts information like uptime, vendor, os_version, serial_number, model, hostname, fqdn, and interface_list.
  • get_interfaces(): extracts information about interfaces including status and speed.
  • get_bgp_neighbors(): extracts information about BGP neighbors.
  • get_interfaces_counters(): extracts information about counters.

Demo

Now let’s see an example of how to use NAPALM.

The first thing we’re gonna do is to connect to our remote device specifying the OS type, username, password and IP address.


from netmiko import ConnectHandler
from napalm import get_network_driver
get_network_driver('ios')
driver = get_network_driver('ios')
device = driver('172.16.1.1', 'gabriele', 'gabriele')
device.open()

Once we’re done, if everything went fine we’ll see the Python interactive shell confirming the SSH session has been established.

python1

Now we can start to interact with our device. Let’s ask for some facts, for example:

python2

As we can see, it’s a Cisco 3640 device whose IOS version is 12.4(16). It has 3 interfaces and its uptime value is set to 9 minutes.

Cool, right? 😀

Let’s try some other methods:

python3

Earlier we discovered 3 interfaces exist in our device. Now we’ve just obtained some specific information about them using the get_interfaces() as well as BGP neighbors information thanks to get_bgp_neighbors().

Let’s see how NAPALM can help us with configuration management. Imagine we want to implement OSPF on our network. Just to keep it simple, we want to push the following configuration:


router ospf 1
!
network 0.0.0.0 0.0.0.0 area 1
!

Using load_merge_candidate(filename=new_good.conf), we’ll load our configuration (assuming new_good.conf is the textfile containing the above config). Then, we can see what changes would be implemented using compare_config(). At that point, we can decide to either commit or discard these changes.

Here we’ve sudden realized our OSPF area should be and not 1. So we decide to discard the candidate configuration with discard_config(). We can use compare_config() to confirm that every possible change’s been discarded.

python4

Since our last compare_config() doesn’t show anything, it means everything went fine.

Anyway, we still want to implement OSPF, so we fix the configuration and give it another try. This time we want to use a configuration string instead of a .conf file. We do this with load_merge_candidate(config=’router ospf 1\nnetwork 0.0.0.0 0.0.0.0 area 0′). Then, if we are happy with it, we can commit.

python5

..and this is how the router’s config look like:

python6

At this point, if we want to rollback the change we can simply use rollback().

Conclusion

It’s been lot of fun to work on this patch and I’m happy to announce that now NAPALM supports IOS too since my PullRequest has been merged to the main repo 🙂

NAPALM is a really cool project and it’s popular among NetOps community and it’s also been presented at a NANOG conference. Here you can find the video from the awesome guys who actually designed and implemented it. Enjoy 🙂


 

P.S.

If you’re interested about NAPALM or Network Automation in general you should definitely join the SLACK channel at network.toCode(). Here you’ll find lots of cool guys discussing fancy stuff on networking 🙂

 

Enabling Network Automation using NTC-Ansible

A couple of months ago I wrote about Ansible and how it can enhance network automation. I also did a (very) little multi-vendor lab to show an example of what can be achieved with this tool.

Today I’m gonna write about an interesting project called ntc-ansible which exploits Ansible awesomeness into networks.

NTC-ANSIBLE

It’s an Open Source project launched by Jason Edelman (LinkedIn page, Blog) and his new-founded company Network to Code (it couldn’t have a better name 😀 ). You can find and download it here. It’s mainly composed by 2 module: ntc_show_commands and ntc_config_commands.

ntc_show_commands

The problem is this: most of the network equipments, doesn’t matter the vendor who made them, don’t return structured data like XML or JSON back from show commands but just simple text. This can be painful if you want to automate some task, like gaining devices’ inventory information. A possible solution could be writing a Python module that would connect to your devices using SSH API, execute commands, retrieve output and, finally, parse the output using Regular Expressions. This is OK, but it’s a lenghty and not always simple procedure.

Ntc-ansible want to simplify all the above.

“ntc_show_commans is a multi-vendor module that can automate converting raw text from show commands into structured data, namely JSON.”

How? This module exploits another interesting tool called TextFSM which is a “Python module that implements a template based state machine for parsing semi-formatted text.” Basically, it takes two inputs, a raw file containing a show command output and a well defined template, and it returns a list of records that contains the data parsed from the text.

So, using ntc-ansible’s ntc_show_commands module we can write a simple Ansible playbook to obtain structured data from network devices in an easy way. Then, we can also use those data to do whatever we want (we’ll see an example of this later on this post).

Example:


- ntc_show_command:
    connection:ssh
    platform:cisco_nxos
    command:'show vlan'
    host:{{ inventory_hostname }}
    username:{{ username }}
    password:{{ password }}

ntc_config_commands

This module enables us to write commands to devices that don’t have API. This can be done in two ways: specifying a list of commands or passing a file containing the commands to be executed. This is an useful module and later I’ll show an example of how this can help to do some auto-remediation.

Example:

# write vlan data
- ntc_config_command:
    connection: ssh
    platform: cisco_nxos
    commands:
      - vlan 10
      - name vlan_10
      - end
    host: "{{ inventory_hostname }}"
    username: "{{ username }}"
    password: "{{ password }}"
    secret: "{{ secret }}"

# write config from file
- ntc_config_command:
    connection: ssh
    platform: cisco_nxos
    commands_file: "dynamically_created_config.txt"
    host: "{{ inventory_hostname }}"
    username: "{{ username }}"
    password: "{{ password }}"
    secret: "{{ secret }}"

Auto-Remediation with NTC-Ansible

The above introduction is far to be enough to let you understand how this tool really works and what it really does. So, in this section we’ll move together across all the steps needed to implement a simple auto-remediation.

Scenario

A common routine task is to take a network node offline gracefully. This can be done prepending AS_PATH in BGP. Let’s imagine something happened into our network requiring us to drain traffic from a device (like a fan problem or something else). We’ll exploit ntc-ansible to write an Ansible playbook to achieve it (this can also be automatically triggered to be fully automated, but let’s focus on the playbook for now).

A common BGP prepending configuration looks like this (actually this is the one I have configured on my virtual lab):


route-map prepend permit 10
set as-path prepend last-as 3

router bgp 10
neighbor 172.16.2.2 remote-as 20
neighbor 172.16.2.2 route-map prepend out
neighbor 172.16.3.2 remote-as 30
neighbor 172.16.3.2 route-map prepend out

So, we’ll need to write a template extracting BGP neighbors information (from the show ip bgp summary command). We’ll achieve this using the ntc_show_commands module. Then, we’ll need to generate the proper configuration and to push it to the device using the ntc_config_commands module.

But first, we have to write a template for our show command.

TextFSM Template

The template file consists of two top level sections.

  • The Value definitions, which describe the columns of data to extract.
  • One or more State definitions, describing the various states of the engine whilst parsing data. A set of rules is defined in order to perform the parsing. The first and mandatory State is Start.

You can find a great guide on how to write one here.

My show ip bgp summary output looks like this:

BGP router identifier 1.1.1.1, local AS number 10
BGP table version is 21, main routing table version 21
7 network entries using 819 bytes of memory
8 path entries using 416 bytes of memory
3/2 BGP path/bestpath attribute entries using 372 bytes of memory
1 BGP AS-PATH entries using 24 bytes of memory
0 BGP route-map cache entries using 0 bytes of memory
0 BGP filter-list cache entries using 0 bytes of memory
BGP using 1631 total bytes of memory
BGP activity 9/2 prefixes, 13/5 paths, scan interval 60 secs

Neighbor      V   AS   MsgRcvd   MsgSent   TblVer  InQ OutQ    Up/Down State/PfxRcd
172.16.2.2   4   20       297             435          21       0      0        06:48:44         3
172.16.3.2   4   30       409             414          21       0      0        06:45:40         0

I have highlighted the field I’m interested in. These are my Values. Now I need to define RegEx for them.


Value BGP_NEIGH (\d+\.\d+\.\d+\.\d+)
Value NEIGH_AS (\d+)
Value STATE_PFXRCD (\S+)

Start
^${BGP_NEIGH}\s+\S+\s+${NEIGH_AS}.*\s\d+:\d+:\d+\s+${STATE_PFXRCD} -> Record

Regular Expressions on Value lines are used to define what we are interested in, while those inside the State are used to find the defined pattern inside each output line. We’ll save this new template inside the ./ntc_templates directory. This directory contains all the commands supported by the ntc_show_commands module as well as an index file with a list of them. Each line of this file is in this format:

cisco_ios_show_ip_bgp_summary.template, .*, cisco_ios, sh[[ow]] ip bgp sum[[mary]]

The highlited RegEx is used to select the rigth template based on the chosen command inside the playbook.

Playbooks

Using this template I’ll have a table with the selected values. Now I can make a first test with ntc_show_commands


---

- name: GET STRUCTURED DATA BACK FROM CLI DEVICES
  hosts: cisco_ios
  connection: local
  gather_facts: False

  tasks:
    - name: TEST TEMPLATE
      ntc_show_command:
        connection: ssh                   
        platform: cisco_ios               
        command: "show ip bgp summary"   
        host: "{{ inventory_hostname }}"  
        username: "{{ username }}"
        password: "{{ password }}"
      register: results

    - debug: var=results.response

On the same directory I have an inventory file looking like this:

[cisco_ios]
172.16.1.1 username=cisco password=cisco secret=cisco

[cisco_ios:vars]
as = 10

If you are familiar with Ansible all the fields are self-explanatory. Running this we’ll obtain the following:

ansible1

As we can see, the command has been correctly executed and data is well parsed in a JSON-like manner. Now we can decide to use these data to do whatever we want.

Our goal is to drain traffic from the device making it less prefered through BGP AS prepending. So, we need to configure AS prependig for every BGP neighbor. Thanks to ntc_show_commands now we know who BGP neighbors are and we can use this knowledge to build the proper configuration.

We write two other task in order to do so:


    - name: BUILDING CONFIGURATION
      lineinfile: dest=./drain.config line="router bgp {{ as }}\nneighbor {{ item['bgp_neigh'] }} route-map prepend out" state=present create=True
      with_items:
        - "{{ results.response }}"

    - name: ADDING ROUTE-MAP
      lineinfile: dest=./drain.config line="route-map prepend permit 10\nset as-path prepend last-as 3"

The first one builds the actual BGP configuration and store it inside the drain.config file. The state flagsetted to present, and the create flag, setted to True, let the playbook create a new file if it does not exist yet. Let’s run the expanded playbook now:

Senza titolo-7

If the playbook works fine, the drain.config file will appear like this:

router bgp 10
neighbor 172.16.2.2 route-map prepend out
router bgp 10
neighbor 172.16.3.2 route-map prepend out

route-map prepend permit 10
set as-path prepend last-as 3

At this point we write the ntc_config_commands task to configure our device:


    - name: PUSH CONFIGURATION
      ntc_config_command:
        connection: ssh
        platform: cisco_ios
        commands:
          - router bgp "{{ as }}"
        commands_file: "./drain.config"
        host: "{{ inventory_hostname }}"
        username: "{{ username }}"
        password: "{{ password }}"
        secret: "{{ secret }}"

..and now let’s run it again..

Senza titolo-9

As the screen shows, everything has been successful and we can verify it from a show ip bgp on a neighboring device (of course we could do this inside the playbook itself writing another task. Ignore BGP configuration, I’ve just built a sample lab to show ntc-ansible usage).

Senza titolo-4

..and inside red circles we can see the effect of our run.

Conclusions

This post wanted to be a practical introduction to ntc-ansible and a way to let me do some practice with its modules. I think this project to be really interesting and it’s also receiving lot of good feedbacks from people who are already using it in their environments.

Senza titolo-2

If you find interesting too, you can contribute writing more templates. I did it and I’m doing it right now 🙂