Ansible playbook之任务控制

文章目录

任务控制

使用循环迭代任务

​ 通过使用循环,我们无需编写多个使用同一模块的任务。例如,他们不必编写五个任务来确保存在五个用户,而是只需编写一个任务来对含有五个用户的列表迭代,从而确保它们都存在。

Ansible支持使用loop关键字对一组项目迭代任务。可以配置循环以利用列表中的各个项目、列表中各个文件的内容、生成的数字序列或更为复杂的结构来重复任务。

简单循环

​ 简单循环对一组项目迭代任务。loop关键字添加到任务中,将应对其迭代任务的项目列表取为值。循环变量item保存每个迭代过程中使用的值。

比较:

创建三个用户:

[root@localhost ansible]# cat vars/user.yml 
---
 - name: create user
   hosts: "*"
   gather_facts: no
   tasks:
     - name: create three user
       user:
         name: tom
         state: present
     - name: create harry
       user:     
         name: harry
         state: present
     - name: create jarry
       user:    
         name: jarry
         state: present      

创建三个用户可以重新编写为使用一个简单循环:

---
 - name: create user
   hosts: "*"
   tasks:
     - name: create user three
       user:
         name: "{{ item }}"
         state: present
       loop:
         - ton
         - jurry
         - herry         

可以通过一个变量提供loop所使用的列表。在以下示例:

---
 - name: create user
   hosts: "*"
   vars:
     users_1:
        - tob
        - jnrry
        - horry     
   tasks:
     - name: create user three
       user:
         name: "{{ item }}"
         state: present
       loop: "{{ users_1 }}"

循环散列或字典列表

loop列表不需要是简单值列表。在以下示例中,列表中的每个项实际上是散列或字典。示例中的每个散列或字典具有两个键,即namegroups,当前item循环变量中每个键的值可以分别通过item.nameitem.groups变量来检索。

---
 - name: create user
   hosts: "*"
   tasks:
     - name: create user three
       user:
         name: "{{ item.name }}"
         state: present
         groups: "{{ item.groups }}"
       loop:
         - name: jone
           groups: harry
         - name: joun
           groups: tom         

这一示例中结果是用户jone存在且为组harry的成员,并且用户joun存在且为组tom的成员。

较早样式的循环关键字

​ 在Ansible2.5之前,大多数playbook使用不同的循环语法。提供了多个循环关键字,前缀为whth_,后面跟Ansible查找插件的名称。这种循环语法在现有playbook中很常见,但在将来的某个时候可能会被弃用。

较早样式的Ansible循环

循环关键字 描述
with_items 行为与简单列表的loop关键字相同,例如字符串列表或散列/字典列表。 但与loop不同的是,如果为with_items提供了列表的列表, 它们将被扁平化为单级列表。循环变量item保存每次迭代过程中使用的列表项。
with_file 此关键字需要控制节点文件名列表。循环变量item在每次迭代过程中保存文件列表中相应文件的内容。
with_sequence 此关键字不需要列表,而是需要参数来根据数字序列生成值列表。 循环变量item在每次迭代过程中保存生成的序列中的一个生成项的值。

playbook中的with_items的示例如下所示:

---
 - name: create user
   hosts: "*"
   vars:
     qwq:
       - user1
       - user2
       - user3      
   tasks:
     - name: create user three
       user:
         name: "{{ item }}"
         state: present 
       with_items: "{{ qwq }}"

从Ansible2.5开始,建议使用loop关键字编写循环。

将Register变量与Loop一起使用

register关键字也可以捕获循环任务的输出。以下代码片段显示了循环任务中register变量的结构:

---
 - name: create user
   hosts: "*"       
   tasks:
     - name: test
       command: "echo 'hello {{ item }},how are you.'" 
       loop:
         - tom
         - jarry
         - alis
       register: result
     - debug:                            # 将结果输出到屏幕
           var: result                            
[root@localhost ansible]# ansible-playbook user.yml
......
"stdout_lines": [
     "hello tom,how are you."
"stdout_lines": [
    "hello jarry,how are you."
"stdout_lines": [
    "hello alis,how are you."
.....

有条件地运行任务

​ Ansible可使用conditionals在符合特定条件时执行任务或play。例如,可以利用一个条件在Ansible安 装或配置服务前确定受管主机上的可用内存。

我们可以利用条件来区分不同的受管主机,并根据它们所符合的条件来分配功能角色。Playbook变量、注册的变量和Ansible事实都可通过条件来进行测试。可以使用比较字符串、数字数据和布尔值的运算符。

以下场景说明了在Ansible中使用条件的情况:

  • 可以在变量中定义硬限制(如min_memory)并将它与受管主机上的可用内存进行比较。
  • Ansible可以捕获并评估命令的输出,以确定某一任务在执行进一步操作前是否已经完成。例如,如果某一程序失败,则将跳过批处理。
  • 可以利用Ansible事实来确定受管主机网络配置,并决定要发送的模板文件(如,网络绑定或中继)。
  • 可以评估CPU的数量,来确定如何正确调节某一Web服务器。
  • 将注册的变量与预定义的变量进行比较,以确定服务是否已更改。例如,测试服务配置文件的MD5检验以和查看服务是否已更改。

条件任务语法

when语句用于有条件地运行任务。它取要测试的条件为值。如果条件满足,则运行任务。如果条件不满足,则跳过任务。

可以测试的一个最简单条件是某一布尔变量是True还是False。以下示例中的when语句导致任务仅在为True时运行:


---
 - name: create user
   hosts: "*"       
   vars:
         implement: true                                # 当implement为true时执行
   tasks:
     - name: test
       command: "echo 'hello {{ item }},how are you.'"
       loop:
         - tom
         - jarry
         - alis
       when: implement                                   #  条件                     
       
[root@localhost ansible]# ansible-playbook vars/user.yml 

PLAY [create user] ************************************************************************************

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

TASK [test] *******************************************************************************************
changed: [192.168.220.8] => (item=tom)
changed: [192.168.220.8] => (item=jarry)
changed: [192.168.220.8] => (item=alis)

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

下面演示了等于某个值时运行,不等于时就跳过

---
 - name: create user
   hosts: "*"       
   tasks:
     - name: test
       user:
         name: "{{ item }}"
         state: present      
       loop:
         - tom
         - jarry
         - alis
       when: '"{{ item }}" == "jarry"'    # 当条件为 name==jarry,执行命令,否则跳过
       
[root@localhost ansible]# ansible-playbook vars/user.yml 
......
TASK [test] *******************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {%
%}. Found: "{{ item }}" == "jarry"
skipping: [192.168.220.8] => (item=tom)              # 不满足条件跳过
changed: [192.168.220.8] => (item=jarry)             # 执行满足条件的
skipping: [192.168.220.8] => (item=alis)             # 不满足条件跳过
......

当用户被定义后可以执行,没定义就会跳过

---
 - name: create user
   hosts: "*"  
   vars: 
         user: liyuan                      # 定义 user  
   tasks:
     - name: test
       user:
         name: "{{ user }}"
         state: present
       when:  user is defined(定义)         # user被定义就可以执行
       
[root@localhost ansible]# ansible-playbook vars/user.yml 
......
************************************************************************
changed: [192.168.220.8]                   # 执行成功

PLAY RECAP *******************************************************************************  ......

下表显示了在处理条件时可使用的一些运算:
示例条件

操作 示例
等于(值为字符串) 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
---
 - name: test
   gather_facts: yes
   hosts: "*"
   vars:
     platform:       # 平台,当前主机的操作系统是centos则条件通过且任务运行
       - CentOS
   tasks:
     - name: facts 
       yum:
         name: vsftpd
         state: present
       when: ansible_facts['distribution'] in platform  # 事实

​ 注意 when 语句的缩进。由于when语句不是模块变量,它必须通过缩进到任务的*别,放置在模块的外面。

​ 任务是YAML散列/字典,when语句只是任务中的又一个键,就如任务的名称以及它所使用的模块一样。通常的惯例是将可能存在的任何when关键字放在任务名称和模块(及模块参数)的后面。

多个条件测试

​ 一个 when 语句可用于评估多个条件。使用andor关键字组合条件,并使用括号分组条件。

如果任一条件为真时满足条件语句,则应当使用or语句。例如,如果计算机上运行的是 Centos 7 或 8,则下述条件得到满足:

---
- name: test
  gather_facts: yes
  hosts: "*"
  tasks:
    - name: facts 
      yum:
        name: vsftpd
        state: present
  when:  ansible_facts['distribution_major_version'] == "Centos  7" or ansible_facts['distribution_major_version'] == "Centos  8"

​ 使用 and 运算时,两个条件都必须为真,才能满足整个条件语句。例如,如果远程主机是CentOS主机,并且安装的内核是指定版本8,则将满足以下条件:

when:  ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "8"

when关键字还支持使用列表来描述条件列表。向when关键字提供列表时,将使用and运算组合所有条件。下面的示例演示了使用and运算符组合多个条件语句的另一方式

when:  
        - ansible_facts['distribution'] == "CentOS" 
        - ansible_facts['distribution_major_version'] == "8"

这种格式提高了可读性,而可读性是良好编写Ansible Playbook的关键目标。

​ 通过使用括号分组条件,可以表达更复杂的条件语句。例如,如果计算机上运行的是红帽企业Linux7或CentOS8,则下述条件语句得到满足。此示例使用大于字符,这样长条件就可以在playbook中分成多行,以便于阅读。

when: >  
        ( ansible_facts['distribution'] == "CentOS" and
        ansible_facts['distribution_major_version'] == "8" ) or 
        ( ansible_facts['distribution'] == "redhat" and
        ansible_facts['distribution_major_version'] == "7" ) 

组合循环和有条件任务

循环和条件可以组合使用。

​ 下面演示中,yum模块将安装mariadb-server软件包,只要/上挂载的文件系统具有超过300MB的可用空间。ansible_mounts事实是一组字典,各自代表一个已挂载文件系统的相关事实。循环迭代列表中每一字典,只有找到了代表两个条件都为真的已挂载文件系统的字典时,条件语句才得到满足。

对某个任务结合使用when和loop时,将对每个项检查when语句---
- name: test
  gather_facts: yes
  hosts: "*"
  tasks:
    - name: installed service
      yum:
        name: mariadb-server
        state: present
      loop: "{{ ansible_facts['mounts'] }}"
      when: item.mount == "/" and item.size_available > 300000000

对某个任务结合使用whenloop时,将对每个项检查when语句。

下面是组合使用条件和注册变量的另一个示例。

---
- name: test
  gather_facts: yes
  hosts: "*"
  tasks:
    - name: test
      command: "systemctl stop firewalld"  # 关闭防火墙
      ignore_errors: yes # 如果它不在运行并且命令失败,则不停止处理。
      register: result  #  将模块的结果信息保存在名为result的变量中

    - name: create user
      user:
        name: lll
        state: present
      when: result.rc == 0  # 如果关闭防火墙成功,就创建用户

实施处理程序

ansible处理程序

​ Ansible模块设计为具有幂等性。这表示,在正确编写的playbook中,playbook及其任务可以运行多次而不会改变受管主机,除非需要进行更改使受管主机进入所需的状态。

​ 但有时候,在任务确实更改系统时,可能需要运行进一步的任务。例如,更改服务配置文件时可能要求重新加载该服务以便使其更改的配置生效。

​ 处理程序是响应由其他任务触发的通知的任务。仅当任务在受管主机上更改了某些内容时,任务才通知其处理程序。每个处理程序具有全局唯一的名称,在playbook中任务块的末尾触发。如果没有任务通过名称通知处理程序,处理程序就不会运行。如果一个或多个任务通知处理程序,处理程序就会在play中的所有其他任务完成后运行一次。因为处理程序就是任务,所以可以在处理程序中使用他们将用于任何其他任务的模块。通常而言,处理程序被用于重新引导主机和重启服务。

​ 处理程序可视为非活动任务,只有在使用notify语句显式调用时才会被触发。在下列代码片段中,只有配置文件更新并且通知了该任务,restart apache处理程序才会重启Apache服务器:notify: 触发任务器

---
- name: test
  gather_facts: yes
  hosts: "*"
  tasks:
    - name: create user        # 创建用户
      user: 
        name: harrp
        state: present
      notify:                  # notify语句指出该任务需要触发一个处理程序
        - restart firewalld
  handlers:                    # handlers关键字表示处理程序任务列表的开头
     - name: restart firewalld # 成功就重启防火墙
       service:                # 用于该处理程序的模块
         name: firewalld
         state: restarted       

​ 在上面的例子中,restart firewalld 处理程序只有在user任务通知已发生更改时才会触发。一个任务可以在其notify部分中调用多个处理程序。Ansible将notify语句视为数组,并且迭代处理程序名称:

---
- name: test
  gather_facts: yes
  hosts: "*"
  tasks:
    - name: test
      user: 
        name: harry
        state: present
      notify:  
        - restart firewalld
        - restart httpd   
  handlers:
     - name: restart firewalld
       service:
         name: firewalld
         state: restarted
     - name: restart http
       service:
         name: httpd
         state: restarted        

使用处理程序的好处

使用处理程序时需要牢记几个重要事项:

  • 处理程序始终按照playhandlers部分指定的顺序运行。它们不按在任务中由notify语句列出的顺序运行,或按任务通知它们的顺序运行。

  • 处理程序通常在相关play中的所有其他任务完成后运行。playbooktasks部分中某一任务调用的处理程序,将等到tasks下的所有任务都已处理后才会运行。

  • 处理程序名称存在于各play命名空间中。如果两个处理程序被错误地给予相同的名称,则仅会运行一个。

  • 即使有多个任务通知处理程序,该处理程序依然仅运行一次。如果没有任务通知处理程序,它就不会运行。

  • 如果包含notify语句的任务没有报告changed结果(例如,软件包已安装并且任务报告ok),则处理程序不会获得通知。处理程序将被跳过,直到有其他任务通知它。只有相关任务报告了changed状态,Ansible才会通知处理程序。

    处理程序用于在任务对受管主机进行更改时执行额外操作。它们不应用作正常任务的替代。

处理任务失败

管理play中的任务错误

Ansible评估任务的返回代码,从而确定任务是成功还是失败。通常而言,当任务失败时,Ansible将立即在该主机上中止play的其余部分并且跳过所有后续任务。

但有些时候,可能希望即使在任务失败时也继续执行play。例如,或许预期待定任务有可能会失败,并且希望通过有条件地运行某项其他任务来修复。

Ansible有多种功能可用于管理任务错误。

忽略任务失败

​ 默认情况下,任务失败时play会中止。不过,可以通过忽略失败的任务来覆盖此行为。可以在任务中使用ignore_errors关键字来实现此目的。

下列代码片段演示了如何在任务中使用ignore_errors,以便在任务失败时也继续在主机上执行playbook。例如,如果hellopackages软件包不存在,则yum模块将失败,但若将ignore_errors设为yes,则执行将继续。

---
- name: test
  hosts: "*"
  tasks:
    - name: packages
      yum:
        name: hellopackages
        state: present
      ignore_errors: yes  

任务失败后强制执行处理程序

​ 通常而言,如果任务失败并且play在该主机上中止,则收到play中早前任务通知的处理程序将不会运行。如果在play中设置force_handlers: yes关键字,则即使play因为后续任务失败而中止也会调用被通知的处理程序。

下列代码片段演示了如何在play中使用force_handlers关键字,以便在任务失败时也强制执行相应的处理程序:

---
- name: test
  hosts: "*"
  force_handlers: yes   # 任务失败后强制执行处理程序
  tasks:
    - name: create user # 创建用户
      user:
        name: fire 
        state: present
      notify:            # 创建成功后启动防火墙
       - started firewalld        

    - name: package      # 不存在hellopackages,将执行失败
      yum:
        name: hellopackages
        state: present  
  handlers:                  # 强制执行
    - name: started firewalld
      service: 
        name: firewalld
        state: started 
[root@localhost ansible]# ansible-playbook lw.yml 
......
TASK [create user] ********************************************************************************************
changed: [192.168.220.8]

TASK [package]                   # 失败,但后面的任务继续执行了 ************************************************************************************************
fatal: [192.168.220.8]: FAILED! => {"changed": false, "failures": ["No package hellopackages available."], "msg": "Failed to install some of the specified packages", "rc": 1, "results": []}

RUNNING HANDLER [started firewalld]   # 执行,启动防火墙***************************************************************************
changed: [192.168.220.8]
......     

请记住,处理程序会在任务报告changed结果时获得通知,而在任务报告okfailed结果时不会获得通知。

指定任务失败条件

​ 可以在任务中使用failed_when关键字来指定表示任务已失败的条件。这通常与命令模块搭配使用,这些模块可能成功执行了某一命令,但命令的输出可能指示了失败。

例如,可以运行输出错误消息的脚本,并使用该消息定义任务的失败状态。下列代码片段演示了如何在任务中使用failed_when关键字:

[root@localhost ~]# cat tset.sh # 受管主机上的脚本创建用户,但用户已存在,在ansible控制机上执行是成功的,但是应该用户存在报告失败,
#!/bin/bash
useradd harry
echo "123456" |passwd --stdin harry

---
- name: test  
  hosts: "*"
  tasks:         
    - name: create user
      command: /root/tset.sh # 执行受管主机上的脚本
      register: result  # 注册变量到result
      failed_when: "'already exists' in result.stderr" # 已经存在报告失败

[root@localhost ansible]# ansible-playbook ll.yml 
.......
TASK [create user] ********************************************************************************************
fatal: [192.168.220.8]: FAILED! => {"changed": true, "cmd": ["/root/tset.sh"], "delta": "0:00:00.012299", "end": "2021-07-27 01:48:43.687132", "failed_when_result": true, "rc": 0, "start": "2021-07-27 01:48:43.674833", "stderr": "useradd: user 'harry' already exists", "stderr_lines": ["useradd: user 'harry' already exists"], "stdout": "Changing password for user harry.\npasswd: all authentication tokens updated successfully.", "stdout_lines": ["Changing password for user harry.", "passwd: all authe
......

fail模块也可用于强制任务失败:

---
- name: test  
  hosts: "*"
  tasks:         
    - name: create user
      command: /root/tset.sh
      register: result    
      ignore_errors: yes  # 有错误的话忽略继续执行
    - name: print output
      fail:
        msg: "用户已经创建过了"   # 打印输出失败    
      failed_when: "'already exists' in result.stderr" 
      
[root@localhost ansible]# ansible-playbook ll.yml 
.....
TASK [print output] *******************************************************************************************
fatal: [192.168.220.8]: FAILED! => {"changed": false, "failed_when_result": true, "msg": "已经创建过了"}
......

我们可以使用fail模块为任务提供明确的失败消息。此方法还支持延迟失败,允许在运行中间任务以完成或回滚其他更改。

指定何时任务报告"Changed"结果

​ 当任务对托管主机进行了更改时,会报告 changed 状态并通知处理程序。如果任务不需要进行更改,则会报告ok并且不通知处理程序。

changed_when关键字可用于控制任务在何时报告它已进行了更改。例如,下一示例中的shell模块将用于获取供后续任务使用的Kerberos凭据。它通常会在运行时始终报告changed。为抵制这种更改,应设置changed_when: false,以便它仅报告okfailed

以下示例使用shell模块,根据通过已注册变量收集的模块的输出来报告changed

# 密码已经设置过了,再次执行playbook状态还是changed的,设置过了让它变成ok状态
---
- name: test  
  hosts: "*"
  tasks:         
    - name: create passwd
      shell: echo "123456"| passwd --stdin harry
      register: result
    # changed_when: "'successfully' not in result.stdout" # 注释掉
      notify:
        - print info     
  handlers:
    - name: print info
      debug:
         msg: "密码设置成功" 
         
[root@localhost ansible]# ansible-playbook ll.yml 

TASK [create passwd] # 创建密码**************************************************************
changed: [192.168.220.8] # 还是改变的状态

RUNNING HANDLER [print info] **********************************************************************************
ok: [192.168.220.8] => {
    "msg": "密码设置成功"
}
......

取消注释后,让它的状态变成ok
[root@localhost ansible]# ansible-playbook ll.yml 
.....
TASK [create passwd] ******************************************************************************************
ok: [192.168.220.8]    # 状态变成ok
.....

Ansible块和错误处理

​ 在playbook中,块是对任务进行逻辑分组的子句,可用于控制任务的执行方式。例如,任务块可以含有when关键字,以将某一条件应用到多个任务:

---
- name: test  
  hosts: "*"
  tasks:
    - name: installed packages
      block:             # 把下面两个任务当成一个整体就是块,满足when条件
        - name: installd httpd
          yum:
            name: httpd
            state: present
        - name: lineinfile 
          lineinfile:
            path: /etc/selinux/config
            regexp: "SELINUX="
            line: "SELINUX=enforcing"
          when: ansible_facts['distribution'] == "CentOS"       

通过块,也可结合rescuealways语句来处理错误。如果块中的任何任务失败,则执行其rescue块中的任务来进行恢复。在block子句中的任务以及rescue子句中的任务(如果出现故障)运行之后,always子句中的任务运行。总结:

  • block:定义要运行的主要任务
  • rescue:定义要在block子句中定义的任务失败时运行的任务
  • always:定义始终都独立运行的任务,不论blockrescue子句中定义的任务是成功还是失败

​ 以下示例演示了如何在playbook中实施块。即使block子句中定义的任务失败,rescue和always子句中定义的任务也会执行。

---
- name: test  
  hosts: "*"
  tasks:
    - name: Perform tasks
      block:                 # 删除用户,如果用户不存在,强制失败
        - name: delete user 
          user:
            name: harry
            state: absent
          register: result
          failed_when: "'false' in result.changed "   # 强制失败   
      rescue:                     # block子句失败,执行rescue创建用户
        - name: create user
          user:
            name: harry
            state: present 
      always:   # 启动防火墙,独立运行的任务,不论block和rescue子句中定义任务是成功还是失败
        - name: start firewalld
          service:
            name: firewalld
            state: started      
[root@localhost ansible]# ansible-playbook ll.yml 
.....
TASK [delete user] # blokc失败 ,执行rescue字句  ***************************************************************************
fatal: [192.168.220.8]: FAILED! => {"msg": "The conditional check ''false' in result.changed ' failed. The error was: Unexpected templating type error occurred on ({% if 'false' in result.changed  %} True {% else %} False {% endif %}): argument of type 'bool' is not iterable"}

TASK [create user] # 执行创建用户,成功 *****************************************************************************
changed: [192.168.220.8]

TASK [start firewalld] # 独立运行,不受影响 *********************************************************************
changed: [192.168.220.8]
......

block中的when条件也会应用到其rescue和always子句(若存在)。

上一篇:VHDL的并行语句


下一篇:dscp hftp