It is typically straightforward to resolve Ansible errors where a simple variable used within an action is not defined. However, when dealing with nested dictionary variables in your Ansible tasks, it can become more complex because not only can the leaf be missing, but also any of the parent container paths.
For example, the nested dictionary path ‘nested_simple.name’ below shows how attempting to use an invalid parent path or leaf key will cause an error.
vars: nested_simple: name: bob tasks: - debug: msg: this would succeed = {{ nested_simple.name }} - debug: msg: this would fail because leaf = {{ nested_simple.dne }} ignore_errors: yes - debug: msg: this would fail because parent = {{ nested_dne.name }} ignore_errors: yes
The last two actions would each throw the error, ‘includes an option with an undefined variable’. However, this can be addressed if you first check for an empty parent path, then provide a default for the leaf element.
The first action below pulls from a path that exists ‘nested_simple.name’, while the second one pulls from a path that does not exist. A jinja2 filter is used to provide a default object ‘{}’ for an empty parent path and the last default filter supplies a value in case the leaf does not exist.
- debug: msg: (use var value) nested_simple.name = {{ (nested_simple | default({})).name | default('default name') }} - debug: msg: (no var, use default) nested_simple_dne.name = {{ (nested_simple_dne | default({})).name | default('default name') }}
This will result in the following output, where the first action accurately outputs ‘bob’ from the dictionary ‘nested_simple.name’. In contrast, the second action outputs the default because the variable path does not exist.
TASK [debug] ****************** ok: [localhost] => { "msg": "(use var value) nested_simple.name = bob" } TASK [debug] ****************** ok: [localhost] => { "msg": "(no var, use default) nested_simple_dne.name = default name" }
This approach is passable if you have one nested level, but it gets more difficult to grok if you must go deeper. Continuing down our original approach, if we introduced another level ‘nested_root.nested_child.name’, the logic would get even more complex.
vars: nested_root: nested_child: name: charlie tasks: - debug: msg: (use var value) nested_root.nested_child.name = {{ ( (nested_root | default({})).nested_child ).name | default('default name') }}
The alternative is to use a filter named ‘json_query’. Even with a deeper dictionary path of ‘nested_root.nested_child.final_level.name’, you just have to provide this path to json_query and it can pull the value out.
vars: nested_root: nested_child: final_level: name: charlie - debug: msg: (use var value) nested_root.nested_child.final_level.name = {{ (nested_root | default({})) | json_query('nested_child.final_level.name') | default('default name',true) }}
We still must provide a default ‘{}’ for the parent level to make sure it exists, and we still have to provide a default value for the leaf value in case it is not found by json_query.
Note that the use of json_query requires the ‘jmespath’ python module and ‘community.general’ Ansible Galaxy module.
# required pip module pip3 install jmespath # 'community.general' ansible-galaxy collection install community.general
Or via Ansible like below.
- name: ensure jmespath is installed to support json_query filter become: yes pip: name: jmespath - name: install community.general collection from ansible galaxy command: cmd: ansible-galaxy collection install community.general
Here is my playbook-nested-dict.yml on github.
REFERENCES
github issue, shows syntax for using nested drill down and json_query
stackoverflow, use default var if not defined