Ansible 的循环

我们在编写 Playbook 的时候,不可避免的要执行一些重复性操作,比如指定安装软件包,批量创建用户,操作某个目录下的所有文件等。Ansible 一门简单的自动化语言,所以流程控制、循环语句这些编程语言的基本元素它同样都具备。 Ansible 提供了两个用于创建循环的关键字: loop 和 with_,Ansible 2.5 中添加了 loop,但它还不是 with_ 的完全替代品。在官方推荐使用 loop,但我们现在还可以在大多数用例中使用 with_,但是随着 loop 语法的不断改进,with_ 以后可能会失效

标准循环

使用 with_items 关键字创建一个循环的列表,with_items 会把列表的每一条信息,单独放到 item 变量里面,然后循环打印每次 item 变量的值

# 方式1
---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: "{{ item }}"
    with_items:
    - 1
    - 2
    - 3

# 方式2
---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug: msg={{ item }}
    with_items: [1,2,3]

# 方式3
---
- hosts: dbserver
  remote_user: root
  vars:
    list:
    - a
    - b
    - c
  tasks:
  - debug: msg={{ item }}
    with_items: '{{ list }}'

# 方式4
---
- hosts: dbserver
  remote_user: root
  vars:
    list: [1,2,3]
  tasks:
  - debug: msg={{ item }}
    with_items: '{{ list }}'

# 添加多个用户
---
- hosts: dbserver
  remote_user: root
  tasks:
  - name: add server users
  user:
    name: "{{ item }}"
    state: present
    groups: server
  with_items:
    - server1
    - server2

定义稍微复杂的列表

自定义列表中的每一个键值对都是一个对象,我们可以通过对象的属性对应的 “键”,获取到对应的 “值”,执行下面的 Playbook 之后,mm 和 nn 都会被输出

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: "{{ item.name }}"
    with_items:
    - { name: mm, age: 23}
    - { name: nn, age: 24}

ansible-playbook item.yaml    # 执行playbook
ok: [dbserver] => (item={u'age': 23, u'name': u'mm'}) => {
    "msg": "mm"
}
ok: [dbserver] => (item={u'age': 24, u'name': u'nn'}) => {
    "msg": "nn"
}

利用循环创建多个文件

# 没学习循环之前可能这样创建多个文件
---
- hosts: dbserver
  remote_user: root
  gather_facts: no
  tasks:
  - file:
      path: '/opt/a'
      state: touch
  - file:
      path: '/opt/b'
      state: touch
  - file:
      path: '/opt/c'
      state: touch
  - file:
      path: '/opt/d'
      state: touch

# 使用循环的方式
---
- hosts: dbserver
  remote_user: root
  gather_facts: no
  vars:
    dirs:
    - '/opt/a'
    - '/opt/b'
    - '/opt/c'
    - '/opt/d'
  tasks:
  - file:
      path: '{{ item }}'
      state: touch
    with_items: '{{ dirs }}'

利用循环多次调用模块

不使用循环的情况下调用模块,返回的信息是这样的

---
- hosts: dbserver
  remote_user: root
  tasks:
  - shell: 'ls /etc'
    register: returnvalue
  - debug:
      var: returnvalue

ansible-playbook item.yaml
ok: [dbserver] => {
    "returnvalue": {
        "changed": true, 
        "cmd": "ls /etc", 
        "delta": "0:00:00.025062", 
        "end": "2020-06-02 01:06:34.741709", 
        "failed": false, 
        "rc": 0, 
        "start": "2020-06-02 01:06:34.716647", 
        "stderr": "", 
        "stderr_lines": [],这里省略......
}

我们使用循环重复调用了 shell 模块两次,分别执行了两条命令,然后将 shell 模块的返回值存放到了 returnvalue 变量中,最后使用 debug 模块输出了 returnvalue 变量的值。当使用了循环之后,每次 shell 模块执行后的返回值都会放入一个名为 results 的序列中,其实,results 也是一个返回值,当模块中使用了循环时,模块每次执行的返回值都会追加存放到 results 这个返回值中,所以,我们可以通过 results 关键字获取到每次模块执行后的返回值

---
- hosts: dbserver
  remote_user: root
  tasks:
  - shell: '{{ item }}'
    with_items:
    - 'ls /etc'
    - 'ls /var'
    register: returnvalue
  - debug:
      var: returnvalue

ansible-playbook item.yaml
ok: [dbserver] => {
    "returnvalue": {
        "changed": true, 
        "msg": "All items completed", 
        "results": [    
            {
                "ansible_loop_var": "item", 
                "changed": true, 
                "cmd": "ls /etc", 
                "delta": "0:00:00.026532", 
                "end": "2020-06-02 01:08:22.264277", 
                "failed": false, 
                "invocation": { 这里省略......
}

先使用循环重复的调用了 shell 模块,然后将 shell 模块每次执行后的返回值注册到了变量 returnvalue 中,之后,在使用 debug 模块时,通过返回值 results 获取到了之前每次执行shell模块的返回值(shell 每次执行后的返回值已经被放入到 item 变量中),最后又通过返回值 stdout 获取到了每次 shell 模块执行后的标准输出

---
- hosts: dbserver
  remote_user: root
  tasks:
  - shell: '{{ item }}'
    with_items:
    - 'ls /etc'
    - 'ls /var'
    register: returnvalue
  - debug:
      msg: '{{ item.stdout }}'
    with_items: '{{ returnvalue.results}}'

打印序列中的序列

with_items 块序列下面有一个自定义列表 [1,2,3],执行 Playbook 会循环打印 [1,2,3] 列表里的每一个值

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: "{{ item }}"
    with_items: [1,2,3]

ok: [dbserver] => (item=1) => {
    "msg": 1
}
ok: [dbserver] => (item=2) => {
    "msg": 2
}
ok: [dbserver] => (item=3) => {
    "msg": 3
}

with_items 块序列下面有两个自定义列表,执行 Playbook 还是会循环打印两个列表里的每一个值

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: "{{ item }}"
    with_items: 
    - [1,2,3]
    - [4,5,6]

ok: [dbserver] => (item=1) => {
    "msg": 1
}
ok: [dbserver] => (item=2) => {
    "msg": 2
}
ok: [dbserver] => (item=3) => {
    "msg": 3
}
ok: [dbserver] => (item=4) => {
    "msg": 4
}
ok: [dbserver] => (item=5) => {
    "msg": 5
}
ok: [dbserver] => (item=6) => {
    "msg": 6
}

当 with_items 块序列下面有两个自定义的列表时,我们如何让 debug 模块将每个小列表作为一个小整体输出,而不应该输出小列表中的每个元素呢?我们可以使用 with_list 关键字,替换上例 Playbook 中的 with_items 关键字

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: "{{ item }}"
    with_list: 
    - [1,2,3]
    - [4,5,6]

ok: [dbserver] => (item=[1, 2, 3]) => {        # with_list块序列只会循环最外层的每一项,而with_items则是循环处理每一个元素
    "msg": [
        1, 
        2, 
        3
    ]
}
ok: [dbserver] => (item=[4, 5, 6]) => {
    "msg": [
        4, 
        5, 
        6
    ]
}

元素对齐合并

with_together 可以将两个列表中的元素对齐合并,如果两个列表元素不一致,缺少的元素值为 null

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: '{{ item }}'
    with_together:
    - [1,2,3]
    - [a,b,c]

ok: [dbserver] => (item=[1, u'a']) => {
    "msg": [
        1, 
        "a"
    ]
}
ok: [dbserver] => (item=[2, u'b']) => {
    "msg": [
        2, 
        "b"
    ]
}
ok: [dbserver] => (item=[3, u'c']) => {
    "msg": [
        3, 
        "c"
    ]
}

元素两两组合

需求:我们需要创建三个目录,这三个目录下面都有相同的子目录,我们使用 ansible-playbook 的方式去循环创建,需要用到 with_cartesian 这个关键字

# 需要创建的目录结构如下:
dir1/sofm    dir1/bin
dir2/sofm    dir2/bin
dir3/sofm    dir3/bin

---
- hosts: dbserver
  remote_user: root
  tasks:
  - file:
      state: directory
      path: '/{{ item[0] }}/{{ item[1] }}'
    with_cartesian:
    - [dir1,dir2,dir3]
    - [sofm,bin]

执行 Playbook 会将两个列表的元素两两组合,使用 item[0] 和 item[1] 来获取每一次循环的值

PLAY [dbserver] ******************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************
ok: [dbserver]

TASK [file] **********************************************************************************************************************
changed: [dbserver] => (item=[u'dir1', u'sofm'])
changed: [dbserver] => (item=[u'dir1', u'bin'])
changed: [dbserver] => (item=[u'dir2', u'sofm'])
changed: [dbserver] => (item=[u'dir2', u'bin'])
changed: [dbserver] => (item=[u'dir3', u'sofm'])
changed: [dbserver] => (item=[u'dir3', u'bin'])

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

列表元素添加索引编号

使用 with_indexed_items 关键字可以为列表的每一个元素添加索引编号,索引编号从0开始,我们可以在出来列表每一项元素的时候获取到索引编号

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: 'index is {{ item[0] }},value is {{ item[1] }}'
    with_indexed_items:
    - index1
    - index2
    - index3

ok: [dbserver] => (item=[0, u'index1']) => {
    "msg": "index is 0,value is index1"
}
ok: [dbserver] => (item=[1, u'index2']) => {
    "msg": "index is 1,value is index2"
}
ok: [dbserver] => (item=[2, u'index3']) => {
    "msg": "index is 2,value is index3"
}

生成数字序列

假如需要在管理节点创建 dir2、dir4、dir6 这样的目录,我们该如何使用循环去创建呢,这里就需要使用到 with_sequence 这个关键字去生成数字序列。debug 模块被调用了4次,从2开始,到8结束,每一次增加2(步长),看到这是不是有了 Python 的感觉呢

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: '{{ item }}'
    with_sequence:
      start=2
      end=8
      stride=2

ok: [dbserver] => (item=2) => {
    "msg": "2"
}
ok: [dbserver] => (item=4) => {
    "msg": "4"
}
ok: [dbserver] => (item=6) => {
    "msg": "6"
}
ok: [dbserver] => (item=8) => {
    "msg": "8"
}

创建 dir2、dir4、dir6 这样不连续的目录

---
- hosts: dbserver
  remote_user: root
  tasks:
  - file:
      path: /dir{{ item }}
      state: directory
    with_sequence:
      start=2
      end=6
      stride=2

输出更简单的连续序列

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: '{{ item }}'
    with_sequence:
      count=5

注意:当我们不指定 start 的值时,start 的值默认为1,但是当 end 的值小于 start 时则必须指定 stride,而且 stride 的值必须是负数

返回一个随机值

使用 with_random_choice 这个关键字可以让我们从一个列表的多个值中随机返回一个值

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: '{{ item }}'
    with_random_choice:
    - qwe
    - rtd
    - fdv
    - oki

ok: [dbserver] => (item=oki) => {
    "msg": "oki"    # 随机返回列表中的一个值
}

使用循环去操作字典

这里我们学习一个叫 with_dict 的字典关键字,下面来看看字典的使用场景

定义一个 users 变量,users 有两个用户,我们使用 with_dict 关键字处理这个字典格式的变量

---
- hosts: dbserver
  remote_user: root
  vars:
    users:    
      alix: feom
      boo: mair
  tasks:
  - debug:
      msg: '{{ item }}'
    with_dict: '{{ users }}'

ok: [dbserver] => (item={'value': u'feom', 'key': u'alix'}) => {
    "msg": {
        "key": "alix",         # users变量经过with_dict处理之后,键值对分别被放入key和value关键字中
        "value": "feom"
    }
}
ok: [dbserver] => (item={'value': u'mair', 'key': u'boo'}) => {
    "msg": {
        "key": "boo",     # 我们可以通过key关键字和value关键字分别获取到字典中键值对的键和值
        "value": "mair"
    }
}

字典定义和取值

---
- hosts: dbserver
  remote_user: root
  vars:
    users:
      alix:
        name: feom
        gender: female
        phone: 155464615
      boo:
        name: mair
        gender: male
        phone: 179444684
  tasks:
  - debug:
      msg: '{{ item }} alix phone is {{ item.value.phone }}'    # 使用item.value.phone的方法取某一项的值
    with_dict: '{{ users }}'

ok: [dbserver] => (item={'value': {u'gender': u'female', u'name': u'feom', u'phone': 155464615}, 'key': u'alix'}) => {
    "msg": "{'key': u'alix', 'value': {u'gender': u'female', u'name': u'feom', u'phone': 155464615}} alix phone \\n is 155464615"
}
ok: [dbserver] => (item={'value': {u'gender': u'male', u'name': u'mair', u'phone': 179444684}, 'key': u'boo'}) => {
    "msg": "{'key': u'boo', 'value': {u'gender': u'male', u'name': u'mair', u'phone': 179444684}} alix phone \\n is 179444684"
}

遍历每一项子元素

users 变量列表中有两个块序列,这两个块序列分别代表两个用户,bob 和alice,变量 users 经过 with_subelements 处理时还指定一个hobby属性,hobby 属性正是 users 变量中每个用户的子属性

---
- hosts: dbserver
  remote_user: root
  vars:
    users:
    - name: bob
      gender: male
      hobby:
        - skateboard
        - videogame
    - name: alice
      gender: female
      hobby:
        - music
    - name: qwe
      hobby:
        - da
  tasks:
  - debug:
      msg: "{{ item }}"
    with_subelements:
    - "{{ users }}"
    - hobby

上面的 Playbook 执行后得到如下结果,我们在使用 with_subelements 处理变量 users 时指定了 hobby 属性,hobby 属性中的每一个子元素都被当做一个整体,而其他的子元素作为另一个整体,组成了键值对

ok: [dbserver] => (item=[{u'gender': u'male', u'name': u'bob'}, u'skateboard']) => {
    "msg": [
        {
            "gender": "male", 
            "name": "bob"
        }, 
        "skateboard"
    ]
}
ok: [dbserver] => (item=[{u'gender': u'male', u'name': u'bob'}, u'videogame']) => {
    "msg": [
        {
            "gender": "male", 
            "name": "bob"
        }, 
        "videogame"
    ]
}
ok: [dbserver] => (item=[{u'gender': u'female', u'name': u'alice'}, u'music']) => {
    "msg": [
        {
            "gender": "female", 
            "name": "alice"
        }, 
        "music"
    ]
}
ok: [dbserver] => (item=[{u'name': u'qwe'}, u'da']) => {
    "msg": [
        {
            "name": "qwe"
        }, 
        "da"
    ]
}

获取控制节点的文件内容

我想要获取控制节点上的几个文件的内容,那么可以使用 with_file 关键字,循环获取到文件的内容,这里 hosts 指定的是 dbserver 这个管理节点,但是无论管理节点写的是什么都不影响,因为我们读取的是管理节点的文件

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: '{{ item }}'
    with_file:
    - /etc/passwd
    - /etc/hosts

ok: [dbserver] => (item=root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin) => {
    "msg": "root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/bin:/sbin/nologin\ndaemon:x:2:2:daemon:/sbin:/sbin/nologin\nadm:x:3:4:adm:/var/adm:/sbin/nologin\nlp:x:4:7:lp:/var/spool/lpd:/sbin/nologin\nsync:x:5:0:sync:/sbin:/bin/sync\nshutdown:x:6:0:shutdown:/sbin:/sbin/shutdown\nhalt:x:7:0:halt:/sbin:/sbin/halt\nmail:x:8:12:mail:/var/spool/mail:/sbin/nologin\noperator:x:11:0:operator:/root:/sbin/nologin\ngames:x:12:100:games:/usr/games:/sbin/nologin\nftp:x:14:50:FTP User:/var/ftp:/sbin/nologin\nnobody:x:99:99:Nobody:/:/sbin/nologin\nsystemd-network:x:192:192:systemd Network Management:/:/sbin/nologin\ndbus:x:81:81:System message bus:/:/sbin/nologin\npolkitd:x:999:998:User for polkitd:/:/sbin/nologin\nsshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin\npostfix:x:89:89::/var/spool/postfix:/sbin/nologin\nntp:x:38:38::/etc/ntp:/sbin/nologin"
}
ok: [dbserver] => (item=127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6) => {
    "msg": "127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4\n::1         localhost localhost.localdomain localhost6 localhost6.localdomain6"
}

匹配控制节点的文件

我们可以通过通配符去匹配控制节点上的文件,这里需要使用到 with_fileglob 这个关键字。注意 with_fileglob 只能是匹配到文件

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: '{{ item }}'
    with_fileglob:
    - /etc/*
    - /tmp/*        # 这里写成/dir/的话是匹配不到文件的,需要使用*通配符

ok: [dbserver] => (item=/etc/fstab) => {
    "msg": "/etc/fstab"
}
ok: [dbserver] => (item=/etc/crypttab) => {
    "msg": "/etc/crypttab"
}
ok: [dbserver] => (item=/etc/mtab) => {
    "msg": "/etc/mtab"
}
ok: [dbserver] => (item=/etc/resolv.conf) => {
    "msg": "/etc/resolv.conf"
}
ok: [dbserver] => (item=/etc/magic) => {
    "msg": "/etc/magic"
}......    

Ansible 的 loop 循环

在2.5版本之前的 Ansible 中,大多数人习惯使用 “with_X” 风格的关键字操作循环,从2.6版本开始,官方开始推荐使用 “loop” 关键字代替 “with_X” 风格的关键字。现在就来聊聊这种新的方式,以便能够更好的从老版本的使用习惯过渡过来

loop标准循环

---
- hosts: dbserver
  remote_user: root
  tasks:
  - debug:
      msg: "{{ item }}"
    loop:
      - abc
      - cde

loop 循环安装软件

---
- hosts: dbserver
  remote_user: root
  tasks:
    - name: install packages
      yum: 
        name: "{{ item }}"
        state: latest
      loop:
        - rsync
        - sl
        - psmisc

loop 批量创建用户

---
- hosts: dbserver
  remote_user:
  tasks:
    - name: "add user"
      user:
        name: "{{ item.name }}"
        state: present
        groups: "{{ item.groups }}"
      loop:
        - {name: "abc",groups: "root"}
        - {name: "cde",groups: "root"}