stackprotector
stackprotector

Reputation: 13578

Loop through subelements of subelements

I am using the following data structure in Ansible:

datacenters:
  - name: Datacenter1
    clusters:
      - name: ClusterA
        hosts:
          - 192.168.0.1
          - 192.168.0.2
      - name: ClusterB
        hosts:
          - 192.168.1.1
          - 192.168.1.2
  - name: Datacenter2
    clusters:
      - name: ClusterC
        hosts:
          - 192.168.2.1
          - 192.168.2.2

In a task, I want to iterate over each host while having access to the data of all the parent layers. If there is only one nesting level, it can easily be done with the subelements filter:

loop: '{{ datacenters | subelements(''clusters'') }}'

This will give me access to the data like this:

'Datacenter: {{ item.0.name }}, Cluster: {{ item.1.name }}'

I was hoping to be able to extend this concept like this:

loop: '{{ datacenters | subelements(''clusters'') | subelements(''hosts'') }}'

And being able to access the data like this:

'Datacenter: {{ item.0.name }}, Cluster: {{ item.1.name }}, Host: {{ item.2 }}'

But that does not work and I get the following error message instead:

Unexpected templating type error occurred on ({{ datacenters | subelements('clusters') | subelements('hosts') }}): the key hosts should point to a dictionary, got ...(the result of the first layer)

I found this question, which solves a similar problem, but relies on having distinct dict keys on all nesting levels, which I don't, because datacenters and clusters have the same name key.

So, how can I iterate over subelements of subelements in Ansible?

Upvotes: 2

Views: 3054

Answers (2)

stackprotector
stackprotector

Reputation: 13578

Different approach

While Zeitounator answers the question already, I still want to present a different approach to the looping problem. It still can become difficult to loop over multiple nesting layers if there are more than two or three of them.

Another way to solve this problem is to use ansible.builtin.include_tasks and distinct task files for each layer to loop over the whole data structure. Example for the data structure from the question:

  • configure-environment.yaml:

    # Configure datacenters
    - name: Configure datacenters
      ansible.builtin.include_tasks: configure-datacenter.yaml
      loop: '{{ datacenters }}'
      loop_control:
        loop_var: datacenter
        label: '{{ datacenter.name }}'
    
  • configure-datacenter.yaml:

    # Configure datacenter
    - name: Configure datacenter
      ansible.builtin.debug:
        msg: Configure datacenter {{ datacenter.name }}.
    
    # Configure clusters
    - name: Configure clusters
      ansible.builtin.include_tasks: configure-cluster.yaml
      loop: '{{ datacenter.clusters }}'
      loop_control:
        loop_var: cluster
        label: '{{ cluster.name }}'
    
  • configure-cluster.yaml:

    # Configure cluster
    - name: Configure cluster
      ansible.builtin.debug:
        msg: Configure cluster {{ cluster.name }} of datacenter {{ datacenter.name }}.
    
    # Configure hosts
    - name: Configure hosts
      ansible.builtin.debug:
        msg: Configure host {{ my_host }} of cluster {{ cluster.name }} of datacenter {{ datacenter.name }}.
      loop: '{{ cluster.hosts }}'
      loop_control:
        loop_var: my_host
    

Upvotes: 0

Zeitounator
Zeitounator

Reputation: 44779

A bit far-fetched but the following playbook will achieve your goal:

 ---
 - hosts: localhost
   gather_facts: false
 
   vars:
     datacenters:
     - name: Datacenter1
       clusters:
         - name: ClusterA
           hosts:
             - 192.168.0.1
             - 192.168.0.2
         - name: ClusterB
           hosts:
             - 192.168.1.1
             - 192.168.1.2
     - name: Datacenter2
       clusters:
         - name: ClusterC
           hosts:
             - 192.168.2.1
             - 192.168.2.2
 
     # Get the list of datacenters
     _dcs: "{{ datacenters | map(attribute='name') }}"
     # Get the corresponding list of clusters with subelements on hosts
     _clusters: "{{ datacenters | map(attribute='clusters') | map('subelements', 'hosts') }}"
     # Recreate a list with the sublisted hosts per clusters and create subelements on that final result
     _overall: "{{ dict(_dcs | zip(_clusters)) | dict2items(key_name='datacenter', value_name='clusters') | subelements('clusters') }}"
 
   tasks:
     - name: Loop on the result
       debug:
         msg:
           - "DC: {{ item.0.datacenter }}"
           - "Cluster: {{ item.1.0.name }}"
           - "Host: {{ item.1.1 }}"
       loop: "{{ _overall }}"
       loop_control:
         label: "{{ item.0.datacenter }} - {{ item.1.0.name }}"

This gives:

PLAY [localhost] **************************************************************************************************************************************************

TASK [Loop on the result] *****************************************************************************************************************************************
ok: [localhost] => (item=Datacenter1 - ClusterA) => {
    "msg": [
        "DC: Datacenter1",
        "Cluster: ClusterA",
        "Host: 192.168.0.1"
    ]
}
ok: [localhost] => (item=Datacenter1 - ClusterA) => {
    "msg": [
        "DC: Datacenter1",
        "Cluster: ClusterA",
        "Host: 192.168.0.2"
    ]
}
ok: [localhost] => (item=Datacenter1 - ClusterB) => {
    "msg": [
        "DC: Datacenter1",
        "Cluster: ClusterB",
        "Host: 192.168.1.1"
    ]
}
ok: [localhost] => (item=Datacenter1 - ClusterB) => {
    "msg": [
        "DC: Datacenter1",
        "Cluster: ClusterB",
        "Host: 192.168.1.2"
    ]
}
ok: [localhost] => (item=Datacenter2 - ClusterC) => {
    "msg": [
        "DC: Datacenter2",
        "Cluster: ClusterC",
        "Host: 192.168.2.1"
    ]
}
ok: [localhost] => (item=Datacenter2 - ClusterC) => {
    "msg": [
        "DC: Datacenter2",
        "Cluster: ClusterC",
        "Host: 192.168.2.2"
    ]
}

PLAY RECAP ********************************************************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Upvotes: 3

Related Questions