In many cases, you need to do some complex operation with your variables, while Ansible is not recommended as a data processing/manipulation tool, you can use the existing Jinja2 templating in conjunction with the many added Ansible filters, lookups and tests to do some very complex transformations.
with_<lookup>
construct, but they can be used independently to return data for processing. They normally return a list due to their primary function in loops as mentioned previously. Used with the lookup
or query
Jinja2 operators.|
Jinja2 operator.is
Jinja2 operator.Most programming languages have loops (for
, while
, and so on) and list comprehensions to do transformations on lists including lists of objects. Jinja2 has a few filters that provide this functionality: map
, select
, reject
, selectattr
, rejectattr
.
The Python equivalent code would be:
chains = [1, 2] for chain in chains: for config in chains_config[chain]['configs']: print(config['type'])
There are several ways to do it in Ansible, this is just one example:
tasks: - name: Show extracted list of keys from a list of dictionaries debug: msg="{{ chains | map('extract', chains_config) | map(attribute='configs') | flatten | map(attribute='type') | flatten }}" vars: chains: [1, 2] chains_config: 1: foo: bar configs: - type: routed version: 0.1 - type: bridged version: 0.2 2: foo: baz configs: - type: routed version: 1.0 - type: bridged version: 1.1
ok: [localhost] => { "msg": [ "routed", "bridged", "routed", "bridged" ] }
In this case, we want to find the mount point for a given path across our machines, since we already collect mount facts, we can use the following:
- hosts: all gather_facts: True vars: path: /var/lib/cache tasks: - name: The mount point for {{path}}, found using the Ansible mount facts, [-1] is the same as the 'last' filter debug: msg="{{(ansible_facts.mounts | selectattr('mount', 'in', path) | list | sort(attribute='mount'))[-1]['mount']}}"
The special omit
variable ONLY works with module options, but we can still use it in other ways as an identifier to tailor a list of elements:
- name: enable a list of Windows features, by name set_fact: win_feature_list: "{{ namestuff | reject('equalto', omit) | list }}" vars: namestuff: - "{{ (fs_installed_smb_v1 | default(False)) | ternary(omit, 'FS-SMB1') }}" - "foo" - "bar"
Another way is to avoid adding elements to the list in the first place, so you can just use it directly:
- name: build unique list with some items conditionally omittted set_fact: namestuff: ' {{ (namestuff | default([])) | union([item]) }}' when: item != omit loop: - "{{ (fs_installed_smb_v1 | default(False)) | ternary(omit, 'FS-SMB1') }}" - "foo" - "bar"
Jinja provides filters for simple data type transformations (int
, bool
, and so on), but when you want to transform data structures things are not as easy. You can use loops and list comprehensions as shown above to help, also other filters and lookups can be chained and leveraged to achieve more complex transformations.
In most languages it is easy to create a dictionary (a.k.a. map/associative array/hash and so on) from a list of pairs, in Ansible there are a couple of ways to do it and the best one for you might depend on the source of your data.
These example produces {"a": "b", "c": "d"}
vars: single_list: [ 'a', 'b', 'c', 'd' ] mydict: "{{ dict(single_list) | slice(2) | list }}"
vars: list_of_pairs: [ ['a', 'b'], ['c', 'd'] ] mydict: "{{ dict(list_of_pairs) }}"
Both end up being the same thing, with the slice(2) | list
transforming single_list
to the same structure as list_of_pairs
.
A bit more complex, using set_fact
and a loop
to create/update a dictionary with key value pairs from 2 lists:
- name: Uses 'combine' to update the dictionary and 'zip' to make pairs of both lists set_fact: mydict: "{{ mydict | default({}) | combine({item[0]: item[1]}) }}" loop: "{{ (keys | zip(values)) | list }}" vars: keys: - foo - var - bar values: - a - b - c
This results in {"foo": "a", "var": "b", "bar": "c"}
.
You can even combine these simple examples with other filters and lookups to create a dictionary dynamically by matching patterns to variable names:
vars: myvarnames: "{{ q('varnames', '^my') }}" mydict: "{{ dict(myvarnames | zip(q('vars', *myvarnames))) }}"
A quick explanation, since there is a lot to unpack from these two lines:
varnames
lookup returns a list of variables that match “begin with my
”.vars
lookup to get the list of values. The *
is used to ‘dereference the list’ (a pythonism that works in Jinja), otherwise it would take the list as a single argument.zip
filter to pair them off into a unified list (key, value, key2, value2, …).An example on how to use facts to find a host’s data that meets condition X:
vars: uptime_of_host_most_recently_rebooted: "{{ansible_play_hosts_all | map('extract', hostvars, 'ansible_uptime_seconds') | sort | first}}"
Using an example from @zoradache on reddit, to show the ‘uptime in days/hours/minutes’ (assumes facts where gathered). https://www.reddit.com/r/ansible/comments/gj5a93/trying_to_get_uptime_from_seconds/fqj2qr3/
- debug: msg: Timedelta {{ now() - now().fromtimestamp(now(fmt='%s') | int - ansible_uptime_seconds) }}
See also
Jinja2 filters included with Ansible
Jinja2 tests included with Ansible
Jinja2 documentation, includes lists for core filters and tests
© 2012–2018 Michael DeHaan
© 2018–2019 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/ansible/2.10/user_guide/complex_data_manipulation.html