06 实施任务控制
1. 编写循环和条件任务
1.1 利用循环迭代任务
通过利用循环,我们无需编写多个使用同一模块的任务。例如,他们不必编写五个任务来确保存在五个用户,而是只需编写一个任务来对含有五个用户的列表迭代,从而确保它们都存在。
Ansible支持使用loop关键字对一组项目迭代任务。可以配置循环以利用列表中的各个项目、列表中各个文件的内容、生成的数字序列或更为复杂的结构来重复任务。
1.1.1 简单循环
简单循环对一组项目迭代任务。loop关键字添加到任务中,将应对其迭代任务的项目列表取为值。循环变量item保存每个迭代过程中使用的值。
请思考以下代码片段,它使用两次service模块来确保两个网络服务处于运行状态:
[root@master project]
---
- hosts: httpd
tasks:
- name: Apache is running
service:
name: httpd
state: started
- name: Php is runing
service:
name: php-fpm
state: started
[root@master project]
PLAY [httpd] **************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [httpd]
TASK [Apache is running] **************************************************************************************
ok: [httpd]
TASK [Php is runing] ******************************************************************************************
ok: [httpd]
PLAY RECAP ****************************************************************************************************
httpd : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
这两个任务可以重新编写为使用一个简单循环,从而只需一个任务来确保两个服务都在运行:
[root@master project]
---
- hosts: httpd
tasks:
- name: Service is running
service:
name: "{{ item }}"
state: started
loop:
- httpd
- php-fpm
[root@master project]
PLAY [httpd] **************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [httpd]
TASK [Apache is running] **************************************************************************************
ok: [httpd] => (item=httpd)
ok: [httpd] => (item=php-fpm)
PLAY RECAP ****************************************************************************************************
httpd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
可以通过一个变量提供loop所使用的列表。在以下示例中,变量mail_services含有需要处于运行状态的服务的列表。
[root@master project]
---
- hosts: httpd
vars:
start_service:
- httpd
- php-fpm
tasks:
- name: Service is running
service:
name: "{{ item }}"
state: started
loop: "{{ start_service }}"
[root@master project]
PLAY [httpd] **************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [httpd]
TASK [Service is running] *************************************************************************************
ok: [httpd] => (item=httpd)
ok: [httpd] => (item=php-fpm)
PLAY RECAP ****************************************************************************************************
httpd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
1.1.2 循环散列或字典列表
loop列表不需要是简单值列表。在以下示例中,列表中的每个项实际上是散列或字典。示例中的每个散列或字典具有两个键,即name和groups,当前item循环变量中每个键的值可以分别通过item.name和item.groups变量来检索。
[root@master project]
---
- hosts: httpd
tasks:
- name: Create groups
group:
name: "{{ item }}"
state: present
system: yes
loop:
- jerry
- tom
- name: Users exist and are in the correct groups
user:
name: "{{ item.name }}"
state: present
system: yes
groups: "{{ item.groups }}"
loop:
- name: alice
groups: jerry
- name: natasha
groups: tom
[root@master project]
PLAY [httpd] **************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [httpd]
TASK [Create groups] ******************************************************************************************
changed: [httpd] => (item=jerry)
changed: [httpd] => (item=tom)
TASK [Users exist and are in the correct groups] **************************************************************
changed: [httpd] => (item={'name': 'alice', 'groups': 'jerry'})
changed: [httpd] => (item={'name': 'natasha', 'groups': 'tom'})
PLAY RECAP ****************************************************************************************************
httpd : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
这一示例中结果是用户alice存在且为组jerry的成员,并且用户joe存在且为组tom的成员
1.1.3 较早样式的循环关键字
在Ansible2.5之前,大多数playbook使用不同的循环语法。提供了多个循环关键字,前缀为whth_,后面跟Ansible查找插件的名称。这种循环语法在现有playbook中很常见,但在将来的某个时候可能会被弃用。
较早样式的Ansible循环
循环关键字 | 描述 |
---|
with_items | 行为与简单列表的loop关键字相同,例如字符串列表或散列/字典列表。 但与loop不同的是,如果为with_items提供了列表的列表,它们将被扁平化为单级列表。循环变量item保存每次迭代过程中使用的列表项。 | with_file | 此关键字需要控制节点文件名列表。循环变量item在每次迭代过程中保存文件列表中相应文件的内容。 | with_sequence | 此关键字不需要列表,而是需要参数来根据数字序列生成值列表。 循环变量item在每次迭代过程中保存生成的序列中的一个生成项的值。 |
playbook中的with_items的示例如下所示:
[root@master project]
---
- hosts: httpd
tasks:
vars:
data:
- user1
- user2
- user3
tasks:
- name: "with_items "
debug:
msg: "{{ item }} "
with_items: "{{ data }}"
[root@master project]
[WARNING]: While constructing a mapping from /opt/site1.yaml, line 2, column 5, found a duplicate dict key
(tasks). Using last defined value only.
PLAY [httpd] **************************************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [httpd]
TASK [with_items] *********************************************************************************************
ok: [httpd] => (item=user1) => {
"msg": "user1 "
}
ok: [httpd] => (item=user2) => {
"msg": "user2 "
}
ok: [httpd] => (item=user3) => {
"msg": "user3 "
}
PLAY RECAP ****************************************************************************************************
httpd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
从Ansible2.5开始,建议使用loop关键字编写循环。
1.1.4 将Register变量与Loop一起使用
register关键字也可以捕获循环任务的输出。以下代码片段显示了循环任务中register变量的结构:
[root@master project]
---
- name: Loop Register Test
hosts: httpd
gather_facts: no
tasks:
- name: Looping Echo Task
shell: "echo You are the best {{ item }} "
loop:
- one
- two
register: echo_results
- name: Show echo_results variable
debug:
var: echo_results
[root@master project]
PLAY [Loop Register Test] *************************************************************************************
TASK [Looping Echo Task] **************************************************************************************
changed: [httpd] => (item=one)
changed: [httpd] => (item=two)
TASK [Show echo_results variable] *****************************************************************************
ok: [httpd] => {
"echo_results": {
"changed": true,
"msg": "All items completed",
"results": [
{
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo You are the best one ",
"delta": "0:00:00.002302",
"end": "2021-07-25 20:45:44.261775",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo You are the best one ",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true,
"warn": true
}
},
"item": "one",
"rc": 0,
"start": "2021-07-25 20:45:44.259473",
"stderr": "",
"stderr_lines": [],
"stdout": "You are the best one",
"stdout_lines": [
"You are the best one"
]
},
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo You are the best two ",
"delta": "0:00:00.002334",
"end": "2021-07-25 20:45:44.532415",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo You are the best two ",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true,
"warn": true
}
},
"item": "two",
"rc": 0,
"start": "2021-07-25 20:45:44.530081",
"stderr": "",
"stderr_lines": [],
"stdout": "You are the best two",
"stdout_lines": [
"You are the best two"
]
}
]
}
}
PLAY RECAP ****************************************************************************************************
httpd : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
在上面的例子中,results键包含一个列表。在下面,修改了playbook,使第二个任务迭代此列表:
[root@master project]
---
- name: Loop Register Test
hosts: httpd
gather_facts: no
tasks:
- name: Looping Echo Task
shell: "echo You are the best {{ item }} "
loop:
- one
- two
register: echo_results
- name: Show echo_results variable
debug
msg: "STDOUT from previous task: {{ item.stdout }}"
loop: "{{ echo_results['results'] }}"
[root@master project]
PLAY [Loop Register Test] *************************************************************************************
TASK [Looping Echo Task] **************************************************************************************
changed: [httpd] => (item=one)
changed: [httpd] => (item=two)
TASK [Show echo_results variable] *****************************************************************************
ok: [httpd] => (item={'cmd': 'echo You are the best one ', 'stdout': 'You are the best one', 'stderr': '', 'rc': 0, 'start': '2021-07-25 20:49:23.158506', 'end': '2021-07-25 20:49:23.160898', 'delta': '0:00:00.002392', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'echo You are the best one ', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['You are the best one'], 'stderr_lines': [], 'ansible_facts': {'discovered_interpreter_python': '/usr/libexec/platform-python'}, 'failed': False, 'item': 'one', 'ansible_loop_var': 'item'}) => {
"msg": "STDOUT from previous task: You are the best one"
}
ok: [httpd] => (item={'cmd': 'echo You are the best two ', 'stdout': 'You are the best two', 'stderr': '', 'rc': 0, 'start': '2021-07-25 20:49:23.412420', 'end': '2021-07-25 20:49:23.415124', 'delta': '0:00:00.002704', 'changed': True, 'invocation': {'module_args': {'_raw_params': 'echo You are the best two ', '_uses_shell': True, 'warn': True, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['You are the best two'], 'stderr_lines': [], 'failed': False, 'item': 'two', 'ansible_loop_var': 'item'}) => {
"msg": "STDOUT from previous task: You are the best two"
}
PLAY RECAP ****************************************************************************************************
httpd : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
1.2 有条件地运行任务
Ansible可使用conditionals在符合特定条件时执行任务或play。例如,可以利用一个条件在Ansible安装或配置服务前确定受管主机上的可用内存。
我们可以利用条件来区分不同的受管主机,并根据它们所符合的条件来分配功能角色。Playbook变量、注册的变量和Ansible事实都可通过条件来进行测试。可以使用比较字符串、数字数据和布尔值的运算符。
以下场景说明了在Ansible中使用条件的情况:
- 可以在变量中定义硬限制(如min_memory)并将它与受管主机上的可用内存进行
- Ansible可以捕获并评估命令的输出,以确定某一任务在执行进一步操作前是否已经完成。例如,如果某一程序失败,则将路过批处理。
- 可以利用Ansible事实来确定受管主机网络配置,并决定要发送的模板文件(如,网络绑定或中继)。
- 可以评估CPU的数量,来确定如何正确调节某一Web服务器。
- 将注册的变量与预定义的变量进行比较,以确定服务是否已更改。例如,测试服务配置文件的MD5检验以和查看服务是否已更改。
1.2.1 条件任务语法
when语句用于有条件地运行任务。它取要测试的条件为值。如果条件满足,则运行任务。如果条件不满足,则跳过任务。
可以测试的一个最简单条件是某一布尔变量是True还是False。以下示例中的when语句导致任务仅在run_my_task为True时运行:
[root@master project]
---
- name: Simple Boolean Task Demo
hosts: httpd
vars:
run_my_task: True
tasks:
- name: httpd package is installed
yum:
name: httpd
when: run_my_task
[root@master project]
PLAY [Simple Boolean Task Demo] *******************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [httpd]
TASK [httpd package is installed] *****************************************************************************
ok: [httpd]
PLAY RECAP ****************************************************************************************************
httpd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
以下示例测试my_service变量是否具有值。若有值,则将my_service的值用作要安装的软件包的名称。如果未定义my_service变量,则跳过任务且不显示错误。
[root@master project]
---
- name: Simple Boolean Task Demo
hosts: httpd
vars:
my_service: httpd
tasks:
- name: "{{ my_service }} package is installed "
yum:
name: "{{ my_service }} "
when: my_service is defined
[root@master project]
PLAY [Simple Boolean Task Demo] *******************************************************************************
TASK [Gathering Facts] ****************************************************************************************
ok: [httpd]
TASK [httpd package is installed] *****************************************************************************
ok: [httpd]
PLAY RECAP ****************************************************************************************************
httpd : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
下表显示了在处理条件时可使用的一些运算: 示例条件
操作 | 示例 |
---|
等于(值为字符串) | ansible_machine == “x86_64” | 等于(值为数字) | max_memory == 512 | 小于 | min_memory < 128 | 大于 | min_memory > 256 | 小于等于 | min_memory <= 256 | 大于等于 | min_memory >= 512 | 不等于 | min_memory != 512 | 变量存在 | min_memory is defined | 变量不存在 | min_memory is not defined | 布尔变量是True。1、True或yes的求值为True | memory_available | 布尔变量是False。0、False或no的求值为False | not memory_available | 第一个变量的值存在,作为第二个变量的列表中的值 | ansible_distribution in supported_distros |
上表中的最后一个条目初看起来有些令人困惑。下例演示了它的工作原理。
在示例中,ansible_distribution变量是在b任务期间确定的事实,用于标识托管主机的操作系统分支。变量supported_distros由playbook创建,包含该playbook支持的操作系统分发列表。如果ansible_distribution的值在supported_distros列表中,则条件通过且任务运行。
---
- name: Demonstrale the "in" keyword
hosts: 192.168.98.129
gather_facts: yes
vars:
supported_distros:
- RedHat
- Fedora
tasks:
- name: Install httpd using yum, where supported
yum:
name: httpd
state: present
when: ansible_distribution in supported_distros
注意when语句的缩进。由于when语句不是模块变量,它必须通过缩进到任务的最高级别,放置在模块的外面。
任务是YAML散列/字典,when语句只是任务中的又一个键,就如任务的名称以及它所使用的模块一样。通常的惯例是将可能存在的任何when关键字放在任务名称和模块(及模块参数)的后面。
1.2.2 测试多个条件
一个when语句可用于评估多个条件。使用and和or关键字组合条件,并使用括号分组条件。
如果任一条件为真时满足条件语句,则应当使用or语句。例如,如果计算机上运行的是红帽企业linux或Fedora,则下述条件得到满足:
when: ansible_distribution == "Redhat" or ansible_distribution == "Fedora"
使用and运算时,两个条件都必须为真,才能满足整个条件语句。例如,如果远程主机是红帽企业Linux7.5主机,并且安装的内核是指定版本,则将满足以下条件:
when: ansible_distribution_version == "7.5" and ansible_kernel == "3.10.0-327.el7.x86_64"
when关键字还支持使用列表来描述条件列表。向 when关键字提供列表时,将使用and运算组合所有条件。下面的示例演示了使用and运算符组合多个条件语句的另一方式:
when:
- ansible_distribution_version == "7.5"
- ansible_kernel == "3.10.0-327.el7.x86_64"
这种格式提高了可读性,而可读性是良好编写Ansible Playbook的关键目标。
通过使用括号分组条件,可以表达更复杂的条件语句。例如,如果计算机上运行的是红帽企业Linux7或Fedora28,则下述条件语句得到满足。此示例使用大于字符,这样长条件就可以在playbook中分成多行,以便于阅读。
when: >
( ansible_distribution == "Redhat" and
ansible_distribution_major_version == "7" )
or
( ansible_distribution == "Fedora" and
ansible_distribution_major_version == "28" )
1.3 组合循环和有条件任务
循环和条件可以组合使用。
在下例中,yum模块将安装mariadb-server软件包,只要/上挂载的文件系统具有超过300MB的可用空间。ansible_mounts事实是一组字典,各自代表一个已挂载文件系统的相关事实。循环迭代列表中每一字典,只有找到了代表两个条件都为真的已挂载文件系统的字典时,条件语句才得到满足。
- name: install mariadb-server if enough space on root
yum:
name: mariadb-server
state: latest
loop: "{{ ansible_mounts }}"
when: item.mount == "/" and item.size_available > 300000000
对某个任务结合使用when和loop时,将对每个项检查when语句。
下面是组合使用条件和注册变量的另一个示例。
---
- name: Restart HTTPD if Postfix is Running
hosts: 192.168.91.129
tasks:
- name: Get Postfix server status
command: /usr/bin/systemctl is-active postfix
ignore_errors: yes
register: result
- name: Restart Apache HTTPD based on Postfix status
service:
name: httpd
state: restarted
when: result.rc == 0
|