I have a data structure that looks like this:
all_vms:
clusterA:
group1:
- vm-01
- vm-02
group2:
- vm-03
- vm-04
clusterB:
group1:
- vm-05
- vm-06
The key names are not known beforehand. There may be shared "group" key names between clusters.
I want to loop through that data structure and run a task with each of the arrays, but I need the names of the keys they're contained within. The looping task would look something like:
- task:
vms: "{{ item.value }}"
group: "{{ item.key }}"
cluster: "{{ item.parentkey }}"
loop: "{{ all_vms | ??? }}"
And that would unroll to:
- task:
vms:
- vm-01
- vm-02
group: group1
cluster: clusterA
- task:
vms:
- vm-03
- vm-04
group: group2
cluster: clusterA
- task:
vms:
- vm-05
- vm-06
group: group3
cluster: clusterB
I cannot change the main cluster/group structure, but I can change the structure of the elements that are currently arrays. I have considered just duplicating the keys as values, like this:
all_vms:
clusterA:
group1:
cluster: "clusterA"
group: "group1"
vms:
- vm-01
- vm-02
group2:
cluster: "clusterA"
group: "group2"
vms:
- vm-03
- vm-04
clusterB:
group1:
cluster: "clusterB"
group: "group1"
vms:
- vm-05
- vm-06
I would rather not do that, because it's terrible, but I can. But I can't even figure out a way to pop each of those things out into an array. (Edit: Actually, I think figured that out right after posting: all_vms | json_query('*.* | []')
. I guess I can go with that if there's not a way to use the tidier data structure.)
Or if I could just use a @!#$% nested loop, if ansible would let me:
- block:
- task:
vms: "{{ item.value }}"
group: "{{ item.key }}"
cluster: "{{ cluster.key }}"
loop: "{{ cluster.value | dict2items }}"
loop: "{{ all_vms | dict2items }}"
loop_control:
loop_var: cluster
(Yes, I could do this with include_tasks
, but having to have a separate file for a nested loop is just ridiculous.)
Any ideas how to iterate over this data structure without having to resort to a separate file just to do nested looping?
CodePudding user response:
Although this is definitely possible using several combinations of filters directly in Ansible/jinja2, my 2 cent: save you nerves and time using a custom filter.
Demo below is for a a filter in the dedicated filters_plugin
folder adjacent to your playbook. See the roles and collections documentations for better ways to distribute your filter if need be. Note that this was written real quick to put you on track and although it is fully functional with your above data example, you will need to add some data and errors checks if you intend to use this for a larger audience.
filters_plugins/my_cluster_filters.py
def all_vms_list(vm_dict):
"""Return a list of dicts for all vms with their cluster an group information"""
vms_list = []
for cluster, groups in vm_dict.items():
for group_name, hosts in groups.items():
for host in hosts:
current_vm = {
'cluster': cluster,
'group': group_name,
'host': host
}
vms_list.append(current_vm)
return vms_list
class FilterModule(object):
"""my cluster data filters."""
def filters(self):
"""Return the filter list."""
return {
'all_vms_list': all_vms_list
}
Then the test playbook.yml
:
---
- hosts: localhost
gather_facts: false
vars:
all_vms:
clusterA:
group1:
- vm-01
- vm-02
group2:
- vm-03
- vm-04
clusterB:
group1:
- vm-05
- vm-06
tasks:
- debug:
msg: "Host {{ item.host }} belongs to group {{ item.group }} inside cluster {{ item.cluster }}"
loop: "{{ all_vms | all_vms_list }}"
gives:
PLAY [localhost] **************************************************************************************************************************************************************************************************************
TASK [debug] ******************************************************************************************************************************************************************************************************************
ok: [localhost] => (item={'cluster': 'clusterA', 'group': 'group1', 'host': 'vm-01'}) => {
"msg": "Host vm-01 belongs to group group1 inside cluster clusterA"
}
ok: [localhost] => (item={'cluster': 'clusterA', 'group': 'group1', 'host': 'vm-02'}) => {
"msg": "Host vm-02 belongs to group group1 inside cluster clusterA"
}
ok: [localhost] => (item={'cluster': 'clusterA', 'group': 'group2', 'host': 'vm-03'}) => {
"msg": "Host vm-03 belongs to group group2 inside cluster clusterA"
}
ok: [localhost] => (item={'cluster': 'clusterA', 'group': 'group2', 'host': 'vm-04'}) => {
"msg": "Host vm-04 belongs to group group2 inside cluster clusterA"
}
ok: [localhost] => (item={'cluster': 'clusterB', 'group': 'group1', 'host': 'vm-05'}) => {
"msg": "Host vm-05 belongs to group group1 inside cluster clusterB"
}
ok: [localhost] => (item={'cluster': 'clusterB', 'group': 'group1', 'host': 'vm-06'}) => {
"msg": "Host vm-06 belongs to group group1 inside cluster clusterB"
}
PLAY RECAP ********************************************************************************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
CodePudding user response:
And here is the solution using several combinations of filters directly in Ansible / Jinja.
It combines the first level keys and values with a zip
folder, in order to have a know subelements name — 1
— on which we can then use a subelements
.
The second level key / value pair is accessible thanks to a dict2items
mapped on the first level values.
The task ends up being
- set_fact:
tasks: "{{ tasks | default([]) [_task] }}"
loop: >-
{{
all_vms.keys()
| zip(all_vms.values() | map('dict2items'))
| subelements([1])
}}
loop_control:
label: "{{ item.0.0 }} — {{ item.1.key }}"
vars:
_task:
task:
vms: "{{ item.1.value }}"
group: "{{ item.1.key }}"
cluster: "{{ item.0.0 }}"
Given the playbook:
- hosts: localhost
gather_facts: no
tasks:
- set_fact:
tasks: "{{ tasks | default([]) [_task] }}"
loop: >-
{{
all_vms.keys()
| zip(all_vms.values() | map('dict2items'))
| subelements([1])
}}
loop_control:
label: "{{ item.0.0 }} — {{ item.1.key }}"
vars:
_task:
task:
vms: "{{ item.1.value }}"
group: "{{ item.1.key }}"
cluster: "{{ item.0.0 }}"
all_vms:
clusterA:
group1:
- vm-01
- vm-02
group2:
- vm-03
- vm-04
clusterB:
group1:
- vm-05
- vm-06
- debug:
var: tasks
This yields:
TASK [set_fact] ***************************************************************
ok: [localhost] => (item=clusterA — group1)
ok: [localhost] => (item=clusterA — group2)
ok: [localhost] => (item=clusterB — group1)
TASK [debug] ******************************************************************
ok: [localhost] =>
tasks:
- task:
cluster: clusterA
group: group1
vms:
- vm-01
- vm-02
- task:
cluster: clusterA
group: group2
vms:
- vm-03
- vm-04
- task:
cluster: clusterB
group: group1
vms:
- vm-05
- vm-06
CodePudding user response:
To iterate all combinations use to_paths lookup plugin. For example,
- debug:
msg: "Host {{ item.value }}
belongs to group {{ item.key.split('.').1.split('[').0 }}
inside cluster {{ item.key.split('.').0 }}"
loop: "{{ lookup('ansible.utils.to_paths', all_vms)|dict2items }}"
gives (abridged)
msg: Host vm-01 belongs to group group1 inside cluster clusterA
msg: Host vm-02 belongs to group group1 inside cluster clusterA
msg: Host vm-03 belongs to group group2 inside cluster clusterA
msg: Host vm-04 belongs to group group2 inside cluster clusterA
msg: Host vm-05 belongs to group group1 inside cluster clusterB
msg: Host vm-06 belongs to group group1 inside cluster clusterB
To iterate the tasks create the file below
shell> cat inner_loop.yml
- debug:
msg: |
task:
vms: {{ item.value }}
group: {{ item.key }}
cluster: {{ outer_item.key }}
loop: "{{ outer_item.value|dict2items }}"
and include it in the loop. For example,
- include_tasks: inner_loop.yml
loop: "{{ all_vms|dict2items }}"
loop_control:
loop_var: outer_item
gives (abridged)
msg: |-
task:
vms: ['vm-01', 'vm-02']
group: group1
cluster: clusterA
msg: |-
task:
vms: ['vm-03', 'vm-04']
group: group2
cluster: clusterA
msg: |-
task:
vms: ['vm-05', 'vm-06']
group: group1
cluster: clusterB