Jinja2 教程 - 第 6 部分 - 包含和导入

介绍

IncludeImport语句是 Jinja 为我们提供帮助组织模板集合的一些工具,尤其是当这些模板的大小增加时。

通过使用这些结构,我们可以将模板拆分为更小的逻辑单元,从而生成具有明确定义范围的文件。反过来,当出现新需求时,这将使修改模板变得更加容易。

结构良好的模板集合的最终目标是提高可重用性和可维护性。

目的和语法

“包含”语句允许您将大型模板分解为更小的逻辑单元,然后可以在最终模板中组装这些单元。

当你使用时,include你引用另一个模板并告诉 Jinja 渲染引用的模板。Jinja 然后将渲染文本插入到当前模板中。

语法为include

{% include 'path_to_template_file' %}

其中 'path_to_template_file' 是我们想要包含的模板的完整路径。

例如,下面我们有一个名为模板的模板,cfg_draft.j2它告诉 Jinja 找到名为模板的模板,渲染它,并用渲染的文本users.j2替换块。{% include ... %}

cfg_draft.j2

{% include 'users.j2' %}

users.j2

username przemek privilege 15 secret NotSoSecret

最后结果:

username przemek privilege 15 secret NotSoSecret

使用“包含”拆分大型模板

如果您查看典型的设备配置,您将看到与给定功能相对应的许多部分。您可能有接口配置部分、路由协议一、访问列表、路由策略等。我们可以编写单个模板来生成整个配置:

device_config.j2

hostname {{ hostname }}

banner motd ^
===========================================
|   This device is property of BigCorpCo  |
|   Unauthorized access is unauthorized   |
|  Unless you're authorized to access it  |
|  In which case play nice and stay safe  |
===========================================
^

no ip domain lookup
ip domain name local.lab
ip name-server {{ name_server_pri }}
ip name-server {{ name_server_sec }}

ntp server {{ ntp_server_pri }} prefer
ntp server {{ ntp_server_sec }}

{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 {{ idata.description }}
 {{ idata.ipv4_address }}
{% endfor %}

{% for pl_name, pl_lines in prefix_lists.items() -%}
ip prefix-list {{ pl_name }}
{%- for line in pl_lines %}
 {{ line -}}
{%  endfor -%}
{% endfor %}

router bgp {{ bgp.as_no }}
{%- for peer in bgp.peers %}
 neighbor {{ peer.ip }} remote-as {{ peer.as_no }}
 neighbor {{ peer.ip }} description {{ peer.description }}
{%- endfor %}

 

通常我们会有机地扩展我们的模板,并将一个接一个的部分添加到负责生成配置的单个模板中。然而,随着时间的推移,这个模板变得太大,并且变得难以维护。

处理日益增长的复杂性的一种方法是识别大致对应于单个特征的部分。然后我们可以将它们移动到自己的模板中,这些模板将包含在最后一个模板中。

我们的目标是使用一些较小的模板来处理明确定义的功能配置部分。这样,当您需要进行更改时,更容易找到要修改的文件。也更容易分辨哪个模板做什么,因为我们现在可以给它们适当的名称,如“bgp.j2”和“acls.j2”,而不是一个名为“device_config.j2”的大模板。

使用前面的模板,我们可以将其分解为更小的逻辑单元:

base.j2

hostname {{ hostname }}

banner motd ^
{% include 'common/banner.j2' %}
^
dns.j2

no ip domain lookup
ip domain name local.lab
ip name-server {{ name_server_pri }}
ip name-server {{ name_server_sec }}
ntp.j2

ntp server {{ ntp_server_pri }} prefer
ntp server {{ ntp_server_sec }}
interfaces.j2

{% for iname, idata in interfaces.items() -%}
interface {{ iname }}
 {{ idata.description }}
 {{ idata.ipv4_address }}
{% endfor %}
prefix_lists.j2

{% for pl_name, pl_lines in prefix_lists.items() -%}
ip prefix-list {{ pl_name }}
{%- for line in pl_lines %}
 {{ line -}}
{%  endfor -%}
{% endfor %}
bgp.j2

router bgp {{ bgp.as_no }}
{%- for peer in bgp.peers %}
 neighbor {{ peer.ip }} remote-as {{ peer.as_no }}
 neighbor {{ peer.ip }} description {{ peer.description }}
{%- endfor %}

 

我们现在有一组单独的模板,每个模板都有一个明确的名称来传达其用途。虽然我们的示例没有太多行,但我认为您会同意我的观点,即我们得出的逻辑分组将更易于使用,并且可以更快地建立关于这里发生的事情的心理模型。

随着功能移动到单独的模板,我们终于可以使用include语句来组成我们的最终配置模板:

config_final.j2

{# Hostname and banner -#}
{% include 'base.j2' %}

{% include 'dns.j2' %}

{% include 'ntp.j2' %}

{% include 'interfaces.j2' %}

{% include 'prefix_lists.j2' %}

{# BGP instance and peering -#}
{% include 'bgp.j2' %}

 

您打开此模板,只需快速浏览一下,您就应该能够了解它正在尝试做什么。它更简洁,我们可以轻松添加不会在数百行其他行中丢失的注释。

作为奖励,您可以通过注释单行或简单地暂时删除它来快速测试禁用一个功能的模板。

现在更容易进行更改,并且可以更快地识别特征和相应的模板,而不是搜索一个可能有数百行的大模板。

同样,当需要新部分时,我们可以创建单独的模板并将其包含在最终模板中,从而实现我们增加模块化的目标。

带有“包含”的共享模板片段

您可能还注意到其中一个包含的模板有自己的include声明。

base.j2

hostname {{ hostname }}

banner motd ^
{% include 'common/banner.j2' %}
^

您可以include在模板层次结构中的任何级别以及您想要的模板中的任何位置使用语句。这正是我们在这里所做的;我们将横幅的文本移动到一个单独的文件中,然后我们将其包含在base.j2模板中。

我们可以争辩说,横幅本身并不重要,不足以保证有自己的模板。但是,还有另一类include有用的用例。我们可以维护跨许多不同模板使用的通用片段库

这与我们之前的示例不同,在之前的示例中,我们将一个大模板分解为更小的逻辑单元,所有这些单元都与最终模板紧密相关。使用通用库,我们可以在许多不同的模板中重复使用这些单元,这些模板可能没有任何相似之处。

缺失和替代模板

Jinja 允许我们optionally通过ignore missinginclude.

{% include 'guest_users.j2' ignore missing %}

它本质上告诉 Jinja 寻找guest_users.j2模板并在找到时插入呈现的文本。如果找不到模板,这将导致空行,但不会引发错误

我通常建议不要在您的模板中使用它。它不是被广泛使用的东西,所以阅读您的模板的人可能不知道它的用途。最终结果还取决于特定文件的存在,这可能会使故障排除更加困难。

有更好的方法来处理可选特性,其中一些依赖于我们将在下一篇文章中讨论的模板继承。

与“忽略缺失”密切相关的是提供要包含的模板列表的可能性。Jinja 将检查模板是否存在,包括第一个存在的模板。

在下面的示例中,如果local_users.j2不存在但radius_users.j2存在,则渲染radius_users.j2最终将被插入

{% include ['local_users.j2', 'radius_users.j2'] %}

您甚至可以将模板列表与ignore missing参数结合起来:

{% include ['local_users.j2', 'radius_users.j2'] ignore missing %}

这将导致搜索列出的模板,如果没有找到它们,则不会引发错误。

同样,虽然很诱人,但我建议不要使用此功能,除非您用尽其他途径。如果在我的最终渲染中有些东西看起来不正确,我不会喜欢弄清楚列出的模板中的哪一个最终被包含在内。

总而言之,您可以使用“包含”来:

  • 将大模板拆分为较小的逻辑单元
  • 重复使用跨多个模板共享的片段

Import声明

在 Jinja 中,我们使用import语句来访问保存在其他模板中的宏。这个想法是经常在他们自己的文件中使用宏,然后将这些宏导入需要它们的模板中。

这与include声明不同,这里不进行渲染。相反,它的工作方式与 Python 中的 import 语句非常相似。导入的宏可用于导入它们的模板。

三种导入方式

我们可以通过三种方式导入宏。

所有三种方式都将使用导入以下模板:

macros/ip_funcs.j2

{% macro ip_w_wc(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('hostmask')}}
{%- endmacro -%}

{% macro ip_w_netm(ip_net) -%}
{{ ip_net|ipaddr('network') }} {{ ip_net|ipaddr('netmask') }}
{%- endmacro -%}

{% macro ip_w_pfxlen(ip_net) -%}
{{ ip_net|ipaddr('network/prefix') }}
{%- endmacro -%}

 

  1. 导入整个模板并将其分配给变量。宏是变量的属性。

    imp_ipfn_way1.j2

    {% import 'macros/ip_funcs.j2' as ipfn %}
    
    {{ ipfn.ip_w_wc('10.0.0.0/24') }}
    {{ ipfn.ip_w_netm('10.0.0.0/24') }}
    {{ ipfn.ip_w_pfxlen('10.0.0.0/24') }}
    
  2. 将特定宏导入当前命名空间。

    imp_ipfn_way2.j2

    {% from 'macros/ip_funcs.j2' import ip_w_wc, ip_w_pfxlen %}
    
    {{ ip_w_wc('10.0.0.0/24') }}
    {{ ip_w_pfxlen('10.0.0.0/24') }}
    
  3. 将特定宏导入当前命名空间并为其赋予别名。

    imp_ipfn_way3

    {% from 'macros/ip_funcs.j2' import ip_w_wc as ipwild %}
    
    {{ ipwild('10.0.0.0/24') }}
    

您还可以将 2 与 3 结合起来:

imp_ipfn_way2_3

{% from 'macros/ip_funcs.j2' import ip_w_wc as ipwild, ip_w_pfxlen %}

{{ ipwild('10.0.0.0/24') }}
{{ ip_w_pfxlen('10.0.0.0/24') }}

 

我的建议是始终使用1这迫使您通过显式命名空间访问宏。方法23风险与当前命名空间中定义的变量和宏发生冲突。正如 Jinja 中经常出现的情况一样,显式优于隐式。

缓存和上下文变量

导入被缓存,这意味着它们在每次后续使用时都会非常快速地加载。这是要付出代价的,即导入的模板无法访问导入它们的模板中的变量。

这意味着默认情况下,您无法访问从另一个文件导入的宏内部的上下文中传递的变量。

相反,您必须构建宏,以便它们仅依赖于显式传递给它们的值。

为了说明这一点,我编写了两个版本的宏命名def_if_desc,一个尝试访问可用于模板导入的变量。另一个宏依赖于通过值显式传递给它的字典。

两个版本都使用以下数据:

default_desc.yaml

interfaces:
 Ethernet10:
   role: desktop
  • 版本访问模板变量:

    macros/def_desc_ctxvars.j2
    
    {% macro def_if_desc(ifname) -%}
    Unused port, dedicated to {{ interfaces[ifname].role }} devices
    {%- endmacro -%}
    im_defdesc_vars.j2
    
    {% import 'macros/def_desc_ctxvars.j2' as desc -%}
    
    {{ desc.def_if_desc('Ethernet10') }}

     

    当我尝试渲染时,im_defdesc_vars.j2我得到以下回溯:

    ...(cut for brevity)
      File "F:\projects\j2-tutorial\templates\macros\def_desc_ctxvars.j2", line 2, in template
        Unused port, dedicated to {{ interfaces[ifname].descri }} devices
      File "F:\projects\j2-tutorial\venv\lib\site-packages\jinja2\environment.py", line 452, in getitem
        return obj[argument]
    jinja2.exceptions.UndefinedError: 'interfaces' is undefined
    

    您可以看到 Jinja 抱怨它无法访问interfaces这和我们预期的一样。

  • 通过导入模板显式传递的字典的版本访问键。

    default_desc.j2
    
    {% macro def_if_desc(intf_data) -%}
    Unused port, dedicated to {{ intf_data.role }} devices
    {%- endmacro -%}
    im_defdesc.j2
    
    {% import 'macros/default_desc.j2' as desc -%}
    
    {{ desc.def_if_desc(interfaces['Ethernet10']) }}

     

    这渲染得很好:

    Unused port, dedicated to desktop devices
    

希望现在您可以看到导入宏时的默认行为。由于上下文中的变量值可以随时更改,Jinja 引擎无法缓存它们,我们也不允许从宏中访问它们。

禁用宏缓存

但是,如果出于某种原因您认为允许宏访问上下文变量是个好主意,您可以使用with context传递给import语句的附加参数来更改默认行为。

Note: This will automatically disable caching.

为了完整起见,这是我们如何“修复”失败的宏:

macros/def_desc_ctxvars.j2

{% macro def_if_desc(ifname) -%}
Unused port, dedicated to {{ interfaces[ifname].role }} devices
{%- endmacro -%}
im_defdesc_vars_wctx.j2

{% import 'macros/def_desc_ctxvars.j2' as desc with context -%}

{{ desc.def_if_desc('Ethernet10') }}

 

现在它起作用了:

Unused port, dedicated to devices

就个人而言,我不认为import一起使用是一个好主意with context。从单独的文件中导入宏的全部意义在于允许它们在其他模板中使用并利用缓存。可能有数百个,如果不是数千个,并且一旦您使用with context缓存就消失了。

我还可以在依赖于从模板上下文访问变量的宏中看到一些非常微妙的错误。

为了安全起见,我会说坚持标准导入并始终使用命名空间,例如

 

{% import 'macros/ip_funcs.j2' as ipfn %}

 

结论

我们了解了两种 Jinja 构造,它们可以帮助我们管理随着模板大小增加而出现的复杂性。通过利用importinclude语句,我们可以提高可重用性并使我们的模板更易于维护。我希望包含的示例向您展示了如何使用这些知识来使您的模板集合更好地组织和更容易理解。

这是我在以下情况下使用的内容的快速摘要:

 

Import

Include
目的 从其他模板导入宏 渲染其他模板并插入结果
上下文变量 不可访问(默认) 无障碍
适合 创建共享宏库 将模板拆分为逻辑单元和常用片段

我希望你发现这篇文章很有用,并期待更多。下一篇文章将继续讨论通过模板继承来组织模板的方法。敬请关注!

参考

posted @ 2022-04-04 16:55  DaisyLinux  阅读(894)  评论(0编辑  收藏  举报