Skip to main content

A Basic introduction to Nornir (with lab!)

·1376 words·7 mins·
Npa Showcases Containerlab Nornir Network Automation Python
Andrew Jones
Author
Andrew Jones
Network Engineering & Automation Generalist
Table of Contents

Introduction
#

In this article, we will learn the basics of Nornir along with how to use it to interact with Juniper and Arista devices.

What is Nornir?
#

From the Nornir documentation

Nornir is an automation framework written in python to be used with python. Just imagine Nornir as the Flask of automation. Nornir will take care of dealing with the inventory where you have your host information, it will take care of dispatching the tasks to your devices and will provide a common framework to write “plugins”.

Where I think it shines compared to other products like Ansible is the “Pure Python” framework. If you can program in Python, then you can utilize Nornir to integrate with any product or API endpoint and create logic for any automation scenario.

Setup
#

The ready to go example lab for this article is the basic-nornir topology. Navigate into this directory and execute manage.sh build & manage.sh run

Note: If this is your first time here, see the NPA Showcases documentation for initial installation steps on your local machine or a GitHub codespace

Topology
#

The folder structure of our Nornir server is below

FileDescription
/app/config.ymlOur basic Nornir config file
/app/inventory/defaults.ymlDefault inventory variables, applies to all hosts
/app/inventory/groups.ymlGroup inventory variables, applies to hosts in this group
/app/inventory/hosts.ymlHost inventory variables, specific to the host
/app/tasks/task-inventory.pyExample task showing inventory management
/app/tasks/task-napalm.pyExample task to interact with devices using NAPALM plugin
/app/tasks/task-netmiko.pyExample task to interact with devices using NETMIKO plugin
/app/tasks/task-scrapli.pyExample task to interact with devices using SCRAPLI plugin
/app/templates/ceos1.j2Jinja2 template for our cEOS device
/app/templates/crpd1.j2Jinja2 template for our cRPD device

Interacting with Devices
#

Now the boilerplate is out of the way, let’s jump right in! Assuming the manage.sh script worked, you should see the below output:

╭─────────────────┬───────────────────┬─────────┬────────────────╮
│       Name      │     Kind/Image    │  State  │ IPv4/6 Address │
├─────────────────┼───────────────────┼─────────┼────────────────┤
│ clab-lab-ceos1  │ arista_ceos       │ running │ 172.20.0.12    │
│                 │ ceos:latest       │         │ N/A            │
├─────────────────┼───────────────────┼─────────┼────────────────┤
│ clab-lab-crpd1  │ juniper_crpd      │ running │ 172.20.0.11    │
│                 │ crpd:latest       │         │ N/A            │
├─────────────────┼───────────────────┼─────────┼────────────────┤
│ clab-lab-nornir │ linux             │ running │ 172.20.0.100   │
│                 │ lab-nornir:latest │         │ N/A            │
╰─────────────────┴───────────────────┴─────────┴────────────────╯
Done. Sleeping for 5 seconds to allow the containers to fully boot

From here, SSH (admin/admin) into our Nornir server:

ssh admin@clab-lab-nornir

Inventory
#

See the contents of the /app/tasks/task-inventory.py file below:

Expand / Collapse
#!/usr/bin/env python3

from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result as print_result_nornir
from nornir_rich.functions import print_inventory, print_result

nr = InitNornir(config_file="/app/config.yml")

# Basic task
def hello_world(task: Task) -> Result:
    return Result(
        host=task.host,
        result=f"{task.host.name} says hello world!"
    )

def main():
    print("--------------------------------")
    print("Default Nornir Inventory \n--------------------------------")
    print(f"Hosts: {nr.inventory.hosts}")
    print(f"Groups: {nr.inventory.groups}")
    print(f"Specific host details (ceos1):")
    for key, value in nr.inventory.hosts["ceos1"].items():
        print(f"  {key}: {value}")
    print("--------------------------------")

    print("Nornir Inventory with Rich \n--------------------------------")
    print_inventory(nr)
    print("--------------------------------")

    result = nr.run(task=hello_world)
    print("Default Nornir task \n--------------------------------")
    print_result_nornir(result)
    print("--------------------------------")
    print("Nornir task with Rich \n--------------------------------")
    print_result(result)
    
    print("--------------------------------")
    print("Nornir task with Rich (Filtered) \n--------------------------------")
    result = nr.filter(platform="junos").run(task=hello_world)
    print_result(result)

if __name__ == "__main__":
    main()

This script shows you how to:

  • Initialize a Nornir runner and your inventory
  • Use the runner to execute a basic task
  • Utilize the nornir_rich plugin to enhance your output

NAPALM
#

See the contents of the /app/tasks/task-napalm.py file below:

Expand / Collapse
#!/usr/bin/env python3
# Basic Nornir script to demonstrate the use of NAPALM to interact with network devices
# Note: Nokia SR OS devices are not supported by NAPALM it seems (not sure how to load the community plugin)
# https://napalm.readthedocs.io/en/latest/support/index.html

from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_napalm.plugins.tasks import napalm_get
from nornir_napalm.plugins.tasks import napalm_cli
from nornir_napalm.plugins.tasks import napalm_configure
from nornir.core.helpers.jinja_helper import render_from_file
from nornir.core.helpers.jinja_helper import render_from_string
from nornir.core.filter import F

def configure_device(task: Task) -> Result:
    #template = "set system host-name {{ task.host }}-test"
    #rendered_template = render_from_string(template, task=task)
    file = f"{task.host}.j2"
    rendered_template = render_from_file(
        template=file,
        path="/app/templates/",
        task=task)
    task.run(
        task=napalm_configure,
        configuration=rendered_template,
    )
    return Result(
        host=task.host,
        result=f"Configured {task.host.name} with template: {file}",
    )

def main():
    nr = InitNornir(config_file="/app/config.yml")
    filter = nr.filter(F(platform="junos") | F(platform="eos"))
    filter_junos = nr.filter(F(platform="junos"))
    filter_eos = nr.filter(F(platform="eos"))
    
    # Get example
    # Note: getters=["facts"] doesn't work on crpd:
    # jnpr.junos.exception.RpcError: RpcError(severity: error, bad_element: None, message: command is not valid on the crpd)
    result = filter.run(
        task=napalm_get,
        getters=["config"],
    )
    print_result(result)
    
    # CLI example
    result = filter.run(
        task=napalm_cli,
        commands=["show version"],
    )
    print_result(result)
    
    # Configuration example
    result = filter.run(
        task=configure_device,
    )
    print_result(result)

if __name__ == "__main__":
    main()

This script shows you how to use NAPALM plugin to perform the below on EOS and JUNOS devices:

  • Retrieve the running configuration
  • Execute a CLI command (show version)
  • Render a Jinja2 configuration template and apply it

NETMIKO
#

See the contents of the /app/tasks/task-netmiko.py file below:

Expand / Collapse
#!/usr/bin/env python3
# Basic Nornir script to demonstrate the use of Netmiko to interact with network devices.
# Note: Nokia SR OS devices are not supported by Netmiko it seems

from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_netmiko.tasks import netmiko_send_command
from nornir_netmiko.tasks import netmiko_send_config
from nornir_netmiko.tasks import netmiko_commit
from nornir_netmiko.tasks import netmiko_file_transfer
from nornir.core.helpers.jinja_helper import render_from_file
from nornir.core.helpers.jinja_helper import render_from_string
from nornir.core.filter import F

def configure_device(task: Task) -> Result:
    #template = "set system host-name {{ task.host }}-test"
    #rendered_template = render_from_string(template, task=task)
    file = f"{task.host}.j2"
    rendered_template = render_from_file(
        template=file,
        path="/app/templates/",
        task=task)
    task.run(
        task=netmiko_send_config,
        config_commands=rendered_template.split("\n"),
        enable=True,
    )
    if task.host.platform == "junos":
        task.run(
            task=netmiko_commit,
        )
    return Result(
        host=task.host,
        result=f"Configured {task.host.name} with template: {file}",
    )

def main():
    nr = InitNornir(config_file="/app/config.yml")
    filter = nr.filter(F(platform="junos") | F(platform="eos"))
    filter_junos = nr.filter(F(platform="junos"))
    filter_eos = nr.filter(F(platform="eos"))
    
    # File transfer example
    # Note: Doesn't work on my setup
    # ValueError: Invalid output from MD5 command: /usr/libexec/ui/md5: invalid option -- 'X'
    # result = filter_junos.run(
    #     task=netmiko_file_transfer,
    #     source_file="/app/tasks/testfile",
    #     dest_file="testfile",
    # )
    # print_result(result)

    # CLI example
    result = filter.run(
        task=netmiko_send_command,
        command_string="show version",
    )
    print_result(result)
    
    # Configuration example
    result = filter.run(
        task=configure_device,
    )
    print_result(result)

if __name__ == "__main__":
    main()

This script shows you how to use NETMIKO plugin to perform the below on EOS and JUNOS devices:

  • Execute a CLI command (show version)
  • Render a Jinja2 configuration template and apply it

SCRAPLI
#

See the contents of the /app/tasks/task-scrapli.py file below:

Expand / Collapse
#!/usr/bin/env python3
# Basic Nornir script to demonstrate the use of Scrapli to interact with network devices.
# Note: Nokia SR OS devices are not supported by Scrapli it seems

from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
from nornir_scrapli.tasks import send_command
from nornir_scrapli.tasks import send_configs
from nornir_scrapli.functions import print_structured_result
from nornir.core.helpers.jinja_helper import render_from_file
from nornir.core.filter import F

def configure_device_junos(task: Task) -> Result:
    file = f"{task.host}.j2"
    rendered_template = render_from_file(
        template=file,
        path="/app/templates/",
        task=task)
    # Load the config
    task.run(
        task=send_configs,
        configs=rendered_template.split("\n"),
        eager=True,
    )
    if task.host.platform == "junos":
        task.run(
            task=send_configs,
            configs=["show | compare", "commit check", "commit"],
        )
    return Result(
        host=task.host,
        result=f"Configured {task.host.name} with template: {file}",
    )

def main():
    nr = InitNornir(config_file="/app/config.yml")
    filter = nr.filter(F(platform="junos") | F(platform="eos"))
    filter_junos = nr.filter(F(platform="junos"))
    filter_eos = nr.filter(F(platform="eos"))

    # CLI example
    result = filter.run(
        task=send_command,
        command="show version",
    )
    print_structured_result(result, fail_to_string=True)
    
    # Configuration example
    result = filter_junos.run(
        task=configure_device_junos,
    )
    print_result(result)

if __name__ == "__main__":
    main()

This script shows you how to use SCRAPLI plugin to perform the below on EOS and JUNOS devices:

  • Execute a CLI command (show version)
  • Render a Jinja2 configuration template and apply it
Note: The scrapli plugin requires a straight to CLI user (admin/clab123) on our cRPD device. This should be automatically applied but if not the commands are below
set system login user admin uid 100 class super-user
set system login user admin authentication plain-text-password

Final thoughts
#

We have barely scratched the surface on what Nornir and its plugins are capable of here but I hope this helps you get started with your Nornir journey.

I’ve found that even in these basic examples, you can see that the code does vary a bit just to do the basics depending on your plugin of choice. A lot of basic documentation seems to be missing from some of the plugins as well, with the exception of the SCRAPLI plugin which I feel at moment is the best of the bunch, but YMMV.

Please let me know in the comments below if you have any thoughts, or if any of these scripts aren’t working and I will update this article. Thanks for reading!