Why Nornir matters after you have met Ansible ?
You've automated your network with Ansible. You've written playbooks, felt the power of applying changes repeatedly without altering the result, and watched the play recap go green. So why would you ever need something else?

Ansible is brilliant. It lowered the barrier to network automation so dramatically that engineers who had never touched Python could automate hundreds of devices overnight. If Ansible solved your problem, keep using it. But if you've ever stared at a complex Ansible playbook and thought 'I could do this faster in a Python script' — Nornir is what you were looking for without knowing its name.
"Nornir is not a replacement for Ansible. It's what happens when Python engineers decide they want the power of Ansible's inventory and parallelism without leaving Python."
The Ansible Model
When you write Ansible, you're writing YAML that describes a desired state or sequence of tasks. Ansible translates that YAML into actions. This abstraction serves as both your strength and limitation.
Clean, declarative, and easily understood by your manager. However, if you need to query each device, parse responses, make conditional decisions based on model and current configuration, apply different templates for each device family, and log every step with timing data to a database, your playbook turns into a creative exercise involving set_fact, when conditions, and Jinja2 filters.
Enter Nornir
Nornir is a Python automation framework. It's not a DSL or a YAML interpreter—it's a Python library. You write in Python. Nornir integrates Ansible's strengths in inventory management and parallel task execution into the Python environment, and then lets you take the lead.
Head to head: where they differ
| Ansible | Nornir |
|---|---|
| YAML-based DSL, low Python requirement | Pure Python, full language available |
| Agentless, SSH/API by default | Threaded concurrency (fast, lightweight) |
| Massive module library (Galaxy) | Plugin ecosystem (Netmiko, NAPALM, etc.) |
| Readable by non-developers | Requires Python comfort |
| Parallelism via forks (process-based) | Testable with pytest |
| Complex logic = complex YAML | No overhead — just a library |
Nornir Framework Overview
Unlike Ansible, which abstracts logic behind a DSL, Nornir remains in Python, offering the full capabilities of the language for data manipulation, error handling, testing, and integration with external systems. Its architecture is centered around three pillars: Inventory, Tasks, and Runners.
Inventory : Defines the devices (hosts), their attributes, and group memberships. Pluggable — SimpleInventory, NetBox, Ansible, custom.
Tasks : Pure Python functions that accept a Task object and run operations on a single host. Composable and independently testable (eg config push).
Runners : Control how tasks are executed across the inventory. ThreadedRunner (default) runs tasks concurrently using a thread pool.
Plugins : Connection drivers (Netmiko, NAPALM, Scrapli) and utility tools (print_result, write_file) extend core functionality.
Results : AggregatedResult and MultiResult objects give full per-host, per-task outcome data — iterable, filterable Python objects.
Inventory Management
Nornir's inventory system is entirely pluggable. One of its standout features is how its inventory concept closely mirrors Ansible's. You have hosts, groups, and variables, and you can even utilize your existing Ansible inventory files with the AnsibleInventory plugin.
For lab and small-scale deployments, SimpleInventory reads from YAML files. For production at 100+ devices, integrating directly with NetBox gives you a single source of truth.
SimpleInventory — file structure
File : config.yaml
inventory:
plugin: SimpleInventory
options:
host_file: inventory/hosts.yaml
group_file: inventory/groups.yaml
defaults_file: inventory/defaults.yml
runner:
plugin: threaded
options:
num_workers: 10
File : inventory/groups.yaml
sros:
platform: nokia_sros
username: <username>
password: <password>
connection_options:
netmiko:
extras:
secret: ""
global_delay_factor: 2
File : inventory/hosts.yaml
router1:
hostname: 172.20.20.13
groups:
- sros
router2:
hostname: 172.20.20.14
groups:
- sros
Tasks, Runners & Plugins
Understanding the execution model is critical before scaling to a large number of devices. Each task is a Python function. The runner controls concurrency. Plugins provide the connection layer to devices.
The plugin ecosystem
Nornir itself is deliberately minimal — it handles inventory and task running. Everything else is a plugin. The main ones you'll use immediately:
| Plugin | Use Case |
|---|---|
| nornir-napalm | multi-vendor getters and config push |
| nornir-netmiko | Netmiko connection plugin (SSH) |
| nornir-utils | print_result, print_title helpers |
| nornir-scrapli | Scrapli async connections |
| nornir-netconf | NETCONF operations |
| nornir-ansible | run Ansible modules from Nornir tasks |
Yes — nornir-ansible is real. You can call Ansible modules from inside a Nornir task. The ecosystems aren't enemies.
ThreadedRunner — concurrency model
The ThreadedRunner utilizes Python's concurrent.futures.ThreadPoolExecutor. Each host receives a thread from the pool. With num_workers set to 50, Nornir can run 50 SSH sessions at once. For 100 devices, this results in two batches.
Use Case : Nokia SROS NTP Deployment Script
Installing Nornir
python -m venv .venv
source .venv/bin/activate
pip install nornir nornir-utils nornir-netmiko nornir-napalm
The Complete Script
Here is the full working script before we break it down line by line. Read it once, then follow the steps below to understand exactly what each part does.
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_config
from nornir_utils.plugins.functions import print_result
from nornir.core.task import Result
from netmiko import ConnectHandler
from datetime import datetime
nr = InitNornir(config_file="config.yaml")
def deploy_services(task):
device = {
"device_type": "nokia_sros",
"host": task.host.hostname,
"username": task.host.username,
"password": task.host.password,
"secret": "",
}
commands = [
"configure",
" system",
" time",
" ntp",
" server 192.168.1.1",
" no shutdown",
" exit",
" exit",
" exit",
"exit",
]
with ConnectHandler(**device) as conn:
output = conn.send_config_set(commands)
print(f"[{task.host.name}] Output:\n{output}")
verify = conn.send_command("show system ntp")
print(f"[{task.host.name}] Verification:\n{verify}")
return Result(host=task.host, result=output, changed=True)
start_time = datetime.now()
results = nr.run(task=deploy_services)
end_time = datetime.now()
print_result(results)
print(f"Execution Time: {end_time - start_time}")
Step-by-step breakdown
1. Import the Libraries
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_config
from nornir_utils.plugins.functions import print_result
from nornir.core.task import Result
from netmiko import ConnectHandler
from datetime import datetime
InitNornir loads the framework and inventory. netmiko_send_config is the Nornir plugin task for pushing config — imported but not used directly in this script (we bypass it with raw ConnectHandler instead, due to a nornir-netmiko bug with Nokia SROS 24.10.R5 discovered while testing). print_result gives a formatted per-host output table. Result is the object we return from our task to tell Nornir what happened. ConnectHandler is Netmiko's core SSH connection class. datetime is used to measure total execution time
2. Initialise Nornir from config file
nr = InitNornir(config_file="config.yaml")
This single line reads config.yaml and builds the entire Nornir runtime: the inventory (hosts.yaml + groups.yaml), the runner (ThreadedRunner with 10 workers).
3. Define the task function
def deploy_services(task):
This is a Nornir task — a plain Python function that accepts a single argument called task. Nornir calls this function once per host in your inventory, passing a Task object. Nornir uses threads, this function runs simultaneously across all hosts.
4. Build the device connection dictionary
device = {
"device_type": "nokia_sros",
"host": task.host.hostname,
"username": task.host.username,
"password": task.host.password,
"secret": "",
}
This dictionary is passed directly to Netmiko's ConnectHandler. device_type tells Netmiko which SSH driver to use — nokia_sros loads the Nokia-specific handler that understands SROS prompts and CLI behaviour.
5. Define the CLI commands
commands = [
"configure",
" system",
" time",
" ntp",
" server 192.168.1.1",
" no shutdown",
" exit",
" exit",
" exit",
"exit",
]
In this example Nokia SROS Classic CLI uses a hierarchical config tree. You must navigate down into each level with the context keyword (configure, system, time, ntp) and then back out with exit at each level.
6. Open the SSH connection with ConnectHandler
with ConnectHandler(**device) as conn:
ConnectHandler(**device) unpacks the device dictionary as keyword arguments and opens an SSH session to the router. Using ConnectHandler directly (rather than through nornir-netmiko) bypasses the plugin bug where extras from the Nornir inventory were not being passed through to ConnectHandler.
7. Send the configuration commands
output = conn.send_config_set(commands)
print(f"[{task.host.name}] Output:\n{output}")
send_config_set() sends each command in the list one by one over SSH and captures the router's response after each command. For SROS it handles the prompt detection automatically — it waits for the next prompt before sending the next command. The print statement immediately shows what the router returned
8. Verify the configuration was applied
verify = conn.send_command("show system ntp")
print(f"[{task.host.name}] Verification:\n{verify}")
send_command() sends a single show command and returns the output as a string. Running show system ntp immediately after the config push confirms the NTP server is now present in the running config
9. Return a Result object to Nornir
return Result(host=task.host, result=output, changed=True)
Every Nornir task should return a Result object. host=task.host tells Nornir which device this result belongs to. result=output stores the raw command output in the result
10. Run the task across all hosts and measure time
start_time = datetime.now()
results = nr.run(task=deploy_services)
end_time = datetime.now()
datetime.now() captures the wall-clock time before and after execution
11. Print results and execution time
print_result(results)
print(f"Execution Time: {end_time - start_time}")
print_result() from nornir_utils formats the AggregatedResult into the familiar Nornir output tree
Execution Output
[router1] Output:
configure
A:PE1>config# system
A:PE1>config>system# time
A:PE1>config>system>time# ntp
A:PE1>config>system>time>ntp# server 192.168.1.1
*A:PE1>config>system>time>ntp# no shutdown
*A:PE1>config>system>time>ntp# exit
*A:PE1>config>system>time# exit
*A:PE1>config>system# exit
*A:PE1>config# exit
*A:PE1# exit all
*A:PE1#
[router1] Verification:
===============================================================
NTP Status
===============================================================
Configured : Yes Stratum : -
Admin Status : up Oper Status : up
Server Enabled : No Server Authenticate : No
Clock Source : none
Auth Check : Yes
Auth Keychain :
Current Date & Time: 2026/04/14 00:14:27 UTC
===============================================================
deploy_services************************************************
* router1 ** changed : True ***********************************
vvvv deploy_services ** changed : True vvvvvvvvvvvvvvvvvvv INFO
configure
A:PE1>config# system
A:PE1>config>system# time
A:PE1>config>system>time# ntp
A:PE1>config>system>time>ntp# server 192.168.1.1
*A:PE1>config>system>time>ntp# no shutdown
*A:PE1>config>system>time>ntp# exit
*A:PE1>config>system>time# exit
*A:PE1>config>system# exit
*A:PE1>config# exit
*A:PE1# exit all
*A:PE1#
^^^^ END deploy_services ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Conclusion
We explored the fundamental Nornir concepts, such as creating an inventory, defining and executing tasks (with multithread execution), and handling the results. These are the essential elements for developing complex network automation tasks.
Nornir's true power lies in its extensibility through plug-ins. While using Python directly might seem daunting at first, gaining proficiency with it makes Nornir an excellent choice for complete control over your network automation tasks.
With Nornir, you unlock:
Massive speed improvements
True parallel execution
Full Python flexibility
For large-scale Nokia SR OS environments, Nornir becomes a game-changer for automation efficiency.



