Process complex variables with set_fact and with_items

Need to process complex data? Like rearranging keys in a list of nested dicts. No problem! You can do it with set_fact, with_items and register.

Let's take this list of users as example data:

users:  
  - name: John Smith
    login: jsmith
    birth: 1960-01-01
  - name: Jane Smith
    login: jane
    birth: 1970-12-12

And somewhere in our playbook we got their passwords:

passwords:  
  jsmith: pwd1234
  jane: qwerty

Now we will create a list of users with their age and password:

---
- hosts: localhost
  vars:
    users:
      - name: John Smith
        login: jsmith
        birth: '1960-01-01'
      - name: Jane Smith
        login: jane
        birth: '1970-12-12'
    passwords:
      jsmith: pwd1234
      jane: qwerty
  tasks:
      # Make temporary list (will get it as tmp_users.results)
    - set_fact:
        tmp_user:
          password: "{{ passwords[item.login] }}"
          age: "{{ ((ansible_date_time.date | to_datetime('%Y-%m-%d')
                   - (item.birth | to_datetime('%Y-%m-%d'))).days/365) | int }}"
      with_items: "{{ users }}"
      register: tmp_users
      # Join original items with temporary ones
    - set_fact:
        tmp_user: "{{ item.item | combine(item.ansible_facts.tmp_user) }}"
      with_items: "{{ tmp_users.results }}"
      register: tmp_users
      # Get clean results my mapping only one key from tmp_user.results
    - debug:
        msg: "{{ tmp_users.results | map(attribute='ansible_facts.tmp_user') | list }}"

Here we use the trick with a combination of with_ loop and register variable:

  • set_fact task has ansible_facts dictionary in its result (this way Ansible knows that it need to add new facts).
  • register and with_ loop gives a complex dictionary, which always has results list inside where each task iteration result is stored.

So we actually don't care about tmp_user fact (which gets overwritten on each iteration) as a result of set_fact task call, but use the tasks result object in registered list.

If we inspect tmp_users.results after first task, we will find a list of such items:

{
    "_ansible_item_result": true,
    "_ansible_no_log": false,
    "ansible_facts": {
        "tmp_user": {
            "age": "57",
            "password": "pwd1234"
        }
    },
    "changed": false,
    "invocation": {
        "module_args": {
            "tmp_user": {
                "age": "57",
                "password": "pwd1234"
            }
        },
        "module_name": "set_fact"
    },
    "item": {
        "birth": "1960-01-01",
        "login": "jsmith",
        "name": "John Smith"
    }
}

Where item is original list element and ansible_facts has tmp_user.

As we also need our original data fields, and not just age and password, we combine item and ansible_facts.tmp_user in a similar way.

And on the last step, we can get a "clean" list of users with map filter, extracting only ansible_facts.tmp_user attribute:

tmp_users.results | map(attribute='ansible_facts.tmp_user') | list