Having the privilege to maintain one of the widely used tools for network automation, NAPALM, I am often asked various questions, many of them related to the extensibility of the library. It is a given, and a design choice that NAPALM does not and cannot possibly fulfill all the existing scenarios and features when interacting with the network device - either retrieving or pushing data.

Some of the requests we receive do make sense to be implemented into the NAPALM core library, however we are also seeing a large number that would solve a very particular need, or subset. I am also one of the people that are using NAPALM and had this sort of need to implement a niche requirement or a very specific business logic that would definitely not have its place into the public library.

At the beginning, I started extending NAPALM into a custom library, by subclassing NAPALM’s native classes (i.e., JunOSDriver, EOSDriver, etc.) and extending them by adding more methods I needed. While this approach was easy enough, after writing the code, there were two additional steps required: 1. release a new version of this custom library, and 2. install it. While both are easy enough in theory, when releasing code often (several times a day) it can get tedious, especially when you have to mess with Python’s poor packaging system, and, on top of that, it is little point to do so when using Salt which provides mature ways of spreading out new code, out of the box. Salt’s native filesystem can be used to release new code in the form of Execution Modules. In the latest Salt release, 2019.2.0 - codename Fluorine, there are many new features that expose the low level API that NAPALM is using (and beyond that), which you can invoke from your own custom modules to implement the business logic you need. The implementation is much easier that it sounds. Check out the release notes to before going any further.

Note: If you are not running 2019.2.0 yet, don’t worry, you are covered: all you have to do in order to follow when I’m describing in the steps below, is to copy the files directly from GitHub. For example, one of the files required is napalm_mod.py. To be able to use this module with older Salt releases, simply copy this file into one of your file_roots directory. For example, if one of the file_roots directories is /etc/salt/extmods, then you only have to execute, e.g.,

$ cd /etc/salt/extmods
$ mkdir _modules && cd _modules/
$ wget https://raw.githubusercontent.com/saltstack/salt/2019.2/salt/modules/napalm_mod.py

Remember to always download the file using the raw link from GitHub, otherwise you’ll download an HTML document instead of a Python module. :-)

The same goes with any other module type to port features from 2019.2.0 and use them in older releases. Check out the list of features for network automation available with this release: Salt 2019.2.0 release notes.

To make it easier, I have added them in the napalm-salt GitHub repository, under the fluorine directory, so you can simply clone the repo and add the path to your file_roots.

Additionally, if you want to quickly have a play with these new features, you might want to take a look at the napalmautomation/salt-master Docker image, currently offering two versions: 2018.3.3-fluorine providing the ports for the Fluorine release (2019.2.0), based on the 2018.3.3 Salt stable code, and 2017.7.8-fluorine based on the 2017.7.8 stable release, respectively. For example, if you want to run the 2017.7.8 release armed with the modules available in 2019.2.0, you’d execute, e.g., $ docker run -it napalmautomation/salt-master:2017.7.8-fluorine.

These features in the latest Salt release provide low level interaction with the network device, by exposing you directly the available API (if any, otherwise you can use screen scraping via the Netmiko library). Using these, you can very easily extend Salt’s capabilities for your own needs, but writing a small Execution Module. That said, let’s take a look at how to write Execution Modules in your own environment.

Writing Execution Modules is easy

I borrwoed this title from the official Salt documentation for writing Execution Modules. This document explains very well this subject, and covers many aspects that you may need sooner or later. Please take a moment and skim through it, then re-read it thoroughly later. I wouldn’t have much to add to it, however I would like to insist a little on the aspect of the file_roots which is often neglected or incompletely understood.

I will start by linking to the Salt file server documentation, and I will speak more specific in the context of Execution Modules. Say you have the following configuration on the Master (this is what I prefer to use):

file_roots:
  - /etc/salt
  - /etc/salt/states

Salt is going to load your custom Execution Modules from both /etc/salt/_modules and /etc/salt/states/_modules when available. To keep it clear, I put my modules in /etc/salt/_modules only, using the other path for States only.

Remember this structure, as this is what I’m going to refer to within this post.

Your first custom module

Let’s start with something easy. Say we define a function that always returns boolean True. In a file, say /etc/salt/_modules/example.py define a simple Python function:

/etc/salt/_modules/example.py

def first():
    return True

As you can notice, the code is linear, and similar to writing an arbitrary Python scripts without any specifics.

After loading the module by executing saltutil.sync_modules, you can go ahead an invoke the newly defined function:

$ salt 'your-device' example.first
your-device:
    True

Cross-calling Executon Modules

This section is already well detailed in this section of the documentation, but I wanted to make sure you’re not going to skip it. I also wanted to reassure you that you can equally cross-call native functions, as well as custom functions you define in your environment.

As an example, let’s define two functions, say example.second and example.third that call the native test.false Execution Function, and the example.first function defined above:

/etc/salt/_modules/example.py

def second():
    return __salt__['test.false']()


def third():
    return __salt__['example.first']()

Again, after reloading the modules by executing saltutil.sync_modules, you can execute from the CLI:

$ salt 'your-device' example.second
your-device:
    False
$ salt 'your-device' example.third
your-device:
    True

Remember: always use the open-close braces ({ and )) to properly invoke the function – __salt__ is a mapping to all the available functions Salt is aware of, each element pointing to the memory address where the function is available for execution. Therefore, it is important to have the braces even though you may not pass any arguments to the function, as in the previous examples.

Salt functions for low-level API calls

The modules newly added in the 2019.2 Salt release, provide direct API call for the following platforms:

  • Arista, over eAPI, via the pyEAPI Python library.
  • Cisco Nexus, over the NX-API (direct HTTP request)
  • Junos, over NETCONF, through the existing junos module.
  • A variety of platforms, using screen-scraping over either SSH or Telnet (i.e., issue CLI commands and return the text output, as you’d normally have on your terminal when executing the command(s) manually). You can find the complete list of supported platforms on GitHub.

    While this is not encouraged usage, it is sometimes the only available choice, particularly on platforms that fail to offer a (proper) or consistent API, but not limited to: for example, on Junos (and others) that have high coverage over the API for the available commands, there are cases when some are not currently supported. As an example, on Junos you cannot gather the MTR results to a destination over NETCONF; instead, using Netmiko you can collect these very easily by simply issuing the CLI command you’d normally use.

    This module might go very well together with the TextFSM Execution Module added in the 2018.3.0 release, that allows raw text parsing though Google’s TextFSM Python library, and exposing Salt’s simplicity for managing the templates in the local or remote environment. See the documentation for more details and usage examples.

For NAPALM users, the following functions have been added to make API calls without any further effort: once the modules are available, you can start using:

  • Junos:
    • napalm.junos_rpc: execute RPC calls and return the output as Python object.
    • napalm.junos_cli: execute CLI commands and return the output as either text or Python object.
    • napalm.junos_commit: for more flexible options when committing the configuration on Junos, that NAPALM doesn’t currently have, e.g., commit confirmed, synchronise, force sync, etc.
  • Arista:
  • Cisco Nexus:
  • Any platform supported by Netmiko (including the ones above):
    • napalm.netmiko_commands: send one or more CLI commands to be executed on the device, and return the output as plain text.
    • napalm.netmiko_config: Load one or more configuration commands on the network device. The commands can equally be loaded from a local or remote path, and passed through Salt’s template rendering pipeline (by default using Jinja as the template rendering engine).

Let’s take a look at some examples, from the command line:

napalm.junos_cli

Let’s gather the RPKI statistics using the Juniper CLI command show validation statistics as we would execute manually from the CLI of the router:

$ salt 'juniper-router' napalm.junos_cli 'show validation statistics'
juniper-router:
    ----------
    message:
        
        Total RV records: 78242
        Total Replication RV records: 78242
          Prefix entries: 73193
          Origin-AS entries: 78242
        Memory utilization: 15058371 bytes
        Policy origin-validation requests: 0
          Valid: 0
          Invalid: 0
          Unknown: 0
        BGP import policy reevaluation notifications: 1925885
          inet.0, 1763165
          inet6.0, 162720
    out:
        True

The output returned is plain text, as you’d normally get when executing manually directly on the device.

The napalm.junos_cli command is smart enough to execute RPC requests even though you’re invoking via the CLI command, by passing the format=xml argument, e.g,

$ salt 'juniper-router' napalm.junos_cli 'show validation statistics' format=xml
juniper-router:
    ----------
    message:
        ----------
        rv-statistics-information:
            ----------
            rv-statistics:
                ----------
                rv-bgp-import-policy-reevaluations:
                    1925885
                rv-bgp-import-policy-rib-name:
                    - inet.0
                    - inet6.0
                rv-bgp-import-policy-rib-reevaluations:
                    - 1763165
                    - 162720
                rv-memory-utilization:
                    15058761
                rv-origin-as-count:
                    78244
                rv-policy-origin-validation-requests:
                    0
                rv-policy-origin-validation-results-invalid:
                    0
                rv-policy-origin-validation-results-unknown:
                    0
                rv-policy-origin-validation-results-valid:
                    0
                rv-prefix-count:
                    73195
                rv-record-count:
                    78244
                rv-replication-record-count:
                    78244
    out:
        True

In this case, the output returned by napalm.junos_cli is a Python dictionary. If you’re not familiar with Salt yet, to convince yourself, let’s take a look at the raw object (the above is a visual display of the same, for a more human readable output):

$ salt 'juniper-router' napalm.junos_cli 'show validation statistics' format=xml --out=raw
{'juniper-router': {'message': {'rv-statistics-information': {'rv-statistics': {'rv-bgp-import-policy-rib-reevaluations': ['1763165', '162720'], 'rv-policy-origin-validation-requests': '0', 'rv-policy-origin-validation-results-valid': '0', 'rv-origin-as-count': '78244', 'rv-memory-utilization': '15058761', 'rv-prefix-count': '73195', 'rv-record-count': '78244', 'rv-policy-origin-validation-results-unknown': '0', 'rv-bgp-import-policy-rib-name': ['inet.0', 'inet6.0'], 'rv-replication-record-count': '78244', 'rv-bgp-import-policy-reevaluations': '1925885', 'rv-policy-origin-validation-results-invalid': '0'}}}, 'out': True}}

napalm.junos_rpc

While the CLI command is easier to use (and more readable / self-explanatory), it is often better to invoke the RPC as that is the same cross-platform / cross-version, while the CLI command is not guaranteed to be consistent. To find out the RPC request to use, from the CLI of your router you can append | display xml rpc to the usual command, e.g.,

[email protected]> show validation statistics | display xml rpc 
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/18.1R3/junos">
    <rpc>
        <get-validation-statistics-information>
        </get-validation-statistics-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

The RPC in this case is get-validation-statistics-information, and this is what we’re going to pass to the napalm.junos_rpc function:

$ salt 'juniper-router' napalm.junos_rpc get-validation-statistics-information
juniper-router:
    ----------
    comment:
    out:
        ----------
        rv-statistics-information:
            ----------
            rv-statistics:
                ----------
                rv-bgp-import-policy-reevaluations:
                    1925885
                rv-bgp-import-policy-rib-name:
                    - inet.0
                    - inet6.0
                rv-bgp-import-policy-rib-reevaluations:
                    - 1763165
                    - 162720
                rv-memory-utilization:
                    15058761
                rv-origin-as-count:
                    78244
                rv-policy-origin-validation-requests:
                    0
                rv-policy-origin-validation-results-invalid:
                    0
                rv-policy-origin-validation-results-unknown:
                    0
                rv-policy-origin-validation-results-valid:
                    0
                rv-prefix-count:
                    73195
                rv-record-count:
                    78244
                rv-replication-record-count:
                    78244
    result:
        True

napalm.pyeapi_run_commands

In a similar way, we can invoke show-like commands on Arista switches, and the napalm.pyeapi_run_commands accepts one or more commands to be executed, by default returning the output as Python object, e.g.,

$ salt 'arista-switch' napalm.pyeapi_run_commands 'show version'
arista-switch:
    |_
      ----------
      architecture:
          i386
      bootupTimestamp:
          1534844216.0
      hardwareRevision:
          11.03
      internalBuildId:
          5c08e74b-ab2b-49fa-bde3-ef7238e2e1ca
      internalVersion:
          4.20.8M-9384033.4208M
      isIntlVersion:
          False
      uptime:
          18767827.61
      version:
          4.20.8M

To execute more than one operational command, pass them sequentially, e.g., salt 'arista-switch' napalm.pyeapi_run_commands 'show lldp neighbors' 'bash timeout 10 ping 1.1.1.1 -c 5'.

To receive the output as text (instead of Python object), set the argument encoding as text, e.g., salt 'arista-switch napalm.pyeapi_run_commands 'show version' encoding=text.

napalm.netmiko_commands

I’m going to skip the other functions mentioned earlier and focus on the Netmiko functions for a little. Firstly, let’s take a look at some examples when using Netmiko through the napalm Salt module, even when working with platforms already covered and provide good API support (though not 100% complete), such as Arista, e.g.,

$ salt 'arista-switch' napalm.netmiko_commands 'show version'
arista-switch:
    - Hardware version:    11.03
      
      Software image version: 4.20.8M
      Architecture:           i386
      Internal build version: 4.20.8M-9384033.4208M
      Internal build ID:      5c08e74b-ab2b-49fa-bde3-ef7238e2e1ca
      
      Uptime:                 31 weeks, 0 days, 5 hours and 28 minutes

Which this usage may be superfluous in this particular case, this function can be very handy when you require the output of a specific command, that is not available over the API. For example, on Junos platforms I needed to collect MTR results automatically, but the traceroute monitor command is not available over NETCONF. So here’s how napalm.netmiko_commands can save the day:

$ salt 'juniper-router' napalm.netmiko_commands 'traceroute monitor 1.1.1.1 summary'
juniper-router:
    - 
      HOST: juniper-router              Loss%   Snt   Last   Avg  Best  Wrst StDev
        1. one.one.one.one               0.0%    10    0.6   1.0   0.5   4.2   1.1

While on platforms that provide good API support such as Junos or Arista napalm.netmiko_commands is only used in corner cases, it’s one of the few options you can have when working with operating systems such as Cisco IOS, IOS-XR, and many others.

In a similar way to napalm.pyeapi_run_commands, you can pass multiple CLI commands to execute and collect their text output.

As a side note, in order to process that output further you might prefer using the TextFSM module natively integrated into Salt starting with release 2018.3.0, which is also very easy to use.

napalm.netmiko_config

This function can help you send configuration commands to the targeted device, through Netmiko (thus over SSH). You can equally load one or more configuration commands, and it reproduces the behaviour of an user that would normally be configuring the device manually, e.g.,

$ sudo salt 'arista-swtich' napalm.netmiko_config 'ntp server 10.10.10.1'
arista-swtich:
    config term
    arista-swtich(config)#ntp server 10.10.10.1
    arista-swtich(config)#end
    arista-swtich#

One particular note to keep in mind with this command is that on platforms such as Juniper, where you need to commit the configuration, and transfer the changes into the running config, you will need to explicitly pass the argument commit=True, otherwise it’s going to discard the changes. It is always a good practice to use the commit argument whenever you are absolutely confident that the changes are correct:

$ salt 'juniper-router' napalm.netmiko_config 'set system ntp server 10.10.10.1'
juniper-router:
    configure 
    Entering configuration mode
    
    [edit]
    [email protected]# set system ntp server 10.10.10.1 
    
    [edit]
    [email protected]# exit configuration-mode 
    The configuration has been changed but not committed
    Exiting configuration mode
    
    [email protected]> 

The correct usage for the above would be:

$ salt 'juniper-router' napalm.netmiko_config 'set system ntp server 10.10.10.1' commit=True
juniper-router:
    configure 
    Entering configuration mode
    The configuration has been changed but not committed
    
    [edit]
    [email protected]# set system ntp server 10.10.10.1 
    
    [edit]
    [email protected]# commit 
    commit complete
    
    [edit]
    [email protected]#

That said, I am not aware of any use case where you’d require Netmiko for configuration management on Juniper platforms, but I wanted to share this however, as Netmiko covers a large variety of platforms, and you might find this helpful at some point.

Calling low-level API functions from your custom functions

To be able to fully understand this section, I would need you to make sure the previous paragraphs are entirely clear to you.

Looking again at the Cross-calling Executon Modules paragraph, you can invoke any of the functions previously presented, from your custom module. For example, let’s start with something very simple and write a function that returns the operating system version. For an Arista switch, the code would be as simple as:

/etc/salt/_modules/example.py

def version():
    res = __salt__['napalm.pyeapi_run_commands'](show version)
    return res[0]['version']

As napalm.pyeapi_run_commands can execute one or more commands, the output is a list, which is why you need to do a lookup at index 0, then return the operating system version under the version key. To check the exact structure and know exactly what keys to lookup for, I always execute the equivalent CLI command checking the raw Python output:

$ salt 'arista-switch' napalm.pyeapi_run_commands 'show version' --out=raw
{'arista-switch': [{'uptime': 6776189.66, 'internalVersion': '4.20.8M-9384033.4208M', 'architecture': 'i386', 'bootupTimestamp': 1534844216.0, 'version': '4.20.8M', 'isIntlVersion': False, 'internalBuildId': '5c08e74b-ab2b-49fa-bde3-ef7238e2e1ca', 'hardwareRevision': '11.03'}]}

Note: The example above has been crafted for exemplification purpose only, to be easier to be understood, and focus on how to achieve something very easy. In this particular case, if you need to check the running version, you can this information into th Grains, and access it from a module as __grains__['version']. The example stands however when you would need to gather other pieces of information from the network device, that are not present into the Grains.

Once saved and synchronised, you can execute as:

$ salt 'arista-switch' example.version
arista-switch:
    4.20.8M

Cross-platform implementation

The features presented work independently for every platform, and their output is similarly different (as the network device presents it, over the API of choice). Now, in order to preserve one of the most important advantages of NAPALM, you have to abstract away the functionality, so the end users are able to use the exact syntax regardless of the platform they are targeting. There are two good ways do do so, and it mainly depends on personal preference which one you prefer.

Say we continue the previous example, and write a function that returns the operating system version, while preserving the same syntax when invoking the function. In both cases, the key is using the os Grain which tells you what operating system are you executing against.

Abstract away inside the function

I’ll firstly provide the code example, then explain below:

/etc/salt/_modules/example.py

def version():
    if __grains__['os'] == 'junos':
        ret = __salt__['napalm.junos_cli']('show version', format='xml')
        return ret['message']['software-information']['junos-version']
    elif __grains__['os'] == 'eos':
        ret = __salt__['napalm.pyeapi_run_commands']('show version')
        return ret[0]['version']
    elif __grains__['os'] == 'nxos':
        ret = __salt__['napalm.nxos_api_rpc']('show version')
        return ret[0]['result']['body']['sys_ver_str']
    return 'Not supported on this platform'

What the code does, is simply checking the operating system name, by inspecting the os Grain, and then invoke the appropriate Salt function. This way, when the function is executed against a Juniper device, it’s going to cross-call napalm.junos_cli then do the lookup under the message, software-information, and junos-version keys (do check the raw output to make sure you understand why), while against an Arista switch, it’s invoking napalm.pyeapi_run_commands as explained previously.

Separate modules per platform

While in the above the logic per platform is implemented inside the same function, when we have to put a lot more business logic, the code may get too complex or harder to follow (arguably, again, depends on personal preference).

The alternative is splitting the code into separate files, one per platform. This is using Salt’s feature for registering different physical modules under the same virtual name. Check out the documentation for virtual modules.

The core idea is that you create one file for each platform, and declare the same virtual name. Say we want to continue using the module name from the previous examples, example, then the files would be called example_junos.py, example_eos.py, and example_nxos.py. Note, you can choose whatever filename you may prefer, the platform availability is made inside the __virtual__ function:

/etc/salt/_modules/example_junos.py

def __virtual__():
    if __grains__['os'] == 'junos':
        return 'example'
    else:
        return (False, 'Not loading this module, as this is not a Junos device')


def version():
    ret = __salt__['napalm.junos_cli']('show version', format='xml')
    return ret['message']['software-information']['junos-version']

The __virtual__ function returns example (which is the virtual name of the module, i.e., the name you’re going to use on the CLI, or other Execution Modules), but only when the os Grain is junos, otherwise, the code from example_junos.py will not be loaded (and it may load the code from a different physical module instead). Let’s take a look at the equivalent module for eos:

/etc/salt/_modules/example_eos.py

def __virtual__():
    if __grains__['os'] == 'eos':
        return 'example'
    else:
        return (False, 'Not loading this module, as this is not an Arista switch')


def version():
    ret = __salt__['napalm.pyeapi_run_commands']('show version')
    return ret[0]['version']

Both options are good, you can use whichever you feel more comfortable to with. I use both, depending on the complexity of the code.

Happy hacking

Using this methodology, I’ve been able to significantly speed up the development process, and make it more enjoyable at the same time. It goes without saying that you would still be able to help the community with your contributions by submitting the modules to the official Salt codebse so they will be available in the next Salt release, or into the salt-contrib repository, napalm-salt, or even your own repository (and please let everyone know – if you prefer, let me know, and I would be happy spread the word for awareness).

Beyond the modules I have expanded on, you also have an army of few other features such as the ciscoconfparse module for the ciscoconfparse Python library, iosconfig for transforming Cisco IOS-like text-based configuration style into Python structures to be easier to work with. These are similarly integrated within the napalm Execution Module for ease of use – see for example napalm.config_filter_lines, napalm.config_tree, or napalm.scp_put and napalm.scp_get which can help you implement the business logic you need. In the end, I’d also like to invite you to take a look at the PeeringDB module, as well as the module for NetBox, which now has many more features available. I hope it’s pretty clear that now it is easier than ever to get started to automate and write code for your network, by simply reusing and invoking features you have at your fingertips.

If you have a story to share in this direction, I’d love to hear it, and learn from your experience.