Omit attribute when fact is not defined - ansible

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

Related

Ansible task with switching variables

I'm trying to create ansible playbook that will use variables if they are defined without using "while:" and manually typing the undefined variables & duplicating tasks.
For example I have the below variables:
vars:
service_List:
- 1:
state: present
address_type: ipv4
ip: 10.0.0.0
- 2:
state: present
jump: true
ip: 10.5.5.0
hold_true: yes
- 3:
state: present
address_type: ipv4
is_enabled: true
dhcp: none
I want to have a single task that will use the above variables on a specific module.
example of a task: (notice the with_dict)
tasks:
- name: task name here
some_module:
**This here will include the code for adding the variables form vars**
**So for example, for 1st dict it will include state, address_type and ip**
**for 2nd dict it will include variables state,jump,ip,hold_true**
**example: state: "{{ item.value.state }}"
with_dict: "{{ service_List }}"
Please help with missing code inside the task
It depends on the some_module use case. In particular, whether the parameters are required or not. And, if required, whether there is a default value or not. There are three options if a parameter is missing in the dictionary
The parameter is not required. Use default(omit)
The parameter is required. Use default(defaul_value_of_this_param)
The parameter is required but there is no default. The module will crash.
For example,
tasks:
- name: task name here
some_module:
state: "{{ item.value.state }}"
address_type: "{{ item.value.address_type|default('ipv4') }}"
ip: "{{ item.value.ip|default(omit) }}"
jump: "{{ item.value.jump|default(False) }}"
hold_true: "{{ item.value.hold_true|default(omit) }}"
with_dict: "{{ service_List }}"
In addition to this, you can use Module defaults.

How to use ansible when condition when string contains '#'

I am writing playbook to check user principal in kerberos servers. If principal exists it should skip task and if not it should create user principal. I am not sure how to use string with when condition I am trying below but getting errors
"ERROR! Syntax Error while loading YAML.
expected <block end>, but found '<scalar>'
The error appears to be in '/home/revyas/RHELProjects/Atlas/eda-datalake/playbooks/provision-emr.yml': line 42, column 31, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
stat:
path: "{{ user_dir }}"/"{{ keytab_name }}"
^ here
We could be wrong, but this one looks like it might be an issue with
missing quotes. Always quote template expression brackets when they
start a value. For instance:
with_items:
- {{ foo }}
Should be written as:
with_items:
- "{{ foo }}"
Playbook:
- name: Check if user principals exist
command: 'kadmin -w "{{ emr_kdc_admin_password }}" -p kadmin/admin listprincs'
register: user_princs
delegate_to : "{{ emr_kerberos_master }}"
tags: "emr_acct"
- name: Create user kerberos principal if not exist
command: 'kadmin -w {{ emr_kdc_admin_password }} -p kadmin/admin addprinc -randkey {{ kerberos_username }}#{{ emr_kerberos_realm }}'
when: "{{ kerberos_username }}#{{ emr_kerberos_realm }}" not in user_princs.stdout
delegate_to: "{{ emr_kerberos_master }}"
tags: "emr_acct"
User principal from kdc have format given below:
emr-test1-aren-reetika#abd.xyz.com
emr-test-aren#bd.xyz.com
emr-test-integration-test#bd.xyz.com
For the first cited issue, yaml doesn't behave like python or shell which automatically concatenate string literals together
You'll want:
stat:
path: "{{ user_dir }}/{{ keytab_name }}"
And the second error is because yaml believes the leading " is the start of a YAML literal, but in fact it's the start of a Jinja2 literal, thus:
when: '"{{ kerberos_username }}#{{ emr_kerberos_realm }}" not in user_princs.stdout'
Or you can use any of the scalar folding syntaxes, if you prefer that:
when: >-
"{{ kerberos_username }}#{{ emr_kerberos_realm }}"
not in user_princs.stdout
when: "{{ kerberos_username }}#{{ emr_kerberos_realm }}" not in user_princs.stdout
change to
when: "{{ kerberos_username }}\\#{{ emr_kerberos_realm }}" not in user_princs.stdout

Omit os_subnet's variable dns_nameservers in Ansible

I would like to omit the dns_nameservers variable from the following Openstack function if the value does not appear in the variable file:
os_subnet:
cloud: "{{ item.cloud }}"
state: present
validate_certs: no
no_gateway_ip: yes
dns_nameservers:
- "{{ item.dns | default(None) }}"
enable_dhcp: yes
name: "{{ item.subnet }}"
network_name: "{{ item.network }}"
cidr: "{{ item.cidr }}"
allocation_pool_start: "{{ item.allocation_pool_start }}"
allocation_pool_end: "{{ item.allocation_pool_end }}"
host_routes: "{{ item.host_routes | default(omit) }}"
with_items:
- "{{ subnets }}"
tags: subnets
Until now, I have tried to omit it with | default(omit) and | default(None), but it is not working. Is any filter that might help or any other way?
EDIT:
Variable file:
- cloud: tenant1
network: nw
subnet: nw_subnet
cidr: 172.12.17.64/26
dns:
- 8.8.8.8
- 8.8.8.9
allocation_pool_start: 172.12.17.68
allocation_pool_end: 172.12.17.70
host_routes:
- destination: 0.0.0.0/0
nexthop: 172.12.17.65
I am getting the following error:
Reason: '[u'8.8.8.8', u'8.8.8.9']' is not a valid
nameserver. '[u'8.8.8.8', u'8.8.8.9']' is not a valid
IP address.\", \"type\": \"HTTPBadRequest\", \"detail\": \"\"}}"}
You want to either pass a list with a single element or pass an omit keyword (placeholder object), which tells Ansible not to pass the whole parameter (dns_nameservers here) to the module:
dns_nameservers: "{{ [item.dns] if item.dns is defined else omit }}"
In your example, if item.dns was undefined, you passed a list with a single element being an omit placeholder. In such case the dns_nameservers parameter is defined (that list which is hardcoded in the code) and behaviour is undefined (likely depends on module).

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

Conditionally define variable in Ansible

I want to conditionally define a variable in an Ansible playbook like this:
my_var: "{{ 'foo' if my_condition}}"
I would like the variable to remain undefined if the condition does not resolve to true.
Ansible gives the following error if I try to execute the code:
fatal: [foo.local] => {'msg': 'AnsibleUndefinedVariable: One or more undefined
variables: the inline if-expression on line 1 evaluated
to false and no else section was defined.', 'failed': True}
Why is this an error anyway?
The complete case looks like this:
{role: foo, my_var: "foo"}
If my_var is defined, the role does something special. In some cases, I don't want the role to do this. I could use when: condition, but then I would have to copy the whole role block. I could also use an extra bool variable, but I would like a solution without having to change the "interface" to the role.
Any ideas?
You could use something like this:
my_var: "{{ 'foo' if my_condition else '' }}"
The 'else' will happen if condition not match, and in this case will set a empty value for the variable. I think this is a short, readable and elegant solution.
This code may help you to define a variable with condition.
- hosts: node1
gather_facts: yes
tasks:
- name: Check File
shell: ls -ld /etc/postfix/post-install
register: result
ignore_errors: yes
- name: Define Variable
set_fact:
exists: "{{ result.stdout }}"
when: result|success
- name: Display Variable
debug: msg="{{ exists }}"
ignore_errors: yes
So here the exists will display only if the condition is true.
My example, after https://stackoverflow.com/a/43403229/5025060:
vars:
sudoGroup: "{{ 'sudo' if ansible_distribution == 'Ubuntu' else 'wheel' }}"
Because of the different sudo conventions used by Ubuntu versus other platforms, here I am telling Ansible to set a variable named sudoGroup to sudo if the platform is Ubuntu, otherwise set it to wheel.
Later in my playbook, I combine the variable with Ansible's user module to add either sudo or wheel to an account's secondary groups depending on the OS Ansible is running on:
- name: Add or update bob account
user:
name: bob
uid: 3205
groups: "{{ sudoGroup }}"
append: yes
NOTES:
Double quotes around the {{ variable }} are required in the user: groups: definition above.
Once I define sudoGroup as above in my playbook's global vars: section, Ansible configures it at run time (based on ansible_distribution) for each target I define in my hosts: section.
I believe you're after the default(omit) filter. (Reference).
As per the example, mode will behave like it wasn't set at all for the first two items in the loop.
- name: touch files with an optional mode
file:
dest: "{{item.path}}"
state: touch
mode: "{{item.mode|default(omit)}}"
loop:
- path: /tmp/foo
- path: /tmp/bar
- path: /tmp/baz
mode: "0444"
This can be set as with bool:
- name: Conditional (true and false)
set_fact:
my_boolean_set_to_be: "{{ 'true' if my_var == 'foo' else 'false' }}"
- name: Display Variable
debug: msg="{{ my_boolean_set_to_be }}"
This can be set as for more conditionals like 'if-ifelse-else' statements:
- name: Conditional for 'my_var' (2 options and one default)
set_fact:
my_var_set_to_be: "{{ 'breakfast' if my_var == 'morning' else 'lunch' if my_var == 'afternoon' else 'dinner' }}"
- name: Display Variable
debug: msg="{{ my_var_set_to_be }}"

Resources