首页 > 编程笔记 > Python笔记 阅读:12

Python Jinja2模板引擎的用法(超级详细)

在网络运维领域中,对网络设备进行配置是一项至关重要的任务。这些配置通常是由网络工程师根据官方手册、自身经验以及实际场景的需求来准备的。

然而,很多网络运维组织在管理这些配置时,缺乏一种有效的标准化管理方式,这可能会导致配置的准确性受到影响。此外,由于网络运维的特性,配置过程中往往有很多重复、机械的操作,这也会提高配置出错的概率和风险。

为了解决网络配置难题,网络运维自动化开发人员引入了模板引擎 Jinja2。Jinja2 利用模板,提高了配置的准确性和一致性,并通过结构化数据驱动批量自动生成配置文件,减少了人工操作,降低了出错风险。

模板引擎的基本原理

模板引擎是 Web 开发中比较常用的一个名词,它是为了将业务数据和用户界面分离而产生的一种工具。

众所周知,Web 界面都是由 HTML 语言编写的,其本质是文本。当每次渲染页面时,业务数据可能变化,例如一些个人信息页,其样式大体是固定的,改变的主要是用户名、邮箱、头像等个人信息。因此,开发者发明了模板引擎,当用户访问指定个人信息页(用户界面)时,Web 程序先获取指定页面的模板(用户界面模板,包含了众多样式和界面的基本布局),同时从后台获取用户信息(业务数据),将用户信息渲染到指定的页面模板中。

模板引擎的运行逻辑如下图所示:


图 1 模板引擎的运行逻辑

Web 开发者开发出了很多基于 Python 的模板引擎,用于动态生成标准且统一的 Web 界面,如下是一段 Django(一款基于 Python 的 Web 开发框架)页面的模板。
<h1>用户列表</h1>
<ul>
    {% for user in users %}
    <li><a href="{{ user.url }}">{{ user.username }}</a></li>
    {% endfor %}
</ul>
在模板中使用“{{}}”定义变量,在动态获取页面时通过内置的 API 将结构化的用户数据填充到这个模板的 user 变量当中,从而生成标准且统一的页面,填充的过程被称为渲染(render)。

网络运维自动化开发人员发现这种模板引擎非常适合网络配置的模板化管理。通过模板引擎,开发者可以将网络配置标准落实到模板文件中,并借助 YAML、表格中的结构化数据驱动批量、自动化生成配置文件。这样可以有效地推动网络配置标准化的进程,还提高配置准备工作的效率,降低出错的风险。

Jinja2是什么

基于 Python 的模板引擎比较多,其中的佼佼者便是 Jinja2。

在网络运维自动化领域,Jinja2 的使用场景广泛,很多自动化框架都使用 Jinja2 作为模板引擎。Jinja2 是一款基于 Python 开发的模板引擎,最初用于基于 Python 的 Web 开发,其语法规则参考了 Django 的模板引擎。

随着网络运维自动化的兴起,Jinja2 因其简洁、可扩展、灵活和功能强大等特点,成为网络配置模板化管理的首选工具。

下面通过一个实例演示 Jinja2 的基本使用。
from jinja2 import Template
 
# 创建模板对象
templ = Template("Let's study {{ course }} now!")
 
# 传入变量渲染模板
result = templ.render(course='NetDevOps') # 渲染方法一
# result = templ.render({'course': 'NetDevOps'}) # 渲染方法二
 
print(result)
# 输出Let's study NetDevOps now!
在这段代码当中,首先通过模板文本(字符串)创建了一个 Template 模板类的模板对象 templ,然后调用模板对象的 render() 方法,将模板中的变量 course 赋值为 NetDevOps,最后返回一段渲染后的文本。

render() 方法支持利用两种赋值方法去渲染数据:
两种方法各有优点,读者可以根据个人习惯选择使用。这里使用第一种方法来实现渲染。

上面实例是 Jinja2 渲染模板的基本使用方法,在此基础之上可以将模板与数据分离,模板以文件的方式保存,数据以表格或者 YAML 文件等方式保存,后续将结合实际场景为读者讲解这种方法,在此之前,读者需要先掌握 Jinja2 的基础语法及其使用。

Jinja2的基础语法及其使用

Jinja2 的基础语法非常简单、易上手,同时又提供了一些高级的 API 和语法。

Jinja2的基础语法

在 Jinja2 中,最基础的语法是变量的定义、判断与循环。为了方便演示 Jinja2 的语法,先将模板与数据都放置在代码中,然后将输出的结果以 print() 方法打印出来。在实际使用中,模板多放置在以 .j2 为扩展名的文本文件中,输出的结果也会被保存到文本文件中,或者与其他函数、程序对接,执行进一步的自动化任务。

1) 变量的定义及渲染

Jinja2 直接使用双花括号“{{}}”定义变量,双花括号中放的是变量的名称,形如“{{ variable }}”。为提高可读性,变量名称左右一般各留一个空格。

Jinja2 需要先将模板内容(字符串)加载成模板对象(Template 对象),再调用模板对象的 render() 方法,对模板中的变量逐一赋值。render() 方法返回的是渲染后的文本字符串。

在默认情况下,如果渲染时未对某变量赋值,那么其对应显示处为空白;如果渲染时未在模板中声明某变量,那么 Jinja2 不会做任何处理,也不会抛出异常。变量的数据类型可以是 Python 支持的任意数据类型,这里先以字符串变量为例,演示变量的定义和渲染:
from jinja2 import Template
 
# 定义模板,模板中有name 和desc两个变量
templ_str="""interface {{ name }}
description {{ desc }}
undo shutdown"""
 
# 使用字符串创建模板对象
templ = Template(source=templ_str)
# 调用模板对象的render方法,对模板中的name和desc变量赋值渲染
result = templ.render(name='Eth1/1',desc='gen by jinja2')
 
print(result)
代码的输出结果为:

interface Eth1/1
description gen by jinja2
undo shutdown

程序中,name 和 desc 是定义在 Jinja2 模板中的两个变量,在 render() 方法中直接以这两个变量名称作为参数进行赋值,便可以完成渲染。

Jinja2 的变量可以赋值为 Python 支持的任意数据类型,例如字符串、数字、字典和列表,它们在渲染时都会被强制转换为字符串类型。同时,用户也可以在模板的变量中访问对象的成员或者属性。

下面实例演示了 Jinja2 中数字、字典和列表类型变量的定义及渲染。
from jinja2 import Template
 
# 定义了数字my_num、字典my_dict和列表my_list,并访问了字典和列表成员
templ_str = """传入的数字为:{{ my_num }},
传入的字典为:{{ my_dict }},
传入的列表为:{{ my_list }},
通过key访问并使用字典成员的值:{{ my_dict['course'] }}
通过索引访问并使用列表成员:{{ my_list[2] }}"""
 
# 创建模板对象
templ = Template(templ_str)
# 渲染数据,为my_num、my_dict、my_list赋值
result = templ.render(my_num=100,
                      my_dict={'course': 'NetDevOps'},
                      my_list=['1', 2, {"course": "NetDevOps"}]
                      )
print(result)
其输出结果为:

传入的数字为:100,
传入的字典为:{'course': 'NetDevOps'},
传入的列表为:['1', 2, {'course': 'NetDevOps'}],
通过key访问并使用字典成员的值:NetDevOps
通过索引访问并使用列表成员:{'course': 'NetDevOps'}

2) 判断

在网络配置的生成过程中,经常需要根据变量的值进行判断,从而进入不同的分支,并生成不同的配置。

Jinja2 的判断语法与 Python 的判断语法相似,也是 if、else、elif 的组合使用。判断属于控制结构,Jinja2 的判断语句需要被包裹在控制符号 {% ... %},以明确标识出 if、else、elif 等判断标签。

在判断逻辑结束的地方,必须有一个配对的判断结束标签“{% endif %}”。

举个简单的例子:
from jinja2 import Template
 
templ_str = """interface {{ name }}
description {{ desc }}
{% if shutdown=='yes' %}
shutdown
{% elif shutdown=='no' %}
undo shutdown
{% else %}
请人工确认端口状态配置
{% endif %}"""
 
templ = Template(templ_str)
 
result = templ.render(name='Eth1/1', desc='gen by jinja2', shutdown='no')
 
print(result)
在程序的端口配置示例中,配置完的端口默认是开启的,如果用户想要控制端口的开关,那么需要在模板中添加用于控制判断的变量,借助 Jinja2 的判断语法,可以根据该变量的不同取值,展示相应的内容。

程序中,“{% if shutdown=='yes' %}”代表了判断的开始,判断条件中的 shutdown 是一个变量,在渲染时需要传入赋值:
最后在判断逻辑的末尾,通过添加“{% endif %}”结束整个判断逻辑。程序的执行结果如下:

interface Eth1/1
description gen by jinja2
 
undo shutdown

3) 空白控制

细心的读者会发现,为什么在上面实例的输出中间会多一个空白行呢?

空白行来自条件成立时的“{% elif shutdown=='no' %}”这行代码,在模板中,此控制结构后面有一个换行符,当此条件成立时,后面逻辑块中的内容都会保留,包括“看不到”的换行符。

在网络配置中,大部分场景下空白行对配置结果无影响,例如端口配置中的空白行并不会影响配置写入网络设备的结果。但在有些情况下,网络配置下发过程中会出现 yes 或者 no 的输入提示,如果多了一个空白行,就相当于直接敲了一下回车键,进而影响配置的准确性。为了处理那些有影响的空白符,或者为了提升输出内容的美观度,开发者需要进行空白控制。

在控制符号“{%...%}”内的开始或者结尾百分号的内侧添加减号,形如“{%−...%}”或者“{%...−%}”,就可以清除开始或者结尾处的多个连续空白符(包含空格、制表符与空白行)。

例如,上面实例中空白行产生的原因是“{% elif shutdown=='no' %}”右侧的换行符,那么只需要将其中的“%}”改为“−%}”,就可以去除空白行。

4) 循环

Jinja2 只支持 for 循环,用法上与 Python 的 for 循环相似,可以访问字典、列表和元组等常见的 Python 数据对象的成员。

在 Jinja2 中,for 循环的实现是在成对的控制符号内嵌套了 for 标签,并在 for 循环逻辑结束的地方添加循环结束标签“{% endfor %}”。

for 循环的成员可以在循环的逻辑块中作为局部变量而被直接使用。下面实例演示了 for 循环的基本使用:
from jinja2 import Template
 
# 对data数据进行for循环访问,intf作为局部变量,可以在for循环体内被使用
templ_str = """{% for intf in data -%}
interface {{ intf['name'] }}
description {{ intf.desc }}
{% if intf.shutdown=='yes' -%}
shutdown
{% elif intf.shutdown=='no' -%}
undo shutdown
{% else -%}
请人工确认端口状态配置
{% endif -%}
{% endfor -%}"""
 
templ = Template(templ_str)
 
data = [{'name': 'Eth1/1', 'desc': 'gen by jinja2', 'shutdown': 'yes'},
        {'name': 'Eth1/2', 'desc': 'gen by jinja2', 'shutdown': 'no'}]
 
result = templ.render(data=data)
 
print(result)
程序演示了 Jinja2 中 for 循环的基本使用,实现了端口配置批量生成的功能。模板中的变量 data 代表端口列表,每个成员是一个字典。使用“{% for intf in data −%}”对变量 data 进行循环访问。每次循环访问的成员都赋值给局部变量 intf,以便在后续的判断和渲染中直接访问 intf。

用户可以直接采用 Python 字典的取值方法来从 intf 中取值,也可以使用类似对象属性访问的方法来取值,后一种方式的编写更加简洁,上面实例中同时展现了这两种方式。

由于循环控制自己单独占用一行,首行末尾会有一个看不见的换行符,这时可以在对应控制符的右侧添加减号来去除换行。这样能保证输出中没有多余的空白行,也不会串行(空白控制符出现在左侧时特别容易串行)。实例程序的运行结果如下:

interface Eth1/1
description gen by jinja2
shutdown
interface Eth1/2
description gen by jinja2
undo shutdown

2、文件系统管理配置模板

在实际生产和使用中,Jinja2 的模板会和代码分离,模板内容会被写入以 .j2 为扩展名的文本文件中,并放置到某个目录中进行管理。

Jinja2 提供了将文件系统中的模板文件加载成模板对象的方法,此方法涉及两个类,分别是 Environment 类和 FileSystemLoader 类。

Environment 类是 Jinja2 模板引擎的核心类之一,它用于配置和管理模板的环境。Environment 类提供了许多选项和方法,用于自定义模板的加载、渲染和处理。创建环境对象(Environment对象)时,最关键的参数是 loader,该参数需要被赋值为加载器对象(Loader 对象)。

Jinja2 内置了多种模板的加载器,其中,FileSystemLoader 类创建的加载器对象可以通过文件目录加载模板。先使用 FileSystemLoader 类指定某目录,创建加载器对象。然后,用此加载器对象创建环境对象,再调用环境对象的 get_template() 方法,就可以通过模板的文件路径完成模板的加载,并返回一个模板对象。

按照文件系统管理配置模板的思路,改写上面的实例:
使用 Environment 类和 FileSystemLoader 类实现文件系统管理配置模板,如下面实例所示,此代码与 jinja2_templates 位于同级目录中。
from jinja2 import Environment, FileSystemLoader
 
# 通过目录创建加载器
loader = FileSystemLoader("jinja2_templates")
 
# 通过文件系统加载器创建环境
env = Environment(loader=loader)
 
# 获取指定Jinja2模板文件
template = env.get_template("jinja2_demo.j2")
 
data = [{'name': 'Eth1/1', 'desc': 'gen by jinja2', 'shutdown': 'yes'},
        {'name': 'Eth1/2', 'desc': 'gen by jinja2', 'shutdown': 'no'}]
 
result = template.render(data=data)
 
print(result)
上述代码的运行结果为:

interface Eth1/1
description gen by jinja2
shutdown
interface Eth1/2
description gen by jinja2
undo shutdown

3、过滤器的定义与使用

过滤器(filter)是 Jinja2 中的特殊函数,它只有一个参数(一般写作 value)和一个返回值。

在 Jinja2 模板中使用过滤器时,要用管道符“|”将变量和过滤器连接。在渲染时,Jinja2 将变量赋值给 value,过滤器函数将其转换为一个新值,并渲染到指定位置。

Jinja2 中内置了很多和 Web 开发相关的过滤器。Jinja2 常用的内置过滤器及其说明如下表所示。

表:Jinja2 常用的内置过滤器及其说明
内置过滤器 说明
abs 返回给定数字的绝对值
capitalize 字符串的首字母大写,其他字母小写
default 如果变量不存在或为空,就使用默认值
float 将字符串转换为浮点数
int 将字符串转换为整数
length 返回字符串、列表或字典的长度
lower 将字符串转换为小写
upper 将字符串转换为大写
replace 将字符串中的某个子串替换为另一个子串
round 对浮点数进行四舍五入
title 字符串中的每个单词的首字母大写
trim 去除字符串两端的空白字符
truncate 将字符串按指定长度截断,并添加省略号

本节以提供默认值的内置过滤器 upper 为例,简单演示过滤器的使用方法和其效果,参考如下代码示例:
from jinja2 import Template
 
templ = Template("hostname {{ hostname|upper }}")
 
result = templ.render(hostname='netdevops01')
 
print(result)
# 输出hostname NETDEVOPS01
在网络配置的相关场景中,过滤器可以在很多方面发挥作用,例如将不同格式的 IP 地址转换为设备能接受的 IP 地址、带宽速率的转换、时间格式的转换等。由于 Jinja2 内置的过滤器主要面向 Web 开发的场景,因此在实际使用中,需要用户根据场景自定义过滤器。

本节以 IP 地址转换的过滤器为例,将 CIDR 标记法的 IP 地址转换为网络设备能接受的掩码形式,过滤器的名称为 ip_to_mask_format,过滤器函数名为 ip_to_mask_format(二者名称可以不一致)。过滤器函数能够借助 netaddr 包,将 IP 地址变量 value 转换为掩码形式并返回。

按照上述实例的思路创建环境对象,环境对象的 filters 属性定义了此环境对象支持的过滤器,此属性为字典数据,其中 key 为过滤器的名称,value 为过滤器的函数。可以使用字典更新数据的方式,为环境对象的 filters 属性添加成员,将自定义过滤器注册到环境对象中,这样就可以在 Jinja2 模板中使用自定义过滤器了。

下面实例演示了定义 Jinja2 过滤器并注册到环境对象中:
from jinja2 import Environment, FileSystemLoader
from netaddr import IPNetwork
 
def ip_to_mask_format(value):
    ip_network = IPNetwork(value)
    ip_address = str(ip_network.ip)
    subnet_mask = str(ip_network.netmask)
    return '{} {}'.format(ip_address, subnet_mask)
 
if __name__ == '__main__':
    loader = FileSystemLoader('jinja2_templates')
    # 通过文件系统加载器创建环境
    env = Environment(loader=loader)
    # 添加自定义过滤器
    env.filters['ip_to_mask_format'] = ip_to_mask_format
    # 获取指定的Jinja2模板文件
    template = env.get_template('filter_demo.j2')
    result = template.render(ip='192.168.1.5/24')
    print(result)
    # 输出结果 ip addr 192.168.1.5 255.255.255.0

filter_demo.j2 的模板内容如下:
ip addr {{ ip|ip_to_mask_format }}
过滤器在一定程度上可以简化用户操作,保证网络配置的标准化,提高模板的容错率。用户可以根据自己的需求定义若干过滤器,并按照一定的组织结构放到专门的模块中,使用时通过函数将它们全部注册到环境对象中。

4、原子模板的嵌套组合

在日常运维中,网络配置经常会由众多配置项组合在一起。此时我们可以把配置拆解成比较小的原子模块,然后借助 Jinja2 的 include 语法对模板进行嵌套组合。

假设有两个原子模板 A.j2 和 B.j2,便可以基于这两个原子模板的组合,构建出第三个模板 C.j2,然后在 C.j2 中直接使用 include 语法,声明包含原子模板文件的路径,参考如下示例:
我们来演示两个模板组合生成一个新的模板
这是第一个模板A.j2渲染的文本
{% include 'A.j2' %}
这是第二个模板B.j2渲染的文本
{% include 'B.j2' %}
include 语法非常简单,使用 include 标签加控制符,在 include 后面传入要嵌入的模板路径即可,模板路径是字符串类型,需要用单引号包裹。include 语法也无须使用 end 符号声明结束。

在应用 C.j2 模板时,其效果直接等同于直接将 A、B 模板的内容复制并粘贴到对应位置。A 模板和 B 模板中定义的变量,也可以直接在 C 模板中使用。为了避免多个模板中的变量有冲突,笔者建议读者将传入的变量定义为字典,并统一命名为 data。每个字典成员的 key 对应原子模板的名称,原子模板通过 key 访问自己的数据并完成渲染。

假设有一台网络设备要完成 hostname、ntp、interfaces 等配置项,我们先要设计出渲染模板所需要的数据结构,这样才能更好地编写原子模板。基于配置项,我们可以设计出用于完成一次完整的网络配置所需的数据结构:
data = {
    'hostname': 'netdevops',
    'ntp': ['192.168.137.1'],
    'interfaces': [{'name': 'Eth1/1',
                    'desc': 'gen by jinja2',
                    'shutdown': 'yes'},
                   {'name': 'Eth1/2',
                    'desc': 'gen by jinja2',
                    'shutdown': 'no'}]
}
我们将变量命名为 data,每个 key 对应一个配置项。接下来要编写原子模板,所有的原子配置项都被放置到 jinja2_templates 目录下的 huawei 文件夹中。此文件夹包含 3 个原子模板,hostname 原子模板 hostname.j2 如下面代码所示:
hostname {{ data.hostname }}

ntp 原子模板 ntp.j2 如下面代码所示:
{% for server_ip in data.ntp -%}
ntp-service server {{ server_ip }}
{% endfor -%}

interfaces 原子模板 interfaces.j2 如下面代码所示:
{% for intf in data.interfaces -%}
interface {{ intf['name'] }}
description {{ intf.desc }}
{% if intf.shutdown=='yes' -%}
shutdown
{% elif intf.shutdown=='no' -%}
undo shutdown
{% else -%}
请人工确认端口状态配置
{% endif -%}
{% endfor -%}

嵌套组合模板 config.j2 如下面代码所示:
system-view
{% include 'huawei/hostname.j2' %}
{% include 'huawei/ntp.j2' %}
{% include 'huawei/interfaces.j2' %}
commit
return
save
y

在上述代码的嵌套组合模板 config.j2 中,使用 include 语法包含了 hostname.j2、ntp.j2 和 interfaces.j2 这 3 个原子模板。使用原子模板嵌套组合并进行渲染,如下面代码所示:
from jinja2 import Environment, FileSystemLoader
 
# 通过目录创建加载器
loader = FileSystemLoader("jinja2_templates")
# 通过文件系统加载器创建环境
env = Environment(loader=loader)
# 获取指定Jinja2模板文件
template = env.get_template("huawei/config.j2")
 
data = {
    'hostname': 'netdevops',
    'ntp': ['192.168.137.1'],
    'interfaces': [{'name': 'Eth1/1',
                   'desc': 'gen by jinja2',
                   'shutdown': 'yes'},
                  {'name': 'Eth1/2',
                   'desc': 'gen by jinja2',
                   'shutdown': 'yes'}]
}
result = template.render(data=data)
print(result)
首先将网络配置模板拆解成众多原子模板,然后在不同场景中组合使用原子模板,这样可以让运维工作更加高效。

结构化数据驱动的Jinja2实战

文件系统加载模板的方式使模板和代码完成了分离。接下来将在此基础上进一步将数据与模板分离,同时将生成的配置输出到指定文件中。

1) 利用表格承载数据并批量生成网络配置文件

表格是网络工程师最熟悉的数据承载格式,它可以承载众多同质化数据。假设在服务器批量上线或者有其他业务需求的场景下,需要对若干接入交换机的若干接入端口进行相关配置。

这里我们借助 Python 脚本和 Jinja2 模板文件,利用表格文件承载数据,从而批量生成此类场景的网络配置文件。

基于原子模板和 Jinja2 的 include 语法,借助之前编写的原子模板创建一个名为 interfaces_config.j2 的模板,放置到 jinja2_templates 的 huawei 文件夹中,模板内容如下:
system-view
{% include 'huawei/interfaces.j2' %}
commit
return
save
y
由于此处的原子模板仅作演示,因此比较简单,读者可以根据自身情况丰富原子模板内容。准备好 Jinja2 模板之后,创建一个名为 jinja2_data 的文件夹,并在文件夹中创建一个表格,以设备的 IP 地址(192.168.135.201.xlsx)命名,然后根据 interfaces.j2 模板中对局部变量的引用要求,定义表格的表头(name、desc 和 shutdown),并将它们放置在第一个工作表中。

用于渲染模板的端口列表内容如下表所示:

表:用于渲染模板的端口列表内容
name desc shutdown
Eth1/1 gen by jinja2 yes
Eth1/2 gen by jinja2 no

此外,读者可以根据情况复制并粘贴出若干份 192.168.135.201.xlsx 中的内容,修改其中的 IP 地址和数据,从而轻松模拟出拥有多台设备的环境。

批量生成配置脚本代码的基本思路是:
当读者进行开发时,建议将一些功能比较明确的逻辑部分抽象并封装成函数,以提高代码可读性和可维护性,提高开发效率。根据此思路,这里抽象出了以下 4 个函数:
通过表格数据批量生成配置文件的通用脚本如下代码所示:
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
import pandas as pd
 
def get_files(folder_path):
    """获取指定文件目录中的所有文件,只遍历第一层"""
    files = []
    # 使用pathlib的Path,调用iterdir获取所有文件
    folder = Path(folder_path)
    for file_path in folder.iterdir():
        if file_path.is_file():
            files.append(file_path)
    return files
 
def get_jinja2_templ(templ, dir='jinja2_templates'):
    '''将指定目录的指定模板文件加载成Jinja2的模板对象'''
    # 通过目录创建加载器
    loader = FileSystemLoader(dir)
    # 通过文件系统加载器创建环境
    env = Environment(loader=loader)
    # 获取指定的Jinja2模板文件
    template = env.get_template(templ)
    return template
 
def get_data_from_excel(file):
    '''使用pandas从表格文件中读取数据,加载成Python字典的列表数据'''
    df = pd.read_excel(file)
    data = df.to_dict(orient='records')
    return data
 
def gen_configs_by_excel(j2_file,
         j2_data_name,
         j2_data_path='jinja2_data',
         j2_templ_path='jinja2_templates',
         config_path='configs'):
    '''
    批量生成配置文件
    :param j2_file:Jinja2模板名称
    :param j2_data_name:表格文件放置到data变量中的key名称
    :param j2_data_path:放置表格数据的目录
    :param j2_templ_path:放置Jinja2模板的目录
    :param config_path:文件输出的目录
    :return:None
    '''
    # 获取表格文件列表
    excel_files = get_files(j2_data_path)
    # 获取Jinja2模板对象
    template = get_jinja2_templ(j2_file, j2_templ_path)
    # 遍历表格文件,根据表格中的数据渲染出配置文件并输出到指定文件夹中
    for file in excel_files:
        # 从表格中获取单台设备的数据
        data_in_excel = get_data_from_excel(file)
        # 生成用于渲染模板的数据、字典的格式
        data = {j2_data_name: data_in_excel}
        # 渲染模板生成字配置文本
        config_text = template.render(data=data)
        # 生成输出的文件名称(含路径)
        output_file = '{}/{}'.format(config_path, file.name.replace('xlsx', 'config'))
        # 将配置写入指定文件
        with open(output_file, mode='w', encoding='utf8') as f:
            f.write(config_text)
 
if __name__ == '__main__':
    gen_configs_by_excel(j2_file='huawei/interfaces_config.j2',
                         j2_data_name='interfaces')
程序看似很复杂,但在核心函数 gen_configs_by_excel() 中实际只有 9 行代码,这是因为相关功能被抽象并封装到了函数中,再结合适当的注释,代码可读性很好。

任何复杂的功能,只要梳理清楚其基本逻辑,将其拆解成小块的任务,即可简化问题。在编写每个函数的过程中,都可以借助 main() 函数,测试编写的函数是否达到预期,并不断调整和优化函数。

程序执行完后,便可以在 configs 文件夹中找到对应的网络配置文件了。

2) 利用YAML文件承载数据并批量生成网络配置文件

使用表格承载数据并批量生成网络配置文件,这种方式比较适合列表类的数据驱动场景。

在一些场景下,网络配置整体是呈树状结构的。针对配置项比较多的网络配置批量生成场景,一般利用 YAML 文件来承载数据。

以嵌套组合模板 config.j2 为例,假设要批量生成若干网络设备的配置文件。按照上节的基本思路,编写一个嵌套组合的配置模板并将其放置到 jinja2_templates 的 huawei 文件夹中,此处使用配置模板 config.j2。按设备准备对应的 YAML 文件,用户承载生成配置的数据,并以设备 IP 地址将文件命名为 192.168.135.201.yml,其内容如下:
hostname: netdevops
ntp:
  - 192.168.137.1
interfaces:
  - desc: gen by jinja2
    name: Eth1/1
    shutdown: 'yes'
  - desc: gen by jinja2
    name: Eth1/2
    shutdown: 'no'
需要注意的是,yes、no 在 YAML 中会被识别为布尔值,而在模板中会将其视为字符串进行判断,所以需要在 YAML 文件中用引号将其声明为字符串。借助前面实例的思路,很快就可以写出使用 YAML 文件中的数据来批量生成网络配置文件的通用脚本,如下面实例所示:
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
import yaml
 
def get_files(folder_path):
    """获取指定文件目录内的所有文件,只遍历第一层"""
    files = []
    folder = Path(folder_path)
    for file_path in folder.iterdir():
        if file_path.is_file():
            files.append(file_path)
    return files
 
def get_jinja2_templ(templ, dir='jinja2_templates'):
    '''将指定目录的指定文件加载成为Jinja2的模板对象'''
    # 通过目录创建加载器
    loader = FileSystemLoader(dir)
    # 通过文件系统加载器创建环境
    env = Environment(loader=loader)
    # 获取指定的Jinja2模板文件
    template = env.get_template(templ)
    return template
 
def get_data_from_yaml(file):
    '''使用PyYAML从YAML文件中读取数据'''
    with open(file, encoding='utf8') as f:
        data = yaml.safe_load(stream=f)
        return data
 
def gen_configs_by_yaml(j2_file,
                        j2_data_path='jinja2_data',
                        j2_templ_path='jinja2_templates',
                        config_path='configs'):
    '''
    批量生成配置文件
    :param j2_file:Jinja2模板名称
    :param j2_data_path:放YAML数据的目录
    :param j2_templ_path:放置Jinja2模板的目录
    :param config_path:文件输出的目录
    :return:None
    '''
    # 获取文件列表
    yaml_files = get_files(j2_data_path)
    # 获取Jinja2模板对象
    template = get_jinja2_templ(j2_file, j2_templ_path)
    # 遍历表格文件,根据表格中的数据渲染出配置文件并输出到指定文件夹中
    for file in yaml_files:
        # 从YAML中获取单台设备的数据
        data_in_yaml = get_data_from_yaml(file)
        # 渲染模板生成字配置文本
        config_text = template.render(data=data_in_yaml)
        # 生成输出的文件名称(含路径)
        output_file = '{}/{}'.format(
                       config_path, file.name.replace('yml', 'config'))
        # 将配置写入指定文件
        with open(output_file, mode='w', encoding='utf8') as f:
            f.write(config_text)
 
if __name__ == '__main__':
    gen_configs_by_yaml(j2_file='huawei/config.j2')
和前面的实例相比,程序添加了 get_data_from_yaml() 函数,使用 PyYAML 将 YAML 文件中的数据转换为字典数据,此数据可以直接用于渲染模板,无须转换。

程序还添加了 gen_configs_by_yaml() 函数,与 gen_configs_by_excel() 函数相比,去除了 j2_data_name 参数,并调用了 get_data_from_yaml() 函数来获取数据,且无须转换。

相关文章