1.. _playbooks_conditionals:
2
3************
4Conditionals
5************
6
7In a playbook, you may want to execute different tasks, or have different goals, depending on the value of a fact (data about the remote system), a variable, or the result of a previous task. You may want the value of some variables to depend on the value of other variables. Or you may want to create additional groups of hosts based on whether the hosts match other criteria. You can do all of these things with conditionals.
8
9Ansible uses Jinja2 :ref:`tests <playbooks_tests>` and :ref:`filters <playbooks_filters>` in conditionals. Ansible supports all the standard tests and filters, and adds some unique ones as well.
10
11.. note::
12
13  There are many options to control execution flow in Ansible. You can find more examples of supported conditionals at `<https://jinja.palletsprojects.com/en/master/templates/#comparisons>`_.
14
15.. contents::
16   :local:
17
18.. _the_when_statement:
19
20Basic conditionals with ``when``
21================================
22
23The simplest conditional statement applies to a single task. Create the task, then add a ``when`` statement that applies a test. The ``when`` clause is a raw Jinja2 expression without double curly braces (see :ref:`group_by_module`). When you run the task or playbook, Ansible evaluates the test for all hosts. On any host where the test passes (returns a value of True), Ansible runs that task. For example, if you are installing mysql on multiple machines, some of which have SELinux enabled, you might have a task to configure SELinux to allow mysql to run. You would only want that task to run on machines that have SELinux enabled:
24
25.. code-block:: yaml
26
27    tasks:
28      - name: Configure SELinux to start mysql on any port
29        ansible.posix.seboolean:
30          name: mysql_connect_any
31          state: true
32          persistent: yes
33        when: ansible_selinux.status == "enabled"
34        # all variables can be used directly in conditionals without double curly braces
35
36Conditionals based on ansible_facts
37-----------------------------------
38
39Often you want to execute or skip a task based on facts. Facts are attributes of individual hosts, including IP address, operating system, the status of a filesystem, and many more. With conditionals based on facts:
40
41  - You can install a certain package only when the operating system is a particular version.
42  - You can skip configuring a firewall on hosts with internal IP addresses.
43  - You can perform cleanup tasks only when a filesystem is getting full.
44
45See :ref:`commonly_used_facts` for a list of facts that frequently appear in conditional statements. Not all facts exist for all hosts. For example, the 'lsb_major_release' fact used in an example below only exists when the lsb_release package is installed on the target host. To see what facts are available on your systems, add a debug task to your playbook::
46
47    - name: Show facts available on the system
48      ansible.builtin.debug:
49        var: ansible_facts
50
51Here is a sample conditional based on a fact:
52
53.. code-block:: yaml
54
55    tasks:
56      - name: Shut down Debian flavored systems
57        ansible.builtin.command: /sbin/shutdown -t now
58        when: ansible_facts['os_family'] == "Debian"
59
60If you have multiple conditions, you can group them with parentheses:
61
62.. code-block:: yaml
63
64    tasks:
65      - name: Shut down CentOS 6 and Debian 7 systems
66        ansible.builtin.command: /sbin/shutdown -t now
67        when: (ansible_facts['distribution'] == "CentOS" and ansible_facts['distribution_major_version'] == "6") or
68              (ansible_facts['distribution'] == "Debian" and ansible_facts['distribution_major_version'] == "7")
69
70You can use `logical operators <https://jinja.palletsprojects.com/en/master/templates/#logic>`_ to combine conditions. When you have multiple conditions that all need to be true (that is, a logical ``and``), you can specify them as a list::
71
72    tasks:
73      - name: Shut down CentOS 6 systems
74        ansible.builtin.command: /sbin/shutdown -t now
75        when:
76          - ansible_facts['distribution'] == "CentOS"
77          - ansible_facts['distribution_major_version'] == "6"
78
79If a fact or variable is a string, and you need to run a mathematical comparison on it, use a filter to ensure that Ansible reads the value as an integer::
80
81    tasks:
82      - ansible.builtin.shell: echo "only on Red Hat 6, derivatives, and later"
83        when: ansible_facts['os_family'] == "RedHat" and ansible_facts['lsb']['major_release'] | int >= 6
84
85.. _conditionals_registered_vars:
86
87Conditions based on registered variables
88----------------------------------------
89
90Often in a playbook you want to execute or skip a task based on the outcome of an earlier task. For example, you might want to configure a service after it is upgraded by an earlier task. To create a conditional based on a registered variable:
91
92  #. Register the outcome of the earlier task as a variable.
93  #. Create a conditional test based on the registered variable.
94
95You create the name of the registered variable using the ``register`` keyword. A registered variable always contains the status of the task that created it as well as any output that task generated. You can use registered variables in templates and action lines as well as in conditional ``when`` statements. You can access the string contents of the registered variable using ``variable.stdout``. For example::
96
97    - name: Test play
98      hosts: all
99
100      tasks:
101
102          - name: Register a variable
103            ansible.builtin.shell: cat /etc/motd
104            register: motd_contents
105
106          - name: Use the variable in conditional statement
107            ansible.builtin.shell: echo "motd contains the word hi"
108            when: motd_contents.stdout.find('hi') != -1
109
110You can use registered results in the loop of a task if the variable is a list. If the variable is not a list, you can convert it into a list, with either ``stdout_lines`` or with ``variable.stdout.split()``. You can also split the lines by other fields::
111
112    - name: Registered variable usage as a loop list
113      hosts: all
114      tasks:
115
116        - name: Retrieve the list of home directories
117          ansible.builtin.command: ls /home
118          register: home_dirs
119
120        - name: Add home dirs to the backup spooler
121          ansible.builtin.file:
122            path: /mnt/bkspool/{{ item }}
123            src: /home/{{ item }}
124            state: link
125          loop: "{{ home_dirs.stdout_lines }}"
126          # same as loop: "{{ home_dirs.stdout.split() }}"
127
128The string content of a registered variable can be empty. If you want to run another task only on hosts where the stdout of your registered variable is empty, check the registered variable's string contents for emptiness:
129
130.. code-block:: yaml
131
132    - name: check registered variable for emptiness
133      hosts: all
134
135      tasks:
136
137          - name: List contents of directory
138            ansible.builtin.command: ls mydir
139            register: contents
140
141          - name: Check contents for emptiness
142            ansible.builtin.debug:
143              msg: "Directory is empty"
144            when: contents.stdout == ""
145
146Ansible always registers something in a registered variable for every host, even on hosts where a task fails or Ansible skips a task because a condition is not met. To run a follow-up task on these hosts, query the registered variable for ``is skipped`` (not for "undefined" or "default"). See :ref:`registered_variables` for more information. Here are sample conditionals based on the success or failure of a task. Remember to ignore errors if you want Ansible to continue executing on a host when a failure occurs:
147
148.. code-block:: yaml
149
150    tasks:
151      - name: Register a variable, ignore errors and continue
152        ansible.builtin.command: /bin/false
153        register: result
154        ignore_errors: true
155
156      - name: Run only if the task that registered the "result" variable fails
157        ansible.builtin.command: /bin/something
158        when: result is failed
159
160      - name: Run only if the task that registered the "result" variable succeeds
161        ansible.builtin.command: /bin/something_else
162        when: result is succeeded
163
164      - name: Run only if the task that registered the "result" variable is skipped
165        ansible.builtin.command: /bin/still/something_else
166        when: result is skipped
167
168.. note:: Older versions of Ansible used ``success`` and ``fail``, but ``succeeded`` and ``failed`` use the correct tense. All of these options are now valid.
169
170
171Conditionals based on variables
172-------------------------------
173
174You can also create conditionals based on variables defined in the playbooks or inventory. Because conditionals require boolean input (a test must evaluate as True to trigger the condition), you must apply the ``| bool`` filter to non boolean variables, such as string variables with content like 'yes', 'on', '1', or 'true'. You can define variables like this:
175
176.. code-block:: yaml
177
178    vars:
179      epic: true
180      monumental: "yes"
181
182With the variables above, Ansible would run one of these tasks and skip the other:
183
184.. code-block:: yaml
185
186    tasks:
187        - name: Run the command if "epic" or "monumental" is true
188          ansible.builtin.shell: echo "This certainly is epic!"
189          when: epic or monumental | bool
190
191        - name: Run the command if "epic" is false
192          ansible.builtin.shell: echo "This certainly isn't epic!"
193          when: not epic
194
195If a required variable has not been set, you can skip or fail using Jinja2's `defined` test. For example:
196
197.. code-block:: yaml
198
199    tasks:
200        - name: Run the command if "foo" is defined
201          ansible.builtin.shell: echo "I've got '{{ foo }}' and am not afraid to use it!"
202          when: foo is defined
203
204        - name: Fail if "bar" is undefined
205          ansible.builtin.fail: msg="Bailing out. This play requires 'bar'"
206          when: bar is undefined
207
208This is especially useful in combination with the conditional import of vars files (see below).
209As the examples show, you do not need to use `{{ }}` to use variables inside conditionals, as these are already implied.
210
211.. _loops_and_conditionals:
212
213Using conditionals in loops
214---------------------------
215
216If you combine a ``when`` statement with a :ref:`loop <playbooks_loops>`, Ansible processes the condition separately for each item. This is by design, so you can execute the task on some items in the loop and skip it on other items. For example:
217
218.. code-block:: yaml
219
220    tasks:
221        - name: Run with items greater than 5
222          ansible.builtin.command: echo {{ item }}
223          loop: [ 0, 2, 4, 6, 8, 10 ]
224          when: item > 5
225
226If you need to skip the whole task when the loop variable is undefined, use the `|default` filter to provide an empty iterator. For example, when looping over a list:
227
228.. code-block:: yaml
229
230        - name: Skip the whole task when a loop variable is undefined
231          ansible.builtin.command: echo {{ item }}
232          loop: "{{ mylist|default([]) }}"
233          when: item > 5
234
235You can do the same thing when looping over a dict:
236
237.. code-block:: yaml
238
239        - name: The same as above using a dict
240          ansible.builtin.command: echo {{ item.key }}
241          loop: "{{ query('dict', mydict|default({})) }}"
242          when: item.value > 5
243
244.. _loading_in_custom_facts:
245
246Loading custom facts
247--------------------
248
249You can provide your own facts, as described in :ref:`developing_modules`.  To run them, just make a call to your own custom fact gathering module at the top of your list of tasks, and variables returned there will be accessible to future tasks:
250
251.. code-block:: yaml
252
253    tasks:
254        - name: Gather site specific fact data
255          action: site_facts
256
257        - name: Use a custom fact
258          ansible.builtin.command: /usr/bin/thingy
259          when: my_custom_fact_just_retrieved_from_the_remote_system == '1234'
260
261.. _when_with_reuse:
262
263Conditionals with re-use
264------------------------
265
266You can use conditionals with re-usable tasks files, playbooks, or roles. Ansible executes these conditional statements differently for dynamic re-use (includes) and for static re-use (imports). See :ref:`playbooks_reuse` for more information on re-use in Ansible.
267
268.. _conditional_imports:
269
270Conditionals with imports
271^^^^^^^^^^^^^^^^^^^^^^^^^
272
273When you add a conditional to an import statement, Ansible applies the condition to all tasks within the imported file. This behavior is the equivalent of :ref:`tag_inheritance`. Ansible applies the condition to every task, and evaluates each task separately. For example, you might have a playbook called ``main.yml`` and a tasks file called ``other_tasks.yml``::
274
275    # all tasks within an imported file inherit the condition from the import statement
276    # main.yml
277    - import_tasks: other_tasks.yml # note "import"
278      when: x is not defined
279
280    # other_tasks.yml
281    - name: Set a variable
282      ansible.builtin.set_fact:
283        x: foo
284
285    - name: Print a variable
286      ansible.builtin.debug:
287        var: x
288
289Ansible expands this at execution time to the equivalent of::
290
291    - name: Set a variable if not defined
292      ansible.builtin.set_fact:
293        x: foo
294      when: x is not defined
295      # this task sets a value for x
296
297    - name: Do the task if "x" is not defined
298      ansible.builin.debug:
299        var: x
300      when: x is not defined
301      # Ansible skips this task, because x is now defined
302
303Thus if ``x`` is initially undefined, the ``debug`` task will be skipped. If this is not the behavior you want, use an ``include_*`` statement to apply a condition only to that statement itself.
304
305You can apply conditions to ``import_playbook`` as well as to the other ``import_*`` statements. When you use this approach, Ansible returns a 'skipped' message for every task on every host that does not match the criteria, creating repetitive output. In many cases the :ref:`group_by module <group_by_module>` can be a more streamlined way to accomplish the same objective; see :ref:`os_variance`.
306
307.. _conditional_includes:
308
309Conditionals with includes
310^^^^^^^^^^^^^^^^^^^^^^^^^^
311
312When you use a conditional on an ``include_*`` statement, the condition is applied only to the include task itself and not to any other tasks within the included file(s). To contrast with the example used for conditionals on imports above, look at the same playbook and tasks file, but using an include instead of an import::
313
314    # Includes let you re-use a file to define a variable when it is not already defined
315
316    # main.yml
317    - include_tasks: other_tasks.yml
318      when: x is not defined
319
320    # other_tasks.yml
321    - name: Set a variable
322      ansible.builtin.set_fact:
323        x: foo
324
325    - name: Print a variable
326      ansible.builtin.debug:
327        var: x
328
329Ansible expands this at execution time to the equivalent of::
330
331    # main.yml
332    - include_tasks: other_tasks.yml
333      when: x is not defined
334      # if condition is met, Ansible includes other_tasks.yml
335
336    # other_tasks.yml
337    - name: Set a variable
338      ansible.builtin.set_fact:
339        x: foo
340      # no condition applied to this task, Ansible sets the value of x to foo
341
342    - name: Print a variable
343      ansible.builtin.debug:
344        var: x
345      # no condition applied to this task, Ansible prints the debug statement
346
347By using ``include_tasks`` instead of ``import_tasks``, both tasks from ``other_tasks.yml`` will be executed as expected. For more information on the differences between ``include`` v ``import`` see :ref:`playbooks_reuse`.
348
349Conditionals with roles
350^^^^^^^^^^^^^^^^^^^^^^^
351
352There are three ways to apply conditions to roles:
353
354  - Add the same condition or conditions to all tasks in the role by placing your ``when`` statement under the ``roles`` keyword. See the example in this section.
355  - Add the same condition or conditions to all tasks in the role by placing your ``when`` statement on a static ``import_role`` in your playbook.
356  - Add a condition or conditions to individual tasks or blocks within the role itself. This is the only approach that allows you to select or skip some tasks within the role based on your ``when`` statement. To select or skip tasks within the role, you must have conditions set on individual tasks or blocks, use the dynamic ``include_role`` in your playbook, and add the condition or conditions to the include. When you use this approach, Ansible applies the condition to the include itself plus any tasks in the role that also have that ``when`` statement.
357
358When you incorporate a role in your playbook statically with the ``roles`` keyword, Ansible adds the conditions you define to all the tasks in the role. For example:
359
360.. code-block:: yaml
361
362   - hosts: webservers
363     roles:
364        - role: debian_stock_config
365          when: ansible_facts['os_family'] == 'Debian'
366
367.. _conditional_variable_and_files:
368
369Selecting variables, files, or templates based on facts
370-------------------------------------------------------
371
372Sometimes the facts about a host determine the values you want to use for certain variables or even the file or template you want to select for that host. For example, the names of packages are different on CentOS and on Debian. The configuration files for common services are also different on different OS flavors and versions. To load different variables file, templates, or other files based on a fact about the hosts:
373
374  1) name your vars files, templates, or files to match the Ansible fact that differentiates them
375
376  2) select the correct vars file, template, or file for each host with a variable based on that Ansible fact
377
378Ansible separates variables from tasks, keeping your playbooks from turning into arbitrary code with nested conditionals. This approach results in more streamlined and auditable configuration rules because there are fewer decision points to track.
379
380Selecting variables files based on facts
381^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
382
383You can create a playbook that works on multiple platforms and OS versions with a minimum of syntax by placing your variable values in vars files and conditionally importing them. If you want to install Apache on some CentOS and some Debian servers, create variables files with YAML keys and values. For example::
384
385    ---
386    # for vars/RedHat.yml
387    apache: httpd
388    somethingelse: 42
389
390Then import those variables files based on the facts you gather on the hosts in your playbook::
391
392    ---
393    - hosts: webservers
394      remote_user: root
395      vars_files:
396        - "vars/common.yml"
397        - [ "vars/{{ ansible_facts['os_family'] }}.yml", "vars/os_defaults.yml" ]
398      tasks:
399      - name: Make sure apache is started
400        ansible.builtin.service:
401          name: '{{ apache }}'
402          state: started
403
404Ansible gathers facts on the hosts in the webservers group, then interpolates the variable "ansible_facts['os_family']" into a list of filenames. If you have hosts with Red Hat operating systems (CentOS, for example), Ansible looks for 'vars/RedHat.yml'. If that file does not exist, Ansible attempts to load 'vars/os_defaults.yml'. For Debian hosts, Ansible first looks for 'vars/Debian.yml', before falling back on 'vars/os_defaults.yml'. If no files in the list are found, Ansible raises an error.
405
406Selecting files and templates based on facts
407^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
408
409You can use the same approach when different OS flavors or versions require different configuration files or templates. Select the appropriate file or template based on the variables assigned to each host. This approach is often much cleaner than putting a lot of conditionals into a single template to cover multiple OS or package versions.
410
411For example, you can template out a configuration file that is very different between, say, CentOS and Debian::
412
413    - name: Template a file
414      ansible.builtin.template:
415        src: "{{ item }}"
416        dest: /etc/myapp/foo.conf
417      loop: "{{ query('first_found', { 'files': myfiles, 'paths': mypaths}) }}"
418      vars:
419        myfiles:
420          - "{{ ansible_facts['distribution'] }}.conf"
421          -  default.conf
422        mypaths: ['search_location_one/somedir/', '/opt/other_location/somedir/']
423
424.. _commonly_used_facts:
425
426Commonly-used facts
427===================
428
429The following Ansible facts are frequently used in conditionals.
430
431.. _ansible_distribution:
432
433ansible_facts['distribution']
434-----------------------------
435
436Possible values (sample, not complete list)::
437
438    Alpine
439    Altlinux
440    Amazon
441    Archlinux
442    ClearLinux
443    Coreos
444    CentOS
445    Debian
446    Fedora
447    Gentoo
448    Mandriva
449    NA
450    OpenWrt
451    OracleLinux
452    RedHat
453    Slackware
454    SLES
455    SMGL
456    SUSE
457    Ubuntu
458    VMwareESX
459
460.. See `OSDIST_LIST`
461
462.. _ansible_distribution_major_version:
463
464ansible_facts['distribution_major_version']
465-------------------------------------------
466
467The major version of the operating system. For example, the value is `16` for Ubuntu 16.04.
468
469.. _ansible_os_family:
470
471ansible_facts['os_family']
472--------------------------
473
474Possible values (sample, not complete list)::
475
476    AIX
477    Alpine
478    Altlinux
479    Archlinux
480    Darwin
481    Debian
482    FreeBSD
483    Gentoo
484    HP-UX
485    Mandrake
486    RedHat
487    SGML
488    Slackware
489    Solaris
490    Suse
491    Windows
492
493.. Ansible checks `OS_FAMILY_MAP`; if there's no match, it returns the value of `platform.system()`.
494
495.. seealso::
496
497   :ref:`working_with_playbooks`
498       An introduction to playbooks
499   :ref:`playbooks_reuse_roles`
500       Playbook organization by roles
501   :ref:`playbooks_best_practices`
502       Tips and tricks for playbooks
503   :ref:`playbooks_variables`
504       All about variables
505   `User Mailing List <https://groups.google.com/group/ansible-devel>`_
506       Have a question?  Stop by the google group!
507   `irc.libera.chat <https://libera.chat/>`_
508       #ansible IRC chat channel
509