条件语句

在 playbook 中,您可能希望根据事实(有关远程系统的数据)、变量或先前任务的结果执行不同的任务或具有不同的目标。您可能希望某些变量的值取决于其他变量的值。或者,您可能希望根据主机是否匹配其他条件创建其他主机组。您可以使用条件语句完成所有这些操作。

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

注意

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

使用 when 的基本条件语句

最简单的条件语句适用于单个任务。创建任务,然后添加一个 when 语句来应用测试。 when 子句是一个原始的 Jinja2 表达式,没有双花括号(请参阅 group_by_module)。当您运行任务或 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 的条件语句

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

  • 您可以仅在操作系统为特定版本时安装某个软件包。

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

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

请参阅 常用事实,了解在条件语句中经常出现的常识列表。并非所有事实都存在于所有主机上。例如,下面示例中使用的“lsb_major_release”事实仅在目标主机上安装了 lsb_release package 时才存在。要查看您的系统上有哪些事实可用,请将调试任务添加到您的 playbook 中。

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

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

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"

如果事实或变量是字符串,并且您需要对其进行数学比较,请使用过滤器以确保 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 事实存储为变量以用于条件逻辑,如下例所示

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(而不是“未定义”或“默认”)。请参阅 注册变量 以获取更多信息。以下是基于任务成功或失败的条件语句示例。请记住,如果您希望 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

加载自定义 facts

你可以提供自己的 facts,如 是否应该开发模块? 中所述。要运行它们,只需在任务列表的顶部调用你自己的自定义 fact 收集模块,然后在以后的任务中即可访问此处返回的变量

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'

带有重用的条件语句

你可以在可重用任务文件、playbook 或角色中使用条件语句。Ansible 对动态重用(包含)和静态重用(导入)执行这些条件语句的方式有所不同。有关 Ansible 中重用的更多信息,请参阅 重用 Ansible 工件

带有导入的条件语句

当你向导入语句添加条件时,Ansible 会将该条件应用于导入文件中所有任务。此行为等效于 标签继承:向多个任务添加标签。Ansible 将条件应用于每个任务并分别评估每个任务。例如,如果你想定义然后显示一个以前未定义的变量,你可能有一个名为 main.yml 的 playbook 和一个名为 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 最初未定义,则调试任务将被跳过,因为条件是针对每个导入的任务进行评估的。对于 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 最初未定义,则调试任务不会被跳过,因为条件是在包含时进行评估的,并且不适用于各个任务。

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

带有包含的条件语句

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

# 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_tasks,将按预期执行 other_tasks.yml 中的两个任务。有关 includeimport 之间差异的更多信息,请参阅 重用 Ansible 工件

带有角色的条件语句

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

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

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

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

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

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

根据 facts 选择变量、文件或模板

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

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

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

Ansible 将变量与任务分开,防止你的 playbook 变成包含嵌套条件的任意代码。这种方法可以产生更简洁、更易于审计的配置规则,因为需要跟踪的决策点更少。

根据 facts 选择变量文件

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

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

然后根据你在 playbook 中收集的主机的事实导入这些变量文件

---
- 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 组中的主机上收集 facts,然后将变量“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 将引发错误。

根据 facts 选择文件和模板

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

例如,你可以为 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 语句来确定条件是否评估为 truefalse。条件语句中意外行为的一个常见原因是将整数作为字符串或将字符串作为整数进行测试。要调试条件语句,请将整个语句作为 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
}

常用 facts

以下 Ansible facts 经常用于条件语句中。

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

Playbook 简介

角色

按角色组织 Playbook

一般提示

Playbook 提示和技巧

使用变量

关于变量的一切

沟通

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