Ansible block and rescue for error handling with examples

Blocks in Ansible allow you to logically group a set of tasks together, primarily for one of two purposes. One might be to apply conditional logic to an entire set of tasks. We can use ansible block, rescue and always to perform different kinds of error handling where with the rescue block we can perform cleanup action. We will learn more about this in detail with examples in this section.

 

Example-1: Why we should use blocks

I have written a small playbook ansible-blocks-1.yml which will help you understand how ansible blocks can be useful and why it should be used. In this playbook we have 4 tasks wherein 3 tasks requires root level privilege while one of the task can be executed without root level privilege.

Now I have used become: yes 3 times which is just increasing the number of lines in the playbook.

[ansible@controller ~]$ vim ansible-blocks-1.yml
---
 - name: Ansible Blocks
   hosts: server1
   gather_facts: false

   tasks:
     - name: List usr directory content
       command: "ls -l /usr/"
       become: yes

     - name: List root partition content
       command: "ls -l /root/"
       become: yes

     - name: List ansible user's home directory content
       command: "ls -l ~/"

     - name: List bin diretcory content
       command: "ls -l /bin/"
       become: yes

We could have added become: yes in the starting of the play but then that variable will be applied to the entire play wherein we don't want this permission for task 3. To overcome this we have block level solution wherein we can place all the common tasks under single block.

 

This is how the playbook would look with blocks. Here we have combined all the tasks which requires root level privilege into single block and instead of using become 3 times, we have applied this to the entire block.

---
 - name: Ansible Blocks
   hosts: server1
   gather_facts: false

   tasks:
     - block:
        - name: List usr directory content
          command: "ls -l /usr/"

        - name: List root partition content
          command: "ls -l /root/"

        - name: List bin directory content
          command: "ls -l /bin/"
       become: yes

     - name: List ansible user's home directory content
       command: "ls -l ~/"

NOTE:

Observe the alignment which is important. The become key has same number of whitespace compared to block so the become value is applicable for the entire block.

Now we can execute the playbook:

[ansible@controller ~]$ ansible-playbook ansible-blocks-1.yml

PLAY [Ansible Blocks] ************************************************************************************************

TASK [List usr directory content] ************************************************************************************
changed: [server1]

TASK [List root partition content] ***********************************************************************************
changed: [server1]

TASK [List bin directory content] ************************************************************************************
changed: [server1]

TASK [List ansible user's home directory content] ********************************************************************
changed: [server1]

PLAY RECAP ***********************************************************************************************************
server1                    : ok=4    changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

 

Example-2: Perform error recovery with rescue block

We have additional benefit while using ansible blocks to perform recovery operation i.e. if any of the tasks within a block fails then in a ideal scenario the playbook would exit (unless you are using ignore_errors: yes). But with blocks we can assign a rescue block which can again contain a bunch of tasks. So if any of the tasks within block fails then automatically the tasks from recovery block will be executed which can perform any clean up activity which you would want to do in case of failure.

Lastly we also have an always block which will be called independent of the task execution status which can be used to give something like SUMMARY or anything as per your requirement.

In this sample playbook ansible-blocks-2.yml we have created two tasks inside the block wherein one task will pass while the other is expected to fail. In which case the rescue block will be executed followed by the always block.

---
 - name: Ansible Blocks
   hosts: server1
   gather_facts: false

   tasks:
     - block:
        - name: List home directory content
          command: "ls -l ~/"

        - name: Failing intentionally
          command: "ls -l /tmp/does-not-exist"

       rescue:
        - name: Rescue block (perform recovery)
          debug:
            msg: "Something went wrong, cleaning up.."

       always:
        - name: This will execute always
          debug:
            msg: "I will execute even in failure scenario"

 

Let us execute this playbook:

[ansible@controller ~]$ ansible-playbook ansible-blocks-2.yml

PLAY [Ansible Blocks] ************************************************************************************************

TASK [List home directory content] ***********************************************************************************
changed: [server1]

TASK [Failing intentionally] *****************************************************************************************
fatal: [server1]: FAILED! => {"changed": true, "cmd": ["ls", "-l", "/tmp/does-not-exist"], "delta": "0:00:00.003050", "end": "2020-09-26 10:29:13.864820", "msg": "non-zero return code", "rc": 2, "start": "2020-09-26 10:29:13.861770", "stderr": "ls: cannot access '/tmp/does-not-exist': No such file or directory", "stderr_lines": ["ls: cannot access '/tmp/does-not-exist': No such file or directory"], "stdout": "", "stdout_lines": []}

TASK [Rescue block (perform recovery)] *******************************************************************************
ok: [server1] => {
    "msg": "Something went wrong, cleaning up.."
}

TASK [This will execute always] **************************************************************************************
ok: [server1] => {
    "msg": "I will execute even in failure scenario"
}

PLAY RECAP ***********************************************************************************************************
server1                    : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

As you can see, our task1 was successful but task2 has failed after which the recovery block was executed followed by always block.

 

Now assuming none of the tasks failed in the block section in which case the rescue block will not be executed, but always block will be executed independent of the task status.

I have updated my playbook to list /tmp directory content so that the second task does not fail, let's observe the output after execution:

[ansible@controller ~]$ ansible-playbook ansible-blocks-2.yml

PLAY [Ansible Blocks] ************************************************************************************************

TASK [List home directory content] ***********************************************************************************
changed: [server1]

TASK [List tmp directory content] ************************************************************************************
changed: [server1]

TASK [This will execute always] **************************************************************************************
ok: [server1] => {
    "msg": "I will execute even in failure scenario"
}

PLAY RECAP ***********************************************************************************************************
server1                    : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

So this time the rescue block has not been called but always block was executed as expected.

 

Example-3: Practical example of error and recovery with blocks

Now that we know the concept of block and rescue let's use this in practical use case. I will create one playbook which will perform the following task under our block

  • Install vsftpd package
  • Take a backup of /etc/vsftpd/vsftpd.conf on the same managed node
  • Copy vsftpd.j2 jinja template from controller node to the managed node and replace /etc/vsftpd/vsftpd.conf
  • Intentionally fail the block by trying to access incorrect location

Next I will have a rescue block so that if something goes wrong we will perform some clean up/fallback. In this I have two tasks:

  • Print a message on the console for the recovery
  • Restore vsftpd.conf using the backup file vsftpd.conf.bkp on the managed node

Lastly we will have always block will restart the vsftpd service independent of the task execution status

This is our playbook ansible-blocks-3.yml:

---
 - name: Install vsftpd
   hosts: server1
   become: yes
   vars:
     anonymous_enable: yes
     local_enable: yes
     write_enable: yes
     anon_upload_enable: yes
   tasks:
     - block:
        - name: install vsftp
          yum:
            name: vsftpd
        - name: take backup of existing config
          copy:
            src: /etc/vsftpd/vsftpd.conf
            dest: /etc/vsftpd/vsftpd.conf.bkp
            remote_src: yes
        - name: use Jinja2 template to configure vsftpd
          template:
            src: vsftpd.j2
            dest: /etc/vsftpd/vsftpd.conf
        - name: This will fail
          command: "ls -l /tmp/does-not-exist"

       rescue:
        - name: Recovery block
          debug:
            msg: "something failed, restoring vsftpd.conf from backup"
        - name: Restoring vsftpd.conf
          copy:
            src: /etc/vsftpd/vsftpd.conf.bkp
            dest: /etc/vsftpd/vsftpd.conf
            remote_src: yes

       always:
        - name: Restarting vsftpd
          service:
            name: vsftpd
            state: restarted

 

This is our sample jinj2 template. We had used the same template while working with Jinja2 templates chapter earlier.

[ansible@controller ~]$ cat vsftpd.j2
anonymous_enable={{ anonymous_enable }}
local_enable={{ local_enable }}
write_enable={{ write_enable }}
anon_upload_enable={{ anon_upload_enable }}
dirmessage_enable=YES
xferlog_enable=YES
connect_from_port_20=YES
pam_service_name=vsftpd
userlist_enable=YES
# MY IP Address={{ ansible_facts['default_ipv4']['address'] }}

Let us execute this playbook and verify the steps:

[ansible@controller ~]$ ansible-playbook ansible-blocks-3.yml

PLAY [Install vsftpd] ************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************
ok: [server1]

TASK [install vsftp] *************************************************************************************************
ok: [server1]

TASK [take backup of existing config] ********************************************************************************
changed: [server1]

TASK [use Jinja2 template to configure vsftpd] ***********************************************************************
changed: [server1]

TASK [This will fail] ************************************************************************************************
fatal: [server1]: FAILED! => {"changed": true, "cmd": ["ls", "-l", "/tmp/does-not-exist"], "delta": "0:00:00.003701",                                                                                                                        "end": "2020-09-26 10:56:49.076834", "msg": "non-zero return code", "rc": 2, "start": "2020-09-26 10:56:49.073133", "s                                                                                                                       tderr": "ls: cannot access '/tmp/does-not-exist': No such file or directory", "stderr_lines": ["ls: cannot access '/tm                                                                                                                       p/does-not-exist': No such file or directory"], "stdout": "", "stdout_lines": []}

TASK [Recovery block] ************************************************************************************************
ok: [server1] => {
    "msg": "something failed, restoring vsftpd.conf from backup"
}

TASK [Restoring vsftpd.conf] *****************************************************************************************
changed: [server1]

TASK [Restarting vsftpd] *********************************************************************************************
changed: [server1]

PLAY RECAP ***********************************************************************************************************
server1                    : ok=7    changed=4    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

As expected TASK 1 and TASK 2 have successfully executed while TASK 3 has failed. Since one of the task in our block has failed, the rescue block is executed which will restore our vsftpd.conf file. Lastly the always block is executed to restart the vsftpd service.

 

We can also connect to server1 and check if vsftpd.conf.bkp is created:

[ansible@server-1 ~]$ sudo ls -l /etc/vsftpd/vsftpd.conf*
-rw------- 1 root root 5098 May 14  2019 /etc/vsftpd/vsftpd.conf
-rw-r--r-- 1 root root 5098 May 14  2019 /etc/vsftpd/vsftpd.conf.bkp

 

What's Next

Next in our Ansible Tutorial we will learn about include and import module in Ansible with mutliple examples.

 

Leave a Comment

Please use shortcodes <pre class=comments>your code</pre> for syntax highlighting when adding code.