router image

An Introduction to Nornir

By Kirk Byers
2018-08-07

Recently David Barroso and others (including me) have created a new network automation framework named Nornir. The vast majority of the credit belongs to David, however.

Nornir is a Python framework that provides inventory management and concurrency. It belongs in the same category as Ansible and Salt.

I am excited about Nornir and the possibility of an all Python automation framework.

Note, while I refer to an all 'Python framework', this is referring to the execution environment. In other words, what you write your program in and what you execute. Nornir's inventory system supports a YAML-based inventory format. There is some nuance in this statement as Nornir's inventory has multiple plugins which I will discuss below. You can also couple Nornir to Jinja2 for configuration templating. This is similar to coupling Python to Jinja2.



Let's use a simple inventory.

Now the first thing to note about Nornir is that there are two components to the inventory system. There are core inventory objects and there are plugins that feed/parse some data source to create these core inventory objects.

For example, Nornir has a SimpleInventory plugin which uses two YAML files (hosts.yaml and groups.yaml). These YAML files are parsed and used to create the core inventory objects. Similarly, there is a Nornir-Ansible inventory plugin and a Nornir-NetBox inventory plugin is being worked on. You could also create your own inventory plugin.

For this article, I am just going to use the SimpleInventory plugin. Additionally, I will only have three devices in my inventory: one Cisco IOS device, one Arista EOS device, and one Juniper SRX. While this is a very simple inventory, it will illustrate many of the relevant principles.


We will start with a blank slate: two empty inventory files named hosts.yaml and groups.yaml.

Now, we need to figure out how these inventory files should be structured so let's look at the Nornir docs and copy an example from there:

Nornir Inventory Documentation


Note, I am using Nornir 1.1.0 and am referring to documentation corresponding to that. Nornir's inventory will be changing in a meaningful way in Nornir 2.0 so you will need to review the differences between Nornir 1.x and Nornir 2.x (once Nornir 2.x is released).

I am going to copy the below block directly from the above referenced tutorial and place it into a file named hosts.yaml.

---
# Strings were quoted to render better on the web
host1.cmh:
    nornir_host: '127.0.0.1'
    nornir_ssh_port: 2201
    nornir_username: 'vagrant'
    nornir_password: 'vagrant'
    site: 'cmh'
    role: 'host'
    groups:
        - 'cmh'
    nornir_nos: 'linux'
    type: 'host'  

I will start with the following groups.yaml file which once again is copy-and-pasted from the above tutorial. I then removed a set of unused groups.

---
# Strings were quoted to render better on the web
defaults:
    domain: 'acme.local'

cmh:
    asn: 65000
    vlans:
      100: 'frontend'
      200: 'backend'      


I then updated the hosts.yaml to reflect one of the devices in my environment.

---
# Strings were quoted to render better on the web
rtr1:
    nornir_host: 'cisco1.domain.com'
    nornir_ssh_port: 22
    nornir_username: 'admin'
    nornir_password: 'password'
    groups:
        - 'cisco_ios'
    nornir_nos: 'cisco_ios'   

Similarly I updated the groups.yaml as follows:

---
defaults: {}

cisco_ios: {}      

For now I just set groups.yaml to a null dictionary for both the 'cisco_ios' group and for the 'defaults'. The defaults is a catch-all that applies if there is not a more specific setting at the groups or hosts level.


Now let's see if this inventory is parsed and loaded into core Nornir inventory objects.

In order to do this, we can create a Python program that only contains:

from nornir.core import InitNornir
nr = InitNornir()       

Now I can test this using the Python debugger:

$ python -m pdb simple_test.py

[Output simplified]

(Pdb) list .
  1  ->    from nornir.core import InitNornir
  2        nr = InitNornir()
[EOF]

(Pdb) n
(Pdb) n
(Pdb) p nr
<nornir.core.Nornir object at 0x7f1c928ebf28>           

I can see that 'nr' is a Nornir object.

I can also further dig into the Nornir object and see that the inventory has been parsed:

(Pdb) p nr.inventory.hosts
{'rtr1': Host: rtr1}
(Pdb) p nr.inventory.groups
{'cisco_ios': Group: cisco_ios}
(Pdb)       


Now let's expand our inventory to three devices:

---
# hosts.yaml file
# Strings were quoted to render better on the web
rtr1:
    nornir_host: 'cisco1.domain.com'
    groups:
        - 'cisco_ios'

sw1:
    nornir_host: 'arista1.domain.com'
    groups:
        - 'arista'

srx1:
    nornir_host: 'srx1.domain.com'
    groups:
        - 'juniper'  
---
# groups.yaml file
# Strings were quoted to render better on the web
defaults:
    nornir_username: 'admin'
    nornir_password: 'password'

cisco_ios:
    nornir_nos: 'cisco_ios'

arista:
    nornir_nos: 'arista_eos'

juniper:
    nornir_nos: 'juniper_junos'       

I moved the 'nornir_username' and 'nornir_password' to the 'defaults' section as the credentials are the same for all of the devices. I also moved the nornir_nos attribute to each group as I am anticipating I will eventually have multiple devices in each group and this attribute will be the same for all of them.



Now let's once again repeat our Python program and inspect the inventory parsing:

from nornir.core import InitNornir
nr = InitNornir()   

And in Pdb:

$ python -m pdb simple_test.py

[Output simplified]

(Pdb) list .
  1  ->    from nornir.core import InitNornir
  2        nr = InitNornir()
[EOF]

(Pdb) n
(Pdb) n

(Pdb) p nr.inventory.hosts
{'rtr1': Host: rtr1, 'sw1': Host: sw1, 'srx1': Host: srx1}
(Pdb) p nr.inventory.groups
{'cisco_ios': Group: cisco_ios, 'arista': Group: arista, 'juniper': Group: juniper}    

We are once again parsing the inventory and now have three hosts and three different groups (in addition to the defaults).



Now we have an initial inventory setup, let's try to do something with these devices.

In particular, let's try to execute a Netmiko task on all three devices. Now one nice thing about Nornir is that concurrency is built-in so execution of this task will happen concurrently on all three devices. Additionally, Nornir has Netmiko plugins built into it (i.e. there is a built-in integration to Netmiko). In particular, there are the following Netmiko plugins:

Netmiko Task Plugins


Additionally, there is a separate Netmiko connection plugin:

Netmiko Connection Plugin


Let's use this netmiko_send_command plugin and retrieve 'show arp' from all three devices.

Note, I intentionally chose a command that would directly work on all three platforms (IOS, EOS, Junos). Our code now looks as follows:

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_send_command
from nornir.plugins.functions.text import print_result

nr = InitNornir()

result = nr.run(
    task=netmiko_send_command,
    command_string="show arp"
)

print_result(result)        

We have our InitNornir as we previously had. We now also have an import for the 'netmiko_send_command' plugin and also an import or the 'print_result' task. I will explain 'print_result' and why we need it shortly.

Now, let's look at where we actually execute our code against all of the devices in the inventory. This happens here:

result = nr.run(
    task=netmiko_send_command,
    command_string="show arp"
)           

Basically, the Nornir object ('nr' in this example) has a method named 'run' which by default will run the specified task concurrently on the hosts in the inventory. We then specify an additional argument named 'command_string' which is just the first argument passed into the Netmiko send_command() method (i.e. the command to be run on the remote devices).

When we execute this, the task will be executed concurrently on all three of the devices in the inventory. Additionally, Nornir will automatically establish the Netmiko connection using information that we provided in inventory.



Now there is a bit of complexity with the 'result'.

Let's look at this in the Python debugger:

(Pdb) p result
AggregatedResult (netmiko_send_command): 
{
    'rtr1': MultiResult: [Result: "netmiko_send_command"], 
    'sw1': MultiResult: [Result: "netmiko_send_command"], 
    'srx1': MultiResult: [Result: "netmiko_send_command"]
}       

We see that 'result' is an object of type 'AggregatedResult'. AggregatedResult is an object that contains the results for all the hosts the task was executed on. The AggregatedResult object behaves like a dictionary so you can look at a particular host-key and see:

(Pdb) p result['rtr1']
MultiResult: [Result: "netmiko_send_command"]       

This entry returns a 'MultiResult' object. MultiResult covers the case where a given task might have multiple sub-tasks each with their own result. In our code, we only have one task. Consequently, MultiResult only has a single entry (for each host). MultiResult behaves like a list so I can do the following:

(Pdb) p result['rtr1']
(Pdb) p result['rtr1'][0]
Result: "netmiko_send_command"  

Now finally I have the actual 'Result' object (i.e. the object that contains the 'show arp' output). For this object, I need to access the 'result' attribute:

(Pdb) !print(result['rtr1'][0].result)
Protocol  Address  Age (min) Hardware Addr   Type   Interface
Internet  10.220.88.1     26 0062.ec29.70fe  ARPA   FastEthernet4
Internet  10.220.88.20     - c89c.1dea.0eb6  ARPA   FastEthernet4
Internet  10.220.88.21    62 1c6a.7aaf.576c  ARPA   FastEthernet4
Internet  10.220.88.28   173 5254.aba8.9aea  ARPA   FastEthernet4
Internet  10.220.88.29    67 5254.abbe.5b7b  ARPA   FastEthernet4
Internet  10.220.88.30   138 5254.ab71.e119  ARPA   FastEthernet4
Internet  10.220.88.32   230 5254.abc7.26aa  ARPA   FastEthernet4
Internet  10.220.88.33    60 5254.ab3a.8d26  ARPA   FastEthernet4
Internet  10.220.88.35   222 5254.abfb.af12  ARPA   FastEthernet4
Internet  10.220.88.37    12 0001.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.38   203 0002.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.39     2 6464.9be8.08c8  ARPA   FastEthernet4       


That was quite a bit of processing to just obtain the output, but 'print_result' will unwrap all of this for you automatically.

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_send_command
from nornir.plugins.functions.text import print_result

nr = InitNornir()

result = nr.run(
    task=netmiko_send_command,
    command_string="show arp"
)

print_result(result)        

Let's see what happens when we execute this entire program. Note, I cleaned up the output a little bit to make it easier to read.

$ date; python simple_test.py; date
Mon Aug  6 13:42:12 PDT 2018

netmiko_send_command *************************
* rtr1 ** changed : False ********************
vvvv netmiko_send_command ** changed : False vvvvvvvv INFO
Protocol Address Age (min) Hardware Addr Type Interface
Internet 10.220.88.1     1 0062.ec29.70fe  ARPA   FastEthernet4
Internet  10.220.88.20   - c89c.1dea.0eb6  ARPA   FastEthernet4
Internet  10.220.88.21  69 1c6a.7aaf.576c  ARPA   FastEthernet4
Internet  10.220.88.28 180 5254.aba8.9aea  ARPA   FastEthernet4
Internet  10.220.88.29  75 5254.abbe.5b7b  ARPA   FastEthernet4
Internet  10.220.88.30 145 5254.ab71.e119  ARPA   FastEthernet4
Internet  10.220.88.32 238 5254.abc7.26aa  ARPA   FastEthernet4
Internet  10.220.88.33  67 5254.ab3a.8d26  ARPA   FastEthernet4
Internet  10.220.88.35 230 5254.abfb.af12  ARPA   FastEthernet4
Internet  10.220.88.37  20 0001.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.38 211 0002.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.39  10 6464.9be8.08c8  ARPA   FastEthernet4
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^

* srx1 ** changed : False *******************
vvvv netmiko_send_command ** changed : False vvvvvvvvv INFO

MAC Address       Address       Name         Interface   Flags
00:62:ec:29:70:fe 10.220.88.1   10.220.88.1  vlan.0      none
c8:9c:1d:ea:0e:b6 10.220.88.20  10.220.88.20 vlan.0      none
Total entries: 2

^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^

* sw1 ** changed : False ************************
vvvv netmiko_send_command ** changed : False vvvvvv INFO
Address    Age (min)  Hardware Addr   Interface
10.220.88.1        0  0062.ec29.70fe  Vlan1, Ethernet1
10.220.88.20       0  c89c.1dea.0eb6  Vlan1, not learned
10.220.88.21       0  1c6a.7aaf.576c  Vlan1, not learned
10.220.88.29       0  5254.abbe.5b7b  Vlan1, not learned
10.220.88.30       0  5254.ab71.e119  Vlan1, not learned
10.220.88.31       0  5254.ab81.5693  Vlan1, not learned
10.220.88.37       0  0001.00ff.0001  Vlan1, not learned
10.220.88.38       0  0002.00ff.0001  Vlan1, not learned
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^

Mon Aug  6 13:42:21 PDT 2018        


As you can see, I retrieved the output from all three devices using 'print_result'.

Additionally, I recorded the execution time and it took nine seconds to execute. It is a bit hard to see given there are only three devices, but the SSH connections are being executed concurrently.



For additional references see:

Nornir Tutorial

Exploring Nornir: The Python Automation Framework




If you want to learn more about network automation, Python, and Ansible—then join my email-list. I also periodically run a free Python for Network Engineers email course which you can sign-up for here.

router image

Kirk Byers

router image

An Introduction to Nornir

By Kirk Byers
2018-08-07

Recently David Barroso and others (including me) have created a new network automation framework named Nornir. The vast majority of the credit belongs to David, however.

Nornir is a Python framework that provides inventory management and concurrency. It belongs in the same category as Ansible and Salt.

I am excited about Nornir and the possibility of an all Python automation framework.

Note, while I refer to an all 'Python framework', this is referring to the execution environment. In other words, what you write your program in and what you execute. Nornir's inventory system supports a YAML-based inventory format. There is some nuance in this statement as Nornir's inventory has multiple plugins which I will discuss below. You can also couple Nornir to Jinja2 for configuration templating. This is similar to coupling Python to Jinja2.



Let's use a simple inventory.

Now the first thing to note about Nornir is that there are two components to the inventory system. There are core inventory objects and there are plugins that feed/parse some data source to create these core inventory objects.

For example, Nornir has a SimpleInventory plugin which uses two YAML files (hosts.yaml and groups.yaml). These YAML files are parsed and used to create the core inventory objects. Similarly, there is a Nornir-Ansible inventory plugin and a Nornir-NetBox inventory plugin is being worked on. You could also create your own inventory plugin.

For this article, I am just going to use the SimpleInventory plugin. Additionally, I will only have three devices in my inventory: one Cisco IOS device, one Arista EOS device, and one Juniper SRX. While this is a very simple inventory, it will illustrate many of the relevant principles.


We will start with a blank slate: two empty inventory files named hosts.yaml and groups.yaml.

Now, we need to figure out how these inventory files should be structured so let's look at the Nornir docs and copy an example from there:

Nornir Inventory Documentation


Note, I am using Nornir 1.1.0 and am referring to documentation corresponding to that. Nornir's inventory will be changing in a meaningful way in Nornir 2.0 so you will need to review the differences between Nornir 1.x and Nornir 2.x (once Nornir 2.x is released).

I am going to copy the below block directly from the above referenced tutorial and place it into a file named hosts.yaml.

---
# Strings were quoted to render better on the web
host1.cmh:
    nornir_host: '127.0.0.1'
    nornir_ssh_port: 2201
    nornir_username: 'vagrant'
    nornir_password: 'vagrant'
    site: 'cmh'
    role: 'host'
    groups:
        - 'cmh'
    nornir_nos: 'linux'
    type: 'host'  

I will start with the following groups.yaml file which once again is copy-and-pasted from the above tutorial. I then removed a set of unused groups.

---
# Strings were quoted to render better on the web
defaults:
    domain: 'acme.local'

cmh:
    asn: 65000
    vlans:
      100: 'frontend'
      200: 'backend'      


I then updated the hosts.yaml to reflect one of the devices in my environment.

---
# Strings were quoted to render better on the web
rtr1:
    nornir_host: 'cisco1.domain.com'
    nornir_ssh_port: 22
    nornir_username: 'admin'
    nornir_password: 'password'
    groups:
        - 'cisco_ios'
    nornir_nos: 'cisco_ios'   

Similarly I updated the groups.yaml as follows:

---
defaults: {}

cisco_ios: {}      

For now I just set groups.yaml to a null dictionary for both the 'cisco_ios' group and for the 'defaults'. The defaults is a catch-all that applies if there is not a more specific setting at the groups or hosts level.


Now let's see if this inventory is parsed and loaded into core Nornir inventory objects.

In order to do this, we can create a Python program that only contains:

from nornir.core import InitNornir
nr = InitNornir()       

Now I can test this using the Python debugger:

$ python -m pdb simple_test.py

[Output simplified]

(Pdb) list .
  1  ->    from nornir.core import InitNornir
  2        nr = InitNornir()
[EOF]

(Pdb) n
(Pdb) n
(Pdb) p nr
<nornir.core.Nornir object at 0x7f1c928ebf28>           

I can see that 'nr' is a Nornir object.

I can also further dig into the Nornir object and see that the inventory has been parsed:

(Pdb) p nr.inventory.hosts
{'rtr1': Host: rtr1}
(Pdb) p nr.inventory.groups
{'cisco_ios': Group: cisco_ios}
(Pdb)       


Now let's expand our inventory to three devices:

---
# hosts.yaml file
# Strings were quoted to render better on the web
rtr1:
    nornir_host: 'cisco1.domain.com'
    groups:
        - 'cisco_ios'

sw1:
    nornir_host: 'arista1.domain.com'
    groups:
        - 'arista'

srx1:
    nornir_host: 'srx1.domain.com'
    groups:
        - 'juniper'  
---
# groups.yaml file
# Strings were quoted to render better on the web
defaults:
    nornir_username: 'admin'
    nornir_password: 'password'

cisco_ios:
    nornir_nos: 'cisco_ios'

arista:
    nornir_nos: 'arista_eos'

juniper:
    nornir_nos: 'juniper_junos'       

I moved the 'nornir_username' and 'nornir_password' to the 'defaults' section as the credentials are the same for all of the devices. I also moved the nornir_nos attribute to each group as I am anticipating I will eventually have multiple devices in each group and this attribute will be the same for all of them.



Now let's once again repeat our Python program and inspect the inventory parsing:

from nornir.core import InitNornir
nr = InitNornir()   

And in Pdb:

$ python -m pdb simple_test.py

[Output simplified]

(Pdb) list .
  1  ->    from nornir.core import InitNornir
  2        nr = InitNornir()
[EOF]

(Pdb) n
(Pdb) n

(Pdb) p nr.inventory.hosts
{'rtr1': Host: rtr1, 'sw1': Host: sw1, 'srx1': Host: srx1}
(Pdb) p nr.inventory.groups
{'cisco_ios': Group: cisco_ios, 'arista': Group: arista, 'juniper': Group: juniper}    

We are once again parsing the inventory and now have three hosts and three different groups (in addition to the defaults).



Now we have an initial inventory setup, let's try to do something with these devices.

In particular, let's try to execute a Netmiko task on all three devices. Now one nice thing about Nornir is that concurrency is built-in so execution of this task will happen concurrently on all three devices. Additionally, Nornir has Netmiko plugins built into it (i.e. there is a built-in integration to Netmiko). In particular, there are the following Netmiko plugins:

Netmiko Task Plugins


Additionally, there is a separate Netmiko connection plugin:

Netmiko Connection Plugin


Let's use this netmiko_send_command plugin and retrieve 'show arp' from all three devices.

Note, I intentionally chose a command that would directly work on all three platforms (IOS, EOS, Junos). Our code now looks as follows:

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_send_command
from nornir.plugins.functions.text import print_result

nr = InitNornir()

result = nr.run(
    task=netmiko_send_command,
    command_string="show arp"
)

print_result(result)        

We have our InitNornir as we previously had. We now also have an import for the 'netmiko_send_command' plugin and also an import or the 'print_result' task. I will explain 'print_result' and why we need it shortly.

Now, let's look at where we actually execute our code against all of the devices in the inventory. This happens here:

result = nr.run(
    task=netmiko_send_command,
    command_string="show arp"
)           

Basically, the Nornir object ('nr' in this example) has a method named 'run' which by default will run the specified task concurrently on the hosts in the inventory. We then specify an additional argument named 'command_string' which is just the first argument passed into the Netmiko send_command() method (i.e. the command to be run on the remote devices).

When we execute this, the task will be executed concurrently on all three of the devices in the inventory. Additionally, Nornir will automatically establish the Netmiko connection using information that we provided in inventory.



Now there is a bit of complexity with the 'result'.

Let's look at this in the Python debugger:

(Pdb) p result
AggregatedResult (netmiko_send_command): 
{
    'rtr1': MultiResult: [Result: "netmiko_send_command"], 
    'sw1': MultiResult: [Result: "netmiko_send_command"], 
    'srx1': MultiResult: [Result: "netmiko_send_command"]
}       

We see that 'result' is an object of type 'AggregatedResult'. AggregatedResult is an object that contains the results for all the hosts the task was executed on. The AggregatedResult object behaves like a dictionary so you can look at a particular host-key and see:

(Pdb) p result['rtr1']
MultiResult: [Result: "netmiko_send_command"]       

This entry returns a 'MultiResult' object. MultiResult covers the case where a given task might have multiple sub-tasks each with their own result. In our code, we only have one task. Consequently, MultiResult only has a single entry (for each host). MultiResult behaves like a list so I can do the following:

(Pdb) p result['rtr1']
(Pdb) p result['rtr1'][0]
Result: "netmiko_send_command"  

Now finally I have the actual 'Result' object (i.e. the object that contains the 'show arp' output). For this object, I need to access the 'result' attribute:

(Pdb) !print(result['rtr1'][0].result)
Protocol  Address  Age (min) Hardware Addr   Type   Interface
Internet  10.220.88.1     26 0062.ec29.70fe  ARPA   FastEthernet4
Internet  10.220.88.20     - c89c.1dea.0eb6  ARPA   FastEthernet4
Internet  10.220.88.21    62 1c6a.7aaf.576c  ARPA   FastEthernet4
Internet  10.220.88.28   173 5254.aba8.9aea  ARPA   FastEthernet4
Internet  10.220.88.29    67 5254.abbe.5b7b  ARPA   FastEthernet4
Internet  10.220.88.30   138 5254.ab71.e119  ARPA   FastEthernet4
Internet  10.220.88.32   230 5254.abc7.26aa  ARPA   FastEthernet4
Internet  10.220.88.33    60 5254.ab3a.8d26  ARPA   FastEthernet4
Internet  10.220.88.35   222 5254.abfb.af12  ARPA   FastEthernet4
Internet  10.220.88.37    12 0001.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.38   203 0002.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.39     2 6464.9be8.08c8  ARPA   FastEthernet4       


That was quite a bit of processing to just obtain the output, but 'print_result' will unwrap all of this for you automatically.

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_send_command
from nornir.plugins.functions.text import print_result

nr = InitNornir()

result = nr.run(
    task=netmiko_send_command,
    command_string="show arp"
)

print_result(result)        

Let's see what happens when we execute this entire program. Note, I cleaned up the output a little bit to make it easier to read.

$ date; python simple_test.py; date
Mon Aug  6 13:42:12 PDT 2018

netmiko_send_command *************************
* rtr1 ** changed : False ********************
vvvv netmiko_send_command ** changed : False vvvvvvvv INFO
Protocol Address Age (min) Hardware Addr Type Interface
Internet 10.220.88.1     1 0062.ec29.70fe  ARPA   FastEthernet4
Internet  10.220.88.20   - c89c.1dea.0eb6  ARPA   FastEthernet4
Internet  10.220.88.21  69 1c6a.7aaf.576c  ARPA   FastEthernet4
Internet  10.220.88.28 180 5254.aba8.9aea  ARPA   FastEthernet4
Internet  10.220.88.29  75 5254.abbe.5b7b  ARPA   FastEthernet4
Internet  10.220.88.30 145 5254.ab71.e119  ARPA   FastEthernet4
Internet  10.220.88.32 238 5254.abc7.26aa  ARPA   FastEthernet4
Internet  10.220.88.33  67 5254.ab3a.8d26  ARPA   FastEthernet4
Internet  10.220.88.35 230 5254.abfb.af12  ARPA   FastEthernet4
Internet  10.220.88.37  20 0001.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.38 211 0002.00ff.0001  ARPA   FastEthernet4
Internet  10.220.88.39  10 6464.9be8.08c8  ARPA   FastEthernet4
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^

* srx1 ** changed : False *******************
vvvv netmiko_send_command ** changed : False vvvvvvvvv INFO

MAC Address       Address       Name         Interface   Flags
00:62:ec:29:70:fe 10.220.88.1   10.220.88.1  vlan.0      none
c8:9c:1d:ea:0e:b6 10.220.88.20  10.220.88.20 vlan.0      none
Total entries: 2

^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^

* sw1 ** changed : False ************************
vvvv netmiko_send_command ** changed : False vvvvvv INFO
Address    Age (min)  Hardware Addr   Interface
10.220.88.1        0  0062.ec29.70fe  Vlan1, Ethernet1
10.220.88.20       0  c89c.1dea.0eb6  Vlan1, not learned
10.220.88.21       0  1c6a.7aaf.576c  Vlan1, not learned
10.220.88.29       0  5254.abbe.5b7b  Vlan1, not learned
10.220.88.30       0  5254.ab71.e119  Vlan1, not learned
10.220.88.31       0  5254.ab81.5693  Vlan1, not learned
10.220.88.37       0  0001.00ff.0001  Vlan1, not learned
10.220.88.38       0  0002.00ff.0001  Vlan1, not learned
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^

Mon Aug  6 13:42:21 PDT 2018        


As you can see, I retrieved the output from all three devices using 'print_result'.

Additionally, I recorded the execution time and it took nine seconds to execute. It is a bit hard to see given there are only three devices, but the SSH connections are being executed concurrently.



For additional references see:

Nornir Tutorial

Exploring Nornir: The Python Automation Framework




If you want to learn more about network automation, Python, and Ansible—then join my email-list. I also periodically run a free Python for Network Engineers email course which you can sign-up for here.

router image

Kirk Byers