2

I have two lists:

the_list:
  - { name: foo }
  - { name: bar }
  - { name: baz }

and I use a task which gets some value for its every element:

- name: Get values
  shell:
    magic_command {{ item.name }}
  with_items: the_list
  register: spells

from now on I can use the_list and its correspondig values together:

- name: Use both
  shell:
    do_something {{ item.0.name }} {{ item.1.stdout }}
  with_together:
    - "{{ the_list }}"
    - "{{ spells.results }}"

All works fine but it's uncomfortable to use with_together for many tasks and it'll be hard to read that code in a future so I would be more than happy to build merged_list from that which I can use in a simple way. Let say something like this:

merged_list:
 - { name: foo, value: jigsaw }
 - { name: bar, value: crossword }
 - { name: baz, value: monkey }

which makes the puzzle. Anyone can help ?

pawel7318
  • 3,383
  • 2
  • 28
  • 44

3 Answers3

2

I wrote two ansible filters to tackle this problem: zip and todict which are available in my repo at https://github.com/ahes/ansible-filter-plugins

Sample ansible playbook:

- hosts: localhost
  vars:
    users:
      - { name: user1 }
      - { name: user2 }
  tasks:
    - name: Get uids for users
      command: id -u {{ item.name }}
      register: uid_results
      with_items: users

    - set_fact:
        uids: "{{ uid_results.results | map(attribute='stdout') | todict('uid') }}"

    - set_fact:
        users: "{{ users | zip(uids) }}"

    - name: Show users with uids
      debug: var=users

Result would be:

TASK [Show users with uids] ****************************************************
ok: [localhost] => {
    "users": [
        {
            "name": "user1",
            "uid": "1000"
        },
        {
            "name": "user2",
            "uid": "2000"
        }
    ]
}
ahes
  • 31
  • 2
1

It may be an overkill but you should try to write a custom filter plugin.

Each time you iterates the_list you simple wants to add value to that dict {name: 'foo'} right?

After the update you just want that the new dict has the value like: {name: 'foo', value: 'jigsaw'}

The filter plugin for that it's pretty simple:

def foo(my_list, spells):
    try:
        aux = my_list

        for i in xrange(len(aux)):
            my_list[i].update(spells[i])

        return my_list

    except Exception, e:
        raise errors.AnsibleFilterError('Foo plugin error: %s, arguments=%s' % str(e), (my_list,spells) )

class FilterModule(object):

    def filters(self):
       return {
            'foo' : foo
       }

After adding this python code inside your plugins directory, you can easily call the foo plugin passing the spells list as a parameter:

 - name: Get values
    shell:
      magic_command {{ item.name }}
    with_items: the_list
    register: spells

  - name: Use both
    shell:
      do_something {{ item.name }} {{ item.value }}
    with_items:
      - "{{ the_list | foo(spells.results) }}"

NOTE: The python code it's just an example. Read the ansible documentations about developing filter plugins.

Bernardo Vale
  • 3,224
  • 4
  • 21
  • 34
1

I think I've found a cleaner, easier way to deal with these kind of things. Ansible runs all strings through jinja and then tries to load the result as yaml. This is because jinja only outputs strings so that allows it to load a data structure from a variable if there is one.

So any valid yaml in a string is loaded as a data structure -- so if you template valid yaml it will get loaded as data.

Trying to template correct yaml in the conventional, human form is tricky. But yaml loads all json. So, json is easier because there is no need to worry about whitespace. One bonus though, yaml does not care about extra commas, so that makes templating it easier.

In this case here is the playbook from the top answer rewritten to use this method.

- hosts: localhost
  vars:
    users:
      - { name: "user1" }
      - { name: "user2" }
  tasks:
    - name: Get uids for users
      command: id -u {{ item.name }}
      register: uid_results
      loop: "{{ users }}"

    - name: Show users with uids
      debug: var=users_with_uids
      vars:
        users_with_uids: |
          [
            {% for user_dict, uid in users | zip(uids) %}
              {
                "name": {{ user_dict['name'] | to_json }},
                "uid": {{ uid | to_json }},
              },
            {% endfor %}
          ]
        uids: "{{ uid_results.results | map(attribute='stdout') }}"

Notes

The | character tells yaml to load a multi-line string. Instead of putting the variables in quotes I use the to_json filter which will quote it and, more importantly, automatically escape anything in the variable that needs escaping. Also, remember commas after list or dictionary elements.

The results should be the same:

TASK [Show users with uids] ************************************************************
ok: [localhost] => {
    "users_with_uids": [
        {
            "name": "user1",
            "uid": "1000"
        },
        {
            "name": "user2",
            "uid": "1001"
        }
    ]
}

One more thing

I like to use the yaml callback especially for testing this. That way if my json-looking yaml doesn't get loaded I'll see a json-like structure. Otherwise it will come back in normal looking yaml if it was loaded. You can enable this by environment variable -- export ANSIBLE_STDOUT_CALLBACK=community.general.yaml.

Kevin Joyce
  • 91
  • 1
  • 4