Nornir 3.0 Introduction

What is Nornir

Nornir is an automation framework written in python to be used with python. It takes care of dealing with inventory which contains the host information against which you want to run the scripts. It takes care of dispatching the tasks to the specified devices and provides a common framework to write plugins. In that respect, it can be considered similar to frameworks like Ansible. Yet, Nornir allows you to write pure Python code, which is not the case for tools like Ansible (they use a pseudo language).

Installation

WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 -m venv venv
WAUTERW-M-65P7:Nornir_Intro wauterw$ source venv/bin/activate
(venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ 
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir_napalm
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir_netmiko
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir_netbox
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir_ansible
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir_scrapli
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir_utils
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ pip3 install nornir_jinja2

For all the examples in this post, we will use a Cisco sandbox environment delivered by Cisco Devnet. Go check out Devnet, really brilliant. To get a list of all sandboxes, check out this link. For this tutorial, I’m using the IOS XE sandbox mainly (see here).

Configuration and Inventory

We first need to initialize Nornir with a configuration file. A sample configuration file could look as follows:

---
runners:
  plugin: threaded
  options:
    num_workers: 10

inventory:
  plugin: "SimpleInventory"
  options: 
    host_file: "./inventory/hosts.yml"
    group_file: "./inventory/groups.yml"
    defaults_file: "./inventory/defaults.yml"

We will need to create a folder called inventory where we will keep all of our inventory files. The inventory is comprised of hosts, groups and defaults. Let’s check it out in more detail.

# Nornir Hosts File 
---
DEV01: 
  hostname: ios-xe-mgmt.cisco.com
  groups: 
    - Devnet

DEV02: 
  hostname: ios-xe-mgmt-latest.cisco.com
  groups: 
    - Devnet

In the above hosts.yml we define our devices. For each we specify the hostname and the group they belong to. Note that this is a very minimal configuration. One can also specify the port, username, password and many more parameters. Instead I have opted here to store these parameters in the default.yml file.

---
platform: ios
port: 8181
username: "developer"
password: "C1sco12345"

The above will apply to all our devices.

Lastly, we also have a groups.yml file. In this file, we created a group called Devnet and we provide it with some data.

---
Devnet: 
  data: 
    ntp: 
      servers: 
        - 1.1.1.1

Let’s now experiment a bit with these constructs. In the below snippet, we will have our Python program interact with these files.

from nornir import InitNornir
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')
print(nornir.inventory.hosts)

Let’s execute this script:

~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part1.yml
~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$: {'DEV01': Host: DEV01, 'DEV02': Host: DEV02}

As you can see, a dictionary is returned with our devices. Let’s now print some more details on one of these devices:

r1 = nornir.inventory.hosts['DEV01']

print(f"Group: {r1.groups}")
print(f"Hostname: {r1.hostname}")
print(f"Username: {r1.username}")
print(f"Password: {r1.password}")

This will result in the following output:

~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part1.py
Group: [Group: Devnet]
Hostname: ios-xe-mgmt.cisco.com
Username: developer
Password: C1sco12345

Working with Netmiko plugin

In this section, we will experiment a bit with Netmiko. In a previous blog post (see here), we worked already with plain Netmiko library. In the below script we will again execute some commands but now through the Netmiko plugin within Nornir.

Essentially the below script will execute the show ip int brief and show version commands againt all the devices.

from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command, netmiko_send_config
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')

result = nornir.run(netmiko_send_command, command_string="show ip int br")#
print_result(result)

The result is:

~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part2.py                                                      

netmiko_send_command************************************************************
* DEV01 ** changed : False *****************************************************
vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO

Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up      
GigabitEthernet2       10.255.255.240  YES other  up                    up      
GigabitEthernet3       unassigned      YES NVRAM  administratively down down    
Loopback78             unassigned      YES unset  up                    up      
Loopback88             unassigned      YES unset  up                    up      
Loopback89             unassigned      YES unset  up                    up      
Loopback90             unassigned      YES unset  up                    up      
<TRUNCATED> 
Loopback1233333        11.21.31.41     YES other  up                    up      
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* DEV02 ** changed : False *****************************************************
vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES other  up                    up      
GigabitEthernet2       unassigned      YES NVRAM  up                    up      
GigabitEthernet3       unassigned      YES NVRAM  up                    up      

As you will notice from the output above, the show ip int br command was run on both our devices (configured in the host.yml file). Let’s say we want to execute these commands only on 1 of our devices. That’s simple as well through using Nornir’s built in filters:

from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command, netmiko_send_config
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')

r2 = nornir.filter(name="DEV02")

result = r2.run(netmiko_send_command, command_string="show ip int br")#
print_result(result)

As you can already guess, the above script will run only against DEV02.

Next, let’s try to change some configuration:

from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command, netmiko_send_config
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')

description = 'Description set with Nornir Netmiko'

description_config = [
    "interface GigabitEthernet3",
    f"description {description}",
]
   
result = nornir.run(netmiko_send_config, config_commands=description_config)
print_result(result)

As you can imagine, the above snippet will set the description of our GigabitEthernet3 interface to ‘Description set with Nornir Netmiko’.

Here is the output:

~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part2.py                                                      
netmiko_send_config*************************************************************
* DEV01 ** changed : True ******************************************************
vvvv netmiko_send_config ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO

csr1000v#configure terminal
Enter configuration commands, one per line.  End with CNTL/Z.
csr1000v(config)#interface GigabitEthernet3
csr1000v(config-if)#description Description set with Nornir Netmiko
csr1000v(config-if)#end
csr1000v#
^^^^ END netmiko_send_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* DEV02 ** changed : True ******************************************************
vvvv netmiko_send_config ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO

csr1000v-1#configure terminal
csr1000v-1(config)#interface GigabitEthernet3
csr1000v-1(config-if)#description Description set with Nornir Netmiko
csr1000v-1(config-if)#end
csr1000v-1#
^^^^ END netmiko_send_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

And when we check the interfaces on both devices we get indeed that the description for interface 3 has been changed.

csr1000v#show interface description
Interface                      Status         Protocol Description
Gi1                            up             up       MANAGEMENT INTERFACE - DON'T TOUCH ME
Gi2                            up             up       Configured by NETCONF
Gi3                            admin down     down     Description set with Nornir Netmiko
Lo78                           up             up       Configured by Ansible
Lo88                           up             up       Configure by Ansible

The code of the above examples can be found on my Github account.

Working with Napalm plugin

In what follows, we will have a quick look at the Napalm plugin for Nornir. In case you are not familiar with Napalm, check out some of my previous posts (see here for part 1 and here for part 2).

In the below example, we will run some commands again against one of the sandbox devices that are configured in the hosts.yml file. We will use Nornir’s filter method to ensure the commands are only run against DEV02.

from nornir import InitNornir
from nornir_napalm.plugins.tasks import napalm_get, napalm_cli
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')

result = nornir.run(task=napalm_get, getters=["interfaces"])
print_result(result)

The result is as follows:

~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part3.py                                              
napalm_get**********************************************************************
* DEV01 ** changed : False *****************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'interfaces': { 'GigabitEthernet1': { 'description': 'MANAGEMENT INTERFACE - '
                                                       "DON'T TOUCH ME",
                                        'is_enabled': True,
                                        'is_up': True,
                                        'last_flapped': -1.0,
                                        'mac_address': '00:50:56:BB:E1:4E',
                                        'mtu': 1500,
                                        'speed': 1000},
                  'GigabitEthernet2': { 'description': 'Configured by NETCONF',
                                        'is_enabled': True,
                                        'is_up': True,
                                        'last_flapped': -1.0,
                                        'mac_address': '00:50:56:BB:10:5E',
                                        'mtu': 1500,
                                        'speed': 1000},
                  'GigabitEthernet3': { 'description': 'Description set with '
                                                       'Nornir Netmiko',
                                        'is_enabled': False,
                                        'is_up': False,
                                        'last_flapped': -1.0,
                                        'mac_address': '00:50:56:BB:2E:4E',
                                        'mtu': 1500,
                                        'speed': 1000},
                  <truncated>
                  'Loopback9999': { 'description': 'Added with RESTCONF this '
                                                   'is from Alex',
                                    'is_enabled': True,
                                    'is_up': True,
                                    'last_flapped': -1.0,
                                    'mac_address': '',
                                    'mtu': 1514,
                                    'speed': 8000}}}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* DEV02 ** changed : False *****************************************************
vvvv napalm_get ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'interfaces': { 'GigabitEthernet1': { 'description': 'MANAGEMENT INTERFACE - '
                                                       "DON'T TOUCH ME",
                                        'is_enabled': True,
                                        'is_up': True,
                                        'last_flapped': -1.0,
                                        'mac_address': '00:50:56:BB:E9:9C',
                                        'mtu': 1500,
                                        'speed': 1000},
                  'GigabitEthernet2': { 'description': 'Network Interface',
                                        'is_enabled': True,
                                        'is_up': True,
                                        'last_flapped': -1.0,
                                        'mac_address': '00:50:56:BB:77:1A',
                                        'mtu': 1500,
                                        'speed': 1000},
                  'GigabitEthernet3': { 'description': 'Description set with '
                                                       'Nornir Netmiko',
                                        'is_enabled': True,
                                        'is_up': True,
                                        'last_flapped': -1.0,
                                        'mac_address': '00:50:56:BB:EB:1E',
                                        'mtu': 1500,
                                        'speed': 1000}}}
^^^^ END napalm_get ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Again, also here, let’s ensure we only run our set of commands on a single device. Again we will use Nornir’s filter mechanism.

from nornir import InitNornir
from nornir_napalm.plugins.tasks import napalm_get, napalm_cli
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')

r2 = nornir.filter(name="DEV02")

result = r2.run(napalm_cli, commands=['show version', 'show interface brief'])
print_result(result)

Execution of the above script indeed returns the output of both these commands.

~ (venv) WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part3.py                                                          ✔ │ at 16:38:50 
napalm_cli**********************************************************************
* DEV02 ** changed : False *****************************************************
vvvv napalm_cli ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'show ip int brief': 'Interface              IP-Address      OK? Method '
                       'Status                Protocol\n'
                       'GigabitEthernet1       10.10.20.48     YES other  '
                       'up                    up      \n'
                       'GigabitEthernet2       unassigned      YES NVRAM  '
                       'up                    up      \n'
                       'GigabitEthernet3       unassigned      YES NVRAM  '
                       'up                    up',
  'show version': 'Cisco IOS XE Software, Version 16.11.01a\n'
                  'Cisco IOS Software [Gibraltar], Virtual XE Software '
                  '(X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 16.11.1a, '
                  <TRUNCATED>
                  'Configuration register is 0x2102'}
^^^^ END napalm_cli ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The code for these Napalm scripts can be found here.

Working with Scrapli plugin

Similar to the above sections, we will now have a look at the Scrapli plugin for Nornir. Again, in case you are new to Scrapli, you might want to check out my post on it (see here)

Below is a very straightforward example that shows once again the power of Nornir. We will execute (again) some commands against the devices specified in the hosts.yml file. This tome Scrapli will do the hard work for us.

from nornir import InitNornir
from nornir_scrapli.tasks import (
    get_prompt,
    send_command,
    send_configs
)
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')

command_results = nornir.run(task=send_command, command="show ip int brief")
print("Result for DEV01:")
print(command_results["DEV01"].result)

print("Result for DEV02:")
print(command_results["DEV02"].result)

The result is:

WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part4.py                                                         
Result for DEV01:
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES other  up                    up
GigabitEthernet2       10.255.255.240  YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
<TRUNCATED>
Loopback1233333        11.21.31.41     YES other  up                    up
Result for DEV02:
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES other  up                    up
GigabitEthernet2       unassigned      YES NVRAM  up                    up
GigabitEthernet3       unassigned      YES NVRAM  up                    up

In the previous example, we have been running 1 single command against both devices. Nornir and Scrapli also support sending multiple commands. See the snippet below:

from nornir import InitNornir
from nornir_scrapli.tasks import (
    get_prompt,
    send_command, send_commands,
    send_configs
)
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')

command_results = nornir.run(task=send_commands, commands=["show version", "show ip int brief"])
print("Result for DEV01:")
print(command_results["DEV01"].result)

print("Result for DEV02:")
print(command_results["DEV02"].result)

You’ll see in the output below that the Nornir-Scrapli combination is walking over both devices and for each device it will run the two commands and display the results. All that with very few lines of code in fact. Amazing!

WAUTERW-M-65P7:Nornir_Intro wauterw$  python3 nornir_part4.py                                                      
Result for DEV01:
### Result of the "show version"
Cisco IOS XE Software, Version 16.09.03
<TRUNCATED>
### Result of the "show ip int brief"
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES other  up                    up
GigabitEthernet2       10.255.255.240  YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
<TRUNCATED>
Result for DEV02:
### Result of the "show version"
Cisco IOS XE Software, Version 16.11.01a
<TRUNCATED>
### Result of the "show ip int brief"
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES other  up                    up
GigabitEthernet2       unassigned      YES NVRAM  up                    up
GigabitEthernet3       unassigned      YES NVRAM  up                    up

Next, let’s configure something through Nornir-Scrapli.

from nornir import InitNornir
from nornir_scrapli.tasks import (
    get_prompt,
    send_command, send_commands,
    send_configs
)
from nornir_utils.plugins.functions import print_result

nornir = InitNornir('nornir_config.yml')
config_results = nornir.run(
    task=send_configs,
   configs=["interface GigabitEthernet3", "description Configured by Scrapli through Nornir"],
)
print(config_results["iosxe-1"].result)

Let’s execute the script:

WAUTERW-M-65P7:Nornir_Intro wauterw$ python3 nornir_part4.py                                                         
interface GigabitEthernet3
description Configured by Scrapli through Nornir

And finally let’s verify if everything worked.

csr1000v#show interf description
Interface                      Status         Protocol Description
Gi1                            up             up       MANAGEMENT INTERFACE - DON'T TOUCH ME
Gi2                            up             up       Configured by NETCONF
Gi3                            admin down     down     Configured by Scrapli through Nornir
Lo78                           up             up       Configured by Ansible

And yes, it worked! That’s how easy it is to configure stuff on a network device through Nornir and Scrapli.

The code for the example above can be found here.

That’s it in terms of experimenting a bit with Nornir 3.0. Amazing tool. Code for all examples can be found here.