Skip to main content

Command Palette

Search for a command to run...

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?

Published
10 min read
Why Nornir matters after you have met Ansible ?

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
💡
YAML configuration file is defined for Nornir settings. You could define these settings directly using a Python dictionary

File : inventory/groups.yaml

sros:
  platform: nokia_sros
  username: <username>
  password: <password>
  connection_options:
    netmiko:
      extras:
        secret: ""
        global_delay_factor: 2
💡
Nokia SROS does NOT use enable mode hence an "empty" secret is defined to overcome a nornir-netmiko issue

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
💡
A virtual environment is an isolated Python workspace where you can install packages without affecting the system-wide Python installation

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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
💡
This was tested on a Nokia SROS running 24.10.R4 release

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.