《从零开始NetDevOps》是本人8年多的NetDevOps实战总结的一本书(且称之为书,通过公众号连载的方式,集结成册,希望有天能以实体书的方式和大家相见)。
NetDevOps是指以网络工程师为主体,针对网络运维场景进行自动化开发的工作思路与模式,是2014年左右从国外刮起来的一股“网工学Python”的风潮,最近几年在国内逐渐兴起。本人在国内某大型金融机构的数据中心从事网络自动化开发8年之久,希望能通过自己的知识分享,给大家呈现出一个不同于其他人的实战为指导、普适性强、善于抠细节、知其然知其所以然风格、深入浅出的NetDevOps知识体系,给大家一个不同的视角,一个来自于实战中的视角。
由于时间比较仓促,文章中难免有所纰漏,敬请谅解,同时笔者也会在每个章节完成后进行修订再发布,欢迎大家持续关注
本系列文章会连载于“NetDevOps加油站”公众号,欢迎大家点赞关注
相关系列文章《从零开始NetDevOps》,从零开始学,从最基础的Python开始学~
本篇是Nornir 2023新编的最后一篇文章,讲的是如何实现批量生成配置,以及配置的批量推送。
自此Nornir新编暂时告一个段落,后续会对Nornir的一些其他技巧不定期展开分享,欢迎大家关注公众号!
本篇4000余字,耗时估计15分钟,适用于已经掌握Python且具备Netmiko、TextFSM、Jinja2等提高内容的网络工程师。
本人知乎《NetDevOps加油站》同名专栏链接
https://www.zhihu.com/column/feifeiflight
也欢迎大家多关注评论交流。
欢迎按需加入我的读者群,关注NetDevOps公众号,回复加群即可获取我的微信,备注公司和城市(只要正面你不是机器人就行)!
读者二群虚位以待~
6.8.3 批量配置生成
之前的章节,我们为大家介绍过Jinja2模板引擎,通过它我们可以便捷地生成标准化的配置。在网络日常运维中,也经常会有针对一批设备的批量配置,比如调整开启端口调整端口描述,修改SNMP相关配置等等。之前的Jinja2章节我们为大家介绍的是单设备的相关配置生成方法,在这里我们引入之前介绍的nornir_jinja2模块,来实现众多设备的批量配置备份。
在这里我们主要用到以下几个模块的相关内容:
- nornir_jinja2生成标准配置。
- nornir_utils中的write_file将标准配置写入指定的文件当中。
- nornir_table_inventory来实现对网络设备的批量管理。
首先我们还是准确之前的方式,使用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 |
之后写一个批量配置生成的task函数,当然我们还是设计参数和返回结果,中间过程先省略。我们的思路是延续之前Jinja2篇章的思路,使用表格来管理众多配置项,加载对应模板生成对应配置,所以参数是需要表格文件路径data_file和模板文件路径j2_template,我们的Jinja2模板使用FileSystemLoader来进行管理,所以还需要一个参数Jinja2模板的文件夹路径j2_template_dir,返回的结果我们是渲染生成配置的路径,结果我们统一放到指定目录configs_dir这个参数中。函数名我们定义为gen_config_by_j2,意为通过Jinja2生成配置,按照惯例对应的py文件与函数名同名,即gen_config_by_j2.py,放在tasks文件夹中。
以上思路,我们经过仔细思考后会发现一个问题,在runbook中调用的时候j2_template、j2_template_dir、configs_dir各个设备是统一的,但是各个设备用于渲染模板所需的数据表格文件是不同的,而我们在runbook中通过Nornir对象调用task函数时,只能赋值对应参数为统一值。这个时候这个task函数的data_file参数通过外部传入的方式是有问题的。我们需要更改一下思路,通过设备IP对自动拼接出表格文件名称,比如192.168.137.201设备的数据存放在表格文件192.168.137.201.xlsx中。为了对数据文件进行有效组织,我们可以把所有的表格文件统一放入j2_data_dir目录中。综上task函数的主体如下:
from nornir.core.task import Result
def gen_config_by_j2(task_context,
j2_template,
j2_data_dir='j2_data',
j2_template_dir='j2_template',
configs_dir='configs'):
"""
通过表格文件和Jinja2模板渲染生成标准化配置
Args:
j2_template: 调用的jinja2模板
j2_data_dir: 承载数据的表格文件所放目录
j2_template_dir: jinja2模板所存放的目录
configs_dir: 渲染生成的模板放置的目录
Returns:渲染生成的配置的路径
"""
result = 'filepath'
return Result(host=task_context.host, result=result)
为了使用方便,我们把相关目录都赋予了默认值,按照默认值创建好对应的文件夹,这样task函数被调用的时候更加灵活而又方便,runbook也使用之前的主体,调用task函数只需要传入对应Jinja2模板即可,模板我们使用的是之前Jinja2篇章的模板,runbook代码内容如下:
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from tasks.gen_config_by_j2 import gen_config_by_j2
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=gen_config_by_j2, j2_template='huawei/my_include_demo.j2')
print_result(result)
上述代码运行结果如下:
gen_config_by_j2****************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv gen_config_by_j2 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
filepath
^^^^ END gen_config_by_j2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv gen_config_by_j2 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
filepath
^^^^ END gen_config_by_j2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
当代码主体可以正常运行的时候,接下来我们就需要细化task函数的内容了。
细化的整体思路是使用nornir_jinja2这个插件实现模板的渲染,然后再写入对应的文件中。我们先来实现第一步,通过设备IP去指定文件夹读取对应数据表格,然后渲染配置。
from pathlib import Path
import logging
import pandas as pd
from nornir.core.task import Result
from nornir_jinja2.plugins.tasks import template_file
def get_complex_data_from_excel(file='data.xlsx'):
# 通过表格获取数据,参考jinja2篇章代码
data = {}
df_dict = pd.read_excel(file, sheet_name=None)
for i in df_dict:
data[i] = df_dict[i].to_dict(orient='records')
return data
def gen_config_by_j2(task_context,
j2_template,
j2_data_dir='j2_data',
j2_template_dir='j2_template',
configs_dir='configs'):
"""
通过表格文件和Jinja2模板渲染生成标准化配置
Args:
j2_template: 调用的jinja2模板
j2_data_dir: 承载数据的表格文件所放目录
j2_template_dir: jinja2模板所存放的目录
configs_dir: 渲染生成的模板放置的目录
Returns:渲染生成的配置的路径
"""
# 拼接数据表格文件路径
data_file = '{}.xlsx'.format(task_context.host.hostname)
data_file = str(Path(j2_data_dir, data_file))
# 获取表格中的数据
data = get_complex_data_from_excel(data_file)
# 通过nornir_jinja2渲染生成配置
multi_result = task_context.run(task=template_file,
template=j2_template,
path=j2_template_dir,
data=data,
severity_level=logging.DEBUG)
config_str = multi_result[0].result
result = 'filepath'
return Result(host=task_context.host, result=config_str)
我们通过设备的IP地址拼接出对应的文件路径,然后Jinja2相关篇章的get_complex_data_from_excel函数获取对应的复杂数据,之后调用nornir_jinja的template_file函数,传入相关参数生成对应配置,在这一步我们将配置结果写入Result对象,同时为了结果打印“清爽”,我们调用子task的时候,设置了severity_level为DEBUG级别,上述runbook的运行结果如下:
gen_config_by_j2****************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv gen_config_by_j2 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
system-view
sysname netdevops01
ntp unicast-server 192.168.137.1 vpn-instance
info-center source default channel 2 log level debugging
info-center loghost source vlanif15
info-center loghost 192.168.137.10
interface Eth1/1
description gen by jinja2
shutdown
interface Eth1/2
description gen by jinja2
undo shutdown
return
save
y
^^^^ END gen_config_by_j2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv gen_config_by_j2 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
system-view
sysname netdevops01
ntp unicast-server 192.168.137.1 vpn-instance
info-center source default channel 2 log level debugging
info-center loghost source vlanif15
info-center loghost 192.168.137.10
interface Eth1/1
description gen by jinja2
shutdown
interface Eth1/2
description gen by jinja2
undo shutdown
return
save
y
^^^^ END gen_config_by_j2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
配置生成层面满足了我们的基本要求。之后我们使用nornir_utils的write_file,将字符串内容写入文件,由于批量生成配置的文本量并不大,所以不会侵占过多资源,所以我们使用了现成的Nornir组件。
from pathlib import Path
import logging
import pandas as pd
from nornir.core.task import Result
from nornir_jinja2.plugins.tasks import template_file
from nornir_utils.plugins.tasks.files import write_file
def get_complex_data_from_excel(file='data.xlsx'):
# 通过表格获取数据,参考jinja2篇章代码
data = {}
df_dict = pd.read_excel(file, sheet_name=None)
for i in df_dict:
data[i] = df_dict[i].to_dict(orient='records')
return data
def gen_config_by_j2(task_context,
j2_template,
j2_data_dir='j2_data',
j2_template_dir='j2_template',
configs_dir='configs'):
"""
通过表格文件和Jinja2模板渲染生成标准化配置
Args:
j2_template: 调用的jinja2模板
j2_data_dir: 承载数据的表格文件所放目录
j2_template_dir: jinja2模板所存放的目录
configs_dir: 渲染生成的模板放置的目录
Returns:渲染生成的配置的路径
"""
# 拼接数据表格文件路径
data_file = '{}.xlsx'.format(task_context.host.hostname)
data_file = str(Path(j2_data_dir, data_file))
# 获取表格中的数据
data = get_complex_data_from_excel(data_file)
# 通过nornir_jinja2渲染生成配置
multi_result = task_context.run(task=template_file,
template=j2_template,
path=j2_template_dir,
data=data,
severity_level=logging.DEBUG)
config_str = multi_result[0].result
# 根据IP生成配置文件的名称
file_name = '{}.txt'.format(task_context.host.hostname)
file_name = str(Path(configs_dir, file_name))
# 使用nornir_utils的write_file将配置写入文件
task_context.run(write_file, filename=file_name, content=config_str, severity_level=logging.DEBUG)
return Result(host=task_context.host, result=file_name)
上述runbook运行结果如下,对应文件夹内也生成了指定额配置文本。
gen_config_by_j2****************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv gen_config_by_j2 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configs192.168.137.201.txt
^^^^ END gen_config_by_j2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv gen_config_by_j2 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configs192.168.137.202.txt
^^^^ END gen_config_by_j2 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
基于这个runbook,我们修改对应的参数文件和模板,就可以基于不同场景生成不同的配置,其思路与Ansible中的批量配置生成到本地是类似的。生成的配置我们在风险可控的前提下,可以通过Netmiko推送给相关设备。
6.8.4 配置推送及其相关说明
使用自动化脚本向网络设备推送配置的时候,笔者一直强调的是风险可控,不同的场景,不同的网络区域,不同的行业,对于配置推送相关的自动化的风险要求是不一样的。笔者从事数据中心网络自动化相关工作内容,对于网络配置推送这个场景比较敏感,所以在网上(包含国外)的一些示例简单粗暴地推送网络配置,一直是持反对态度的。我们一定要认清对应的操作可能引发的风险是否可控,比如上架交换机后,连接服务器,后续整理需要对端口描述做一些规范化的配置,这个风险相对是可控的。对路由的相关调整,如果是在生产网络当中,我们一定是要反复核对的,要谨慎行之。对于一台设备的版本升级,要结合其区域,承载的业务,是否重启等多个维度去评估。任何场景,一旦发现其风险比较高的时候,我们要适当调整自动化策略,比如只生成标准配置,采取人工推送的方法,或者只推送风险相对可控部分的配置等等。
对于配置推送,我们可以采取Nornir,然后通过nornir_netmiko插件获取Netmiko连接,使用Netmiko的相关操作来进行与设备的交互,而不是使用nornir_netmiko的相关task任务,这样更加灵活可控。
在此,我们仅以一个批量修改端口描述的场景来进行简单演示,在实际使用过程中,大家一定要时刻牢记,网络配置的变化是牵一发可动全身的,大家一定要控制风险。
这个批量修改端口描述的场景出现频率比较高,风险且可控,这就是我们进行自动化配置推送优先考虑的两点,再结合Jinja2的配置标准化,我们可以进一步降低风险。配置的生成我们可以复用上一节内容,使用表格和Jinja2模板用于生成配置模板,使用的网络设备表格,我们仍使用以前的表格,此处不再赘述。我们的端口描述相关模板采用之前Jinja2篇章的模板端口部分,这里会稍微修改一下,由于我们是通过Netmiko推送,要结合实际交互添加一条commit命令。所以我们的模板命名为interface_with_commit.j2,内容如下:
{% for intf in data.interface -%}
interface {{ intf['name'] }}
description {{ intf.desc }}
{% if intf.shutdown=='yes' -%}
shutdown
{% elif intf.shutdown=='no' -%}
undo shutdown
{% else -%}
请人工确认端口状态配置
{% endif -%}
{% endfor -%}
commit
生成标准配置的task函数我们沿用之前的,表格数据是按IP命名的,文件夹可以以日期命名也可以使用默认的,内容上只保留interface页签的数据。
所以runbook中生成标准配置的部分如下:
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from tasks.gen_config_by_j2 import gen_config_by_j2
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=gen_config_by_j2,
j2_template='huawei/interface_with_commit.j2',
name='生成端口描述标准配置')
print_result(result)
这里我们复用了之前的代码,与之前不同之处在于:
- 修改了数据表格内容。
- 调用了新的配置模板。
- 对应的Nornir对象调用run方法时,我们传入了参数name,这样这部分task任务执行的时候,会显示name中的内容,可读性更好。
上述runbook的执行结果如下:
生成端口描述标准配置**********************************************************************
* netdevops01 ** changed : True ************************************************
vvvv 生成端口描述标准配置 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configs192.168.137.201.txt
^^^^ END 生成端口描述标准配置 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : True ************************************************
vvvv 生成端口描述标准配置 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configs192.168.137.202.txt
^^^^ END 生成端口描述标准配置 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
然后我们着手编写推送配置的task函数,这里会使用nornir_netmiko帮我们创建的Netmiko连接,然后使用Netmiko的对应方法执行命令。task函数我们命名为push_configs,放在tasks文件夹下的push_configs.py文件中,内容如下:
from pathlib import Path
from nornir.core.task import Result
def push_configs(task_context, config_dir='configs'):
"""
从指定文件夹内读取设备IP.txt的配置文件,推送给网络设备
Args:
config_dir: 配置文件的目录
Returns:
配置过程的回显
"""
output = ''
# 拼接配置文件路径,任何对象在format函数中使用的时候都会强制用str转为字符串
config_file = '{}.txt'.format(Path(config_dir, task_context.host.hostname))
# 获取netmiko连接
net_conn = task_context.host.get_connection('netmiko', task_context.nornir.config)
# 判断是否enable
if net_conn.secret:
net_conn.enabled()
# 通过配置文件推送配置
output = output + net_conn.send_config_from_file(config_file=config_file)
# 保存配置
output = output + net_conn.save_config()
# 回显结果放入结果中,变更成功,配置发生了变化。
return Result(host=task_context.host, result=output, changed=True)
这个task函数的内容比较简单,通过nornir_netmiko获取了Netmiko连接,然后使用了原生的Netmiko进行了配置的推送。在之前的runbook中调用上述task函数,runbook内容如下:
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from tasks.gen_config_by_j2 import gen_config_by_j2
from tasks.push_configs import push_configs
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=gen_config_by_j2,
j2_template='huawei/interface_with_commit.j2',
name='生成端口描述标准配置')
print_result(result)
result = nr.run(task=push_configs,
config_dir='configs',
name='推送配置到网络设备')
print_result(result)
我们先执行第一个任务,打印第一个任务的结果,然后执行第二个任务,打印第二个任务的结果。结合实际情况,我们也可以对指定设备进行筛选,大家要灵活变化。runbook执行结果如下:
生成端口描述标准配置**********************************************************************
* netdevops01 ** changed : False ***********************************************
vvvv 生成端口描述标准配置 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configs192.168.137.201.txt
^^^^ END 生成端口描述标准配置 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : False ***********************************************
vvvv 生成端口描述标准配置 ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
configs192.168.137.202.txt
^^^^ END 生成端口描述标准配置 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
推送配置到网设备************************************************************************
* netdevops01 ** changed : True ************************************************
vvvv 推送配置到网设备 ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
system-view
Enter system view, return user view with return command.
[~netdevops01]interface ge1/0/0
[~netdevops01-GE1/0/0] description gen by nornir
[*netdevops01-GE1/0/0]undo shutdown
[*netdevops01-GE1/0/0]interface ge1/0/1
[*netdevops01-GE1/0/1] description gen by nornir
[*netdevops01-GE1/0/1]undo shutdown
[*netdevops01-GE1/0/1]interface ge1/0/2
[*netdevops01-GE1/0/2] description gen by nornir
[*netdevops01-GE1/0/2]undo shutdown
[*netdevops01-GE1/0/2]interface ge1/0/3
[*netdevops01-GE1/0/3] description gen by nornir
[*netdevops01-GE1/0/3]undo shutdown
[*netdevops01-GE1/0/3]commit
[~netdevops01-GE1/0/3]return
save
Warning: The current configuration will be written to the device. Continue? [Y/N]:y
Now saving the current configuration to the slot 17
Info: Save the configuration successfully.
^^^^ END 推送配置到网络设备 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* netdevops02 ** changed : True ************************************************
vvvv 推送配置到网络设备 ** changed : True vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
system-view
Enter system view, return user view with return command.
[~netdevops02]interface ge1/0/0
[~netdevops02-GE1/0/0] description gen by nornir
[~netdevops02-GE1/0/0]undo shutdown
[~netdevops02-GE1/0/0]interface ge1/0/1
[~netdevops02-GE1/0/1] description gen by nornir
[~netdevops02-GE1/0/1]undo shutdown
[~netdevops02-GE1/0/1]interface ge1/0/2
[~netdevops02-GE1/0/2] description gen by nornir
[~netdevops02-GE1/0/2]undo shutdown
[~netdevops02-GE1/0/2]interface ge1/0/3
[~netdevops02-GE1/0/3] description gen by nornir
[~netdevops02-GE1/0/3]undo shutdown
[~netdevops02-GE1/0/3]commit
[~netdevops02-GE1/0/3]return
save
Warning: The current configuration will be written to the device. Continue? [Y/N]:y
Now saving the current configuration to the slot 17
Info: Save the configuration successfully.
^^^^ END 推送配置到网设备 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
至此我们完成了一个由数据驱动的配置推送场景,配置数据的驱动可以保证配置的标准化和准确性,这种场景比较适合高频低风险的配置推送,相对应的业务参数比较明确,可以抽象出模板,进而生成标准配置推送给设备。
在实际使用中,一定要在测试环境进行对应测试,才可以将这些runbook上生产环境。其中标准化配置这部分,在风险可控的前提下可以适当放宽,直接手写好配置,经过多人复核后,调用此runbook的后半段,也可以节省很多的人力,大家结合自身情况选择使用。
zhi乎也可以搜九净或者NetDevOps加油站关注我~
欢迎按需加入我的读者群,关注NetDevOps公众号,回复加群即可获取我的微信,备注公司和城市!
读者二群虚位以待~
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
http://www.tuicool.com/articles/BB3eArJ 服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net 机房租用,北京机房租用,IDC机房托管, http://www.e1idc.net