paramiko连接华为交换机配置自动化备份实践--优化篇(异常处理+concurrent.futures并发)

在上一篇文章中 paramiko连接华为交换机配置自动化备份实践(基础篇) ,我实践了通过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条件判断语句是一个意思。

网络设备登录异常通常有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并打印出来。

并发运行 concurrent.futures

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

其中,本次会用到concurrent.futures的其中一个子类ThreadPoolExecutor,用于创建线程池。另一个子类是ProcessPoolExecutor进程池,适用于CPU密集计算的场景,本次用不到。

多线程这个实验我会对代码稍微改造一下,作为把代码写成函数的方式。首先分别定义2个被调函数,以及1个主函数,在主函数里分别调用这2个被调函数。


代码如下:

第一部分

# 设备信息函数
def device_info():
    ip_list = []
    name_list = []
    f = open('devices_list.txt', 'r')
    for line in f.readlines():
        line_s = line.split()
        device_ip = line_s[0]
        device_name = line_s[1]
        ip_list.append(device_ip)
        name_list.append(device_name)
    f.close()
    return ip_list, name_list
# SSH备份操作函数
def ssh_backup(ipaddr, devname):
    device_authentication_failed = []
    device_not_reachable = []
    username = 'python'
    password = 'python123'
    date = time.strftime('%Y-%m-%d')
    try:
        ssh_client = paramiko.SSHClient()
        ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh_client.connect(hostname=ipaddr, username=username, password=password, timeout=3, look_for_keys=False)
        print(f'成功连接上: {ipaddr}')
        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 + '_' + devname + '_vrpcfg.zip\n')
        command.send('bye\n')
        time.sleep(1)
        print(f'已完成备份... {ipaddr}')
    except paramiko.ssh_exception.AuthenticationException:
        print(f'用户认证失败 {ipaddr} ')
        device_authentication_failed.append(ipaddr)
    except socket.error:
        print(f'{ipaddr} 网络不可达')
        device_not_reachable.append(ipaddr)
    ssh_client.close()
    return device_authentication_failed, device_not_reachable

首先定义一个名字叫device_info的函数,作用是将遍历后的设备IP、设备名分别存放在ip_list及name_list两个列表里,并对这两个列表return出来(传递出来)

然后定义一个名字叫ssh_backup的函数,设置两个形参ipaddr及devname。作用是ssh的登录及命令的操作。ssh connect这里的我加了个timeout=3秒,以免连接不上时等待时间过久。

第二部分

# 主函数
def main():
    device_authentication_failed_ls = []
    device_not_reachable_ls = []
    ip_list, name_list = device_info()
    with ThreadPoolExecutor() as executor:
        #map()传入的参数为可遍历的列表
        results = executor.map(ssh_backup, ip_list, name_list)
    for result in results:
        if result[0]:  #认证失败的列表为非空
            device_authentication_failed_ls.append(result[0])
        if result[1]:  #不可达的列表为非空
            device_not_reachable_ls.append(result[1])
   # 打印出连接失败的设备IP
    print('\n以下设备认证失败:')
    if device_authentication_failed_ls:
        for i in device_authentication_failed_ls:
            print(i[0])
    else:
        print('无')
    print('\n以下设备网络不可达:')
    if device_not_reachable_ls:
        for i in device_not_reachable_ls: