MINI MINI MANI MO
#!/usr/local/bin/perl
#
# $Header: rdbms/admin/sqlpatch/sqlpatch.pm /st_rdbms_18.0/18 2018/09/03 10:30:21 sspulava Exp $
#
# sqlpatch.pm
#
# Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
#
# NAME
# sqlpatch.pm
#
# DESCRIPTION
# This package is a utility package which is used by the sqlpatch.pl
# installation utility and SQL backport tests. This package does all the
# work for patching. It contains the following public exported functions:
#
# initialize(%sqlpatch_parameters)
# Initializes the sqlpatch package based on the input parameters
#
# patch()
# Complete patching based on the initialized parameters.
#
# finalize()
# Cleans up and closes the database connection
#
# usage()
# Prints the usage (via sqlpatch.pl) to the screen and invocation log
#
# sqlpatch_log()
# Logs to the screen and/or invocation log
#
# NOTES
# Primarily used by sqlpatch.pl
#
# MODIFIED (MM/DD/YY)
# sspulava 09/01/18 - For RU patches, load RMAN packages at the end
# surman 07/14/18 - Backport surman_bug-28320117_18.3 from
# apfwkr 06/29/18 - Backport
# apfwkr_blr_backport_27849825_18.2.0.0.180417dbru
# from st_rdbms_18.2.0.0.0dbru
# surman 06/08/18 - Backport
# surman_ci_backport_28098865_18.3.0.0.180717dbru
# from st_rdbms_18.3.0.0.0dbru
# sspulava 08/30/18 - 28553468: Do not try to rollback if SQL version is
# already at FeatureRelease
# sspulava 08/13/18 - 28410364: All pdbs should be retried in case of
# surman 07/13/18 - 28320117: Handle non-English locales
# surman 06/07/18 - 28098865: Fix inventory XML parsing
# surman 05/11/18 - 27999597: App mode and PDBs
# surman 05/22/18 - Backport surman_bug-27999597 from main
# raeburns 05/15/18 - Bug 28006704: Revert dbms_registry build parameters
# surman 05/10/18 - 28000612: Revert FULL_VERSION
# surman 04/25/18 - 27925437: Backout 27587672
# surman 04/18/18 - 27882176: Non existent components are not OK
# apfwkr 04/16/18 - Backport raeburns_bug-27591842 from main
# surman 04/13/18 - RTI 19714523: Add more version constants
# apfwkr 04/13/18 - Backport surman_bug-27587672 from main
# apfwkr 04/05/18 - Backport
# surman_ci_backport_27786772_18.2.0.0.180417dbru
# from st_rdbms_18.2.0.0.0dbru
# apfwkr 06/07/18 - Backport surman_bug-27849825 from main
# surman 04/03/18 - Backport surman_bug-27786772 from st_rdbms_18.0
# surman 04/01/18 - 27786772: patch_directory in original registry
# from st_rdbms_18.2.0.0.0dbru
# surman 03/02/18 - 27591842: Pass build description and ts to ru_apply
# surman 02/23/18 - 27587672: Seconds count
# apfwkr 02/21/18 - Backport surman_bug-27283029 from main
# surman 05/29/18 - 27849825: Recreate dbms_registry before all
# patching
# surman 02/05/18 - 27283029: Add dba_registry_sqlpatch_ru_info
# surman 01/16/18 - 27283029: RUI -> ID
# surman 11/30/17 - 27187652: RUI and CU patches
# surman 11/30/17 - 27200573: Reload rman packages before RU install
# surman 11/21/17 - XbranchMerge surman_bug-26281129 from main
# surman 09/12/17 - 26281129: Support for new release model
# surman 09/01/17 - 26712331: Handle errors in finalize
# surman 07/24/17 - 26499153: Check missing nodes for app patches only
# in app mode
# surman 06/28/17 - 26365451: No duplicate flags
# surman 06/16/17 - 25425451: Pass directory even for rollback
# surman 05/22/17 - LRG 20310071: Better error handling in
# check_global_prereqs
# surman 05/16/17 - 25690207: Merge ignorable errors
# kquinn 05/04/17 - 18474855: validate -pdbs arguments
# surman 05/09/17 - Pass bundledata.xml to set_patch_metadata
# surman 04/26/17 - 25512969: Pass debug to QI
# surman 04/17/17 - 25269268: Update dba_registry_history for bundle
# patches
# surman 04/07/17 - 25507396: Updated requirements for application
# patches
# sanagara 04/06/17 - 25823532: diagnosibility improvements
# kquinn 04/03/17 - 19681455: improve -force handling
# sanagara 03/30/17 - 24374976: correct handling of rollback with errors
# sanagara 03/24/17 - 25661639: fix error message
# sanagara 03/11/17 - RTI 20162585: don't set _kolfuseslf for app patch
# surman 03/06/17 - 25425451: Intelligent bootstrap
# surman 03/01/17 - 25539063: Query patch descriptor as CLOB
# surman 03/01/17 - 25539063: LongReadLen in database handle
# sanagara 03/01/17 - 24798218: concurrent invocation in a CDB
# surman 02/28/17 - 25161298: Retryable bootstrap
# surman 02/27/17 - 25546608: Change to oracle_home at start
# sanagara 02/21/17 - 25303756: set _kolfuseslf to true
# surman 02/14/17 - 24352981: Quotes in patch description
# sanagara 02/08/17 - 24320719: check if db is open before getting lock
# surman 02/07/17 - 24346821: PL/SQL in description
# surman 01/11/17 - 25324543: Support dot releases
# pyam 11/18/16 - pass extra argument to catcon_init
# surman 11/16/16 - 25056052: Errors in comments
# surman 11/15/16 - 24450424: Process read only PDBs
# surman 10/10/16 - 23113885: Add event_value
# surman 10/10/16 - 24693382: Many patches missing nodes
# surman 09/30/16 - 21503113: JSON orchestration logs
# surman 09/27/16 - 24684759: Ensure seed PDB is open before catcon
# init
# surman 09/21/16 - 24437510: Case insensitive comparisons
# surman 08/17/16 - 23533524: Correct bootstrap timeout
# surman 08/16/16 - 24397438: ORA-6502 calling get_pending_activity
# surman 08/01/16 - 23170620: Rework set_patch_metadata and state table
# surman 07/22/16 - 24341018: Use comments column for build label
# surman 07/20/16 - 23170620: Add patch_directory
# surman 07/18/16 - 24308635: Skip partially installed series
# surman 07/12/16 - 24285405: Handle PROMPT in regular expression
# surman 06/29/16 - 23113885: Post patching support
# surman 06/14/16 - 22694961: Application patches
# surman 06/06/16 - 23501901: Switching trains on multiple nodes
# surman 04/27/16 - 23177001: Disable PDB lockdown
# surman 04/13/16 - 22923409: Connect to root before catcon
# surman 04/07/16 - 23025340: Switching trains project
# surman 03/02/16 - 22165897: Rollback all skip rolled back patches
# surman 01/25/16 - 22349063: Add -noqi
# surman 12/22/15 - 22359063: Use patch descriptors
# surman 12/13/15 - Use catconst package
# surman 11/19/15 - 22204310: Fix perl warning
# surman 11/18/15 - 22204310: Release lock after closing catcon
# jerrede 11/17/15 - 21917884: Add Datapatch support for user
# other than default authentication via upgrade
# surman 11/16/15 - Perl warning
# surman 10/19/15 - 21620471: Skip partially installed series
# surman 08/26/15 - 21700056: Update for Perl 5.22
# surman 06/26/15 - 21052955: Exit if read only instance or database
# surman 06/17/15 - 21273804: Add ORA-2289 to ignorable error list
# nmuthukr 06/02/15 - bug 21139090 - disable MP/MT connection for
# sqlpatch
# surman 04/29/15 - 20939028: Change upgrade mode check
# raeburns 03/12/15 - add Query to catconExec
# surman 03/04/15 - 18361221: Add -userid
# surman 02/28/15 - 20440930: Locale-independent directory names
# surman 01/30/15 - 20348653: Partially installed patches
# surman 12/26/14 - 19883092: Add skip_upgrade_check
# surman 12/05/14 - 20099675: Rollback of bundle patches
# surman 10/21/14 - 19849791: Increase description length
# surman 10/06/14 - 19723336: Proper check of upgrade mode
# surman 10/02/14 - 19708632: Install OJVM patches first
# surman 09/11/14 - 19547370: Much better logging
# surman 09/08/14 - 19521006: Bundle patches with different UIDs
# surman 08/29/14 - 19520602: Bundle patch rollback
# surman 08/25/14 - 19501299: Check catcon return codes
# surman 08/18/14 - Always reload dbms_sqlpatch
# surman 08/07/14 - 14643995: Only one invocation at a time
# surman 08/04/14 - 19044962: Handle more errors
# surman 07/21/14 - 19189525: Bootstrapping
# surman 07/10/14 - 19189317: Handle quotes for -pdb
# surman 06/24/14 - 19051526: Add QI prereq
# surman 06/15/14 - 18986292: Fix assignment
# surman 05/20/14 - 18537739: Handle -rollback all
# mpradeep 05/13/14 - 17665122: Check if patches require upgrade mode
# mpradeep 05/05/14 - 18456455 - Replace ORA-20006 with a message
# surman 04/21/14 - 17277459: datapatch replaces catbundle
# surman 03/19/14 - 17665117: Patch UID
# surman 03/14/14 - 17898119: Fix -oh
# surman 03/13/14 - 18396989: Suppress warning on seed open
# mpradeep 03/12/14 - 17992382 - Add check for orabase command
# surman 03/11/14 - 18355572: Exit if prereqs fail and bundle fixes
# surman 02/21/14 - Add -bundle_series
# jerrede 01/22/14 - 17898118: Set PDB$SEED read write.
# surman 12/20/13 - 17974971: Validate correct log file in multitenant
# surman 12/20/13 - 17981677: Add ignorable_errors
# surman 12/10/13 - 17922172: Handle multiple bundles
# jerrede 10/30/13 - Add new catcon.pm parameter
# surman 10/24/13 - 14563594: Add version to registry$sqlpatch
# surman 10/22/13 - Use orabase
# surman 10/16/13 - 17354355: Log directory under cfgtoologs
# surman 10/11/13 - 17596344: Handle large number of patch files
# jerrede 05/20/13 - Bug 17550069: Add new catcon.pm parameter
# surman 09/17/13 - 17442449: Handle RAC better
# surman 08/30/13 - 17354111: Validate catbundle logfiles
# surman 08/06/13 - 17005047: datapatch calls catbundle
# akruglik 07/24/13 - Bug 16603368: catconExec expects 3 more parameters
# jerrede 05/20/13 - Add new catcon.pm parameter
# surman 11/27/12 - 15911401: Trap ORA-20012 on rac
# surman 11/09/12 - 15857388: Fix bind
# surman 10/19/12 - 14787047: CDB support
# surman 09/21/12 - 14372248: Handle sqlplus errors
# surman 09/13/12 - 14624172: Add status column
# surman 09/07/12 - 14563601: DB name and timestamp for logfile
# surman 08/28/12 - 14503324: New QI interface
# surman 07/14/12 - Windows changes
# surman 07/13/12 - 14165102: Creation
#
use strict;
package sqlpatch;
use File::Spec;
use File::Path qw(make_path);
use XML::Simple;
use DBI;
use DBI qw(:sql_types);
use DBD::Oracle qw(:ora_session_modes :ora_types);
use Data::Dumper;
use catcon;
use POSIX qw(strftime);
use Term::ReadKey;
use catconst;
use Sys::SigAction qw( set_sig_handler );
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use IO::String;
use File::Slurp;
use Fcntl;
use JSON;
use Time::HiRes qw(time);
use POSIX;
use Cwd;
####################################################################
# PUBLIC FUNCTIONS #
####################################################################
# -------------------------------- initialize --------------------------------
# NAME
# initialize
#
# DESCRIPTION
# Initializes the sqlpatch package with the user specified parameters,
# passed via a hash. All parameters are passed as a string, and are
# optional.
#
# ARGUMENTS
# $params_ref: Reference to a hash of parameters.
#
# RETURNS
# 0 for success, 1 for prereq failure with the arguments
#
# JSON SUMMARY LOG:
# initialize : {
# databaseConnect : <"SUCCESS" or error string>
# invocationLog : <full path to invocation log>
# parameters : <name/value pairs of parameters>
# buildInfo: : {
# buildVersion : <build version>
# buildLabel : <build label>
# }
# }
sub initialize($);
# ----------------------------- patch ---------------------------------------
# NAME
# patch
#
# DESCRIPTION
# Performs the entire patching process, consisting of:
# * Determine current state if needed (-force not specified)
# * Add patches to installation queue
# * Install patches (using catcon)
# * Validate the resulting logfiles for non-ignorable errors
#
# ARGUMENTS
# None. All parameters should have been set by initialize().
#
# RETURNS
# 0 for success, 1 for prereq failure, 2 for non-ignorable error during
# patch installation.
sub patch();
# ---------------------------- finalize --------------------------------------
# NAME
# finalize
#
# DESCRIPTION
#
# Cleans up from patching, including closing the database connection.
#
# ARGUMENTS
# None
#
# RETURNS
# None
#
# JSON SUMMARY LOG:
# summary : {
# returnCode : <0, 1, 2>
# totalExecutionTime : <total time in seconds>
# failureReason : <text of the failure reason or undef>
# totalInstalledPatches : <total number of patches installed>
# }
sub finalize();
# 19547370: Logging levels
use constant LOG_DEBUG => 1;
use constant LOG_INVOCATION => 2;
use constant LOG_VERBOSE => 3;
use constant LOG_ALWAYS => 4;
# --------------------------- log --------------------------------------------
# NAME
# log
#
# DESCRIPTION
#
# Prints to either the screen or invocation log, or both
#
# ARGUMENTS
# $level: One of the LOG_* constants:
# LOG_DEBUG: Print to stdout and invocation log only if $debug
# is true
# LOG_INVOCATION: Print to invocation log always, also to stdout if
# $debug is true
# LOG_VERBOSE: Print to invocation log always, also to stdout if
# $verbose is true
# LOG_ALWAYS: Print to invocation log and stdout always
# $message: String to print
#
# Bug 25823532: LOG_DEBUG output is written to the newly introduced
# debug log unconditionally. If $debug is true, then LOG_DEBUG behaves
# as before and output is written to stdout and the invocation log.
#
# RETURNS
# None
sub sqlpatch_log($$);
# ----------------------------- usage ---------------------------------------
# NAME
# usage
#
# DESCRIPTION
#
# Prints the usage (via sqlpatch.pl) to the screen and invocation log
#
# ARGUMENTS
# None
#
# RETURNS
# None
sub usage();
####################################################################
# PRIVATE DECLARATIONS #
####################################################################
# Configuration variables (as passed by user)
my @apply_list = (); # User specified list of patches to apply
my @rollback_list = (); # User specified list of patches to roll back
my $force = 0; # Apply or rollback even if already installed
my $prereq_only = 0; # Run prereq checks only, do not install
my $verbose; # Output extra information
my $debug; # Even more debugging information
my $dbh; # Main database connection handle
my @user_ignorable_errors; # 17981677: User specified ignorable errors
my $database_name; # Database name, used to generate unique log files
my $user_oh = undef; # 17898119: User specified oracle_home
my $upgrade_mode_only; # patches that require upgrade mode
my $container_db; # global variable for checking container database
my $full_bootstrap; # 25425451: Call all bootstrap files
my $skip_bootstrap; # 25425451: Skip bootstrap entirely
my @pdb_list = (); # Array of PDBs to be processed
my @retry_pdb_list = (); # 25425451: Array of PDBs to retry
my $version_only = 0; # Output version info and exit
my $skip_upgrade_check; # 19883092: Skip check for upgrade_mode
my $userid; # 18361221: User specified id for connection
my $noqi; # 22359063: Avoid queryable inventory entirely
my @user_pdbs = (); # 24798218: User-specified PDB list
my $allow_pdb_mismatch; # 24798218: Allow patch mismatch between PDB and ROOT
my $database_feature_version;# 26281129: Database feature version
my $feature_release_description; # Description of the feature (base) release
# 26281129: Current RU version in binary. If there are no RUs installed then
# this will be equal to $feature_release_description. This is the key to the
# full RU hash in %all_ru_versions.
my $binary_ru_version_description = undef;
my $binary_ru_missing_nodes = 0; # Set if any RU patch is missing nodes
my $instance_version = undef; # version_full value from v$instance
# 22694961/25507396: Application patching configuration variables
my $app_mode; # True if we are in application patching mode
my $binary_config; # Binary configuration generated by opatchauto
my $connect_string; # Connect string to remote database
my $local_inventory; # True if we need to call opatch lsinventory
# True if we are allowed to issue alter session set container
my $set_container = 1;
# Other useful package global variables
my $work_to_do = 0; # True if there are any patches to install
my $oracle_home; # Value of ORACLE_HOME
my $oracle_base; # Value of ORACLE_BASE
my $global_lockhandle = undef; # 14643995: User lock handle
my $catcon_ok = 1; # 19501299: True if last caton call was successful
my $connected_ok; # 19520602: True if we successfully connected to DB
my $catcon_init = 0; # 19547370: True if catconInit was successful
my $global_failure = 0; # True if initialize() or patch() was unsuccessful
my $binary_config_hash; # 22694961: Hash of binary config XML
my $opatch_inventory_hash; # 25507396: Hash of opatch lsinventory -xml
my $nothing_sql; # 25425451: Value of dbms_registry.nothing_script;
# 22359063: Useful directory locations. These are based on $user_oh or
# $oracle_home as needed
my $sqlpatch_dir; # Full path to sqlpatch directory
my $admin_dir; # Full path to rdbms/admin directory
my $opatch_executable; # Full patch to OPatch
# 19547370: invocation log variables
my $invocation_logdir; # Unique directory for this invocation
my $invocation_handle; # File handle
my $invocation_timestamp; # Timestamp of invocation
my $invocation_log; # Full path to log file
my $invocation_log_ok = 0; # True when invocation log is ready
my @invocation_log_stored; # Array of printed messages before log is ready
my $global_prereq_failure; # Text of global prereq failure, if any
# 25823532: debug log variables
my $debug_log; # Full path to debug log file
my $debug_handle; # File handle
my $debug_log_ok = 0; # True when debug log is ready
# 22165897: These are set if -apply or -rollback is passed on the command
# line without the other also.
my $apply_only = 0;
my $rollback_only = 0;
# 18361221: userid and password to connect
my $db_user = "";
my $db_password = "";
# Controlling events
# If 23113885 is set then calls to the fix control package are skipped
my $event_23113885 = 0;
# 21503113: Orchestration variables
my $orchestration_summary_json; # Full path to orchestration summary
my $orchestration_summary_handle; # Summary file handle
my $orchestration_summary_ok; # True when log is ready
my $orchestration_progress_json; # Full path to orchestration progress
my $orchestration_progress_handle; # Progress file handle
my $orchestration_progress_ok; # True when log is ready
my $start_time = time(); # Start time
my $percent_complete = 0; # Overall percent complete
my $estimated_time_remaining = 0; # Estimated time remaining
my $orchestration_summary_hash; # Overall summary hashref to be written
my $JSON = JSON->new; # JSON reading and writing object
my $global_prereq_failure; # Text of global prereq failure, if any
my $global_return; # Final return code for sqlpatch
# Total number of patches installed (including retries)
my $global_total_patches;
# 27591842: Central place for timestamp format
my $timestamp_format = "YYMMDDHH24MI";
# 22923409: Default timeouts for connect and bootstrap (in seconds)
use constant CONNECT_TIMEOUT => 120;
use constant BOOTSTRAP_TIMEOUT => 120;
# 19219946: Assemble build version and copyright string dynamically
my $build_version =
catconst::CATCONST_BUILD_VERSION . " " .
ucfirst(catconst::CATCONST_BUILD_STATUS);
my $copyright =
"Copyright (c) 2012, " . catconst::CATCONST_CURRENT_YEAR .
", Oracle. All rights reserved.";
# 17981677 and 21273804: We have a known set of ignorable errors which have
# been documented for bundle patches. In addition, the user may specify a set
# of ignorable errors on the command line.
# If the user specifies an ignorable error list (stored in
# @user_ignorable_errors) then this list will be used for all patches to be
# installed. If the user does not specify an ignorable error list, then
# we will use @standard_ignorable_errors for all patches to be installed.
# Both of these are in addition to any ignorable errors in the SQL files
# themselves, which are checked for all patches.
my @standard_ignorable_errors =
(
"ORA-00942", # table or view does not exist
"ORA-00955", # name is already used by an existing object
"ORA-01430", # column being added already exists in table
"ORA-01432", # public synonym to be dropped does not exist
"ORA-01434", # private synonym to be dropped does not exist
"ORA-01435", # user does not exist
"ORA-01917", # user or role '%s' does not exist
"ORA-01920", # user name '%s' conflicts with another user or role name
"ORA-01921", # role name '%s' conflicts with another user or role name
"ORA-01927", # cannot REVOKE privileges you did not grant
"ORA-01952", # system privileges not granted to '%s
"ORA-06512", # at %sline %s
"ORA-02303", # cannot drop or replace a type with type or table dependents
"ORA-02443", # Cannot drop constraint - nonexistent constraint
"ORA-02289", # sequence does not exist
"ORA-04043", # object %s does not exist
"ORA-14452", # attempt to create, alter or drop an index on temporary table already in use
"ORA-29809", # cannot drop an operator with dependent objects
"ORA-29931", # specified association does not exist
"ORA-29830", # operator does not exist
"ORA-29832", # cannot drop or replace an indextype with dependent indexes
"ORA-29844", # duplicate operator name specified
);
# 25161298: Retryable errors. Any of these errors are considered to be
# retryable during bootstrap or patching.
my @retryable_errors =
("ORA-00604", # error occurred at recursive SQL level
"ORA-00060", # deadlock detected while waiting for resource
"ORA-04020", # deadlock detected while trying to lock object
"ORA-04021", # timeout occurred while waiting to lock object
"ORA-04061", # existing state of package body has been discarded
"ORA-04065", # not executed, altered or dropped package body
"ORA-04068", # existing state of packages has been discarded
"ORA-06508", # PL/SQL: could not find program unit being called
);
# Hash of RU versions. This contains information about all of the RU
# versions that have been found. The key is the full version description, i.e.
# "18.2.0.0.0 Release_Update 1804150900", with the following fields:
# $version_description: Nicely formatted string with the complete info
# (same as the hash key)
# $patch_type: RU, RUR, ID, CU
# $version_string: 5 digit version (i.e. 18.3.2.0.0)
# $build_timestamp: Build timestamp
# $build_description: Build description
# @missing_nodes: Array of missing nodes in binary
# $patch_key: Key of this version
# $rollback_files_dir: Name of the entry under rollback_files for this RU
my %all_ru_versions;
# Hash of patch descriptions, indexed by (patchid/uid). Each description is
# itself a hash reference containing the following fields:
# $patchid: Patch ID
# $patchuid: Patch UID
# $patch_key: Patch key (ID/UID)
# $description: Patch description
# $patch_type: Patch type (INTERIM, RU, RUR, ID, CU)
# $build_timestamp: Timestamp specified (used as ID for a CU)
# $build_description: "Release_Update_Revision", etc.
# $feature_version: First digit of version for this patch (i.e. 18 or 19)
# $ru_version: Five digit version to be installed if RU patch, or NONE
# $ru_version_description: Key to entry in %all_ru_versions if RU patch
# @rollback_versions: Array of 5 digit versions under rollback_files
# $patchdir: Full path to the directory containing the patch scripts
# $xml_descriptor: Full path to XML patch descriptor
# $xml_descriptor_hash: Hash ref with contents of descriptor
# $xml_descriptor_source: Location from which the descriptor was found
# $mode: "apply" or "rollback"
# $startup_mode: "normal" or "upgrade"
# $jvm_patch: true if this patch updates OJVM and hence should be run first
# $application_patch: true if this is an application patch
# $flags: patch flags (as seen in dba_registry_sqlpatch)
# $apply_script: Full path to the apply script
# $rollback_script: Full path to the rollback script
# $logdir: Full path to the directory for the log file created when the
# install script is run
# $prereq_ok: 1 if the patch is OK to install (i.e. the script exists)
# $prereq_failed_reason: Reason why $prereq_ok = 0
# @missing_nodes: Array of nodes which do not have the binary portion of
# the patch installed
# $installed_binary: True if the binary portion of the patch is installed
# (on all nodes in RAC)
# $pdb_info: Hash ref of information for this patch in each pdb. The key is
# the pdb name. If we are not in multitenant, the key will be the database
# name. $description->{pdb_info} will always be defined, but the fields
# within it may be undef.
# The hash has the following fields within it:
# $sql_state: Current state of this patch in this PDB. The value is
# "<action>/<status>", i.e. "APPLY/SUCCESS" or
# "ROLLBACK/WITH ERRORS". If the patch is not present in the
# SQL registry for this PDB the value will be undef.
# $ru_info: True if there is an entry in dba_registry_sqlpatch_ru_info
# for this PDB
# $last_action_time: Timestamp of the last action for this patch
# $retry: True if this patch needs to be retried for this PDB
# @components: Array of components that will be processed by this patch
# $files_hash: Hash ref of information about the files in this patch, as read
# from the XML descriptor. The key is the file name, with fields:
# $mode: "apply", "rollback", "apply/rollback"
# $new: True if the file is new, false otherwise
# $sequence: Ordering for the file
# $estimated_time: Time in seconds to run the file
# $component: Component for this file
# $highest_version_description: Key to entry in %all_ru_versions for the
# highest version in which this file appears (only defined for
# RU patches)
# $lowest_version_description: Key to entry in %all_ru_versions for the
# lowest version in which this file appears (only defined for RU patches)
my %patch_descriptions;
# Array of hash refs which represent the patch queue. Each element of the
# array represents a set of patches to be installed for a set of PDBs. If
# the DB is not a container database, then there will be only 1 element in the
# array. Each hash reference contains the following fields:
# @pdbs: Array of PDB names. If the DB is not a container database, there
# will be only one element of the array with the database name.
# $num_pdbs: Number of pdbs to be patched
# $patch_string: Encodes the checks for uniqueness for combining entries
# $combined: true if this entry has been combined with another entry
# @interim_rollbacks, @interim_applys, @ru_installs: Arrays of hash refs
# representing the patches to be installed, in the order in which
# they will be installed. Each install entry contains the following fields:
# $patch_key: Key of patch to be installed
# $mode: apply or rollback
# $source_ru_description: Starting version info
# $target_ru_description: Ending version info
# $num_files: Number of files to be installed
# @install_files: Path from oracle home for each file
# @actual_files: Ultimate path to file to be run
# @notok_components: Array of not OK components to be patched
# $work_to_do: Set if there are patches to be installed in this entry
my @patch_queue;
# 25425451: Same structure as @patch_queue, but for retries (if needed)
my @retry_patch_queue;
# Array of hash refs which represent the results of patch application. Each
# element represents one patch application, and contains the following fields:
# $pdb: PDB in which this patch was installed, or the database name if
# not multitenant
# $patch: Key of the patch which was installed
# $mode: "apply" or "rollback"
# $logfile: Full path to the logfile
# @error_lines: Line numbers in the log file of errors
# @errors: Actual error strings. $errors[$i] occurred at
# line $error_lines[$i]. If there are no errors, @error_lines and @errors
# will be undefined.
# $ru_logfile: Full path to the RU logfile, undef if not RU
# @ru_error_lines, @ru_errors: Same as error_lines and errors
# for the RU logfile
# $status: "SUCCESS", "WITH ERRORS", "BEGIN", "WITH ERRORS (RU)", "END",
# "WITH ERRORS (PRIOR RU)"
# $retryable: True if all errors are retryable
my @patch_results;
# 25425451: Same structure as @patch_results, but for retries (if needed)
my @retry_patch_results;
# Hash of hash refs with information about the pluggable databases to be
# patched. The key is the PDB name, with a hash ref containing the following
# fields. If the DB is not a container database, there will be one entry
# with the key being the database name.
# $startup_mode: "NORMAL", "UPGRADE", "MIGRATE"
# $pdb_name: Name of the PDB (or database if not multitenant)
# $ru_version_description: Key to entry in %all_ru_versions representing the
# current RU installed, or undef if there is no RU installed
# $ru_sql_state: <action>/<status> for the current RU, or undef if no RU
# $ru_last_action_time: Timestamp of the last action for the current RU
# $last_successful_ru_version_description: Key representing the last
# successful RU installed
# %bootstrap_info: 25425451: Hash with the following entries:
# $bootstrapXXX_log: Full path to log of the bootstrap operation for attempt
# XXX (1, 2)
# $bootstrapXXX_results: Hash ref of bootstrap results for attempt XXX
# (same as patch_results entry)
# $build_label: Build label as stored in registry$history
# $dbmssqlpatch_header: ADE checkout header from dbmssqlpatch.sql
# $prvtsqlpatch_header: ADE checkout header from prvtsqlpatch.plb
# $dbmsqopi_header: ADE checkout header from dbmsqopi.sql
# $prvtqopi_header: ADE checkout header from prvtqopi.plb
# $bootstrap_needed: 27999597:true if bootstrap is needed for this PDB
# %component_info: 25037621: Hash with information about the components
# in this PDB. The key is the component ID (CATALOG, JAVAVM, DV, etc.)
# If a component is not present in this hash, it is not present in the
# component registry (dba_registry view). It has the following entries:
# $component_name: Full component name
# $registry_status: Status in dba_registry
# $registry_version: Value of version column in dba_registry
# $registry_version_full: Value of version_full column in dba_registry
# Note that $registry_version and $registry_version_full will have the
# values before patching is done, i.e. they will not match the target
# values after an RU install
# $lockhandle: PDB-specific lock for preventing concurrent datapatch sessions
my %pdb_info;
# 17277459/20099675/22359063:
# Hash of hash refs with information about all bundle series in play. The key
# is the bundle series name, with a hash ref containing the following fields.
# $binary_id: ID installed in binary
# $binary_key: patch key for the binary patch
# @missing_nodes: Nodes for which there is a patch for this series that
# is not installed (in which case the series is skipped)
# $bundledata_xml: 25074866: Contents of bundledata.xml for this series
my %all_series;
# ------------------------------- component_ok -------------------------------
# NAME
# component_ok
#
# DESCRIPTION
# Returns true if the given component is OK for the given PDB, based
# on the component registry status.
#
# ARGUMENTS
# $component: ID of the component to be checked
# $pdb: PDB of the registry. If not multitenant, the database name.
#
# RETURNS
# True if the component is OK, false if not
#
sub component_ok($$) {
my ($component, $pdb) = @_;
sqlpatch_log(LOG_DEBUG, "component_ok entry for $component and $pdb\n");
my %info = %{$pdb_info{$pdb}{component_info}};
# 21421886: Add more comparisons
# 24764876: OPTION OFF should be considered as OK. The statuses
# mentioned here are the ones which are not OK.
# 26281129: Do not compare version as it will not have been updated yet
# 27882176: Non existent components are not OK
if ((!exists($info{$component})) ||
(exists($info{$component}) &&
($info{$component}{registry_status} =~ /REMOVING|REMOVED|DOWNGRADED|DOWNGRADING|NO SCRIPT|INVALID/))) {
sqlpatch_log(LOG_DEBUG, "$component NOT OK\n");
return 0;
}
else {
sqlpatch_log(LOG_DEBUG, "$component OK\n");
return 1;
}
}
# --------------------------- no_components_ok -------------------------------
# NAME
# no_components_ok
#
# DESCRIPTION
# Returns true if none of the components for the given patch are OK for the
# the given PDB, based on the component registry status.
#
# ARGUMENTS
# $patch: Key of the patch to be checked
# $pdb: PDB of the registry. If not multitenant, the database name.
#
# RETURNS
# True if none of the components are OK, false if not
#
sub no_components_ok($$) {
my ($patch, $pdb) = @_;
sqlpatch_log(LOG_DEBUG, "no_components_ok entry for $patch and $pdb\n");
# If no components then we return false
if (scalar @{$patch_descriptions{$patch}->{components}} eq 0) {
sqlpatch_log(LOG_DEBUG,
"no_components_ok no components: returning false\n");
return 0;
}
# Loop over all components for this patch, and if any are OK then
# we return false.
foreach my $component (@{$patch_descriptions{$patch}->{components}}) {
if (component_ok($component, $pdb)) {
sqlpatch_log(LOG_DEBUG, "no_components_ok returning false\n");
return 0;
}
}
sqlpatch_log(LOG_DEBUG, "no_components_ok returning true\n");
sqlpatch_log(LOG_INVOCATION, "Patch $patch for PDB $pdb: No components OK and will be skipped\n");
return 1;
}
# -------------------------------- initialize --------------------------------
# NAME
# initialize
#
# DESCRIPTION
# Initializes the sqlpatch package with the user specified parameters,
# passed via a hash. All parameters are passed as a string, and are
# optional.
#
# ARGUMENTS
# $params_ref: Reference to a hash of parameters
#
# RETURNS
# 0 for success, 1 for prereq failure with the arguments
#
# JSON SUMMARY LOG:
# initialize : {
# databaseConnect : <"SUCCESS" or error string>
# invocationLog : <full path to invocation log>
# parameters : <name/value pairs of parameters>
# buildInfo: : {
# buildVersion : <build version>
# buildLabel : <build label>
# }
# }
sub initialize($) {
my $params_ref = @_[0];
my $ret = 0;
my $o_initialize; # 21503113: Fill in summary orchestration log
my $o_params;
my $initialize_notes; # Notes & warnings to be printed after the "done" line
my $waited_for_pdb_lock = 0;
eval {
$Data::Dumper::Indent = 1; # Somewhat pretty printing
$Data::Dumper::Sortkeys = 1; # Sort output
# We need $debug and $verbose first
# 25823532: For better diagnosibility, we always turn verbose on. The
# command line option is still retained for backward compatibility but
# is ignored.
$verbose = 1;
$debug = $params_ref->{"debug"};
$o_params->{debug} = $debug;
$o_params->{verbose} = $verbose;
$version_only = defined($params_ref->{"version"}) ? 1 : 0;
$o_params->{version_only} = $version_only;
$o_initialize->{buildInfo} =
{"buildVersion" => $build_version,
"buildLabel" => catconst::CATCONST_BUILD_LABEL};
sqlpatch_log(LOG_ALWAYS,
"SQL Patching tool version $build_version on " . (localtime) . "\n");
sqlpatch_log(LOG_ALWAYS, "$copyright\n\n");
if ($version_only) {
sqlpatch_log(LOG_ALWAYS,
"Build label: " . catconst::CATCONST_BUILD_LABEL . "\n");
goto initialize_complete;
}
else {
sqlpatch_log(LOG_INVOCATION,
"Build label: " . catconst::CATCONST_BUILD_LABEL . "\n");
}
sqlpatch_log(LOG_DEBUG, "initialize entry\n");
sqlpatch_log(LOG_DEBUG,
"params_ref: " . Data::Dumper->Dumper(\$params_ref) . "\n");
# 19547370: Establish the value of $oracle_home and $oracle_base first
# so we can start the invocation log.
if (defined($ENV{ORACLE_HOME})) {
$oracle_home = $ENV{ORACLE_HOME};
}
else {
$oracle_home = $ENV{SQLPATCH_ORACLE_HOME};
}
# 17354355: Log directory should be under $ORACLE_BASE rather than
# $ORACLE_HOME if it is defined. We can use the orabase executable to
# determine the value of $ORACLE_BASE. If $ORACLE_BASE is not defined
# then orabase will return the value of $ORACLE_HOME instead.
my $orabase = File::Spec->catfile($oracle_home, "bin", "orabase");
$oracle_base = `$orabase`;
chomp($oracle_base); # Need to remove the newline from running orabase
if ((! -d $oracle_base) || ($oracle_base eq ' ')){
$oracle_base = $oracle_home;
}
$invocation_timestamp = strftime("%Y_%m_%d_%H_%M_%S", localtime);
$invocation_logdir =
File::Spec->catdir($oracle_base, "cfgtoollogs", "sqlpatch",
"sqlpatch_" . $$ . "_" . $invocation_timestamp);
# Create directory if needed
unless (-e $invocation_logdir) {
sqlpatch_log(LOG_DEBUG,
"Creating invocation log dir $invocation_logdir\n");
eval {
make_path($invocation_logdir);
};
if ($@) {
# Could not create directory
$global_prereq_failure =
"Could not create invocation log directory $invocation_logdir: $@";
$ret = 1;
goto initialize_complete;
}
}
# 25823532: Debug log file. This file contains debug mode output
# that is written to unconditionally, regardless of the $debug setting.
$debug_log =
File::Spec->catfile($invocation_logdir, "sqlpatch_debug.log");
unless (open($debug_handle, ">", $debug_log)) {
$global_prereq_failure =
"Cound not open debug log $debug_log for writing";
$ret = 1;
goto initialize_complete;
}
$debug_log_ok = 1;
$invocation_log =
File::Spec->catfile($invocation_logdir, "sqlpatch_invocation.log");
# Log invocation to history log
my $history_file =
File::Spec->catfile($oracle_base, "cfgtoollogs", "sqlpatch",
"sqlpatch_history.txt");
my $history_handle;
unless (open($history_handle, ">>", $history_file)) {
$global_prereq_failure =
"Could not open sqlpatch history file $history_file for writing";
$ret = 1;
goto initialize_complete;
}
print $history_handle
"sqlpatch invocation with log directory $invocation_logdir\n";
close($history_handle);
# 19547370: We want to redirect STDERR to the invocation log also (two
# handles to the same file). So we first open a dummy handle, close it,
# then we can open two handles with append mode.
my $dummy_handle;
unless (open($dummy_handle, ">", $invocation_log)) {
$global_prereq_failure =
"Could not open invocation log $invocation_log for writing";
$ret = 1;
goto initialize_complete;
}
close($dummy_handle);
open($invocation_handle, ">>", $invocation_log);
select $invocation_handle;
$| = 1;
# Save original stderr first
open(SAVED_STDERR, ">&STDERR");
open(STDERR, ">>", $invocation_log);
select STDERR;
$| = 1;
select STDOUT;
$invocation_log_ok = 1;
sqlpatch_log(LOG_VERBOSE,
"Log file for this invocation: $invocation_log\n\n");
$o_initialize->{invocationLog} = $invocation_log;
# Copy parameters from hash to our storage and print to invocation log
sqlpatch_log(LOG_INVOCATION, "SQL Patching arguments:\n");
sqlpatch_log(LOG_INVOCATION, " debug: $debug\n");
sqlpatch_log(LOG_INVOCATION, " verbose: $verbose\n");
if (defined($params_ref->{"apply"})) {
@apply_list = split(/,/, $params_ref->{"apply"});
sqlpatch_log(LOG_INVOCATION,
" apply: " . $params_ref->{"apply"} . "\n");
}
$o_params->{apply} = $params_ref->{apply};
if (defined($params_ref->{"rollback"})) {
@rollback_list = split(/,/, $params_ref->{"rollback"});
sqlpatch_log(LOG_INVOCATION, " rollback: " .
$params_ref->{"rollback"} . "\n");
}
$o_params->{rollback} = $params_ref->{rollback};
$force = defined($params_ref->{"force"}) ? 1 : 0;
sqlpatch_log(LOG_INVOCATION, " force: $force\n");
$o_params->{force} = $force;
$prereq_only = defined($params_ref->{"prereq"}) ? 1 : 0;
sqlpatch_log(LOG_INVOCATION, " prereq: $prereq_only\n");
$o_params->{prereq_only} = $prereq_only;
$upgrade_mode_only = $params_ref->{"upgrade_mode_only"};
sqlpatch_log(LOG_INVOCATION, " upgrade_mode_only: $upgrade_mode_only\n");
$o_params->{upgrade_mode_only} = $upgrade_mode_only;
$user_oh = $params_ref->{"oh"};
sqlpatch_log(LOG_INVOCATION, " oh: $user_oh\n");
$o_params->{oh} = $user_oh;
@user_ignorable_errors = split(',', $params_ref->{"ignorable_errors"});
sqlpatch_log(LOG_INVOCATION, " ignorable_errors: " .
$params_ref->{"ignorable_errors"} . "\n");
$o_params->{ignorable_errors} = $params_ref->{ignorable_errors};
$full_bootstrap = $params_ref->{"bootstrap"};
sqlpatch_log(LOG_INVOCATION, " bootstrap: $full_bootstrap\n");
$o_params->{bootstrap} = $full_bootstrap;
$skip_bootstrap = $params_ref->{"skip_bootstrap"};
sqlpatch_log(LOG_INVOCATION, " skip_bootstrap: $skip_bootstrap\n");
$o_params->{skip_bootstrap} = $skip_bootstrap;
$skip_upgrade_check = $params_ref->{"skip_upgrade_check"};
sqlpatch_log(LOG_INVOCATION,
" skip_upgrade_check: $skip_upgrade_check\n");
$o_params->{skip_upgrade_check} = $skip_upgrade_check;
$userid = $params_ref->{"userid"};
sqlpatch_log(LOG_INVOCATION, " userid: $userid\n");
$o_params->{userid} = $userid;
# 19189317: Strip single and double quotes from -pdbs
my $stripped_pdbs = $params_ref->{"pdbs"};
sqlpatch_log(LOG_INVOCATION, " pdbs: $stripped_pdbs\n");
$o_params->{pdbs} = $stripped_pdbs;
$stripped_pdbs =~ s/\'//g;
$stripped_pdbs =~ s/\"//g;
# 24798218: normalize the given names to upper-case
@user_pdbs = split(',', uc $stripped_pdbs);
sqlpatch_log(LOG_DEBUG, "user_pdbs: @user_pdbs\n");
# 24798218: This parameter allows application of a patch in
# a PDB even if that patch does not exist in CDB$ROOT.
$allow_pdb_mismatch = $params_ref->{"allow_pdb_mismatch"};
sqlpatch_log(LOG_INVOCATION,
" allow_pdb_mismatch: $allow_pdb_mismatch\n");
$o_params->{allow_pdb_mismatch} = $allow_pdb_mismatch;
$noqi = $params_ref->{"noqi"};
sqlpatch_log(LOG_INVOCATION, " noqi: $noqi\n");
$o_params->{noqi} = $noqi;
$app_mode = defined($params_ref->{app}) ? 1 : 0;
sqlpatch_log(LOG_INVOCATION, " app: $app_mode\n");
$o_params->{app} = $app_mode;
$binary_config = $params_ref->{binary_config};
sqlpatch_log(LOG_INVOCATION, " binary_config: $binary_config\n");
$o_params->{binary_config} = $binary_config;
$connect_string = $params_ref->{connect_string};
sqlpatch_log(LOG_INVOCATION, " connect_string: $connect_string\n");
$o_params->{connect_string} = $connect_string;
$local_inventory = $params_ref->{local_inventory};
sqlpatch_log(LOG_INVOCATION, " local_inventory: $local_inventory\n");
$o_params->{local_inventory} = $local_inventory;
# 21503113: Set orchestration variables
if (defined($params_ref->{orchestration_summary})) {
# 25546608: Convert to absolute path
$orchestration_summary_json =
File::Spec->rel2abs($params_ref->{orchestration_summary});
}
else {
$orchestration_summary_json =
File::Spec->catfile($invocation_logdir, "sqlpatch_summary.json");
}
sqlpatch_log(LOG_INVOCATION,
" orchestration_summary: $orchestration_summary_json\n");
$o_params->{orchestration_summary} = $orchestration_summary_json;
if (defined($params_ref->{orchestration_progress})) {
# 25546608: Convert to absolute path
$orchestration_progress_json =
File::Spec->rel2abs($params_ref->{orchestration_progress});
}
else {
$orchestration_progress_json =
File::Spec->catfile($invocation_logdir, "sqlpatch_progress.json");
}
sqlpatch_log(LOG_INVOCATION,
" orchestration_progress: $orchestration_progress_json\n");
$o_params->{orchestration_progress} = $orchestration_progress_json;
sqlpatch_log(LOG_INVOCATION, "\n");
$o_initialize->{parameters} = $o_params;
# 25546608: Change directory to oracle_home first
unless (chdir $oracle_home) {
sqlpatch_log(LOG_ALWAYS, "Cannot chdir to $oracle_home: $!\n");
$ret = 1;
goto initialize_complete;
}
# We now have all the parameters, check them for correctness before
# attempting to connect to the database.
# 21503113: Ensure we can write to the progress and summary orchestration
# logs
if (write_progress("initialize", 5)) {
$ret = 1;
goto initialize_complete;
}
$orchestration_progress_ok = 1;
unless (open($orchestration_summary_handle, ">",
$orchestration_summary_json)) {
$global_prereq_failure =
"Could not open orchestration summary file $orchestration_summary_json for writing";
$ret = 1;
goto initialize_complete;
}
$orchestration_summary_ok = 1;
# 22694961: Raise error if both -app and -bootstrap are specified
if ($app_mode and $full_bootstrap) {
$global_prereq_failure =
"-bootstrap cannot be specified along with -app";
$ret = 1;
goto initialize_complete;
}
if ($app_mode) {
if ($connect_string) {
# 25507396: If -app and -connect_string are both specified then
# the connect string must point to a single PDB, and we are not allowed
# to switch containers because the user will be a local user for that
# PDB.
$set_container = 0;
}
}
else {
# Also raise errors if -binary_config, connect_string, or
# local_inventory are specified without -app
if ($binary_config) {
$global_prereq_failure =
"-binary_config is supported only with -app";
$ret = 1;
goto initialize_complete;
}
if ($connect_string) {
$global_prereq_failure =
"-connect_string is supported only with -app";
$ret = 1;
goto initialize_complete;
}
if ($local_inventory) {
$global_prereq_failure =
"-local_inventory is supported only with -app";
$ret = 1;
goto initialize_complete;
}
}
# 25507396: If -connect_string is specified then -userid and
# either -local_inventory or -binary_config must also be specified.
if ($connect_string) {
if (!$userid) {
$global_prereq_failure =
"-userid must be specified when -connect_string is specified";
$ret = 1;
goto initialize_complete;
}
if (!$local_inventory && !$binary_config) {
$global_prereq_failure =
"-local_inventory or -binary_config must be specified when -connect_string is specified";
$ret = 1;
goto initialize_complete;
}
}
# 22165897: If -apply is specified without -rollback, or -rollback is
# specified without -apply, set $apply_only or $rollback_only.
if (@apply_list && !@rollback_list) {
sqlpatch_log(LOG_DEBUG, "Setting apply_only to 1\n");
$apply_only = 1;
}
if (@rollback_list && !@apply_list) {
sqlpatch_log(LOG_DEBUG, "Setting rollback_only to 1\n");
$rollback_only = 1;
}
# 22359063/19681455: If neither apply nor rollback is specified, then
# -force and -noqi are not allowed
if (!defined($params_ref->{"apply"}) &&
!defined($params_ref->{"rollback"})) {
if (defined($noqi)) {
$global_prereq_failure =
"-apply and/or -rollback must be specified when -noqi is specified";
$ret = 1;
goto initialize_complete;
}
if ($force) {
$global_prereq_failure =
"-apply and/or -rollback must be specified when -force is specified";
$ret = 1;
goto initialize_complete;
}
}
# 22359063 & 17898119: Determine the value of our useful directories
# based on the user specified -oh if needed. Note that we only use -oh
# for the location of the install scripts and bundledata files, and
# not the log directory.
if ($user_oh ne "") {
$sqlpatch_dir = File::Spec->catdir($user_oh, "sqlpatch");
$admin_dir = File::Spec->catdir($user_oh, "rdbms", "admin");
$opatch_executable = File::Spec->catfile($user_oh, "OPatch", "opatch");
}
else {
$sqlpatch_dir = File::Spec->catdir($oracle_home, "sqlpatch");
$admin_dir = File::Spec->catdir($oracle_home, "rdbms", "admin");
$opatch_executable =
File::Spec->catfile($oracle_home, "OPatch", "opatch");
}
# 17277459: Connect to database within initialize so the database handle
# does not need to be passed.
eval {
# 22923409: Timeout after CONNECT_TIMEOUT (default 120) seconds. This
# includes both the prompting for a username/password as well as the
# actual connect. This way we won't block forever waiting for input
# on stdin.
my $handler = set_sig_handler('ALRM', sub {die "alarm";});
eval {
alarm(CONNECT_TIMEOUT);
# 18361221: If userid was specified, prompt for a password
if ($userid) {
my $Catcon_EnvTag = "";
$db_user = $userid;
$db_password = "";
if ($^O eq 'MSWin32') {
$Catcon_EnvTag = catcon::catconGetUsrPasswdEnvTag();
$db_password = $ENV{$Catcon_EnvTag};
if ($db_password) {
$db_password =~ tr/"//d; # remove double quotes "
}
}
if (!$db_password) {
sqlpatch_log(LOG_ALWAYS, "Enter password for $userid: ");
ReadMode('noecho');
$db_password = <STDIN>;
ReadMode('normal');
chomp($db_password);
sqlpatch_log(LOG_ALWAYS, "\n");
alarm(CONNECT_TIMEOUT); # Reset alarm for the connect
}
}
else {
# Disable threaded mode connection for sqlpatch with environment
# variable 'ORA_SERVER_THREAD_ENABLED', as this script requires to
# connect through bequeath. Threaded mode requires password file
# authentication, leading to connection failures and hang. Refer
# bug 21139090 for details.
$ENV{'ORA_SERVER_THREAD_ENABLED'}='FALSE';
}
my $attempts = 0;
connection_attempt:
$attempts++;
sqlpatch_log(LOG_ALWAYS, "Connecting to database...");
my $dbi_connect_string = "dbi:Oracle:";
my %connect_properties;
$connect_properties{RaiseError} = 0;
$connect_properties{PrintError} = 0;
$connect_properties{AutoCommit} = 1;
# 22694961/25507396: Support connection over network in application
# mode when -connect_string is passed. If we are connecting locally
# then the user needs sysdba privs
if ($connect_string) {
$dbi_connect_string .= $connect_string
}
else {
$connect_properties{ora_session_mode} = DBD::Oracle::ORA_SYSDBA;
}
$dbh = DBI->connect($dbi_connect_string, $db_user, $db_password,
\%connect_properties);
if (!$dbh) {
# 18361221: If $userid was not specified then prompt for userid and
# password and try connection again
if ($attempts == 1 && !defined($userid) && $DBI::err == 1017) {
sqlpatch_log(LOG_ALWAYS, "\n");
sqlpatch_log(LOG_ALWAYS,
"Connection using O/S authentication failed.\n");
sqlpatch_log(LOG_ALWAYS,
"Enter userid that can connect as SYSDBA: ");
$db_user = <STDIN>;
chomp($db_user);
sqlpatch_log(LOG_ALWAYS, "Enter password for $db_user: ");
ReadMode('noecho');
$db_password = <STDIN>;
ReadMode('normal');
chomp ($db_password);
sqlpatch_log(LOG_ALWAYS, "\n");
goto connection_attempt;
}
else {
$connected_ok = 0;
}
}
else {
# Connection successful
alarm(0);
$dbh->{RaiseError} = 1;
$connected_ok = 1;
$o_initialize->{databaseConnect} = "SUCCESS";
sqlpatch_log(LOG_ALWAYS, "OK\n");
}
};
if ($@) {
die($@);
}
};
if ($@) {
if ($@ =~ /^alarm/) {
# Connect timed out
if ($verbose) {
sqlpatch_log(LOG_ALWAYS, "\n");
}
$global_prereq_failure = $o_initialize->{databaseConnect} =
"Database connect timed out after " . CONNECT_TIMEOUT . " seconds";
$connected_ok = 0;
$ret = 1;
goto initialize_complete;
}
else {
# Connect failed with other error
if ($verbose) {
sqlpatch_log(LOG_ALWAYS, "\n");
}
$global_prereq_failure = $o_initialize->{databaseConnect} =
"Database connect failed with: $@";
$connected_ok = 0;
$ret = 1;
goto initialize_complete;
}
}
if (!$connected_ok) {
# Connect failed with Oracle error
if ($verbose) {
sqlpatch_log(LOG_ALWAYS, "\n");
}
$global_prereq_failure = $o_initialize->{databaseConnect} =
"Database connect failed with: " . $DBI::errstr;
$ret = 1;
goto initialize_complete;
}
sqlpatch_log(LOG_ALWAYS, "Gathering database info...");
sqlpatch_log(LOG_DEBUG, "\n");
$dbh->{LongReadLen} = 1000000;
if ($userid) {
# 22694961/25507396: Ensure that the user has been granted the
# datapatch or dba role. We need to do this first before we start
# querying data dictionary tables
(my $cnt) =
$dbh->selectrow_array(
"SELECT count(*) FROM user_role_privs
WHERE granted_role IN ('DATAPATCH_ROLE', 'DBA')");
if (!$cnt) {
$global_prereq_failure =
"Specified user has not been granted the datapatch or dba role";
$ret = 1;
goto initialize_complete;
}
}
# 14563601: Determine database name for the log file.
# 17777061: Also determine if we are in a container database.
# 24320719: Check if database is open
my $c_db;
my $o_mode;
eval {
($c_db, $database_name, $o_mode) =
$dbh->selectrow_array("SELECT cdb, name, open_mode FROM v\$database");
if ($o_mode eq "MOUNTED") {
$global_prereq_failure = "Database is MOUNTED but not OPEN.".
" Please open the database before invoking datapatch.\n";
$ret = 1;
goto initialize_complete;
}
};
if ($@) {
my $errmsg = $dbh->err() ? $dbh->errstr() : $@;
$global_prereq_failure = "Fatal error: $errmsg";
$ret = 1;
goto initialize_complete;
}
if ($c_db eq "NO") {
$container_db = 0;
}
else {
$container_db = 1;
# 21059255: Switch to the root after connecting
if ($set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
}
# 21052955: If we are connected to a read only database (or read only node
# in a RAC database), exit without doing any patching. This needs to be
# the first thing we check after connecting and determining if we
# are in a container database.
# This query will handle all the following cases:
# * Non container DB in read only mode (single instance or RAC)
# * Container DB with root in read only mode (single instance or RAC)
# * RAC DB connected to read only instance
my $mode_query =
"SELECT open_mode FROM v\$containers WHERE con_id = " .
($container_db ? "1" : "0");
my ($open_mode) = $dbh->selectrow_array($mode_query);
sqlpatch_log(LOG_DEBUG, "open_mode: $open_mode\n");
if ($open_mode eq "READ ONLY") {
$global_prereq_failure =
"The database or instance is in read only mode";
$ret = 1;
goto initialize_complete;
}
# Set up for oracle scripts
# 22694961: Only if not in application mode
if (!$app_mode) {
$dbh->do("ALTER SESSION SET \"_oracle_script\" = TRUE");
# 25303756: This parameter is needed for performing bfile
# operations on files that are accessed via symbolic/hard links
# or Windows junctions.
# RTI 20162585: We can't do this for application patches because
# the RDBMS code explicitly prohibits any non-SYS user from setting
# this parameter.
$dbh->do("ALTER SESSION SET \"_kolfuseslf\" = TRUE");
}
if ((!$container_db ) && @user_pdbs) {
$global_prereq_failure =
"-pdbs specified but $database_name is not a container database";
$ret = 1;
goto initialize_complete;
}
# 14643995: Get lock to ensure that only one datapatch session is active
# at a time. First request the lock with a timeout of 1 second.
# 24798218: To allow concurrent invocation, we now have a global lock
# and PDB-level locks. If this is not a container database or we are
# patching all PDBs, then the global lock is obtained in exclusive
# mode. If we are patching a subset of PDBs, then the global lock is
# obtained in shared mode and individual PDB-level locks will be obtained
# in exclusive mode later below.
my $global_lock_mode = @user_pdbs ? "SHARED" : "EXCLUSIVE";
sqlpatch_log(LOG_DEBUG, "Getting global lock in $global_lock_mode mode\n");
my $lock_request_sql =
"DECLARE
lockhandle VARCHAR2(128);
lockmode INTEGER;
BEGIN
dbms_lock.allocate_unique('sqlpatch_global_lock', lockhandle);
IF ? = 'SHARED' THEN
lockmode := dbms_lock.s_mode;
ELSE
lockmode := dbms_lock.x_mode;
END IF;
? := lockhandle;
? := dbms_lock.request(lockhandle, lockmode, 1, false);
END;";
my $request_ret;
my $lock_request_stmt = $dbh->prepare($lock_request_sql);
$lock_request_stmt->bind_param(1, $global_lock_mode);
$lock_request_stmt->bind_param_inout(2, \$global_lockhandle, 128);
$lock_request_stmt->bind_param_inout(3, \$request_ret, 128);
$lock_request_stmt->execute;
# If the return value is 1 then we timed out. In that case, print a
# message and wait until it's released.
if ($request_ret eq 1) {
sqlpatch_log(LOG_ALWAYS,
"Unable to acquire sqlpatch global lock in $global_lock_mode mode \n");
sqlpatch_log(LOG_ALWAYS,
"because another datapatch session is currently running.\n");
sqlpatch_log(LOG_ALWAYS,
"Waiting for that session to complete before continuing...\n");
$lock_request_sql =
"DECLARE
lockmode INTEGER;
BEGIN
IF ? = 'SHARED' THEN
lockmode := dbms_lock.s_mode;
ELSE
lockmode := dbms_lock.x_mode;
END IF;
? := dbms_lock.request(?, lockmode, dbms_lock.maxwait, false);
END;";
$lock_request_stmt = $dbh->prepare($lock_request_sql);
$lock_request_stmt->bind_param(1, $global_lock_mode);
$lock_request_stmt->bind_param_inout(2, \$request_ret, 128);
$lock_request_stmt->bind_param(3, $global_lockhandle);
$lock_request_stmt->execute;
}
sqlpatch_log(LOG_DEBUG, "Global lock obtained in $global_lock_mode mode\n");
# 25425451/26281129: Determine nothing_sql and database feature version
($database_feature_version) =
$dbh->selectrow_array("SELECT SUBSTR(version, 1,
INSTR(version, '.', 1) - 1)
FROM v\$instance");
my $feature_hash =
create_version_hash("FEATURE",
$database_feature_version . ".1.0.0.0",
undef,
"Feature Release",
undef);
$feature_release_description = $feature_hash->{version_description};
sqlpatch_log(LOG_DEBUG,
"feature_release_description: $feature_release_description\n");
# 8498426: Set namespace
$dbh->do("BEGIN sys.dbms_registry.set_session_namespace('SERVER'); END;");
# 19189525: Initialize nothing_sql after setting the namespace
($nothing_sql) =
$dbh->selectrow_array(
"SELECT dbms_registry.nothing_script FROM sys.dual");
if ($container_db) {
sqlpatch_log(LOG_DEBUG, "container database!\n");
$initialize_notes .= "
Note: Datapatch will only apply or rollback SQL fixes for PDBs
that are in an open state, no patches will be applied to closed PDBs.
Please refer to Note: Datapatch: Database 12c Post Patch SQL Automation
(Doc ID 1585822.1)\n\n";
# Determine the pluggable databases that we are going to process
# 15873839: Restrict to PDBs which are open read write and the seed
# 17777061: Subject to the passed in list
# 24450424: Process PDBs that are open read only as well
# 25507396: Don't include PDB$SEED for application patches
my $container_query;
my %user_pdb_hash;
my $seed_clause;
if (@user_pdbs) {
# 18474855: Convert the array into a hash for later lookup. We will
# use values of 'INIT' (not found), 'FETCHED' (found) and 'CLOSED'
# (the container was not open)..
%user_pdb_hash = map { $_ => 'INIT' } @user_pdbs;
$seed_clause = $app_mode ? "name != 'PDB\$SEED' AND" : "";
$container_query =
"SELECT name,open_mode
FROM v\$containers
WHERE $seed_clause name IN ('" . join("','" , @user_pdbs) . "')
ORDER BY con_id";
}
else {
$seed_clause = $app_mode ? "WHERE name != 'PDB\$SEED'" : "";
$container_query =
"SELECT name,open_mode FROM v\$containers
$seed_clause ORDER BY con_id";
}
sqlpatch_log(LOG_DEBUG, "container query: $container_query\n");
my $container_stmt = $dbh->prepare($container_query);
$container_stmt->execute();
my $rows_found=0;
my $bogus_containers = "";
my $closed_containers = "";
while (my ($name, $open_mode) =
$container_stmt->fetchrow_array()) {
# 25507396: If we are connecting remotely, the connect string needs
# to point to a specific PDB, which cannot be the root or seed.
# Thus if we have found more than one row in v$containers we need
# to raise an error
if ($connect_string) {
if ($rows_found) {
$global_prereq_failure =
"connect_string $connect_string does not point to a specific PDB";
$ret = 1;
goto initialize_complete;
}
elsif ($name =~ /CDB\$ROOT|PDB\$SEED/) {
$global_prereq_failure =
"connect_string $connect_string cannot point to the root or seed";
$ret = 1;
goto initialize_complete;
}
}
sqlpatch_log(LOG_DEBUG, "sql row: $name $open_mode \n");
# 24684759: Ensure seed PDB is open
if (($name eq "PDB\$SEED") && ($open_mode eq "MOUNTED")) {
$global_prereq_failure = "PDB\$SEED is in MOUNTED state";
$ret = 1;
goto initialize_complete;
}
# 18474855: If we have used -pdbs to specify a list of PDBs then
# raise an error if the open_mode is incorrect. When we did not use
# -pdbs then we will just record a warning and continue on.
# (we skip this test for PDB$SEED to support previous behaviour).
if ($name ne "PDB\$SEED" &&
$open_mode ne "READ WRITE" && $open_mode ne "READ ONLY" &&
$open_mode ne "MIGRATE") {
if (@user_pdbs && defined($user_pdb_hash{$name})) {
$user_pdb_hash{$name} = 'CLOSED';
next;
}
elsif (!@user_pdbs) {
$initialize_notes .=
"Warning: PDB $name is in mode $open_mode and will be skipped.\n";
next;
}
}
if (defined($user_pdb_hash{$name})) {
$user_pdb_hash{$name} = 'FETCHED';
}
$rows_found=1;
my $info = {};
$info->{"pdb_name"} = $name;
$info->{"startup_mode"} = $open_mode;
$info->{"lockhandle"} = undef;
$pdb_info{$name} = $info;
push(@pdb_list, $name);
}
# Build a list of problem containers.
foreach my $item (keys(%user_pdb_hash)) {
if ($user_pdb_hash{$item} eq 'INIT') {
$bogus_containers = $bogus_containers . " " . $item;
}
elsif ($user_pdb_hash{$item} eq 'CLOSED') {
$closed_containers = $closed_containers . " " . $item;
}
}
# 18474855: return bogus names first and then closed containers next.
if ($bogus_containers) {
$global_prereq_failure =
"Some of the -pdbs containers were not found in the database. " .
"Bad containers:$bogus_containers";
$ret = 1;
}
if ($closed_containers) {
$global_prereq_failure =
$ret ? $global_prereq_failure . "\nSome" : "Some";
$global_prereq_failure = $global_prereq_failure . " of the -pdbs " .
"containers were not open. Bad containers:$closed_containers";
$ret = 1;
goto initialize_complete;
}
sqlpatch_log(LOG_DEBUG, " container database ! \n");
if (!$rows_found) {
$global_prereq_failure =
"No PDBs (subject to -pdbs parameter) are open";
$ret = 1;
goto initialize_complete;
}
}
else {
sqlpatch_log(LOG_DEBUG, "not container database!\n");
# 20939028: Use the previously selected value of $open_mode
my $info = {};
$info->{"pdb_name"} = $database_name;
$info->{"startup_mode"} = $open_mode;
$info->{"lockhandle"} = undef;
$pdb_info{$database_name} = $info;
push(@pdb_list, $database_name);
}
# 25425451: Get component info for all PDBs
foreach my $pdb (@pdb_list) {
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = $pdb");
}
my $component_stmt =
$dbh->prepare("SELECT comp_id, comp_name, status, version, version_full
FROM dba_registry");
$component_stmt->execute;
while (my $component_ref = $component_stmt->fetchrow_hashref) {
%{$pdb_info{$pdb}{component_info}{$component_ref->{COMP_ID}}} =
(component_name => $component_ref->{COMP_NAME},
registry_version => $component_ref->{VERSION},
registry_version_full => $component_ref->{VERSION},
registry_status => $component_ref->{STATUS});
}
}
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = cdb\$root");
}
sqlpatch_log(LOG_DEBUG, "printing pdbs data\n");
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper(\%pdb_info));
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper(\@pdb_list));
# 24798218: Allow simultaneous invocations of datapatch, as long as they
# are processing separate PDBs. In order to support this, we have already
# gotten the global lock in shared mode.
if (@user_pdbs) {
# We are processing a subset of the available PDBs. Get an exclusive
# lock for each one. We can't use catcon to process this because
# the lock(s) need to be held by the Perl process, thus they need to
# be acquired serially. We sort by the PDB name to ensure that we
# won't deadlock with another datapatch session with overlapping
# PDBs.
foreach my $pdb (sort @user_pdbs) {
sqlpatch_log(LOG_DEBUG, "Getting exclusive lock for PDB $pdb\n");
my $pdb_lock_name = "sqlpatch_${pdb}_lock";
my $lock_request_sql =
"DECLARE
lockhandle VARCHAR2(128);
BEGIN
dbms_lock.allocate_unique(?, lockhandle);
? := lockhandle;
? := dbms_lock.request(lockhandle, dbms_lock.x_mode, 1, false);
END;";
my $request_ret;
my $lock_request_stmt = $dbh->prepare($lock_request_sql);
$lock_request_stmt->bind_param(1, $pdb_lock_name);
$lock_request_stmt->bind_param_inout(2,
\$pdb_info{$pdb}->{lockhandle}, 128);
$lock_request_stmt->bind_param_inout(3, \$request_ret, 128);
$lock_request_stmt->execute;
# If the return value is 1 then we timed out. In that case, print a
# message and wait until it's released.
if ($request_ret eq 1) {
$waited_for_pdb_lock = 1;
sqlpatch_log(LOG_ALWAYS,
"Unable to acquire exclusive sqlpatch PDB lock because\n");
sqlpatch_log(LOG_ALWAYS,
"another datapatch session is currently running against PDB $pdb.\n");
sqlpatch_log(LOG_ALWAYS,
"Waiting for that session to complete before continuing...\n");
$lock_request_sql =
"BEGIN
? := dbms_lock.request(?, dbms_lock.x_mode, dbms_lock.maxwait, false);
END;";
$lock_request_stmt = $dbh->prepare($lock_request_sql);
$lock_request_stmt->bind_param_inout(1, \$request_ret, 128);
$lock_request_stmt->bind_param_inout(2,
\$pdb_info{$pdb}->{lockhandle}, 128);
$lock_request_stmt->execute;
}
}
}
# 19189525: Setup catcon once here
# catcon init parameters
my $catcon_user = "/"; # connect string to run scripts
my $catcon_internal_user = "/"; # connect string for internal statements
if ($db_user ne "") {
# 18361221: Setup to use user and password
$catcon_user = $db_user . "/" . $db_password;
$catcon_internal_user = $db_user . "/" . $db_password;
}
# Connect as sysdba unless over the network
if ($connect_string) {
catcon::catconEZConnect($connect_string);
# If we are connecting over the network then tell catcon
# not to issue any alter session set container statements
catcon::catconEZconnToPdb($pdb_list[0]);
}
else {
$catcon_user .= " AS SYSDBA";
$catcon_internal_user .= " AS SYSDBA";
}
my $catcon_srcdir = 0; # directory for source scripts
my $catcon_logdir = $invocation_logdir; # directory for logs
my $catcon_logbase = "sqlpatch_catcon_"; # logfile base string
my $catcon_pdbs_to_include; # PDBs to use
my $catcon_pdbs_to_exclude = ""; # PDBs to skip
my $catcon_num_processes = 4; # number of processes to use
my $catcon_parallel_degree = 0; # parallel degree
my $catcon_echo = 0; # set echo on?
my $catcon_spool = 0; # spool?
my $catcon_argdelim = 0; # argument delimeter
my $catcon_secret_argdelim = 0; # ssshhhh
my $catcon_error_logging = 0; # turn on errorlogging?
my $catcon_error_logging_id = 0; # errorlogging ID
my @catcon_init_statements = (); # per-process init statements
my @catcon_end_statements = (); # per-process final statements
my $catcon_readwrite_mode = 1; # Set seed to READ WRITE
my $catcon_debug = (defined($debug) ? $debug : 0); # catcon debugging
my $catcon_gui = 0; # Called from cdb_sqlexec
my $catcon_userscript = 0; # Running user supplied scripts?
if (!$connect_string) {
$catcon_pdbs_to_include = $container_db ? join(' ', @pdb_list) : "";
}
# 23177001: Disable PDB lockdown
catcon::catconDisableLockdown(1);
# 24450424: Tell catcon to open all PDBs read write (including the seed)
# if needed. catcon will be responsible for restoring back to the current
# state during catconWrapUp().
catcon::catconPdbMode(catcon::PDB_STRING_TO_MODE_CONST->{"READ WRITE"});
# Tell catcon not to set _oracle_script if we are in application mode
if ($app_mode) {
catcon::catconNoOracleScript();
}
# 19501299: Check return codes from catcon
my $catcon_ret =
catcon::catconInit($catcon_user, $catcon_internal_user, $catcon_srcdir,
$catcon_logdir, $catcon_logbase,
$catcon_pdbs_to_include, $catcon_pdbs_to_exclude,
$catcon_num_processes, $catcon_parallel_degree,
$catcon_echo, $catcon_spool, $catcon_argdelim,
$catcon_secret_argdelim, $catcon_error_logging,
$catcon_error_logging_id, @catcon_init_statements,
@catcon_end_statements, $catcon_readwrite_mode,
$catcon_debug, $catcon_gui,0,0);
if ($catcon_ret) {
$global_prereq_failure = "catconInit failed with $catcon_ret";
$catcon_ok = 0;
$ret = 1;
goto initialize_complete;
}
# 25425451: Invoke catcon with nothing.sql here. This ensures that all
# PDBs will be opened read write for subsequent processing (including
# bootstrap.
# Catcon parameters
my @catcon_scripts = ($nothing_sql); # Array of scripts/statements to run
my $catcon_single_threaded = 1; # Run scripts in order?
my $catcon_root_only = 0; # Run only in the root?
my $catcon_run_per_proc = 0; # Run per process init statements?
my $con_names_incl; # PDBs to include
# 27999597: Do not specify PDBs if we are connecting remotely
if ($container_db && !$connect_string) {
$con_names_incl = join(' ', @pdb_list);
}
else {
$con_names_incl = "";
}
my $con_names_excl; # PDBs to exclude
my $custom_err_logging_ident; # Custom error logging identifier
my $custom_query; # Custom query for PDBs
$catcon_ret =
catcon::catconExec(@catcon_scripts, $catcon_single_threaded,
$catcon_root_only, $catcon_run_per_proc,
$con_names_incl, $con_names_excl,
$custom_err_logging_ident,
$custom_query);
if ($catcon_ret) {
$global_prereq_failure = "catconExec failed with $catcon_ret";
$catcon_ok = 0;
$ret = 1;
goto initialize_complete;
}
$catcon_init = 1;
};
if ($@) {
# 20099675: Trap Perl runtime errors that would otherwise go only to
# stderr (and thus be redirected only to the invocation log) and be sure
# to log to the screen also
$global_prereq_failure = $@;
$ret = 1;
}
initialize_complete:
if (!$ret && !$version_only && !$waited_for_pdb_lock) {
sqlpatch_log(LOG_ALWAYS, "done\n");
if ($initialize_notes) {
sqlpatch_log(LOG_ALWAYS,, $initialize_notes);
}
}
sqlpatch_log(LOG_DEBUG, "initialize complete, final configuration:\n");
sqlpatch_log(LOG_DEBUG, " pdb_list: @pdb_list\n");
sqlpatch_log(LOG_DEBUG, " apply_list: @apply_list\n");
sqlpatch_log(LOG_DEBUG, " rollback_list: @rollback_list\n");
sqlpatch_log(LOG_DEBUG, " upgrade_mode_only: $upgrade_mode_only\n");
sqlpatch_log(LOG_DEBUG, " force: $force\n");
sqlpatch_log(LOG_DEBUG, " prereq_only: $prereq_only\n");
sqlpatch_log(LOG_DEBUG, " user_oh: $user_oh\n");
sqlpatch_log(LOG_DEBUG, " verbose: $verbose\n");
sqlpatch_log(LOG_DEBUG, " debug: $debug\n");
sqlpatch_log(LOG_DEBUG, " database name: $database_name\n");
if (write_progress("initialize", 10)) {
$ret = 1;
}
if ($ret) {
report_prereq_errors();
$global_failure = 1;
}
# 21503113: Fill in summary orchestration log
$orchestration_summary_hash->{initialize} = $o_initialize;
$global_return = $ret;
return $ret;
}
# ----------------------------- patch ---------------------------------------
# NAME
# patch
#
# DESCRIPTION
# Performs the entire patching process, consisting of:
# * Determine current state if needed (-force not specified)
# * Add patches to installation queue
# * Install patches (using catcon)
# * Validate the resulting logfiles for non-ignorable errors
#
# ARGUMENTS
# None. All parameters should have been set by initialize().
#
# RETURNS
# 0 for success, 1 for prereq failure, 2 for non-ignorable error during
# patch installation.
sub patch() {
my $ret = 0;
my $prereq_failed = 0;
my $attempt = 1;
eval {
if ($version_only) {
goto patch_complete; # Nothing to do
}
# 19051526: Verify that general prereqs are met
if ($prereq_failed = check_global_prereqs()) {
goto patch_complete;
}
# 18537739: If -rollback all is specified we need to determine the rollback
# list based on the current state
if (@rollback_list[0] eq "all") {
@rollback_list = applied_patches();
}
# First we need to get the current state into the patch descriptions
if ($prereq_failed = get_current_patches()) {
goto patch_complete;
}
attempt_patch:
# Attempt 1: Determine the patch queue based on the current patches
if ($attempt eq 1) {
if ($prereq_failed = add_to_queue()) {
goto patch_complete;
}
}
else {
# Attempt 2: Determine the retry patch queue based on the patch queue
# and the patch results
if ($prereq_failed = add_to_retry_queue()) {
goto patch_complete;
}
}
if ($prereq_only) {
goto patch_complete;
}
# $work_to_do was set by add_to_queue()
if ($work_to_do) {
# Go for it!
$global_total_patches += install_patches($attempt);
if (!$catcon_ok) {
# 19501299: catcon failed at some point during patch installation,
# exit immediately without validating logfiles.
$ret = 1;
goto patch_complete;
}
$ret = validate_logfiles($attempt);
if (($ret eq 3) && ($attempt eq 1)) {
$attempt++;
sqlpatch_log(LOG_ALWAYS, "\n");
goto attempt_patch;
}
}
patch_complete:
if ($prereq_failed) {
report_prereq_errors();
sqlpatch_log(LOG_ALWAYS,
"Prereq check failed, exiting without installing any patches.\n");
$ret = 1;
}
};
if ($@) {
# 20099675: Trap Perl runtime errors that would otherwise go only to
# stderr (and thus be redirected only to the invocation log) and be sure
# to log to the screen also
sqlpatch_log(LOG_ALWAYS, "\n$@\n");
$ret = 1;
}
if ($ret) {
$global_failure = 1;
}
log_state(LOG_INVOCATION, "final state end of patching");
$global_return = $ret;
return $ret;
}
# ---------------------------- finalize --------------------------------------
# NAME
# finalize
#
# DESCRIPTION
#
# Cleans up from patching, including closing the database connection.
#
# ARGUMENTS
# None
#
# RETURNS
# None
#
# JSON SUMMARY LOG:
# summary : {
# returnCode : <0, 1, 2>
# totalExecutionTime : <total time in seconds>
# failureReason : <text of the failure reason or undef>
# totalInstalledPatches : <total number of patches installed>
# }
sub finalize() {
sqlpatch_log(LOG_DEBUG, "finalize entry\n");
my $finalize_error;
# 14643995: Release lock. If for some reason this fails, it will be released
# when the session ends.
# 19520602: Only if we successfully connected in the first place
if ($connected_ok) {
# 19189525: Close catcon
# 19501299: Only if the last catcon operation was successful.
# 22204310: Close catcon before releasing the lock
# Switch to root first so that we are not holding a session connected
# to PDB$SEED. catconWrapUp() attempts to close and reopen it.
if ($container_db && $set_container) {
sqlpatch_log(LOG_DEBUG, "Switching to CDB\$ROOT before wrapup\n");
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
if ($catcon_init && $catcon_ok) {
my $catcon_ret = catcon::catconWrapUp();
if ($catcon_ret) {
$finalize_error = 1;
sqlpatch_log(LOG_INVOCATION,
"\ncatconWrapUp returned $catcon_ret during finalize\n");
}
}
# 24798218: Release locks
my $lock_release_sql =
"DECLARE
ret NUMBER;
BEGIN
ret := dbms_lock.release(?);
END;";
my $lock_release_stmt = $dbh->prepare($lock_release_sql);
$lock_release_stmt->{RaiseError} = 0;
if (defined($global_lockhandle)) {
$lock_release_stmt->bind_param(1, $global_lockhandle);
$lock_release_stmt->execute;
if ($lock_release_stmt->err) {
# Retry once if the error is retryable
my $error_string = $lock_release_stmt->errstr;
$error_string =~ /(ORA-\d\d\d\d\d):.*/;
if (grep(/$1/, @retryable_errors)) {
sqlpatch_log(LOG_DEBUG,
"Received retryable error on global lock release\n");
$lock_release_stmt->execute;
}
}
# Check error after possible retry
if ($lock_release_stmt->err) {
$finalize_error = 1;
sqlpatch_log(LOG_INVOCATION,
"\nError releasing global lock:\n" .
$lock_release_stmt->errstr . "\n");
}
}
# If only a subset of PDBs were specified, then release the PDB-specific
# locks as well.
if (@user_pdbs) {
foreach my $pdb (sort @user_pdbs) {
if (defined($pdb_info{$pdb}->{lockhandle})) {
$lock_release_stmt->bind_param(1, $pdb_info{$pdb}->{lockhandle});
$lock_release_stmt->execute;
if ($lock_release_stmt->err) {
# Retry once if the error is retryable
my $error_string = $lock_release_stmt->errstr;
$error_string =~ /(ORA-\d\d\d\d\d):.*/;
if (grep(/$1/, @retryable_errors)) {
sqlpatch_log(LOG_DEBUG,
"Received retryable error pdb $pdb lock release\n");
$lock_release_stmt->execute;
}
}
# Check error after possible retry
if ($lock_release_stmt->err) {
$finalize_error = 1;
sqlpatch_log(LOG_INVOCATION,
"\nError releasing pdb $pdb lock:\n" .
$lock_release_stmt->errstr . "\n");
}
}
}
}
$dbh->{RaiseError} = 0;
$dbh->disconnect();
if ($dbh->err) {
$finalize_error = 1;
sqlpatch_log(LOG_INVOCATION,
"\nError during disconnect:\n" . $dbh->errstr . "\n");
}
}
# 21503113: Output orchestration summary
if ($orchestration_summary_ok) {
$orchestration_summary_hash->{summary} =
{"returnCode" => $global_return,
"totalExecutionTime" => ((time() - $start_time)),
"totalInstalledPatches" => defined($global_total_patches) ? $global_total_patches : 0};
if ($global_prereq_failure) {
$orchestration_summary_hash->{summary}->{failureReason} =
$global_prereq_failure;
}
else {
$orchestration_summary_hash->{summary}->{failureReason} = undef;
}
my $sqlpatch_hash;
$sqlpatch_hash->{sqlpatch} = $orchestration_summary_hash;
my $json_text;
eval {
$json_text = to_json($sqlpatch_hash, {pretty => 1, canonical => 1});
};
if ($@) {
sqlpatch_log(LOG_ALWAYS,
"Error creating JSON text from summary hash: $@");
$global_failure = 1;
}
else {
# Summary log has already been opened in initialize
print $orchestration_summary_handle $json_text;
close($orchestration_summary_handle);
}
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper($sqlpatch_hash));
}
if ($finalize_error) {
# 26712331: Print a message alerting the user of the generally non-fatal
# error during finalize
sqlpatch_log(LOG_ALWAYS,
"\nWarning: An error occurred during datapatch cleanup, after patching");
sqlpatch_log(LOG_ALWAYS,
"\nwas complete. Please refer to the invocation log\n");
sqlpatch_log(LOG_ALWAYS, $invocation_log);
sqlpatch_log(LOG_ALWAYS, "\nfor details.\n\n");
}
if ($global_failure) {
# 17974971: Print a message pointing the user to the support note explaing
# what to do
if (defined($invocation_log)) {
sqlpatch_log(LOG_ALWAYS,
"\nPlease refer to MOS Note 1609718.1 and/or the invocation log\n");
sqlpatch_log(LOG_ALWAYS, $invocation_log);
}
else {
sqlpatch_log(LOG_ALWAYS,"\nPlease refer to MOS Note 1609718.1");
}
sqlpatch_log(LOG_ALWAYS,
"\nfor information on how to resolve the above errors.\n\n");
}
sqlpatch_log(LOG_ALWAYS,
"SQL Patching tool complete on " . (localtime) . "\n");
# Final progress update. We don't check errors here because we've already
# closed all the orchestration log files.
write_progress("finalize", 100);
if (!$version_only) {
if ($invocation_log_ok) {
# 19547370: Close invocation log
close($invocation_handle);
# 19547370: Restore original stderr
close(STDERR);
open(STDERR, ">&SAVED_STDERR");
}
# 25823532: Close the debug log
if ($debug_log_ok) {
close($debug_handle);
}
}
}
# --------------------------- log --------------------------------------------
# NAME
# log
#
# DESCRIPTION
#
# Prints to either the screen or invocation log, or both
#
# ARGUMENTS
# $level: One of the LOG_* constants:
# LOG_DEBUG: Print to stdout and invocation log only if $debug
# is true
# LOG_INVOCATION: Print to invocation log always, also to stdout if
# $debug is true
# LOG_VERBOSE: Print to invocation log always, also to stdout if
# $verbose is true
# LOG_ALWAYS: Print to invocation log and stdout always
# $message: String to print
#
# RETURNS
# None
sub sqlpatch_log($$) {
my ($level, $message) = @_;
# Print to stdout if needed
if (($level == LOG_ALWAYS) ||
($level == LOG_VERBOSE && $verbose) ||
($level == LOG_INVOCATION && $debug) ||
($level == LOG_DEBUG && $debug)) {
print $message;
}
# Print to invocation log (or @invocation_log_stored)
if (($level == LOG_ALWAYS) ||
($level == LOG_VERBOSE) ||
($level == LOG_INVOCATION) ||
($level == LOG_DEBUG && $debug)) {
if ($invocation_log_ok) {
# Check if this is the first call with the log set up
if ($#invocation_log_stored != -1) {
foreach my $msg (@invocation_log_stored) {
print $invocation_handle $msg;
}
@invocation_log_stored = ();
}
print $invocation_handle $message;
}
else {
push(@invocation_log_stored, $message);
}
}
# 25823532: Write to the debug log.
if ($debug_log_ok) {
print $debug_handle $message;
}
}
# -------------------------- get_current_patches ----------------------------
# NAME
# get_current_patches
#
# DESCRIPTION
# Loads the %patch_descriptions hash with all the information about the
# currently installed patches, both SQL and binary state. The SQL state is
# determined by querying the SQL registry table dba_registry_sqlpatch.
# The binary state is determined (if $force or $noqi is not set) by queryable
# inventory.
#
# ARGUMENTS
# None
#
# RETURNS
# 0 for success, 1 for prereq failure
#
# JSON SUMMARY LOG:
# currentState : {
# binary/<PDB name>/<database name> :
# [ {
# patchID : patch ID
# patchUID : patch UPI
# description : patch description
# patchType : patch type (INTERIM, RU, RUR, ID, CU)
# missingNodes : <missing nodes or undef, for binary entry only>
# sqlState : <APPLY/ROLLBACK>/<SUCCESS/WITH ERRORS> for SQL entries
# } ...
# ]
# } ...
sub get_current_patches() {
my $ret = 0;
sqlpatch_log(LOG_ALWAYS, "Determining current state...");
sqlpatch_log(LOG_DEBUG, "\n");
# If -force or -noqi is specified, we need to get the state based on the
# supplied apply and rollback lists without consulting queryable inventory.
if ($force || $noqi) {
foreach my $apply_patch (@apply_list) {
sqlpatch_log(LOG_DEBUG, "Checking apply patch $apply_patch\n");
my $description = {};
$description->{"prereq_ok"} = 1;
if ($apply_patch =~ /(\d+)\/(\d+)/) {
# User specified both patch id and UID, so we're good
$description->{"patchid"} = $1;
$description->{"patchuid"} = $2;
}
else {
$description->{"patchid"} = $apply_patch;
# We need to determine the uid by looking for the install directory
my $patchdir = File::Spec->catdir($sqlpatch_dir,
$description->{"patchid"});
if (-e $patchdir) {
# If there is not exactly 1 subdirectory here then we can't determine
# the UID and need to return an error
opendir (PATCHDIR, $patchdir);
my @subdirs;
foreach my $dir (readdir(PATCHDIR)) {
if (($dir !~ /^\./) && (-d File::Spec->catfile($patchdir, $dir))) {
push(@subdirs, $dir);
}
}
close (PATCHDIR);
sqlpatch_log(LOG_DEBUG,
"found subdirs @subdirs for patch id $apply_patch\n");
if ($#subdirs != 0) {
# More than 1 subdirectory, so we can't determine the UID.
# Mark as prereq failed so we will fail.
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Could not determine unique patch ID for patch " .
$description->{"patchid"} . " due to more than one subdirectory " .
"under $patchdir";
$ret = 1;
my $key = $description->{"patchid"} . "/";
$patch_descriptions{$key} = $description;
next;
}
else {
$description->{"patchuid"} = @subdirs[0];
}
}
else {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Patch directory $patchdir does not exist";
my $key = $description->{"patchid"} . "/";
$patch_descriptions{$key} = $description;
$ret = 1;
next;
}
}
$description->{"mode"} = "apply";
# We've now determined the patch ID and patch UID, determine the
# rest of the patch properties based on the XML descriptor only.
if (load_patch_metadata($description, 0, undef, undef, undef)) {
$ret = 1;
}
my $key = $description->{"patchid"} . "/" . $description->{"patchuid"};
$patch_descriptions{$key} = $description;
}
foreach my $rollback_patch (@rollback_list) {
sqlpatch_log(LOG_DEBUG, "Checking rollback patch $rollback_patch\n");
my $description = {};
my $patch_rowid = undef;
$description->{"prereq_ok"} = 1;
if ($rollback_patch =~ /(\d+)\/(\d+)/) {
# User specified both patch id and UID, so we're good
$description->{"patchid"} = $1;
$description->{"patchuid"} = $2;
# Query the SQL registry for the rowid of the latest entry for
# this patch key using our nifty dbms_sqlpatch table function.
# In a multitenant environment, this query will be executed against
# the root.
my $patches_query =
"SELECT registry_rowid
FROM TABLE(dbms_sqlpatch.all_patches)
WHERE patch_id = ?
AND patch_uid = ?";
($patch_rowid) =
$dbh->selectrow_array($patches_query, undef,
$description->{patchid},
$description->{patchuid});
}
else {
$description->{patchid} = $rollback_patch;
# Determine the patch UID by querying the SQL registry again using
# our nifty table function. Because there could be more than one
# entry with the same patch ID, we sort by install_id and action_time
# and just keep the first returned row.
# In a multitenant environment, this query will be executed against
# the root.
my $patches_uid_query =
"SELECT patch_uid, registry_rowid
FROM TABLE(dbms_sqlpatch.all_patches)
WHERE patch_id = ?
ORDER BY install_id DESC, action_time DESC";
($description->{"patchuid"}, $patch_rowid) =
$dbh->selectrow_array($patches_uid_query, undef, $rollback_patch);
if (!defined($description->{"patchuid"})) {
sqlpatch_log(LOG_DEBUG, "patchuid not defined\n");
# The patch was not found in the SQL registry, so we can't determine
# the UID.
# 19520602: Before we fail with a prereq error check the install
# directory like we do for the apply -force case.
my $patchdir = File::Spec->catdir($sqlpatch_dir,
$description->{"patchid"});
if (-e $patchdir) {
# If there is not exactly 1 subdirectory here then we can't
# determine the UID and need to return an error
opendir (PATCHDIR, $patchdir);
my @subdirs;
foreach my $dir (readdir(PATCHDIR)) {
if (($dir !~ /^\./) &&
(-d File::Spec->catfile($patchdir, $dir))) {
push(@subdirs, $dir);
}
}
close (PATCHDIR);
sqlpatch_log(LOG_DEBUG, "found subdirs @subdirs for patch id " .
$description->{"patchid"} . "\n");
if ($#subdirs != 0) {
# More than 1 subdirectory, so we can't determine the UID.
# Mark as prereq failed so we will fail.
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Could not determine unique patch ID for patch " .
$description->{"patchid"} . " because it is not present in " .
"the SQL registry and there is more than one subdirectory " .
"under $patchdir";
my $key = $description->{"patchid"} . "/";
$patch_descriptions{$key} = $description;
$ret = 1;
next;
}
else {
$description->{"patchuid"} = @subdirs[0];
}
}
else {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Patch directory $patchdir does not exist";
my $key = $description->{"patchid"} . "/";
$patch_descriptions{$key} = $description;
$ret = 1;
next;
}
}
}
$description->{"mode"} = "rollback";
# We've now determined the patch ID and patch UID, determine the
# rest of the patch properties based on the XML descriptor or SQL
# registry.
if (load_patch_metadata($description, 0, $patch_rowid, undef,
"CDB\$ROOT")) {
$ret = 1;
}
my $key = $description->{"patchid"} . "/" . $description->{"patchuid"};
$patch_descriptions{$key} = $description;
}
log_state(LOG_DEBUG,
"get_current_patches after reading -apply and -rollback lists");
}
# 19681455: If we have seen problems then let's skip further actions.
if ($ret == 1) {
goto get_current_patches_end;
}
# First get the binary state. We do this if -force is not specified.
# There are 3 possible ways: passed in XML binary_config, local opatch
# inventory, or via queryable inventory.
if (!$force) {
if ($binary_config) {
# 22694961: Get binary state from supplied XML file
sqlpatch_log(LOG_DEBUG,
"get_current_patches: getting state from binary_config\n");
sqlpatch_log(LOG_DEBUG, "binary_config_hash:" . Data::Dumper->Dumper($binary_config_hash));
# The binary config format is organized by node, not by patch. So we
# need to setup a local structure to ensure we have the patches
# installed on all nodes
my @nodes;
my %binary_patches;
my $hosts = $binary_config_hash->{host};
foreach my $host (keys %$hosts) {
sqlpatch_log(LOG_DEBUG, "Checking host $host\n");
push(@nodes, $host);
foreach my $home (@{$hosts->{$host}->{oraclehome}}) {
sqlpatch_log(LOG_DEBUG, "Checking home $home->{path}, type $home->{type}\n");
next if $home->{type} ne "cluster";
foreach my $patch (keys %{$home->{patches}->{patch}}) {
sqlpatch_log(LOG_DEBUG, "Checking patch $patch\n");
if ($home->{patches}->{patch}->{$patch}->{issqlpatch} eq "true") {
sqlpatch_log(LOG_DEBUG, "Found sql patch $patch $home->{patches}->{patch}->{$patch}->{unique_patch_id} host $host\n");
my $patch_key =
$patch . "/" .
$home->{patches}->{patch}->{$patch}->{unique_patch_id};
push(@{$binary_patches{$patch_key}{nodes}}, $host);
$binary_patches{$patch_key}{patchid} = $patch;
$binary_patches{$patch_key}{patchuid} =
$home->{patches}->{patch}->{$patch}->{unique_patch_id};
}
}
}
}
# We now have an array @nodes with all of the nodes, and a hash
# %binary_patches with a @nodes entry, so we can ensure that the patch
# is installed on all nodes
sqlpatch_log(LOG_DEBUG, "nodes: " . join(' ', @nodes) . "\n");
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper(\%binary_patches));
foreach my $patch_key (keys %binary_patches) {
# Add to the patch descriptions hash.
# Since we start with the binary query we know it's not there.
my $description = {};
$description->{patchid} = $binary_patches{$patch_key}{patchid};
$description->{patchuid} = $binary_patches{$patch_key}{patchuid};
$description->{installed_binary} = 1;
$description->{prereq_ok} = 1;
if ((scalar @nodes) == (scalar @{$binary_patches{$patch_key}{nodes}})) {
# Patch is present on all nodes.
$description->{mising_nodes} = undef;
}
else {
# Patch is missing nodes, figure out which ones are missing
foreach my $node (@nodes) {
sqlpatch_log(LOG_DEBUG,
"Checking node $node against $binary_patches{$patch_key}{nodes}\n");
if (!grep(/$node/, @{$binary_patches{$patch_key}{nodes}})) {
push(@{$description->{missing_nodes}}, $node);
}
}
}
foreach my $pdb (@pdb_list) {
$description->{pdb_info}{$pdb}{sql_state} = undef;
$description->{pdb_info}{$pdb}{last_action_time} = undef;
$description->{pdb_info}{$pdb}{ru_info} = 0;
}
# Load the rest of the properties from the XML descriptor only
if (load_patch_metadata($description, 0, undef, undef, undef)) {
$ret = 1;
}
# Add the description to the hash
$patch_descriptions{$patch_key} = $description;
}
}
elsif ($local_inventory) {
# 25507396: Get and parse output of opatch lsinventory -xml
sqlpatch_log(LOG_DEBUG,
"get_current_patches: Parsing opatch inventory\n");
foreach my $entry (@{$opatch_inventory_hash->{patches}->{patch}}) {
if ($entry->{sqlPatch} eq "true") {
# Found a SQL patch. Add to the patch descriptions hash.
# Since we start with the binary query we know it's not there.
my $description = {};
$description->{patchid} = $entry->{patchID};
$description->{patchuid} = $entry->{uniquePatchID};
$description->{installed_binary} = 1;
$description->{prereq_ok} = 1;
# Assume patch is present on all nodes.
$description->{mising_nodes} = undef;
foreach my $pdb (@pdb_list) {
$description->{pdb_info}{$pdb}{sql_state} = undef;
$description->{pdb_info}{$pdb}{last_action_time} = undef;
$description->{pdb_info}{$pdb}{ru_info} = 0;
}
# Load the rest of the properties from the XML descriptor only
if (load_patch_metadata($description, 0, undef, undef, undef)) {
$ret = 1;
}
# Add the description to the hash
$patch_descriptions{$description->{patchid} . "/" . $description->{patchuid}} = $description;
}
}
}
elsif (!$noqi) {
# 22359063: Get binary state if not -force and not -noqi
sqlpatch_log(LOG_DEBUG, "get_current_patches: getting binary state\n");
# 22923409: In case queryable inventory is blocked in pluggable databases
# switch to root
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = cdb\$root");
}
my $pending_activity;
# 24397438: Get pending activity as a LOB so that it can be any length.
# First determine how long it is.
my ($pending_activity_len) =
$dbh->selectrow_array(
"SELECT dbms_lob.getlength(XMLSerialize(CONTENT dbms_sqlpatch.opatch_registry_state INDENT)) FROM dual");
$dbh->{LongReadLen} = $pending_activity_len;
sqlpatch_log(LOG_DEBUG, "pending_activity_len: $pending_activity_len\n");
my @qi_debug = $dbh->func('dbms_output_get');
foreach my $line (@qi_debug) {
sqlpatch_log(LOG_DEBUG, "$line\n");
}
# Returns a list of all currently installed SQL patches for single
# instance, and partially installed patches for RAC. The XMLSerialize
# ensures that each tag is on a different line.
my $activity_sql =
"BEGIN
SELECT XMLSerialize(CONTENT dbms_sqlpatch.opatch_registry_state INDENT)
INTO ?
FROM dual;
END;";
$dbh->{LongReadLen} = $pending_activity_len;
my $activity_h = $dbh->prepare($activity_sql);
$activity_h->bind_param_inout(1, \$pending_activity,
0, {ora_type=>ORA_CLOB});
# 19051526: Errors returned by get_pending_activity were checked for
# already in check_global_prereqs
$activity_h->execute;
my @qi_debug = $dbh->func('dbms_output_get');
foreach my $line (@qi_debug) {
sqlpatch_log(LOG_DEBUG, "$line\n");
}
# 23501901: Record pending activity to invocation log
sqlpatch_log(LOG_INVOCATION,
"pending activity XML: " . $pending_activity . "\n");
# 17665117: Handle uid
my $patchid = undef;
my $patchuid = undef;
my $missing_nodes = undef;
foreach my $line (split("\n", $pending_activity)) {
sqlpatch_log(LOG_DEBUG, "l: '$line'\n");
if ($line =~ /activityRoot/) {
next;
}
elsif ($line =~ /^ *<p(\d+)>$/) {
# Found new patch, reset UID and missing_nodes
$patchid = $1;
$patchuid = undef;
$missing_nodes = undef;
}
elsif ($line =~ /^ *<patchUId>(\d+)<\/patchUId>$/) {
$patchuid = $1;
}
elsif ($line =~ /^ *<nodeName>(.*)<\/nodeName>$/) {
$missing_nodes = $1;
}
elsif ($line =~ /^ *<\/p(\d+)>$/) {
sqlpatch_log(LOG_DEBUG,
"Found binary patch $patchid/$patchuid, missing nodes $missing_nodes\n");
# Found a new binary patch. Add to the hash of patch descriptions.
# Since we start with the binary query we know it's not there.
my $description = {};
# 20348653: If the patch is missing on more than one node, then
# there will be multiple tags in the return from
# get_pending_activity. Check to see if it's already present in
# the patch descriptions, and if so just add to missing_nodes.
if (exists($patch_descriptions{"$patchid/$patchuid"})) {
$description = $patch_descriptions{"$patchid/$patchuid"};
push(@{$description->{missing_nodes}}, $missing_nodes);
}
else {
$description->{"patchid"} = $patchid;
$description->{"patchuid"} = $patchuid;
if (defined($missing_nodes)) {
push(@{$description->{"missing_nodes"}}, $missing_nodes);
}
$description->{"installed_binary"} = 1;
$description->{"prereq_ok"} = 1;
foreach my $pdb (@pdb_list) {
$description->{pdb_info}{$pdb}{sql_state} = undef;
$description->{pdb_info}{$pdb}{last_action_time} = undef;
$description->{pdb_info}{$pdb}{ru_info} = 0;
}
# Load the rest of the properties from the XML descriptor and
# queryable inventory.
if (load_patch_metadata($description, 1, undef, undef, undef)) {
$ret = 1;
}
# Add the description to the hash
$patch_descriptions{"$patchid/$patchuid"} = $description;
}
}
}
}
}
log_state(LOG_DEBUG, "get_current_patches after getting binary state");
# 19681455: If we have seen problems then let's skip further actions.
if ($ret == 1) {
goto get_current_patches_end;
}
# 22359063: Get SQL state regardless of the settings of $force and $qi
# 26281129: Use the nifty table functions in dbms_sqlpatch
my $all_patches_query =
"SELECT * FROM TABLE(dbms_sqlpatch.all_patches)";
my $all_patches_stmt = $dbh->prepare($all_patches_query);
my $current_ru_query =
"SELECT patch_type, target_version,
TO_CHAR(target_build_timestamp,'$timestamp_format') target_build_timestamp,
target_build_description, action, status, action_time
FROM TABLE(dbms_sqlpatch.current_ru_version)";
my $current_ru_stmt = $dbh->prepare($current_ru_query);
my $last_successful_ru_query =
"SELECT patch_type, target_version,
TO_CHAR(target_build_timestamp, '$timestamp_format') target_build_timestamp,
target_build_description, action, status, action_time
FROM TABLE(dbms_sqlpatch.last_successful_ru_version)";
my $last_successful_ru_stmt = $dbh->prepare($last_successful_ru_query);
my $ru_info_query =
"SELECT patch_id, patch_uid, rowid
FROM sys.dba_registry_sqlpatch_ru_info";
my $ru_info_stmt = $dbh->prepare($ru_info_query);
# For each PDB we need to alter session to that PDB, then get the list
# of patches
# 26281129: Ensure that we will get information about CDB$ROOT as well, even
# if it is not specified by the user. This way we will be able to find
# the information in check_pdb_root_mismatch().
# 27999597: Only if we can switch containers (i.e. not in app mode)
my @pdb_list_and_root = @pdb_list;
if ($container_db && $set_container &&
!grep(/CDB\$ROOT/, @pdb_list_and_root)) {
sqlpatch_log(LOG_DEBUG, "Adding root to pdb list for get SQL patches\n");
push(@pdb_list_and_root, "CDB\$ROOT");
}
foreach my $pdb (@pdb_list_and_root) {
if ($container_db && $set_container) {
sqlpatch_log(LOG_DEBUG, "get SQL patches switching to pdb $pdb\n");
$dbh->do("ALTER SESSION SET CONTAINER = " . $pdb);
}
$all_patches_stmt->execute();
while (my $patch_ref = $all_patches_stmt->fetchrow_hashref) {
sqlpatch_log(LOG_DEBUG,
"sql row: " . Data::Dumper->Dumper(\$patch_ref));
my $patchkey =
$patch_ref->{"PATCH_ID"} . "/" . $patch_ref->{"PATCH_UID"};
# 20348653: Merge properties found in the SQL registry with
# any existing properties found in the binary registry.
my $description;
# Found a new SQL patch. Add to the hash descriptions if needed
if (!exists($patch_descriptions{$patchkey})) {
# Patch does not yet exist, create it first
sqlpatch_log(LOG_DEBUG, "Found new SQL patch $patchkey\n");
$description = {};
$description->{"patchid"} = $patch_ref->{"PATCH_ID"};
$description->{"patchuid"} = $patch_ref->{"PATCH_UID"};
$description->{"installed_binary"} = 0;
$patch_descriptions{$patchkey} = $description;
$description->{"prereq_ok"} = 1;
}
else {
$description = $patch_descriptions{$patchkey};
}
$description->{pdb_info}{$pdb}{sql_state} =
$patch_ref->{"ACTION"} . "/" . $patch_ref->{"STATUS"};
$description->{pdb_info}{$pdb}{last_action_time} =
$patch_ref->{ACTION_TIME};
$description->{pdb_info}{$pdb}{ru_info} = 0;
# Load the rest of the properties from the descriptor or SQL registry
if (load_patch_metadata($description, 0, $patch_ref->{"REGISTRY_ROWID"},
undef, $pdb)) {
$ret = 1;
}
}
# 27283029: Query dba_registry_sqlpatch_ru_info
$ru_info_stmt->execute();
while (my $patch_ref = $ru_info_stmt->fetchrow_hashref) {
sqlpatch_log(LOG_DEBUG,
"sql row: " . Data::Dumper->Dumper(\$patch_ref));
my $patchkey =
$patch_ref->{"PATCH_ID"} . "/" . $patch_ref->{"PATCH_UID"};
# 20348653: Merge properties found in the SQL registry with
# any existing properties found in the binary registry.
my $description;
# Found a new SQL patch. Add to the hash descriptions if needed
if (!exists($patch_descriptions{$patchkey})) {
# Patch does not yet exist, create it first
sqlpatch_log(LOG_DEBUG, "Found new SQL patch $patchkey\n");
$description = {};
$description->{"patchid"} = $patch_ref->{"PATCH_ID"};
$description->{"patchuid"} = $patch_ref->{"PATCH_UID"};
$description->{"installed_binary"} = 0;
$description->{"prereq_ok"} = 1;
$description->{pdb_info}{$pdb}{sql_state} = undef;
$description->{pdb_info}{$pdb}{last_action_time} = undef;
$patch_descriptions{$patchkey} = $description;
}
else {
$description = $patch_descriptions{$patchkey};
}
$description->{pdb_info}{$pdb}{ru_info} = 1;
# Load the rest of the properties from the descriptor or SQL registry
if (load_patch_metadata($description, 0, undef,
$patch_ref->{"ROWID"}, $pdb)) {
$ret = 1;
}
}
# 26281129: Determine current RU state for this PDB
# Default to the feature release if there is nothing installed in SQL
$pdb_info{$pdb}->{ru_version_description} = $feature_release_description;
$current_ru_stmt->execute();
while (my $patch_ref = $current_ru_stmt->fetchrow_hashref) {
sqlpatch_log(LOG_DEBUG,
"current RU sql row: " . Data::Dumper->Dumper(\$patch_ref));
my $ru_hash =
create_version_hash($patch_ref->{PATCH_TYPE},
$patch_ref->{TARGET_VERSION},
$patch_ref->{TARGET_BUILD_TIMESTAMP},
$patch_ref->{TARGET_BUILD_DESCRIPTION});
# Add to all_ru_versions if needed
if (!exists($all_ru_versions{$ru_hash->{version_description}})) {
$all_ru_versions{$ru_hash->{version_description}} = $ru_hash;
}
$pdb_info{$pdb}->{ru_version_description} =
$ru_hash->{version_description};
$pdb_info{$pdb}->{ru_sql_state} =
$patch_ref->{ACTION} . "/" . $patch_ref->{STATUS};
$pdb_info{$pdb}->{ru_last_action_time} =
$patch_ref->{ACTION_TIME};
}
$last_successful_ru_stmt->execute();
while (my $patch_ref = $last_successful_ru_stmt->fetchrow_hashref) {
sqlpatch_log(LOG_DEBUG,
"last successful RU sql ro: " . Data::Dumper->Dumper(\$patch_ref));
my $ru_hash =
create_version_hash($patch_ref->{PATCH_TYPE},
$patch_ref->{TARGET_VERSION},
$patch_ref->{TARGET_BUILD_TIMESTAMP},
$patch_ref->{TARGET_BUILD_DESCRIPTION});
# Add to all_ru_versions if needed
if (!exists($all_ru_versions{$ru_hash->{version_description}})) {
$all_ru_versions{$ru_hash->{version_description}} = $ru_hash;
}
$pdb_info{$pdb}->{last_successful_ru_version_description} =
$ru_hash->{version_description};
}
}
if ($container_db && $set_container) {
# Set container back to root when we're done
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
log_state(LOG_DEBUG, "get_current_patches after getting SQL state");
# 19681455: If we have seen problems then let's skip further actions.
if ($ret == 1) {
goto get_current_patches_end;
}
# 25425451: Look for any directories under ?/sqlpatch that could be SQL
# patches.
opendir (SQLPATCH_DIR, $sqlpatch_dir);
foreach my $patch_dir (readdir(SQLPATCH_DIR)) {
# Patch directories will be all numbers
next if $patch_dir !~ /^(\d)+$/;
if (-d File::Spec->catfile($sqlpatch_dir, $patch_dir)) {
opendir (PATCH_DIR, File::Spec->catfile($sqlpatch_dir, $patch_dir));
foreach my $uid_dir (readdir(PATCH_DIR)) {
# Patch directories will be all numbers
next if $uid_dir !~ /^(\d)+$/;
if (-d File::Spec->catfile($sqlpatch_dir, $patch_dir, $uid_dir)) {
# We've found a potential SQL patch. If we don't already have
# an entry for it in the patch descriptions, then read the XML
# descriptor to find it.
my $patch_key = "$patch_dir/$uid_dir";
if (!exists($patch_descriptions{$patch_key})) {
sqlpatch_log(LOG_DEBUG, "Found new key $patch_key while scanning directories\n");
my $description = {};
$description->{patchid} = $patch_dir;
$description->{patchuid} = $uid_dir;
$description->{installed_binary} = 0;
$description->{prereq_ok} = 1;
foreach my $pdb (@pdb_list_and_root) {
$description->{pdb_info}{$pdb}{sql_state} = undef;
$description->{pdb_info}{$pdb}{last_action_time} = undef;
$description->{pdb_info}{$pdb}{ru_info} = 0;
}
# We don't care if we couldn't load the metadata (the descriptor
# could be missing or an old format) since this potential patch
# is not in either the binary or SQL registry. Add to the hash
# only if we were successful loading the metadata.
if (!load_patch_metadata($description, 0, undef, undef, undef)) {
$patch_descriptions{$patch_key} = $description;
}
}
}
}
close (PATCH_DIR);
}
}
close (SQLPATCH_DIR);
log_state(LOG_DEBUG, "get_current_patches after querying directories");
# 19681455: If we have seen problems then let's skip further actions.
if ($ret == 1) {
goto get_current_patches_end;
}
# 26281129: Determine the binary RU description and set missing nodes for the
# entry in %all_ru_versions.
# Default to the feature release if there is nothing installed in binary
$binary_ru_version_description = $feature_release_description;
foreach my $key (sort keys %patch_descriptions) {
my $description = $patch_descriptions{$key};
if ($description->{PATCH_TYPE} eq "INTERIM" ||
!($description->{installed_binary})) {
next;
}
my $ru_version_description = $description->{ru_version_description};
# If the patch is missing nodes, and is not present on the current node,
# then we will not have an XML descriptor or queryable inventory. Thus
# the ru_version_description will be undef. Skip to the next patch
# in this case.
next if (!defined($ru_version_description));
sqlpatch_log(LOG_DEBUG,
"Checking binary RU key $key ru description $ru_version_description binary description $binary_ru_version_description\n");
if (version_compare($ru_version_description,
$binary_ru_version_description) > 0) {
# Current descriptor is higher than the current binary version
$binary_ru_version_description = $ru_version_description;
}
if ($description->{missing_nodes}) {
sqlpatch_log(LOG_DEBUG, "Setting missing_nodes\n");
$all_ru_versions{$ru_version_description}->{missing_nodes} =
$description->{missing_nodes};
$binary_ru_missing_nodes = 1;
}
}
if ($container_db && $set_container) {
# Set container back to root when we're done
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
log_state(LOG_DEBUG, "get_current_patches complete");
get_current_patches_end:
sqlpatch_log(LOG_ALWAYS, "done\n");
# Print out the current state if needed
print_current_state();
# 21503113: Fill in summary orchestration log
my $o_current_state;
# Setup state structure for binary and current set of PDBs
$o_current_state->{binary} = undef;
foreach my $pdb (@pdb_list) {
$o_current_state->{$pdb} = undef;
}
# Now loop over the patch descriptions and fill in the state structure.
# This is the same logic as in print_current_state().
# First consider interim patches
foreach my $key (sort keys %patch_descriptions) {
my $description = $patch_descriptions{$key};
if ($description->{patch_type} eq "INTERIM" &&
(($app_mode && $description->{application_patch}) ||
(!$app_mode && !$description->{application_patch}))) {
foreach my $pdb (keys %{$description->{pdb_info}}) {
if (defined($description->{pdb_info}{$pdb}{sql_state})) {
push(@{$o_current_state->{$pdb}},
{patchID => $description->{patchid},
patchUID => $description->{patchuid},
patchType => $description->{patch_type},
sqlState => $description->{pdb_info}{$pdb}{sql_state}});
}
}
if (defined($description->{missing_nodes}) ||
$description->{installed_binary}) {
push(@{$o_current_state->{binary}},
{patchID => $description->{patchid},
patchUID => $description->{patchuid},
description => $description->{description},
patchType => $description->{patch_type},
missingNodes => $description->{missing_nodes}});
}
}
}
# Now use $binary_ru_version_description and $pdb_info for RU patches
if (defined($binary_ru_version_description)) {
my $version_hash = $all_ru_versions{$binary_ru_version_description};
my $binary_key = $version_hash->{patch_key};
my $description = $patch_descriptions{$binary_key};
push(@{$o_current_state->{binary}},
{patchID => $description->{patchid},
patchUID => $description->{patchuid},
description => $description->{description},
patchType => $description->{patch_type},
missingNodes =>
$binary_ru_missing_nodes ?
join(',', sort(@{$version_hash->{missing_nodes}})) : "undef"});
foreach my $pdb (@pdb_list) {
if (defined($pdb_info{$pdb}->{ru_version_description}) &&
$pdb_info{$pdb}->{ru_version_description} ne $feature_release_description) {
my $sql_ru_version_description =
$pdb_info{$pdb}->{ru_version_description};
my $version_hash = $all_ru_versions{$sql_ru_version_description};
my $sql_key = $version_hash->{patch_key};
my $description = $patch_descriptions{$sql_key};
push(@{$o_current_state->{$pdb}},
{patchID => $description->{patchid},
patchUID => $description->{patchuid},
description => $description->{description},
patchType => $description->{patch_type},
sqlState => $version_hash->{ru_sql_state}});
}
}
}
$orchestration_summary_hash->{currentState} = $o_current_state;
sqlpatch_log(LOG_DEBUG, "get_current_patches returning $ret\n");
return $ret;
}
# ------------------------- print_current_state -----------------------------
# NAME
# print_current_state
#
# DESCRIPTION
# Prints the current state (install status) for all patches based on the
# patch descriptions, if -verbose or -debug is true.
#
# ARGUMENTS
# None
#
# RETURNS
# None
sub print_current_state() {
sqlpatch_log(LOG_VERBOSE, "\nCurrent state of interim SQL patches:\n");
my $found_interim = 0;
foreach my $key (sort keys %patch_descriptions) {
my $description = $patch_descriptions{$key};
sqlpatch_log(LOG_DEBUG, "Checking description $key\n");
# 20099675/26281129: Use %pdb_info to print the RU state, so
# we only need to print out information here for interim patches.
# 22694961: Subject to app_mode
if (($description->{patch_type} eq "INTERIM") &&
(($app_mode && $description->{application_patch}) ||
(!$app_mode && !$description->{application_patch}))) {
sqlpatch_log(LOG_VERBOSE, "Interim patch " . $description->{"patchid"} .
" (" . $description->{"description"} . "):\n");
$found_interim = 1;
sqlpatch_log(LOG_VERBOSE, " Binary registry: ");
# 20348653: Display information about missing nodes
# 26281129: Check force and noqi
if ($force || $noqi) {
sqlpatch_log(LOG_VERBOSE, "Unknown as -force or -noqi specified\n");
}
elsif ($description->{missing_nodes}) {
sqlpatch_log(LOG_VERBOSE, "Not installed on nodes " .
join(',', sort(@{$description->{missing_nodes}})) . "\n");
}
elsif ($description->{installed_binary}) {
sqlpatch_log(LOG_VERBOSE, "Installed\n");
}
else {
sqlpatch_log(LOG_VERBOSE, "Not installed\n");
}
foreach my $pdb (sort @pdb_list) {
if ($container_db) {
sqlpatch_log(LOG_VERBOSE, " PDB $pdb: ");
}
else {
sqlpatch_log(LOG_VERBOSE, " SQL registry: ");
}
if (!defined($description->{pdb_info}{$pdb}{sql_state})) {
sqlpatch_log(LOG_VERBOSE, "Not installed\n");
}
elsif ($description->{pdb_info}{$pdb}{sql_state} eq "APPLY/SUCCESS") {
sqlpatch_log(LOG_VERBOSE, "Applied successfully on " .
$description->{pdb_info}{$pdb}{last_action_time} . "\n");
}
elsif ($description->{pdb_info}{$pdb}{sql_state} =~
/APPLY\/WITH ERRORS.*/) {
sqlpatch_log(LOG_VERBOSE, "Applied with errors on " .
$description->{pdb_info}{$pdb}{last_action_time} . "\n");
}
elsif ($description->{pdb_info}{$pdb}{sql_state} eq
"ROLLBACK/SUCCESS") {
sqlpatch_log(LOG_VERBOSE, "Rolled back successfully on " .
$description->{pdb_info}{$pdb}{last_action_time} . "\n");
}
else {
sqlpatch_log(LOG_VERBOSE, "Rolled back with errors on " .
$description->{pdb_info}{$pdb}{last_action_time} . "\n");
}
}
}
}
if (!$found_interim) {
sqlpatch_log(LOG_VERBOSE, " No interim patches found\n");
}
sqlpatch_log(LOG_VERBOSE,
"\nCurrent state of release update SQL patches:\n");
# Now we can print information about RU patches based on
# $binary_ru_version_description and $pdb_info.
sqlpatch_log(LOG_VERBOSE, " Binary registry:\n");
if ($force || $noqi) {
sqlpatch_log(LOG_VERBOSE, " Unknown as -force or -noqi specified\n");
}
elsif (!defined($binary_ru_version_description) ||
$binary_ru_version_description eq $feature_release_description) {
sqlpatch_log(LOG_VERBOSE, " No release update patches installed\n");
}
else {
if ($binary_ru_missing_nodes) {
# Print out the RU patches which are missing nodes
foreach my $ru_description (sort keys %all_ru_versions) {
my $version_hash = $all_ru_versions{$ru_description};
sqlpatch_log(LOG_DEBUG,
"Checking version $ru_description for missing nodes\n");
if ($version_hash->{missing_nodes}) {
sqlpatch_log(LOG_VERBOSE, " " . $ru_description .
": Not installed on nodes " .
join(',', sort(@{$version_hash->{"missing_nodes"}})) . "\n");
}
}
}
else {
# Just print out the highest RU patch in binary
sqlpatch_log(LOG_VERBOSE, " " .
$binary_ru_version_description . ": Installed\n");
}
}
foreach my $pdb (sort @pdb_list) {
sqlpatch_log(LOG_DEBUG, "Checking pdb $pdb for RU state\n");
if ($container_db) {
sqlpatch_log(LOG_VERBOSE, " PDB $pdb:\n");
}
else {
sqlpatch_log(LOG_VERBOSE, " SQL registry:\n");
}
if (!defined($pdb_info{$pdb}->{ru_version_description}) ||
(!defined($pdb_info{$pdb}->{ru_last_action_time}) &&
$pdb_info{$pdb}->{ru_version_description} eq $feature_release_description)) {
sqlpatch_log(LOG_VERBOSE, " No release update patches installed\n");
}
else {
my $sql_description = $pdb_info{$pdb}->{ru_version_description};
if ($pdb_info{$pdb}->{ru_sql_state} eq "APPLY/SUCCESS") {
sqlpatch_log(LOG_VERBOSE,
" Applied $sql_description successfully on " .
$pdb_info{$pdb}->{ru_last_action_time} . "\n");
}
elsif ($pdb_info{$pdb}->{ru_sql_state} =~ /APPLY\/WITH ERRORS.*/) {
sqlpatch_log(LOG_VERBOSE,
" Applied $sql_description with errors on " .
$pdb_info{$pdb}->{ru_last_action_time} . "\n");
}
elsif ($pdb_info{$pdb}->{ru_sql_state} eq "ROLLBACK/SUCCESS") {
sqlpatch_log(LOG_VERBOSE,
" Rolled back to $sql_description successfully on " .
$pdb_info{$pdb}->{ru_last_action_time} . "\n");
}
else {
sqlpatch_log(LOG_VERBOSE,
"Rolled back to $sql_description with errors on " .
$pdb_info{$pdb}->{ru_last_action_time} . "\n");
}
}
}
sqlpatch_log(LOG_VERBOSE, "\n");
}
# ---------------------------- add_to_queue ---------------------------------
# NAME
# add_to_queue
#
# DESCRIPTION
# Creates the patch queue based on the patch descriptions.
# Also checks the final patch queue for prereqs, namely that the install
# script exists and the log file directory can be created.
#
# ARGUMENTS
# None
#
# RETURNS
# 0 for success, 1 for prereq check failed
#
sub add_to_queue {
my $prereq_failed = 0;
sqlpatch_log(LOG_ALWAYS,
"Adding patches to installation queue and performing prereq checks...");
# Pseudo code for determining the patch queue:
# 1) Create one entry for each PDB
# 2) Foreach queue entry:
# If $force or $noqi:
# Use command line options to set @interim_rollbacks,
# @interim_applys, @ru_installs
# Else:
# Foreach patch description (interim pass):
# If interim patch, add to @interim_rollbacks or @interim_applys
# RU pass:
# Compare binary and PDB RU versions to create @ru_installs
# Set @notok_components
# 3) Combine entries
# 4) Foreach remaining entry:
# Set source and target versions for @interim_rollbacks and
# @interim_applys
# 5) Print the queue and check queue prereqs, which includes determining the
# contents of @install_files and @actual_files for all of the install
# arrays, and calling set_patch_metadata
# Step 1: Create one entry for each PDB
foreach my $pdb (@pdb_list) {
my $queue_rec = {};
@{$queue_rec->{pdbs}} = ($pdb);
$queue_rec->{num_pdbs} = 1;
$queue_rec->{patch_string} = undef;
$queue_rec->{combined} = 0;
@{$queue_rec->{notok_components}} = ();
push(@patch_queue, $queue_rec);
}
log_state(LOG_DEBUG, "add_to_queue after step 1 (initial create)");
# Step 2: Setup entry based on binary and SQL state
foreach my $entry (@patch_queue) {
my $pdb = @{$entry->{pdbs}}[0]; # Current PDB
sqlpatch_log(LOG_DEBUG, "add_to_queue start of step 2 for PDB $pdb\n");
# Local arrays of install hashrefs for this queue entry
my @interim_applys = (); # Interim patch(es) to be applied
my @interim_rollbacks = (); # Interim patch(es) to be rolled back
my @ru_installs = (); # RU patches to be installed
# Determine apply/rollback JVM, interim, and RU patches
if ($force || $noqi) {
# We need separate apply and rollback entries for RUs so we can
# assemble them in the right order
my @ru_applys;
my @ru_rollbacks;
# Loop over the patch descriptions and add as needed based on $mode and
# $jvm_patch. $mode was set in get_current_patches() based on the
# apply and rollback command line arguments.
# 22359063: If we found a patch while scanning the SQL registry that was
# not in the apply or rollback list, $mode will not be defined and hence
# it will be skipped here.
foreach my $key (sort keys %patch_descriptions) {
my $description = $patch_descriptions{$key};
next if (!defined($description->{mode}));
if ($description->{mode} eq "apply") {
# Add to appropriate apply list if force is set, or
# * the patch is not successfully applied in this PDB, and
# * 25037621: if at least one component is OK
if ($force ||
(!defined($description->{pdb_info}{$pdb}{sql_state}) ||
$description->{pdb_info}{$pdb}{sql_state} ne "APPLY/SUCCESS") &&
!no_components_ok($key, $pdb)) {
sqlpatch_log(LOG_DEBUG, "force/noqi: Adding $key to apply list\n");
$work_to_do = 1;
my $install_entry =
{patch_key => $key, mode => "apply"};
if ($description->{patch_type} eq "INTERIM") {
if ($description->{jvm_patch}) {
# JVM patches go at the front of the list
unshift(@interim_applys, $install_entry);
}
else {
push(@interim_applys, $install_entry);
}
}
else {
# Apply from feature release to RU release
$install_entry->{source_ru_description} =
$feature_release_description;
$install_entry->{target_ru_description} =
$description->{ru_version_description};
push(@ru_applys, $install_entry);
}
}
}
else {
# Add to appropriate rollback list if force is set, or
# * the patch is not successfully rolled back in this PDB, and
# * 25037621: if at least one component is OK
if ($force ||
((defined($description->{pdb_info}{$pdb}{sql_state})) &&
$description->{pdb_info}{$pdb}{sql_state} ne
"ROLLBACK/SUCCESS") &&
!no_components_ok($key, $pdb)) {
sqlpatch_log(LOG_DEBUG,
"force/noqi: Adding $key to rollback list\n");
$work_to_do = 1;
my $install_entry =
{patch_key => $key, mode => "rollback"};
if ($description->{patch_type} eq "INTERIM") {
if ($description->{jvm_patch}) {
# JVM patches go at the front of the list
unshift(@interim_rollbacks, $install_entry);
}
else {
push(@interim_rollbacks, $install_entry);
}
}
else {
# Rollback from RU release to feature release
$install_entry->{source_ru_description} =
$description->{ru_version_description};
$install_entry->{target_ru_description} =
$feature_release_description;
push(@ru_rollbacks, $install_entry);
}
}
}
}
# Set entries in the actual patch queue hash
@{$entry->{interim_rollbacks}} = @interim_rollbacks;
@{$entry->{interim_applys}} = @interim_applys;
push(@{$entry->{ru_installs}}, @ru_rollbacks);
push(@{$entry->{ru_installs}}, @ru_applys);
# Set notok_components
foreach my $patch (@{$entry->{interim_rollbacks}},
@{$entry->{interim_applys}},
@{$entry->{ru_installs}}) {
foreach my $component (@{$patch_descriptions{$patch}->{components}}) {
sqlpatch_log(LOG_DEBUG, "Checking okness for $component $pdb\n");
if (!grep(/$component/, @{$entry->{notok_components}}) &&
(!component_ok($component, $pdb))) {
push(@{$entry->{notok_components}}, $component);
}
}
}
# And move on to the next queue entry
next;
}
# Neither $force nor $qi is set
# Interim patch pass: Loop over all interim patches, and add as needed
# based on binary and SQL state for this PDB.
# 20348653: Skip any patches which are partially installed (missing nodes)
foreach my $key (sort keys %patch_descriptions) {
my $description = $patch_descriptions{$key};
next if (($description->{patch_type} ne "INTERIM") ||
$description->{missing_nodes});
sqlpatch_log(LOG_DEBUG, "add_to_queue step 2 checking interim $key\n");
# If the following conditions are met then this patch needs to
# be applied:
# The binary portion of the patch is installed
if ($description->{installed_binary} &&
# The patch is not installed successfully in this PDB
(!defined($description->{pdb_info}{$pdb}{sql_state}) ||
$description->{pdb_info}{$pdb}{sql_state} ne "APPLY/SUCCESS") &&
# 17665117: Subject to the passed in apply list if present
(!@apply_list || grep(/$key/, @apply_list) ||
grep(/$description->{"patchid"}/, @apply_list)) &&
# 17665122: Subject to upgrade_mode_only
(!$upgrade_mode_only ||
($description->{startup_mode} eq "upgrade")) &&
# 22165897: And $rollback_only
!$rollback_only &&
# 22694961: And $app_mode
(($app_mode && $description->{application_patch}) ||
(!$app_mode && !$description->{application_patch})) &&
# 25037621: At least one component is OK
!no_components_ok($key, $pdb)) {
sqlpatch_log(LOG_DEBUG,
"add_to_queue step 2A adding $key to apply list\n");
$work_to_do = 1;
my $install_entry =
{patch_key => $key, mode => "apply"};
# Set the mode in the patch description
$description->{mode} = "apply";
if ($description->{jvm_patch}) {
# JVM patches go at the front of the list
unshift(@interim_applys, $install_entry);
}
else {
push(@interim_applys, $install_entry);
}
}
# If the following conditions are met then this patch needs to
# be rolled back:
# The binary portion of the patch is not installed
if (!$description->{installed_binary} &&
# The SQL portion is not successfully rolled back
(defined($description->{pdb_info}{$pdb}{sql_state}) &&
$description->{pdb_info}{$pdb}{sql_state} ne
"ROLLBACK/SUCCESS") &&
# 17665117: Subject to the passed in rollback list if present
(!@rollback_list || grep(/$key/, @rollback_list) ||
grep(/$description->{patchid}/, @rollback_list)) &&
# 17665122: Subject to upgrade_mode_only
(!$upgrade_mode_only ||
($description->{startup_mode} eq "upgrade")) &&
# 22165897: And $apply_only
!$apply_only &&
# 22694961: Add $app_mode
(($app_mode && $description->{application_patch}) ||
(!$app_mode && !$description->{application_patch})) &&
# 25037621: And if at least one component is OK
!no_components_ok($key, $pdb)) {
sqlpatch_log(LOG_DEBUG,
"add_to_queue step 2 adding $key to rollback list\n");
$work_to_do = 1;
my $install_entry =
{patch_key => $key, mode => "rollback"};
# Set the mode in the patch description
$description->{mode} = "rollback";
if ($description->{jvm_patch}) {
# JVM patches go at the front of the list
unshift(@interim_rollbacks, $install_entry);
}
else {
push(@interim_rollbacks, $install_entry);
}
}
}
# RU patch pass: Add as needed based on the binary RU and PDB RU versions
if ($binary_ru_missing_nodes) {
# Don't consider RU patches if any are missing nodes in binary
goto end_of_step2_loop;
}
my $sql_ru_version_description = $pdb_info{$pdb}->{ru_version_description};
my $sql_ru_sql_state = $pdb_info{$pdb}->{ru_sql_state};
my $sql_key = $all_ru_versions{$sql_ru_version_description}->{patch_key};
my $binary_key = $all_ru_versions{$binary_ru_version_description}->{patch_key};
sqlpatch_log(LOG_DEBUG, "add_to_queue RU pass " .
"binary: $binary_key $binary_ru_version_description " .
"sql: $sql_key $sql_ru_version_description " .
$sql_ru_sql_state . "\n");
# Helper routine to add to @ru_installs and set the mode in the patch
# descriptions if there is at least one component ok for the patch for
# this pdb.
local *add_ru_install = sub {
my $pdb = shift;
my $key = shift;
my $mode = shift;
my $source_description = shift;
my $target_description = shift;
if (!no_components_ok($key, $pdb)) {
push(@ru_installs,
{patch_key => $key,
mode => $mode,
source_ru_description => $source_description,
target_ru_description => $target_description});
$patch_descriptions{$key}->{mode} = $mode;
}
};
# Before we start comparing the binary and SQL RU state we need to handle
# the case where the SQL RU is not installed successfully.
if (defined($sql_ru_version_description) && defined($sql_ru_sql_state) &&
$sql_ru_sql_state !~ /.*\SUCCESS/) {
if ($sql_ru_version_description eq $binary_ru_version_description) {
# The last RU install was not successful, and the binary version
# matches the version to which we tried to go. This means that the
# user has retried datapatch with the same binary state as the
# previous failed attempt. We need to set the SQL version to the
# last successful RU installed version, and then continue with the
# scenarios. This way if we have a merged RU install, and only the
# second operation failed, we will only re-attempt that second
# operation.
sqlpatch_log(LOG_INVOCATION,
"Current RU not successful, reattempting last install\n");
$sql_ru_version_description =
$pdb_info{$pdb}->{last_successful_ru_version_description};
sqlpatch_log(LOG_DEBUG, "Set sql_ru_version_description to $sql_ru_version_description\n");
$sql_key = $all_ru_versions{$sql_ru_version_description}->{patch_key};
}
else {
# The last RU was not successful, but the current binary version
# is different from the version to which we tried to go. This means
# that the user has changed the state since the last unsuccessful
# attempt (perhaps they rolled back to the original binary level).
# The safest thing to do here is roll back to the feature release from
# the RU to which we tried to install, and then apply to the current
# binary.
sqlpatch_log(LOG_INVOCATION,
"Current RU not successful, binary changed, rolling back to feature and reapplying\n");
#28553468: Do not perform rollback if sql is already at feature_release
if ($feature_release_description ne $sql_ru_version_description) {
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$feature_release_description);
}
# 28391582: In this case, do not perform 'apply' if binary is
# at feature_release
if ($feature_release_description ne $binary_ru_version_description) {
add_ru_install($pdb, $binary_key, "apply",
$feature_release_description,
$binary_ru_version_description);
}
goto end_of_step2_loop;
}
}
# First check the cases where one or both RUs are not defined.
if (!defined($binary_ru_version_description) ||
($binary_ru_version_description eq $feature_release_description)) {
# No RU installed in binary
if ($sql_ru_version_description eq $feature_release_description) {
# No RU installed in binary or SQL
sqlpatch_log(LOG_INVOCATION, "No RU installed in binary or SQL: no install needed\n");
goto end_of_step2_loop;
}
# Rollback the SQL patch to feature
sqlpatch_log(LOG_INVOCATION, "No RU in binary, RU in SQL: Rollback SQL to feature\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description, $feature_release_description);
goto end_of_step2_loop;
}
if (!defined($sql_ru_version_description) ||
($sql_ru_version_description eq $feature_release_description)) {
# No RU installed in SQL. Apply the binary version
sqlpatch_log(LOG_INVOCATION, "No RU in SQL: Apply binary\n");
add_ru_install($pdb, $binary_key, "apply",
$feature_release_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# Both binary and SQL have RU patches installed successfully.
# Useful variables for comparing the binary and SQL RU versions
my $binary_ru_version = $all_ru_versions{$binary_ru_version_description};
my $binary_type = $binary_ru_version->{patch_type};
my $sql_ru_version = $all_ru_versions{$sql_ru_version_description};
my $sql_type = $sql_ru_version->{patch_type};
$binary_ru_version->{version_string} =~ /\d\.(\d)\.(\d).*/;
my $binary_2 = $1; # Second digit of binary version
my $binary_3 = $2; # Third digit of binary version
sqlpatch_log(LOG_DEBUG, "binary_2 $binary_2 binary_3 $binary_3\n");
$sql_ru_version->{version_string} =~ /\d\.(\d)\.(\d).*/;
my $sql_2 = $1; # Second digit of SQL version
my $sql_3 = $2; # Third digit of SQL version
sqlpatch_log(LOG_DEBUG, "sql_2 $sql_2 sql_3 $sql_3\n");
my $ru_compare =
version_compare($binary_ru_version_description,
$sql_ru_version_description);
sqlpatch_log(LOG_DEBUG, "ru_compare: $ru_compare\n");
# 27283029: Determine the common version. This will either be an
# RU or RUR, as those are the only patch types which can have multiple
# branches off them.
my $common_ru_version_string;
if ($binary_2 ne $sql_2) {
# Second digits are not equal. The common version will be the RU
# corresponding to the lowest of them. I.e.
# RUR 18.2.1 vs RUR 18.3.1 -> common = RU 18.2.0
$common_ru_version_string = $database_feature_version . "." .
(($binary_2 < $sql_2) ? $binary_2 : $sql_2) . ".0.0.0";
}
else {
# Second digits are equal, compare the third digit.
if ($binary_3 ne $sql_3) {
# Third digts are not equal. The common version will be the RU or
# RUR corresponding to the lowest of them, i.e.
# RUR 18.2.2 vs RUR 18.2.4 -> common = RUR 18.2.0
# RUR 18.2.2 vs. RU 18.2.
$common_ru_version_string =
$database_feature_version . "." . $binary_2 . "." .
(($binary_3 < $sql_3) ? $binary_3 : $sql_3) . ".0.0";
}
else {
# Second and third digits are both equal. The common version
# will be the RUR corresponding to them.
$common_ru_version_string =
$database_feature_version . "." . $binary_2 . "." .
$binary_3 . ".0.0";
}
sqlpatch_log(LOG_DEBUG,
"common_ru_version_string: $common_ru_version_string\n");
}
# Loop through the RU version hash to find the corresponding one. We
# reverse sort based on the version descriptions, so that if there is
# more than one release of the RU or RUR (with different build timestamps)
# we will pick the most recent one.
my $common_ru_version_description = undef;
foreach my $version_description (reverse sort keys %all_ru_versions) {
if ($all_ru_versions{$version_description}->{patch_type} =~ /(RU|RUR)/ &&
$all_ru_versions{$version_description}->{version_string} eq $common_ru_version_string) {
$common_ru_version_description = $version_description;
last;
}
}
sqlpatch_log(LOG_DEBUG,
"common_ru_version_description: $common_ru_version_description\n");
sqlpatch_log(LOG_DEBUG,
"binary type $binary_type sql type $sql_type\n");
if (!defined($ru_compare)) {
# The version compare is undefined. This means we could not determine
# which was greater, i.e.
# 18.3.2 CU "ODA_Update" -> 18.3.2 CU "Cloud_Emergency_Update"
# Rollback from SQL to common (which could be an RUR); apply from
# common to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary/SQL comparison undefined: rollback from SQL to common; apply from common to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$common_ru_version_description);
add_ru_install($pdb, $binary_key, "apply",
$common_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
if ($ru_compare > 0) {
# Binary RU version > SQL RU version
sqlpatch_log(LOG_DEBUG, "binary RU > SQL RU\n");
if ($sql_type eq "RU" || $sql_type eq "ID") {
# SQL is RU or ID, i.e.
# 18.2.0 RU -> 18.2.1 RUR
# 18.2.0 RU -> 18.3.0 RU
# 18.2.0 RU -> 18.4.0 RUI 0723
# 18.4.0 RU -> 18.4.0 CU "Cloud_Emergency_Update"
# 18.3.0 ID ts1 -> 18.3.0 RU
# Regardless of the binary type, we can do a straight apply from
# SQL to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary > SQL, SQL = RU/ID: apply from SQL to binary\n");
add_ru_install($pdb, $binary_key, "apply",
$sql_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# 27283029: SQL is on an RUR branch (either RUR or CU), and binary > SQL.
# Check the second digit.
if ($binary_2 > $sql_2) {
# SQL is on an RUR branch, and binary is on the RU or a later
# RUR branch, i.e.
# 18.2.1 RUR -> 18.3.0 RU
# 18.2.1 RUR -> 18.3.0 ID
# 18.2.0 CU -> 18.4.2 CU
# Rollback from SQL to common, apply from common to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary > SQL, binary_2 > sql_2, sql = RUR/CU: rollback from SQL to common; apply from common to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$common_ru_version_description);
add_ru_install($pdb, $binary_key, "apply",
$common_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# $binary_2 eq $sql_2. We know that $binary_2 cannot be < $sql_2
# because the binary version > the SQL version.
# 27283029: Check the patch type and third digit.
if ($sql_type eq "RUR") {
# SQL is an RUR, binary > SQL on the same branch (because the second
# digits are the same), i.e.
# 18.2.1 RUR -> 18.2.2 RUR
# 18.2.2 RUR -> 18.2.3 CU
# Regardless of the binary type, we can do a straight apply from
# SQL to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary > SQL, binary_2 = sql_2, SQL = RUR: apply from SQL to binary\n");
add_ru_install($pdb, $binary_key, "apply",
$sql_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# SQL is a CU.
if ($binary_3 eq $sql_3) {
# SQL is a CU, third digits are equal. Since binary > SQL, the
# only possibility is that both binary and SQL are on the same
# CU branch, i.e.
# 18.2.2 CU "ODA_Update" ts1 -> 18.2.2 CU "ODA_Update" ts2
# The build descriptions have to be the same, because otherwise
# the version comparison would have been undefined.
# Apply from SQL to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary > SQL, binary_2 = sql_2, SQL = CU, binary_3 = sql_3: apply from SQL to binary\n");
add_ru_install($pdb, $binary_key, "apply",
$sql_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# SQL is a CU, and $binary_3 > $sql_3. We know that $binary_3 cannot
# be < $sql_3 because the binary version > the SQL version, i.e.
# 18.2.2 CU "Cloud_Emergency_Update" -> 18.2.3 RUR
# 18.2.2 CU "ODA_Update" -> 18.2.4 CU "ODA_Update"
# Rollback from SQL to common; apply from common to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary > SQL, binary_2 = sql_2, sql = CU, binary_3 > sql_3: rollback from SQL to common; apply from common to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$common_ru_version_description);
add_ru_install($pdb, $binary_key, "apply",
$common_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
if ($ru_compare < 0) {
# Binary RU version < SQL RU version
sqlpatch_log(LOG_DEBUG, "binary RU < SQL RU\n");
if ($binary_type eq "RU" || $binary_type eq "ID") {
# Binary is RU or ID, i.e.
# 18.3.2 RUR -> 18.2.0 RU
# 18.3.0 RU -> 18.2.0 RU
# 18.7.19 CU "ODA Update" -> 18.4.0 ID ts1
# 18.3.0 ID ts1 -> 18.2.0 ID ts2
# Regardless of the SQL type, we can do a straight rollback from SQL
# to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary < SQL, binary = RU: rollback from SQL to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# 27283029: Binary is on an RUR branch (either RUR or CU), and binary
# < SQL. Check the second digit.
if ($binary_2 < $sql_2) {
# Binary is on an RUR branch, and SQL is on the RU or a later RUR
# branch, i.e.
# 18.3.0 RU -> 18.2.1 RUR
# 18.3.1 RUR -> 18.2.3 CU
# Rollback from SQL to common, apply from common to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary < SQL, binary_2 < sql_2, binary = RUR/CU: rollback from SQL to common; apply from common to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$common_ru_version_description);
add_ru_install($pdb, $binary_key, "apply",
$common_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# binary_2 eq $sql_2. We know that $binary_2 cannot be > $sql_2
# because the binary version < the SQL version.
# 27283029: Check the patch type and third digit.
if ($binary_type eq "RUR") {
# Binary is an RUR, binary < SQL on the same branch (because the
# second digits are the same), i.e.
# 18.3.3 RUR -> 18.3.1 RUR
# 18.3.2 CU -> 18.3.2 RUR
# Regardless of the SQL type, we can do a straight rollback from
# SQL to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary < SQL, binary_2 = sql_2, binary = RUR: rollback from SQL to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# Binary is a CU.
if ($binary_3 eq $sql_3) {
# Binary is a CU, third digits are equal. Since binary < SQL, the
# only possibility is that both binary and SQL are on the same CU
# branch, i.e.
# 18.2.2 CU "ODA_Update" ts2 -> 18.2.2 CU "ODA_Update" ts1
# The build descriptions have to be the same, because otherwise
# the version comparison would have been undefined.
# Rollback from SQL to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary < SQL, binary_2 = sql_2, binary = CU, binary_3 = sql_3: rollback from SQL to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# Binary is a CU, and $binary_3 < $sql_3. We know that $binary_3 cannot
# be > $sql_3 because the binary version < the SQL version, i.e.
# 18.3.2 RUR -> 18.3.1 CU
# 18.3.3 CU "ODA_Update" -> 18.3.2 CU "ODA_Update"
# Rollback from SQL to common; apply from common to binary.
sqlpatch_log(LOG_INVOCATION,
"Binary < SQL, binary_2 = sql_2, binary = CU, binary_3 < sql_3: rollback from SQL to common; apply from common to binary\n");
add_ru_install($pdb, $sql_key, "rollback",
$sql_ru_version_description,
$common_ru_version_description);
add_ru_install($pdb, $binary_key, "apply",
$common_ru_version_description,
$binary_ru_version_description);
goto end_of_step2_loop;
}
# Binary and SQL have the same RU version - no RU install needed
sqlpatch_log(LOG_INVOCATION, "Binary RU = SQL RU: no install needed\n");
goto end_of_step2_loop;
end_of_step2_loop:
# Set entries in the actual patch queue hash
@{$entry->{interim_rollbacks}} = @interim_rollbacks;
@{$entry->{interim_applys}} = @interim_applys;
@{$entry->{ru_installs}} = @ru_installs;
# Check @ru_installs against $apply_only, $rollback_only, and/or the
# command line apply and rollback lists.
foreach my $install_entry(@ru_installs) {
my $patch_id =
$patch_descriptions{$install_entry->{patch_key}}->{patchid};
if ($install_entry->{mode} eq "apply" &&
($rollback_only ||
(@apply_list &&
!grep(/$install_entry->{patch_key}/, @apply_list) &&
!grep(/$patch_id/, @apply_list)))) {
sqlpatch_log(LOG_DEBUG,
"Clearing ru_installs because " . $install_entry->{patch_key} . " is not in the apply list or rollback_only is set\n");
@{$entry->{ru_installs}} = ();
last;
}
if ($install_entry->{mode} eq "rollback" &&
($apply_only ||
(@rollback_list &&
!grep(/$install_entry->{patch_key}/, @rollback_list) &&
!grep(/$patch_id/, @rollback_list)))) {
sqlpatch_log(LOG_DEBUG,
"Clearing ru_installs because " . $install_entry->{patch_key} . " is not in the rollback list or apply_only is set\n");
@{$entry->{ru_installs}} = ();
last;
}
}
# Set notok_components
foreach my $install_entry (@{$entry->{interim_rollbacks}},
@{$entry->{interim_applys}},
@{$entry->{ru_installs}}) {
my $patch = $install_entry->{patch_key};
sqlpatch_log(LOG_DEBUG, "Checking patch $patch for okness\n");
foreach my $component (@{$patch_descriptions{$patch}->{components}}) {
sqlpatch_log(LOG_DEBUG, "Checking okness for $component $pdb\n");
if (!grep(/$component/, @{$entry->{notok_components}}) &&
(!component_ok($component, $pdb))) {
push(@{$entry->{notok_components}}, $component);
}
}
}
}
log_state(LOG_DEBUG, "add_to_queue after step 2");
# Step 3: Combine queue entries subject to the following criteria:
# * @interim_rollbacks, @interim_applys, and @ru_installs are the same
# * (starting) RU version for the PDBs are the same
# * 25037621: list of notok components are also the same
# We do this by generating a patch string and then using a hash to test
# for uniqueness. We also set $work_to_do for the queue entry.
my %unique_patch_strings;
for (my $queue_index = 0; $queue_index <= $#patch_queue; $queue_index++) {
my $entry = $patch_queue[$queue_index];
my $pdb = @{$entry->{pdbs}}[0];
$entry->{work_to_do} = 0;
sqlpatch_log(LOG_DEBUG,
"Checking queue entry $queue_index for $pdb for combining\n");
# Add contents of install arrays, seperated by spaces
my $patch_string = "IR:";
foreach my $install_entry (@{$entry->{interim_rollbacks}}) {
$patch_string .= " " . $install_entry->{patch_key};
$entry->{work_to_do} = 1;
$work_to_do = 1;
}
$patch_string .= " IA:";
foreach my $install_entry (@{$entry->{interim_applys}}) {
$patch_string .= " " . $install_entry->{patch_key};
$entry->{work_to_do} = 1;
$work_to_do = 1;
}
$patch_string .= " RI:";
foreach my $install_entry (@{$entry->{ru_installs}}) {
$patch_string .= " " . $install_entry->{patch_key};
$entry->{work_to_do} = 1;
$work_to_do = 1;
}
# Add starting RU version
$patch_string .= " " . $pdb_info{$pdb}->{ru_version_description};
# Add list of notok_components
$patch_string .= " NO: " . join(",", @{$entry->{notok_components}});
sqlpatch_log(LOG_DEBUG, "patch_string for $pdb: $patch_string\n");
$entry->{"patch_string"} = $patch_string;
# See if it's unique by adding to the hash
if (exists($unique_patch_strings{$patch_string})) {
# Entry already in the hash
push(@{($patch_queue[$unique_patch_strings{$patch_string}])->{"pdbs"}},
@{$entry->{"pdbs"}}[0]);
# Mark the current entry as combined so it won't be processed
$entry->{"combined"} = 1;
(@patch_queue[$unique_patch_strings{$patch_string}])->{"num_pdbs"} += 1;
}
else {
$unique_patch_strings{$patch_string} = $queue_index;
}
}
log_state(LOG_DEBUG, "add_to_queue after step 3 (combined entries)");
# Step 4: Set source and target versions for interim patches
foreach my $queue_entry (@patch_queue) {
next if ($queue_entry->{combined});
my $pdb = @{$queue_entry->{pdbs}}[0]; # Current PDB
foreach my $install_entry (@{$queue_entry->{interim_rollbacks}}) {
# For an interim rollback, the source and target version will be
# the binary RU description (assuming any RU install is successful,
# that's the version we will be at when the patches are rolled back).
$install_entry->{source_ru_description} =
$install_entry->{target_ru_description} =
$binary_ru_version_description;
}
foreach my $install_entry (@{$queue_entry->{interim_applys}}) {
my $patch_key = $install_entry->{patch_key};
my $description = $patch_descriptions{$patch_key};
# For an interim apply, the source and target version will be
# the current RU for this PDB.
$install_entry->{source_ru_description} =
$install_entry->{target_ru_description} =
$pdb_info{$pdb}->{ru_version_description};
}
}
log_state(LOG_DEBUG, "end of add_to_queue");
sqlpatch_log(LOG_ALWAYS, "done\n");
# Step 5: The queue is complete. Print it out and check for prereqs.
print_queue(1);
$prereq_failed = check_queue_prereqs();
sqlpatch_log(LOG_DEBUG, "add_to_queue returning $prereq_failed\n");
return $prereq_failed;
}
# -------------------------- add_to_retry_queue -------------------------------
# NAME
# add_to_retry_queue
#
# DESCRIPTION
# Creates the retry patch queue based on the patch queue and patch results.
#
# ARGUMENTS
# None
#
# RETURNS
# 0 for success, 1 for prereq check failed
#
sub add_to_retry_queue {
sqlpatch_log(LOG_ALWAYS,
"Adding patches to retry installation queue...");
# Step One: For each PDB in the patch queue, if that PDB is in the
# retry list, create an entry in the retry queue with the patches that need
# to be retried. We will retry the first failed patch and all subsequent
# patches.
foreach my $queue_entry(@patch_queue) {
if (!$queue_entry->{work_to_do} || $queue_entry->{"combined"}) {
next; # Skip PDBs with nothing to do
}
foreach my $retry_pdb (@{$queue_entry->{pdbs}}) {
# 28410364: Should retry all PDBs in retry_pdb_list . Using a different
# grep call to avoid treating '$' in PDB$SEED or CDB$ROOT as regex.
next if (!(grep { $_ eq $retry_pdb } @retry_pdb_list));
sqlpatch_log(LOG_DEBUG,
"add_to_retry_queue processing retry PDB $retry_pdb\n");
# Create our new retry entry by copying the fields from the original
# entry.
my $retry_queue_entry = {};
@{$retry_queue_entry->{pdbs}} = ($retry_pdb);
$retry_queue_entry->{combined} = 0;
@{$retry_queue_entry->{interim_applys}} = ();
@{$retry_queue_entry->{interim_rollbacks}} = ();
@{$retry_queue_entry->{ru_installs}} = ();
$retry_queue_entry->{num_pdbs} = 1;
@{$retry_queue_entry->{notok_components}} = $queue_entry->{notok_components};
my $found_retry = 0;
foreach my $install_entry (@{$queue_entry->{interim_rollbacks}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
if ($description->{pdb_info}{$retry_pdb}{retry}) {
$found_retry = 1;
}
if ($found_retry) {
push(@{$retry_queue_entry->{interim_rollbacks}}, $install_entry);
# 26365451: No duplicate flags
if ($description->{flags} !~ /R/) {
$description->{flags} .= "R";
}
}
}
foreach my $install_entry (@{$queue_entry->{ru_installs}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
if ($description->{pdb_info}{$retry_pdb}{retry}) {
$found_retry = 1;
}
if ($found_retry) {
push(@{$retry_queue_entry->{ru_installs}}, $install_entry);
# 26365451: No duplicate flags
if ($description->{flags} !~ /R/) {
$description->{flags} .= "R";
}
}
}
foreach my $install_entry (@{$queue_entry->{interim_applys}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
if ($description->{pdb_info}{$retry_pdb}{retry}) {
$found_retry = 1;
}
if ($found_retry) {
push(@{$retry_queue_entry->{interim_applys}}, $install_entry);
# 26365451: No duplicate flags
if ($description->{flags} !~ /R/) {
$description->{flags} .= "R";
}
}
}
push(@retry_patch_queue, $retry_queue_entry);
}
}
log_state(LOG_DEBUG, "add_to_retry_queue after first pass");
# Step Two: Combine queue entries using the same logic as in add_to_queue:
# * @interim_rollbacks, @interim_applys, and @ru_installs are the same
# * (starting) RU version for the PDBs are the same
# * 25037621: list of notok components are also the same
# We do this by generating a patch string and then using a hash to test
# for uniqueness. We also set $work_to_do for the queue entry.
my %unique_patch_strings;
for (my $queue_index = 0; $queue_index <= $#patch_queue; $queue_index++) {
my $entry = $retry_patch_queue[$queue_index];
my $pdb = @{$entry->{pdbs}}[0];
$entry->{work_to_do} = 0;
# Add contents of install arrays, seperated by spaces
my $patch_string = "IR:";
foreach my $install_entry (@{$entry->{interim_rollbacks}}) {
$patch_string .= " " . $install_entry->{patch_key};
$entry->{work_to_do} = 1;
$work_to_do = 1;
}
$patch_string .= " IA:";
foreach my $install_entry (@{$entry->{interim_applys}}) {
$patch_string .= " " . $install_entry->{patch_key};
$entry->{work_to_do} = 1;
$work_to_do = 1;
}
$patch_string .= " RI:";
foreach my $install_entry (@{$entry->{ru_installs}}) {
$patch_string .= " " . $install_entry->{patch_key};
$entry->{work_to_do} = 1;
$work_to_do = 1;
}
# Entries with nothing to do don't need to be combined
next if (!$entry->{work_to_do});
# Add starting RU version
$patch_string .= " " . $pdb_info{$pdb}->{ru_version_description};
# Add list of notok_components
$patch_string .= " NO: " . join(",", @{$entry->{notok_components}});
$entry->{"patch_string"} = $patch_string;
# See if it's unique by adding to the hash
if (exists($unique_patch_strings{$patch_string})) {
# Entry already in the hash
push(@{($retry_patch_queue[$unique_patch_strings{$patch_string}])->{"pdbs"}},
@{$entry->{"pdbs"}}[0]);
# Mark the current entry as combined so it won't be processed
$entry->{"combined"} = 1;
(@retry_patch_queue[$unique_patch_strings{$patch_string}])->{"num_pdbs"} += 1;
}
else {
$unique_patch_strings{$patch_string} = $queue_index;
}
}
log_state(LOG_DEBUG, "add_to_retry_queue after second pass");
sqlpatch_log(LOG_ALWAYS, "done\n");
print_queue(2);
# We've already checked the prereqs for the original queue, but we need
# to call update_patch_metadata for each patch being retried to update the
# flags.
foreach my $queue_entry (@retry_patch_queue) {
next if ($queue_entry->{combined} || !$queue_entry->{work_to_do});
foreach my $pdb (@{$queue_entry->{pdbs}}) {
# Switch to the container for this queue entry if needed
if ($container_db && $set_container) {
sqlpatch_log(LOG_DEBUG,
"Switching to container $pdb for add_to_retry_queue\n");
$dbh->do("ALTER SESSION SET CONTAINER = $pdb");
}
my $update_sql =
"BEGIN
sys.dbms_sqlpatch.update_patch_metadata(
p_patch_id => ?,
p_patch_uid => ?,
p_flags => ?);
END;";
my $update_stmt = $dbh->prepare($update_sql);
foreach my $install_entry (@{$queue_entry->{interim_applys}},
@{$queue_entry->{interim_rollbacks}},
@{$queue_entry->{ru_installs}}) {
my $patch_key = $install_entry->{patch_key};
my $description = $patch_descriptions{$patch_key};
$update_stmt->execute($description->{patchid},
$description->{patchuid},
$description->{flags});
}
}
}
# Switch back to the root if needed
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
return 0;
}
# ------------------------- report_prereq_errors -----------------------------
# NAME
# report_prereq_errors
#
# DESCRIPTION
# Scans the patch descriptions for any patches which have errors, and
# reports them to the user
#
# ARGUMENTS
# None
#
# RETURNS
# 1 if any prereq errors where found, 0 otherwise
sub report_prereq_errors {
if (!$debug && !$verbose) {
sqlpatch_log(LOG_ALWAYS, "\n");
}
my $found = 0;
if (defined($global_prereq_failure)) {
$found = 1;
sqlpatch_log(LOG_ALWAYS, "Error: prereq checks failed!\n");
sqlpatch_log(LOG_ALWAYS, "$global_prereq_failure\n");
}
# 18355572: Print errors so the user will know what went wrong
foreach my $patch (sort keys %patch_descriptions) {
if ($patch ne "") {
if (!$patch_descriptions{$patch}->{"prereq_ok"}) {
if (!$found) {
$found = 1;
sqlpatch_log(LOG_ALWAYS, "Error: prereq checks failed!\n");
}
sqlpatch_log(LOG_ALWAYS,
" patch " . $patch_descriptions{$patch}->{"patchid"} . ": " .
$patch_descriptions{$patch}->{"prereq_failed_reason"} . "\n");
}
}
}
return $found;
}
# ----------------------------- print_queue ----------------------------------
# NAME
# print_queue
#
# DESCRIPTION
# Prints patch queue to the user
#
# ARGUMENTS
# None
#
# RETURNS
# None
#
# JSON SUMMARY LOG:
# patchQueue : {
# <PDB name>/<database name> :
# [ {
# patchID : patch ID
# patchUID : patch UPI
# patchType : patch type
# action : <"APPLY" or "ROLLBACK">
# sourceVersion : source version
# targetVersion : target version
# } ...
# ]
# } ...
sub print_queue($) {
my ($attempt) = @_;
if ($attempt eq 1) {
sqlpatch_log(LOG_ALWAYS, "Installation queue:\n");
}
else {
sqlpatch_log(LOG_ALWAYS, "Retry installation queue:\n");
}
my $o_queue;
foreach my $queue_entry ($attempt eq 1 ? @patch_queue : @retry_patch_queue) {
if ($queue_entry->{"combined"}) {
next;
}
# 18355572: Print out work to be done based on final queue
my $indent;
if ($container_db) {
sqlpatch_log(LOG_ALWAYS,
" For the following PDBs: " . join(' ', @{$queue_entry->{"pdbs"}}) .
"\n");
$indent = " ";
}
else {
$indent = " ";
}
foreach my $pdb (@{$queue_entry->{pdbs}}) {
$o_queue->{$pdb} = undef;
}
if (scalar @{$queue_entry->{interim_rollbacks}}) {
sqlpatch_log(LOG_ALWAYS,
$indent . "The following interim patches will be rolled back:\n");
foreach my $install_entry (@{$queue_entry->{"interim_rollbacks"}}) {
my $key = $install_entry->{patch_key};
sqlpatch_log(LOG_ALWAYS,
$indent . " " . $patch_descriptions{$key}->{"patchid"} .
" (" . $patch_descriptions{$key}->{"description"} . ")\n");
foreach my $pdb (@{$queue_entry->{pdbs}}) {
my @o_pdb_queue;
push(@o_pdb_queue,
{patchID => $patch_descriptions{$key}->{patchid},
patchUID => $patch_descriptions{$key}->{patchuid},
patchType => $patch_descriptions{$key}->{patch_type},
action => "ROLLBACK",
sourceVersion => $install_entry->{source_ru_description},
targetVersion => $install_entry->{target_ru_description}});
push(@{$o_queue->{$pdb}}, @o_pdb_queue);
}
}
}
else {
sqlpatch_log(LOG_ALWAYS,
$indent . "No interim patches need to be rolled back\n");
}
if (scalar @{$queue_entry->{ru_installs}}) {
foreach my $install_entry(@{$queue_entry->{ru_installs}}) {
my $key = $install_entry->{patch_key};
sqlpatch_log(LOG_ALWAYS,
$indent . "Patch " . $patch_descriptions{$key}->{"patchid"} .
" (" . $patch_descriptions{$key}->{"description"} . "):\n");
sqlpatch_log(LOG_ALWAYS,
$indent . " " . ucfirst($install_entry->{mode}) .
" from " . $install_entry->{source_ru_description} .
" to " . $install_entry->{target_ru_description} . "\n");
}
}
else {
sqlpatch_log(LOG_ALWAYS,
$indent . "No release update patches need to be installed\n");
}
if (scalar @{$queue_entry->{interim_applys}}) {
sqlpatch_log(LOG_ALWAYS,
$indent . "The following interim patches will be applied:\n");
foreach my $install_entry (@{$queue_entry->{"interim_applys"}}) {
my $key = $install_entry->{patch_key};
sqlpatch_log(LOG_ALWAYS,
$indent . " " . $patch_descriptions{$key}->{"patchid"} .
" (" . $patch_descriptions{$key}->{"description"} . ")\n");
foreach my $pdb (@{$queue_entry->{pdbs}}) {
my @o_pdb_queue;
push(@o_pdb_queue,
{patchID => $patch_descriptions{$key}->{patchid},
patchUID => $patch_descriptions{$key}->{patchuid},
patchType => $patch_descriptions{$key}->{patch_type},
action => "APPLY",
sourceVersion => $install_entry->{source_ru_description},
targetVersion => $install_entry->{target_ru_description}});
push(@{$o_queue->{$pdb}}, @o_pdb_queue);
}
}
}
else {
sqlpatch_log(LOG_ALWAYS,
$indent . "No interim patches need to be applied\n");
}
}
# 21503113: Fill in summary orchestration log
$orchestration_summary_hash->{$attempt eq 1 ? "patchQueue" : "retryPatchQueue"} = $o_queue;
sqlpatch_log(LOG_ALWAYS, "\n");
}
# ----------------------------- check_queue_prereqs --------------------------
# NAME
# check_queue_prereqs
#
# DESCRIPTION
# Verify final prereqs based on the patch queue:
# * The install script for each patch exists and the log file directory
# either exists or can be created.
# * 23170620: (During rollback) if the install directory does not exist then
# we will read it from the SQL registry. (During apply) we will create the
# zip of the directory for insertion into the SQL registry.
# * 24798218: Check whether a queue entry for a PDB will create a mismatch
# with the root
# * Ensure the feature version for the patch matches the current database
# version
# * Determine the install and actual files for each patch and call
# set_patch_metadata for all patches in all PDBs
#
# ARGUMENTS
# None
#
# RETURNS
# 0 for success, 1 if any prereqs failed
# existance of script
sub check_queue_prereqs {
my $prereq_failed = 0;
# Helper routine that returns true if the passed file is an infrastructure
# file
local *is_infrastructure = sub {
my $file = shift;
if ($file eq "rdbms/admin/dbmssqlpatch.sql" ||
$file eq "rdbms/admin/prvtsqlpatch.plb" ||
$file eq "rdbms/admin/dbmsqopi.sql" ||
$file eq "rdbms/admin/prvtqopi.plb") {
return 1;
}
else {
return 0;
}
};
# Helper routine that returns true if the passed file is an RMAN file
local *is_rman_file = sub {
my $file = shift;
if ( ($file =~ /\/dbmsrman\.sql$/) ||
($file =~ /\/dbmsbkrs\.sql$/) ||
($file =~ /\/prvtrmns\.plb$/) ||
($file =~ /\/prvtbkrs\.plb$/) ) {
return 1;
}
else {
return 0;
}
};
foreach my $queue_entry (@patch_queue) {
next if ($queue_entry->{combined});
sqlpatch_log(LOG_INVOCATION,
"Checking prereqs for queue entry for PDBs " .
join(',', @{$queue_entry->{pdbs}}) . "\n");
# Determine @install_files and @actual_files for each patch in this
# queue entry. This will be the same for all PDBs.
foreach my $install_entry(@{$queue_entry->{interim_rollbacks}},
@{$queue_entry->{interim_applys}}) {
my $patch_key = $install_entry->{patch_key};
my $description = $patch_descriptions{$patch_key};
my $mode = $install_entry->{mode};
sqlpatch_log(LOG_INVOCATION, " Interim $mode patch $patch_key:\n");
# Ensure the feature version for the patch matches the current DB version
if ($description->{feature_version} ne $database_feature_version) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"Feature version " . $description->{feature_version} . " in XML " .
"descriptor does not match database feature version $database_feature_version";
$prereq_failed = 1;
next;
}
# Interim patches will only have one rollback version. Non overlays
# will have a version equal to the feature release (i.e. 18.1.0.0.0),
# while overlays will have a version equal to the RU on which they are
# built (i.e. 18.2.3.0.0). Either way there is still only one
# version.
my $rollback_version = @{$description->{rollback_versions}}[0];
my $rollback_directory =
File::Spec->catdir("?", "sqlpatch", $patch_key, "rollback_files",
$rollback_version);
my $oh_directory = File::Spec->catdir("?");
foreach my $file (keys %{$description->{files_hash}}) {
my $file_hash = $description->{files_hash}->{$file};
# 25425451: Never run infrastructure files
if (is_infrastructure($file)) {
sqlpatch_log(LOG_INVOCATION,
" $file: Infrastructure, never install\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}}, $nothing_sql);
next;
}
if (grep(/$file_hash->{component}/,
@{$queue_entry->{notok_components}})) {
# Component not ok, never install
sqlpatch_log(LOG_INVOCATION,
" $file: Component " . $file_hash->{component} . " not OK, never install\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}}, $nothing_sql);
next;
}
if ($mode eq "apply") {
sqlpatch_log(LOG_INVOCATION,
" $file: apply, always install $oh_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($oh_directory, $file));
next;
}
if ($file_hash->{new}) {
sqlpatch_log(LOG_INVOCATION,
" $file: New rollback file, install $rollback_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($rollback_directory, $file));
next;
}
else {
sqlpatch_log(LOG_INVOCATION,
" $file: Existing rollback file, install $oh_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($oh_directory, $file));
next;
}
}
}
foreach my $install_entry(@{$queue_entry->{ru_installs}}) {
my $patch_key = $install_entry->{patch_key};
my $description = $patch_descriptions{$patch_key};
my $mode = $description->{mode};
my $source_version_description = $install_entry->{source_ru_description};
my $target_version_description = $install_entry->{target_ru_description};
sqlpatch_log(LOG_DEBUG, "RU install $patch_key mode $mode source $source_version_description target $target_version_description\n");
# Ensure the feature version for the patch matches the current DB version
if ($description->{feature_version} ne $database_feature_version) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"Feature version " . $description->{feature_version} . " in XML " .
"descriptor does not match database feature version $database_feature_version";
$prereq_failed = 1;
next;
}
sqlpatch_log(LOG_INVOCATION, " Release Update patch $patch_key:\n");
my $source_rollback_directory =
File::Spec->catdir("?", "sqlpatch", $patch_key, "rollback_files",
$all_ru_versions{$source_version_description}->{rollback_files_dir});
my $target_rollback_directory =
File::Spec->catdir("?", "sqlpatch", $patch_key, "rollback_files",
$all_ru_versions{$target_version_description}->{rollback_files_dir});
my $feature_rollback_directory =
File::Spec->catdir("?", "sqlpatch", $patch_key, "rollback_files",
$all_ru_versions{$feature_release_description}->{rollback_files_dir});
sqlpatch_log(LOG_DEBUG,
"source_rollback_directory $source_rollback_directory\n");
sqlpatch_log(LOG_DEBUG,
"target_rollback_directory $target_rollback_directory\n");
sqlpatch_log(LOG_DEBUG,
"feature_rollback_directory $feature_rollback_directory\n");
my $oh_directory = File::Spec->catdir("?");
foreach my $file (keys %{$description->{files_hash}}) {
my $file_hash = $description->{files_hash}->{$file};
my $file_highest_version_description =
$file_hash->{highest_version_description};
my $file_lowest_version_description =
$file_hash->{lowest_version_description};
sqlpatch_log(LOG_DEBUG, "checking $file with mode " .
$file_hash->{mode} . " against mode $mode\n");
# Skip files which do not match the current mode
next if ($file_hash->{mode} !~ /$mode/);
# 25425451: Never run infrastructure files
if (is_infrastructure($file)) {
sqlpatch_log(LOG_INVOCATION,
" $file: Infrastructure, never install\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}}, $nothing_sql);
next;
}
# 28478676: Skip RMAN files in the patch content to avoid any conflicts
# or compilations errors in case of RU patches. We recreate all RMAN
# packages at the end of RU installs.
if (is_rman_file($file)) {
sqlpatch_log(LOG_INVOCATION,
" $file: RMAN file, Skipping RMAN files present in patch content\n");
sqlpatch_log(LOG_INVOCATION,
" RMAN files(present in OH/rdbms/admin) will be run at the end of all RU installs\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}}, $nothing_sql);
next;
}
if (grep(/$file_hash->{component}/,
@{$queue_entry->{notok_components}})) {
# Component not ok, never install
sqlpatch_log(LOG_INVOCATION,
" $file: Component " . $file_hash->{component} . " not OK, never install\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}}, $nothing_sql);
next;
}
if ($force) {
if ($mode eq "apply") {
sqlpatch_log(LOG_INVOCATION,
" $file: force apply, always install $oh_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($oh_directory, $file));
next;
}
else {
if ($file_hash->{new}) {
sqlpatch_log(LOG_INVOCATION,
" $file: force rollback, new file, always install $source_rollback_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($source_rollback_directory, $file));
next;
}
else {
sqlpatch_log(LOG_INVOCATION,
" $file: force rollback, existing file, always install $feature_rollback_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($feature_rollback_directory, $file));
next;
}
}
}
if ($mode eq "apply") {
if (version_compare($file_highest_version_description,
$source_version_description) > 0) {
sqlpatch_log(LOG_INVOCATION,
" $file: apply, highest version $file_highest_version_description > source version $source_version_description, install $oh_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($oh_directory, $file));
next;
}
else {
sqlpatch_log(LOG_INVOCATION,
" $file: apply, highest version $file_highest_version_description <= source version $source_version_description, don't install\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}}, $nothing_sql);
next;
}
}
if (version_compare($file_highest_version_description,
$target_version_description) > 0) {
if ($file_hash->{new}) {
sqlpatch_log(LOG_INVOCATION,
" $file: New rollback, highest version $file_highest_version_description > target version $target_version_description, install $source_rollback_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($source_rollback_directory, $file));
next;
}
else {
# Existing file, if the target version is the current version in
# binary, we can run the files from oracle home.
if ($target_version_description eq $binary_ru_version_description) {
sqlpatch_log(LOG_INVOCATION,
" $file: Existing rollback, highest version $file_highest_version_description > target version $target_version_description, target eq binary, install $oh_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($oh_directory, $file));
next;
}
# 27283029: Check the lowest version description (which is the
# first version in which this file appears).
if (version_compare($file_lowest_version_description,
$target_version_description) > 0) {
# File was not present in the target release, so we can
# safely run the version found in the feature release.
sqlpatch_log(LOG_INVOCATION,
" $file: Existing rollback, highest version $file_highest_version_description > target version $target_version_description, lowest version $file_lowest_version_description > target version $target_version_description, target ne binary, install $feature_rollback_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($feature_rollback_directory, $file));
next;
}
else {
# Otherwise we need to run the files from the target
# rollback directory.
sqlpatch_log(LOG_INVOCATION,
" $file: Existing rollback, highest version $file_highest_version_description > target version $target_version_description, lowest version $file_lowest_version_description >= target version $target_version_description, target ne binary, install $target_rollback_directory\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}},
File::Spec->catfile($target_rollback_directory, $file));
next;
}
}
}
else {
sqlpatch_log(LOG_INVOCATION,
" $file: rollback, highest version $file_highest_version_description <= target version $target_version_description, don't install\n");
push(@{$install_entry->{install_files}}, $file);
push(@{$install_entry->{actual_files}}, $nothing_sql);
next;
}
} # End of $files_hash loop
} # End of @ru_installs loop
# We've identified the set of actual files to be installed for this
# entry, so we can loop over the PDBs and call set_patch_metadata
foreach my $pdb (@{$queue_entry->{pdbs}}) {
# Switch to the container for this queue entry if needed.
if ($container_db && $set_container) {
# 24798218: Check whether this operation would create a mismatch
# with root. We do this before switching into the PDB because
# this check will require a switch into root.
if (check_pdb_root_mismatch($queue_entry, $pdb)) {
$prereq_failed = 1;
next;
}
sqlpatch_log(LOG_DEBUG,
"Switching to container $pdb for check_queue_prereqs\n");
$dbh->do("ALTER SESSION SET CONTAINER = $pdb");
}
# 23170620: Clear existing saved state
$dbh->do("BEGIN sys.dbms_sqlpatch.clear_state; END;");
foreach my $install_entry (@{$queue_entry->{interim_applys}},
@{$queue_entry->{interim_rollbacks}},
@{$queue_entry->{ru_installs}}) {
my $patch_key = $install_entry->{patch_key};
my $description = $patch_descriptions{$patch_key};
sqlpatch_log(LOG_DEBUG,
"check_queue_prereqs checking patch $patch_key for PDB $pdb\n");
# Don't check again if we've already marked it as a prereq failure
next if (!$description->{prereq_ok});
# Check for the existence of the log directory, and create if necessary
unless (-e $description->{"logdir"}) {
sqlpatch_log(LOG_DEBUG,
"creating logdir " . $description->{"logdir"} . "\n");
eval {
make_path($description->{"logdir"});
};
if ($@) {
# Could not create the log directory
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Could not create log directory " . $description->{"logdir"} .
": $@";
$prereq_failed = 1;
next;
}
}
# 23170620: Check for the existence of the patch directory and/or
# install script, and zip/unzip the patch directory if needed.
if (-e $description->{patchdir}) {
if (directory_zip($description->{patch_key}, 1)) {
$prereq_failed = 1;
next;
}
}
else {
if (directory_zip($description->{patch_key}, 0)) {
$prereq_failed = 1;
next;
}
}
# Ensure the install script exists
if ($description->{mode} eq "apply") {
unless (-e $description->{apply_script}) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"apply script " .
$description->{apply_script} . " does not exist";
$prereq_failed = 1;
next;
}
}
else {
unless (-e $description->{rollback_script}) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"rollback script " . $description->{rollback_script} .
" does not exist";
$prereq_failed = 1;
next;
}
}
# 19883092: Skip upgrade check if needed
if (!$skip_upgrade_check &&
$description->{"startup_mode"} eq "upgrade") {
if ($container_db) {
if ($pdb_info{$pdb}->{"startup_mode"} ne "MIGRATE") {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"The pluggable databases that need to be patched must be in upgrade mode";
$prereq_failed = 1;
next;
}
}
else {
# 19723336: For the single tenant case just check %pdb_info and not
# the pdbs list in the queue entry.
# 20939028: The startup mode will be MIGRATE since we now query
# v$containers instead of v$instance
if ($pdb_info{$database_name}->{"startup_mode"} ne "MIGRATE") {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"The database must be in upgrade mode";
$prereq_failed = 1;
next;
}
}
}
# 26281129: Call set_patch_metadata
if (!$prereq_failed) {
my $patch_metadata_sql =
"DECLARE
rec dba_registry_sqlpatch\%ROWTYPE;
BEGIN
rec.patch_id := :patch_id;
rec.patch_uid := :patch_uid;
rec.patch_type := :patch_type;
rec.action := :action;
rec.description := :description;
rec.flags := :flags;
rec.patch_descriptor := :patch_descriptor;
rec.patch_directory := :patch_directory;
rec.source_version := :source_version;
rec.source_build_description := :source_build_description;
rec.source_build_timestamp :=
TO_TIMESTAMP(:source_build_timestamp, '$timestamp_format');
rec.target_version := :target_version;
rec.target_build_description := :target_build_description;
rec.target_build_timestamp :=
TO_TIMESTAMP(:target_build_timestamp, '$timestamp_format');
sys.dbms_sqlpatch.set_patch_metadata(rec);
END;";
my $file_metadata_sql =
"BEGIN
sys.dbms_sqlpatch.set_file_metadata(
p_patch_id => :patch_id,
p_patch_uid => :patch_uid,
p_install_file => :install_file,
p_actual_file => :actual_file);
END;";
my $patch_metadata_stmt = $dbh->prepare($patch_metadata_sql);
$patch_metadata_stmt->bind_param(":patch_id",
$description->{patchid});
$patch_metadata_stmt->bind_param(":patch_uid",
$description->{patchuid});
$patch_metadata_stmt->bind_param(":patch_type",
$description->{patch_type});
$patch_metadata_stmt->bind_param(":action",
$install_entry->{mode});
$patch_metadata_stmt->bind_param(":description",
$description->{description});
# Add 'M' to flags if there is more than one RU install. We
# can't set this in the patch description itself as it depends on
# the PDB state.
my $flags = $description->{flags};
if ($description->{patch_type} ne "INTERIM" &&
scalar @{$queue_entry->{ru_installs}} > 1) {
$flags .= "M";
}
$patch_metadata_stmt->bind_param(":flags", $flags);
my $descriptor =
read_file($description->{xml_descriptor}, {err_mode => 'quiet'});
if (defined($descriptor)) {
sqlpatch_log(LOG_DEBUG,
"descriptor from File::Slurp: $descriptor\n");
$patch_metadata_stmt->bind_param(":patch_descriptor", $descriptor,
{ora_type => ORA_XMLTYPE});
}
else {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"read_file returned $! for " . $description->{xml_descriptor};
$prereq_failed = 1;
next;
}
# 27283029: Pass the patch directory only for interim patches
# 27786772: Pass it for all patch types. This way for an out of
# place rollback to the 18.1 feature release (which does not have the
# fix for 27283029) the directory will be in dba_registry_sqlpatch.
my $patch_zip =
File::Spec->catfile($description->{patchdir},
$description->{patchid} . ".zip");
sqlpatch_log(LOG_DEBUG, "preparing to read $patch_zip\n");
my $directory =
read_file($patch_zip, {err_mode => 'quiet', binmode => ':raw'});
if (defined($directory)) {
sqlpatch_log(LOG_DEBUG,
"Read " . length($directory) . " bytes\n");
$patch_metadata_stmt->bind_param(":patch_directory", $directory,
{ora_type => ORA_BLOB});
}
else {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"read_file returned $! for $patch_zip";
$prereq_failed = 1;
next;
}
my $source_version_description =
$install_entry->{source_ru_description};
my $target_version_description =
$install_entry->{target_ru_description};
$patch_metadata_stmt->bind_param(":source_version",
$all_ru_versions{$source_version_description}->{version_string});
$patch_metadata_stmt->bind_param(":source_build_description",
$all_ru_versions{$source_version_description}->{build_description});
$patch_metadata_stmt->bind_param(":source_build_timestamp",
$all_ru_versions{$source_version_description}->{build_timestamp});
$patch_metadata_stmt->bind_param(":target_version",
$all_ru_versions{$target_version_description}->{version_string});
$patch_metadata_stmt->bind_param(":target_build_description",
$all_ru_versions{$target_version_description}->{build_description});
$patch_metadata_stmt->bind_param(":target_build_timestamp",
$all_ru_versions{$target_version_description}->{build_timestamp});
$patch_metadata_stmt->execute();
# TODO: Perhaps array execute?
my $file_metadata_stmt = $dbh->prepare($file_metadata_sql);
for (my $i = 0; $i <= $#{$install_entry->{install_files}}; $i++) {
$file_metadata_stmt->bind_param(":patch_id",
$description->{patchid});
$file_metadata_stmt->bind_param(":patch_uid",
$description->{patchuid});
$file_metadata_stmt->bind_param(":install_file",
$install_entry->{install_files}[$i]);
$file_metadata_stmt->bind_param(":actual_file",
$install_entry->{actual_files}[$i]);
$file_metadata_stmt->execute();
}
}
} # End of patch loop
# 27283029: Insert into dbms_sqlpatch_ru_info any RU patches which we
# have found that are not already present.
my $load_ru_info_sql =
"BEGIN
INSERT INTO sys.dba_registry_sqlpatch_ru_info
(patch_id,
patch_uid,
patch_descriptor,
ru_version,
ru_build_description,
ru_build_timestamp,
patch_directory)
VALUES
(:patch_id,
:patch_uid,
:patch_descriptor,
:ru_version,
:ru_build_description,
TO_TIMESTAMP(:ru_build_timestamp, '$timestamp_format'),
:patch_directory);
COMMIT;
END;";
my $load_ru_info_stmt = $dbh->prepare($load_ru_info_sql);
foreach my $key (sort keys %patch_descriptions) {
my $description = $patch_descriptions{$key};
next if (($description->{patch_type} eq "INTERIM") ||
(!defined($description->{pdb_info}{$pdb}{ru_info})) ||
($description->{pdb_info}{$pdb}{ru_info}) ||
($description->{missing_nodes}));
sqlpatch_log(LOG_DEBUG, "Calling load_ru_info for " .
$description->{patch_key} . "\n");
# Create the zip if it does not already exist
if (directory_zip($description->{patch_key}, 1)) {
$prereq_failed = 1;
next;
}
$load_ru_info_stmt->bind_param(":patch_id",
$description->{patchid});
$load_ru_info_stmt->bind_param(":patch_uid",
$description->{patchuid});
my $descriptor =
read_file($description->{xml_descriptor}, {err_mode => 'quiet'});
if (defined($descriptor)) {
$load_ru_info_stmt->bind_param(":patch_descriptor", $descriptor,
{ora_type => ORA_XMLTYPE});
}
else {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"read_file returned $! for " . $description->{xml_descriptor};
$prereq_failed = 1;
next;
}
$load_ru_info_stmt->bind_param(":ru_version",
$description->{ru_version});
$load_ru_info_stmt->bind_param(":ru_build_description",
$description->{build_description});
$load_ru_info_stmt->bind_param(":ru_build_timestamp",
$description->{build_timestamp});
my $patch_zip =
File::Spec->catfile($description->{patchdir},
$description->{patchid} . ".zip");
sqlpatch_log(LOG_DEBUG, "preparing to read $patch_zip\n");
my $directory =
read_file($patch_zip, {err_mode => 'quiet', binmode => ':raw'});
if (defined($directory)) {
sqlpatch_log(LOG_DEBUG,
"Read " . length($directory) . " bytes\n");
$load_ru_info_stmt->bind_param(":patch_directory", $directory,
{ora_type => ORA_BLOB});
}
else {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"read_file returned $! for $patch_zip";
$prereq_failed = 1;
next;
}
$load_ru_info_stmt->execute();
}
} # End of PDB loop
} # End of entry loop
# Switch back to root if needed
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
return $prereq_failed;
}
# ----------------------------- install_patches ------------------------------
# NAME
# install_patches
#
# DESCRIPTION
# Installs (applies or rolls back) the patches in the execution queue using
# catcon.
#
# ARGUMENTS
# $attempt: Current patch attempt
#
# RETURNS
# Number of patches installed in total across all PDBs
sub install_patches($) {
my ($attempt) = @_;
my $total_patches = 0;
my $install_iteration = 0;
# 17665122 - We want to ensure that there are no sessions in pdb$seed
# so we switch to cdb$root.
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = cdb\$root");
}
# For each entry in either the patch_queue or the retry_patch_queue
foreach my $queue_entry ($attempt eq 1 ? @patch_queue : @retry_patch_queue) {
if (!$queue_entry->{work_to_do} || $queue_entry->{"combined"}) {
next; # Skip PDBs with nothing to do
}
$install_iteration++;
if ($total_patches == 0) {
sqlpatch_log(LOG_ALWAYS, "Installing patches...\n");
}
# Catcon parameters
my @catcon_scripts; # Array of scripts/statements to run
my $catcon_single_threaded = 1; # Run scripts in order?
my $catcon_root_only = 0; # Run only in the root?
my $catcon_run_per_proc = 0; # Run per process init statements?
my $con_names_incl; # PDBs to include
my $con_names_excl; # PDBs to exclude
my $custom_err_logging_ident; # Custom error logging identifier
my $custom_query; # Custom query for PDBs
if ($container_db && !$connect_string) {
$con_names_incl = join(' ', @{$queue_entry->{"pdbs"}});
}
else {
$con_names_incl = "";
}
# Like we do for bootstrap, print the commands for this queue entry to
# a file which will then be run by catcon. This way we have a log of the
# commands, and our spooling will not include the internal catcon
# statements. The command SQL file will be in the invocation directory,
# with the name install<iteration>.sql. Including the PDB name(s) in
# the file name could generate an extremely long file name.
my $install_sql_file =
File::Spec->catfile($invocation_logdir,
"install" . $install_iteration . ".sql");
my $install_sql_handle;
unless (open($install_sql_handle, ">", $install_sql_file)) {
$total_patches = 0;
$global_prereq_failure =
"Could not create install SQL file $install_sql_file";
return 0;
}
# Utility function to print message with a timestamp
sub install_message {
my ($handle, $msg) = @_;
sqlpatch_log(LOG_DEBUG, "install_message entry with $msg\n");
print $handle
"SET PAGESIZE 0
SELECT '$msg on ' || SYSTIMESTAMP FROM sys.dual;
SET PAGESIZE 10\n";
}
# Header info
print $install_sql_handle
"PROMPT $install_sql_file generated on " . (localtime) . "\n";
if ($container_db) {
print $install_sql_handle
"PROMPT PDBs to be processed by this file: ".
join(' ', @{$queue_entry->{pdbs}}) . "\n\n";
}
else {
print $install_sql_handle
"PROMPT Database to be processed by this file: $database_name\n\n";
}
# 27849825: Reload dbms_registry and RMAN packages before installing
# any patches, in case we need to call them during an interim
# rollback.
my $first_ru = 1;
foreach my $install_entry (@{$queue_entry->{ru_installs}}) {
# 27214132: Check no_components_ok
next if ($install_entry->{no_components_ok});
my $description = $patch_descriptions{$install_entry->{patch_key}};
# Generating only one RU logfile which will record all actions of
# DBMS_REGISTRY and RMAN. Previously we are missing out on validating
# DBMS_REGISTRY because we are not storing that logfile anywhere, it
# is being overshadowed by the next RU log file
if ($first_ru) {
# Generate the ru_logfile name
print $install_sql_handle
"COLUMN ru_logfile NEW_VALUE full_ru_logfile\n";
my $logfile_select = sprintf(
"SELECT '%s' || '%s_ru_%s_' ||
CASE WHEN (sys_context('userenv', 'cdb_name') IS NULL)
THEN name
ELSE name || '_' || replace(sys_context('userenv', 'con_name'), '\$')
END ||
TO_CHAR(systimestamp,
'_YYYYMonDD_HH24_MI_SS',
'NLS_DATE_LANGUAGE=AMERICAN') || '.log' as ru_logfile
FROM v\$database;\n\n",
$description->{logdir},
$description->{patchid},
$install_entry->{mode});
print $install_sql_handle $logfile_select;
$first_ru = 0;
if (!$force && !$noqi) {
# Before invoking the first install script, reload dbmscr.sql and
# prvtcr.plb from the admin directory. This will update the
# dbms_registry constants with the current binary version as stored
# in rdbms/admin/dbms_registry_basic.sql.
# 27200573: Also reload RMAN packages as they depend on the
# binary version as well
print $install_sql_handle "SPOOL &full_ru_logfile APPEND\n";
install_message($install_sql_handle,
"Recreating dbms_registry for " .
$install_entry->{patch_key});
print $install_sql_handle "SET ECHO ON\n";
install_message($install_sql_handle, "Calling dbmscr.sql");
print $install_sql_handle "@?/rdbms/admin/dbmscr.sql\n";
install_message($install_sql_handle, "Calling prvtcr.plb");
print $install_sql_handle "@?/rdbms/admin/prvtcr.plb\n";
install_message($install_sql_handle,
"Recreating dbms_registry complete");
print $install_sql_handle "SPOOL OFF\n";
}
}
}
# 17277459: We need to first execute session_initialize before
# installing any patches
my $session_init_sql =
"BEGIN sys.dbms_sqlpatch.session_initialize(";
$session_init_sql .= ($force ? "TRUE" : "FALSE") . ", ";
$session_init_sql .= ($debug ? "TRUE" : "FALSE") . ", ";
$session_init_sql .= ($app_mode ? "TRUE" : "FALSE") . ", ";
# 25425451: Pass nothing_sql and attempt
$session_init_sql .= "'$nothing_sql', ";
$session_init_sql .= $attempt . ")";
$session_init_sql .= "; END;\n/\n";
print $install_sql_handle $session_init_sql;
# Install interim rollbacks, then RU patches, then interim applys
foreach my $install_entry (@{$queue_entry->{interim_rollbacks}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
# We need to increment total patches by the number of PDBs in this
# entry since we are going to run it in each PDB
$total_patches += $queue_entry->{"num_pdbs"};
install_message($install_sql_handle,
"Calling interim rollback patch " . $install_entry->{patch_key});
if ($install_entry->{mode} eq "apply") {
print $install_sql_handle "@" . $description->{apply_script};
}
else {
print $install_sql_handle "@" . $description->{rollback_script};
}
# 26281129: Only the logfile directory is passed
print $install_sql_handle " " . $description->{logdir} . "\n";
}
my $ru_installs_exist = 0;
foreach my $install_entry (@{$queue_entry->{ru_installs}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
# We need to increment total patches by the number of PDBs in this
# entry since we are going to run it in each PDB
$total_patches += $queue_entry->{"num_pdbs"};
$ru_installs_exist = 1;
# Update the row in the state table
print $install_sql_handle
"BEGIN
sys.dbms_sqlpatch.update_patch_metadata(
p_patch_id => " . $description->{patchid} . ",
p_patch_uid => " . $description->{patchuid} . ",
p_ru_logfile => '&full_ru_logfile');
END;\n/\n";
if ($install_entry->{mode} eq "apply") {
print $install_sql_handle "@" . $description->{apply_script};
}
else {
print $install_sql_handle "@" . $description->{rollback_script};
}
# 26281129: Only the logfile directory is passed
print $install_sql_handle " " . $description->{logdir} . "\n";
# Invoke RU_apply or RU_rollback to update the versions in dba_registry
# and whatever other housekeeping is needed
print $install_sql_handle "SPOOL &full_ru_logfile APPEND\n";
install_message($install_sql_handle,
"Calling RU_apply/RU_rollback for " . $install_entry->{patch_key});
my $target_version =
$all_ru_versions{$install_entry->{target_ru_description}}->{version_string};
if ($install_entry->{mode} eq "apply") {
print $install_sql_handle
"exec sys.dbms_registry.RU_apply('" . $target_version . "');\n";
}
else {
print $install_sql_handle
"exec sys.dbms_registry.RU_rollback('" . $target_version . "');\n";
}
print $install_sql_handle "SPOOL OFF\n";
}
# 28478676: Recreate RMAN packages at the end of all RU installs
# If there are any RU installs, then ru_installs_exist would be set to 1 in
# the above loop. We skip RMAN files while installing RU pactch content
# to avoid any compilation errors and also because we load them at the
# end(here) of RU installs anyway.
# 27200573: Also reload RMAN packages as they depend on the
# binary version as well
if ($ru_installs_exist) {
print $install_sql_handle "SPOOL &full_ru_logfile APPEND\n";
install_message($install_sql_handle,
"Recreating RMAN packages ");
print $install_sql_handle "ALTER SESSION SET CURRENT_SCHEMA = SYS;\n";
print $install_sql_handle "SET ECHO ON\n";
install_message($install_sql_handle, "Calling dbmsrman.sql");
print $install_sql_handle "@?/rdbms/admin/dbmsrman.sql\n";
install_message($install_sql_handle, "Calling dbmsbkrs.sql");
print $install_sql_handle "@?/rdbms/admin/dbmsbkrs.sql\n";
install_message($install_sql_handle, "Calling prvtrmns.plb");
print $install_sql_handle "@?/rdbms/admin/prvtrmns.plb\n";
install_message($install_sql_handle, "Calling prvtbkrs.plb");
print $install_sql_handle "@?/rdbms/admin/prvtbkrs.plb\n";
print $install_sql_handle "ALTER SESSION SET CURRENT_SCHEMA = SYS;\n";
install_message($install_sql_handle,
"Recreating RMAN packages complete");
print $install_sql_handle "SPOOL OFF\n";
}
foreach my $install_entry (@{$queue_entry->{interim_applys}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
# We need to increment total patches by the number of PDBs in this
# entry since we are going to run it in each PDB
$total_patches += $queue_entry->{"num_pdbs"};
install_message($install_sql_handle,
"Calling interim apply patch " . $install_entry->{patch_key});
if ($install_entry->{mode} eq "apply") {
print $install_sql_handle "@" . $description->{apply_script};
}
else {
print $install_sql_handle "@" . $description->{rollback_script};
}
# 26281129: Only the logfile directory is passed
print $install_sql_handle " " . $description->{logdir} . "\n";
}
close($install_sql_handle);
push(@catcon_scripts, "@" . $install_sql_file);
# 19189525: Set PDBs to include for catconExec not catcatonInit
my $catcon_ret =
catcon::catconExec(@catcon_scripts, $catcon_single_threaded,
$catcon_root_only, $catcon_run_per_proc,
$con_names_incl, $con_names_excl,
$custom_err_logging_ident,
$custom_query);
if ($catcon_ret) {
# 19501299: Fail immediately and return without installing further
# patches.
sqlpatch_log(LOG_ALWAYS,
"\ncatconExec failed during patch installation\n");
$catcon_ok = 0;
return 0;
}
}
install_patches_complete:
sqlpatch_log(LOG_ALWAYS,
"Patch installation complete. Total patches installed: $total_patches\n\n");
return $total_patches;
}
# ---------------------------- validate_logfiles ----------------------------
# NAME
# validate_logfiles
#
# DESCRIPTION
# Scans each logfile for ignorable errors, and updates the SQL registry
# (for each PDB if necessary) indicating success or failure.
#
# ARGUMENTS
# $attempt: Current patch attempt
#
# RETURNS
# 0 for success, 2 if one or more logfiles contains a non-ignorable error,
# 3 if one or more logfiles contains a retryable error (and there are no
# non-ignorable errors).
#
# JSON SUMMARY LOG:
# patchResults : {
# <PDB name>/<database name> :
# [ {
# patchID : patch ID
# patchUID : patch UID
# mode : patch mode
# status : SUCCESS/WITH ERRORS
# logfile : path to logfile
# errors :
# [ {
# line : <line no>
# error : <error text>
# } ...
# ]
# ru_logfile : path to RU logfile or undef
# ru_errors :
# [ {
# line : <line no>
# error : <error text>
# } ...
# ]
# } ...
# ]
# } ...
sub validate_logfiles($) {
my ($attempt) = @_;
my $ret = 0;
my $found_error = 0;
my $found_retryable_error = 0;
my $o_results;
sqlpatch_log(LOG_ALWAYS, "Validating logfiles...");
if ($debug) {
sqlpatch_log(LOG_ALWAYS, "\n");
}
my $registry_update_sql =
"UPDATE dba_registry_sqlpatch
SET status = ?, action_time = SYSTIMESTAMP
WHERE rowid =
(SELECT s_current_registry_rowid
FROM sys.dbms_sqlpatch_state
WHERE s_current_patch_id = ?
AND s_current_patch_uid = ?)";
my $registry_update_stmt = $dbh->prepare($registry_update_sql);
# For each entry in either the patch_queue or the retry_patch_queue
foreach my $queue_entry ($attempt eq 1 ? @patch_queue : @retry_patch_queue) {
if (!$queue_entry->{work_to_do} || $queue_entry->{"combined"}) {
next;
}
foreach my $pdb (@{$queue_entry->{pdbs}}) {
my $retry_pdb = 0;
if ($container_db && $set_container) {
sqlpatch_log(LOG_DEBUG,
"validate_logfiles switching to pdb $pdb\n");
# Switch to PDB first
$dbh->do("ALTER SESSION SET CONTAINER = $pdb");
# 24450424: No need to open the seed read write as catcon now
# does it for us
}
my $prior_ru_failed = 0;
foreach my $install_entry (@{$queue_entry->{interim_rollbacks}},
@{$queue_entry->{ru_installs}},
@{$queue_entry->{interim_applys}}) {
my $patch_key = $install_entry->{patch_key};
my $description = $patch_descriptions{$patch_key};
my $result_rec = {};
$prior_ru_failed = 0 if ($description->{patch_type} eq "INTERIM");
$ret =
validate_one_logfile($pdb, $patch_key, $install_entry->{mode},
$attempt, $result_rec, $prior_ru_failed);
if ($ret eq 2) {
$found_error = 1;
$prior_ru_failed = 1 if ($description->{patch_type} ne "INTERIM");
}
elsif ($ret eq 3) {
# 25425451: Set retryable properties
$patch_descriptions{$install_entry->{patch_key}}->{pdb_info}{$pdb}{retry} = 1;
$found_retryable_error = 1;
$retry_pdb = 1;
$prior_ru_failed = 1 if ($description->{patch_type} ne "INTERIM");
}
sqlpatch_log(LOG_DEBUG, "set prior_ru_failed to $prior_ru_failed\n");
# Store result and update the SQL registry
if ($attempt eq 1) {
push(@patch_results, $result_rec);
}
else {
push(@retry_patch_results, $result_rec);
}
$registry_update_stmt->execute($result_rec->{status},
$description->{patchid},
$description->{patchuid});
sqlpatch_log(LOG_DEBUG,
"updated registry to " . $result_rec->{"status"} .
" for $patch_key mode " . $install_entry->{mode} . "\n");
}
if ($retry_pdb) {
push(@retry_pdb_list, $pdb);
}
}
}
sqlpatch_log(LOG_DEBUG,
"Patch results:\n" . Data::Dumper->Dumper(\@patch_results));
# All log files have been validated, display results if needed
sqlpatch_log(LOG_ALWAYS, "done\n");
foreach my $result_entry ($attempt eq 1 ? @patch_results : @retry_patch_results) {
my $patchid = $patch_descriptions{$result_entry->{"patch"}}->{"patchid"};
my $o_entry =
{patchID => $patchid,
patchUID => $patch_descriptions{$result_entry->{patch}}->{patchuid},
mode => $result_entry->{mode},
status => $result_entry->{status},
logfile => $result_entry->{logfile},
ru_logfile => $result_entry->{ru_logfile}};
sqlpatch_log(LOG_ALWAYS, "Patch $patchid " .
$result_entry->{"mode"} .
($container_db ? (" (pdb " . $result_entry->{"pdb"} . ")") : "") .
": " . $result_entry->{"status"} . "\n");
foreach my $type ("", "ru_") {
# 23113885/26281129: Display errors for ru_logfile only if needed
my $log_location = LOG_ALWAYS;
if ($type eq "ru_" && $result_entry->{status} ne "WITH ERRORS (RU)") {
$log_location = LOG_INVOCATION;
}
next if (!defined($result_entry->{$type . "logfile"}));
sqlpatch_log($log_location, " $type" . "logfile: " .
$result_entry->{$type . "logfile"});
if ($result_entry->{status} eq "WITH ERRORS (PRIOR RU)") {
sqlpatch_log($log_location, " (not checked)\n");
$o_entry->{$type . "errors"} = undef;
}
elsif ($#{$result_entry->{$type . "errors"}} eq -1) {
sqlpatch_log($log_location, " (no errors)\n");
$o_entry->{$type . "errors"} = undef;
}
else {
sqlpatch_log($log_location, " (errors)\n");
for (my $i = 0; $i <= $#{$result_entry->{$type . "errors"}}; $i++) {
sqlpatch_log($log_location,
" Error at line " .
$result_entry->{$type . "error_lines"}[$i] .
": " . $result_entry->{$type . "errors"}[$i] . "\n");
push(@{$o_entry->{$type . "errors"}},
{$type . "line" => $result_entry->{$type . "error_lines"}[$i],
$type . "error" => $result_entry->{$type . "errors"}[$i]});
}
}
}
push(@{$o_results->{$result_entry->{pdb}}}, $o_entry);
}
# 21503113: Fill in summary orchestration log
$orchestration_summary_hash->{$attempt eq 1 ? "patchResults" : "retryPatchResults"} = $o_results;
#22204310: Switch back to root
if ($container_db && $set_container) {
sqlpatch_log(LOG_DEBUG, "Switching to CDB\$ROOT before wrapup\n");
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
if ($found_error) {
# For the orchestration failure reason
$global_prereq_failure = "patch install failure";
return 2;
}
elsif ($found_retryable_error) {
return 3;
}
else {
return 0;
}
}
# ------------------------------- validate_one_logfile -----------------------
# NAME
# validate_one_logfile
#
# DESCRIPTION
# Determines the logfile(s) for a given patch installation or bootstrap
# attempt and checks for errors. The result record is returned, but
# no registries or other data structures are updated.
#
# ARGUMENTS
# $pdb: PDB to which this patch was installed (database name if not a
# container DB). The session should already be pointing to the
# correct PDB prior to calling this routine.
# $patch_key: key of the patch which was installed
# $mode: "apply", "rollback", "bootstrap"
# $attempt: Attempt number
# $result_rec: Reference to result record to be updated
# $prior_ru_failed: If true, assume a prior RU installation failed and
# thus fail this one without scanning the logfile
#
# RETURNS
# 0 for no errors, 2 if a non-ignorable error appears in the logfile
# or if the second update in the install script did not complete.
# 25161298/25425451: If all errors are retryable, then $retryable is set
# in the result rec, and 3 is returned.
sub validate_one_logfile($$$$$$) {
my ($pdb, $patch_key, $mode, $attempt, $result_rec, $prior_ru_failed) = @_;
my $retval;
my $patchid;
my $patchuid;
my $found_non_retryable_error = 0;
my $found_retryable_error = 0;
sqlpatch_log(LOG_DEBUG,
"validate_one_logfile($pdb, $patch_key, $mode, $attempt, --, $prior_ru_failed) entry\n");
# 17277459: Handle bootstrap mode
if ($mode eq "bootstrap") {
# 25161298: And attempt
$result_rec->{"logfile"} =
$pdb_info{$pdb}{bootstrap_info}{"bootstrap" . $attempt . "_log"};
$result_rec->{patch} = undef;
$result_rec->{pdb} = $pdb;
$result_rec->{mode} = undef;
$result_rec->{ru_logfile} = undef;
$result_rec->{retryable} = 0;
}
else {
# 17665117: Split into patch id and UID
($patchid, $patchuid) = split(/\//, $patch_key);
sqlpatch_log(LOG_DEBUG,
"validate_one_logfile checking patch $patch_key mode $mode\n");
# Get logfile by querying the registry
# 23113885: Also get ru_logfile
# 25425451: Use saved rowid
my $registry_query =
"SELECT status, logfile, ru_logfile
FROM dba_registry_sqlpatch
WHERE rowid =
(SELECT s_current_registry_rowid
FROM sys.dbms_sqlpatch_state
WHERE s_current_patch_id = ?
AND s_current_patch_uid = ?)";
($result_rec->{status}, $result_rec->{logfile},
$result_rec->{ru_logfile}) =
$dbh->selectrow_array($registry_query, undef,
($patchid, $patchuid));
$result_rec->{"patch"} = $patch_key;
$result_rec->{"mode"} = $mode;
$result_rec->{"pdb"} = $pdb;
}
sqlpatch_log(LOG_DEBUG,
"validate_one_logfile checking " . $result_rec->{logfile} .
", " . $result_rec->{ru_logfile} . ", status " .
$result_rec->{status} . "\n");
# 26281129: We need to handle RU validation delicately. If there is
# only one RU install, then we call validate_one_logfile and whatever
# it returns is the final status. If there is more than one, then
# if the first fails with either a retryable or non ignorable error,
# we don't validate subsequent installs and instead mark them as
# WITH ERRORS (PRIOR RU).
if ($prior_ru_failed) {
$result_rec->{status} = "WITH ERRORS (PRIOR RU)";
return 2;
}
# 17354355: Don't return if the status is not END, we need to check the
# logfiles to be completely sure.
# 17277459: No need to scan the catcon log
my $sqlpatch_logok = 1;
my $ru_logok = 1;
# 17981677/25690207: Set up the list of local ignorable errors, which will
# be checked in addition to the file ignorable errors. If the user
# specified ignorable errors, add them to the standard list. If not, then
# just use the standard list. For bootstrap, we use only the user list
# (if any).
my @local_ignorable_errors;
if ($mode eq "bootstrap") {
@local_ignorable_errors = @user_ignorable_errors;
}
else {
@local_ignorable_errors =
(@standard_ignorable_errors, @user_ignorable_errors);
}
sqlpatch_log(LOG_DEBUG,
"local ignorable errors: @local_ignorable_errors\n");
# Check log files for errors
# 17354111: Check catbundle logs as well
# 17277459: No need for catbundle logs
# 23113885: Check ru logfile
foreach my $type ("sqlpatch", "ru") {
# In bootstrap mode we only want to process the sqlpatch type
next if (($mode eq "bootstrap") && ($type ne "sqlpatch"));
my @file_ignorable_errors = undef;
my $lineno = 0;
my $lines;
my $errors;
my $logfile;
my $ok_ref;
if ($type eq "sqlpatch") {
$lines = "error_lines";
$errors = "errors";
$logfile = $result_rec->{"logfile"};
$ok_ref = \$sqlpatch_logok;
}
elsif ($type eq "ru") {
$lines = "ru_error_lines";
$errors = "ru_errors";
$logfile = $result_rec->{ru_logfile};
$ok_ref = \$ru_logok;
}
if (defined($logfile)) {
sqlpatch_log(LOG_DEBUG, "Scanning file $logfile for errors\n");
# 14372248: sqlplus errors are of the format SP2-xxxx
# 19044962: Catch PL/SQL errors and compilation warnings
# ORA- and SP2-
my $error_regexp = "(.*PL/SQL: |^)[A-Z]{2}[A-Z2]-\\d{4,5}";
# PLS-xxxxx
$error_regexp .= "|PLS-\\d\\d\\d\\d\\d";
# compilation errors
$error_regexp .= "|^Warning: .* with compilation errors";
# PL/SQL errors in show errors output
$error_regexp .= "| PL/SQL: .*";
if (! -e $logfile) {
# Logfile should always be present
$sqlpatch_logok = 0;
push(@{$result_rec->{$lines}}, 0);
push(@{$result_rec->{$errors}}, "Could not open logfile $logfile");
$result_rec->{retryable} = 0;
}
my $current_file = undef;
open (LOGFILE, $logfile);
while (my $line = <LOGFILE>) {
chomp $line;
$lineno++;
# 24346821: Set current_file by looking for the entries printed at
# the start
if ($line =~ /^Calling (.*) on (.*)$/) {
sqlpatch_log(LOG_DEBUG, "Set current_file to $current_file\n");
$current_file = $1;
}
# 25056052: If echo is on we will see comments, which may include error
# strings. Hence we check for comments and skip as needed.
# Due to the complexity of parsing multiline comments (which could be
# unbalanced if quoted), we explicitly are only checking for the
# various forms of single line comments that sqlplus supports.
# 24346821: With a possibly different sqlprompt
if ($line =~ /^.*(rem|--|DOC>)/i) {
# One line comment of various forms
next;
}
# 24285405: Handle the case where echo is on
# 24346821: With a possibly different sqlprompt
if ($line =~ /^(.*PROMPT )*IGNORABLE ERRORS: (.*)/) {
sqlpatch_log(LOG_DEBUG, " Found new ignorable error line: $line\n");
if ($2 eq "NONE") {
@file_ignorable_errors = undef;
}
else {
@file_ignorable_errors = split(/,/, $2)
}
sqlpatch_log(LOG_DEBUG,
" file ignorable errors: @file_ignorable_errors\n");
}
elsif ($line =~ /($error_regexp)/) {
# 24346821: If we are not executing a file (i.e. still in the top
# portion where we are echoing the values of the arguments) then
# do not raise the error (as long as we are processing an apply
# or rollback file, not post patching or bootstrap).
next if (($type eq "sqlpatch") &&
(($mode eq "apply") || ($mode eq "rollback")) &&
!$current_file);
my $error_line = $1;
# 21273084: Strip PL/SQL from error line
if ($error_line =~ /.*PL\/SQL: (.*)/) {
$error_line = $1;
}
sqlpatch_log(LOG_DEBUG, " Found error line $error_line\n");
if ((grep(/$error_line/, @file_ignorable_errors) eq 0) &&
(grep(/$error_line/, @local_ignorable_errors) eq 0)) {
# Found error line which is not in the current list
sqlpatch_log(LOG_DEBUG, " Error at line $lineno: $line\n");
push(@{$result_rec->{$lines}}, $lineno);
push(@{$result_rec->{$errors}}, $line);
$$ok_ref = 0;
# 25161298: Calculate retryable
if (grep(/$error_line/, @retryable_errors)) {
sqlpatch_log(LOG_DEBUG, " Found retryable error\n");
$found_retryable_error = 1;
}
else {
$found_non_retryable_error = 1;
}
}
}
}
}
else {
# 23113885: logfile is not defined, which means that we couldn't
# find it in the SQL registry. If we are supposed to have it then
# mark as failed.
if ($type eq "sqlpatch") {
# Logfile should always be present
$$ok_ref = 0;
push(@{$result_rec->{$lines}}, 0);
push(@{$result_rec->{$errors}},
"Could not find logfile in SQL registry");
$result_rec->{logfile} = "unknown"; # Set to defined value
}
elsif (($type eq "ru") &&
($patch_descriptions{$patch_key}->{patch_type} ne "INTERIM")) {
# RU logfile is supposed to be present
$$ok_ref = 0;
push(@{$result_rec->{$lines}}, 0);
push(@{$result_rec->{$errors}},
"Could not find RU logfile in SQL registry");
$result_rec->{ru_logfile} = "unknown"; # Set to defined value
}
}
}
if ($sqlpatch_logok && $ru_logok) {
$result_rec->{"status"} = "SUCCESS";
$retval = 0;
}
else {
$result_rec->{"status"} =
(!$sqlpatch_logok) ? "WITH ERRORS" : "WITH ERRORS (RU)";
if ($found_retryable_error && !$found_non_retryable_error) {
$result_rec->{status} = "WITH ERRORS (RETRYABLE)";
$result_rec->{retryable} = 1;
$retval = 3;
}
else {
$retval = 2;
}
}
return $retval;
}
# ------------------------------- bootstrap ----------------------------------
# NAME
# bootstrap
#
# DESCRIPTION
# Bootstraps the registry and package if needed. If the SQL registry table
# and view are missing columns, then they are added. If the dbms_sqlpatch
# package is missing, invalid, or the wrong version, it is (re)created.
#
# 2516120: Retries once in case of timeout errors
#
# ARGUMENTS
# None
#
# RETURNS
# 0 for success, 1 for failure during the bootstrap process
#
# JSON SUMMARY LOG:
# initialize : {
# bootstrap : {
# <PDB name>/<database name> :
# status : SUCCESS/WITH ERRORS
# bootstrapLog : path to log file
# bootstrapErrors : {
# [ {
# line : <line no>
# error : <error text>
# } ...
# ]
# bootstrapRetryLog : path to retried log file
# bootstrapRetyErrors : {
# [ {
# line : <line no>
# error : <error text>
# } ...
# ]
# } ...
# } ...
# }
sub bootstrap {
my $ret;
my $attempt = 1;
my @retry_pdb_list;
my $o_bootstrap = {};
my $bootstrap_iteration = 0;
my %binary_bootstrap_info;
if ($skip_bootstrap || $prereq_only) {
# Nothing to do
goto bootstrap_complete;
}
# 22923409: Ensure that we are not connected to pdb$seed
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = cdb\$root");
}
attempt_bootstrap:
sqlpatch_log(LOG_DEBUG, "bootstrap attempt $attempt\n");
if ($attempt eq 1) {
sqlpatch_log(LOG_ALWAYS,
"Bootstrapping registry and package to current versions...");
}
else {
sqlpatch_log(LOG_ALWAYS,
"\nRetrying bootstrap after retryable errors or timeout....");
}
sqlpatch_log(LOG_DEBUG, "\n");
# 25425451: Array of hash refs which represent the bootstrap queue. This
# will be sorted the same way as @pdb_list, i.e. ordered by con_id so
# CDB$ROOT is first. Each entry is a hash ref with the following fields:
# @pdbs: Array of PDB names. If the DB is not a container database,
# there will be only one entry with the database name.
# $combined: true if this entry has been combined with another entry
# $bootstrap_string: Encodes the bootstrap scripts to be run
# $bootstrap_needed: True if at least 1 of the bootstrap scripts needs
# to be run
# $run_sqlpatch_bootstrap: True if sqlpatch_bootstrap needs to be run
# $run_dbmssqlpatch: True if dbmssqlpatch.sql needs to be run
# $run_prvtsqlpatch: True if prvtsqlpatch.plb needs to be run
# $run_dbmsqopi: True if dbmsqopi.sql needs to be run
# $run_prvtqopi: True if prvtqopi.plb needs to be run
my @bootstrap_queue = ();
my @local_pdb_list;
if ($attempt eq 1) {
# Bootstrap step 1: Get the database bootstrap state. We need to ensure
# that we get the state (build label and checkout headers) for each PDB
# as well as the root, even if we are not going to process the root.
# This way we can check to ensure that the root has been bootstrapped.
@local_pdb_list = @pdb_list;
if ($container_db && ($pdb_list[0] ne "CDB\$ROOT")) {
# Root is not in the list, add it
unshift(@local_pdb_list, "CDB\$ROOT");
}
}
else {
@local_pdb_list = @retry_pdb_list;
}
sqlpatch_log(LOG_DEBUG,
"bootstrap attempt $attempt, local_pdb_list: " .
Data::Dumper->Dumper(\@local_pdb_list));
sqlpatch_log(LOG_DEBUG,
"bootstrap attempt $attempt, retry_pdb_list: " .
Data::Dumper->Dumper(\@retry_pdb_list));
foreach my $pdb (@local_pdb_list) {
my $queue_entry;
# Get the database bootstrap state for this PDB
my $bootstrap_status_sql =
"BEGIN
:build_label := 'NONE';
:dbmssqlpatch_header := 'NONE';
:prvtsqlpatch_header := 'NONE';
:dbmsqopi_header := 'NONE';
:prvtqopi_header := 'NONE';
BEGIN
-- 24341018: database version in the version column, build label
-- in the comments column
SELECT comments
INTO :build_label
FROM sys.registry\$history
WHERE namespace = 'DATAPATCH'
AND action = 'BOOTSTRAP'
AND version = '$database_feature_version';
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END;
-- Each of the calls to build_header is dynamic because the
-- existing version of the package may not define it. Any errors
-- raised will be ignored, leaving us with the default value of
-- NONE for the header, which will result in loading the file.
BEGIN
EXECUTE IMMEDIATE
'BEGIN :h := sys.dbms_sqlpatch.build_header; END;'
USING IN OUT :dbmssqlpatch_header;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
BEGIN
EXECUTE IMMEDIATE
'BEGIN :h := sys.dbms_sqlpatch.body_build_header; END;'
USING IN OUT :prvtsqlpatch_header;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
BEGIN
EXECUTE IMMEDIATE
'BEGIN :h := sys.dbms_qopatch.build_header; END;'
USING IN OUT :dbmsqopi_header;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
BEGIN
EXECUTE IMMEDIATE
'BEGIN :h := sys.dbms_qopatch.body_build_header; END;'
USING IN OUT :prvtqopi_header;
EXCEPTION
WHEN OTHERS THEN NULL;
END;
END;";
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = $pdb");
}
my %bootstrap_info;
my $bootstrap_status_stmt = $dbh->prepare($bootstrap_status_sql);
$bootstrap_status_stmt->bind_param_inout(":build_label",
\$bootstrap_info{build_label}, 300);
$bootstrap_status_stmt->bind_param_inout(":dbmssqlpatch_header",
\$bootstrap_info{dbmssqlpatch_header}, 300);
$bootstrap_status_stmt->bind_param_inout(":prvtsqlpatch_header",
\$bootstrap_info{prvtsqlpatch_header}, 300);
$bootstrap_status_stmt->bind_param_inout(":dbmsqopi_header",
\$bootstrap_info{dbmsqopi_header}, 300);
$bootstrap_status_stmt->bind_param_inout(":prvtqopi_header",
\$bootstrap_info{prvtqopi_header}, 300);
$bootstrap_status_stmt->execute;
%{$pdb_info{$pdb}->{bootstrap_info}} = %bootstrap_info;
# Regardless of attempt#, add the new entry to the bootstrap queue.
my $queue_entry = {};
@{$queue_entry->{pdbs}} = ($pdb);
$queue_entry->{bootstrap_needed} = 0;
$queue_entry->{run_sqlpatch_bootstrap} = 0;
$queue_entry->{run_dbmssqlpatch} = 0;
$queue_entry->{run_prvtsqlpatch} = 0;
$queue_entry->{run_dbmsqopi} = 0;
$queue_entry->{run_prvtqopi} = 0;
push(@bootstrap_queue, $queue_entry);
}
if ($attempt eq 1) {
# Bootstrap step 2: Determine the bootstrap state of the file system.
# As the file system state will not change, we only need to do this on the
# first attempt.
# This will go into binary_bootstrap_info, which is a hash with the same
# fields as bootstrap_info in %pdb_info.
# We need to read each of the 4 bootstrap PL/SQL files and parse for the
# ADE checkout header.
foreach my $current_file ("dbmssqlpatch", "prvtsqlpatch",
"dbmsqopi", "prvtqopi") {
my $full_path =
File::Spec->catfile($admin_dir,
$current_file =~ /dbms.*/ ? $current_file . ".sql" :
$current_file . ".plb");
sqlpatch_log(LOG_DEBUG, "Checking $full_path for checkout header\n");
if (! -e $full_path) {
$ret = 1;
$global_prereq_failure =
"Datapatch infrastructure file $full_path does not exist";
goto bootstrap_complete;
}
open (BOOTSTRAP_FILE, $full_path);
while (my $line = <BOOTSTRAP_FILE>) {
chomp $line;
if ($line =~ /.*(\$Header.*\$).*/) {
sqlpatch_log(LOG_DEBUG, "Found header $1 for $full_path\n");
$binary_bootstrap_info{$current_file . "_header"} = $1;
last;
}
}
close (BOOTSTRAP_FILE);
}
$binary_bootstrap_info{build_label} = catconst::CATCONST_BUILD_LABEL;
sqlpatch_log(LOG_DEBUG,
"binary_bootstrap_info: " . Data::Dumper->Dumper(\%binary_bootstrap_info));
}
# Bootstrap step 3: Loop for the bootstrap queue (each entry is currently
# a single PDB) and determine which of the 5 files need to be run.
foreach my $entry (@bootstrap_queue) {
(my $pdb) = @{$entry->{"pdbs"}};
sqlpatch_log(LOG_DEBUG, "bootstrap step 3: checking pdb $pdb\n");
my %database_bootstrap_info = %{$pdb_info{$pdb}{bootstrap_info}};
sqlpatch_log(LOG_DEBUG, "database_bootstrap_info: " .
Data::Dumper->Dumper(\%database_bootstrap_info));
# We need to always run all 5 files in the following cases:
# * -bootstrap was specified on the command line
# * The build label in dba_registry_history is empty
my $run_all_files = 0;
if ($full_bootstrap ||
$database_bootstrap_info{build_label} eq 'NONE') {
$run_all_files = 1;
}
if ($run_all_files ||
($database_bootstrap_info{build_label} ne
$binary_bootstrap_info{build_label})) {
# Need to run sqlpatch_bootstrap.sql
$entry->{run_sqlpatch_bootstrap} = 1;
$entry->{bootstrap_needed} = 1;
}
if ($run_all_files ||
($database_bootstrap_info{dbmssqlpatch_header} ne
$binary_bootstrap_info{dbmssqlpatch_header})) {
# dbms_sqlpatch package header needs to be run. Therefore we also
# need to run the dbms_sqlpatch package body.
$entry->{run_dbmssqlpatch} = 1;
$entry->{run_prvtsqlpatch} = 1;
$entry->{bootstrap_needed} = 1;
}
if ($run_all_files ||
($database_bootstrap_info{prvtsqlpatch_header} ne
$binary_bootstrap_info{prvtsqlpatch_header})) {
# dbms_sqlpatch package body needs to be run. None of the other
# infrastructure packages need to be run.
$entry->{run_prvtsqlpatch} = 1;
$entry->{bootstrap_needed} = 1;
}
if ($run_all_files ||
($database_bootstrap_info{dbmsqopi_header} ne
$binary_bootstrap_info{dbmsqopi_header})) {
# dbms_qopatch package header needs to be run. Therefore we also
# need to run the dbms_qopatch package body and the dbms_sqlpatch
# package body.
$entry->{run_dbmsqopi} = 1;
$entry->{run_prvtqopi} = 1;
$entry->{run_prvtsqlpatch} = 1;
$entry->{bootstrap_needed} = 1;
}
if ($run_all_files ||
($database_bootstrap_info{prvtqopi_header} ne
$binary_bootstrap_info{prvtqopi_header})) {
# dbms_qopatch package body needs to be run. None of the other
# infrastructure packages need to be run.
$entry->{run_prvtqopi} = 1;
$entry->{bootstrap_needed} = 1;
}
if (($attempt > 1) && $entry->{bootstrap_needed}) {
# On the second attempt, if we need to run any of the files then
# we will run all of them. We can't just set $run_all_files on the
# second attempt because we need to test them individually.
$entry->{run_sqlpatch_bootstrap} =
$entry->{run_dbmssqlpatch} = $entry->{run_prvtsqlpatch} =
$entry->{run_dbmsqopi} = $entry->{run_prvtqopi} = 1;
}
# Now we can set the bootstrap_string. Just concatenate each of the
# 5 run_XXX variables.
$entry->{bootstrap_string} =
$entry->{run_sqlpatch_bootstrap} .
$entry->{run_dbmssqlpatch} . $entry->{run_prvtsqlpatch} .
$entry->{run_dbmsqopi} . $entry->{run_prvtqopi};
if ($app_mode && $entry->{bootstrap_needed}) {
$ret = 1;
$global_prereq_failure =
"bootstrap cannot be run in application mode";
goto bootstrap_complete;
}
# 27999597: Record $bootstrap_needed in %pdb_info
$pdb_info{$pdb}->{bootstrap_info}{bootstrap_needed} =
$entry->{bootstrap_needed};
}
sqlpatch_log(LOG_DEBUG, "Bootstrap queue after step 3: " .
Data::Dumper->Dumper(\@bootstrap_queue));
# Bootstrap step 4: Combine queue entries using a hash to check for
# uniqueness.
my %unique_bootstrap_strings = {};
for (my $queue_index = 0;
$queue_index <= $#bootstrap_queue;
$queue_index++) {
my $entry = $bootstrap_queue[$queue_index];
my $bootstrap_string = $entry->{bootstrap_string};
# See if it's unique by adding to the hash
if (exists($unique_bootstrap_strings{$bootstrap_string})) {
# Entry already in the hash
push(@{($bootstrap_queue[$unique_bootstrap_strings{$bootstrap_string}])->{"pdbs"}},
@{$entry->{"pdbs"}}[0]);
# Mark the current entry as combined so it won't be processed
$entry->{"combined"} = 1;
}
else {
$unique_bootstrap_strings{$bootstrap_string} = $queue_index;
}
}
sqlpatch_log(LOG_INVOCATION,
"\nFinal binary_bootstrap_info: " . Data::Dumper->Dumper(\%binary_bootstrap_info));
sqlpatch_log(LOG_INVOCATION, "Final bootstrap queue: " .
Data::Dumper->Dumper(\@bootstrap_queue));
# 22923409: Ensure that we are not connected to pdb$seed
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = cdb\$root");
}
# Verify that if we are not patching the root, then root does not need to
# be bootstrapped.
if ($container_db && ($pdb_list[0] ne "CDB\$ROOT") &&
$bootstrap_queue[0]->{bootstrap_needed}) {
$ret = 1;
$global_prereq_failure =
"CDB\$ROOT is not in the PDBs to be processed but needs to be bootstrapped";
goto bootstrap_complete;
}
# For each entry in the bootstrap queue
foreach my $queue_entry (@bootstrap_queue) {
next if $queue_entry->{combined} || !($queue_entry->{bootstrap_needed});
$bootstrap_iteration++;
# catcon parameters
my @catcon_scripts = (); # Array of scripts/statements to run
my $catcon_single_threaded = 1; # Run scripts in order?
my $catcon_root_only = 0; # Run only in the root?
my $catcon_run_per_proc = 0; # Run per process init statements?
my $con_names_incl; # PDBs to include
my $con_names_excl; # PDBs to exclude
my $custom_err_logging_ident; # Custom error logging identifier
my $custom_query; # Custom query to run in PDBs
# 27999597: Do not specify PDBs if we are connecting remotely
if ($container_db && !$connect_string) {
$con_names_incl = join(' ', @{$queue_entry->{pdbs}});
}
else {
$con_names_incl = "";
}
# Print the commands for this queue entry to a file which will then be
# run by catcon. This way we have a log of the commands, and our spooling
# will not include the internal catcon statements. The command SQL
# file will be in the invocation directory, with the name
# bootstrap<iteration>.sql. Including the PDB name(s) in the file name
# could generate an extremely long file name.
my $bootstrap_sql_file =
File::Spec->catfile($invocation_logdir,
"bootstrap" . $bootstrap_iteration . ".sql");
my $bootstrap_sql_handle;
unless (open($bootstrap_sql_handle, ">", $bootstrap_sql_file)) {
$ret = 1;
$global_prereq_failure =
"Could not create bootstrap SQL file $bootstrap_sql_file";
goto bootstrap_complete;
}
# Utility function to print message with a timestamp
sub bootstrap_message {
my ($handle, $msg) = @_;
sqlpatch_log(LOG_DEBUG, "bootstrap_message entry with $msg\n");
print $handle
"SET PAGESIZE 0
SELECT '$msg on ' || SYSTIMESTAMP FROM sys.dual;
SET PAGESIZE 10\n";
}
# First start spooling
print $bootstrap_sql_handle
"COLUMN bootstrap_logfile NEW_VALUE full_logfile\n";
if ($container_db) {
print $bootstrap_sql_handle
"SELECT '$invocation_logdir' ||
'/bootstrap" . $attempt . "_' || name || '_' ||
replace(sys_context('userenv', 'con_name'), '\$') || '.log'
AS bootstrap_logfile
FROM v\$database;\n";
}
else {
print $bootstrap_sql_handle
"SELECT '$invocation_logdir' ||
'/bootstrap" . $attempt . "_' || name || '.log'
AS bootstrap_logfile
FROM v\$database;\n";
}
print $bootstrap_sql_handle "SPOOL &full_logfile\n\n";
# Header info
print $bootstrap_sql_handle
"PROMPT $bootstrap_sql_file generated on " . (localtime) . "\n";
if ($container_db) {
print $bootstrap_sql_handle
"PROMPT PDBs to be bootstrapped by this file: ".
join(' ', @{$queue_entry->{pdbs}}) . "\n\n";
}
else {
print $bootstrap_sql_handle
"PROMPT Database to be bootstrapped by this file: $database_name\n\n";
}
bootstrap_message($bootstrap_sql_handle, "Starting bootstrap driver");
my $bootstrap_file;
if ($queue_entry->{run_sqlpatch_bootstrap}) {
$bootstrap_file =
File::Spec->catfile($sqlpatch_dir, "sqlpatch_bootstrap.sql");
bootstrap_message($bootstrap_sql_handle, "Calling sqlpatch_bootstrap.sql");
print $bootstrap_sql_handle "\@$bootstrap_file\n";
}
else {
bootstrap_message($bootstrap_sql_handle, "Not calling sqlpatch_bootstrap.sql");
}
if ($queue_entry->{run_dbmsqopi}) {
$bootstrap_file =
File::Spec->catfile($admin_dir, "dbmsqopi.sql");
bootstrap_message($bootstrap_sql_handle, "Calling dbmsqopi.sql");
print $bootstrap_sql_handle "\@$bootstrap_file\n";
}
else {
bootstrap_message($bootstrap_sql_handle, "Not calling dbmsqopi.sql");
}
if ($queue_entry->{run_prvtqopi}) {
$bootstrap_file =
File::Spec->catfile($admin_dir, "prvtqopi.plb");
bootstrap_message($bootstrap_sql_handle, "Calling prvtqopi.plb");
print $bootstrap_sql_handle "\@$bootstrap_file\n";
}
else {
bootstrap_message($bootstrap_sql_handle, "Not calling prvtqopi.plb");
}
if ($queue_entry->{run_dbmssqlpatch}) {
$bootstrap_file =
File::Spec->catfile($admin_dir, "dbmssqlpatch.sql");
bootstrap_message($bootstrap_sql_handle, "Calling dbmssqlpatch.sql");
print $bootstrap_sql_handle "\@$bootstrap_file\n";
}
else {
bootstrap_message($bootstrap_sql_handle, "Not calling dbmssqlpatch.sql");
}
if ($queue_entry->{run_prvtsqlpatch}) {
$bootstrap_file =
File::Spec->catfile($admin_dir, "prvtsqlpatch.plb");
bootstrap_message($bootstrap_sql_handle, "Calling prvtsqlpatch.plb");
print $bootstrap_sql_handle "\@$bootstrap_file\n";
}
else {
bootstrap_message($bootstrap_sql_handle, "Not calling prvtsqlpatch.plb");
}
bootstrap_message($bootstrap_sql_handle, "Finished bootstrap driver");
print $bootstrap_sql_handle "SPOOL off\n";
close($bootstrap_sql_handle);
push(@catcon_scripts, "\@$bootstrap_sql_file");
# Record bootstrap log
if ($container_db) {
foreach my $pdb (@{$queue_entry->{pdbs}}) {
my $local_name = $pdb;
$local_name =~ s/\$//;
$pdb_info{$pdb}->{bootstrap_info}{"bootstrap" . $attempt . "_log"} =
File::Spec->catfile($invocation_logdir,
"bootstrap" . $attempt . "_" .
$database_name . "_" .
$local_name . ".log");
}
}
else {
$pdb_info{$database_name}->{bootstrap_info}{"bootstrap" . $attempt . "_log"} =
File::Spec->catfile($invocation_logdir,
"bootstrap" . $attempt . "_"
. $database_name .".log");
}
sqlpatch_log(LOG_DEBUG,
"bootstrap pdb info: " . Data::Dumper->Dumper(\%pdb_info) . "\n");
my $catcon_ret;
my $timeout;
if ($container_db) {
$timeout = BOOTSTRAP_TIMEOUT * (scalar @local_pdb_list);
}
else {
$timeout = BOOTSTRAP_TIMEOUT;
}
eval {
# 22923409: Timeout after BOOTSTRAP_TIMEOUT (default 120) * <no of PDBs>
# seconds. This way in case there is a catcon or other problem we won't
# block forever.
my $handler = set_sig_handler('ALRM', sub {die "alarm";});
eval {
alarm($timeout);
$catcon_ret =
catcon::catconExec(@catcon_scripts, $catcon_single_threaded,
$catcon_root_only, $catcon_run_per_proc,
$con_names_incl, $con_names_excl,
$custom_err_logging_ident,
$custom_query);
alarm(0);
};
if ($@) {
die($@);
}
};
if ($@) {
if ($@ =~ /^alarm/) {
# catcon timed out
sqlpatch_log(LOG_ALWAYS,
"\nBootstrap timed out after $timeout seconds\n");
# 25161298: Retry once with the same set of PDBs
if ($attempt eq 1) {
$attempt++;
my $ret = catcon::catconBounceProcesses();
if ($ret) {
sqlpatch_log(LOG_ALWAYS, "catconBounceProcesses failed with $ret\n");
$global_prereq_failure = "catconBounceProcesses failed with $ret";
$catcon_ok = 0;
$ret = 1;
goto bootstrap_complete;
}
else {
@retry_pdb_list = @local_pdb_list;
goto attempt_bootstrap;
}
}
else {
$catcon_ok = 0;
$ret = 1;
$global_prereq_failure = "Bootstrap timed out after $timeout seconds";
# 25425451: Call hanganalyze before exiting
$dbh->do("ALTER SESSION SET tracefile_identifier = 'datapatch'");
$dbh->do("ALTER SESSION SET EVENTS 'immediate trace name hanganalyze level 3'");
goto bootstrap_complete;
}
}
else {
# catcon failed with other error
sqlpatch_log(LOG_ALWAYS,
"\nBootstrap failed with $@\n");
$global_prereq_failure = "Bootstrap failed with $@";
$catcon_ok = 0;
$ret = 1;
goto bootstrap_complete;
}
}
if ($catcon_ret) {
# 19501299: Fail immediately and return without installing further
# patches.
sqlpatch_log(LOG_ALWAYS, "\ncatconExec failed during bootstrap\n");
$global_prereq_failure = "catconExec failed during bootstrap";
$catcon_ok = 0;
$ret = 1;
goto bootstrap_complete;
}
}
sqlpatch_log(LOG_ALWAYS, "done\n");
# Reset package state to avoid ORA-4068 when we call dbms_sqlpatch again
# from the Perl session
$dbh->do("BEGIN dbms_session.reset_package; END;");
my $retry = 0;
foreach my $pdb (@local_pdb_list) {
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = $pdb");
}
my $o_result =
($attempt > 1) ? $o_bootstrap->{$pdb} : {};
if (!($pdb_info{$pdb}->{bootstrap_info}{bootstrap_needed})) {
# 2799597: Just add to the JSON summary, no need to actually validate
# since we did not actually bootstrap.
$o_result->{bootstrapNeeded} = 0;
}
else {
$o_result->{bootstrapNeeded} = 1;
my $result_rec = {};
my $validate_result =
validate_one_logfile($pdb, undef, "bootstrap", $attempt, $result_rec, 0);
# Record bootstrap result into $pdb_info
$pdb_info{$pdb}->{bootstrap_info}{"bootstrap" . $attempt . "_results"} =
$result_rec;
$o_result->{$attempt eq 1 ? "bootstrapLog" : "bootstrapRetryLog"} =
$result_rec->{logfile};
if ($validate_result) {
sqlpatch_log(LOG_ALWAYS,
" Error in bootstrap log " . $result_rec->{"logfile"} . ":\n");
for (my $i = 0; $i <= $#{$result_rec->{"errors"}}; $i++) {
sqlpatch_log(LOG_ALWAYS,
" Error at line " . $result_rec->{"error_lines"}[$i] .
": " . $result_rec->{"errors"}[$i] . "\n");
push(@{$o_result->{$attempt eq 1 ? "bootstrapErrors" :
"bootstrapRetryErrors"}},
{line => $result_rec->{error_lines}[$i],
error => $result_rec->{errors}[$i]});
}
if ($attempt eq 1) {
if ($result_rec->{retryable}) {
$retry = 1;
push(@retry_pdb_list, $pdb);
# 25425451: Clear the build label in registry$history. This will
# ensure we run all 5 files on a subsequent invocation.
$dbh->do("DELETE sys.registry\$history
WHERE namespace = 'DATAPATCH'
AND action = 'BOOTSTRAP'
AND version = '$database_feature_version'");
$dbh->do("COMMIT");
}
else {
$ret = 1;
$global_prereq_failure = "Failure during bootstrap";
}
}
else {
$ret = 1;
$global_prereq_failure = "Failure during bootstrap (after retry)";
}
}
else {
$o_result->{status} = "SUCCESS";
if (!$app_mode) {
# We have already done the check where we will error if bootstrap is
# actually needed in application mode. We can't update
# registry$history because we aren't connected as sys, and don't
# need to.
# 25425451: Update the build label in registry$history for success.
$dbh->do(
"BEGIN
UPDATE sys.registry\$history
SET comments = '" . catconst::CATCONST_BUILD_LABEL . "'
WHERE namespace = 'DATAPATCH'
AND action = 'BOOTSTRAP'
AND version = '$database_feature_version';
IF SQL%NOTFOUND THEN
INSERT INTO sys.registry\$history
(namespace, action, version, comments)
VALUES ('DATAPATCH', 'BOOTSTRAP',
'$database_feature_version',
'" . catconst::CATCONST_BUILD_LABEL . "');
END IF;
COMMIT;
END;");
}
}
}
if ($attempt eq 1) {
$o_bootstrap->{$pdb} = $o_result;
}
}
if ($retry) {
$attempt++;
goto attempt_bootstrap;
}
bootstrap_complete:
$orchestration_summary_hash->{initialize}->{bootstrap} = $o_bootstrap;
if ($ret) {
sqlpatch_log(LOG_ALWAYS, "\n");
}
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT");
}
return $ret;
}
# ------------------------------ applied_patches -----------------------------
# NAME
# applied_patches
#
# DESCRIPTION
# Returns the current set of applied patches in all PDBs as an array.
# Used for -rollback all
#
# ARGUMENTS
# None
#
# RETURNS
# Array consisting of patch id/patch UID strings
sub applied_patches {
my @ret;
# 22165897: Restrict to patches applied or unsuccessfully rolled back
# for interim patches
# 26281129: And RUs that are not rolled back to the feature release
my $applied_query =
"SELECT patch_id || '/' || patch_uid
FROM TABLE(dbms_sqlpatch.installed_patches)
WHERE (patch_type = 'INTERIM' AND
(action = 'APPLY' OR (action = 'ROLLBACK' AND status != 'SUCCESS')))
UNION SELECT patch_id || '/' || patch_uid
FROM TABLE(dbms_sqlpatch.last_successful_ru_version)
WHERE target_version != '$database_feature_version" . ".1.0.0.0'";
my $applied_stmt = $dbh->prepare($applied_query);
# For each PDB we need to alter session to that PDB, then get the list
# of applied patches.
foreach my $pdb (@pdb_list) {
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = $pdb");
}
push(@ret, @{$dbh->selectcol_arrayref($applied_stmt)});
}
sqlpatch_log(LOG_DEBUG, "applied patches returning @ret\n");
return @ret;
}
# ------------------------- load_patch_metadata -----------------------------
# NAME
# load_patch_metadata
#
# DESCRIPTION
# Loads the patch description with metadata from the XML descriptor.
# The descriptor must be in either the file system or the SQL registry.
#
# Notably, this routine does not set metadata related to the binary or
# SQL install state such as $missing_nodes or $pdb_info{sql_state}, that is
# the responsibility of the calling function (get_current_patches).
#
# 26281129: If the description already has existing metadata (for example
# loaded from queryable inventory, and now loading from the SQL registry),
# or we are querying the SQL registry in addition to the XML descriptor,
# then a prereq error is raised if the metadata does not match.
#
# ARGUMENTS
# $description: Reference to patch description to load. The only required
# fields are $patchid and $patchuid.
# $description{"patchdir"} must be set prior to the call.
# $use_binary: If true, metadata that can be gleaned from the queryable
# inventory XML (such as startup mode) is also compared.
# $sql_registry_rowid: If defined, rowid for this patch in the SQL registry
# from which we will read the metadata.
# $ru_info_rowid: If defined, rowid for this patch in the ru_info registry
# from which we will read the metadata.
# $current_pdb: PDB to which we are pointing
#
# RETURNS
# 0: successful load of metadata
# 1: An error occurred. In this case $prereq_ok and $prereq_failed_reason
# will be set in the descriptor.
sub load_patch_metadata($$$$) {
my ($description, $use_binary, $sql_registry_rowid, $ru_info_rowid,
$current_pdb) = @_;
my $sql_registry_ref;
my $ret = 0;
my $descriptor_source;
my $new_source;
sqlpatch_log(LOG_DEBUG,
"load_patch_metadata entry for ID " . $description->{"patchid"} . "/" .
$description->{"patchuid"} . "\n");
# Helper routine to fail if new metadata does not match existing
local *check_and_set = sub {
my $metadata = shift;
my $new_value = shift;
my $new_source = shift;
sqlpatch_log(LOG_DEBUG,
"check_and_set $metadata $new_value $new_source\n");
# If the patch is missing nodes, then the QI information may not be
# present, so we can return without error or setting the metadata.
if (defined($description->{missing_nodes}) &&
($new_source eq "queryable inventory")) {
sqlpatch_log(LOG_DEBUG, "check_and_set QI missing nodes\n");
return 0;
}
if (!defined($new_value)) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"Value of $metadata is undefined in $new_source";
$ret = 1;
return 1;
}
# 27283029: Accept both RUI and ID as patch_type
if ($metadata eq "patch_type" &&
$new_value eq "RUI") {
$new_value = "ID";
}
if ($description->{$metadata} &&
$description->{$metadata} ne $new_value) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"Existing value of $metadata (" . $description->{$metadata} .
") in descriptor found in $descriptor_source does not match" .
" new value of $metadata (" . $new_value . ") in $new_source";
$ret = 1;
return 1;
}
else {
sqlpatch_log(LOG_DEBUG,
"check_and_set setting $metadata to $new_value\n");
$description->{$metadata} = $new_value;
return 0;
}
};
if (defined($sql_registry_rowid)) {
# 25475853: Specify columns to avoid querying patch_directory
# 25539063: Query patch descriptor as CLOB not XMLType
my ($descriptor_len) = $dbh->selectrow_array(
"SELECT dbms_lob.getlength(XmlType.GetClobVal(patch_descriptor))
FROM dba_registry_sqlpatch
WHERE rowid = ?", undef, $sql_registry_rowid);
sqlpatch_log(LOG_DEBUG,
"descriptor_len: $descriptor_len\n");
$dbh->{LongReadLen} = $descriptor_len;
my $registry_stmt =
$dbh->prepare("SELECT patch_id, patch_uid, patch_type, flags, action,
status, description,
XMLType.GetClobVal(patch_descriptor) patch_descriptor
FROM dba_registry_sqlpatch
WHERE rowid = ?");
$registry_stmt->execute($sql_registry_rowid);
$sql_registry_ref = $registry_stmt->fetchrow_hashref;
sqlpatch_log(LOG_DEBUG, "load_patch_metadata sql row:\n");
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper($sql_registry_ref));
}
if (defined($ru_info_rowid)) {
my ($descriptor_len) = $dbh->selectrow_array(
"SELECT dbms_lob.getlength(XmlType.GetClobVal(patch_descriptor))
FROM dba_registry_sqlpatch_ru_info
WHERE rowid = ?", undef, $ru_info_rowid);
sqlpatch_log(LOG_DEBUG,
"ru_info descriptor_len: $descriptor_len\n");
$dbh->{LongReadLen} = $descriptor_len;
my $registry_stmt =
$dbh->prepare("SELECT patch_id, patch_uid,
ru_version, ru_build_description,
TO_CHAR(ru_build_timestamp, '$timestamp_format') ru_build_timestamp,
XMLType.GetClobVal(patch_descriptor) patch_descriptor
FROM dba_registry_sqlpatch_ru_info
WHERE rowid = ?");
$registry_stmt->execute($ru_info_rowid);
$sql_registry_ref = $registry_stmt->fetchrow_hashref;
sqlpatch_log(LOG_DEBUG, "load_patch_metadata sql row:\n");
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper($sql_registry_ref));
}
# Start with the known properties based solely on ID and UID.
# These properties are always the same, so we do not need to check if they
# are always defined first.
$description->{"patchdir"} =
File::Spec->catdir($sqlpatch_dir,
$description->{"patchid"},
$description->{"patchuid"});
$description->{"xml_descriptor"} =
File::Spec->catfile($description->{"patchdir"},
$description->{"patchid"} . ".xml");
$description->{"apply_script"} =
File::Spec->catfile($description->{"patchdir"},
$description->{"patchid"} . "_apply.sql");
$description->{"rollback_script"} =
File::Spec->catfile($description->{"patchdir"},
$description->{"patchid"} . "_rollback.sql");
# 17354355: Log directory should be under $ORACLE_BASE rather than
# $ORACLE_HOME if it is defined.
$description->{"logdir"} =
File::Spec->catdir($oracle_base, "cfgtoollogs", "sqlpatch",
$description->{"patchid"},
$description->{"patchuid"}) . "/";
$description->{patch_key} =
$description->{patchid} . "/" . $description->{patchuid};
my $xml_ref = undef;
# Read from XML descriptor in the file system if it exists
if (-e $description->{xml_descriptor}) {
$xml_ref = eval { XMLin($description->{xml_descriptor},
ForceArray => ['component', 'ru', 'file', 'rollbackVersion']) };
if ($@) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"Error reading descriptor " . $description->{"xml_descriptor"} .
": " . $@;
$ret = 1;
goto load_patch_metadata_complete;
}
sqlpatch_log(LOG_DEBUG, "XML descriptor hash from file system:\n");
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper($xml_ref));
$new_source = $description->{xml_descriptor};
if (!defined($description->{xml_descriptor_source})) {
$description->{xml_descriptor_source} = $new_source;
}
}
else {
# Read from the XML descriptor in the SQL registry if it exists
if ($sql_registry_ref->{"PATCH_DESCRIPTOR"}) {
$xml_ref = eval { XMLin($sql_registry_ref->{"PATCH_DESCRIPTOR"},
ForceArray => ['component', 'ru', 'file', 'rollbackVersion'])};
if ($@) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"Error reading descriptor from registry: " . $@;
$ret = 1;
goto load_patch_metadata_complete;
}
sqlpatch_log(LOG_DEBUG, "XML descriptor hash from registry:\n");
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper($xml_ref));
$new_source = "the patch_descriptor column in " .
($container_db ? "the SQL registry in PDB $current_pdb" : "the SQL registry");
if (!defined($description->{xml_descriptor_source})) {
$description->{xml_descriptor_source} = $new_source;
}
}
else {
# 26281129: Fail if we have no descriptor either in the file system
# or registry, unless the patch is missing nodes. In that case, the
# XML descriptor and/or QI information may not be present.
if (!defined($description->{missing_nodes})) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"XML descriptor does not exist in either the file system or SQL registry";
$ret = 1;
}
goto load_patch_metadata_complete;
}
}
# We have found an XML descriptor from either the file system or registry,
# use it for the metadata.
$descriptor_source = $description->{xml_descriptor_source};
sqlpatch_log(LOG_DEBUG, "set descriptor_source to $descriptor_source\n");
if (defined($xml_ref->{bundle})) {
# descriptor is pre 18.1 format
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"XML descriptor is in pre 18.1 format";
$ret = 1;
goto load_patch_metadata_complete;
}
# 24437510: Compare case insensitively
if (lc($xml_ref->{startupMode}) eq "normal") {
if (check_and_set("startup_mode", "normal", $new_source)) {
goto load_patch_metadata_complete;
}
}
else {
if (check_and_set("startup_mode", "upgrade", $new_source)) {
goto load_patch_metadata_complete;
}
}
if (check_and_set("description", $xml_ref->{"patchDescription"},
$new_source)) {
sqlpatch_log(LOG_DEBUG, "crap 1\n");
goto load_patch_metadata_complete;
}
if (check_and_set("jvm_patch",
($xml_ref->{"jvm"} eq "YES" ? 1 : 0), $new_source)) {
goto load_patch_metadata_complete;
}
# 22694961: Set application patch property
if (check_and_set("application_patch",
($xml_ref->{applicationPatch} eq "YES" ? 1 : 0),
$new_source)) {
goto load_patch_metadata_complete;
}
# 25507396: Ensure the patch is not missing nodes
# 26499153: Only if we are in app mode
if ($app_mode && $description->{application_patch} &&
$description->{missing_nodes}) {
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} =
"Application patches must be installed on all nodes in binary";
$ret = 1;
goto load_patch_metadata_complete;
}
# 25425451: Load @components
my @components;
foreach my $component_ref (@{$xml_ref->{components}->{component}}) {
push(@components, $component_ref->{content});
}
my $component_string =
join(",", sort(@components));
if (check_and_set("component_string", $component_string, $new_source)) {
goto load_patch_metadata_complete;
}
@{$description->{components}} = split(/,/, $description->{component_string});
# Load @rollback_versions
@{$description->{rollback_versions}} =
@{$xml_ref->{rollbackFilesData}->{rollbackVersion}};
# Set new metadata as of 18.1
if (check_and_set("patch_type", $xml_ref->{patchType}, $new_source) ||
check_and_set("build_timestamp", $xml_ref->{buildTimestamp},
$new_source) ||
check_and_set("build_description", $xml_ref->{buildDescription},
$new_source) ||
check_and_set("feature_version", $xml_ref->{featureVersion}, $new_source) ||
check_and_set("ru_version", $xml_ref->{ruVersion}, $new_source)) {
goto load_patch_metadata_complete;
}
if ($description->{patch_type} ne "INTERIM") {
my $ru_hash =
create_version_hash($description->{patch_type},
$description->{ru_version},
$description->{build_timestamp},
$description->{build_description},
$description->{patchid} . "/" . $description->{patchuid});
$description->{ru_version_description} = $ru_hash->{version_description};
}
# 26281129: Load file data
my $files_hash;
#print Data::Dumper->Dumper($xml_ref->{sqlFiles});
foreach my $xml_file_hash (@{$xml_ref->{sqlFiles}->{file}}) {
my $file_name = $xml_file_hash->{content};
$file_name =~ s/^\s+|\s+$//g; # Trim white space from start and end
sqlpatch_log(LOG_DEBUG, "loading info for $file_name\n");
my $file_hash = {mode => $xml_file_hash->{mode},
new => ($xml_file_hash->{new} eq "yes") ? 1 : 0,
sequence => $xml_file_hash->{sequence},
estimated_time => $xml_file_hash->{estimatedTime},
component => $xml_file_hash->{component},
highest_version_description => undef};
$files_hash->{$file_name} = $file_hash;
}
if ($description->{patch_type} ne "INTERIM") {
# Loop over all of the entries in the ruData element to find the
# highest version for each file
foreach my $xml_file_hash (@{$xml_ref->{ruData}->{ru}}) {
my $ru_version =
create_version_hash($xml_file_hash->{ruType},
$xml_file_hash->{version},
$xml_file_hash->{buildTimestamp},
$xml_file_hash->{buildDescription},
undef);
my $ru_version_description = $ru_version->{version_description};
# Loop over the files modified in this version, and compare the
# current version against the version for the file
foreach my $file (@{$xml_file_hash->{file}}) {
sqlpatch_log(LOG_DEBUG,
"File $file comparing RU $ru_version_description against high " .
$files_hash->{$file}->{highest_version_description} .
" low " . $files_hash->{$file}->{lowest_version_description} . "\n");
if (!defined($files_hash->{$file}->{highest_version_description}) ||
version_compare(
$ru_version_description,
$files_hash->{$file}->{highest_version_description}) > 0) {
sqlpatch_log(LOG_DEBUG,
"Setting high for $file to $ru_version_description\n");
$files_hash->{$file}->{highest_version_description} =
$ru_version_description;
}
# 27283029: Calculate lowest version description
if (!defined($files_hash->{$file}->{lowest_version_description}) ||
version_compare(
$ru_version_description,
$files_hash->{$file}->{lowest_version_description}) < 0) {
sqlpatch_log(LOG_DEBUG,
"Setting low for $file to $ru_version_description\n");
$files_hash->{$file}->{lowest_version_description} =
$ru_version_description;
}
}
}
}
$description->{files_hash} = $files_hash;
if ($use_binary) {
sqlpatch_log(LOG_DEBUG, "load_patch_metadata useqi\n");
# Read from queryable inventory
# Determine description and startup mode
my $description_mode_query =
"SELECT description, startup_mode
FROM XMLTable('/InventoryInstance/patches/patch[patchID=" .
$description->{"patchid"} . "]'
PASSING sys.dbms_sqlpatch.get_opatch_lsinventory
COLUMNS description VARCHAR2(100) PATH 'patchDescription',
startup_mode VARCHAR2(7) PATH 'sqlPatchDatabaseStartupMode')";
my ($qi_description, $qi_startup_mode) =
$dbh->selectrow_array($description_mode_query);
my @qi_debug = $dbh->func('dbms_output_get');
foreach my $line (@qi_debug) {
sqlpatch_log(LOG_DEBUG, "$line\n");
}
if (check_and_set("description", $qi_description, "queryable inventory")) {
goto load_patch_metadata_complete;
}
# 24437510: Compare case insensitively
if (lc($qi_startup_mode) eq "normal") {
if (check_and_set("startup_mode", "normal", "queryable inventory")) {
goto load_patch_metadata_complete;
}
}
else {
if (check_and_set("startup_mode", "upgrade", "queryable inventory")) {
goto load_patch_metadata_complete;
}
}
# Determine jvm_patch status
my $files_query =
"SELECT filename
FROM XMLTable('/InventoryInstance/patches/patch[patchID=" .
$description->{"patchid"} . "]//file'
PASSING sys.dbms_sqlpatch.get_opatch_lsinventory
COLUMNS filename VARCHAR2(50) PATH '.')";
my @patch_files = @{$dbh->selectcol_arrayref($files_query)};
my @qi_debug = $dbh->func('dbms_output_get');
foreach my $line (@qi_debug) {
sqlpatch_log(LOG_DEBUG, "$line\n");
}
my $jvm_patch = 0;
# Loop through the file list to see if we have jvmpsu.sql
for my $patch_file (@patch_files) {
sqlpatch_log(LOG_DEBUG,
"Checking patch file $patch_file for jvmpsu.sql\n");
if ($patch_file eq "jvmpsu.sql") {
# 19708632: This is a JVM patch, mark it as such
$jvm_patch = 1;
sqlpatch_log(LOG_DEBUG, "Set jvm_patch to true\n");
last;
}
}
if (check_and_set("jvm_patch", $jvm_patch, "queryable inventory")) {
goto load_patch_metadata_complete;
}
}
if (defined($sql_registry_rowid)) {
sqlpatch_log(LOG_DEBUG, "load_patch_metadata SQL\n");
my $n_s =
($container_db ? "the SQL registry in PDB $current_pdb" : "the SQL registry");
# Read properties from SQL registry and compare against existing
# descriptor.
if (check_and_set("startup_mode",
($sql_registry_ref->{FLAGS} =~ /U/ ? "upgrade" : "normal"), $n_s) ||
check_and_set("jvm_patch",
($sql_registry_ref->{FLAGS} =~ /J/ ? 1 : 0), $n_s) ||
check_and_set("application_patch",
($sql_registry_ref->{FLAGS} =~ /A/ ? 1 : 0), $n_s) ||
check_and_set("description", $sql_registry_ref->{DESCRIPTION}, $n_s) ||
check_and_set("patch_type", $sql_registry_ref->{PATCH_TYPE}, $n_s)) {
goto load_patch_metadata_complete;
}
}
if (defined($ru_info_rowid)) {
sqlpatch_log(LOG_DEBUG, "load_patch_metadata ru_info\n");
my $n_s =
($container_db ? "dba_registry_sqlpatch_ru_info in PDB $current_pdb" : "dba_registry_sqlpatch_ru_info");
# Read properties from the ru_info registry and compare against the existing
# descriptor
if (check_and_set("ru_version",
$sql_registry_ref->{RU_VERSION}, $n_s) ||
check_and_set("build_description",
$sql_registry_ref->{RU_BUILD_DESCRIPTION}, $n_s) ||
check_and_set("build_timestamp",
$sql_registry_ref->{RU_BUILD_TIMESTAMP}, $n_s)) {
goto load_patch_metadata_complete;
}
}
load_patch_metadata_complete:
if (!$ret) {
# Metadata checks were successful, set the flags
$description->{flags} =
($description->{startup_mode} eq "normal" ? "N" : "U") .
($description->{jvm_patch} ? "J" : "") .
($description->{application_patch} ? "A" : "");
if ($force) {
$description->{flags} .= "F";
}
# Set hash and xml_descriptor_source in the $description
$description->{xml_descriptor_hash} = $xml_ref;
$description->{xml_descriptor_source} = $new_source;
}
sqlpatch_log(LOG_DEBUG, "load_patch_metadata returning $ret\n");
return $ret;
}
# ------------------------ check_global_prereqs ------------------------------
# NAME
# check_global_prereqs
#
# DESCRIPTION
# Verifies that any global prereqs are satisfied:
# 19051526: Check that the queryable inventory package is working properly
# by calling dbms_sqlpatch.verify_queryable_inventory.
#
# ARGUMENTS
# None
#
# RETURNS
# 0 for success, 1 for prereq failure
sub check_global_prereqs {
my $ret = 0;
# 19189525: Call bootstrap
# 25425451: Even if $force is specified
if (bootstrap()) {
$ret = 1;
goto check_global_prereqs_complete;
}
if ($force) {
# -force specified, skip the rest of the checks
return 0;
}
# 23113885: Get the value of the FCP event after we call bootstrap
($event_23113885) = $dbh->selectrow_array(
"SELECT sys.dbms_sqlpatch.event_value(23113885) FROM sys.dual");
# 25512969: Enable QI debugging after bootstrap
$dbh->func(1000000, 'dbms_output_enable');
$dbh->do("BEGIN sys.dbms_qopatch.set_debug(TRUE); END;");
# 22359063: Call verify_queryable_inventory only if -noqi was not specified
# 22694961: And if $binary_config was not specified
if (!$noqi && !$binary_config && !$local_inventory) {
if ($container_db && $set_container) {
$dbh->do("ALTER SESSION SET CONTAINER = cdb\$root");
}
sqlpatch_log(LOG_DEBUG,
"verify_qi calling verify_queryable_inventory...\n");
my $verify_query =
"SELECT sys.dbms_sqlpatch.verify_queryable_inventory FROM sys.dual";
my ($qi_status) = $dbh->selectrow_array($verify_query);
sqlpatch_log(LOG_DEBUG, "qi status: $qi_status\n");
my @qi_debug = $dbh->func('dbms_output_get');
foreach my $line (@qi_debug) {
sqlpatch_log(LOG_DEBUG, "$line\n");
}
if ($qi_status eq "OK") {
return 0;
}
else {
sqlpatch_log(LOG_INVOCATION,
"verify_queryable_inventory returned $qi_status\n");
$global_prereq_failure =
"verify_queryable_inventory returned $qi_status";
$ret = 1;
goto check_global_prereqs_complete;
}
}
elsif ($binary_config) {
# Load contents of specified file
if (! -e $binary_config) {
$global_prereq_failure =
"Binary config file $binary_config does not exist";
$ret = 1;
goto check_global_prereqs_complete;
}
$binary_config_hash = eval { XMLin($binary_config) };
if ($@) {
$global_prereq_failure =
"Error reading binary config file $binary_config: $@";
$ret = 1;
goto check_global_prereqs_complete;
}
}
elsif ($local_inventory) {
# 25507396: Local opatch lsinventory output
my $inventory_file =
File::Spec->catfile($invocation_logdir, "opatch_inventory.xml");
my $opatch_command =
"$opatch_executable lsinventory -xml $inventory_file";
sqlpatch_log(LOG_DEBUG, "opatch command: $opatch_command\n");
my $opatch_output;
{
# Reset handler because otherwise $? is not dependable
local $SIG{CHLD} = 'DEFAULT';
$opatch_output = `$opatch_command`;
$ret = POSIX::WEXITSTATUS($?);
}
if ($ret) {
$global_prereq_failure = "$opatch_command failed:\n";
if ($opatch_output eq "") {
$global_prereq_failure .= $!;
}
else {
$global_prereq_failure .= $opatch_output;
}
goto check_global_prereqs_complete;
}
if (! -e $inventory_file) {
$global_prereq_failure =
"opatch inventory file $inventory_file does not exist";
$ret = 1;
goto check_global_prereqs_complete;
}
$opatch_inventory_hash = eval { XMLin($inventory_file,
ForceArray => ['patch']) };
if ($@) {
$global_prereq_failure =
"Error reading opatch inventory file $inventory_file: $@";
$ret = 1;
goto check_global_prereqs_complete;
}
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper(\$opatch_inventory_hash));
}
check_global_prereqs_complete:
sqlpatch_log(LOG_DEBUG, "check_global_prereqs returning $ret\n");
return $ret;
}
# ------------------------------- log_state --------------------------------
# NAME
# log_state
#
# DESCRIPTION
# Dumps the current data structures to the specified ouput. This includes
# %patch descriptions, %pdb_info, %all_series, @patch_queue, @patch_results
#
# ARGUMENTS
# $level: One of the LOG_ levels
# $message: String to print at beginning and end of dump
#
# RETURNS
# None
sub log_state($$) {
my ($level, $message) = @_;
sqlpatch_log($level, "*** START $message ***\n");
sqlpatch_log($level,
"patch_descriptions: " . Data::Dumper->Dumper(\%patch_descriptions));
sqlpatch_log($level,
"pdb_info: " . Data::Dumper->Dumper(\%pdb_info));
sqlpatch_log($level,
"binary_ru_version_description: $binary_ru_version_description\n");
sqlpatch_log($level,
"all_ru_versions: " . Data::Dumper->Dumper(\%all_ru_versions));
sqlpatch_log($level,
"patch_queue: " . Data::Dumper->Dumper(\@patch_queue));
sqlpatch_log($level,
"retry patch queue: " . Data::Dumper->Dumper(\@retry_patch_queue));
sqlpatch_log($level,
"patch_results: " . Data::Dumper->Dumper(\@patch_results));
sqlpatch_log($level,
"retry_patch_results: " . Data::Dumper->Dumper(\@retry_patch_results));
sqlpatch_log($level, "*** END $message ***\n");
}
# ----------------------------- write_progress ------------------------------
# NAME
# write_progress
#
# DESCRIPTION
# Outputs the current progress of sqlpatch to the JSON orchestration
# progress log.
#
# ARGUMENTS
# $phase: current phase of sqlpatch (initialize, prereq, validate, etc.)
# $percent_complete: new percentage complete
#
# RETURNS
# 0 for success, 1 for write failure. If there is an error and
# $global_prereq_failure is not defined it is updated with the failure
# message.
sub write_progress($$) {
my ($phase, $pc) = @_;
my %progress_hash;
my $ret = 0;
my $json_text;
# Return if -version was specified since we have no orchestration logs,
# or if there is already a global failure.
if ($version_only) {
return 0;
}
$percent_complete = $pc;
$progress_hash{"currentPhase"} = $phase;
$progress_hash{"percentComplete"} = $percent_complete;
$progress_hash{"estimatedTimeRemaining"} = $estimated_time_remaining;
$progress_hash{"elapsedTime"} = (time() - $start_time);
eval {
$json_text = to_json(\%progress_hash, {pretty => 1, canonical => 1});
};
if ($@) {
if (!defined($global_prereq_failure)) {
$global_prereq_failure = "Error creating JSON text from progress hash: $@";
}
$ret = 1;
goto progress_complete;
}
if (!open($orchestration_progress_handle, ">",
$orchestration_progress_json)) {
if (!defined($global_prereq_failure)) {
$global_prereq_failure =
"Could not open orchestration progress log $orchestration_progress_json";
}
$ret = 1;
goto progress_complete;
}
print $orchestration_progress_handle $json_text;
progress_complete:
close($orchestration_progress_handle);
return $ret;
}
# -------------------------- check_pdb_root_mismatch ------------------------
# NAME
# check_pdb_root_mismatch
#
# DESCRIPTION
# Verifies that the patch operation being performed on a PDB would not
# cause its patches to become inconsisent with the ROOT since that would
# prevent the PDB from opening. In short, if an interim patch is being applied
# on the PDB, that patch needs to be present in ROOT and if a interim patch is
# being rolled back from the PDB, then it must not be in ROOT.
# Furthermore, the target RU must also match the current RU successfully
# installed in ROOT.
#
# ARGUMENTS
# $$queue_entry - patch queue entry
# $pdb - pdb that this patch applies to
#
# RETURNS
# 0 for success, 1 for failure.
sub check_pdb_root_mismatch($$) {
my ($queue_entry, $pdb) = @_;
my $failure = 0;
sqlpatch_log(LOG_DEBUG, "check_pdb_root_mismatch entry for PDB $pdb\n");
sqlpatch_log(LOG_DEBUG, Data::Dumper->Dumper(\$queue_entry));
# Return success immediately if the user did not specify a list of PDBs
# or if the user specifically allowed mismatches the PDB and ROOT.
return 0 if ($force || $allow_pdb_mismatch || (!@user_pdbs));
# If CDB$ROOT is part of the set of PDBs for this operation, then we need
# not check anything more.
return 0 if (grep(/CDB\$ROOT/, @{$queue_entry->{pdbs}}));
foreach my $install_entry (@{$queue_entry->{interim_rollbacks}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
my $root_sql_state = $description->{pdb_info}{"CDB\$ROOT"}{sql_state};
sqlpatch_log(LOG_DEBUG,
"check_pdb_root_mismatch root state: $root_sql_state\n");
if ($root_sql_state eq "APPLY/SUCCESS") {
# The patch is successfully applied in the root, but we are about to
# rollback from the pdb. Raise an error since this operation will put
# the pdb into restricted mode due to patch mismatch.
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} = qq(
Is successfully applied in CDB\$ROOT, but is to be rolled back from $pdb.
This will cause a patch mismatch between $pdb and CDB\$ROOT and prevent
the PDB from opening unrestricted. Please use the allow_pdb_mismatch option
if this is really intended.
);
$failure = 1;
}
}
foreach my $install_entry (@{$queue_entry->{interim_applys}}) {
my $description = $patch_descriptions{$install_entry->{patch_key}};
my $root_sql_state = $description->{pdb_info}{"CDB\$ROOT"}{sql_state};
sqlpatch_log(LOG_DEBUG,
"check_pdb_root_mismatch root state: $root_sql_state\n");
if ($root_sql_state ne "APPLY/SUCCESS") {
# The patch is not successfully applied in the root, but we are about to
# apply it in the pdb. Raise an error since this operation will put
# the pdb into restricted mode due to patch mismatch.
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} = qq(
Is not successfully installed in CDB\$ROOT but is to be applied to $pdb.
This will cause a patch mismatch between $pdb and CDB\$ROOT and prevent
the PDB from opening unrestricted. Please use the allow_pdb_mismatch option
if this is really intended.
);
$failure = 1;
}
}
my @ru_installs = @{$queue_entry->{ru_installs}};
my $install_entry = $ru_installs[$#ru_installs];
my $description = $patch_descriptions{$install_entry->{patch_key}};
my $target_ru_description = $install_entry->{target_ru_description};
if (defined($target_ru_description) &&
$target_ru_description ne $pdb_info{"CDB\$ROOT"}->{ru_version_description}) {
# The eventual RU to be installed for this PDB does not match the the RU
# currently installed in the root. Raise an error since this operation
# will put the pdb into restricted mode due to patch mismatch.
$description->{prereq_ok} = 0;
$description->{prereq_failed_reason} = qq(
Release update $target_ru_description is being installed to $pdb but is not
installed in CDB\$ROOT. This will cause a patch mismatch between this PDB and
CDB\$ROOT and prevent the PDB from opening. Please use the allow_pdb_mismatch
option if this is really intended.
);
$failure = 1;
}
return $failure;
}
# ------------------------- version_compare ---------------------------------
# NAME
# version_compare
#
# DESCRIPTION
# Compares 2 RU version hashes, both of which need to already be in
# %all_ru_versions.
#
# ARGUMENTS
# $ver1: Description key for first version hash
# $ver2: Description key for second version hash
#
# RETURNS
# -1 if $ver1 < $ver2, +1 if $ver1 > $ver2, 0 if they are equal. The
# comparison takes into account the patch type, version string, build
# timestamp, and build description.
# NOTE: In the case of two different CU patches on top of the same RU or
# RUR, we cannot determine which is higher and undef will be returned.
sub version_compare($$) {
my ($ver1_description, $ver2_description) = @_;
my $ret;
sqlpatch_log(LOG_DEBUG, "version_compare($ver1_description, $ver2_description) entry\n");
# If either version is undef, the other is higher.
if (!defined($ver1_description) && defined($ver2_description)) {
$ret = -1;
goto version_compare_done;
}
elsif (defined($ver1_description) && !defined($ver2_description)) {
$ret = 1;
goto version_compare_done;
}
elsif (!defined($ver1_description) && !defined($ver2_description)) {
# Both undef
$ret = undef;
goto version_compare_done;
}
# If the descriptions are equal, the versions to which they point are
# equal.
if ($ver1_description eq $ver2_description) {
$ret = 0;
goto version_compare_done;
}
my $ver1 = $all_ru_versions{$ver1_description};
my $ver2 = $all_ru_versions{$ver2_description};
# Start with the version string. We can't simply compare it using string
# operators because that does a string compare and not a numeric compare,
# so we seperate out each digit into an array.
my @version_digits1 = split(/\./, $ver1->{version_string});
my @version_digits2 = split(/\./, $ver2->{version_string});
# First digit (feature release)
if ($version_digits1[0] < $version_digits2[0]) {
$ret = -1;
goto version_compare_done;
}
elsif ($version_digits1[0] > $version_digits2[0]) {
$ret = 1;
goto version_compare_done;
}
# Second digit (RU release)
if ($version_digits1[1] < $version_digits2[1]) {
$ret = -1;
goto version_compare_done;
}
elsif ($version_digits1[1] > $version_digits2[1]) {
$ret = 1;
goto version_compare_done;
}
# Third digit (RUR release)
if ($version_digits1[2] < $version_digits2[2]) {
$ret = -1;
goto version_compare_done;
}
elsif ($version_digits1[2] > $version_digits2[2]) {
$ret = 1;
goto version_compare_done;
}
# All three digits are the same.
if ($ver1->{patch_type} eq $ver2->{patch_type}) {
if ($ver1->{patch_type} ne "CU") {
# Same digits and patch type, but not CU. Compare build timestamp.
# I.e. 18.3.2.0.0 RUR ts1 vs. 18.3.2.0.0 RUR ts2
if ($ver1->{build_timestamp} < $ver2->{build_timestamp}) {
$ret = -1;
goto version_compare_done;
}
elsif ($ver1->{build_timestamp} > $ver2->{build_timestamp}) {
$ret = 1;
goto version_compare_done;
}
else {
$ret = 0;
goto version_compare_done;
}
}
else {
# Same digits and both CU patches. Compare build timestamp if the
# build description is the same
# I.e. 18.3.2.0.0 CU "Cloud_Emergency_Update" ts1 vs.
# 18.3.2.0.0 CU "Cloud_Emergency_Update" ts2
if ($ver1->{build_description} eq $ver2->{build_description}) {
if ($ver1->{build_timestamp} < $ver2->{build_timestamp}) {
$ret = -1;
goto version_compare_done;
}
elsif ($ver1->{build_timestamp} > $ver2->{build_timestamp}) {
$ret = 1;
}
else {
goto version_compare_done;
$ret = 0;
}
}
else {
# Build descriptions not the same, return undef
# I.e. 18.3.2.0.0 CU "Cloud_Emergency_Update" ts1 vs.
# 18.3.2.0.0 CU "ODA Patch" ts2
$ret = undef;
goto version_compare_done;
}
}
}
else {
# Same digits but different patch type.
# 27283029: We have the following possibilities:
# RU vs. CU: CU patch is higher
# RUR vs. CU: CU patch is higher
# RU vs. ID: RU is higher
if ($ver1->{patch_type} eq "CU") {
# CU patch is always higher
$ret = 1;
goto version_compare_done;
}
elsif ($ver2->{patch_type} eq "CU") {
$ret = -1;
goto version_compare_done;
}
else {
# The only remaining case is RU vs. ID. The RU is higher.
if ($ver1->{patch_type} eq "RU") {
$ret = 1;
goto version_compare_done;
}
elsif ($ver2->{patch_type} eq "RU") {
$ret = -1;
goto version_compare_done;
}
else {
# Should never happen
$ret = undef;
goto version_compare_done;
}
}
}
version_compare_done:
sqlpatch_log(LOG_DEBUG, "version_compare returning $ret\n");
return $ret;
}
# --------------------------- create_version_hash --------------------------
# NAME
# create_version_hash
#
# DESCRIPTION
# Creates a version hash based on the version metadata, and adds to
# %all_ru_versions if needed.
#
# ARGUMENTS
# $patch_type
# $version_string
# $build_timestamp
# $build_description
# $patch_key (may be undef)
#
# RETURNS
# Reference to the completed version hash
sub create_version_hash($$$$$) {
my ($patch_type, $version_string, $build_timestamp,
$build_description, $patch_key) = @_;
my $version_description;
my $version_hash;
sqlpatch_log(LOG_DEBUG,
"create_version_hash($patch_type, $version_string, $build_timestamp, $build_description, $patch_key) entry\n");
# Create version description first. The timestamp is not needed for
# the feature release
if ($version_string ne "NONE") {
$version_description =
$version_string . " " . $build_description .
(($build_description ne "Feature Release") ? " " . $build_timestamp : "");
}
else {
$version_description = undef;
}
if ($patch_type eq "INTERIM") {
# Create a new hash ref
$version_hash->{version_description} = $version_description;
$version_hash->{patch_type} = $patch_type;
$version_hash->{version_string} = $version_string;
$version_hash->{build_timestamp} = $build_timestamp;
$version_hash->{build_description} = $build_description;
$version_hash->{patch_key} = $patch_key;
}
else {
# Check %all_ru_versions
if (exists($all_ru_versions{$version_description})) {
# Description already exists in %all_ru_versions, update the patch key
# if needed
if ($patch_key &&
!defined($all_ru_versions{$version_description}->{patch_key})) {
$all_ru_versions{$version_description}->{patch_key} = $patch_key;
}
$version_hash = $all_ru_versions{$version_description};
}
else {
# Create a new hash ref and add to %all_ru_versions
$version_hash->{version_description} = $version_description;
$version_hash->{patch_type} = $patch_type;
$version_hash->{version_string} = $version_string;
$version_hash->{build_timestamp} = $build_timestamp;
$version_hash->{build_description} = $build_description;
$version_hash->{patch_key} = $patch_key;
$version_hash->{rollback_files_dir} =
($patch_type eq "FEATURE" ? $version_string :
$version_string . "-" . $patch_type . "-" . $build_description . "-" .
$build_timestamp);
$all_ru_versions{$version_description} = $version_hash;
}
}
return $version_hash;
}
# --------------------------------- directory_zip ---------------------------
# NAME
# directory_zip
#
# DESCRIPTION
# Zip or unzip the patch directory for the given patch key
#
# ARGUMENTS
# $patch_key: Patch to zip or unzip
# $zip: If true, patch directory will be zipped. If false, it will be
# unzipped from the SQL registry.
# RETURNS
# 1 if an error occured; the patch description will have prereq_ok and
# prereq_failed_reason set,
# 0 if success
sub directory_zip($$) {
my ($patch_key, $zip) = @_;
my $description = $patch_descriptions{$patch_key};
my $patch_zip =
File::Spec->catfile($description->{patchdir},
$description->{patchid} . ".zip");
sqlpatch_log(LOG_DEBUG, "patch_zip: $patch_zip\n");
if ($zip) {
unless (-e $patch_zip) {
# Patch directory zipfile does not exist, create it.
sqlpatch_log(LOG_DEBUG,
"Creating zip file $patch_zip\n");
# 25546608: Wrap calls to Archive in eval block to catch all errors.
my $zip;
eval {
$zip = Archive::Zip->new();
};
if ($@) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Error instantiating Archive::Zip object: $@";
return 1;
}
if (!defined($zip)) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Could not instantiate Archive::Zip object";
return 1;
}
my $zip_rc;
# 25546608: Wrap calls to Archive in eval block to catch all errors.
eval {
$zip_rc = $zip->addTree($description->{patchdir});
};
if ($@) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Error calling Archive::Zip->addTree: $@";
return 1;
}
if ($zip_rc != AZ_OK) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Archive::Zip->addTree returned $zip_rc";
return 1;
}
# 25546608: Wrap calls to Archive in eval block to catch all errors.
eval {
$zip_rc = $zip->writeToFileNamed($patch_zip);
};
if ($@) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Error calling Archive::Zip->writeToFileNamed: $@";
return 1;
}
if ($zip_rc != AZ_OK) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Archive::Zip->writeToFileNamed returned $zip_rc";
return 1;
}
}
# Zip operation successful
return 0;
}
else {
# Query the registry for the zip stored during a previous install.
# 27283029: Query dba_registry_sqlpatch for interim patches, and
# dba_registry_sqlpatch_ru_info for RU patches.
my $registry_table;
my $rowid;
if ($description->{patch_type} eq "INTERIM") {
$registry_table = "dba_registry_sqlpatch";
# Get rowid for the most recent entry (either apply or rollback)
# for this patch key.
($rowid) = $dbh->selectrow_array(
"SELECT rowid
FROM dba_registry_sqlpatch
WHERE patch_id = ?
AND patch_uid = ?
AND action_time =
(SELECT MAX(action_time)
FROM dba_registry_sqlpatch
WHERE patch_id = ?
AND patch_uid = ?)",
undef, $description->{patchid}, $description->{patchuid},
$description->{patchid}, $description->{patchuid});
sqlpatch_log(LOG_DEBUG, "rowid: $rowid rows: " . $DBI::rows . "\n");
}
else {
$registry_table = "dba_registry_sqlpatch_ru_info";
# Get rowid for the entry for this patch key.
($rowid) = $dbh->selectrow_array(
"SELECT rowid
FROM dba_registry_sqlpatch_ru_info
WHERE patch_id = ?
AND patch_uid = ?",
undef, $description->{patchid}, $description->{patchuid});
sqlpatch_log(LOG_DEBUG, "rowid: $rowid rows: " . $DBI::rows . "\n");
}
if (!defined($rowid)) {
# Patch directory does not exist and it cannot be found in the
# registry
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"patch directory " . $description->{patchdir} . " does not exist";
return 1;
}
# Determine the length of the blob
my ($zipfile_size) =
$dbh->selectrow_array("SELECT dbms_lob.getlength(patch_directory)
FROM $registry_table
WHERE rowid = ?",
undef, $rowid);
$dbh->{LongReadLen} = $zipfile_size;
sqlpatch_log(LOG_DEBUG, "Zipfile size: $zipfile_size\n");
# Ensure the zipfile is non zero
if (!$zipfile_size) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Archived patch directory is empty";
return 1;
}
# Now we can query the zipfile content
my ($zipfile_content) =
$dbh->selectrow_array("SELECT patch_directory
FROM $registry_table
WHERE rowid = ?",
undef, $rowid);
# 25546608: Wrap calls to Archive in eval block to catch all errors.
my $zip;
eval {
$zip = Archive::Zip->new();
};
if ($@) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Error instantiating Archive::Zip object: $@";
return 1;
}
if (!defined($zip)) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Could not instantiate Archive::Zip object";
return 1;
}
my $SH = IO::String->new(\$zipfile_content);
if (!defined($SH)) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Could not instantiate IO::String object";
return 1;
}
my $zip_rc;
# 25546608: Wrap calls to Archive in eval block to catch all errors.
eval {
$zip_rc = $zip->readFromFileHandle($SH);
};
if ($@) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Error calling Archive::Zip->readFromFileHandle: $@";
return 1;
}
if ($zip_rc != AZ_OK) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Archive::Zip->readFromFileHandle returned $zip_rc";
return 1;
}
eval {
make_path($description->{patchdir});
};
if ($@) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Could not create patch directory " .
$description->{patchdir} . ": $@";
return 1;
}
# 25546608: Wrap calls to Archive in eval block to catch all errors.
eval {
$zip_rc = $zip->writeToFileNamed($patch_zip);
};
if ($@) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Error calling Archive::Zip->writeToFileNamed: $@";
return 1;
}
if ($zip_rc != AZ_OK) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Archive::Zip->writeToFileNamed returned $zip_rc";
return 1;
}
# 25546608: Wrap calls to Archive in eval block to catch all errors.
eval {
$zip_rc = $zip->extractTree('', $description->{patchdir});
};
if ($zip_rc != AZ_OK) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Error calling Archive::Zip->extractTree: $@";
return 1;
}
if ($zip_rc != AZ_OK) {
$description->{"prereq_ok"} = 0;
$description->{"prereq_failed_reason"} =
"Archive::Zip->extractTree returned $zip_rc";
return 1;
}
}
# Zip operation successful
return 0;
}
# ----------------------------- usage ---------------------------------------
# NAME
# usage
#
# DESCRIPTION
# Prints the usage (via sqlpatch.pl) to the screen and invocation log
#
# ARGUMENTS
# None
#
# RETURNS
# None
sub usage() {
sqlpatch_log(LOG_ALWAYS,
"SQL Patching tool version $build_version on " . (localtime) . "\n");
sqlpatch_log(LOG_ALWAYS, "$copyright\n\n");
sqlpatch_log(LOG_ALWAYS, q{
datapatch usage:
All arguments are optional, if there are no arguments datapatch will
automatically determine which SQL scripts need to be run in order to complete
the installation of any interim or release update SQL patches.
Optional arguments:
-db <db name>
Use the specified database rather than \$ORACLE_SID
-apply <patch1,patch2,...,patchn>
Only consider the specified patch list for apply operations
-rollback <patch1,patch2,...,patchn>
Only consider the specified patch list for rollback operations
-force
Run the specified apply and/or rollback scripts regardless of the state of
the binary or SQL registry
-prereq
Run prerequisite checks only, do not actually install anything
-pdbs <pdb1,pdb2,...,pdbn>
Only consider the specified list of PDBs for patching. All other PDBs will
not be patched
-oh <directory>
Use the specified directory as the oracle_home to look for SQL scripts
-verbose
Output additional diagnostic information
-help
Output this message and exit
-debug
Output even more diagnostic information
-ignorable_errors=ORA-xxxxx,ORA-yyyyy
Ignore the specified errors in addition to the default list
-version
Output build information and exit
-upgrade_mode_only
Only consider patches that require upgrade mode
-bootstrap
Always run all bootstrap files
-allow_pdb_mismatch
Allow a patch to be applied in a PDB even if the patch does not exist in ROOT
-skip_bootstrap
Do not run any bootstrap files
-skip_upgrade_check
Do not check for upgrade mode
-userid=<user>
Connect as the specified user
-noqi
Do not use queryable inventory
-app
Enable application mode
-binary_config=<config file>
Path to binary config file to use instead of queryable inventory
-orchestration_summary=<filename>
Path to location of summary JSON log
-orchestration_progress=<filename>
Path to location of progress JSON log
-connect_string=<string>
Connect string for application mode
-local_inventory
Do not use queryable inventory, instead call opatch lsinventory directly
});
}
1;
OHA YOOOO