
At its core, Nornir is a Python-based network automation framework designed specifically for engineers who need fine-grained control of the automation logic. Unlike Ansible, which is declarative and YAML-heavy, Nornir is imperative, giving users the ability to write and control their workflows in Python.
Why is Nornir gaining traction in enterprise environments? Because it enables automation at scale without sacrificing flexibility. Network engineers can integrate with Python libraries like Netmiko, Napalm, or even custom APIs, all while maintaining consistent data structures and threading support.
Nornir fills the gap between low-level scripting and high-level playbook-driven automation, offering a middle ground where performance and control live in harmony. For full documentation, visit the official Nornir documentation.
To understand why you might select Nornir over Ansible or Netmiko, we need to examine the nuances in their design and purpose.
Explore more in the official Ansible documentation.
Nornir is a powerful Python automation framework designed specifically for network professionals. Unlike other automation tools that use their own domain-specific language, Nornir leverages pure Python, making it flexible, extensible, and ideal for complex enterprise network automation tasks.
Before starting with Nornir, ensure you have:
It’s recommended to use a virtual environment to keep your Nornir project dependencies isolated:
# Create a virtual environment
python -m venv nornir-env
# Activate the environment
# On Linux/Mac:
source nornir-env/bin/activate
# On Windows:
nornir-env\Scripts\activate
# Install core Nornir package
pip install nornir
# Install commonly used plugins
pip install nornir-netmiko # For connecting to devices
pip install nornir-napalm # For multi-vendor support
pip install nornir-utils # For utility functions
pip install nornir-jinja2 # For templating
Create a basic project structure as follows:
nornir-project/
├── inventory/
│ ├── hosts.yaml
│ ├── groups.yaml
│ └── defaults.yaml
├── templates/
│ └── vlan_config.j2
├── config.yaml
└── scripts/
├── configure_vlans.py
└── backup_configs.py
This file defines all your network devices:
# inventory/hosts.yaml
router1:
hostname: 192.168.1.1
groups:
- cisco_ios
data:
role: core_router
location: main_datacenter
switch1:
hostname: 192.168.1.2
groups:
- cisco_ios
data:
role: access_switch
location: floor1
switch2:
hostname: 192.168.1.3
groups:
- cisco_ios
data:
role: access_switch
location: floor2
This file defines device groups with shared attributes:
# inventory/groups.yaml
cisco_ios:
platform: ios
connection_options:
netmiko:
extras:
device_type: cisco_ios
timeout: 120
napalm:
extras:
optional_args:
transport: telnet
arista_eos:
platform: eos
connection_options:
netmiko:
extras:
device_type: arista_eos
napalm:
extras:
optional_args: {}
This file sets default parameters for all devices:
# inventory/defaults.yaml
username: admin
password: secure_password
connection_options:
netmiko:
extras:
timeout: 60
This file defines the Nornir configuration:
# config.yaml
inventory:
plugin: SimpleInventory
options:
host_file: "inventory/hosts.yaml"
group_file: "inventory/groups.yaml"
defaults_file: "inventory/defaults.yaml"
runner:
plugin: threaded
options:
num_workers: 10
Create a script to configure VLANs on all switches:
# scripts/configure_vlans.py
from nornir import InitNornir
from nornir_netmiko import netmiko_send_config
from nornir_utils.plugins.functions import print_result
from nornir.core.filter import F
# Initialize Nornir with our config file
nr = InitNornir(config_file="config.yaml")
# Filter for only access switches
switches = nr.filter(F(data__role="access_switch"))
# Define the configuration commands
vlan_config = [
"vlan 100",
"name Engineering",
"vlan 200",
"name Sales",
"vlan 300",
"name Management",
"exit"
]
# Execute the configuration on all devices
results = switches.run(
task=netmiko_send_config,
config_commands=vlan_config
)
# Print the results
print_result(results)
Create a script to backup configurations from all devices:
# scripts/backup_configs.py
from nornir import InitNornir
from nornir_netmiko import netmiko_send_command
from nornir_utils.plugins.functions import print_result
import os
from datetime import datetime
# Initialize Nornir
nr = InitNornir(config_file="config.yaml")
# Create backups directory if it doesn't exist
backup_dir = "backups"
if not os.path.exists(backup_dir):
os.makedirs(backup_dir)
# Get current date for filename
date_str = datetime.now().strftime("%Y-%m-%d")
def backup_config(task):
# Run "show running-config" command
config_result = task.run(
task=netmiko_send_command,
command_string="show running-config"
)
# Create a location-specific directory
device_location = task.host.get("location", "unknown")
location_dir = f"{backup_dir}/{device_location}"
if not os.path.exists(location_dir):
os.makedirs(location_dir)
# Save the config to a file
filename = f"{location_dir}/{task.host.name}_{date_str}.txt"
with open(filename, 'w') as f:
f.write(config_result.result)
return f"Backup saved to {filename}"
# Run the backup task on all devices
result = nr.run(task=backup_config)
# Print results
print_result(result)
First, create a Jinja2 template:
{# templates/vlan_config.j2 #}
{% for vlan in vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% endfor %}
Then create a script to use this template:
# scripts/template_config.py
from nornir import InitNornir
from nornir_jinja2.plugins.tasks import template_file
from nornir_netmiko import netmiko_send_config
from nornir_utils.plugins.functions import print_result
from nornir.core.filter import F
# Initialize Nornir
nr = InitNornir(config_file="config.yaml")
# Filter for only switches
switches = nr.filter(F(data__role="access_switch"))
# Define VLAN data
vlan_data = {
"vlans": [
{"id": 100, "name": "Engineering"},
{"id": 200, "name": "Sales"},
{"id": 300, "name": "Management"}
]
}
def configure_from_template(task):
# Generate config from template
template_result = task.run(
task=template_file,
path="templates",
template="vlan_config.j2",
**vlan_data
)
# Extract the rendered configuration
config = template_result.result
# Apply the configuration
task.run(
task=netmiko_send_config,
config_commands=config.splitlines()
)
# Run the configuration task
results = switches.run(task=configure_from_template)
# Print results
print_result(results)
Implement proper error handling in your scripts:
from nornir.core.exceptions import NornirExecutionError
try:
results = nr.run(task=configure_from_template)
print_result(results)
# Check for failures
failed_hosts = [host for host, result in results.items() if result.failed]
if failed_hosts:
print(f"Configuration failed on hosts: {', '.join(failed_hosts)}")
except NornirExecutionError as e:
print(f"Error occurred: {e}")
Adjust the number of concurrent connections based on your network capacity:
nr = InitNornir(
config_file="config.yaml",
runner={
"plugin": "threaded",
"options": {
"num_workers": 20, # Increase for larger networks
},
},
)
Use environment variables for credentials:
# Import os module at the top
import os
# In your script
nr = InitNornir(config_file="config.yaml")
# Set credentials from environment variables
nr.inventory.defaults.username = os.environ.get("NET_USER")
nr.inventory.defaults.password = os.environ.get("NET_PASS")
Nornir offers powerful filtering to target specific devices:
# Filter by multiple criteria
floor1_cisco_devices = nr.filter(
F(data__location="floor1") &
F(platform="ios")
)
# Filter by role and exclude specific hosts
core_devices_except_router1 = nr.filter(
F(data__role="core_router") &
~F(name="router1")
)
def check_compliance(task):
# Check for approved NTP servers
ntp_result = task.run(
task=netmiko_send_command,
command_string="show run | include ntp server"
)
approved_ntp = ["10.0.1.1", "10.0.1.2"]
current_ntp = ntp_result.result
compliant = all(server in current_ntp for server in approved_ntp)
return {
"compliant": compliant,
"details": current_ntp
}
results = nr.run(task=check_compliance)
For large-scale changes across the network:
def deploy_acl_changes(task):
# First backup the current config
backup_result = task.run(task=backup_config)
# Deploy the new ACL
acl_config = [
"ip access-list extended UPDATED_ACL",
"permit ip any host 10.0.0.1",
"permit ip any host 10.0.0.2",
"deny ip any any log"
]
task.run(
task=netmiko_send_config,
config_commands=acl_config
)
# Verify the deployment
verify_result = task.run(
task=netmiko_send_command,
command_string="show ip access-list UPDATED_ACL"
)
return {
"backup": backup_result,
"verification": verify_result.result
}
results = nr.run(task=deploy_acl_changes)
If you encounter connection problems:
# Increase timeout for specific devices
nr.inventory.hosts["slow_device"].connection_options["netmiko"] = {
"extras": {"timeout": 300}
}
When tasks fail:
import logging
# Enable logging
logging.basicConfig(
filename="nornir.log",
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# Test on a single device
test_device = nr.filter(name="router1")
result = test_device.run(task=problematic_task)
Nornir provides a powerful, flexible framework for network automation that can scale from simple tasks to complex enterprise deployments. By leveraging Python’s full capabilities, you can build sophisticated, maintainable automation solutions that integrate with the rest of your operational tools and processes.
As you grow more comfortable with Nornir, explore the rich ecosystem of plugins and contributors in the Nornir community. The official documentation and GitHub repositories are excellent resources for keeping up with the latest developments.
Additional resources to help you get started.