If you really want to append to content, you will need to use the set_fact module. But if you just want to use the merged lists it is as easy as this:
{{ list1 + list2 }}
With set_fact it would look like this:
- set_fact:
list_merged: "{{ list1 + list2 }}"
NOTE: If you need to do additional operations on the concatenated lists be sure to group them like so:
- set_fact:
list_merged: "{{ (list1 + list2) | ... }}"
Answer from udondan on Stack OverflowIf you really want to append to content, you will need to use the set_fact module. But if you just want to use the merged lists it is as easy as this:
{{ list1 + list2 }}
With set_fact it would look like this:
- set_fact:
list_merged: "{{ list1 + list2 }}"
NOTE: If you need to do additional operations on the concatenated lists be sure to group them like so:
- set_fact:
list_merged: "{{ (list1 + list2) | ... }}"
The following worked for me with Ansible 2.1.2.0:
- name: Define list of mappings
set_facts:
something:
- name: bla
mode: 1
- name: Append list with additional mapping
set_facts:
something: "{{ something + [{'name': 'blabla', 'mode': 1}] }}"
Combine Lists of Objects in Ansible - DevOps Stack Exchange
Combining lists - Ansible Project - Ansible
merge - Ansible - How to combine multiple disparate lists into to use in tasks loop - Stack Overflow
ansible - How to combine two lists together? - Stack Overflow
Hi all,
Looking for some assistance please...
I am attempting to not only append two lists together ( varCombined: "{{ var1 + var2}}" ) but also merge the lists so that there are no duplicates.
My playbook is as follows:
- name: testing
hosts: localhost
tasks:
- name: set vars
set_fact:
paths_blah:
- includeFilePath: "/path1"
excludeFilePaths:
- "/path1/aaa"
- "/path1/bbb"
- includeFilePath: "/path2"
excludeFilePaths:
- "/path2/aaa"
paths_blob:
- includeFilePath: "/path3"
excludeFilePaths:
- "/path3/aaa"
- "/path3/bbb"
- includeFilePath: "/path1"
excludeFilePaths:
- "/path1/zzz"
paths_berry:
- includeFilePath: "/path4"
excludeFilePaths:
- "/path4/aaa"
- "/path4/bbb"
- includeFilePath: "/path2"
excludeFilePaths:
- "/path1/zzz"
- set_fact:
paths_combined: "{{ paths_blah + paths_blob + paths_berry }}"
- name: print vars paths_combined
debug:
var: paths_combined.. when run, the output of which is:
PLAY [print a message to screen] *****************************************************************************************************************
<snip>
TASK [print vars paths_combined] *****************************************************************************************************************
ok: [localhost] => {
"paths_combined": [
{
"excludeFilePaths": [
"/path1/aaa",
"/path1/bbb"
],
"includeFilePath": "/path1"
},
{
"excludeFilePaths": [
"/path2/aaa"
],
"includeFilePath": "/path2"
},
{
"excludeFilePaths": [
"/path3/aaa",
"/path3/bbb"
],
"includeFilePath": "/path3"
},
{
"excludeFilePaths": [
"/path1/zzz"
],
"includeFilePath": "/path1"
},
{
"excludeFilePaths": [
"/path4/aaa",
"/path4/bbb"
],
"includeFilePath": "/path4"
},
{
"excludeFilePaths": [
"/path1/zzz"
],
"includeFilePath": "/path2"
}
]
}
.. you can see the lists are only appended, not merged: path1 and path2 appear twice in the list.
The desired output is a list with only unique items, merged rather than appended. So taking the above example the desired output would be:
paths_combined:
- includeFilePath: "/path1"
excludeFilePaths:
- "/path1/aaa"
- "/path1/bbb"
- "/path1/zzz"
- includeFilePath: "/path2"
excludeFilePaths:
- "/path2/aaa"
- "/path2/zzz"
- includeFilePath: "/path3"
excludeFilePaths:
- "/path3/aaa"
- "/path3/bbb"
- includeFilePath: "/path4"
excludeFilePaths:
- "/path4/aaa"
- "/path4/bbb"Can anybody suggest how I may accomplish this?
Thanks!
loop requires a valid list. But we got here is,
[({'i_am': 'sam', 'eggs_ham': '456'}, AnsibleUndefined),
({'i_am': 'sam', 'eggs_ham': 'ham'}, {'transport': 'train'}),
({'i_am': 'eggs', 'eggs_ham': '456'}, AnsibleUndefined)]
Since no value is assigned to transport_fact_results when common_thing_2 is not defined.
if you remove the when condition it will work.
CODE:
- hosts: localhost
vars:
default_thing_1: 123
default_thing_2: 456
default_transport: train
list_of_things:
- name: foo
common_thing_1: "sam"
- name: bar
common_thing_1: "sam"
common_thing_2: "ham"
- name: biz
common_thing_1: "eggs"
tasks:
- name: Set the Things Facts
set_fact:
thing:
i_am: "{{ item.common_thing_1 | default(default_thing_1) }}"
eggs_ham: "{{ item.common_thing_2 | default(default_thing_2) }}"
loop: "{{ list_of_things }}"
register: things_fact_results
- name: Set Optional Things Facts
set_fact:
thing:
transport: "{{ item.transport | default(default_transport) }}"
when: "item.common_thing_2 is defined and item.common_thing_2 == 'ham'"
loop: "{{ list_of_things }}"
register: transport_fact_results
- name: Debug the Facts
debug:
msg: "{{ item.0 | combine(item.1|default({})) }}"
loop: "{{
things_fact_results.results |
map(attribute='ansible_facts.thing') | list |
zip(
transport_fact_results.results |
map(attribute='ansible_facts.thing') | list
) | list
}}"
OUTPUT:
PLAY [localhost] ************************************************************************************************************************************************************************************************
TASK [Gathering Facts] ******************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Set the Things Facts] *************************************************************************************************************************************************************************************
ok: [localhost] => (item={u'common_thing_1': u'sam', u'name': u'foo'})
ok: [localhost] => (item={u'common_thing_1': u'sam', u'common_thing_2': u'ham', u'name': u'bar'})
ok: [localhost] => (item={u'common_thing_1': u'eggs', u'name': u'biz'})
TASK [Set Optional Things Facts] ********************************************************************************************************************************************************************************
ok: [localhost] => (item={u'common_thing_1': u'sam', u'name': u'foo'})
ok: [localhost] => (item={u'common_thing_1': u'sam', u'common_thing_2': u'ham', u'name': u'bar'})
ok: [localhost] => (item={u'common_thing_1': u'eggs', u'name': u'biz'})
TASK [Debug the Facts] ******************************************************************************************************************************************************************************************
ok: [localhost] => (item=[{u'eggs_ham': u'456', u'i_am': u'sam'}, {u'transport': u'train'}]) => {
"msg": {
"eggs_ham": "456",
"i_am": "sam",
"transport": "train"
}
}
ok: [localhost] => (item=[{u'eggs_ham': u'ham', u'i_am': u'sam'}, {u'transport': u'train'}]) => {
"msg": {
"eggs_ham": "ham",
"i_am": "sam",
"transport": "train"
}
}
ok: [localhost] => (item=[{u'eggs_ham': u'456', u'i_am': u'eggs'}, {u'transport': u'train'}]) => {
"msg": {
"eggs_ham": "456",
"i_am": "eggs",
"transport": "train"
}
}
PLAY RECAP ******************************************************************************************************************************************************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
So the way I figured it out was to put the when condition into Jinja.
- hosts: localhost
vars:
default_thing_1: 123
default_thing_2: 456
default_transport: train
list_of_things:
- name: foo
common_thing_1: "sam"
- name: bar
common_thing_1: "sam"
common_thing_2: "ham"
- name: biz
common_thing_1: "eggs"
tasks:
- name: Set the Things Facts
set_fact:
thing:
i_am: "{{ item.common_thing_1 | default(default_thing_1) }}"
eggs_ham: "{{ item.common_thing_2 | default(default_thing_2) }}"
loop: "{{ list_of_things }}"
register: things_fact_results
- name: Set Optional Things Facts
set_fact:
thing: |
{% if item.common_thing_2 is defined and item.common_thing_2 == 'ham' %}
{ "transport": "{{ item.transport | default(default_transport) }}" }
{% else %}
{}
{% endif %}
loop: "{{ list_of_things }}"
register: transport_fact_results
- name: Debug the Facts
debug:
msg: "{{ item.0 | combine(item.1) }}"
loop: "{{
things_fact_results.results |
map(attribute='ansible_facts.thing') | list |
zip(
transport_fact_results.results |
map(attribute='ansible_facts.thing') | list
) | list
}}"
This is kinda sloppy but does allow me to specify the else condition to set the result over every iteration to something instead of AnsibleUndefined.
I don't really like this solution but it works.
$ ansible-playbook playbook.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [localhost] *******************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************
ok: [localhost]
TASK [Set the Things Facts] ********************************************************************************************************************************************************
ok: [localhost] => (item={'name': 'foo', 'common_thing_1': 'sam'})
ok: [localhost] => (item={'name': 'bar', 'common_thing_1': 'sam', 'common_thing_2': 'ham'})
ok: [localhost] => (item={'name': 'biz', 'common_thing_1': 'eggs'})
TASK [Set Optional Things Facts] ***************************************************************************************************************************************************
ok: [localhost] => (item={'name': 'foo', 'common_thing_1': 'sam'})
ok: [localhost] => (item={'name': 'bar', 'common_thing_1': 'sam', 'common_thing_2': 'ham'})
ok: [localhost] => (item={'name': 'biz', 'common_thing_1': 'eggs'})
TASK [Debug the Facts] *************************************************************************************************************************************************************
ok: [localhost] => (item=[{'i_am': 'sam', 'eggs_ham': '456'}, {}]) => {
"msg": {
"eggs_ham": "456",
"i_am": "sam"
}
}
ok: [localhost] => (item=[{'i_am': 'sam', 'eggs_ham': 'ham'}, {'transport': 'train'}]) => {
"msg": {
"eggs_ham": "ham",
"i_am": "sam",
"transport": "train"
}
}
ok: [localhost] => (item=[{'i_am': 'eggs', 'eggs_ham': '456'}, {}]) => {
"msg": {
"eggs_ham": "456",
"i_am": "eggs"
}
}
PLAY RECAP *************************************************************************************************************************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now the transport key is defined in the one object but not the other. So I can pass the item off to a complex object that's part of an API call.
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"
}
]
}
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.