Python

Nornir Network Automation

Nornir Network Automation
In: Python, NetDevOps

Nornir is a Python library designed for Network Automation tasks. It enables Network Engineers to use Python to manage and automate their network devices. Unlike tools like Ansible which rely on domain-specific languages, Nornir leverages the full power of Python, giving you more flexibility and control over your automation scripts.

Nornir feels like what you'd get if Ansible and Python had a baby. If you're used to Ansible, you know that you first set up your inventory, write tasks, and execute them on all or selected devices concurrently. Nornir operates similarly, but the big difference is you use Python code instead of any Domain Specific Language.

My Life Without Nornir

Before I discovered Nornir, my approach to Python automation involved manually setting up a list of devices, specifying each one's vendor, and credentials. This setup could be a simple Python list or a dictionary. Then, I'd loop through each device with a for loop, using libraries like Netmiko or Napalm to execute tasks. These tasks ranged from getting data from the devices to sending configurations. Here is a very simple snippet of managing the devices and using them with Netmiko. This method can get complicated very easily once you start adding more and more devices.

username = 'username'
password = os.environ.get('passwd')

switches = {
    "switch-01": "10.1.10.1",
    "switch-02": "10.1.10.2",
    "switch-03": "10.1.10.3",
    "switch-04": "10.1.10.4",
    "switch-05": "10.1.10.5",
}

gateway = {
    "device_type": "cisco_ios",
    "host": '10.10.10.1',
    "username": username,
    "password": password
}

While this method served its purpose, it came with a significant drawback, the lack of an inventory system. Adding to that, I tried making things faster by using concurrent.futures to run tasks in parallel across different devices. This meant tasks could be done more quickly, as several devices could be worked on at the same time. However, this wasn't a built-in feature of the tools I was using; it required extra coding and effort to implement parallel execution. Nornir addresses all of these shortcomings.

Prerequisites and Key Points

First off, diving into Nornir assumes you have a fair understanding of Python and its basics. If you're starting from scratch with Python, I highly recommend checking out my study guide to get up to speed. It’s designed to build a solid foundation in Python.

It’s also crucial to remember that Nornir isn’t here to replace tools like Netmiko or Napalm, rather, it's meant to work alongside them. Think of Nornir as a framework that orchestrates your automation tasks. For you to SSH to network devices, you'll still rely on plugins like Netmiko. We'll dive deeper into how these tools integrate with Nornir in the upcoming sections.

Installing Nornir is as simple as running the pip install command.

pip install nornir

Nornir Introduction

Here's a quick look at the main building blocks of Nornir, together, these components form a robust framework for network automation.

  1. Inventory - This is where you store information about your devices. Nornir's inventory system is flexible, allowing you to define devices, their credentials, and other details in a structured format.
  2. Tasks - These are the actions you want to perform on your devices, like sending commands or configurations. In Nornir, you write tasks as Python functions.
  3. Plugins - Nornir supports plugins to extend its functionality. Plugins can be used for tasks, inventory management, or adding new features.
  4. Parallel Execution - One of Nornir's strengths is its ability to run tasks in parallel across multiple devices. This built-in feature speeds up network automation tasks significantly, especially when dealing with large networks.
  5. Results - Nornir has a powerful feature known as Results. After executing tasks on your devices, Nornir collects and stores the outcomes in a Results object.

We will go through each one of them in detail using a few examples.

Nornir Files & Directory Structure

Here is my directory structure and the files (please ignore nornir.log which get created automatically)

(venv) suresh@ubuntu-desktop:~/Documents/blog/nornir_blog$ tree -I venv
.
├── basic.py
├── config.yaml
├── defaults.yaml
├── groups.yaml
├── hosts.yaml
└── nornir.log

0 directories, 6 files

config.yaml

This config.yml file is a configuration for Nornir that outlines how it should manage its inventory and execute tasks. It's written in YAML, a human-readable data serialization standard, making it straightforward to understand and modify.

#config.yaml
---
inventory:
  plugin: SimpleInventory
  options:
    host_file: 'hosts.yaml'
    group_file: 'groups.yaml'
    defaults_file: 'defaults.yaml'

runner:
  plugin: threaded
  options:
    num_workers: 5
  • Inventory - Specifies how Nornir should load information about network devices. It uses the SimpleInventory plugin, pointing to three files (We also have other inventory plugins which can read from Anisble's inventory files or NST tools like NetBox)
    • hosts.yaml for individual device details
    • groups.yaml for settings common to groups of devices, and
    • defaults.yaml for default settings applicable to all devices if not overridden in the other files.
  • Runner - Controls how Nornir runs tasks across devices. Here, the threaded plugin is used with num_workers set to 5, meaning tasks will be executed in parallel across up to 5 devices at a time.

hosts.yaml

This file contains details about each network device. For every device, you can specify parameters such as its hostname, IP address, platform type (e.g., Cisco, Arista), and credentials. Nornir uses this information to connect to and manage the devices individually. For this example, I'm starting out with five devices. (two Ciscos and three Aristas)

#hosts.yaml
---
csr-01:
  hostname: 10.10.50.41
  groups:
    - cisco

csr-02:
  hostname: 10.10.50.42
  groups:
    - cisco

eos-01:
  hostname: 192.168.100.11
  groups:
    - arista
  
eos-02:
  hostname: 192.168.100.12
  groups:
    - arista

eos-03:
  hostname: 192.168.100.13
  groups:
    - arista

groups.yaml

The groups.yaml file is used to define common settings for groups of devices. For example, if you have several devices from the same vendor or within the same part of your network, you can group them and assign shared parameters like vendor or credentials. Devices in hosts.yaml can be associated with one or more groups, inheriting the group's settings. Here, I'm defining the platform for each group and the credentials for the 'arista' group.

#groups.yaml
---
cisco:
  platform: cisco_ios

arista:
  platform: arista_eos
  username: admin
  password: admin

defaults.yaml

defaults.yaml provides default settings that apply to all devices unless explicitly overridden in hosts.yaml or groups.yaml. This is useful for global settings like default credentials, timeout values, or any other parameters you want to apply network-wide. Here, I've defined the default credentials.

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

When Nornir runs, it combines these files to build a complete picture of your network. It starts with the defaults.yaml as the base, then layers on any relevant settings from groups.yaml, and finally applies any specific settings from hosts.yaml. This hierarchical approach allows for both broad and granular control over device management and task execution, ensuring that each device is configured and managed with the right parameters.

💡
In production environments, please avoid storing credentials in plain text files.

Inventory Inheritance Example

Let's take csr-01 from the hosts.yaml as an example to explain the inheritance.

  • csr-01 Details - It has its own unique hostname (10.10.50.41) and is part of the cisco group.
  • Inheritance from groups.yaml - Since csr-01 is associated with the cisco group, it inherits the platform: cisco_ios setting defined under the cisco group in groups.yaml.
  • Inheritance from defaults.yaml - The global username and password are not specified directly in csr-01 or under the cisco group. So, csr-01 inherits the default credentials (username: admin, password: Cisco123) specified in defaults.yaml.

This way, csr-01 gets a complete set of parameters through inheritance, its own hostname, the platform from its group, and the global username and password for login. Not only you can group devices with vendors, you can also group devices using their function, geographical location etc.

💡
When a host falls under multiple groups in Nornir and there are common parameters defined across these groups, the host will inherit the parameters based on the hierarchy or order of the groups as they are defined. If there’s a conflict—meaning the same parameter is defined differently in multiple groups—the host will inherit the parameter from the first group mentioned. This means the order in which groups are listed for a host matters.

Creating Our First Nornir Script

Let's look at a simple example to understand how our first Nornir script works, especially using the inventory examples we discussed before (with csr and eos devices).

from nornir import InitNornir

def say_hello(task):
    print('My Task Works! Yaay')

nr = InitNornir(config_file='config.yaml')
nr.run(task=say_hello)
#output

My Task Works! Yaay
My Task Works! Yaay
My Task Works! Yaay
My Task Works! Yaay
My Task Works! Yaay

Importing Nornir - The script starts by importing InitNornir Class from the Nornir library. This is essential for initializing our Nornir environment.

Defining a Task Function - Next, we define a simple task function say_hello that takes task as an argument. This function merely prints a message, My Task Works! Yaay. In Nornir, tasks are functions that you want to execute on your network devices. The task argument is a key part of this; it represents the task being executed and carries information and context about the current device it's running against.

Initializing Nornir - We then create an instance of Nornir using InitNornir, specifying our config.yaml as the configuration file. This configuration includes our inventory setup with hosts.yaml, groups.yaml, and defaults.yaml, defining our network devices and their properties.

Running the Task - Finally, we use the .run() method on our Nornir instance to execute the say_hello task across all devices specified in our inventory. Because our config.yaml specifies a runner with 5 workers, tasks can be executed in parallel across up to 5 devices at a time.

Output - Given our inventory setup, the script prints My Task Works! Yaay once for each device in the inventory. Since we have five devices (csr-01, csr-02, eos-01, eos-02, eos-03), we see the message printed five times, indicating the task executed successfully on each device.

print_result plugin

Let's look at our second example on how to use the print_result plug-in. If you have used Ansible before, you would know that it gives a nice output showing what's going on.

You can install the plug-in using pip install command.

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

def say_hello(task):
    return 'My Task Works! Yaay'

nr = InitNornir(config_file='config.yaml')
result = nr.run(task=say_hello)
print_result(result)

In the updated example, the significant addition is the use of print_result from the nornir_utils plugin. This function is designed to neatly display the results of tasks executed by Nornir on your network devices.

Importing print_result - We've added a new import statement to bring in the print_result function. This plug-in is used for formatting and printing the outcome of our tasks in a readable manner.

Storing and Printing Results - Instead of directly printing a message within the say_hello task, we now return the message. The main script captures the output of the nr.run method in a variable named result. This variable holds detailed information about the task execution on each device. Finally, print_result(result) is called to display this information.

The output is structured to provide detailed feedback on the task execution across each device in your inventory. Here’s a breakdown.

  • Task Name - The script starts by mentioning the task name (say_hello) as a header for the output section.
  • Device Name and Change Status - Each device the task was executed on is listed with its name (e.g., csr-01, eos-01) and a change status (changed : False). This status indicates whether the task made any changes to the device's state. In this case, no changes were made.
  • Task Outcome: Below each device name, the result of the task is shown. Since our task simply returns a message, "My Task Works! Yaay" is printed under each device.
  • Start and End Markers - Each device's result section is enclosed within vvvv and ^^^^ markers, providing clear visual segmentation. The INFO label next to the task name within this block indicates the nature of the output.

Accessing the host's parameters

The task.host object allows us to access various parameters of the host on which the task is currently executing. You can retrieve specific details like.

  • task.host - The name of the current device.
  • task.host.groups - The group(s) the device belongs to.
  • task.host.hostname - The hostname or IP address of the device.

By using task.host along with its attributes, we're able to dynamically insert each device's specific information into our task's return message.

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

def say_hello(task):
    return f'My Task Works! Yaay {task.host} - {task.host.groups} - {task.host.hostname}'

nr = InitNornir(config_file='config.yaml')
result = nr.run(task=say_hello)
print_result(result)

Filtering Devices

Here is another example of how you can run the tasks on specific devices. For the sake of simplicity, I'm going to run the task only on Arista devices. You can learn more about filtering here.

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

def say_hello(task):
    return f'My Task Works! Yaay {task.host} - {task.host.groups} - {task.host.hostname}'

nr = InitNornir(config_file='config.yaml')
nr = nr.filter(platform='arista_eos')

result = nr.run(task=say_hello)
print_result(result)

nornir_netmiko plug-in

Now, we've come to the really cool part where we actually start executing commands on devices and seeing the output. You might think, like I did when I was just getting started, "Alright, I'll just create a new function, import Netmiko's ConnectHandler, and get on with it, right?"

But here’s a pleasant surprise, the awesome teams behind Nornir and Netmiko have already done a lot of the heavy lifting for us. They've created plug-ins that we can easily import. To get the netmiko plug-in, all you need to do is run pip install nornir_netmiko. This simple command fetches and installs everything you need to start sending commands to your network devices through your Nornir scripts.

pip install nornir_netmiko
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result


nr = InitNornir(config_file='config.yaml')

results = nr.run(task=netmiko_send_command, command_string='show ip interface brief | excl down')
print_result(results)

In this script, we're using the nornir_netmiko plugin, specifically the netmiko_send_command function, to execute commands on network devices. After initializing Nornir, we call nr.run, passing in netmiko_send_command as the task. We specify the command we want to run on our devices with command_string='show ip interface brief | excl down'

Configuring Devices with Nornir and Netmiko

The netmiko_send_config function is used to push configuration commands to devices, in this case specifically targeting Arista devices with platform='arista_eos'.

After filtering for these devices, we execute netmiko_send_config to send configuration commands. The output marked changed : True indicates that the configuration was successfully applied, reflecting changes made on 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


nr = InitNornir(config_file='config.yaml')
nr = nr.filter(platform='arista_eos')
results = nr.run(task=netmiko_send_config, config_commands=['ntp server 1.1.1.1'])
print_result(results)

Modified Script and Inventory 'data'

We made a slight change to the script to demonstrate a more dynamic feature of Nornir, accessing host-specific data within a task function for customized configurations across devices.

In the updated groups.yaml file, you'll notice an additional data section under the arista group. This section allows us to define custom data applicable to all devices within this group. Here, we've specified an NTP server address (1.1.1.1) under data, making it accessible to devices associated with the arista group.

#groups.yaml

---
cisco:
  platform: cisco_ios

arista:
  platform: arista_eos
  username: admin
  password: admin
  data:
    ntp: 1.1.1.1
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command, netmiko_send_config
from nornir_utils.plugins.functions import print_result

def set_ntp(task):
    ntp_server = task.host['ntp']
    task.run(task=netmiko_send_config, config_commands=[f'ntp server {ntp_server}'])

nr = InitNornir(config_file='config.yaml')
nr = nr.filter(platform='arista_eos')

results = nr.run(task=set_ntp)
print_result(results)

The function set_ntp fetches this NTP server address using task.host['ntp'], dynamically inserting it into the configuration command. This method ensures that the NTP server setting applied to each device is retrieved from the inventory, allowing for centralized management of device configurations.

In this example, you would have seen two different ways to run tasks, task.run and nr.run. Here’s a brief explanation of the difference between the two.

  • task.run - This is used within a task function to execute another task. Think of it as calling a sub-task within your main task. When you use task.run, you're essentially saying, "While performing this task, go ahead and run these additional tasks as part of it."
  • nr.run - On the other hand, nr.run is used to kick off tasks at the top level. This is the method you call when you want to start your automation process and execute tasks across your inventory of devices.

In summary, nr.run is used to initiate your automation tasks on your network devices, while task.run allows you to organize and modularize your tasks by calling other tasks within a task.

Closing Up

I hope you found this post helpful. There's so much to explore with Nornir, and it was quite a challenge to squeeze everything into just one post. I'm considering putting together a study guide on Nornir to dive deeper into its capabilities. If that's something you'd be interested in, please do let me know in the comments.

Written by
Suresh Vina
Tech enthusiast sharing Networking, Cloud & Automation insights. Join me in a welcoming space to learn & grow with simplicity and practicality.
Comments
More from Packetswitch
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.