How to combine two lists together? - ansible

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 ?

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"
}
]
}

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.

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.

Related

How to use jmespath operator to match key on non array list

I am trying to match certain key from an ansible output/vars, and return the value of another key.
Here is my vars.
{
"_meta": {
"hostvars": {
"ansibletower1_422c3aed-780c-8c33-3054-d32e330c9285": {
"guest.hostName": "ansibletower1.rh.pro",
"name": "ansibletower1"
},
"child domain_422c4cd1-d644-7eeb-df7c-c32a2a05c030": {
"guest.hostName": null,
"name": "child domain"
}
}
}
}
My non-working playbook
- hosts: ansibletower1.rh.pro
tasks:
- debug:
msg: "{{ _meta | json_query(querystr) }}"
vars:
querystr: "[?hostvars.*.\"guest.hostName\"=='{{inventory_hostname}}'].name"
I am trying to follow the method here, https://blog.networktocode.com/post/ansible-filtering-json-query/. However in my scenario is not array, which is different than the example in the link.
My end goal is to match guest.hostName with {{ inventory_hostname }}, and return the value of the name by using - set_fact: to register it to another variable.
I'd try with:
tasks:
- name: Loop over data and continue if string was found.
debug:
msg: "{{ _meta | json_query(querystr) }}"
vars:
querystr: "hostvars.* | [?\"guest.hostName\"==`{{inventory_hostname}}`].name"
when you select all keys with .* you get an array back, so it is piped into another query that filters and returns name.

Combine attribute value using json_query in ansible

I want to combine two attribute into single string separated by delimiter using the json_query in ansible
Sample data
{
"locations": [
{"name": "Seattle", "state": "WA"},
{"name": "New York", "state": "NY"},
{"name": "Bellevue", "state": "WA"},
{"name": "Olympia", "state": "WA"}
]
}
As shown in above data set i'm trying to filter the state "WA" and execpted output is:
[
"Seattle-WA",
"Bellevue-WA",
"Olympia-WA"
]
What i have tried as of now:
- debug:
msg: "{{ chart_list.HELM_CHARTS | json_query(\"[?state == 'WA'].{name:name,state:state}\") }}"
Output:
[
{
"name": "Seattle",
"state": "WA"
},
{
"name": "Bellevue",
"state": "WA"
},
{
"name": "Olympia",
"state": "WA"
}
]
Updated :
I was able to get the expected result by trial and error method and these are my findings:
[?state == 'WA'].[join('-',[name,state])][]
Output:
[
"Seattle-WA",
"Bellevue-WA",
"Olympia-WA"
]
Also if the input which you give is in unicode format, i suggest you to add to_json | from_json expressions as mentioned below:
selected_cities: "{{ test.locations| to_json | from_json | json_query(\"[?state == 'WA'].[join('-',[name,state])][]\") }}"
Using above expression will eliminate unicode error whil using the values or in any condition.
Check JMESPath site for more detail on the json_query, it was really helpful in resolving the issue.
For example
- debug:
msg: "{{ locations|
json_query('[?state == `WA`].[name,state]')|
map('join', '-')|list }}"
gives
msg:
- Seattle-WA
- Bellevue-WA
- Olympia-WA
The same result gives the task below using Jinja2 filters only
- debug:
msg: "{{ _names|zip(_states)|map('join', '-')|list }}"
vars:
_locations: "{{ locations|selectattr('state', 'eq', 'WA')|list }}"
_names: "{{ _locations|map(attribute='name')|list }}"
_states: "{{ _locations|map(attribute='state')|list }}"
json_query issue (fixed in 2.10 and later)
There is JMESPath join. Unfortunately
- debug:
msg: "{{ locations|
json_query('[].join(`-`, [name,state])') }}"
fails
msg: |-
JMESPathError in json_query filter plugin:
In function join(), invalid type for value: Seattle, expected one of: ['array-string'], received: "AnsibleUnicode"
to_json|from_json workaround
Quoting from json_query: Add examples for starts_with and contains #72821
data structure returned from register variables needs to be parsed using to_json | from_json in order to get a correct result. Fixes: ansible-collections/community.general#320
- debug:
msg: "{{ locations|to_json|from_json|
json_query('[].join(`-`, [name,state])') }}"
gives
msg:
- Seattle-WA
- New York-NY
- Bellevue-WA
- Olympia-WA
Just for the sake of a pure JMESPath way of doing it, as your trial and error solution still have an unneeded extra layer of complexity.
When you are doing
[?state == 'WA'].[join('-', [name, state])][]
You are creating an array [join('-', [name, state])] then flattening it [] for no reason.
You can just go to the solution with a shorter approach:
[?state == `WA`].join(`-`, [name, state])
Also mind that you can overcome the quotes in quotes (simple or double) complication for JMESPath queries using:
YAML multilines string: How do I break a string in YAML over multiple lines?
Backticks in your JMESPath query, as pointed in the documentation:
In the example above, quoting literals using backticks avoids escaping quotes and maintains readability.
Source: https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html#selecting-json-data-json-queries
So you end up with (see note below if you are on an Ansible version < 2.10):
- debug:
msg: >-
{{ test.locations
| json_query('[?state == `WA`].join(`-`, [name, state])') }}
Please note: as raised by #Vladimir Botka on the versions prior to 2.10, you will be affected by this issue: https://github.com/ansible/ansible/issues/27299#issuecomment-331068246, forcing you to add a | to_json | from_json filter on the list.
Given the playbook:
- hosts: all
gather_facts: yes
tasks:
- debug:
msg: >-
{{ test.locations
| json_query('[?state == `WA`].join(`-`, [name, state])')
}}
vars:
test:
locations:
- name: Seattle
state: WA
- name: New York
state: NY
- name: Bellevue
state: WA
- name: Olympia
state: WA
This yields:
[
"Seattle-WA",
"Bellevue-WA",
"Olympia-WA"
]

Ansible: set fact variable is getting overwritten while iterating over a loop

I am trying to create a list out of a dictionary based on condition. But when I pass it through a loop, the last value of loop overwrites the fact instead of creating a list
input.yml
execution:
pre-deploy:
post-deploy:
shell-files:
name: abc, def, gef
type: deploy
target_host: server1
check: enabled
xml-files:
name: xyz, uvw
type: deploy
target_host: server2
check: enabled
shell-files:
name: pqr
type: migrate
target_host: server1
check: enabled
My Code:
Hosts: local
vars_file:
- input.yml
vars:
post_list:"{{ lookup( 'dict', operations.post-deploy, wantList=Ture ) }}"
tasks:
- set_fact:
get_deploy_list: "{{ item.key }}: {{ item.value.name.split(',') | list }}"
get_host_list: "{{ item.value.target_host }}"
when: ( item.value.type == "deploy" and item.value.check == "enabled")
loop:"{{ post_list | items2dict }}"
- debug: msg="{{ get_deploy_list }}"
Expected Output:
debug:
[ {
shell-files: abc,
shell-files: def,
shell-files: ghi
}
{
xml-files: xyz,
xml-files: uvw
} ]
Actual output:
[{
xml-files: xyz,
xml-files: uvw
} ]
The last value of list overwrites the fact.
The situation is the same as in any programming language with loops: if you don't reference the existing list, then it is just repeatedly reassigning a variable and you will end up with the last state of the world as the loop exits
The traditional way I have seen that solved is via | default and | combine
- set_fact:
get_deploy_list: >-
{{ (get_deploy_list|default([]))
| combine({item.key: item.value.name.split(',') | list})
}}
loop: "{{ post_list | items2dict }}"
although in my playbooks, I consider that pattern a bug since jinja is perfectly capable of building up dictionaries using its looping syntax, without invoking set_fact repeatedly (which, by definition, will open connections to every host in the inventory multiple times)
be aware that I didn't get your exact output format with that code snippet, because there was already too much wrong with your playbook; this answer was just "why did the assignment overwrite the fact all the time"

how use vars created in the with_elements and used in a with_subelement loop

I want to include tasks and create users with the item var created by the with_items function
It's a part of a big code. I can't post here the entire code but i need to create a user by multiple way and very different way.
in my create.yml i make differents tests to choose the good file to include. In this exemple i's because the type of the user is "simple" so i include the create_simple.yml file
For exemple :
file create.yml :
- name: 'Create'
include_tasks: create_simple.yml user='{{ item }}'
with_items:
- '{{ userslist }}'
when: item.simple
Here a exemple of the userslist :
userslist:
- name: 'user1'
simple: False
uid: '1002'
password: 'test'
shell: '/bin/bash'
comment: 'comment user1'
primary: 'groupe1'
gid: '30002'
groups:
- gname: 'secondary'
gid: 30000
remote_groups:
- gname: 'remote'
expires: 1580472000
generate_ssh_key: true
ssh_key_type: 'rsa'
- name: 'test'
simple: True
uid: '1002'
password: 'test'
shell: '/bin/bash'
comment: 'test'
gid: '30003'
groups:
- gname: 'test'
gid: 30000
expires: 1580472000
generate_ssh_key: true
ssh_key_type: 'rsa'
in the create_simple.yml file i have this :
- name: 'test'
debug: msg='{{ resultat.1.gname }}'
with_subelements:
- "{{ lookup('dict', user) }}"
- groups
loop_control:
loop_var: resultat
But i have this message :
FAILED! => {"msg": "An unhandled exception occurred while running the lookup plugin 'dict'. Error was a <class 'ansible.errors.AnsibleError'>, original message: with_dict expects a dict"}
the user var in the with_items of the create.yml file seem to be a dict
I have test to convert the user var in a dict but it doesn't work.
with_subelements requires a list of hashes (i.e. dictionaries) and a key to lookup in each element on this list, but the list of hashes returned by lookup('dict', user) will return something like:
[
{
"key": "comment",
"value": "comment user1"
},
{
"key": "shell",
"value": "/bin/bash"
},
{
"key": "name",
"value": "user1"
}
...
]
And with_subelements will try to find key groups in each of the dictionary in this list, which fails.
with_subelements:
- "{{ lookup('dict', user) }}" # Wrong: items in this list of dict does not have 'groups' keys
- groups # OK: this a field name to lookup
In included create_simple.yml, the variable user will be a dictionary such as:
name: 'user1'
simple: False
groups:
- gname: 'secondary'
gid: 30000
...
If you want to list groups each time create_simple.yml is included, you can do in create_simple.yml:
- name: 'test'
debug:
msg: '{{ resultat.gname }}'
with_items: "{{ user.groups }}"
loop_control:
loop_var: resultat
Or even simpler if you only need a single task in create_simple.yml, you can use directly with_subelements instead of include_tasks in create.yml:
# this will lookup field 'groups' in each element of the 'userslist' variable
- name: 'Create'
debug:
msg: "Group for user {{ resultat.0.name }}: {{ resultat.1.gname }}"
with_subelements:
- "{{ userslist }}"
- groups
loop_control:
loop_var: resultat

convert dictionary keys in playbook

I have an existing playbook variable dictionary defined like:
vars:
resource_tags: {
Name: "some name"
Service: "some service"
}
This is used in various calls to tasks in this form. But in another task, I need it in a different format, and rather than have it hard-coded, I was wondering if it could be built in a task.
I need it to look like:
{
"tag:Name": "some name"
"tag:Service": "some service"
}
I tried iterating using with_dict and setting a fact with combine:
- set_fact:
ec2_remote_facts_filter: "{{ ec2_remote_facts_filter | default({}) | combine( { 'tag:'item.name: item.val } ) }}"
with_dict: "{{ ec2_count_resource_tags }}"
And obviously that doesn't work.
Is this even possible?
If you don't mind a bit of hackery:
- debug: msg="{{ resource_tags | to_json(indent=0) | regex_replace('\n\"','\n\"tag:') }}"
This will convert your dict into JSON-formatted string with indent=0, meaning each key will start from new line; then insert tag: after first double quote on every line.
Because the result is valid JSON, Ansible template engine will convert it back into dict as the last step of variable substitution, giving you:
ok: [localhost] => {
"msg": {
"tag:Name": "some name",
"tag:Service": "some service"
}
}
I suppose there may be some corner cases if there are newlines inside your values, but in general it should be fine.
Maybe you need a custom lookup plugin in your case.
1) Edit file ansible.cfg and uncomment key 'lookup_plugins' with value './plugins/lookup'
2) Create a plugin file named 'ec2remote.py' in './plugins/lookup'
3) Use it in your playbook:
- debug:
msg: "{{ item }}"
with_ec2remote: "{{ ec2_count_resource_tags }}"
4) Implements your ec2remote.py (many examples here)
class LookupModule(LookupBase):
def run(self, terms, **kwargs):
result = {}
for k,v in terms.items():
result["tag:"+k] = v
return result
Usually, I prefer to develop plugins that are easily usable and testable and thus preserve an understandable playbook.

Resources