条件语句

在 Playbook 中,您可能希望根据 Fact(关于远程系统的数据)、变量或先前任务的结果的值来执行不同的任务或实现不同的目标。您可能希望某些变量的值依赖于其他变量的值。或者,您可能希望根据主机是否符合其他标准来创建其他主机组。您可以使用条件语句执行所有这些操作。

Ansible 在条件语句中使用 Jinja2 测试过滤器。Ansible 支持所有标准的测试和过滤器,并添加了一些独特的测试和过滤器。

注意

在 Ansible 中有很多选项来控制执行流程。您可以在 https://jinja.flask.org.cn/en/latest/templates/#comparisons 找到更多受支持的条件语句示例。

使用 when 的基本条件语句

最简单的条件语句应用于单个任务。创建任务,然后添加一个应用测试的 when 语句。when 子句是一个原始的 Jinja2 表达式,没有双大括号(请参阅 引用简单变量)。当您运行任务或 Playbook 时,Ansible 会评估所有主机的测试。在任何测试通过(返回值 True)的主机上,Ansible 都会运行该任务。例如,如果您在多台机器上安装 mysql,其中一些机器启用了 SELinux,您可能有一个配置 SELinux 以允许 mysql 运行的任务。您只希望该任务在启用了 SELinux 的机器上运行。

tasks:
  - name: Configure SELinux to start mysql on any port
    ansible.posix.seboolean:
      name: mysql_connect_any
      state: true
      persistent: true
    when: ansible_selinux.status == "enabled"
    # all variables can be used directly in conditionals without double curly braces

基于 ansible_facts 的条件语句

通常,您希望根据 Facts 执行或跳过任务。Facts 是各个主机的属性,包括 IP 地址、操作系统、文件系统的状态等等。通过基于 Facts 的条件语句

  • 您只能在操作系统是特定版本时安装某个软件包。

  • 您可以在具有内部 IP 地址的主机上跳过配置防火墙。

  • 您只能在文件系统即将满时执行清理任务。

请参阅 常用 Facts 以获取条件语句中经常出现的 Facts 列表。并非所有 Facts 都适用于所有主机。例如,下面示例中使用的 'lsb_major_release' Fact 仅当目标主机上安装了 lsb_release package 时才存在。要查看您的系统上可用的 Facts,请将调试任务添加到您的 Playbook 中

- name: Show facts available on the system
  ansible.builtin.debug:
    var: ansible_facts

这是一个基于 Fact 的示例条件语句

tasks:
  - name: Shut down Debian flavored systems
    ansible.builtin.command: /sbin/shutdown -t now
    when: ansible_facts['os_family'] == "Debian"

如果您有多个条件,可以使用括号将它们分组

tasks:
  - name: Shut down CentOS 6 and Debian 7 systems
    ansible.builtin.command: /sbin/shutdown -t now
    when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6") or
          (ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "7")

您可以使用 逻辑运算符 来组合条件。当您有多个条件都必须为真(即逻辑 and)时,您可以将它们指定为列表

tasks:
  - name: Shut down CentOS 6 systems
    ansible.builtin.command: /sbin/shutdown -t now
    when:
      - ansible_facts['distribution'] == "CentOS"
      - ansible_facts['distribution_major_version'] == "6"

如果 Fact 或变量是一个字符串,并且您需要对其运行数学比较,请使用过滤器确保 Ansible 将该值读取为整数

tasks:
  - ansible.builtin.shell: echo "only on Red Hat 6, derivatives, and later"
    when: ansible_facts['os_family'] == "RedHat" and ansible_facts['lsb']['major_release'] | int >= 6

您可以将 Ansible Facts 存储为变量以用于条件逻辑,如下例所示

tasks:
    - name: Get the CPU temperature
      set_fact:
        temperature: "{{ ansible_facts['cpu_temperature'] }}"

    - name: Restart the system if the temperature is too high
      when: temperature | float > 90
      shell: "reboot"

基于注册变量的条件语句

通常在 Playbook 中,您希望根据早期任务的结果来执行或跳过任务。例如,您可能希望在早期任务升级服务后配置该服务。要创建基于注册变量的条件语句

  1. 将早期任务的结果注册为变量。

  2. 基于注册变量创建条件测试。

您可以使用 register 关键字创建注册变量的名称。注册变量始终包含创建它的任务的状态以及任务生成的任何输出。您可以在模板和操作行以及条件 when 语句中使用注册变量。您可以使用 variable.stdout 访问注册变量的字符串内容。例如

- name: Test play
  hosts: all

  tasks:

      - name: Register a variable
        ansible.builtin.shell: cat /etc/motd
        register: motd_contents

      - name: Use the variable in conditional statement
        ansible.builtin.shell: echo "motd contains the word hi"
        when: motd_contents.stdout.find('hi') != -1

如果变量是列表,您可以在任务的循环中使用注册结果。如果变量不是列表,您可以使用 stdout_linesvariable.stdout.split() 将其转换为列表。您还可以按其他字段分割行

- name: Registered variable usage as a loop list
  hosts: all
  tasks:

    - name: Retrieve the list of home directories
      ansible.builtin.command: ls /home
      register: home_dirs

    - name: Add home dirs to the backup spooler
      ansible.builtin.file:
        path: /mnt/bkspool/{{ item }}
        src: /home/{{ item }}
        state: link
      loop: "{{ home_dirs.stdout_lines }}"
      # same as loop: "{{ home_dirs.stdout.split() }}"

注册变量的字符串内容可以为空。如果您只想在注册变量的 stdout 为空的主机上运行另一个任务,请检查注册变量的字符串内容是否为空

- name: check registered variable for emptiness
  hosts: all

  tasks:

      - name: List contents of directory
        ansible.builtin.command: ls mydir
        register: contents

      - name: Check contents for emptiness
        ansible.builtin.debug:
          msg: "Directory is empty"
        when: contents.stdout == ""

Ansible 始终在每个主机的注册变量中注册一些内容,即使在任务失败或 Ansible 因不满足条件而跳过任务的主机上也是如此。要在这些主机上运行后续任务,请查询注册变量是否 is skipped(而不是“undefined”或“default”)。有关更多信息,请参阅 注册变量。以下是基于任务成功或失败的示例条件语句。请记住,如果您希望 Ansible 在发生故障时继续在主机上执行,请忽略错误

tasks:
  - name: Register a variable, ignore errors and continue
    ansible.builtin.command: /bin/false
    register: result
    ignore_errors: true

  - name: Run only if the task that registered the "result" variable fails
    ansible.builtin.command: /bin/something
    when: result is failed

  - name: Run only if the task that registered the "result" variable succeeds
    ansible.builtin.command: /bin/something_else
    when: result is succeeded

  - name: Run only if the task that registered the "result" variable is skipped
    ansible.builtin.command: /bin/still/something_else
    when: result is skipped

  - name: Run only if the task that registered the "result" variable changed something.
    ansible.builtin.command: /bin/still/something_else
    when: result is changed

注意

旧版本的 Ansible 使用 successfail,但 succeededfailed 使用了正确的时态。所有这些选项现在都是有效的。

基于变量的条件语句

您还可以创建基于 Playbook 或清单中定义的变量的条件语句。由于条件语句需要布尔输入(测试必须评估为 True 才能触发条件),因此您必须将 | bool 过滤器应用于非布尔变量,例如内容为 ‘yes’、‘on’、‘1’ 或 ‘true’ 的字符串变量。您可以像这样定义变量

vars:
  epic: true
  monumental: "yes"

使用上面的变量,Ansible 将运行其中一个任务并跳过另一个任务

tasks:
    - name: Run the command if "epic" or "monumental" is true
      ansible.builtin.shell: echo "This certainly is epic!"
      when: epic or monumental | bool

    - name: Run the command if "epic" is false
      ansible.builtin.shell: echo "This certainly isn't epic!"
      when: not epic

如果未设置必需的变量,您可以使用 Jinja2 的 defined 测试来跳过或失败。例如

tasks:
    - name: Run the command if "foo" is defined
      ansible.builtin.shell: echo "I've got '{{ foo }}' and am not afraid to use it!"
      when: foo is defined

    - name: Fail if "bar" is undefined
      ansible.builtin.fail: msg="Bailing out. This play requires 'bar'"
      when: bar is undefined

这在与 vars 文件的条件导入结合使用时尤其有用(请参见下文)。正如示例所示,您不需要使用 {{ }} 在条件语句中使用变量,因为这些已经暗示了。

在循环中使用条件语句

如果你将 when 语句与 循环 结合使用,Ansible 会为每个项目单独处理条件。这是设计使然,因此你可以在循环中的某些项目上执行任务,而在其他项目上跳过它。例如:

tasks:
    - name: Run with items greater than 5
      ansible.builtin.command: echo {{ item }}
      loop: [ 0, 2, 4, 6, 8, 10 ]
      when: item > 5

如果需要在循环变量未定义时跳过整个任务,请使用 |default 过滤器提供一个空的迭代器。例如,当循环遍历一个列表时:

- name: Skip the whole task when a loop variable is undefined
  ansible.builtin.command: echo {{ item }}
  loop: "{{ mylist|default([]) }}"
  when: item > 5

当循环遍历字典时,你可以执行相同的操作:

- name: The same as above using a dict
  ansible.builtin.command: echo {{ item.key }}
  loop: "{{ query('dict', mydict|default({})) }}"
  when: item.value > 5

加载自定义事实

你可以提供自己的事实,如 你是否应该开发一个模块? 中所述。要运行它们,只需在你的任务列表顶部调用你自己的自定义事实收集模块,那里返回的变量将可用于后续任务。

tasks:
    - name: Gather site specific fact data
      action: site_facts

    - name: Use a custom fact
      ansible.builtin.command: /usr/bin/thingy
      when: my_custom_fact_just_retrieved_from_the_remote_system == '1234'

可重用的条件

你可以将条件与可重用的任务文件、剧本或角色一起使用。Ansible 对动态重用(includes)和静态重用(imports)以不同的方式执行这些条件语句。有关 Ansible 中重用的更多信息,请参阅 重用 Ansible 工件

带有导入的条件

当你向 import 语句添加条件时,Ansible 会将该条件应用于导入文件中的所有任务。此行为等效于 标签继承:向多个任务添加标签。Ansible 将条件应用于每个任务并单独评估每个任务。例如,如果你想定义然后显示一个之前未定义的变量,你可能有一个名为 main.yml 的剧本和一个名为 other_tasks.yml 的任务文件:

# all tasks within an imported file inherit the condition from the import statement
# main.yml
- hosts: all
  tasks:
  - import_tasks: other_tasks.yml # note "import"
    when: x is not defined

# other_tasks.yml
- name: Set a variable
  ansible.builtin.set_fact:
    x: foo

- name: Print a variable
  ansible.builtin.debug:
    var: x

Ansible 在执行时将其扩展为等效于:

- name: Set a variable if not defined
  ansible.builtin.set_fact:
    x: foo
  when: x is not defined
  # this task sets a value for x

- name: Do the task if "x" is not defined
  ansible.builtin.debug:
    var: x
  when: x is not defined
  # Ansible skips this task, because x is now defined

如果 x 最初已定义,则两个任务都将按预期跳过。但是,如果 x 最初未定义,则 debug 任务将被跳过,因为该条件是针对每个导入的任务评估的。该条件对于 set_fact 任务的评估结果为 true,这将定义该变量,并导致 debug 条件的评估结果为 false

如果这不是你想要的行为,请使用 include_* 语句仅将条件应用于该语句本身。

# using a conditional on include_* only applies to the include task itself
# main.yml
- hosts: all
  tasks:
  - include_tasks: other_tasks.yml # note "include"
    when: x is not defined

现在,如果 x 最初未定义,则 debug 任务将不会被跳过,因为该条件是在包含时评估的,并且不适用于单个任务。

你可以将条件应用于 import_playbook 以及其他 import_* 语句。当你使用此方法时,Ansible 会为每个不符合条件的宿主机上的每个任务返回一个“skipped”消息,从而产生重复的输出。在许多情况下,group_by 模块 可以是实现相同目标的更简洁的方法;请参阅 处理操作系统和发行版差异

带有包含的条件

当你在 include_* 语句上使用条件时,该条件仅应用于 include 任务本身,而不应用于包含文件中的任何其他任务。为了与上述用于导入条件的示例进行对比,请查看相同的剧本和任务文件,但使用 include 而不是 import:

# Includes let you reuse a file to define a variable when it is not already defined

# main.yml
- include_tasks: other_tasks.yml
  when: x is not defined

# other_tasks.yml
- name: Set a variable
  ansible.builtin.set_fact:
    x: foo

- name: Print a variable
  ansible.builtin.debug:
    var: x

Ansible 在执行时将其扩展为等效于:

# main.yml
- include_tasks: other_tasks.yml
  when: x is not defined
  # if condition is met, Ansible includes other_tasks.yml

# other_tasks.yml
- name: Set a variable
  ansible.builtin.set_fact:
    x: foo
  # no condition applied to this task, Ansible sets the value of x to foo

- name: Print a variable
  ansible.builtin.debug:
    var: x
  # no condition applied to this task, Ansible prints the debug statement

通过使用 include_tasks 而不是 import_tasksother_tasks.yml 中的两个任务都将按预期执行。有关 includeimport 之间差异的更多信息,请参阅 重用 Ansible 工件

带有角色的条件

有三种方法可以将条件应用于角色:

  • 通过将 when 语句放在 roles 关键字下,将相同的一个或多个条件添加到角色中的所有任务。请参阅本节中的示例。

  • 通过在你的剧本中的静态 import_role 上放置 when 语句,将相同的一个或多个条件添加到角色中的所有任务。

  • 将一个或多个条件添加到角色本身的单个任务或代码块。这是唯一一种允许你根据 when 语句选择或跳过角色中某些任务的方法。要选择或跳过角色中的任务,你必须在单个任务或代码块上设置条件,在你的剧本中使用动态 include_role,并将该条件或多个条件添加到 include 中。当你使用此方法时,Ansible 会将该条件应用于 include 本身以及角色中也具有该 when 语句的任何任务。

当你使用 roles 关键字在剧本中静态地合并角色时,Ansible 会将你定义的条件添加到角色中的所有任务。例如:

- hosts: webservers
  roles:
     - role: debian_stock_config
       when: ansible_facts['os_family'] == 'Debian'

基于事实选择变量、文件或模板

有时,有关主机的 事实 决定了你想要用于某些变量的值,甚至是你想为该主机选择的文件或模板。例如,在 CentOS 和 Debian 上,软件包的名称是不同的。常见服务的配置文件在不同的操作系统风格和版本上也不同。要基于有关主机的 事实 加载不同的变量文件、模板或其他文件:

  1. 将你的变量文件、模板或文件命名为与区分它们的 Ansible 事实相匹配的名称

  2. 使用基于 Ansible 事实的变量,为每个主机选择正确的变量文件、模板或文件

Ansible 将变量与任务分开,从而防止你的剧本变成带有嵌套条件的任意代码。这种方法可以产生更精简和可审计的配置规则,因为需要跟踪的决策点更少。

基于事实选择变量文件

你可以通过将你的变量值放在变量文件中并有条件地导入它们,来创建一个可在多个平台和操作系统版本上运行的剧本,且语法最少。如果你想在某些 CentOS 和某些 Debian 服务器上安装 Apache,请创建具有 YAML 键和值的变量文件。例如:

---
# for vars/RedHat.yml
apache: httpd
somethingelse: 42

然后,根据你在剧本中主机上收集的事实导入这些变量文件:

---
- hosts: webservers
  remote_user: root
  vars_files:
    - "vars/common.yml"
    - [ "vars/{{ ansible_facts['os_family'] }}.yml", "vars/os_defaults.yml" ]
  tasks:
  - name: Make sure apache is started
    ansible.builtin.service:
      name: '{{ apache }}'
      state: started

Ansible 在 webservers 组中的主机上收集事实,然后将变量 “ansible_facts[‘os_family’]” 插入到文件名列表中。如果你的主机使用 Red Hat 操作系统(例如 CentOS),则 Ansible 会查找“vars/RedHat.yml”。如果该文件不存在,则 Ansible 会尝试加载“vars/os_defaults.yml”。对于 Debian 主机,Ansible 首先查找“vars/Debian.yml”,然后再回退到“vars/os_defaults.yml”。如果找不到列表中的任何文件,则 Ansible 会引发错误。

基于事实选择文件和模板

当不同的操作系统风格或版本需要不同的配置文件或模板时,你可以使用相同的方法。根据分配给每个主机的变量选择适当的文件或模板。这种方法通常比在单个模板中放入大量条件来覆盖多个操作系统或软件包版本要干净得多。

例如,你可以模板化一个在 CentOS 和 Debian 之间差异很大的配置文件:

- name: Template a file
  ansible.builtin.template:
    src: "{{ item }}"
    dest: /etc/myapp/foo.conf
  loop: "{{ query('first_found', { 'files': myfiles, 'paths': mypaths}) }}"
  vars:
    myfiles:
      - "{{ ansible_facts['distribution'] }}.conf"
      -  default.conf
    mypaths: ['search_location_one/somedir/', '/opt/other_location/somedir/']

调试条件

如果你的条件 when 语句的行为不如预期,你可以添加一个 debug 语句来确定该条件的评估结果是 true 还是 false。条件中意外行为的常见原因是将整数作为字符串或将字符串作为整数进行测试。要调试条件语句,请将整个语句添加为 debug 任务中的 var: 值。然后,Ansible 会显示测试以及语句的评估方式。例如,以下是一组任务和示例输出:

- name: check value of return code
  ansible.builtin.debug:
    var: bar_status.rc

- name: check test for rc value as string
  ansible.builtin.debug:
    var: bar_status.rc == "127"

- name: check test for rc value as integer
  ansible.builtin.debug:
    var: bar_status.rc == 127
TASK [check value of return code] *********************************************************************************
ok: [foo-1] => {
    "bar_status.rc": "127"
}

TASK [check test for rc value as string] **************************************************************************
ok: [foo-1] => {
    "bar_status.rc == \"127\"": false
}

TASK [check test for rc value as integer] *************************************************************************
ok: [foo-1] => {
    "bar_status.rc == 127": true
}

常用事实

以下 Ansible 事实在条件中经常使用。

ansible_facts[‘distribution’]

可能的值(示例,不完整列表)

Alpine
Altlinux
Amazon
Archlinux
ClearLinux
Coreos
CentOS
Debian
Fedora
Gentoo
Mandriva
NA
OpenWrt
OracleLinux
RedHat
Slackware
SLES
SMGL
SUSE
Ubuntu
VMwareESX

ansible_facts[‘distribution_major_version’]

操作系统的主要版本。例如,对于 Ubuntu 16.04,该值为 16

ansible_facts[‘os_family’]

可能的值(示例,不完整列表)

AIX
Alpine
Altlinux
Archlinux
Darwin
Debian
FreeBSD
Gentoo
HP-UX
Mandrake
RedHat
SMGL
Slackware
Solaris
Suse
Windows

另请参阅

使用 Playbook

剧本简介

角色

按角色组织剧本

一般提示

剧本的技巧和窍门

使用变量

关于变量的全部信息

沟通

有问题吗?需要帮助吗?想分享你的想法吗?请访问 Ansible 通信指南