Mantra Networking Mantra Networking

Normir Automation: Deep Dive

Normir Automation: Deep Dive
Created By: Lauren Garcia

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:

  1. 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.
  2. 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.
  3. 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.
  4. Result Handling
    • Results are structured objects, not just raw text, making it easy to analyze outcomes, handle errors, and take further actions programmatically.
  5. 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 the inventory/ 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:

    1. Install the NetBox inventory plugin:
      python3 -m pip install nornir-netbox
      
    2. 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
      
    3. Initialize Nornir in your script:
      from nornir import InitNornir
      nr = InitNornir(config_file="config.yaml")
      
    4. 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.
    Example Script:
    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)
    

  • 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 in templates/. 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.

Flow Chart of Nornir Architecture and Workflow

+--------------------+ | 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:
    1. Initialize Nornir:
      • Load configuration (config.yaml or Python dict).
      • Select inventory plugin (SimpleInventory, NetBox, Nautobot).
    2. Inventory Loaded:
      • Hosts, groups, and defaults are built from static or dynamic sources.
      • Host data is available for filtering and task parameterization.
    3. 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.
    4. Task Definition & Execution:
      • Tasks are written as Python functions or use plugins.
      • Runner (Threaded or Serial) manages parallel or sequential execution across inventory.
    5. 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.
  • 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:

    1. 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
      
    2. Configure Nornir to use the SimpleInventory plugin:
      from nornir import InitNornir
      nr = InitNornir(config_file="config.yaml")
      
    3. Verify inventory:
      print(nr.inventory.hosts)
      

  • 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.
    Examples:
    • 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:

    1. Define or import a task:
      from nornir_netmiko.tasks import netmiko_send_command
      
    2. Run the task across the inventory:
      result = nr.run(task=netmiko_send_command, command_string="show ip int brief")
      
    3. 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)
      
    4. Example with Napalm:
      from nornir_napalm.tasks import napalm_get
      result = nr.run(task=napalm_get, getters=["interfaces"])
      
    5. 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)
      

  • 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:

    1. Configure runner settings in config.yaml:
      runner:
        plugin: threaded
        options:
          num_workers: 20
      
    2. Initialize Nornir with runner config:
      nr = InitNornir(config_file="config.yaml")
      
    3. Run tasks with concurrency:
      result = nr.run(task=netmiko_send_command, command_string="show version")
      

  • 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:

    1. Run a task and capture the result:
      result = nr.run(task=netmiko_send_command, command_string="show version")
      
    2. Access result details:
      for host, multi_result in result.items():
          print(f"{host}: {multi_result[0].result}")
      
    3. Handle errors and exceptions:
      for host, multi_result in result.items():
          if multi_result.failed:
              print(f"Error on {host}: {multi_result.exception}")
      
    4. 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']}")
      
    5. 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']}")
      

  • 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:

    1. Create a config.yaml file:
      core:
        num_workers: 10
      logging:
        enabled: true
        level: INFO
      
    2. Initialize Nornir with the config file:
      nr = InitNornir(config_file="config.yaml")
      
    3. Override configuration in Python if needed:
      nr = InitNornir(core={"num_workers": 5})
      

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.
    This structure allows for inheritance and overrides, giving you granular control over device parameters[2][3][4].

  • 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:

    1. 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
      
    2. 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"
      
    3. Initialize and verify inventory in Python:
      from nornir import InitNornir
      nr = InitNornir(config_file="config.yaml")
      print(nr.inventory.hosts)
      
    4. 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.

  • 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:

    1. Install the NetBox inventory plugin:
      python3 -m pip install nornir-netbox
      
    2. Update config.yaml to use NetBoxInventory2:
      inventory:
        plugin: NetBoxInventory2
        options:
          nb_url: "https://netbox.local:8000"
          nb_token: "YOUR_NETBOX_API_TOKEN"
      
    3. Initialize Nornir with NetBox inventory:
      from nornir import InitNornir
      nr = InitNornir(config_file="config.yaml")
      
    4. 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.
    5. 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)
      
    6. 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.

  • 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:

    1. Install the Nautobot Nornir plugin:
      pip install nautobot-plugin-nornir
      
    2. 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"}
          }
        }
      )
      
    3. 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.
    4. 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.

  • 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.
    Example: Using NetBox as Inventory
    # 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.
    Example: Netmiko Task Plugin
    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 Plugin
    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']}")
    
    Example: REST API Task with Requests + Jinja2
    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']}")
    
    Example: 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)
    
    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.
    Example: Specifying Netmiko Connection in Inventory
    # 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.
    Example: Using PrintResult
    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 Plugin
    from 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 Role

    core_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 Command

    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: Push Configurations Rendered with Jinja2
    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)
    

  • 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 Facts

    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']}")
    
    Example: Load and Commit Configuration
    from 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 Payload

    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']}")
    
    Example: GET Data from Device 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)
    

  • Chaining and Composing Tasks:

    Tasks can be chained inside a custom function for complex workflows.
    Example: Run Multiple Commands in Sequence

    def 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 Tasks
    def 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 in config.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:

    1. Set runner options in config.yaml:
      runner:
        plugin: threaded
        options:
          num_workers: 20
      
    2. Initialize Nornir with the runner configuration:
      from nornir import InitNornir
      nr = InitNornir(config_file="config.yaml")
      
    3. 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))
      
    4. 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")
      

  • 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 Devices

    nr = InitNornir(config_file="config.yaml")
    result = nr.run(task=netmiko_send_command, command_string="show ip int brief")
    
    Example: Parallel REST API Calls
    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)
    

  • 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 Collection

    from 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_workers
    SerialRunner 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.
    - Results can be processed, filtered, and exported for further use.

  • Accessing and Handling Results:

    Step-by-step:

    1. 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")
      
    2. Iterate Over Results for Each Host:
      for host, multi_result in result.items():
          print(f"{host}: {multi_result[0].result}")
      
    3. 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}")
      
    4. 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)
      

  • 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 Output

    for 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 Device

    for 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 Facts

    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']}")
    

  • 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 Status

    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']}")
    

  • Chained Results and Subtasks:

    When chaining tasks, MultiResult holds the output of each sub-task in order.
    Example: Print Results of Each Step

    def 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:

    1. 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"
      
    2. Configure the Runner:
      runner:
        plugin: threaded
        options:
          num_workers: 20
      
    3. Set Logging and Core Options:
      core:
        num_workers: 20
      logging:
        enabled: true
        level: INFO
        file: "nornir.log"
      
    4. Specify Connection Options (if needed):
      connection_options:
        netmiko:
          extras:
            timeout: 60
            fast_cli: true
        napalm:
          extras:
            optional_args:
              global_delay_factor: 2
      
    Best Practices:
    • 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.

  • 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 Programmatically

    from nornir import InitNornir
    
    nr = InitNornir(config_file="config.yaml", core={"num_workers": 10})
    
    Example: Pass Custom Plugin Options
    nr = 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 and device_type in inventory for correct plugin selection and connection parameters.
    Napalm: Use optional_args for vendor-specific tweaks.
    REST API: Store API endpoints, credentials, and headers in defaults.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 Task
    import 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!

Read next

OpsMill InfraHub: Deep Dive

OpsMill InfraHub: Deep Dive

Table of Contents * Overview * Core Components * Comparative Analysis * Use Cases and Applications * Roadmap and Future Enhancements * Schema Design Example * Getting Started with InfraHub * End-to-end Example

Lauren Garcia • • 13 min read
Netmiko: Deep Dive

Netmiko: Deep Dive

Table of Contents * Overview * Core Components * Installation and Setup * Basic Usage Example * Advanced Use Cases * Troubleshooting and Best Practices * Common Examples * Configuration Guide * Conclusion Netmiko:

Lauren Garcia • • 48 min read
Nautobot: Deep Dive

Nautobot: Deep Dive

Table of Contents * Overview * Core Components * Comparative Analysis * Nautobot Use Cases * Nautobot App Ecosystem * Deep Dive: Nautobot Jobs * Data Integrity and Governance * Deployment and Integration

Lauren Garcia • • 32 min read