summaryrefslogtreecommitdiffstats
path: root/docs/docsite/rst/playbook_guide/complex_data_manipulation.rst
diff options
context:
space:
mode:
Diffstat (limited to 'docs/docsite/rst/playbook_guide/complex_data_manipulation.rst')
-rw-r--r--docs/docsite/rst/playbook_guide/complex_data_manipulation.rst314
1 files changed, 314 insertions, 0 deletions
diff --git a/docs/docsite/rst/playbook_guide/complex_data_manipulation.rst b/docs/docsite/rst/playbook_guide/complex_data_manipulation.rst
new file mode 100644
index 0000000..11ed3c3
--- /dev/null
+++ b/docs/docsite/rst/playbook_guide/complex_data_manipulation.rst
@@ -0,0 +1,314 @@
+.. _complex_data_manipulation:
+
+Manipulating data
+#################
+
+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.
+
+Let's start with a quick definition of each type of plugin:
+ - lookups: Mainly used to query 'external data', in Ansible these were the primary part of loops using the ``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.
+ - filters: used to change/transform data, used with the ``|`` Jinja2 operator.
+ - tests: used to validate data, used with the ``is`` Jinja2 operator.
+
+.. _note:
+ * Some tests and filters are provided directly by Jinja2, so their availability depends on the Jinja2 version, not Ansible.
+
+.. _for_loops_or_list_comprehensions:
+
+Loops and list comprehensions
+=============================
+
+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``.
+
+- map: this is a basic for loop that just allows you to change every item in a list, using the 'attribute' keyword you can do the transformation based on attributes of the list elements.
+- select/reject: this is a for loop with a condition, that allows you to create a subset of a list that matches (or not) based on the result of the condition.
+- selectattr/rejectattr: very similar to the above but it uses a specific attribute of the list elements for the conditional statement.
+
+
+.. _exponential_backoff:
+
+Use a loop to create exponential backoff for retries/until.
+
+.. code-block:: yaml
+
+ - name: retry ping 10 times with exponential backoff delay
+ ping:
+ retries: 10
+ delay: '{{item|int}}'
+ loop: '{{ range(1, 10)|map("pow", 2) }}'
+
+
+.. _keys_from_dict_matching_list:
+
+Extract keys from a dictionary matching elements from a list
+------------------------------------------------------------
+
+The Python equivalent code would be:
+
+.. code-block:: python
+
+ 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:
+
+.. code-block:: YAML+Jinja
+ :emphasize-lines: 4
+ :caption: Way to extract matching keys from a list of dictionaries
+
+ tasks:
+ - name: Show extracted list of keys from a list of dictionaries
+ ansible.builtin.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
+
+
+.. code-block:: ansible-output
+ :caption: Results of debug task, a list with the extracted keys
+
+ ok: [localhost] => {
+ "msg": [
+ "routed",
+ "bridged",
+ "routed",
+ "bridged"
+ ]
+ }
+
+
+.. code-block:: YAML+Jinja
+ :caption: Get the unique list of values of a variable that vary per host
+
+ vars:
+ unique_value_list: "{{ groups['all'] | map ('extract', hostvars, 'varname') | list | unique}}"
+
+
+.. _find_mount_point:
+
+Find mount point
+----------------
+
+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:
+
+.. code-block:: YAML+Jinja
+ :caption: Use selectattr to filter mounts into list I can then sort and select the last from
+ :emphasize-lines: 8
+
+ - 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
+ ansible.builtin.debug:
+ msg: "{{(ansible_facts.mounts | selectattr('mount', 'in', path) | list | sort(attribute='mount'))[-1]['mount']}}"
+
+
+.. _omit_elements_from_list:
+
+Omit elements from a list
+-------------------------
+
+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:
+
+.. code-block:: YAML+Jinja
+ :caption: Inline list filtering when feeding a module option
+ :emphasize-lines: 3, 6
+
+ - name: Enable a list of Windows features, by name
+ ansible.builtin.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:
+
+.. code-block:: YAML+Jinja
+ :caption: Using set_fact in a loop to increment a list conditionally
+ :emphasize-lines: 3, 4, 6
+
+ - name: Build unique list with some items conditionally omitted
+ ansible.builtin.set_fact:
+ namestuff: ' {{ (namestuff | default([])) | union([item]) }}'
+ when: item != omit
+ loop:
+ - "{{ (fs_installed_smb_v1 | default(False)) | ternary(omit, 'FS-SMB1') }}"
+ - "foo"
+ - "bar"
+
+
+
+.. _combine_optional_values:
+
+Combine values from same list of dicts
+---------------------------------------
+Combining positive and negative filters from examples above, you can get a 'value when it exists' and a 'fallback' when it doesn't.
+
+.. code-block:: YAML+Jinja
+ :caption: Use selectattr and rejectattr to get the ansible_host or inventory_hostname as needed
+
+ - hosts: localhost
+ tasks:
+ - name: Check hosts in inventory that respond to ssh port
+ wait_for:
+ host: "{{ item }}"
+ port: 22
+ loop: '{{ has_ah + no_ah }}'
+ vars:
+ has_ah: '{{ hostvars|dictsort|selectattr("1.ansible_host", "defined")|map(attribute="1.ansible_host")|list }}'
+ no_ah: '{{ hostvars|dictsort|rejectattr("1.ansible_host", "defined")|map(attribute="0")|list }}'
+
+
+.. _custom_fileglob_variable:
+
+Custom Fileglob Based on a Variable
+-----------------------------------
+
+This example uses `Python argument list unpacking <https://docs.python.org/3/tutorial/controlflow.html#unpacking-argument-lists>`_ to create a custom list of fileglobs based on a variable.
+
+.. code-block:: YAML+Jinja
+ :caption: Using fileglob with a list based on a variable.
+
+ - hosts: all
+ vars:
+ mygroups:
+ - prod
+ - web
+ tasks:
+ - name: Copy a glob of files based on a list of groups
+ copy:
+ src: "{{ item }}"
+ dest: "/tmp/{{ item }}"
+ loop: '{{ q("fileglob", *globlist) }}'
+ vars:
+ globlist: '{{ mygroups | map("regex_replace", "^(.*)$", "files/\1/*.conf") | list }}'
+
+
+.. _complex_type_transformations:
+
+Complex Type transformations
+=============================
+
+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 used to achieve more complex transformations.
+
+
+.. _create_dictionary_from_list:
+
+Create dictionary from list
+---------------------------
+
+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"}``
+
+.. code-block:: YAML+Jinja
+ :caption: Simple list to dict by assuming the list is [key, value , key, value, ...]
+
+ vars:
+ single_list: [ 'a', 'b', 'c', 'd' ]
+ mydict: "{{ dict(single_list | slice(2)) }}"
+
+
+.. code-block:: YAML+Jinja
+ :caption: It is simpler when we have a list of pairs:
+
+ vars:
+ list_of_pairs: [ ['a', 'b'], ['c', 'd'] ]
+ mydict: "{{ dict(list_of_pairs) }}"
+
+Both end up being the same thing, with ``slice(2)`` transforming ``single_list`` to a ``list_of_pairs`` generator.
+
+
+
+A bit more complex, using ``set_fact`` and a ``loop`` to create/update a dictionary with key value pairs from 2 lists:
+
+.. code-block:: YAML+Jinja
+ :caption: Using set_fact to create a dictionary from a set of lists
+ :emphasize-lines: 3, 4
+
+ - name: Uses 'combine' to update the dictionary and 'zip' to make pairs of both lists
+ ansible.builtin.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:
+
+.. code-block:: YAML+Jinja
+ :caption: Using 'vars' to define dictionary from a set of lists without needing a task
+
+ vars:
+ xyz_stuff: 1234
+ xyz_morestuff: 567
+ myvarnames: "{{ q('varnames', '^xyz_') }}"
+ mydict: "{{ dict(myvarnames|map('regex_replace', '^xyz_', '')|list | zip(q('vars', *myvarnames))) }}"
+
+A quick explanation, since there is a lot to unpack from these two lines:
+
+ - The ``varnames`` lookup returns a list of variables that match "begin with ``xyz_``".
+ - Then feeding the list from the previous step into the ``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.
+ - Both lists get passed to the ``zip`` filter to pair them off into a unified list (key, value, key2, value2, ...).
+ - The dict function then takes this 'list of pairs' to create the dictionary.
+
+
+An example on how to use facts to find a host's data that meets condition X:
+
+
+.. code-block:: YAML+Jinja
+
+ vars:
+ uptime_of_host_most_recently_rebooted: "{{ansible_play_hosts_all | map('extract', hostvars, 'ansible_uptime_seconds') | sort | first}}"
+
+An example to show a host uptime in days/hours/minutes/seconds (assumes facts were gathered).
+
+.. code-block:: YAML+Jinja
+
+ - name: Show the uptime in days/hours/minutes/seconds
+ ansible.builtin.debug:
+ msg: Uptime {{ now().replace(microsecond=0) - now().fromtimestamp(now(fmt='%s') | int - ansible_uptime_seconds) }}
+
+
+.. seealso::
+
+ :ref:`playbooks_filters`
+ Jinja2 filters included with Ansible
+ :ref:`playbooks_tests`
+ Jinja2 tests included with Ansible
+ `Jinja2 Docs <https://jinja.palletsprojects.com/>`_
+ Jinja2 documentation, includes lists for core filters and tests