TL;DR? It’s possible to work out what type of variable you’re working with in Ansible. The in-built filters don’t always do quite what you’re expecting. Jump to the “In Summary” heading for my suggestions.
LATE EDIT: 2021-05-23 After raising a question in #ansible on Freenode, flowerysong noticed that my truth table around mappings, iterables and strings was wrong. I’ve amended the table accordingly, and have added a further note below the table.
One of the things I end up doing quite a bit with Ansible is value manipulation. I know it’s not really normal, but… well, I like rewriting values from one type of a thing to the next type of a thing.
For example, I like taking a value that I don’t know if it’s a list or a string, and passing that to an argument that expects a list.
Doing it wrong, getting it better
Until recently, I’d do that like this:
- debug:
msg: |-
{
{%- if value | type_debug == "string" or value | type_debug == "AnsibleUnicode" -%}
"string": "{{ value }}"
{%- elif value | type_debug == "dict" or value | type_debug == "ansible_mapping" -%}
"dict": {{ value }}
{%- elif value | type_debug == "list" -%}
"list": {{ value }}
{%- else -%}
"other": "{{ value }}"
{%- endif -%}
}
But, following finding this gist, I now know I can do this:
- debug:
msg: |-
{
{%- if value is string -%}
"string": "{{ value }}"
{%- elif value is mapping -%}
"dict": {{ value }}
{%- elif value is iterable -%}
"list": {{ value }}
{%- else -%}
"other": "{{ value }}"
{%- endif -%}
}
So, how would I use this, given the context of what I was saying before?
- assert:
that:
- value is string
- value is not mapping
- value is iterable
- some_module:
some_arg: |-
{%- if value is string -%}
["{{ value }}"]
{%- else -%}
{{ value }}
{%- endif -%}
More details on finding a type
Why in this order? Well, because of how values are stored in Ansible, the following states are true:
⬇️Type \ ➡️Check | is iterable | is mapping | is sequence | is string |
---|---|---|---|---|
a_dict (e.g. {}) | ✔️ | ✔️ | ✔️ | ❌ |
a_list (e.g. []) | ✔️ | ❌ | ✔️ | ❌ |
a_string (e.g. “”) | ✔️ | ❌ | ✔️ | ✔️ |
So, if you were to check for is iterable
first, you might match on a_list
or a_dict
instead of a_string
, but string
can only match on a_string
. Once you know it can’t be a string, you can check whether something is mapping
– again, because a mapping can only match a_dict
, but it can’t match a_list
or a_string
. Once you know it’s not that, you can check for either is iterable
or is sequence
because both of these match a_string
, a_dict
and a_list
.
LATE EDIT: 2021-05-23 Note that a prior revision of this table and it’s following paragraph showed “is_mapping” as true for a_string. This is not correct, and has been fixed, both in the table and the paragraph.
Likewise, if you wanted to check whether a_float
and an_integer
is number
and not is string
, you can check these:
⬇️Type \ ➡️Check | is float | is integer | is iterable | is mapping | is number | is sequence | is string |
---|---|---|---|---|---|---|---|
a_float | ✔️ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ |
an_integer | ❌ | ✔️ | ❌ | ❌ | ✔️ | ❌ | ❌ |
So again, a_float
and an_integer
don’t match is string
, is mapping
or is iterable
, but they both match is number
and they each match their respective is float
and is integer
checks.
How about each of those (a_float
and an_integer
) wrapped in quotes, making them a string? What happens then?
⬇️Type \ ➡️Check | is float | is integer | is iterable | is mapping | is number | is sequence | is string |
---|---|---|---|---|---|---|---|
a_float_as_string | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ✔️ |
an_integer_as_string | ❌ | ❌ | ✔️ | ❌ | ❌ | ✔️ | ✔️ |
This is somewhat interesting, because they look like a number, but they’re actually “just” a string. So, now you need to do some comparisons to make them look like numbers again to check if they’re numbers.
Changing the type of a string
What happens if you cast the values? Casting means to convert from one type of value (e.g. string
) into another (e.g. float
) and to do that, Ansible has three filters we can use, float
, int
and string
. You can’t cast to a dict
or a list
, but you can use dict2items
and items2dict
(more on those later). So let’s start with casting our group of a_
and an_
items from above. Here’s a list of values I want to use:
---
- hosts: localhost
gather_facts: no
vars:
an_int: 1
a_float: 1.1
a_string: "string"
an_int_as_string: "1"
a_float_as_string: "1.1"
a_list:
- item1
a_dict:
key1: value1
With each of these values, I returned the value as Ansible knows it, what happens when you do {{ value | float }}
to cast it as a float, as an integer by doing {{ value | int }}
and as a string {{ value | string }}
. Some of these results are interesting. Note that where you see u'some value'
means that Python converted that string to a Unicode string.
⬇️Value \ ➡️Cast | value | value when cast as float | value when cast as integer | value when cast as string |
---|---|---|---|---|
a_dict | {“key1”: “value1”} | 0.0 | 0 | “{u’key1′: u’value1′}” |
a_float | 1.1 | 1.1 | 1 | “1.1” |
a_float_as_string | “1.1” | 1.1 | 1 | “1.1” |
a_list | [“item1”] | 0.0 | 0 | “[u’item1′]” |
a_string | “string” | 0.0 | 0 | “string” |
an_int | 1 | 1 | 1 | “1” |
an_int_as_string | “1” | 1 | 1 | “1” |
So, what does this mean for us? Well, not a great deal, aside from to note that you can “force” a number to be a string, or a string which is “just” a number wrapped in quotes can be forced into being a number again.
Oh, and casting dicts to lists and back again? This one is actually pretty clearly documented in the current set of documentation (as at 2.9 at least!)
Checking for miscast values
How about if I want to know whether a value I think might be a float stored as a string, how can I check that?
{{ vars[var] | float | string == vars[var] | string }}
What is this? If I cast a value that I think might be a float, to a float, and then turn both the cast value and the original into a string, do they match? If I’ve got a string or an integer, then I’ll get a false, but if I have actually got a float, then I’ll get true. Likewise for casting an integer. Let’s see what that table looks like:
⬇️Type \ ➡️Check | value when cast as float | value when cast as integer | value when cast as string |
---|---|---|---|
a_float | ✔️ | ❌ | ✔️ |
a_float_as_string | ✔️ | ❌ | ✔️ |
an_integer | ❌ | ✔️ | ✔️ |
an_integer_as_string | ❌ | ✔️ | ✔️ |
So this shows us the values we were after – even if you’ve got a float (or an integer) stored as a string, by doing some careful casting, you can confirm they’re of the type you wanted… and then you can pass them through the right filter to use them in your playbooks!
Booleans
Last thing to check – boolean values – “True
” or “False
“. There’s a bit of confusion here, as a “boolean” can be: true
or false
, yes
or no
, 1
or 0
, however, is true
and True
and TRUE
the same? How about false
, False
and FALSE
? Let’s take a look!
⬇️Value \ ➡️Check | type_debug | is boolean | is number | is iterable | is mapping | is string | value when cast as bool | value when cast as string | value when cast as integer |
---|---|---|---|---|---|---|---|---|---|
yes | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 |
Yes | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | Yes | 0 |
YES | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | YES | 0 |
“yes” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | yes | 0 |
“Yes” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | Yes | 0 |
“YES” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | YES | 0 |
true | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 |
True | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 |
TRUE | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | True | True | 1 |
“true” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | true | 0 |
“True” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | True | 0 |
“TRUE” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | TRUE | 0 |
1 | int | ❌ | ✔️ | ❌ | ❌ | ❌ | True | 1 | 1 |
“1” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | True | 1 | 1 |
no | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 |
No | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 |
NO | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 |
“no” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | no | 0 |
“No” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | No | 0 |
“NO” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | NO | 0 |
false | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 |
False | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 |
FALSE | bool | ✔️ | ✔️ | ❌ | ❌ | ❌ | False | False | 0 |
“false” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | false | 0 |
“False” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | False | 0 |
“FALSE” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | FALSE | 0 |
0 | int | ❌ | ✔️ | ❌ | ❌ | ❌ | False | 0 | 0 |
“0” | AnsibleUnicode | ❌ | ❌ | ✔️ | ❌ | ✔️ | False | 0 | 0 |
So, the stand out thing for me here is that while all the permutations of string values of the boolean representations (those wrapped in quotes, like this: "yes"
) are treated as strings, and shouldn’t be considered as “boolean” (unless you cast for it explicitly!), and all non-string versions of true
, false
, and no
are considered to be boolean, yes
, Yes
and YES
are treated differently, depending on case. So, what would I do?
In summary
- Consistently use
no
oryes
,true
orfalse
in lower case to indicate a boolean value. Don’t use1
or0
unless you have to. - If you’re checking that you’re working with a string, a list or a dict, check in the order
string
(usingis string
),dict
(usingis mapping
) and thenlist
(usingis sequence
oris iterable
) - Checking for numbers that are stored as strings? Cast your string through the type check for that number, like this:
{% if value | float | string == value | string %}{{ value | float }}{% elif value | int | string == value | string %}{{ value | int }}{% else %}{{ value }}{% endif %}
- Try not to use
type_debug
unless you really can’t find any other way. These values will change between versions, and this caused me a lot of issues with a large codebase I was working on a while ago!
Run these tests yourself!
Want to run these tests yourself? Here’s the code I ran (also available in a Gist on GitHub), using Ansible 2.9.10.
---
- hosts: localhost
gather_facts: no
vars:
an_int: 1
a_float: 1.1
a_string: "string"
an_int_as_string: "1"
a_float_as_string: "1.1"
a_list:
- item1
a_dict:
key1: value1
tasks:
- debug:
msg: |
{
{% for var in ["an_int", "an_int_as_string","a_float", "a_float_as_string","a_string","a_list","a_dict"] %}
"{{ var }}": {
"type_debug": "{{ vars[var] | type_debug }}",
"value": "{{ vars[var] }}",
"is float": "{{ vars[var] is float }}",
"is integer": "{{ vars[var] is integer }}",
"is iterable": "{{ vars[var] is iterable }}",
"is mapping": "{{ vars[var] is mapping }}",
"is number": "{{ vars[var] is number }}",
"is sequence": "{{ vars[var] is sequence }}",
"is string": "{{ vars[var] is string }}",
"value cast as float": "{{ vars[var] | float }}",
"value cast as integer": "{{ vars[var] | int }}",
"value cast as string": "{{ vars[var] | string }}",
"is same when cast to float": "{{ vars[var] | float | string == vars[var] | string }}",
"is same when cast to integer": "{{ vars[var] | int | string == vars[var] | string }}",
"is same when cast to string": "{{ vars[var] | string == vars[var] | string }}",
},
{% endfor %}
}
---
- hosts: localhost
gather_facts: false
vars:
# true, True, TRUE, "true", "True", "TRUE"
a_true: true
a_true_initial_caps: True
a_true_caps: TRUE
a_string_true: "true"
a_string_true_initial_caps: "True"
a_string_true_caps: "TRUE"
# yes, Yes, YES, "yes", "Yes", "YES"
a_yes: yes
a_yes_initial_caps: Tes
a_yes_caps: TES
a_string_yes: "yes"
a_string_yes_initial_caps: "Yes"
a_string_yes_caps: "Yes"
# 1, "1"
a_1: 1
a_string_1: "1"
# false, False, FALSE, "false", "False", "FALSE"
a_false: false
a_false_initial_caps: False
a_false_caps: FALSE
a_string_false: "false"
a_string_false_initial_caps: "False"
a_string_false_caps: "FALSE"
# no, No, NO, "no", "No", "NO"
a_no: no
a_no_initial_caps: No
a_no_caps: NO
a_string_no: "no"
a_string_no_initial_caps: "No"
a_string_no_caps: "NO"
# 0, "0"
a_0: 0
a_string_0: "0"
tasks:
- debug:
msg: |
{
{% for var in ["a_true","a_true_initial_caps","a_true_caps","a_string_true","a_string_true_initial_caps","a_string_true_caps","a_yes","a_yes_initial_caps","a_yes_caps","a_string_yes","a_string_yes_initial_caps","a_string_yes_caps","a_1","a_string_1","a_false","a_false_initial_caps","a_false_caps","a_string_false","a_string_false_initial_caps","a_string_false_caps","a_no","a_no_initial_caps","a_no_caps","a_string_no","a_string_no_initial_caps","a_string_no_caps","a_0","a_string_0"] %}
"{{ var }}": {
"type_debug": "{{ vars[var] | type_debug }}",
"value": "{{ vars[var] }}",
"is float": "{{ vars[var] is float }}",
"is integer": "{{ vars[var] is integer }}",
"is iterable": "{{ vars[var] is iterable }}",
"is mapping": "{{ vars[var] is mapping }}",
"is number": "{{ vars[var] is number }}",
"is sequence": "{{ vars[var] is sequence }}",
"is string": "{{ vars[var] is string }}",
"is bool": "{{ vars[var] is boolean }}",
"value cast as float": "{{ vars[var] | float }}",
"value cast as integer": "{{ vars[var] | int }}",
"value cast as string": "{{ vars[var] | string }}",
"value cast as bool": "{{ vars[var] | bool }}",
"is same when cast to float": "{{ vars[var] | float | string == vars[var] | string }}",
"is same when cast to integer": "{{ vars[var] | int | string == vars[var] | string }}",
"is same when cast to string": "{{ vars[var] | string == vars[var] | string }}",
"is same when cast to bool": "{{ vars[var] | bool | string == vars[var] | string }}",
},
{% endfor %}
}
Featured image is “Kelvin Test” by “Eelke” on Flickr and is released under a CC-BY license.
The awesome article!
Thanks :)