The Overview

Recently I had to provide the ability to validate network state on a specific environment. While this offered an excellent opportunity to use NAPALMs' validation features, it also became an exercise in recursion. The validation with NAPALM is relatively straightforward, but the python dictionary returned is complex and of an unknown origin. To display the dictionary without coding out every single use case, I wrote a recursive function to do it for me.

The Topology

The topology is pretty simple with three routers where router-A connects to both router-B and router-C. If you are interested in the configuration, you can reference the previous blog post here.

Topology

The Objective

The overall goal is to validate the state of the network and effectively communicate the results of that validation to the user.

The Script

First, we’ll want to import the get_network driver from NAPALM.

from napalm import get_network_driver

We will then import some more utility modules.

from colors import bcolors
from os.path import exists
from os import system, name

Now let’s define some utility functions. The clear() function will clear the CLI screen in case there is any stale output still there. We leverage the system module to determine if we are running the script on a nix or windows based environment.

def clear():
    if name == 'nt':
        _ = system('cls')
    else:
        _ = system('clear')

The following utility function will print our test results in a more readable format, with special Unicode characters.

def print_result(test_name, test_result, level):
    if test_result:
        print((" " * level) + u'\u2705' + " - " + test_name)
    else:
        print((" " * level) + u'\u274c' + " - " + test_name)

The next function is the real “meat and potatoes” of the entire compliance script. It is a recursive function that will allow us to move through the JSON blob returned by the NAPALM compliance_report() method.

def display_compliance_info(output, level=0):
    keywords = ['extra', 'missing', 'skipped', 'complies', 'present', 'diff']
    for check, value in output.items():
        if check not in keywords:
            print_result(check, value['complies'], level)
            if 'present' in value.keys():
                display_compliance_info(value['present'], level+2)
            elif 'diff' in value.keys():
                if len(value['diff']['missing']) > 0:
                    for missing_value in value['diff']['missing']:
                        print_result(missing_value, False, level+2)
                display_compliance_info(value['diff']['present'], level+2)

To better understand what this function does, let’s first look at the output that we receive from NAPALM.

{
  "get_facts": {
    "complies": true,
    "present": { "hostname": { "complies": true, "nested": false } },
    "missing": [],
    "extra": []
  },
  "get_interfaces_ip": {
    "complies": true,
    "present": {
      "Ethernet1": { "complies": true, "nested": true },
      "Ethernet2": { "complies": true, "nested": true }
    },
    "missing": [],
    "extra": []
  },
  "get_bgp_neighbors": {
    "complies": false,
    "present": {
      "global": {
        "diff": {
          "complies": false,
          "present": {
            "router_id": { "complies": true, "nested": false },
            "peers": {
              "diff": {
                "complies": false,
                "present": {
                  "192.168.0.2": { "complies": true, "nested": true }
                },
                "missing": ["10.0.0.1"],
                "extra": ["10.0.0.2"]
              },
              "complies": false,
              "nested": true
            }
          },
          "missing": [],
          "extra": []
        },
        "complies": false,
        "nested": true
      }
    },
    "missing": [],
    "extra": []
  },
  "skipped": [],
  "complies": false
}


As we can see, the dictionary can be reasonably complex and of an unknown depth. Because of those two potential items, recursion will be the best way to tackle the iteration through this function. The general premise of the function is to loop through each initial level of the dictionary. In the event of a unique key (get_facts, get_interface_ip, get_bgp_neighbors, global, etc.), we print the value of the complies key, which represents the overall result for the section. If we see the key “present,” we know there is another section to the output, so we call the function again, passing it the value of present, and do it all over again. We do the same thing if we see a non-empty value in the “missing” value of the “diff” section.

The result of this function allows us to do two things.

  1. We can keep the code short since we do not have to plan for every possible variation that the NAPALM compliance function returns.
  2. We also don’t have to rewrite the script every time a new check is added to NAPALM. All we need to do is update our check file.

The remaining portion of the script will loop through the defined devices, and for each device it will check to see if a compliance file exists for the device. If the file exists, it will run the compliance_report function and display the output.

def main():
    clear()

    devices = [
        {'name': 'router-A', 'host': '192.168.86.201'},
        {'name': 'router-B', 'host': '192.168.86.202'},
        {'name': 'router-C', 'host': '192.168.86.203'},
    ]

    driver = get_network_driver("eos")

    for device in devices:
        with driver(device['host'], 'admin', 'test') as active_device:
            compliance_file = f"compliance/{device['name']}.yml"
            if exists(compliance_file):
                print(
                    f"Checking {bcolors.HEADER}{device['name']}{bcolors.ENDC} for compliance...")
                result = active_device.compliance_report(compliance_file)
                display_compliance_info(result)
                print('\n')


if __name__ == "__main__":
    main()

The Compliance File

NAPALMs compliance file is pretty elegant. All checks are defined as YAML and pretty self-explanatory. Below, we are checking for three things: the device’s hostname, the interface configuration, and the BGP configuration. If any of these checks fail, the overall parent object also fails.

---
- get_facts:
    hostname: router-A

- get_interfaces_ip:
    Ethernet1:
      ipv4:
        10.0.0.1:
          prefix_length: 30
    Ethernet2:
      ipv4:
        192.168.0.1:
          prefix_length: 30

- get_bgp_neighbors:
    global:
      router_id: 1.1.1.1
      peers:
        _mode: strict
        10.0.0.1:
          is_enabled: true
          is_up: true
        192.168.0.2:
          is_enabled: true
          is_up: true

Output

Below is a screenshot of the output for the three devices. I purposely entered some incorrect information into the compliance file for router-a to see the result of a failed compliance run.

Output

Summary

This little project ended up being more work than I originally anticipated, but it gave me some good practice on recursion. While sometimes it may initially seem easier to just brute force your way through a problem, spending the time upfront on the algorithmic side can save you a lot of work on the backend.

Resources

Code: https://github.com/barryCrunch/lab-automation-demo

Napalm Documentation: https://napalm.readthedocs.io/en/latest/index.html