router image

Using Nornir for OS Upgrades (Part 1)

By Kirk Byers
2018-08-28

Previously I wrote an article that Introduces Nornir

In this article, I want to expand on the Nornir introduction and do something more interesting. In particular, I want to use Nornir to Secure Copy files to a set of network devices. I then want to use that file transfer process and some additional code to perform an OS upgrade on two Cisco IOS routers (part 2).

Unfortunately, I am going to switch from using Nornir 1.1 to using Nornir 2.0. There are some meaningful differences between the two versions. Additionally, Nornir 2.0 is not released yet so I am going to be working out of the 2.0 branch in GitHub.



Nornir 2.0 Install Process


[All in a Python virtual environment]
$ git clone https://github.com/nornir-automation/nornir
Cloning into 'nornir'...

# CD into the repo
$ cd nornir/

# Verify current branch
$ git branch
* develop

$ git fetch origin

# Create a 2.0 branch that tracks the origin/2.0 branch
$ git checkout -b 2.0 origin/2.0
Branch 2.0 set up to track remote branch 2.0 from origin.
Switched to a new branch '2.0'

# Verify we are on the 2.0 branch
$ git branch
* 2.0
  develop

# Install Nornir and its dependencies (including development dependencies)
$ pip install -r ./requirements-dev.txt
$ pip install -e .   


Nornir Inventory

Now that I have Nornir installed, let's look at my Nornir Inventory.

Note, as per my previous Nornir Introduction article, I am using the Nornir SimpleInventory plugin. Here, I am going to show you my new inventory files (hosts.yaml and groups.yaml files). In this environment, I have two Cisco IOS routers, eight Arista vEOS switches, and two NX-OSv switches.

My hosts.yaml file is as follows:

---
pynet-rtr1:
    hostname: 'cisco1.domain.com'
    groups: 
      - 'cisco-ios' 

pynet-rtr2:
    hostname: 'cisco2.domain.com'
    groups: 
      - 'cisco-ios'

arista1:
    hostname: 'arista1.domain.com'
    groups: 
      - 'arista'

arista2:
    hostname: 'arista2.domain.com'
    groups: 
      - 'arista'

arista3:
    hostname: 'arista3.domain.com'
    groups: 
      - 'arista'

arista4:
    hostname: 'arista4.domain.com'
    groups: 
      - 'arista'

arista5:
    hostname: 'arista5.domain.com'
    groups: 
      - 'arista'

arista6:
    hostname: 'arista6.domain.com'
    groups: 
      - 'arista'

arista7:
    hostname: 'arista7.domain.com'
    groups: 
      - 'arista'

arista8:
    hostname: 'arista8.domain.com'
    groups: 
      - 'arista'

nxos1:
    hostname: 'nxos1.domain.com'
    groups: 
      - 'nxos'

nxos2:
    hostname: 'nxos2.domain.com'
    groups: 
      - 'nxos'  

Note, each host belongs to a group that corresponds to its platform-type ('cisco-ios', 'arista', 'nxos'). Also note, I had to add quotes to some of my YAML strings to make it render reasonably on the web.


My groups.yaml is as follows:

---
defaults: {}
  # username: admin
  # password: 

cisco-ios:
  platform: 'cisco_ios'
  img: 'c880data-universalk9-mz.155-3.M8.bin'
  backup_img: 'c880data-universalk9-mz.154-2.T1.bin'

arista:
  platform: 'arista_eos'
  img: 'test_arista.txt'

nxos:
  platform: 'cisco_nxos'
  img: 'test_nxos.txt'   

I could have set the username and password in the 'defaults' section (as shown by my commented out lines), but instead I am going to pass the username and password in using a function. This is so I can store my inventory in GitHub without having the credential embedded in my inventory.

The 'platform' specified in the inventory was intentionally chosen to match the Netmiko 'device_type'.

Finally, the 'img' variable indicates the filename that I am going to transfer. In this article, I am ultimately going to transfer a real image file to the Cisco IOS devices and just .txt files to the other devices.


Different platforms have different OS upgrade processes. I am going to show the file transfer process (Secure Copy) for all of the above devices (twelve total devices, three different platforms). After that, I will show the remaining steps that would be needed to complete the OS upgrade on the two Cisco IOS device. This second part will be in a later article.



File Transfer to the Twelve Devices

Now let's get a file transfer working to the twelve devices. At this point, I am going to transfer a simple text file as it will be much quicker to test and validate (i.e. the file transfer and MD5 will happen much faster).

Now Nornir has a set of imports that we need to perform in our Python code. This includes InitNornir which simplifies the Nornir initialization (basically the use of our SimpleInventory and the creation of a Nornir object). Additionally, we are going to need to import the 'netmiko_file_transfer' plugin. Our imports initially look as follows:

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_file_transfer    

I also have a couple of minor functions that I am importing: one that handles setting the username/password and one that prints out the output from the task. I add the following import:

from nornir_utilities import nornir_set_creds, std_print    

I then need to create the Nornir object (remember this is what parses my inventory):

norn = InitNornir(config_file="nornir.yml")   

I then call my nornir_set_creds(norn) function passing in the Nornir object.



At this point, I am ready to execute the 'netmiko_file_transfer' task.

Nornir has threading automatically built into it so Nornir will automatically transfer the file to the twelve devices in parallel.

My entire code at this point is as follows:

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_file_transfer

from nornir_utilities import nornir_set_creds, std_print


# Initialize Nornir object using hosts.yaml and groups.yaml
norn = InitNornir(config_file="nornir.yml")
nornir_set_creds(norn)
test_file = 'test_file4.txt'

result = norn.run(
    task=netmiko_file_transfer,
    source_file=test_file,
    dest_file=test_file,
    direction='put',
    num_workers=20,
)
std_print(result)   

Now let's look at this norn.run() call a bit more closely. 'norn' is the Nornir object and then you call the 'run' method on that object. Internally, this will cause Nornir to run the task that we specify generally concurrently. The task is the first argument that you pass into 'run'. Above, our task is the 'netmiko_file_transfer' task.

You can find the tasks that are built-into Nornir here and you can also create your own tasks.

Note, there is a bit of magic that happens when we execute this task. Behind the scenes Nornir automatically creates a Netmiko connection that will be open for the duration of the script. There are ways you can manually control the creation/closing of the Netmiko connections. The three arguments 'source_file', 'dest_file', and 'direction' are all passed into the netmiko_file_transfer task and tell Netmiko what to do.


You can look at the Netmiko_file_transfer plugin code and see that I use Netmiko's file_transfer() function and that I pass the arguments it (including **kwargs arguments).

Now the final argument 'num_workers=20' tells Nornir-run() to use a thread pool of size twenty.

One thing to note here is that for debugging with Pdb, it is very helpful to set num_workers=1 (i.e. to disable concurrency). Also Pdb is going to be very helpful when using Nornir. See this earlier article I wrote on Pdb.



Let's now execute this entire program and see what happens:


$ python netmiko_file_transfer.py 
Enter username: admin
Password: 

--------------------------------------------------
pynet-rtr1
True
--------------------------------------------------

--------------------------------------------------
pynet-rtr2
True
--------------------------------------------------

--------------------------------------------------
arista1
True
--------------------------------------------------

--------------------------------------------------
arista2
True
--------------------------------------------------

--------------------------------------------------
arista3
True
--------------------------------------------------

--------------------------------------------------
arista4
True
--------------------------------------------------

--------------------------------------------------
arista5
True
--------------------------------------------------

--------------------------------------------------
arista6
True
--------------------------------------------------

--------------------------------------------------
arista7
True
--------------------------------------------------

--------------------------------------------------
arista8
True
--------------------------------------------------

--------------------------------------------------
nxos1
True
--------------------------------------------------

--------------------------------------------------
nxos2
True
--------------------------------------------------   

I also verified that the file was transferred to all twelve of the remote devices.


At this point, we have a Nornir script that transfers the same text file to twelve remote devices for three different platforms. In a subsequent article, I will expand on this and handle transferring the image for Cisco IOS, setting and verifying the boot variable, and handle the reload operation. I have already done this, but I still need to create an article for it.



Reference code and inventory files used in this article (with some minor modifications).




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

Using Nornir for OS Upgrades (Part 1)

By Kirk Byers
2018-08-28

Previously I wrote an article that Introduces Nornir

In this article, I want to expand on the Nornir introduction and do something more interesting. In particular, I want to use Nornir to Secure Copy files to a set of network devices. I then want to use that file transfer process and some additional code to perform an OS upgrade on two Cisco IOS routers (part 2).

Unfortunately, I am going to switch from using Nornir 1.1 to using Nornir 2.0. There are some meaningful differences between the two versions. Additionally, Nornir 2.0 is not released yet so I am going to be working out of the 2.0 branch in GitHub.



Nornir 2.0 Install Process


[All in a Python virtual environment]
$ git clone https://github.com/nornir-automation/nornir
Cloning into 'nornir'...

# CD into the repo
$ cd nornir/

# Verify current branch
$ git branch
* develop

$ git fetch origin

# Create a 2.0 branch that tracks the origin/2.0 branch
$ git checkout -b 2.0 origin/2.0
Branch 2.0 set up to track remote branch 2.0 from origin.
Switched to a new branch '2.0'

# Verify we are on the 2.0 branch
$ git branch
* 2.0
  develop

# Install Nornir and its dependencies (including development dependencies)
$ pip install -r ./requirements-dev.txt
$ pip install -e .   


Nornir Inventory

Now that I have Nornir installed, let's look at my Nornir Inventory.

Note, as per my previous Nornir Introduction article, I am using the Nornir SimpleInventory plugin. Here, I am going to show you my new inventory files (hosts.yaml and groups.yaml files). In this environment, I have two Cisco IOS routers, eight Arista vEOS switches, and two NX-OSv switches.

My hosts.yaml file is as follows:

---
pynet-rtr1:
    hostname: 'cisco1.domain.com'
    groups: 
      - 'cisco-ios' 

pynet-rtr2:
    hostname: 'cisco2.domain.com'
    groups: 
      - 'cisco-ios'

arista1:
    hostname: 'arista1.domain.com'
    groups: 
      - 'arista'

arista2:
    hostname: 'arista2.domain.com'
    groups: 
      - 'arista'

arista3:
    hostname: 'arista3.domain.com'
    groups: 
      - 'arista'

arista4:
    hostname: 'arista4.domain.com'
    groups: 
      - 'arista'

arista5:
    hostname: 'arista5.domain.com'
    groups: 
      - 'arista'

arista6:
    hostname: 'arista6.domain.com'
    groups: 
      - 'arista'

arista7:
    hostname: 'arista7.domain.com'
    groups: 
      - 'arista'

arista8:
    hostname: 'arista8.domain.com'
    groups: 
      - 'arista'

nxos1:
    hostname: 'nxos1.domain.com'
    groups: 
      - 'nxos'

nxos2:
    hostname: 'nxos2.domain.com'
    groups: 
      - 'nxos'  

Note, each host belongs to a group that corresponds to its platform-type ('cisco-ios', 'arista', 'nxos'). Also note, I had to add quotes to some of my YAML strings to make it render reasonably on the web.


My groups.yaml is as follows:

---
defaults: {}
  # username: admin
  # password: 

cisco-ios:
  platform: 'cisco_ios'
  img: 'c880data-universalk9-mz.155-3.M8.bin'
  backup_img: 'c880data-universalk9-mz.154-2.T1.bin'

arista:
  platform: 'arista_eos'
  img: 'test_arista.txt'

nxos:
  platform: 'cisco_nxos'
  img: 'test_nxos.txt'   

I could have set the username and password in the 'defaults' section (as shown by my commented out lines), but instead I am going to pass the username and password in using a function. This is so I can store my inventory in GitHub without having the credential embedded in my inventory.

The 'platform' specified in the inventory was intentionally chosen to match the Netmiko 'device_type'.

Finally, the 'img' variable indicates the filename that I am going to transfer. In this article, I am ultimately going to transfer a real image file to the Cisco IOS devices and just .txt files to the other devices.


Different platforms have different OS upgrade processes. I am going to show the file transfer process (Secure Copy) for all of the above devices (twelve total devices, three different platforms). After that, I will show the remaining steps that would be needed to complete the OS upgrade on the two Cisco IOS device. This second part will be in a later article.



File Transfer to the Twelve Devices

Now let's get a file transfer working to the twelve devices. At this point, I am going to transfer a simple text file as it will be much quicker to test and validate (i.e. the file transfer and MD5 will happen much faster).

Now Nornir has a set of imports that we need to perform in our Python code. This includes InitNornir which simplifies the Nornir initialization (basically the use of our SimpleInventory and the creation of a Nornir object). Additionally, we are going to need to import the 'netmiko_file_transfer' plugin. Our imports initially look as follows:

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_file_transfer    

I also have a couple of minor functions that I am importing: one that handles setting the username/password and one that prints out the output from the task. I add the following import:

from nornir_utilities import nornir_set_creds, std_print    

I then need to create the Nornir object (remember this is what parses my inventory):

norn = InitNornir(config_file="nornir.yml")   

I then call my nornir_set_creds(norn) function passing in the Nornir object.



At this point, I am ready to execute the 'netmiko_file_transfer' task.

Nornir has threading automatically built into it so Nornir will automatically transfer the file to the twelve devices in parallel.

My entire code at this point is as follows:

from nornir.core import InitNornir
from nornir.plugins.tasks.networking import netmiko_file_transfer

from nornir_utilities import nornir_set_creds, std_print


# Initialize Nornir object using hosts.yaml and groups.yaml
norn = InitNornir(config_file="nornir.yml")
nornir_set_creds(norn)
test_file = 'test_file4.txt'

result = norn.run(
    task=netmiko_file_transfer,
    source_file=test_file,
    dest_file=test_file,
    direction='put',
    num_workers=20,
)
std_print(result)   

Now let's look at this norn.run() call a bit more closely. 'norn' is the Nornir object and then you call the 'run' method on that object. Internally, this will cause Nornir to run the task that we specify generally concurrently. The task is the first argument that you pass into 'run'. Above, our task is the 'netmiko_file_transfer' task.

You can find the tasks that are built-into Nornir here and you can also create your own tasks.

Note, there is a bit of magic that happens when we execute this task. Behind the scenes Nornir automatically creates a Netmiko connection that will be open for the duration of the script. There are ways you can manually control the creation/closing of the Netmiko connections. The three arguments 'source_file', 'dest_file', and 'direction' are all passed into the netmiko_file_transfer task and tell Netmiko what to do.


You can look at the Netmiko_file_transfer plugin code and see that I use Netmiko's file_transfer() function and that I pass the arguments it (including **kwargs arguments).

Now the final argument 'num_workers=20' tells Nornir-run() to use a thread pool of size twenty.

One thing to note here is that for debugging with Pdb, it is very helpful to set num_workers=1 (i.e. to disable concurrency). Also Pdb is going to be very helpful when using Nornir. See this earlier article I wrote on Pdb.



Let's now execute this entire program and see what happens:


$ python netmiko_file_transfer.py 
Enter username: admin
Password: 

--------------------------------------------------
pynet-rtr1
True
--------------------------------------------------

--------------------------------------------------
pynet-rtr2
True
--------------------------------------------------

--------------------------------------------------
arista1
True
--------------------------------------------------

--------------------------------------------------
arista2
True
--------------------------------------------------

--------------------------------------------------
arista3
True
--------------------------------------------------

--------------------------------------------------
arista4
True
--------------------------------------------------

--------------------------------------------------
arista5
True
--------------------------------------------------

--------------------------------------------------
arista6
True
--------------------------------------------------

--------------------------------------------------
arista7
True
--------------------------------------------------

--------------------------------------------------
arista8
True
--------------------------------------------------

--------------------------------------------------
nxos1
True
--------------------------------------------------

--------------------------------------------------
nxos2
True
--------------------------------------------------   

I also verified that the file was transferred to all twelve of the remote devices.


At this point, we have a Nornir script that transfers the same text file to twelve remote devices for three different platforms. In a subsequent article, I will expand on this and handle transferring the image for Cisco IOS, setting and verifying the boot variable, and handle the reload operation. I have already done this, but I still need to create an article for it.



Reference code and inventory files used in this article (with some minor modifications).




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