ansible merge two lists based on an attribute - ansible

I have a role in my playbook that generates two lists using set_fact.
The two facts are used in different tasks.
I then need to merge them for a final task.
list1:
- name: alice
roles: ['role1', 'role2']
- name: bob
roles: ['role1']
list2:
- name: alice
roles: ['role3']
- name: charlie
roles: ['role2']
For my final task I need the output to be:
list3:
- name: alice
roles: ['role1', 'role2', 'role3']
- name: bob
roles: ['role1']
- name: charlie
roles: ['role2']

I asked about lists vs dictionaries in the comment because of the impact it will have on the solution. If you were to restructure your data like this:
dict1:
alice: ['role1', 'role2']
bob: ['role1']
dict2:
alice: ['role3']
charlie: ['role2']
Then your solution becomes:
- set_fact:
dict3: >-
{{
dict3|default([])|combine({
item: (dict1[item]|default([]) + dict2[item]|default([]))|unique
})
}}
loop: "{{ (dict1.keys()|list + dict2.keys()|list)|unique }}"
- debug:
var: dict3
Which outputs:
TASK [debug] **********************************************************************************************************************************************************************************
ok: [localhost] => {
"dict3": {
"alice": [
"role1",
"role2",
"role3"
],
"bob": [
"role1"
],
"charlie": [
"role2"
]
}
}
If you're stuck with using lists, we can improve upon the json_query solution that Zeitounator suggested:
- set_fact:
list3: >-
{{
list3|default([]) + [{
'name': item,
'roles': (list1|json_query('[?name==`' + item + '`].roles[]') + list2|json_query('[?name==`' + item + '`].roles[]'))|unique
}]
}}
loop: "{{ (list1|json_query('[].name') + list2|json_query('[].name'))|unique }}"
- debug:
var: list3
This produces your desired output:
TASK [debug] **********************************************************************************************************************************************************************************
ok: [localhost] => {
"list3": [
{
"name": "alice",
"roles": [
"role1",
"role2",
"role3"
]
},
{
"name": "bob",
"roles": [
"role1"
]
},
{
"name": "charlie",
"roles": [
"role2"
]
}
]
}

This is a solution in plain ansible. If it becomes out of control because of your datastructure growth, you should consider writing your own filter (example)
---
- name: demo playbook for deeps dictionary merge
hosts: localhost
gather_facts: false
vars:
list1:
- name: alice
roles: ['role1', 'role2']
- name: bob
roles: ['role1']
list2:
- name: alice
roles: ['role3']
- name: charlie
roles: ['role2']
tasks:
- debug:
msg: >-
roles for {{ item }}:
{{
(list1 | json_query("[?name == '" + item +"'].roles")).0 | default([])
+
(list2 | json_query("[?name == '" + item +"'].roles")).0 | default([])
}}
loop: >-
{{
(
list1 | json_query("[].name")
+
list2 | json_query("[].name")
)
| unique
}}
Which gives:
PLAY [demo playbook for deeps dictionary merge] ************************************************************************************************************************************************************************************************************************
TASK [debug] ************************************************************************************************************************************************************************************************************************************************************
Wednesday 24 April 2019 16:23:07 +0200 (0:00:00.046) 0:00:00.046 *******
ok: [localhost] => (item=alice) => {
"msg": "roles for alice: ['role1', 'role2', 'role3']"
}
ok: [localhost] => (item=bob) => {
"msg": "roles for bob: ['role1']"
}
ok: [localhost] => (item=charlie) => {
"msg": "roles for charlie: ['role2']"
}

Related

Ansible: Merge 2 lists using common key

I have a use case where I need to merge 2 lists on common key name using Ansible.
List1:
{
"poc-cu2": [
"40:A6:B7:5E:22:11",
"40:A6:B7:5E:22:22"
],
"test2211": [
"40:A6:B7:5E:33:11",
"40:A6:B7:5E:33:22"
],
"test2244": [
"40:A6:B7:5E:22:45",
"40:A6:B7:5E:22:46"
]
}
List2:
{
"poc-cu2": [
"root",
"9WKA3KK3XN39",
"9.3.13.44"
],
"test2211": [
"root2211",
"221122112211",
"9.3.13.82"
]
}
Expected:
List3:
{
"poc-cu2": [
"root",
"9WKA3KK3XN39",
"9.3.13.44",
"40:A6:B7:5E:22:11",
"40:A6:B7:5E:22:22"
],
"test2211": [
"root2211",
"221122112211",
"9.3.13.82",
"40:A6:B7:5E:33:11",
"40:A6:B7:5E:33:22"
]
}
I got how to merge 2 lists using unique key but on my case I need to merge only on common key, please suggest.
You can get most of what you want using the combine filter, like this:
- hosts: localhost
gather_facts: false
tasks:
- set_fact:
dict3: "{{ dict1|combine(dict2, list_merge='append') }}"
- debug:
var: dict3
This will produce:
TASK [debug] *******************************************************************
ok: [localhost] => {
"dict3": {
"poc-cu2": [
"40:A6:B7:5E:22:11",
"40:A6:B7:5E:22:22",
"root",
"9WKA3KK3XN39",
"9.3.13.44"
],
"test2211": [
"40:A6:B7:5E:33:11",
"40:A6:B7:5E:33:22",
"root2211",
"221122112211",
"9.3.13.82"
],
"test2244": [
"40:A6:B7:5E:22:45",
"40:A6:B7:5E:22:46"
]
}
}
If you want the final result to consist of only the keys common to both
dictionaries it gets a little trickier, but this seems to work:
- hosts: localhost
gather_facts: false
tasks:
- set_fact:
dict3: "{{ dict3|combine({item: dict1[item] + dict2[item]}) }}"
when: item in dict2
loop: "{{ dict1.keys() }}"
vars:
dict3: {}
- debug:
var: dict3
Which produces:
TASK [debug] *******************************************************************
ok: [localhost] => {
"dict3": {
"poc-cu2": [
"40:A6:B7:5E:22:11",
"40:A6:B7:5E:22:22",
"root",
"9WKA3KK3XN39",
"9.3.13.44"
],
"test2211": [
"40:A6:B7:5E:33:11",
"40:A6:B7:5E:33:22",
"root2211",
"221122112211",
"9.3.13.82"
]
}
}
The above works by iterating over the keys in dict1, and for each
key from dict1 that also exists in dict2, we synthesize a new
dictionary containing the corresponding values from both dict1 and dict2 and then merge it into our final dictionary using the combine filter.
Given the data
dict1:
poc-cu2:
- 40:A6:B7:5E:22:11
- 40:A6:B7:5E:22:22
test2211:
- 40:A6:B7:5E:33:11
- 40:A6:B7:5E:33:22
test2244:
- 40:A6:B7:5E:22:45
- 40:A6:B7:5E:22:46
dict2:
poc-cu2:
- root
- 9WKA3KK3XN39
- 9.3.13.44
test2211:
- root2211
- '221122112211'
- 9.3.13.82
Iteration is not needed. Put the declarations below as appropriate. The dictionary dict_cmn keeps the merged common attributes of dict1 and dict2
dict_1_2: "{{ dict1|combine(dict2, list_merge='append') }}"
keys_cmn: "{{ dict1.keys()|intersect(dict2.keys()) }}"
vals_cmn: "{{ keys_cmn|map('extract', dict_1_2) }}"
dict_cmn: "{{ dict(keys_cmn|zip(vals_cmn)) }}"
give
dict_1_2:
poc-cu2:
- 40:A6:B7:5E:22:11
- 40:A6:B7:5E:22:22
- root
- 9WKA3KK3XN39
- 9.3.13.44
test2211:
- 40:A6:B7:5E:33:11
- 40:A6:B7:5E:33:22
- root2211
- '221122112211'
- 9.3.13.82
test2244:
- 40:A6:B7:5E:22:45
- 40:A6:B7:5E:22:46
keys_cmn:
- poc-cu2
- test2211
vals_cmn:
- - 40:A6:B7:5E:22:11
- 40:A6:B7:5E:22:22
- root
- 9WKA3KK3XN39
- 9.3.13.44
- - 40:A6:B7:5E:33:11
- 40:A6:B7:5E:33:22
- root2211
- '221122112211'
- 9.3.13.82
dict_cmn:
poc-cu2:
- 40:A6:B7:5E:22:11
- 40:A6:B7:5E:22:22
- root
- 9WKA3KK3XN39
- 9.3.13.44
test2211:
- 40:A6:B7:5E:33:11
- 40:A6:B7:5E:33:22
- root2211
- '221122112211'
- 9.3.13.82

Why is ansible creating a new host scoped list variables for each host?

I need to create a variable of ip addresses that are derived from the run state of all hosts. I expect to insert a new string value and have it appended to the list.
When I run the following ansible-playbook, it creates what looks to be a new list instance for each host instead of modifying the playbook level vars.
My understanding is the set_fact below should concatenate the lists and assign them back to the play scoped var ip_list_from_terraform. Why am I getting host scoped results?
---
- name: Bootstrap nodes
hosts: all
become: true
gather_facts: true
vars:
ip_list_from_terraform: ['Verify']
tasks:
- name: assign a list of all the physical network private IPs
set_fact:
ip_list_from_terraform: "{{ ip_list_from_terraform + [ item ] }}"
with_items: " {{ hostvars[inventory_hostname]['ansible_' + ansible_default_ipv4['interface']]['ipv4']['address'] }} "
register: ip_list_from_terraform_list
- name: Debug global var
debug:
var: ip_list_from_terraform
- name: Debug register result
debug:
var: ip_list_from_terraform_list
Expected:
ok: [shrimp-master-0] => {
"ip_list_from_terraform": [
"Verify",
"10.0.2.41",
"10.0.2.172",
"10.0.2.33",
"10.0.2.215",
"10.0.2.131",
"10.0.2.168",
"10.0.2.118"
]
}
Actual:
TASK [Debug global var] ********************************************************************************************************************************************************************************************************************
ok: [shrimp-master-0] => {
"ip_list_from_terraform": [
"Verify",
"10.0.2.12"
]
}
ok: [shrimp-master-1] => {
"ip_list_from_terraform": [
"Verify",
"10.0.2.33"
]
}
ok: [shrimp-master-2] => {
"ip_list_from_terraform": [
"Verify",
"10.0.2.215"
]
}
ok: [shrimp-worker-0-super-wallaby] => {
"ip_list_from_terraform": [
"Verify",
"10.0.2.131"
]
}
ok: [shrimp-gpu-worker-0-settled-wombat] => {
"ip_list_from_terraform": [
"Verify",
"10.0.2.151"
]
}
Let's simplify the case. Given the inventory
shell> cat hosts
host1 test_ip=10.0.2.41
host2 test_ip=10.0.2.172
host3 test_ip=10.0.2.33
the playbook
- hosts: host1,host2,host3
vars:
ip_list_from_terraform: ['Verify']
tasks:
- set_fact:
ip_list_from_terraform: "{{ ip_list_from_terraform + [ item ] }}"
with_items: "{{ hostvars[inventory_hostname]['test_ip'] }}"
- debug:
var: ip_list_from_terraform
gives
ok: [host2] =>
ip_list_from_terraform:
- Verify
- 10.0.2.172
ok: [host1] =>
ip_list_from_terraform:
- Verify
- 10.0.2.41
ok: [host3] =>
ip_list_from_terraform:
- Verify
- 10.0.2.33
As a side-note, with_items is needed because the argument is a string. loop would crash with the error 'Invalid data passed to ''loop'', it requires a list,.
Q: "set_fact should concatenate the lists"
A: Run once and loop ansible_play_hosts. For example
- set_fact:
ip_list_from_terraform: "{{ ip_list_from_terraform +
[hostvars[item]['test_ip']] }}"
loop: "{{ ansible_play_hosts }}"
run_once: true
gives
ip_list_from_terraform:
- Verify
- 10.0.2.41
- 10.0.2.172
- 10.0.2.33

Ansible: create/concatenate list from individual items plus list from a variable

lets assume I have this:
list1:
- "item1"
- "item2"
And now I want to build another list by using the first one
list2:
- "item3"
- "item4"
+ "{{ list1 }}"
This doesn't work, what works is:
list2: "{{ ['item3', 'item4'] + list2 }}"
But I find it hard to read and hard to extend with new items like item5 etc.
Is there a way I can create list2 with something similar to the first version?
***** Update *****
The specific situation I want to create this list is in a host_var file, let's say in host_var/my_server.yml
You can use another var
---
- hosts: localhost
gather_facts: false
vars:
list1:
- "item1"
- "item2"
list2:
- "item3"
- "item4"
list4: "{{ list1 | default([]) + list2 | default([]) + list3 | default([]) }}"
tasks:
- name: debug
debug:
msg: "{{ list4 }}"
And the output:
TASK [debug] ****************************************************
ok: [localhost] => {
"msg": [
"item1",
"item2",
"item3",
"item4"
]
}

anible loops and subdictionaries

I'm trying to loop through some parameters in Ansible using a jinja template and a yaml variable file.
The content of the jinja template isn't important here, but I can share a version of the playbook and the yaml variable file below.
What I want to be able to do is generate 4 files, each of them with the name of the peer in the filename. At the moment the playbook works, but generates one file called test-peers. I've tried with_nested and with_subelements - subelements only seems to want to work with a list rather than a dictionary, and I can't find a way to list the values within peers with with_nested. Is there a solution to this?
The YAML file:
customer: test_customer
routers:
router1:
router_number: 01
router_model: ISR4431
peers:
America:
hostname: America
priority: Primary
number: 1
Asia:
hostname: Asia
priority: Backup
number: 2
router2:
router_number: 02
router_model: ISR4431
peers:
America:
hostname: America
priority: Primary
number: 1
Asia:
hostname: Asia
priority: Backup
number: 2
The task in the playbook:
tasks:
- name: WAN gateway testing
template:
src: my.template.j2
dest: "{{ 'config/' + customer + '-' + item.1 }}"
with_nested:
- "{{ routers }}"
- peers
You have to transform your data structure to something you can use the intended way. Here is one possible way to do it in the below playbook:
---
- hosts: localhost
gather_facts: false
vars:
customer: test_customer
# Your orig data on a single json line for compactness
routers: {"router1": {"router_number": 1, "router_model": "ISR4431", "peers": {"America": {"hostname": "America", "priority": "Primary", "number": 1}, "Asia": {"hostname": "Asia", "priority": "Backup", "number": 2}}}, "router2": {"router_number": 2, "router_model": "ISR4431", "peers": {"America": {"hostname": "America", "priority": "Primary", "number": 1}, "Asia": {"hostname": "Asia", "priority": "Backup", "number": 2}}}}
tasks:
- name: Transform our dict to something easier to loop with subelements
vars:
current_router:
router_number: "{{ item.router_number }}"
router_model: "{{ item.router_model }}"
peers: "{{ item.peers | dict2items | map(attribute='value') }}"
set_fact:
routers_transformed: "{{ routers_transformed | default([]) + [current_router] }}"
loop: "{{ routers | dict2items | map(attribute='value') }}"
- name: Show the transformed data
debug:
var: routers_transformed
- name: Create a file name for each router/peer association in the structure as per requirement
debug:
msg: "Filename would be config/{{ customer }}/router{{ item.0.router_number }}/{{ item.1.hostname}}"
loop: "{{ routers_transformed | subelements('peers') }}"
loop_control:
label: "router {{ item.0.router_number }} for peer {{ item.1.hostname }}"
which gives:
PLAY [localhost] ***********************************************************************************************************************************************************************************************************************
TASK [Transform our dict to something easier to loop with subelements] *****************************************************************************************************************************************************************
ok: [localhost] => (item={'router_number': 1, 'router_model': 'ISR4431', 'peers': {'America': {'hostname': 'America', 'priority': 'Primary', 'number': 1}, 'Asia': {'hostname': 'Asia', 'priority': 'Backup', 'number': 2}}})
ok: [localhost] => (item={'router_number': 2, 'router_model': 'ISR4431', 'peers': {'America': {'hostname': 'America', 'priority': 'Primary', 'number': 1}, 'Asia': {'hostname': 'Asia', 'priority': 'Backup', 'number': 2}}})
TASK [Show the transformed data] *******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"routers_transformed": [
{
"peers": [
{
"hostname": "America",
"number": 1,
"priority": "Primary"
},
{
"hostname": "Asia",
"number": 2,
"priority": "Backup"
}
],
"router_model": "ISR4431",
"router_number": "1"
},
{
"peers": [
{
"hostname": "America",
"number": 1,
"priority": "Primary"
},
{
"hostname": "Asia",
"number": 2,
"priority": "Backup"
}
],
"router_model": "ISR4431",
"router_number": "2"
}
]
}
TASK [Create a file name for each router/peer association in the structure as per requirement] *****************************************************************************************************************************************
ok: [localhost] => (item=router 1 for peer America) => {
"msg": "Filename would be config/test_customer/router1/America"
}
ok: [localhost] => (item=router 1 for peer Asia) => {
"msg": "Filename would be config/test_customer/router1/Asia"
}
ok: [localhost] => (item=router 2 for peer America) => {
"msg": "Filename would be config/test_customer/router2/America"
}
ok: [localhost] => (item=router 2 for peer Asia) => {
"msg": "Filename would be config/test_customer/router2/Asia"
}
PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Q: "Restructure the original yaml file."
A: Use template. For example
shell> cat templates/routers.j2
customer: {{ customer }}
routers:
{{ routers2|to_nice_yaml|indent(2) }}
The tasks below do the job
- set_fact:
routers2: "{{ routers2|default({})|
combine({item.0.key: item.0.value|
combine({'peers': item.1})}) }}"
loop: "{{ routers|dict2items|zip(peers)|list }}"
vars:
peers: "{{ routers|json_query('*.peers')|
map('dict2items')|
map('json_query', '[].value')|list }}"
- template:
src: routers.j2
dest: routers.yml
shell> cat routers.yml
customer: test_customer
routers:
router1:
peers:
- hostname: America
number: 1
priority: Primary
- hostname: Asia
number: 2
priority: Backup
router_model: ISR4431
router_number: '01'
router2:
peers:
- hostname: America
number: 1
priority: Primary
- hostname: Asia
number: 2
priority: Backup
router_model: ISR4431
router_number: '02'
Then the loop below
- include_vars: routers.yml
- debug:
msg: >-
{{ item.0.router_number }}
{{ item.0.router_model }}
{{ item.1.hostname }}
{{ item.1.number }}
{{ item.1.priority }}
with_subelements:
- "{{ routers }}"
- peers
gives (abridged)
msg: 01 ISR4431 America 1 Primary
msg: 01 ISR4431 Asia 2 Backup
msg: 02 ISR4431 America 1 Primary
msg: 02 ISR4431 Asia 2 Backup

All combination of 2 lists in ansible

I have 2 variables as a list in ansible
host_list:
- 1.1.1.1
- 2.2.2.2
port_list:
- 443
- 80
and I want to get the 3rd variable as a list of lists:
all_comb = [[1.1.1.1, 443], [1.1.1.1, 80], [2.2.2.2, 443], [2.2.2.2, 80]]
How can i get it in Ansible?
Use product filter
all_comb: "{{ host_list|product(port_list) }}"
gives
all_comb:
- [1.1.1.1, 443]
- [1.1.1.1, 80]
- [2.2.2.2, 443]
- [2.2.2.2, 80]
you can use the with_nested or a query('nested', listA, listB) loop, see both implementations below:
with_nested:
- name: merge lists
set_fact:
merged_list: "{{ merged_list|default([]) + [item] }}"
with_nested:
- "{{ host_list }}"
- "{{ port_list }}"
- name: display result
debug:
var: merged_list
with query:
- name: merge lists
set_fact:
merged_list: "{{ merged_list|default([]) + [item] }}"
loop: "{{ query('nested', host_list, port_list) }}"
- name: display result
debug:
var: merged_list
result of the latter:
PLAY [localhost] ******************************************************************************************************************************************************************************************************
TASK [merge lists] ****************************************************************************************************************************************************************************************************
ok: [localhost] => (item=[u'1.1.1.1', 443])
ok: [localhost] => (item=[u'1.1.1.1', 80])
ok: [localhost] => (item=[u'2.2.2.2', 443])
ok: [localhost] => (item=[u'2.2.2.2', 80])
TASK [display result] *************************************************************************************************************************************************************************************************
ok: [localhost] => {
"merged_list": [
[
"1.1.1.1",
443
],
[
"1.1.1.1",
80
],
[
"2.2.2.2",
443
],
[
"2.2.2.2",
80
]
]
}
cheers

Resources