S_lion's Studio

(九)ansible流程控制

字数统计: 3.5k阅读时长: 17 min
2021/12/01 Share

流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的“经脉”。

条件判断

ansible提供的条件判断只有when指令,因为可以写Jinja2条件判断表达式,所以判断方式比较灵活。

同时满足多个条件

常见的编程语言在多条件判断时要么使用逻辑与(and&&),要么使用逻辑或(or||)。ansible同样支持这种写法。

1
when: age > 18 and age < 30

也可以将这些条件以列表的方式提供。例如:

1
2
3
4
5
6
7
8
9
- hosts: localhost
gather_facts: false
tasks:
- debug:
var: item
when:
- item > 3
- item < 5
loop: [1,2,3,4,5,6]

按条件导入文件

1
2
3
4
5
6
7
8
9
---
- hosts: localhost
gather_facts: yes
tasks:
- include_tasks: RedHat.yml
when: ansible_os_family == "RedHat"
- include_tasks: Centos.yml
when: ansible_os_family == "Centos"

更加精炼的写法可以参考

1
2
3
4
5
---
- hosts: localhost
gather_facts: yes
tasks:
- include_tasks: "{{ansible_os_family}}.yml"

when和循环

当when指令和循环指令一起使用时,when的判断操作在每轮循环内执行。详细内容下文中描述。

循环迭代

ansible 2.5之前的循环迭代都使用with_xxx来完成,比如with_items,后面加入了loop指令,loop指令与with_list指令等价。

with_xxx语法都使用对应的lookup插件来实现(比如with_list使用的是lookup的list插件),如果存在某lookup插件xxx,就可以使用with_xxx来迭代。

with_list

直接上例子:

1
2
3
4
5
6
7
8
9
- hosts: localhost
gather_facts: false
tasks:
- file:
name: "{{item}}"
state: touch
with_list:
- aaa
- bbb

与上面with_list等价的loop语法:

1
2
3
4
5
6
- file:
name: "{{item}}"
state: touch
loop:
- aaa
- bbb

with_items和with_flattened

with_list用于迭代简单列表,有时候列表中会嵌套列表。

1
2
3
4
5
6
7
8
- hosts: localhost
gather_facts: false
vars:
name: [a,[b1,b2],c]
tasks:
- debug:
var: "{{item}}"
with_items: "{{name}}"

注意,with_items只压平嵌套列表的第一层,不会递归压平第二层、第三层…

与with_items等价的loop指令的写法为:

1
loop: "{{ nested_list | flatten(levels=1) }}"

筛选器函数flatten()默认会递归压平所有嵌套列表,如果只是压平第一层,需指定参数levels=1。

此外,还存在lookup插件:items、flattened,前者只压第一层,后者递归压平所有嵌套层次。例如:

1
2
3
4
5
6
7
8
- hosts: localhost
gather_facts: false
vars:
name: [a,[b1,b2],c,[d1,d2,[e1,e2,e3]]]
tasks:
- debug:
var: "{{item}}"
with_flattened: "{{name}}"

with_dict

with_dict用于迭代一个字典结构,迭代时可以使用item.key表示每个字典元素的key,item.value表示每个字典元素的value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- hosts: localhost
gather_facts: false
vars:
users:
slions:
name: slions
age: 29
zhangsan:
name: zhangsan
age: 18
tasks:
- debug:
msg: "who: {{item.key}}
name: {{item.value.name}}
age: {{item.value.age}}"
with_dict: "{{users}}"

with_dict等价的loop指令有两种写法:

1
2
loop: "{{lookup('dict', users)}}"
loop: "{{users | dict2items}}"

另外,在Ansible 2.8中可以自定义dict2items筛选器函数得到的key和value的名称。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- hosts: localhost
gather_facts: false
vars:
users:
slions:
name: slions
age: 29
zhangsan:
name: zhangsan
age: 18
tasks:
- debug:
msg: "who: {{item.k}}
name: {{item.v.name}}
age: {{item.v.age}}"
loop: "{{users|dict2items(key_name='k',value_name='v')}}"

with_sequence

Ansible的lookup插件sequence也可以用来生成连续数(Jinja2的range也可以生成连续数)。其中:

  • start参数指定序列的起始数,不指定该参数时默认从1开始
  • end参数指定序列的终止数
  • stride参数指定序列的步进值。不指定该参数时,步进为1
  • format参数指定序列的输出格式,遵循printf风格
  • count参数指定生成序列数的个数,不能和end参数共存

此外,sequence插件的各个参数可以简写为如下格式:

1
[start-]end[/stride][:format]

例如

1
2
3
4
5
6
7
---
- hosts: localhost
gather_facts: false
tasks:
- debug:
msg: "_{{item}}_"
with_sequence: start=0 end=5 stride=2 format=a%02d

执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PLAY [localhost] *********************************************************************************************

TASK [debug] *************************************************************************************************
ok: [localhost] => (item=a00) => {
"msg": "_a00_"
}
ok: [localhost] => (item=a02) => {
"msg": "_a02_"
}
ok: [localhost] => (item=a04) => {
"msg": "_a04_"
}

PLAY RECAP ***************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

也可简写为:

1
with_sequence: 0-5/2:a%02d

因为生成的每个序列数都会经过字符串格式化,所以得到的每个序列元素都是字符串。如果想要转换成数值,需使用Jinja2的Filter。例如:

1
2
3
4
5
6
7
---
- hosts: localhost
gather_facts: false
tasks:
- debug:
msg: "{{1 + item|int}}"
with_sequence: start=0 end=3

与with_sequence等价的loop写法为:

1
2
3
- debug:
msg: "{{ 'a%02d' | format(item) }}"
loop: "{{ range(0, 5, 2)|list }}"

Jinja2的range()也可以生成序列数。语法:

1
range(start,end,step)

注意range()不包含结尾数end。

with_fileglob

with_fileglob用于迭代通配到的每个文件名。

例如:

1
2
3
4
5
6
7
8
9
10
---
- hosts: localhost
gather_facts: no
tasks:
- copy:
src: "{{item}}"
dest: /tmp/
with_fileglob:
- /etc/m*.conf
- /etc/*.cnf

执行结果为:

1
2
3
4
5
6
7
8
9
PLAY [localhost] *********************************************************************************************

TASK [copy] **************************************************************************************************
changed: [localhost] => (item=/etc/man_db.conf)
changed: [localhost] => (item=/etc/mke2fs.conf)
changed: [localhost] => (item=/etc/my.cnf)

PLAY RECAP ***************************************************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

with_lines

with_lines用于迭代命令输出结果的每一行。

这功能也是非常实用的,如下示例:find找出一堆文件,然后进行操作(比如copy)。

1
2
3
4
5
6
7
8
9
---
- hosts: localhost
gather_facts: false
tasks:
- copy:
src: "{{item}}"
dest: /home/myscript/
with_lines:
- find ~ -maxdepth 1 -type f -name "*.sh"

循环和when

with_xxxloop指令和when指令一起使用时,when将在循环的内部进行条件判断。也就是说,when决定每轮迭代时是否执行一个任务,而不是决定整个循环是否进行。

循环和register

1
2
3
4
5
6
7
8
9
10
11
12
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11, 22]
tasks:
- debug:
var: item
loop: "{{ mylist }}"
register: res
- debug:
var: res

其中第二个debug任务的执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ok: [localhost] => {
"res": {
"changed": false,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
"changed": false,
"failed": false,
"item": 11
},
{
"ansible_loop_var": "item",
"changed": false,
"failed": false,
"item": 22
}
]
}
}

再来一个shell模块的示例:

1
2
3
4
5
6
7
8
9
10
11
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- shell: echo {{item}}
loop: "{{ mylist }}"
register: res
- debug:
var: res

其中第二个任务的执行结果为:

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
28
29
30
31
32
33
34
35
36
37
38
39
ok: [localhost] => {
"res": {
"changed": true,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo 11",
"delta": "0:00:00.001643",
"end": "2021-12-02 11:40:05.012729",
"failed": false,
"invocation": {
"module_args": {
......
}
},
"item": 11,
"rc": 0,
"start": "2021-12-02 11:40:05.011086",
"stderr": "",
"stderr_lines": [],
"stdout": "11",
"stdout_lines": [
"11"
]
},
{
"ansible_loop_var": "item",
......
"stdout": "22",
"stdout_lines": [
"22"
]
}
]
}
}

可见,当register和循环指令结合时,会将每轮迭代的模块执行结果以一个字典的方式追加在一个名为results的列表中。即:

1
2
3
4
5
6
7
8
9
10
11
12
"res": {
"changed": true,
"msg": "All items completed",
"results": [
{
...第一轮迭代模块返回值...
},
{
...第二轮迭代模块返回值...
}
]
}

所以,可使用res.results来访问每轮的迭代结果。例如,再次迭代遍历这些结果:

1
2
3
4
5
6
7
8
9
10
11
12
---
- 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参数

循环迭代时,每轮迭代过程中都会将当前的item输出(要么输出到屏幕,要么输出到日志)。如果所迭代的每项的内容(即每个item)很短,这倒无所谓,但如果item的内容很长,则可读性比较差。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- shell: echo {{item}}
loop: "{{ mylist }}"
register: res
- debug:
var: item.stdout
loop: "{{res.results}}"

输出如下:

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
28
29
30
31
32
33
34
35
36
TASK [debug] *****************************************************************************************************
ok: [localhost] => (item={u'stderr_lines': [], u'ansible_loop_var': u'item', u'end': u'2021-12-02 11:57:33.161219', u'stderr': u'', u'stdout': u'11', u'changed': True, u'failed': False, u'delta': u'0:00:00.040706', u'cmd': u'echo 11', u'item': 11, u'rc': 0, u'invocation': {u'module_args': {u'warn': False, u'executable': None, u'_uses_shell': True, u'strip_empty_ends': True, u'_raw_params': u'echo 11', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin_add_newline': True, u'stdin': None}}, u'stdout_lines': [u'11'], u'start': u'2021-12-02 11:57:33.120513'}) => {
"ansible_loop_var": "item",
"item": {
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo 11",
"delta": "0:00:00.040706",
"end": "2021-12-02 11:57:33.161219",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo 11",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true,
"warn": false
}
},
"item": 11,
"rc": 0,
"start": "2021-12-02 11:57:33.120513",
"stderr": "",
"stderr_lines": [],
"stdout": "11",
"stdout_lines": [
"11"
]
},
"item.stdout": "11"

使用label参数可以自定义迭代时显示的内容来替代默认显示的item。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
- 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: "this is test play"

输出为:

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
28
29
30
31
32
33
34
35
36
TASK [debug] *****************************************************************************************************
ok: [localhost] => (item=this is test play) => {
"ansible_loop_var": "item",
"item": {
"ansible_loop_var": "item",
"changed": true,
"cmd": "echo 11",
"delta": "0:00:00.034184",
"end": "2021-12-02 12:21:47.385021",
"failed": false,
"invocation": {
"module_args": {
"_raw_params": "echo 11",
"_uses_shell": true,
"argv": null,
"chdir": null,
"creates": null,
"executable": null,
"removes": null,
"stdin": null,
"stdin_add_newline": true,
"strip_empty_ends": true,
"warn": false
}
},
"item": 11,
"rc": 0,
"start": "2021-12-02 12:21:47.350837",
"stderr": "",
"stderr_lines": [],
"stdout": "11",
"stdout_lines": [
"11"
]
},
"item.stdout": "11"

pause参数

loop_controlpause参数可以控制每轮迭代之间的时间间隔。

以下示例表示第一轮迭代后,等待一秒,再进入第二轮迭代。

1
2
3
4
5
6
7
8
9
10
11
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- debug:
var: item
loop: "{{mylist}}"
loop_control:
pause: 1

index_var参数

index_var参数可以指定一个变量,这个变量可以记录每轮循环迭代过程中的索引位,也即表示当前是第几轮迭代。

1
2
3
4
5
6
7
8
9
10
11
---
- hosts: localhost
gather_facts: false
vars:
mylist: [11,22]
tasks:
- debug:
msg: "index: {{idx}}, value: {{item}}"
loop: "{{mylist}}"
loop_control:
index_var: idx

输出结果:

1
2
3
4
5
6
ok: [localhost] => (item=11) => {
"msg": "index: 0, value: 11"
}
ok: [localhost] => (item=22) => {
"msg": "index: 1, value: 22"
}

通过index_var,可以进行一些条件判断。比如只在第一轮循环时执行某任务:

1
2
3
4
5
6
7
8
9
10
11
12
---
- 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

其他流程控制

pause模块

Ansible中,可以使用pause模块或wait_for模块来实现睡眠等待的功能,先简单演示pause模块的用法。

pause可以等待几分钟、几秒钟、等待交互式输入确定。

例如,先睡眠10秒,再执行debug任务:

1
2
3
4
5
6
7
8
---
- hosts: localhost
gather_facts: false
tasks:
- pause:
seconds: 10
- debug:
msg: "hello world"

睡眠1分钟:

1
2
- pause: 
minutes: 1

交互式输入Enter键确认:

1
2
3
4
tasks:
- pause:
- debug:
msg: "hello world"

带提醒的交互式输入:

1
2
- pause:
prompt: "输入你的用户名!"

隐藏用户的输入:

1
2
3
- pause:
prompt: "输入你的用户名"
echo: no

将用户交互式输入内容注册成变量:

1
2
3
4
5
6
7
8
9
10
---
- hosts: localhost
gather_facts: false
tasks:
- pause:
prompt: "输入用户密码"
echo: no
register: passwd
- debug:
msg: "{{passwd.user_input}}"

wait_for模块

wait_for模块可以等待多种事件的发生,常用的功能有:

  1. 等待端口打开和端口关闭
  2. 等待没有活动连接(在等待移除某个负载均衡节点时可能会有用)\
  3. 等待文件被创建或移除
  4. 等待或睡眠指定秒数
  5. 等待系统重启(即等待SSH连接重新建立)
  6. 等待文件中出现某个字符串
  7. 等待进程退出

常见用法:

睡眠几秒后,任务继续。

1
2
3
# 休眠五秒,然后任务继续
- wait_for:
timeout: 5

等待文件存在后,任务继续。

1
2
3
4
5
- wait_for:
path: /tmp/a.log
deley: 3 # 3秒后才开始进入第一轮事件等待检查(默认值为0)
sleep: 1 # 每隔1秒进行一次事件等待检查(默认值为1)
timeout: 20 # 最多等待20秒(默认值为300),如果20秒内未等待到事件发生,则wait_for任务失败并报错

等待文件不存在后,任务继续。

1
2
3
- wait_for:
path: /tmp/a.log
state: absent

等待进程不存在后,任务继续。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- hosts: localhost
gather_facts: no
tasks:
- pids:
name: "sleep"
register: sleep_pids

- wait_for:
path: "/proc/{{item}}"
state: absent
loop: "{{sleep_pids.pids}}"

- debug:
msg: 'hello world'

pids模块可以根据进程名获取进程PID列表(可能是空列表、单元素列表、多元素列表)。

注意该模块要求先安装python的psutil模块,所以如果要使用pids,可执行:

$ yum install python3-devel
$ pip3 install psutil

等待文件中出现某字符串后,任务继续。

1
2
3
- wait_for:
path: /tmp/a.log
search_regex: completed|finished

等待某端口打开,然后任务继续。

1
2
3
4
5
6
7
8
9
10
11
- wait_for:
port: "{{ item }}"
state: started
delay: 3
timeout: 100
loop:
- 10251
- 10252
- 2379
- 6443
- 8443
CATALOG
  1. 1. 条件判断
    1. 1.1. 同时满足多个条件
    2. 1.2. 按条件导入文件
    3. 1.3. when和循环
  2. 2. 循环迭代
    1. 2.1. with_list
    2. 2.2. with_items和with_flattened
    3. 2.3. with_dict
    4. 2.4. with_sequence
    5. 2.5. with_fileglob
    6. 2.6. with_lines
    7. 2.7. 循环和when
    8. 2.8. 循环和register
    9. 2.9. 循环控制
      1. 2.9.1. label参数
      2. 2.9.2. pause参数
      3. 2.9.3. index_var参数
  3. 3. 其他流程控制
    1. 3.1. pause模块
    2. 3.2. wait_for模块