'Loop over list of dictionaries or flow mapping using Ansible?

I have the following data structure. I'm struggling to know precisely how to describe this data structure. My first instinct is to call it a "list of dictionaries" but I've also been told it's called a "flow mapping".

site_subnets:
    - control: { network: "100.99.97.0/24", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
    - storage: { network: "100.99.98.0/24", mtu: "9000", dhcp_from_ip: "30", dhcp_to_ip: "50" }
    - data: { network: "100.99.99.0/24", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
    - site: { network: "100.99.0.0/16", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
    - local: { network: "100.99.44.1/22", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }

For this particular exercise, I'd like to loop over the data structure and simply print the name of the item.

Here's the playbook I've written:

---
- hosts: localhost
  vars:
    site_subnets:
      - control: { network: "100.99.97.0/24", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - storage: { network: "100.99.98.0/24", mtu: "9000", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - data: { network: "100.99.99.0/24", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - site: { network: "100.99.0.0/16", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - local: { network: "100.99.44.1/22", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
  tasks:
    - name: site_subnet network names.
      debug:
        msg: "subnet name is: {{ item }}"
      loop: "{{ site_subnets }}"
                                   

What I'm hoping to see is something like the following:

subnet name is: control
subnet name is: storage
subnet name is: data
subnet name is: site
subnet name is: local

Instead I'm seeing the entire dictionary returned upon evaluating each loop.



Solution 1:[1]

Yes, it is most certainly a list of dicts, and the flow mapping is what the yaml spec calls the style of syntax that you used to define the interior { key: value } pairs. If you were to say - { control: { network: "... then all of your dict structures would be in "flow mapping" style

Anyway, since you only care about the keys of those embedded dicts, dict2items will allow you to "pivot" those dicts into {key: control, value: {network: ...}} shape, allowing map(attribute="key") to pull out just the keys. That first in in there because your top-level dict only has one key-value pair

  - vars:
      site_subnets:
      - control: { network: "100.99.97.0/24", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - storage: { network: "100.99.98.0/24", mtu: "9000", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - data: { network: "100.99.99.0/24", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - site: { network: "100.99.0.0/16", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
      - local: { network: "100.99.44.1/22", mtu: "1500", dhcp_from_ip: "30", dhcp_to_ip: "50" }
    debug:
        msg: "subnet name is: {{ item }}"
    loop: "{{ site_subnets | map('dict2items') | map('first') | map(attribute='key') }}"

yielding

ok: [localhost] => (item=control) => {
    "msg": "subnet name is: control"
}
ok: [localhost] => (item=storage) => {
    "msg": "subnet name is: storage"
}
...

Solution 2:[2]

There are many options. For example,

  1. The simplest option is to get the key of a dictionary in a loop
    - debug:
        msg: "subnet name is: {{ item.keys()|first }}"
      loop: "{{ site_subnets }}"

gives (abridged)

  msg: 'subnet name is: control'
  msg: 'subnet name is: storage'
  msg: 'subnet name is: data'
  msg: 'subnet name is: site'
  msg: 'subnet name is: local'

or, in a Jinja block

    - debug:
        msg: |-
          {% for item in site_subnets %}
          subnet name is: {{ item.keys()|first }}
          {% endfor %}

gives (abridged) in a single string

  msg: |-
    subnet name is: control
    subnet name is: storage
    subnet name is: data
    subnet name is: site
    subnet name is: local

  1. The next option is the conversion of the dictionaries. Create attributes key and value
  site_subnets_attr: "{{ site_subnets|map('dict2items')|flatten }}"

gives

  site_subnets_attr:
  - key: control
    value:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.97.0/24
  - key: storage
    value:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '9000'
      network: 100.99.98.0/24
  - key: data
    value:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.99.0/24
  - key: site
    value:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.0.0/16
  - key: local
    value:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.44.1/22

Then, the iteration is trivial

    - debug:
        msg: "subnet name is: {{ item.key }}"
      loop: "{{ site_subnets_attr }}"

gives (abridged) the same result

  msg: 'subnet name is: control'
  msg: 'subnet name is: storage'
  msg: 'subnet name is: data'
  msg: 'subnet name is: site'
  msg: 'subnet name is: local'

  1. The next option is the conversion of the list to a dictionary
  site_subnets_dict: "{{ site_subnets|combine }}"

gives

  site_subnets_dict:
    control:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.97.0/24
    data:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.99.0/24
    local:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.44.1/22
    site:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '1500'
      network: 100.99.0.0/16
    storage:
      dhcp_from_ip: '30'
      dhcp_to_ip: '50'
      mtu: '9000'
      network: 100.99.98.0/24

Then, iterate the list created by dict2items

    - debug:
        msg: "subnet name is: {{ item.key }}"
      loop: "{{ site_subnets_dict|dict2items }}"

or, simply use filter list that extracts the dictionaries' keys

    - debug:
        msg: "subnet name is: {{ item }}"
      loop: "{{ site_subnets_dict|list }}"

Both options give (abridged) the same result

  msg: 'subnet name is: control'
  msg: 'subnet name is: storage'
  msg: 'subnet name is: data'
  msg: 'subnet name is: site'
  msg: 'subnet name is: local'

This procedure will work if the keys of the dictionaries are unique, of course. To be sure, test it

    - assert:
        that: site_subnets|length == site_subnets_dict|length

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 mdaniel
Solution 2