Ansible Patching for DBAs

Oracle 19.28 Ansible Playbooks for Grid Infrastructure and Database Patching

In my previous post, I introduced my approach and shared the 19.26 version of these playbooks. This time, I’m publishing an updated version for Oracle 19.28 Release Update, along with improvements based on real-world usage and feedback.

As always, the goal is simple:
* Make patching predictable, repeatable, and stress-free.

This release includes updates aligned with the latest RU cycle and enhancements in automation logic:

  • Oracle 19.28 Release Update
  • Latest MRP (Monthly Recommended Patches)
  • Updated one-off patches (Doc ID 555.1)
  • Improvements in precheck validations
  • Better handling of edge cases during patching
  • Enhancements for RAC environments

I continue to follow my standard approach:

👉 Stay one RU behind (n-1 strategy) and apply the latest MRP on top.
This provides a good balance between stability and security.

Patching is one of those tasks that is simple in theory but painful in practice.

Using Ansible gives:

  • Consistency → Same steps every time
  • Automation → Less manual intervention
  • Repeatability → Easier rollback & retry
  • Confidence → Especially in multi-node RAC setups

As I mentioned before, Ansible is agentless and designed for orchestration, which makes it particularly powerful for DBAs managing multiple servers.

My previous posts to power up:

Oracle Database Patching Tips

Oracle Grid Patching by using Ansible

Oracle Database Patching by Using Ansible

Automate Oracle Database Patching with Ansible Playbooks – 19.24

Ansible Playbooks for Oracle Grid and Database Release Update (19.26 with MRP & One-Off Fixes)

For Grid Infrastructure patching, you’ll need to download the following setup files from Oracle Support. Place them under the /u01/app/Setup directory, which should be owned by the ansible user. That’s all for the preparation phase. For a more detailed explanation, you can refer to my blog post: Oracle Grid Patching by using Ansible.

  • Grid Home Setup – LINUX.X64_193000_grid_home.zip    
  • Grid Infrastructure Release Update 19.28.0.0.250719 37957391 (GIRU System Patch involves DBRU) – p37957391_190000_Linux-x86-64.zip
  • GI MRP 19.28.0.0.251021 38509394 (GIMRP involve Database Montly Recommended Paches) – p38509394_1928000DBRU_Linux-x86-64.zip
  • OPatch 12.2.0.1.48 (Version 48) –  p6880880_122010_Linux-x86-64.zip
  • Recommended one of patch 34672698 – p34672698_1928000DBRU_Linux-x86-64.zip

To begin and finish the patching process, simply run the main playbook. It will complete all necessary steps with a single command:

[ansible@blt01 ansible]$ ansible-playbook -i hosts projects/dbru/apply_grid_update_main.yml
[ansible@blt01 ansible]$ cat projects/dbru/apply_grid_update_main.yml
- name: Import playbook to prepare new Oracle Grid Infrastructure Home
ansible.builtin.import_playbook: 1_prepare_grid_allnodes.yml
- name: Import playbook to apply Oracle Grid Infrastructure Release Update
ansible.builtin.import_playbook: 2_apply_giru_onfirstnode.yml
- name: Import playbook to finish Oracle Grid Infrastructure Release Update
ansible.builtin.import_playbook: 3_finish_giru_allnodes.yml
- name: Import playbook to start Database Recovery Process on Standby Databases
ansible.builtin.import_playbook: 4_start_recover_allnodes.yml
- name: Import playbook to remove old grid home
ansible.builtin.import_playbook: 5_remove_giru_allnodes.yml

Prepare Step – 1_prepare_grid_allnodes.yml

- name: Prepare new grid home directories
hosts: allhosts
remote_user: root
vars:
new_grid_home: /u01/app/19.28/grid
patch_dir: /u01/app/Setup
tasks:
- name: Ensure patch directory exists
ansible.builtin.file:
path: "{{ patch_dir }}"
state: directory
owner: oracle
group: oinstall
- name: Ensure new grid home directory exists
ansible.builtin.file:
path: "{{ new_grid_home }}"
state: directory
owner: oracle
group: oinstall

Apply Grid Infrastructure Release Update using Out of Place Patching – 2_apply_giru_onfirstnode.yml

- name: Apply Oracle Grid Infrastructure Release Update
hosts: allhosts
remote_user: oracle
run_once: true
vars:
u01_size_mb: 13356
new_grid_home: /u01/app/19.28/grid
old_grid_home: /u01/app/19.26/grid
patch_dir: /u01/app/Setup
source_patch_dir: /u01/app/Setup
tasks:
- name: Copy setup files to target server
ansible.builtin.copy:
src: "{{ source_patch_dir }}/"
dest: "{{ patch_dir }}/"
owner: oracle
group: oinstall
- name: Unzip new grid home
ansible.builtin.unarchive:
src: "{{ patch_dir }}/LINUX.X64_193000_grid_home.zip"
dest: "{{ new_grid_home }}"
remote_src: yes
- name: Unzip OPatch to new grid home
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p6880880_190000_Linux-x86-64.zip"
dest: "{{ new_grid_home }}"
remote_src: yes
- name: Prepare patches directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: oracle
group: oinstall
loop:
- "{{ patch_dir }}/GIRU"
- "{{ patch_dir }}/GIRUOneOff"
- "{{ patch_dir }}/GIMRP"
- name: Unzip GIRU system patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p37957391_190000_Linux-x86-64.zip"
dest: "{{ patch_dir }}/GIRU"
remote_src: yes
- name: Unzip GIMRP patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p38509394_1928000DBRU_Linux-x86-64.zip"
dest: "{{ patch_dir }}/GIMRP"
remote_src: yes
- name: Unzip GIRU oneoff patch ORA-800
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p34672698_1928000DBRU_Linux-x86-64.zip"
dest: "{{ patch_dir }}/GIRUOneOff"
remote_src: yes
- name: Register cluster nodes for response file
ansible.builtin.shell:
cmd: "{{ old_grid_home }}/bin/olsnodes | tr '\n' ',' | sed 's/,$//'"
register: cluster_nodes
- name: Register OSDBA value for response file
ansible.builtin.shell:
cmd: "grep 'OSDBA=' {{ old_grid_home }}/install/response/grid_20*.rsp"
register: osdba
- name: Register OSOPER value for response file
ansible.builtin.shell:
cmd: "grep 'OSOPER=' {{ old_grid_home }}/install/response/grid_20*.rsp"
register: osoper
- name: Register OSASM value for response file
ansible.builtin.shell:
cmd: "grep 'OSASM=' {{ old_grid_home }}/install/response/grid_20*.rsp"
register: osasm
- name: Create contents of response file
ansible.builtin.copy:
dest: "{{ patch_dir }}/grid_oop.rsp"
content: |
oracle.install.responseFileVersion=/oracle/install/rspfmt_crsinstall_response_schema_v19.0.0
INVENTORY_LOCATION=/u01/app/oraInventory
oracle.install.option=CRS_SWONLY
ORACLE_BASE=/u01/app/oracle
{{ osdba.stdout }}
{{ osoper.stdout }}
{{ osasm.stdout }}
oracle.install.crs.config.clusterNodes={{ cluster_nodes.stdout }}
ORACLE_HOME={{ new_grid_home }}
oracle.install.crs.config.ClusterConfiguration=STANDALONE
oracle.install.crs.config.configureAsExtendedCluster=false
oracle.install.crs.config.gpnp.configureGNS=false
oracle.install.crs.config.autoConfigureClusterNodeVIP=false
- name: Clean up grid_home setup zip files
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "{{ patch_dir }}/LINUX.X64_193000_grid_home.zip"
- "{{ patch_dir }}/p37957391_190000_Linux-x86-64.zip"
- "{{ patch_dir }}/p38509394_1928000DBRU_Linux-x86-64.zip"
- "{{ patch_dir }}/p34672698_1928000DBRU_Linux-x86-64.zip"
- name: Check u01 free disk space
ansible.builtin.shell:
cmd: "df -P /u01 | awk 'END { print $4 }'"
register: u01size
failed_when: u01size.stdout | int < u01_size_mb * 1024
- name: Apply GIRU and oneoffs
ansible.builtin.shell:
cmd: |
{{ new_grid_home }}/gridSetup.sh -silent \
-ignorePrereqFailure -waitforcompletion \
-applyRU {{ patch_dir }}/GIRU/37957391 \
-applyOneOffs {{ patch_dir }}/GIMRP/38509394/36913519,{{ patch_dir }}/GIMRP/38509394/38311920,{{ patch_dir }}/GIRUOneOff/34672698 \
-responsefile {{ patch_dir }}/grid_oop.rsp
environment:
CV_ASSUME_DISTID: OL7
NEWGRIDHOME: "{{ new_grid_home }}"
register: apply_giru
failed_when: "'Successfully Setup Software' not in apply_giru.stdout"
- name: Clean up grid_home setup and patch files directory and contents
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "{{ patch_dir }}/GIRU"
- "{{ patch_dir }}/GIRUOneOff"
- "{{ patch_dir }}/GIMRP"
- name: Output patching success message
ansible.builtin.debug:
msg: "Patching succeeded. InsaneDBA was here."
when: apply_giru.rc in [6,0]

Switch to new Grid Home – 3_finish_giru_allnodes.yml

- name: Finish Oracle Grid Infrastructure Release Update
hosts: allhosts
remote_user: root
vars:
crs_value: CRS="true"/>
new_grid_home: /u01/app/19.28/grid
old_grid_home: /u01/app/19.26/grid
patch_dir: /u01/app/Setup
tasks:
- name: Register cluster nodes for response file
ansible.builtin.shell:
cmd: "{{ old_grid_home }}/bin/olsnodes | tr '\n' ',' | sed 's/,$//'"
register: cluster_nodes
- name: Tag the new grid home in inventory
ansible.builtin.shell:
cmd: |
$NEWGRIDHOME/gridSetup.sh -silent \
-switchGridHome oracle.install.option=CRS_SWONLY \
ORACLE_HOME={{ new_grid_home }} \
oracle.install.crs.config.clusterNodes={{ cluster_nodes.stdout }} \
oracle.install.crs.rootconfig.executeRootScript=false
become: yes
become_user: oracle
environment:
NEWGRIDHOME: "{{ new_grid_home }}"
- name: Check CRS flag in inventory
ansible.builtin.shell:
cmd: |
grep "{{ new_grid_home }}" /u01/app/oraInventory/ContentsXML/inventory.xml |
grep CRS |
awk 'END { print $6 }'
register: crsflag
failed_when: crsflag.stdout != crs_value
- name: Execute root.sh and start grid from new home
ansible.builtin.command:
cmd: "{{ new_grid_home }}/root.sh"
throttle: 1
- name: Remove response file from stage directory
ansible.builtin.file:
path: "{{ patch_dir }}/grid_oop.rsp"
state: absent
- name: Copy old grid home dependent files to the new grid home
ansible.builtin.copy:
src: "{{ old_grid_home }}/sqlplus/admin/glogin.sql"
dest: "{{ new_grid_home }}/sqlplus/admin/"
owner: oracle
group: oinstall
remote_src: yes
- name: Replace old_grid_home value in .bashrc files with the new_gridhome value
ansible.builtin.replace:
path: "{{ item }}"
regexp: "{{ old_grid_home }}"
replace: "{{ new_grid_home }}"
loop:
- /root/.bashrc
- /home/oracle/.bashrc

Start Database Recovery Process on Standby Databases – it will only run on standby databases, otherwise it will not do anything. 4_start_recover_allnodes.yml

- name: Start Database Recovery Process on Standby Databases
hosts: allhosts
remote_user: oracle
vars:
db_home: /u01/app/oracle/product/19.26/dbhome_1
recovery_script_dest_dir: /home/oracle
recovery_script_dir: /etc/ansible/projects
patch_dir: /u01/app/Setup
tasks:
- name: Register instances
ansible.builtin.shell: "ps -ef | grep ora_pmon | grep -v grep | awk '{ print $NF }' | cut -c10-50"
register: find_instances
- name: Copy start recovery file to remote server
ansible.builtin.copy:
src: "{{ recovery_script_dir }}/start_recover_onremote.sh"
dest: "{{ recovery_script_dest_dir }}"
owner: oracle
group: oinstall
mode: '0750'
- name: Debug List found instances
ansible.builtin.debug:
msg: "Found database instance: {{ item }}"
loop: "{{ find_instances.stdout_lines }}"
- name: Start recovery
ansible.builtin.shell: "{{ recovery_script_dest_dir }}/start_recover_onremote.sh {{ item }} {{ db_home }}"
loop: "{{ find_instances.stdout_lines }}"
register: recovery_output
- name: Debug Recovery output
ansible.builtin.debug:
var: item.stdout
loop: "{{ recovery_output.results }}"

Optional : Remove Old Grid Home – 5_remove_giru_allnodes.yml

- name: Deinstall Oracle Grid Infrastructure
hosts: allhosts
remote_user: root
vars:
removed_value: 'REMOVED="T"'
old_grid_home: /u01/app/19.26/grid
patch_dir: /u01/app/Setup
ansible_python_interpreter: /usr/bin/python3
tasks:
- name: Create response file for silent deinstallation
ansible.builtin.shell:
cmd: "{{ old_grid_home }}/deinstall/deinstall -checkonly"
become: yes
become_user: oracle
become_method: su
throttle: 1
- name: Register old grid_home response file for deinstallation
ansible.builtin.shell:
cmd: "ls -lsrt /tmp/deinstall*/response/*.rsp | tail -1 | awk '{print $NF}'"
register: oldgridresponse
- name: Unlock old grid_home for deinstallation
ansible.builtin.shell:
cmd: "{{ old_grid_home }}/crs/install/rootcrs.sh -unlock -crshome {{ old_grid_home }} -paramfile {{ oldgridresponse.stdout }}"
- name: Recursively change ownership of old_grid_home directory
ansible.builtin.file:
path: "{{ old_grid_home | dirname }}"
state: directory
recurse: yes
owner: oracle
group: oinstall
- name: Deinstall old grid_home
ansible.builtin.expect:
command: "/bin/bash -c '{{ old_grid_home }}/deinstall/deinstall'"
responses:
'Do you want to continue': 'y'
echo: yes
timeout: 600
become: yes
become_user: oracle
run_once: true
register: deinstall_result
- name: Check inventory whether old_grid_home is tagged with removed flag
ansible.builtin.shell:
cmd: "grep '{{ old_grid_home }}' /u01/app/oraInventory/ContentsXML/inventory.xml | awk '{ print $NF }'"
register: removedflag
- name: Remove old grid_home directory
ansible.builtin.file:
path: "{{ old_grid_home | dirname }}"
state: absent
when: deinstall_result.rc == 0 or removedflag.stdout == removed_value

For Database patching, you’ll need to download the following setup files from Oracle Support. Place them under the /u01/app/Setup directory, which should be owned by the ansible user. That’s all for the preparation phase. For a more detailed explanation, you can refer to my blog post: Oracle Database Patching by Using Ansible.

  • Database home Setup – LINUX.X64_193000_db_home.zip
  • Grid Infrastructure Release Update 19.28.0.0.250719 37957391 (GIRU System Patch involves DBRU) – p37957391_190000_Linux-x86-64.zip
  • GI MRP 19.28.0.0.251021 38509394 (GIMRP involve Database Montly Recommended Paches) – p38509394_1928000DBRU_Linux-x86-64.zip
  • OPatch 12.2.0.1.48 (Version 48) –  p6880880_122010_Linux-x86-64.zip
  • Data Pump Bundle Patch – p38170982_1928000DBRU_Generic.zip
  • Spatial Bundle Patch – p38599552_1928000DBRU_Linux-x86-64.zip
  • Oracle Text Bundle Patch – p38219529_1928000DBRU_Linux-x86-64.zip
  • OJVM Patch – p37847857_190000_Linux-x86-64.zip
  • Recommended one of patch – p34672698_1928000DBRU_Linux-x86-64.zip

To begin and finish the patching process, simply run the main playbook. It will complete all necessary steps with a single command:

- name: Import playbook to prepare new Oracle Database Home
ansible.builtin.import_playbook: 1_prepare_dbru.yml
- name: Import playbook to apply Oracle Oracle Database Release Update
ansible.builtin.import_playbook: 2_apply_dbru_onfirstnode.yml
- name: Import playbook to run root.sh in new Oracle Database Homes
ansible.builtin.import_playbook: 3_apply_dbru_runrootallnodes.yml
- name: Import playbook to finish Oracle Database Release Update
ansible.builtin.import_playbook: 4_finish_dbru_onallnodes.yml
- name: Import playbook to apply Datapatch on Primary Databases
ansible.builtin.import_playbook: 5_apply_datapatch_onprimary.yml
- name: Import playbook to Remove Old DB Home
ansible.builtin.import_playbook: 6_remove_dbru_onallnodes.yml

Prepare Step – 1_prepare_dbru_allnodes.yml

- name: Configure New Oracle Database Homes
hosts: allhosts
remote_user: oracle
vars:
new_db_home: /u01/app/oracle/product/19.28/dbhome_1
patch_dir: /u01/app/Setup
tasks:
- name: Prepare patch directory for setup files
ansible.builtin.file:
path: "{{ patch_dir }}"
state: directory
owner: oracle
group: oinstall
- name: Prepare new database home
ansible.builtin.file:
path: "{{ new_db_home }}"
state: directory
owner: oracle
group: oinstall

Apply Database Release Update using Out of Place Patching – 2_apply_dbru_onfirstnode.yml

- name: Apply Oracle Database Release Update
hosts: allhosts
vars:
u01_size_mb: 13356
new_db_home: /u01/app/oracle/product/19.28/dbhome_1
old_db_home: /u01/app/oracle/product/19.26/dbhome_1
grid_home: /u01/app/19.28/grid
patch_dir: /u01/app/Setup
source_patch_dir: /u01/app/Setup
remote_user: oracle
run_once: true
tasks:
- name: Copy setup files to target server
ansible.builtin.copy:
src: "{{ source_patch_dir }}/"
dest: "{{ patch_dir }}/"
owner: oracle
group: oinstall
- name: Unzip new DB_HOME
ansible.builtin.unarchive:
src: "{{ patch_dir }}/LINUX.X64_193000_db_home.zip"
dest: "{{ new_db_home }}"
remote_src: yes
- name: Unzip OPatch to new DB_HOME
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p6880880_190000_Linux-x86-64.zip"
dest: "{{ new_db_home }}"
remote_src: yes
- name: Prepare patches directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: oracle
group: oinstall
loop:
- "{{ patch_dir }}/GIRU"
- "{{ patch_dir }}/OJVM"
- "{{ patch_dir }}/DBRUOneOff"
- "{{ patch_dir }}/GIMRP"
- name: unzip GIRU system patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p37957391_190000_Linux-x86-64.zip"
dest: "{{ patch_dir }}/GIRU"
remote_src: yes
- name: Unzip OJVM patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p37847857_190000_Linux-x86-64.zip"
dest: "{{ patch_dir }}/OJVM"
remote_src: yes
- name: unzip GIMRP
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p38509394_1928000DBRU_Linux-x86-64.zip"
dest: "{{ patch_dir }}/GIMRP"
remote_src: yes
- name: unzip DBRU oneoff patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p34672698_1928000DBRU_Linux-x86-64.zip"
dest: "{{ patch_dir }}/DBRUOneOff"
remote_src: yes
- name: unzip Data Pump Bundle patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p38170982_1928000DBRU_Generic.zip"
dest: "{{ patch_dir }}/DBRUOneOff"
remote_src: yes
- name: unzip Spatial Bundle patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p38599552_1928000DBRU_Linux-x86-64.zip"
dest: "{{ patch_dir }}/DBRUOneOff"
remote_src: yes
- name: unzip Oracle text Bundle patch
ansible.builtin.unarchive:
src: "{{ patch_dir }}/p38219529_1928000DBRU_Linux-x86-64.zip"
dest: "{{ patch_dir }}/DBRUOneOff"
remote_src: yes
- name: Register cluster nodes for response file
ansible.builtin.shell:
cmd: "{{ grid_home }}/bin/olsnodes | tr '\n' ',' | sed 's/,$//'"
register: cluster_nodes
- name: Register osdba value for response file
ansible.builtin.shell:
cmd: "grep 'OSDBA_GROUP=' {{ old_db_home }}/install/response/db_20*.rsp"
register: osdba
- name: Register osoper value for response file
ansible.builtin.shell:
cmd: "grep 'OSOPER_GROUP=' {{ old_db_home }}/install/response/db_20*.rsp"
register: osoper
- name: Register osbackupdba value for response file
ansible.builtin.shell:
cmd: "grep 'OSBACKUPDBA_GROUP=' {{ old_db_home }}/install/response/db_20*.rsp"
register: osbackupdba
- name: Register osdgdba value for response file
ansible.builtin.shell:
cmd: "grep 'OSDGDBA_GROUP=' {{ old_db_home }}/install/response/db_20*.rsp"
register: osdgdba
- name: Register oskmdba value for response file
ansible.builtin.shell:
cmd: "grep 'OSKMDBA_GROUP=' {{ old_db_home }}/install/response/db_20*.rsp"
register: oskmdba
- name: Register osracdba value for response file
ansible.builtin.shell:
cmd: "grep 'OSRACDBA_GROUP=' {{ old_db_home }}/install/response/db_20*.rsp"
register: osracdba
- name: Create contents of response file
ansible.builtin.copy:
dest: "{{ patch_dir }}/db_oop.rsp"
content: |
oracle.install.responseFileVersion=/oracle/install/rspfmt_dbinstall_response_schema_v19.0.0
oracle.install.option=INSTALL_DB_SWONLY
UNIX_GROUP_NAME=oinstall
INVENTORY_LOCATION=/u01/app/oraInventory
ORACLE_BASE=/u01/app/oracle
ORACLE_HOME={{ new_db_home }}
oracle.install.db.InstallEdition=EE
{{ osdba.stdout }}
{{ osoper.stdout }}
{{ osbackupdba.stdout }}
{{ osdgdba.stdout }}
{{ oskmdba.stdout }}
{{ osracdba.stdout }}
oracle.install.db.CLUSTER_NODES={{ cluster_nodes.stdout }}
SECURITY_UPDATES_VIA_MYORACLESUPPORT=false
DECLINE_SECURITY_UPDATES=true
- name: Clean up DB home zip files
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "{{ patch_dir }}/LINUX.X64_193000_db_home.zip"
- "{{ patch_dir }}/LINUX.X64_193000_grid_home.zip"
- "{{ patch_dir }}/p37957391_190000_Linux-x86-64.zip"
- "{{ patch_dir }}/p37847857_190000_Linux-x86-64.zip"
- "{{ patch_dir }}/p38509394_1928000DBRU_Linux-x86-64.zip"
- "{{ patch_dir }}/p34672698_1928000DBRU_Linux-x86-64.zip"
- "{{ patch_dir }}/p38170982_1928000DBRU_Generic.zip"
- "{{ patch_dir }}/p38599552_1928000DBRU_Linux-x86-64.zip"
- "{{ patch_dir }}/p38219529_1928000DBRU_Linux-x86-64.zip"
- "{{ patch_dir }}/p6880880_122010_Linux-x86-64.zip"
- name: Check u01 free disk space
ansible.builtin.shell:
cmd: "df -P /u01 | awk 'END { print $4 }'"
register: u01size
failed_when: u01size.stdout|int < u01_size_mb * 1024
- name: Apply DBRU and one-offs
ansible.builtin.shell:
cmd: |
{{ new_db_home }}/runInstaller -silent -ignorePrereqFailure -waitforcompletion \
-applyRU {{ patch_dir }}/GIRU/37957391 \
-applyOneOffs \
{{ patch_dir }}/OJVM/37847857,{{ patch_dir }}/GIMRP/38509394/36913519,{{ patch_dir }}/GIMRP/38509394/37019735,{{ patch_dir }}/GIMRP/38509394/37082638,{{ patch_dir }}/GIMRP/38509394/37914344,{{ patch_dir }}/GIMRP/38509394/38039345,{{ patch_dir }}/GIMRP/38509394/38191300,{{ patch_dir }}/GIMRP/38509394/38213005,{{ patch_dir }}/GIMRP/38509394/38284844,{{ patch_dir }}/GIMRP/38509394/38295523,{{ patch_dir }}/GIMRP/38509394/38326922,{{ patch_dir }}/GIMRP/38509394/38327069,{{ patch_dir }}/GIMRP/38509394/38387135,{{ patch_dir }}/GIMRP/38509394/38389186,{{ patch_dir }}/GIMRP/38509394/38410961,{{ patch_dir }}/GIMRP/38509394/38447750,{{ patch_dir }}/DBRUOneOff/34672698,{{ patch_dir }}/DBRUOneOff/38170982,{{ patch_dir }}/DBRUOneOff/38599552,{{ patch_dir }}/DBRUOneOff/38219529 \
-responsefile {{ patch_dir }}/db_oop.rsp
environment:
CV_ASSUME_DISTID: OL7
NEWDBHOME: "{{ new_db_home }}"
register: apply_dbru
failed_when: "'Successfully Setup Software' not in apply_dbru.stdout"
async: 10800
poll: 60
- name: Output patching success message
ansible.builtin.debug:
msg: "Patching succeeded. InsaneDBA was here."
when: apply_dbru.rc in [6,0]

Run root.sh step – 3_apply_dbru_runrootallnodes.yml

- name: Run root.sh in the new Oracle database home
hosts: allhosts
vars:
new_db_home: /u01/app/oracle/product/19.28/dbhome_1
old_db_home: /u01/app/oracle/product/19.26/dbhome_1
remote_user: root
tasks:
- name: Execute root.sh
ansible.builtin.command:
cmd: "{{ new_db_home }}/root.sh"
throttle: 1
- name: Replace old_dbhome value in .bashrc files with the new_dbhome value
ansible.builtin.replace:
path: "{{ item }}"
regexp: "{{ old_db_home }}"
replace: "{{ new_db_home }}"
loop:
- /root/.bashrc
- /home/oracle/.bashrc

Finish Oracle Database Release Update and start databases from new database home – 4_finish_dbru_onallnodes.yml

- name: Finish Database Release Update
hosts: allhosts
vars:
new_db_home: /u01/app/oracle/product/19.28/dbhome_1
old_db_home: /u01/app/oracle/product/19.26/dbhome_1
patch_dir: /u01/app/Setup
recovery_script_dest_dir: /home/oracle
recovery_script_dir: /etc/ansible/projects
remote_user: oracle
tasks:
- name: Remove response file from stage directory
ansible.builtin.file:
path: "{{ patch_dir }}/db_oop.rsp"
state: absent
- name: Copy old db home dependent network files to the new db home
ansible.builtin.copy:
src: "{{ old_db_home }}/network/admin/"
dest: "{{ new_db_home }}/network/admin/"
owner: oracle
group: oinstall
remote_src: yes
- name: Copy old db home dependent dbs parameter files to the new db home
ansible.builtin.copy:
src: "{{ old_db_home }}/dbs/"
dest: "{{ new_db_home }}/dbs/"
owner: oracle
group: oinstall
remote_src: yes
- name: Copy old db home dependent customized files to the new db home
ansible.builtin.copy:
src: "{{ old_db_home }}/sqlplus/admin/glogin.sql"
dest: "{{ new_db_home }}/sqlplus/admin/"
owner: oracle
group: oinstall
remote_src: yes
- name: Relink unified auditing in new_dbhome
ansible.builtin.command:
cmd: "make -f ins_rdbms.mk uniaud_on ioracle"
chdir: "{{ new_db_home }}/rdbms/lib"
environment:
ORACLE_HOME: "{{ new_db_home }}"
- name: Register databases
ansible.builtin.shell:
cmd: "ps -ef | grep ora_pmon | grep -v grep | awk '{ print $NF }' | cut -c10-50 | awk '{print substr($1, 1, length($1)-1)}'"
register: find_databases
- name: Register instances
ansible.builtin.shell:
cmd: "ps -ef | grep ora_pmon | grep -v grep | awk '{ print $NF }' | cut -c10-50"
register: find_instances
- name: Modify databases with srvctl to run from new_dbhome
ansible.builtin.command:
cmd: "srvctl modify database -d {{ item }} -oraclehome {{ new_db_home }}"
loop: "{{ find_databases.stdout_lines }}"
run_once: true
- name: Bounce instances and start from new_dbhome
ansible.builtin.shell:
cmd: |
export dbname=$(echo {{ item }} | awk '{print substr($1, 1, length($1)-1)}')
srvctl stop instance -d $dbname -i {{ item }} -f
srvctl start instance -d $dbname -i {{ item }}
loop: "{{ find_instances.stdout_lines }}"
throttle: 1
- name: Remove patch setup folders from stage directory
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- "{{ patch_dir }}/GIRU"
- "{{ patch_dir }}/DBRUOneOff"
- "{{ patch_dir }}/OJVM"
- "{{ patch_dir }}/GIMRP"
- name: Copy start recovery file to remote server
ansible.builtin.copy:
src: "{{ recovery_script_dir }}/start_recover_onremote.sh"
dest: "{{ recovery_script_dest_dir }}"
owner: oracle
group: oinstall
mode: '0750'
- name: Start recovery
ansible.builtin.command:
cmd: "{{ recovery_script_dest_dir }}/start_recover_onremote.sh {{ item }} {{ new_db_home }}"
loop: "{{ find_instances.stdout_lines }}"
register: recovery_output
- name: Debug Recovery output
ansible.builtin.debug:
var: item.stdout
loop: "{{ recovery_output.results }}"

Apply datapatch – 5_apply_datapatch_onprimary.ymlOptional : Remove Old Database Home – remove_dbru_onallnodes.yml

- name: Apply Data Patch on First Nodes of Primary Databases
hosts: allhosts
vars:
new_db_home: /u01/app/oracle/product/19.28/dbhome_1
db_role: PRIMARY
remote_user: oracle
run_once: true
tasks:
- name: Register instances
ansible.builtin.shell:
cmd: ps -ef | grep ora_pmon | grep -v grep | awk '{ print $NF }' | cut -c10-50
register: find_instances
- name: Apply datapatch
ansible.builtin.shell:
cmd: |
export instname={{ item }}
export dbname=$(echo $instname | awk '{print substr($1, 1, length($1)-1)}')
export inst_id=$(echo $instname | awk '{print substr($1,length($1),1)}')
export dbrole=$(srvctl config database -d $dbname | grep "Database role" | awk '{ print $NF }')
if [ "$inst_id" == 1 ] && [ "$dbrole" == "{{ db_role }}" ]; then
export ORACLE_HOME={{ new_db_home }}
export ORACLE_SID=$instname
$ORACLE_HOME/OPatch/datapatch -verbose
fi
register: datapatch_candidates
loop: "{{ find_instances.stdout_lines }}"
throttle: 1
- name: Debug datapatch output
ansible.builtin.debug:
var: item.stdout
loop: "{{ datapatch_candidates.results }}"

Optional : Remove Old Database Home – 6_remove_dbru_onallnodes.yml

- name: Deinstall old Oracle Database Software and remove old oracle database home
hosts: allhosts
vars:
old_db_home: /u01/app/oracle/product/19.26/dbhome_1
patch_dir: /u01/app/Setup
ansible_python_interpreter: /usr/bin/python3
removed_value: REMOVED="T"/>
remote_user: root
tasks:
- name: Deinstall old_dbhome
become: yes
become_user: oracle
become_method: su
ansible.builtin.expect:
command: /bin/bash -c {{ old_db_home }}/deinstall/deinstall
responses:
'Do you want to continue': 'y'
'Specify the list of database names': ''
echo: yes
timeout: 600
run_once: true
register: deinstall_result
- name: Check inventory whether old_dbhome is tagged with removed flag
ansible.builtin.shell:
cmd: "grep '{{ old_db_home }}' /u01/app/oraInventory/ContentsXML/inventory.xml | awk '{ print $NF }'"
register: removedflag
- name: Remove parent directory of old_db_home
ansible.builtin.file:
path: "{{ old_db_home | dirname }}"
state: absent
environment:
OLDDBHOME: "{{ old_db_home }}"
when: deinstall_result.rc == 0 or removedflag.stdout == removed_value

Content of the start_recover_onremote.sh

#!/bin/bash
export ORACLE_SID=$1
export ORACLE_HOME=$2
$ORACLE_HOME/bin/sqlplus -s /nolog << END_SQL
conn / as sysdba
set serveroutput on feedback off;
DECLARE
inst_id number;
mrp number;
inst_name varchar2(50);
BEGIN
IF sys_context ('userenv','database_role') = 'PHYSICAL STANDBY'
THEN
select instance_number, instance_name into inst_id, inst_name from v\$instance ;
select count(*) into mrp from v\$managed_standby where process='MRP0';
if inst_id = 1 and mrp = 0
THEN
execute immediate 'ALTER DATABASE RECOVER MANAGED STANDBY DATABASE DISCONNECT';
dbms_output.put_line('Recovery started on: ' || inst_name);
ELSE
dbms_output.put_line('No action taken. ');
END IF;
ELSE
dbms_output.put_line('No action taken. ');
END IF;
END;
/
exit;
END_SQL

The playbooks orchestrate the full lifecycle:

  • Preparation
  • Patching
  • Root execution
  • Datapatch
  • Cleanup

Automation does not remove responsibility. Even with Ansible:

  • Always test in non-production environments
  • Validate patch conflicts
  • Ensure backups and fallback plans

Ansible makes things easier, but not risk-free.

You can find the latest version here:

👉 GitHub Repository (Insane-DBAs-Backpack)

Feel free to:

  • Fork
  • Improve
  • Suggest changes

I always appreciate feedback from fellow DBAs. If you are managing Oracle environments and still doing patching manually…

You are working too hard 🙂

Hope it helps.


Discover More from Osman DİNÇ


Comments

Leave your comment