In this lab, we will work through exercises to help us understand how to use conditional elements in our playbooks to allow for decisions to be made based on information provided. Conditionals provide a way for you to build more advanced playbooks that are efficient and streamlined.
Ensure you are in the ciq-basics directory
Create a folder for this lab, let’s call it lab01
You are now ready to start the lab.
Often in a playbook it may be useful to store the result of a given command in a variable and access it later. Use of the shell module in this way can in many ways eliminate the need to write site specific facts, for instance, you could test for the existence of a particular program.
The register module decides what variable to save a result in. The resulting variables can be used in templates, action lines, or when statements. In our first exercise we will build a playbook that runs a command and registers the standard out response to a variable. We’ll then call that variable and display it in the debug output.
Create a new file called 01_register.yml:
Enter the following lines into your playbook:
- name: Register a Variable and send the content to the debug log
hosts: all
gather_facts: false
tasks:
- shell: hostname
register: my_hostname
- debug:
msg: "my hostname is {{ my_hostname.stdout }}"Sample Output
PLAY [Register a Variable and send the content to the debug log] ***************
TASK [shell] *******************************************************************
changed: [node-1]
TASK [debug] *******************************************************************
ok: [node-1] => {
"msg": "my hostname is node1"
}
PLAY RECAP *********************************************************************
node-1 : ok=2 changed=1 unreachable=0 failed=0
If all things go well, you should see output that looks similar to the sample output above. Note that in log output we see the debug task displaying our message with our registered variable included.
Registering variables is great but what’s even better is to have some mechanism for Ansible to gather a baseline of information about the systems we are automating and keep track of them so that you don’t have to run commands. You can do this with gather_facts. You will note that in our previous playbook we had a line in this playbook that told ansible to NOT gather facts. In this next exercise we are going to turn that off and see what Ansible discovers about the system.
Copy your existing playbook from 01_register.yml to 02_facts.yml:
Edit 02_facts.yml and change the contents to look like the playbook below:
- name: Use gather_facts to send the content to the debug log
hosts: all
gather_facts: true
tasks:
- debug:
msg: "my hostname is {{ ansible_fqdn }}"As you see we changed gather_facts to be true. We remove the need of setting a variable and we changed our debug message to use a new variable. Let’s run the playbook and see what happens.
Sample Output
PLAY [Register a Variable and send the content to the debug log] ***************
TASK [Gathering Facts] *********************************************************
ok: [node-1]
TASK [debug] *******************************************************************
ok: [node-1] => {
"msg": "my hostname is node1.example.com"
}
PLAY RECAP *********************************************************************
node-1 : ok=2 changed=0 unreachable=0 failed=0
Isn’t that interesting, we removed code and didn’t set a variable, yet we were able to use a variable to bring in information?! The reason we can do this is because when we set gather_facts to be true, we were telling Ansible that we want it to go out and find out all the information it can about the systems we are managing. This is a very good way of making your automation more efficient. In this example I told you what the fact name was that would give us the fully qualified name of the host. You might be asking yourself, how do I find out all the other facts about this host? One way to do this is to run the Ansible setup module as an ad-hoc command.
Sample Output
...
"type": "ether"
},
"ansible_fips": false,
"ansible_form_factor": "Other",
"ansible_fqdn": "node1.example.com",
"ansible_hostname": "node1",
"ansible_interfaces": [
"lo",
"eth0"
],
"ansible_is_chroot": true,
"ansible_iscsi_iqn": "",
"ansible_kernel": "3.10.0-327.36.3.el7.x86_64",
"ansible_lo": {
"active": true,
"device": "lo",
"features": {
"busy_poll": "off [fixed]",
"fcoe_mtu": "off [fixed]",
...
You should see a long list of JSON formatted data speed by. You can scroll up in your browswer and see all of the information about the system that Ansible was able to gather when we ran setup (e.g. gathered facts). I snipped out some of the data so you can see where I found the ansible_fqdn variable that contained the data i wanted. What is great is that you will see that we are gathering facts about every machine we are managing. So if you had 200 servers in your inventory we would have all of that information and could use it to uniquely automate each host.
Sometimes you will want to skip certain tasks you your playbook depending on certain criteria. This could be something as simple as not installing a certain package if the operating system is a particular version, or it could be something like performing some cleanup steps if a filesystem is getting full. This is easy to do in Ansible with the when clause. As you might guess, when allows you to determine when an action runs. Let’s take a look at that in action.
Create a new file called 03_when.yml:
Enter the following lines into your playbook:
- name: Use gather_facts and when to control which block of code to use
hosts: all
gather_facts: true
tasks:
- name: "Print out a custom message when the OS is Debian"
debug:
msg: "Im running Debian"
when: ansible_os_family == "Debian"
- name: "Print out a custom message when the OS is RedHat"
debug:
msg: "Im running Rocky"
when: ansible_os_family == "RedHat"In this playbook we have created two different tasks that only run when a certain condition is met. In this case we are matching the value of the ansible_os_family fact that is gathered when we run the playbook. Run the playbook and check the output.
Sample Output
PLAY [Use gather_facts and when to control which block of code to use] *********
TASK [Gathering Facts] *********************************************************
ok: [node-1]
TASK [Print out a custom message when the OS is Debian] ************************
skipping: [node-1]
TASK [Print out a custom message when the OS is RedHat] ************************
ok: [node-1] => {
"msg": "Im running Rocky"
}
PLAY RECAP *********************************************************************
node-1 : ok=2 changed=0 unreachable=0 failed=0
Here we see that Ansible matched the value of the fact and then ran the appropriate block of code.
There are often times when you may want to do a number of tasks on each host you are managing. For this example we can imagine being tasked with adding a number of users to our web servers. We can employ the use of a loop and iterate through a list of users, ensuring that every one is added to the system. In our exercise below we will do just that.
- name: Create multiple users using a loop
hosts: all
gather_facts: false
become: true
tasks:
- name: add several users
user:
name: "{{ item }}"
state: present
groups: "wheel"
password: "{{ 'Ans1bl3R0cks!' | password_hash('sha512') }}"
loop:
- testuser1
- testuser2
- testuser3Notice that we are using the command loop and providing it a list of users below it. We are then using the variable item to provide each username to the name parameter. the combination of the loop command and the item variable we create a loop inside the user module that has the effect of creating each user. Without using loop we would have to have 3 different tasks in our playbook that each called out the testusers that we want to create. Run the playbook and let’s observe the results.
Sample Output
PLAY [Create multiple users using a loop] **************************************
TASK [add several users] *******************************************************
changed: [node-1] => (item=testuser1)
changed: [node-1] => (item=testuser2)
changed: [node-1] => (item=testuser3)
PLAY RECAP *********************************************************************
node-1 : ok=1 changed=1 unreachable=0 failed=0
Take note of the become: true, since we are running as a normal user, we need to elevate to the root user to run privileged commands. This parameter tells it to do so. We can do it at the top level like this to run every command elevated, or we can add this parameter per task.
Here we see quite a few changes that were made to the systems we are managing. In a few seconds we created 3 users, that is 3 changes you would be making that is probably quite easy to do by hand. However, What if we had 50 users on 100 servers, that’s 5000 changes, probably not something you would want to tackle by hand. In our case using ansible and a short and simple playbook we can execute this in a matter of a minute or two and we’d be assured that it was done perfectly every time.
We now have some really good conditionals that we can use to make our playbook more efficient and more modular. In this exercise we will combine register and loop to set variables and iterate through them in loops.
Create a new file called 05_register_loop.yml:
Enter the following lines into your playbook:
- name: loop through a set of registered variables
hosts: all
gather_facts: false
tasks:
- name: echo the contents of a loop
shell: "echo {{ item }}"
loop:
- "one"
- "two"
- "three"
register: echo
- name: Fail if return code is not 0
fail:
msg: "The command ({{ item.cmd }}) did not have a 0 return code"
when: item.rc != 0
loop: "{{ echo.results }}"You can see that we are defining a loop with the values one, two, three and using those to run a shell command. We then can take take the results of that and use that in another loop and combine it with a when statement to check the contents of the output. In this case we are looking for the result codes that are returned when we echo one, two and three. if the result code returned by that command failed and returned something other than 0 it would fail and spit out a failure message. Let’s see it in action.
Sample Output
PLAY [loop through a set of registered variables] ******************************
TASK [echo the contents of a loop] *********************************************
changed: [node-1] => (item=one)
changed: [node-1] => (item=two)
changed: [node-1] => (item=three)
TASK [Fail if return code is not 0] ********************************************
skipping: [node-1] => (item={'_ansible_parsed': True, 'stderr_lines': [], '_ansible_item_result': True, u'end': u'2018-09-26 15:59:58.930947', '_ansible_no_log': False, u'stdout': u'one', u'cmd': u'echo one', u'rc': 0, 'item': u'one', u'delta': u'0:00:00.002395', '_ansible_item_label': u'one', u'stderr': u'', u'changed': True, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'_raw_params': u'echo one', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin': None}}, 'stdout_lines': [u'one'], u'start': u'2018-09-26 15:59:58.928552', '_ansible_ignore_errors': None, 'failed': False})
skipping: [node-1] => (item={'_ansible_parsed': True, 'stderr_lines': [], '_ansible_item_result': True, u'end': u'2018-09-26 15:59:59.065822', '_ansible_no_log': False, u'stdout': u'two', u'cmd': u'echo two', u'rc': 0, 'item': u'two', u'delta': u'0:00:00.002410', '_ansible_item_label': u'two', u'stderr': u'', u'changed': True, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'_raw_params': u'echo two', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin': None}}, 'stdout_lines': [u'two'], u'start': u'2018-09-26 15:59:59.063412', '_ansible_ignore_errors': None, 'failed': False})
skipping: [node-1] => (item={'_ansible_parsed': True, 'stderr_lines': [], '_ansible_item_result': True, u'end': u'2018-09-26 15:59:59.199715', '_ansible_no_log': False, u'stdout': u'three', u'cmd': u'echo three', u'rc': 0, 'item': u'three', u'delta': u'0:00:00.002401', '_ansible_item_label': u'three', u'stderr': u'', u'changed': True, u'invocation': {u'module_args': {u'warn': True, u'executable': None, u'_uses_shell': True, u'_raw_params': u'echo three', u'removes': None, u'argv': None, u'creates': None, u'chdir': None, u'stdin': None}}, 'stdout_lines': [u'three'], u'start': u'2018-09-26 15:59:59.197314', '_ansible_ignore_errors': None, 'failed': False})
PLAY RECAP *********************************************************************
node-1 : ok=1 changed=1 unreachable=0 failed=0
We can see from the output that in the first task it looped through the shell module. We then see ansible skipping the three results because they didn’t return a non zero return code.
When working with complex registered variables in loops, the output can become very verbose and difficult to read. Ansible provides loop_control with loop_label to make the output more readable by displaying only specific parts of the loop items.
Create a new file called 05b_register_loop_label.yml:
Enter the following lines into your playbook:
- name: loop through a set of registered variables with cleaner output
hosts: all
gather_facts: false
tasks:
- name: echo the contents of a loop
shell: "echo {{ item }}"
loop:
- "one"
- "two"
- "three"
register: echo
- name: Fail if return code is not 0
fail:
msg: "The command ({{ item.cmd }}) did not have a 0 return code"
when: item.rc != 0
loop: "{{ echo.results }}"
loop_control:
loop_var: item
label: "{{ item.item }}"Sample Output
PLAY [loop through a set of registered variables with cleaner output] **********
TASK [echo the contents of a loop] *********************************************
changed: [node-1] => (item=one)
changed: [node-1] => (item=two)
changed: [node-1] => (item=three)
TASK [Fail if return code is not 0] ********************************************
skipping: [node-1] => (item=one)
skipping: [node-1] => (item=two)
skipping: [node-1] => (item=three)
PLAY RECAP *********************************************************************
node-1 : ok=1 changed=1 unreachable=0 failed=0
Notice how the output is much cleaner and more readable compared to the previous example. The loop_label shows only the original item value (one, two, three) instead of the entire registered result dictionary.
Another way to control your loops is to pause your loops. This capability allows you to control the time (in seconds) between execution of items in a task loop. One way you might use this is when you have tasks that take a random amount of time to execute. Before moving on to the next task in your loop you could have it pause 2 minutes to wait for the task to be completed.
Create a new file called 06_pause_loop.yml:
Enter the following lines into your playbook:
- name: loop through a set of shell commands pausing between each loop item
hosts: all
gather_facts: false
tasks:
- name: list a set of directories, pause 30s between loop items
shell: "ls {{ item }}"
loop:
- /etc
- /home
- /var
loop_control:
pause: 30Sample Output
PLAY [loop through a set of shell commands pausing between each] ***************
TASK [ls as set of directories, pause 30s between loop items] ******************
changed: [node-1] => (item=/etc)
changed: [node-1] => (item=/home)
changed: [node-1] => (item=/var)
PLAY RECAP *********************************************************************
node-1 : ok=1 changed=1 unreachable=0 failed=0
We can see in our output that between each directory we list Ansible pauses 30 seconds before moving to the next directory in the loop.
If you need to keep track of where you are in a loop, you can use the index_var option in loop_control to specify a variable name to contain the current loop index.
Create a new file called 07_index_loop.yml:
Enter the following lines into your playbook:
- name: loop through a list of items and display its index
hosts: all
gather_facts: false
tasks:
- name: count our items
debug:
msg: "{{ item }} with index {{ my_idx }}"
loop:
- pen
- pineapple
- apple
- pen
loop_control:
index_var: my_idxSample Output
PLAY [loop through a list of items and display its index] *********************
TASK [count our fruit] *********************************************************
ok: [node-1] => (item=pen) => {
"msg": "pen with index 0"
}
ok: [node-1] => (item=pineapple) => {
"msg": "pineapple with index 1"
}
ok: [node-1] => (item=apple) => {
"msg": "apple with index 2"
}
ok: [node-1] => (item=pen) => {
"msg": "pen with index 3"
}
PLAY RECAP *********************************************************************
node-1 : ok=1 changed=0 unreachable=0 failed=0
Having access to the index of a loop allows you to have a numeric value that represents both the number of items in your loop as well as where you are numerically when looping through items. An example of how you might build upon this is to add a when statement that does a task once you’ve looped through a certain number of items.
There are often times that you may want to retry a task until a certain condition is met. Perhaps we are waiting for a socket to open up to connect to or maybe we want to update a system with irregular connectivity we can use the until command to create a loop condition with a specified number of retries and time between retries.
Create a new file called 08_until.yml:
Enter the following lines into your playbook:
- name: run a command until it displays a certain message
hosts: all
gather_facts: false
tasks:
- name: run a command until it displays a certain message
shell: badcommand
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 5We are using some new elements in this playbook besides the until command. Along with the until command is a bit of code that allows us to take the variable that we registered “result” and grab the standard out and then use a filter called find to look for specific words in the result variable. We then apply a condition. In this case the command we are running will never work so when it fails it returns a return code of -1. Our condition says that as long as we are getting a return code of -1 we will keep retrying the task every 5 seconds for a total of 5 times. Let’s see it in action.
Sample Output
PLAY [run a command until it displays a certain message] ***********************
TASK [run a command until it displays a certain message] ***********************
FAILED - RETRYING: run a command until it displays a certain message (5 retries left).
FAILED - RETRYING: run a command until it displays a certain message (4 retries left).
FAILED - RETRYING: run a command until it displays a certain message (3 retries left).
FAILED - RETRYING: run a command until it displays a certain message (2 retries left).
FAILED - RETRYING: run a command until it displays a certain message (1 retries left).
fatal: [node-1]: FAILED! => {"attempts": 5, "changed": true, "cmd": "badcommand", "delta": "0:00:00.002652", "end": "2018-09-26 18:08:13.711280", "msg": "non-zero return code", "rc": 127, "start": "2018-09-26 18:08:13.708628", "stderr": "badcommand: command not found", "stderr_lines": ["badcommand: command not found"], "stdout": "", "stdout_lines": []}
PLAY RECAP *********************************************************************
node-1 : ok=0 changed=0 unreachable=0 failed=1
As we said, this particular playbook is one that we built intentionally to fail so that we could see the retry in action.
When working with complex automation scenarios, you might need to loop through multiple dimensions of data while including external task files. This requires careful management of loop variables to avoid conflicts between the main playbook and included tasks.
An important limitation in Ansible is that you cannot directly loop over a block of tasks. However, you can achieve similar functionality by using include_tasks with loops. This approach allows you to effectively “loop over a block” by placing multiple related tasks in a separate file and then including that file multiple times with different variables.
Create a new file called 09_nested_include.yml:
Enter the following lines into your playbook:
- name: Process multiple services with their configurations
hosts: all
gather_facts: false
vars:
services:
- name: "webserver"
configs: ["httpd.conf", "ssl.conf", "vhosts.conf"]
- name: "database"
configs: ["my.cnf", "users.conf"]
- name: "cache"
configs: ["redis.conf", "cluster.conf", "sentinel.conf"]
tasks:
- name: Process each service
include_tasks: process_service.yml
loop: "{{ services }}"
loop_control:
loop_var: service_item- name: Display service name
debug:
msg: "Processing service: {{ service_item.name }}"
- name: Process each configuration file for {{ service_item.name }}
debug:
msg: " Configuring {{ service_item.name }} with {{ item }}"
loop: "{{ service_item.configs }}"Sample Output
PLAY [Process multiple services with their configurations] *********************
TASK [Process each service] *****************************************************
included: /path/to/process_service.yml for node-1
included: /path/to/process_service.yml for node-1
included: /path/to/process_service.yml for node-1
TASK [Display service name] *****************************************************
ok: [node-1] => {
"msg": "Processing service: webserver"
}
TASK [Process each configuration file for webserver] ***************************
ok: [node-1] => (item=httpd.conf) => {
"msg": " Configuring webserver with httpd.conf"
}
ok: [node-1] => (item=ssl.conf) => {
"msg": " Configuring webserver with ssl.conf"
}
ok: [node-1] => (item=vhosts.conf) => {
"msg": " Configuring webserver with vhosts.conf"
}
TASK [Display service name] *****************************************************
ok: [node-1] => {
"msg": "Processing service: database"
}
TASK [Process each configuration file for database] ****************************
ok: [node-1] => (item=my.cnf) => {
"msg": " Configuring database with my.cnf"
}
ok: [node-1] => (item=users.conf) => {
"msg": " Configuring database with users.conf"
}
Note how we use loop_var: service_item in the main playbook to avoid conflicts with the item variable used in the included tasks file. This allows both loops to work independently.
In this lab we dove into what conditionals can do for you inside your playbooks. Hopefully you’ve seen how, just like Ansible modules, conditionals can be used together and in conjunction with another to answer all of your automation decision points.