在上一篇文章中,我实践了通过Python的paramiko模块来SSH登录到华为交换机上批量备份配置文件到FTP服务器上。最后总结存在两个问题,一个是在脚本运行过程中,如果中间某台设备因用户名密码错误或者网络不可达的情况,会导致脚本中断运行无法继续执行下去,后面设备的备份也无法完成。第二个就是由于python默认是单线程串行执行的,在设备量大的时候效率可能不高,脚本需要运行的时间比较长。

本篇文章将会通过try…except来实现异常处理,以及通过threading模块来解决多线程问题,并发执行多台设备同时进行备份操作。

本实验通过学习@弈心大佬的《网络工程师的Python之路》后结合自己的理解和思路形成。

实验环境及拓扑仍然跟上次一样,python实验平台与5台交换机桥接在一起。

异常处理try…except

异常是一个事件,在程序执行过程中python无法正常处理的程序发生的话,就会影响程序的正常执行。Python可以通过try…except语句来检测try语句块中的错误,并且让except语句来捕获异常信息并进行处理。

首先基于上一次的实验的基础上,加入异常处理的机制,以下是加入异常处理机制后完整的代码。

下面我们来对代码进行分析。

第一部分

for line in f.readlines():
try:
line_s = line.split( )
device_ip = line_s[0]
device_name = line_s[1]
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect(hostname=device_ip, username=username, password=password)
print('成功连接上 ', device_ip)
command = ssh_client.invoke_shell()
command.send('save\n')
command.send('y\n')
time.sleep(1)
command.send('ftp 192.168.134.1\n')
command.send('ftpuser\n')
command.send('ftpuser\n')
command.send('put vrpcfg.zip ' + date + '_' + device_name + '_vrpcfg.zip\n')
command.send('bye\n')
time.sleep(1)
output = command.recv(65535)
print(output.decode('UTF-8'))
except paramiko.ssh_exception.AuthenticationException:
print(device_ip + ' 用户认证失败..')
device_authentication_failed_list.append(device_ip)
except socket.error:
print(device_ip + ' 网络不可达..')
device_not_reachable_list.append(device_ip)主代码跟上一次实验是一样的,只是在主代码前面加入try,并在主代码后加入except捕获具体的异常信息。如果try后的语句执行时发生异常,python就会跳到except来判断异常信息,并执行响应动作。如果try后的语句执行正常,则会跳过except,就跟if...else条件判断语句是一个意思。

主代码跟上一次实验是一样的,只是在主代码前面加入try,并在主代码后加入except捕获具体的异常信息。如果try后的语句执行时发生异常,python就会跳到except来判断异常信息,并执行相应的动作。如果try后的语句执行正常,则会跳过except,就跟if…else条件判断语句是一个意思。

网络设备登录异常通常有2个场景,一个就是用户名密码错误,即认证失败无法登录上设备,第二个就是这台网络设备断网了无法连接上。

通过paramiko登录设备认证失败的话,python会抛出异常信息 paramiko.ssh_exception.AuthenticationException,因此我们通过except来捕获这个异常报错信息来执行下面的动作,这里我们会先打印一个信息提示这个IP认证失败的原因,并且将该异常设备的IP地址添加进先前定义好的空列表device_authentication_failed_list里。网络不可达的异常需要引入socket模块来处理,当设备连接不上时会报socket.error,同样的把这个异常情况打印出来并放到定义好的空列表device_not_reachable_list里。

第二部分

print('\n以下设备认证失败无法登录: ')
if device_authentication_failed_list == []: #判断是否为空列表
print('无')
else:
for i in device_authentication_failed_list:
print(i)
print('\n以下设备网络不可达: ')
if device_not_reachable_list == []:
print('无')
else:
for i in device_not_reachable_list:
print(i)

最后,当程序执行完把所有出现登录异常的设备的打印出来,后期人工再做后续处理。判断如果列表为空,则打印“无”,否则非空则分别打印出列表里的异常设备IP。

异常处理实验验证

把SW2的登录密码修改为Cisco@123(模拟认证失败),把SW4连HUB的接口G0/0/1 shutdown掉(模拟网络不可达),再运行脚本。

可以看到,在脚本执行过程中对于无法登录的设备会打印出异常信息,并继续执行下一台设备的备份操作而不会中途中断。在所有设备备份执行完之后,会统计出所有异常设备的IP并打印出来。

多线程 threading

Python默认是单线程的,在执行备份脚本的时候通过输出可以看到是在备份完一台设备后再备份另外一台这样串行执行的。由于本次实验总共就5台设备,所以整个备份时间都可以接受,那如果现网中有几十上百台设备的话,单线程执行的过程会是比较慢的,下面就介绍通过python自带的threading模块来实现多线程并发执行多台设备同时备份,加快程序执行效率。据说有更好用的多线程模块,后面我也会继续研究学习一下。

本次实验我会对代码稍微改造一下,将SSH登录及命令执行封装成函数ssh_f,然后用后面代码调用ssh_f函数。全部代码如下。

代码解析

第一部分

def ssh_f(ip, username, password, device_name, date): #封装成函数ssh_f
try:
ssh_client = paramiko.SSHClient()
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh_client.connect(hostname=ip, username=username, password=password)
print('成功连接上: ', ip)
time.sleep(2)
command = ssh_client.invoke_shell()
command.send('save\n')
command.send('y\n')
time.sleep(1)
command.send('ftp 192.168.134.1\n')
command.send('ftpuser\n')
command.send('ftpuser\n')
command.send('put vrpcfg.zip ' + date + '_' + device_name + '_vrpcfg.zip\n')
command.send('bye\n')
time.sleep(1)
print('已完成备份... ', ip)
except paramiko.ssh_exception.AuthenticationException:
print('用户认证失败 ' + ip + '.')
device_authentication_failed_list.append(ip)
except socket.error:
print(ip + ' 网络不可达')
device_not_reachable_list.append(ip)
ssh_client.close()

此部分只是把先前的代码封装成了函数,定义了函数名为ssh_f,函数内的代码跟先前是一样的,只是把ssh登录跟下发命令的代码单独抽取了出来放到函数里,这样能够方便后面代码的调用。并对ssh_f函数定义了这几个参数,ip, username, password, device_name, date,后面调用时会传参进去。

第二部分

username = 'python'
password = 'Python@123'
date = time.strftime('%Y-%m-%d')
threads = [10]
device_authentication_failed_list = []
device_not_reachable_list = []
print('开始执行备份操作...')
f = open('devices_list.txt', 'r')
for line in f.readlines():
line_s = line.split( )
ip_address = line_s[0]
device_name = line_s[1]
#使用threading的Thread()函数为ssh_2函数创建一个线程并将它赋值给变量a,注意Thread()函数的target参数对应的是函数名称(即ssh_2)
#args对应的是该ssh_f函数的参数
a = threading.Thread(target=ssh_f, args=(ip_address, username, password, device_name, date))
a.start()
f.close()
time.sleep(5) #先等待所有操作执行完,再打印异常列表
print('\n以下设备认证失败无法登录: ')
if device_authentication_failed_list == []: # 判断是否为空列表
print('无')
else:
for i in device_authentication_failed_list:
print(i)
print('\n以下设备网络不可达: ')
if device_not_reachable_list == []:
print('无')
else:
for i in device_not_reachable_list:
print(i)

接下来的代码其实跟之前的主代码基本差不多,不再一一解释。增加的点就是在for循环里增加了threading的代码,并且在threading里调用刚才定义的ssh_f函数用于ssh连接及命令执行。

使用threading的Thread()函数为ssh_2函数创建一个线程并将它赋值给变量a,Thread()函数的target参数对应的是函数名称ssh_2,args对应的是该ssh_f函数的参数,相关参数都已经定义并传参进来。

然后a.start() 启动线程。使用threading多线程,这样下一个操作就不需要等上一个操作执行完后再开始执行,实现了并行执行。

实验验证

下面我们就对这个脚本运行一下,再来看看这5台设备的备份效果。特意制作了以下的GIF动图,可以看到5台设备的备份过程基本上是同时连接上,并且是几乎同时完成的,不再是前面实验一样先完成一台后再执行下一台,这样,这个脚本的执行效率就会大大的提升,如果设备多的话将会脚本运行的效率就会有量级的提升。

可以看到服务器文件目录里已经能够正常获取到5台交换机的配置文件。

总结

通过以上两个实验案例,通过异常处理及多线程,就可以优化上一个实验遗留下来的2个问题了。以上的实践尤其是多线程并发处理,肯定还有很多方式可以实现,接下来我也会继续探索更多Netdevops的内容。由于目前还处于小白的阶段,以上编写的代码和解读肯定还有很多不足和理解不够深入之处,如有任何错误的地方还请各位朋友指出,共同学习!