Processing results

In this tutorial we are going to see how we can process the results of running tasks.

Let’s start with some code similar to what we have seen already:

[1]:
import logging

from nornir import InitNornir
from nornir.core.task import Task, Result

# instantiate the nr object
nr = InitNornir(config_file="config.yaml")
# let's filter it down to simplify the output
cmh = nr.filter(site="cmh", type="host")

def count(task: Task, number: int) -> Result:
    return Result(
        host=task.host,
        result=f"{[n for n in range(0, number)]}"
    )

def say(task: Task, text: str) -> Result:
    if task.host.name == "host2.cmh":
        raise Exception("I can't say anything right now")
    return Result(
        host=task.host,
        result=f"{task.host.name} says {text}"
    )

Nothing new so far, we have hardcoded an error though in the say task, this will help us illustrate some concepts. Now let’s create a variant of the grouped task we saw in an earlier tutorial:

[2]:
def greet_and_count(task: Task, number: int):
    task.run(
        name="Greeting is the polite thing to do",
        severity_level=logging.DEBUG,
        task=say,
        text="hi!",
    )

    task.run(
        name="Counting beans",
        task=count,
        number=number,
    )
    task.run(
        name="We should say bye too",
        severity_level=logging.DEBUG,
        task=say,
        text="bye!",
    )

    # let's inform if we counted even or odd times
    even_or_odds = "even" if number % 2 == 1 else "odd"
    return Result(
        host=task.host,
        result=f"{task.host} counted {even_or_odds} times!",
    )

It is pretty much the same grouped task as before with the difference we are passing severity_level=logging.DEBUG to the first and last subtasks. We will see what that means later on.

Now, let’s call the task group so we can start inspecting the result object:

[3]:
result = cmh.run(
    task=greet_and_count,
    number=5,
)

The easy way

Most of the time you will just want to provide some feedback on what’s going on. For that you can use the print_result function that comes with the package nornir_utils:

[4]:
from nornir_utils.plugins.functions import print_result

print_result(result)
greet_and_count*****************************************************************
* host1.cmh ** changed : False *************************************************
vvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
host1.cmh counted even times!
---- Counting beans ** changed : False ----------------------------------------- INFO
[0, 1, 2, 3, 4]
^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* host2.cmh ** changed : False *************************************************
vvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR
Subtask: Greeting is the polite thing to do (failed)

---- Greeting is the polite thing to do ** changed : False --------------------- ERROR
Traceback (most recent call last):
  File "/nornir/core/task.py", line 99, in start
    r = self.task(self, **self.params)
  File "/tmp/ipykernel_8460/3588580986.py", line 19, in say
    raise Exception("I can't say anything right now")
Exception: I can't say anything right now

^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You should also be able to print a single host:

[5]:
print_result(result["host1.cmh"])
vvvv host1.cmh: greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
host1.cmh counted even times!
---- Counting beans ** changed : False ----------------------------------------- INFO
[0, 1, 2, 3, 4]
^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Or even a single task:

[6]:
print_result(result["host1.cmh"][2])
---- host1.cmh: Counting beans ** changed : False ------------------------------ INFO
[0, 1, 2, 3, 4]

As you probably noticed, not all the tasks were printed. This is due to the severity_level argument we passed. This let’s us flag tasks with any of the logging levels. Then print_result is able to follow logging rules to print the results. By default only tasks marked as INFO will be printed (this is also the default for the tasks if none is specified).

A failed task will always have its severity level changed to ERROR regardless of the one specified by the user. You can see that in the task Greeting is the polite thing to do for host2.cmh.

Now let’s tell print_result to print tasks marked as DEBUG.

[7]:
print_result(result, severity_level=logging.DEBUG)
greet_and_count*****************************************************************
* host1.cmh ** changed : False *************************************************
vvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
host1.cmh counted even times!
---- Greeting is the polite thing to do ** changed : False --------------------- DEBUG
host1.cmh says hi!
---- Counting beans ** changed : False ----------------------------------------- INFO
[0, 1, 2, 3, 4]
---- We should say bye too ** changed : False ---------------------------------- DEBUG
host1.cmh says bye!
^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* host2.cmh ** changed : False *************************************************
vvvv greet_and_count ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv ERROR
Subtask: Greeting is the polite thing to do (failed)

---- Greeting is the polite thing to do ** changed : False --------------------- ERROR
Traceback (most recent call last):
  File "/nornir/core/task.py", line 99, in start
    r = self.task(self, **self.params)
  File "/tmp/ipykernel_8460/3588580986.py", line 19, in say
    raise Exception("I can't say anything right now")
Exception: I can't say anything right now

^^^^ END greet_and_count ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Now we got all tasks printed. You can also see the severity level at the rightmost column of the output.

The programmatic way

We have hinted at how to deal with result objects already, but let’s elaborate on that. To begin with, task groups will return an AggregatedResult. This object is a dict-like object you can use to iterate over or access hosts directly:

[8]:
result
[8]:
AggregatedResult (greet_and_count): {'host1.cmh': MultiResult: [Result: "greet_and_count", Result: "Greeting is the polite thing to do", Result: "Counting beans", Result: "We should say bye too"], 'host2.cmh': MultiResult: [Result: "greet_and_count", Result: "Greeting is the polite thing to do"]}
[9]:
result.keys()
[9]:
dict_keys(['host1.cmh', 'host2.cmh'])
[10]:
result["host1.cmh"]
[10]:
MultiResult: [Result: "greet_and_count", Result: "Greeting is the polite thing to do", Result: "Counting beans", Result: "We should say bye too"]

You probably noticed that inside each key in AggregatedResult there is a MultiResult object. This object is a list-like object you can use to iterate over or access any Result you want:

[11]:
result["host1.cmh"][0]
[11]:
Result: "greet_and_count"

Both MultiResult and Result should clearly indicate if there was some error or change in the system:

[12]:
print("changed: ", result["host1.cmh"].changed)
print("failed: ", result["host1.cmh"].failed)
changed:  False
failed:  False

[13]:
print("changed: ", result["host2.cmh"][0].changed)
print("failed: ", result["host2.cmh"][0].failed)
changed:  False
failed:  True