RHCE9学习指南 第32章 jinja2模板的使用
可以使用copy模块把本地的一个文件拷贝到远端,下面再次复习一下。
本章的练习都在目录/home/lduan/demo4/中练习,先把这个目录创建出来并把ansible.cfg和hosts拷贝进去,命令如下。
[lduan@server ~]$ mkdir demo4
[lduan@server ~]$ cp ansible.cfg hosts demo4
[lduan@server ~]$ cd demo4
[lduan@server demo4]$
练习1:
用copy拷贝一个文件到db主机组。
有一个文件aa.txt,内容如下。
[lduan@server demo4]$ cat aa.txt
+--------------------------------------------------+
| 我的IP地址是: {{ansible_default_ipv4.address}}
+--------------------------------------------------+
[lduan@server demo4]$
这个文件中包含一个fact变量 ansible_default_ipv4.address。
写一个playbook,内容如下。
[lduan@server demo4]$ cat 1.yaml
---
- hosts: db
tasks:
- name: 拷贝一个文件到远端主机
copy: src=aa.txt dest=/opt/aa.txt
[lduan@server demo4]$
运行此playbook,结果如下。
[lduan@server demo4]$ ansible-navigator -m stdout run 1.yaml
...输出...
TASK [拷贝一个文件到远端主机]
changed: [server2]
changed: [server3]
...输出...
[lduan@server demo4]$
现在已经把本地的aa.txt拷贝到server2和server3的/opt目录中了,那么现在查看这两台主机上/opt/aa.txt的内容。
[lduan@server demo4]$ ansible db -m shell -a "cat /opt/aa.txt"
server2 | CHANGED | rc=0 >>
+--------------------------------------------------+
| 我的IP地址是: {{ansible_default_ipv4.address}}
+--------------------------------------------------+
server3 | CHANGED | rc=0 >>
+--------------------------------------------------+
| 我的IP地址是: {{ansible_default_ipv4.address}}
+--------------------------------------------------+
[lduan@server demo4]$
这里可以看到,当用copy拷贝一个文件到远端机器,如果这个文件中如果有变量,拷贝过去的文件中的变量并不会变成具体的值。
如果希望文件拷贝过去之后,文件中的变量变成具体的值,那么就不能使用copy模块而是要使用template模块了。
练习2:
修改1.yaml的内容如下。
[lduan@server demo4]$ cat 1.yaml
---
- hosts: db
tasks:
- name: 拷贝一个文件到远端主机
template: src=aa.txt dest=/opt/aa.txt
[lduan@server demo4]$
这里与刚才比,只是把copy换成了template模块了。template模块的用法和copy一致,所以这里选项并没有变。运行此playbook,命令如下。
[lduan@server demo4]$ ansible-navigator -m stdout run 1.yaml
...输出...
TASK [拷贝一个文件到远端主机]
changed: [server2]
changed: [server3]
...输出...
[lduan@server demo4]$
再次查看两台主机上/opt/aa.txt的内容。
[lduan@server demo4]$ ansible db -m shell -a "cat /opt/aa.txt"
server2 | CHANGED | rc=0 >>
+--------------------------------------------------+
| 我的IP地址是: 192.168.26.102
+--------------------------------------------------+
server3 | CHANGED | rc=0 >>
+--------------------------------------------------+
| 我的IP地址是: 192.168.26.103
+--------------------------------------------------+
[lduan@server demo4]$
可以看到,通过template拷贝含有变量的文件时,拷贝到远端机器之后,文件中的变量会换成具体的值。
这个通过template拷贝的、含有变量的文件,我们称之为jinja2模板,jinja2文件的后缀一般使用j2,这不是必需的,但是建议使用j2作为后缀。
所以,需要修改aa.txt的文件为aa.j2:
[lduan@server demo4]$ mv aa.txt aa.j2
同时修改1.yaml中对应的内容,如下所示。
- hosts: db
tasks:
- name: 拷贝一个文件到远端主机
template: src=aa.j2 dest=/opt/aa.txt
这里如果jinja2模板文件没有写路径,例如,例子中src=aa.j2的aa.j2没写路径,则优先到当前目录的templates中找aa.j2,如果没有,然后到当前目录中找aa.j2。
练习3:
验证,命令如下。
[lduan@server demo4]$ mkdir templates
在templates目录中创建aa.j2,内容如下。
[lduan@server demo4]$ cat templates/aa.j2
+--------------------------------------------------+
| 我的主机名是: {{ansible_fqdn}}
+--------------------------------------------------+
[lduan@server demo4]$
这样我们一共有了两个aa.j2了,还有一个是当前目录下的aa.j2,如下所示。
[lduan@server demo4]$ cat aa.j2
+--------------------------------------------------+
| 我的IP地址是: {{ansible_default_ipv4.address}}
+--------------------------------------------------+
[lduan@server demo4]$
再次运行此playbook,命令如下。
[lduan@server demo4]$ ansible-navigator -m stdout run 1.yaml
然后查看两台主机中/opt/aa.txt的内容,命令如下。
[lduan@server demo4]$ ansible db -m shell -a "cat /opt/aa.txt"
server2 | CHANGED | rc=0 >>
+--------------------------------------------------+
| 我的主机名是: server2.rhce.cc
+--------------------------------------------------+
server3 | CHANGED | rc=0 >>
+--------------------------------------------------+
| 我的主机名是: server3.rhce.cc
+--------------------------------------------------+
[lduan@server demo4]$
可以看到,显示的主机名,所以是templates目录中的aa.j2生效。
32.1 if判断
jinja2模板文件中,我们也是可以使用if判断语句的,语法格式如下。
{% if 判断1 %}
内容1
{% elif 判断2 %}
内容2
...多个elif ...
{% else %}
内容3
{% endif %}
注1:%两边有没有空格都可以,不过所有的%前后空格要保持一致,即要有都有要没有都没有。
注2:if和elif中的内容如果太长了,是可以另起一行写的。
如果判断1成立,则打印内容1,后面的判断不再判断,直接跳转到endif后面的内容,如果判断1不成立,则elif后面的判断2,如果成立则打印内容2,后面的判断不再判断,直接跳转到endif后面的内容。以此类推,如果所有的if和elif都不成立,则打印else中的内容。
注意3:elif和else不是必需的。
练习1:
写一个jinja2模板,内容如下。
[lduan@server demo4]$ cat templates/bb.j2
1111
{% if ansible_fqdn=="server2.rhce.cc" %}
{{ansible_fqdn}}
{% else %}
aaaa
{% endif %}
3333
[lduan@server demo4]$
这里jinja2模板所生成的文件一共会产生三行内容,第一行的1111 和第三行的3333 是必打印出来的,第二行的内容具体是什么要看情况。如果在server2上执行,则显示主机名,如果在其他机器上执行则显示aaaa。
写一个playbook内容如下,命令如下。
[lduan@server demo4]$ cat 2.yaml
---
- hosts: db
tasks:
- name: 拷贝一个文件过去
template: src=bb.j2 dest=/opt/bb.conf
[lduan@server demo4]$
这里是把templates/bb.j2拷贝到两台机器的/opt中并命名为bb.conf,通过下面的命令运行此playbook。
ansible-playbook 2.yaml
查看两台机器上/opt/bb.conf的内容,命令如下。
[lduan@server demo4]$ ansible db -m shell -a "cat /opt/bb.conf"
server2 | CHANGED | rc=0 >>
1111
server2.rhce.cc
2222
server3 | CHANGED | rc=0 >>
1111
aaaa
3333
[lduan@server demo4]$
可以看到,server2的/opt/bb.conf的第二行显示的是主机名, server3的/opt/bb.conf第二行显示的是aaaa。
在if和elif后面是可以写多个判断的,用or或and作为连接符。语法如下。
判断1 or 判断11 这里判断1和判断11只要有一个成立就算是成立,只有全不成立,整体才不成立。
判断1 and 判断11 这里判断1和判断11要有全部成立才算成立,只要有一个不成立就算不成立。
查看下面的模板文件。
[lduan@server demo4]$ cat templates/cc.j2
1111
{% if ansible_fqdn=="server2.rhce.cc"
and
ansible_distribution_major_version=="7" %}
{{ansible_fqdn}}
{% else %}
aaaa
{% endif %}
3333
[lduan@server demo4]$
这个jinja2模板会打印3行内容,第一行和第三行内容是写死的,为1111和3333。第二行内容是什么,要看是否满足条件这里判断被管主机名server2.rhce.cc及系统主版本号为7,二者都要满足第二行才会显示主机名,否则现在aaaa。需要注意的是,这里if判断语句太长,特意写成了3行也是没问题的。
playbook文件内容如下。
[lduan@server demo4]$ cat 3.yaml
---
- hosts: db
tasks:
- name: 我要拷贝一个文件过去
template: src=cc.j2 dest=/opt/cc.conf
[lduan@server demo4]$
通过下面的命令运行此playbook。
ansible-playbook 3.yaml
查看两台机器上/opt/cc.conf的内容,命令如下。
[lduan@server demo4]$ ansible db -m shell -a "cat /opt/cc.conf"
server2 | CHANGED | rc=0 >>
1111
aaaa
3333
server3 | CHANGED | rc=0 >>
1111
aaaa
3333
[lduan@server demo4]$
32.2 for循环
一个列表中有多个元素,如果需要依次对列表中的每个元素操作,则可以使用for循环来实现,for循环的语法如下。
{% for i in 列表名 %}
{{i}}
{% endfor %}
这里首先把列表中的第一个元素赋值给i,然后执行中间的操作,之后把第二个元素赋值给i然后执行中间的操作,以此类推直到把最后一个元素赋值给i。看下面的例子。
[lduan@server demo4]$ cat templates/dd.conf.j2
{% set list1=['aa','bb','cc'] %}
1111
{% for i in list1 %}
{{i}}
{% endfor %}
5555
[lduan@server demo4]$
这里手动的在jinja2模板中定义了一个列表(注意定义列表的方式)list1,里面有3个元素分别为aa, bb, cc。然后对这个列表的内容进行循环。
这个jinja2模板生成的文件会有5行内容,第1行和第5行是写死的,为1111和5555。第2~4行时循环list1列表中的值,为aa、bb、cc。
写一个playbook内容如下。
[lduan@server demo4]$ cat 4.yaml
---
- hosts: server2
tasks:
- name: 拷贝一个文件到远端主机
template: src=dd.j2 dest=/opt/dd.conf
[lduan@server demo4]$
通过下面的命令运行此playbook。
ansible-navigator -m stdout run 4.yaml
然后查看两台机器上/opt/dd.conf的内容。
[lduan@server demo4]$ ansible server2 -m shell -a "cat /opt/dd.conf"
server2 | CHANGED | rc=0 >>
1111
aa
bb
cc
5555
[lduan@server demo4]$
除了jinja2模板中手动定义列表,一般情况下,我们会在playbook中定义列表,然后对列表中的元素进行循环。
练习:
写一个变量文件users_list.txt,里面包含一个名称为users的列表,命令如下。
[lduan@server demo4]$ cat users_list.txt
users:
- uname: tom
age: 20
sex: man
- uname: bob
age: 21
sex: man
- uname: mary
age: 22
sex: woman
- uname: wangw
age: 23
sex: man
[lduan@server demo4]$
在templates目录下写一个ee.j2,里面写一个for语句循环users列表,内容如下。
[lduan@server demo4]$ cat templates/ee.j2
现在公司中所有的员工姓名是:
{% for i in users %}
{{i.uname}}
{% endfor %}
[lduan@server demo4]$
循环每个元素时,只打印元素中的uname变量。写一个playbook 5.yaml,加载变量文件users_list.txt内容如下。
[lduan@server demo4]$ cat 5.yaml
---
- hosts: server2
vars_files:
- users_list.txt
tasks:
- name: 拷贝一个文件到远端主机
template: src=ee.j2 dest=/opt/ee.conf
[lduan@server demo4]$
这里通过template模块把ee.j2拷贝到被管理主机的/opt/中命名为ee.conf,命令如下。
[lduan@server demo4]$ ansible server2 -m shell -a "cat /opt/ee.conf"
server2 | CHANGED | rc=0 >>
现在公司中所有的员工姓名是:
tom
bob
mary
wangw
[lduan@server demo4]$
查看被管理主机的/opt/ee.conf,里面包括的都是users列表中所有的用户名。
32.3 handlers
前面讲了模板的使用,但是后期我们可能需要修改模板的内容,然后重新拷贝到各个机器,此时需要重启httpd才会生效,先看下面的例子。
先获取httpd.conf的配置文件,获取的httpd.conf中没有任何的空白行和注释行。
[lduan@server demo4]$ egrep -v '#|^#' /etc/httpd/conf/httpd.conf > httpd.conf.j2
[lduan@server demo4]$
修改此httpd.conf.j2的第三行,把原来Listen 后面的端口80换成{{myport}},让httpd.conf.j2引用myport变量,内容如下。
[lduan@server demo4]$ head -3 httpd.conf.j2
ServerRoot "/etc/httpd"
Listen {{myport}}
Include conf.modules.d/*.conf
[lduan@server demo4]$
为了不让例子变的太复杂,先临时关闭server2上的selinux,命令如下。
[root@server2 ~]# setenforce 0
[root@server2 ~]# getenforce 0
Permissive
[root@server2 ~]#
编写名称为hand-1.yaml的playbook,内容如下。
[lduan@server demo4]$ cat hand-1.yaml
---
- hosts: server2
vars:
myport: 80
tasks:
- name: task1安装httpd
yum: name=httpd state=installed
- name: task2拷贝配置文件
template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
- name: task3启动httpd服务
service: name=httpd state=started
[lduan@server demo4]$
第一个task用于安装httpd,第二个task用于把模板httpd.conf.j2拷贝到被管机器,第三个task用于启动httpd服务,第一次是可以正常运行的。
下面我们修改myport的值为808,内容如下。
[lduan@server demo4]$ cat hand-1.yaml
---
- hosts: server2
vars:
myport: 808
tasks:
- name: task1安装httpd
yum: name=httpd state=installed
- name: task2拷贝配置文件
template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
- name: tasdk3启动httpd服务
service: name=httpd state=started
[lduan@server demo4]$
再次执行此playbook文件,因为httpd已经安装过了,状态并没有发生任何的改变,所以这次第一个task不会执行。因为改变了myport的值,httpd.conf.j2中的端口发生也就了变化,第二个task再次把模板文件复制到被管机器。因为httpd已经处于启动状态,所以第三个task也并不会执行,从而导致第二个task拷贝过去的新的模板文件并不会生效,因为httpd并没有重启。
下面修改hand-1.yaml内容如下。
[lduan@server demo4]$ cat hand-1.yaml
---
- hosts: server2
vars:
myport: 808
tasks:
- name: task1安装httpd
yum: name=httpd state=installed
- name: task2拷贝配置文件
template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
- name: task3启动httpd服务
service: name=httpd state=restarted
[lduan@server demo4]$
这里把第三个task中started改成了restarted,如果我们修改了myport的值之后执行此playbook时,第二个task会正常执行,因为httpd.conf.conf.j2发生了变化。第三个task总是会执行,因为第三个task中state的值为restarted,这样所做的修改会生效。
但是如果什么都不修改,只是重复执行playbook,第三个task也会重启。所以,这里就有一个问题了,第三个task中state不论写started还是restarted,都不正确。
写started,配置文件修改了不会重启,所以不会生效,
写restarted,配置文件不修改也会重启。
但我们想要的是只有第二个task发生了变化才会重启httpd,否则是不需要重启的,这种情况下,就需要使用handler了。
hander的用法如图32-1所示。
图32-1 hander的用法
需要注意的是,这里handlers和tasks是同级的。
在tasks中定义了3个task,在handlers中定义了2个handler。当运行playbook时,handlers中定义的hander并不会运行,他们只是在被触发时才会运行。在某个task中写了一个notify,其后面的值要和handlers中的某个handler的name进行匹配。
例如,图33-1中,编号2号中nodify后面的值是aaa,这个值跟第一个handler(编号4)的name值一样,这样当运行playbook时,如果编号2这个task执行了,则会触发编号4这个handler。
修改hand-1.yaml内容如下。
[lduan@server demo4]$ cat hand-1.yaml
---
- hosts: server2
vars:
myport: 808
tasks:
- name: task1安装httpd
yum: name=httpd state=installed
- name: task2拷贝配置文件
template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
notify: restart httpd1
- name: task3启动httpd服务
service: name=httpd state=started
handlers:
- name: restart httpd1
service: name=httpd state=restarted
- name: restart httpd2
service: name=httpd state=restarted
[lduan@server demo4]$
第三个task的state仍然设置为started,只要httpd是启动的,第三个task就不会执行。
这里定义两个hander,名称分别是"restart httpd1"和"restart httpd2"。在第二个task中通过notify指定了第一个handler即"restart httpd1"。
如果不修改myport变量的值,则第二个task不会执行,从而不会触发“restart httpd1”。如果myport的值发生了变化,则第二个task会执行,从而触发“restart httpd1”,重启httpd服务使得我们所做的修改生效。
作业
1.已经存在了一个变量文件varfile1,内容如下。
[lduan@server ~]$ cat varfile1
users:
- uname: tom
age: 20
sex: man
- uname: bob
age: 22
sex: man
- uname: mary
age: 20
sex: woman
[lduan@server ~]$
写一个jinja2文件aa.j2,要求此文件第一行和最后一行分别是111和222,然后以一行一个用户名的格式,用for循环把users列表中所有的用户名(uname的值)写在111和222两行之间。
2.写一个名称为chap32-1.yaml的playbook,把aa.j2拷贝到server2的/opt目录并命名为aa.conf。