--- # Playbook: ora_standalone_patch.yml # Usage: ansible-playbook playbooks/ora_standalone_patch.yml -e "server=togoria oracle_sid=NABOOPRD opatch_zip=/mnt/yavin4/kit/Oracle/opatch/p6880880_190000_Linux-x86-64.zip dbru_zip=/mnt/yavin4/kit/Oracle/Oracle_Database_19/patch/GIRU_19.29/p38298204_190000_Linux-x86-64.zip" - name: Gather facts for a specific inventory host hosts: "{{ server }}" gather_facts: yes become: true become_user: oracle become_method: su pre_tasks: - name: Ensure 'server' variable is provided assert: that: server is defined fail_msg: "Please provide the server via -e 'server='" - name: Ensure 'oracle_sid' variable is provided assert: that: oracle_sid is defined fail_msg: "Please provide the oracle_sid via -e 'oracle_sid='" - name: Ensure 'opatch_zip' variable is provided assert: that: opatch_zip is defined fail_msg: "Please provide the opatch_zip via -e 'opatch_zip='" - name: Ensure 'dbru_zip' variable is provided assert: that: dbru_zip is defined fail_msg: "Please provide the dbru_zip via -e 'dbru_zip='" tasks: - name: Show selected system facts debug: msg: hostname: "{{ (ansible_facts.nodename | default(ansible_facts.hostname)) | default('unknown') }}" os_family: "{{ ansible_facts.os_family | default(ansible_facts.distribution | default('unknown')) }}" operating_system: "{{ ansible_facts.distribution | default('unknown') }} {{ ansible_facts.distribution_version | default('') }}" ip_address: "{{ ansible_default_ipv4.address | default('unknown') }}" hardware_model: "{{ ansible_facts['machine'] | default('unknown') }}" cpu_count: "{{ ansible_facts['processor_vcpus'] | default(ansible_facts['processor_count'] | default('unknown')) }}" ram_gb: "{{ (ansible_facts['memtotal_mb'] / 1024) | int | default('unknown') }}" - name: Source oraenv and display OPatch informations shell: | . oraenv <<< {{ oracle_sid }} && $ORACLE_HOME/OPatch/opatch version && echo && echo && $ORACLE_HOME/OPatch/opatch lspatches register: opatch_output when: oracle_sid is defined - name: Display OPatch output debug: var: opatch_output.stdout_lines when: oracle_sid is defined - name: Check Oracle instance status via sqlplus as SYSDBA shell: | . oraenv <<< {{ oracle_sid }} >/dev/null 2>&1 $ORACLE_HOME/bin/sqlplus -s / as sysdba <<'SQL' set heading off feedback off verify off pagesize 0 select status from v$instance; exit SQL register: instance_status changed_when: false when: oracle_sid is defined - name: Show Oracle instance status debug: msg: "Oracle instance {{ oracle_sid }} status: {{ instance_status.stdout | trim | default('unknown') }}" when: oracle_sid is defined - name: Fail if Oracle instance is not OPEN fail: msg: "Oracle instance {{ oracle_sid }} is not OPEN (status: {{ instance_status.stdout | default('unknown') }})" when: oracle_sid is defined and ("OPEN" not in (instance_status.stdout | default(''))) - name: List PDBs (if multitenant) shell: | . oraenv <<< {{ oracle_sid }} >/dev/null 2>&1 $ORACLE_HOME/bin/sqlplus -s / as sysdba <<'SQL' set heading off feedback off verify off pagesize 0 select name||':'||open_mode from v$pdbs where name!='PDB$SEED'; exit SQL register: pdb_list failed_when: false changed_when: false when: oracle_sid is defined - name: Determine if database is multitenant set_fact: is_multitenant: "{{ (pdb_list.stdout is defined) and ('ORA-' not in pdb_list.stdout) and (pdb_list.stdout | trim != '') }}" when: oracle_sid is defined - name: Show PDB list (if multitenant) debug: var: pdb_list.stdout_lines when: oracle_sid is defined and is_multitenant - name: Check for non-OPEN PDBs shell: | . oraenv <<< {{ oracle_sid }} >/dev/null 2>&1 $ORACLE_HOME/bin/sqlplus -s / as sysdba <<'SQL' set heading off feedback off verify off pagesize 0 select name||':'||open_mode from v$pdbs where name!='PDB$SEED' and upper(open_mode) not like '%READ WRITE%'; exit SQL register: pdb_not_open failed_when: false changed_when: false when: oracle_sid is defined and is_multitenant - name: Filter non-empty PDB results set_fact: pdb_not_open_filtered: "{{ pdb_not_open.stdout_lines | select('match', '^.+:.+$') | list }}" when: oracle_sid is defined and is_multitenant - name: Fail if any PDB is not OPEN fail: msg: "PDB(s) not OPEN: {{ pdb_not_open_filtered }}" when: oracle_sid is defined and is_multitenant and (pdb_not_open_filtered | length) > 0 - name: Remove $ORACLE_HOME/OPatch directory shell: | . oraenv <<< {{ oracle_sid }} if [ -d "$ORACLE_HOME/OPatch" ]; then rm -rf "$ORACLE_HOME/OPatch" echo removed else echo not_found fi args: executable: /bin/bash register: remove_opatch changed_when: "'removed' in remove_opatch.stdout" failed_when: false when: oracle_sid is defined - name: Show OPatch removal result debug: msg: "OPatch removal: {{ remove_opatch.stdout | default('no_output') }}" when: oracle_sid is defined - name: Copy OPatch zip to remote host copy: src: "{{ opatch_zip }}" dest: "/tmp/{{ opatch_zip | basename }}" mode: '0644' when: opatch_zip is defined - name: Extract OPatch zip into $ORACLE_HOME shell: | . oraenv <<< {{ oracle_sid }} unzip -o "/tmp/{{ opatch_zip | basename }}" -d "$ORACLE_HOME" args: executable: /bin/bash register: opatch_unzip changed_when: opatch_unzip.rc == 0 and (opatch_unzip.stdout is defined) failed_when: opatch_unzip.rc != 0 when: opatch_zip is defined - name: Remove uploaded OPatch zip from remote file: path: "/tmp/{{ opatch_zip | basename }}" state: absent when: opatch_zip is defined - name: Source oraenv and display OPatch informations shell: | . oraenv <<< {{ oracle_sid }} && $ORACLE_HOME/OPatch/opatch version && echo && echo && $ORACLE_HOME/OPatch/opatch lspatches register: opatch_output when: oracle_sid is defined - name: Display OPatch output debug: var: opatch_output.stdout_lines when: oracle_sid is defined - name: Remove /app/oracle/staging_area if present file: path: /app/oracle/staging_area state: absent - name: Recreate /app/oracle/staging_area file: path: /app/oracle/staging_area state: directory mode: '0755' - name: Check local dbru_zip path exists on controller (only if provided) stat: path: "{{ dbru_zip }}" delegate_to: localhost run_once: true register: local_zip_stat when: dbru_zip is defined - name: Fail if local dbru_zip file not found on controller fail: msg: "Local zip file {{ dbru_zip }} not found on controller" when: dbru_zip is defined and (local_zip_stat.stat is not defined or not local_zip_stat.stat.exists) run_once: true - name: Copy local dbru_zip to remote staging_area copy: src: "{{ dbru_zip }}" dest: "/app/oracle/staging_area/{{ dbru_zip | basename }}" mode: '0644' when: dbru_zip is defined - name: Unarchive uploaded zip into staging_area unarchive: src: "/app/oracle/staging_area/{{ dbru_zip | basename }}" dest: /app/oracle/staging_area remote_src: yes when: dbru_zip is defined - name: Remove uploaded zip from remote staging_area file: path: "/app/oracle/staging_area/{{ dbru_zip | basename }}" state: absent when: dbru_zip is defined - name: Find extracted directories under staging_area find: paths: /app/oracle/staging_area file_type: directory depth: 1 register: staging_dirs when: dbru_zip is defined - name: Show staging directories found debug: var: staging_dirs.files when: dbru_zip is defined - name: Find all subdirectories inside extracted main dirs (depth=2) shell: | find /app/oracle/staging_area -mindepth 2 -maxdepth 2 -type d -print args: executable: /bin/bash register: all_subdirs changed_when: false when: dbru_zip is defined - name: Show all subdirectories to process debug: var: all_subdirs.stdout_lines when: dbru_zip is defined and all_subdirs is defined - name: Apply OPatch for each extracted subdirectory (clean chdir) shell: | . oraenv <<< {{ oracle_sid }} export PATH=$PATH:$ORACLE_HOME/OPatch if [ -x "$ORACLE_HOME/OPatch/opatch" ]; then $ORACLE_HOME/OPatch/opatch apply -silent else echo "opatch_not_found" exit 2 fi args: executable: /bin/bash chdir: "{{ item }}" loop: "{{ all_subdirs.stdout_lines | default([]) }}" loop_control: label: "{{ item }}" register: opatch_apply_results failed_when: false changed_when: false when: dbru_zip is defined and all_subdirs is defined and (all_subdirs.stdout_lines | length) > 0 - name: Show OPatch apply results (stdout lines) debug: var: opatch_apply_results.results | map(attribute='stdout_lines') | list when: dbru_zip is defined and opatch_apply_results is defined - name: Collect failed OPatch applies set_fact: failed_applies: "{{ opatch_apply_results.results | selectattr('rc','ne',0) | map(attribute='item') | list }}" when: dbru_zip is defined and opatch_apply_results is defined - name: Fail if any OPatch apply failed fail: msg: "OPatch apply failed in: {{ failed_applies }}" when: dbru_zip is defined and failed_applies is defined and (failed_applies | length) > 0