Nornir Result Object (IV)
So far in this Nornir series, we've covered what Nornir is, the problems it solves, and how to use it for tasks like connecting to multiple devices, managing inventory, and integrating Netmiko. In this part, we will explore the Nornir Result Object.
As we know, Nornir can run one or more tasks against one or more devices, providing results for each. But how exactly do we retrieve and interpret these results? For example, if you want to run two show commands on each device and save the outputs to a file or print the output to the terminal, how would you go about doing it?
Setting up the Environment
Since we covered the Nornir basics extensively in the previous parts, I won't spend much time on this. For the examples in this part, I will be working with three Arista devices but if you are using Cisco or any other vendor, you should be able to follow along. Here is the directory structure and the files we will be using.
.
├── config.yaml
├── defaults.yaml
├── groups.yaml
├── hosts.yaml
└── result_1.py
0 directories, 6 files
#config.yaml
---
inventory:
plugin: SimpleInventory
options:
host_file: 'hosts.yaml'
group_file: 'groups.yaml'
defaults_file: 'defaults.yaml'
runner:
plugin: threaded
options:
num_workers: 3
#hosts.yaml
---
r1:
hostname: 192.168.100.206
groups:
- arista
r2:
hostname: 192.168.100.207
groups:
- arista
r3:
hostname: 192.168.100.208
groups:
- arista
#groups.yaml
---
arista:
platform: arista_eos
#defaults.yaml
---
username: admin
password: admin
Introduction to Nornir Result Object
In the script below, we have two tasks, each runs a specific show command
on three Arista devices. The tasks execute sequentially - the first task retrieves the output from show ip interface brief | excl down
and the second task gets the output from show ip arp
, showing the ARP table entries.
After executing these commands on each device, the results are displayed in the terminal using the print_result
plugin.
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command
from nornir_utils.plugins.functions import print_result
def get_output(task):
task.run(task=netmiko_send_command, command_string='show ip interface brief | excl down')
task.run(task=netmiko_send_command, command_string='show ip arp')
nr = InitNornir(config_file='config.yaml')
results = nr.run(task=get_output)
print_result(results)
This works well for viewing results directly, but what if you want to capture the raw output from the device and use it for further processing? Simply printing the results
variable as it is won't work. Here's what you see when you try to print it.
print(results)
AggregatedResult (get_output): {
'r1': MultiResult: [Result: "get_output", Result: "netmiko_send_command", Result: "netmiko_send_command"],
'r2': MultiResult: [Result: "get_output", Result: "netmiko_send_command", Result: "netmiko_send_command"],
'r3': MultiResult: [Result: "get_output", Result: "netmiko_send_command", Result: "netmiko_send_command"]
}
This output, which is of type <class 'nornir.core.task.AggregatedResult'>
, consists of a collection of results for each device, each containing multiple task results.
Aggregated Result and Multi Result
In Nornir, when you run tasks across multiple devices, the results are organized into two main types - AggregatedResult
and MultiResult
.
- AggregatedResult - This is a container that groups results by device. Each device entry in an AggregatedResult holds a MultiResult, which represents all the tasks that were run on that device.
- MultiResult - A MultiResult is a list-like object containing the results of each individual task executed on a device. It helps you track each task's output and status separately within a single device's workflow.
As we discussed earlier, an AggregatedResult
is a container that holds results from all tasks run against all devices. Looking into this container, you find three MultiResults
Within each MultiResult
, you have three result objects (each is represented by a different colour, yellow, green, and blue in the diagram
The yellow represents our main function that contains two sub-tasks. When you examine the MultiResult for r1
, it appears like a list containing three items - the first item (in yellow) is the main function, and the subsequent two are the individual sub-tasks (green and blue) generated by the function.
So, if you want to access the output for the second command (show ip arp
) on r1, you'd navigate through the AggregatedResult
, which is like a dictionary. You'd specify the key for the router, r1
in this case, and then look at the third item in the list (index 2) because it corresponds to the second sub-task.
print(results['r1'][2].result)
Address Age (sec) Hardware Addr Interface
10.99.0.2 0:52:13 aac1.ab00.7c1d Ethernet2
Similarly, if you want to see the first sub-task (show ip interface brief
) output from r3, you would navigate through the result object like this.
print(results['r3'][1].result)
Address
Interface IP Address Status Protocol MTU Owner
----------------- ------------------------ ------------ -------------- ----------- -------
Ethernet1 172.16.1.3/24 up up 1500
Ethernet2 10.99.0.6/30 up up 1500
Loopback0 10.0.0.3/32 up up 65535
Management0 192.168.100.208/24 up up 1500
What Else Can We See from the Nornir Result Object?
The Nornir Result Object provides more than just raw output from devices. It includes several useful attributes related to each specific task. These attributes can tell you whether the task Failed. If it made any changes to the device and even the name of the task.
You can use the dir()
function to view all available properties and methods of a result object. The following dir()
method will list attributes like failed
, changed
, name
, and more, showing you the various properties you can access to get more details about the execution of each task.
dir(results['r3'][1])
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'changed', 'diff', 'exception', 'failed', 'host', 'name', 'result', 'severity_level', 'stderr', 'stdout']
print(results['r3'][1].name)
# Output: netmiko_send_command
print(results['r3'][1].failed)
# Output: False
print(results['r3'][1].changed)
# Output: False
Closing Thoughts
I hope this all makes sense to you and if you have any comments or feedback, please let me know in the comments.