1、自动化运维,paramiko 和 netmiko 模块
    自动化运维是用计算机替代人力,完成某些方面的运维工作。在有些场合使用自动化运维,比如巡检和监控、批量化配置等,能显著提高工作效率。

    自动化运维大概分为两类。第一类使用普通的、面向运维人员的 ssh 协议接口来实现。即用计算机模拟运维人员的登录和操作设备的过程,处理网络设备返回的字符串类数据内容。第二类使用特定协议和数据结构来实现。一般是网络设备提供提供特殊的RPC过程调用方法,网络设备作为服务器,管理端作为客户端,通过C/S架构在客户端和服务器之间交互特定的数据结构(XML,json等),实现网络设备的配置和维护。典型的协议是netconf 。

    典型的第一类自动化运维方案,是使用Python 的第三方模块paramiko 或netmiko 。paramiko 是基于通用的ssh协议实现的。所有基于ssh 协议的运维工作都可以用paramiko 来代替。包括Linux运维,数据库运维,网络运维等。由于其为了保证通用性,故抽象封装力度不大,用来做网络设备运维感觉有点繁琐。netmiko就是在paramiko的基础上进一步作了抽象和封装,专门用于网络设备的自动化运维,使用起来相对简洁很多。

    本章节重点讲第一类自动化运维的实现。第二类下一章节再讲。

    2、paramiko 模块介绍
    paramiko 是一个用于自动化运维的第三方模块。不是Python 自带的模块,故需要用 pip3 install 命令安装。
    paramiko 包含两个核心组件:SSHClient和SFTPClient。

    SSHClient 是一个类,用于构造一个支持SSH会话的对象。该类包含的最重要的两个方法:

    connect():连接一个实际的ssh接口。比如:
    connect(hostname=’10.16.46.90’, username=’yiyong.zhou’, password=’密码’) //关键字传参。用给定的账号密码连接到指定IP的ssh 端口。默认端口是22,可以修改。注意,这个函数执行后打开了目标 22 端口的socket连接。但并未开始ssh 登录认证。

    invoke_shell():该方法创建交互式的对象,也叫channel对象。channel对象支持send()方法和recv()方法。send()方法发送命令,recv()方法接收回显数据。实际上这个时候,即创建交互式channel对象时,才开始登录设备。交换机上dis users 能看到登录用户。这个时候不发送命令也能recv()数据,能看到banner 和第一行hostname和提示符。因为第一次登录,交互界面会回显数据,能recv()数据。逻辑上 invoke_shell()就是在模拟交互式登录过程。下面可以看到,发送命令和接收回显是严格按次匹配的。一次send()的回显数据只能recv()一次。

    channel对象支持send()方法,用于发送命令至设备。一次只能发一条命令,且命令后面要有换行符号,如:send(‘dis ver’+’\n’) 。send()方法有返回值,返回的是发送出去的命令字节长度,一般没啥用。我们需要接收命令输出的回显信息,就要在send()方法后紧跟着recv()方法,接收上个命令的回显。recv()方法使用时一般要指定参数。参数是一个数字,表示一次接收多少个字节的数据,可以设置得大一些。recv()方法的返回值就是接收到的数据,是字节码,需要解码后才能表示为字符串。一般用法是: output = term.recv(10000).decode() 。decode()函数默认用utf-8 规则解码,得到字符串后返回该字符串。

    注意,recv()方法不能连续执行两次。因为回显数据是一次性的,recv()后就没了。也不能在send()没有回显数据的命令后跟着执行recv()方法。比如 shutdown 一个端口。因为这时没有回显数据,recv()会一直等待,直到超时。

    connect(self, hostname, port=22, username=None, password=None, pkey=None, key_filename=None, timeout=None, allow_agent=True, look_for_keys=True, compress=False, sock=None, gss_auth=False, gss_kex=False, gss_deleg_creds=True, gss_host=None, banner_timeout=None, auth_timeout=None, gss_trust_dns=True, passphrase=None, disabled_algorithms=None)
    | Connect to an SSH server and authenticate to it. The server’s host key
    | is checked against the system host keys (see load_system_host_keys)
    | and any local host keys (load_host_keys). If the server’s hostname
    | is not found in either set of host keys, the missing host key policy
    | is used (see set_missing_host_key_policy). The default policy is
    | to reject the key and raise an .SSHException.
    | client = SSHClient()
    | client.load_system_host_keys()
    | client.connect(‘ssh.example.com’)
    | stdin, stdout, stderr = client.exec_command(‘ls -l’)
    | invoke_shell(self, term=’vt100’, width=80, height=24, width_pixels=0, height_pixels=0, environment=None)
    | Start an interactive shell session on the SSH server. A new .Channel
    | is opened and connected to a pseudo-terminal using the requested
    | terminal type and size.

    import paramiko //导入第三方模块
    ssh = paramiko.SSHClient() // 实例化一个 SSHClient()对象。
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) //这个对象支持一些秘钥缺失情况下的认证策略。
    ssh.connect(hostname=’10.16.46.90’, username=’yiyong.zhou’, password=’lzhlmclyhblsqt@321314’) // ssh 连接已经建立,交换机上能看到22端口会话,但没有登录,看不到登录用户

    term = ssh.invoke_shell() //创建一个channel对象。channel对象支持send()方法和recv()方法。send()方法发送命令,recv()方法接收回显数据。实际上这个时候才开始登录设备。交换机上dis users 能看到登录用户。这个时候不发送命令也能recv()数据,能看到banner 和第一行hostname和提示符。因为第一次登录,交互界面会回显数据。能recv()数据。逻辑上 invoke_shell()就是在模拟交互式登录过程。下面可以看到,发送命令和接收回显是严格按次匹配的。

    term.send(‘dis ver’+’\n’) // 登录,看到欢迎界面,输入命令必须加个’\n’。。。。

    output = term.recv(10000).decode() // 发送命令和接收回显是“按次”匹配的。发一次命令,接收一次回显。必须按照这个顺序进行。如连续recv()两次,会卡住。

    channel对象 和recv() 函数怎么对应起来的:是按 send()执行的次数, 执行一次,缓存里就有recv()可以接收的数据,但只能接收一次。接收一次后数据就没有了,再去接收就会卡住。。只能在send()一个命令或空格过去,再接收recv()一次数据。但直接接收的数据是字节码,不能用 + 拼接。 可以用decode()方法解码成字符串。然后拼接。

    注意, output = term.recv(10000) 。 这里output 是一个 字节码对象,class byte 。支持decode()方法,转换为字符串。但默认的解码方式是 UTF-8 。有些华三的设备的输出,用UTF-8无法解码。可以把解码方式改为 gbk。

    output1 = output.decode(‘gbk’)
    print(output1)

    3、通过python连接交换机并且取下来配置
    代码如下:

    1. import paramiko
    2. import os
    3. import time
    4. from sys import argv
    5. script, username, password = argv
    6. def ssh2(ip, username, password):
    7. ssh = paramiko.SSHClient()
    8. ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    9. ssh.connect(ip, 22, username, password, timeout = 5)
    10. ssh_shell = ssh.invoke_shell()
    11. return ssh_shell
    12. def save_file(filename, content):
    13. path = os.path.join(os.getcwd(), filename)
    14. with open (path, mode='a',) as outputfile:
    15. outputfile.write(content)
    16. def get_config(ip, username, password, cmds):
    17. ssh = ssh2(ip, username, password)
    18. for cmd in cmds:
    19. ssh.send(cmd+'\n')
    20. time.sleep(2)
    21. result = ssh.recv(100000).decode()
    22. save_file(ip, result)
    23. ssh.close()
    24. with open('./cmd.txt', 'r') as f:
    25. cmds = f.readlines()
    26. f.close()
    27. with open('./ip.txt', 'r') as f:
    28. ips = f.readlines()
    29. f.close()
    30. if __name__ == '__main__':
    31. for ip in ips:
    32. get_config(ip, username, password, cmds)
    33. ------代码结束---
    34. ------运行代码时指定用户名,密码---
    35. python3 getconfig.py <用户名> <密码>
    36. 下面是运行目录下的文件及内容,可以逐行添加:
    37. ip.txt
    38. 10.16.46.90
    39. cmd.txt
    40. screen-length disable
    41. dis version
    42. dis ip int b
    43. dis cur
    44. 代码运行结束后会在运行目录下生成以‘IP’命名的文件,包含所有命令输出结果。

    4、netmiko 模块介绍
    netmiko模块是基于paramiko模块做的二次开发,专门用于网络设备的自动化运维。故其专门对网络设备的认证登录、输入命令、获取回显数据等过程做了高度抽象和封装。
    netmiko这个模块里,最重要的一个函数是:ConnectHandler(**device_dict) 。 这个函数是一个工厂函数,即运行这个函数时需要给它特定的参数,函数将根据参数的不同,返回一个特定的对象。这个函数用于构建一个连接到特定类型设备上的,交互式 SSH 会话对象。这个会话对象支持多种方法。有方法获取当前命令提示符,也有方法可以直接输入命令,获得回显数据。这个对象支持send_command()方法。这个方法可以直接输入字符串形式的命令,获得函数返回值即是命令执行后的回显数据。这样就避免了paramiko 里那种需要用send()方法和recv()方法,并以按次匹配的方式获取回显数据,逻辑上更连贯,更高效。

    使用说明:
    1、import netmiko
    // 导入模块
    2、h3c = {‘device_type’: ‘hp_comware’, ‘host’: ‘10.16.46.90’, ‘username’:’yiyong.zhou’, ‘password’:’XXXXXX’}
    //定义一个字典,用于后续初始化SSH连接。字典的 key 值有特殊的要求。后面初始化ssh 会话的函数会用到这个字典做参数(多类型传参)。
    3、net_conn = netmiko.ConnectHandler(**h3c)
    // 实例化一个H3C的交互式SSH登录会话对象。这个时候用户已经登录了,可以执行命令,获得回显数据。ConnectHandler()是一个工厂方法,接收特定参数,返回相应的ssh会话对象。这里会根据传入的’device_type’,核实设备实际的banner回显,如果不匹配的话会报错。比如指定登录的是 huawei 的设备,但实际是一台 h3c 的设备,就会报错。
    4、cur_view = net_conn.find_prompt()
    //可以获得当前的命令提示符,即当前的配置模式。获得后不会变化,如需最新的,需要重新执行一次该方法获取。
    5、output = net_conn.send_command(‘dis ip int b’)
    //发送一条命令过去执行(命令后面不需要加’\n’表示回车了,这点相对paramiko来说改进了),并立刻返回回显数据。这样发送命令的方法就和接收回显直接“按次”匹配上了。而不像paramiko里哪样,send()方法返回的是发出去的命令字符数量,没卵用。获得回显还需要额外执行recv()方法,还得注意按次匹配send()和recv()。不然recv()接收不到数据会一直保持在接收状态,程序就会卡住。 注意这个send_command()方法也有个问题,和recv()相同,那就是如果发出的命令没有回显,比如‘sys’这种,只是提升权限,命令提示符会变,但没有回显数据,send_command()就会卡住。感觉这个send_command()就是封装了paramiko的send()方法和recv()方法,和recv()方法有同样的问题。这个send_command()还有一个改进就是接收数据量。默认情况下会一直接收,直到出现命令提示符。但是注意,如果命令没有回显数据,程序就会卡住,且没有timeout之类的机制退出。。 所以send_command()函数一般用于 show 之类的命令。如要输入没回显的命令,可以用send_config_set()方法。
    下面的方法说明:
    send_command()
    Execute command_string on the SSH channel using a pattern-based mechanism. Generally
    used for show commands. By default this method will keep waiting to receive data until the
    network device prompt is detected. The current network device prompt will be determined
    automatically.
    6、net_connect.send_config_set()
    如果要输入没有回显数据的命令,比如,sys, 进入一个接口,shutdown 一个接口等操作,就要用send_config_set()方法。这个方法输入的参数是一个命令列表。列表里的命令会顺序执行,而不管是否有输出回显数据。默认情况下,会在列表最后自动加一个 ‘return’ 命令,回到用户模式。即不管执行了那些命令,命令列表执行完成后,当前的登录状态都会回到用户模式(比如H3C的 模式)。这个方法的返回值是输入命令过程的回显数据。如有多个命令都有回显,回显数据会自动地累加。
    config_commands = [‘sys’, ‘interface GigabitEthernet1/0/1’, ‘shutdown’]
    output = net_connect.send_config_set(config_commands)

    注意:
    1 send_config_set() 函数,在执行时会自动地先输入一个 system-view 命令 ,命令执行完后会自动地输入一个 return 命令, 返回<>模式。
    2 给send_config_set() 函数执行的命令,包括给 send_command()函数的命令,都要写全,不要简写,避免出问题。
    3 send_command()函数 和send_config_set() 函数 都有一个问题。那就是他们会导致无法停留在某一个命令执行层级里面。send_command()函数无法在命令层级里面跳转,因为跳转命令没有输出,执行了后函数会卡主。send_config_set() 函数也无法停留在某一个命令层级里,因为他执行完参数的命令后会自动地加一个 return 命令,返回用户模式:<> 。 那就只能把需要在哪个命令层级需要做的事情,用一个列表全写好,一起做了。
    4 send_config_set() 函数和 send_command()函数的输出是一个多行字符串。经常需要在里面去寻找某一行后的某个特定参数。比如,某个接口下面的vlan配置。 接口编号和vlan号配置不是在一行的,那么用正则,直接重接口就找到vlan 号就很麻烦。可以用一种方法,把多行字符串转换为列表。每行字符串为一个列表的元素。方法如下:
    cmd_set = [‘interface Asy1/2/1/0’, ‘display this’]
    a = net_conn.send_config_set(cmd_set)
    a1 = a.splitlines()
    然后在列表里枚举含特定字符串的行,通过下标就可以找到他对应的配置。比如某个接口的vlan配置等。
    5 但是,send_config_set() 函数仍然有问题。这个函数是用来做配置用的。默认就会进入配置模式,默认执行完后自动退出配置模式。是否退出可以用关键字传参修改。但是,自动进入配置模式这一点无法修改。因为他就是用来做配置的! 这就导致一个问题,有些只能在用户模式下的命令执行不了。send_command()函数也执行不了。比如 以这个设备为跳板,去 telnet 其他机器。。。
    6 综合考虑,netmiko 对paramiko 进行了封装和二次开发,对很多操作进行了原子化,就是给他参数,他运行后给你结果,中间不需要人工干预。这样更符合自动化的思想,但是有些事情就做不了,比如,我远程console server 路由器,用它跳到其他机器上。netmiko 就没有考虑过这种情况,做不到了。。

    7、net_conn.disconnect()
    //断开连接。只能用disconnect() 。用发送“quit”或“exit”命令退出后会卡住。因为发送命令后接收不到数据了,实际命令已执行,但没有回显,就卡住了。

    其他netmiko.ConnectHandler(kwargues)对象的常用函数:
    net_connect = netmiko.ConnectHandler(
    h3c)
    net_connect.config_mode() — 进入配置模式
    net_connect.check_config_mode() — 检查是否在配置模式中,返回布尔值
    net_connect.exit_config_mode() — 退出配置模式
    net_connect.clear_buffer() — 清除在远程设备上输出的缓存
    net_connect.enable() — 进入enable模式
    net_connect.exit_enable_mode() — 退出enable模式
    net_connect.find_prompt() — 返回当前设备的prompt
    net_connect.commit(arguments) — 执行一个提交动作在juniper和IOS-XR上
    net_connect.disconnect() — 关闭ssh连接
    net_connect.send_command(arguments) — 发送命令,返回输出结果,默认超时时间为self.timeout的设置时间,默认为8秒
    net_connect.send_command_expect(arguments) #默认超时时间100秒
    net_connect.send_command_timing(arguments)
    net_connect.send_config_set(arguments) — 发送一组配置命令给远程设备
    net_connect.send_config_from_file(arguments) — 发送一组从本地写好的文件中的配置命令
    net_connect.send_config_from_file() # 发送从文件加载的配置命令
    net_connect.save_config() # 将running#config保存到startup#config

    5、使用netmiko,批量连接网络设备,执行多条命令,获取命令输出回显
    下面的脚本,用从CMDB获得的数据作为所有设备的统计信息。用netmiko依次连接每台设备,输入和设备厂商对应的巡检命令。将巡检输出结果保存到相应文件夹里。
    从CMDB里获得的原始文件需要处理一下才能使用。直到处理为下文中演示的那种结构才能在本脚本中使用。处理CMDB文件的脚本见下一节。
    输出的巡检结果放到指定的路径下。每次巡检生成一个巡检结果目录,里面有数据中心子目录,每个数据中心有设备厂商子目录,这里面才是设备巡检输出文件的位置。文件以IP地址命名。

    1. import netmiko
    2. import os
    3. import time
    4. from datetime import datetime
    5. from sys import argv
    6. def makedir(fulltime):
    7. os.mkdir('Network-' + fulltime)
    8. os.chdir('Network-' + fulltime)
    9. os.mkdir('DCLJ')
    10. os.chdir('DCLJ')
    11. os.mkdir('h3c')
    12. os.mkdir('juniper')
    13. os.mkdir('a10')
    14. os.mkdir('huawei')
    15. os.chdir('..')
    16. os.mkdir('DCXN')
    17. os.chdir('DCXN')
    18. os.mkdir('h3c')
    19. os.mkdir('juniper')
    20. os.mkdir('a10')
    21. os.mkdir('huawei')
    22. os.chdir('..')
    23. os.mkdir('DCXY')
    24. os.chdir('DCXY')
    25. os.mkdir('h3c')
    26. os.mkdir('juniper')
    27. os.mkdir('a10')
    28. os.mkdir('huawei')
    29. os.chdir('..')
    30. def save_file(dcname, vender, name, content):
    31. filename = dcname + '/' + vender + '/' + name + '_' + date
    32. fullpath = os.path.join(os.getcwd(), filename)
    33. with open (fullpath, mode='a',) as outputfile:
    34. outputfile.write(content)
    35. def get_device(allip):
    36. alldevice = []
    37. for i in range(len(allip)):
    38. dict_dev = {}
    39. dict_dev['dcname'] = allip[i].split(':')[0]
    40. dict_dev['vender'] = allip[i].split(':')[1]
    41. dict_dev['device_type'] = allip[i].split(':')[1]
    42. dict_dev['host'] = allip[i].split(':')[2].strip('\n')
    43. dict_dev['username'] = username
    44. dict_dev['password'] = password
    45. alldevice.append(dict_dev)
    46. for i in range(len(alldevice)):
    47. if alldevice[i]['device_type'] == 'h3c':
    48. alldevice[i]['device_type'] = 'hp_comware'
    49. elif alldevice[i]['device_type'] == 'juniper':
    50. alldevice[i]['device_type'] = 'juniper_junos'
    51. elif alldevice[i]['device_type'] == 'a10':
    52. pass
    53. elif alldevice[i]['device_type'] == 'huawei':
    54. pass
    55. else:
    56. pass
    57. return alldevice
    58. def get_config(device_dict, cmds, dcname, vender):
    59. net_conn = netmiko.ConnectHandler(**device_dict)
    60. for cmd in cmds:
    61. output = '\n' + net_conn.find_prompt() + cmd
    62. output += net_conn.send_command(cmd)
    63. save_file(dcname, vender, device_dict['host'], output)
    64. net_conn.disconnect()
    65. """
    66. 'allip.txt' is a file under the running directory, with many lines. Each line is composed
    67. of dcname, vendername and IP address, which represents a device. It's content like this:
    68. DCLJ:h3c:10.125.254.100
    69. DCXY:huawei:10.169.100.100
    70. <etc.>
    71. cmd_h3c.txt; cmd_a10.txt; cmd_juniper.txt; cmd_huawei.txt also are files under the
    72. running directory. These files also have many lines. Each line has string which
    73. represent a command for a special device. like:
    74. dis cur
    75. dis ip int b
    76. <etc.>
    77. """
    78. if __name__ == "__main__":
    79. script, username, password = argv
    80. fulltime = str(datetime.now())
    81. date = fulltime[0:10]
    82. makedir(fulltime)
    83. with open('../allip.txt', 'r') as f:
    84. allip = f.readlines()
    85. with open('../cmd_h3c.txt', 'r') as f:
    86. cmds_h3c = f.readlines()
    87. with open('../cmd_a10.txt', 'r') as f:
    88. cmds_a10 = f.readlines()
    89. with open('../cmd_juniper.txt', 'r') as f:
    90. cmds_juniper = f.readlines()
    91. with open('../cmd_huawei.txt', 'r') as f:
    92. cmds_huawei = f.readlines()
    93. alldevice = get_device(allip)
    94. for i in range(len(alldevice)):
    95. dcname = alldevice[i].pop('dcname')
    96. vender = alldevice[i].pop('vender')
    97. if vender == 'h3c':
    98. get_config(alldevice[i], cmds_h3c, dcname, vender)
    99. elif vender == 'a10':
    100. get_config(alldevice[i], cmds_a10, dcname, vender)
    101. elif vender == 'juniper':
    102. get_config(alldevice[i], cmds_juniper, dcname, vender)
    103. elif vender == 'huawei':
    104. get_config(alldevice[i], cmds_huawei, dcname, vender)
    105. else:
    106. pass

    总结:
    这个脚本是用面向过程的思路来写的。主要是对过程的抽象,将几个主要的过程抽象成函数加以实现。
    第一个过程:保存文件。将巡检输出结果保存为一个文件。在加上一些细节控制,比如文件的名称:IP地址。文件保存的路径:巡检结果目录,数据中心目录,厂商目录。还有一个要素就是要保存的内容。把这些需要外部输入的要素作为函数形参,组织起来即可构成这一保存文件的函数: save_file(dcname, vender, name, content): 这个函数没有返回值,只有一个“副作用”,就是保存文件。

    第二个过程:获取设备信息。从处理后的CMDB文件中提取需要的信息,将这些信息整理为一个字典组成的列表。这个列表里的每个元素都是一个字典,每个字典代表一台设备。之所以这样组织数据,是因为列表很容易被迭代处理,循环取出每个元素,即处理每台设备。且,后面netmiko 里的ConnectHandler()函数构造对象时需要输入字典参数。所以这里就将设备的信息整理成字典。注意两点:第一,ConnectHandler()函数接收字典参数,字典的键值有固定的要求。具体要求参照源码。构造字典参数时要注意,原来CMDB里的设备类型和ConnectHandler()函数字典参数类型不一样,需要对应地修改,比如 ‘h3c’ 要改为 ‘hp_comware’等。第二,为了使设备的巡检信息能保存到相应目录,需要在字典里添加额外的信息:数据中心和厂商。这样设备在巡检时输出结果保存在哪里就能直接在设备信息里找到。但这两个信息是额外的键值对,不能被ConnectHandler()函数,故要在使用这个函数时要把这两个键值对pop 出来,用两个局部变量保存。第二个过程抽象为函数:get_device(allip):输入是CMDB里提取出来的信息(readlines()读取出来的列表),输出是按照netmiko需求组织好了的字典组成的列表。每个字典代表一个网络设备,字典里额外添加了数据中心信息和厂商信息,用于后续指定巡检结果保存位置。这个函数在调用时(107行),用一个全局变量保存其返回的结果,供后续函数调用使用。

    第三个过程:根据设备字典信息和巡检命令,完成巡检,并调用保存文件函数保存文件。这个过程抽象为一个函数,将设备字典信息,对应的巡检命令传进去,即可调用ConnectHandler()函数构造会话对象,输入相应命令获取返回值,就是巡检结果。由于要调用文件保存函数:save_file(dcname, vender, name, content): ,这里就需要额外的参数,dcname, vender 。这个参数虽然和设备字典信息对应,但毕竟不完全一样,故这两个参数额外作为形参从外部传递进来。免得在处理一次字典信息。
    第64行,调用ConnectHandler()函数构造设备连接对象,然后后面用一个for循环,遍历命令列表里的命令,依次输入这个连接对象,并获取返回值。返回值默认就是字符串,可以用拼接的方式组合为更好看的形式,然后以追加的形式保存在一个文件里。save_file() 函数默认就是追加的形式。

    最后,脚本运行时需要指定用户名和密码,用户名和密码作为参数变量传递进脚本,这样避免密码泄露,也增加适用性。

    如何处理CMDB文件,获取本脚本需要的 ‘’DCLJ:h3c:10.125.254.100‘’ 格式的文件,见下一节:Python处理CMDB文件-字符串处理和json模块