Ansible with_items if item is defined - ansible

Ansible 1.9.4.
The script should execute some task only on hosts where some variable is defined. It works fine normally, but it doesn't work with the with_items statement.
- debug: var=symlinks
when: symlinks is defined
- name: Create other symlinks
file: src={{ item.src }} dest={{ item.dest }} state=link
with_items: "{{ symlinks }}"
when: symlinks is defined
But I get:
TASK: [app/symlinks | debug var=symlinks] *********************
skipping: [another-host-yet]
TASK: [app/symlinks | Create other symlinks] ******************
fatal: [another-host-yet] => with_items expects a list or a set
Maybe I am doing something wrong?

with_items: "{{ symlinks | default([]) }}"

The reason for this behavior is conditions work differently inside loops. If a loop was defined the condition is evaluated for every item while iterating over the items. But the loop itself requires a valid list.
This is also mentioned in the docs:
Note that when combining when with with_items (see Loops), be aware that the when statement is processed separately for each item. This is by design:
tasks:
- command: echo {{ item }}
with_items: [ 0, 2, 4, 6, 8, 10 ]
when: item > 5
I think this is a bad design choice and for this functionality they better should have introduced something like with_when.
As you have already figured out yourself, you can default to an empty list.
with_items: "{{ symlinks | default([]) }}"
Finally if the list is dynamically loaded from a var, say x, use:
with_items: "{{ symlinks[x|default('')] | default([])}}"
This will default to an empty list when 'x' is undefined
Accordingly, fall back to an empty dict with default({}):
# service_facts skips, then dict2items fails?
with_dict: "{{ ansible_facts.services|default({})|dict2items|selectattr('key', 'match', '[^#]+#.+\\.service')|list|items2dict }}"

Related

Ansible conditionals with nested loops

I've read the ansible docs on conditionals and loops. But it's still not clear to me how it exactly works.
my yaml structure looks like this:
---
myusers:
- username: user1
homedir: 'home1'
sshkey: 'ssh-rsa bla1'
- username: user2
homedir: 'home2'
sshkey: 'ssh-rsa bla2'
process:
- transfer:
transtype: 'curl'
traname: 'ftps://targetsystem'
my playbook part looks like this:
- name: test j2
debug:
msg: |-
dest: "/var/tmp/{{ item.0.username }}/{{ item.1.traname }} {{ item.1.transtype }}"
when: item.0.process is not none
loop: "{{ myusers | subelements('process')}}"
Now I only want to loop when the sub-element process exists. I had this working at one point but don't understand what I changed to break it.
Mainly I don't understand what the effect of the sequence of 'when' and 'loop' has. It appears to me when I run it that the condition 'when' is ignored. Also when I swap the sequence of when and loop.
The error I get when running the playbook is :
FAILED! => {"msg": "could not find 'process' key in iterated item {u'username': u'user1' ...
I've also tried with different conditions like:
item.0.process is defined
myusers.username.process is not none
etc...
By default, the subelements filter (and the corresponding lookup) requires each top level element to have the subelement key (and will error with the above message if it does not exist)
You can change this behavior by setting the skip_missing parameter (note: I also fixed the index to address the traname key which was the wrong one in your question example)
- name: test j2
debug:
msg: |-
dest: "/var/tmp/{{ item.0.username }}/{{ item.1.traname }} {{ item.1.transtype }}"
loop: "{{ myusers | subelements('process', skip_missing=true) }}"

Ansible how to get equivalent of nested for-loop in playbook

I am writing an ansible-playbook and am trying to accomplish the following:
there are the existing directories /home/user1 , /home/user2, ... , /home/user20
in each of these directories I want to create subdirectories /foo1 , ... , /foo5
Now I COULD just make use of a with_nested loop, where I provide two lists with all the indices that I need, but that is just silly.
Instead, I would like to define two number-ranges or -sequences and the relevant task is then looped over using the value-pairs from their cartesian product.
Is that reasonably possible and if so how do I go about it?
See ansible-doc -t lookup sequence and Forcing lookups to return lists e.g.
- debug:
msg: "Create /home/user{{ item.0 }}/foo{{ item.1 }}"
with_nested:
- "{{ query('sequence', 'start=1 end=3') }}"
- "{{ query('sequence', 'start=1 end=2') }}"
gives
msg: Create /home/user1/foo1
msg: Create /home/user1/foo2
msg: Create /home/user2/foo1
msg: Create /home/user2/foo2
msg: Create /home/user3/foo1
msg: Create /home/user3/foo2
The parametrized task below gives the same result
- debug:
msg: "Create /home/user{{ item.0 }}/foo{{ item.1 }}"
with_nested:
- "{{ query('sequence', user_range) }}"
- "{{ query('sequence', dir_range) }}"
vars:
user_start: 1
user_end: 3
user_range: "start={{ user_start }} end={{ user_end }}"
dir_start: 1
dir_end: 2
dir_range: "start={{ dir_start }} end={{ dir_end }}"

How to loop over a nested dictionary in ansible?

How can I loop over this dictionary?
# nested dictionary to loop over:
vars:
commands:
group1:
cmd1:
run: foo
cmd2:
run: bar
group2:
cmd3:
run: zoo
# expected loop:
- group1, cmd1={...}
- group1, cmd2={...}
- group2, cmd4={...}
I tried using various combinations of dict2items and with_nested but so far I was not able to achieve that goal, and I would really want to avoid using loops with include tasks or writing a python module that does the flattening.
I still hope there is a pure-ansible way of doing it.
It's possible to loop tasks included by included_tasks. For example the file
shell> cat inner-loop.yml
- debug:
msg: "{{ outer_item.key }}, {{ item.key }}={{ item.value }}"
loop: "{{ outer_item.value|dict2items }}"
included from the task
- include_tasks: inner-loop.yml
loop: "{{ commands|dict2items }}"
loop_control:
loop_var: outer_item
gives
"msg": "group1, cmd1={u'run': u'foo'}"
"msg": "group1, cmd2={u'run': u'bar'}"
"msg": "group2, cmd3={u'run': u'zoo'}"

Is there any method to map multiple attributes in ansible

I have an output from yum list module. The thing is I want to display the output without those number (epoch attribute). The problem is I couldn't find any solutions of mapping 2 attributes (name and version). All the solutions I found are connected to only 1 attribute ( envra) in my case.
- name: check packages
become: true
yum:
list: installed
register: output
- name: add lines to files
lineinfile:
dest: "./file.txt"
line: "{{ inventory_hostname }} {{ item }}"
with_items:
- "{{ output.results | map(attribute='envra') |list }}"
delegate_to: localhost
This is the output without any mapping. As you can see there are multiple attributes. I would like to display only name and version of the package.
10.112.65.15 {u'envra': u'0:GeoIP-1.5.0-14.el7.x86_64', u'name': u'GeoIP', u'repo': u'installed', u'epoch': u'0', u'version': u'1.5.0', u'release': u'14.el7', u'yumstate': u'installed', u'arch': u'x86_64'}
The closest to expected values is envra attribute, but still has those epoch number inside...
10.112.65.15 0:GeoIP-1.5.0-14.el7.x86_64
As I mentioned at the begging I would like to get output of something like that
10.112.65.15 GeoIP 1.5.0
or at least without epoch attribute.
I've also change approach and tried this method
- name: add lines to files
lineinfile:
dest: "./file.txt"
line: "{{ inventory_hostname }} {{ item }} "
with_items:
- "{{ output | json_query(my_query) | list }}"
delegate_to: localhost
vars:
my_query: "results[].[name, version]"
but received result was with '[]' and u' which I'd like to delete but don't exactly know how.
10.112.65.15 [u'GeoIP', u'1.5.0']
Why do you extract the attribute or use json query ? Simply use the hash and print out the needed fields. The following should work out of the box.
- name: add lines to files
lineinfile:
dest: "./file.txt"
line: "{{ inventory_hostname }} {{ item.name }} {{ item.version }}"
with_items:
- "{{ output.results }}"
delegate_to: localhost

Ansible how to ignore undefined item in a loop

I'm trying to have list of paths.
The variables folder (web_folder, app_folder or db_folder ) may be undefined and is expected to be undefined.
In this case I just don't want the undefined value in the the list.
- set_fact: root_paths="{{ root_paths | default([]) + [ item ] }}"
loop:
- "{{ web_folder }}"
- "{{ app_folder }}"
- "{{ db_folder }}"
when: item is defined
When I do this I get an error message 'The task includes an option with an undefined variable.'
I can make this work if I define default values e.g. '-' and replace the when condition with
when: item != '-'
I don't like this solution.
I tried a few things like
'when: vars[item] is undefined' from this post, but it didn't work for me.
I also replaces 'loop' with 'with_items' didn't work either
Any suggestions?
The variables folder (web_folder, app_folder or db_folder ) may be undefined and is expected to be undefined.
you could use the default filter to initialize them to an empty array, in case they are not defined:
- set_fact: root_paths="{{ root_paths | default([]) + [ item ] }}"
loop:
- "{{ web_folder | default([]) }}"
- "{{ app_folder | default([]) }}"
- "{{ db_folder | default([]) }}"
when: item is defined
hope it helps.
Q: I get an error message 'The task includes an option with an undefined variable.' I can make this work if I define default values e.g. '-' and replace the when condition with when: item != '-'. I don't like this solution.
A: The expansion of an undefined variable causes the error. Either the variables are tested explicitly or default filter is used.
Testing each variable is cumbersome
tasks:
- set_fact:
folders_defined: []
- set_fact:
folders_defined: "{{ folders_defined + [web_folder] }}"
when: web_folder is defined
- set_fact:
folders_defined: "{{ folders_defined + [app_folder] }}"
when: app_folder is defined
- set_fact:
folders_defined: "{{ folders_defined + [db_folder] }}"
when: db_folder is defined
- debug:
var: folders_defined
The next option (which is very similar to the one you don't like) is to test a default value assigned to undefined variables. For example, the playbook below tests default empty string. If necessary fit this condition to your needs.
- hosts: localhost
vars:
web_folder: /my/web/folder
app_folder: /my/app/folder
folders: [web_folder, app_folder, db_folder]
tasks:
- set_fact:
folders_defined: "{{ folders_defined|default([]) +
[lookup('vars', item)] }}"
loop: "{{ folders }}"
when: lookup('vars', item, default='')|length > 0
- debug:
var: folders_defined
gives
ok: [localhost] => {
"folders_defined": [
"/my/web/folder",
"/my/app/folder"
]
}

Resources