Categories
Network Automation

Python: Using Netmiko & Multithreading to configure multiple devices.

I’m Calling an audible!

In my last post, I use Paramiko to connect to networking devices. This time, I will use Netmiko. The scripts used with Netmiko are a lot shorter than with Paramiko; as it comes with a tone of helpful functions to make your coding easier.

Moving along…

In my last post, configuring, we learned how to configure multiple devices using paramiko. You can see that post here. I wasn’t the biggest fan of the code presented in that post because it relied on a static configuring within one of the functions. When you’re building a script, you shouldn’t need to statically configure anything to get things rolling. The script should accept input and work with no problem. The only time you should be touching the code is if you need to upgrade a feature or fix a bug.

There was another problem with the code created in that last post. It took way too long. How? Well, the way how the it was built, it would complete the task for each router one at a time….not really a problem if you have 2 devices on your network. Here’s the thing….happens if you have 50 -100 devices that need to be catered to? That 100th device is gonna be sitting in line waiting to be logged into for at least a day…maybe more.

To fix this we’ll introduce multi-threading to speed things up a bit. I will show the difference between running a script with and without multi-threading. So let’s get started.

Our Topology:

Per the above, I’ve got all routers within my GNS3 lab connected (via Bridge) to my home network. Each router in my lab is running DHCP and pulling an address from my actual home router.

We will be configuring backing up the configuration for all the devices in our topology. There will be a separate file containing the IP addresses of the devices in the topology; which the script will use as input to complete the task.

Prior to beginning your script, you should verify SSH connectivity to each of your devices. I’ve already done that, so we can not move forward.

Let’s import our modules:

from netmiko import ConnectHandler
import time
import concurrent.futures
import datetime
import re

Let me explain why we will be using the above modules:

  • ConnectHandler – This is what we will use to connect to our networking devices. This python Class comes with a tone of methods that we can use to fulfill many different tasks.
  • time – This is what we will use to time how long the script takes to run to completion.
  • concurrent.futures – This here is our secret weapon. This is the module that will give us the ability to setup multi-threading.
  • datetime – We will use this to add some detail to name of the files we will be backing up.
  • re – python’s regular expression module. you’ll see.

I will take a different approach in this post by first displaying the code that we will be using; which I will then break apart to describe what each section does. I recommend you copy and paste this script into your text editor so you can easily follow along….why reach a particular section of this post only to then scroll all the way back up to see what I’m talk’n about??? So, here you go:

from netmiko import ConnectHandler
import time
import concurrent.futures
import datetime
import re

ip_address_file = input('Enter name of file for host addresses: ').strip()
t1 = time.perf_counter()

def fetch_ip_addresses():
    with open(ip_address_file) as devices:
        addresses = devices.read().splitlines()
    return addresses
    
def backgup_file(filename, output):    
    with open(filename,'w') as backup_file:
        backup_file.write(output)
        print('Configurations were successfully backed up!')
    return

def backup_rtr_configuration(address):
    todays_date = datetime.datetime.now()
    year = todays_date.year
    day = todays_date.day
    month = todays_date.month

    ios_device_info = {
        'ip': address,
        'port': 22,
        'username': 'admin',
        'password': 'cisco123',
        'device_type': 'cisco_ios',
        'verbose': True
    }

    print(f'Connecting to host {address}...')
    ssh_connection = ConnectHandler(**ios_device_info)

    print(f'Generating running configuration for host {address}...')
    output = ssh_connection.send_command('show run')
    prompt_hostname = ssh_connection.find_prompt()[0:-1]
    filename = f'{prompt_hostname}_{month}_{day}_{year}_backgup.cfg'
 
    print(f'Backing up configuration for host {address}')
    time.sleep(1)
    backgup_file(filename, output)
    ssh_connection.disconnect()
    return

with concurrent.futures.ThreadPoolExecutor() as exe:
    ip_addresses = fetch_ip_addresses()
    results = exe.map(backup_rtr_configuration, ip_addresses)

t2 = time.perf_counter()
print(f'The script finished executing in {round(t2-t1,2)} seconds.')

Here is our input file:

192.168.0.131
192.168.0.206
192.168.0.207
192.168.0.132

Let us start with the backup_rtr_configuration function:

  def backup_rtr_configuration(address): 
      todays_date = datetime.datetime.now()
      year = todays_date.year
      day = todays_date.day
      month = todays_date.month

The function accepts an IP_address ( a string ) as input. The variables created will be used to properly label our output files when the backup is complete.

    ios_device_info = {
        'ip': address,
        'port': 22,
        'username': 'admin',
        'password': 'cisco123',
        'device_type': 'cisco_ios',
        'verbose': True
    }

    print(f'Connecting to host {address}...')
    ssh_connection = ConnectHandler(**ios_device_info)

Per the above code, a dictionary containing all the things our script will need to connect to each device. Each key within the dictionary is are required parameters that the “ConnectHandler” method needs in order to connect to a device.

We’re using python’s unpacking ( ** ) feature to feed the connectHandler what it needs to connect to the devices.

    print(f'Generating running configuration for host {address}...')
    output = ssh_connection.send_command('show run')
    prompt_hostname = ssh_connection.find_prompt()[0:-1]
    filename = f'{prompt_hostname}_{month}_{day}_{year}_backgup.cfg'

We first print a state to the user, advising them that we’re generating the configuration for the router. We then use netmiko’s ‘send_command‘ to the device and assign the value to output.

Netmiko also has a very helpful function that pulls device’ss prompt; find_prompt.

For example, when you’re in global-config-mode in a router, you see the following:

R1(config)# <---------

In our script, we’re simply using this function to extract the hostname of the device, only to then use to label the output file. So in the script above, we use slicing to extract just the hostname to then assign it to variable prompt_hostname.

Now you probably caught on to the fact that the slicing we’re doing will return not only the hostname, but it will also return (config). Here’s the cool thing about Netmiko. When you’re done sending a command, it automagically exits global-config mode, placing you back in enable mode:

R1#  <-- enable mode

So with that being said, after slicing, we’re only left with the hostname.

Then, finally, we’re using python’s string formatting to dynamically label our output files only to then assign that to the variable filename.

    print(f'Backing up configuration for host {address}')
    time.sleep(1)
    backgup_file(filename, output)
    ssh_connection.disconnect()
    return

Per the code above, we begin the backing up process by calling the backup_file function, which we will discuss, next. After the function runs we disconnect the SSH session for the device.

Let us start with the backup_file function:

def backgup_file(filename, output):    
    with open(filename,'w') as backup_file:
        backup_file.write(output)
        print('Configurations were successfully backed up!')
    return

The backup_file function contains two positional parameters, the name of the file and the output ( the router’s configuration ). We then shove that into a with statement where it will begin creating a file and writing the output to it.

Let us start with the backup_file function:

t1 = time.perf_counter()
ip_address_file = input('Enter name of file for host addresses: ').strip()

def fetch_ip_addresses():
    with open(ip_address_file) as devices:
        addresses = devices.read().splitlines()
    return addresses
  
with concurrent.futures.ThreadPoolExecutor() as exe:
  ip_addresses = fetch_ip_addresses()
  results = exe.map(backup_rtr_configuration, ip_addresses)

t2 = time.perf_counter()
print(f'The script finished executing in {round(t2-t1,2)} seconds.')

In the above code we become by timing the speed of the whole script with the time.pert_counter expression. The second half of the timer is all the way at the bottom of the script after the with the statement; this is our ‘stop-watch‘.

Let us turn our attention to the with statement. This is how we accomplish multi-threading. There is different way, a more ‘manual’ way of doing this, however, I prefer this way as it is short and very effective. There will be a separate post breaking down how concurrent.futures.ThreadPoolExecutor() works, so I will not go into details here.

Within this with statement we first call the fetch_ip_addressess() function to them assign it’s value, an array, to variable ip_addresses. We then utilize the map function to loop through the array of IP address, passing each address into the backup_rtr_configuration function for each call and assign it’s value to results. Since this block of code is written in the global-scope, with the exception of time.perf_couter(), this is where our code begins to run. Since we’re outputting this into a file, there’s no need to print the results, but you can if you want to.

So…I will not now make a slight adjustment to the code by commenting out the block of code where we implement multi-threading and replace it with a general for-loop to demonstrate how long the script will take without multi-threading.

# with concurrent.futures.ThreadPoolExecutor() as exe:
  # ip_addresses = fetch_ip_addresses()
  # results = exe.map(backup_rtr_configuration, ip_addresses)

ip_addresses = fetch_ip_addresses()  
for address in ip_addresses:
  backup_rtr_configuration(address)

After running the code above it took almost a minute for it to complete!:

Configurations were successfully backed up!
The script finished executing in 46.14 seconds. 
H:\>

Now I will delete the for loop, uncomment our secret weapon and run the script again:

Configurations were successfully backed up!
The script finished executing in 10.48 seconds.
H:\> 

Wow, talk about a huge difference!

The differences are pretty obvious, yeah? Now you shouldn’t run multi-threading on all your scripts. Whether you run multi-threading or not, depends on what you’re actually doing within your script; as multi-threading can actually slow down your script’s execution speed; there will be a separate post on that topic so you will know when to use it.

Hope this helps