第 4-3 章 Linux Ansible 实践梳理
前面通过理论和实践结合对 ansible 有了一些了解,在生产上用起来不会有什么大的问题。不过,ansible 还有很多功能和细节没有涉及到,生产上遇到了再说,下面只是针对实践中主要的功能梳理一下,用起来更顺。
4.1、变量
Ansible 的变量很复杂,因为 Ansible 的变量来源很多,查一下官方手册对变量的介绍,相当震撼,还搞了一个优先级,不过前面我们提到的一些变量也能把 ansible 用起来,不必非得都了解。
4.1.1、访问列表、字典变量
Ansible 中经常需要访问列表和字典类型的变量。例如下面的字典类型:
p:
a: aa
b: bb
files:
- /tmp/a.txt
- /tmp/b.txt
对于这类变量的访问,Ansible 中提供了两种方式:
-
按照 Python 字典或列表的索引下标方式访问。例如
p["a"]
、files[0]
-
按照对象访问方式。例如
p.b
、files.0
值得一提的是,如果字典的 key 名称和 Python 的字典方法名冲突了,就会有问题。比如:
p:
a: aa
b: bb
keys: "tic"
如果用 p.keys
来访问这个字典变量会出错,因为 Python 的字典类型有一个称为 keys
的方法名。
4.1.2、–extra-vars 选项
ansible-playbook 命令的 -e
选项或 --extra-vars
选项可定义变量或引入变量文件。
# 定义单个变量
$ ansible-playbook -e 'var1="value1"' xxx.yml
# 定义多个变量
$ ansible-playbook -e 'var1="value1" var2="value2"' xxx.yml
# 引入单个变量文件
$ ansible-playbook -e '@varfile1.yml' xxx.yml
# 引入多个变量文件
$ ansible-playbook -e '@varfile1.yml' -e '@varfile2.yml' xxx.yml
因为是通过选项的方式来定义变量的,所以它所定义的变量是全局的,对所有 play 都有效。
通常来说不建议使用 -e
选项,因为这对用户来说是不透明也不友好的,要求用户记住定义了哪些变量,冲突可能性也比较大。
4.1.3、inventory 变量
在解析 inventory 时,会收集 inventory 相关的变量。
inventory 变量主要分为两种:
-
连接目标节点时的行为控制变量,即决定如何连接目标节点
-
主机变量
行为控制变量(如 ansible_port
、ansible_host
等)用于指定 Ansible 端连接目标节点时的连接参数,可设置的参数项比较多,可参见官方手册。
inventory 的主机变量有多种定义途径,例如直接在 inventory 文件中为某个主机定义变量,也可以在主机组中定义变量,主机组变量会在解析 inventory 的时候整理到主机变量中去。此外还可以将变量定义在 host_vars/
和 group_vars/
目录内。
# 在主机上直接定义变量
[dev]
192.168.200.42 aaa=333 bbb=444 ansible_port=22
192.168.200.43
192.168.200.44
# 在主机组上定义变量
[dev:vars]
xxx=555
yyy=666
ansible_port=22
# 也可以在特殊的主机组ungrouped和all上定义变量
[all]
zzz=777
主机变量除了可以直接定义在 inventory 文件中,还可以定义在和 inventory 文件同目录的 host_vars
和 group_vars
目录中,其中 host_vars
目录中定义主机变量,group_vars
目录中定义主机组变量。
例如,默认的 inventory 文件是 /etc/ansible/hosts,那么可以在 /etc/ansible 目录下创建 host_vars
和 group_vars
目录,并在其中创建一些主机、主机组的变量文件或变量目录:
$ tree /etc/ansible/
/etc/ansible/
├── ansible.cfg
├── hosts
├── group_vars
│ ├── all.yml # all主机组变量文件
│ ├── nginx # nginx主机组变量目录,其内所有文件都会被读取
│ │ ├── main.yml # nginx的主配置变量文件
│ │ └── vhost.yml # nginx的虚拟主机配置变量文件
│ └── php.yml # php主机组变量文件
└── host_vars
└── 192.168.200.42.yml # 192.168.200.42节点的变量文件
上面的目录结构已经解释完所有内容了,这里总结下:
- 定义在
group_vars/
目录中的变量文件可以是普通文件(比如 all.yml、php.yml),也可以是变量目录(比如 nginx 目录)。 - 如果是目录,则目录名必须和主机组名相同,目录中的所有文件都会在解析 inventory 的时候被读取。
- 如果是普通文件,则文件名可带可不带后缀,不带后缀时,文件名和主机组名相同,带后缀时,前缀和主机组名相同,后缀只允许
.yml .yaml .json
。 host_vars/
目录只能为每个节点都单独定义属于它们的变量文件。
记住比较实用的 group_vars/
、host_vars/
存放位置:
- inventory 文件的同目录
- playbook 文件的同目录
所以,下面和 playbook 文件 lnmp.yml 同目录层次的 group_vars/
也是有效的:
# tree -L 2 -F .
.
├── common.yml
├── group_vars/
│ └── all.yml
├── inventory_lnmp
├── lnmp.yml
├── mysql.yml
├── nginx.yml
├── php.yml
└── roles/
├── common/
├── mysql/
├── nginx/
└── php/
在 inventory 文件同目录下创建 {group,host}_vars/
好理解,为什么还要支持在 playbook 同目录下允许这两个目录呢?主要原因还是为了扩充 Role 的能力。
lnmp 实践中,在 nginx Role 中想要跨 Role 访问 php Role 中的变量 phpfpm_port
,其中方法之一也是最友好的方法,就是在 playbook 文件的同目录下提供 {group,host}_vars/
,并在其中设置多个 Role 共享的变量。
也就是说,在 playbook 文件同层次的 {group,host}_vars/
下定义主机或主机组变量,可以实现 playbook 全局变量的功能。任何人都能一眼看到这两个目录,而且只要看到 playbook 文件同目录下有这两个目录,就知道它们定义了全局变量,是所有 Role 都能访问到的变量。
需要说明或提醒几点:
-
不要忘记 all 主机组的存在,为 all 主机组设置变量表示为所有节点设置主机变量
-
主机变量是绑定在主机上的,和 play、task 没有关系,所以这些变量都是全局变量,甚至节点 A 执行任务时还能访问节点 B 的主机变量。
-
所有的主机变量都可以通过
ansible-inventory
工具列出来。 -
所有变量,包括主机变量,都保存在 Ansible 的变量表
hostvars
中,通过这个全局变量表,任何一个节点都能访问其它节点的变量。
4.1.4、Role 变量
Role 中主要有两个地方定义变量:
-
roles/ROLE_NAME/defaults/main.yml
-
roles/ROLE_NAME/vars/main.yml
需要提醒大家,Role defaults 变量的优先级非常低,几乎可以被其它任何同名变量覆盖。
Role 变量都是 play 级别的变量。换句话说,如果 play 中执行了 Role 之后还有 tasks
指令的任务,则 tasks 的任务中可以引用 Role 中的变量。
---
- hosts: localhost
gather_facts: false
roles:
- role: test_role
tasks:
- debug:
var: var_from_role
4.1.5、play 变量
play 级别可以通过 vars
、vars_files
、vars_prompt
指令来定义变量。因为它们属于 play 级别,所以只在当前 play 有效。另一方面,每个 play 都有选中的目标节点,所以所有选中的目标节点都能访问这些 play 变量。
关于 vars
和 vars_files
前面已经解释过,所以不再解释,这里简单介绍下 vars_prompt
指令的用法。
vars_prompt
指令用于交互式提示用户输入数据,并将输入内容赋值给指定的变量。
---
- hosts: localhost
gather_facts: false
vars_prompt:
- name: username
prompt: "Your Name?"
private: no
default: "root"
- name: passwd
prompt: "Your Password"
tasks:
- debug:
msg: "username: {{username}}, password: {{passwd}}"
上面定义了两个变量 username
和 passwd
,都会提示用户输入对应的值。private: no
表示不要隐藏用户输入的字符(默认会隐藏),default
表示指定变量的默认值。
vars_prompt
主要用于保护隐私数据,比如密码,有时候也用于交互式选择,比如让用户自己输入要安装的软件包名称。它的用法不难,更详细的用法参见官方手册。
4.1.6、task 变量
task 变量有多种定义方式,稍作总结:
-
register 指令
-
set_fact 指令
-
vars 指令
-
include_vars 指令
它们的用法都介绍过,下面给个示例看一眼即可:
---
- hosts: localhost
gather_facts: false
tasks:
# var.yml变量文件中定义了变量a
- include_vars:
file: var.yml
- shell: echo tictoc
register: res
- set_fact: name="{{res.stdout}}"
- debug:
msg: "a: {{a}}, name: {{name}}, age: {{age}}"
vars:
age: 18
gender: male
4.1.7、block 变量
block 作为一个特殊的层次级别,它也支持定义变量,只不过这个 block 层次的变量只对当前 block 内的所有任务有效。
---
- hosts: localhost
gather_facts: false
tasks:
- block:
- debug:
var: name
vars:
name: "toc"
4.1.8、预定义特殊变量
Ansible 作为一个功能复杂的程序,它自身也维护了一些暴露给用户的预定义变量,这些变量都是特殊变量(官方也称为魔法变量),它们都是能直接访问的变量,且对用户只读。其实在之前的文章中已经接触了好几个这类变量,比如 hostvars
。
写 playbook 的时候如果想要用关于 play、role、task、inventory、host 等 Ansible 内部信息,可以查找预定义变量官方手册。
- ansible_forks
表示最大的进程数。这也暗含了多少个节点作为一批。(如果忘记了一批是什么意思,可回介绍playbook的那一章末尾复习)
- hostvars
保存了inventory中所有主机和主机变量
- inventory_hostname
当前执行任务的节点在inventory中的主机名
- inventory_hostname_short
当前执行任务的节点在inventory中的短主机名
- inventory_dir
inventory文件所在的目录
- inventory_file
inventory文件名
- group_names
当前正在执行任务的节点所在主机组列表,注意是一个列表,因为一个节点可能存在于多个主机组
- groups
inventory中所有的主机组以及各组内的主机列表
- ansible_play_batch
当前play中可执行任务的主机列表。Ansible动态维护该变量,默认情况下执行任务失败或连接失败的节点会从此变量中移除
- ansible_play_hosts
等价于ansible_play_batch
- play_hosts
已废弃,等价于ansible_play_batch
- playbook_dir
playbook所在目录,该playbook是ansible-playbook命令所执行的playbook,而不是import_playbook导入的playbook
- ansible_play_name
当前正在执行的play的name。Ansible 2.8才添加的变量
- ansible_play_hosts_all
当前play所选中的所有节点,等价于ansible_play_batch + 失败的节点
- ansible_play_role_names
当前play中包含的Role列表。注意,因依赖关系而隐式导入的Role不在列表内
- role_names
已废弃,等价于ansible_play_role_names
- ansible_role_names
当前play中包含的Role列表,包括因依赖关系而隐式导入的Role
- role_name
当前正在执行的Role的名称
- role_path
当前正在执行的Role的路径
ansible_run_tags
所有--tags筛选出来的tag列表
ansible_skip_tags
所有--skip_tags筛选出来的tag列表
ansible_version
Ansible版本号信息,是一个字典,字典的key: full, major, minor, revision以及string
- omit
这是一个非常特殊的变量,可直接忽略一个模块的参数。通常结合Filter和`default(omit)`使用。用法见下文
下面介绍下 omit 变量的用法:
- name: touch files with an optional mode
file:
dest: "{{ item.path }}"
state: touch
mode: "{{ item.mode | default(omit) }}"
loop:
- path: /tmp/foo
- path: /tmp/bar
- path: /tmp/baz
mode: "0444"
上面的示例通过迭代的方式创建多个文件,其中迭代创建前两个文件时,将以 umask 值来设置所创建文件的权限,而第三个文件因为存在 mode,所以将权限设置为 0444。{ item.mode | default(omit) }
的作用是:如果 item.mode
不存在,则忽略 file 模块的 mode 参数,否则 mode 参数则生效。
4.1.9、变量作用域
前面介绍了几种主要的变量类型,除了需要知道它们的用法之外,还需要搞清楚这些变量的生效范围,也即它们的作用域。其实在前面介绍各种变量的时候都提到过它们各自的生效范围,这里做个总结。
Ansible中变量主要有五种作用域概念:
-
全局作用域:Ansible 配置文件、环境变量、命令行选项
-e,--extra-vars
设置的变量都是全局变量。 -
Play 作用域:整个 Play 中都有效的变量,
vars_files
、vars_prompt
、play 级别的vars
以及 Role 的变量,它们都是 play 级别的变量。 -
主机变量:绑定在各主机上的变量,各种方式定义的 inventory 变量、Facts 信息变量(这个就划分在这吧)、
set_fact
、register
、include_vars
都是主机变量。 -
任务变量:只在当前任务中生效的变量,task 级别的
vars
定义的变量属于任务变量。 -
block 变量:只在当前 block 内生效,block 级别的
vars
定义的变量属于 block 变量。
最后还有预定义特殊变量未分类,这些变量由 Ansible 自身内部维护,有些是全局变量,有些是 play 变量,有些是主机变量,所以不方便对它们分类。
4.2、handler
前面介绍过 handler,这里再对其做一点补充,如何触发多个 handler 任务。
另外,还有一个关于”如何解决因某任务失败而导致 handler 未执行”的问题,该内容将在后文介绍异常和错误处理的时候再做补充。
如何触发执行多个 handler 任务?比如,将重启 nginx 的 handler 分为多步:
- 检查 nginx 语法
- 检查 nginx 进程是否已存在
- 如果 nginx 进程已存在,则 reload
- 如果 nginx 进程还不存在,则 start
第一种实现方式,在 handler 任务中使用 notify,将多个任务链在一起。
# check config file syntax
- name: "reload nginx step 1"
shell: |
nginx -c /etc/nginx/nginx.conf -t
changed_when: true
notify: "reload nginx step 2"
# check nginx process is started or not
- name: "reload nginx step 2"
shell: |
killall -0 nginx &>/dev/null
notify:
- "start nginx"
- "reload nginx"
changed_when: true
register: step2
failed_when: false
# start nginx when nginx process is not running
- name: "start nginx"
shell: |
nginx -c /etc/nginx/nginx.conf
when: step2.rc == 1
# reload nginx when nginx is running
- name: "reload nginx"
shell: |
nginx -s reload -c /etc/nginx/nginx.conf
when: step2.rc == 0
第二种方式,在触发 handler 处定义 handler 列表:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- check nginx syntax
- check nginx process
- start nginx
- reload nginx
注意,handler 任务的执行顺序不是根据 notify 顺序决定的,而是根据 handler 任务的定义顺序。
然后定义如下 handlers:
- name: "check nginx syntax"
shell: |
nginx -c /etc/nginx/nginx.conf -t
changed_when: true
- name: "check nginx process"
shell: |
killall -0 nginx &>/dev/null
changed_when: true
register: step2
failed_when: false
- name: "start nginx"
shell: |
nginx -c /etc/nginx/nginx.conf
when: step2.rc == 1
- name: "reload nginx"
shell: |
nginx -s reload -c /etc/nginx/nginx.conf
when: step2.rc == 0
第三种方式,在每个 handler 任务中使用 Ansible 2.2 提供的 listen 指令,它可以监听 notify 发送的信息:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: start or reload nginx
然后定义如下 handlers,每个 handler 都定义 listen: start or reload nginx
,只要 notify 发送 start or reload nginx,则这些任务都会被触发。
- name: "check nginx syntax"
shell: |
nginx -c /etc/nginx/nginx.conf -t
changed_when: true
listen: start or reload nginx
- name: "check nginx process"
shell: |
killall -0 nginx &>/dev/null
changed_when: true
register: step2
failed_when: false
listen: start or reload nginx
- name: "start nginx"
shell: |
nginx -c /etc/nginx/nginx.conf
when: step2.rc == 1
listen: start or reload nginx
- name: "reload nginx"
shell: |
nginx -s reload -c /etc/nginx/nginx.conf
when: step2.rc == 0
listen: start or reload nginx
4.3、when 条件判断
4.3.1、同时满足多个条件
按照一般编程语言的语法,结合多个条件判断语句时要么使用逻辑与符号(通常是 and
或 &&
),要么使用逻辑或符号(通常是 or
或 ||
)。Ansible 当然也支持这种结合方式,例如:
when: item > 3 and item < 10
但如果是想要同时满足多个条件,则可以将这些条件以列表的方式提供。例如:
---
- hosts: localhost
gather_facts: false
tasks:
- debug:
var: item
when:
- item > 3
- item < 5
loop: [1,2,3,4,5,6]
4.3.2、按条件导入文件
Linux 发行版不同,服务名称、软件包名称等都不相同,对于这种因环境不同而配置不同任务的场景,通常是为不同环境编写不同任务文件或不同变量文件,然后根据when的环境判断加载不同环境的文件。
以 Redhat 和 Debian 系列安装 Apache httpd 软件包的任务文件为例:
---
- hosts: localhost
gather_facts: yes
tasks:
- include_tasks: RedHat.yml
when: ansible_os_family == "RedHat"
- include_tasks: Debian.yml
when: ansible_os_family == "Debian"
4.4、循环迭代
4.4.1、with_list
最简单的循环就是迭代一个列表。例如,在 Ansible 本地端的 /tmp 目录下创建两个文件:
---
- hosts: localhost
gather_facts: false
tasks:
- file:
name: "/tmp/{{item}}"
state: touch
with_list:
- "filename1"
- "filename2"
与上面 with_list
等价的 loop 语法:
- file:
name: "/tmp/{{item}}"
state: touch
loop:
- "filename1"
- "filename2"
4.4.2、with_items
有时候列表中会嵌套列表,如果想要迭代嵌套列表结构,使用 with_items
。注意,它也可以迭代普通非嵌套列表,所以它可以完全替代 with_list
。
---
- hosts: localhost
gather_facts: false
vars:
a: [b, c, [d, e], f]
tasks:
- debug:
var: item
with_items: "{{a}}"
注意,with_items
只压平嵌套列表的第一层,不会递归压平第二层、第三层…
[b, c, [d, e], f, [g, h, [i, j], k]]
# with_items压平后得到:
[b, c, d, e, f, g, h, [i, j], k]
与 with_items
等价的 loop 指令的写法为:
loop: "{{ nested_list | flatten(levels=1) }}"
由上面的写法可推测,筛选器函数 flatten()
默认会递归压平所有嵌套列表,如果只是压平第一层,需指定参数 levels=1
。
4.4.3、with_flattened
递归压平所有嵌套层次:
---
- hosts: localhost
gather_facts: false
vars:
a: [b, c, [d, e], f, [g,h,[i,j], k]]
tasks:
- debug:
var: item
with_flattened: "{{a}}"
4.4.4、with_dict
with_dict
用于迭代一个字典结构,迭代时可以使用 item.key
表示每个字典元素的 key,item.value
表示每个字典元素的 value。
---
# test.yml
- hosts: localhost
gather_facts: false
vars:
users:
junmajinlong_key:
name: junmajinlong
age: 18
fairy_key:
name: fairy
age: 22
tasks:
- debug:
msg: "who: {{item.key}} &&
name: {{item.value.name}} &&
age: {{item.value.age}}"
with_dict: "{{users}}"
与 with_dict
等价的 loop 指令有两种写法:
loop: "{{lookup('dict', users)}}"
loop: "{{users | dict2items}}"
4.4.5、循环控制loop_control
loop_control
指令可以控制循环时的特性,该指令有一些参数,每种参数都是一个控制开关。
4.4.5.1、label 参数
使用 label
参数可以自定义迭代时显示的内容来替代默认显示的 item。例如:
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- shell: echo {{item}}
loop: "{{ mylist }}"
register: res
- debug:
var: item.stdout
loop: "{{res.results}}"
loop_control:
label: "toc"
对比一下,看使用 label 和不使用 label 的区别。
4.4.5.2、pause 参数
loop_control
的 pause 参数可以控制每轮迭代之间的时间间隔。
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- debug:
var: item
loop: "{{mylist}}"
loop_control:
pause: 1
这表示第一轮迭代后,等待一秒,再进入第二轮迭代。
4.4.5.3、index_var 参数
index_var
参数可以指定一个变量,这个变量可以记录每轮循环迭代过程中的索引位,也即表示当前是第几轮迭代。
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- debug:
msg: "index: {{idx}}, value: {{item}}"
loop: "{{mylist}}"
loop_control:
index_var: idx
通过 index_var
,可以进行一些条件判断。比如只在第一轮循环时执行某任务:
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- debug:
msg: "index: {{idx}}, value: {{item}}"
when: idx == 0
loop: "{{mylist}}"
loop_control:
index_var: idx
4.4.5.4、extended 参数
从 Ansible 2.8 开始,还支持使用 extended
参数来获取更多循环的信息。
loop_control:
extended: yes
打开 extended
的开关后,将可以在循环内部使用下面一些变量。
变量名 | 含义 |
---|---|
ansible_loop.allitems | 循环中所有的item |
ansible_loop.index | 本轮迭代的索引位,即第几轮迭代(从1开始计数) |
ansible_loop.index0 | 本轮迭代的索引位,即第几轮迭代(从0开始计数) |
ansible_loop.revindex | 本轮迭代的逆向索引位(距离最后一个item的长度,从1开始计数) |
ansible_loop.revindex0 | 本轮迭代的逆向索引位(距离最后一个item的长度,从0开始计数) |
ansible_loop.first | 如果本轮迭代是第一轮,则该变量值为True |
ansible_loop.last | 如果本轮迭代是最后一轮,则该变量值为True |
ansible_loop.length | 循环要迭代的轮数,即item的数量 |
ansible_loop.previtem | 本轮迭代的前一轮的item值,如果当前是第一轮,则该变量未定义 |
ansible_loop.nextitem | 本轮迭代的下一轮的item值,如果当前是最后一轮,则该变量未定义 |
来个示例,循环迭代一个包含 4 个元素的列表,并在第一轮迭代时输出上面所有变量的值。
---
- hosts: localhost
gather_facts: false
tasks:
- debug:
msg:
- "ansible_loop.allitems: {{ansible_loop.allitems}}"
- "ansible_loop.index: {{ansible_loop.index}}"
- "ansible_loop.index0: {{ansible_loop.index0}}"
- "ansible_loop.revindex: {{ansible_loop.revindex}}"
- "ansible_loop.revindex0: {{ansible_loop.revindex0}}"
- "ansible_loop.first: {{ansible_loop.first}}"
- "ansible_loop.last: {{ansible_loop.last}}"
- "ansible_loop.length: {{ansible_loop.length}}"
- "ansible_loop.previtem: {{ansible_loop.previtem | default('')}}"
- "ansible_loop.nextitem: {{ansible_loop.nextitem}}"
when: ansible_loop.first
loop: "{{mylist}}"
loop_control:
extended: yes
vars:
mylist: ['a','b','c','d']
上面的 when 指令使用了 ansible_loop.first
来判断是否是第一轮迭代,通过 ansible_loop.index
也可以判断。此外,刚才也介绍过使用 index_var
的方式来判断处于第几轮判断。
另外,上面的 msg 参数使用了列表形式,如果不使用列表,debug 输出的时候,所有内容都会输出在同一行中,而使用列表则每项都单独成行输出。
4.5、异常
任何程序都有 bug,如果你没有遇到,说明你的程序没有遇到合适的时间,地点,场景,或者用户。
默认情况下,Ansible 端无法连接某个节点时、节点执行某个任务失败时,Ansible 都会将这个节点从活动节点列表中(play_hosts
)移除,以避免该节点继续执行之后的任务。
用户可以去修改 Ansible 对这种异常现象的默认处理方式,比如遇到错误也不让该节点退出舞台,而是继续执行后续任务,又或者某节点执行任务失败就让整个 play 都失败。
4.5.1、fail 模块
使用 fail
模块,可以人为制造一个失败的任务。
---
- hosts: nginx
gather_facts: no
tasks:
- fail:
msg: "oh, not me"
when: inventory_hostname == groups['nginx'][0]
- debug:
msg: "hello"
上面的 fail 会任务失败,并使得此节点不会执行后续任务,但其它节点会继续执行任务。
4.5.2、assert 模块
对于当满足某某条件时就失败的逻辑,可以使用 fail
模块加 when 指令来实现,也可使用更为直接的 assert 模块进行断言。
---
- hosts: localhost
gather_facts: no
tasks:
- assert:
that:
- 100 > 20
- 200 > 200
fail_msg: "oh, not me"
success_msg: "oh, it's me"
其中 that
参数接收一个列表,用于定义一个或多个条件,如果条件全为 true,则任务成功,只要有一个条件为 false,则任务失败。fail_msg
定义任务失败时的信息,success_msg
定义任务成功时的信息。
4.5.3、ignore_errors
当某个任务执行失败(或被 Ansible 认为失败,比如通过返回值判断)时,如果不想让这个失败的任务导致节点退出,可以使用 ignore_errors
指令来忽略失败。
---
- hosts: localhost
gather_facts: no
tasks:
- shell: ls /tmp/klakas/kjlasd8293
ignore_errors: yes
- debug:
msg: "hello world"
4.5.4、failed_when
当条件表达式为 true 时任务强制失败,当条件表达式为 false 时,任务强制不失败。
例如,下面的示例中不管 shell 模块是否正确执行,都认为这个任务成功执行:
---
- hosts: localhost
gather_facts: no
tasks:
- shell: ls /tmp/klakas/kjlasd8293
failed_when: false
- debug:
msg: "hello world"
failed_when
经常会和 shell 或 command 模块以及 register 指令一起使用,用来手动定义失败的退出状态码。比如,退出状态码为 0 1 2
都认为任务成功执行,其它状态码都认为执行失败。
- shell: COMMAND
register: res
failed_when: res.rc not in (0, 1, 2)
如果这时候去查看 res 变量,将会发现多出了一项 failed_when_result
:
{
"changed": true,
"cmd": "ls /tmp/klakas/kjlasd8293",
"delta": "0:00:00.002469",
"end": "2020-01-17 00:18:49.437885",
"failed": false,
"failed_when_result": false,
"msg": "non-zero return code",
"rc": 2,
"start": "2020-01-17 00:18:49.435416",
"stderr": "ls: cannot access /tmp/klakas/kjlasd8293: No such file or directory",
"stderr_lines": [
"ls: cannot access /tmp/klakas/kjlasd8293: No such file or directory"
],
"stdout": "",
"stdout_lines": []
}
failed_when_result
记录了 failed_when
指令的渲染结果是 true 还是 false,本例中渲染结果为 false。
此外,failed_when
和 when
一样都可以将多个条件表达式写成列表的形式来表示逻辑与。例如:
- shell: xxxxxx
register: res
failed_when:
- res.rc != 0
- res.stdout == "yyyyy"
4.5.5、rescue 和 always
Ansible 允许在任务失败的时候,去执行某些任务,还允许不管任务失败与否,都执行某些任务。这功能类似于编程语言的 try…catch
异常捕获。
关于这两个指令:
1、rescue 和 always 都是 block 级别的指令
2、rescue 表示 block 中任意任务失败后,都执行 rescue 中定义的任务,但如果 block 中没有任务失败,则不执行 rescue 中的任务
3、always 表示 block 中任务无论失败与否,都执行 always 中定义的任务
---
- hosts: localhost
gather_facts: no
tasks:
- block:
- fail:
- debug: msg="hello world"
rescue:
- debug: msg="rescue1"
- debug: msg="rescue2"
always:
- debug: msg="always1"
- debug: msg="always2"
block 中的 fail 任务会失败,于是跳转到 rescue 中开始执行任务,然后再跳转到 always 中执行任务。
如果注释掉 block 中的 fail 模块任务,则 block 中没有任务失败,于是 rescue 中的任务不会执行,但是在执行完 block 中所有任务后会跳转到 always 中继续执行任务。
4.5.6、any_errors_fatal
如果想让某个失败的任务直接导致整个 play 的失败,可在 play 级别使用 any_errors_fatal
指令。
---
- hosts: nginx
gather_facts: no
any_errors_fatal: true
tasks:
- fail:
msg: "oh, not me"
when: inventory_hostname == groups['nginx'][0]
- debug:
msg: "hello"
- hosts: localhost
gather_facts: no
tasks:
- debug:
msg: "HELLO WORLD"
将 any_errors_fatal
设置为 true 后,nginx 组第一个节点只要一开始执行 fail 任务,整个 playbook 中所有后续任务都将不再执行,就连其它 play 也一样不执行。
注意观察 playbook 的执行结果,它将提示 ”NO MORE HOSTS LEFT”:
.........
TASK [fail] *********************
fatal: [192.168.200.42]: FAILED! => {"changed": false, "msg": "oh, not me"}
skipping: [192.168.200.43]
skipping: [192.168.200.44]
NO MORE HOSTS LEFT **************
PLAY RECAP *************
.........
4.5.7、异常打断 handler 处理
如果某节点执行某任务失败,但在失败任务之前已经触发了 handler,那么该节点将因为失败而无法执行 handler 任务。
有时候这种默认的异常处理并非理想的处理方式。比如 copy 了一个 nginx 配置文件并触发了重启 Nginx 服务的 handler,但是在重启之前执行某个任务失败了,那么该节点将不会重启 nginx,但配置文件确实已经拷贝过去了且发生改变了。
可以在命令行上使用 --force-handlers
选项,也可在 play 级别使用 force_handlers: true
指令,它们都表示即使该节点执行任务失败了,也会执行已经触发的 handler 任务。
但要注意,只有因任务执行失败的情况才能强制执行 Handler 任务,如果是因为 unreachable 而导致的失败,显然是没有办法的。
--force-handlers
或 force_handlers: true
是对 play 全局生效的,如果想针对单个任务,也可以使用 rescue 或 always 的方式来 flush handler。例如:
tasks:
- block:
- template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
rescue:
- meta: flush_handlers