Jinja2 过滤器概述

让我们直接进入。 Jinja2 过滤器是我们用来转换变量中保存的数据的东西。|我们通过在变量后放置管道符号和过滤器名称来应用过滤器。

过滤器可以更改源数据的外观和格式,甚至可以生成从输入值派生的新数据。重要的是原始数据被转换的结果替换,这就是最终呈现的模板。

下面是一个示例,展示了一个简单的过滤器的作用:

模板:

First name: {{ first_name | capitalize }}

数据:

first_name: przemek

结果:

First name: Przemek

我们将first_name变量传递给capitalize过滤器。正如过滤器的名称所暗示的那样,变量保存的字符串最终将大写。这正是我们所看到的。很酷,对吧?

将过滤器视为将 Jinja2 变量作为参数的函数可能会有所帮助,与标准 Python 函数的唯一区别是我们使用的语法。

Python 等价物capitalize看起来像这样:

def capitalize(word): return word.capitalize() first_name = "przemek" print("First name: {}".format(capitalize(first_name))) 

太好了,你说。但我怎么知道这capitalize是一个过滤器?它从哪里来的?

这里没有魔法。有人必须对所有这些过滤器进行编码,并让它们可供我们使用。Jinja2 带有许多有用的过滤器,capitalize就是其中之一。

所有内置过滤器都记录在 Jinja2 官方文档中。我在参考文献中包含了链接,在这篇文章的后面,我将展示一些在我看来更有用的过滤器示例。

多个参数

我们不仅限于简单的过滤器,例如capitalize一些过滤器可以在括号中使用额外的参数。这些可以是关键字或位置参数。

下面是一个采用额外参数的过滤器示例。

模板:

ip name-server {{ name_servers | join(" ") }}

数据:

name_servers:
  - 1.1.1.1
  - 8.8.8.8
  - 9.9.9.9
  - 8.8.4.4

结果:

ip name-server 1.1.1.1 8.8.8.8 9.9.9.9 8.8.4.4

过滤器通过将列表的元素与空格作为分隔符粘合在一起来获取join存储的列表并创建一个字符串。name_servers分隔符是我们在括号中提供的参数,我们可以根据需要使用不同的参数。

您应该参考文档以了解哪些参数(如果有)可用于给定过滤器。大多数过滤器使用合理的默认值,并且不需要显式指定所有参数。

链接过滤器

我们已经看到了基本的过滤器用法,但我们可以做得更多。我们可以将过滤器链接在一起。这意味着可以一次使用多个过滤器,每个过滤器用管道分隔|

Jinja 从左到右应用链式过滤器。来自最左边过滤器的值被送入下一个过滤器,并重复该过程直到没有更多过滤器。只有最终结果才会出现在渲染模板中。

让我们看看它是如何工作的。

数据:

scraped_acl:
  - "   10 permit ip 10.0.0.0/24 10.1.0.0/24"
  - "   20 deny ip any any"

模板

{{ scraped_acl | first | trim }}

结果

10 permit ip 10.0.0.0/24 10.1.0.0/24

我们传递了包含两个要first过滤的项目的列表。这从列表中返回了第一个元素,并将其交给trim过滤器删除了前导空格。

最终结果是 line 10 permit ip 10.0.0.0/24 10.1.0.0/24

过滤器链接是一项强大的功能,它允许我们一次执行多个转换。另一种方法是存储中间结果,这会降低可读性并且不会那么优雅。

附加过滤器和自定义过滤器

尽管它们很棒,但内置过滤器非常通用,许多用例需要更具体的过滤器。这就是为什么像 Ansible 或 Salt 这样的自动化框架提供了许多额外的过滤器来覆盖广泛的场景。

在这些框架中,您会发现过滤器可以转换 IP 对象、在 YAML/Json 中显示数据,甚至应用正则表达式,仅举几例。参考资料中,您可以找到每个框架中可用过滤器的文档链接。

最后,您可以自己创建新的过滤器!Jinja2 提供了添加自定义过滤器的钩子。这些只是 Python 函数,因此如果您在编写 Python 函数之前也可以编写自己的过滤器!

上述自动化框架也支持自定义过滤器,编写它们的过程类似于 vanilla Jinja2。您再次需要编写 Python 函数,然后给定工具的文档将向您显示将模块注册为过滤器所需的步骤。

为什么要使用过滤器?

没有工具能很好地解决所有问题。有些工具是寻找问题的解决方案。那么,为什么要使用 Jinja2 过滤器呢?

与大多数模板语言一样,Jinja 的创建考虑了 Web 内容。虽然数据以标准化格式存储在数据库中,但我们在向用户显示文档时经常需要对其进行转换。这就是像 Jinja 这样的语言及其过滤器可以随时随地修改数据的呈现方式,而无需触及后端。这就是过滤器的卖点。

下面是我个人的看法,为什么我认为 Jinja2 过滤器是该语言的一个很好的补充:

1.它们允许非程序员执行简单的数据转换。

这适用于普通过滤器以及自动化框架提供的额外过滤器。例如,网络工程师知道他们的 IP 地址,他们可能希望在没有任何编程知识的情况下在模板中对其进行操作。过滤器来拯救!

2. 你会得到可预测的结果。

如果你使用一般可用的过滤器,任何有一些 Jinja2 经验的人都会知道他们在做什么。这使人们在查看其他人编写的模板时能够加快速度。

3. 过滤器维护良好且经过测试。

内置过滤器以及自动化框架提供的过滤器被很多人广泛使用。这使您对他们给出正确的结果并且没有很多错误充满信心。

4. 最好的代码是完全没有代码。

在您向程序添加数据转换操作或创建新过滤器的那一刻,您将永远对代码负责。任何错误、功能请求和测试都将在解决方案的整个生命周期内出现。在学习时写尽可能多的东西,但尽可能在生产中使用已经可用的解决方案。

什么时候不使用过滤器?

过滤器非常强大,可以为我们节省大量时间。但权力越大,责任越大。过度使用过滤器,您最终会得到难以理解和维护的模板。

你知道那些没有人,包括你自己,能在几个月后理解的聪明的一班人吗?通过链接大量过滤器很容易进入这些情况,尤其是那些接受多个参数的过滤器。

我使用以下启发式方法来帮助我确定我所做的是否太复杂:

  • 我写的东西是我理解的极限吗?
  • 我是不是觉得我刚刚写的很聪明?
  • 我是否以一种起初看起来并不明显的方式使用了许多链式过滤器?

如果您对上述至少一项的回答是肯定的,那么您可能正在处理“为自己好而太聪明”的情况。您的用例可能没有更简单的解决方案,但您可能需要进行重构。如果您不确定是否是这种情况,最好询问您的同事或咨询社区。

为了向您展示事情会变得多么糟糕,这是我几年前写的 Jinja2 行的示例。这些使用 Ansible 提供的过滤器,它变得非常复杂,以至于我不得不定义中间变量。

看看它并尝试弄清楚它做了什么,更重要的是,它是如何做到的。

模板,为了简洁而删减:

{% for p in ibgp %}
{%  set jq = "[?name=='" + p.port + "'].{ myip: ip, peer: peer }" %}
{%  set el = ports | json_query(jq) %}
{%  set peer_ip = hostvars[el.0.peer] | json_query('ports[*].ip') | ipaddr(el.0.myip) %}
...
{%  endfor %}

 

与模板一起使用的示例数据:

ibgp:
  - { port: Ethernet1 }
  - { port: Ethernet2 }
..
ports:
  - { name: Ethernet1, ip: "10.0.12.1/24", speed: 1000full, desc: "vEOS-02

 

这里有很多东西要解压。在第一行中,我将查询字符串分配给变量作为字符转义问题的解决方法。在第二行中,我使用json_query来自第一行变量的参数应用过滤器,结果存储在另一个辅助变量中。最后,在第三行中,我应用了两个链式过滤器json_queryipaddr.

这三行的最终结果应该是在给定接口上找到的 BGP 对等体的 IP 地址。

我相信你会同意我的看法,这很糟糕。这个解决方案在我之前提到的启发式方法旁边是否有任何标记?是的!他们三个!这是重构的主要候选者。

在这种情况下,我们通常可以做两件事:

  • 在调用渲染的上层对数据进行预处理,例如 Python、Ansible 等。
  • 编写自定义过滤器。
  • 修改数据模型,看看是否可以简化。

在这种情况下,我选择了选项 2,我编写了自己的过滤器,幸运的是,这是我们列表中的下一个主题。

编写自己的过滤器

正如我已经提到的,要编写自定义过滤器,您需要亲自动手并编写一些 Python 代码。不过不要害怕!如果你曾经写过一个带参数的函数,那么你已经得到了它所需要的一切。没错,我们不需要做太花哨的事情,任何普通的 Python 函数都可以成为过滤器。它只需要至少接受一个参数并且它必须返回一些东西。

这是我们将在 Jinja2 引擎中注册为过滤器的函数示例:

# hash_filter.py
import hashlib


def j2_hash_filter(value, hash_type="sha1"):
    """
    Example filter providing custom Jinja2 filter - hash

    Hash type defaults to 'sha1' if one is not specified

    :param value: value to be hashed
    :param hash_type: valid hash type
    :return: computed hash as a hexadecimal string
    """
    hash_func = getattr(hashlib, hash_type, None)

    if hash_func:
        computed_hash = hash_func(value.encode("utf-8")).hexdigest()
    else:
        raise AttributeError(
            "No hashing function named {hname}".format(hname=hash_type)
        )

    return computed_hash

 

在 Python 中,这是我们告诉 Jinja2 过滤器的方式:

# hash_filter_test.py
import jinja2
from hash_filter import j2_hash_filter

env = jinja2.Environment()
env.filters["hash"] = j2_hash_filter

tmpl_string = """MD5 hash of '$3cr3tP44$$': {{ '$3cr3tP44$$' | hash('md5') }}"""

tmpl = env.from_string(tmpl_string)

print(tmpl.render())

 


渲染结果:
MD5 hash of '$3cr3tP44$$': ec362248c05ae421533dd86d86b6e5ff

看那个!我们自己的过滤器!它的外观和感觉就像内置的 Jinja 过滤器,对吧?

它有什么作用?它公开了 Pythonhashlib库中的散列函数,以允许在 Jinja2 模板中直接使用散列。如果你问我,那就太好了。

简而言之,以下是创建自定义过滤器所需的步骤:

  1. 创建一个至少接受一个参数并返回一个值的函数。第一个参数始终是|符号前面的 Jinja 变量。括号中提供了后续参数(...)
  2. 在 Jinja2 环境中注册函数。在 Python 中,将您的函数插入到filters字典中,这是Environment对象的一个​​属性。键名是您希望调用过滤器的名称,在这里hash,值是您的功能。
  3. 您现在可以像使用任何其他 Jinja 过滤器一样使用您的过滤器。

使用 Ansible 自定义过滤器修复“太聪明”的解决方案

我们知道如何编写自定义过滤器,所以现在我可以向您展示我是如何用聪明的技巧替换模板的一部分的。

这是我完全荣耀的自定义过滤器:

# get_peer_info.py
import ipaddress


def get_peer_info(our_port_info, hostvars):
    peer_info = {"name": our_port_info["peer"]}
    our_net = ipaddress.IPv4Interface(our_port_info["ip"]).network
    peer_vars = hostvars[peer_info["name"]]

    for _, peer_port_info in peer_vars["ports"].items():
        if not peer_port_info["ip"]:
            continue
        peer_net_obj = ipaddress.IPv4Interface(peer_port_info["ip"])
        if our_net == peer_net_obj.network:
            peer_info["ip"] = peer_net_obj.ip
            break
    return peer_info


class FilterModule(object):

    def filters(self):
        return {
            'get_peer_info': get_peer_info
        } 

 

第一部分是你以前见过的,它是一个 Python 函数,它接受两个参数并返回一个值。当然,它比“聪明的”三行更长,但它的可读性要强得多。

这里有更多的结构,变量有有意义的名字,我可以马上知道它在做什么。更重要的是,我知道它是如何做到的,过程被分解成许多易于遵循的单独步骤。

Ansible 中的自定义过滤器

我的解决方案的第二部分与普通 Python 示例有点不同:

class FilterModule(object):

    def filters(self):
        return {
            'get_peer_info': get_peer_info
        } 

 

这就是你告诉 Ansible 你想get_peer_info注册为 Jinja2 过滤器的方式。

FilterModule使用一种名为 的方法创建名为的类filters此方法必须返回带有过滤器的字典。字典中的键是过滤器的名称,值是函数。我说的是过滤器而不是过滤器,因为您可以在一个文件中注册多个过滤器。如果您愿意,您可以选择为每个文件设置一个过滤器。

完成所有操作后,您需要将 Python 模块放入filter_plugins目录中,该目录应位于存储库的根目录中。有了这些,您就可以在 Ansible Playbooks 和 Jinja2 模板中使用您的过滤器。

您可以在下面看到我的剧本与模块相关的目录deploy_base.yml结构get_peer_info.py

.
├── ansible.cfg
├── deploy_base.yml
├── filter_plugins
│   └── get_peer_info.py
├── group_vars
    ...
├── hosts
├── host_vars
    ...
└── roles
    └── base

Jinja2 过滤器 - 使用示例

所有 Jinja2 过滤器都在官方文档中有详细记录,但我觉得其中一些可以使用更多示例。您将在下面找到我的主观选择以及一些评论和解释。

batch(value, linecount, fill_with=None)- 允许您将列表元素分组到多个存储桶中,每个存储桶最多包含n 个元素,其中n是我们指定的数字。可选地,我们还可以要求batch使用默认条目填充存储桶,以使所有存储桶的长度恰好为 n结果是列表列表。

我发现将项目分成固定大小的组很方便。

模板:

{% for i in  sflow_boxes|batch(2) %}
Sflow group{{ loop.index }}: {{ i | join(', ') }}
{% endfor %}

 

数据:

sflow_boxes:
 - 10.180.0.1
 - 10.180.0.2
 - 10.180.0.3
 - 10.180.0.4
 - 10.180.0.5

结果:

Sflow group1: 10.180.0.1, 10.180.0.2
Sflow group2: 10.180.0.3, 10.180.0.4
Sflow group3: 10.180.0.5

中央

center(value, width=80)- 通过添加空格填充在给定宽度的字段中居中值。在向报告添加格式时很方便。

模板:

{{ '-- Discovered hosts --' | center }}
{{ hosts | join('\n') }}

 

数据:

hosts:
 - 10.160.0.7
 - 10.160.0.9
 - 10.160.0.3

结果:

                             -- Discovered hosts --                             
10.160.0.7
10.160.0.9
10.160.0.15

默认

default(value, default_value='', boolean=False)- 如果未指定传递的变量,则返回默认值。用于防范未定义的变量。也可以用于我们想要设置为默认值的可选属性。

在下面的示例中,我们将接口放置在其配置的 vlan 中,或者如果未指定 vlan,我们默认将它们分配给 vlan 10。

模板:

{% for intf in interfaces %}
interface {{ intf.name }}
  switchport mode access
  switchport access vlan {{ intf.vlan | default('10') }}
{% endfor %}

 

数据:

interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
  - name: Ethernet4

结果:

interface Ethernet1
  switchport mode access
  switchport access vlan 50
interface Ethernet2
  switchport mode access
  switchport access vlan 50
interface Ethernet3
  switchport mode access
  switchport access vlan 10
interface Ethernet4
  switchport mode access
  switchport access vlan 10

字典排序

dictsort(value, case_sensitive=False, by='key', reverse=False)- 允许我们对字典进行排序,因为它们在 Python 中默认不排序。默认情况下按键排序,但您可以使用属性请求按by='value'排序。

在下面的示例中,我们按名称(dict 键)对前缀列表进行排序:

模板:

{% for pl_name, pl_lines in prefix_lists | dictsort %}
ip prefix list {{ pl_name }}
  {{ pl_lines | join('\n') }}
{% endfor %}

 

数据:

prefix_lists:
  pl-ntt-out:
    - permit 10.0.0.0/23
  pl-zayo-out:
    - permit 10.0.1.0/24
  pl-cogent-out:
    - permit 10.0.0.0/24

结果:

ip prefix list pl-cogent-out
  permit 10.0.0.0/24
ip prefix list pl-ntt-out
  permit 10.0.0.0/23
ip prefix list pl-zayo-out
  permit 10.0.1.0/24

在这里,我们按优先级(dict值)排序一些对等列表,更高的值更受欢迎,因此使用reverse=true

模板:

BGP peers by priority

{% for peer, priority in peer_priority | dictsort(by='value', reverse=true) %}
Peer: {{ peer }}; priority: {{ priority }}
{% endfor %}

 

数据:

peer_priority:
  ntt: 200
  zayo: 300
  cogent: 100

结果:

BGP peers by priority

Peer: zayo; priority: 300
Peer: ntt; priority: 200
Peer: cogent; priority: 100

漂浮

float(value, default=0.0)- 将值转换为浮点数。API 响应中的数值有时以字符串形式出现。通过float我们可以确保在进行比较之前转换字符串。

这是一个使用float.

模板:

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

 

数据:

eos_ver: "4.10"

结果

Detected EOS ver 4.10, using old command syntax.

通过...分组

groupby(value, attribute)- 用于根据属性之一对对象进行分组。您可以选择使用点表示法按嵌套属性进行分组。此过滤器可用于基于特征值的报告或为仅适用于对象子集的操作选择项目。

在下面的示例中,我们根据分配给它们的 vlan 对接口进行分组:

模板:

{% for vid, members in  interfaces | groupby(attribute='vlan') %}
Interfaces in vlan {{ vid }}: {{ members | map(attribute='name') | join(', ') }}
{% endfor %}

 

数据:

interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
    vlan: 50
  - name: Ethernet4
    vlan: 60

结果:

Interfaces in vlan 50: Ethernet1, Ethernet2, Ethernet3
Interfaces in vlan 60: Ethernet4

整数

int(value, default=0, base=10)- 与浮点数相同,但在这里我们将值转换为整数。也可用于将其他基数转换为十进制基数:

下面的示例显示了十六进制到十进制的转换。

模板:

LLDP Ethertype
hex: {{ lldp_ethertype }} 
dec: {{ lldp_ethertype | int(base=16) }}

 

数据:

lldp_ethertype: 88CC

结果:

LLDP Ethertype
hex: 88CC 
dec: 35020

加入

join(value, d='', attribute=None)- 非常非常有用的过滤器。获取序列的元素并将连接的元素作为字符串返回。

对于只想显示项目而不进行任何操作的情况,它可以代替for循环。在这些情况下,我发现join版本更具可读性。

模板:

ip name-server {{ name_servers | join(" ") }}

 

数据:

name_servers:
  - 1.1.1.1
  - 8.8.8.8
  - 9.9.9.9
  - 8.8.4.4

结果:

ip name-server 1.1.1.1 8.8.8.8 9.9.9.9 8.8.4.4

地图

map(*args, **kwargs)- 可用于查找属性或对序列中的所有对象应用过滤器。

例如,如果您想跨设备名称规范字母大小写,您可以一次性应用过滤器。

模板:

Name-normalized device list:
{{ devices | map('lower') | join('\n') }}

 

数据:

devices:
 - Core-rtr-warsaw-01
 - DIST-Rtr-Prague-01
 - iNET-rtR-berlin-01

结果:

Name-normalized device list:
core-rtr-warsaw-01
dist-rtr-prague-01
Inet-rtr-berlin-01

就我个人而言,我发现它对于跨大量对象检索属性及其值最有用。这里我们只对name属性的值感兴趣:

模板:

Interfaces found:
{{ interfaces | map(attribute='name') | join('\n') }}

 

数据:

interfaces:
  - name: Ethernet1
    mode: switched
  - name: Ethernet2
    mode: switched
  - name: Ethernet3
    mode: routed
  - name: Ethernet4
    mode: switched

结果:

Interfaces found:
Ethernet1
Ethernet2
Ethernet3
Ethernet4

拒绝

reject(*args, **kwargs)- 通过应用 Jinja2 测试并拒绝测试成功的对象来过滤项目序列。如果测试结果为 ,则该项目将从最终列表中删除true

在这里,我们只想显示公共 BGP AS 编号。

模板:

Public BGP AS numbers:
{% for as_no in as_numbers| reject('gt', 64495) %}
{{ as_no }}
{% endfor %}

 

数据:

as_numbers:
 - 1794
 - 28910
 - 65203
 - 64981
 - 65099

结果:

Public BGP AS numbers:
1794
28910

拒绝属性

rejectattr(*args, **kwargs)- 与reject过滤器相同,但测试应用于对象的选定属性。

如果您选择的测试需要参数,请在测试名称之后提供它们,用逗号分隔。

在此示例中,我们希望通过将 test 应用于“mode”属性来从列表中删除“switched”接口。

模板:

Routed interfaces:

{% for intf in interfaces | rejectattr('mode', 'eq', 'switched') %}
{{ intf.name }}
{% endfor %}

 

数据:

interfaces:
  - name: Ethernet1
    mode: switched
  - name: Ethernet2
    mode: switched
  - name: Ethernet3
    mode: routed
  - name: Ethernet4
    mode: switched

结果:

Routed interfaces:

Ethernet3

选择

select(*args, **kwargs)- 通过仅保留通过 Jinja2 测试的元素来过滤序列。这个过滤器是相反的reject您可以使用其中任何一种,具体取决于在给定场景中感觉更自然的情况。

与此类似,reject还有一个selectattr过滤器,其工作原理与每个对象的属性相同,select但适用于每个对象的属性。

下面我们要报告在我们的设备上找到的私有 BGP AS 编号。

模板:

Private BGP AS numbers:
{% for as_no in as_numbers| select('gt', 64495) %}
{{ as_no }}
{% endfor %}

 

数据:

as_numbers:
 - 1794
 - 28910
 - 65203
 - 64981
 - 65099

结果:

Private BGP AS numbers:
65203
64981
65099

Tojson

tojson(value, indent=None)- 以 JSON 格式转储数据结构。当需要 JSON 的应用程序使用呈现的模板时很有用。也可以用作pprint美化变量调试输出的替代方法。

模板:

{{ interfaces | tojson(indent=2) }}

 

数据:

interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
    vlan: 50
  - name: Ethernet4
    vlan: 60

结果:

[
  {
    "name": "Ethernet1",
    "vlan": 50
  },
  {
    "name": "Ethernet2",
    "vlan": 50
  },
  {
    "name": "Ethernet3",
    "vlan": 50
  },
  {
    "name": "Ethernet4",
    "vlan": 60
  }
]

独特

unique(value, case_sensitive=False, attribute=None)- 返回给定集合中唯一值的列表。与过滤器很好地配对,map用于查找用于给定属性的一组值。

在这里,我们正在查找我们跨接口使用的访问 VLAN。

模板:

Access vlans in use: {{ interfaces | map(attribute='vlan') | unique | join(', ') }}

 

数据:

interfaces:
  - name: Ethernet1
    vlan: 50
  - name: Ethernet2
    vlan: 50
  - name: Ethernet3
    vlan: 50
  - name: Ethernet4
    vlan: 60

结果:

Access vlans in use: 50, 60

结论

有了这个相当长的示例列表,我们到了教程的这一部分。Jinja2 过滤器可以成为一个非常强大的工具,我希望我的解释能帮助你看到它们的潜力。

您确实需要记住明智地使用它们,如果它开始看起来笨拙并且感觉不对,请查看替代方案。看看您是否可以将复杂性移到模板之外,修改您的数据模型,或者如果这不可能,请编写您自己的过滤器。

这都是我的。一如既往,期待再次见到您,更多 Jinja2 帖子即将发布!

参考