ansible筆記(41):jinja2模板(四)
- A+
所屬分類:ansible ofollow,noindex" target="_blank">運維技術
在本部落格中,ansible是一個系列文章,我們會盡量以通俗易懂的方式總結ansible的相關知識點。
ansible系列博文直達連結:ansible輕鬆入門系列
"ansible系列"中的每篇文章都建立在前文的基礎之上,所以, 請按照順序閱讀這些文章,否則有可能在閱讀中遇到障礙。
包含
在jinja2中,也可以像其他語言一樣使用"include"對其他檔案進行包含,比如,我有兩個模板檔案,test.j2和test1.j2,我想要在test.j2中包含test1.j2,則可以使用如下方法
# cat test.j2 test................... test................... {% include 'test1.j2' %} test................... # cat test1.j2 test1.j2 start {% for i in range(3) %} {{i}} {% endfor %} test1.j2 end
如上例所示,我在test.j2中使用了"{%include%}"對test1.j2檔案進行了包含,渲染test.j2模板,最終結果如下:
test................... test................... test1.j2 start 0 1 2 test1.j2 end test...................
如果在test.j2中定義了一個變數,那麼在被包含的test1.j2中可以使用這個在test.j2中的變數嗎?我們來試試,示例如下:
# cat test.j2 {% set varintest='var in test.j2' %} test................... test................... {% include 'test1.j2' %} test................... # cat test1.j2 test1.j2 start {{ varintest }} test1.j2 end
如上例所示,我們在test.j2中定義了varintest變數,然後在test1.j2中引用了這個變數,那麼渲染test.j2模板,最終結果如下
test................... test................... test1.j2 start var in test.j2 test1.j2 end test...................
由此可見,被包含的檔案在預設情況下是可以使用test.j2中定義的變數的,這是因為在預設情況下,使用"include"時,會匯入當前環境的上下文,通俗點說就是,如果你在外部檔案中定義了變數,通過include包含了檔案以後,被包含檔案中可以使用之前外部檔案中定義的變數。
當然,如果你不想讓被包含檔案能夠使用到外部檔案中定義的變數,則可以使用"without context"顯式的設定"include",當"include"中存在"without context"時,表示不匯入對應的上下文,示例如下:
# cat test.j2 {% set varintest='var in test.j2' %} test................... test................... {% include 'test1.j2' without context %} test................... # cat test1.j2 test1.j2 start {{ varintest }} test1.j2 end
如上例所示,我們在test.j2中包含了test1.j2檔案,在包含時使用了"without context",同時,在test1.j2中呼叫了test.j2中定義的變數,此時如果渲染test.j2檔案,則會報錯,這是因為我們顯式的設定了不匯入上下文,所以我們無法在test1.j2中使用test.j2中定義的變數,按照上例渲染test.j2檔案,會出現如下錯誤:
# ansible test70 -m template -a "src=test.j2 dest=/opt/test" test70 | FAILED! => { "changed": false, "msg": "AnsibleError: Unexpected templating type error occurred on ({% set varintest='var in test.j2' %}\ntest...................\ntest...................\n{% include 'test1.j2' without context %}\n\ntest...................\n): argument of type 'NoneType' is not iterable" }
注意:如果在"include"時設定了"without context",那麼在被包含的檔案中使用for迴圈時,不能讓使用range()函式,也就是說,下例中的test.j2檔案無法被正常渲染
# cat test.j2 test................... test................... {% include 'test1.j2' without context %} test................... # cat test1.j2 test1.j2 start {% for i in range(3) %} {{i}} {% endfor %} test1.j2 end
在ansible中渲染上例中的test.j2檔案,會報錯,報錯資訊中同樣包含"argument of type 'NoneType' is not iterable"。
我們也可以顯式的指定"with context",表示匯入上下文,示例如下:
cat test.j2 test................... test................... {% include 'test1.j2' with context %} test...................
如上例所示,在使用"include"時,顯式指定了"with context",表示匯入對應的上下文,當然,在預設情況下,即使不使用"with context","include"也會匯入對應的上下文,所以,如下兩種寫法是等效的。
{% include 'test1.j2' %} {% include 'test1.j2' with context %}
預設情況下,如果指定包含的檔案不存在,則會報錯,示例如下:
# cat test.j2 test................... test................... {% include 'test1.j2' with context %} test................... {% include 'test2.j2' with context %}
如上例所示,我們在test.j2中指定包含了兩個檔案,test1.j2和test2.j2,但是,我們並沒有編寫所謂的test2.j2,所以,當我們渲染test.j2模板時,會報如下錯誤:
# ansible test70 -m template -a "src=test.j2 dest=/opt/test" test70 | FAILED! => { "changed": false, "msg": "TemplateNotFound: test2.j2" }
那麼有沒有一種方法,能夠在指定包含的檔案不存在時,自動忽略包含對應的檔案呢?答案是肯定的,我們使用"ignore missing"標記皆可,示例如下:
# cat test.j2 test................... test................... {% include 'test1.j2' with context %} test................... {% include 'test2.j2' ignore missing with context %}
如上例所示,雖然test2.j2檔案不存在,但是渲染test.j2檔案時不會報錯,因為使用"ignore missing"後,如果需要包含的檔案不存在,會自動忽略對應的檔案,不會報錯。
匯入
說完了"{% include %}",我們再來聊聊"{% import %}",include的作用是在模板中包含另一個模板檔案,而import的作用是在一個檔案中匯入其他檔案中的巨集,在前一篇文章中,我們總結了巨集的用法,在前一篇文章的示例中,所有巨集都是在當前檔案中定義的,也就是說,無論是定義巨集,還是呼叫巨集,都是在同一個模板檔案中完成的,那麼能不能實現在A檔案中定義巨集,在B檔案中使用巨集呢?完全可以,通過import即可實現,示例如下:
# cat function_lib.j2 {% macro testfunc() %} test function {% for i in varargs %} {{ i }} {% endfor %} {% endmacro %} {% macro testfunc1(tv1=1) %} {{tv1}} {% endmacro %} # cat test.j2 {% import 'function_lib.j2' as funclib %} something in test.j2 {{ funclib.testfunc(1,2,3) }} something in test.j2 {{ funclib.testfunc1('aaaa') }}
如上例所示,我們在function_lib.j2檔案中定義了兩個巨集,testfunc巨集和testfunc1巨集(如果你不明白這兩個巨集的含義,請回顧前文),我們並沒有在function_lib.j2檔案中呼叫這兩個巨集,而是需要在test.j2檔案中呼叫這些巨集,所以我們使用了"import"將function_lib.j2檔案中的巨集匯入到了當前檔案中,如下程式碼表示將function_lib.j2檔案中的巨集匯入到funclib變數中。
{% import 'function_lib.j2' as funclib %}
由於我們已經將"function_lib.j2"檔案中的巨集匯入到了"funclib"變數中,所以當我們需要呼叫"function_lib.j2"檔案中的testfunc巨集時,直接使用瞭如下程式碼即可。
{{ funclib.testfunc(1,2,3) }}
上述程式碼表示使用funclib中的testfunc巨集,並且傳入了3個數字作為引數,呼叫testfunc1巨集也是同樣的道理。
除了上述方法能夠呼叫其他檔案中定義的巨集,其實還有另外一種方法,示例如下
# cat function_lib.j2 {% macro testfunc() %} test function {% for i in varargs %} {{ i }} {% endfor %} {% endmacro %} {% macro testfunc1(tv1=111) %} test function1 {{tv1}} {% endmacro %} # cat test1.j2 {% from 'function_lib.j2' import testfunc as tf, testfunc1 as tf1 %} something in test1.j2 {{ tf(1,2) }} something in test1.j2 {{ tf1('a') }}
如上例所示,我們使用瞭如下語法匯入了'function_lib.j2'檔案中的兩個巨集
{% from 'function_lib.j2' import testfunc as tf, testfunc1 as tf1 %}
上述語法表示:
從'function_lib.j2'檔案中將testfunc巨集匯入為tf巨集
從'function_lib.j2'檔案中將testfunc1巨集匯入為tf1巨集
匯入後,直接呼叫tf巨集和tf1巨集,即為呼叫'function_lib.j2'檔案中對應的巨集,上例中,我一次性匯入了'function_lib.j2'中的兩個巨集,你也可以使用如下方法匯入指定的巨集,比如,只匯入testfunc1
{% from 'function_lib.j2' import testfunc1 as tf1 %}
聰明如你,一定已經看出了兩種import方法的不同之處
方法一如下: {% import 'function_lib.j2' as funclib %} 表示一次性匯入'function_lib.j2' 檔案中的所有巨集,呼叫巨集時使用對應的變數進行呼叫。 方法二如下: {% from 'function_lib.j2' import testfunc1 as tf1 %} 表示匯入'function_lib.j2' 檔案中指定的巨集,呼叫巨集時使用對應的新名稱進行呼叫。
import和include不同,include預設會匯入上下文環境,而import預設則不會,所以,如果想要讓巨集被import以後能夠使用到對應的上下文環境,則需要顯式的配置"with context",示例如下:
# cat function_lib.j2 {% macro testfunc1(tv1=111) %} test function1 {{tv1}} {{outvartest}} {% endmacro %} # cat test.j2 {% set outvartest='00000000' %} {% import 'function_lib.j2' as funclib with context%} something in test.j2 {{ funclib.testfunc1() }}
如上例所示,在import巨集時,顯式的使用了"with context",在使用"import"並且顯式的配置"with context"時,有如下兩個注意點。
一、在外部定義變數的位置需要在import之前,也就是說,上例中定義outvartest變數的位置在import之前。
二、只能使用上述方法一對巨集進行匯入,經測試,使用方法二匯入巨集後,即使顯式的指定了"with context",仍然無法找到對應的變數。
注意:巨集中如果包含for迴圈並且for迴圈中使用了range()函式,那麼在"import"巨集時則必須顯式的指定"with context",否則在ansible中渲染對應模板時,會出現包含如下資訊的報錯。
"argument of type 'NoneType' is not iterable"
注意:巨集如果以一個或多個下劃線開頭,則表示這個巨集為私有巨集,這個巨集不能被匯入到其他檔案中使用,示例如下:
# cat func.j2 {% macro _test() %} something in test macro {% endmacro %} {{_test()}}
上述巨集不能被匯入到其他檔案,只能在當前檔案中被呼叫。
繼承
除了"包含"和"匯入"的能力,jinja2模板引擎還有一個非常棒的能力,就是"繼承","繼承"可以幫助我們更加靈活的生成模板檔案。
我們先大概的描述一下繼承的使用方法,以便你能夠形成大致的概念,此處有疑問沒有關係,後文會進行示例和解釋。
我們可以先定義一個父模板,然後在父模板中定義一些"塊",不同的內容放在不同的塊中,之後再定義一個子模板,這個子模板繼承自剛才定義的父模板,我們可以在子模板中寫一些內容,這些內容可以覆蓋父模板中對應的內容,這樣說可能不太容易理解,先來看個小例子。
# cat test.j2 something in test.j2... something in test.j2... {% block test %} Some of the options that might be replaced {% endblock %} something in test.j2... something in test.j2...
如上例所示,test.j2就是剛才描述的"父模板"檔案,這個檔案中並沒有太多內容,只是有一些文字,以及一個"塊",這個塊通過"{% block %}"和"{% endblock %}"定義,塊的名字為"test",test塊中有一行文字,我們可以直接渲染這個檔案,渲染後的結果如下
something in test.j2... something in test.j2... Some of the options that might be replaced something in test.j2... something in test.j2...
如你所見,直接渲染這個父模板,父模板中的塊並沒有對父模板有任何影響,現在,定義一個子模板檔案,並且指明這個子模板繼承自這個父模板,示例如下:
# cat test1.j2 {% extends 'test.j2' %} {% block test %} aaaaaaaaaaaaaa 11111111111111 {% endblock %}
如上所示,"{% extends 'test.j2' %}"表示當前檔案繼承自test.j2檔案,test.j2檔案中的內容都會被繼承過來,而test1.j2檔案中同樣有一個test塊,test塊中有兩行文字,那麼渲染test1.j2檔案,得到的結果如下:
something in test.j2... something in test.j2... aaaaaaaaaaaaaa 11111111111111 something in test.j2... something in test.j2...
從上述結果可以看出 ,最終生成的內容中,子模板中的test塊中的內容覆蓋了父模板中的test塊的內容。
這就是繼承的使用方法,其實很簡單,你也可以在父模板的塊中不寫任何內容,而是靠子模板去填充對應的內容,示例如下:
# cat test.j2 something in test.j2... something in test.j2... {% block test %} {% endblock %} something in test.j2... something in test.j2... # cat test1.j2 {% extends 'test.j2' %} {% block test %} aaaaaaaaaaaaaa 11111111111111 {% endblock %}
其實上例與之前的示例並沒有什麼區別,只是上例中父模板的塊中沒有預設的內容,而之前的示例中父模板的塊中有預設的內容而已。
父模板中也可以存在多個不同名稱的block,以便將不同的內容從邏輯上分開,放置在不同的塊中。
你可能會有疑問,為什麼我們不直接修改模板檔案,而是通過繼承的方式去填充或者覆蓋呢?原因如下:
在編寫配置檔案的模板時,我們通常會將比較通用的設定編寫成模板,以便能夠適應於大多數的情況,但是總有些配置是需要經常修改的,或者是需要根據實際情況進行填充的,如果每次都直接修改預設的通用模板,那麼通用的模板就會慢慢變得不通用,所以,我們可以通過繼承的方式,將比較穩定的部分或者比較公共的部分提取到父模板中,將可能需要經常修改的部分或者不是那麼公共的部分寫到父模板的對應的塊中,如果通用的模板可以滿足你,直接渲染通用的父模板即可,如果你覺得需要修改其中的一部分,但是同時又想保留大多數通用的配置,則可以編寫一個子模板,來繼承父模板,然後只覆蓋需要修改的塊即可,這樣你就能夠得到一個你想要的結果,並且不用修改對應的父模板,保留了比較通用的配置,在下一次遇到比較通用的情況時,仍然可以使用父模板進行渲染,而且公共的部分仍然可以留給別人使用,所以,我們得出結論,當使用繼承時,需要把公共的、通用的、穩定的部分提取出來,將可能需要修改或動態填充的部分定義在對應的塊中,以便以後能夠通過繼承的方式靈活的覆蓋、填充。
使用繼承的一些優點如下:
1、將公共的部分提取出來,規範統一公共部分
2、將穩定的部分提取出來 ,提高複用率
3、靈活的覆蓋或者填充可能需要修改的部分,同時保留其他大部分未修改的預設配置
4、為別人的修改留下一定的空間,並且不會影響預設的配置
當然,如果沒有必要,你也不必強行的使用繼承
塊中也可以巢狀另一個塊,示例如下:
something in test.j2... {% block test %} something in block test {% block t1 %} something in block t1 {% endblock %} something in block test {% endblock %}
如上例所示,test塊中還有一個t1塊,這樣也是完全可行的,不過,上例中存在一個小問題,問題就是無論test塊還是t1塊,都使用"{% endblock %}"作為結尾,雖然能夠正常 解析,但是可讀性比較差,所以,我們可以在endblock中也加入對應的塊名稱以提高可讀性,示例如下:
something in test.j2... {% block test %} something in block test {% block t1 %} something in block t1 {% endblock t1 %} something in block test {% endblock test %} something in test.j2...
在子模板替換對應的塊時,也可以在endblock塊中寫入對應的塊名稱。
如果你需要在一個模板中多次的引用同一個塊,則可以使用self特殊變數來引用模板自身的某個塊,示例如下:
# cat test.j2 something in test.j2... {% block test %} something in block test something else in block test {% endblock test %} {{ self.test() }} something in test.j2...
如上例所示,模板中定義了一個test塊,在這個塊之後,使用了"{{ self.test() }}",這表示呼叫當前模板中的test塊,上例模板渲染後結果如下
# cat test something in test.j2... something in block test something else in block test something in block test something else in block test something in test.j2...
如你所見,test塊中的內容被引用了兩次,如果還有其他塊名,你也可以使用"self.blockname()"來呼叫,如果你修改了上例中test塊中的內容,所有引用test塊中的內容都會隨之改變,同理,如果你在子模板中覆蓋了test塊,那麼所有引用test塊的部分都會被覆蓋。
如果你並不想完全覆蓋父模板中的塊,而是想要在父模板某個塊的基礎之上進行擴充套件,那麼則可以子模板中使用super塊來完成,這樣說可能不太容易理解,不如先來看一個小示例,如下:
# cat test.j2 something in test.j2... {% block test %} something in block test something else in block test {% endblock test %} something in test.j2... # cat test1.j2 {% extends 'test.j2' %} {% block test%} aaaaaaaaaaaaaa {{ super() }} 11111111111111 {% endblock test %}
如上例所示,test1.j2繼承自test.j2檔案,同時,test1.j2中指明要修改test塊,如你所見,子模板的test塊中包含"{{ super() }}",這表示父模板中test塊中的內容會替換到"{{ super() }}"對應的位置,換句話說就是,我們可以通過"{{ super() }}"來獲取父級塊中的內容,上例test1.j2的渲染結果如下:
# cat test1 something in test.j2... aaaaaaaaaaaaaa something in block test something else in block test 11111111111111 something in test.j2...
如你所見,父級塊中的內容保留了,我們加入的兩行文字也在對應的位置生成了,這樣就能夠在保留父級塊內容的前提下,加入更多的內容,不過上例中有一個小問題,就是super塊在渲染後會自動換行,細心如你一定已經發現了,之前示例中使用"self"變數時,也會出現相同的問題,解決這個問題很簡單,我們之前在使用for迴圈時就遇到過類似的問題,沒錯,使用"空白控制符"即可,在super塊的末尾加入空白控制符"減號"就可以將自動換行去掉,示例如下:
{{ super() -}}
你有可能會使用for迴圈去迭代一個塊,但是你在塊中無法獲取到for的迴圈變數,示例如下:
# cat test.j2 something in test.j2... {%set testvar=123%} {% block test %} something in block test ---- {{testvar}} {% endblock %} {% for i in range(3) -%} {% block test1 %} something in block test1 ---- {{i}} {% endblock %} {%- endfor %} something in test.j2...
上述模板中有兩個塊,test塊和test1塊,test塊未使用for迴圈,test1塊使用for迴圈進行處理,渲染上述模板,會報如下錯誤
"msg": "AnsibleUndefinedVariable: 'i' is undefined"
提示未定義變數,這是因為當test1塊被for迴圈處理時,無法在塊中獲取到for的迴圈變數造成的,如果想要在上述情況中獲取到for的迴圈變數,則可以在塊中使用scoped修飾符,示例如下
# cat test.j2 something in test.j2... {%set testvar=123%} {% block test %} something in block test ---- {{testvar}} {% endblock %} {% for i in range(3) -%} {% block test1 scoped %} something in block test1 ---- {{i}} {% endblock %} {%- endfor %} something in test.j2...
上例渲染後結果如下
something in test.j2... something in block test ---- 123 something in block test1 ---- 0 something in block test1 ---- 1 something in block test1 ---- 2 something in test.j2...
在繼承模板時,如果父模板在當前目錄的子目錄中,則可以使用如下方法繼承對應的父模板
# tree . ├── parent │ └── test.j2 └── test1.j2 # cat test1.j2 {% extends 'parent/test.j2' %} {% block test%} {{ super() -}} 11111111111111 {% endblock test %}
如上例所示,test1.j2為子模板,test.j2為父模板,父模板在子模板所在目錄的子目錄中,此時,可以使用'parent/test.j2'引用test.j2模板。
我的微信公眾號
關注"實用運維筆記"微信公眾號,當部落格中有新文章時,可第一時間得知哦~