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:
- Checks patch 123 for incompatibilities with PSU 180717
- Followed by patch# 234 …
- 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.