Automate Cisco Router and Switches IOS Upgrade in Bulk using Python

Automate IOS upgrade for Cisco Routers and Cisco Switches in Bulk

I am new to python and have written this script based on my experience so far with python and I am sure that this script can be optimized and enhanced in much better way. Please do share your comments/review and optimization ideas.

Upgrading Cisco Routers and Switches is fun and easy but when the estate is huge and there are different models of those switches and routers, then it is really a painful task. As this is ongoing activity in any business as Cisco releases new IOS versions to fix vulnerabilities and bugs. In order to mitigate the vulnerabilities, any organization would prefer to upgrade their devices to the latest and greatest stable version. To ease the work, you can use “Automation” and get it completed without much human effort.

The script I have written is for Cisco 2801 and Cisco 2811 and I have tested it in my lab multiple times and it was working as expected. You can modify the script based on your business requirement. I am using netmiko in the script.  In case if you missed to read the previous article on Netmiko, please feel free to read it.

Netmiko SSH – Python Automation on Cisco Routers and Switches using SSH

Here is the YouTube video on automating ciscoIOS upgrade in bulk for cisco routers and cisco switches using Python script

https://www.youtube.com/watch?v=nxwfuHxBkKA

 

When python script is ran, it will follow the below sequence:

  • The script will fetch a pre-upgrade report and the report will have information (like IP address, hostname, uptime, current version, current image, serial number, device model and device memory of the router) before it is upgraded.
  • Then it will push the IOS image on the cisco router/switch.
  • It will check the MD5 checksum.
  • It will change the boot configuration and save the configuration.
  • It will then reload the router.
  • The script will then wait for 300 seconds and then it will fetch a post-upgrade report.

The following files will be generated by the python script while running the script:

  • Pre-Upgrade.csv
  • Post-Upgrade.csv
  • Logs.txt

Below is the python script to automate Cisco IOS upgrade in bulk.

#############################################
####	Cisco IOS Upgrade in Bulk    	####
####	Script written by UC Collabing	####
####	https://www.uccollabing.com		####
#############################################

import subprocess, re, time, netmiko
from netmiko import ConnectHandler
from netmiko.ssh_exception import NetMikoTimeoutException
from netmiko.ssh_exception import SSHException
from netmiko.ssh_exception import AuthenticationException
from netmiko import SCPConn
from datetime import datetime

#Important parameters that can be changed and controlled from here#

ip_list = ['192.168.1.179', '192.168.1.178']

#Cisco IOS 2801 Data
new_ios_2801 = "c2801-ipvoice_ivs-mz.150-1.M7.bin"
new_ios_2801_size = "41735808"
new_ios_2801_md5 = "66c50292167f2b1c1ccd9ead3d5a5db4"


#Cisco IOS 2811 Data
new_ios_2811 = "c2800nm-ipvoice_ivs-mz.151-2.T1.bin"
new_ios_2811_size = "51707860"
new_ios_2811_md5 = "bf3e0811b5626534fd2e4b68cdd042df"

copy_from = "tftp"
copy_to = "flash:"
tftp_ip = "192.168.1.100"
reload_wait_time = "300"
auto_copy_to_flash = "Yes"
auto_change_boot_sequence ="Yes"
auto_reload = "Yes"

#########################################



#Creating the CSV files for pre and post upgrade#

#clearing the old data from the CSV file and writing the headers
f = open("pre_upgrade.csv", "w+")
f.write("IP Address, Hostname, Uptime, Current_Version, Current_Image, Serial_Number, Device_Model, Device_Memory")
f.write("\n")
f.close()


#clearing the old data from the CSV file and writing the headers
f = open("post_upgrade.csv", "w+")
f.write("IP Address, Hostname, Uptime, Current_Version, Current_Image, Serial_Number, Device_Model, Device_Memory")
f.write("\n")
f.close()


#clearing the old data from the logs file and writing the headers
f = open("logs.txt", "w+")
f.close()


now = datetime.now()
logs_time = now.strftime("%H:%M:%S")


#############################################################################################################################



def preupgrade():

	
	for ip in ip_list:
		cisco = {
		'device_type':'cisco_ios_telnet',
		'ip':ip,
		'username':'cisco',     #ssh username
		'password':'cisco',  #ssh password
		'secret': 'cisco',   #ssh_enable_password
		'ssh_strict':False,  
		'fast_cli':False,
		}
		
		now = datetime.now()
		logs_time = now.strftime("%H:%M:%S")
		print("" + logs_time + ": " + ip  + " Checking this device, Collecting pre-report ")
		#handling exceptions errors
		
		try:
			net_connect = ConnectHandler(**cisco)
		
		except (NetMikoTimeoutException, AuthenticationException, SSHException, ValueError, TimeoutError, ConnectionError, ConnectionResetError, OSError):
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " device login issue " + "\n" )
			f.close()
			continue

		try:
			net_connect.enable()

		
		#handling exceptions errors		
		except (NetMikoTimeoutException, AuthenticationException, SSHException, ValueError, TimeoutError, ConnectionError, ConnectionResetError):
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " device login issue " + "\n" )
			f.close()
			continue
		

		#list where informations will be stored
		pre_upgrade_devices = []

				
		# execute show version on router and save output to output object	
		sh_ver_output = net_connect.send_command('show version')   

		#finding hostname in output using regular expressions
		regex_hostname = re.compile(r'(\S+)\suptime')
		hostname = regex_hostname.findall(sh_ver_output)

		#finding uptime in output using regular expressions
		regex_uptime = re.compile(r'\S+\suptime\sis\s(.+)')
		uptime = regex_uptime.findall(sh_ver_output)
		uptime = str(uptime).replace(',' ,'').replace("'" ,"")
		uptime = str(uptime)[1:-1] 
		
		
		#finding version in output using regular expressions
		regex_version = re.compile(r'Cisco\sIOS\sSoftware.+Version\s([^,]+)')
		version = regex_version.findall(sh_ver_output)

		#finding serial in output using regular expressions
		regex_serial = re.compile(r'Processor\sboard\sID\s(\S+)')
		serial = regex_serial.findall(sh_ver_output)

		#finding ios image in output using regular expressions
		regex_ios = re.compile(r'System\simage\sfile\sis\s"([^ "]+)')
		ios = regex_ios.findall(sh_ver_output)

		#finding model in output using regular expressions
		regex_model = re.compile(r'[Cc]isco\s(\S+).*memory.')
		model = regex_model.findall(sh_ver_output)
		

		#finding the router's memory using regular expressions
		regex_memory = re.search(r'with (.*?) bytes of memory', sh_ver_output).group(1)
		memory = regex_memory
		
		
		
		#append results to table [hostname,uptime,version,serial,ios,model]
		pre_upgrade_devices.append([ip, hostname[0],uptime,version[0],ios[0], serial[0],model[0], memory])
		
		

		#print all results (for all routers) on screen    
		for i in pre_upgrade_devices:
			i = ", ".join(i)	  
			f = open("pre_upgrade.csv", "a")
			f.write(i)
			f.write("\n")
			f.close()	
			
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
	
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " collecting pre upgrade report " + "\n" )
			f.close()
		
			#If Auto Copy to flash is enabled - Then start copying the files to Flash#				
		if auto_copy_to_flash == "Yes":
			#print("Checking Auto Copy to Flash as Yes")
			
		
			#Check necessary space on Flash:#	
			output = net_connect.send_command('show flash')
			output = re.findall(r"\w+(?= bytes available)", output)
			output = ", ".join(output)

			if model[0] == "2801":		
				check_if_space_available = int(output) - int(new_ios_2801_size)
				#print(str(check_if_space_available))
				
				if int(check_if_space_available) > 0 :
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " Sufficient space available ")
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " Sufficent space available" + "\n" )
					f.close()
					time.sleep(2)
					pass
					
				elif int(check_if_space_available) < 0: now = datetime.now() logs_time = now.strftime("%H:%M:%S") print("" + logs_time + ": " + ip + " Not enough space ") f = open("logs.txt", "a") f.write("" + logs_time + ": " + ip + " not enough space" + "\n" ) f.close() continue if model[0] == "2811": check_if_space_available = int(output) - int(new_ios_2811_size) #print(str(check_if_space_available)) if int(check_if_space_available) > 0 :
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " Sufficient space available ")
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " Sufficent space available" + "\n" )
					f.close()
					time.sleep(2)
					pass
					
				elif int(check_if_space_available) < 0:	
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " Not enough space ")
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " not enough space" + "\n" )
					f.close()
					continue


					
			#Copy TFTP to FLASH#
			command = "copy " + copy_from + " " + copy_to
			start_time = datetime.now()
			output = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=1)
			
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			print("" + logs_time + ": " + ip  + " " + output)
			#print(output)
			
			#Entering the TFTP IP Address#
			command = tftp_ip
			start_time = datetime.now()
			#output = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=2)
			output = net_connect.send_command(command, expect_string=r']?')
			
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			print("" + logs_time + ": " + ip  + " " + output)
				
			
			#If the router model is 2801 - Run this code#
			if model[0] == "2801":			
				command = new_ios_2801
				start_time = datetime.now()
				#output = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=1)
				output = net_connect.send_command(command, expect_string=r']?')
				now = datetime.now()
				logs_time = now.strftime("%H:%M:%S")
				print("" + logs_time + ": " + ip  + " " + output)
				
				command = new_ios_2801
				start_time = datetime.now()
				#net_connect.send_config_set(config_commands)
				output = net_connect.send_command_timing(command, delay_factor=10, strip_prompt=False, strip_command=False)
				now = datetime.now()
				logs_time = now.strftime("%H:%M:%S")
				print("" + logs_time + ": " + ip  + " " + output)

									
				if re.search(r'\sbytes copied\b',output):
    					
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " File copied successfully ")	
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " file copied successfully" + "\n" )
					f.close()
					
					
					
					command = "verify /md5 flash:" + new_ios_2801
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					#output = net_connect.send_command(command, expect_string=r']?')
					output = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=5)
					print("" + logs_time + ": " + ip  + "Calculated MD5 is : " + output + "\n" "Expected MD5 is : " + new_ios_2801_md5 )
					try:
						output = re.search(' = (\w+)',output)
						print(output)	
						
					except AttributeError:
						output = re.search(' = (\w+)',output), output.group(1)
						#print(output.group(1))	
					
					if new_ios_2801_md5 == str(output.group(1)):
    					
						now = datetime.now()
						logs_time = now.strftime("%H:%M:%S")
						print("" + logs_time + ": " + ip  + " MD5 checksum verified ")
						
						f = open("logs.txt", "a")
						f.write("" + logs_time + ": " + ip  + " MD5 checksum verified" + "\n" )
						f.close()
						
						
						
											
					elif new_ios_2801_md5 != str(output.group(1)):
						now = datetime.now()
						logs_time = now.strftime("%H:%M:%S")
						print("" + logs_time + ": " + ip  + " MD5 checksum mismatch ")
				
						f = open("logs.txt", "a")
						f.write("" + logs_time + ": " + ip  + " MD5 checksum mismatch" + "\n" )
						f.close()					
						
						continue
				
								
				
				elif re.search(r'\%Error copying\b',output):
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " Error copying the file ")
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " error copying file" + "\n" )
					f.close()
					continue
					
				elif re.search(r'\%Error opening\b',output):
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " File does not exist ")
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " file does not exist" + "\n" )
					f.close()

					continue
					
				elif re.search(r'\bAccessing',output):
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " Please check your TFTP/SCP service/network network connectivity ")
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " check your TFTP/SCP service/network network connectivity" + "\n" )
					f.close()				


					continue
					
					
		

			
			#If the router model is 2811 - Run this code#
			elif model[0] == "2811":			
				command = new_ios_2811
				start_time = datetime.now()
				#output = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=1)
				output = net_connect.send_command(command, expect_string=r']?')
				now = datetime.now()
				logs_time = now.strftime("%H:%M:%S")
				print("" + logs_time + ": " + ip  + " " + output)
				
				command = new_ios_2811
				start_time = datetime.now()
				#net_connect.send_config_set(config_commands)
				output = net_connect.send_command_timing(command, delay_factor=10, strip_prompt=False, strip_command=False)
				now = datetime.now()
				logs_time = now.strftime("%H:%M:%S")
				print("" + logs_time + ": " + ip  + " " + output)
									
				if re.search(r'\sbytes copied\b',output):
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " File copied successfully ")
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " file copied successfully" + "\n" )
					f.close()

					
					
					command = "verify /md5 flash:" + new_ios_2811
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					#output = net_connect.send_command(command, expect_string=r']?')
					output = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=5)
					print("" + logs_time + ": " + ip  + "Calculated MD5 is : " + output + "\n" "Expected MD5 is : " + new_ios_2811_md5 )
					#print(output)
					try:
						output = re.search(' = (\w+)',output)
						#print(output)	
						
					except AttributeError:
						output = re.search(' = (\w+)',output), output.group(1)
						#print(output.group(1))	
					
					if new_ios_2811_md5 == str(output.group(1)):
						now = datetime.now()
						logs_time = now.strftime("%H:%M:%S")
						print("" + logs_time + ": " + ip  + " MD5 checksum successfully matched ")
						
						f = open("logs.txt", "a")
						f.write("" + logs_time + ": " + ip  + " MD5 checksum verified" + "\n" )
						f.close()

						
											
					elif new_ios_2811_md5 != str(output.group(1)):
						now = datetime.now()
						logs_time = now.strftime("%H:%M:%S")
						print("" + logs_time + ": " + ip  + " MD5 checksum mismatch ")
					
						f = open("logs.txt", "a")
						f.write("" + logs_time + ": " + ip  + " MD5 checksum mismatch" + "\n" )
						f.close()					

						continue
					
								
				
				elif re.search(r'\%Error copying\b',output):
    					
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " Error copying the file ")	
					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " error copying file" + "\n" )
					f.close()
					continue
					
				elif re.search(r'\%Error opening\b',output):
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " File does not exist ")	

					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " file does not exist" + "\n" )
					f.close()	
					continue	
					
				elif re.search(r'\bAccessing',output):
					now = datetime.now()
					logs_time = now.strftime("%H:%M:%S")
					print("" + logs_time + ": " + ip  + " Please check your TFTP/SCP service/network network connectivity ")

					f = open("logs.txt", "a")
					f.write("" + logs_time + ": " + ip  + " check your TFTP/SCP service/network network connectivity" + "\n" )
					f.close()				
					continue		
					
					
							

					
			#If Auto Copy to flash is disabled - Then continue to next step#		
		elif auto_copy_to_flash == "No":
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			print("" + logs_time + ": " + ip  + " Auto copy to flash is set as No ")
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " Auto copy to flash is set as No " + "\n" )
			f.close()
			pass				
						
		if auto_change_boot_sequence == "Yes":		
		
			#Enter the New IOS#
			output = net_connect.send_command('show version') 
			regex_ios = re.compile(r'System\simage\sfile\sis\s"([^ "]+)')
			current_ios = regex_ios.findall(output)
			current_ios = ", ".join(current_ios)
			
			remove_boot = "no boot system"
			remove_boot = net_connect.send_config_set(remove_boot)
			

			if model[0] == "2801":
				add_boot1 = "boot system flash:" + new_ios_2801
				add_boot1 = net_connect.send_config_set(add_boot1)
				
			elif model[0] == "2811":
				add_boot1 = "boot system flash:" + new_ios_2811
				add_boot1 = net_connect.send_config_set(add_boot1)
				
			

			add_boot2 =  "boot system " + str(current_ios)
			add_boot2 = net_connect.send_config_set(add_boot2)
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			print("" + logs_time + ": " + ip  + " Boot sequence changed ")
			


			#write_config = net_connect.send_command('wr mem', expect_string='[OK]')
			command = "wr mem"
			write_config = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=2)
			

			command = ""
			write_config = net_connect.send_command_timing(command, strip_prompt=False, strip_command=False, delay_factor=2)
			

			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			print("" + logs_time + ": " + ip  + " Configuration saved ")
			
			
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " configuration saved" + "\n" )
			f.close()


						
		elif auto_change_boot_sequence == "No":
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			print("" + logs_time + ": " + ip  + " Please change the boot sequence manually! and proceed with reload ")
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " Please change the boot sequence manually! and proceed with reload" + "\n" )
			f.close()
			pass

				
		if auto_reload == "Yes":
		
			try:
				
				confirm_reload = net_connect.send_command('reload', expect_string='[confirm]')
				confirm_reload = net_connect.send_command('\n', expect_string='[confirm]')
				now = datetime.now()
				logs_time = now.strftime("%H:%M:%S")
				
				f = open("logs.txt", "a")
				f.write("" + logs_time + ": " + ip  + " Reload command sent" + "\n" )
				f.close()
				
				print("" + logs_time + ": " + ip  + " Sending reload command ")

			except Exception as e:
				print(e)
				f = open("logs.txt", "a")
				f.write("" + logs_time + ": " + ip  + " Reload command sent" + "\n" )
				f.close()
				
				print("" + logs_time + ": " + ip  + " Sending reload command ")



		elif auto_reload== "No":
    		
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			print("" + logs_time + ": " + ip  + " Please reload the router manually ")

			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " Please reload the router manually" + "\n" )
			f.close()

			pass
					
					
						
preupgrade()

def sleeptime():
	now = datetime.now()
	logs_time = now.strftime("%H:%M:%S")
	print("" + logs_time + ": Wait time activated, please wait for " + str(reload_wait_time) + " seconds")
	time.sleep(int(reload_wait_time))

sleeptime()	
					
def	postupgrade():

	
	for ip in ip_list:
		cisco = {
		'device_type':'cisco_ios_telnet',
		'ip':ip,
		'username':'cisco',     #ssh username
		'password':'cisco',  #ssh password
		'secret': 'cisco',   #ssh_enable_password
		'ssh_strict':False,  
		'fast_cli':False,
		}
	
		now = datetime.now()
		logs_time = now.strftime("%H:%M:%S")
		print("" + logs_time + ": " + ip  + " Checking this device, Collecting post-report ")
					
		try:
			#time.sleep(int(reload_wait_time))
			net_connect = ConnectHandler(**cisco)
		except (NetMikoTimeoutException, AuthenticationException, SSHException, ValueError, TimeoutError, ConnectionError, ConnectionResetError):
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " device login issue " + "\n" )
			f.close()
			continue

		try:
			net_connect.enable()

			

		#handling exceptions errors		
		except (NetMikoTimeoutException, AuthenticationException, SSHException, ValueError, TimeoutError, ConnectionError, ConnectionResetError, OSError):
			now = datetime.now()
			logs_time = now.strftime("%H:%M:%S")
			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " device login issue " + "\n" )
			f.close()
			continue
		
		#list where informations will be stored
		post_upgrade_devices = []
		
		# execute show version on router and save output to output object	
		sh_ver_output = net_connect.send_command('show version')   

		#finding hostname in output using regular expressions
		regex_hostname = re.compile(r'(\S+)\suptime')
		hostname = regex_hostname.findall(sh_ver_output)

		#finding uptime in output using regular expressions
		regex_uptime = re.compile(r'\S+\suptime\sis\s(.+)')
		uptime = regex_uptime.findall(sh_ver_output)
		uptime = str(uptime).replace(',' ,'').replace("'" ,"")
		uptime = str(uptime)[1:-1] 
		
		
		#finding version in output using regular expressions
		regex_version = re.compile(r'Cisco\sIOS\sSoftware.+Version\s([^,]+)')
		version = regex_version.findall(sh_ver_output)

		#finding serial in output using regular expressions
		regex_serial = re.compile(r'Processor\sboard\sID\s(\S+)')
		serial = regex_serial.findall(sh_ver_output)

		#finding ios image in output using regular expressions
		regex_ios = re.compile(r'System\simage\sfile\sis\s"([^ "]+)')
		ios = regex_ios.findall(sh_ver_output)

		#finding model in output using regular expressions
		regex_model = re.compile(r'[Cc]isco\s(\S+).*memory.')
		model = regex_model.findall(sh_ver_output)
		

		#finding the router's memory using regular expressions
		regex_memory = re.search(r'with (.*?) bytes of memory', sh_ver_output).group(1)
		memory = regex_memory
		
		
		
		#append results to table [hostname,uptime,version,serial,ios,model]
		post_upgrade_devices.append([ip, hostname[0],uptime,version[0],ios[0], serial[0],model[0], memory])
				


	#print all results (for all routers) on screen    
		for i in post_upgrade_devices:
			i = ", ".join(i)	  
			f = open("post_upgrade.csv", "a")
			f.write(i)
			f.write("\n")
			f.close()

			f = open("logs.txt", "a")
			f.write("" + logs_time + ": " + ip  + " collecting post upgrade report " + "\n" )
			f.close()

postupgrade()

 

Note: We recommend you to try it in your lab first before you try it in production. Please try this at your own risk, we are not responsible for any failure/damages to your devices.

You may also like...

1 Response

  1. manuel villapudua says:

    I am very interested in putting together a very similar list. If you found success, would you allow me to ask a few questions?

Leave a Reply

Your email address will not be published. Required fields are marked *