Nornir guide

Nornir, explained: inventory, tasks, plugins, and when to stop building

Nornir is an open source, pure Python automation framework with inventory management and multi-threaded task execution, used directly from Python rather than through a DSL.

Nornir at a glance

Nornir is an open source, pure Python automation framework with inventory management and multi-threaded task execution, used directly from Python rather than through a domain-specific language. Its GitHub repository describes it as “Pluggable multi-threaded framework with inventory management to help operate collections of devices”.

Latest release
3.6.0, released 2 August 2026 (release page on PyPI)
Licence
Apache-2.0
Python
3.10 or newer. PyPI lists Python 3.10 to 3.14
Created by
David Barroso
Current steward
OpsMill (nornir.tech)

Verified on against PyPI, the GitHub repository and release v3.6.0, nornir.tech and the Nornir 3.6.0 documentation. The code examples were checked against the 3.6.0 source. None was run against a network device.

What is Nornir?

Most network automation reaches the same point quickly: you have a task that works on one device, and you now need it on several hundred. That asks for three things around the task itself: a record of which devices exist and how to reach them, a way to run the task on many of them at once, and a clear account of what happened on each. Nornir supplies those three things and leaves the task itself to you.

The Nornir documentation describes it as an automation framework written in Python to be used with Python. It contrasts that with frameworks that hide their implementation language behind a pseudo-language, which the documentation says tends to lack tooling to debug and troubleshoot. With Nornir your automation is ordinary Python, so you can step through it in a debugger, cover it with unit tests, and import any library you already use.

Engineers who like Nornir tend to like exactly that: the inventory is data, the tasks are functions, and the results are objects you can loop over. Nornir does not talk to devices itself. Connection plugins such as Netmiko, NAPALM and Scrapli do that, and Nornir runs them across your inventory. OpsMill, which now stewards the project, calls it one of the most trusted automation frameworks in networking.

Nornir is a good fit when the people writing the automation are comfortable in Python and want full control of the logic. It is deliberately a framework to build with, not a finished application, and the later sections cover what that means in practice.

Who maintains Nornir?

David Barroso created Nornir, and he is the author listed on the project in PyPI. The Nornir community has built the framework and its plugin ecosystem around that core, and the community plugin list in the documentation shows how many different people maintain the pieces.

nornir.tech announces that OpsMill is the new steward of Nornir. Using the project's own words:

  • Nornir “will remain open source and community-driven”.
  • Your plugins, scripts and workflows “keep working exactly as they do today”, and the framework stays free.
  • David Barroso “continues to support the project as a Technical Advisor”.
  • The focus now turns to modernising Nornir and shaping its roadmap together with the community.

OpsMill's announcement adds that it is committing engineering time to maintain Nornir and support the community, and that you can keep using Nornir on its own: OpsMill also builds Infrahub, and says Infrahub is an option that fits naturally, never a requirement. This guide does not speculate about the roadmap. Read nornir.tech and the full announcement for the detail from the people making the commitments.

Core concepts

Nornir has a small number of moving parts. Once you can name them, the documentation and the plugins are much easier to read. The official tutorial walks through the same ideas with a larger example.

How inventory, the runner, tasks and plugins fit together in NornirFour layers from top to bottom. The inventory lists the hosts, groups and defaults. The runner executes your task on each host using a pool of threads. Your tasks are Python functions. Connection plugins such as Netmiko, NAPALM and Scrapli carry each task to the network device.InventoryHosts, groups and defaults, from YAML files or a source of truthRunnerRuns the task on each host, on a pool of worker threadsTasksYour Python functions, and the tasks that plugins provideConnection pluginsNetmiko, NAPALM, Scrapli, then your network devices
The inventory says which hosts exist, the runner executes your task on them, and connection plugins do the talking to each device.

Inventory

The inventory is made of hosts, groups and defaults. The default inventory plugin, SimpleInventory, reads them from three YAML files. A host holds its own settings and lists the groups it belongs to. A value set on the host wins over the same value on its groups, and group values win over defaults. If more than one group sets a value, the group listed first on the host wins. Free-form values go under data, and any task can read them from task.host.

Tasks

A task is a Python function whose first argument is a Task and which returns a Result. Plugins ship ready-made tasks, and you write your own for anything else. A task can call task.run to run other tasks as subtasks on the same host, which is how you group several steps into one unit of work.

The runner and num_workers

The runner decides how a task is executed across the hosts. The default is the threaded runner, which runs the task for each host on a pool of threads sized by num_workers. In the 3.6.0 source the default is 20, and the configuration example in the documentation also sets 20. A serial runner is also included, which runs one host at a time. You set the runner in config.yaml, in code, or with the NORNIR_RUNNER_OPTIONS environment variable, and the configuration reference gives the order of precedence: code, then the configuration file, then the environment.

Set the number of worker threads in code
from nornir import InitNornir

# Code takes precedence over config.yaml, which takes precedence over
# the NORNIR_RUNNER_OPTIONS environment variable.
nr = InitNornir(
    config_file="config.yaml",
    runner={"plugin": "threaded", "options": {"num_workers": 5}},
)

Results

nr.run() returns an AggregatedResult, which behaves like a dictionary from host name to a MultiResult. A MultiResult is a list of Result objects: the first is the task itself and any subtasks follow it. Each Result carries result, changed, diff, failed and any exception.

Read an AggregatedResult: host, then MultiResult, then Result
results = nr.run(task=describe_host)

print(results.failed)  # True if any host failed

for host_name, multi_result in results.items():
    first = multi_result[0]  # the task itself; subtasks follow
    print(host_name, first.failed, first.result)

Filtering

nr.filter() returns a new Nornir object that holds only the matching hosts, so you can run a task on part of the inventory. Keyword arguments match attributes or data values, and F objects add lookups such as __contains__, __in, __any and __all, combined with &, | and ~.

Filter the inventory with keyword arguments and F objects
from nornir import InitNornir
from nornir.core.filter import F

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

ios_hosts = nr.filter(platform="ios")
dc1_hosts = nr.filter(F(groups__contains="dc1"))
dc1_access = nr.filter(F(groups__contains="dc1") & F(role="access"))
not_junos = nr.filter(~F(platform="junos"))

print(list(dc1_access.inventory.hosts))
print(len(not_junos.inventory.hosts))

Processors

Processors are plugins that run code on events: a task starting, a host finishing, a subtask completing. The documentation presents them as an alternative way to deal with results, with the advantage that you handle each host as soon as it completes, without waiting for the rest.

A processor that reports progress as each host finishes
from nornir import InitNornir
from nornir.core.inventory import Host
from nornir.core.task import AggregatedResult, MultiResult, Task


class ProgressPrinter:
    def task_started(self, task: Task) -> None:
        print(f"started: {task.name}")

    def task_completed(self, task: Task, result: AggregatedResult) -> None:
        print(f"completed: {task.name}")

    def task_instance_started(self, task: Task, host: Host) -> None:
        pass

    def task_instance_completed(
        self, task: Task, host: Host, result: MultiResult
    ) -> None:
        state = "failed" if result.failed else "ok"
        print(f"  {host.name}: {state}")

    def subtask_instance_started(self, task: Task, host: Host) -> None:
        pass

    def subtask_instance_completed(
        self, task: Task, host: Host, result: MultiResult
    ) -> None:
        pass


nr = InitNornir(config_file="config.yaml").with_processors([ProgressPrinter()])

Install Nornir and run a first task

Nornir 3.6.0 needs Python 3.10 or newer. Install it into a virtual environment with pip, together with the plugins you will use. Nornir finds installed plugins by itself when you create the Nornir object.

Examples written for Nornir 3.6.0 and checked against its source. Run them against a lab before production. The inventory, runner, filtering, results, processor, failure and Jinja2 rendering examples were also run against a stub inventory using plain Python tasks that open no device connection. None of the examples was run against a network device.

Install Nornir and the plugins this guide uses
python3 -m venv .venv
source .venv/bin/activate
pip install nornir nornir-utils nornir-netmiko nornir-napalm

Take credentials from environment variables instead of writing them into the inventory files, and use documentation addresses (192.0.2.0/24 and 198.51.100.0/24) while you experiment. The load_credentials transform function in nornir_utils reads NORNIR_USERNAME and NORNIR_PASSWORD and sets them on every host.

Set device credentials in the environment, never in the YAML
# Set these in your shell, or load them from a secret store.
# Never commit them to a repository.
export NORNIR_USERNAME="netops"
export NORNIR_PASSWORD="replace-with-your-password"

config.yaml tells Nornir where the inventory files are, how to run and how to log.

config.yaml: where the inventory lives, how to run, how to log
---
inventory:
  plugin: SimpleInventory
  options:
    host_file: "inventory/hosts.yaml"
    group_file: "inventory/groups.yaml"
    defaults_file: "inventory/defaults.yaml"
  # Reads NORNIR_USERNAME and NORNIR_PASSWORD from the environment
  # and sets them on every host.
  transform_function: load_credentials
runner:
  plugin: threaded
  options:
    num_workers: 10
logging:
  level: INFO
  log_file: nornir.log

hosts.yaml lists the devices. The platform on each host comes from its group in the next file.

inventory/hosts.yaml: one entry per device
---
edge-sw01:
  hostname: 192.0.2.11
  groups:
    - ios
    - dc1
  data:
    role: access

edge-sw02:
  hostname: 192.0.2.12
  groups:
    - ios
    - dc1
  data:
    role: access

core-rtr01:
  hostname: 198.51.100.21
  groups:
    - junos
    - dc2
  data:
    role: core
inventory/groups.yaml and defaults.yaml: shared settings
# inventory/groups.yaml
---
ios:
  platform: ios

junos:
  platform: junos

dc1:
  data:
    site: dc1

dc2:
  data:
    site: dc2

# inventory/defaults.yaml
---
port: 22

The first task below reads only the inventory, so it needs no device. If it prints one result per host, the inventory, credentials and runner are wired up correctly. Use it as a smoke test before you add a connection plugin.

A first task that needs no device, to prove the inventory loads
from nornir import InitNornir
from nornir.core.task import Result, Task
from nornir_utils.plugins.functions import print_result


def describe_host(task: Task) -> Result:
    role = task.host.get("role", "unknown")
    return Result(
        host=task.host,
        result=f"{task.host.name} ({task.host.hostname}) has role {role}",
    )


nr = InitNornir(config_file="config.yaml")
results = nr.run(task=describe_host)
print_result(results)

Nornir plugins

Nornir itself is small. Almost everything that touches a device or an outside system is a plugin: connection plugins, tasks, inventory sources, processors and runners. A plugin is an ordinary Python package. When you create the Nornir object, Nornir registers the plugins it finds installed, so you reference them by name in the configuration or import their tasks.

The documentation keeps a community plugin list, with each plugin's type and maintainers. Plugins are separate projects with their own maintainers and release schedules, so check the release date and Python support of each one before you rely on it. The table shows the five used in this guide, as listed on PyPI when it was verified.

Nornir plugins used in this guide, with PyPI package name, release and author
PyPI packageProvidesLatest release on PyPIAuthor on PyPI
nornir-netmikoNetmiko connection plugin and tasks1.0.1, 7 December 2023Kirk Byers
nornir_napalmNAPALM connection plugin and tasks0.6.0, 3 August 2026David Barroso
nornir-scrapliScrapli connection plugins and tasks2025.1.30, 31 January 2025Carl Montanari
nornir-utilsprint_result, YAMLInventory, load_credentials, write_file and other helpers with no external dependencies0.3.0, 3 August 2026David Barroso
nornir_jinja2Tasks that render Jinja2 templates1.0.0, 3 August 2026David Barroso

PyPI treats hyphens and underscores in project names as the same, so pip install nornir-napalm and pip install nornir_napalm install the same package. In Python you import the underscore form, for example nornir_napalm.

nornir_netmiko

The Netmiko plugin runs Netmiko sessions inside Nornir tasks. Netmiko handles prompts, paging and configuration modes for each device, and Nornir runs the work across your inventory. Its tasks include netmiko_send_command, netmiko_send_config, netmiko_save_config and netmiko_file_transfer. We installed version 1.0.1 next to Nornir 3.6.0 and it imported and registered as a connection plugin, but we did not connect to a device. For the Netmiko library itself, read the Netmiko guide.

nornir_netmiko: run a show command on every IOS host
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")
ios_hosts = nr.filter(platform="ios")

results = ios_hosts.run(
    task=netmiko_send_command,
    command_string="show version",
)
print_result(results)

To group several plugin tasks into one unit of work, call them from your own task with task.run.

Group plugin tasks into your own task with task.run
from nornir.core.task import Result, Task
from nornir_netmiko.tasks import netmiko_send_command


def count_config_lines(task: Task) -> Result:
    output = task.run(
        task=netmiko_send_command,
        command_string="show running-config",
    )
    lines = len(output.result.splitlines())
    return Result(host=task.host, result=f"{lines} lines of configuration")


results = ios_hosts.run(task=count_config_lines)

nornir_napalm

The NAPALM plugin gives you NAPALM's getters and its configuration merge and replace, run across the inventory. napalm_get returns structured data, napalm_cli sends raw commands, and napalm_configure loads configuration and supports dry_run to preview the diff first.

nornir_napalm: read structured facts through NAPALM getters
from nornir import InitNornir
from nornir_napalm.plugins.tasks import napalm_get
from nornir_utils.plugins.functions import print_result

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

results = nr.run(task=napalm_get, getters=["facts"])
print_result(results)

for host_name, multi_result in results.items():
    if not multi_result.failed:
        print(host_name, multi_result[0].result["facts"]["os_version"])
nornir_napalm: preview a change with dry_run before applying it
from nornir_napalm.plugins.tasks import napalm_configure
from nornir_utils.plugins.functions import print_result

# dry_run=True asks NAPALM for the diff and does not commit it.
preview = nr.run(
    task=napalm_configure,
    filename="changes/ntp.cfg",
    dry_run=True,
)
print_result(preview)  # print_result shows each host's diff

nornir_scrapli

The Scrapli plugin offers the same pattern with Scrapli as the connection library, and also provides plugins for Scrapli's configuration and NETCONF drivers. In the installed 2025.1.30 source the platform names ios, nxos, iosxr, eos and junos are mapped to their Scrapli names.

nornir_scrapli: the same pattern with the Scrapli plugin
from nornir import InitNornir
from nornir_scrapli.tasks import send_command
from nornir_utils.plugins.functions import print_result

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

results = nr.filter(platform="ios").run(
    task=send_command,
    command="show version",
)
print_result(results)

nornir_utils

nornir_utils is the helper package most scripts install first. It provides print_result and print_title for readable output, the load_credentials transform function used above, an alternative YAMLInventory, and tasks such as write_file, echo_data, load_yaml and tcp_ping.

nornir_utils: print one attribute, then only the hosts that failed
from nornir_utils.plugins.functions import print_result, print_title

print_title("Show version")
print_result(results, vars=["result"])  # skip diff and stdout

print_title("Hosts that failed")
for multi_result in results.failed_hosts.values():
    print_result(multi_result)

nornir_jinja2

The Jinja2 plugin provides template_file and a companion string task, so a per-host configuration can be rendered from a template and the host's inventory data. Rendering is local and needs no device. Pair it with napalm_configure and dry_run=True to review the result before anything is committed.

templates/ntp.j2
ntp server {{ ntp_server }}
nornir_jinja2: render a template per host, then preview with NAPALM
from nornir import InitNornir
from nornir.core.task import Result, Task
from nornir_jinja2.plugins.tasks import template_file
from nornir_napalm.plugins.tasks import napalm_configure
from nornir_utils.plugins.functions import print_result


def render_and_preview(task: Task) -> Result:
    # templates/ntp.j2 contains: ntp server {{ ntp_server }}
    rendered = task.run(
        task=template_file,
        template="ntp.j2",
        path="templates",
        ntp_server="198.51.100.123",
    )
    task.run(
        task=napalm_configure,
        configuration=rendered.result,
        dry_run=True,
    )
    return Result(host=task.host, result="previewed, nothing committed")


nr = InitNornir(config_file="config.yaml")
print_result(nr.run(task=render_and_preview))

Platform names across plugins

One inventory platform value can serve several plugins. In the installed source, the Netmiko plugin translates NAPALM-style names (ios, nxos, nxos_ssh, eos, junos, iosxr) to Netmiko device types such as cisco_ios and juniper_junos, and the Scrapli plugin does the same for its own names. Any other value is passed through unchanged, so use the library's own name for platforms outside those maps.

Dynamic inventory from a source of truth

YAML files are a good start, and they become a second place to keep correct once you already have a source of truth for your devices. An inventory plugin replaces the YAML with a query to that source, so the Nornir inventory is built from it each time you run.

NetBox with nornir_netbox

nornir_netbox is the inventory plugin for NetBox. Use NetBoxInventory2, because in the installed 0.3.0 source the older NBInventory raises a deprecation warning pointing to it. filter_parameters passes NetBox API filters, so you can select devices by site, role or tag, and use_platform_napalm_driver takes each host's platform from the NetBox platform's NAPALM driver setting. The plugin reads NB_URL and NB_TOKEN if you do not pass them, and falls back to a local URL and a placeholder token when they are missing, so set both explicitly. The latest release on PyPI is dated 20 September 2021 and its author is Wim Van Deun, so try it against your NetBox version in a lab first. We did not run it against a NetBox server.

nornir_netbox: build the inventory from NetBox instead of YAML
import os

from nornir import InitNornir

nr = InitNornir(
    inventory={
        "plugin": "NetBoxInventory2",
        "options": {
            "nb_url": os.environ["NB_URL"],
            "nb_token": os.environ["NB_TOKEN"],
            "filter_parameters": {"site": "dc1"},
            "use_platform_napalm_driver": True,
        },
    },
)
print(len(nr.inventory.hosts))

Infrahub with nornir-infrahub

OpsMill, the steward of Nornir, also publishes nornir-infrahub, an inventory plugin for Infrahub, at version 1.2.0 on PyPI (30 July 2026, Apache-2.0). It maps an Infrahub node kind to Nornir hosts and can build groups from attributes or relations. The mappings name attributes in your own Infrahub schema, so the ones below are placeholders to replace with yours. OpsMill states that using Nornir does not require Infrahub.

nornir-infrahub: build the inventory from Infrahub
import os

from nornir import InitNornir

nr = InitNornir(
    inventory={
        "plugin": "InfrahubInventory",
        "options": {
            "address": os.environ["INFRAHUB_ADDRESS"],
            "token": os.environ["INFRAHUB_API_TOKEN"],
            "host_node": {"kind": "InfraDevice"},
            "schema_mappings": [
                {"name": "hostname", "mapping": "primary_address.address"},
                {"name": "platform", "mapping": "platform.nornir_platform"},
            ],
            "group_mappings": ["site.name"],
        },
    },
)
print(list(nr.inventory.hosts))

rConfig's own integration pages

If NetBox or Infrahub is also your source of truth for rConfig, see rConfig with NetBox and rConfig with Infrahub, which describe using them for configuration backup, change tracking and compliance evidence. Those pages describe rConfig's integrations with NetBox and Infrahub, not an integration with Nornir. The integrations overview lists the rest.

Nornir vs Ansible

Both run the same kind of work across many devices, so the comparison comes up often. They differ mostly in how you write and review the automation, not in what network operations they can drive. Neither is the better tool in the abstract.

Nornir and Ansible compared on authoring, inventory, concurrency, device connections and ecosystem
AspectNornirAnsible
How you write automationPython. Tasks are functions, run from a script you can step through in a debugger and cover with tests.Playbooks, which the documentation says you express in YAML format with a minimum of syntax. Ansible itself is written and executed in Python, and the network guide describes separating the data model (playbook or role) from the execution layer (modules). Source
InventoryHosts, groups and defaults, from YAML files with SimpleInventory or from an inventory plugin such as NetBox or Infrahub. A host value beats its groups, which beat the defaults.Composed from one or more inventory sources. INI and YAML are built in, inventory plugins support other formats and dynamic sources, and a group can have multiple parents and children. Source
ConcurrencyA pool of threads in one Python process, set by num_workers, which defaults to 20 in the 3.6.0 source. A serial runner is also included.Forks, which default to 5. The default linear strategy runs each task on all hosts in a play before starting the next task. The free strategy lets each host run to the end of the play as fast as it can, and the serial keyword runs a play in batches of hosts. Source
Talking to network devicesThrough connection plugins such as Netmiko, NAPALM and Scrapli, which you choose per task.Network modules run on the control node, and the ansible_connection variable selects the protocol: network_cli (CLI over SSH), netconf (XML over SSH) or httpapi (API over HTTP or HTTPS). Each needs the network_os setting. Source
EcosystemA community plugin list in the Nornir documentation, plus the whole Python ecosystem for anything else. SourceCollections organised by network platform, for example arista.eos, cisco.ios, cisco.nxos and junipernetworks.junos, published on Ansible Galaxy. Source

The Ansible column restates the Ansible documentation, read on 2 October 2026, and each cell links the page it comes from. The defaults are from the documentation and Ansible's configuration schema, and they can change between releases, so check the forks setting and the strategies page for your version.

When Nornir fits

Nornir tends to suit a team that is comfortable in Python and wants full control of the logic: branching on results, calling an API partway through a task, reusing a library, or writing unit tests for the automation. Everything is Python, so the usual engineering practices apply without translation.

When Ansible fits

Ansible tends to suit a team that already runs it, or that prefers automation written as YAML playbooks that people who are not Python developers can read and review. Its network guide presents it as one agentless tool that network, operations and development teams can all use, and its platform collections cover a wide range of vendors. If your servers and network are automated in the same tool today, that is a real advantage.

Using both

They are not exclusive. Some teams use Ansible for the work that fits its model and Nornir where they need custom logic, and Nornir's community plugin list includes an inventory plugin that reads Ansible inventories, so one inventory can feed both.

Nornir, Netmiko and NAPALM

Netmiko and NAPALM are libraries that talk to devices. Nornir does not replace them. It drives them through the nornir_netmiko and nornir_napalm plugins, adding the inventory, the concurrency and the results handling around them. For what Netmiko itself does, how it differs from the other libraries and how to use it directly, read the Netmiko guide.

Troubleshooting common Nornir problems

Most first problems are one of five things: a host that failed and is now being skipped, connection settings that differ by platform, too many threads, silent logging, or an inventory that does not load. Everything below is behaviour that the Nornir 3.6.0 documentation or source confirms. The documentation covers more.

Failed hosts

By default a task that raises an exception does not stop the run. Nornir records the failure in the result and adds the host to a set of failed hosts, and later calls to nr.run() skip those hosts unless you pass on_failed=True. Inspect results.failed and results.failed_hosts, and read multi_result[0].exception for the cause. Clear the list with nr.data.reset_failed_hosts(), or one host with nr.data.recover_host(name). If you would rather stop at the first failure, set raise_on_error=True, which raises NornirExecutionError. When a subtask fails, the task that called it stops at that point and the failure is recorded for the host.

Inspect failed hosts, retry them, and reset the failed list
from nornir import InitNornir
from nornir.core.exceptions import NornirExecutionError
from nornir.core.task import Result, Task


def flaky(task: Task) -> Result:
    if task.host.name == "edge-sw02":
        raise ConnectionError("simulated: device unreachable")
    return Result(host=task.host, result="ok")


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

results = nr.run(task=flaky)
for host_name, multi_result in results.failed_hosts.items():
    print(host_name, repr(multi_result[0].exception))

# A failed host is skipped by later runs unless you ask for it.
again = nr.run(task=flaky)
print(sorted(again))  # edge-sw02 is not in here

retry = nr.run(task=flaky, on_failed=True, on_good=False)
print(sorted(retry))  # only edge-sw02

nr.data.reset_failed_hosts()  # every host is eligible again

# Or have Nornir raise instead of recording the failure.
try:
    nr.run(task=flaky, raise_on_error=True)
except NornirExecutionError as error:
    print("raised:", sorted(error.failed_hosts))

netmiko_send_config also raises ValueError when the run is a dry run, because in the installed nornir_netmiko source it does not support dry runs. Use napalm_configure with dry_run=True when you want a preview.

Connection options per platform

Connection settings that work on one platform are often wrong on another: a slower device needs a longer timeout, a different OS needs a different driver name. Put them on the group, under connection_options, keyed by the connection plugin. The extras dictionary is passed to the underlying library, so for Netmiko it holds ConnectHandler arguments such as conn_timeout, and for NAPALM it holds driver arguments such as timeout. If a connection fails at once, also check the platform name: the Netmiko and Scrapli plugins translate only the names listed in the plugins section and pass the rest through.

Per-platform connection options in groups.yaml
---
ios:
  platform: ios
  connection_options:
    netmiko:
      extras:
        conn_timeout: 30   # passed to Netmiko's ConnectHandler
    napalm:
      extras:
        timeout: 120       # passed to the NAPALM driver

junos:
  platform: junos

Thread count and device rate limits

The threaded runner uses a thread pool of num_workers threads, and in the 3.6.0 source the default is 20. Nornir does not otherwise pace the work, so 20 simultaneous logins can be too many for a TACACS+ or RADIUS server, a jump host or an older device that limits concurrent sessions. If you see timeouts or login failures only on large runs, lower num_workers, or use the serial runner to confirm that concurrency is the cause. Raise it again gradually once the run is stable.

Logging

Nornir configures logging itself when you create the Nornir object. By default it writes INFO level messages to nornir.log in the working directory, rotating the file at 10 MB, and does not print to the console. If a task fails and the console shows nothing, read that file. Set level: DEBUG in the logging section for more detail, and to_console: true to see it live. If your application already configures Python logging, Nornir emits a ConflictingConfigurationWarning; set logging.enabled to false and let your own configuration take over.

Turn on debug logging, written to nornir.log
---
logging:
  level: DEBUG
  log_file: nornir.log
  to_console: false

Inventory loading errors

An inventory problem stops InitNornir before any task runs, which is the best place for it. The three common ones behave as follows. A missing file raises FileNotFoundError naming the path, so check the paths in config.yaml against the directory you run from. Invalid YAML raises a parser error from the YAML library, with the line of the problem. A host that lists a group missing from groups.yaml raises a KeyError naming that group. After a change to the inventory, a short script that loads it and prints the hosts, groups and whether credentials are set is a quick check.

Check the inventory loads before running anything
from nornir import InitNornir

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

print(len(nr.inventory.hosts), "hosts")
print(sorted(nr.inventory.groups))
for name, host in nr.inventory.hosts.items():
    print(name, host.hostname, host.platform, host.username is not None)

What it takes to run Nornir at enterprise scale

Nornir is a framework, and it is deliberately scoped as one: it runs your tasks across an inventory and reports the results. Running automation across a whole estate means providing, integrating or maintaining several capabilities around it. Many organisations already have some of them, such as a source of truth, a scheduler, a secret store, an identity provider and central logging, so the work is often integration, not building from scratch.

The list below is a set of requirements to check, not a build plan. Which ones apply depends on your estate, your auditors and your team. This is engineering work that comes with building on a framework, and none of it is a flaw in Nornir.

  • Inventory and a source of truth

    Nornir needs to know which devices exist, their platforms and their addresses. YAML files work for a lab. At scale somebody has to keep them correct as devices are added, moved and retired, or you point an inventory plugin at the system that already holds the truth.

  • Concurrency and rate limiting

    The runner decides how many hosts run at once, and it does nothing to protect your AAA servers or the devices themselves. Choosing num_workers, and pacing work against device limits, is your decision.

  • Scheduling

    Regular collection needs something to start it, retries for unreachable devices, and a way to see that a run finished. Cron, a CI system or an orchestrator may cover the scheduling, leaving the retries and the visibility to integrate.

  • Configuration versioning and diff

    Collecting a configuration is one step. The requirement is to keep each version with its device and time, compare versions sensibly while ignoring noise such as timestamps, and present the result in a way people will read. A Git repository, an existing archive or a dedicated tool can fill that role.

  • Compliance checks

    Checking configurations against policy means defining the rules, running them on each collection and reporting results by device and by rule. Where an auditor needs the result, it should be repeatable.

  • Role-based access

    Once more than one person uses the automation, who may run which task against which devices needs an answer. Permissions you already have in a CI system or on a jump host may provide part of it.

  • Sign-in for people (SSO and RADIUS)

    If your automation is a service that people use, they will usually sign in through the identity provider or RADIUS server you already run. This is separate from how the automation signs in to your network devices.

  • Credentials for devices, and rotation

    Reading device credentials from the environment is the right start. At scale the requirement is a secret store, accounts scoped per device or group, and a way to rotate credentials without breaking the scheduled jobs that use them.

  • Audit trail

    Auditors may ask who ran what, against which device, and with what result. Existing logging, CI job history or a SIEM may already capture some of it. The requirement is that the evidence exists and is kept for as long as you need it.

  • High availability

    If runs start from one host, that host becomes part of the operations process. Your existing platform for scheduled jobs may already be resilient, and the requirement is to make sure the automation benefits from it.

  • Ownership of plugin and driver breakage

    Plugins are separate projects with their own release schedules, and the libraries beneath them follow vendor changes. A prompt, a banner or a command output can change between OS releases. Somebody has to own noticing, fixing and testing against each new release, and deciding when a plugin you depend on needs a replacement.

Using Nornir alongside rConfig

“Alongside” means the two can coexist. rConfig can take on the configuration management responsibilities described above, and your custom Python workflows can keep using Nornir. This guide does not describe a native integration between them.

What rConfig can cover, by edition

Not every edition includes every capability. This is how the current edition comparison on the rConfig site divides the capabilities discussed in this guide.

rConfig capabilities by edition, as listed in the edition comparison
EditionAdds
rConfig Core (free, open source)Scheduled backups, Compare configuration versions, Single sign-on
Starter and abovePolicy checks and compliance status, Role-based access control, RADIUS authentication, LDAP and Active Directory, User audit log, Restore a previous configuration, Push configuration changes
Standard and aboveCheck compliance continuously, Evidence you can hand to an auditor
Enterprise / MSPDistributed collectors, High availability, Manage every rConfig instance centrally

What stays with Nornir

rConfig does not claim to match Nornir for scripted change or automation. Custom workflows, bespoke logic and one-off jobs are where Nornir remains the right tool, and teams can keep it for those while rConfig handles collection, history and review. See how automation and configuration management differ and the rConfig solution overview.

When it makes sense to stop building

This is not an argument against Nornir. It is a good fit for custom automation, and plenty of teams keep their Nornir code running alongside a platform for exactly that. Whether to keep building the surrounding capabilities, adopt a platform for part of the work, or do both depends on your requirements, the engineering capacity you can give the work, what the surrounding capabilities cost to maintain, and the infrastructure you already have. If your source of truth, scheduler, secret store and logging already meet the requirements in the previous section, extending your scripts may be the best answer.

These questions can help you decide:

  • Does an auditor need evidence that your scripts do not produce today?
  • Is maintaining the surrounding capabilities taking more of your time than the automation itself?
  • Do several teams need controlled access to the same collected configurations?
  • Is the work described in the previous section growing faster than the capacity you have to build and maintain it?

If the answers point that way, a platform that supplies some of those capabilities may be worth evaluating next to what you already run.

Evaluate rConfig Core alongside your existing scripts

rConfig V8 Core is free and open source, with no time limit and no device limit, so you can run it next to your Nornir code and see which of the requirements above it covers for your estate. If you want to talk through the paid editions, you can book a demo.

Nornir FAQ

What is Nornir?

Nornir is an open source, pure Python automation framework with inventory management and multi-threaded task execution. You use it directly from Python rather than through a domain-specific language, so you can write, debug and test your automation with ordinary Python tools.

Is Nornir a Python library or a framework?

Its own repository describes it as a pluggable multi-threaded framework, and its documentation calls it an automation framework written in Python to be used with Python. You install it with pip and import it, like a library, but it also supplies the structure around your code: an inventory, a runner that executes tasks across hosts, and a plugin system for connections and data sources.

Who maintains Nornir?

David Barroso created Nornir. nornir.tech states that OpsMill is now the steward of the project, that Nornir will remain open source and community-driven, and that Barroso continues to support it as a Technical Advisor.

How do I install Nornir?

Create a virtual environment and run pip install nornir, plus the plugins you need, for example nornir-utils, nornir-netmiko or nornir-napalm. Nornir 3.6.0 needs Python 3.10 or newer. Credentials are best read from environment variables rather than written into the inventory files.

Should I use Nornir or Ansible?

They are not mutually exclusive, and the choice is mostly about how your team prefers to write and review automation. Nornir tasks are Python functions, run on a thread pool that defaults to 20 workers in the 3.6.0 source. Ansible playbooks are written in YAML, and by default Ansible runs five forks with the linear strategy, which finishes each task on all hosts before starting the next. For network devices Ansible offers the network_cli, netconf and httpapi connection plugins. Pick Nornir for custom logic and Python testing, Ansible where YAML playbooks and an existing Ansible estate suit your team, and note that some teams use both.

How does Nornir use Netmiko?

Through the nornir_netmiko plugin, which provides a Netmiko connection plugin and tasks such as netmiko_send_command and netmiko_send_config. Netmiko handles each device session, while Nornir handles the inventory and runs the task across many hosts.

How does Nornir use NAPALM?

Through the nornir_napalm plugin, which provides a NAPALM connection plugin and tasks such as napalm_get, napalm_cli and napalm_configure. NAPALM supplies the cross-vendor getters and configuration merge and replace, while Nornir runs them across your inventory.

Can Nornir use NetBox as its inventory?

Yes. The nornir_netbox plugin is an inventory plugin that builds the Nornir inventory from NetBox over its API, with filter parameters to select devices. Check the plugin against your NetBox version in a lab first, because its latest PyPI release is dated 20 September 2021.

What are Nornir plugins?

Plugins are installable Python packages that add inventory sources, connection types, tasks, processors and runners to Nornir. The Nornir documentation keeps a community plugin list, and plugins register themselves when installed, so you name them in your configuration or import their tasks.

Is Nornir free?

Yes. Nornir is open source under the Apache-2.0 licence, and nornir.tech states that the framework stays free.

Can I use Nornir alongside rConfig?

Yes, they can run side by side. Teams keep Nornir for custom workflows and scripted work, and can use rConfig for configuration management. rConfig Core is free and open source and covers scheduled backups, comparison between configuration versions and single sign-on. Compliance policy checks, role-based access control, RADIUS sign-in and a user audit log are in the paid editions from Starter, scheduled compliance checks and reports from Standard, and distributed collection and high availability in Enterprise and MSP. This guide does not describe a native integration between rConfig and Nornir.

Further reading and credit

Nornir

Plugins used in this guide

On rConfig

Thank you to David Barroso, who created Nornir; to OpsMill, for taking up its stewardship; and to the contributors who have built and maintained Nornir and its plugins for the whole community.