循环

通常你想在一个任务中干很多事,比如创建一群用户,安装很多包,或者重复一个轮询步骤直到收到某个特定结果.

本章将对在playbook中如何使用循环做全面的介绍.

标准循环

为了保持简洁,重复的任务可以用以下简写的方式:

  1. - name: add several users
  2. user: name={{ item }} state=present groups=wheel
  3. with_items:
  4. - testuser1
  5. - testuser2

如果你在变量文件中或者 ‘vars’ 区域定义了一组YAML列表,你也可以这样做:

  1. with_items: "{{somelist}}"

以上写法与下面是完全等同的:

  1. - name: add user testuser1
  2. user: name=testuser1 state=present groups=wheel
  3. - name: add user testuser2
  4. user: name=testuser2 state=present groups=wheel

yum和apt模块中使用with_items执行时会有较少的包管理事务.

请note使用 ‘with_items’ 用于迭代的条目类型不仅仅支持简单的字符串列表.如果你有一个哈希列表,那么你可以用以下方式来引用子项:

  1. - name: add several users
  2. user: name={{ item.name }} state=present groups={{ item.groups }}
  3. with_items:
  4. - { name: 'testuser1', groups: 'wheel' }
  5. - { name: 'testuser2', groups: 'root' }

请note如果同时使用 whenwith_items (或其它循环声明),when声明会为每个条目单独执行.请参见 the_when_statement 示例.

嵌套循环

循环也可以嵌套:

  1. - name: give users access to multiple databases
  2. mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
  3. with_nested:
  4. - [ 'alice', 'bob' ]
  5. - [ 'clientdb', 'employeedb', 'providerdb' ]

和以上介绍的’with_items’一样,你也可以使用预定义变量.:

  1. - name: here, 'users' contains the above list of employees
  2. mysql_user: name={{ item[0] }} priv={{ item[1] }}.*:ALL append_privs=yes password=foo
  3. with_nested:
  4. - "{{users}}"
  5. - [ 'clientdb', 'employeedb', 'providerdb' ]

对哈希表使用循环

New in version 1.5.

假如你有以下变量:

  1. ---
  2. users:
  3. alice:
  4. name: Alice Appleworth
  5. telephone: 123-456-7890
  6. bob:
  7. name: Bob Bananarama
  8. telephone: 987-654-3210

你想打印出每个用户的名称和电话号码.你可以使用 with_dict 来循环哈希表中的元素:

  1. tasks:
  2. - name: Print phone records
  3. debug: msg="User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
  4. with_dict: "{{users}}"

对文件列表使用循环

with_fileglob 可以以非递归的方式来模式匹配单个目录中的文件.如下面所示:

  1. ---
  2. - hosts: all
  3.  
  4. tasks:
  5.  
  6. # first ensure our target directory exists
  7. - file: dest=/etc/fooapp state=directory
  8.  
  9. # copy each file over that matches the given pattern
  10. - copy: src={{ item }} dest=/etc/fooapp/ owner=root mode=600
  11. with_fileglob:
  12. - /playbooks/files/fooapp/*

Note

当在role中对 with_fileglob 使用相对路径时, Ansible会把路径映射到roles/<rolename>/files目录.

对并行数据集使用循环

Note

这是一个不常见的使用方式,但为了文档完整性我们还是把它写出来.你可能不会经常使用这种方式.

假设你通过某种方式加载了以下变量数据:

  1. ---
  2. alpha: [ 'a', 'b', 'c', 'd' ]
  3. numbers: [ 1, 2, 3, 4 ]

如果你想得到’(a, 1)’和’(b, 2)’之类的集合.可以使用’with_together’:

  1. tasks:
  2. - debug: msg="{{ item.0 }} and {{ item.1 }}"
  3. with_together:
  4. - "{{alpha}}"
  5. - "{{numbers}}"

对子元素使用循环

假设你想对一组用户做一些动作,比如创建这些用户,并且允许它们使用一组SSH key来登录.

如何实现那? 先假设你有按以下方式定义的数据,可以通过”vars_files”或”group_vars/all”文件加载:

  1. ---
  2. users:
  3. - name: alice
  4. authorized:
  5. - /tmp/alice/onekey.pub
  6. - /tmp/alice/twokey.pub
  7. mysql:
  8. password: mysql-password
  9. hosts:
  10. - "%"
  11. - "127.0.0.1"
  12. - "::1"
  13. - "localhost"
  14. privs:
  15. - "*.*:SELECT"
  16. - "DB1.*:ALL"
  17. - name: bob
  18. authorized:
  19. - /tmp/bob/id_rsa.pub
  20. mysql:
  21. password: other-mysql-password
  22. hosts:
  23. - "db1"
  24. privs:
  25. - "*.*:SELECT"
  26. - "DB2.*:ALL"

那么可以这样实现:

  1. - user: name={{ item.name }} state=present generate_ssh_key=yes
  2. with_items: "{{users}}"
  3.  
  4. - authorized_key: "user={{ item.0.name }} key='{{ lookup('file', item.1) }}'"
  5. with_subelements:
  6. - users
  7. - authorized

根据mysql hosts以及预先给定的privs subkey列表,我们也可以在嵌套的subkey中迭代列表:

  1. - name: Setup MySQL users
  2. mysql_user: name={{ item.0.user }} password={{ item.0.mysql.password }} host={{ item.1 }} priv={{ item.0.mysql.privs | join('/') }}
  3. with_subelements:
  4. - users
  5. - mysql.hosts

Subelements walks a list of hashes (aka dictionaries) and then traverses a list with a given key inside of thoserecords.

你也可以为字元素列表添加第三个元素,该元素可以放置标志位字典.现在你可以加入’skip_missing’标志位.如果设置为True,那么查找插件会跳过不包含指定子键的列表条目.如果没有该标志位,或者标志位值为False,插件会产生错误并指出缺少该子键.

这就是authorized_key模式中key的获取方式.

对整数序列使用循环

with_sequence 可以以升序数字顺序生成一组序列.你可以指定起始值、终止值,以及一个可选的步长值.

指定参数时也可以使用key=value这种键值对的方式.如果采用这种方式,’format’是一个可打印的字符串.

数字值可以被指定为10进制,16进制(0x3f8)或者八进制(0600).负数则不受支持.请看以下示例:

  1. ---
  2. - hosts: all
  3.  
  4. tasks:
  5.  
  6. # create groups
  7. - group: name=evens state=present
  8. - group: name=odds state=present
  9.  
  10. # create some test users
  11. - user: name={{ item }} state=present groups=evens
  12. with_sequence: start=0 end=32 format=testuser%02x
  13.  
  14. # create a series of directories with even numbers for some reason
  15. - file: dest=/var/stuff/{{ item }} state=directory
  16. with_sequence: start=4 end=16 stride=2
  17.  
  18. # a simpler way to use the sequence plugin
  19. # create 4 groups
  20. - group: name=group{{ item }} state=present
  21. with_sequence: count=4

随机选择

‘random_choice’功能可以用来随机获取一些值.它并不是负载均衡器(已经有相关的模块了).它有时可以用作一个简化版的负载均衡器,比如作为条件判断:

  1. - debug: msg={{ item }}
  2. with_random_choice:
  3. - "go through the door"
  4. - "drink from the goblet"
  5. - "press the red button"
  6. - "do nothing"

提供的字符串中的其中一个会被随机选中.

还有一个基本的场景,该功能可用于在一个可预测的自动化环境中添加混乱和兴奋点.

Do-Until循环

有时你想重试一个任务直到达到某个条件.比如下面这个例子:

  1. - action: shell /usr/bin/foo
  2. register: result
  3. until: result.stdout.find("all systems go") != -1
  4. retries: 5
  5. delay: 10

上面的例子递归运行shell模块,直到模块结果中的stdout输出中包含”all systems go”字符串,或者该任务按照10秒的延迟重试超过5次.”retries”和”delay”的默认值分别是3和5.

该任务返回最后一个任务返回的结果.单次重试的结果可以使用-vv选项来查看.被注册的变量会有一个新的属性’attempts’,值为该任务重试的次数.

查找第一个匹配的文件

Note

这是一个不常见的使用方式,但为了文档完整性我们还是把它写出来.你可能不会经常使用这种方式.

这其实不是一个循环,但和循环很相似.如果你想引用一个文件,而该文件是从一组文件中根据给定条件匹配出来的.这组文件中部分文件名由变量拼接而成.针对该场景你可以这样做:

  1. - name: INTERFACES | Create Ansible header for /etc/network/interfaces
  2. template: src={{ item }} dest=/etc/foo.conf
  3. with_first_found:
  4. - "{{ansible_virtualization_type}}_foo.conf"
  5. - "default_foo.conf"

该功能还有一个更完整的版本,可以配置搜索路径.请看以下示例:

  1. - name: some configuration template
  2. template: src={{ item }} dest=/etc/file.cfg mode=0444 owner=root group=root
  3. with_first_found:
  4. - files:
  5. - "{{inventory_hostname}}/etc/file.cfg"
  6. paths:
  7. - ../../../templates.overwrites
  8. - ../../../templates
  9. - files:
  10. - etc/file.cfg
  11. paths:
  12. - templates

迭代程序的执行结果

Note

这是一个不常见的使用方式,但为了文档完整性我们还是把它写出来.你可能不会经常使用这种方式.

有时你想执行一个程序,而且按行循环该程序的输出.Ansible提供了一个优雅的方式来实现这一点.但请记住,该功能始终在控制机上执行,而不是本地机器:

  1. - name: Example of looping over a command result
  2. shell: /usr/bin/frobnicate {{ item }}
  3. with_lines: /usr/bin/frobnications_per_host --param {{ inventory_hostname }}

好吧,这好像有点随意.事实上,如果你在做一些与inventory有关的事情,比如你想编写一个动态的inventory源(参见 动态 Inventory),那么借助该功能能够快速实现.

如果你想远程执行命令,那么以上方法则不行.但你可以这样写:

  1. - name: Example of looping over a REMOTE command result
  2. shell: /usr/bin/something
  3. register: command_result
  4.  
  5. - name: Do something with each result
  6. shell: /usr/bin/something_else --param {{ item }}
  7. with_items: "{{command_result.stdout_lines}}"

使用索引循环列表

Note

这是一个不常见的使用方式,但为了文档完整性我们还是把它写出来.你可能不会经常使用这种方式.

如果你想循环一个列表,同时得到一个数字索引来标明你当前处于列表什么位置,那么你可以这样做.虽然该方法不太常用:

  1. - name: indexed loop demo
  2. debug: msg="at array position {{ item.0 }} there is a value {{ item.1 }}"
  3. with_indexed_items: "{{some_list}}"

循环配置文件

ini插件可以使用正则表达式来获取一组键值对.因此,我们可以遍历该集合.以下是我们使用的ini文件:

  1. [section1]
  2. value1=section1/value1
  3. value2=section1/value2
  4.  
  5. [section2]
  6. value1=section2/value1
  7. value2=section2/value2

以下是使用 with_ini 的例子:

  1. - debug: msg="{{item}}"
  2. with_ini: value[1-2] section=section1 file=lookup.ini re=true

以下是返回的值:

  1. {
  2. "changed": false,
  3. "msg": "All items completed",
  4. "results": [
  5. {
  6. "invocation": {
  7. "module_args": "msg=\"section1/value1\"",
  8. "module_name": "debug"
  9. },
  10. "item": "section1/value1",
  11. "msg": "section1/value1",
  12. "verbose_always": true
  13. },
  14. {
  15. "invocation": {
  16. "module_args": "msg=\"section1/value2\"",
  17. "module_name": "debug"
  18. },
  19. "item": "section1/value2",
  20. "msg": "section1/value2",
  21. "verbose_always": true
  22. }
  23. ]
  24. }

扁平化列表

Note

这是一个不常见的使用方式,但为了文档完整性我们还是把它写出来.你可能不会经常使用这种方式.

在罕见的情况下,你可能有几组列表,列表中会嵌套列表.而你只是想迭代所有列表中的每个元素.比如有一个非常疯狂的假定的数据结构:

  1. ----
  2. # file: roles/foo/vars/main.yml
  3. packages_base:
  4. - [ 'foo-package', 'bar-package' ]
  5. packages_apps:
  6. - [ ['one-package', 'two-package' ]]
  7. - [ ['red-package'], ['blue-package']]

你可以看到列表中的包到处都是.那么如果想安装两个列表中的所有包那?:

  1. - name: flattened loop demo
  2. yum: name={{ item }} state=installed
  3. with_flattened:
  4. - packages_base
  5. - packages_apps

这就行了!

循环中使用注册器

当对处于循环中的某个数据结构使用 register 来注册变量时,结果包含一个 results 属性,这是从模块中得到的所有响应的一个列表.

以下是在 with_items 中使用 register 的示例:

  1. - shell: echo "{{ item }}"
  2. with_items:
  3. - one
  4. - two
  5. register: echo

返回的数据结构如下,与非循环结构中使用 register 的返回结果是不同的:

  1. {
  2. "changed": true,
  3. "msg": "All items completed",
  4. "results": [
  5. {
  6. "changed": true,
  7. "cmd": "echo \"one\" ",
  8. "delta": "0:00:00.003110",
  9. "end": "2013-12-19 12:00:05.187153",
  10. "invocation": {
  11. "module_args": "echo \"one\"",
  12. "module_name": "shell"
  13. },
  14. "item": "one",
  15. "rc": 0,
  16. "start": "2013-12-19 12:00:05.184043",
  17. "stderr": "",
  18. "stdout": "one"
  19. },
  20. {
  21. "changed": true,
  22. "cmd": "echo \"two\" ",
  23. "delta": "0:00:00.002920",
  24. "end": "2013-12-19 12:00:05.245502",
  25. "invocation": {
  26. "module_args": "echo \"two\"",
  27. "module_name": "shell"
  28. },
  29. "item": "two",
  30. "rc": 0,
  31. "start": "2013-12-19 12:00:05.242582",
  32. "stderr": "",
  33. "stdout": "two"
  34. }
  35. ]
  36. }

随后的任务可以用以下方式来循环注册变量,用来检查结果值:

  1. - name: Fail if return code is not 0
  2. fail:
  3. msg: "The command ({{ item.cmd }}) did not have a 0 return code"
  4. when: item.rc != 0
  5. with_items: "{{echo.results}}"

自定义迭代

虽然你通常无需自定义实现自己的迭代,但如果你想按你自己的方式来循环任意数据结构,你可以阅读:doc:developing_plugins 来作为开始.以上的每个功能都以插件的方式来实现,所以有很多的实现可供引用.

See also