Creating custom drivers for NAPALM

Intro

Any discussion about network automation today would be incomplete without mentioning NAPALM: a library providing a vendor agnostic interface to network devices. It does so by acting as an abstraction layer, exposing a high-level function to the user while handling the details of accessing (for example via SSH/API/NetConf) and configuring the device in question. Combine it with Ansible and you have at your fingertips a versatile and powerful software stack for all kinds of automation scenarios.

While Ansible (especially in conjunction with napalm_ansible) is a great tool, I found it a little clumsy for data processing. Therefore I was looking for a way to remove more complex business logic from my playbooks in order to keep them clean and maintainable. Searching the NAPALM docs I found the mechanism documented below which I would consider the canonical way to extend existing modules using customer-specific code. This allows you to leverage all of Python for your data processing and keep your playbooks sleek and maintainable.

In the example below we will subclass the IOS driver, thereby inheriting all of its functionality. We’ll also implement a custom method for getting all the blah. Then we verify the functionality by instantiating the driver and calling our method from an Ipython shell. Finally, we will look into getting our driver to work with napalm_ansible.

Setup

Our driver will be located inside its own directory in Python’s site-packages folder. NAPALMs get_network_driver() will try to find our driver there when loading it by name. The resulting directory structure looks like this:

site-packages
+-- napalm
    +-- __init__.py
    +-- ios
    +-- junos
    +-- ...
+-- custom_napalm
    +-- __init__.py
    +-- marsrover.py

Our directory will include an empty __init__.py as well as the source file called marsrover.py. Conveniently, get_network_driver() knows which class to return because it will look members which derive from the base class NetworkDriver.

# marsrover.py

from napalm.ios import IOSDriver

class MarsRoverDriver(IOSDriver):
    def __init__(self, hostname, username, password, timeout=60, optional_args=None):
        super(MarsRoverDriver, self).__init__(hostname. username, password, timeout, optional_args)
    
    def get_all_the_blah(self):
        return ['blah', 'blah', 'blah']
    
    def get_server_ports(self):
        return [ i for i,v in self.get_interfaces() if "Server" in v['description'] ]

Finally, we will be able to load and use the driver:

# ipython shell

In [1]: import napalm
   ...: import getpass
   ...: drv = napalm.get_network_driver('marsrover')
   ...: mrover = drv('198.100.51.123', 'user', getpass.getpass()) 
   ...: mrover.open()
   ...: mrover.get_all_the_blah()

Out[4]: ['blah', 'blah', 'blah']

Ansible

Once the custom driver is ready we can reference it in our Ansible playbooks. Notice how the getter’s result is stored in the hostvars as a key using it’s name prefixed with napalm_.

# demo_playbook.yml
---
- hosts: all
  connection: local

  vars:
    napalm_provider:
      hostname: "{{ ansible_hostname }}"
      username: "username"
      password: "password"
      dev_os:   "marsrover"

  tasks:
    - name: get all the blahs
      napalm_get_facts:
        provider: "{{ napalm_provider }}"
        filter: "all_the_blah"
    
    - debug:
        var: hostvars[inventory_hostname]['napalm_all_the_blah']
# ansible-playbook demo_playbook.yml

PLAY: [all] ***************************************************************************

TASK: [Gathering Factos] **************************************************************
ok: [demohost]

TASK: [get all the blahs] *************************************************************
ok: [demohost]

TASK: [debug] *************************************************************************
ok: [demohost] => {
     "hostvars[inventory_hostname]['napalm_all_the_blah']": [
       "blah",
       "blah",
       "blah"
     ]
}

PLAY RECAP *****************************************************************************
demohost            : ok=3      changed=0       unreachable=0       failed=0