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.