Ansible: Skip loop when list is undefined - ansible

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)

Related

Ansible: Output of 'loop' with 'register' module

I'm trying to get the output of my Ansible script using register module but the loop I'm using is probably causing some issue.
Whats a default way of using register module with loop?
Code:
---
- name:
hosts:
gather_facts:
tasks:
- name: Execute file
ansible.builtin.shell:
environment:
"setting environment"
register: output
loop:
"value"
- debug:
vars: output.std_lines
Whats a default way of using register module with loop?
It is just registering the result.
The only difference will be, that a single task will provide you just with an dictionary result (or output in your example) and registering in a loop will provide with a list result.results (or output.results in your example). To access the .stdout_lines you will need to loop over the result set .results too.
You may have a look into the following example playbook which will show some aspects of Registering variables, Loops, data structures, dicts and lists and type debugging.
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Create STDOUT output (single)
command: 'echo "1"'
register: result
- name: Show full result (single)
debug:
var: result
- name: Show '.stdout' (single)
debug:
msg: "The result in '.stdout': {{ result.stdout }} is of type {{ result.stdout | type_debug }}"
- name: Create STDOUT output (loop)
command: 'echo "{{ item }}"'
register: result
loop: [1, 2, 3]
loop_control:
label: "{{ item }}"
- name: Show full result (loop)
debug:
var: result
- name: Show '.stdout' (loop)
debug:
msg: "The result in '.stdout': {{ item.stdout }} is of type {{ item.stdout | type_debug }}"
loop: "{{ result.results }}"
loop_control:
label: "{{ item.item }}"
By running it and going through the output you can get familiar with the differences in your tasks.
Further Q&A
Register Variables in Loop in an Ansible Playbook
Ansible: loop, register, and stdout
Register variables in loop in Ansible playbook
... and many more here on SO.

Ansible - check variable type

Apparently, according to several hours of searching nobody has encountered this use-case:
Its simple - I would like to execute some ansible logic depending on variable type. Basically equivalent of e.g. instanceof(dict, var_name) but in Ansible:
- name: test
debug:
msg: "{{ instanceof(var_name, dict) | ternary('is a dictionary', 'is something else') }}"
Is there any way this can be done?
Q: "Execute some ansible logic depending on the variable type."
A: The tests including mapping work as expected. For example, the tasks below
- set_fact:
myvar:
key1: value1
- debug:
msg: "{{ (myvar is mapping)|
ternary('is a dictionary', 'is something else') }}"
give
msg: is a dictionary
Q: "Ansible - check variable type"
A: An option would be to discover the data type and dynamically include_tasks. For example, the tasks below
shell> cat tasks-int
- debug:
msg: Processing integer {{ item }}
shell> cat tasks-str
- debug:
msg: Processing string {{ item }}
shell> cat tasks-list
- debug:
msg: Processing list {{ item }}
shell> cat tasks-dict
- debug:
msg: Processing dictionary {{ item }}
with this playbook
- hosts: localhost
vars:
test:
- 123
- '123'
- [a,b,c]
- {key1: value1}
tasks:
- include_tasks: "tasks-{{ item|type_debug }}"
loop: "{{ test }}"
give (abridged)
msg: Processing integer 123
msg: Processing string 123
msg: Processing list ['a', 'b', 'c']
msg: 'Processing dictionary {''key1'': ''value1''}'
If you want to simulate the switch statement create a dictionary
case:
int: tasks-int
str: tasks-str
list: tasks-list
dict: tasks-dict
default: tasks-default
and use it in the include
- include_tasks: "{{ case[item|type_debug]|d(case.default) }}"
loop: "{{ test }}"
Since Ansible version 2.3 there is type_debug:
- name: test
debug:
msg: "{{ (( var_name | type_debug) == 'dict') | ternary('is a dictionary', 'is something else') }}"
Note that the docs state a preference for 'type tests'.
Older question, but you can do this easily with a Python custom filter plugin. Granted it would give you Python specific types, but that may be fine for your use case.
This could work. It just needs to be placed in a folder named filter_plugins alongside your playbook or role.
import sys
if sys.version_info[0] < 3:
raise Exception("Must be using Python 3")
def get_type(var, **kwargs):
return type(var)
class FilterModule(object):
def filters(self):
return {
"get_type": get_type
}
Then in your playbook:
- name: test
debug:
msg: "{{ var_name | get_type }}"

how can i loop over a variable that might have single value?

I'm writing a playbook and want to loop a role over a variable that gets its value from the user. however that value might not always be a list of items, it might be a single value and whenever that happens it throws an error.
My Task:
- name: task name
include role:
name: role name
vars:
cluster_name: '{{ item }}'
loop: "{{ list_or_not }}"
loop_control:
loop_var: item
error:
...Invalid data passed to 'loop', it requires a list...
Have you tried the: "| list" filter?
Sorry cannot test at the moment.
You could test if the variable is a string, and if so, transform it into a single-item list. Something like this:
---
- hosts: localhost
gather_facts: false
tasks:
- set_fact:
list_or_not: ["{{ list_or_not }}"]
when: list_or_not is string
- debug:
msg: "{{ item }}"
loop: "{{ list_or_not }}"

Ansible item in list explicitly

item_list: [data, database, proddata]
I'm trying to do a conditional when: item.name in item_list
How do I make the when clause explicit so that if my list of items I'm looping thru are [data] only, that 'database' and 'proddata' don't pass the condition as true? I want only EXACT spelling to pass.
Example
---
- name: Explicit example
hosts: localhost
connection: local
vars:
source_list:
- name: proddata
- name: data
- name: other
dest_list1: [this, thing]
dest_list2: [database, proddata]
final_list: '{{ dest_list1 | union(dest_list2) | join(",") }}'
tasks:
- name: test
debug:
msg: '{{ item.name }}'
with_items:
- '{{ source_list }}'
when: item.name in final_list
As you can see, data is not in final_list but it shows up true. I believe because data is contained in proddata and database. I wish to do when: item.name == (item.name in final_list) if possible. I'm just not sure which filter I need.
It would work if your final_list was a list and not a string with comma separated values.
So if your var definition was like this:
final_list: '{{ dest_list1 | union(dest_list2) }}'
the condition
when: item.name in final_list
would be working.
But I assume you have it stored as a string for a reason because you use it somewhere else. If that's the case and you can not change it (you could do the join where you need it as a string) you can simply split it in your condition:
when: item.name in final_list.split(",")

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