Rafał W.
Rafał W.

Reputation: 119

Iterating list of dictionaries with dictionary

I'm having trouble grasping data manipulation in Ansible. Here's the setup:

- hosts: emirr01
  gather_facts: no
  vars:
    setup:
      - emirr01: { label: "label1" }
      - emirr02: { label: "label2" }
      - emirr03: { label: "label3" }
    lookup: [ "emirr01", "emirr02"]
    use_labels: [ ]
  tasks:
    - debug: msg="setup={{ setup }}"
    - debug: msg="lookup={{ lookup }}"

    - debug: msg="item0={{ item.0 }} item1={{ item.1 }}"
      when: inventory_hostname == item.1
      with_nested:
        - "{{ setup }}"
        - "{{ lookup }}"

    - set_fact:
        use_labels: "{{ use_labels + [ item.1.label ] }}"
      when: item.0 == item.1.label
      with_nested:
        - "{{ setup }}"
        - "{{ lookup }}"

    - debug: msg="use_labels={{ use_labels }}"

What I need is to set a fact use_labels which is a list of labels as defined in setup list for each host found in lookup list. What I expect is a list like this:

[ "label1", "label2" ]

My problem is not being able to "reach" label in a list I get in item.1 which is (example):

item1=[{'emirr02': {'label': 'label2'}}

Here's is the error:

$ ansible-playbook test.yml -l emirr01

PLAY [emirr01] ****************************************************************************************************************************************************************************************************************************************************************************************************************************

TASK [debug] ******************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [emirr01] =>
  msg: 'setup=[{''emirr01'': {''label'': ''label1''}}, {''emirr02'': {''label'': ''label2''}}, {''emirr03'': {''label'': ''label3''}}]'

TASK [debug] ******************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [emirr01] =>
  msg: lookup=['emirr01', 'emirr02']

TASK [debug] ******************************************************************************************************************************************************************************************************************************************************************************************************************************
ok: [emirr01] => (item=[{'emirr01': {'label': 'label1'}}, 'emirr01']) =>
  msg: 'item0={''emirr01'': {''label'': ''label1''}} item1=emirr01'
skipping: [emirr01] => (item=[{'emirr01': {'label': 'label1'}}, 'emirr02'])
ok: [emirr01] => (item=[{'emirr02': {'label': 'label2'}}, 'emirr01']) =>
  msg: 'item0={''emirr02'': {''label'': ''label2''}} item1=emirr01'
skipping: [emirr01] => (item=[{'emirr02': {'label': 'label2'}}, 'emirr02'])
ok: [emirr01] => (item=[{'emirr03': {'label': 'label3'}}, 'emirr01']) =>
  msg: 'item0={''emirr03'': {''label'': ''label3''}} item1=emirr01'
skipping: [emirr01] => (item=[{'emirr03': {'label': 'label3'}}, 'emirr02'])

TASK [set_fact] ***************************************************************************************************************************************************************************************************************************************************************************************************************************
fatal: [emirr01]: FAILED! =>
  msg: |-
    The conditional check 'item.0 == item.1.label' failed. The error was: error while evaluating conditional (item.0 == item.1.label): 'ansible.utils.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'label'

    The error appears to be in '/home/ansible/test.yml': line 21, column 7, but may
    be elsewhere in the file depending on the exact syntax problem.

    The offending line appears to be:


        - set_fact:
          ^ here

It all boils down to "digging" for a value from a dictionary in a list. Does anyone have an idea of how to reach item.1.label? It's a little bit frustrating since this is trivial in Python or other programming languages but in Ansible... Or maybe my mistake is to use a wrong kind of with_* loop for this purpose?

Upvotes: 2

Views: 624

Answers (2)

Vladimir Botka
Vladimir Botka

Reputation: 68034

Q: "Expect a list like this [label1, label2]"

    setup:
      - emirr01: { label: "label1" }
      - emirr02: { label: "label2" }
      - emirr03: { label: "label3" }
    lookup: [ "emirr01", "emirr02"]

A: This is a typical example of a wrong data structure for this use-case. A list of dictionaries can't be used in mapping the filter extract. Moreover, the keys in the dictionaries don't have to be unique, because they are hidden in the items of the list. The solution is simple with the data in the dictionary

    setup:
      emirr01: { label: "label1" }
      emirr02: { label: "label2" }
      emirr03: { label: "label3" }
    - set_fact:
        use_labels: "{{ lookup|map('extract', setup, 'label')|list }}"

gives

  use_labels:
  - label1
  - label2

One of the options is creating a dictionary from the list first. For example

    - set_fact:
        setup2: "{{ setup2|default({})|combine(item) }}"
      loop: "{{ setup }}"

Then the dictionary setup2 can be used to get the same result


It's possible to filter the list directly. For example, the task below gives the same result too

    - set_fact:
        use_labels: "{{ setup|map('dict2items')|map('first')|
                        selectattr('key', 'in', lookup )|
                        map(attribute='value.label')|
                        list }}"

Upvotes: 2

β.εηοιτ.βε
β.εηοιτ.βε

Reputation: 39119

Your issue with the with_nested is actually coming from how those "two-lists" loops are working.

When you are using loops like with_nested, the item.0 is the item of the first list, while the item.1 is the item of the second list.

Because lookup is a list and not a list of dictionaries like

lookup:
  - label: "emirr01"
  - label: "emirr02"

You can use the value of item.1 right away.

So the way to have you issue fixed is to use it this way:

- set_fact:
    use_labels: "{{ use_labels + [ item.0[item.1].label ] }}"
  when: item.0[item.1].label is defined
  with_nested:
    - "{{ setup }}"
    - "{{ lookup }}"

Here is a playbook demonstrating this:

- hosts: localhost
  gather_facts: no
      
  tasks:  
    - set_fact:
        use_labels: "{{ use_labels | default([]) + [ item.0[item.1].label ] }}"
      when: item.0[item.1].label is defined
      with_nested:
        - "{{ setup }}"
        - "{{ lookup }}"
      vars:
        setup:
          - emirr01: 
              label: "label1"
          - emirr02: 
              label: "label2"
          - emirr03: 
              label: "label3"
        lookup: 
          - "emirr01"
          - "emirr02"

    - debug:
        msg: "{{ use_labels }}"

Wich gives:

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

TASK [set_fact] ****************************************************************************************************
ok: [localhost] => (item=[{'emirr01': {'label': 'label1'}}, 'emirr01'])
skipping: [localhost] => (item=[{'emirr01': {'label': 'label1'}}, 'emirr02']) 
skipping: [localhost] => (item=[{'emirr02': {'label': 'label2'}}, 'emirr01']) 
ok: [localhost] => (item=[{'emirr02': {'label': 'label2'}}, 'emirr02'])
skipping: [localhost] => (item=[{'emirr03': {'label': 'label3'}}, 'emirr01']) 
skipping: [localhost] => (item=[{'emirr03': {'label': 'label3'}}, 'emirr02']) 

TASK [debug] *******************************************************************************************************
ok: [localhost] => {
    "msg": [
        "label1",
        "label2"
    ]
}

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

Upvotes: 1

Related Questions