Looping or repeating a group of tasks until success - loops

I currently have a playbook which includes a task file. In that task file, I would like to check for a condition. If the exit code of that condition is not equal to 0, all steps in the task file should be repeated. I have tried a few variations with block and loops but I have not figured out a way to make it do what I described above.
Currently I have something like this:
tasks:
- name: call task file
include: task_file.yml
In task_file.yml,
- name: perform an operations
shell: do A
- name: check
shell: do B
register: result
Next, I would like to tell the main playbook that if result.rc != 0, please repeat task_file.yml until result.rc == 0.
Any pointers would be greatly appreciated
The playbook seems to end no matter what the exit code.

There's no direct way to reach your goal as include_tasks does not support the retry/until loop keywords.
There is an attempt to circumvent that limitation by teaching ansible a new loop_control.until keyword for loops which could be used for includes. Unfortunately, the pull request has been opened since Sep. 2019 and has still not reached a realease.
The good news is you can implement that with some work by using include recursion with a block. The below example is largely inspired by a blog article on https://dev.to. I adapted to the current context, fixed some good practice and added features like flexible retries number and delay between retries. Here we go:
The tasks to be retried go in task_file.yml
---
- name: group of tasks to repeat until success
block:
- name: increment attempts counter
ansible.builtin.set_fact:
attempt_number: "{{ attempt_number | d(0) | int + 1 }}"
- name: dummy task
ansible.builtin.debug:
msg: "I'm a dummy task"
- name: task to check for success.
# Just for the example. Will return success on attempt number 3
ansible.builtin.command: "[ {{ attempt_number | int }} -eq 3 ]"
changed_when: false
rescue:
- name: "Fail if we reached the max of {{ max_attempts | d(3) }} attempts"
# Default will be 3 attempts if max_attempts is not passed as a parameter
ansible.builtin.fail:
msg: Maximum number of attempts reached
when: attempt_number | int == max_attempts | int | d(3)
- ansible.builtin.debug:
msg: "group of tasks failed on attempt {{ attempt_number }}. Retrying"
- name: add delay if needed
# no delay if retry_delay is not passed as parameter
ansible.builtin.wait_for:
timeout: "{{ retry_delay | int | d(omit) }}"
when: retry_delay is defined
# include ourselves to retry.
- ansible.builtin.include_tasks: task_file.yml
As you can see, the file includes itself again in case of failure until success of max attempts is reached. Also, note that a retry will happen if any task fails inside the block, not only the last one. If you have a more complex scenario, you can implement more checks for fail/not fail in the rescue section an even add an always section if needed. See anbile blocks
Then you can call this file from your playbook:
---
- hosts: localhost
gather_facts: false
tasks:
- name: Include tasks to retry. 7 attempts max with 1 second delay
ansible.builtin.include_tasks: task_file.yml
vars:
max_attempts: 7
retry_delay: 1
Playing this example succeeds on third attempt as hardcoded and expected. (You can play around with the parameters to test a fail scenario)
$ ansible-playbook playbook.yml
PLAY [localhost] ***********************************************************************************************************************************************************************************************************************
TASK [Include tasks to retry] **********************************************************************************************************************************************************************************************************
included: /tmp/toto/task_file.yml for localhost
TASK [increment attempts counter] ******************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [dummy task] **********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "I'm a dummy task"
}
TASK [task to check for success.] *******************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": ["[", "1", "-eq", "3", "]"], "delta": "0:00:00.002104", "end": "2022-12-08 14:16:27.850578", "msg": "non-zero return code", "rc": 1, "start": "2022-12-08 14:16:27.848474", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [Fail if we reached the max of 7 attempts] ****************************************************************************************************************************************************************************************
skipping: [localhost]
TASK [ansible.builtin.debug] ***********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "group of tasks failed on attempt 1. Retrying"
}
TASK [add delay if needed] *************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [ansible.builtin.include_tasks] ***************************************************************************************************************************************************************************************************
included: /tmp/toto/task_file.yml for localhost
TASK [increment attempts counter] ******************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [dummy task] **********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "I'm a dummy task"
}
TASK [task to check for success.] *******************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": ["[", "2", "-eq", "3", "]"], "delta": "0:00:00.004009", "end": "2022-12-08 14:16:29.496509", "msg": "non-zero return code", "rc": 1, "start": "2022-12-08 14:16:29.492500", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
TASK [Fail if we reached the max of 7 attempts] ****************************************************************************************************************************************************************************************
skipping: [localhost]
TASK [ansible.builtin.debug] ***********************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "group of tasks failed on attempt 2. Retrying"
}
TASK [add delay if needed] *************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [ansible.builtin.include_tasks] ***************************************************************************************************************************************************************************************************
included: /tmp/toto/task_file.yml for localhost
TASK [increment attempts counter] ******************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [dummy task] **********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "I'm a dummy task"
}
TASK [task to check for success.] *******************************************************************************************************************************************************************************************************
ok: [localhost]
PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost : ok=14 changed=0 unreachable=0 failed=0 skipped=2 rescued=2 ignored=0

Related

How to exit/skip loop in Ansible for include_task

Below is my ansible playbook that checks for file/dir permission and should exit the loop (BUT not stop the play) with the details of the file/dir not meeting the permission check (others should not have execute, or write permissions)
---
- name: "Play 1"
hosts: localhost
serial: true
any_errors_fatal: false
tasks:
- name: Checking permissions with include task
include_tasks: "{{ playbook_dir }}/internalcheckperm.yml"
loop:
- "~/.ssh/known_hosts"
- "~/.ssh/authorized_keys"
- "~/"
- "~/.ssh"
when: permchangeflag | default(false) == false
- name: Send Email
debug:
msg: "Sending out email and other stuff below"
cat internalcheckperm.yml
- stat:
path: "{{ item }}"
register: fp
- set_fact:
permchangeflag: true
when: fp.stat.exists == true and fp.stat.xoth == true and fp.stat.woth == true and fp.stat.isdir == false
- debug:
msg: "permchangeflag value: {{ permchangeflag | default(false) }} on {{ inventory_hostname }} for file {{ item }}"
when: fp.stat.exists == true
Run as ansible-playbook test.yml
Requirement: if ~/.ssh/known_hosts is permission 777 then the condition when: permchangeflag | default(false) == false should fail and loop should not run (skip) for the remaining files/dir in the loop.
However, despite permchangeflag becoming true as evident from the output below the loop continues to run for all the files.
TASK [debug] *******************************************************************************************************************
Tuesday 15 November 2022 08:12:17 -0600 (0:00:00.031) 0:00:02.454 ******
ok: [myremote7] => {
"msg": "permchangeflag value: True on myremote7 for file ~/.ssh/known_hosts and when not True"
From the output it seems the when condition does not kick for one element of the loop at a time but all at once which is something I don't want.
TASK [Checking permissions with include task] ******************************************************************************************************************************************************************
included: /root/internalcheckperm.yml for localhost => (item=~/.ssh/known_hosts)
included: /root/internalcheckperm.yml for localhost => (item=~/.ssh/authorized_keys)
included: /root/internalcheckperm.yml for localhost => (item=~/)
included: /root/internalcheckperm.yml for localhost => (item=~/.ssh)
Can you please suggest how can i break/skip the loop as soon as the first when permission condition is not met?

Ansible - How to save output from loop cycle 1 that I can use in the following loop cycles (2,3...)

I have a task with an API call that returns dictionary as output. From this output I need only an ID. This API call is triggered only once (when item == "1"), but I need it's output available also in the following cycles. Here is the code example I used:
register: output
when: item == "1"
ignore_errors: yes
- debug:
var: output.json.id
- name: show id
debug:
msg: output.json.id is "{{ output.json.id }}"
This is filtered output result I get in 1st cycle:
ok: [localhost] => {
"msg": "output.json.id is \"kjfld4343009394\""
}
In the 2nd cycle API call is skipped (item is not 1) but output from previous cycle is not available any more :
ok: [localhost] => {
"output.json.id": "VARIABLE IS NOT DEFINED!: 'dict object' has no attribute 'json'"
}
BTW In case "debug: var: output.json.id" should be executed just in first cycle, I tried with putting it with conditional item=1 and ignore_errors=yes but that didn't help.
- debug:
var: output.json.id
when: item == "1"
ignore_errors: yes
What can I do to have this output available in other cycles?
Thanks!
I just found solution with set_fact.
- name: Set var id (set_fact)
set_fact:
var_id: "{{ output.json.id }}"
when: item == "1"
- debug:
msg: id is "{{ var_id }}"
When saved like this var_id can be used in the following loop cycles.

How to store grep command result in ansible variable?

I am trying to set an Ansible variable based on the count returned by the grep command.
The code is as follows.
tasks:
- name: Check for 12.1 databases
shell: "grep -c /u1/app/oracle/product/12.1.0/dbhome_1 /etc/oratab"
register: grep_output
- name: Print
debug:
msg="grep_output.stdout"
- name: Set the variable
set_fact:
oracle_version_12_1_0: "true"
when: "grep_output.stdout > 0"
- name: Print variable
command: echo "{{ oracle_version_12_1_0 }}"
The code errors out as follows.
PLAY [oradbpoc1] ***********************************************************************************************************************************************
TASK [Gathering Facts] *********************************************************************************************************
ok: [oradbpoc1]
TASK [Check for 12.1 databases] *********************************************************************************************************
fatal: [oradbpoc1]: FAILED! => {"changed": true, "cmd": "grep -c /u1/app/oracle/product/12.1.0/dbhome_1 /etc/oratab", "delta": "0:00:00.003425", "end": "2020-08-06 18:33:04.476299", "msg": "non-zero return code", "rc": 1, "start": "2020-08-06 18:33:04.472874", "stderr": "", "stderr_lines": [], "stdout": "0", "stdout_lines": ["0"]}
PLAY RECAP *********************************************************************************************************
oradbpoc1 : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
PS: Also, is there a way to assign a default value ("false") to the oracle_version_12_1_0 variable?
Thanks
FR
With regards to the Fail condition, it is because a grep -c PATTERN FILE will return a non-zero exit code if the PATTERN is not found in a file.
Quick example:
# grep -c root /etc/passwd; echo $?
2
0
# grep -c rootNO /etc/passwd; echo $?
0
1
So, I believe there are two options:
Just base upon the exit code
Set to ignore the error, and deal with the value
Since the count should be sufficient, Option 2 is shown here:
- hosts: localhost
tasks:
- name: get value
shell:
cmd: grep -c root /etc/passwd
register: grep_output
ignore_errors: yes
changed_when: false
- name: Print
debug:
msg: "{{ inventory_hostname }}: '{{ grep_output.stdout }}'"
- name : Set the variable
set_fact:
oracle_version_12_1_0: "{% if grep_output.stdout == '0' %}false{% else %}true{% endif %}"
- name: Print
debug:
msg: "val: {{ oracle_version_12_1_0 }}
Adjust root to rootNo (for example) to see the difference in true/false.
Also, depending upon exactly what you are wishing to do, it is also possible to register the variable as indicated in the get value, and then use it in a when check (i.e., avoid setting the variable):
- name : task name
...
when: grep_output.stdout != '0'
Whether this latter approach is helpful or not depends on the overall use case.

How to use ansible loop

Variable file
I have a variable file
fruits:
- apple
- banana
- orange
- strawberry
- grapes
- papaya
users:
- user_name: 'john'
user_age: 45
- user_name: 'yash'
user_age: 95
- user_name: 'srk'
user_age: 52
- user_name: 'alia'
user_age: 26
Playbook Tasks
and my ansible tasks, just trying to make a text file and adding variables in file in vertical order.
- hosts: localhost
gather_facts: true
vars_files:
- variables.var # this is my variable file in the same dir that playbook have.
tasks:
- name: add fruits to the list
lineinfile:
create: yes
line: "{{ item }}"
path: /home/ansible/ansible-demo2/fruits.txt
loop:
- "{{ fruits|flatten }}"
- name: add uses to the list
lineinfile:
create: yes
line: "{{ item.user_name }} ==> {{ item.user_age }}"
path: /home/ansible/ansible-demo2/users.txt
loop:
- "{{ users|flatten(levels=1) }}"
Errors
But I am getting weird behavior. Below is the output of fruits task and error of the users task.
TASK [add fruits to the list] ***************************************************************************************************************************
ok: [localhost] => (item=[u'apple', u'banana', u'orange', u'strawberry', u'grapes', u'papaya'])
[WARNING]: The value ['apple', 'banana', 'orange', 'strawberry', 'grapes', 'papaya'] (type list) in a string field was converted to u"['apple',
'banana', 'orange', 'strawberry', 'grapes', 'papaya']" (type string). If this does not look like what you expect, quote the entire value to ensure it
does not change.
TASK [add uses to the list] *****************************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'list object' has no attribute 'user_name'
\n\nThe error appears to be in '/home/ansible/ansible-demo2/myansible.yml': line 18, column 7, but may\nbe elsewhere in the file depending on the exact s
yntax problem.\n\nThe offending line appears to be:\n\n\n - name: add uses to the list\n ^ here\n"}
Expected Output
List in vertical order in text file.
Comment on question:
I am amazed every thing works fine with with_items and not with loop, even user_name variable is defined but still it is saying undefined. Now I unable to find out what's wrong going on.
Reference:
Here is the doc that I am referencing : Migrating from with_X to loop
Edit: Variables debug output
I debug the variables. output is below
TASK [debug] ********************************************************************************************************************************************
ok: [localhost] => {
"fruits": [
"apple",
"banana",
"orange",
"strawberry", "grapes", "papaya"
]
}
TASK [debug] ********************************************************************************************************************************************
ok: [localhost] => {
"users": [
{
"user_age": 45,
"user_name": "john"
},
{
"user_age": 95,
"user_name": "yash"
},
{
"user_age": 52,
"user_name": "srk"
},
{
"user_age": 26,
"user_name": "alia"
}
]
}
Q: *"[WARNING]: The value ['apple', ... ] (type list) in a string field was converted to u"['apple', ... ]" (type string).
A: From the code, it's not clear what is the reason for this conversion. The data and the playbook below work as expected.
shell> cat data.yml
fruits:
- apple
- banana
- orange
- strawberry
- grapes
- papaya
users:
- user_name: 'john'
user_age: 45
- user_name: 'yash'
user_age: 95
- user_name: 'srk'
user_age: 52
- user_name: 'alia'
user_age: 26
shell> cat playbook.yml
- hosts: localhost
vars_files:
- data.yml
tasks:
- name: add fruits to the list
lineinfile:
create: yes
line: "{{ item }}"
path: fruits.txt
loop: "{{ fruits }}"
- name: add uses to the list
lineinfile:
create: yes
line: "{{ item.user_name }} ==> {{ item.user_age }}"
path: users.txt
loop: "{{ users }}"
Give
shell> cat fruits.txt
apple
banana
orange
strawberry
grapes
papaya
shell> cat users.txt
john ==> 45
yash ==> 95
srk ==> 52
alia ==> 26
Q: "2 variables fruits1 and fruits2 ... append their data into single file ... in single task with 2 vars"
A: With the modified data
shell> cat data.yml
fruits1:
- apple
- banana
- orange
fruits2:
- strawberry
- grapes
- papaya
this task gives the same result
- name: add fruits to the list
lineinfile:
create: yes
line: "{{ item }}"
path: fruits.txt
loop: "{{ fruits1 + fruits2 }}"
See Append list variable to another list in Ansible.
Comment on the question: "I am amazed everything works fine with with_items and not with loop"
A: See Comparing loop and with_*.

Ansible - remove the single quote (') character from a string

I'm new to Ansible, trying to use Assert module to validate the length
of a string. The result register contains string: "'MWCC' | length == 3".
Could you please help to remove the single quotes ' inside the string so the
result would be like this: "MWCC | length == 3”?
Thanks.
- set_fact:
test_code: 'MWCC'
- name: validate three characters code
assert:
that:
"'{{test_code}}' | length == 3 "
ignore_errors: True
register: code_result
- debug: var=code_result.assertion
- name: extract the string from assertion test output
set_fact:
extract_result: "{{code_result.assertion |regex_replace('\'')}}"
TASK [set_fact] *********************************************************************************
ok: [localhost] => {
"ansible_facts": {
"test_code": "MWCC"
},
"changed": false
}
TASK [validate three characters code] ****************************************************************************
fatal: [localhost]: FAILED! => {
"assertion": "'MWCC' | length == 3 ",
"changed": false,
"evaluated_to": false
}
...ignoring
TASK [debug] ***************************************************************************
ok: [localhost] => {
"code_result.assertion": "'MWCC' | length == 3 "
}
TASK [extract the string from assertion test output] **********************************************************************
fatal: [localhost]: FAILED! => {
"msg": "template error while templating string: unexpected char u\"'\" at 41. String: {{code_result.assertion |regex_replace(''')}}"
}
"{{ test_string |replace(\"'\",'') }}"
This has the replace Jinja2 filter with two arguments:
the single quote, that you want to replace, surrounded by escaped double-quotes.
the empty string denoted by two single quotes.
I think your assert should not have another pair of quotes, like this:
"{{test_code}} | length == 3 "
And surely, 4 character string length should be 4, shouldn't it?

Resources