Linux Ansible自动化运维 template模块 jinja2模板引擎

一、ansible 中的模板的作用

当安装完 redis 以后,redis默认配置的监听地址为"127.0.0.1",这样是安全的,但是,如果我需要让redis监听在非"127.0.0.1"的IP地址上,以便让其他主机也能够使用本机上的redis服务,那么我就需要修改默认的配置,没错,修改redis配置文件中的bind设置,即可将redis绑定在指定的IP上,假设,现在需要一次性在10台主机上安装redis,并且让安装后的redis都监听在redis所在主机的非"127.0.0.1"的IP地址上,我们该怎么办呢?通过ansible在10台主机上安装redis很容易,但是安装完成后,10台主机中的redis都是使用默认的监听地址"127.0.0.1"的,难道我们要在安装完成后,挨个的手动修改这10台主机中的redis配置文件吗?显然,应该有更加方便的方法能够解决这个问题,没错,这个方法就是使用"模板"。

如果想要解决上述问题,我们可以先创建一个"模板"文件,ansible会根据"模板"文件,为每一台主机生成对应的配置文件,大致步骤如下:

1、找一个现成的redis配置文件,作为"模板"文件,你可以从之前安装过redis的主机中拷贝一份,也可以从redis的rpm包中提取一份。

2、修改模板文件,将IP设置部分使用变量进行替换。

3、使用ansible调用"template"模块,对"模板文件"进行渲染,根据模板生成每个主机对应的配置文件,并将最终生成的配置文件拷贝到目标主机中。

那么现在,我们来看看具体该怎样实现:

首先,就是准备一个redis配置文件作为模板文件,所有主机的redis配置文件都是根据这个模板文件生成的,此处,我已经从之前安装过redis的主机中拷贝了一个redis.conf文件到ansible控制机中,打开这个配置文件,可以看到,默认监听的地址为"127.0.0.1",如下所示

# vim /etc/redis/6379.conf
  70 bind 127.0.0.1

 
   
   
   
   
  • 1
  • 2
  • 3

按照要求,每个主机上的redis都应该监听在自己的非本地回环地址上,但是,每个主机的IP地址都不一样,我们怎样才能获取到每个机器的IP地址呢?你肯定已经想到了,我们在使用ansible连接到对应主机时,就已经获取到了对应主机的IP地址,对应的IP就存放在"ansible_host"变量中(在配置ansible清单时也是将远程主机的IP地址写入到ansible_host变量中,但是需要注意,ansible控制机对应的ansible_host变量的值并不是自己的IP,而是在清单中配置的主机别名),所以,我们只要在bind设置中使用"ansible_host"变量进行替换即可,修改redis.conf文件,如下所示:

[root@server4 ~]# vim 6379.conf 
bind {
     {
     ansible_host}} 127.0.0.1

 
   
   
   
   
  • 1
  • 2
  • 3

完成上述修改后,模板配置文件就算准备好了,那么,我们就来编写一个playbook,来完成上述工作场景,示例playbook如下:

[root@server4 ~]# vim template.yml
[root@server4 ~]# cat template.yml 
---
- hosts: testB
  remote_user: root
  gather_facts: no
  tasks:
  - template:
      src: /root/6379.conf
      dest: /etc/redis/6379.conf
  - name: restart redis
    shell: /etc/init.d/redis_6379 restart

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

如上例所示,先调用template模块,上例template模块使用了两个参数,src参数和dest参数,src参数对应的文件就是ansible主机中的模板文件,即我们刚才修改过的redis.conf配置文件模板,dest参数表示将最终生成的配置文件拷贝到目标主机的所在路径,也就是说,上例playbook会调用template模块,使用ansible主机中的/root/redis.conf 文件作为模板文件,根据模板文件,对目标主机testB生成最终的配置文件,最终的配置文件会被拷贝到testB主机的/etc/redis/目录中,并且文件名为6379.conf,安装redis后默认的配置文件路径即为/etc/redis/6379.conf ,我们使用template模块生成了最终的配置文件后,覆盖了默认配置文件,如上例所示,template模块不仅能够根据模板文件生成文件,还会自动将生成的文件拷贝到远程主机中的指定位置,那么我们来运行一下上例playbook

[root@server4 ~]# ansible-playbook template.yml 

 
   
   
   
   
  • 1

Linux Ansible自动化运维 template模块 jinja2模板引擎_第1张图片

playbook执行完毕后,我们到目标主机testB上查看/etc/redis/6379.conf文件,6379.conf中的bind配置如下所示

[root@server3 ~]# vim /etc/redis/6379.conf
bind 172.25.63.3 127.0.0.1

 
   
   
   
   
  • 1
  • 2
  • 3

可以看到,testB中的6379.conf文件中的bind配置对应的IP地址为172.25.63.3 127.0.0.1
,这第一个IP正是testB主机的IP地址,这种效果完全符合我们的预期,在testB主机中安装完redis以后,并没有手动的修改配置文件,而是利用"模板",自动生成了对应配置文件,并拷贝到了远程主机中的指定位置,上例中,为了演示方便,目标主机只指定了testB一台主机,你也可以在上例playbook中一次性的指定多台目标主机试试,最终的效果就是每台主机中的redis都监听在自己的IP地址上。

经过上述描述,我们可以看出,模板功能可以帮助我们灵活的生成配置文件,我们只需要先选择一个文件作为模板文件,然后修改模板文件中需要灵活生成的部分,使用变量进行替换(对应的变量必须提前定义好或者能够在运行时获取),模板文件中不需要灵活生成的部分保持不变即可,当需要为各个目标主机生成配置文件时,只需调用template模块,templdte模块会在ansible控制机中对模板文件进行渲染,最终生成各个主机对应的配置文件,然后拷贝到远程主机的指定位置中

除了在playbook中能够使用template模块,在ad-hoc命令中也可以直接调用templdate模块,在ad-hoc命令中使用templdate模块比较方便我们测试模板文件的最终生成效果,示例如下:

[root@server4 ~]# ansible testB -m template -a "src=/root/6379.conf dest=/etc/redis/6379.conf"

 
   
   
   
   
  • 1

直接在ansible主机中执行上述ad-hoc命令,执行完成后,即可在目标主机中的对应位置获取到最终生成的文件。

二、template模块

由于template模块还负责将最终生成的文件拷贝到远程主机上,所以还有一些常用的参数,可以用于设置配置文件的权限,如下:

  • owner参数: 指定最终生成的文件拷贝到远程主机后的属主。
  • group参数: 指定最终生成的文件拷贝到远程主机后的属组。
  • mode参数:指定最终生成的文件拷贝到远程主机后的权限,如果你想将权限设置为"rw-r–r--",则可以使用mode=0644表示,如果你想要在user对应的权限位上添加执行权限,则可以使用mode=u+x表示。

除了上述参数,还有如下参数也很常用

  • force参数:当远程主机的目标路径中已经存在同名文件,并且与最终生成的文件内容不同时,是否强制覆盖,可选值有yes和no,默认值为yes,表示覆盖,如果设置为no,则不会执行覆盖拷贝操作,远程主机中的文件保持不变。
  • backup参数:当远程主机的目标路径中已经存在同名文件,并且与最终生成的文件内容不同时,是否对远程主机的文件进行备份,可选值有yes和no,当设置为yes时,会先备份远程主机中的文件,然后再将最终生成的文件拷贝到远程主机。

三、jinja2模板引擎

前文中我们提到过,ansible使用的是jinja2模板引擎,我们可以看出,当templdate模块对模板文件进行渲染时,使用的就是jinja2模板引擎,所以,如果想要更加灵活的编辑模板文件,最好还要了解一些与jinja2有关的基本知识点,其实,我们一直都在使用jinja2的语法,当我们在playbook中引用变量时,会将变量用双括号"{ { }}“括起,这就是jinja2的语法,在jinja2中,使用”{ { }}“装载变量,除了”{ { }}",还有一些其他的jinja2基本语法,我们一起来了解一下,如下:

  • { { }} :用来装载表达式,比如变量、运算表达式、比较表达式等。
  • {% %} :用来装载控制语句,比如 if 控制结构,for循环控制结构。
  • {# #} :用来装载注释,模板文件被渲染后,注释不会包含在最终生成的文件中。

我们可以用几个示例来熟悉上述基础语法吧,在ansible主机中准备测试文件

变量操作示例

仍然从我们最熟悉的变量操作开始测试,test.j2中写入如下内容:

[root@server4 jinja2]# vim test1.j2
[root@server4 jinja2]# cat test1.j2 
test jinja2 variable
test {
     {
      testvar1 }} test

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

然后在ansible主机中执行如下测试命令:

[root@server4 jinja2]# ansible testB -m template -e "testvar1=redhat" -a "src=test1.j2 dest=/opt/test1"

 
   
   
   
   
  • 1

如上例所示,我们在渲染模板时传入了变量testvar1,其变量值为teststr,上述命令执行完成后,登录目标主机,即testB主机,查看渲染后的文件内容,如下:

[root@server3 redis]# cd /opt/
[root@server3 opt]# cat test1 
test jinja2 variable
test redhat test

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

上述示例中,"{ { }}“中包含的就是一个变量,当模板被渲染后,变量的值被替换到了最终的配置文件中,当然,既然在模板中定义了变量,那么就要保证在渲染模板时,可以调用到对应的变量,否则就会报错,”{ { }}"中除了能够包含变量,还能够包含哪些东西呢?如后文所示。

注:为了方便示例,之后的示例会直接给出模板文件内容与最终生成文件的内容,省略对应渲染模板的过程,渲染模板过程参考上述示例即可。

上文中提到过,除了变量,"{ { }}“中还可以包含一些表达式,那么我们一起来看看那些常用的表达式在”{ { }}"中的示例。

比较表达式示例

比较表达式的相关示例如下:

模板文件内容如下:

[root@server4 jinja2]# vim test2.j2 
[root@server4 jinja2]# cat test2.j2 
{
     {
      1 == 1 }}
{
     {
      2 != 2 }}
{
     {
      2 > 1 }}
{
     {
      2 >= 1 }}
{
     {
      2 < 1 }}
{
     {
      2 <= 1 }}
[root@server4 jinja2]# ansible testB -m template -a "src=test2.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

生成文件内容如下:

[root@server3 opt]# cat test
True
False
True
True
False
False

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

逻辑运算示例

逻辑运算的相关示例如下:

模板文件内容:

[root@server4 jinja2]# vim test3.j2 
[root@server4 jinja2]# cat test3.j2 
{
     {
      (2 > 1) or (1 > 2) }}
{
     {
      (2 > 1) and (1 > 2) }}
{
     {
      not true }}
{
     {
      not True }}
{
     {
      not false }}
{
     {
      not False }}
[root@server4 jinja2]# ansible testB -m template -a "src=test3.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

生成文件内容:

[root@server3 opt]# cat test
True
False
False
False
True
True

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

算数运算示例

算数运算的相关示例如下:

模板文件内容

[root@server4 jinja2]# vim test4.j2 
[root@server4 jinja2]# cat test4.j2 
{
     {
      3 + 2 }}
{
     {
      3 - 4 }}
{
     {
      3 * 5 }}
{
     {
      2 ** 3 }}
{
     {
      7 / 5 }}
{
     {
      7 // 5 }}
{
     {
      17 % 5 }}
[root@server4 jinja2]# ansible testB -m template -a "src=test4.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

生成文件内容

[root@server3 opt]# cat test
5
-1
15
8
1.4
1
2

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

成员运算示例

成员运算的相关示例如下:

模板文件内容

[root@server4 jinja2]# vim test5.j2 
[root@server4 jinja2]# cat test5.j2 
{
     {
      1 in [1,2,3,4] }}
{
     {
      1 not in [1,2,3,4] }}
[root@server4 jinja2]# ansible testB -m template -a "src=test5.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

生成文件内容

[root@server3 opt]# cat test
True
False

 
   
   
   
   
  • 1
  • 2
  • 3

在上述成员运算的示例中,in运算符后面对应的是一个"列表",所以我们可以看出,一些基础的数据类型,都可以包含在"{ { }}“中,jinja2本身就是基于 python 的模板引擎,所以,python的基础数据类型都可以包含在”{ { }}"中,这也是再自然不过的了,相关示例如下:

python的基础数据类型

模板文件内容如下:

[root@server4 jinja2]# vim test6.j2 
[root@server4 jinja2]# cat test6.j2 
### str
{
     {
      'testString' }}
{
     {
      "testString" }}
### num
{
     {
      15 }}
{
     {
      18.8 }}
### list
{
     {
      ['Aa','Bb','Cc','Dd'] }}
{
     {
      ['Aa','Bb','Cc','Dd'].1 }}
{
     {
      ['Aa','Bb','Cc','Dd'][1] }}
### tuple
{
     {
      ('Aa','Bb','Cc','Dd') }}
{
     {
      ('Aa','Bb','Cc','Dd').0 }}
{
     {
      ('Aa','Bb','Cc','Dd')[0] }}
### dic
{
     {
      {
     'name':'bob','age':18} }}
{
     {
      {
     'name':'bob','age':18}.name }}
{
     {
      {
     'name':'bob','age':18}['name'] }}
### Boolean
{
     {
      True }}
{
     {
      true }}
{
     {
      False }}
{
     {
      false }}
[root@server4 jinja2]# ansible testB -m template -a "src=test6.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

生成文件内容如下:

[root@server3 opt]# cat test
### str
testString
testString
### num
15
18.8
### list
['Aa', 'Bb', 'Cc', 'Dd']
Bb
Bb
### tuple
('Aa', 'Bb', 'Cc', 'Dd')
Aa
Aa
### dic
{
     'age': 18, 'name': 'bob'}
bob
bob
### Boolean
True
True
False
False

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

从上述示例模板文件可以看出,字符串、数值、列表、元组、字典、布尔值等数据类型均可在"{ { }}“使用,但是,通常我们不会像上述示例那样使用它们,因为通常我们会通过变量将对应的数据传入,而不是将数据直接写在”{ { }}“中,即使直接将数据写在”{ { }}"中,也会配合其他表达式或者函数进行处理,所以,我们可以把上述模板文件的内容改为如下内容进行测试:

[root@server4 jinja2]# vim test7.j2 
[root@server4 jinja2]# cat test7.j2 
{
     {
      teststr }}
{
     {
      testnum }}
{
     {
      testlist[1] }}
{
     {
      testlist1[1] }}
{
     {
      testdic['name'] }}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

此处我们使用如下playbook对上述模板文件进行测试,而不是使用ad-hoc命令,原因稍后解释,测试playbook如下:

[root@server4 jinja2]# vim temptest1.yml
[root@server4 jinja2]# cat temptest1.yml 
---
- hosts: testB
  remote_user: root
  gather_facts: no
  vars:
    teststr: 'redhat'
    testnum: 18
    testlist: ['aA','bB','cC']
    testlist1: 
    - AA
    - BB
    - CC
    testdic:
      name: pp
      age: 22
  tasks:
  - template:
      src: /root/jinja2/test7.j2
      dest: /opt/test

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

运行上例playbook以后,最终生成的文件如下

[root@server4 jinja2]# ansible-playbook temptest1.yml 

 
   
   
   
   
  • 1
[root@server3 opt]# cat test
redhat
18
bB
BB
pp

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

刚才之所以用playbook进行测试,是因为如果通过命令把列表当做参数传入后,列表会被当做字符串处理,所以没有通过ad-hoc直接进行测试,而是通过playbook的方式渲染模板。

过滤器示例

除了变量和各种常用的运算符,过滤器也可以直接在"{ { }}"中使用,与前文示例中的用法没有任何区别,示例如下:

模板文件内容

[root@server4 jinja2]# vim test8.j2 
[root@server4 jinja2]# cat test8.j2 
{
     {
      'abcd' | upper}}
[root@server4 jinja2]# ansible testB -m template -a "src=test8.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

生成文件内容:

[root@server3 opt]# cat test
ABCD

 
   
   
   
   
  • 1
  • 2

tests示例

当然,jinja2的tests自然也能够在"{ { }}"中使用,与前文中的用法也是一样的,如下:

模板文件内容

[root@server4 jinja2]# vim test9.j2 
[root@server4 jinja2]# cat test9.j2 
{
     {
      testvar1 is defined }}
{
     {
      testvar1 is undefined }}
{
     {
      '/opt' is exists }}
{
     {
      '/opt' is file }}
{
     {
      '/opt' is directory }}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

执行命令时传入变量

[root@server4 jinja2]# ansible testB -m template -e "testvar1=redhat" -a "src=test9.j2 dest=/opt/test"

 
   
   
   
   
  • 1

生成文件内容

[root@server3 opt]# cat test
True
False
True
False
True

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

lookup示例

“lookup”,示例如下:

模板文件内容如下

 [root@server4 jinja2]# vim test11.j2 
[root@server4 jinja2]# cat test11.j2 
jinja2 test
{
     {
      lookup('file','/testdir/testfile') }}			#显示文件内容
{
     {
      lookup('env','PATH') }}				#显示环境变量
test jinja2
[root@server4 jinja2]# ansible testB -m template -a "src=test11.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

ansible主机中的testfile内容如下

[root@server4 jinja2]# cat /testdir/testfile
aaaaaaaaaaaaa
bbbbbbbbbbbbbbb
ccccccccccc

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

生成文件内容如下

[root@server3 opt]# cat test
jinja2 test
aaaaaaaaaaaaa
bbbbbbbbbbbbbbb
ccccccccccc
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
test jinja2

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

{# #}用法示例

经过上述描述,你肯定已经发现了,前文中总结的很多知识点都能用上,而且经过上述描述,"{ { }}“的用法你肯定也已经掌握了,说完”{ { }}",顺势来聊聊"{# #}",上文中提到过,在jinja2中,使用"{# #}“包含注释信息,所以,如果我们需要在模板文件中对某些配置进行注释,则可以将注释信息写入到”{# #}"中,示例如下:

模板文件内容如下:

[root@server4 jinja2]# vim test10.j2 
[root@server4 jinja2]# cat test10.j2 
jinja2 test
{
     #这是一行注释信息#}
jinja2 test
{
     #
这是多行注释信息,
模板被渲染以后,
最终的文件中不会包含这些信息
#}
jinja2 test
[root@server4 jinja2]# ansible testB -m template -a "src=test10.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

生成文件内容如下:

[root@server3 opt]# cat test
jinja2 test
jinja2 test
jinja2 test

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

上述示例表示{# #}中的注释内容不会在最终生成的文件中显示。

"{ { }}“和”{# #}“的用法就先总结到这里,下一篇文章中,我们再来总结一下”{% %}"的用法。

一、"if"控制语句

if结构

先来聊聊if,与其他语言相同,if用来进行条件判断,在jinja2中,if的语法如下:

{
     % if 条件 %}
...
...
...
{
     % endif %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

如你所见,当使用if语法时,需要使用endif作为结束,示例模板如下:

[root@server4 jinja2-2]# vim test1.j2
[root@server4 jinja2-2]# cat test1.j2 
{
     % if testnum > 3 %}
greater than 3
{
     % endif %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

使用如下playbook对模板文件进行渲染

注:不要使用ad-hoc命令调用template模块进行渲染,因为使用命令调用template模块时,无论你传入的数据是哪种类型,都会被当做字符串进行处理,所以此处使用playbook渲染模板,以保证数据类型的正确。

[root@server4 jinja2-2]# vim temptest2.yml
[root@server4 jinja2-2]# cat temptest2.yml 
---
- hosts: testB
  remote_user: root
  gather_facts: no
  tasks:
  - template:
      src: /root/jinja2-2/test1.j2
      dest: /opt/test
    vars:
      testnum: 5

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

最终生成的文件内容如下:

[root@server3 opt]# cat test
greater than 3

 
   
   
   
   
  • 1
  • 2

使用if进行条件判断,能够让我们的模板变得更加灵活。

if…else…结构

除了"if"结构,当然还有"if…else…"结构,语法如下:

{
     % if 条件 %}
...
{
     % else %}
...
{
     % endif %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

"if…else if…"结构

与其他语言一样,也有"if…else if…"的语法结构,如下:

{
     % if 条件一 %}
...
{
     % elif 条件二 %}
...
{
     % elif 条件N %}
...
{
     % endif %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

或者结合在一起使用,语法如下

{
     % if 条件一 %}
...
{
     % elif 条件N %}
...
{
     % else %}
...
{
     % endif %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上述语法就是"if"控制语句的语法。

二、if表达式

其实,说到"if",不仅仅有"if"控制语句,还有"if"表达式,利用"if"表达式,可以实现类似三元运算的效果,示例模板内容如下:

[root@server4 jinja2-2]# vim test2.j2
[root@server4 jinja2-2]# cat test2.j2 
{
     {
      'a' if 2>1 else 'b' }}

 
   
   
   
   
  • 1
  • 2
  • 3
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test2.j2 dest=/opt/test"

 
   
   
   
   
  • 1

渲染后的文件内容如下

[root@server3 opt]# cat test
a

 
   
   
   
   
  • 1
  • 2

如果你使用过其他语言的三目运算,或者你使用过 python 的三元运算,那么你一定已经看明白了上述表达式的含义,上例中的if表达式的含义为,如果2>1这个条件为真,则使用’a’,如果2>1这个条件不成立,则使用’b’,而2必定大于1,所以条件成立,最终使用’a’,在jinja2中,if表达式的语法如下:

 <do something> if <something is true> else <do something else>

 
   
   
   
   
  • 1

"if"表达式和"if"控制语句并不是一个东西,"if"表达式可以与其他的控制语句结合使用

在前文的示例中,我们都是在playbook中定义变量,然后在模板文件中使用变量,其实,我们也可以直接在模板文件中定义变量,示例如下:

[root@server4 jinja2-2]# vim test3.j2
[root@server4 jinja2-2]# cat test3.j2 
{
     % set teststr='abc' %}
{
     {
      teststr }}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

如你所见,在jinja2中,使用set关键字定义变量,执行如下ad-hoc命令渲染模板

[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test3.j2 dest=/opt/test"

 
   
   
   
   
  • 1

最终生成的文件内容如下

[root@server3 opt]# cat test
abc

 
   
   
   
   
  • 1
  • 2

直接在模板中定义变量可以方便我们的测试。

三、for循环

for循环基本语法

刚才说完了"if",现在来聊聊"for",for循环的基本语法如下:

{
     % for 迭代变量 in 可迭代对象 %}
{
     {
      迭代变量 }}
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3

没错,如你所见,与"if"很像,"for"需要使用"endfor"作为结束,jinja2的控制语句大多都会遵循这个规则,即"XXX"控制语句需要使用"endXXX"作为结尾,之后不再进行赘述。

先看一个简单的for循环示例,如下

[root@server4 jinja2-2]# vim test4.j2
[root@server4 jinja2-2]# cat test4.j2 
{
     % for i in [3,1,7,8,2] %}
{
     {
      i }}
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

上例中我们直接在模板文件的for循环中定义了一个列表,执行如下命令,对模板进行渲染

[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test4.j2 dest=/opt/test"

 
   
   
   
   
  • 1

最终生成的文件内容如下:

[root@server3 opt]# cat test
3
1
7
8
2

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

从生成的内容可以看出,每次循环后都会自动换行,如果不想要换行,则可以使用如下语法

[root@server4 jinja2-2]# vim test4.j2
[root@server4 jinja2-2]# cat test4.j2 
{
     % for i in [3,1,7,8,2] -%}
{
     {
      i }}
{
     %- endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

在for的结束控制符"%}“之前添加了减号”-"

在endfor的开始控制符"{%“之后添加到了减号”-"

渲染上述模板,最终的生成效果如下:

[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test4.j2 dest=/opt/test"

 
   
   
   
   
  • 1
[root@server3 opt]# cat test
31782

 
   
   
   
   
  • 1
  • 2

如上所示,列表中的每一项都没有换行,而是连在了一起显示,如果你觉得这样显示有些"拥挤",则可以稍微改进一下上述模板,如下:

[root@server4 jinja2-2]# cat test4.j2 
{
     % for i in [3,1,7,8,2] -%}
{
     {
      i }}{
     {
      '  ' }}
{
     %- endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

如上例所示,我们在循环每一项时,在每一项后面加入了一个空格字符串,所以,最终生成的效果如下:

[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test4.j2 dest=/opt/test"

 
   
   
   
   
  • 1
[root@server3 opt]# cat test
3  1  7  8  2 

 
   
   
   
   
  • 1
  • 2

其实,还有更加简洁的写法,就是将上述模板内容修改为如下内容:

[root@server4 jinja2-2]# vim test4.j2
[root@server4 jinja2-2]# cat test4.j2 
{
     % for i in [3,1,7,8,2] -%}
{
     {
      i~' ' }}
{
     %- endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

如上例所示,我们直接在迭代变量的后面使用了波浪符~,并且用波浪符将迭代变量和空格字符串连在一起,渲染上述模板内容,最终生成内容的效果与刚才示例中的效果是相同的,在jinja2中,波浪符"~"就是字符串连接符,它会把所有的操作数转换为字符串,并且连接它们。

for循环操作字典

"for"除了能够循环操作列表,也能够循环操作字典,示例如下:

[root@server4 jinja2-2]# vim test5.j2
[root@server4 jinja2-2]# cat test5.j2 
{
     % for key,val in {
     'name':'redhat','age':18}.iteritems() %}
{
     {
      key ~  ':' ~ val }}
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

如上所示,在循环操作字典时,先使用iteritems函数对字典进行处理,然后使用key和val两个变量作为迭代变量,分别用于存放字典中键值对的"键"和"值",所以,直接输出两个变量的值即可,key和val是我随意起的变量名,你可以自己定义这两个迭代变量的名称,而且,上例中的iteritems函数也可以替换成items函数,但是推荐使用iteritems函数,上例最终生成内容如下:

[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test5.j2 dest=/opt/test"

 
   
   
   
   
  • 1
[root@server3 opt]# cat test
age:18
name:redhat

 
   
   
   
   
  • 1
  • 2
  • 3

在使用for循环时,有一些内置的特殊变量可以使用,比如,如果我想要知道当前循环操作为整个循环的第几次操作,则可以借助"loop.index"特殊变量,示例如下:

[root@server4 jinja2-2]# vim test6.j2 
[root@server4 jinja2-2]# cat test6.j2 
{
     % for i in [3,1,7,8,2] %}
{
     {
      i ~ '----' ~ loop.index }}
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test6.j2 dest=/opt/test"

 
   
   
   
   
  • 1

最终生成文件内容如下:

[root@server3 opt]# cat test
3----1
1----2
7----3
8----4
2----5

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

除了内置特殊变量"loop.index",还有一些其他的内置变量,它们的作用如下(此处先简单的进行介绍,之后会给出示例):

变量 作用
loop.index 当前循环操作为整个循环的第几次循环,序号从1开始
loop.index0 当前循环操作为整个循环的第几次循环,序号从0开始
loop.revindex 当前循环操作距离整个循环结束还有几次,序号到1结束
loop.revindex0 当前循环操作距离整个循环结束还有几次,序号到0结束
loop.first 当操作可迭代对象中的第一个元素时,此变量的值为true
loop.last 当操作可迭代对象中的最后一个元素时,此变量的值为true
loop.length 可迭代对象的长度
loop.depth 当使用递归的循环时,当前迭代所在的递归中的层级,层级序号从1开始
loop.depth0 当使用递归的循环时,当前迭代所在的递归中的层级,层级序号从0开始
loop.cycle() 这是一个辅助函数,通过这个函数我们可以在指定的一些值中进行轮询取值,具体参考之后的示例

注:我当前使用的ansible版本为2.7.0,此版本的ansible对应的jinja2模板引擎的版本为2.7.2,上述内置变量为jinja2的2.7.2版本中的内置变量,目前,较新的jinja2版本为2.10,在2.10版的jinja2中还可以使用loop.previtem、loop.nextitem等特殊内置变量。

如果你只是想单纯的对一段内容循环的生成指定的次数,则可以借助range函数完成,比如,循环3次

[root@server4 jinja2-2]# vim test6.j2 
[root@server4 jinja2-2]# cat test6.j2 
{
     % for i in range(5) %}
{
     {
      i }}
{
     % endfor %}
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test6.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
[root@server3 opt]# cat test
0
1
2
3
4

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当然,range函数可以指定起始数字、结束数字、步长等,默认的起始数字为0,

[root@server4 jinja2-2]# vim test6.j2 
[root@server4 jinja2-2]# cat test6.j2 
{
     % for i in range(1,8,2) %}
{
     {
      i }}
{
     % endfor %}
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test6.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
[root@server3 opt]# cat test
1
3
5
7

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

上例表示从1开始,到8结束(不包括4),步长为2,也就是说只有1,3,5,7会输出。

默认情况下,模板中的for循环不能像其他语言中的 for循环那样使用break或者continue跳出循环,但是你可以在"for"循环中添加"if"过滤条件,以便符合条件时,循环才执行真正的操作,示例如下:

[root@server4 jinja2-2]# vim test7.j2 
[root@server4 jinja2-2]# cat test7.j2 
{
     % for i in [7,1,5,3,9] if i > 3 %}
  {
     {
      i }}
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test7.j2 dest=/opt/test"

 
   
   
   
   
  • 1
[root@server3 opt]# cat test
  7
  5
  9

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

上述 示例表示只有列表中的数字大于3时,才输出列表中的元素,刚才在介绍if表达式时,我们说过,if表达式可以和其他控制语句结合使用,就是这个意思,上例的语法就是 “if内联表达式” 和 “for循环控制结构” 结合在一起的使用方式。

你可能会问,我们在for循环中使用if判断控制语句进行判断不是也可以实现上述语法的效果吗?比如,使用如下示例的写法。

[root@server4 jinja2-2]# vim test8.j2 
[root@server4 jinja2-2]# cat test8.j2 
{
     % for i in [7,1,5,3,9] %}
  {
     % if i>3 %}
    {
     {
      i }}
  {
     %endif%}
{
     % endfor %}
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test8.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
[root@server3 opt]# cat test
      7
          5
          9

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

没错,如果仅仅是为了根据条件进行过滤,上述两种写法并没有什么不同,但是,如果你需要在循环中使用到loop.index这种计数变量时,两种写法则会有所区别,具体区别渲染如下模板内容后则会很明显的看出来:

[root@server4 jinja2-2]# vim test9.j2 
[root@server4 jinja2-2]# cat test9.j2 
{
     % for i in [7,1,5,3,9] if i>3 %}
{
     {
      i ~'----'~ loop.index }}
{
     % endfor %}
{
     % for i in [7,1,5,3,9] %}
{
     % if i>3 %}
{
     {
      i ~'----'~ loop.index}}
{
     % endif %}
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test9.j2 dest=/opt/test"

 
   
   
   
   
  • 1

最终生成的内容如下

[root@server3 opt]# cat test
7----1
5----2
9----3 
7----1
5----3
9----5

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

从上述结果可以看出,当使用if内联表达式时,如果不满足对应条件,则不会进入当次迭代,所以loop.index也不会进行计算,而当使用if控制语句进行判断时,其实已经进入了当次迭代,loop.index也已经进行了计算。

当for循环中使用了if内联表达式时,还可以与else控制语句结合使用,示例如下:

[root@server4 jinja2-2]# vim test10.j2
[root@server4 jinja2-2]# cat test10.j2 
{
     % for i in [7,1,5,3,9] if i>10 %}
{
     {
      i }}
{
     %else%}
no one is greater than 10
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test10.j2 dest=/opt/test"

 
   
   
   
   
  • 1
[root@server3 opt]# cat test
no one is greater than 10

 
   
   
   
   
  • 1
  • 2

如上例所示,for循环中存在if内联表达式,if对应的条件为i > 10,即元素的值必须大于10,才回执行一次迭代操作,而for循环中还有一个else控制语句,else控制语句之后也有一行文本,那么上例是什么意思呢?上述示例表示,如果列表中的元素大于10,则进入当次迭代,输出"i"的值,if对应的条件成立时,else块后的内容不执行,如果列表中没有任何一个元素大于10,即任何一个元素都不满足条件,则渲染else块后面的内容

其实,当for循环中没有使用if内联表达式时,也可以使用else块,示例如下

{
     % for u in userlist %}
  {
     {
      u.name }}
{
     %else%}
  no one
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5

上例中,只有userlist列表为空时,才会渲染else块后的内容。

所以,综上所述,如果因序列为空或者有条件过滤了序列中的所有项目而没有执行循环时,你可以使用else渲染一个用于替换的块。

for循环递归操作

for循环也支持递归操作,递归示例如下:

[root@server4 jinja2-2]# vim test11.j2
[root@server4 jinja2-2]# cat test11.j2 
{
     % set dictionary={
      'name':'bob','son':{
      'name':'tom','son':{
      'name':'jerry' } } }  %} 
{
     % for key,value in dictionary.iteritems() recursive %}
  {
     % if key == 'name' %}
    {
     % set fathername=value %}
  {
     % endif %} 
  {
     % if key == 'son' %}
    {
     {
      fathername ~"'s son is "~ value.name}}
    {
     {
      loop( value.iteritems() ) }}
  {
     % endif %}
{
     % endfor %}
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test11.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

如上例所示,我们定义了一个字典变量,从字典中可以看出,bob的儿子是tom,tom的儿子是jerry,然后我们使用for循环操作了这个字典,如前文所示,我们在操作字典时,使用了iteritems函数,在for循环的末尾,我们添加了recursive 修饰符,当for循环中有recursive时,表示这个循环是一个递归的循环,当我们需要在for循环中进行递归时,只要在需要进行递归的地方调用loop函数即可,没错,如你所见,上例中的"loop( value.iteritems() )"即为调用递归的部分,由于value也是一个字典,所以需要使用iteritems函数进行处理。

渲染上述模板内容,最终效果如下

[root@server3 opt]# cat test
 bob's son is tom
tom's son is jerry 

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

loop.cycle()辅助函数

刚才在总结与循环有关的内置变量时,还提到了一个辅助函数,它就是"loop.cycle()",它能够让我们在指定的一些值中进行轮询取值,这样说可能不够直观,不如来看一个小示例,如下:

[root@server4 jinja2-2]# vim test12.j2
[root@server4 jinja2-2]# cat test12.j2 
{
     % set userlist=['Naruto','Kakashi','Sasuke','Sakura','Lee','Gaara','Itachi']  %}
{
     % for u in userlist %}
{
     {
      u ~'----'~ loop.cycle('team1','team2','team3')}}
{
     %endfor%}
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test12.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

上例中,我们定义了一个用户列表,这个列表里面有一些人,现在,我想要将这些人分组,按照顺序将这些人分别分配到三个组中,直到分完为止,三个组的组名分别为team1、team2、team3,渲染上例的内容,最终生成内容如下:

[root@server3 opt]# cat test 
Naruto----team1
Kakashi----team2
Sasuke----team3
Sakura----team1
Lee----team2
Gaara----team3
Itachi----team1

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从生成的内容可以看出,用户与三个组已经轮询的进行了结合。

四、ansible启用break和continue扩展

刚才我们提到过,默认情况下,模板中的for循环无法使用break和continue,不过jinja2支持一些扩展,如果我们在ansible中启用这些扩展,则可以让模板中的for循环支持break和continue,方法如下:

如果想要开启对应的扩展支持,需要修改ansible的配置文件/etc/ansible/ansible.cfg,默认情况下未启用jinja2的扩展,如果想要启用jinja2扩展,则需要在配置文件132行设置jinja2_extension选项,这个设置项默认情况下是注释的,我的默认设置如下

132 #jinja2_extensions = jinja2.ext.do,jinja2.ext.i18n

 
   
   
   
   
  • 1

把注释符去掉,默认已经有两个扩展了,如果想要支持break和continue,则需要添加一个loopcontrols扩展,最终配置如下

jinja2_extensions = jinja2.ext.do,jinja2.ext.i18n,jinja2.ext.loopcontrols

 
   
   
   
   
  • 1

完成上述配置步骤即可在for循环中使用break和continue控制语句,与其他语言一样,break表示结束整个循环,continue表示结束当次循环,示例如下:

[root@server4 jinja2-2]# vim test13.j2
[root@server4 jinja2-2]# cat test13.j2 
{
     % for i in [7,1,5,3,9] %}
  {
     % if loop.index is even %}
    {
     %continue%}
  {
     %endif%}
  {
     {
      i ~'----'~ loop.index }}
{
     % endfor %}
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test13.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
[root@server3 opt]# cat test
    7----1
          5----3
          9----5

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

上述示例表示偶数次的循环将会跳过,even这个tests在前文中已经总结过,此处不再赘述。

break的示例如下:
[

root@server4 jinja2-2]# vim test14.j2
[root@server4 jinja2-2]# cat test14.j2 
{
     % for i in [7,1,5,3,9] %}
  {
     % if loop.index > 3 %}
    {
     %break%}
  {
     %endif%}
  {
     {
     i ~'---'~ loop.index}}
{
     % endfor %}
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test14.j2 dest=/opt/test"

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
[root@server3 opt]# cat test
    7---1
    1---2
    5---3

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4

上例表示3次迭代以后的元素不会被处理

五、ansible使用do扩展修改列表

如果我们想要在jinja2中修改列表中的内容,则需要借助jinja2的另一个扩展,这个扩展的名字就是"do",我们可以发现,刚才修改jinja2_extensions配置的时候,默认就有这个扩展,它的名字是jinja2.ext.do,通过do扩展修改列表的示例如下:

[root@server4 jinja2-2]# vim test15.j2
[root@server4 jinja2-2]# cat test15.j2 
{
     % set testlist=[3,5] %} 
{
     % for i in testlist  %}
  {
     {
     i}}
{
     % endfor %}
{
     %do testlist.append(7)%} 
{
     % for i in testlist  %}
  {
     {
     i}}
{
     % endfor %}

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
[root@server4 jinja2-2]# ansible testB -m template -a "src=/root/jinja2-2/test15.j2 dest=/opt/test"

 
   
   
   
   
  • 1
[root@server3 opt]# cat test 
  3
  5 
  3
  5
  7

 
   
   
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如上例所示,我们先定义了一个列表,然后遍历了这个列表,使用 do在列表的末尾添加了一个元素,数字7,然后又遍历 了它。

你可能感兴趣的:(Linux Ansible自动化运维 template模块 jinja2模板引擎)