Nornir Automation Framework: Deep Dive
Everything You Need to Know
Table of Contents
- Overview
- Themes and Symbolic Aspects
- Project Structure
- Comparative Analysis
- Nornir Architecture Flow Chart
- Core Components
- Conclusion
Overview of Nornir Automation
What is Nornir?
Nornir is an open-source, Python-based automation framework designed specifically for network automation. Unlike other automation tools that require you to write configurations in a domain-specific language (DSL) like YAML, Nornir lets you use pure Python to define, control, and execute automation tasks. This gives engineers and developers more flexibility, power, and integration options with the broader Python ecosystem.
Key characteristics of Nornir:
- Python-native: Tasks and workflows are written directly in Python.
- Multi-threaded: Executes tasks concurrently across multiple devices, improving efficiency for large-scale networks.
- Plugin-based: Easily extended with plugins for device communication, inventory management, and custom features.
- Inventory management: Built-in system for organizing and targeting devices.
- Open-source and community-driven: Supported by a growing user base and plugin library.
Why You Need to Know About Nornir
Nornir is gaining traction among network engineers and automation professionals for several reasons:
- Fine-grained Control: Because you write automation logic in Python, you can create highly customized workflows and integrate with any Python library or API.
- Scalability: Nornir’s multi-threaded design allows automation tasks to run in parallel across hundreds or thousands of devices, making it ideal for enterprise-scale environments.
- Flexibility: You’re not locked into a specific DSL or workflow. Nornir can be adapted for configuration management, network discovery, compliance checks, state validation, and more.
- Efficiency: Automates repetitive, error-prone tasks, freeing up time for more strategic work and reducing the risk of manual errors.
- Consistency and Compliance: Ensures that configurations and changes are applied uniformly across all devices, improving reliability and compliance with organizational standards.
- Integration: Seamlessly integrates with popular Python libraries like Netmiko and Napalm, as well as inventory sources like NetBox.
How Nornir Works
Nornir’s workflow can be summarized in a few core concepts:
- Inventory Management
- Devices (hosts) are defined in inventory files (often YAML or via plugins).
- Inventory contains details like IP addresses, credentials, device types, and groupings.
- Task Definition
- Tasks are Python functions that define what you want to do (e.g., send a command, push a configuration, gather data).
- Tasks can use plugins (like Netmiko or Napalm) to interact with devices.
- Parallel Execution
- Nornir runs tasks concurrently across all targeted devices using multi-threading.
- Each device runs the task independently, and results are aggregated for easy review.
- Result Handling
- Results are structured objects, not just raw text, making it easy to analyze outcomes, handle errors, and take further actions programmatically.
- Extensibility
- The plugin architecture allows you to add or customize inventory sources, task types, and result processing as needed.
Example Workflow
- Define your network inventory (devices, groups, credentials).
- Write a Python function (task) to perform an action (e.g., retrieve interface status).
- Use Nornir to execute this task across all devices in your inventory, in parallel.
- Collect and analyze structured results to verify success or troubleshoot issues.
Nornir stands out by offering the power and flexibility of Python for network automation, making it a preferred choice for professionals who want scalable, customizable, and efficient automation without the limitations of a DSL
Themes and Symbolic Aspects of Nornir Automation
Understanding Nornir automation requires grasping its foundational themes and how they shape its use in network engineering:
- Python-Native Approach: Nornir is built for Python users, allowing automation tasks to be written in pure Python. This empowers engineers to leverage the full Python ecosystem and create highly customized workflows.
- Inventory-Centric Design: The framework centers around a structured inventory system, where network devices and their attributes are defined. This enables precise targeting and grouping of devices for automation tasks.
- Parallel Execution: Nornir runs tasks concurrently across multiple devices using multi-threading. This parallelism dramatically speeds up operations and is essential for scaling automation in large environments.
- Plugin Ecosystem: Extensibility is a core theme. Nornir supports plugins for device communication (like Netmiko or Napalm), inventory sources, and custom tasks, making it adaptable to diverse network environments.
- Structured Results: Results from automation tasks are returned as structured Python objects, not just raw text. This makes it easier to analyze, process, and act on outcomes programmatically.
- Integration and Flexibility: Nornir integrates smoothly with other Python libraries and external systems, allowing for advanced automation scenarios, reporting, and compliance checks.
Recommended Project Structure for Nornir Network Automation
Organizing your Nornir automation project with a clear structure is essential for scalability, reusability, and maintainability. Below is a best-practices project layout, with explanations and examples for creating reusable modules and using Nornir’s terminology effectively. This example also explains the difference between static and dynamic inventory, and how to use NetBox as a dynamic inventory source.
-
Project Directory Layout:
A typical Nornir project should be organized as follows:
nornir_project/ ├── inventory/ │ ├── hosts.yaml │ ├── groups.yaml │ └── defaults.yaml ├── templates/ │ └── config_template.j2 ├── modules/ │ ├── __init__.py │ ├── vlan.py │ ├── backup.py │ └── utils.py ├── scripts/ │ ├── configure_vlans.py │ └── backup_configs.py ├── config.yaml ├── requirements.txt └── README.md
- inventory/: All device, group, and default parameter definitions (YAML).
- templates/: Jinja2 templates for dynamic configuration generation.
- modules/: Custom, reusable Python modules for tasks and logic.
- scripts/: Entry-point scripts that import and run tasks/workflows.
- config.yaml: Nornir configuration file (inventory plugin, runner, etc.).
- requirements.txt: Python dependencies.
-
Static Inventory Explained:
By default, Nornir projects use a static inventory—device data is defined in YAML files (
hosts.yaml
,groups.yaml
,defaults.yaml
) under theinventory/
directory. This approach is simple and great for small or stable environments.
Example:# inventory/hosts.yaml router1: hostname: 192.168.1.1 groups: - cisco_ios data: role: core_router location: main_datacenter
# inventory/groups.yaml cisco_ios: platform: ios connection_options: netmiko: extras: device_type: cisco_ios
# inventory/defaults.yaml username: admin password: secure_password
- Easy to version control and audit.
- Manual updates required as devices change.
- Best for static or small environments.
-
Dynamic Inventory with NetBox:
For larger or frequently changing networks, a dynamic inventory is preferred. NetBox is a popular Source of Truth (SoT) and can be used as Nornir’s inventory via the
nornir_netbox
plugin.
How to Use NetBox as Inventory:- Install the NetBox inventory plugin:
python3 -m pip install nornir-netbox
- Update
config.yaml
to use NetBoxInventory2:# config.yaml inventory: plugin: NetBoxInventory2 options: nb_url: "https://netbox.local:8000" nb_token: "YOUR_NETBOX_API_TOKEN" runner: plugin: threaded options: num_workers: 10
- Initialize Nornir in your script:
from nornir import InitNornir nr = InitNornir(config_file="config.yaml")
- How it works:
- Devices, groups, and attributes are pulled live from NetBox.
- Groups are created based on attributes like site, platform, device_role, device_type, and manufacturer.
- All other device attributes are available under
host.data
in Nornir. - Inventory is always up-to-date with NetBox, reducing manual effort and errors.
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") result = nr.run(task=netmiko_send_command, command_string="show version") print_result(result)
- Install the NetBox inventory plugin:
-
Best Practices for Inventory:
- Use static inventory for small, stable environments or testing.
- Use NetBox for dynamic, scalable, and real-time inventory management in production.
- Keep credentials and sensitive data out of version control—use environment variables or secret managers.
- Document inventory structure and update procedures.
-
Reusable Modules and Templates:
Place reusable task logic in
modules/
and templates intemplates/
. This keeps your code organized and easy to maintain.
Example VLAN Module:# modules/vlan.py from nornir_netmiko.tasks import netmiko_send_config def configure_vlans(task, vlan_list): commands = [] for vlan in vlan_list: commands.append(f"vlan {vlan['id']}") commands.append(f"name {vlan['name']}") task.run(task=netmiko_send_config, config_commands=commands)
Example Jinja2 Template:# templates/config_template.j2 hostname {{ host.name }} interface Loopback0 ip address {{ host.loopback_ip }} 255.255.255.255
-
Summary Table: Static vs. Dynamic Inventory
Inventory Type Source Update Method Best For Static YAML files Manual Small, stable environments, testing Dynamic (NetBox) NetBox API Automatic (live sync) Large, dynamic, production environments
Comparative Analysis
This section compares Nornir with other popular network automation frameworks, highlighting their core differences and use cases through the lens of the network automation lifecycle (Day 0, Day 1, Day 2):
Network Automation Lifecycle: Day 0, Day 1, Day 2
- Day 0: Network design, infrastructure planning, and device bootstrapping.
- Day 1: Initial configuration and deployment of network services.
- Day 2: Ongoing operations—monitoring, compliance, updates, troubleshooting, and scaling.
How Nornir Compares to Other Tools
Tool | Language/Interface | Flexibility |
---|---|---|
Nornir | Python-native | Maximum, full Python logic |
Ansible | YAML (DSL) | Moderate, limited advanced logic |
Terraform | HCL (HashiCorp Config Lang) | Declarative, less procedural |
Pulumi | General-purpose languages | High, supports Python, Go, etc. |
Chef | Ruby DSL | Moderate, Ruby required |
SaltStack | YAML/Jinja2 + Python | High, event-driven extensions |
Execution Model
Tool | Execution Model | Parallelism | Agent Requirement |
---|---|---|---|
Nornir | Multi-threaded, parallel | High | Agentless |
Ansible | Sequential (forks for parallelism) | Moderate | Agentless |
Terraform | Plan/apply cycle | Resource-based | Agentless |
Pulumi | Plan/apply, procedural | Resource-based | Agentless |
Chef | Agent-based (Chef client) | Moderate | Agent required |
SaltStack | Agent-based (minions) | High, async | Agent required |
Inventory and State Management
Tool | Inventory Handling | State Awareness |
---|---|---|
Nornir | YAML, plugins, Python | Ephemeral, user-managed |
Ansible | INI/YAML/dynamic inventory | Ephemeral, user-managed |
Terraform | HCL, state files | Persistent, stateful |
Pulumi | Code, state files | Persistent, stateful |
Chef | Node objects, data bags | Persistent (server) |
SaltStack | Roster, external sources | Persistent, event-driven |
Extensibility and Ecosystem
- Nornir: Highly extensible via plugins for inventory, connections, tasks, and result processing.
- Ansible: Large ecosystem of modules and plugins, but advanced customization requires Python knowledge.
- Terraform/Pulumi: Extensive provider ecosystems for cloud and infrastructure, less focus on network device automation.
- Chef/SaltStack: Mature ecosystems for server configuration, with some network device support.
Lifecycle Alignment: Day 0, Day 1, Day 2
Tool | Day 0 (Design/Bootstrap) | Day 1 (Deploy/Config) | Day 2 (Operate/Update) |
---|---|---|---|
Nornir | Limited (can bootstrap via Python) | Excellent (custom, parallel, Python logic) | Excellent (monitoring, compliance, troubleshooting) |
Ansible | Moderate (templates, playbooks) | Good (playbooks, roles) | Good (idempotent tasks, audits) |
Terraform | Excellent (infra provisioning) | Good (initial config) | Moderate (state drift detection) |
Pulumi | Excellent (infra provisioning) | Good (initial config) | Moderate (state drift detection) |
Chef | Moderate (bootstrapping nodes) | Good (policy enforcement) | Good (ongoing config management) |
SaltStack | Moderate (event-driven bootstrap) | Good (state modules) | Excellent (event-driven ops) |
Why Nornir is Mantra Networking’s Standard for Day 1 and Day 2 Operations
- Day 1 (Deployment/Configuration):
- Nornir’s Python-native approach allows Mantra Networking to build custom, reusable, and complex workflows tailored to each network environment.
- Its parallel execution model means configuration changes and deployments are fast and scalable across hundreds or thousands of devices.
- Inventory flexibility lets teams target exactly the right devices and groups for each deployment.
- Day 2 (Operations/Compliance/Troubleshooting):
- Nornir’s structured results and Python integration make it ideal for monitoring, compliance checks, and automated troubleshooting.
- Engineers can build advanced scripts for tasks like config drift detection, audit reporting, or automated remediation, all within Python.
- The plugin ecosystem allows for seamless integration with monitoring tools, ticketing systems, and custom dashboards.
- Why Not Others for Day 1/Day 2?
- Ansible is excellent for simple, repeatable tasks but less suited for highly customized, logic-heavy workflows.
- Terraform/Pulumi shine in infrastructure provisioning (Day 0) but lack the device-by-device control and operational flexibility needed for Day 1/Day 2 network ops.
- Chef/SaltStack are powerful for server config management but are less Pythonic and have steeper learning curves for network engineers.
Summary Table: Nornir vs. Other Tools Across the Lifecycle
Tool | Day 0 (Provision) | Day 1 (Deploy/Config) | Day 2 (Operate/Update) | Best For |
---|---|---|---|---|
Nornir | Moderate | Excellent | Excellent | Python-driven, custom workflows |
Ansible | Good | Good | Good | Simple automation, mixed envs |
Terraform | Excellent | Good | Moderate | Infra provisioning, cloud |
Pulumi | Excellent | Good | Moderate | Infra provisioning, multi-language |
Chef | Moderate | Good | Good | Server config mgmt, policy |
SaltStack | Moderate | Good | Excellent | Event-driven, large-scale ops |
Nornir Architecture Flow Chart
Understanding the flow of Nornir’s architecture is essential for efficient development and troubleshooting. The following flow chart provides a visual reference for how the core components interact during a typical Nornir automation workflow, whether you use static YAML, NetBox, or Nautobot for inventory, and Netmiko, Napalm, or REST APIs for automation tasks.
+--------------------+ | 1. Initialize | | Nornir | +--------------------+ | v +--------------------+ | 2. Load Inventory | | (YAML/NetBox/ | | Nautobot) | +--------------------+ | v +---------------------------+ | 3. Register Plugins | | - Inventory Plugin | | - Task Plugin | | - Connection Plugin | | - Processor Plugin | +---------------------------+ | v +------------------------------+ | 4. Define & Run Tasks | | - Python functions/plugins | | - Use Runner (Threaded/ | | Serial) for execution | +------------------------------+ | v +-------------------------------+ | 5. Collect & Process Results | | - Result objects per host | | - Processor plugins handle | | output/logging/errors | | - Export/filter/integrate | +-------------------------------+
-
Step-by-Step Flow:
-
Initialize Nornir:
- Load configuration (
config.yaml
or Python dict). - Select inventory plugin (SimpleInventory, NetBox, Nautobot).
- Load configuration (
-
Inventory Loaded:
- Hosts, groups, and defaults are built from static or dynamic sources.
- Host data is available for filtering and task parameterization.
-
Plugin Registration:
- Inventory plugin: Loads and manages device data.
- Task plugin: Defines automation actions (Netmiko, Napalm, custom REST tasks).
- Connection plugin: Manages how Nornir connects (SSH, NETCONF, HTTP).
- Processor plugin: Handles result formatting, logging, or custom processing.
-
Task Definition & Execution:
- Tasks are written as Python functions or use plugins.
- Runner (Threaded or Serial) manages parallel or sequential execution across inventory.
-
Result Collection & Processing:
- Each task returns a structured Result object per host.
- Processor plugins handle output, logging, and error reporting.
- Results can be exported, filtered, or integrated with external systems.
-
Initialize Nornir:
-
Best Practices:
- Centralize configuration and inventory for consistency and scalability.
- Leverage plugin system for modularity and vendor abstraction.
- Use runner concurrency settings to balance speed and system resources.
- Always inspect and handle results for robust automation and troubleshooting.
Core Components
Nornir is a powerful Python automation framework designed for network engineers. Its architecture is modular, flexible, and built for scalability. Below, each core component is explained in detail, with step-by-step examples showing how to use Netmiko, Napalm, and REST APIs (with requests and Jinja2) for real-world automation tasks.
-
Inventory:
The inventory is the foundation of Nornir. It defines all network devices, their groups, connection parameters, and custom data. Nornir supports inventory sources like YAML files, NetBox, or custom plugins.
Step-by-step:- Create inventory files:
# hosts.yaml router1: hostname: 192.168.1.1 groups: - routers platform: ios switch1: hostname: 192.168.1.2 groups: - switches platform: eos
- Configure Nornir to use the SimpleInventory plugin:
from nornir import InitNornir nr = InitNornir(config_file="config.yaml")
- Verify inventory:
print(nr.inventory.hosts)
- Create inventory files:
-
Plugins:
Nornir’s extensibility comes from its plugin system. There are several types:
- Inventory Plugins: Load and manage device inventories from various sources.
- Task Plugins: Encapsulate automation logic, such as sending commands or deploying configs.
- Connection Plugins: Manage how Nornir connects to devices (e.g., SSH, NETCONF, HTTP).
- Processor Plugins: Allow custom processing of task results, such as logging or error handling.
-
Netmiko (SSH CLI):
from nornir_netmiko.tasks import netmiko_send_command result = nr.run(task=netmiko_send_command, command_string="show version") for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result}")
-
Napalm (Multi-vendor abstraction):
from nornir_napalm.tasks import napalm_get result = nr.run(task=napalm_get, getters=["facts"]) for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result['facts']}")
-
REST API with requests + Jinja2 templating:
import requests from jinja2 import Template def rest_api_task(task): payload_template = Template('{"hostname": "{{ host.hostname }}"}') payload = payload_template.render(host=task.host) response = requests.post( url=f"https://{task.host.hostname}/api/config", headers={"Content-Type": "application/json"}, data=payload, auth=("admin", "password"), verify=False ) task.host["api_response"] = response.status_code result = nr.run(task=rest_api_task) for host, multi_result in result.items(): print(f"{host}: {nr.inventory.hosts[host]['api_response']}")
-
Netmiko with Jinja2 templating:
from jinja2 import Template from nornir_netmiko.tasks import netmiko_send_config def config_with_template(task): config_template = Template("hostname {{ host.name }}\ninterface Loopback0\n ip address {{ host.loopback_ip }} 255.255.255.255") config = config_template.render(host=task.host) task.run(task=netmiko_send_config, config_commands=config.splitlines()) result = nr.run(task=config_with_template)
-
Tasks:
Tasks are the core units of work in Nornir. Each task represents an automation action, like running a command or pushing a configuration. Tasks can be chained, parameterized, and run in parallel across devices.
Step-by-step:- Define or import a task:
from nornir_netmiko.tasks import netmiko_send_command
- Run the task across the inventory:
result = nr.run(task=netmiko_send_command, command_string="show ip int brief")
- Chain tasks or use custom Python logic:
def custom_task(task): task.run(task=netmiko_send_command, command_string="show version") task.run(task=netmiko_send_command, command_string="show run") result = nr.run(task=custom_task)
- Example with Napalm:
from nornir_napalm.tasks import napalm_get result = nr.run(task=napalm_get, getters=["interfaces"])
- Example with REST API:
def rest_get_interfaces(task): url = f"https://{task.host.hostname}/api/interfaces" response = requests.get(url, auth=("admin", "password"), verify=False) task.host["interfaces"] = response.json() result = nr.run(task=rest_get_interfaces)
- Define or import a task:
-
Runner:
The runner manages task execution across your inventory. It controls concurrency, error handling, and result aggregation, ensuring scalable and reliable automation.
Step-by-step:- Configure runner settings in
config.yaml
:runner: plugin: threaded options: num_workers: 20
- Initialize Nornir with runner config:
nr = InitNornir(config_file="config.yaml")
- Run tasks with concurrency:
result = nr.run(task=netmiko_send_command, command_string="show version")
- Configure runner settings in
-
Result:
Every task produces a result object. This object contains the output, status, and any exceptions encountered, making it easy to analyze and troubleshoot automation runs.
Step-by-step:- Run a task and capture the result:
result = nr.run(task=netmiko_send_command, command_string="show version")
- Access result details:
for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result}")
- Handle errors and exceptions:
for host, multi_result in result.items(): if multi_result.failed: print(f"Error on {host}: {multi_result.exception}")
- Example with Napalm:
result = nr.run(task=napalm_get, getters=["facts"]) for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result['facts']}")
- Example with REST API:
result = nr.run(task=rest_api_task) for host, multi_result in result.items(): print(f"{host}: {nr.inventory.hosts[host]['api_response']}")
- Run a task and capture the result:
-
Configuration:
Nornir is highly configurable via YAML or Python. You can set logging, threading, connection parameters, and plugin options to tailor automation to your workflow.
Step-by-step:- Create a
config.yaml
file:core: num_workers: 10 logging: enabled: true level: INFO
- Initialize Nornir with the config file:
nr = InitNornir(config_file="config.yaml")
- Override configuration in Python if needed:
nr = InitNornir(core={"num_workers": 5})
- Create a
1. Inventory: Static and Dynamic Approaches
The inventory is the heart of Nornir network automation. It defines all network devices, their groups, connection parameters, and custom data. Nornir’s flexible inventory system supports both static (YAML files) and dynamic (NetBox, Nautobot, or custom plugins) sources, making it adaptable for any environment. Here’s an in-depth look at how to use, structure, and optimize your Nornir inventory.
-
Inventory Structure and Concepts:
Nornir inventory consists of three main components:
- Hosts: Individual devices with attributes like hostname, platform, credentials, and custom data.
- Groups: Collections of hosts that share common attributes (e.g., platform, location, role).
- Defaults: Global attributes applied to all hosts unless overridden.
-
Static Inventory (YAML Files):
The SimpleInventory plugin reads inventory from YAML files. This is straightforward, version-controlled, and ideal for small or stable environments.
Step-by-step:- Create inventory files:
# inventory/hosts.yaml router1: hostname: 192.168.1.1 groups: - routers platform: ios data: site: HQ role: core switch1: hostname: 192.168.1.2 groups: - switches platform: eos data: site: Branch role: access
# inventory/groups.yaml routers: platform: ios data: vendor: Cisco switches: platform: eos data: vendor: Arista
# inventory/defaults.yaml username: admin password: "{{ lookup('env', 'NORNIR_PASSWORD') }}" data: ntp_server: 192.168.100.1
- Configure Nornir to use SimpleInventory:
# config.yaml inventory: plugin: SimpleInventory options: host_file: "inventory/hosts.yaml" group_file: "inventory/groups.yaml" defaults_file: "inventory/defaults.yaml"
- Initialize and verify inventory in Python:
from nornir import InitNornir nr = InitNornir(config_file="config.yaml") print(nr.inventory.hosts)
- Best Practices for Static Inventory:
- Keep inventory files under version control (e.g., Git).
- Use group inheritance to avoid repetition.
- Store secrets outside of YAML (use environment variables or vaults).
- Document custom data fields for consistency.
- Create inventory files:
-
Dynamic Inventory with NetBox:
For dynamic, scalable environments, use NetBox as a Source of Truth with the NetBoxInventory2 plugin. This keeps your inventory in sync with your live network database[1][5][6].
Step-by-step:- Install the NetBox inventory plugin:
python3 -m pip install nornir-netbox
- Update
config.yaml
to use NetBoxInventory2:inventory: plugin: NetBoxInventory2 options: nb_url: "https://netbox.local:8000" nb_token: "YOUR_NETBOX_API_TOKEN"
- Initialize Nornir with NetBox inventory:
from nornir import InitNornir nr = InitNornir(config_file="config.yaml")
- How it works:
- All devices, groups, and attributes are pulled live from NetBox.
- Groups are created based on NetBox attributes (site, platform, role, type, manufacturer).
- Custom device fields and tags in NetBox are accessible via
host.data
. - Inventory is always up-to-date, reducing manual effort and errors.
- Example: Run a command on all NetBox devices
from nornir_netmiko.tasks import netmiko_send_command from nornir_utils.plugins.functions import print_result result = nr.run(task=netmiko_send_command, command_string="show version") print_result(result)
- Best Practices for Dynamic Inventory:
- Use NetBox for production environments where device inventory changes frequently.
- Leverage NetBox device roles, tags, and custom fields for advanced filtering.
- Restrict API tokens and use HTTPS for security.
- Document how NetBox attributes map to Nornir inventory fields.
- Install the NetBox inventory plugin:
-
Dynamic Inventory with Nautobot:
Nautobot, a NetBox fork, can also serve as a dynamic inventory source using the nautobot-plugin-nornir plugin[18].
Step-by-step:- Install the Nautobot Nornir plugin:
pip install nautobot-plugin-nornir
- Register and configure the plugin in your script:
from nornir import InitNornir from nornir.core.plugins.inventory import InventoryPluginRegister from nautobot_plugin_nornir.plugins.inventory.nautobot_orm import NautobotORMInventory InventoryPluginRegister.register("nautobot-inventory", NautobotORMInventory) nr = InitNornir( inventory={ "plugin": "nautobot-inventory", "options": { "queryset": Device.objects.all(), "params": {"use_fqdn": True, "fqdn": "acme.com"}, "defaults": {"company": "acme"} } } )
- How it works:
- Devices and attributes are queried directly from Nautobot’s database.
- Supports advanced filtering using Django ORM queries.
- Custom fields and tags are available in
host.data
.
- Best Practices for Nautobot Inventory:
- Use Nautobot’s rich metadata and relationships for powerful device targeting.
- Maintain inventory accuracy by integrating with discovery and change management processes.
- Install the Nautobot Nornir plugin:
-
Custom Inventory Plugins:
If your infrastructure uses a different source of truth, you can write a custom Nornir inventory plugin by subclassing
nornir.core.deserializer.inventory.Inventory
[9].
Example:from nornir.core.deserializer.inventory import Inventory class MyInventory(Inventory): def __init__(self, **kwargs): hosts = {...} # your host data groups = {...} # your group data defaults = {...} super().__init__(hosts=hosts, groups=groups, defaults=defaults, **kwargs)
This enables integration with CMDBs, spreadsheets, or any data source. -
Filtering and Targeting Devices:
Nornir’s inventory supports advanced filtering to select exactly the devices you want to automate, using host/group attributes, custom data, or dynamic queries[16].
Example:core_routers = nr.filter(role="core_router", site="HQ") result = core_routers.run(task=netmiko_send_command, command_string="show ip route")
This ensures precise, safe, and scalable automation. -
Summary Table: Static vs. Dynamic Inventory
Inventory Type Source Update Method Best For Static YAML files Manual Small, stable environments, testing Dynamic (NetBox/Nautobot) NetBox/Nautobot API Automatic (live sync) Large, dynamic, production environments Custom Any data source Manual or programmatic Integrations, special requirements
2. Plugins: Types, Usage, and Best Practices
Plugins are the backbone of Nornir’s extensibility and power. They enable Nornir to support a wide range of devices, protocols, data sources, and result processing workflows. Plugins are categorized by their function—Inventory, Task, Connection, and Processor. Below is an in-depth look at each plugin type, with practical examples for NetBox/Nautobot inventory, Netmiko, Napalm, and REST APIs using requests and Jinja2 templating.
-
Inventory Plugins:
Inventory plugins define how Nornir loads device data. The most common are:
- SimpleInventory: Loads static inventory from YAML files.
- NetBoxInventory2: Loads dynamic inventory from NetBox via API.
- NautobotInventory: Loads dynamic inventory from Nautobot via API.
# config.yaml inventory: plugin: NetBoxInventory2 options: nb_url: "https://netbox.local:8000" nb_token: "YOUR_NETBOX_API_TOKEN"
from nornir import InitNornir nr = InitNornir(config_file="config.yaml") print(nr.inventory.hosts)
Best Practices:- Use dynamic plugins (NetBox/Nautobot) for large, changing environments.
- Document custom fields and tags for advanced filtering.
- Secure API tokens and restrict permissions.
-
Task Plugins:
Task plugins encapsulate the automation logic. Common options:
- Netmiko: For sending CLI commands to network devices via SSH.
- Napalm: For multi-vendor abstraction and state retrieval/configuration.
- Custom Python Tasks: For REST API calls, configuration rendering, or any logic using Python and Jinja2.
from nornir_netmiko.tasks import netmiko_send_command result = nr.run(task=netmiko_send_command, command_string="show version") for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result}")
Example: Napalm Task Pluginfrom nornir_napalm.tasks import napalm_get result = nr.run(task=napalm_get, getters=["facts"]) for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result['facts']}")
Example: REST API Task with Requests + Jinja2import requests from jinja2 import Template def rest_api_task(task): payload_template = Template('{"hostname": "{{ host.hostname }}"}') payload = payload_template.render(host=task.host) response = requests.post( url=f"https://{task.host.hostname}/api/config", headers={"Content-Type": "application/json"}, data=payload, auth=("admin", "password"), verify=False ) task.host["api_response"] = response.status_code result = nr.run(task=rest_api_task) for host, multi_result in result.items(): print(f"{host}: {nr.inventory.hosts[host]['api_response']}")
Example: Netmiko with Jinja2 Templatingfrom jinja2 import Template from nornir_netmiko.tasks import netmiko_send_config def config_with_template(task): config_template = Template("hostname {{ host.name }}\ninterface Loopback0\n ip address {{ host.loopback_ip }} 255.255.255.255") config = config_template.render(host=task.host) task.run(task=netmiko_send_config, config_commands=config.splitlines()) result = nr.run(task=config_with_template)
Best Practices:- Use task plugins for vendor abstraction and repeatability.
- Leverage Jinja2 for dynamic config rendering.
- Write custom tasks for unique workflows or REST API integrations.
-
Connection Plugins:
Connection plugins manage how Nornir connects to devices. The plugin is selected based on device platform or explicitly in inventory.
- Netmiko: SSH CLI for most network platforms.
- Napalm: Multi-vendor support for SSH/NETCONF/REST.
- Requests (custom): For REST API endpoints.
# groups.yaml cisco_ios: platform: ios connection_options: netmiko: extras: device_type: cisco_ios
Best Practices:- Set platform and device_type in inventory for automatic plugin selection.
- Use connection_options for advanced parameters (timeouts, SSH keys, etc.).
- For REST, handle sessions and authentication securely in your custom task.
-
Processor Plugins:
Processor plugins handle result processing, logging, and error handling after tasks execute.
- PrintResult: Formats and prints results to the console.
- Custom Processors: For custom logging, notifications, or integrations.
from nornir_utils.plugins.functions import print_result result = nr.run(task=netmiko_send_command, command_string="show version") print_result(result)
Example: Custom Processor Pluginfrom nornir.core.plugins.processors import Processor class MyLogger(Processor): def task_started(self, task): print(f"Task {task.name} started on {task.host.name}") nr = InitNornir(config_file="config.yaml", process=[MyLogger()])
Best Practices:- Use PrintResult for quick feedback during development.
- Implement custom processors for audit trails, dashboards, or alerting.
-
Summary Table: Nornir Plugin Types and Examples
Plugin Type Purpose Examples Inventory Load device data SimpleInventory, NetBoxInventory2, NautobotInventory Task Automation logic netmiko_send_command, napalm_get, custom REST/Jinja2 tasks Connection Device connectivity Netmiko, Napalm, requests (custom) Processor Result handling PrintResult, custom processors -
General Best Practices for Plugins:
- Choose inventory plugins that match your environment’s scale and change rate (NetBox/Nautobot for dynamic, SimpleInventory for static).
- Reuse task plugins for common operations and write custom tasks for unique logic or REST APIs.
- Leverage connection plugins for protocol and vendor abstraction.
- Use processor plugins for robust result handling, logging, and integration with external systems.
- Document plugin usage and configuration for team collaboration.
3. Tasks: Design, Execution, and Best Practices
Tasks are the core automation units in Nornir. They represent actions such as sending commands, pushing configurations, or making API calls. Nornir tasks are Python functions that can be executed in parallel across your inventory, and can be composed, parameterized, and chained for complex workflows. Below is an in-depth look at how to design, execute, and optimize tasks in a modern Nornir automation framework using NetBox/Nautobot for inventory and Netmiko, Napalm, or REST APIs (with Jinja2 templating).
-
Task Structure and Concepts:
- Tasks are Python functions that receive a
task
object (context for the host, inventory, and results).
- Tasks can call other tasks, access host data, and return results.
- Tasks can be imported from plugins (like Netmiko/Napalm) or defined as custom Python functions. -
Using NetBox/Nautobot Inventory with Tasks:
When using dynamic inventory, tasks automatically receive host data from NetBox or Nautobot, including custom fields and tags.
Example: Filter and Run a Task on All Devices with a Specific Rolecore_routers = nr.filter(role="core_router") result = core_routers.run(task=netmiko_send_command, command_string="show ip route")
-
Netmiko Tasks (CLI Automation):
Netmiko tasks are used for sending commands and configurations to devices via SSH.
Example: Send a Show Commandfrom nornir_netmiko.tasks import netmiko_send_command result = nr.run(task=netmiko_send_command, command_string="show version") for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result}")
Example: Push Configurations Rendered with Jinja2from jinja2 import Template from nornir_netmiko.tasks import netmiko_send_config def config_with_template(task): config_template = Template("hostname {{ host.name }}\ninterface Loopback0\n ip address {{ host.loopback_ip }} 255.255.255.255") config = config_template.render(host=task.host) task.run(task=netmiko_send_config, config_commands=config.splitlines()) result = nr.run(task=config_with_template)
-
Napalm Tasks (Multi-Vendor Abstraction):
Napalm tasks allow you to retrieve state or push configuration across different vendor platforms using a unified API.
Example: Retrieve Device Factsfrom nornir_napalm.tasks import napalm_get result = nr.run(task=napalm_get, getters=["facts"]) for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result['facts']}")
Example: Load and Commit Configurationfrom nornir_napalm.tasks import napalm_configure from jinja2 import Template def push_config(task): with open("templates/config_template.j2") as f: template = Template(f.read()) config = template.render(host=task.host) task.run(task=napalm_configure, configuration=config) result = nr.run(task=push_config)
-
REST API Tasks (requests + Jinja2):
For devices or controllers with REST APIs, use custom tasks with the
requests
library and Jinja2 for payload templating.
Example: POST to Device API with Rendered Payloadimport requests from jinja2 import Template def rest_api_task(task): payload_template = Template('{"hostname": "{{ host.hostname }}"}') payload = payload_template.render(host=task.host) response = requests.post( url=f"https://{task.host.hostname}/api/config", headers={"Content-Type": "application/json"}, data=payload, auth=("admin", "password"), verify=False ) task.host["api_response"] = response.status_code result = nr.run(task=rest_api_task) for host, multi_result in result.items(): print(f"{host}: {nr.inventory.hosts[host]['api_response']}")
Example: GET Data from Device APIdef rest_get_interfaces(task): url = f"https://{task.host.hostname}/api/interfaces" response = requests.get(url, auth=("admin", "password"), verify=False) task.host["interfaces"] = response.json() result = nr.run(task=rest_get_interfaces)
-
Chaining and Composing Tasks:
Tasks can be chained inside a custom function for complex workflows.
Example: Run Multiple Commands in Sequencedef multi_step(task): task.run(task=netmiko_send_command, command_string="show version") task.run(task=netmiko_send_command, command_string="show interfaces") result = nr.run(task=multi_step)
Example: Conditional Logic in Tasksdef conditional_task(task): result = task.run(task=netmiko_send_command, command_string="show version") if "XE" in result.result: task.run(task=netmiko_send_command, command_string="show platform software status control-processor brief") result = nr.run(task=conditional_task)
-
Best Practices for Tasks:
- Write reusable, parameterized task functions for modularity.
- Use Jinja2 templating for dynamic configuration generation.
- Leverage host/group data from inventory for targeting and customization.
- Handle exceptions and log results for troubleshooting.
- Document task logic and expected inputs/outputs.
- Test tasks independently before integrating into larger workflows.
-
Summary Table: Nornir Task Types and Examples
Task Type Purpose Example Plugin/Approach Netmiko CLI command/config via SSH netmiko_send_command, netmiko_send_config Napalm Multi-vendor abstraction, state/config napalm_get, napalm_configure REST API API automation, controller integration requests + Jinja2 in custom task Custom Python Any logic, chaining, conditionals Custom task functions
4. Runner: Concurrency, Execution, and Best Practices
The Runner is the engine that executes tasks across your Nornir inventory. It manages concurrency, parallelism, error handling, and result aggregation, ensuring that automation is both efficient and reliable. The runner is highly configurable and adapts to your environment—whether you use NetBox/Nautobot for inventory and Netmiko, Napalm, or REST API automation with Jinja2 templating.
-
Runner Structure and Concepts:
- The runner determines how tasks are executed across devices: sequentially or in parallel.
- It controls the number of concurrent workers, error propagation, and how results are collected.
- The runner is specified inconfig.yaml
or programmatically via Python. -
Types of Runners:
- ThreadedRunner (default): Executes tasks in parallel using threads; ideal for network automation.
- SerialRunner: Executes tasks sequentially; useful for debugging or when order matters.
Best Practice: Use ThreadedRunner for most workflows to maximize speed, especially with Netmiko, Napalm, or REST API tasks.
-
Configuring the Runner:
Step-by-step:
- Set runner options in
config.yaml
:runner: plugin: threaded options: num_workers: 20
- Initialize Nornir with the runner configuration:
from nornir import InitNornir nr = InitNornir(config_file="config.yaml")
- Override runner settings in Python (optional):
from nornir.core.runners import ThreadedRunner nr = InitNornir(runner={"plugin": "threaded", "options": {"num_workers": 10}}) # or explicitly nr = InitNornir(runner=ThreadedRunner(num_workers=10))
- Run a task using your configured runner:
from nornir_netmiko.tasks import netmiko_send_command result = nr.run(task=netmiko_send_command, command_string="show version")
- Set runner options in
-
Runner with NetBox/Nautobot Inventory:
When using dynamic inventory, the runner seamlessly executes tasks in parallel across all devices retrieved from NetBox or Nautobot.
Example: Run a Netmiko Task on All Devicesnr = InitNornir(config_file="config.yaml") result = nr.run(task=netmiko_send_command, command_string="show ip int brief")
Example: Parallel REST API Callsimport requests from jinja2 import Template def rest_api_task(task): payload_template = Template('{"hostname": "{{ host.hostname }}"}') payload = payload_template.render(host=task.host) response = requests.post( url=f"https://{task.host.hostname}/api/config", headers={"Content-Type": "application/json"}, data=payload, auth=("admin", "password"), verify=False ) task.host["api_response"] = response.status_code result = nr.run(task=rest_api_task)
-
Runner with Netmiko and Napalm Tasks:
The runner ensures that tasks like CLI commands (Netmiko) or state/config retrieval (Napalm) are executed efficiently, leveraging parallelism for speed.
Example: Parallel Napalm Data Collectionfrom nornir_napalm.tasks import napalm_get result = nr.run(task=napalm_get, getters=["facts"])
-
Advanced Runner Features:
- num_workers: Controls the number of devices processed in parallel (tune based on device count and resource limits).
- Stop on Failure: Optionally halt execution on errors for safety-critical workflows.
- Result Aggregation: The runner collects all task results for post-processing, logging, or reporting.
Best Practice: Start with a moderate number of workers (e.g., 10-20), monitor resource usage, and adjust as needed.
-
Best Practices for Runners:
- Use ThreadedRunner for most network automation to maximize concurrency.
- Set num_workers based on your network size and system resources.
- Use SerialRunner for step-by-step debugging or when device order is important.
- Monitor task execution and handle exceptions for robust automation.
- Document runner settings and rationale for future maintainers.
-
Summary Table: Nornir Runner Options
Runner Type Concurrency Use Case How to Configure ThreadedRunner Parallel (multi-threaded) Production, large-scale, speed-critical automation plugin: threaded
options: num_workersSerialRunner Sequential (one device at a time) Debugging, order-dependent workflows plugin: serial
5. Result: Structure, Handling, and Best Practices
The Result object in Nornir is central to understanding, troubleshooting, and acting upon automation outcomes. Every task execution returns a structured result, which includes the output, status, exceptions, and metadata for each device. This enables robust analysis, error handling, and integration with reporting or monitoring systems. Below is a detailed guide to working with Nornir results in environments using NetBox/Nautobot for inventory, and Netmiko, Napalm, or REST APIs (with Jinja2 templating).
-
Result Object Structure and Concepts:
- Each task run returns a nested dictionary:
{host: MultiResult}
.
- MultiResult contains a list of Result objects (one per sub-task or chain).
- Each Result object includes:- result: The output (string, dict, etc.) from the task.
- failed: Boolean flag indicating if the task failed.
- exception: Exception object if the task raised an error.
- host: The host object the result pertains to.
- name: The task name.
-
Accessing and Handling Results:
Step-by-step:
- Run a Task and Capture the Result:
from nornir_netmiko.tasks import netmiko_send_command result = nr.run(task=netmiko_send_command, command_string="show version")
- Iterate Over Results for Each Host:
for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result}")
- Check for Failures and Exceptions:
for host, multi_result in result.items(): if multi_result.failed: print(f"Error on {host}: {multi_result.exception}") else: print(f"{host}: {multi_result[0].result}")
- Export or Process Results (e.g., to JSON):
import json output = {host: multi_result[0].result for host, multi_result in result.items()} with open("results.json", "w") as f: json.dump(output, f, indent=2)
- Run a Task and Capture the Result:
-
Results with NetBox/Nautobot Inventory:
When using dynamic inventory, results automatically map to hosts and groups from NetBox/Nautobot. Host metadata and custom fields are available for contextual reporting.
Example: Report Device Role and Command Outputfor host, multi_result in result.items(): role = nr.inventory.hosts[host].data.get("role", "unknown") print(f"{host} ({role}): {multi_result[0].result}")
-
Results with Netmiko Tasks:
Netmiko task results typically return command output as strings.
Example: Print Output or Error for Each Devicefor host, multi_result in result.items(): if multi_result.failed: print(f"{host} FAILED: {multi_result.exception}") else: print(f"{host} OUTPUT:\n{multi_result[0].result}")
-
Results with Napalm Tasks:
Napalm task results are usually dictionaries with structured data.
Example: Access Device Factsfrom nornir_napalm.tasks import napalm_get result = nr.run(task=napalm_get, getters=["facts"]) for host, multi_result in result.items(): print(f"{host}: {multi_result[0].result['facts']}")
-
Results with REST API Tasks (requests + Jinja2):
Custom REST API tasks can store status codes, JSON responses, or any data in
host.data
.
Example: Store and Print API Statusdef rest_api_task(task): payload_template = Template('{"hostname": "{{ host.hostname }}"}') payload = payload_template.render(host=task.host) response = requests.post( url=f"https://{task.host.hostname}/api/config", headers={"Content-Type": "application/json"}, data=payload, auth=("admin", "password"), verify=False ) task.host["api_response"] = response.status_code result = nr.run(task=rest_api_task) for host, multi_result in result.items(): print(f"{host}: {nr.inventory.hosts[host]['api_response']}")
-
Chained Results and Subtasks:
When chaining tasks, MultiResult holds the output of each sub-task in order.
Example: Print Results of Each Stepdef multi_step(task): r1 = task.run(task=netmiko_send_command, command_string="show version") r2 = task.run(task=netmiko_send_command, command_string="show interfaces") # Access results by index print(f"Version: {r1.result}") print(f"Interfaces: {r2.result}") result = nr.run(task=multi_step)
-
Advanced Result Handling:
- Aggregate results for reporting, dashboards, or compliance checks.
- Integrate with logging systems or ticketing tools via custom processors.
- Use structured results for automated remediation or audits.
-
Best Practices for Results:
- Always check failed and exception fields for robust error handling.
- Leverage host.data for storing additional context or API responses.
- Export results in structured formats (JSON, CSV) for downstream processing.
- Document result structures for team collaboration and integration.
-
Summary Table: Nornir Result Patterns and Examples
Task Type Result Format Access Pattern Netmiko String (CLI output) multi_result[0].result Napalm Dict (structured data) multi_result[0].result["facts"] REST API Status code, JSON, or custom host["api_response"] or host["interfaces"] Chained Tasks MultiResult (list of Result) multi_result[i].result (per sub-task)
6. Configuration: Setup, Customization, and Best Practices
The Configuration component in Nornir governs how the framework operates—from inventory and runner setup to logging, connection options, and plugin parameters. Proper configuration ensures your automation is scalable, secure, and aligned with your environment, whether you're using NetBox/Nautobot for inventory and Netmiko, Napalm, or REST APIs (with Jinja2 templating) for automation.
-
Configuration Structure and Concepts:
- Nornir is configured via a
config.yaml
file or directly in Python.
- Configuration covers inventory plugins, runner settings, logging, connection options, and plugin-specific parameters.
- Centralizing configuration promotes consistency and makes your automation portable and maintainable. -
Setting Up
config.yaml
:Step-by-step:
- Define Inventory Plugin and Options:
inventory: plugin: NetBoxInventory2 options: nb_url: "https://netbox.local:8000" nb_token: "YOUR_NETBOX_API_TOKEN"
Or for Nautobot:inventory: plugin: nautobot-inventory options: nautobot_url: "https://nautobot.local:8443" nautobot_token: "YOUR_NAUTOBOT_API_TOKEN"
- Configure the Runner:
runner: plugin: threaded options: num_workers: 20
- Set Logging and Core Options:
core: num_workers: 20 logging: enabled: true level: INFO file: "nornir.log"
- Specify Connection Options (if needed):
connection_options: netmiko: extras: timeout: 60 fast_cli: true napalm: extras: optional_args: global_delay_factor: 2
- Store
config.yaml
at the project root for easy access and version control. - Keep API tokens and sensitive data out of source control—use environment variables or secret managers.
- Document all configuration parameters for team clarity.
- Define Inventory Plugin and Options:
-
Overriding Configuration in Python:
You can override or extend
config.yaml
options directly in your scripts for advanced use cases or dynamic behavior.
Example: Set Runner Workers Programmaticallyfrom nornir import InitNornir nr = InitNornir(config_file="config.yaml", core={"num_workers": 10})
Example: Pass Custom Plugin Optionsnr = InitNornir( config_file="config.yaml", runner={"plugin": "threaded", "options": {"num_workers": 5}}, inventory={ "plugin": "NetBoxInventory2", "options": { "nb_url": "https://netbox.local:8000", "nb_token": "YOUR_NETBOX_API_TOKEN" } } )
-
Configuring for Netmiko, Napalm, and REST API Workflows:
Netmiko: Set
platform
anddevice_type
in inventory for correct plugin selection and connection parameters.
Napalm: Useoptional_args
for vendor-specific tweaks.
REST API: Store API endpoints, credentials, and headers indefaults.yaml
or as environment variables for use in custom tasks.# inventory/defaults.yaml api_base_url: "https://api.network.local" api_username: "apiuser" api_password: "{{ lookup('env', 'API_PASSWORD') }}"
Example: Use Config Data in a REST Taskimport requests def rest_api_task(task): url = f"{task.host.defaults['api_base_url']}/device/{task.host.hostname}/config" response = requests.get( url, auth=(task.host.defaults['api_username'], task.host.defaults['api_password']), verify=False ) task.host["api_response"] = response.status_code
-
Advanced Configuration Patterns:
- Use environment variables for secrets and credentials.
- Leverage group and host inheritance for DRY (Don’t Repeat Yourself) configuration.
- Centralize Jinja2 template paths and common data in
defaults.yaml
for consistency. - Enable and tune logging for troubleshooting and audit trails.
- Document all configuration files and options for maintainability.
-
Summary Table: Nornir Configuration Components
Component Purpose Where Configured Inventory Plugin Defines device data source config.yaml (NetBoxInventory2, nautobot-inventory, SimpleInventory) Runner Controls concurrency & execution config.yaml or Python (threaded, serial) Logging Enables audit and troubleshooting config.yaml Connection Options Customizes device/plugin parameters config.yaml, groups.yaml, or defaults.yaml Plugin Options Fine-tunes plugin behavior config.yaml or Python Secrets/Credentials Secures sensitive data Environment variables, vaults, defaults.yaml
Conclusion
Throughout this blog post, we’ve taken a comprehensive journey into the world of Nornir automation. Here’s a recap of the key takeaways:
- Nornir is a Python-native automation framework designed for network engineers who want full programmatic control, flexibility, and scalability in their automation workflows.
- Its inventory-centric design and plugin ecosystem make it adaptable to a wide range of network environments and tasks.
- Parallel execution via multi-threading allows Nornir to efficiently automate operations across hundreds or thousands of devices, saving time and reducing manual errors.
- The structured results model enables advanced error handling, data analysis, and seamless integration with monitoring and compliance systems.
- Compared to other tools like Ansible and SaltStack, Nornir stands out for its Python-first approach, extensibility, and suitability for complex, customized automation scenarios.
- The open-source community and active development ensure that Nornir continues to evolve, with new plugins, integrations, and best practices emerging regularly.
Whether you’re just starting out with network automation or looking to level up your existing workflows, Nornir provides a robust, modern foundation that empowers you to automate with confidence and creativity.
Thanks for joining us on this deep dive into Nornir automation! If you have questions, ideas, or experiences to share, feel free to connect with the community and keep the conversation going. Happy automating!