歡迎來到我的 Jinja2 教程的第 2 部分。在第 1 部分中,我們了解了 Jinja2 是什么,它的用途是什么,並開始研究模板基礎知識。接下來是循環和條件語句,其中包含測試和大量示例!
控制結構
在 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指的是Ethernet1和Ethernet2鍵。要訪問每個接口的屬性,我們需要使用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 以
and, or 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- 檢查變量是否為數字,整數和浮點數均返回 Truestring- 檢查變量是否為字符串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 }}
在某些情況下,我們知道字典或字符串不太可能出現,因此我們可以通過擺脫mapping或string測試來縮短檢查:
{{ 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 部分。接下來我將介紹空格,以便您可以使您的文檔看起來恰到好處,我們將繼續研究語言功能。我希望你在這里學到了一些有用的東西,並且回來獲得更多!
參考
- Jinja2 (2.11.x) 最新版本的官方文檔。可在:https ://jinja.palletsprojects.com/en/2.11.x/
- Jinja2 內置測試。可在:https ://jinja.palletsprojects.com/en/2.11.x/templates/#list-of-builtin-tests
- PyPi 上的 Jinja2 Python 庫。可在:https ://pypi.org/project/Jinja2/
- 帶有 Jinja 源代碼的 GitHub 存儲庫。可在:https ://github.com/pallets/jinja/
- 包含這篇文章資源的 GitHub 存儲庫。可在:https ://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p2-loops-conditionals
