3 round results to print out the execution time of calling the functions
This is a comparison in execution time by sending show version to three Cisco ASA – fw01, fw02 and fw03.
All connections with netmiko.ConnectHandler has a global_delay_factor of 0.5s.
I have made three functions:
- connect_device_type_1: This function does not use threading but use netmiko’s ConnectHandler to send show version with use_textfsm.
- connect_device_type_2: This function uses concurrent.futures ThreadPoolExecutor to do the same thing.
- connect_device_type_3: This function uses modified threading subclass
- There is no significant time saver, only save 2-3 seconds
- The commands and use of textfsm parsing may not be time consuming enough to show significant difference with and without threading
- ThreadPoolExecutor is easy to use, and have similar effect as the threading.Thread class,
- With concurrent.futures.ThreadPoolExecutor’s futures.result returns the value of the function running in threads without the need to create a subclass from threading.Thread.
First round:
Second round:
Third round:
The conclusions:
timer decorator
This decorator decorates on the functions for testing.
def timer(fn): @wraps(fn) def wrapper(*args, **kwargs): start_time = time() result = fn(*args, **kwargs) print(f"Running {fn.__name__} takes {time() - start_time} seconds\n") return result return wrapper
connect_device_type_1 code
This is a normal usage of netmiko’s ConnectHandler.
@timer def connect_device_type_1(cmd=None, devices=None, **kwargs): collect_results = [] for device in devices: config = get_asa_credential(hostname=device) config.update({"device_type": "cisco_asa", "global_delay_factor": 0.5}) with ConnectHandler(**config) as asa: try: collect_results.append(asa.send_command(cmd, use_textfsm=True)) except Exceptions as e: return e return collect_results
connect_device_type_2 code
This uses ThreadPoolExecutor, I have abbreviate the module name to tpe.
@timer def connect_device_type_2(cmd=None, devices=None, **kwargs): collect_results = [] with tpe(max_workers=4) as executor: for device in devices: config = get_asa_credential(hostname=device) config.update({"device_type": "cisco_asa", "global_delay_factor": 0.5}) try: with ConnectHandler(**config) as asa: result = executor.submit(asa.send_command, cmd, use_textfsm=True) collect_results.append(result.result()) except Exceptions as e: return e return collect_results
connect_device_type_3 code
This uses a subclass I created from threading.Thread. The effect is the same as ThreadPoolExecutor.
@timer def connect_device_type_3(cmd=None, devices=None, **kwargs): collect_results = [] threads = [] for device in devices: config = get_asa_credential(hostname=device) config.update({"device_type": "cisco_asa", "global_delay_factor": 0.5}) with ConnectHandler(**config) as asa: try: t = CmdThread(target=asa.send_command, args=(cmd,), kwargs={"use_textfsm": True}) threads.append(t) t.start() except Exceptions as e: print(e) for thread in threads: collect_results.append(thread.join()) return collect_results
threading.Thread subclass
from threading import Thread """ See reference: https://stackoverflow.com/questions/6893968/how-to-get-the-return-value-from-a-thread-in-python/40344234#40344234 Check the answer by GuySoft. """ class CmdThread(Thread): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._return = None def run(self): """ The original run method does not return value after self._target is run. This child class added a return value. :return: """ if self._target is not None: self._return = self._target(*self._args, **self._kwargs) def join(self, *args, **kwargs): """ Join normally like the parent class, but added a return value which the parent class join method does not have. """ super().join(*args, **kwargs) return self._return
Entire code for testing
from netmiko import ConnectHandler from network.ciscoasa import get_asa_credential from netmiko.ssh_exception import NetMikoTimeoutException, NetMikoAuthenticationException, AuthenticationException from paramiko.ssh_exception import SSHException from textfsm.parser import TextFSMError from concurrent.futures import ThreadPoolExecutor as tpe from time import time from network.threads import CmdThread from functools import wraps Exceptions = (NetMikoAuthenticationException, NetMikoAuthenticationException, NetMikoTimeoutException, AuthenticationException, SSHException, TextFSMError) def timer(fn): @wraps(fn) def wrapper(*args, **kwargs): start_time = time() result = fn(*args, **kwargs) print(f"Running {fn.__name__} takes {time() - start_time} seconds\n") return result return wrapper @timer def connect_device_type_1(cmd=None, devices=None, **kwargs): collect_results = [] for device in devices: config = get_asa_credential(hostname=device) config.update({"device_type": "cisco_asa", "global_delay_factor": 0.5}) with ConnectHandler(**config) as asa: try: collect_results.append(asa.send_command(cmd, use_textfsm=True)) except Exceptions as e: return e return collect_results @timer def connect_device_type_2(cmd=None, devices=None, **kwargs): collect_results = [] with tpe(max_workers=4) as executor: for device in devices: config = get_asa_credential(hostname=device) config.update({"device_type": "cisco_asa", "global_delay_factor": 0.5}) try: with ConnectHandler(**config) as asa: result = executor.submit(asa.send_command, cmd, use_textfsm=True) collect_results.append(result.result()) except Exceptions as e: return e return collect_results @timer def connect_device_type_3(cmd=None, devices=None, **kwargs): collect_results = [] threads = [] for device in devices: config = get_asa_credential(hostname=device) config.update({"device_type": "cisco_asa", "global_delay_factor": 0.5}) with ConnectHandler(**config) as asa: try: t = CmdThread(target=asa.send_command, args=(cmd,), kwargs={"use_textfsm": True}) threads.append(t) t.start() except Exceptions as e: print(e) for thread in threads: collect_results.append(thread.join()) return collect_results if __name__ == "__main__": devices = ["fw01", "fw02", "fw03"] payload = { "cmd": "show version", "devices": devices } print("Without threading:") connect_device_type_1(**payload) print("With ThreadPoolExecutor:") connect_device_type_2(**payload) print("With subclass Threading:") connect_device_type_3(**payload)