《从零开始NetDevOps》是本人8年多的NetDevOps实战总结的一本书(且称之为书,通过公众号连载的方式,集结成册,希望有天能以实体书的方式和大家相见)。
NetDevOps是指以网络工程师为主体,针对网络运维场景进行自动化开发的工作思路与模式,是2014年左右从国外刮起来的一股“网工学Python"的风潮,最近几年在国内逐渐兴起。本人在国内某大型金融机构的数据中心从事网络自动化开发8年之久,希望能通过自己的知识分享,给大家呈现出一个不同于其他人的实战为指导、普适性强、善于抠细节、知其然知其所以然风格、深入浅出的NetDevOps知识体系,给大家一个不同的视角,一个来自于实战中的视角。
由于时间比较仓促,文章中难免有所纰漏,敬请谅解,同时笔者也会在每个章节完成后进行修订再发布,欢迎大家持续关注
本文主要介绍基于Nornir的网络自动化巡检。Nornir的基础知识篇请参考【拳打Ansible,脚踢Puppet】Nornir宝典2023新编——基础篇
Nornir 2023新编-实用插件篇
本篇一万余字,耗时估计25分钟,适用于已经掌握Python且具备Netmiko、TextFSM、Jinja2等提高内容的网络工程师。
6.8 实战案例
之前的章节为大家介绍了Nornir的基本使用及一些常用的Nornir插件包,这个篇章我们将带领大家编写一些场景下的实战案例。这些场景我们会整合一些过往的工具包,有的场景可能会有多个版本,对应不同的思路,供大家参考。这些脚本来自于对实战的抽象,在实际使用中有可能要结合实际情况做相关调整。
6.8.1 批量配置备份
批量配置备份场景是NetDevOps的经典场景之一,在Netmiko篇章我们结合pandans处理表格,循环登录众多网络设备进行配置备份,for循环的方式效率略显低下。这次我们使用Nornir来为大家实现这个场景,弥补之前效率不高的缺点。
基于nornir_netmiko的版本
配置备份需要与网络设备进行交互,我们先写一个完全基于nornir_netmiko与设备进行CLI的交互的版本,要用到的插件包涉及:
- nornir_table_inventory,使用表格来加载网络设备。
- nornir_netmiko,实现与网络设备的CLI交互。
- nornir_utils,将配置备份结果写入文件,以及结果的打印。
首先我们编写一个Excekl表格文件inventory.xlsx,按照nornir_table_inventory的规范填写内容。其中每台设备配置备份的命令我们放入一个自定义字段cmds,nornir_table_inventory插件中所有自定义字段仅支持字符串类型,所以我们通过英文逗号将对应命令隔开。表格内容如下:
name | hostname | platform | port | username | password | model | netmiko_timeout | netmiko_secret | cmds |
netdevops01 | 192.168.137.201 | huawei | 22 | netdevops | Admin123! | ce6800 | 60 | display version,display current-configuration | |
netdevops02 | 192.168.137.202 | huawei | 22 | netdevops | Admin123! | ce6800 | 60 | display version,display current-configuration |
我们在编写这个runbook的时候,希望它能兼容各类厂家的设备。所以不同厂商型号的设备执行的命令,我们在自定义参数cmds中进行了相关配置。我们实验环境是两台华为CE交换机的模拟器,所以platform设置为Netmiko中的华为设备的驱动——huawei。在这个清单中,我们如果有华三或者思科的设备也可以追加其中,将参数填写好,在cmds中添加对应的相关命令,用英文逗号隔开即可。关于提权的secret参数,我们表格中列了出来,无相关需求的其值空出来即可。一会在执行命令时,我们根据netmiko_secret参数是否为空,来判断是否提权。
接下来我们编写一个组合task函数config_backup,它实现了登录网络设备执行命令,并将回显保存到指定目录指定文本的功能。我们以第一视角,将整个runbook编写和调试的过程一步步为大家展示出来。代码的编写过程,笔者认为是一个由大到小,由粗到细的过程,我们要清楚自己编写脚本的目标功能,然后“大事化小”,逐步去实现这个脚本。
这次的代码我们将task函数写到单独的模块脚本中,放到tasks目录底下,脚本名称定义为config_backup.py 文件,文件中先写一个极简的task函数config_backup,这个代码的核心是取到要执行的命令并去执行,我们这个函数先编写第一步,获取要执行的命令,写到结果Result中去,其代码如下:
from nornir.core.task import Result
def config_backup(task_context):
cmds = task_context.host.data['cmds']
cmds = cmds.split(',')
return Result(host=task_context.host, result=cmds)
这个task函数中,我们通过task上下文task_context对象,获取其host属性,获取当前task运行的网络设备,然后再通过data属性获取其所有的自定义字段(是一个字典格式的对象),然后通过字典的访问方式,访问cmds对应的值,即我们要执行的命令。这个命令的格式是字符串类型,以英文逗号分隔,所以我们用字符串的方法split进行分隔,获取其列表格式。
编写完这个task函数之后,我们转而去搭runbook的整体框架。这个脚本比较简单,通过nornir_table_inventory加载网络设备,然后调用配置备份的task函数,将结果打印,脚本名称我们定义为config_backup_runbook.py,其内容如下:
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from tasks.config_backup import config_backup
if __name__ == '__main__':
runner = {
"plugin": "threaded",
"options": {
"num_workers": 100,
},
}
inventory = {
"plugin": "ExcelInventory",
"options": {
"excel_file": "inventory.xlsx",
},
}
nr = InitNornir(runner=runner, inventory=inventory)
result = nr.run(task=config_backup)
print_result(result)
这个runbook脚本比较简单,每次我们编写不同的task函数,都可以基于这个脚本去修改测试然后上线使用,脚本通过nornir_table_inventory中的ExcelInventory插件,加载表格中的网络设备创建Nornir对象,调用其run方法,传入我们的配置备份task函数config_backup,最后打印结果。
这段代码运行结果如下:
config_backup*******************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv config_backup ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
['display version', 'display current-configuration']
^^^^ END config_backup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv config_backup ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
['display version', 'display current-configuration']
^^^^ END config_backup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
runbook整体运行无误,编写的task函数目前达到预期——获取到了待执行的命令,且是以列表的格式。接下来我们就要去打磨细节——task函数实现功能需求。
我们的整体思路是循环命令列表,然后调用netmiko_send_command的方法,再从结果中获取文本回,通过nornir_utils中的write_file将设备的回显写入到指定文件。我们先按“日期-设备IP-配置命令”的方式命名文件,后续再按照文件夹的方式进一步优化。
from datetime import date
from nornir.core.task import Result
from nornir_netmiko import netmiko_send_command
from nornir_utils.plugins.tasks.files import write_file
def config_backup(task_context):
cmds = task_context.host.data['cmds']
cmds = cmds.split(',')
date_str = date.today().strftime('%Y%m%d')
ip = task_context.host.hostname
for cmd in cmds:
cmd_multi_result_objs = task_context.run(task=netmiko_send_command, command_string=cmd)
output = cmd_multi_result_objs[0].result
filepath = '{}_{}_{}.txt'.format(date_str,ip,cmd)
file_multi_result_objs = task_context.run(task=write_file,
filename=filepath,
content=output)
return Result(host=task_context.host, result=cmds)
代码整体比较简单,需要留意的是我们使用了Python的内置模块datetime,调取其类date的today方法,获取当前日期对象并格式化,获取了当前日期字符串。获取设备的IP地址后,然后循环cmds待执行命令列表,使用task上下文调用netmiko_send_command子task函数,返回一个MultiResult对象,使用索引0获取我们需要的结果Result对象,取其result属性获取回显output,格式化好文件名称路径,调用write_file子task函数,传给其参数操作的文件名称filename及写入文件内容content即可,最后task函数需要构建一个Result对象返回。其运行结果如下(只做部分截图展示):
从截图中,我们发现,所有task任务执行的结果都做了展示,会比较冗长。这是由于Nornir默认所有的结果级别都是INFO级别,且print_result打印的时候也用的是INFO级别,最终所有task任务结果都做了展示,而netmiko_send_command的结果是执行命令回显,write_file的结果打印的是文件的差异(展示所有文本内容,同时其中穿插差异行),两者内容都比较长,非常不利于结果查看。这个时候我们可以考虑优化结果展示,结合之前所讲,我们可以调整子task函数的结果级别,调整为DEBUG级别,这样结果展示的时候,相关的子task函数执行结果都不会展示出来。同时我们的返回结果内容也可以进行相关优化,对是否需要提权进行判断,整体对代码的修改内容如下:
- 子task函数的执行结果日志级别调整为DEBUG,这个在task上下文使用run方法的时候传参即可。
- config_backup函数的执行结果进行优化,将执行的命令及对应的文件封装到结果中。
- 调用子task任务netmiko_send_commad的时候,判断其secret是否为空,以便确定是否提权。
我们将代码优化,如下:
import logging
from datetime import date
from nornir.core.task import Result
from nornir_netmiko import netmiko_send_command
from nornir_utils.plugins.tasks.files import write_file
def config_backup(task_context):
# 返回结果,key为对应的cmd,value是对应备份文件的路径。
result = {}
cmds = task_context.host.data['cmds']
cmds = cmds.split(',')
date_str = date.today().strftime('%Y%m%d')
ip = task_context.host.hostname
# 通过对Host对象的connection_options属性进行相关操作,获取netmiko参数中的secret
secret = task_context.host.connection_options.get('netmiko').extras.get('secret')
# 有secret参数,则enable为True,再调用netmiko_send_command时传入
if secret:
enable = True
else:
enable = False
for cmd in cmds:
cmd_multi_result_objs = task_context.run(task=netmiko_send_command,
command_string=cmd,
enable=enable,
severity_level=logging.DEBUG)
output = cmd_multi_result_objs[0].result
filepath = '{}_{}_{}.txt'.format(date_str, ip, cmd)
file_multi_result_objs = task_context.run(task=write_file,
filename=filepath,
content=output,
severity_level=logging.DEBUG)
# 返回结果,key为对应的cmd,value是文件的路径。
result[cmd] = {'filepath': filepath}
# 将结果封装到Result对象中,由于配置备份成功,
# 生成了新的配置备份文件,按照自动化框架的一些约定俗成的规定,则将changed改为True,
return Result(host=task_context.host, result=result, changed=True)
在task上下文调用子task函数的两处,我们均赋值了参数severity_level为DEBUG级别(需要引入logging模块),这样打印最终整体运行结果的时候,我们只会输出config_backup函数的运行结果。config_backup函数的返回结果我们也进行了优化,加入了执行的命令以及对应配置备份的文件路径。对是否提权,进入enable模式,我们需要取Host对象中的secret参数,这个需要层层下钻,比较复杂,初学者执行task_context.host.connection_options.get('netmiko').extras.get('secret')
代码即可,通过最后的一个get方法,我们可以获取除基本属性之外的所有netmiko连接的相关参数。
runbook运行结果如下:
* netdevops01 ** changed : True ************************************************
vvvv config_backup ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'display current-configuration': { 'filepath': '20221129_192.168.137.201_display '
'current-configuration.txt'},
'display version': { 'filepath': '20221129_192.168.137.201_display '
'version.txt'}}
^^^^ END config_backup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : True ************************************************
vvvv config_backup ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'display current-configuration': { 'filepath': '20221129_192.168.137.202_display '
'current-configuration.txt'},
'display version': { 'filepath': '20221129_192.168.137.202_display '
'version.txt'}}
^^^^ END config_backup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
相较于之前的runbook,整体返回结果比较清晰,整体达到了预期。
基于netmiko的版本
我们完全基于nornir_netmiko包去完成了与网络设备的CLI交互,在一些比较复杂的场景之下,频繁与网络设备交互,需要调用一些特殊的模式,使用原生的Netmiko可能会更加灵活便捷。另外还有一个重要的原因,我们执行之前的runbook,所有task任务的执行结果都会保存在Nornir对象的相关属性之中,而这些数据都会临时放在内存中,即每台设备的回显都会放在内存中,假如我们对大批量设备执行的是show running-configuration类似的命令,则其回显文本比较大,且都会放置在内存中,设备数量一多,会对我们执行脚本的服务器资源有一定浪费。当然目前而言,这些对于几千台设备而言,这些回显文本还撑不爆内存,但也会拖慢执行效率。
出于多方面考虑,我们可以选择进行优化,在执行相关命令时,选择原生的Netmiko连接,然后调用其send_command之类的方法,而不是netmiko_send_command、write_file这种task函数,这样回显获取后写入文本,相对应变量会由Python自动清理,Nornir中只存放相关结果的数据,便不会给内存资源过多压力。当然在计算资源比较富裕的当今,使用完全基于nornir_netmiko的runbook也是没问题的,完全取决于大家的使用习惯和资源情况,个人觉得使用Netmiko连接的话,可以更好地节约资源,更加灵活,且最大化利用以前开发的脚本。
我们以编写一个Netmiko的版本的task函数,先将netmiko_send_command函数优化掉。结合之前的获取Netmiko连接的代码,比较容易实现:
import logging
from datetime import date
from nornir.core.task import Result
from nornir_utils.plugins.tasks.files import write_file
def config_backup(task_context):
# 返回结果,key为对应的cmd,value是对应备份文件的路径。
result = {}
cmds = task_context.host.data['cmds']
cmds = cmds.split(',')
date_str = date.today().strftime('%Y%m%d')
ip = task_context.host.hostname
# 通过task上下文获取Host对象,然后调用其get_connection方法获取Netmiko的连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# secret参数可以直接从Netmiko连接中获取
secret = net_conn.secret
# 有secret参数,不为空,则调用enable方法
if secret:
net_conn.enable()
for cmd in cmds:
output = net_conn.send_command(cmd)
filepath = '{}_{}_{}.txt'.format(date_str, ip, cmd)
file_multi_result_objs = task_context.run(task=write_file,
filename=filepath,
content=output,
severity_level=logging.DEBUG)
# 返回结果,key为对应的cmd,value是文件的路径。
result[cmd] = {'filepath': filepath}
# 将结果封装到Result对象中,由于配置备份成功,
# 生成了新的配置备份文件,按照自动化框架的一些约定俗成的规定,则将changed改为True,
return Result(host=task_context.host, result=result, changed=True)
这段代码中,我们通过task上下文获取网络设备Host对象,然后调用其get_connection方法获取Netmiko连接,调用的时候注意我们的Netmiko连接的名称是“netmiko”,这个是第一个参数,第二个是传入Nornir对象的相关配置,使用task上下文获取。获取的Netmiko连接net_conn变量就可以直接使用了,我们可以判断其secret参数是否有值来决定是否开启enable模式。与网络设备的CLI交互,我们也可以直接使用send_command方法获取回显,这样就不会将有netmiko_send_command子task任务相关结果的占用内存资源了。
write_file子task函数的Result结果对象中,会有diff,print_result函数会将Result对象中的result属性和diff属性都打印出来,这个内容是上次文本内容以及本次文本内容的差异化展示,类似如下内容:
--- 20221129_192.168.137.201_display version.txt
+++ new
@@ -1,5 +1,5 @@
Huawei Versatile Routing Platform Software
VRP (R) software, Version 8.180 (CE12800 V200R005C10SPC607B607)
Copyright (C) 2012-2018 Huawei Technologies Co., Ltd.
-HUAWEI CE12800 uptime is 0 day, 3 hours, 15 minutes
+HUAWEI CE12800 uptime is 0 day, 3 hours, 33 minutes
SVRP Platform Version 1.0
从结果中我们可以看到,diff这块内容肯定也是占用了一部分内存的,且基本等于回显的长短,如果我们进一步优化,也可以将这块内容优化一下,自己编写一个普通函数。同时我们添加上目录结构的调整,之前所有的配置备份文件都是在一个目录中,设备比较多的情况下略显杂乱,这里我们先以日期为维度创建一个文件夹,然后再以设备IP为维度创建对应文件夹,最后各自命令对应的配置内容放入到不同的文本中。我们先编写这个处理目录和文件的代码,内容如下:
from pathlib import Path
def write_content2file(filename, content, dirs):
# 通过pathlib模块的Path对象 拼接一个路径
dir_path = Path(*dirs)
# parents,exist_ok一定都置为True,这样父目录未创建则自动创建,目录存在不做任何动作也不会报错
dir_path.mkdir(parents=True, exist_ok=True)
# 拼接完整文件的路径,使用不定参数的赋值方式。
filepath = Path(*dirs, filename)
with filepath.open(mode='w', encoding='utf8') as f:
f.write(content)
return str(filepath)
这里我们使用了pathlib这个模块,它是一个Python3中出现的模块,一般我们在网上搜索资料时,遇到的创建文件夹的方式都是os模块,在一些场景下,示例pathlib模块对文件(含文件夹)的操作更加便捷,从上述代码中我们可以看到,通过Path类,传入目录层级的列表,就可以自动创建一个路径对象,它会根据当前的操作系统自动拼接合适的路径。如果想用这个路径创建路径上所有的文件夹,我们可以直接调用Path对象的mkdir方法,它最便捷的两个参数parents和exist_ok,可以帮助我们自动判断目录是否存在,不存在则自动创建,存在也不会报错。我们再接一个文件名称的变量,与之前目录层级一起构建一个新的文件的路径,这个方法可以直接调用open方法,类似于Python内置的操作文件的open函数一样,而我们无需再传入路径的字符串,其他参数一致,我们赋值模式mode为w(写模式),编码字符集encoding为utf8。然后对这个文件对象调用write方法写入内容即可,且可以和with上下文管理器一共使用。
其中在方法或者函数中类似“*dirs”的形式,称之为不定参数,可以让函数和方法非常灵活地接受参数,参考如下代码:
def args_func(*args):
# 传入众多参数,数量可变,这些参数都会打包统一放到args中,它是一个元组
print(args, type(args))
if __name__ == '__main__':
args_func(1, 2, 3)
args_func(1, 2, 3, 4)
a = [1, 2, 3, 4, 5]
# 注意一定要写*a,会将a展开,传入多个参数给函数; 不写*,直接传入a会认为传入了一个成员,这个成员值为list
args_func(*a)
# 列表a会展开和6一起打包传入
args_func(*a,6)
我们设计了一个可变参数的函数,调用的时候我们可以传入三个参数,也可以传入四个参数,可以将列表前加星号“*”(可以认为讲列表展开成单独的成员传入函数),可以列表加星号后再接变量。其中列表可以替换为元组、集合等其他数据对象。
上述代码运算结果为:
(1, 2, 3) <class 'tuple'>
(1, 2, 3, 4) <class 'tuple'>
(1, 2, 3, 4, 5) <class 'tuple'>
(1, 2, 3, 4, 5, 6) <class 'tuple'>
大家可以借助这段代码理解不定参数。Path类在初始化的时候,路径上的节点是不定的,有可能三层,有可能四层,所以它设计成了不参数。
这段函数结合我们之前的task函数进一步修改,其最终版本为:
from datetime import date
from pathlib import Path
from nornir.core.task import Result
def write_content2file(filename, content, dirs):
# 通过pathlib模块的Path对象 拼接一个路径
dir_path = Path(*dirs)
# parents,exist_ok一定都置为True,这样父目录未创建则自动创建,目录存在不做任何动作也不会报错
dir_path.mkdir(parents=True, exist_ok=True)
# 拼接完整文件的路径,使用不定参数的赋值方式。
filepath = Path(*dirs, filename)
with filepath.open('w', encoding='utf8') as f:
f.write(content)
return str(filepath)
def config_backup(task_context):
# 返回结果,key为对应的cmd,value是对应备份文件的路径。
result = {}
cmds = task_context.host.data['cmds']
cmds = cmds.split(',')
date_str = date.today().strftime('%Y%m%d')
ip = task_context.host.hostname
# 通过task上下文获取Host对象,然后调用其get_connection方法获取Netmiko的连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# secret参数可以直接从Netmiko连接中获取
secret = net_conn.secret
# 有secret参数,不为空,则调用enable方法
if secret:
net_conn.enable()
for cmd in cmds:
output = net_conn.send_command(cmd)
filename = '{}.txt'.format(cmd)
filepath = write_content2file(filename=filename,content=output,dirs=[date_str, ip])
result[cmd] = {'filepath': filepath}
# 将结果封装到Result对象中,由于配置备份成功,
# 生成了新的配置备份文件,按照自动化框架的一些约定俗成的规定,则将changed改为True,
return Result(host=task_context.host, result=result, changed=True)
我们使用之前的runbook调用这个task函数,即可看到其效果:
config_backup*******************************************************************
* netdevops01 ** changed : True ************************************************
vvvv config_backup ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'display current-configuration': { 'filepath': '20221129\\192.168.137.201\\display '
'current-configuration.txt'},
'display version': { 'filepath': '20221129\\192.168.137.201\\display '
'version.txt'}}
^^^^ END config_backup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : True ************************************************
vvvv config_backup ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{ 'display current-configuration': { 'filepath': '20221129\\192.168.137.202\\display '
'current-configuration.txt'},
'display version': { 'filepath': '20221129\\192.168.137.202\\display '
'version.txt'}}
^^^^ END config_backup ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
符合我们的预期,结果如同之前的比较清爽一些,最主要的是释放了在配置备份这种场景下,设备众多,文本量比较大对内存的侵占。
我们还是要再次强调这个不是绝对的,毕竟在目前情况下,一般脚本所部属的主机,内存都是比较大的。最终决定权,还是结合大家自己的使用习惯。
6.8.2 批量信息采集
设备配置的批量备份,是以文本的方式,对网络配置进行一个记录,是网络运维的一种常见手段。网络配置(广义的网络配置)中有着众多的信息,平时的运维离不开这些信息,我们经常会统计全网交换机的端口使用情况,这需要我们有一个端口信息的台账。有时我们需要对全网的软件版本等信息进行统计,对于低版本的设备进行升级变更。诸如此类的场景,从一个侧面折射出信息的重要性。这个章节,我们为大家进行批量信息的采集,这部分我们会整合之前所学的表格处理、TextFSM解析引擎等相关信息。
设备信息的批量采集主要涉及到三个环节:
- 登录设备执行相关CLI,获取回显文本。
- 基于TextFSM将文本解析成格式化数据。
- 将格式化数据写入表格。
我们按照这个思路一步步实现信息的批量采集,在这个过程中不断迭代我们的代码。
信息批量采集
对于网络设备清单的管理,我们仍然使用之前的表格承载的思路。通过表格加载Inventory,然后加载Nornir对象。然后设计我们的task函数,用于完成对应信息的采集,我们以采集端口信息为例,task函数的名称我们定位collect_interfaces。这些初步设想好以后我们开始准备自己的所有物料。
网络设备清单——inventory.xlsx,如下的表格。
name | hostname | platform | port | username | password | model | netmiko_timeout | netmiko_secret | cmds |
netdevops01 | 192.168.137.201 | huawei | 22 | netdevops | Admin123! | ce6800 | 60 | display version,display current-configuration | |
netdevops02 | 192.168.137.202 | huawei | 22 | netdevops | Admin123! | ce6800 | 60 | display version,display current-configuration |
这次cmds字段的信息在本场景下无需使用。之后准备我们的task函数,将其放到tasks文件夹内的collection_interfaces.py中,函数名为collection_interfaces,写一个大致的框架即可:
from nornir.core.task import Result
def collect_interfaces(task_context):
""""
收集网络设备的端口信息数据,并写入到Excel表格中
返回结果result 为字典,包含表格文件路径,与端口总数
{’filepath':'XX','interface_total':100}
"""
result = {'interface_total': None, 'filepath': None}
return Result(host=task_context.host, result=result)
返回的结果我们设计为一个字典,包含采集信息写入表格的路径,以及端口统计。这个根据自己的情况而定,不一定完全参照这个。
之后我们编写runbook,将其命名为collect_interfaces_runbook.py,相当于成语的入口,task任务的执行还是依赖于runbook的执行,其内容如下:
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from tasks.collection_interfaces import collection_interfaces
if __name__ == '__main__':
runner = {
"plugin": "threaded",
"options": {
"num_workers": 100,
},
}
inventory = {
"plugin": "ExcelInventory",
"options": {
"excel_file": "inventory.xlsx",
},
}
nr = InitNornir(runner=runner, inventory=inventory)
result = nr.run(task=config_backup)
print_result(result)
所有物料准备好之后,我们可以运行runbook,其结果如下:
collect_interfaces**************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv collect_interfaces ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{'filepath': None, 'interface_total': None}
^^^^ END collect_interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv collect_interfaces ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{'filepath': None, 'interface_total': None}
^^^^ END collect_interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
框架已经搭好,剩下的就是细化task函数,实现我们的需求。我们聚焦于我们的单台设备的业务逻辑即可,所以我们倾其全力写好collect_interfaces函数。
我们分两步走,第一步实现信息的获取,第二步将信息写入表格。
在信息的获取,我们进一步拆解,逻辑大体是,先通过task上下文获取Netmiko连接,然后根据Host的platform判断其为何种设备,执行对应的命令获取端口信息,使用Netmiko与TextFSM的结合,指定解析模板获取格式化信息。对于不支持的设备,我们直接抛出异常,对于不同设备的兼容性,我们先按下不表,一会在代码的迭代中体现出来。按照这个思路,我们先写第一个版本:
from nornir.core.task import Result
from pathlib import Path
# 静态变量,指向解析模板的目录
TEXTFSM_TEMPLATE_DIR = 'textfsm_templates'
def collect_interfaces(task_context):
""""
收集网络设备的端口信息数据,并写入到Excel表格中
返回结果result 为字典,包含表格文件路径,与端口总数
{’filepath':'XX','interface_total':100}
"""
huawei_platform = 'huawei'
huawei_cmd = 'display interface brief'
# 通过pathlib的Path类,加强在windows和linux系统中的兼容性,参考之前textfsm篇章的模板,创建此解析模板
huawei_textfsm = str(Path(TEXTFSM_TEMPLATE_DIR, 'huawei_display_interface_brief.textfsm'))
result = {'interface_total': None, 'filepath': None}
host_obj = task_context.host
platform = host_obj.platform
if platform == huawei_platform:
# 获取Netmiko连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# secret参数可以直接从Netmiko连接中获取
secret = net_conn.secret
if secret:
net_conn.enable()
# 执行命令,并进行解析
data = net_conn.send_command(huawei_cmd,
use_textfsm=True,
textfsm_template=huawei_textfsm)
# 如果返回的数据是字符串,证明解析失败,返回的是回显文本
if isinstance(data, str):
# 抛出异常,进而关注解析模板的准确性和兼容性
raise Exception('解析数据失败,未获取到有效数据,请确认解析模板是否编写正确')
# 计算返回数据的长度,即端口数量
interface_total = len(data)
# 更新数据到返回结果中
result['interface_total'] = interface_total
# 暂时将端口列表返回到结果,方便中间调试查看,后期可以去除
# result['data'] = data
else:
raise Exception('暂时不支持此平台设备的端口信息采集')
return Result(host=task_context.host, result=result)
这段代码中,我们设计了一个目录,专门存放解析模板,使用TEXTFSM_TEMPLATE_DIR静态变量保存,静态变量可以理解为几乎不变的变量,一般用全大写字母表示。在task函数中,我们先定义我们支持的platform,这个平台查看端口的命令以及对应的解析模板。示例中,我们仅做华为交换机的展示,所以我们只定义了
huawei_platform、huawei_cmd、huawei_textfsm三个变量。后续在判断及使用中我们直接用变量即可,这样可以提高代码的可读性,如果有多处判断,也可以防止一个字符串在两处编写不一致的错误出现。
代码的逻辑主题我们判断当前设备是否为华为的设备,如果是则进行相关采集解析,如果否则抛出异常,后续我们没增加一种平台都可以在这段代码中写对应的判断逻辑。目前来看,如果设备是华为的平台,我们则可以获取Netmiko连接,并判断是否开启enable模式(实际华为设备一般不涉及此操作,且Netmiko也不支持此方法)。
获取到网络设备的Netmiko连接对象之后,我们就可以调用其send_command方法,传入我们要执行的命令,告知其开启textfsm解析功能以及解析模板。为了让task函数更加智能、人性化,我们判断返回的数据是否为字符串,如果是字符串,则证明TextFSM解析失败。解析失败的清晰,需要我们关注是否为模板准确性或者兼容性不够导致的。如果解析成功,我们计算返回数据的长度,即为端口的总数量。由于结果返回比较多,所以笔者在设计这个task函数的时候,没有将其写入返回的结果中,因为这样的整体runbook运行结果冗长。
至此第一阶段获取格式化的信息已经结束,运行runbook,可以查看脚本的基本运行情况,上述runbook运行结果如下:
collect_interfaces**************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv collect_interfaces ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{'filepath': None, 'interface_total': 13}
^^^^ END collect_interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv collect_interfaces ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{'filepath': None, 'interface_total': 12}
^^^^ END collect_interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
至此信息的批量采集基本完成,这个时候,我们观察脚本,对兼容不同的平台这部分进一步优化。如果我们写一个思科ios平台的采集解析,可能又需要定义三个变量cisco_ios_platform、cisco_ios_cmd、cisco_ios_textfsm,然后在之前判断的基础上写一个elif的判断分支,代码大体如下:
from nornir.core.task import Result
from pathlib import Path
# 静态变量,指向解析模板的目录
TEXTFSM_TEMPLATE_DIR = 'textfsm_templates'
def collect_interfaces(task_context):
""""
收集网络设备的端口信息数据,并写入到Excel表格中
返回结果result 为字典,包含表格文件路径,与端口总数
{’filepath':'XX','interface_total':100}
"""
huawei_platform = 'huawei'
huawei_cmd = 'display interface brief'
# 通过pathlib的Path类,加强在windows和linux系统中的兼容性,
# 参考之前textfsm篇章的模板,创建此解析模板
huawei_textfsm = str(Path(TEXTFSM_TEMPLATE_DIR, 'huawei_display_interface_brief.textfsm'))
cisco_ios_platform = 'cisco_ios'
cisco_ios_cmd = 'show interface brief'
cisco_ios_textfsm = str(Path(TEXTFSM_TEMPLATE_DIR, 'cisco_ios_show_interface_brief.textfsm'))
result = {'interface_total': None, 'filepath': None}
host_obj = task_context.host
platform = host_obj.platform
if platform == huawei_platform:
# 获取Netmiko连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# secret参数可以直接从Netmiko连接中获取
secret = net_conn.secret
if secret:
net_conn.enable()
# 执行命令,并进行解析
data = net_conn.send_command(huawei_cmd,
use_textfsm=True,
textfsm_template=huawei_textfsm)
# 如果返回的数据是字符串,证明解析失败,返回的是回显文本
if isinstance(data, str):
# 抛出异常,进而关注解析模板的准确性和兼容性
raise Exception('解析数据失败,未获取到有效数据,请确认解析模板是否编写正确')
# 计算返回数据的长度,即端口数量
interface_total = len(data)
# 更新数据到返回结果中
result['interface_total'] = interface_total
# 暂时将端口列表返回到结果,方便中间调试查看,后期可以去除
# result['data'] = data
elif platform ==cisco_ios_platform:
# 获取Netmiko连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# secret参数可以直接从Netmiko连接中获取
secret = net_conn.secret
if secret:
net_conn.enable()
# 执行命令,并进行解析
data = net_conn.send_command(cisco_ios_cmd,
use_textfsm=True,
textfsm_template=cisco_ios_textfsm)
# 如果返回的数据是字符串,证明解析失败,返回的是回显文本
if isinstance(data, str):
# 抛出异常,进而关注解析模板的准确性和兼容性
raise Exception('解析数据失败,未获取到有效数据,请确认解析模板是否编写正确')
# 计算返回数据的长度,即端口数量
interface_total = len(data)
# 更新数据到返回结果中
result['interface_total'] = interface_total
# 暂时将端口列表返回到结果,方便中间调试查看,后期可以去除
# result['data'] = data
else:
raise Exception('暂时不支持此平台设备的端口信息采集')
return Result(host=task_context.host, result=result)
我们观察这段代码发现,其实新增一个设备平台的支持,差别只在判断条件和调用send_command这两处,其他部分的代码都是一样的,这个时候我们可以考虑将代码进行优化,笔者的思路是借助一个字典,这个字典的key是对应支持的platform的值,value是实际要执行的命令和解析模板,这样我们只需判断当前平台是否在字典的key中出现,出现则支持此平台的端口信息采集。如果支持此平台的端口信息采集,我们通过字典取其相关信息,执行一次CLI交互与数据解析。按照这个思路,我们改造task函数,内容如下:
from nornir.core.task import Result
from pathlib import Path
# 静态变量,指向解析模板的目录
TEXTFSM_TEMPLATE_DIR = 'textfsm_templates'
# 支持的平台,其对应的执行的命令及解析模板
PLATFORM_PARSE_INFO = {
'huawei': {'cmd': 'display interface brief', 'textfsm': 'huawei_display_interface_brief.textfsm'},
'cisco_ios': {'cmd': 'show interface brief', 'textfsm': 'cisco_ios_show_interface_brief.textfsm'},
}
def collect_interfaces(task_context):
""""
收集网络设备的端口信息数据,并写入到Excel表格中
返回结果result 为字典,包含表格文件路径,与端口总数
{’filepath':'XX','interface_total':100}
"""
result = {'interface_total': None, 'filepath': None}
host_obj = task_context.host
platform = host_obj.platform
# 判断当前平台是否在支持的平台解析字典当中
if platform in PLATFORM_PARSE_INFO:
# 获取Netmiko连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# secret参数可以直接从Netmiko连接中获取
secret = net_conn.secret
if secret:
net_conn.enable()
# 获取执行的命令和对应的解析模板
cmd = PLATFORM_PARSE_INFO[platform]['cmd']
textfsm_template = str(Path(TEXTFSM_TEMPLATE_DIR, PLATFORM_PARSE_INFO[platform]['textfsm']))
# 执行命令,并进行解析
data = net_conn.send_command(cmd,use_textfsm=True,textfsm_template=textfsm_template)
# 如果返回的数据是字符串,证明解析失败,返回的是回显文本
if isinstance(data, str):
# 抛出异常,进而关注解析模板的准确性和兼容性
raise Exception('解析数据失败,未获取到有效数据,请确认解析模板是否编写正确')
# 计算返回数据的长度,即端口数量
interface_total = len(data)
# 更新数据到返回结果中
result['interface_total'] = interface_total
# 暂时将端口列表返回到结果,方便中间调试查看,后期可以去除
# result['data'] = data
else:
raise Exception('暂时不支持此平台设备的端口信息采集')
return Result(host=task_context.host, result=result)
我们设计了一个字典格式的静态变量PLATFORM_PARSE_INFO,将不同平台应该执行的命令和解析模板对应起来,这样一套代码逻辑就可以支持众多平台设备。我们只需判断当前设备的platform是否在PLATFORM_PARSE_INFO的key当中,可以直接使用in操作进行判断。支持的话则取出其中的命令和解析模板,剩下的逻辑就与之前完全一致了,最后runbook的运行结果与之前毫无差别。
信息批量写入表格
配置的解析部分完成之后,我们就可以编写一个函数,将结果导入到Excel表格了,这块我们可以参考本书的极简表格操作方法,使用pandas几行代码解决这个问题。我们涉及一个data2excel的函数,代码如下:
import pandas as pd
def data2excel(data, filename, dirs):
"""
将指定字典的列表数据写入Excel表格的函数
Args:
data: 字典的列表数据
filename: Excel文件名称
dirs: 表格所处的目录,列表格式
Returns:
Excel文件路径
"""
# 创建对应的目录层级
dir_path = Path(*dirs)
# parents,exist_ok一定都置为True,这样父目录未创建则自动创建,目录存在不做任何动作也不会报错
dir_path.mkdir(parents=True, exist_ok=True)
# 拼接完整文件的路径
filepath = str(Path(*dirs, filename))
# 通过pandas写入数据
df = pd.DataFrame(data)
df.to_excel(filepath, sheet_name='interfaces', index=False)
return filepath
这段代码我们涉及了三个参数,详情可以参考函数的docstring。我们也按照层次目录进行组织,不同设备的端口数据放到不同的设备IP文件夹中。只需将函数在数据查询到后待用此函数更新返回结果即可。更新后的代码如下:
from nornir.core.task import Result
from pathlib import Path
import pandas as pd
# 静态变量,指向解析模板的目录
TEXTFSM_TEMPLATE_DIR = 'textfsm_templates'
# 支持的平台,其对应的执行的命令及解析模板
PLATFORM_PARSE_INFO = {
'huawei': {'cmd': 'display interface brief', 'textfsm': 'huawei_display_interface_brief.textfsm'},
'cisco_ios': {'cmd': 'show interface brief', 'textfsm': 'cisco_ios_show_interface_brief.textfsm'},
}
def data2excel(data, filename, dirs):
"""
将指定字典的列表数据写入Excel表格的函数
Args:
data: 字典的列表数据
filename: Excel文件名称
dirs: 表格所处的目录,列表格式
Returns:
Excel文件路径
"""
# 创建对应的目录层级
dir_path = Path(*dirs)
# parents,exist_ok一定都置为True,这样父目录未创建则自动创建,目录存在不做任何动作也不会报错
dir_path.mkdir(parents=True, exist_ok=True)
# 拼接完整文件的路径
filepath = str(Path(*dirs, filename))
# 通过pandas写入数据
df = pd.DataFrame(data)
df.to_excel(filepath, sheet_name='interfaces', index=False)
return filepath
def collect_interfaces(task_context):
""""
收集网络设备的端口信息数据,并写入到Excel表格中
返回结果result 为字典,包含表格文件路径,与端口总数
{’filepath':'XX','interface_total':100}
"""
result = {'interface_total': None, 'filepath': None}
host_obj = task_context.host
platform = host_obj.platform
# 判断当前平台是否在支持的平台解析字典当中
if platform in PLATFORM_PARSE_INFO:
# 获取Netmiko连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# secret参数可以直接从Netmiko连接中获取
secret = net_conn.secret
if secret:
net_conn.enable()
# 获取执行的命令和对应的解析模板
cmd = PLATFORM_PARSE_INFO[platform]['cmd']
textfsm_template = str(Path(TEXTFSM_TEMPLATE_DIR, PLATFORM_PARSE_INFO[platform]['textfsm']))
# 执行命令,并进行解析
data = net_conn.send_command(cmd, use_textfsm=True, textfsm_template=textfsm_template)
# 如果返回的数据是字符串,证明解析失败,返回的是回显文本
if isinstance(data, str):
# 抛出异常,进而关注解析模板的准确性和兼容性
raise Exception('解析数据失败,未获取到有效数据,请确认解析模板是否编写正确')
# 计算返回数据的长度,即端口数量
interface_total = len(data)
# 更新数据到返回结果中
result['interface_total'] = interface_total
# 暂时将端口列表返回到结果,方便中间调试查看,后期可以去除
# result['data'] = data
filepath = data2excel(data=data,filename='interfaces.xlsx', dirs=['data', host_obj.hostname])
result['filepath'] = filepath
else:
raise Exception('暂时不支持此平台设备的端口信息采集')
return Result(host=task_context.host, result=result)
我们只需要在数据解析成功后调用data2excel函数,目录我们涉及两层,第一层是data文件夹,第二次是设备IP地址文件夹,然后将文件路径更新到结果中,runbook调用此task函数的结果是:
collect_interfaces**************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv collect_interfaces ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{'filepath': 'data\\192.168.137.201\\interfaces.xlsx', 'interface_total': 13}
^^^^ END collect_interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv collect_interfaces ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
{'filepath': 'data\\192.168.137.202\\interfaces.xlsx', 'interface_total': 12}
^^^^ END collect_interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
我们也可以在data文件夹中看到设备IP的对应文件夹,以及其中的存有端口信息的interfacesxlsx文件,表格内容如下:
综上,我们实现了基本的批量收集数据的功能,按照这个套路我们还可以继续实现收集软件版本、MAC地址表、ARP表、路由表等众多的数据信息。在这个过程中,我们的代码可以进一步抽象,提高我们的开发效率。在这个场景当中我们选择将数据写入表格当中,我们也可以有一些“大开脑洞”的想法,比如将数据写入到数据库、推送给某自动化平台的API、利用某自动化平台的SDK写入自动化平台的数据库等等。数据是运维的基石,随着我们的NetDevOps之旅的不断深入,我们一定要建立 这样的意识,提升自己的数据思维。