# 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`

```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
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">YAML configuration file is defined for Nornir settings. You could define these settings directly using a Python dictionary</div>
</div>

**File** : `inventory/groups.yaml`

```yaml
sros:
  platform: nokia_sros
  username: <username>
  password: <password>
  connection_options:
    netmiko:
      extras:
        secret: ""
        global_delay_factor: 2
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Nokia SROS does NOT <strong>use enable mode </strong>hence an "empty" secret is defined to overcome a nornir-netmiko issue</div>
</div>

**File** : `inventory/hosts.yaml`

```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
python -m venv .venv
source .venv/bin/activate
pip install nornir nornir-utils nornir-netmiko nornir-napalm
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">A virtual environment is an <strong>isolated Python workspace</strong> where you can install packages <strong>without affecting the system-wide Python installation</strong></div>
</div>

## 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.

```python
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**

```python
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**

```python
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**

```python
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**

```python
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**

```python
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**

```python
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**

```python
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**

```python
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**

```python
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**

```python
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**

```python
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

```python
[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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This was tested on a Nokia SROS running 24.10.R4 release</div>
</div>

# 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.
