Ansibe: concatenation of items from with_items - ansible

I'm trying to get a variable which will contain comma separated items from with_itmes loop as follow:
- hosts: localhost
connection: local
gather_facts: no
tasks:
- name: set_fact
set_fact:
foo: "{{ foo }}{{ item }},"
with_items:
- "one"
- "two"
- "three"
vars:
foo: ""
- name: Print the var
debug:
var: foo
It works as expected but what I'm getting at the end is trailing comma.
Is there any way to remove it?

There is a join filter that we can use with lists to concatenate list elements with a given character.
If we are passing the list directly to with_items or loop then we can use loop_control to "extend" some more loop information to get ansible_loop.allitems. Then this can be joined with the join filter.
Example:
- set_fact:
foo: "{{ ansible_loop.allitems|join(',') }}"
loop:
- one
- two
- three
loop_control:
extended: true
Otherwise a more straightforward way is to define a variable with list and use join filter on elements of that variable.
Example:
- set_fact:
foo: "{{ mylist|join(',') }}"
vars:
mylist:
- one
- two
- three

No clue if this is correct way to do but it does the job:
- name: Print the var
debug:
msg: "LIST: {{ foo | regex_replace(',$','') }}"

Related

Ansible: Skip loop when list is undefined

Example playbook -
---
- hosts: localhost
vars:
lesson:
name: Physics
students:
- Bob
- Joe
tasks:
- name: Display student names
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: item | default("")
The above playbook works well to output the student names.
However, if the input changes (as per below) such that no student names have been defined, then an error occurs. Is there a simple way to have the playbook skip this task if the list is undefined as per the input below? I realize it would work if the input specifies students: [], but as this input is coming from simple users, they're not going to know this. Much Thanks!
vars:
lesson:
name: Physics
students:
Error: fatal: [localhost]: FAILED! =>
msg: 'Invalid data passed to ''loop'', it requires a list, got this instead: . Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup.
Update - I've tried the below variations but still get the same error -
---
- hosts: localhost
vars:
lesson:
name: Physics
students:
tasks:
- name: Display student names variation 1
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: lesson.students is iterable
- name: Display student names variation 2
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: lesson.students is not none
- name: Display student names variation 3
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: ( item | default("") ) or ( item is not none )
The real problem is that loop requires a list, even if it is an empty list.
If your var is undefined/None/empty string, it exists but is not a list and your when condition will never get evaluated because loop will fire an error before it is ever reached.
You have to default your var to an empty list in such cases, which will lead to a 0 size loop equivalent to skipping the task.
Since your var is defined but None you need to use the second optional parameter to default so that empty/false values are replaced as well
Note: I used the short alias d to default in my below examples
- name: Display student names
debug:
msg: '{{ item }}'
loop: "{{ lesson.students | d([], true) }}"
A good practice here that would have nipped that error in the bud would be to have a coherent data declaration by either:
not declaring the key at all and use a simple default i.e.
# ... #
vars:
lesson:
name: Physics
# ... #
loop: "{{ lesson.students | d([]) }}"
declare an empty list for the key rather than a None value i.e.
# ... #
vars:
lesson:
name: Physics
students: []
# ... #
loop: "{{ lesson.students }}"
My first proposition is the safest in this case anyway and will work in for all the above vars declarations.
There is a difference between an undefined variable, and variable having None value.
When you set variable name, but leave the right hand side empty. The variable is defined, but it is set to NoneType.
So your when: condition should have additional check for NoneType:
- hosts: localhost
vars:
lesson:
name: Physics
students:
tasks:
- name: Display student names
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: ( item | default("") ) or ( item is not none )
This will give:
skipping: [localhost] => (item=None)

Select and concatenate dict value lists

I've got a dict of lists like this:
packages:
server:
- foo
- bar
- baz
client:
- spam
- ham
- eggs
runtime:
- corge
- grault
- garply
and I want to generate a list by defining keys to select in another dict, so e.g.:
enable:
server: true
client: false
runtime: true
would result in:
output:
- foo
- bar
- baz
- corge
- grault
- garply
Any ideas how to do this in ansible/jinja?? Ideally without using the loop construct but I could live with that. I suspect it'll need an intermediate variable and so far all I've got is something to extract a list of the keys from enable which have a true value:
- set_fact:
enabled: "{{ (enable | dict2items | selectattr('value') | list | items2dict).keys() }}"
If this looks a bit convoluted its because both packages and enable are existing role variables which I'd prefer not to change. But open to suggestions (especially on packages) if reshaping them makes this much easier.
Let's create the list of enabled hosts first. For example
- set_fact:
my_hosts: "{{ enable|dict2items|
selectattr('value')|
map(attribute='key')|list }}"
- debug:
var: my_hosts
give
my_hosts:
- server
- runtime
Then use this list to extract the packages. For example
- set_fact:
my_list: "{{ my_hosts|map('extract', packages)|list|flatten }}"
- debug:
var: my_list
give
my_list:
- foo
- bar
- baz
- corge
- grault
- garply
The single task below comprises both steps
- set_fact:
my_list: "{{ my_hosts|map('extract', packages)|list|flatten }}"
vars:
my_hosts: "{{ enable|dict2items|
selectattr('value')|
map(attribute='key')|list }}"

ansible lookup: issue in displaying as a list

I have a var file named prop.yml and contains:
var1:
- 'a'
- 'b'
var2:
- 'blah'
- 'blab'
Now, my playbook looks like:
task:
- name: including a variety file
include_vars:
file: prop.yml
name: property
- set_fact:
project: "{{ lookup ('vars', 'property') }}"
- debug:
msg: "{{ project }}"
Now, my output is
var1[
"a"
"b"]
var2[ "blah" , "blab"]
What I want as output is
["a", "b", "blah", "blab"]
Simply add the lists.
- debug:
msg: "{{ project.var1 + project.var2 }}"
The following should get you going:
task:
- name: including a variety file
include_vars:
file: prop.yml
name: property
- set_fact:
project: "{{ property.var1 + property.var2 }}"
- debug:
msg: "{{ project }}"
Rather than using the vars statically we can concatenate all lists in the included vars file by replacing the set_fact task with the following one.
This will only work if all declared vars in the file are all lists. Note the use of the default filter to make sure our result var is always defined. This also uses a loop over a dict
- name: Iteratively concatenate our lists
set_fact:
project: "{{ project | default([]) + item.value }}"
loop: "{{ property | dict2items }}"

Splitting a values as list from a string using ansible filter

How to convert the above string to the list of comma separated strings using ansible filter.I need like this var2 , so that i can loop it and use those values.
Expected:
var2: [arn:aws:sds:ABCDEFGHI123456, arn:aws:sds:HRTYUIOPE89012345]"
Input:
var1:"arn:aws:sds:ABCDEFGHI123456arn:aws:sds:HRTYUIOPE89012345"
thansk!!!
Something like this should work:
- name: play1
hosts: all
vars:
x: "arn:aws:sds:ABCDEFGHI123456arn:aws:sds:HRTYUIOPE89012345"
tasks:
- name: task1
debug:
msg: "arn:{{ item }}"
with_items: "{{ x.split('arn:') }}"

Ansible - How to keep appending new keys to a dictionary when using set_fact module with with_items?

I want to add keys to a dictionary when using set_fact with with_items. This is a small POC which will help me complete some other work. I have tried to generalize the POC so as to remove all the irrelevant details from it.
When I execute following code it is shows a dictionary with only one key that corresponds to the last item of the with_items. It seems that it is re-creating a new dictionary or may be overriding an existing dictionary for every item in the with_items. I want a single dictionary with all the keys.
Code:
---
- hosts: localhost
connection: local
vars:
some_value: 12345
dict: {}
tasks:
- set_fact: {
dict: "{
{{ item }}: {{ some_value }}
}"
}
with_items:
- 1
- 2
- 3
- debug: msg="{{ dict }}"
This can also be done without resorting to plugins, tested in Ansible 2.2.
---
- hosts: localhost
connection: local
vars:
some_value: 12345
dict: {}
tasks:
- set_fact:
dict: "{{ dict | combine( { item: some_value } ) }}"
with_items:
- 1
- 2
- 3
- debug: msg="{{ dict }}"
Alternatively, this can be written without the complex one-liner with an include file.
tasks:
- include: append_dict.yml
with_items: [1, 2, 3]
append_dict.yml:
- name: "Append dict: define helper variable"
set_fact:
_append_dict: "{ '{{ item }}': {{ some_value }} }"
- name: "Append dict: execute append"
set_fact:
dict: "{{ dict | combine( _append_dict ) }}"
Output:
TASK [debug]
*******************************************************************
ok: [localhost] => {
"msg": {
"1": "12345",
"2": "12345",
"3": "12345"
}
}
Single quotes ' around {{ some_value }} are needed to store string values explicitly.
This syntax can also be used to append from a dict elementwise using with_dict by referring to item.key and item.value.
Manipulations like adding pre- and postfixes or hashes can be performed in the same step, for example
set_fact:
dict: "{{ dict | combine( { item.key + key_postfix: item.value + '_' + item.value | hash('md5') } ) }}"
Use a filter plugin.
First, make a new file in your ansible base dir called filter_plugins/makedict.py.
Now create a new function called "makedict" (or whatever you want) that takes a value and a list and returns a new dictionary where the keys are the elements of the list and the value is always the same.
class FilterModule(object):
def filters(self):
return { 'makedict': lambda _val, _list: { k: _val for k in _list } }
Now you can use the new filter in the playbook to achieve your desired result:
- hosts: 127.0.0.1
connection: local
vars:
my_value: 12345
my_keys: [1, 2, 3]
tasks:
- set_fact: my_dict="{{ my_value | makedict(my_keys) }}"
- debug: msg="{{ item.key }}={{ item.value }}"
with_dict: "{{my_dict}}"
You can customize the location of the filter plugin using the filter_plugins option in ansible.cfg.
this does not seems to work any more on ansible 2.5
---
- hosts: localhost
connection: local
vars:
some_value: 12345
dict: {}
tasks:
- set_fact:
dict: "{{ dict | combine( { item: some_value } ) }}"
with_items:
- 1
- 2
- 3
- debug: msg="{{ dict }}"
returns only last value {"dict":{"3": "some value"}}
I suggest you could do this :
- set_fact:
__dict: |
{% for item in [1,2,3] %}
{{item}}: "value"
{% endfor %}
- set_fact:
final_dict: "{{__dict|from_yaml}}"
- debug:
var: final_dict
Another solution could be this one, tested in Ansible 2.9.6.
This solutions adds the extra benefit that you do not have to declare _dict beforehand in vars section. This is achieved by the | default({}) pipe which ensures that the loop will not fail in the first iteration when _dict is empty.
In addition, the renaming of dict to _dict is necessary since the dict is a special keyword reserved for <class 'dict'>. Referenced (unfortunately only at devel branch yet) here.
---
- hosts: localhost
connection: local
vars:
some_value: 12345
tasks:
- set_fact:
_dict: "{{ _dict | default({}) | combine( { item: some_value } ) }}"
with_items:
- 1
- 2
- 3
- debug: msg="{{ _dict }}"

Resources