Getting Started with Python Network Libraries for Network Engineers – Part 4
This post is the fourth in a series exploring Python libraries that are commonly used for interacting with network devices. The previous entries in this series covered libraries for directly interacting with network devices. Libraries such as netmiko, NAPALM, and Scrapli greatly ease the process of connecting to, and configuring, a network device. This blog will examine Nornir, an automation framework for orchestrating tasks across an entire inventory of devices.
What Is Nornir, and How Does It Compare to Other Automation Frameworks?
Nornir is an automation framework built to address the same use cases as other popular automation frameworks, such as Ansible, Salt, or Puppet. It exists to allow the user to interact with many devices, at-scale, in a programmatic fashion.
- Nornir’s workflow revolves around the definition of tasks and identifying which hosts in your inventory a task should be executed against.
- Nornir is not an opinionated framework. It makes no assumptions around what kind of tasks you want to automate, how to connect to a device, configuration data-models, or any of the myriad implementation details that comprise a comprehensive network automation solution. There are many existing libraries to solve these problems, and these libraries can be called by Nornir during task execution. Nornir remains focused on the issue of task management and is meant to be a single pillar in the construction of a personalized system. Nornir does the scaling; what processes you actually run in your environment are for you to define.
- Nornir has no domain-specific language. It is written, configured, and executed as pure Python code. While a DSL like Ansible’s may be simpler to learn, initially, DSLs can become cumbersome and complex when dealing with complicated logic or when attempting to extend functionality. Without a non-Python abstraction layer you have access to all of its data structures, flow control, and third-party libraries.
If you are familiar with Python web frameworks, you may notice the different philosophies of Ansible and Nornir are similar to the differences between Django and Flask, with very similar pros and cons.
Installation
You can install Nornir via pip:
pip install nornir
Or, if you are using Poetry for dependency management:
poetry add nornir
Nornir is supported by the community in the form of plugins. Plugins provide inventory integrations, predefined tasks, and other tools to simplify development. This demo will be using the nornir-scrapli plugin in order to utilize the popular Scrapli library for connecting to network devices, and Scrapli’s support for Genie to parse device output into structured data.
pip install nornir-scrapli
pip install 'pyats[library]'
poetry add nornir-scrapli
poetry add 'pyats[library]'
Note: This demo will be using the Cisco DevNet Sandbox as target for the example code. Visit Cisco DevNet to sign up and follow along for free.
Configuration and Inventory
Before Nornir can connect to any devices, it must be configured and an inventory must be defined. The “Simple Inventory” plugin is perfect for this demo due to the small size of our test inventory, and it comes built into Nornir. It uses .yaml
files to define the host, group, and default configurations. More specific configuration settings override more generic ones, with default configurations being overwritten first.
# /config.yaml
---
inventory:
plugin: SimpleInventory
options:
host_file: "inventory/hosts.yaml"
group_file: "inventory/groups.yaml"
defaults_file: "inventory/defaults.yaml"
runner:
plugin: threaded
options:
num_workers: 3
Strict SSH key checking must be disabled since the public sandbox devices of the DevNet lab have unknown SSH keys. This specific bit of config is unique to Scrapli, and more info about its options can be found in the scrapli-nornir documentation:
# /inventory/defaults.yaml
connection_options:
scrapli:
extras:
auth_strict_key: False
Three groups have been configured below, one for each different network OS in the sandbox lab. Each device has a unique set of credentials, although those have been redacted here. The credentials are provided when connecting to the DevNet lab and are subject to change at any time. The naming convention of the groups is arbitrary. You can create any number of groups to allow hosts to inherit any kind of data. Hosts can belong to any number of groups.
The platform key is a configuration option that Nornir uses to pass OS information to transport plugins for driver and parser selection. This can be defined at any point in the inventory configuration or defined programmatically:
# /inventory/groups.yaml
nxos_devices:
username: #REDACTED
password: #REDACTED
platform: cisco_nxos
iosxr_devices:
username: #REDACTED
password: #REDACTED
platform: cisco_iosxr
iosxe_devices:
username: #REDACTED
password: #REDACTED
platform: cisco_iosxe
Please note that it is bad practice to place credentials directly into code. It is one of the most common causes of accidentally exposed secrets.
Finally, the individual hosts must be created in the inventory. The top-level key is an arbitrary name used to uniquely identify a single host, and the hostname key is the network address Nornir will pass to any tasks that require connectivity. The groups key is a list of group names of which each host is a member. The information under the data key is special and can be any arbitrary structured data. It allows for per-host custom fields for any kind of configuration, identification, or contextual data that may be helpful in targeting or orchestration.
# /inventory/hosts.yaml
switch1:
hostname: sandbox-nxos-1.cisco.com
groups:
- nxos_devices
data:
site: "cloud"
role: "butcher"
router1:
hostname: sandbox-iosxr-1.cisco.com
groups:
- iosxr_devices
data:
site: "sandbox"
role: "baker"
router2:
hostname: sandbox-iosxe-latest-1.cisco.com
groups:
- iosxe_devices
data:
site: "sandbox"
role: "butcher"
Initializing Nornir
Let’s create a new Python script to truly get started. The following code instantiates a new Nornir object with a copy of the configured inventory.
# /demo.py
from nornir import InitNornir
nr = InitNornir("config.yaml")
Note: It is possible to programmatically configure Nornir, instead of using externally imported .yaml files, or to combine both methods. See Nornir’s documentation for more details.
# /demo.py
from nornir import InitNornir
nr = InitNornir(
runner={
"plugin": "threaded",
"options": {
"num_workers": 3,
},
},
inventory={
"plugin": "SimpleInventory",
"options": {
"host_file": "inventory/hosts.yaml",
"group_file": "inventory/groups.yaml",
"defaults_file": "inventory/defaults.yaml"
},
},
)
Filtering the Inventory
Once the Nornir object is instantiated, you can access the configured inventory data as object properties.
nr.inventory.hosts
and nr.inventory.groups
are dictionary-like objects that can be iterated over to see all of the hosts or groups in the inventory.
>>> nr.inventory.hosts.keys()
dict_keys(['switch1', 'router1', 'router2'])
>>> nr.inventory.groups.keys()
dict_keys(['nxos_devices', 'iosxr_devices', 'iosxe_devices'])
Iterating over the inventory can be useful, but Nornir implements a system of filters that allow a subset of hosts to be efficiently targeted. The following filter works to target all hosts with the “butcher” role:
>>> nr.filter(role="butcher").inventory.hosts
{'switch1': Host: switch1, 'router2': Host: router2}
This filter identifies all of the hosts with their ‘site’ set to sandbox:
>>> nr.filter(site="sandbox").inventory.hosts.keys()
dict_keys(['router1', 'router2'])
Filters can be combined to narrow down search results:
>>> filter1 = nr.filter(site="sandbox")
..: filter1.filter(role="butcher").inventory.hosts
{'router2': Host: router2}
Filtering in Nornir is a deep subject. Please check the Nornir documentation for more advanced topics and other methods of filtering the Nornir inventory.
Executing Tasks
Now that Nornir is initialized and its inventory can be filtered, tasks can be executed against those hosts. This first example is using a predefined task provided by the Scrapli plugin to retrieve the results of show interface status brief
, and having a built-in utility plugin from nornir_utils
display that result in an easy-to-read manner.
# /demo.py
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from nornir_scrapli.tasks import send_command
nr = InitNornir("config.yaml")
result = nr.run(task=send_command, command="show ip interface brief")
print_result(result)
Running the script will produce the following output, which has been truncated for brevity:
send_command********************************************************************
* router1 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Wed Jul 13 18:13:38.817 UTC
Interface IP-Address Status Protocol Vrf-Name
BVI227 unassigned Shutdown Down default
BVI511 10.200.188.33 Down Down default
Bundle-Ether10 unassigned Down Down default
Loopback0 2.2.2.2 Up Up default
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* router2 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Interface IP-Address OK? Method Status Protocol
GigabitEthernet1 10.10.20.48 YES NVRAM up up
GigabitEthernet2 unassigned YES NVRAM administratively down down
Loopback11 unassigned YES unset up up
Loopback111 10.10.30.1 YES manual up up
Tunnel0 10.10.30.1 YES TFTP up up
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* switch1 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
IP Interface Status for VRF "default"(1)
Interface IP Address Interface Status
Vlan10 192.168.10.1 protocol-down/link-down/admin-down
Vlan100 172.16.100.1 protocol-down/link-down/admin-down
Lo1 1.1.1.1 protocol-down/link-down/admin-up
Lo2 2.2.2.2 protocol-up/link-up/admin-up
Eth1/3 192.168.1.10 protocol-down/link-down/admin-down
Eth1/5 172.16.1.1 protocol-down/link-down/admin-down
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you prepend the run
method with one of the filters from earlier, this command can be run against a subset of hosts. The following modification of the demo.py
script will target only hosts with the baker role. (Output is, again, truncated for brevity.)
result = nr.filter(role="baker").run(
task=send_command, command="show ip interface brief"
)
print_result(result)
send_command********************************************************************
* router1 ** changed : False ***************************************************
vvvv send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Wed Jul 13 18:44:47.061 UTC
Interface IP-Address Status Protocol Vrf-Name
BVI227 unassigned Shutdown Down default
BVI511 10.200.188.33 Down Down default
Bundle-Ether10 unassigned Down Down default
Loopback0 2.2.2.2 Up Up default
Loopback1 2.2.2.20 Up Down default
tunnel-ip99 1.1.1.1 Down Down default
MgmtEth0/RP0/CPU0/0 10.10.20.175 Up Up default
GigabitEthernet0/0/0/0 unassigned Down Down default
GigabitEthernet0/0/0/0.100 10.1.1.1 Down Down default
^^^^ END send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Note: These are publicly accessible sandbox devices. Their configuration and output is subject to constant change.
Defining Custom Tasks
A task is a reusable chunk of code that implements logic to be run on a single host. Every task is a function that takes a Task object as its first argument and returns a Result object. Community plugins integrating popular network libraries such as Netmiko and Napalm provide many useful prebuilt tasks, and it is simple to create your own, or to extend existing tasks.
# /demo_2.py
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_utils.plugins.functions import print_result
nr = InitNornir("config.yaml")
def hello_world_task(task: Task) -> Result:
"""
Your first custom Nornir task!
"""
return Result(host=task.host, result=f"{task.host.name} says hello!")
result = nr.filter(site="sandbox").run(task=hello_world_task)
print_result(result)
hello_world_task****************************************************************
* router1 ** changed : False ***************************************************
vvvv hello_world_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
router1 says hello!
^^^^ END hello_world_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* router2 ** changed : False ***************************************************
vvvv hello_world_task ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
router2 says hello!
^^^^ END hello_world_task ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Tasks can call other tasks or manipulate the data in the Nornir object’s inventory. Scrapli can use pyATS Genie to parse device output into a Python dictionary, which can then be associated with the host the data was gathered from. Thanks to the task argument passed to the custom task function, it has access to the inventory data of the targeted hosts.
# /demo_3.py
from pprint import pprint
from nornir import InitNornir
from nornir.core.task import Task, Result
from nornir_scrapli.tasks import send_command
nr = InitNornir("config.yaml")
# This task populates the inventory of the Nornir object with
# the host's "show interface status" data.
def populate_host_interface_status(task: Task) -> None:
"""
Get the live interface status from a device.
Transform the response into structured data associated with each host.
"""
result = task.run(task=send_command, command="show ip interface brief")
task.host["interface_status"] = result.scrapli_response.genie_parse_output()
nr.run(task=populate_host_interface_status)
router2 = nr.inventory.hosts["router2"]
pprint(router2["interface_status"])
Output:
{'interface': {'GigabitEthernet1': {'interface_is_ok': 'YES',
'ip_address': '10.10.20.48',
'method': 'NVRAM',
'protocol': 'up',
'status': 'up'},
'GigabitEthernet2': {'interface_is_ok': 'YES',
'ip_address': '10.255.255.1',
'method': 'other',
'protocol': 'up',
'status': 'up'},
'GigabitEthernet3': {'interface_is_ok': 'YES',
'ip_address': 'unassigned',
'method': 'NVRAM',
'protocol': 'down',
'status': 'administratively down'},
'Loopback100': {'interface_is_ok': 'YES',
'ip_address': '172.16.100.1',
'method': 'other',
'protocol': 'up',
'status': 'up'},
'Loopback11': {'interface_is_ok': 'YES',
'ip_address': 'unassigned',
'method': 'unset',
'protocol': 'up',
'status': 'up'},
'Loopback111': {'interface_is_ok': 'YES',
'ip_address': '10.10.30.1',
'method': 'manual',
'protocol': 'up',
'status': 'up'},
'Loopback21': {'interface_is_ok': 'YES',
'ip_address': 'unassigned',
'method': 'unset',
'protocol': 'up',
'status': 'up'},
'Tunnel0': {'interface_is_ok': 'YES',
'ip_address': '10.10.30.1',
'method': 'TFTP',
'protocol': 'up',
'status': 'up'}}}
Conclusion
Nornir is a powerful framework due to its focus and simplicity. It is an excellent option for teams who do not intend to use the breadth of functionality from a more batteries-included framework, teams with a strong foundation in Python programming, or teams looking to integrate an automation framework into an existing Python ecosystem. Make sure to check out the full Nornir documentation and list of Nornir plugins from the community, and don’t forget Network to Code’s Nornir channel on Slack.
– Boabdil
New to Python libraries? NTC’s Training Academy is holding a 3-day course Automating Networks with Python I on September 26-28, 2022 with 50% labs to get you up to speed.
Visit our 2022 public course schedule to see our full list.
Tags :
Contact Us to Learn More
Share details about yourself & someone from our team will reach out to you ASAP!