Search

Top 60 Oracle Blogs

Recent comments

Ansible tips’n’tricks: executing related tasks together

I have recently written an ansible playbook to apply one-off patches to an Oracle Home. While doing this, I hit a little snag that needed ironing out. Before continuing this post, it’s worth pointing out that I’m on:

$ ansible --version
ansible 2.6.5

And it’s Ansible on Fedora.

Most likely the wrong way to do this …

So after a little bit of coding my initial attempt looked similar to this:

$ cat main.yml 
---
- hosts: 127.0.0.1
  connection: local
  vars:
    - patchIDs:
        - 123
        - 234
        - 456

  tasks:
  - name: say hello
    debug: msg="hello world"

  - name: unzip patch 
    debug: 
      msg: unzipping patch {{ item }}
    loop: "{{ patchIDs }}"
  
  - name: check patch for conflicts with Oracle Home
    debug: 
      msg: checking patch {{ item }} for conflict with $ORACLE_HOME
    loop: "{{ patchIDs }}"
  
  - name: apply patch
    debug: 
      msg: applying patch {{ item }} to $ORACLE_HOME
    loop: "{{ patchIDs }}"
    failed_when: "item == 456"

This is a stub of course … I have stripped any non-essential code from the playbook but it should give you a first idea.

Can you spot the bug

This looks ok-ish, but there’s a (not quite so hidden) bug in there. And no, I didn’t have a failed_when condition in my playbook that would always evaluate to true with this input :) Consider this output:

$ ansible-playbook main.yml 

PLAY [127.0.0.1] ***************************************************************************

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

TASK [say hello] ***************************************************************************
ok: [127.0.0.1] => {}

MSG:

hello world


TASK [unzip patch] *************************************************************************
ok: [127.0.0.1] => (item=123) => {}

MSG:

unzipping patch 123

ok: [127.0.0.1] => (item=234) => {}

MSG:

unzipping patch 234

ok: [127.0.0.1] => (item=456) => {}

MSG:

unzipping patch 456


TASK [check patch for conflicts with Oracle Home] ******************************************
ok: [127.0.0.1] => (item=123) => {}

MSG:

checking patch 123 for conflict with $ORACLE_HOME

ok: [127.0.0.1] => (item=234) => {}

MSG:

checking patch 234 for conflict with $ORACLE_HOME

ok: [127.0.0.1] => (item=456) => {}

MSG:

checking patch 456 for conflict with $ORACLE_HOME


TASK [apply patch] *************************************************************************
ok: [127.0.0.1] => (item=123) => {}

MSG:

applying patch 123 to $ORACLE_HOME

ok: [127.0.0.1] => (item=234) => {}

MSG:

applying patch 234 to $ORACLE_HOME

failed: [127.0.0.1] (item=456) => {}

MSG:

applying patch 456 to $ORACLE_HOME

fatal: [127.0.0.1]: FAILED! => {}

MSG:

All items completed


PLAY RECAP *********************************************************************************
127.0.0.1                  : ok=4    changed=0    unreachable=0    failed=1

Whoops, that went wrong! As you would predict, the last task failed.

The simulated conflict check is performed for each patch, before the patch is applied in the next step. And this is where the bug in the code can hit you. Imagine you are on a recent PSU, let’s say 180717. The playbook:

  1. Checks patch 123 for incompatibilities with PSU 180717
  2. Followed by patch# 234 …
  3. and eventually 456.

No issues are detected. The next step is to apply patch 123 on top of our fictional PSU 180717, followed by 234 on top of 180717 plus 123, and so on. When it comes to patch 456, a conflict is detected: opatch tells you that you can’t apply 456 on top of 180717 + 234 … I have simulated this with the failed_when clause. The bug in this case is my playbook failing to detect a conflict before actually trying to apply a patch.

I need a procedure!

So what now? In a shell script, I would have defined a function, maybe called it apply_patch. It’s task include the unzipping, checking pre-requisites, and eventually the call to opatch apply. In the body of the script I would have looped over all patches to apply and called apply_patch() for each patch to be applied. In other words unzip/check prerequisites/apply are always performed for a given patch before the code advances to the next patch in sequence.

But how can this be done in Ansible? A little bit of research (and a conversation with @fritshoogland who confirmed what I thought was to be changed) later I noticed that you can include tasks and pass variables to them. So what if I rewrote my code to take advantage of that feature? Here’s the end result:

$ cat main.yml 
---
- hosts: 127.0.0.1
  connection: local
  vars:
    - patchIDs:
        - 123
        - 234
        - 456

  tasks:
  - name: say hello
    debug: msg="hello world"

  - name: include a task
    include_tasks: includedtask.yml
    loop: "{{ patchIDs }}"

Note that I’m using the loop syntax recommended from Ansible 2.5 and later (see section “Migrating from with_X to loop”).

The include file references the variable passed as {{ item }}.

$ cat includedtask.yml 
---
- name: unzip patch {{ item }}
  debug: 
    msg: unzipping patch {{ item }}

- name: check patch {{ item }} for conflicts with Oracle Home
  debug: 
    msg: checking patch {{ item }} for conflict with $ORACLE_HOME

- name: apply patch {{ item }}
  debug: 
    msg: applying patch {{ item }} to $ORACLE_HOME
  failed_when: "item == 456"

Now if I run this playbook, each task (unzip/check prerequisites/apply) is executed for each individual patch.

$ ansible-playbook main.yml

PLAY [127.0.0.1] ***************************************************************************

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

TASK [say hello] ***************************************************************************
ok: [127.0.0.1] => {}

MSG:

hello world


TASK [include a task] **********************************************************************
included: /home/martin/ansible/blogpost/including_task/better/includedtask.yml for 127.0.0.1
included: /home/martin/ansible/blogpost/including_task/better/includedtask.yml for 127.0.0.1
included: /home/martin/ansible/blogpost/including_task/better/includedtask.yml for 127.0.0.1

TASK [unzip patch 123] *********************************************************************
ok: [127.0.0.1] => {}

MSG:

unzipping patch 123


TASK [check patch 123 for conflicts with Oracle Home] **************************************
ok: [127.0.0.1] => {}

MSG:

checking patch 123 for conflict with $ORACLE_HOME


TASK [apply patch 123] *********************************************************************
ok: [127.0.0.1] => {}

MSG:

applying patch 123 to $ORACLE_HOME


TASK [unzip patch 234] *********************************************************************
ok: [127.0.0.1] => {}

MSG:

unzipping patch 234


TASK [check patch 234 for conflicts with Oracle Home] **************************************
ok: [127.0.0.1] => {}

MSG:

checking patch 234 for conflict with $ORACLE_HOME


TASK [apply patch 234] *********************************************************************
ok: [127.0.0.1] => {}

MSG:

applying patch 234 to $ORACLE_HOME


TASK [unzip patch 456] *********************************************************************
ok: [127.0.0.1] => {}

MSG:

unzipping patch 456


TASK [check patch 456 for conflicts with Oracle Home] **************************************
ok: [127.0.0.1] => {}

MSG:

checking patch 456 for conflict with $ORACLE_HOME


TASK [apply patch 456] *********************************************************************
fatal: [127.0.0.1]: FAILED! => {}

MSG:

applying patch 456 to $ORACLE_HOME


PLAY RECAP *********************************************************************************
127.0.0.1                  : ok=13   changed=0    unreachable=0    failed=1 

This way, I should be able to stop the playbook as soon as the pre-requisite conflict checker has completed.