When a task is skipped on the basis of condition and the result of register also differ which cause of another task has been failed.
- name: Check if the partition is already mounted
shell: df | grep "{{item.partition}}" | wc -l
with_items:
- "{{ ebs_vol }}"
register: ebs_checked
when: ebs_vol is defined
- name: Make filesystem of the partition
filesystem: fstype=ext4 dev={{item.item.partition}} force=no
when: ( ebs_vol is defined and "{{item.stdout}} == True" )
changed_when: True
with_items:
- ebs_checked.results
Use default filter to handle corner cases:
- name: Make filesystem of the partition
filesystem: fstype=ext4 dev={{item.item.partition}} force=no
when: item.stdout | bool
changed_when: True
with_items: "{{ ebs_checked.results | default([]) }}"
This will iterate over empty list (read "will do nothing") if there is no results in ebs_checked.
Also you should not check for ebs_vol is defined because when statement in looped tasks is applied inside a loop, and keeping in mind that you check for ebs_vol is defined in the previous task, makes this check unnecessary inside a loop.
Related
I've currently many of this tasks. The file module here is just an example.
- file:
path: "{{ datapath }}"
state: "directory"
when:
- "storage is not defined"
- file:
path: "{{ datapath }}"
state: "directory"
delegate_to: "{{ storage.host }}"
when:
- "storage is defined"
- "storage.host is defined"
Which either creates a directory on the inventory_host or on a different host, when the fact is defined.
I wonder, if it is possible to reduce the number of tasks here. Normally I would use the omit filter. But because I've several conditions, I'm not sure what kind of syntax to use here for delegate_to.
You can also use the omit special variable in an inline expression
- file:
path: "{{ datapath }}"
state: directory
delegate_to: "{{ storage.host if storage.host is defined else omit }}"
With this, and because you can chain inline-if's, then you could have multiple conditions that ends in an omit, e.g.
delegate_to: >-
{{
storage.host
if storage.host is defined
else 'localhost'
if for_localhost | default(false)
else omit
}}
Would be:
delegated to storage.host when defined
delegated to localhost when for_localhost is truthy
omitted, otherwise
Background:
We are providing an Ansible utility for the admins to add or remove comments in motd file. We want to restrict any direct edits to motd file. Since there can be previous comments we want to retain them. This means that we parse the file only once and capture existing comments. After which the admins have to use the tool to add/delete comments. Any comments directly added to the file will be discarded.
Requirement:
I have this block which needs to run only once. Not once per execution but once only for many executions. In other words, it should run the first time we execute the script but not after that.
Approach:
To accomplish this, I defined a flag variable and initialized it to 0 like this common_motd_qsc_flag: 0 in defaults/mail.yml. Once I executed a particular task I am trying to update the variable to 1 like this common_motd_qsc_flag: 1. Within the task, I am making sure that the task is executed only when the flag variable is 0 in using the when condition.
Problem:
Every time the script executes it is still running the task that shouldn't be run. I understand why this is happening. It is because during the start of the script it is reading common_motd_qsc_flag: 0 in defaults/main.yml.
Question:
Is there a way to update common_motd_qsc_flag: 1 in defaults/main.yml without using lineinfile module? Any alternative approaches are also appreciated if this an ugly way to handle this requirement.
tasks/main.yml:
- name: Parse all existing comments from /etc/motd
shell: tail --lines=+10 "{{ common_motd_qsc_motd_file }}"
register: existing_comments
when:
- motd_file.stat.exists == True
- common_motd_qsc_flag == 0 # defaults
- name: Update flag variable
set_fact:
common_motd_qsc_flag: 1
when: common_motd_qsc_flag == 0
- name: Add existing comments to the array
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_existing_entry]) }}"
loop: "{{ existing_comments.stdout_lines }}"
when:
- not t_existing_entry is search('Note:')
- not t_existing_entry is search('APPTYPE:')
- not t_existing_entry is search('Comments:')
- t_existing_entry not in common_motd_qsc_comments_array
vars:
t_existing_entry: "{{ item | trim }}"
defaults/main.yml:
common_motd_qsc_flag: 0
I was able to fix this using local facts as per your advice. Thanks much for the pointer. Here is the working code:
- name: Parse all existing comments from /etc/motd
shell: tail --lines=+10 "{{ common_motd_qsc_motd_file }}"
register: existing_comments
when:
- t_common_motd_qsc_check_qsc_file.stat.exists == True
- ansible_local['snps'] is defined
- ansible_local['snps']['cache'] is defined
- ansible_local['snps']['cache']['common_motd_qsc_flag'] is not defined
changed_when: false
- name: Add existing comments to the array
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_existing_entry]) }}"
loop: "{{ existing_comments.stdout_lines }}"
when:
- ansible_local['snps'] is defined
- ansible_local['snps']['cache'] is defined
- ansible_local['snps']['cache']['common_motd_qsc_flag'] is not defined
- not t_existing_entry is search('Note:')
- not t_existing_entry is search('APPTYPE:')
- not t_existing_entry is search('Comments:')
- t_existing_entry not in common_motd_qsc_comments_array
vars:
t_existing_entry: "{{ item | trim }}"
- name: Set common_motd_qsc_flag to facts file
ini_file:
dest: "/etc/ansible/facts.d/snps.fact"
section: 'cache' # [header]
option: 'common_motd_qsc_flag' # key
value: "1" # value
- name: Add a new comment if it does not exist
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | union([t_new_entry]) }}"
loop: "{{ common_motd_qsc_add_comment }}"
when:
- t_new_entry not in common_motd_qsc_comments_array
- t_new_entry|length > 0
vars:
t_new_entry: "{{ item | trim }}"
- name: Delete an existing comment
set_fact:
common_motd_qsc_comments_array: "{{ common_motd_qsc_comments_array | difference([t_new_entry]) }}"
loop: "{{ common_motd_qsc_delete_comment }}"
when:
- t_new_entry in common_motd_qsc_comments_array
- t_new_entry|length > 0
vars:
t_new_entry: "{{ item | trim }}"
- name: Save comments to snps.fact file
ini_file:
dest: "/etc/ansible/facts.d/snps.fact"
section: 'motd' # [header]
option: 'common_motd_qsc_comment_array' # key
value: "{{ common_motd_qsc_comments_array }}" # value
I need to create a check very similar to that one explained here: Ansible to check diskspace for mounts mentioned as variable
Except I need it only for specified paths (for example /var).
{{ ansible_mounts }} is an array of dictionaries, each containing variable mount that is the actual path. I need to perform the check in a loop for all items in {{ ansible_mounts }} only if mount is equal to some value.
Example of what I want to achieve in pseudo code:
foreach (mountpoint in ansible_mounts)
{
if (mountpoint["mount"] == "/var" || mountpoint["mount"] == "/opt")
{
// do the check
}
}
How can I do this in Jinja / Ansible? This code does the check for every single item. I need to filter it only for explicitly specified paths:
- name: Ensure that free space on the tested volume is greater than 15%
assert:
that:
- mount.size_available > mount.size_total|float * 0.15
msg: Disk space has reached 85% threshold
vars:
mount: "{{ ansible_mounts | selectattr('mount','equalto',item.mount) | list | first }}"
with_items:
- "{{ ansible_mounts }}"
you'll need to add a when condition, for example
vars:
my_mounts:
- '/var/log'
- '/var/logs/foo'
tasks:
- name: do the check
when: item.mount in my_mounts
with_items: '{{ ansible_mounts }}'
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 }}"
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' }}"