yun 发表于 2019-9-11 15:21:51

ansible笔记(41):jinja2模板(四)

本帖最后由 yun 于 2019-9-11 17:18 编辑

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模板。
页: [1]
查看完整版本: ansible笔记(41):jinja2模板(四)