Table of Contents
For some people a simple playbook is not always practical. So you find that you need some logic in your playbooks for your not so simple use case. Thankfully Ansible provides multiple ways to do this particularly using Jinja in Ansible’s templating.
However, because of this - I find it quite tricky to understand and use the logic techniques and the different data types that can potentially get passed around in my playbooks because Ansible uses YAML template and Jinja. So I wrote this guide to help your through the hassle of that!
Pre-requisites & Setup
- I’m using
ansible-core 2.14
- Ansible Host is running Python 3 and Managed/remote host is running Python 2.7
- Ansible Host & Managed/remote host is using Ubuntu
You can use a different version of python or Ansible core but just be wary that some of the implementations on this document might not work on much older versions of Ansible or require a different method
What are Ansible Conditionals and Ansible the When Keyword?
Conditionals in Ansible is simply a condition that is set in order to perform a task. This is done by using the when
keyword.
- hosts: docker-server
vars:
trigger_condition: fasle
tasks:
- ansible.builtin.debug:
msg: "This message triggered due to the conditional"
when: trigger_condition == true
- ansible.builtin.debug:
msg: "This message will always run"
What are Ansible Tests?
According to the Ansible docs, Tests are:
a way of evaluating template expressions and returning True or False
Ansible Test vs Filter?
After ansible 2.9 you must use jinja test syntax as opposed to filter syntax when performing a test:
- filter syntax:
{{ name|striptags|title }}
(remove all HTML Tags from variable name and title-case the output (title(striptags(name)))) - test syntax:
{% if loop.index is divisibleby 3 %}
Tests will be used for comparisons, and filter is used for data manipulation. Having said this, You can still use tests in filters e.g. in a select()
filter you can use test logic to select a value for a list
You can use all the built-in Jinja Tests with Ansible, but Ansible also comes with its other set of tests.
What is “assert” in Ansible?
Assert is a core ansible module that is used for asserting if an expression is true whilst displaying a custom messages based on the outcome.
This is a very useful module when you want to add logic to your playbooks that may require multiple conditions.
If you run the playbook example below, it will stop at task 2 because it will fail at task 2. However, if I set ignore_errors
to true
, task 2 will fail but will not stop task 3 from happening:
- hosts: docker-server
vars:
counter1: 0
counter2: 3
counter3: -1
tasks:
- ansible.builtin.assert:
that:
- counter1 >= 0
- counter1 <= 1
success_msg: "counter is between 0 and 2"
fail_msg: "counter needs to be between 0 and 2"
- ansible.builtin.assert:
that:
- counter2 >= 0
- counter2 <= 1
success_msg: "counter is between 0 and 2"
fail_msg: "counter needs to be between 0 and 2"
- ansible.builtin.assert:
that:
- counter3 >= 0
- counter3 <= 1
success_msg: "counter is between 0 and 2"
fail_msg: "counter needs to be between 0 and 2"
Is a variable is defined or registered or undefined
To test if a variable is defined (or not), we simply need to use the defined
and undefined
keywords.
Using the Jinja defined() to check for defined or undefined variables
Variable name
is defined but nothing is assigned, results in “name var is defined”:
- hosts: docker-server
vars:
name:
tasks:
- ansible.builtin.debug:
msg: "name var is defined"
when: name is defined
Variable name
is defined but is an empty string, results in “name var is defined”:
- hosts: docker-server
vars:
name: ""
tasks:
- ansible.builtin.debug:
msg: "name var is defined"
when: name is defined
Variable name
is not defined in the var section, task does not run.
- hosts: docker-server
vars:
tasks:
- ansible.builtin.debug:
msg: "name var is defined"
when: name is defined
Using Jinja undefined() test to check for defined or undefined variables
Variable name
is defined, results in task not running:
- hosts: docker-server
vars:
name:
tasks:
- ansible.builtin.debug:
msg: "name var is defined"
when: name is undefined
- hosts: docker-server
vars:
name:
tasks:
- ansible.builtin.debug:
msg: "name var is defined"
when: name is undefined
Variable name
is not defined, results in the task running:
- hosts: docker-server
vars:
tasks:
- ansible.builtin.debug:
msg: "name var is defined"
when: name is undefined
Is a variable is true or false
You can use the simple is
or is not
in your tests. You can use something like var | bool == True
- But I be wary because it will result in True even if the value is true
or "true"
.
value = true, task will run:
- hosts: docker-server
vars:
value: true
tasks:
- ansible.builtin.debug:
msg: "is true"
when: value is true
value = “true” (Note the quotes), Task will NOT run:
- hosts: docker-server
vars:
value: "true"
tasks:
- ansible.builtin.debug:
msg: "is true"
when: value is true
value = true (Note the quotes), Task will NOT run:
- hosts: docker-server
vars:
value: true
tasks:
- ansible.builtin.debug:
msg: "is true"
when: value is not true
Is a variable is empty
There are multiple ways to check if something is empty, but each method will be different for different data types. On top of that Ansible has a truthy and falsy test which allows us to test in a python-like truthy and falsy way.
This stackOverflow Answer shows us that everything is a considered “truthy” except the following values which are considered “falsy”:
None
False
0
0.0
[]
an empty list{}
an empty dict""
and empty string
Check if string / dict / array empty or None
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "is falsy - value is None"
when: value is falsy
vars:
value:
- ansible.builtin.debug:
msg: "is falsy - empty string"
when: value is falsy
vars:
value: ""
- ansible.builtin.debug:
msg: "is falsy - empty array"
when: value is falsy
vars:
value: []
- ansible.builtin.debug:
msg: "is falsy - empty dict"
when: value is falsy
vars:
value: {}
You can also use not
to achieve the same results as a falsy check:
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "is falsy - empty dict"
when: not value
vars:
value:
Other ways to check string / dict / array is empty
Using the Jinja length
filter.
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "empty string"
when: value | length <= 0
vars:
value: ""
- ansible.builtin.debug:
msg: "empty dict"
when: value.keys() | length <= 0
vars:
value: {}
- ansible.builtin.debug:
msg: "empty list"
when: value | length <= 0
vars:
value: []
Is variable equal to a string
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "string matches"
when: value == "hi"
vars:
value: "hi"
(Advanced) ways to match string e.g. pattern or regex matching
Ansible offers other ways to match string patterns using the match
, search
and regex
tests for this. You also can use the multiline
and ignorecase
if you’re dealing with multi-line strings or if you want to ignore string’s case. More details in the Ansible Docs
Example - wildcard matching file names using match
Using match
We match for any substring that has .log
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "Will not match because ''.log' needs to be at start of string"
when: file_name is match('.log')
vars:
file_name: "test_data.log"
- ansible.builtin.debug:
msg: "match any file name with .log in its name"
when: file_name is match('.*log')
vars:
file_name: "test_data.log"
- ansible.builtin.debug:
msg: "will still match since '.*log was matched'"
when: file_name is match('.*log')
vars:
file_name: "test_data.log.1"
How match
works
match
returns true if the string pattern matches the start of the target string - which is why task 1 fails.- Task 2 and 3 return True because both match even if there are more characters after the matched part
match
is typically used when you want to check if a keyword exists at least once in a string.
Wildcard matching URL pattern using search
Using search
to search for any matching substring pattern.
- ansible.builtin.debug:
msg: "found items str"
when: url is search('items')
vars:
url: "http://example.com/api/1234/items/item/434"
- ansible.builtin.debug:
msg: "found api/.*/items pattern"
when: url is search('/api/.*/items')
vars:
url: "http://example.com/api/1234/items/item/434"
- ansible.builtin.debug:
msg: "Will not find api/.*/update pattern"
when: url is search('/api/.*/update')
vars:
url: "http://example.com/api/1234/items/item/434"
search
works similarly to match
except it looks for a pattern anywhere the target string. You can use wildcards to enhance your pattern.
Using regex matching for simple email validation
regex
works like search
but allows for more options when it comes to other types of tests.
In this example we use regex
for validating if a string is a valid email or not
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "valid email"
when: email is regex('\S+@\S+\.\S+$')
vars:
email: "example@gmail.com"
- ansible.builtin.debug:
msg: "invalid email"
when: email is not regex('\S+@\S+\.\S+$')
vars:
email: "example--gmail.com"
Does the item exist in list (or is not in list)
Here we check if some items are in (or not in) a list
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "hello value in items list"
when: '"hello" in items'
vars:
items:
- "hello"
- "bye"
- ansible.builtin.debug:
msg: "hello value not in items list"
when: '"hello" not in items'
vars:
items:
- "bye"
- ansible.builtin.debug:
msg: "1 value in items list"
when: '1 in items'
vars:
items:
- 1
- "bye"
Does the key or value exists in dictionary
Check if dictionary key exists (or does not), also in nested key
Here we check if a key is in or is not in a dictionary variable. The last example shows how you can check if a key exists in a nested dictionary.
- ansible.builtin.debug:
msg: "key:hello in items dict"
when: '"hello" in items'
vars:
items:
hello: "hi"
- ansible.builtin.debug:
msg: "key:hello not in items dict"
when: '"hello" not in items'
vars:
items:
bye: "bye"
- ansible.builtin.debug:
msg: "key:hello in items dict"
when: '"hello" in items.nested_items'
vars:
items:
nested_items:
hello: "hello"
Check if dictionary value exists (or does not)
Quite simply:
- ansible.builtin.debug:
msg: "value hi equal the 'hello' key"
when: items.hello == "hi"
vars:
items:
hello: "hi"
Example Scenarios
Here we look at different scenarios that use conditionals and tests!
Scenario - Assert Check two string keywords or multiple test conditions
Asserts that “ford” AND “focus” substring is found in the “model” variable. Assert will return success when all conditions are correct
- hosts: docker-server
tasks:
- ansible.builtin.assert:
that:
- "'ford' in model"
- "'focus' in model"
fail_msg: "wrong car model"
success_msg: "correct car model"
vars:
model: "ford focus ST"
Asserts that either “ford” OR “honda” substring is found in the “model” variable.
- ansible.builtin.assert:
that:
- "'ford' in model | lower or 'honda' in model | lower"
fail_msg: "wrong car model"
success_msg: "correct car model"
vars:
model: "Ford civic SE"
Note: I add in the lower()
Jinja filter so that assert
is not case-sensitive.
Scenario - Run Task when variable is undefined
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "name var is undefined"
when: name is undefined
Scenario - Run task only when a variable contains a specific string
There are 2 ways to go about this:
- Use
in
: which can also be used when checking if a value is in a list or dict - Use
search
: which will check if a string is inside your target string
- hosts: docker-server
tasks:
- ansible.builtin.debug:
msg: "value found"
when: "'hi' in message"
vars:
message: "hi, nice to meet you"
- ansible.builtin.debug:
msg: "value found"
when: "message is search('hi')"
vars:
message: "hi nice to meet you"
Scenario - Check that a list of variables have been defined
In this example, specify a list of variables that need to be defined, and we check that each of them are defined. var1 is defined, whilst var2 and var3 are not:
- hosts: docker-server
vars:
var1:
list_of_vars_to_define:
- var1
- var2
- var3
tasks:
- ansible.builtin.debug:
msg: "Need to set variable {{ item }}"
when: vars[item] is undefined
loop: "{{ list_of_vars_to_define }}"
Note:
- we use a loop to repeat the task for each variable name that we want to check
- we use
vars[item]
because if just use ‘'’item’’’ the expression we are evaluating will be"var1" is undefined
Scenario - Assert failure when source list holds more values than target list
In this scenario we want to make compare 2 lists and make sure that the target list’s length is equal to the source list’s length:
- hosts: docker-server
vars:
source_list: [1,2,3,4]
target_list: [1,2,3,4,5,6]
tasks:
- ansible.builtin.assert:
that:
- source_list | length == target_list | length
success_msg: "Both Lists have matching lengths"
fail_msg: "Both Lists DO NOT have matching lengths"
Explanation
- We get the length of each list using the
length
jinja filter - We check that both lists have the same length
Scenario - Check if for a value match in list of dictionary/objects
We have a scenario where we have a list of exempt host names (a list of objects/dictionaries). We want our task to only run when the current ansible_hostname
is not on that list - This is a scenario where we want to find out if a value exists in a list of dictionaries.
- hosts: docker-server
vars:
list_of_exempt_hosts:
- { host_name: "api-server" }
tasks:
- ansible.builtin.debug:
msg: "Current hostname: {{ ansible_hostname }}"
- ansible.builtin.debug:
msg: "current ansible_hostname is not in list of exempt hosts"
when: list_of_exempt_hosts | selectattr("host_name", 'equalto', ansible_hostname) | list | length == 0
Which results in:
TASK [ansible.builtin.debug] **************************************************************************************
ok: [docker-server] => {
"msg": "Current hostname: user-VirtualBox"
}
TASK [ansible.builtin.debug] **************************************************************************************
ok: [docker-server] => {
"msg": "current ansible_hostname is not in list of exempt hosts"
}
When we run our playbook, the ansible_hostname is “user-VirtualBox” which is not defined on our list of exempt hosts therefore the task is run on that host.
Explanation:
We use the selectattr
Jinja Filter to return a filtered version of the list where the only items in the list are dictionaries that have a host_name
equal to the ansible_hostname
. If there is no match (meaning the current ansible_hostname is not in our list_of_exempt_hostnames), then the list will be empty to which we can check by using the length
filter
Scenario - Assert fail when a string is found only once in a string | Using regex_findall() to give all instances of a subtring
In our example, the word “test” is in our string 3 times. We assert that if the string “test” is in some_text more than once, it’s a fail, otherwise a success:
- hosts: docker-server
vars:
some_text: "herhtesteruwe rwutestfugoe goi test ejfvosj ofvi ejoi"
tasks:
- ansible.builtin.assert :
that:
- some_text | regex_findall('test') | length > 1
fail_msg: "the string 'test' has appeared more than once"
success_msg: "the string test has appeared less than once"
Explanation:
- We use ansible’s regex_findall filter which returns a list of all the matched parts of a string.
- We then use length filter to determine if our matched phrase occurs more than once, in which case asserts a fail
Scenario - Filter list based on matched string item | Using select with regex match
In this scenario, we have a list of URLs, and we want to filter that list based on some string matching. Specifically we want to filter for all string items ending with “.ru”:
- hosts: docker-server
vars:
some_list:
- example.com
- helloexample.com
- fhello.com
- sample.ru
- h.ru
- hello.co.uk
- bye.ru.com
- hello.rush.com
tasks:
- ansible.builtin.debug:
msg: "{{ some_list | select('match', '.*.ru$') }}"
The output is:
TASK [ansible.builtin.debug] **************************************************************************************
ok: [docker-server] => {
"msg": [
"sample.ru",
"h.ru"
]
}
Explanation:
- We use the
select
jinja filter to filter our list of string based on a matching pattern - Our matching pattern
.*.ru$
means that we are looking for anything that ends in “.ru” - it has to be at the end of the string item! - We use the regex type matching since we have a regex pattern
Scenario - Ansible Ignore errors in tasks and fail at end of the playbook if any tasks had errors
We want to run a block of tasks (Task 1, Task 2 and Task 3) and if any of these tasks fail we still want all the other tasks to be executed. Instead, we want the “failure” event to happen at the end.
In this example, I purposely make task 2 fail.
- hosts: docker-server
tasks:
- block:
- ansible.builtin.debug:
msg: Task 1
register: t1
- name: Task 2
shell: 'false'
register: t2
- ansible.builtin.debug:
msg: Task 3
register: t3
ignore_errors: yes
always:
- ansible.builtin.debug:
msg: "task results: {{ [t1.failed, t2.failed, t3.failed] }}"
- ansible.builtin.fail:
msg: "at least one task failed"
when: true in [t1.failed, t2.failed, t3.failed]
- name: "some other task which wont run because we will fail on previous task"
ansible.builtin.debug:
msg: "Some other task to run"
This results in:
TASK [ansible.builtin.debug] **************************************************************************************
ok: [docker-server] => {
"msg": "Task 1"
}
TASK [Task 2] *****************************************************************************************************
fatal: [docker-server]: FAILED! => {"changed": true, "cmd": "false", "delta": "0:00:00.001542", "end": "2023-06-12 18:43:47.099767", "msg": "non-zero return code", "rc": 1, "start": "2023-06-12 18:43:47.098225", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring
TASK [ansible.builtin.debug] **************************************************************************************
ok: [docker-server] => {
"msg": "Task 3"
}
TASK [ansible.builtin.debug] **************************************************************************************
ok: [docker-server] => {
"msg": "task results: [False, True, False]"
}
TASK [ansible.builtin.fail] ***************************************************************************************
fatal: [docker-server]: FAILED! => {"changed": false, "msg": "at least one task failed"}
You can see that Task 2 fails, Task 1 and 3 still ran fine. Then the playbook fails after Task 1, 2 and 3 ran. Our very last task which is to be executed after our block of tasks does not even run since the playbook failed after our block of tasks
Explanation
- We use Ansible’s Block structure to group our tasks together and also to collectively
ignore_error
for all tasks within that block. - We
register
the result of each task within the block. - Outside our block, we use Ansible’s Fail module to forcefully stop and fail the playbook if our condition is met.
- We use the following condition with our fail module
when: true in [t1.failed, t2.failed, t3.failed]
so that we fail iftrue
is found in any of the task results.
Scenario - only show assert error output and not assert success
In this scenario we emulate the results of 3 tasks in a results’ dictionary. We want to loop through each result item and assert that the value of the key “failed” == false. If the value is true, then assert false.
We ONLY want to see the message output of the failed assert, we don’t want to show any output from the successful asserts. For this example task item 1 and task item 3 will be a successful assert whilst task item 2 will NOT:
- hosts: docker-server
vars:
results:
- { "task": "task 1", "failed" : false, "misc_data": "blhjassaljsi efwoeirf oewif oi" }
- { "task": "task 2", "failed" : true, "misc_data": "blhjassaljsi efwoeirf oewif oi" }
- { "task": "task 3", "failed" : false, "misc_data": "blhjassaljsi efwoeirf oewif oi" }
tasks:
- ansible.builtin.assert:
that:
- item.failed == false
fail_msg: "{{ item.task }} is false"
quiet: yes
loop: "{{ results }}"
loop_control:
label: "{{ item.task }}"
This outputs:
TASK [ansible.builtin.assert] *************************************************************************************
ok: [docker-server] => (item=task 1)
failed: [docker-server] (item=task 2) => {"ansible_loop_var": "item", "assertion": "item.failed == false", "changed": false, "evaluated_to": false, "item": {"failed": true, "misc_data": "blhjassaljsi efwoeirf oewif oi", "task": "task 2"}, "msg": "task 2 is false"}
ok: [docker-server] => (item=task 3)
Explanation:
- We use Ansible’s Assert
quiet
setting to avoid verbose output. - We then use
label
with Ansible’s Loop Control to control the output of each loop / each assert result. This way only the failed asserts are verbose whilst the successful ones only show the field “task”
FAQ
Should I use assert or failed_when?
Both achieve the same purpose but assert is better when handling multiple conditions and want to get feedback on which assert fail. Whereas use Ansible’s failed_when
when there is only 1 condition, and you want to write less script. Here’s an awesome article written about this subject.
What’s the success equivalent to failed_when?
failed_when is for defining what a task considers as a fail. There is no feature for a “success_when”, but instead you can use Ansible’s Assert
Conclusion
Whew, I haven’t written anything this long, in a long while (well except big project code at work?). But if you’ve read this far, I do hope you found something useful!