Invert a dictionary with list values in ansible - ansible

How do I convert a dictionary like below
{
"host1": ["tag1", "tag2"],
"host2": ["tag1"]
}
to below in ansible:
{
"tag1": ["host1","host2"],
"tag2": ["host1"]
}
I have been trying to do this but got stuck due to the value being a list.

The playbook
- hosts: localhost
vars:
dict1:
host1: [tag1, tag2]
host2: [tag1]
tasks:
- set_fact:
dict2: "{{ dict2|default({})|combine({item: list_of_hosts}) }}"
loop: "{{ dict1.values()|flatten|unique }}"
vars:
list_of_hosts: "{{ dict1|
dict2items|
selectattr('value', 'contains', item)|
map(attribute='key')|
list }}"
- debug:
var: dict2
gives
dict2:
tag1: [host1, host2]
tag2: [host1]
Optionally, use json_query
vars:
list_of_hosts: "{{ dict1|dict2items|json_query(_query) }}"
_query: '[?value.contains(#, `{{ item }}`)].key'

Related

How to parse text with ansible using a nested list/dict?

I have a text file that needs to be parsed so i can use it via a REST API with ansible.
10.0.0.0/16 Building-A
10.1.0.0/16 Building-A
10.2.0.0/16 Building-B
10.3.0.0/16 Building-B
I need to convert this text to something like this:
{
"parsed":[
{
"Building-A":[
"10.0.0.0/16",
"10.1.0.0/16"
]
},
{
"Building-B":[
"10.2.0.0/16",
"10.3.0.0/16"
]
}
]
}
Currently i run the following playbook to test the textparsing, without success. The list created is not unique.
- name: Test
hosts: localhost
tasks:
- name: Combine
ansible.builtin.set_fact:
parsed: "{{ (parsed | default([])) | union( [{item.split()[1]: item.split()[0] }] ) }}"
loop: "{{ lookup('file','hostgroups.txt').strip().splitlines() }}"
- name: Debug
ansible.builtin.debug:
var: parsed
ok: [localhost] => {
"parsed": [
{
"Building-A": "10.0.0.0/16"
},
{
"Building-A": "10.1.0.0/16"
},
{
"Building-B": "10.2.0.0/16"
},
{
"Building-B": "10.3.0.0/16"
}
]
}
Thanks for the tip with the helper variables and the groupby filter. Here is the final playbook:
- name: Test
hosts: localhost
# strategy: free
tasks:
- name: Get List
ansible.builtin.set_fact:
parsed_list: "{{ parsed_list | default([]) + [_item] }}"
loop: "{{ lookup('file','hostgroups.txt').strip().splitlines() }}"
vars:
_list: "{{ item.split() }}"
_item: "{{ {'Name': _list[1], 'Subnet': _list[0] } }}"
- name: Debug parsed_list
ansible.builtin.debug:
var: parsed_list
- name: Group parsed_list
ansible.builtin.set_fact.set_fact:
parsed_group: "{{ parsed_group | default([]) + [{_key: _value}] }}"
loop: "{{ parsed_list | groupby('Name') }}"
vars:
_key: "{{ item.0 }}"
_value: "{{ item.1 | map(attribute='Subnet') | list }}"
- name: Debug parsed_group
ansible.builtin.debug:
var: parsed_group
Parse the content, e.g.
- set_fact:
parsed_list: "{{ parsed_list|d([]) + [_item] }}"
loop: "{{ lookup('file','hostgroups.txt').splitlines() }}"
vars:
_array: "{{ item.split() }}"
_item: "{{ {'building': _array.1, 'ip': _array.0} }}"
gives
parsed_list:
- building: Building-A
ip: 10.0.0.0/16
- building: Building-A
ip: 10.1.0.0/16
- building: Building-B
ip: 10.2.0.0/16
- building: Building-B
ip: 10.3.0.0/16
Then, use filter groupby and create the list
- set_fact:
parsed: "{{ parsed|d([]) + [{_key: _val}] }}"
loop: "{{ parsed_list|groupby('building') }}"
vars:
_key: "{{ item.0 }}"
_val: "{{ item.1|map(attribute='ip')|list }}"
gives
parsed:
- Building-A:
- 10.0.0.0/16
- 10.1.0.0/16
- Building-B:
- 10.2.0.0/16
- 10.3.0.0/16
In some cases, a dictionary might be a better structure, e.g.
- set_fact:
parsed_dict: "{{ parsed_dict|d({})|combine({_key: _val}) }}"
loop: "{{ parsed_list|groupby('building') }}"
vars:
_key: "{{ item.0 }}"
_val: "{{ item.1|map(attribute='ip')|list }}"
gives
parsed_dict:
Building-A:
- 10.0.0.0/16
- 10.1.0.0/16
Building-B:
- 10.2.0.0/16
- 10.3.0.0/16

Exclude AnsibleUndefined from a list

Relevant part of task:
- name: Get join-token for worker nodes
set_fact:
join_token_worker: "{{ ansible_play_hosts_all | map('extract', hostvars, ['result', 'swarm_facts', 'JoinTokens', 'Worker']) }}"
run_once: true
- debug:
msg: "{{ join_token_worker }}"
run_once: true
And the output looks like this:
TASK [deploy_docker : Get join-token for worker nodes] ********************************************************************************************************************************************************************************************************************************************************
ok: [eu-central-1a-01]
TASK [deploy_docker : debug] **********************************************************************************************************************************************************************************************************************************************************************************
ok: [eu-central-1a-01] => {
"msg": "[AnsibleUndefined, AnsibleUndefined, AnsibleUndefined, 'SWMTKN-1-2xgm1l987hh18gbq0tc8vrwn209qlxge6nkwdq4pof2zd4e2vs-7f7ig2ama5amqq8iuii4o8xxo', AnsibleUndefined, AnsibleUndefined, 'SWMTKN-1-2xgm1l987hh18gbq0tc8vrwn209qlxge6nkwdq4pof2zd4e2vs-7f7ig2ama5amqq8iuii4o8xxo', AnsibleUndefined, AnsibleUndefined]"
}
I want to extract the token from this list - but I don't know how to filter out AnsibleUndefined value. I am using docker_swarm module to get result.
Let's minimize the case. For example, the playbook
- hosts: h1,h2,h3
gather_facts: false
vars:
d2:
h1:
k1:
k2: v1
h2:
k1:
kX: v2
h3:
k1:
k2: v3
tasks:
- debug:
msg: "{{ ansible_play_hosts_all|
map('extract', d2, ['k1', 'k2'])|
list }}"
run_once: true
gives
msg: '[''v1'', Undefined, ''v3'']'
There are more options for how to "Exclude AnsibleUndefined from a list".
Use select (credit #mdaniel)
- debug:
msg: "{{ ansible_play_hosts_all|
map('extract', d2, ['k1', 'k2'])|
select|
list }}"
run_once: true
gives
msg:
- v1
- v3
Use regex_replace
- debug:
msg: "{{ ansible_play_hosts_all|
map('extract', d2, ['k1', 'k2'])|
map('regex_replace', '(.*)', '\\1')|
list }}"
run_once: true
gives
msg:
- v1
- ''
- v3
If you want to create a dictionary iterate the hosts. For example
- set_fact:
worker_dict: "{{ worker_dict|default({})|
combine({item: d2[item].k1.k2|default('')}) }}"
loop: "{{ ansible_play_hosts_all }}"
run_once: true
gives
worker_dict:
h1: v1
h2: ''
h3: v3

Ansible: How to filter dict2items and run playbook only for the matched values

I have a dict playbook which looks like this:
x_php_versions_installed:
ea-php71:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
- pecl-memcached
- pecl-imagick
ea-php72:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
- pecl-imagick
I would like to filter them, to write me each item.value which contains 'ea' string but not everything else. My task looks like this:
- name: Write out only the ea packages
debug:
msg: '{{ item.value }}'
when: item.value | selectattr(item.value, 'contains', 'ea')
loop: '{{ x_php_versions_installed | dict2items }}
But it does not work, because it will list all of the packages, not only the ea ones. The expected answer should look like this:
...
"msg": [
"ea-php71-php-bcmath",
"ea-php71-php-xmlrpc",
"ea-php71-php-zip"
]
...
"msg": [
"ea-php72-php-cli",
"ea-php72-php-common",
"ea-php72-php-curl"
]
...
Another possibility is to filter out the 'pecl' string, it will gave me the same result and it also works fine.
Q: "Filter item.value which contains ea string."
A: The task below does the job
- debug:
msg: "{{ item.value|select('match','^ea-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
gives (abridged)
msg:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
msg:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
Note: The test match by default "succeeds if it finds the pattern at the beginning of the string". The task below gives the same result
- debug:
msg: "{{ item.value|select('match', 'ea-')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
Q: "Filter out the pecl string."
A: Change the filter to reject and fit the regex. For example, the task below gives the same result
- debug:
msg: "{{ item.value|reject('match','^pecl-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
Notes:
Select the lists without iteration. Declare the variables
x_php_versions_installed_keys: "{{ x_php_versions_installed.keys()|list }}"
x_php_versions_installed_ea_vals: "{{ x_php_versions_installed|dict2items|
map(attribute='value')|
map('select', 'match', 'ea-')|list }}"
x_php_versions_installed_ea: "{{ dict(x_php_versions_installed_keys|
zip(x_php_versions_installed_ea_vals)) }}"
gives
x_php_versions_installed_ea:
ea-php71:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
ea-php72:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
Example of a complete playbook for testing
- hosts: localhost
vars:
x_php_versions_installed:
ea-php71:
- ea-php71-php-bcmath
- ea-php71-php-xmlrpc
- ea-php71-php-zip
- pecl-memcached
- pecl-imagick
ea-php72:
- ea-php72-php-cli
- ea-php72-php-common
- ea-php72-php-curl
- pecl-imagick
x_php_versions_installed_keys: "{{ x_php_versions_installed.keys()|list }}"
x_php_versions_installed_ea_vals: "{{ x_php_versions_installed|dict2items|
map(attribute='value')|
map('select', 'match', 'ea-')|list }}"
x_php_versions_installed_ea: "{{ dict(x_php_versions_installed_keys|
zip(x_php_versions_installed_ea_vals)) }}"
tasks:
- debug:
msg: "{{ item.value|select('match','^ea-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
msg: "{{ item.value|select('match', 'ea-')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
msg: "{{ item.value|reject('match','^pecl-(.*)$')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
msg: "{{ item.value|reject('match','pecl-')|list }}"
loop: "{{ x_php_versions_installed|dict2items }}"
- debug:
var: x_php_versions_installed_ea

Get facts when var defined in hostvars

ansible 2.9.1
I have inventory:
[group1]
server1 master=yes
server2 master=no
server3 master=no
How get ansible_fqdn with master=yes from server2, server3?
Example:
server2 or server3 facts:
...
master_server: server1
...
I think so, but it did not work:
- name: set fact
set_fact:
master_server: {{ ansible_fqdn }}
when: master == 'yes'
delegate_to: "{{ item }}"
loop: "{{ ansible_play_hosts }}"
UPDATE. RESOLVE
add:
run_once: True
Q: "How to get ansible_fqdn with master=yes from server2, server3?"
A: There are more options. For example, either use selectattr
- hosts: group1
tasks:
- set_fact:
master_server: "{{ (groups.group1|
map('extract', hostvars)|
selectattr('master', 'eq', True)|
list|
first).ansible_fqdn }}"
run_once: true
- debug:
var: master_server
, or use json_query
- hosts: group1
tasks:
- set_fact:
master_server: "{{ groups.group1|
map('extract', hostvars)|
list|
json_query('[?master].ansible_fqdn')|
first }}"
run_once: true
- debug:
var: master_server
Both options give
ok: [test_02] => {
"master_server": "test_01.example.org"
}
ok: [test_01] => {
"master_server": "test_01.example.org"
}
ok: [test_03] => {
"master_server": "test_01.example.org"
}
Inventory used in the examples
$ cat hosts
group1:
hosts:
test_01:
master: yes
ansible_fqdn: test_01.example.org
test_02:
master: no
ansible_fqdn: test_02.example.org
test_03:
master: no
ansible_fqdn: test_03.example.org

Ansible: Remove item from dict

I have a situation where i am trying to remove the item from a list. but i am not getting expected result. Please help me what I am doing wrong here?
here is the list :
"get_ec2_id.instances[0].tags": {
"Name": "test-db-system-2",
"aws:cloudformation:logical-id": "DBInstance",
"aws:cloudformation:stack-id": "arn:aws:cloudformation:us-east-1:123456789012:stack/test-db-system-2/0115v0a0-5d44-17e8-a024-503ama4a5qd1",
"aws:cloudformation:stack-name": "test-db-system-2",
"dbsystem:stack": "test-db-system-2",
"dbsystem:type": "db"
}
}
I am trying to remove the all "aws:cloudformation" tags from a list using below filter:
"{{ get_ec2_id.instances[0].tags | reject('search','aws:') | list }}"
I am getting the below result:
ok: [10.52.8.101] => {
"instances_tags": [
"dbsystem:type",
"dbsystem:stack",
"Name"
]
}
but I am expected below result :
"instances_tags": [
"dbsystem:stack": "test-db-system-2",
"dbsystem:type": "db"
"Name" : "test-db-system-2",
]
}
Help me to solve the issue.
More generic solution where input is a dict and blacklist is a list:
---
- set_fact:
blacklist:
- bad1
- bad2
- set_fact:
output: {}
- name: remove blacklisted items from input
set_fact:
output: "{{ output | combine({item.key: item.value}) }}"
when: item.key not in blacklist
loop: "{{ input | dict2items }}"
Given the data
get_ec2_id:
instances:
- tags:
Name: test-db-system-2
aws:cloudformation:logical-id: DBInstance
aws:cloudformation:stack-id: arn:aws:cloudformation:us-east-1:123456789012:stack/test-db-system-2/0115v0a0-5d44-17e8-a024-503ama4a5qd1
aws:cloudformation:stack-name: test-db-system-2
dbsystem:stack: test-db-system-2
dbsystem:type: db
Use rejectattr. For example
dict2: "{{ get_ec2_id.instances.0.tags|
dict2items|
rejectattr('key', 'search', 'aws:')|
items2dict }}"
gives
dict2:
Name: test-db-system-2
dbsystem:stack: test-db-system-2
dbsystem:type: db
Then, convert the dictionary into a list of dictionaries
instances_tags: "{{ dict2|
dict2items|
json_query('[].[[key, value]]')|
map('community.general.dict')|
list }}"
gives
instances_tags:
- Name: test-db-system-2
- dbsystem:stack: test-db-system-2
- dbsystem:type: db
Use this:
---
- name: dictionary
hosts: localhost
gather_facts: False
connection: local
vars:
get_ec2_id:
instances:
tags:
Name: "test-db-system-2"
"aws:cloudformation:logical-id": "DBInstance"
"aws:cloudformation:stack-id": "arn:aws:cloudformation:us-east-1:123456789012:stack/test-db-system-2/0115v0a0-5d44-17e8-a024-503ama4a5qd1"
"aws:cloudformation:stack-name": "test-db-system-2"
"dbsystem:stack": "test-db-system-2"
"dbsystem:type": "db"
dict2: {}
tasks:
- name: Fact1
set_fact:
dict: "{{ get_ec2_id.instances.tags }}"
- name: Debug1
debug:
var: dict
- name: Fact2
set_fact:
dict2: "{{ dict2 | combine({item.key: item.value}) }}"
when: "{{ item.key.find('aws:') }}"
with_dict: "{{ dict }}"
- name: Debug2
debug:
var: dict2
Output:
TASK [Debug2] ******************************************************************
ok: [localhost] => {
"dict2": {
"Name": "test-db-system-2",
"dbsystem:stack": "test-db-system-2",
"dbsystem:type": "db"
}
}

Resources