Checking the first digit in a number in ansible playbook - ansible

I have an ansible playbook that reads in a vars_file containing usernames and uids
users:
- name: josh
uid: 1201
- name: peter
uid: 1202
- name: paul
uid: 2101
- name: ryan
uid: 2102
I have two host groups in my inventory file, db and web. I want users to be created in db if their uid starts with a 1, and web if it starts with 2.
My playbook so far looks like this
---
- name: users playbook
hosts: all
become: yes
vars_files:
- vars/user_list.yml
tasks:
- name: test debug
debug:
msg: "{{ item.username }}, {{ item.uid }}"
loop: "{{ users }}"
when: '{{ item.uid[0] }} == 1'
But my when conditional throws the error
The error was: error while evaluating conditional ({{ item.uid[0] }} == 1)
Is there a better way of doing this for both conditionals?

Several problems.
First, you are not comparing anything. In the expression '{{ item.uid[0] }} == 1' the last part (i.e. == 1) will be literally treated as a string and written as output. If used in a full jinja2 expression, the comparison must be inside the markers: {{ item.uid[0] == 1 }}
Second, when clauses should not contain any jinja2 markers to expand variables. This is also the case for failed_when and changed_when. See the conditionals doc
Lastly, getting the character with an index will only work if the input is a string and not an int. So you first need to make sure or that by casting it correctly with the string filter. The char you will then get will be itself a string. Comparing it to an integer will always return false. So you either have to write the comparison value as a string (i.e. '1') or cast the extracted car to an integer with the int filter.
This is how I would fix your task:
- name: test debug
debug:
msg: "{{ item.username }}, {{ item.uid }}"
loop: "{{ users }}"
when: (item.uid | string)[0] | int == 1

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 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 Registers - Dynamic naming

I am trying to use a register in Ansible playbook to store my output. Below is the code which i am using.
I have tried below code
- name: Check if Service Exists
stat: path=/etc/init.d/{{ item }}
register: {{ item }}_service_status
with_items:
- XXX
- YYY
- ZZZ
I need different outputs to be stored in different register variables based on the items as mentioned in the code. It is failing and not able to proceed. Any help would be appreciated.
Updated answer
I think you need to put quotes around it:
register: "{{ item }}_service_status"
Or you can use set_fact (1, 2, 3, 4)
register all the output to a single static variable output and then use a loop to iteratively build a new variable service_status (a list) by looping over each item in the static variable output
- name: Check if Service Exists
stat: path=/etc/init.d/{{ item }}
register: output
with_items:
- XXX
- YYY
- ZZZ
- name: Setting fact using output of loop
set_fact:
service_status:
- rc: "{{ item.rc }}"
stdout: "{{ item.stdout }}"
id: "{{ item.id }}"
with_items:
- "{{ output }}"
- debug:
msg: "ID and stdout: {{ item.id }} - {{ item.stdout }}"
with_items:
- "{{ service_status }}"
Initial Answer
IIUC, this link from the Ansible docs shows how to use register inside a loop (see another example in this SO post).
A couple of points
it may be more convenient to assign the list (XXX, YYY, ZZZ) to a separate variable (eg. 1, 2)
I don't know if this is part of the problem, but with_items is no longer the recommended approach to loop over a variable: instead use loop - see here for an example
vars:
items:
- XXX
- YYY
- ZZZ
- name: Check if Service Exists
stat: path=/etc/init.d/{{ item }}
register: service_status
loop: "{{ items|flatten(levels=1) }}"
- name: Show the return code and stdout
debug:
msg: "Cmd {{ item.cmd }}, return code {{ item.rc }}, stdout {{ item.stdout }}"
when: item.rc != 0
with_items: "{{ service_status.results }}"

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