Control structures refer to anything and everything that have an effect on a program's execution flow. Control structures are mainly of the following two types:
- Conditional
- Iterative
At times, we need to execute code conditionally based on a value of a variable, type of platform, or even a result of some other command. There are times when we also need to iterate multiple objects, such as list hashes or multilevel variables.
We will cover following 3 different kinds of conditional statements in this section
when
failed_when
changed_when
Using when statement
The value of the when key is a Jinja2 expression without the curly braces. The task is only executed if this Jinja2 expression evaluates to true. This Jinja2 expression can evaluate the value of a variable. It can contain Jinja2 filters, and it can also include logical operators and groupings.
We will use our existing playbook from last exercise to first understand how when
condition works:
--- - name: when conditional operator hosts: localhost gather_facts: false vars: x: 10 y: 15 list: [10,20,30] tasks: - debug: msg: - "The value of x is {{ x }} and the value of y is {{ y }}" - "Our list contains: {{ list }}" when: x == y - debug: msg: "x is present in the list" when: x in list
NOTE:
when
statement. It should have same indentation as used for the module name (without hyphen). In my case I have placed when
statement with the same number of spaces as given to debug
module or else you will get syntax errorIn this conditional-operator.yml
playbook I have defined a condition using "when
" for the first task using debug
module. So if the condition is TRUE i.e. x == y
then only the first task will be executed or else it will be skipped
Similarly in the second task I have added another condition i.e. if x is present in the list
then only the second task will be executed or else it will be skipped
Let us execute the playbook
[ansible@controller ~]$ ansible-playbook conditional-operator.yml PLAY [when conditional operator] *********************************************************************************** TASK [debug] ******************************************************************************************************* skipping: [localhost] TASK [debug] ******************************************************************************************************* ok: [localhost] => { "msg": "x is present in the list" } PLAY RECAP ********************************************************************************************************* localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
So our first task was skipped as x
is not equal to y
while the second task has been executed because x
was part of the list
Example-1: Install HTTPD
Let us take some practical example now:
--- - name: Installing HTTPD hosts: server2 become: true gather_facts: false vars: pkg: httpd tasks: - shell: rpm -q httpd | head -n 1 register: result - set_fact: result: "{{ result.stdout }}" - debug: msg: "Is the {{ pkg }} installed: {{ result }}" - yum: name: "{{ pkg }}" state: latest when: result | regex_search("not installed")
We will use this playbook install-httpd.yml
to install httpd
package on server2
but with a condition i.e. the playbook should install the package only if the package is "not installed" already. If the pkg is already installed then "do nothing"
I have written 4 TASKS in this playbook
- Check if
httpd
package is installed usingshell
module and register the output intoresult
variable - Use
set_fact
to store the output intoresult
variable as we are doing some operation runtime and storing the variable we couldn't usevars
(this is covered in Ansible Variables and Data Types chapter) - Using
debug
module get the output of our variable to make sure it is getting proper output - Use
yum
module to install thepkg
based on thewhen
condition. We have usedregex_search
to search for string "not installed
" in theresult
variable
Let us execute this playbook. Since I am using rpm command, ansible
is throwing the warning but since we are using rpm
command just to get the package list, the warning can be ignored.
[ansible@controller ~]$ ansible-playbook install-httpd.yml PLAY [Installing HTTPD] ******************************************************************************************** TASK [shell] ******************************************************************************************************* [WARNING]: Consider using the yum, dnf or zypper module rather than running 'rpm'. If you need to use command because yum, dnf or zypper is insufficient you can add 'warn: false' to this command task or set 'command_warnings=False' in ansible.cfg to get rid of this message. changed: [server2] TASK [set_fact] **************************************************************************************************** ok: [server2] TASK [debug] ******************************************************************************************************* ok: [server2] => { "msg": "Is the httpd installed: httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64" } TASK [yum] ********************************************************************************************************* skipping: [server2] PLAY RECAP ********************************************************************************************************* server2 : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
As per the debug
module output, since httpd
was already installed our TASK4 was skipped so no installation was performed. Now I have removed the httpd
package from server2
and let's re-run this playbook:
[ansible@controller ~]$ ansible-playbook install-httpd.yml PLAY [Installing HTTPD] ******************************************************************************************** TASK [shell] ******************************************************************************************************* [WARNING]: Consider using the yum, dnf or zypper module rather than running 'rpm'. If you need to use command because yum, dnf or zypper is insufficient you can add 'warn: false' to this command task or set 'command_warnings=False' in ansible.cfg to get rid of this message. changed: [server2] TASK [set_fact] **************************************************************************************************** ok: [server2] TASK [debug] ******************************************************************************************************* ok: [server2] => { "msg": "Is the httpd installed: package httpd is not installed" } TASK [yum] ********************************************************************************************************* changed: [server2] PLAY RECAP ********************************************************************************************************* server2 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
So this time httpd
package was successfully installed
Using failed_when statement
If we take our last example playbook which we used to install the httpd
package. I had written 4 TASKS to achieve our agenda, although we could remove the debug
task but there is another way to handle such use case.
We can use failed_when
condition to intentionally fail the script based on the condition check. I have updated the same playbook with failed_check
condition and also added warn: false to avoid the warning for using rpm
command
--- - name: Installing HTTPD hosts: server2 become: true gather_facts: false vars: pkg: httpd tasks: - shell: rpm -q httpd | head -n 1 args: warn: false register: result failed_when: "'not installed' not in result.stdout" - yum: name: "{{ pkg }}" state: present
Now in this playbook under shell task, we query the httpd
package and check the stdout
. If the stdout
contains "not installed
" then it means that httpd
package is not installed so the playbook can go ahead and install the pkg
but if not found then it means httpd
package is already in installed state so mark the playbook as failed and exit without executing any further tasks.
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook install-httpd.yml PLAY [Installing HTTPD] ******************************************************************************************** TASK [shell] ******************************************************************************************************* fatal: [server2]: FAILED! => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"}, "changed": true, "cmd": "rpm -q httpd | head -n 1", "delta": "0:00:00.015037", "end": "2020-09-25 07:39:33.713691", "failed_when_result": true, "rc": 0, "start": "2020-09-25 07:39:33.698654", "stderr": "", "stderr_lines": [], "stdout": "httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64", "stdout_lines": ["httpd-2.4.37-21.module_el8.2.0+494+1df74eae.x86_64"]} PLAY RECAP ********************************************************************************************************* server2 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
So the playbook is marked as failed because httpd
package is already installed on server2
. You may use "-vvv
" with the ansible-playbook command to get a verbose output which can you you more idea on the OUTPUT of our task.
Print message with fail
In the above example we did fail the playbook based on a condition match but there was no output on the console so operator may get confuse if the playbook has a BUG or the failure was intentional. So we can use fail module instead of failed_when
to be able to print a message on the console:
I have updated our playbook with fail
module and added one debug
module to check if the playbook reaches that stage or exits at the failed stage itself.
--- - name: Installing HTTPD hosts: server2 become: true gather_facts: false vars: pkg: httpd tasks: - shell: rpm -q httpd | head -n 1 args: warn: false register: result - fail: msg: "Failed because {{ pkg }} is already installed" when: "'not installed' not in result.stdout" - debug: msg: do we get here - yum: name: "{{ pkg }}" state: present
Let us execute this playbook:
[ansible@controller ~]$ ansible-playbook install-httpd.yml PLAY [Installing HTTPD] ******************************************************************************************** TASK [shell] ******************************************************************************************************* changed: [server2] TASK [fail] ******************************************************************************************************** fatal: [server2]: FAILED! => {"changed": false, "msg": "Failed because httpd is already installed"} PLAY RECAP ********************************************************************************************************* server2 : ok=1 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
So the playbook has exited with our custom message and the remaining part of the playbook was never called.
Using changed_when statement
We will continue to use our existing install-httpd.yml
playbook file. If you observe the SUMMARY from the output of our last example:
PLAY RECAP ********************************************************************************************************* server2 : ok=1 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
It shows changed=1
even when we know nothing was changed. The playbook just did a query of httpd
package and since it was already in installed state the playbook was marked as failed and it exited without performing any further tasks.
It is possible some module thinks it changed the state of the target host which is when it reports the changed
status but in this case since we know nothing is changed as part of this module we can suppress that output using "changed: false
"
Here is the updated version of our playbook:
--- - name: Installing HTTPD hosts: server2 become: true gather_facts: false vars: pkg: httpd tasks: - shell: rpm -q httpd | head -n 1 args: warn: false register: result changed_when: false - fail: msg: "Failed because {{ pkg }} is already installed" when: "'not installed' not in result.stdout" - debug: msg: do we get here - yum: name: "{{ pkg }}" state: present
In this playbook I have added "changed: false
" for the shell
module task. Let us execute this play:
[ansible@controller ~]$ ansible-playbook install-httpd.yml PLAY [Installing HTTPD] ******************************************************************************************** TASK [shell] ******************************************************************************************************* ok: [server2] TASK [fail] ******************************************************************************************************** fatal: [server2]: FAILED! => {"changed": false, "msg": "Failed because httpd is already installed"} PLAY RECAP ********************************************************************************************************* server2 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
So the playbook execution is marked as failed as expected and changed=0
so no change was done on the remote host.
Let me manually remove the httpd
package from server2
and see if the playbook succeeds
[ansible@controller ~]$ ansible-playbook install-httpd.yml PLAY [Installing HTTPD] ******************************************************************************************** TASK [shell] ******************************************************************************************************* ok: [server2] TASK [fail] ******************************************************************************************************** skipping: [server2] TASK [debug] ******************************************************************************************************* ok: [server2] => { "msg": "do we get here" } TASK [yum] ********************************************************************************************************* changed: [server2] PLAY RECAP ********************************************************************************************************* server2 : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
So our remaining tasks are also executed and httpd
was installed with changed=1
What's Next
Next in our Ansible tutorial we will learn all about Ansible handlers and how to use handlers in Ansible playbook.