84

In Ansible I've used register to save the results of a task in the variable people. Omitting the stuff I don't need, it has this structure:

{
    "results": [
        {
            "item": {
                "name": "Bob"
            },
            "stdout": "male"
        },
        {
            "item": {
                "name": "Thelma"
            },
            "stdout": "female"
        }
    ]
}

I'd like to use a subsequent set_fact task to generate a new variable with a dictionary like this:

{
    "Bob": "male",
    "Thelma": "female"
}

I guess this might be possible but I'm going round in circles with no luck so far.

Phil Gyford
  • 13,432
  • 14
  • 81
  • 143
  • Be aware that since ansible v2.2, with_items requires explicit jinja2 wrapping. So the first sample would be:
     - name: Populate genders set_fact: genders: "{{ genders|default({}) | combine( {item.item.name: item.stdout} ) }}" with_items: "{{ people.results }}"
    
    – sarraz May 23 '17 at 16:21

2 Answers2

154

I think I got there in the end.

The task is like this:

- name: Populate genders
  set_fact:
    genders: "{{ genders|default({}) | combine( {item.item.name: item.stdout} ) }}"
  with_items: "{{ people.results }}"

It loops through each of the dicts (item) in the people.results array, each time creating a new dict like {Bob: "male"}, and combine()s that new dict in the genders array, which ends up like:

{
    "Bob": "male",
    "Thelma": "female"
}

It assumes the keys (the name in this case) will be unique.


I then realised I actually wanted a list of dictionaries, as it seems much easier to loop through using with_items:

- name: Populate genders
  set_fact:
    genders: "{{ genders|default([]) + [ {'name': item.item.name, 'gender': item.stdout} ] }}"
  with_items: "{{ people.results }}"

This keeps combining the existing list with a list containing a single dict. We end up with a genders array like this:

[
    {'name': 'Bob', 'gender': 'male'},
    {'name': 'Thelma', 'gender': 'female'}
]
Mark
  • 6,269
  • 2
  • 35
  • 34
Phil Gyford
  • 13,432
  • 14
  • 81
  • 143
20

Thank you Phil for your solution; in case someone ever gets in the same situation as me, here is a (more complex) variant:

---
# this is just to avoid a call to |default on each iteration
- set_fact:
    postconf_d: {}

- name: 'get postfix default configuration'
  command: 'postconf -d'
  register: command

# the answer of the command give a list of lines such as:
# "key = value" or "key =" when the value is null
- name: 'set postfix default configuration as fact'
  set_fact:
    postconf_d: >
      {{
        postconf_d |
        combine(
          dict([ item.partition('=')[::2]|map('trim') ])
        )
  with_items: command.stdout_lines

This will give the following output (stripped for the example):

"postconf_d": {
    "alias_database": "hash:/etc/aliases", 
    "alias_maps": "hash:/etc/aliases, nis:mail.aliases",
    "allow_min_user": "no", 
    "allow_percent_hack": "yes"
}

Going even further, parse the lists in the 'value':

- name: 'set postfix default configuration as fact'
  set_fact:
    postconf_d: >-
      {% set key, val = item.partition('=')[::2]|map('trim') -%}
      {% if ',' in val -%}
        {% set val = val.split(',')|map('trim')|list -%}
      {% endif -%}
      {{ postfix_default_main_cf | combine({key: val}) }}
  with_items: command.stdout_lines
...
"postconf_d": {
    "alias_database": "hash:/etc/aliases", 
    "alias_maps": [
        "hash:/etc/aliases", 
        "nis:mail.aliases"
    ], 
    "allow_min_user": "no", 
    "allow_percent_hack": "yes"
}

A few things to notice:

  • in this case it's needed to "trim" everything (using the >- in YAML and -%} in Jinja), otherwise you'll get an error like:

    FAILED! => {"failed": true, "msg": "|combine expects dictionaries, got u\"  {u'...
    
  • obviously the {% if .. is far from bullet-proof

  • in the postfix case, val.split(',')|map('trim')|list could have been simplified to val.split(', '), but I wanted to point out the fact you will need to |list otherwise you'll get an error like:

    "|combine expects dictionaries, got u\"{u'...': <generator object do_map at ...
    

Hope this can help.

RichVel
  • 7,030
  • 6
  • 32
  • 48
bufh
  • 3,153
  • 31
  • 36
  • 1
    Much tanks for the `-%}` note! I have been fighting with this for weeks. – ceving Aug 03 '17 at 17:31
  • 3
    This is a great tip! Just one note, with Ansible 2.4.3 at least every line except the last needs `{%- ... -%}` with the dash on both the open and close bracket, otherwise the dict will be interpreted as a string – Michael Mar 05 '18 at 06:00
  • 1
    Thank you for this tip, the `>-` and `-%}` oddities would have been extremely time consuming to figure out. I owe you. Thanks. – vjt Dec 26 '20 at 21:51
  • What is the `[::2]` after `item.partition('=')`? I guess some index but I'm familiar with Python to tell. – gadamiak Oct 06 '21 at 15:16
  • @gadamiak: [`partition`](https://docs.python.org/3/library/stdtypes.html#str.partition) split the string with the guarantee you get two items in case the delimiter is not found and the obscure [`[::2]`](https://docs.python.org/3/library/stdtypes.html?highlight=slice#common-sequence-operations) syntax is a slice – bufh Oct 07 '21 at 15:50