控制结构

在 Jinja2 中,循环和条件被称为控制结构,因为它们会影响程序的流程。{%控制结构使用由和%}字符包围的块。

循环

我们首先要看的结构是循环。

Jinja2 作为一种模板语言不需要广泛的循环类型选择,所以我们只得到for循环。

For 循环以 开头{% for my_item in my_collection %}和结尾{% endfor %}这与您在 Python 中循环迭代的方式非常相似。

my_item是一个循环变量,它将在我们遍历元素时获取值。并且my_collection是持有对迭代集合的引用的变量的名称。

在循环体内部,我们可以my_item在其他控制结构中使用变量,比如if条件,或者简单地使用{{ my_item }}语句显示它。

好的,但是你会在哪里使用你问的循环?在您的模板中使用单个变量在大多数情况下都可以正常工作,但您可能会发现引入层次结构和循环将有助于抽象您的数据模型。

例如,前缀列表或 ACL 由许多行组成。将这些行表示为单个变量是没有意义的。

最初,您可以使用每行一个变量对特定前缀列表进行建模,如下所示:

PL_AS_65003_IN_line1: "permit 10.96.0.0/24"
PL_AS_65003_IN_line2: "permit 10.97.11.0/24"
PL_AS_65003_IN_line3: "permit 10.99.15.0/24"
PL_AS_65003_IN_line4: "permit 10.100.5.0/25"
PL_AS_65003_IN_line5: "permit 10.100.6.128/25"

可以在以下模板中使用:

# Configuring Prefix List
ip prefix-list PL_AS_65003_IN
 {{ PL_AS_65003_IN_line1 }}
 {{ PL_AS_65003_IN_line2 }}
 {{ PL_AS_65003_IN_line3 }}
 {{ PL_AS_65003_IN_line4 }}
 {{ PL_AS_65003_IN_line5 }}

渲染结果:

# Configuring Prefix List
ip prefix-list PL_AS_65003_IN
 permit 10.96.0.0/24
 permit 10.97.11.0/24
 permit 10.99.15.0/24
 permit 10.100.5.0/25
 permit 10.100.6.128/25

这种方法虽然有效,但也存在一些问题。

如果我们想在前缀列表中有更多行,我们必须创建另一个变量,然后再创建一个,以此类推。我们不仅必须将这些新项目添加到我们的数据结构中,模板还必须单独包含所有这些新变量。这是不可维护的,消耗大量时间并且很容易出错。

有一个更好的方法,考虑下面的数据结构:

PL_AS_65003_IN:
  - permit 10.96.0.0/24
  - permit 10.97.11.0/24
  - permit 10.99.15.0/24
  - permit 10.100.5.0/25
  - permit 10.100.6.128/25

以及模板渲染前缀列表配置:

# Configuring Prefix List
ip prefix-list PL_AS_65003_IN
{%- for line in PL_AS_65003_IN %}
 {{ line -}}
{% endfor %}

 

渲染后:

# Configuring Prefix List
ip prefix-list PL_AS_65003_IN
 permit 10.96.0.0/24
 permit 10.97.11.0/24
 permit 10.99.15.0/24
 permit 10.100.5.0/25
 permit 10.100.6.128/25

 

如果您仔细观察,您会发现这本质上是对同一事物进行建模,即带有多个条目的前缀列表。但是通过使用列表,我们清楚地说明了我们的意图。即使在视觉上,您也可以立即看出所有缩进的行都属于 PL_AS_65003_IN。

在这里添加前缀列表很简单,我们只需要在块中添加一个新行。此外,我们的模板根本不需要更改。如果我们使用循环来迭代,就像我们在这里所做的那样,遍历这个列表,那么如果我们重新运行渲染,新的行将被拾取。小小的改变,但让事情变得容易多了。

您可能已经注意到这里仍有改进的空间。前缀列表的名称在前缀列表定义和我们的for循环中是硬编码的。不要害怕,这是我们很快会改进的。

循环遍历字典

现在让我们看看如何遍历字典。我们将再次使用for循环构造,记住,这就是我们所拥有的!

我们可以使用与迭代列表元素相同的语法,但这里我们将迭代字典键要检索分配给键的值,我们需要使用下标,即[]符号。

使用字典而不是列表的一个优点是我们可以使用元素的名称作为参考,这使得检索对象及其值变得更加容易。

假设我们使用 list 来表示我们的接口集合:

interfaces:
  - Ethernet1:
      description: leaf01-eth51
      ipv4_address: 10.50.0.0/31
  - Ethernet2:
      description: leaf02-eth51
      ipv4_address: 10.50.0.2/31

 

没有简单的方法来检索Ethernet2条目。我们要么必须遍历所有元素并进行键名比较,要么必须求助于高级过滤器。

需要注意的一件事(希望这一点越来越明显)是我们需要花一些时间对数据进行建模,以便于使用。这是您在第一次尝试时很少会做对的事情,所以不要害怕尝试和迭代。

按照我们的示例,我们可以将数据保存在分配给interfaces字典中键的各个接口上,而不是将它们放在列表中

interfaces:
  Ethernet1:
    description: leaf01-eth51
    ipv4_address: 10.50.0.0/31
  Ethernet2:
    description: leaf02-eth51
    ipv4_address: 10.50.0.2/31

 

现在我们可以像这样在模板中访问这些数据:

{% for intf in interfaces -%}
interface {{ intf }}
 description {{ interfaces[intf].description }}
 ip address {{ interfaces[intf].ipv4_address }}
{% endfor %}

 

给我们最终结果:

interface Ethernet1
 description leaf01-eth51
 ip address 10.50.0.0/31
interface Ethernet2
 description leaf02-eth51
 ip address 10.50.0.2/31

 

这里intf指的是Ethernet1Ethernet2键。要访问每个接口的属性,我们需要使用interfaces[intf]符号。

还有另一种迭代字典的方法,我个人更喜欢。items()我们可以通过使用方法同时检索键和它的值。

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

 

最终结果是相同的,但通过使用items()方法,我们简化了对属性的访问。如果您想递归地迭代深度嵌套的字典,这一点就变得尤为重要。

我还承诺展示如何改进前缀列表示例,这就是它的items()用武之地。

我们通过使每个前缀列表名称成为字典中的键来对我们的数据结构进行小修改prefix_lists

prefix_lists:
  PL_AS_65003_IN:
    - permit 10.96.0.0/24
    - permit 10.97.11.0/24
    - permit 10.99.15.0/24
    - permit 10.100.5.0/25
    - permit 10.100.6.128/25

 

我们现在添加外循环迭代字典中的键值对:

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

 

渲染给我们同样的结果:

# Configuring Prefix List
ip prefix-list PL_AS_65003_IN
 permit 10.96.0.0/24
 permit 10.97.11.0/24
 permit 10.99.15.0/24
 permit 10.100.5.0/25
 permit 10.100.6.128/25

 

在这里,不再有对前缀列表名称的硬编码引用!如果您需要另一个前缀列表,您只需将其添加到字典中,它就会被我们的循环prefix_lists自动拾取。for

注意:如果您使用的是 Python < 3.6 的版本,则不订购字典。这意味着您记录数据的顺序可能与模板内处理项目的顺序不同。

如果您依赖于它们被记录的顺序,您应该collections.OrderedDict在 Python 脚本中使用 Jinja2 时使用,或者您可以dictsort在模板中应用过滤器以按键或值对字典进行排序。

按键排序:

{% for k, v in my_dict | dictsort -%}

 

按值排序:

{% for k, v in my_dict | dictsort(by='value') -%}

 

这就结束了 Jinja2 模板中循环的基础知识。上述用例应满足您 95% 的需求。

如果您正在寻找与循环相关的一些高级功能的讨论,请放心,我也会写这些内容。我决定在本教程的最后几章留下更深入的 Jinja2 主题,并专注于让您更快地提高工作效率的核心内容。

条件和测试

现在我们已经完成了循环,是时候继续讨论条件了。

Jinja2 实现了一种条件语句,即if语句。对于分支,我们可以使用elifand else

Jinja2 中的条件可以以几种不同的方式使用。现在,我们将看看一些用例以及它们如何与其他语言特性相结合。

比较

我们首先要看的是将值与条件进行比较,这些使用==, !=, >, >=, <, <=运算符。这些都是相当标准的,但我还是会展示一些例子。

使用比较的一种常见情况是根据已安装操作系统的版本或供应商改变命令语法。例如,前段时间 Arista 由于诉讼而不得不更改一些命令,我​​们可以使用一个简单的 if 语句来确保我们的模板适用于所有 EOS 版本:

使用 EOS 4.19 的主机模板、变量和渲染模板:

(venv) przemek@quasar:~/nauto/jinja/python$ python j2_render.py \
-t templates/eos-ver.j2 -f vars/eos-ver-419.yml -d yaml
###############################################################################
# Loaded template: templates/eos-ver.j2
###############################################################################

hostname {{ hostname }}
{% if eos_ver >= 4.22 -%}
Detected EOS ver {{ eos_ver }}, using new command syntax.
{% else -%}
Detected EOS ver {{ eos_ver }}, using old command syntax.
{% endif %}

###############################################################################
# Render variables
###############################################################################

eos_ver: 4.19
hostname: arista_old_eos

###############################################################################
# Rendered template
###############################################################################

hostname arista_old_eos
Detected EOS ver 4.19, using old command syntax.

 

运行 EOS 4.22 的设备也是如此:

(venv) przemek@quasar:~/nauto/jinja/python$ python j2_render.py \
-t templates/eos-ver.j2 -f vars/eos-ver-422.yml -d yaml
###############################################################################
# Loaded template: templates/eos-ver.j2
###############################################################################

hostname {{ hostname }}
{% if eos_ver >= 4.22 -%}
Detected EOS ver {{ eos_ver }}, using new command syntax.
{% else -%}
Detected EOS ver {{ eos_ver }}, using old command syntax.
{% endif %}

###############################################################################
# Render variables
###############################################################################

eos_ver: 4.22
hostname: arista_new_eos

###############################################################################
# Rendered template
###############################################################################

hostname arista_new_eos
Detected EOS ver 4.22, using new command syntax.

 

非常简单,但非常有用。我们所做的只是检查记录的 EOS 版本是否小于或大于/等于 4.22,这足以确保正确的语法使其进入配置。

为了通过比较显示更复杂的分支,我在这里提供了支持多种路由协议的模板示例,其中仅为每个设备生成相关配置。

首先我们为主机定义一些数据。

运行 BGP 的设备:

hostname: router-w-bgp
routing_protocol: bgp

interfaces:
  Loopback0: 
    ip: 10.0.0.1
    mask: 32

bgp:
  as: 65001

 

运行 OSPF 的设备:

hostname: router-w-ospf
routing_protocol: ospf

interfaces:
  Loopback0:
    ip: 10.0.0.2
    mask: 32

ospf:
  pid: 1

 

仅具有默认路由的设备:

hostname: router-w-defgw

interfaces:
  Ethernet1:
    ip: 10.10.0.10
    mask: 24

default_nh: 10.10.0.1

 

然后我们使用带有分支的条件创建一个模板。可以根据需要轻松添加其他协议选择。

hostname {{ hostname }}
ip routing

{% for intf, idata in interfaces.items() -%}
interface {{ intf }}
  ip address {{ idata.ip }}/{{ idata.mask }}
{%- endfor %}

{% if routing_protocol == 'bgp' -%}
router bgp {{ bgp.as }}
  router-id {{ interfaces.Loopback0.ip }}
  network {{ interfaces.Loopback0.ip }}/{{ interfaces.Loopback0.mask }}
{%- elif routing_protocol == 'ospf' -%}
router ospf {{ ospf.pid }}
  router-id {{ interfaces.Loopback0.ip }}
  network {{ interfaces.Loopback0.ip }}/{{ interfaces.Loopback0.mask }} area 0
{%- else -%}
  ip route 0.0.0.0/0 {{ default_nh }}
{%- endif %}

 

所有设备的渲染结果:

hostname router-w-bgp
ip routing

interface Loopback0
  ip address 10.0.0.1/32

router bgp 65001
  router-id 10.0.0.1
  network 10.0.0.1/32
hostname router-w-ospf
ip routing

interface Loopback0
  ip address 10.0.0.2/32

router ospf 1
  router-id 10.0.0.2
  network 10.0.0.2/32 area 0
hostname router-w-defgw
ip routing

interface Ethernet1
  ip address 10.10.0.10/24

ip route 0.0.0.0/0 10.10.0.1

 

有了它,一个模板支持 3 种不同的配置选项,非常酷。

逻辑运算符

没有逻辑运算符,条件的实现是不完整的。Jinja2 以

andor and not

形式提供这些ornot

这里没什么好说的,所以这里只是一个简短的例子,展示了所有这些在行动中的作用:

(venv) przemek@quasar:~/nauto/jinja/python$ python j2_render.py \
-t templates/if-logic-ops.j2 -f vars/if-logic-ops.yml
###############################################################################
# Loaded template: templates/if-logic-ops.j2
###############################################################################

{% if x and y -%}
Both x and y are True. x: {{ x }}, y: {{ y }}
{%- endif %}

{% if x or z -%}
At least one of x and z is True. x: {{ x }}, z: {{ z }}
{%- endif %}

{% if not z -%}
We see that z is not True. z: {{ z }}
{%- endif %}

###############################################################################
# Render variables
###############################################################################

x: true
y: true
z: false

###############################################################################
# Rendered template
###############################################################################

Both x and y are True. x: True, y: True

At least one of x and z is True. x: True, z: False

We see that z is not True. z: False

真实性

这是查看不同变量类型及其真实性的好地方。与 Python、字符串、列表、字典等的情况一样,如果变量不为空,则它们的计算结果为 True。对于空值,评估结果为 False。

我创建了一个示例来说明非空和空、字符串、列表和字典的真实性:

(venv) przemek@quasar:~/nauto/jinja/python$ python j2_render.py \
-t templates/if-types-truth.j2 -f vars/if-types-truth.yml
###############################################################################
# Loaded template: templates/if-types-truth.j2
###############################################################################

{% macro bool_eval(value) -%}
{% if value -%}
True
{%- else -%}
False
{%- endif %}
{%- endmacro -%}

My one element list has bool value of: {{ bool_eval(my_list) }}
My one key dict has bool value of: {{ bool_eval(my_dict) }}
My short string has bool value of: {{ bool_eval(my_string) }}

My empty list has bool value of: {{ bool_eval(my_list_empty) }}
My empty dict has bool value of: {{ bool_eval(my_dict_empty) }}
My empty string has bool value of: {{ bool_eval(my_string_empty) }}

###############################################################################
# Render variables
###############################################################################

{
    "my_list": [
        "list-element"
    ],
    "my_dict": {
        "my_key": "my_value"
    },
    "my_string": "example string",
    "my_list_empty": [],
    "my_dict_empty": {},
    "my_string_empty": ""
}

###############################################################################
# Rendered template
###############################################################################

My one element list has bool value of: True
My one key dict has bool value of: True
My short string has bool value of: True

My empty list has bool value of: False
My empty dict has bool value of: False
My empty string has bool value of: False

 

我个人建议不要测试非布尔类型的真实性。没有多少情况下这可能有用,它可能会使您的意图不明显。如果您只是想检查变量是否存在is defined,那么我们将很快看到的测试通常是更好的选择。

测试

Jinja2 中的测试与变量一起使用并返回 True 或 False,具体取决于值是否通过测试。要使用此功能is,请在变量后添加和测试名称。

最有用的测试是defined我已经提到的。该测试仅检查给定变量是否已定义,即渲染引擎是否可以在接收到的数据中找到它。

检查是否定义了变量是我在大多数模板中使用的。请记住,默认情况下未定义的变量只会计算为空字符串。通过检查变量是否在其预期用途之前定义,您可以确保您的模板在渲染期间失败。如果没有这个测试,您可能会以不完整的文档结束,并且没有迹象表明有什么不对劲。

我发现方便的另一类测试用于检查变量的类型。某些操作要求两个操作数的类型相同,如果它们不是 Jinja2 将抛出错误。这适用于比较数字或迭代列表和字典之类的事情。

boolean- 检查变量是否为布尔值
integer- 检查变量是否为整数
float- 检查变量是否为浮点数
number- 检查变量是否为数字,整数和浮点数均返回 True
string- 检查变量是否为字符串
mapping- 检查变量是否为映射, 即字典
iterable- 检查变量是否可以迭代,将匹配字符串、列表、字典等
sequence- 检查变量是否为序列

以下是应用了这些测试的一些变量的示例:

(venv) przemek@quasar:~/nauto/jinja/python$ python j2_render.py \
-t templates/tests-type.j2 -f vars/tests-type.yml
###############################################################################
# Loaded template: templates/tests-type.j2
###############################################################################

{{ hostname }} is an iterable: {{ hostname is iterable }}
{{ hostname }} is a sequence: {{ hostname is sequence }}
{{ hostname }} is a string: {{ hostname is string }}

{{ eos_ver }} is a number: {{ eos_ver is number }}
{{ eos_ver }} is an integer: {{ eos_ver is integer }}
{{ eos_ver }} is a float: {{ eos_ver is float }}

{{ bgp_as }} is a number: {{ bgp_as is number }}
{{ bgp_as }} is an integer: {{ bgp_as is integer }}
{{ bgp_as }} is a float: {{ bgp_as is float }}

{{ interfaces }} is an iterable: {{ interfaces is iterable }}
{{ interfaces }} is a sequence: {{ interfaces is sequence }}
{{ interfaces }} is a mapping: {{ interfaces is mapping }}

{{ dns_servers }} is an iterable: {{ dns_servers is iterable }}
{{ dns_servers }} is a sequence: {{ dns_servers is sequence }}
{{ dns_servers }} is a mapping: {{ dns_servers is mapping }}

###############################################################################
# Render variables
###############################################################################

{
    "hostname": "sw-office-lon-01",
    "eos_ver": 4.22,
    "bgp_as": 65001,
    "interfaces": {
        "Ethernet1": "Uplink to core"
    },
    "dns_servers": [
        "1.1.1.1",
        "8.8.4.4",
        "8.8.8.8"
    ]
}

###############################################################################
# Rendered template
###############################################################################

sw-office-lon-01 is an iterable: True
sw-office-lon-01 is a sequence: True
sw-office-lon-01 is a string: True

4.22 is a number: True
4.22 is an integer: False
4.22 is a float: True

65001 is a number: True
65001 is an integer: True
65001 is a float: False

{'Ethernet1': 'Uplink to core'} is an iterable: True
{'Ethernet1': 'Uplink to core'} is a sequence: True
{'Ethernet1': 'Uplink to core'} is a mapping: True

['1.1.1.1', '8.8.4.4', '8.8.8.8'] is an iterable: True
['1.1.1.1', '8.8.4.4', '8.8.8.8'] is a sequence: True
['1.1.1.1', '8.8.4.4', '8.8.8.8'] is a mapping: False

 

您可能已经注意到,其中一些测试可能看起来有点模棱两可。例如,要测试变量是否是列表,仅检查它是序列还是可迭代是不够的。字符串也是序列和可迭代对象。字典也是如此,即使 vanilla Python 将它们分类为 Iterable 和 Mapping 而不是 Sequence:

>>> from collections.abc import Iterable, Sequence, Mapping
>>>
>>> interfaces = {"Ethernet1": "Uplink to core"}
>>>
>>> isinstance(interfaces, Iterable)
True
>>> isinstance(interfaces, Sequence)
False
>>> isinstance(interfaces, Mapping)
True

 

那么这一切意味着什么呢?好吧,我建议对每种类型的变量进行以下测试:

  • Number、Float、Integer - 这些都按预期工作,因此请选择适合您用例的任何内容。

  • 字符串 - 使用string测试就足够了:

{{ my_string is string }}

  • 字典 - 使用mapping测试就足够了:

{{ my_dict is mapping }}

  • 列表 - 这是一个艰难的,完整的检查应该测试变量是否是一个序列,但同时它不能是一个映射或字符串:

{{ my_list is sequence and my list is not mapping and my list is not string }}

在某些情况下,我们知道字典或字符串不太可能出现,因此我们可以通过摆脱mappingstring测试来缩短检查:

{{ my_list is sequence and my list is not string }}
{{ my_list is sequence and my list is not mapping }}

 

有关可用测试的完整列表,请点击参考资料中的链接。

环路滤波

我想简要介绍的最后一件事是循环过滤和in运算符。

环路过滤正如其名称所暗示的那样。它允许您使用if带有循环的语句for来跳过您不感兴趣的元素。

例如,我们可以遍历包含接口的字典并仅处理具有 IP 地址的接口:

(venv) przemek@quasar:~/nauto/jinja/python$ python j2_render.py \
-t templates/loop-filter.j2 -f vars/loop-filter.yml
###############################################################################
# Loaded template: templates/loop-filter.j2
###############################################################################

=== Interfaces with assigned IPv4 addresses ===

{% for intf, idata in interfaces.items() if idata.ipv4_address is defined -%}
{{ intf }} - {{ idata.description }}: {{ idata.ipv4_address }}
{% endfor %}

###############################################################################
# Render variables
###############################################################################

{
    "interfaces": {
        "Loopback0": {
            "description": "Management plane traffic",
            "ipv4_address": "10.255.255.34/32"
        },
        "Management1": {
            "description": "Management interface",
            "ipv4_address": "10.10.0.5/24"
        },
        "Ethernet1": {
            "description": "Span port - SPAN1"
        },
        "Ethernet2": {
            "description": "PortChannel50 - port 1"
        },
        "Ethernet51": {
            "description": "leaf01-eth51",
            "ipv4_address": "10.50.0.0/31"
        },
        "Ethernet52": {
            "description": "leaf02-eth51",
            "ipv4_address": "10.50.0.2/31"
        }
    }
}

###############################################################################
# Rendered template
###############################################################################

=== Interfaces with assigned IPv4 addresses ===

Loopback0 - Management plane traffic: 10.255.255.34/32
Management1 - Management interface: 10.10.0.5/24
Ethernet51 - leaf01-eth51: 10.50.0.0/31
Ethernet52 - leaf02-eth51: 10.50.0.2/31

 

如您所见,我们总共有 6 个接口,但其中只有 4 个分配了 IP 地址。is defined测试添加到循环中,我们过滤掉没有 IP 地址的接口。

当迭代从设备返回的大负载时,循环过滤可能特别强大。在某些情况下,您可以忽略大部分元素并专注于感兴趣的事物。

In操作员

in放置在两个值之间的运算符可用于检查左侧的值是否包含在右侧的值中。您可以使用它来测试元素是否出现在列表中,或者键是否存在于字典中。

运算符的明显用例in是检查我们感兴趣的东西是否仅存在于集合中,我们不一定需要检索该项目。

查看前面的示例,我们可以检查 Loopback0 是否在列表接口中,如果是,我们将使用它来获取管理平面数据包,如果不是,我们将使用 Management1 接口。

模板:

{% if 'Loopback0' in interfaces -%}
sflow source-interface Loopback0
snmp-server source-interface Loopback0
ip radius source-interface Loopback0
{%- else %}
sflow source-interface Management1
snmp-server source-interface Management1
ip radius source-interface Management1
{% endif %}

 

渲染结果:

sflow source-interface Loopback0
snmp-server source-interface Loopback0
ip radius source-interface Loopback0

 

请注意,即使interfaces是一个包含大量数据的字典,我们也没有对其进行迭代或检索任何键。我们只想知道Loopback0钥匙的存在。

老实说,上面的模板可以进行一些调整,我们基本上复制了 3 行配置和硬编码的接口名称。这不是一个很好的做法,我将在下一篇文章中向您展示我们如何在这里进行改进。

至此,我们已经结束了 Jinja2 教程的第 2 部分。接下来我将介绍空格,以便您可以使您的文档看起来恰到好处,我们将继续研究语言功能。我希望你在这里学到了一些有用的东西,并且回来获得更多!

参考