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.