Ansible conditionals with nested loops - ansible

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

Related

Ansible when conditional with variable and item

I'm having hard time trying to figure it out what I'm doing wrong with my Ansible playbook.
I've got a bunch of tasks, which define or not some variables according to context, depending of the result, some task will be ignored or not.
For this specific case, I check if a VlanID already exists, if it doesn't then I create one, and retrieve the new VlanID from the result.
Here is the playbook :
---
#Tasks for portGroup_add
- name: Get all portgroups in dvswitch vDS
community.vmware.vmware_dvs_portgroup_find:
hostname: "{{ vcenter_server }}"
username: "{{ vcenter_user }}"
password: "{{ vcenter_pass }}"
dvswitch: "{{ vcenter_dvSwitch }}"
validate_certs: False
register: portGroup_infos
when: (OLD_VLANID is not defined) or (OLD_VLANID|length < 1)
#Get last VLAN ID for HDS client, and set VLANID + 1
- name: get portGroup_infos
set_fact:
VLANID: "{{ item.vlan_id }}"
with_items: "{{ portGroup_infos.dvs_portgroups}}"
when:
- (portGroup_infos is defined) and (portGroup_infos|length > 0)
- item.name | regex_search("\(HDS :\s*")
While everything is working pretty well for most of the tasks, this one fires the following error :
The conditional check 'item.name | regex_search("\(HDS :\s*")' failed.
The error was: error while evaluating conditional (item.name | regex_search("\(HDS :\s*")): 'item' is undefined
Which is pretty obvious, because the dict portGroup_infos, is not defined.
In order to get the new VlanID, I'm using a "when" conditionnal , which check if in the item, the value "(HDS :" , is present.
But I don't want the task to launch if the portGroup_infos variable defined above is not set, I though I'd should use nested "when", but can't succeed.
Ansible version : 2.10.7
python version : 3.7.3
Thank you for your help.
Put both tasks into a block, e.g.
- block:
- name: Get all portgroups in dvswitch vDS
...
- name: get portGroup_infos
...
when: OLD_VLANID|default('')|length == 0

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)

Ansible with_items if item is defined

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

Ansible script is executing but loop is failing

I am trying to write a script (adduser.yml) to create users in a linux machine which imports users list from userlist.csv file. When I executed adduser.yml, the loop failed creating only 1st user. Can someone help me understand what mistake I am doing and how to correct it?
userlist.csv:
id,username,password,sudo
1,ansible1,ansible#123,yes
2,ansible2,ansible#123,no
3,ansible3,ansible#123,yes
4,ansible4,ansible#123,yes
adduser.yml:
---
## executed but until loop failed; check
- hosts: 192.168.0.3
vars:
count: "{{COUNT}}"
x: "1"
uname: "{{ lookup('csvfile', '{{x}} file=userlist.csv delimiter=, col=1') }}"
password: "{{ lookup('csvfile', '{{x}} file=userlist.csv delimiter=, col=2') }}"
sudo: "{{ lookup('csvfile', '{{x}} file=userlist.csv delimiter=, col=3') }}"
tasks:
name: "user add"
action:
- user:
x: "x+=1"
name: "{{ uname }}"
password: "{{ password }}"
state: present
register: x
until: "{{x > count}}"
name: "add to sudoers"
when: sudo == "yes"
lineinfile:
dest: /etc/sudoers
There are quite some things that will not work as you expected them. First thing, your loop is defined for your first task. That means only the user task will be repeated. Neither the sudo task nor the vars definition at the top will be looped. But don't try to re-define your loop, this is not going to work with vars.
Ansible has no build-in way to read vars from a csv file other than the csv lookup which will read exactly one line. But as said, you can not combine that with a loop.
I see two options you have:
Do not use csv. Ansible is mostly bound to yaml. If you'd had a yaml definition of your users, you simply could use the include_vars module to load these vars.
If you are bound to csv, you could try to use this includecsv module. (I have no experience with it and can not tell if it actually works)
Now, let's assume you have loaded your users into a list either from yaml or from csv with mentioned module. Then you'd simply loop with with_items:
tasks:
- name: "user add"
user:
name: "{{ item['uname'] }}"
password: "{{ item['password'] }}"
state: present
with_items: users_you_loaded
- name: "add to sudoers"
when: "{{ item['sudo'] }} == 'yes'"
...
with_items: users_you_loaded

Ansible - Use default if a variable is not defined

I'm customizing linux users creation inside my role. I need to let users of my role customize home_directory, group_name, name, password.
I was wondering if there's a more flexible way to cope with default values.
I know that the code below is possible:
- name: Create default
user:
name: "default_name"
when: my_variable is not defined
- name: Create custom
user:
name: "{{my_variable}}"
when: my_variable is defined
But as I mentioned, there's a lot of optional variables and this creates a lot of possibilities.
Is there something like the code above?
user:
name: "default_name", "{{my_variable}}"
The code should set name="default_name" when my_variable isn't defined.
I could set all variables on defaults/main.yml and create the user like that:
- name: Create user
user:
name: "{{my_variable}}"
But those variables are inside a really big hash and there are some hashes inside that hash that can't be a default.
You can use Jinja's default:
- name: Create user
user:
name: "{{ my_variable | default('default_value') }}"
Not totally related, but you can also check for both undefined AND empty (for e.g my_variable:) variable. (NOTE: only works with ansible version > 1.9, see: link)
- name: Create user
user:
name: "{{ ((my_variable == None) | ternary('default_value', my_variable)) \
if my_variable is defined else 'default_value' }}"
If anybody is looking for an option which handles nested variables, there are several such options in this github issue.
In short, you need to use "default" filter for every level of nested vars. For a variable "a.nested.var" it would look like:
- hosts: 'localhost'
tasks:
- debug:
msg: "{{ ((a | default({})).nested | default({}) ).var | default('bar') }}"
or you could set default values of empty dicts for each level of vars, maybe using "combine" filter. Or use "json_query" filter. But the option I chose seems simpler to me if you have only one level of nesting.
In case you using lookup to set default read from environment you have also set the second parameter of default to true:
- set_facts:
ansible_ssh_user: "{{ lookup('env', 'SSH_USER') | default('foo', true) }}"
You can also concatenate multiple default definitions:
- set_facts:
ansible_ssh_user: "{{ some_var.split('-')[1] | default(lookup('env','USER'), true) | default('foo') }}"
If you are assigning default value for boolean fact then ensure that no quotes is used inside default().
- name: create bool default
set_fact:
name: "{{ my_bool | default(true) }}"
For other variables used the same method given in verified answer.
- name: Create user
user:
name: "{{ my_variable | default('default_value') }}"
If you have a single play that you want to loop over the items, define that list in group_vars/all or somewhere else that makes sense:
all_items:
- first
- second
- third
- fourth
Then your task can look like this:
- name: List items or default list
debug:
var: item
with_items: "{{ varlist | default(all_items) }}"
Pass in varlist as a JSON array:
ansible-playbook <playbook_name> --extra-vars='{"varlist": [first,third]}'
Prior to that, you might also want a task that checks that each item in varlist is also in all_items:
- name: Ensure passed variables are in all_items
fail:
msg: "{{ item }} not in all_items list"
when: item not in all_items
with_items: "{{ varlist | default(all_items) }}"
The question is quite old, but what about:
- hosts: 'localhost'
tasks:
- debug:
msg: "{{ ( a | default({})).get('nested', {}).get('var','bar') }}"
It looks less cumbersome to me...
#Roman Kruglov mentioned json_query. It's perfect for nested queries.
An example of json_query sample playbook for existing and non-existing value:
- hosts: localhost
gather_facts: False
vars:
level1:
level2:
level3:
level4: "LEVEL4"
tasks:
- name: Print on existing level4
debug:
var: level1 | json_query('level2.level3.level4') # prints 'LEVEL4'
when: level1 | json_query('level2.level3.level4')
- name: Skip on inexistent level5
debug:
var: level1 | json_query('level2.level3.level4.level5') # skipped
when: level1 | json_query('level2.level3.level4.level5')
You can also use an if statement:
# Firewall manager: firewalld or ufw
firewall: "{{ 'firewalld' if ansible_os_family == 'RedHat' else 'ufw' }}"

Resources