Looping over a nested ansible dict - ansible

I have a given vars as a list of kafka topics and possible configurations, like:
kafka_topics:
foo:
retentiontime: 3600
deletepolicy: delete
bar:
retentiontime: 3600
compression: gzip
I have figured multiple ways with dict2items to set them explicit like:
- name: Set RetentionTime for Topics
debug:
msg: "Topic {{ item.key }} get {{ item.value.retentiontime }} for retentiontime "
loop: "{{ lookup('dict', kafka_topics) }}"
when: item.value.retentiontime is defined
But is it possible to get a output like
Topic foo get 3600 for retentiontime
Topic foo get delete for deletepolicy
Topic bar get 3600 for retentiontime
Topic bar get gzip for compression
without defining the value name by name with ansible?
I also tried with
- name: Loop over subelements of the dictionary
debug:
msg: "Key={{ item.0.key }} value={{ item.1 }}"
loop: "{{ lookup('dict', kafka_topics) | list | subelements('value') }}"
which prints Key=bar value={'compression': gzip} but now i'm stuck at seperating those two. Is there a way to extract from item.1 the key and the value?

The simplest option is Jinja. For example
- debug:
msg: |-
{% for k1,v1 in kafka_topics.items() %}
{% for k2,v2 in v1.items() %}
Topic {{ k1 }} get {{ v2 }} for {{ k2 }}
{% endfor %}
{% endfor %}
gives
msg: |-
Topic foo get 3600 for retentiontime
Topic foo get delete for deletepolicy
Topic bar get 3600 for retentiontime
Topic bar get gzip for compression
Create a dictionary of lists first if the iteration is needed. For example the template
shell> cat templates/kafka_facts_list.j2
{% for k1,v1 in kafka_topics.items() %}
{{ k1 }}:
{% for k2,v2 in v1.items() %}
- {key: {{ k2 }}, val: {{ v2 }}}
{% endfor %}
{% endfor %}
- set_fact:
dict2: "{{ lookup('template', 'kafka_facts_list.j2')|from_yaml }}"
- debug:
var: dict2
gives
dict2:
bar:
- key: retentiontime
val: 3600
- key: compression
val: gzip
foo:
- key: retentiontime
val: 3600
- key: deletepolicy
val: delete
Then use subelements
- debug:
msg: "Topic {{ item.0.key }} get {{ item.1.val }} for {{ item.1.key }}"
with_subelements:
- "{{ dict2|dict2items }}"
- value
gives
msg: Topic foo get 3600 for retentiontime
msg: Topic foo get delete for deletepolicy
msg: Topic bar get 3600 for retentiontime
msg: Topic bar get gzip for compression

#Vladimir's answer is indeed the most straightforward one if you need to write some debugging info as above or create a configuration file from a template.
But this might get a little trickier if you actually need to loop over this data in a classic task where you have to pass each value separately to the corresponding moldule's option.
In such cases, here is an alternative that will transform your current dict to list.
The goal is to go from:
"first topic name":
"option 1 name": "option 1 value"
"option 2 name": "option 2 value"
"second topic name": ...
to
- topic_name: "first topic name"
topic_options:
- option_name: "option 1 name"
option_value: "option 1 value"
- option_name: "option 2 name"
option_value: "option 2 value"
- topic_name: "second topic name"
...
This transformed data is then easily loopable with subelements as demonstrated in the following playbook:
---
- hosts: localhost
gather_facts: false
vars:
kafka_topics: {"foo": {"retentiontime": 3600, "deletepolicy": "delete"}, "bar": {"retentiontime": 3600, "compression": "gzip"}}
tasks:
- name: Transform our list to something easier to loop with subelements
vars:
current_topic:
topic_name: "{{ item.topic_name }}"
topic_options: "{{ item.topic_options | dict2items(key_name='option_name', value_name='option_value') }}"
set_fact:
my_topic_list: "{{ my_topic_list | default([]) + [current_topic] }}"
loop: "{{ kafka_topics | dict2items(key_name='topic_name', value_name='topic_options') }}"
- name: Show the transformed var
debug:
var: my_topic_list
- name: Loop over topics and their options
debug:
msg: "Topic `{{ topic.0.topic_name }}` has option `{{ topic.1.option_name }}` with value `{{ topic.1.option_value }}`"
loop: "{{ my_topic_list | subelements('topic_options') }}"
loop_control:
label: "{{ topic.0.topic_name }} - {{ topic.1.option_name }} - {{ topic.1.option_value }}"
loop_var: topic
Which gives:
PLAY [localhost] ***********************************************************************************************************************************************************************************************************************
TASK [Transform our list to something easier to loop with subelements] *****************************************************************************************************************************************************************
ok: [localhost] => (item={'topic_name': 'foo', 'topic_options': {'retentiontime': 3600, 'deletepolicy': 'delete'}})
ok: [localhost] => (item={'topic_name': 'bar', 'topic_options': {'retentiontime': 3600, 'compression': 'gzip'}})
TASK [Show the transformed var] ********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"my_topic_list": [
{
"topic_name": "foo",
"topic_options": [
{
"option_name": "retentiontime",
"option_value": 3600
},
{
"option_name": "deletepolicy",
"option_value": "delete"
}
]
},
{
"topic_name": "bar",
"topic_options": [
{
"option_name": "retentiontime",
"option_value": 3600
},
{
"option_name": "compression",
"option_value": "gzip"
}
]
}
]
}
TASK [Loop over topics and their options] **********************************************************************************************************************************************************************************************
ok: [localhost] => (item=foo - retentiontime - 3600) => {
"msg": "Topic `foo` has option `retentiontime` with value `3600`"
}
ok: [localhost] => (item=foo - deletepolicy - delete) => {
"msg": "Topic `foo` has option `deletepolicy` with value `delete`"
}
ok: [localhost] => (item=bar - retentiontime - 3600) => {
"msg": "Topic `bar` has option `retentiontime` with value `3600`"
}
ok: [localhost] => (item=bar - compression - gzip) => {
"msg": "Topic `bar` has option `compression` with value `gzip`"
}
PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Related

Ansible - List of dictionaries

Let me introduce my problem. I have some list of dictionary in my Ansible code:
my_example_list = [
{
"key1" : "value_of_first_key"
},
{
"key2": "value_of_second_key"
},
{
"key3": "value_of_third_key"
}
]
I need execute command which will iterate over this list and it should look something like:
- name: 'Example'
shell: 'Here is my {{ item.key }} and here is {{ item.value }}'
What I've do or try to do:
I was trying to do that with with_items but i'm not able to point into value of particular key.
I've also try to filter values using | first and | last but it's not worked in my case.
What I want to achieve:
Creating loop which will iterate via that list and inject separated key and value into command.
I was asked to show how I was trying to resolve my issue:
Here is some code:
# Showing last component failing
- name: "Try to show last component of my list"
debug:
msg: "{{ my_example_list[1] | last }}"
# When i'm trying to show first component of my list i get "key1"
- name: "Try to show first component of my list"
debug:
msg: "{{ my_example_list[1] | first }}"
# This shows me my list of dict
- name: "Trying use with_items"
debug:
msg: "{{ item }}"
with_items: "{{ my_example_list }}"
# But when i'm trying point to key and value for example
- name: "Trying use with_items point to key and value"
debug:
msg: "Here is my {{ item.key }} which keep {{ item.value }}"
with_items: "{{ my_example_list }}"
# It's failing.
Sorry it's not maybe solution with using loop. I'm just stack with that issue over few days... And as first step I want to know how correctly point to pair keys and values.
It also works well:
- name: Correct solution
debug:
msg: "This is my {{ item.key }} and my value {{ item.value }}"
with_dict: "{{ my_example_list }}"
Thanks #U880D for help! I'm not able to add some plus for your solution because I'm new joiner. Appreciate your answer! :)
Your data structure and naming seems to be unfavorable. There is no need to number the key name and therefore it should be avoided. Furthermore counting list elements in Python starts at 0 not 1.
The following minimal example playbook
---
- hosts: localhost
become: false
gather_facts: false
vars:
example_list: |
[
{
"key1" : "value_of_first_key"
},
{
"key2": "value_of_second_key"
},
{
"key3": "value_of_third_key"
}
]
tasks:
- name: Example loop
debug:
msg: "{{ item }} is of type {{ item | type_debug }}"
loop: "{{ example_list }}"
- name: Example loop
debug:
msg: "{{ item.values() }}"
loop: "{{ example_list }}"
will result into an output of
TASK [Example loop] ******************************************
ok: [localhost] => (item={u'key1': u'value_of_first_key'}) =>
msg: '{u''key1'': u''value_of_first_key''} is of type dict'
ok: [localhost] => (item={u'key2': u'value_of_second_key'}) =>
msg: '{u''key2'': u''value_of_second_key''} is of type dict'
ok: [localhost] => (item={u'key3': u'value_of_third_key'}) =>
msg: '{u''key3'': u''value_of_third_key''} is of type dict'
TASK [Example loop] ******************************************
ok: [localhost] => (item={u'key1': u'value_of_first_key'}) =>
msg:
- value_of_first_key
ok: [localhost] => (item={u'key2': u'value_of_second_key'}) =>
msg:
- value_of_second_key
ok: [localhost] => (item={u'key3': u'value_of_third_key'}) =>
msg:
- value_of_third_key
Further Readings
How to work with lists and dictionaries in Ansible
Extended loop variables

Variable value for another variable ansible

Sorry if there are many posts about variables inside variable my use case is different.
Trying to access an element from a variable list "efs_list" based on the index-number of the current host. There are three hosts in the inventory
vars:
efs_list:
- efs1
- efs2
- efs3
sdb_index: "{{ groups['all'].index(inventory_hostname) }}"
The values should be as follows
host1- efs1
host2- efs2
host3- efs3
Tried accessing it through efs_list.{{ sdb_index }}
for - debug: var=efs_list.{{ sdb_index }} the output is as intended
ok: [10.251.0.174] => {
"efs_list.0": "efs1"
}
ok: [10.251.0.207] => {
"efs_list.1": "efs2"
}
ok: [10.251.0.151] => {
"efs_list.2": "efs3"
}
But for
- debug:
msg: "{{ efs_list.{{ sdb_index }} }}"
fatal: [10.251.0.174]: FAILED! => {"msg": "template error while templating string: expected name or number. String: {{ efs_list.{{ sdb_index }} }}"}
---
- name: SDB Snapshots Creation
hosts: all
remote_user: "centos"
become: yes
vars:
efs_list:
- efs1
- efs2
- efs3
sdb_index: "{{ groups['all'].index(inventory_hostname) }}"
tasks:
- debug: var=efs_list.{{ sdb_index }}
- debug:
msg: "{{ efs_list.{{ sdb_index }} }}"
- name: Get Filesystem ID
become: false
local_action: command aws efs describe-file-systems --creation-token "{{ efs_list.{{ sdb_index }} }}"
--region us-east-1 --query FileSystems[*].FileSystemId --output text
register: fs_id
It should attribute the element of list to current indexenter code here
extract filter will do the job. The input of the filter must be a list of indices and a container (array in this case). The tasks below
- set_fact:
sdb_index: "{{ [] + [ groups['all'].index(inventory_hostname) ] }}"
- debug:
msg: "{{ sdb_index|map('extract', efs_list)|list }}"
give
ok: [host1] => {
"msg": [
"efs1"
]
}
ok: [host2] => {
"msg": [
"efs2"
]
}
ok: [host3] => {
"msg": [
"efs3"
]
}
If the hosts are not sorted in the inventory it's necessary to sort them in the play
- set_fact:
my_hosts: "{{ groups['all']|sort }}"
- set_fact:
sdb_index: "{{ [] + [ my_hosts.index(inventory_hostname) ] }}"
- debug:
msg: "{{ sdb_index|map('extract', efs_list)|list }}"

Iterate dictionary in dictionary

I have a json structure like this:
{
"group_a": {
"setting_one": "text one",
"setting_two": "text two"
},
"group_b": {
"setting_three": "text three",
"setting_four": "text four"
}
}
And I need to do iterate over the dicts inside the dicts and create tasks, kinda like this:
- name: Task group_a setting_one
...
- name: Task group_a setting_two
...
- name: Task group_b setting_three
...
- name: Task group_b setting_four
...
I was hoping to make something like this:
- name: Insert {{ group }} {{ setting }}
ini_file:
path: settings.ini
section: "{{ group }}"
option: "{{ setting }}"
value: "{{ setting_value }}"
with_nested:
- "{{ data.keys() }}" # Can I get this key and use in the next loop?
- "{{ data.key.keys() }}"
How can I do this? I have been messing about with loop, with_items, with_dict etc, but I can't manage to get it exactly the way I need. I also tried with_nested, but that does not seem to do what I need either.
Kind regards, Jonas
Let's reduce the structure to one with the same piece of information
my_data:
group_a:
- one
- two
group_b:
- three
- four
Then the play below
- debug:
msg: "{{ item.0.key }} setting_{{ item.1 }}"
with_subelements:
- "{{ my_data|dict2items }}"
- value
gives:
"msg": "group_b setting_three"
"msg": "group_b setting_four"
"msg": "group_a setting_one"
"msg": "group_a setting_two"
It's not straightforward to use the original data. The play below separates the keys and values
tasks:
- set_fact:
my_groups: "{{ my_data.keys()}}"
- debug:
msg: "{{ item }} {{ my_data|dict2items|json_query(my_query)|to_yaml }}"
loop: "{{ my_groups }}"
vars:
my_query: "[?key=='{{ item }}'].value"
and gives:
"msg": "group_b - - {setting_three: text three}\n - {setting_four: text four}\n"
"msg": "group_a - - {setting_one: text one}\n - {setting_two: text two}\n"
Multi-step loops are needed to proceed. For example the play below
- set_fact:
my_data1: |
{% for group, values in my_data.items() %}
{% for value in values %}
{% for item_key, item_val in value.items() %}
Task {{ group }} {{ item_key }},
{% endfor %}{% endfor %}{% endfor %}
- debug:
msg: "{{ my_data1.split('\n') }}"
gives:
"msg": [
"Task group_b setting_three,",
"Task group_b setting_four,",
"Task group_a setting_one,",
"Task group_a setting_two,",
""
]

why are stdout_lines [1,2] and [3,4] strings instead of arrays?

A shell command outputs one array per line on the standard output: [1,2] then [3,4]. I would like to use each element of the array as a parameter to a module and decided to loop over stdout_lines. However, item.0 and item.1 contains the first and second character of the string instead of the first and second element of the array.
Why is item a string instead of an array?
What would be the most elegant way to get what I need?
Here is a reproducer to help understand the question, using ansible 2.7.5. The following play:
---
- name: test
hosts: localhost
become: false
gather_facts: false
tasks:
- name: one array per line
shell: |
echo '[1,2]'
echo '[3,4]'
register: result
- name: each item is an array
debug:
msg: "{{ item }}"
loop: "{{ result.stdout_lines }}"
- name: accessing the first element of an item
debug:
msg: "{{ item.0 }} => {{ item.1 }}"
loop: "{{ result.stdout_lines }}"
The actual output is:
TASK [each item is an array] *****************************************************
ok: [localhost] => (item=[1, 2]) => {
"msg": [
1,
2
]
}
ok: [localhost] => (item=[3, 4]) => {
"msg": [
3,
4
]
}
TASK [accessing the first element of an item] ************************************
ok: [localhost] => (item=[1, 2]) => {
"msg": "[ => 1"
}
ok: [localhost] => (item=[3, 4]) => {
"msg": "[ => 3"
}
but I was expecting:
TASK [each item is an array] *****************************************************
ok: [localhost] => (item=[1, 2]) => {
"msg": [
1,
2
]
}
ok: [localhost] => (item=[3, 4]) => {
"msg": [
3,
4
]
}
TASK [accessing the first element of an item] ************************************
ok: [localhost] => (item=[1, 2]) => {
"msg": "1 => 2"
}
ok: [localhost] => (item=[3, 4]) => {
"msg": "3 => 4"
}
An option would be to convert the list of strings to the list of lists.
- name: convert strings to lists
set_fact:
lists: "{{ lists }} + [ {{ item }} ]"
loop: "{{ result.stdout_lines }}"
- name: accessing the 1st and 2nd element of a list
debug:
msg: "{{ item.0 }} => {{ item.1 }}"
loop: "{{ lists }}"
After discussions with Pilou on #ansible-fr#irc.freenode.net, a simple solution was found.
It is enough to trigger a jinja2 evaluation by assigning another variable with the content of item. It will be converted from string to a structure, if possible. See the test playbook updated with the solution below.
debug:
msg: "{{ array.0 }} => {{ array.1 }}"
vars:
array: "{{ item }}"
loop: "{{ result.stdout_lines }}"
There is no rationale explaining why stdout is converted into a structure when possible but not the elements in stdout_lines. It is how the shell module is implemented. It is possible that it will change in the future. If that's the case, the proposed solution will keep working. Only it would not be necessary.
---
- name: test
hosts: localhost
become: false
gather_facts: false
tasks:
- name: one array per line
shell: |
echo '[1,2]'
echo '[3,4]'
register: result
- name: each item is an array
debug:
msg: "{{ item }}"
loop: "{{ result.stdout_lines }}"
- name: accessing the first element of an item
debug:
msg: "{{ array.0 }} => {{ array.1 }}"
vars:
array: "{{ item }}"
loop: "{{ result.stdout_lines }}"

How to create list for with_item from included list

I have this list:
"mylist": [
{ "files": [{"path": "path11"}, {"path": "path12"}] },
{ "files": [{"path": "path21"}, {"path": "path22"}] }
]
and I need to run role with with_items, where items should be path elements from my list.
For example:
- debug: msg="{{ item }}"
with_items: "{{ mylist | some_magic }}"
Needed output:
TASK [test : debug] **********************************
ok: [host] => (item=path11 ) => {
"msg": "path11"
}
ok: [host] => (item=path12 ) => {
"msg": "path12"
}
ok: [host] => (item=path21 ) => {
"msg": "path21"
}
...
Is it possible?
This is what I have already tried:
Constructions look like this:
- debug: msg="{{ item }}"
with_items: "{% for files in mylist | map(attribute='files') %}{% for file in files %}{{file.path}}{% endfor %}{% endfor %}"
Returned value as expected is not a list.
Wrong constructions look like this:
- debug: msg="{{ item }}"
with_items: "{{ mylist | map(attribute='files') | map(attribute='path') | list }}"
It is a legacy with_subelements loop pattern.
Using the loop keyword (Ansible 2.5 and later) you can iterate with:
- debug:
msg: "{{ item.1.path }}"
loop: "{{ mylist | subelements('files') }}"
Or the same using JMESPath (this shows how to create a list out of the whole data structure):
- debug:
msg: "{{ item }}"
loop: "{{ mylist | json_query('[].files[].path') }}"

Resources