• Jinja2 教程 第 5 部分 宏


    什么是宏?

    宏类似于许多编程语言中的函数。我们使用它们来封装用于执行可重复操作的逻辑。宏可以接受参数,也可以在没有参数的情况下使用。

    在宏内部,我们可以使用任何 Jinja 特性和结构。运行宏的结果是一些文本。您基本上可以将宏视为一个也允许参数化的大型评估语句。

    为什么以及如何使用宏

    当我们发现自己在相同的文本和代码行周围复制粘贴时,宏非常适合创建可重用的组件。即使它所做的只是渲染静态文本,您也可能会从宏中受益。

    以设备横幅为例,它们往往是静态的,但会反复使用。您可以创建宏并让它呈现横幅,而不是在模板中复制粘贴横幅的文本。

    您不仅可以减少复制过程中可能发生的错误,而且还可以使横幅的未来更新变得更加容易。现在您只有一个地方需要更改横幅,使用此宏的任何其他内容都会自动反映更改。

    {% macro banner() -%}
    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  |
    ===========================================
    ^
    {% endmacro -%}
    
    {{ banner() }}
    
    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  |
    ===========================================
    ^
    

    这就是我们的第一个宏!

    正如您在上面看到的,我们以宏开头{% macro macro_name(arg1, arg2) %},以 . 结尾{% endmacro %}参数是可选的。

    您在开始和结束标签之间放置的任何内容都将在您调用宏的位置进行处理和呈现。

    一旦我们定义了宏,我们就可以在模板的任何地方使用它。我们可以使用{{ macro_name() }}替换语法直接插入结果。我们也可以在if..else块或for循环等其他结构中使用它。您甚至可以将宏传递给其他宏!

    添加参数

    当您开始在宏中使用参数时,真正的乐趣就开始了。那是他们展示自己真正潜力的时候。

    我们的下一个宏呈现默认界面描述。我们为我们的端口分配了不同的角色,我们希望默认描述能够反映这一点。我们可以通过编写以接口角色为参数的宏来实现这一点。

    数据:

    interfaces:
     - name: Ethernet10
       role: desktop
     - name: Ethernet11
       role: desktop
     - name: Ethernet15
       role: printer
     - name: Ethernet22
       role: voice
    

    带有宏的模板:

    {% macro def_if_desc(if_role) -%}
    Unused port, dedicated to {{ if_role }} devices
    {%- endmacro -%}
    
    {% for intf in interfaces -%}
    interface {{ intf.name }}
      description {{ def_if_desc(intf.role) }}
    {% endfor -%}

    渲染文本:

    interface Ethernet10
      description Unused port, dedicated to desktop devices
      ip address
    interface Ethernet11
      description Unused port, dedicated to desktop devices
      ip address
    interface Ethernet15
      description Unused port, dedicated to printer devices
      ip address
    interface Ethernet22
      description Unused port, dedicated to voice devices
      ip address
    

    宏在这里是否有用可能不会立即显现出来,因为我们在正文中只有一行。我们可以在for循环中写下这一行。不利的一面是我们的意图没有明确传达。

    {% for intf in interfaces -%}
    interface {{ intf.name }}
      description Unused port, dedicated to {{ intf.role }} devices
    {% endfor -%}

    这是可行的,但不是很明显这是我们希望用作默认值的描述。如果我们开始在这里添加更多处理,情况会变得更糟。

    但是,如果我们使用宏,宏的名称清楚地告诉我们将应用默认的接口描述。也就是说,很清楚我们的意图是什么。

    还有真正的踢球者。宏可以移动到单独的文件中,并包含在需要它们的模板中。这意味着您只需要维护这个宏就可以被数百个模板使用!您必须更新默认描述的地方有多少?一个,只有一个。

    用于深度嵌套结构的宏

    宏的另一个很好的用例是访问深度嵌套数据结构中的值。

    现代 API 可以返回具有多级字典和列表的结果,这使得在编写访问这些数据结构中的值的表达式时很容易出错。

    以下是 Arista 设备为命令返回的输出的真实示例:

    sh ip bgp neighbors x.x.x.x received-routes | json

    由于尺寸的原因,我只显示了一条路线条目的完整结果,共 3 条:

    {
      "vrfs": {
          "default": {
          "routerId": "10.3.0.2",
          "vrf": "default",
          "bgpRouteEntries": {
              "10.1.0.1/32": {
              "bgpAdvertisedPeerGroups": {},
              "maskLength": 32,
              "bgpRoutePaths": [
                  {
                  "asPathEntry": {
                      "asPathType": null,
                      "asPath": "i"
                  },
                  "med": 0,
                  "localPreference": 100,
                  "weight": 0,
                  "reasonNotBestpath": null,
                  "nextHop": "10.2.0.0",
                  "routeType": {
                      "atomicAggregator": false,
                      "suppressed": false,
                      "queued": false,
                      "valid": true,
                      "ecmpContributor": false,
                      "luRoute": false,
                      "active": true,
                      "stale": false,
                      "ecmp": false,
                      "backup": false,
                      "ecmpHead": false,
                      "ucmp": false
                  }
                  }
              ],
              "address": "10.1.0.1"
              },
      ...
          "asn": "65001"
          }
      }
    }
    

    这里有很多事情要做,在大多数情况下,您只需要获取其中几个属性的值。

    假设我们只想访问路径的前缀、下一跳和有效性。

    下面是我们需要导航以访问这些值的对象层次结构:

    • vrfs.default.bgpRouteEntries- 前缀在这里(作为键)
    • vrfs.default.bgpRouteEntries[pfx].bgpRoutePaths.0.nextHop- 下一跳
    • vrfs.default.bgpRouteEntries[pfx].bgpRoutePaths.0.routeType.valid- 路线有效性

    我不了解你,但我真的不喜欢将它复制粘贴到我需要访问这些的所有地方。

    所以这是我们可以做的,让我们自己更容易,更明显。

    {% macro print_route_info(sh_bgpr) -%}
    {% for route, routenfo in vrfs.default.bgpRouteEntries.items() -%}
    Route: {{ route }} - Next Hop: {{ 
    routenfo.bgpRoutePaths.0.nextHop }} - Permitted: {{ 
    routenfo.bgpRoutePaths.0.routeType.valid }}
    {% endfor %}
    {%- endmacro -%}
    
    {{ print_route_info(sh_bgp_routes) }}
    Route: 10.1.0.1/32 - Next Hop: 10.2.0.0 - Permitted: True
    Route: 10.1.0.2/32 - Next Hop: 10.2.0.0 - Permitted: True
    Route: 10.1.0.3/32 - Next Hop: 10.2.0.0 - Permitted: True
    

    我将访问属性所涉及的逻辑和复杂性移到了一个名为print_route_info该宏获取我们的 show 命令的输出,然后只返回我们需要的内容。

    如果我们需要访问更多属性,我们只需要更改宏的主体。

    在我们真正需要信息的地方,我们称之为好命名的宏并将命令的输出提供给它。这使得我们想要实现的目标更加明显,并且导航数据结构的机制被隐藏起来。

    在宏内部分支

    让我们再举一个例子,这次我们的宏将有if..else块显示我们可以根据条件检查返回结果。

    我创建了数据模型,其中 BGP 对等 IP 和名称未在我用于指定对等的映射中明确列出。相反,我们将每个对等条目指向我们想要建立对等互连的本地接口。

    我们还假设我们所有的对等互连都使用 /31 掩码。

    interfaces:
      Ethernet1:
        ip_add: 10.1.1.1/31
        peer: spine1
        peer_intf: Ethernet1
      Ethernet2:
        ip_add: 10.1.1.9/31
        peer: spine2
        peer_intf: Ethernet1
    
    bgp:
      as_no: 65001
      peers:
        - intf: Ethernet1
          as_no: 64512
        - intf: Ethernet2
          as_no: 64512

    使用这个数据模型,我们想为 BGP 邻居构建配置。利用ipaddr过滤器,我们可以执行以下操作:

    • 在链接接口上配置的网络中查找第一个 IP 地址。
    • 检查第一个 IP 地址是否等于接口上配置的 IP 地址。
      • 如果相等,则 BGP 对等体的 IP 必须是此 /31 中的第二个 IP 地址。
      • 如果不是,则 BGP 对等 IP 必须是第一个 IP 地址。

    将其转换为 Jinja 语法,我们得到以下结果:

    router bgp {{ bgp.as_no }}
    {%- for peer in bgp.peers -%}
    {% set fst_ip = interfaces[peer.intf].ip_add | ipaddr(0) -%}
    {% if our_ip == fst_ip -%}
    {% set peer_ip = fst_ip | ipaddr(1) | ipaddr('address') -%}
    {% else -%}
    {% set peer_ip = fst_ip | ipaddr('address') -%}
    {% endif %}
     neighbor {{ peer_ip }} remote-as {{ peer.as_no }}
     neighbor {{ peer_ip }} description {{ interfaces[peer.intf].peer }}
    {%- endfor %}

    这是渲染的结果:

    router bgp 65001
     neighbor 10.1.1.0 remote-as 64512
     neighbor 10.1.1.0 description spine1
     neighbor 10.1.1.8 remote-as 64512
     neighbor 10.1.1.8 description spine2
    

    任务完成。我们得到了我们想要的,邻居 IP 从分配给本地接口的 IP 自动计算出来。

    但是,我看的时间越长,我就越不喜欢这种逻辑和操作在实际邻居语句之前的感觉。

    您可能还想在模板的其他地方使用计算对等 IP 的逻辑,这意味着复制粘贴。如果稍后您更改界面上的掩码或想要稍微更改数据结构,则必须找到所有具有逻辑的位置并确保更改所有位置。

    我会说这个案例是构建宏的另一个很好的候选者。

    因此,我将把计算对等 IP 的逻辑移到我正在调用的宏中peer_ip该宏将采用一个参数local_intf,即我们正在为其配置对等互连的接口的名称。

    如果将此版本与非宏版本进行比较,您会发现大部分代码是相同的,只是我们使用替换语句代替设置最终值并将其分配给变量。

    {% macro peer_ip(local_intf) -%}
    {% set local_ip = interfaces[local_intf].ip_add -%}
    {% set fst_ip = local_ip | ipaddr(0) -%}
    {% if fst_ip == local_ip -%}
    {{ fst_ip | ipaddr(1) | ipaddr('address') -}}
    {% else -%}
    {{ fst_ip | ipaddr('address') -}}
    {%- endif -%}
    {% endmacro -%}
    
    router bgp {{ bgp.as_no }}
    {%- for peer in bgp.peers -%}
    {%- set bgp_peer_ip = peer_ip(peer.intf) %}
     neighbor {{ bgp_peer_ip }} remote-as {{ peer.as_no }}
     neighbor {{ bgp_peer_ip }} description {{ interfaces[peer.intf].peer }}
    {%- endfor %}

    我们在函数的一个地方使用这个宏,我们将它返回的值赋给变量bgp_peer_ip然后我们可以bgp_peer_ip在我们的邻居语句中使用。

    {%- set bgp_peer_ip = peer_ip(peer.intf) %}

    我喜欢这种方法的另一件事是我们可以将宏移动到它自己的文件中,然后将其包含在使用它的模板中。

    我们将讨论 Jinja 的导入,并在以后的帖子中包含更多细节。然而,这是一个非常有用的功能,因此在本文后面我将向您展示它们自己文件中的宏的简短示例。

    宏中的宏

    现在这里有一个有趣的。我们可以将宏作为参数传递给其他宏。这类似于 Python,其中函数可以像任何其他对象一样传递。

    我们愿意这样做吗?当然,在某些情况下这可能有用。我可以想到需要有更通用的宏来产生一些结果,并将另一个宏作为参数来启用更改用于呈现结果的格式。

    这意味着我们可以让父宏处理渲染我们感兴趣的输出的公共部分。然后作为参数传递的宏将负责处理渲染特定位的差异,这取决于调用者的需要。

    为了说明这一点并使其更易于可视化,请考虑呈现 ACL 条目的情况。不同的供应商可以并且经常会为 IP 源和目标对象使用不同的格式。有些将使用“net_address/pfxlen”,而有些将使用“net_address 通配符”。

    我们可以编写多个 ACL 渲染宏,每种情况一个。另一种选择是if..else在更大的宏中使用逻辑,宏参数决定使用哪种格式。

    或者我们可以将负责格式转换的逻辑封装在微小的宏中。然后我们可以让宏负责 ACL 渲染,它接收格式转换宏作为参数之一。也就是说,ACL 宏不知道如何进行渲染,它并不关心。它只知道它将从外部获得宏,并且可以在需要的地方应用它。

    这是包含 3 个不同格式宏的实际实现。

    用于我们示例的数据:

    Jinja2 Data

    networks:
      - name: quant_server_net
        prefix: 10.0.0.0/24
        services:
          - computing
    
    svc_def:
      computing:
        - {ip: 10.90.0.5/32, prot: tcp, port: 5008}
        - {ip: 10.91.4.0/255.255.255.0, prot: tcp, port: 5009}
        - {ip: 10.91.6.32/27, prot: tcp, port: 6800}

    带有宏的模板:

    {% 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 -%}
    
    {% macro acl_lines(aclobj, src_pfx, pfx_fmt) -%}
    {% for line in aclobj %}
     permit {{ line.prot }} {{ pfx_fmt(src_pfx) }} {{ pfx_fmt(line.ip) }} 
    {%- if line.prot in ['udp', 'tcp'] %} eq {{ line.port }}{% endif -%}
    {% endfor -%}
    {%- endmacro -%}
    
    {% for net in networks -%}
    ip access-list extended al_{{ net.name }}
    {%-  for svc in net.services -%}
    {{ acl_lines(svc_def[svc], net.prefix, ip_w_pfxlen) }}
    {%  endfor -%}
    {% endfor -%}

    ip_w_wc使用宏渲染结果:

    ip access-list extended al_quant_server_net
     permit tcp 10.0.0.0 0.0.0.255 10.90.0.5 0.0.0.0 eq 5008
     permit tcp 10.0.0.0 0.0.0.255 10.91.4.0 0.0.0.255 eq 5009
     permit tcp 10.0.0.0 0.0.0.255 10.91.6.32 0.0.0.31 eq 6800
    

    ip_w_netm使用宏渲染结果:

    ip access-list extended al_quant_server_net
     permit tcp 10.0.0.0 255.255.255.0 10.90.0.5 255.255.255.255 eq 5008
     permit tcp 10.0.0.0 255.255.255.0 10.91.4.0 255.255.255.0 eq 5009
     permit tcp 10.0.0.0 255.255.255.0 10.91.6.32 255.255.255.224 eq 6800
    

    ip_w_pfxlen使用宏渲染结果:

    ip access-list extended al_quant_server_net
     permit tcp 10.0.0.0/24 10.90.0.5/32 eq 5008
     permit tcp 10.0.0.0/24 10.91.4.0/24 eq 5009
     permit tcp 10.0.0.0/24 10.91.6.32/27 eq 6800
    

    希望现在您可以看到我在这里想要实现的目标。通过简单地提供不同的格式化程序宏,我可以在为不同供应商呈现配置的模板中使用相同的父宏。最重要的是,我们再次明确了我们的意图。

    我们的格式化宏可以在很多地方重复使用,并且很容易添加可以在 ACL 宏和其他地方使用的新格式化程序。

    此外,通过解耦和抽象 IP 前缀格式,我们使 ACL 宏更加集中。

    很多这些决定取决于个人喜好,但我觉得这种技术非常强大,很高兴知道它在你需要的时候就在那里。

    将宏移动到单独的文件

    我现在将向您展示如何将宏移动到单独的模板文件的示例。然后,我们将导入宏并从位于完全不同文件中的模板中调用它。

    我决定采用我们创建的宏来以不同的格式显示 IP 网络。我将 3 个格式化宏移动到单独的文件中,并将 ACL 宏保留在原始模板中。

    结果是两个模板。

    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 -%}

    acl_variants.j2

    {% import 'ip_funcs.j2' as ipfn -%}
    
    {% macro acl_lines(aclobj, src_pfx, pfx_fmt) -%}
    {% for line in aclobj %}
     permit {{ line.prot }} {{ pfx_fmt(src_pfx) }} {{ pfx_fmt(line.ip) }} 
    {%- if line.prot in ['udp', 'tcp'] %} eq {{ line.port }}{% endif -%}
    {% endfor -%}
    {%- endmacro -%}
    Prefix with prefix length ACL:
    {% for net in networks -%}
    ip access-list extended al_{{ net.name }}
    {%-  for svc in net.services -%}
    {{ acl_lines(svc_def[svc], net.prefix, ipfn.ip_w_wc) }}
    {%  endfor -%}
    {% endfor %}
    
    
    Network with Wildcard ACL:
    {% for net in networks -%}
    ip access-list extended al_{{ net.name }}
    {%-  for svc in net.services -%}
    {{ acl_lines(svc_def[svc], net.prefix, ipfn.ip_w_pfxlen) }}
    {%  endfor -%}
    {% endfor %}
    
    
    Network with network Mask ACL:
    {% for net in networks -%}
    ip access-list extended al_{{ net.name }}
    {%-  for svc in net.services -%}
    {{ acl_lines(svc_def[svc], net.prefix, ipfn.ip_w_netm) }}
    {%  endfor -%}
    {% endfor -%}

    第一个模板ip_funcs.j2包含格式化程序宏,仅此而已。请注意,代码没有任何变化,我们逐字逐句复制了这些代码。

    我们的原始模板发生了一些有趣的事情,这里称为acl_variants.j2第一行{% import 'ip_funcs.j2' as ipfn -%}是新的,现在我们调用格式化程序宏的方式不同了。

    Line{% import 'ip_funcs.j2' as ipfn -%}看起来像importPython 中的语句,并且它的工作方式类似。Jinja 引擎将查找被调用的文件ip_funcs.j2,并使该文件中的变量和宏在 namespace 中可用ipfn也就是说,现在可以使用ipfn.符号访问在导入文件中找到的任何内容。

    这就是我们现在需要调用格式化程序的方式。例如,使用ipfn.ip_w_wc语法调用将 IP 前缀转换为网络/通配符形式的宏。

    为了更好地衡量,我将所有格式变体添加到我们的模板中,这是最终结果:

    Prefix with prefix length ACL:
    
    ip access-list extended al_quant_server_net
     permit tcp 10.0.0.0 0.0.0.255 10.90.0.5 0.0.0.0 eq 5008
     permit tcp 10.0.0.0 0.0.0.255 10.91.4.0 0.0.0.255 eq 5009
     permit tcp 10.0.0.0 0.0.0.255 10.91.6.32 0.0.0.31 eq 6800
    
    
    Network with Wildcard ACL:
    
    ip access-list extended al_quant_server_net
     permit tcp 10.0.0.0/24 10.90.0.5/32 eq 5008
     permit tcp 10.0.0.0/24 10.91.4.0/24 eq 5009
     permit tcp 10.0.0.0/24 10.91.6.32/27 eq 6800
    
    
    Network with network Mask ACL:
    
    ip access-list extended al_quant_server_net
     permit tcp 10.0.0.0 255.255.255.0 10.90.0.5 255.255.255.255 eq 5008
     permit tcp 10.0.0.0 255.255.255.0 10.91.4.0 255.255.255.0 eq 5009
     permit tcp 10.0.0.0 255.255.255.0 10.91.6.32 255.255.255.224 eq 6800
    
    

    将宏移动到它们自己的文件中并从其他模板中导入它们是一个非常强大的功能。我将在未来关于导入和包含的帖子中更多地讨论它。

    高级宏使用

    Varargs 和 Kwargs

    在宏内部,您可以访问默认公开的特殊变量。其中一些与内部管道有关,不是很有趣,但您可能会发现其中很少有用途。

    • varargs- 如果给宏的位置参数比宏定义中明确列出的更多,那么 Jinja 会将它们放入名为 的特殊变量varargs中。然后,如果您认为有意义,您可以遍历它们并进行处理。

    • kwargs- 与 类似varargs,任何与显式列出的不匹配的关键字参数都将以kwargs变量结尾。kwargs.items()这可以使用语法进行迭代。

    我个人认为这两种方法在大多数用例中都不是很有用。在 Web 开发的世界中,接受许多元素来呈现表格和其他 HTML 项目可能是有意义的。

    在基础设施自动化领域,我更喜欢明确的论点和明确的意图,我觉得在使用特殊变量时并非如此。

    我确实有一些人为的例子来向你展示如果你觉得你真的可以使用这个特性的话它是如何工作的。

    下面的宏采用一个显式参数vid,它指定我们希望分配为接口的访问端口的 VLAN ID。任何额外的位置参数都将被视为需要为给定 VLAN ID 配置的接口名称。

    {% macro set_access_vlan(vid) -%}
    {% for intf in varargs -%}
    interface {{ intf }}
      switchport
      switchport mode access
      switchport access vlan {{ vid }}
    {% endfor -%}
    {%- endmacro -%}
    
    {{ set_access_vlan(10, "Ethernet10", "Ethernet20") }}

    结果:

    interface Ethernet10
      switchport
      switchport mode access
      switchport access vlan 10
    interface Ethernet20
      switchport
      switchport mode access
      switchport access vlan 10

    下面是类似的宏,但这次我们没有明确的参数。但是,我们将读取任何传递的关键字参数,并将键视为接口名称,将值视为要分配的 VLAN ID。

    {% macro set_access_vlan() -%}
    {% for intf, vid in kwargs.items() -%}
    interface {{ intf }}
      switchport
      switchport mode access
      switchport access vlan {{ vid }}
    {% endfor -%}
    {%- endmacro -%}
    
    {{ set_access_vlan(Ethernet10=10, Ethernet15=15, Ethernet20=20) }}

    渲染结果:

    interface Ethernet10
      switchport
      switchport mode access
      switchport access vlan 10
    interface Ethernet15
      switchport
      switchport mode access
      switchport access vlan 15
    interface Ethernet20
      switchport
      switchport mode access
      switchport access vlan 20
    
    

    这两个例子都有效,甚至做了一些可能有用的事情。这些特殊变量对我来说感觉不对,但是如果您需要它们,它们就在那里。

    Call堵塞

    调用块是调用其他宏的结构,它们本身就是宏,除了它们没有名称,所以它们只在它们出现的地方使用。

    您使用带有{% call called_macro() %}...{% endcal %}语法的调用块。

    这些工作有点像回调,因为它们调用的宏依次回调以执行call宏。您可以在此处看到与使用不同格式宏的 ACL 宏的相似之处。我们可以call使用单个命名宏来拥有许多宏,这些call宏允许命名宏执行的逻辑发生变化。

    我不知道它们的存在是否有历史原因,因为我真的看不出使用这些命名宏有什么好处。它们的使用也不是很直观。但同样,它们就在这里,也许你会发现需要它们。

    如此人为的示例时间!

    波纹管call宏调用make_acl宏,宏又回调执行调用宏:

    Bellow call macro calls make_acl macro which in turn calls back to execute calling macro

    {% macro make_acl(type, name) -%}
    ip access-list {{ type }} {{ name }}
    {{- caller() }}
    {%- endmacro -%}
    
    {% call make_acl('extended', 'al-ext-01') %}
     permit ip 10.0.0.0 0.0.0.255 10.0.0.0.255
     deny ip any any
    {%- endcall %}

    结果:

    ip access-list extended al-ext-01
     permit ip 10.0.0.0 0.0.0.255 10.0.0.0.255
     deny ip any any
    

    我们在这里得到了一些合理的结果,但代价是什么?你看到 ACL 线是如何进入身体的吗?魔术是{{ caller() }}在线的。这里的特殊函数caller()本质上是执行call调用make_acl宏的块的主体。

    这就是发生的事情,一步一步:

    • call推出make_acl
    • make_acl一路走来,渲染东西,直到遇到caller()
    • make_acl执行调用块caller()并插入结果
    • make_aclcaller()通过它身体的其余部分移动过去

    它有效,但我再次看到使用命名宏并明确传递它们没有任何优势。

    乐趣还没有结束,被调用的宏可以调用带有参数的调用者。

    {% macro acl_lines(aclobj, src_pfx) -%}
    {% for line in aclobj %}
     permit {{ line.prot }} {{ caller(src_pfx) }} {{ caller(line.ip) }} 
    {%- if line.prot in ['udp', 'tcp'] %} eq {{ line.port }}{% endif -%}
    {% endfor -%}
    {%- endmacro -%}
    
    {% for net in networks -%}
    ip access-list extended al_{{ net.name }}
    {%-  for svc in net.services -%}
    
    {% call(ip_net) acl_lines(svc_def[svc], net.prefix) -%}
    {{ ip_net|ipaddr('network/prefix') }}
    {%- endcall -%}
    
    {%  endfor -%}
    {% endfor -%}

    这是call我们使用可变格式化程序的 ACL 渲染的块版本。这次我在call块中包含了格式化程序。我们的块接受ip_net它期望调用宏在回调时提供的参数。

    这正是下面一行发生的事情:

    permit {{ line.prot }} {{ caller(src_pfx) }} {{ caller(line.ip) }}

    所以,我们有两个参数的call块调用。然后acl_lines回调履行其合同。acl_linescallcaller(src_pfx)caller(line.ip)

    这里需要注意的是,我们不能重用我们的格式化程序,它都在未命名的宏 akacall块中。一旦它执行,就是这样,如果你想使用格式化程序,你需要一个新的。

    结论

    我认为宏是 Jinja 更强大的功能之一,学习如何使用它们会让您受益匪浅。结合import您将获得可重复使用、定义明确的片段组,这些片段可以与其他模板分开保存。这使我们能够提取可重复的、有时是复杂的逻辑,并使我们的模板更清晰、更易于遵循。

    和往常一样,看看什么对你有用。如果您的宏变得笨拙,请考虑使用自定义过滤器。并且在使用高级macro功能时要小心,这些应该只为特殊情况保留,如果使用的话。

    我希望你从这篇文章中学到了一些东西,它给了你一些想法。更多关于 Jinja2 的帖子即将发布,所以经常来看看有什么新内容 :)

    参考

  • 相关阅读:
    《Unix/Linux系统编程》第九章学习笔记
    第1、2章学习笔记
    第11章学习笔记(20191213兰毅达)
    第7、8章学习笔记(20191213兰毅达)
    sort(20191213兰毅达)
    # 电子公文传输系统团队项目 团队作业(一):团队展示
    第10章学习笔记(20191213兰毅达)
    MyOD(选做)(20191213兰毅达)
    2019-2020-1 20191213兰毅达《信息安全专业导论》第十二周学习总结
    2019-2020-1 20191213兰毅达《信息安全专业导论》第十一周学习总结
  • 原文地址:https://www.cnblogs.com/a00ium/p/16069897.html
Copyright © 2020-2023  润新知