Nornir Framework - Full Tutorial for Beginners

, May 27th 2020

(UPDATE: We now have a video demo to accompany this post! Check it out on YouTube.)

When designing a network automation solution, it is critical to keep scalability as a key priority. How can jobs be executed on a large number of devices in parallel? What about inventory management? One solution is Nornir - an automation framework that uses the Python programming language. Today, let's do a deep dive tutorial. We'll manage a Cisco and Juniper router to show off the power of Nornir.

IF YOU LOVE PYTHON, YOU WILL LOVE NORNIR

Nornir's full integration with Python is one of its greatest highlights. Users will only ever need to write Python code when using the Nornir framework.

This isn't the case for most frameworks. Take Ansible, for example. Playbooks are written with a custom DSL (Domain Specific Language).

IS THE LACK OF A DSL A GOOD THING?

There's going to be pros and cons to every design decision. While a DSL can simplify readability among other benefits, there are advantages in scraping a DSL for a generalized programming language. Here are a few.

  • Smaller Learning Curve - Users will not have to learn any new syntax. As long as they know the programming language, they will rapidly pick up on the development patterns for the DSL-less framework.
  • Seamless User Experience - Users can seamlessly leverage libraries and features of the programming language directly into their code.
  • Easy Debugging - Users can easily debug their code using tools built for the programming language. A framework with a DSL would require its own solution for debugging.

NORNIR OFFERS PARALLEL JOB EXECUTION

A highlight of Nornir is its ability to execute jobs against devices in parallel.

For small networks with few devices, there usually isn't much of a need for parallel job execution. But as networks grow larger, this is bound to change.

Take an example. Imagine a network with 1000 devices and a backup task that takes 10 seconds to execute against a single device. Simple math tells us that these jobs will take almost 3 hours to complete if executed sequentially. With 100 parallel workers, however, these jobs could all be executed in less than two minutes.

NORNIR OFFERS INVENTORY MANAGEMENT

Inventory management is another great highlight of Nornir.

In a large network, how could a user run a task against all Cisco devices but not Juniper devices? How could a user run a task against all routers but not switches?

Nornir's inventory solution allows users to map devices into groups. Users can then easily execute jobs against subsets of their network.

HOW TO INSTALL NORNIR

Let's get into some practical examples of how this all works.

First off, let's open up a terminal and install Nornir using "pip".

pip install nornir

HOW TO SETUP OUR INVENTORY

After Nornir is initialized, inventory is managed inside a Python object. During the initialization, however, we have several options for passing in our inventory data.

For a large production network, our recommendation is to manage inventory data inside a database. For today, we'll keep things basic by utilizing the "SimpleInventory" plugin. This plugin allows us to pass in our inventory data from YAML files.

To get started, let's create an "inventory" folder at the root of our project. We'll then add three files inside this folder.

The "hosts.yaml" file is where we specify metadata about our devices such as IP addresses, FQDNs, credentials, etc. We can also specify a list of groups that each device belongs to.

In our virtual lab, we have one Cisco router and one Juniper router.

# hosts.yaml
---
CSR1000v:
  hostname: 192.168.1.110
  port: 22
  platform: ios
  groups:
    - cisco
  connection_options:
    napalm:
      extras:
        optional_args:
          secret: admin123

vSRX:
  hostname: 192.168.1.120
  port: 22
  platform: junos
  groups:
    - juniper

Most of the data is self-explanatory, except for the "connection_options" object. This object is necessary on the Cisco device to specify a "secret". This is the password that napalm will use to enter enable mode on the router.

Next, we'll create our "groups.yaml" file. These groups correspond to those referenced in our “hosts.yaml” file.

# groups.yaml
---
cisco: {}
juniper: {}

Finally, we'll create our "defaults.yaml" file. This is where default values are specified. Nornir will use these values unless they are overridden.

# defaults.yaml
---
username: admin
password: admin123

HOW TO CREATE A NORNIR CONFIG FILE

With our inventory out of the way, let's now create a new file at the root of our project entitled "config.yaml".

# config.yaml
---
core:
  num_workers: 100

inventory:
  plugin: nornir.plugins.inventory.simple.SimpleInventory
  options:
    host_file: 'inventory/hosts.yaml'
    group_file: 'inventory/groups.yaml'
    defaults_file: 'inventory/defaults.yaml'

Earlier, we discussed parallel job execution. The "num_workers" object allows us to set an upper bound on the maximum number of concurrent jobs. This is useful for granular control over the hardware resources consumed by Nornir.

The remainder of our config file specifies our inventory metadata.

IMPORTING DEPENDENCIES

We'll now create a new file for our python program entitled "nornir_tutorial.py".

At the head of the file, we'll import our dependencies.

#!/usr/bin/env python

# Filename:                     nornir-tutorial.py
# Command to run the program:   python nornir-tutorial.py

# Import dependencies
from nornir import InitNornir
from nornir.plugins.tasks.networking import napalm_get
from nornir.plugins.functions.text import print_result
import json

HOW TO INITIALIZE NORNIR

Let's now define a method for pretty-printing Python dictionaries that will come in handy later.

# Pretty print a python dictionary
def prettyPrintDictionary(dict):
  print(json.dumps(dict, indent=2))

We'll now initialize Nornir. This will read our config file and load our inventory.

# Initialize Nornir
nr = InitNornir(config_file="config.yaml")

We can now print our inventory to the screen to ensure it loaded correctly.

# Pretty print our inventory
prettyPrintDictionary(nr.inventory.get_hosts_dict())

Executing our program will now display our inventory as a JSON object.

{
  "CSR1000v": {
    "hostname": "192.168.1.110",
    "port": 22,
    "username": null,
    "password": null,
    "platform": "ios",
    "groups": ["cisco"],
    "data": {},
    "connection_options": {
      "napalm": {
        "hostname": null,
        "port": null,
        "username": null,
        "password": null,
        "platform": null,
        "extras": {
          "optional_args": {
            "secret": "admin123"
          }
        }
      }
    }
  },
  "vSRX": {
    "hostname": "192.168.1.120",
    "port": 22,
    "username": null,
    "password": null,
    "platform": "junos",
    "groups": ["juniper"],
    "data": {},
    "connection_options": {}
  }
}

You'll notice some null values within our inventory. That's not a problem. Nornir will inherit these values from higher levels in the inventory hierarchy.

HOW TO USE NAPALM TO RETRIEVE CONFIGURATION

We may now use the napalm driver to run a task against our inventory. Let's add code to retrieve the IP interface configuration from our routers.

# Get interface config from our network devices
get_interfaces_ip_result = nr.run(napalm_get, getters=['get_interfaces_ip'])
print_result(get_interfaces_ip_result)

Executing our Python program now yields the Nornir job logs.

napalm_get**********************************************************************
* CSR1000v ** changed : False **************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'get_interfaces_ip': { 'GigabitEthernet1': { 'ipv4': { '192.168.1.110': { 'prefix_length': 24}}}}}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* vSRX ** changed : False ******************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'get_interfaces_ip': { 'ge-0/0/0.0': { 'ipv4': { '192.168.1.120': { 'prefix_length': 24}}}}}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

These logs are readable for humans, but how can we access our results as Python dictionaries? Users need to know this if they wish to parse a response without using complex regular expressions.

So how can we do it? It's actually very easy. Just grab the "result" key inside the returned object for a host. Check out the example below where we grab the result from the Cisco router and print it to the screen.

# Print our CSR1000v result as JSON
prettyPrintDictionary(get_interfaces_ip_result['CSR1000v'].result)

Running our program now yields a nicely formatted JSON response.

{
  "get_interfaces_ip": {
    "GigabitEthernet1": {
      "ipv4": {
        "192.168.1.110": {
          "prefix_length": 24
        }
      }
    }
  }
}

HOW TO EXECUTE A TASK AGAINST A SUBSET OF DEVICES

Earlier, we mentioned that Nornir allows us to control which devices we would like to execute our tasks against. We can achieve this using filters.

Let's create a filter that will only run our job against Cisco devices. We're able to do this by filtering our inventory with the condition that the "platform" must equal "ios".

# Get interface config from our Cisco devices
cisco_hosts = nr.filter(platform="ios")
get_interfaces_ip_result = cisco_hosts.run(napalm_get, getters=['get_interfaces_ip'])
print_result(get_interfaces_ip_result)

Our Nornir job log will now reveal that the task was only run against our Cisco device.

napalm_get**********************************************************************
* CSR1000v ** changed : False **************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'get_interfaces_ip': { 'GigabitEthernet1': { 'ipv4': { '192.168.1.110': { 'prefix_length': 24}}}}}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

There we go, that's everything essential you need to know to get started with Nornir!

ULTRA CONFIG GENERATOR

Nornir is great at managing task execution and network inventory. But how can we automate the generation of network config?

Have you heard of Ultra Config Generator? If you haven't, I highly recommend you check it out.

We designed the product to allow network engineers to generate and automate network configuration in a highly flexible, efficient and elegant manner. Our users love the application and I hope that you will too.

Take care until next time!

Ultra Config


JOIN THE DISCUSSION

Subscribe to the Blog

Subscribe now and never miss a new post!