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