Up until now, we have been establishing connections to devices sequentially, i.e If you have 10 devices from which you want to pull show version data, the script will connect to device1, extract show version, goto device2, extra show version, connect to the device and extract show version and so on until all devices are done. You can imagine how slow this can get once you need to connect to hundreds/thousands of devices to fetch data and further amplified by if you need to get outputs of long-running commands from multiple devices. Let’s see how we can leverage the python threading module to connect multiple devices at once and achieve concurrency to reduce the overall execution time drastically.
MULTITHREADING WITH PYTHON
There are multiple ways to achieve parallel task execution. In this case, we want to connect to multiple devices at once and fetch data from devices instead of sequentially to save time.
import threading # import python's threading module
from netmiko import ConnectHandler
import time
import logging
csr1000v1 = {
'device_type': 'cisco_ios',
'host': 'sandbox-iosxe-latest-1.cisco.com',
'username': 'developer',
'password': 'C1sco12345',
'port' : 22, # optional, defaults to 22
'secret': 'C1sco12345', # optional, defaults to ''
}
csr1000v2 = {
'device_type': 'cisco_ios',
'host': 'sandbox-iosxe-recomm-1.cisco.com',
'username': 'developer',
'password': 'C1sco12345',
'port' : 22,
'secret': 'C1sco12345',
}
iosxrv9000 = {
'device_type': 'cisco_xr',
'host': 'sandbox-iosxr-1.cisco.com',
'username': 'admin',
'password': 'C1sco12345',
'port' : 22,
'secret': 'C1sco12345',
}
nxosv9000 = {
'device_type': 'cisco_nxos',
'host': 'sandbox-nxos-1.cisco.com',
'username': 'admin',
'password': 'Admin_1234!',
'port' : 22,
'secret': 'Admin_1234!',
}
def connect_and_fetch(device_data):
net_connect = ConnectHandler(**device_data)
output = net_connect.send_command('show version', use_genie=True)
print(net_connect.host)
print("*" * len(net_connect.host))
# print(output)
if __name__ == "__main__":
threads = []
all_devices = [csr1000v1, csr1000v2, iosxrv9000, nxosv9000]
for device in all_devices:
# Spawn threads and append to threads list
th = threading.Thread(target=connect_and_fetch, args=(device,))
threads.append(th)
# iterate through threads list and start each thread to perform its task
for thread in threads:
thread.start()
#Once all threads have done the work, join the output of all threads to return the final output.
for thread in threads:
thread.join()
Explanation:-
IMPROVEMENTS:-
The code above works fast sure but is it a good code, no, it is not. There are some loopholes here that we can fix and take care of especially when we are spawning multiple threads and can run into issues like
- We are spawning all threads ahead of time. So if you have hundreds of devices to connect to, you are essentially spawning those many threads before the threads even start to do something.
if __name__ == "__main__":
threads = []
all_devices = [csr1000v1, csr1000v2, iosxrv9000, nxosv9000]
for device in all_devices:
# Spawn threads and append to threads list
th = threading.Thread(target=connect_and_fetch, args=(device,))
threads.append(th)
th.start()
#Once all threads have done the work, join the output of all threads to return the final output.
for thread in threads:
thread.join()
2. The above doesn’t limit the number of threads you can spawn parallel. If you have a thousand devices, it will try to spawn 1000 threads and your application may crash if it’s unable to handle so many threads without proper error handling.
if __name__ == "__main__":
max_threads = 50 # Set max threads to 50. You can see what number works best for you.
threads = []
all_devices = [csr1000v1, csr1000v2, iosxrv9000, nxosv9000]
for device in all_devices:
# Spawn threads and append to threads list
th = threading.Thread(target=connect_and_fetch, args=(device,))
threads.append(th)
th.start()
#After each thread is started and added to dictionary, we are checking if the total number
#of threads is more than what we have configured. If yes, wait or else continue
while True:
alive_cnt = 0
for t in self.threads:
if t.is_alive():
alive_cnt += 1
if alive_cnt >=max_threads:
logging.info('Do not spawn new thread, already reached max limit of alive threads [%s]' % alive_cnt)
time.sleep(2)
continue
break
#Once all threads have done the work, join the output of all threads to return the final output.
for thread in threads:
thread.join()
3. Now that we have the basics right, there is a way to reduce the number of lines of manual coding that we had to do above and let python handle all this for you.
import concurrent.futures
if __name__ == "__main__":
all_devices = [csr1000v1, csr1000v2, iosxrv9000, nxosv9000]
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.map(connect_and_fetch, all_devices)
Funny how python does all the work for you, it’s just about knowing where to find and how to get that done. In the above code, we are just telling python to set max_workers = 4 ( spawn 4 threads ) and execute the connect_and_fetch function on all the devices in all_devices list using the execute.map( ) method. Everything else is handled for you behind the scenes.
This is Multithreading with python for network engineers 101 for you. The next article in line will focus on MultiProcessing with Python for Network Engineers.
Keep in touch !!!
Enter your email address below to join my mailing list.
3 thoughts on “Multithreading with Python for Network Engineers”