#!/usr/local/bin/perl # # $Header: rdbms/admin/sqlpatch/sqlpatch.pm /st_rdbms_12.2.0.1.0dbbp/2 2017/05/15 10:46:35 surman Exp $ # # sqlpatch.pm # # Copyright (c) 2012, 2017, 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) # surman 05/11/17 - Backport surman_bug-25546608 from main # surman 05/11/17 - Backport surman_bug-25539063_main from main # surman 02/27/17 - 25546608: Change to oracle_home at start # surman 11/23/16 - Backport surman_bug-25056052 from main # surman 11/17/16 - Backport surman_bug-24450424 from main # surman 03/01/17 - 25539063: Query patch descriptor as CLOB # surman 03/01/17 - 25539063: LongReadLen in database handle # surman 11/16/16 - 25056052: Errors in comments # surman 11/15/16 - 24450424: Process read only PDBs # surman 11/04/16 - Backport surman_bug-24693382 from main # surman 11/04/16 - Backport surman_bug-24437510 from main # surman 10/10/16 - 24693382: Many patches missing nodes # 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/18/16 - XbranchMerge surman_bug-24285405 from main # surman 07/12/16 - 24285405: Handle PROMPT in regular expression # 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; #################################################################### # 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 # $dbh: database handle, already connected as SYS # $params_ref: Reference to a hash of parameters. The valid parameters are: # apply: Comma separated list of either patch ids or patchid/uid pairs # which represents the list of patches to apply # rollback: Comma separated list of either patch ids or patchid/uid pairs # which represents the list of patches to rollback # force: If true the patches will be applied or rolled back regardless of # the current state # prereq_only: If true, run the prereq checks only and do not install # pdbs: Comma separated list of PDBs to consider # oh: User specified oracle_home to locate install scripts # verbose: If true, output additional information # debug: If true, output even more debugging information # bundle_series: Bundle series for patch (or NONE), used with -force # ignorable_errors: Comma separated list of ignorable errors # # RETURNS # 0 for success, 1 for prereq failure with the arguments 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 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 # # 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 $bundle_series; # 18092561: Bundle series for patch 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; # 17277459: Call bootstrap before patching my @pdb_list = (); # Array of PDBs to be processed 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 $app_connect; # 22694961: Application patch connection string my $binary_config; # 22694961: Binary configuration generated by opatchauto # 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 $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 # 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 # 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 # 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 = ""; # 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 ); # 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 # $description: Patch description # $patchdir: Full path to the directory containing the patch scripts # $xml_descriptor: Full path to XML patch descriptor (undef if there is # no file present) # $xml_descriptor_hash: Hash ref with contents of descriptor if present # $mode: "apply" or "rollback" # $startup_mode: "normal" or "upgrade" # $bundle_series: Series (i.e. PSU, WINBUNDLE) if this is a bundle patch, # undef if not a bundle patch # $bundle_id: ID for this bundle, if we can determine it # $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 # $prereq_patch: 1 if this is a prereq for another bundle patch # @fixed_bugs: Array of bugs fixed by this patch # @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_sql_state: Hash of the action and status for each pdb. The key is the # pdb name. If we are not in multitenant, the key will be 'undef'. # The value is "/", 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. If the patch is not present in # any SQL registries then the entire hash will be undef. # $existing_series: Value of ecpbBundleSeries from descriptor # $existing_bundle_id: Value of ecpbBundleID from descriptor # $superseded: True if this patch is part of a series that will be superseded # $superseding: True if this patch is part of a superseding bundle series 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: space separated list of PDBs. If the DB is not a container # database, $pdbs will be undefined # $num_pdbs: Number of pdbs to be patched # $patch_string: Encodes the apply and rollback list using the format # 'R: A: # with each list being comma separated # @applys: Array of patches to be applied # @rollbacks: Array of patches to be rolled back # $_candidate: Current bundle candidate patch # $_mode: Mode of the current candidate. One of "apply", # "superseding_apply", "rollback", "superseded_rollback" # $apply_jvm: JVM patch to be applied (if any) # $rollback_jvm: JVM patch to be rolled back (if any) my @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 undefined if the DB is # not a container database # $patch: 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. # $generate_logfile: Full path to the catbundle generate logfile, # undef if not a bundle patch # @generate_error_lines, @generate_errors: Same as error_lines and errors # for the generate logfile # $catbundle_logfile: Full patch to the catbundle apply or rollback logfile, # undef if not a bundle patch # @catbundle_error_lines, @catbundle_errors: Same as error_lines and errors # for the apply or rollback logfile # $status: "SUCCESS", "WITH ERRORS", "BEGIN" my @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 an undefined key. # $startup_mode: "NORMAL", "UPGRADE", "MIGRATE" # $bootstrap_log: Full path to log of the bootstrap operation # $bundle__id: ID installed for this series # $bundle__key: key for this ID 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) my %all_series; # -------------------------------- 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 # $dbh: database handle, already connected as SYS # $params_ref: Reference to a hash of parameters. The valid parameters are: # apply: Comma separated list of either patch ids or patchid/uid pairs # which represents the list of patches to apply # rollback: Comma separated list of either patch ids or patchid/uid pairs # which represents the list of patches to rollback # force: If true the patches will be applied or rolled back regardless of # the current state # prereq: If true, run the prereq checks only and do not install # pdbs: Comma separated list of PDBs to consider # oh: User specified oracle_home to locate install scripts # verbose: If true, output additional information # debug: If true, output even more debugging information # bundle_series: Bundle series for patch (or NONE), used with -force # ignorable_errors: Comma separated list of ignorable errors # # RETURNS # 0 for success, 1 for prereq failure with the arguments sub initialize($) { my $params_ref = @_[0]; my $ret = 0; eval { $Data::Dumper::Indent = 1; # Somewhat pretty printing # We need $debug and $verbose first $verbose = defined($params_ref->{"verbose"}) ? 1 : 0; $debug = $params_ref->{"debug"}; # If -debug is specified, turn on verbose too if ($debug) { $verbose = 1; } $version_only = defined($params_ref->{"version"}) ? 1 : 0; 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"); if (!make_path($invocation_logdir)) { # Could not create directory print "Could not create invocation log directory $invocation_logdir\n"; $ret = 1; goto initialize_complete; } } $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; open($history_handle, ">>", $history_file); print $history_handle "sqlpatch invocation with log directory $invocation_logdir\n\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; open($dummy_handle, ">", $invocation_log); 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"); # Copy parameters from hash to our storage and print to invocation log sqlpatch_log(LOG_INVOCATION, "SQL Patching arguments:\n"); sqlpatch_log(LOG_DEBUG, " 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"); } if (defined($params_ref->{"rollback"})) { @rollback_list = split(/,/, $params_ref->{"rollback"}); sqlpatch_log(LOG_INVOCATION, " rollback: " . $params_ref->{"rollback"} . "\n"); } $force = defined($params_ref->{"force"}) ? 1 : 0; sqlpatch_log(LOG_INVOCATION, " force: $force\n"); $prereq_only = defined($params_ref->{"prereq"}) ? 1 : 0; sqlpatch_log(LOG_INVOCATION, " prereq: $prereq_only\n"); $upgrade_mode_only = $params_ref->{"upgrade_mode_only"}; sqlpatch_log(LOG_INVOCATION, " upgrade_mode_only: $upgrade_mode_only\n"); $user_oh = $params_ref->{"oh"}; sqlpatch_log(LOG_INVOCATION, " oh: $user_oh\n"); $bundle_series = $params_ref->{"bundle_series"}; sqlpatch_log(LOG_INVOCATION, " bundle_series: $bundle_series\n"); @user_ignorable_errors = split(',', $params_ref->{"ignorable_errors"}); sqlpatch_log(LOG_INVOCATION, " ignorable_errors: " . $params_ref->{"ignorable_errors"} . "\n"); $full_bootstrap = $params_ref->{"bootstrap"}; sqlpatch_log(LOG_INVOCATION, " bootstrap: $full_bootstrap\n"); $skip_upgrade_check = $params_ref->{"skip_upgrade_check"}; sqlpatch_log(LOG_INVOCATION, " skip_upgrade_check: $skip_upgrade_check\n"); $userid = $params_ref->{"userid"}; sqlpatch_log(LOG_INVOCATION, " userid: $userid\n"); # 19189317: Strip single and double quotes from -pdbs my $stripped_pdbs = $params_ref->{"pdbs"}; sqlpatch_log(LOG_INVOCATION, " pdbs: $stripped_pdbs\n"); $stripped_pdbs =~ s/\'//g; $stripped_pdbs =~ s/\"//g; my @user_pdbs = split(',', $stripped_pdbs); $noqi = $params_ref->{"noqi"}; sqlpatch_log(LOG_INVOCATION, " noqi: $noqi\n"); $app_connect = $params_ref->{app}; sqlpatch_log(LOG_INVOCATION, " app: $app_connect\n"); $binary_config = $params_ref->{binary_config}; sqlpatch_log(LOG_INVOCATION, " binary_config: $binary_config\n"); sqlpatch_log(LOG_INVOCATION, "\n"); sqlpatch_log(LOG_DEBUG, "user_pdbs: @user_pdbs\n"); # 22694961: Raise error if both -app and -bootstrap are specified if (defined($app_connect) and $full_bootstrap) { sqlpatch_log(LOG_ALWAYS, "-bootstrap cannot be specified along with -app\n"); $ret = 1; goto initialize_complete; } # Likewise if -binary_config if specified without -app if (defined($binary_config) && !defined($app_connect)) { sqlpatch_log(LOG_ALWAYS, "-binary_config is supported only with -app\n"); $ret = 1; goto initialize_complete; } # 18092561: If -bundle_series is specified, then -force must also be # specified and we can be installing (applying or rolling back) exactly one # patch specified with -apply or -rollback. if (defined($bundle_series)) { if (!$force) { sqlpatch_log(LOG_ALWAYS, "-force must be specified when -bundle_series is specified\n"); $ret = 1; goto initialize_complete; } # $# is the index of the next element, so a value of -1 indicates # an empty array and a value of 0 indicates an array of 1 element. # Hence the total should be -1 to check that there is exactly 1 element # total in both. if (($#apply_list + $#rollback_list) != -1) { sqlpatch_log(LOG_ALWAYS, "Exactly one patch must be specified to be installed (via -apply\n"); sqlpatch_log(LOG_ALWAYS, "or -rollback) when -bundle_series is specified\n"); $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: If -noqi is specified then -apply and/or -rollback must also # be specified. if (defined($noqi)) { if ($#apply_list eq -1 && $#rollback_list eq -1) { sqlpatch_log(LOG_ALWAYS, "-apply and/or -rollback must be specified when -noqi is specified\n"); $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"); } else { $sqlpatch_dir = File::Spec->catdir($oracle_home, "sqlpatch"); $admin_dir = File::Spec->catdir($oracle_home, "rdbms", "admin"); } # 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; } # 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 = ; 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 $connect_string = "dbi:Oracle:"; my %connect_properties; $connect_properties{RaiseError} = 0; $connect_properties{PrintError} = 0; $connect_properties{AutoCommit} = 1; # 22694961: Support connection over network in application mode if ($app_connect) { $connect_string .= $app_connect; } else { $connect_properties{ora_session_mode} = DBD::Oracle::ORA_SYSDBA; } $dbh = DBI->connect($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 = ; chomp($db_user); sqlpatch_log(LOG_ALWAYS, "Enter password for $db_user: "); ReadMode('noecho'); $db_password = ; 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; sqlpatch_log(LOG_ALWAYS, "OK\n"); } }; if ($@) { die($@); } }; if ($@) { if ($@ =~ /^alarm/) { # Connect timed out sqlpatch_log(LOG_ALWAYS, "Database connect timed out after " . CONNECT_TIMEOUT . " seconds\n"); $connected_ok = 0; $ret = 1; goto initialize_complete; } else { # Connect failed with other error sqlpatch_log(LOG_ALWAYS, "Database connect failed with:\n"); sqlpatch_log(LOG_ALWAYS, "$@\n"); $connected_ok = 0; $ret = 1; goto initialize_complete; } } if (!$connected_ok) { # Connect failed with Oracle error sqlpatch_log(LOG_ALWAYS, "Database connect failed with:\n"); sqlpatch_log(LOG_ALWAYS, "$DBI::errstr\n"); $ret = 1; goto initialize_complete; } $dbh->{LongReadLen} = 1000000; # 22694961: If we are in application patching mode ensure that the user # has been granted the datapatch role if ($app_connect) { (my $cnt) = $dbh->selectrow_array("SELECT count(*) FROM user_role_privs WHERE granted_role = 'DATAPATCH_ROLE'"); if (!$cnt) { sqlpatch_log(LOG_ALWAYS, "Specified user has not been granted the datapatch role\n"); $ret = 1; goto initialize_complete; } } # 14563601: Determine database name for the log file. # 17777061: Also determine if we are in a container database. my $c_db; ($c_db, $database_name) = $dbh->selectrow_array("SELECT cdb, name FROM v\$database"); if ($c_db eq "NO") { $container_db = 0; } else { $container_db = 1; # 21059255: Switch to the root after connecting my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = CDB\$ROOT"); $alter_handle->execute(); } # 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") { sqlpatch_log(LOG_ALWAYS, "\nThe database or instance is in read only mode, exiting\n"); $ret = 1; goto initialize_complete; } # Set up for oracle scripts # 22694961: Only if not in application mode if (!$app_connect) { $dbh->do("ALTER SESSION SET \"_oracle_script\" = TRUE"); } # 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. my $lock_request_sql = "DECLARE lockhandle VARCHAR2(128); BEGIN dbms_lock.allocate_unique(\'sqlpatch_lock\', 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_inout(1, \$lockhandle, 128); $lock_request_stmt->bind_param_inout(2, \$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, "Another datapatch session is currently running.\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(1, $request_ret); $lock_request_stmt->bind_param(2, $lockhandle); $lock_request_stmt->execute; } if ((!$container_db ) && @user_pdbs) { sqlpatch_log(LOG_ALWAYS, "\nError: -pdbs specified but $database_name is not a container database\n"); $ret = 1; goto initialize_complete; } if ($container_db) { sqlpatch_log(LOG_DEBUG, "container database!\n"); sqlpatch_log(LOG_ALWAYS, "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)"); sqlpatch_log(LOG_ALWAYS, "\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 # 17277459: Switch to root first # 24450424: Process PDBs that are open read only as well $dbh->do("ALTER SESSION SET CONTAINER = CDB\$ROOT"); my $container_query; # We still explicitly include PDB$SEED here even though it will # match the READ ONLY query. if (@user_pdbs) { $container_query = "SELECT name,open_mode FROM v\$containers WHERE (name = 'PDB\$SEED' OR open_mode IN ('READ WRITE', 'READ ONLY', 'MIGRATE')) AND name IN ('" . join("','" , @user_pdbs) . "') ORDER BY con_id"; } else { $container_query = "SELECT name,open_mode FROM v\$containers WHERE (name = 'PDB\$SEED' OR open_mode IN ('READ WRITE', 'READ ONLY', 'MIGRATE')) 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; while (my ($name, $open_mode) = $container_stmt->fetchrow_array()) { $rows_found=1; sqlpatch_log(LOG_DEBUG, "sql row: $name $open_mode \n"); my $info = {}; $info->{"pdb_name"} = $name; $info->{"startup_mode"} = $open_mode; $info->{"bootstrap_log"} = undef; $pdb_info{$name} = $info; push(@pdb_list, $name); } sqlpatch_log(LOG_DEBUG, " container database ! \n"); if (!$rows_found) { sqlpatch_log(LOG_ALWAYS, "\nNo PDBs (subject to -pdbs parameter) are open, exiting\n"); $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"} = undef; $info->{"startup_mode"} = $open_mode; $info->{"bootstrap_log"} = undef; $pdb_info{undef} = $info; push(@pdb_list, undef); } 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)); # 19189525: Setup catcon once here # catcon init parameters my $catcon_user = 0; # connect string to run scripts (/ as sysdba) my $catcon_internal_user = 0; # 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; } if ($app_connect) { catcon::catconEZConnect($app_connect); } 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 = undef; # 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 = $debug; # catcon debugging my $catcon_gui = 0; # Called from cdb_sqlexec my $catcon_userscript = 0; # Running user supplied scripts? $catcon_pdbs_to_include = 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"}); # 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); if ($catcon_ret) { sqlpatch_log(LOG_ALWAYS, "\ncatconInit failed, exiting\n"); $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 sqlpatch_log(LOG_ALWAYS, "\n$@\n"); $ret = 1; } initialize_complete: 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, " bundle_series: $bundle_series\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 ($ret) { $global_failure = 1; } 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 $total_patches = 0; 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; } # Now determine the patch queue if ($prereq_failed = add_to_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! $total_patches = install_patches(); 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(); } }; 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; } patch_complete: if ($prereq_failed) { report_prereq_errors(); sqlpatch_log(LOG_ALWAYS, "Prereq check failed, exiting without installing any patches.\n"); $ret = 1; } if ($ret) { $global_failure = 1; } log_state(LOG_INVOCATION, "final state end of patching"); return $ret; } # ---------------------------- finalize -------------------------------------- # NAME # finalize # # DESCRIPTION # # Cleans up from patching, including closing the database connection. # # ARGUMENTS # None # # RETURNS # None sub finalize() { sqlpatch_log(LOG_DEBUG, "finalize entry\n"); # 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) { sqlpatch_log(LOG_DEBUG, "Switching to CDB\$ROOT before wrapup\n"); my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = CDB\$ROOT"); $alter_handle->execute; } if ($catcon_init && $catcon_ok) { catcon::catconWrapUp(); } if (defined($lockhandle)) { 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->bind_param(1, $lockhandle); $lock_release_stmt->execute; } $dbh->disconnect(); } if ($global_failure) { # 17974971: Print a message pointing the user to the support note explaing # what to do sqlpatch_log(LOG_ALWAYS, "\nPlease refer to MOS Note 1609718.1 and/or the invocation log\n"); sqlpatch_log(LOG_ALWAYS, $invocation_log); 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"); if (!$version_only && defined($invocation_handle)) { # 19547370: Close invocation log close($invocation_handle); # 19547370: Restore original stderr close(STDERR); open(STDERR, ">&SAVED_STDERR"); } } # --------------------------- 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); } } } # -------------------------- 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 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) { 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; } else { $description->{"patchuid"} = @subdirs[0]; } } else { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Patch directory $patchdir does not exist"; $ret = 1; } } $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 or # command line options. if (!$ret && load_patch_metadata($description, 1, 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. This is the same query that we use to get the list # of SQL patches except we know the patch id and UID. # In a multitenant environment, this query will be executed against # the root. my $patches_query = "SELECT * FROM (SELECT rowid, patch_id, RANK() OVER (PARTITION BY patch_id, patch_uid ORDER BY action_time DESC) r FROM dba_registry_sqlpatch WHERE version = (SELECT substr(version, 1, instr(version, '.', 1, 4) - 1) FROM v\$instance) AND patch_id = " . $description->{"patchid"} . " AND patch_uid = " . $description->{"patchuid"} . ") WHERE r = 1 ORDER BY patch_id"; ($patch_rowid) = $dbh->selectrow_array($patches_query); } else { $description->{"patchid"} = $rollback_patch; # Determine the patch UID by querying the SQL registry. # This is the same query that we use to get the list of SQL patches # except that just rank() over patch_id so that we will get the UID # for the most recent patch ID. # In a multitenant environment, this query will be executed against # the root. my $patches_uid_query = "SELECT * FROM (SELECT patch_uid, rowid, patch_id, action, status, RANK() OVER (PARTITION BY patch_id ORDER BY action_time DESC) r FROM dba_registry_sqlpatch WHERE version = (SELECT substr(version, 1, instr(version, '.', 1, 4) - 1) FROM v\$instance)) WHERE r = 1 AND ((action = 'APPLY' AND status = 'SUCCESS') OR (action = 'ROLLBACK' AND status != 'SUCCESS')) AND patch_id = $rollback_patch"; ($description->{"patchuid"}, $patch_rowid) = $dbh->selectrow_array($patches_uid_query); 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"; $ret = 1; } else { $description->{"patchuid"} = @subdirs[0]; } } else { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Patch directory $patchdir does not exist"; $ret = 1; } } } $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 (!$ret && load_patch_metadata($description, 0, $patch_rowid)) { $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"); } # 22694961: Get binary state from supplied XML file if needed and not force if (!$force && $binary_config) { 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; $description->{pdb_sql_state} = undef; 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); } } } # Load the rest of the properties from the XML descriptor or # binary config. if (load_patch_metadata($description, 1, undef)) { $ret = 1; } # Add the description to the hash $patch_descriptions{$patch_key} = $description; } } # 22359063: Get binary state if not -force and not -noqi elsif (!$force && !$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) { $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"); sqlpatch_log(LOG_DEBUG, "pending_activity_len: $pending_activity_len\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; # 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 =~ /^ *$/) { # Found new patch, reset UID and missing_nodes $patchid = $1; $patchuid = undef; $missing_nodes = undef; } elsif ($line =~ /^ *(\d+)<\/patchUId>$/) { $patchuid = $1; } elsif ($line =~ /^ *(.*)<\/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; $description->{"pdb_sql_state"} = undef; # Load the rest of the properties from the XML descriptor or # queryable inventory. if (load_patch_metadata($description, 1, 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"); } # 22359063: Get SQL state regardless of the settings of $force and $qi # 14623172: This nifty little query returns the most recent action and # status in the SQL registry for each patch. # 14563594: Restrict to current version # 17665117: Handle uid and get patch description. If the same patch ID is # present in the registry but with 2 different UIDs (both applied), we will # return both rows, even though this situation should not occur. # 17277459: Get bundle series as well as all statuses # 22359063: Just get patch_id, patch_uid, action, status, and rowid my $patches_query = "SELECT * FROM (SELECT patch_id, patch_uid, rowid, action, status, RANK() OVER (PARTITION BY patch_id, patch_uid ORDER BY action_time DESC) r FROM dba_registry_sqlpatch WHERE version = (SELECT substr(version, 1, instr(version, '.', 1, 4) - 1) FROM v\$instance)) WHERE r = 1 ORDER BY patch_id, patch_uid"; my $patches_stmt = $dbh->prepare($patches_query); # For each PDB we need to alter session to that PDB, then get the list # of patches foreach my $pdb (@pdb_list) { if ($container_db) { sqlpatch_log(LOG_DEBUG, "get SQL patches switching to pdb $pdb\n"); my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = " . $pdb); $alter_handle->execute; } $patches_stmt->execute(); while (my $patch_ref = $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; } else { $description = $patch_descriptions{$patchkey}; } $description->{"prereq_ok"} = 1; $description->{"pdb_sql_state"}{$pdb} = $patch_ref->{"ACTION"} . "/" . $patch_ref->{"STATUS"}; # Load the rest of the properties from the descriptor or SQL registry if (load_patch_metadata($description, 0, $patch_ref->{"ROWID"})) { $ret = 1; } } } if ($container_db) { # Set container back to root when we're done my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = CDB\$ROOT"); $alter_handle->execute; } log_state(LOG_DEBUG, "get_current_patches after getting SQL state"); # 22359063/23501901: If a bundle patch is missing from the current node # and there is no XML descriptor, we may not have determined that it was a # bundle patch until afte we query the SQL state. Thus we need to set the # missing_nodes attribute of all_series here. foreach my $key (sort keys %patch_descriptions) { my $description = $patch_descriptions{$key}; my $series = $description->{bundle_series}; if (defined($series) && $description->{missing_nodes}) { # 24693382: Append to the missing_nodes attribute of $all_series # (if it is not already present) rather than assigning. This way if a # later bundle patch is not missing nodes but an earlier one is, we # will still record properly. sqlpatch_log(LOG_DEBUG, "Found missing nodes " . join(' ', @{$description->{missing_nodes}}) . " for $series and $key\n"); foreach my $node (@{$description->{missing_nodes}}) { if (!grep(/$node/, @{$all_series{$series}->{missing_nodes}})) { sqlpatch_log(LOG_DEBUG, "Adding node $node\n"); push(@{$all_series{$series}->{missing_nodes}}, $node); } } } } log_state(LOG_DEBUG, "get_current_patches after setting missing_nodes in all_series"); # 20099675/22359063: Determine the binary and SQL id and key for all # bundle series. The binary id/key are stored in %all_series and the SQL # id/key are stored in %pdb_info. foreach my $series (sort keys %all_series) { sqlpatch_log(LOG_DEBUG, "Looking for IDs for series $series\n"); my $bundledata = File::Spec->catfile($admin_dir, "bundledata_" . $series . ".xml"); if (-e $bundledata) { sqlpatch_log(LOG_DEBUG, "$bundledata exists\n"); my $check_bundledata = 0; # 23025340: Ensure that there are patches for this bundle series # actually installed in binary. In the case where we are switching # trains, the existing bundledata.xml will still be present, along with # the superseding bundledata.xml. But there will be no patches for # the existing bundle series in the opatch registry. foreach my $key (sort keys %patch_descriptions) { my $description = $patch_descriptions{$key}; # The patch is installed if either $installed_binary is true, # or $mode is defined. The latter was set earlier if -force or -qi # was specified, in which case we will be processing this patch and # hence should check for bundledata.xml if (($description->{bundle_series} eq $series) && (($description->{installed_binary}) || (defined($description->{mode})))) { sqlpatch_log(LOG_DEBUG, "Found installed patch for this series\n"); $check_bundledata = 1; } } if ($check_bundledata) { # Default to zero in case bundledata exists, but is empty my $binary_id = 0; open (BUNDLEDATA, $bundledata); while (my $line = ) { if ($line =~ /bundle id=\"(\d*)\"/) { sqlpatch_log(LOG_DEBUG, "Setting (binary) bundle_id for $series to $1\n"); $binary_id = $1; } } close (BUNDLEDATA); $all_series{$series}->{"binary_id"} = $binary_id; $all_series{$series}->{"binary_key"} = undef; # Look for the corresponding key in the patch descriptions. The patch # key will be the highest one for this series that is installed in # binary. foreach my $key (reverse sort keys %patch_descriptions) { my $description = $patch_descriptions{$key}; if (($description->{"bundle_series"} eq $series) && ($description->{"installed_binary"})) { if (defined($description->{"bundle_id"}) && $description->{"bundle_id"} ne $binary_id && !$all_series{$series}->{"missing_nodes"}) { # 24308635: The series is not missing nodes, and # existing bundle ID does not match, mark as prereq failed $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Existing bundle ID " . $description->{"bundle_id"} . " in patch description does not match highest bundle id " . "$binary_id in bundledata.xml"; $ret = 1; } else { $description->{"bundle_id"} = $binary_id; $all_series{$series}->{"binary_key"} = $key; } last; } } } else { # bundledata.xml exists, but there are no installed patches. sqlpatch_log(LOG_DEBUG, "No binary patches for $series, setting id to zero\n"); $all_series{$series}->{"binary_id"} = 0; $all_series{$series}->{"binary_key"} = undef; } } else { sqlpatch_log(LOG_DEBUG, "bundledata not found for $series, setting id to zero\n"); $all_series{$series}->{"binary_id"} = 0; $all_series{$series}->{"binary_key"} = undef; } # We've determined the binary ID/key, now determine the SQL ID/key for # each PDB. This nifty little query returns the ID installed for the last # operation (successful or not). my $bundle_id_query = "SELECT * FROM (SELECT bundle_id, patch_id || '/' || patch_uid patch_key, action, status, RANK() OVER (PARTITION BY bundle_series ORDER BY action_time DESC) r FROM dba_registry_sqlpatch WHERE version = (SELECT substr(version, 1, instr(version, '.', 1, 4) - 1) FROM v\$instance) AND flags LIKE '%B%' AND bundle_series = ?) WHERE r = 1"; foreach my $pdb (@pdb_list) { sqlpatch_log(LOG_DEBUG, "Checking bundles installed in PDB $pdb\n"); my $entry; if ($container_db) { $entry = $pdb_info{$pdb}; sqlpatch_log(LOG_DEBUG, "get SQL patches switching to pdb $pdb\n"); my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = " . $pdb); $alter_handle->execute; } else { $entry = $pdb_info{undef}; } my $registry_stmt = $dbh->prepare($bundle_id_query); $registry_stmt->execute($series); my $registry_ref = $registry_stmt->fetchrow_hashref; # Default to 0 $entry->{"bundle_" . $series . "_id"} = 0; $entry->{"bundle_" . $series . "_key"} = undef; sqlpatch_log(LOG_DEBUG, "registry_ref: " . Data::Dumper->Dumper($registry_ref)); if (defined($registry_ref)) { ($entry->{"bundle_" . $series . "_id"}) = $registry_ref->{"BUNDLE_ID"}; if ($registry_ref->{"ACTION"} eq "APPLY") { # The key is the one which we just fetched. We can't necessarily # rely on just searching the patch descriptions because there could # be more than one entry with the same ID (with different UIDs). $entry->{"bundle_" . $series . "_key"} = $registry_ref->{"PATCH_KEY"}; } elsif (($registry_ref->{"BUNDLE_ID"} eq 0) && ($registry_ref->{"STATUS"} eq "WITH ERRORS")) { # Special case: The last entry in the SQL registry is a rollback to # 0, which failed. In this case, we need to perform the rollback # again. The ID should remain at zero, but the key needs to be # the key which we just fetched, not the key which corresponds # to the ID. $entry->{"bundle_" . $series . "_key"} = $registry_ref->{"PATCH_KEY"}; } else { # Look for the matching key in the patch descriptions. # load_patch_metadata can determine the bundle_id for entries stored # in the SQL registry even if there is no XML descriptor. foreach my $key (sort keys %patch_descriptions) { my $description = $patch_descriptions{$key}; if (($description->{"bundle_series"} eq $series) && ($description->{"bundle_id"} eq $registry_ref->{"BUNDLE_ID"})) { if (($registry_ref->{"ACTION"} eq "APPLY") && ($registry_ref->{"BUNDLE_ID"} ne $description->{"bundle_id"}) && (!$all_series{$series}->{missing_nodes})) { # 24308635: The series is not missing nodes, and # existing bundle ID does not match, mark as prereq failed $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Existing bundle ID " . $description->{"bundle_id"} . " in patch description does not match latest bundle id " . $registry_ref->{"BUNDLE_ID"} . " in SQL registry"; $ret = 1; } else { $entry->{"bundle_" . $series . "_key"} = $key; } last; } } } } } } if ($container_db) { # Set container back to root when we're done my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = CDB\$ROOT"); $alter_handle->execute; } log_state(LOG_DEBUG, "get_current_patches complete"); sqlpatch_log(LOG_ALWAYS, "done\n"); # Print out the current state if needed print_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 SQL patches:\n"); foreach my $key (sort keys %patch_descriptions) { my $description = $patch_descriptions{$key}; sqlpatch_log(LOG_DEBUG, "Checking description $key\n"); # 20099675: Use all_series and pdb_info to print the bundle state, so # we only need to print out information here for non bundle patches. # 22694961: Subject to app_connect if (!$description->{"bundle_series"} && (($app_connect && $description->{application_patch}) || (!$app_connect && !$description->{application_patch}))) { sqlpatch_log(LOG_VERBOSE, "Patch " . $description->{"patchid"} . " (" . $description->{"description"} . "):\n"); my $success_string = undef; my $error_string = undef; my $first_success = 1; my $first_error = 1; foreach my $pdb (sort keys %{$description->{"pdb_sql_state"}}) { if ($description->{"pdb_sql_state"}{$pdb} eq "APPLY/SUCCESS") { if ($pdb ne '') { if ($first_success) { $success_string .= "$pdb"; $first_success = 0; } else { $success_string .= " " . $pdb; } } else { $success_string .= "the SQL registry"; } } elsif ($description->{"pdb_sql_state"}{$pdb} =~ /(.*)\/WITH ERRORS/) { if ($pdb ne '') { if ($first_error) { $error_string .= "$pdb ($1 with errors)"; $first_error = 0; } else { $error_string .= " $pdb ($1 with errors)"; } } else { $error_string .= "$1 with errors in the SQL registry"; } } } # 20348653: Display information about missing nodes if ($description->{"missing_nodes"}) { sqlpatch_log(LOG_VERBOSE, " Not installed on nodes " . join(',', sort(@{$description->{"missing_nodes"}}))); if (defined($success_string) && defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " and $success_string and $error_string\n"); } elsif (!defined($success_string) && defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " and $error_string\n"); } elsif (defined($success_string) && !defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " and $success_string\n"); } else { sqlpatch_log(LOG_VERBOSE, "\n"); } } else { if ($description->{"installed_binary"}) { if (defined($success_string) && defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " Installed in the binary registry and $success_string and $error_string\n"); } elsif (!defined($success_string) && defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " Installed in the binary registry and $error_string\n"); } elsif (defined($success_string) && !defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " Installed in the binary registry and $success_string\n"); } else { sqlpatch_log(LOG_VERBOSE, " Installed in the binary registry only\n"); } } else { if (defined($success_string) && defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " Installed in $success_string and $error_string only\n"); } elsif (!defined($success_string) && defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " $error_string only\n"); } elsif (defined($success_string) && !defined($error_string)) { sqlpatch_log(LOG_VERBOSE, " Installed in $success_string only\n"); } else { sqlpatch_log(LOG_VERBOSE, " Not installed in the binary or the SQL registry\n"); } } } } } # Now we can loop through all_series and print the information there foreach my $series (sort keys %all_series) { sqlpatch_log(LOG_VERBOSE, "Bundle series $series:\n"); # 20348653: Display information about missing nodes if ($all_series{$series}->{"missing_nodes"}) { sqlpatch_log(LOG_VERBOSE, " Not installed on nodes " . join(',', sort(@{$all_series{$series}->{"missing_nodes"}})) . " and "); } elsif ($all_series{$series}->{"binary_id"} eq 0) { sqlpatch_log(LOG_VERBOSE, " Not installed in the binary registry and "); } else { sqlpatch_log(LOG_VERBOSE, " ID " . $all_series{$series}->{"binary_id"} . " in the binary registry and "); } if ($container_db) { my $first = 1; my $found = 0; my $database_string = ""; foreach my $pdb (@pdb_list) { my $sql_id = $pdb_info{$pdb}->{"bundle_" . $series . "_id"}; my $sql_key = $pdb_info{$pdb}->{"bundle_" . $series . "_key"}; if ($sql_id) { if ($first) { $first = 0; } else { $database_string .= ", "; } $found = 1; my $status = $patch_descriptions{$sql_key}->{"pdb_sql_state"}{$pdb}; if ($status =~ /.*WITH ERRORS/) { $database_string .= "ID $sql_id with errors in PDB $pdb"; } else { $database_string .= "ID $sql_id in PDB $pdb"; } } } if (!$found) { sqlpatch_log(LOG_VERBOSE, "not installed in any PDB\n"); } else { sqlpatch_log(LOG_VERBOSE, "$database_string\n"); } } else { my $sql_id = $pdb_info{undef}->{"bundle_" . $series . "_id"}; my $sql_key = $pdb_info{undef}->{"bundle_" . $series . "_key"}; my $status = $patch_descriptions{$sql_key}->{"pdb_sql_state"}{''}; if ($sql_id eq 0) { if ($status =~ /.*WITH ERRORS/) { sqlpatch_log(LOG_VERBOSE, "installed with errors in the SQL registry"); } else { sqlpatch_log(LOG_VERBOSE, "not installed in the SQL registry\n"); } } else { if ($status =~ /.*WITH ERRORS/) { sqlpatch_log(LOG_VERBOSE, "ID $sql_id with errors in the SQL registry\n"); } else { sqlpatch_log(LOG_VERBOSE, "ID $sql_id in the SQL registry\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..."); sqlpatch_log(LOG_VERBOSE, "\n"); # There are 3 steps involved in creating the queue: # 1) Create initial entries based on $installed_sql and $installed_binary # 2) Combine entries that have the same apply and rollback lists # 3) Check prereqs for final queue # Step One: Create initial queue entries if ($force) { # If -force is specified, then our job is simple. We just need to # loop over the patch descriptions and add them to the apply and rollback # set, for all PDBs. my $queue_rec = {}; if ($container_db) { $queue_rec->{"pdbs"} = join(' ', @pdb_list); } else { $queue_rec->{"pdbs"} = undef; } $queue_rec->{"applys"} = []; $queue_rec->{"rollbacks"} = []; $queue_rec->{"num_pdbs"} = 1; # 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) { if (defined($patch_descriptions{$key}->{"mode"})) { if ($patch_descriptions{$key}->{"mode"} eq "apply") { $work_to_do = 1; push(@{$queue_rec->{"applys"}}, $key); } else { $work_to_do = 1; push(@{$queue_rec->{"rollbacks"}}, $key); } } } push(@patch_queue, $queue_rec); log_state(LOG_DEBUG, "Patch queue after force add"); # We don't need to do step 2 skip straight to the prereq check goto queue_complete; } # -force not specified. Our job is not quite as simple. We need to # loop over the patch descriptions and check $installed_binary, # $installed_sql, and/or @installed_pdbs to determine if the patch should # be applied or rolled back. # First setup the patch queue with 1 entry per PDB. foreach my $pdb (@pdb_list) { my $queue_rec = {}; $queue_rec->{"pdbs"} = $pdb; $queue_rec->{"num_pdbs"} = 1; $queue_rec->{"applys"} = []; $queue_rec->{"rollbacks"} = []; push(@patch_queue, $queue_rec); } # Now loop over the patch descriptions and add if needed to the queue foreach my $key (sort keys %patch_descriptions) { sqlpatch_log(LOG_DEBUG, "add_to_queue checking key $key\n"); my $description = $patch_descriptions{$key}; # For each patch description, we need to determine if it should be added # to the apply or rollback list for each entry in the queue. # 22359063: If -noqi is specified then we need to check based on the # supplied -apply and -rollback lists and state of the SQL registry. if ($noqi) { # If the mode is not set then this patch did not appear in either # -apply or -rollback, hence we will not include it. if ($description->{"mode"} eq "apply") { # Patch is specified for apply. Loop over all the queue entries # (each of which is for a single PDB) and add to the apply list # if it is not currently successfully applied. foreach my $entry (@patch_queue) { my $pdb = $entry->{"pdbs"}; if (!defined($description->{"pdb_sql_state"}) || !defined($description->{"pdb_sql_state"}{$pdb}) || $description->{"pdb_sql_state"}{$pdb} ne "APPLY/SUCCESS") { # Patch is not installed successfully in this PDB, add to # the apply queue sqlpatch_log(LOG_DEBUG, "Adding $key to patch queue apply for PDB $pdb\n"); $work_to_do = 1; # 19708632: Check JVM status if ($description->{"jvm_patch"}) { $entry->{"apply_jvm"} = $key; } else { push(@{$entry->{"applys"}}, $key); } } else { sqlpatch_log(LOG_INVOCATION, "Not installing $key because it is already installed"); if (defined($pdb)) { sqlpatch_log(LOG_INVOCATION, " in PDB $pdb\n"); } else { sqlpatch_log(LOG_INVOCATION, "\n"); } } } } elsif ($description->{"mode"} eq "rollback") { # Patch is specified for rollback. Loop over all the queue entries # (each of which is for a single PDB) and add to the rollback list # if it is not currently successfully rolled back. foreach my $entry (@patch_queue) { my $pdb = $entry->{"pdbs"}; if (defined($description->{"pdb_sql_state"}) && defined($description->{"pdb_sql_state"}{$pdb}) && $description->{"pdb_sql_state"}{$pdb} ne "ROLLBACK/SUCCESS") { # Patch is not installed successfully in this PDB, add to # the rollback queue sqlpatch_log(LOG_DEBUG, "Adding $key to patch queue apply for PDB $pdb\n"); $work_to_do = 1; # 19708632: Check JVM status if ($description->{"jvm_patch"}) { $entry->{"rollback_jvm"} = $key; } else { push(@{$entry->{"rollbacks"}}, $key); } } else { sqlpatch_log(LOG_INVOCATION, "Not rolling back $key because it is not installed"); if (defined($pdb)) { sqlpatch_log(LOG_INVOCATION, " in PDB $pdb\n"); } else { sqlpatch_log(LOG_INVOCATION, "\n"); } } } } # Skip rest of the description processing (-noqi is not specified) next; } # 17277459 & 20099675: # We need separate handling for bundle patches vs. non-bundled patches. # So we only consider non bundle patches here. # 20348653: Skip any patches which are partially installed (missing nodes) if (!$description->{"bundle_series"} && !$description->{"missing_nodes"}) { # This is not a bundle patch. # If the following conditions are met then this patch may need to # be applied: # * The binary portion of the patch is installed # * 17665117: Subject to the passed in apply list if present # * 17665122: Subject to upgrade_mode_only # * 22165897: And $rollback_only # * 22694961: And $app_connect if ($description->{"installed_binary"} && (!@apply_list || grep(/$key/, @apply_list) || grep(/$description->{"patchid"}/, @apply_list)) && (!$upgrade_mode_only || ($description->{"startup_mode"} eq "upgrade")) && !$rollback_only && ((defined($app_connect) && $description->{application_patch}) || (!defined($app_connect) && !$description->{application_patch}))) { # This patch is an apply candidate. We now loop over all the queue # entries (each of which is for a single PDB), and check: # * If the patch is not installed in SQL at all, or # * The patch is not installed in the current queue entry # If either of these are true, then we add the patch to the apply # list. foreach my $entry (@patch_queue) { my $pdb = $entry->{"pdbs"}; if (!defined($description->{"pdb_sql_state"}) || !defined($description->{"pdb_sql_state"}{$pdb}) || $description->{"pdb_sql_state"}{$pdb} ne "APPLY/SUCCESS") { # Patch is installed in binary but not in this PDB, add to the # apply queue sqlpatch_log(LOG_DEBUG, "Adding $key to patch queue apply for PDB $pdb\n"); $description->{"mode"} = "apply"; $work_to_do = 1; # 19708632: Check JVM status if ($description->{"jvm_patch"}) { $entry->{"apply_jvm"} = $key; } else { push(@{$entry->{"applys"}}, $key); } } } } # Second check for potential rollback. # If the following conditions are met then this patch may need to be # rolled back: # * The binary portion of the patch is not installed # * The SQL portion is installed (in at least one PDB) # * 17665117: Subject to the passed in rollback list if present # * 17665122: Subject to upgrade_mode_only # * 22165897: And $apply_only # * 22694961: Add $app_connect if (!$description->{"installed_binary"} && defined($description->{"pdb_sql_state"}) && (!@rollback_list || grep(/$key/, @rollback_list) || grep(/$description->{"patchid"}/, @rollback_list)) && (!$upgrade_mode_only || ($description->{"startup_mode"} eq "upgrade")) && !$apply_only && ((defined($app_connect) && $description->{application_patch}) || (!defined($app_connect) && !$description->{application_patch}))) { # This patch is a rollback candidate. We now loop over all the queue # entries (each of which is a single PDB) and check if patch is # installed successfully for this PDB. foreach my $entry (@patch_queue) { my $pdb = $entry->{"pdbs"}; if (defined($description->{"pdb_sql_state"}{$pdb}) && $description->{"pdb_sql_state"}{$pdb} ne "ROLLBACK/SUCCESS") { # Patch is not installed in binary but is successfully applied # in this PDB. Add to the rollback queue. sqlpatch_log(LOG_DEBUG, "Adding $key to patch queue rollback for PDB $pdb\n"); $description->{"mode"} = "rollback"; $work_to_do = 1; # 19708632: Check JVM status if ($description->{"jvm_patch"}) { $entry->{"rollback_jvm"} = $key; } else { push(@{$entry->{"rollbacks"}}, $key); } } } } } } log_state(LOG_DEBUG, "after add_to_queue non bundles"); # 22359063: Now we need to determine the action for each bundle series. # We do this by comparing the bundle IDs installed in binary and in each PDB. # 22359063: Skip if -noqi is specified if (!$noqi) { foreach my $series (sort keys %all_series) { sqlpatch_log(LOG_DEBUG, "Checking candidates for series $series\n"); # 21620471: If the series is partially installed skip it if ($all_series{$series}->{"missing_nodes"}) { sqlpatch_log(LOG_INVOCATION, "Skipping partially installed series $series\n"); next; } # For this series, loop over the queue entries (each of which is for # a single PDB) foreach my $entry (@patch_queue) { my $pdb = $entry->{"pdbs"}; my $binary_id = $all_series{$series}->{"binary_id"}; my $binary_key = $all_series{$series}->{"binary_key"}; my $sql_id; my $sql_key; if ($container_db) { $sql_id = $pdb_info{$pdb}->{"bundle_" . $series . "_id"}; $sql_key = $pdb_info{$pdb}->{"bundle_" . $series . "_key"}; } else { $sql_id = $pdb_info{undef}->{"bundle_" . $series . "_id"}; $sql_key = $pdb_info{undef}->{"bundle_" . $series . "_key"}; } sqlpatch_log(LOG_DEBUG, "Testing series $series binary id/key $binary_id/$binary_key\n"); sqlpatch_log(LOG_DEBUG, "Testing series $series SQL id/key $sql_id/$sql_key\n"); if ($binary_id > $sql_id) { # Bundle ID is higher in binary then in SQL. We need to apply: # Subject to the passed in apply list if present # Subject to upgrade_mode_only # 22165897: And $rollback_only sqlpatch_log(LOG_DEBUG, "Apply candidate is $binary_key\n"); if ((!@apply_list || grep(/$binary_key/, @apply_list) || grep(/$patch_descriptions{$binary_key}->{"patchid"}/, @apply_list)) && (!$upgrade_mode_only || ($patch_descriptions{$binary_key}->{"startup_mode"} eq "upgrade")) && !$rollback_only) { sqlpatch_log(LOG_DEBUG, "Setting apply candidate to $binary_key\n"); $entry->{$series . "_candidate"} = $binary_key; $entry->{$series . "_mode"} = "apply"; } } elsif ($binary_id < $sql_id) { # Bundle ID is higher in SQL then in binary. We need to rollback: # Subject to the passed in rollback list if present # 20348653: And the patch is not partially installed # 22165897: And $apply_only if ((!@rollback_list || grep(/$sql_key/, @rollback_list) || grep(/$patch_descriptions{$sql_key}->{"patch_id"}/, @rollback_list)) && (!$upgrade_mode_only || ($patch_descriptions{$sql_key}->{"startup_mode"} eq "upgrade")) && !$apply_only) { sqlpatch_log(LOG_DEBUG, "Setting rollback candidate to $sql_key\n"); $entry->{$series . "_candidate"} = $sql_key; $entry->{$series . "_mode"} = "rollback"; } } else { # 19521006: Bundle IDs are equal in SQL and binary. Check for # different patch keys, which would indicate a re-issued patch. # In that case we need to force rollback the SQL patch and force apply # the binary patch. if ($sql_key ne $binary_key) { sqlpatch_log(LOG_DEBUG, "Force rollback $sql_key and force apply $binary_key\n"); $patch_descriptions{$sql_key}->{"flags"} .= "F"; push(@{$entry->{"rollbacks"}}, $sql_key); $patch_descriptions{$sql_key}->{"mode"} = "rollback"; $patch_descriptions{$binary_key}->{"flags"} .= "F"; $entry->{$series . "_candidate"} = $binary_key; $entry->{$series . "_mode"} = "apply"; $patch_descriptions{$binary_key}->{"mode"} = "apply"; } else { # 22359063: Check action and status $patch_descriptions{$sql_key}->{"pdb_sql_state"}{$pdb} =~ /(.*)\/(.*)/; my $sql_action = $1; my $sql_status = $2; if ($sql_status eq 'WITH ERRORS') { # We need to redo the last action $entry->{$series . "_candidate"} = $sql_key; $entry->{$series . "_mode"} = lc($sql_action); } } } } } } log_state(LOG_DEBUG, "after add_to_queue bundles"); # 23025340: Loop through the series candidates for each queue entry to see # if any are superseding or superseded. We also set the mode in patch # descriptions. foreach my $entry (@patch_queue) { foreach my $series (sort keys %all_series) { my $candidate = $entry->{$series . "_candidate"}; sqlpatch_log(LOG_DEBUG, "add_to_queue superseding pass checking candidate $candidate\n"); if (defined($candidate)) { my $mode = $entry->{$series . "_mode"}; if (!defined($patch_descriptions{$candidate}->{mode})) { $mode =~ /(superseded_)?(.*)/; $patch_descriptions{$candidate}->{mode} = $2; sqlpatch_log(LOG_DEBUG, "set mode to $2 for candidate $candidate\n"); } if ($mode eq "apply") { my $superseded_series = $patch_descriptions{$candidate}->{existing_series}; sqlpatch_log(LOG_DEBUG, "add_to_queue superseding pass superseded_series = $superseded_series\n"); if ($superseded_series) { # This candidate is due to be applied, and the metadata for it # indicates that it supsersedes an existing bundle series. Check # if that series is due to be rolled back, if (defined($superseded_series . "_candidate") && ($entry->{$superseded_series . "_mode"} eq "rollback")) { # The series is due to be rolled back. Set the mode for both # candidates to superseding/superseded. sqlpatch_log(LOG_DEBUG, "add_to queue setting superseding/superseded\n"); $entry->{$series . "_mode"} = "superseding_apply"; $entry->{$superseded_series . "_mode"} = "superseded_rollback"; } } } } } } log_state(LOG_DEBUG, "after add_to_queue superseding/superseded"); # 19708632: Put any JVM fixes first in the apply or rollback list # 17277459: Add bundle candidates to apply or rollback list foreach my $entry (@patch_queue) { # First put bundle candidates at the front of the list foreach my $series (sort keys %all_series) { my $candidate = $entry->{$series . "_candidate"}; sqlpatch_log(LOG_DEBUG, "add_to_queue bundle pass checking candidate $candidate\n"); if (defined($candidate)) { my $mode = $entry->{$series . "_mode"}; if (($mode eq "apply") || ($mode eq "superseding_apply")) { unshift(@{$entry->{"applys"}}, $candidate); $work_to_do = 1; } else { unshift(@{$entry->{"rollbacks"}}, $candidate); $work_to_do = 1; } } } # Now put JVM fixes at the front of the list if ($entry->{"apply_jvm"}) { unshift(@{$entry->{"applys"}}, $entry->{"apply_jvm"}); } if ($entry->{"rollback_jvm"}) { unshift(@{$entry->{"rollbacks"}}, $entry->{"rollback_jvm"}); } } log_state(LOG_DEBUG, "after add_to_queue JVM and bundle pass"); # Step Two: Combine queue entries # Initial applys and rollbacks are added to the queue. Now we need to # combine queue entries that have the same apply and rollback lists. # We also generate the patch string for each entry. # We do this using a hash that tests for uniqueness. my %unique_patch_strings; for (my $queue_index = 0; $queue_index <= $#patch_queue; $queue_index++) { my $entry = $patch_queue[$queue_index]; my $patch_string = "R:" . join(',', @{$entry->{"rollbacks"}}) . " A:" . join(',', @{$entry->{"applys"}}); $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 (@patch_queue[$unique_patch_strings{$patch_string}])->{"pdbs"} .= (" " . $entry->{"pdbs"}); (@patch_queue[$unique_patch_strings{$patch_string}])->{"escaped_pdbs"} .= (" " . $entry->{"escaped_pdbs"}); # 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; } } queue_complete: log_state(LOG_DEBUG, "end of add_to_queue"); if (!$debug && !$verbose) { sqlpatch_log(LOG_ALWAYS, "done\n"); } # Step Three: The queue is complete. Print it out and check for prereqs. print_queue(); $prereq_failed = check_queue_prereqs(); sqlpatch_log(LOG_DEBUG, "add_to_queue returning $prereq_failed\n"); return $prereq_failed; } # ------------------------- 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; # 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 sub print_queue { sqlpatch_log(LOG_ALWAYS, "Installation queue:\n"); foreach my $queue_entry (@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: " . $queue_entry->{"pdbs"} . "\n"); $indent = " "; } else { $indent = " "; } if (scalar @{$queue_entry->{"rollbacks"}} == 0) { sqlpatch_log(LOG_ALWAYS, $indent . "Nothing to roll back\n"); } else { sqlpatch_log(LOG_ALWAYS, $indent . "The following patches will be rolled back:\n"); foreach my $patch (@{$queue_entry->{"rollbacks"}}) { sqlpatch_log(LOG_ALWAYS, $indent . " " . $patch_descriptions{$patch}->{"patchid"} . " (" . $patch_descriptions{$patch}->{"description"} . ")\n"); } } if (scalar @{$queue_entry->{"applys"}} == 0) { sqlpatch_log(LOG_ALWAYS, $indent . "Nothing to apply\n"); } else { sqlpatch_log(LOG_ALWAYS, $indent . "The following patches will be applied:\n"); foreach my $patch (@{$queue_entry->{"applys"}}) { sqlpatch_log(LOG_ALWAYS, $indent . " " . $patch_descriptions{$patch}->{"patchid"} . " (" . $patch_descriptions{$patch}->{"description"} . ")\n"); } } } sqlpatch_log(LOG_ALWAYS, "\n"); } # ----------------------------- check_queue_prereqs -------------------------- # NAME # check_queue_prereqs # # DESCRIPTION # Verify final prereqs based on the patch queue, namely that the install # script exists and that 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. # Also 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; foreach my $entry (@patch_queue) { foreach my $pdb (defined($entry->{pdbs}) ? split(/ /, $entry->{pdbs}) : (undef)) { # Switch to the container for this queue entry if needed. if ($container_db) { 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 dbms_sqlpatch.clear_state; END;"); foreach my $patch (@{$entry->{"applys"}}, @{$entry->{"rollbacks"}}) { my $description = $patch_descriptions{$patch}; sqlpatch_log(LOG_DEBUG, "check_queue_prereqs checking patch $patch 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"); if (!make_path($description->{"logdir"})) { # 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; } } my $patch_zip = File::Spec->catfile($description->{patchdir}, $description->{patchid} . ".zip"); sqlpatch_log(LOG_DEBUG, "patch_zip: $patch_zip\n"); # 23170620: Check for the existence of the patch directory and/or # install script, and zip/unzip the patch directory if needed. if ($description->{mode} eq "apply") { # Ensure the patch directory exists unless (-e $description->{patchdir}) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "patch directory " . $description->{patchdir} . " does not exist"; $prereq_failed = 1; next; } # Ensure the install script exists 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; } unless (-e $patch_zip) { # Patch directory zipfile does not exist, create it. sqlpatch_log(LOG_DEBUG, "Creating zip file $patch_zip for apply\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: $@"; $prereq_failed = 1; next; } if (!defined($zip)) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Could not instantiate Archive::Zip object"; $prereq_failed = 1; next; } 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: $@"; $prereq_failed = 1; next; } if ($zip_rc != AZ_OK) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Archive::Zip->addTree returned $zip_rc"; $prereq_failed = 1; next; } # 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: $@"; $prereq_failed = 1; next; } if ($zip_rc != AZ_OK) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Archive::Zip->writeToFileNamed returned $zip_rc"; $prereq_failed = 1; next; } } } else { # Patch is being rolled back unless (-e $description->{patchdir}) { # Patch directory does not exist. Query the registry for the # zip stored during a previous apply. # Get rowid for the most recent apply entry for this patch key. my ($rowid) = $dbh->selectrow_array( "SELECT rowid FROM dba_registry_sqlpatch WHERE patch_id = " . $description->{patchid} . " AND patch_uid = " . $description->{patchuid} . " AND action_time = (SELECT MAX(action_time) FROM dba_registry_sqlpatch WHERE patch_id = " . $description->{patchid} . " AND patch_uid = " . $description->{patchuid} . " AND action = 'APPLY')"); sqlpatch_log(LOG_DEBUG, "rowid: $rowid\n"); # Determine the length of the blob my ($zipfile_size) = $dbh->selectrow_array( "SELECT dbms_lob.getlength(patch_directory) FROM dba_registry_sqlpatch WHERE rowid = '$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"; $prereq_failed = 1; next; } # Now we can query the zipfile content my ($zipfile_content) = $dbh->selectrow_array( "SELECT patch_directory FROM dba_registry_sqlpatch WHERE rowid = '$rowid'"); my $zip = Archive::Zip->new(); if (!defined($zip)) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Could not instantiate Archive::Zip object"; $prereq_failed = 1; next; } my $SH = IO::String->new(\$zipfile_content); if (!defined($SH)) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Could not instantiate IO::String object"; $prereq_failed = 1; next; } my $zip_rc; $zip_rc = $zip->readFromFileHandle($SH); if ($zip_rc != AZ_OK) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Archive::Zip->readFromFileHandle returned $zip_rc"; $prereq_failed = 1; next; } if (!make_path($description->{patchdir})) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Could not create patch directory " . $description->{patchdir}; $prereq_failed = 1; next; } $zip->writeToFileNamed($patch_zip); if ($zip_rc != AZ_OK) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Archive::Zip->writeToFileNamed returned $zip_rc"; $prereq_failed = 1; next; } $zip->extractTree('', $description->{patchdir}); if ($zip_rc != AZ_OK) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Archive::Zip->extractTree returned $zip_rc"; $prereq_failed = 1; next; } } # Ensure the install script exists unless (-e $description->{rollback_script}) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "rollback script " . $description->{apply_script} . " does not exist"; $prereq_failed = 1; next; } } # End of rollback # 19883092: Skip upgrade check if needed if (!$skip_upgrade_check && $description->{"startup_mode"} eq "upgrade") { if ($container_db) { # For the multitenant case we need to loop over the pdb list for # this queue entry and compare it to the PDB startup mode foreach my $pdb (split (/ / , $entry -> {"pdbs"})){ 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{undef}->{"startup_mode"} ne "MIGRATE") { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "The database must be in upgrade mode"; $prereq_failed = 1; next; } } } # Call set_patch_metadata to pass the descriptor and patch zip if (!$prereq_failed) { my $superseding = 0; if ($description->{bundle_series}) { my $queue_mode = $entry->{$description->{bundle_series} . "_mode"}; if ($queue_mode eq "superseding_apply") { $superseding = 1; } } my $metadata_sql = "BEGIN dbms_sqlpatch.set_patch_metadata( p_patch_id => " . $description->{patchid} . ", p_patch_uid => " . $description->{patchuid} . ", p_superseding => " . ($superseding ? "TRUE" : "FALSE") . ", p_application_patch => " . ($description->{application_patch} ? "TRUE" : "FALSE"); # Pass the descriptor only if it is defined if (defined($description->{xml_descriptor})) { $metadata_sql .= ", p_patch_descriptor => :descriptor"; } # Pass the directory only during apply if ($description->{mode} eq "apply") { $metadata_sql .= ", p_patch_directory => :directory"; } $metadata_sql .= "); END;"; sqlpatch_log(LOG_DEBUG, "metadata_sql: $metadata_sql\n"); my $metadata_stmt = $dbh->prepare($metadata_sql); my $descriptor; my $directory; if (defined($description->{xml_descriptor})) { $descriptor = read_file($description->{xml_descriptor}, {err_mode => 'quiet'}); if (defined($descriptor)) { sqlpatch_log(LOG_DEBUG, "descriptor from XMLout: $descriptor\n"); $metadata_stmt->bind_param(":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; } } if ($description->{mode} eq "apply") { sqlpatch_log(LOG_DEBUG, "preparing to read $patch_zip\n"); $directory = read_file($patch_zip, {err_mode => 'quiet', binmode => ':raw'}); if (defined($directory)) { sqlpatch_log(LOG_DEBUG, "Read " . length($directory) . " bytes\n"); $metadata_stmt->bind_param(":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; } } $metadata_stmt->execute(); } } # End of patch loop } # End of PDB loop } # End of entry loop # Switch back to root if needed if ($container_db) { $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 # None # # RETURNS # Number of patches installed in total across all PDBs sub install_patches { my $total_patches = 0; # 17665122 - We want to ensure that there are no sessions in pdb$seed # so we switch to cdb$root. if ($container_db){ $dbh->do("ALTER SESSION SET CONTAINER = cdb\$root"); } # For each entry in the patch queue foreach my $queue_entry (@patch_queue) { if ($queue_entry->{"patch_string"} eq "R: A:" || $queue_entry->{"combined"}) { next; # Skip PDBs with nothing to do } # 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) { $con_names_incl = $queue_entry->{"pdbs"}; } else { $con_names_incl = ""; } my $session_initialized = 0; foreach my $patch (@{$queue_entry->{"rollbacks"}}, @{$queue_entry->{"applys"}}) { my ($patchid, $patchuid) = split(/\//, $patch); my $description = $patch_descriptions{$patch}; # 23025340: Determine if this patch is superseding or superseded for # this queue entry by checking the entry in the mode. my $superseded = 0; my $superseding = 0; if ($description->{bundle_series}) { my $queue_mode = $queue_entry->{$description->{bundle_series} . "_mode"}; if ($queue_mode eq "superseding_apply") { $superseding = 1; } elsif ($queue_mode eq "superseded_rollback") { $superseded = 1; } } sqlpatch_log(LOG_DEBUG, "superseded: $superseded superseding: $superseding\n"); if ($total_patches == 0) { sqlpatch_log(LOG_ALWAYS, "Installing patches...\n"); } if (!$session_initialized) { $session_initialized = 1; # 17277459: We need to first execute session_initialize before # installing any patches my $session_init_sql = "BEGIN dbms_sqlpatch.session_initialize('"; if ($user_oh ne "") { $session_init_sql .= $user_oh; } else { $session_init_sql .= $oracle_home; } $session_init_sql .= "', "; $session_init_sql .= $force ? "TRUE, " : "FALSE, "; $session_init_sql .= $debug ? "TRUE) " : "FALSE) "; $session_init_sql .= "; END;"; push (@catcon_scripts, $session_init_sql); } # 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"}; # 23025340: If this patch is superseded, don't call the rollback # script. Instead we want to just call patch_initialize and # patch_finalize. # (This is the model for the future when we create the script # dynamically from the descriptor.) if ($superseded) { push(@catcon_scripts, "COLUMN sqlpatch_logfile NEW_VALUE full_logfile"); push(@catcon_scripts, "SELECT '" . $patch_descriptions{$patch}->{logdir} . "'||'" . "$patchid" . "_rollback_' || " . " 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') || '.log' as sqlpatch_logfile FROM v\$database;"); push(@catcon_scripts, "SPOOL &full_logfile"); push(@catcon_scripts, "SET PAGESIZE 0"); push(@catcon_scripts, "SELECT 'Starting rollback for patch $patchid" . "/" . "$patchuid on ' SYSTIMESTAMP FROM dual"); push(@catcon_scripts, "SET PAGESIZE 10"); my $patch_init_sql = "BEGIN dbms_sqlpatch.patch_initialize(" . "p_patch_id => $patchid," . "p_patch_uid => $patchuid," . "p_flags => '" . $patch_descriptions{$patch}->{flags} . "'||'M'," . "p_description => '" . $patch_descriptions{$patch}->{description} . "'," . "p_action => 'ROLLBACK'," . "p_logfile => '&full_logfile'," . "p_bundle_series => '" . $patch_descriptions{$patch}->{bundle_series} . "'); END;"; push(@catcon_scripts, $patch_init_sql); push(@catcon_scripts, "BEGIN dbms_sqlpatch.patch_finalize; END;"); push(@catcon_scripts, "SET PAGESIZE 0"); push(@catcon_scripts, "SELECT 'Finished rollback for patch $patchid" . "/" . "$patchuid on ' SYSTIMESTAMP FROM dual"); push(@catcon_scripts, "SET PAGESIZE 10"); push(@catcon_scripts, "SPOOL off"); } else { if ($patch_descriptions{$patch}->{"mode"} eq "apply") { push (@catcon_scripts, "@" . $patch_descriptions{$patch}->{"apply_script"}); } else { push (@catcon_scripts, "@" . $patch_descriptions{$patch}->{"rollback_script"}); } # 17277459: Pass all parameters push (@catcon_scripts, "--p\"" . $patch_descriptions{$patch}->{"description"} . "\""); push (@catcon_scripts, "--p" . $patch_descriptions{$patch}->{"logdir"}); push (@catcon_scripts, "--p" . ($superseding ? $patch_descriptions{$patch}->{"flags"} . "M" : $patch_descriptions{$patch}->{"flags"})); if (defined($patch_descriptions{$patch}->{"bundle_series"})) { push (@catcon_scripts, "--p" . $patch_descriptions{$patch}->{"bundle_series"}); } else { # We can't pass a null parameter with catcon, so the series will be # NONE. push (@catcon_scripts, "--pNONE"); } } } if ($debug) { sqlpatch_log(LOG_DEBUG, "catcon statements:\n"); foreach my $entry (@catcon_scripts) { sqlpatch_log(LOG_DEBUG, "$entry\n"); } } # 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, "\ncactonExec failed during patch installation\n"); $catcon_ok = 0; return 0; } } 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 # None # # RETURNS # 0 for success, 2 if one or more logfiles contains a non-ignorable error sub validate_logfiles { my $ret = 0; my $found_error = 0; sqlpatch_log(LOG_ALWAYS, "Validating logfiles..."); if ($debug || $verbose) { sqlpatch_log(LOG_ALWAYS, "\n"); } foreach my $queue_entry (@patch_queue) { if ($queue_entry->{"combined"}) { next; } my @pdbs; if (defined($queue_entry->{"pdbs"})) { @pdbs = split(' ', $queue_entry->{"pdbs"}); } for (my $p = 0; $p <= (@pdbs ? $#pdbs : 0); $p++) { if (@pdbs) { sqlpatch_log(LOG_DEBUG, "validate_logfiles switching to pdb @pdbs[$p]\n"); # Switch to PDB first $dbh->do("ALTER SESSION SET CONTAINER = " . @pdbs[$p]); # 24450424: No need to open the seed read write as catcon now # does it for us } foreach my $patch (@{$queue_entry->{"rollbacks"}}) { $ret = validate_one_logfile(@pdbs ? @pdbs[$p] : undef, $patch, "rollback"); if ($ret) { $found_error = 1; } } foreach my $patch (@{$queue_entry->{"applys"}}) { $ret =validate_one_logfile(@pdbs ? @pdbs[$p] : undef, $patch, "apply"); if ($ret) { $found_error = 1; } } } } sqlpatch_log(LOG_DEBUG, "Patch results:\n" . Data::Dumper->Dumper(\@patch_results)); # All log files have been validated, display results if needed if (!$verbose && !$found_error) { sqlpatch_log(LOG_ALWAYS, "done\n"); return 0; } if (!$verbose) { sqlpatch_log(LOG_ALWAYS, "\n"); } foreach my $result_entry (@patch_results) { my $patchid = $patch_descriptions{$result_entry->{"patch"}}->{"patchid"}; if ($verbose || $result_entry->{"status"} ne "SUCCESS") { sqlpatch_log(LOG_ALWAYS, "Patch $patchid " . $result_entry->{"mode"} . ($result_entry->{"pdb"} ? (" (pdb " . $result_entry->{"pdb"} . ")") : "") . ": " . $result_entry->{"status"} . "\n"); sqlpatch_log(LOG_ALWAYS, " logfile: " . $result_entry->{"logfile"}); if ($#{$result_entry->{"errors"}} eq -1) { sqlpatch_log(LOG_ALWAYS, " (no errors)\n"); } else { sqlpatch_log(LOG_ALWAYS, " (errors)\n"); for (my $i = 0; $i <= $#{$result_entry->{"errors"}}; $i++) { sqlpatch_log(LOG_ALWAYS, " Error at line " . $result_entry->{"error_lines"}[$i] . ": " . $result_entry->{"errors"}[$i] . "\n"); } } } } #22204310: Switch back to root if ($container_db) { sqlpatch_log(LOG_DEBUG, "Switching to CDB\$ROOT before wrapup\n"); my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = CDB\$ROOT"); $alter_handle->execute; } if ($found_error) { return 2; } else { return 0; } } # ------------------------------- validate_one_logfile ----------------------- # NAME # validate_one_logfile # # DESCRIPTION # Determines the logfile for a given patch installation attempt, checks for # errors, and adds the result to @patch_results. Also updates the SQL # registry with the results. # # ARGUMENTS # $pdb: PDB to which this patch was installed (undefined if not a # container DB). The session should already be pointing to the # correct PDB prior to calling this routine. # $patch: patch which was installed # $mode: "apply" or "rollback" # # 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. # Regardless of the return status, a new entry is added to @patch_results. sub validate_one_logfile { my ($pdb, $patch, $mode) = @_; my $result_rec = {}; my $retval; my $patchid; my $patchuid; # 17277459: Handle bootstrap mode if ($mode eq "bootstrap") { if ($container_db) { $result_rec->{"logfile"} = $pdb_info{$pdb}->{"bootstrap_log"}; } else { $result_rec->{"logfile"} = $pdb_info{undef}->{"bootstrap_log"}; } } else { # 17665117: Split into patch id and UID ($patchid, $patchuid) = split(/\//, $patch); sqlpatch_log(LOG_DEBUG, "validate_one_logfile checking patch $patchid UID $patchuid mode $mode\n"); # Get logfile by querying the registry my $registry_query = "SELECT status, logfile 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 = ? AND action = UPPER(?))"; ($result_rec->{"status"}, $result_rec->{"logfile"}) = $dbh->selectrow_array($registry_query, undef, ($patchid, $patchuid, $patchid, $patchuid, $mode)); $result_rec->{"patch"} = $patch; $result_rec->{"mode"} = $mode; $result_rec->{"pdb"} = $pdb; sqlpatch_log(LOG_DEBUG, "validate_one_logfile checking " . $result_rec->{"logfile"} . ", status " . $result_rec->{"status"} . "\n"); } # 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; # 17981677: 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, use them. If not, use the standard list. # PSU list. my @local_ignorable_errors; if ($#user_ignorable_errors ne -1 || ($mode eq "bootstrap")) { @local_ignorable_errors = @user_ignorable_errors; } # 14643995: Skip this check if in bootstrap mode. This avoids putting an # empty entry in the patch decriptions hash. elsif ($mode ne "bootstrap") { sqlpatch_log(LOG_DEBUG, "Setting ignorable errors to standard_ignorable_errors\n"); @local_ignorable_errors = @standard_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 foreach my $type ("sqlpatch") { my @file_ignorable_errors = undef; my $lineno = 0; my $lines; my $errors; my $logfile; my $logok = 1; if ($type eq "sqlpatch") { $lines = "error_lines"; $errors = "errors"; $logfile = $result_rec->{"logfile"}; } 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 my $error_regexp = "(.*PL/SQL: |^)[A-Z]{2}[A-Z2]-\\d{4,5}"; # ORA- and SP2- $error_regexp .= "|PLS-\\d\\d\\d\\d\\d"; # PLS-xxxxx # compilation errors $error_regexp .= "|^Warning: .* with compilation errors"; # PL/SQL errors in show errors output $error_regexp .= "| PL/SQL: .*"; open (LOGFILE, $logfile); while (my $line = ) { chomp $line; $lineno++; # 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. if ($line =~ /^(SQL> )*(rem|--|DOC>)/i) { # One line comment of various forms next; } # 24285405: Handle the case where echo is on if ($line =~ /^(SQL> 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)/) { 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); $logok = 0; } } } } if ($type eq "sqlpatch") { $sqlpatch_logok = $logok; } } if ($mode eq "bootstrap") { if ($sqlpatch_logok) { return 0; } else { 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"); } return 2; } } else { if ($sqlpatch_logok) { $result_rec->{"status"} = "SUCCESS"; $retval = 0; } else { $result_rec->{"status"} = "WITH ERRORS"; $retval = 2; } push(@patch_results, $result_rec); # Update the registry my $update_sql = "UPDATE dba_registry_sqlpatch SET status = ?, action_time = SYSTIMESTAMP WHERE patch_id = ? AND patch_uid = ? AND action = UPPER(?) AND action_time = (SELECT MAX(action_time) FROM dba_registry_sqlpatch WHERE patch_id = ? AND patch_uid = ? AND action = UPPER(?))"; my $sth = $dbh->prepare($update_sql); $sth->execute($result_rec->{"status"}, $patchid, $patchuid, $mode, $patchid, $patchuid, $mode); sqlpatch_log(LOG_DEBUG, "updated registry to " . $result_rec->{"status"} . " for $patchid/$patchuid mode $mode\n"); } 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. # # ARGUMENTS # None # # RETURNS # 0 for success, 1 for failure during the bootstrap process sub bootstrap { my $ret; # 22923409: Ensure that we are not connected to pdb$seed if ($container_db) { $dbh->do("ALTER SESSION SET CONTAINER = cdb\$root"); } if ($verbose || $full_bootstrap) { sqlpatch_log(LOG_ALWAYS, "Bootstrapping registry and package to current versions..."); } sqlpatch_log(LOG_DEBUG, "\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 to run in PDBs if ($container_db) { $con_names_incl = join(' ', @pdb_list); } else { $con_names_incl = ""; } my $oh; if ($user_oh ne "") { $oh = $user_oh; } else { $oh = $oracle_home; } my $bootstrap_file = File::Spec->catfile($user_oh ne "" ? $user_oh : $oracle_home, "sqlpatch", "sqlpatch_bootstrap_driver.sql"); push (@catcon_scripts, "\@$bootstrap_file"); push (@catcon_scripts, "--p$invocation_logdir"); if ($full_bootstrap) { push (@catcon_scripts, "--pTRUE"); } else { push (@catcon_scripts, "--pFALSE"); } # 22694961: Pass the build label and application mode push (@catcon_scripts, "--p" . catconst::CATCONST_BUILD_LABEL); if ($app_connect) { push (@catcon_scripts, "--pTRUE"); } else { push (@catcon_scripts, "--pFALSE"); } # Record bootstrap log if ($container_db) { foreach my $pdb (@pdb_list) { my $local_name = $pdb; $local_name =~ s/\$//; $pdb_info{$pdb}->{"bootstrap_log"} = File::Spec->catfile($invocation_logdir, "bootstrap_" . $database_name . "_" . $local_name . ".log"); } } else { $pdb_info{undef}->{"bootstrap_log"} = File::Spec->catfile($invocation_logdir, "bootstrap_" . $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 @pdb_list); } else { $timeout = BOOTSTRAP_TIMEOUT; } eval { # 22923409: Timeout after BOOTSTRAP_TIMEOUT (default 120) * # 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"); $catcon_ok = 0; $ret = 1; goto bootstrap_complete; } else { # catcon failed with other error sqlpatch_log(LOG_ALWAYS, "\nBootstrap failed with $@\n"); $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"); $catcon_ok = 0; $ret = 1; goto bootstrap_complete; } if ($verbose || $full_bootstrap) { sqlpatch_log(LOG_ALWAYS, "done\n"); } foreach my $pdb (@pdb_list) { if (validate_one_logfile($pdb, undef, "bootstrap")) { $ret = 1; } } bootstrap_complete: 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 my $applied_query = "SELECT patchid || '/' || patchuid FROM XMLTable('/sql_registry_state/patch' PASSING dbms_sqlpatch.sql_registry_state COLUMNS patchid NUMBER PATH '\@id', patchuid NUMBER PATH '\@uid', action VARCHAR2(15) PATH '\@action', status VARCHAR2(15) PATH '\@status') WHERE action = 'APPLY' OR (action = 'ROLLBACK' AND status != 'SUCCESS')"; 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) { my $alter_handle = $dbh->prepare("ALTER SESSION SET CONTAINER = " . $pdb); $alter_handle->execute; } 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 patch, from the # XML descriptor, queryable inventory, and/or the SQL registry. # The fields which are populated include: # $xml_descriptor $xml_descriptor_hash $startup_mode $flags # $bundle $bundle_series $bundle_id (if possible) $jvm # $existing_series $existing_bundle_id # Notably, this routine does not set metadata related to the binary or # SQL install state such as $missing_nodes or $pdb_sql_state, that is # the responsibility of the calling function (get_current_patches). # # If the metadata indicates that this is a bundle patch, an (empty) entry in # all_series is also created if it does not already exist. # # 20348653: If the description already has existing metadata (for example # loaded from queryable inventory, and now loading from the SQL registry), # existing properties will be replaced. # # 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, and the descriptor cannot be found in either the # oracle home or registry, load the metadata from the queryable # inventory XML. If $force is true, then we use only the # -bundle_series command line option. # $sql_registry_rowid: If defined, rowid for this patch in the SQL registry # from which we will read the metadata if we cannot find the # XML descriptor. # Generally, only 1 of $useqi or $sql_registry_rowid will be set at a time, # i.e. we get metadata from only the binary or SQL registry. # # 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) = @_; my $sql_registry_ref; my $ret = 0; sqlpatch_log(LOG_DEBUG, "load_patch_metadata entry for ID " . $description->{"patchid"} . "/" . $description->{"patchuid"} . "\n"); 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, version, flags, action, status, action_time, description, bundle_series, bundle_id, XMLType.GetClobVal(patch_descriptor) patch_descriptor, logfile 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)); } # Start with the known properties based solely on ID and UID. $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"}) . "/"; 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"}) }; 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)); $description->{"xml_descriptor_hash"} = $xml_ref; } else { # Fail if -noqi is set, because the descriptor must be present in the # file system. if ($noqi) { $description->{"prereq_ok"} = 0; $description->{"prereq_failed_reason"} = "Descriptor " . $description->{"xml_descriptor"} . " does not exist"; $ret = 1; goto load_patch_metadata_complete; } 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"}) }; 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)); $description->{"xml_descriptor_hash"} = $xml_ref; } } } if (defined($xml_ref)) { # We have found an XML descriptor from either the file system or registry, # use it for the metadata. # 24437510: Compare case insensitively if (lc($xml_ref->{"startupMode"}) eq "normal") { $description->{"startup_mode"} = "normal"; $description->{"flags"} = "N"; } else { $description->{"startup_mode"} = "upgrade"; $description->{"flags"} = "U"; } $description->{"description"} = $xml_ref->{"patchDescription"}; if ($xml_ref->{"bundle"} eq "NO") { $description->{"bundle_series"} = undef; } else { $description->{"bundle_series"} = $xml_ref->{"bundleSeries"}; $description->{"bundle_id"} = $xml_ref->{"bundleID"}; $description->{"flags"} .= "B"; # 17277459: Add to all_series if (!grep(/$description->{"bundle_series"}/, keys %all_series)) { $all_series{$description->{"bundle_series"}} = {}; } # 23025340: Set switching trains properties if (defined($xml_ref->{ecbpBundleSeries})) { $description->{existing_series} = $xml_ref->{ecbpBundleSeries}; $description->{existing_bundle_id} = $xml_ref->{ecbpBundleID}; } } if ($xml_ref->{"jvm"} eq "YES") { $description->{"jvm_patch"} = 1; $description->{"flags"} .= "J"; } else { $description->{"jvm_patch"} = 0; } # 22694961: Set application patch property if (defined($xml_ref->{applicationPatch})) { $description->{application_patch} = ($xml_ref->{applicationPatch} eq "YES" ? 1 : 0); } else { $description->{application_patch} = 0; } if ($description->{application_patch}) { $description->{flags} .= 'A'; } goto load_patch_metadata_complete; } # Descriptor does not exist $description->{"xml_descriptor"} = undef; # 22694961: Default value for application_patch $description->{application_patch} = 0; if ($use_binary) { sqlpatch_log(LOG_DEBUG, "load_patch_metadata useqi\n"); # Read from queryable inventory if possible if ($force) { # We cannot use queryable inventory, so our only recourse is the # command line parameters $description->{"flags"} = "N"; # Assume normal patch if (defined($bundle_series)) { if ($bundle_series eq "NONE") { $description->{"bundle_series"} = undef; } else { $description->{"bundle_series"} = $bundle_series; $description->{"flags"} .= "B"; $description->{"bundle_id"} = undef; # 17277459: Add to all_series if (!grep(/$description->{"bundle_series"}/, keys %all_series)) { $all_series{$bundle_series} = {}; } } } # That's all we can do } else { # -force not specified, 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 dbms_sqlpatch.get_opatch_lsinventory COLUMNS description VARCHAR2(100) PATH 'patchDescription', startup_mode VARCHAR2(7) PATH 'sqlPatchDatabaseStartupMode')"; ($description->{"description"}, $description->{"startup_mode"}) = $dbh->selectrow_array($description_mode_query); if ($description->{"startup_mode"} eq "upgrade") { $description->{"flags"} = "U"; } else { $description->{"flags"} = "N"; } # Determine bundle_series and jvm_patch status my $files_query = "SELECT filename FROM XMLTable('/InventoryInstance/patches/patch[patchID=" . $description->{"patchid"} . "]//file' PASSING dbms_sqlpatch.get_opatch_lsinventory COLUMNS filename VARCHAR2(50) PATH '.')"; my @patch_files = @{$dbh->selectcol_arrayref($files_query)}; $description->{"bundle_series"} = undef; $description->{"jvm_patch"} = 0; # Loop through the file list to see if we have bundledata.xml for my $patch_file (@patch_files) { sqlpatch_log(LOG_DEBUG, "Checking patch file $patch_file for bundledata.xml\n"); if ($patch_file =~ /^bundledata_(.*)\.xml$/) { $description->{"bundle_series"} = $1; $description->{"flags"} .= "B"; # 17277459: Add to all_series if (!grep(/$description->{"bundle_series"}/, keys %all_series)) { $all_series{$description->{"bundle_series"}} = {}; } sqlpatch_log(LOG_DEBUG, "Set bundle_series to $1\n"); } elsif ($patch_file eq "jvmpsu.sql") { # 19708632: This is a JVM patch, mark it as such $description->{"jvm_patch"} = 1; $description->{"flags"} .= "J"; sqlpatch_log(LOG_DEBUG, "Set jvm_patch to true\n"); } } } } if (defined($sql_registry_rowid)) { sqlpatch_log(LOG_DEBUG, "load_patch_metadata SQL\n"); # Read properties from SQL registry. This will override # existing properties if the patch was previously found in binary. # (maybe raise an error in this case?) $description->{"flags"} = $sql_registry_ref->{"FLAGS"}; $description->{"flags"} =~ s/F//g; # 19521006: Strip existing F if ($description->{"flags"} =~ /U/) { $description->{"startup_mode"} = "upgrade"; } else { $description->{"startup_mode"} = "normal"; } $description->{"description"} = $sql_registry_ref->{"DESCRIPTION"}; if ($description->{"flags"} =~ /B/) { $description->{"bundle_series"} = $sql_registry_ref->{"BUNDLE_SERIES"}; $description->{"bundle_id"} = undef; if ($sql_registry_ref->{"action"} eq "APPLY") { # If this is an APPLY, then we can set bundle_id also $description->{"bundle_id"} = $sql_registry_ref->{"BUNDLE_ID"}; } else { # Not so easy to set bundle_id, as the bundle_id for this row will # be the version we rolled back to, not the version we rolled back # from. But we can find it by looking for a previous apply for # this patch key. sqlpatch_log(LOG_DEBUG, "looking for apply for key/series " . $sql_registry_ref->{"PATCH_ID"} . "/" . $sql_registry_ref->{"PATCH_UID"} . "/" . $description->{"bundle_series"} . "\n"); my $bundle_id_sql = "SELECT UNIQUE bundle_id FROM dba_registry_sqlpatch WHERE action = 'APPLY' AND patch_id = ? AND patch_uid = ?"; ($description->{"bundle_id"}) = $dbh->selectrow_array($bundle_id_sql, undef, $sql_registry_ref->{"PATCH_ID"}, $sql_registry_ref->{"PATCH_UID"}); } # 17277459: Add to all_series if (!grep(/$description->{"bundle_series"}/, keys %all_series)) { $all_series{$description->{"bundle_series"}} = {}; } } else { $description->{"bundle_series"} = undef; } if ($sql_registry_ref->{"FLAGS"} =~ /J/) { $description->{"jvm_patch"} = 1; } else { $description->{"jvm_patch"} = 0; } if ($sql_registry_ref->{FLAGS} =~ /A/) { $description->{application_patch} = 1; } } load_patch_metadata_complete: if ($force) { $description->{"flags"} .= 'F'; } 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 { if ($force) { # -force specified, skip the rest of the checks and bootstrap return 0; } # 19189525: Call bootstrap if (bootstrap()) { return 1; } # 22359063: Call verify_queryable_inventory only if -noqi was not specified # 22694961: And if $binary_config was not specified if (!defined($noqi) && !defined($binary_config)) { if ($container_db) { $dbh->do("ALTER SESSION SET CONTAINER = cdb\$root"); } sqlpatch_log(LOG_DEBUG, "verify_qi calling verify_queryable_inventory...\n"); my $verify_query = "SELECT dbms_sqlpatch.verify_queryable_inventory FROM dual"; my ($qi_status) = $dbh->selectrow_array($verify_query); sqlpatch_log(LOG_DEBUG, "qi status: $qi_status\n"); if ($qi_status eq "OK") { return 0; } else { sqlpatch_log(LOG_INVOCATION, "verify_queryable_inventory returned $qi_status\n"); sqlpatch_log(LOG_ALWAYS, "\nQueryable inventory could not determine the current opatch status.\n"); sqlpatch_log(LOG_ALWAYS, "Execute 'select dbms_sqlpatch.verify_queryable_inventory from dual'\n"); sqlpatch_log(LOG_ALWAYS, "and/or check the invocation log\n" . $invocation_log); sqlpatch_log(LOG_ALWAYS, "\nfor the complete error.\n"); return 1; } } elsif (defined($binary_config)) { # Load contents of specified file if (! -e $binary_config) { sqlpatch_log(LOG_ALWAYS, "Binary config file $binary_config does not exist\n"); return 1; } $binary_config_hash = eval { XMLin($binary_config) }; if ($@) { sqlpatch_log(LOG_ALWAYS, "Error reading binary config file $binary_config: $@\n"); return 1; } } else { return 0; } } # ------------------------------- 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, "all_series: " . Data::Dumper->Dumper(\%all_series)); sqlpatch_log($level, "patch_queue: " . Data::Dumper->Dumper(\@patch_queue)); sqlpatch_log($level, "patch_results: " . Data::Dumper->Dumper(\@patch_results)); sqlpatch_log($level, "*** END $message ***\n"); } # ----------------------------- 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, "sqlpatch usage:\n"); sqlpatch_log(LOG_ALWAYS, "All arguments are optional, if there are no arguments sqlpatch\n"); sqlpatch_log(LOG_ALWAYS, "will automatically determine which SQL scripts need to be run in\n"); sqlpatch_log(LOG_ALWAYS, "order to complete the installation of any SQL patches.\n\n"); sqlpatch_log(LOG_ALWAYS, "Optional arguments:\n"); sqlpatch_log(LOG_ALWAYS, "-db \n"); sqlpatch_log(LOG_ALWAYS, " Use the specified database rather than \$ORACLE_SID\n"); sqlpatch_log(LOG_ALWAYS, "-bundle_series \n"); sqlpatch_log(LOG_ALWAYS, " Specify if the patch is a bundle patch \n"); sqlpatch_log(LOG_ALWAYS, " Should also be accompanied by -force option\n"); sqlpatch_log(LOG_ALWAYS, " if -bundle_series option is specified,only 1 patch will\n"); sqlpatch_log(LOG_ALWAYS, " be considered by the -force command\n"); sqlpatch_log(LOG_ALWAYS, "-apply \n"); sqlpatch_log(LOG_ALWAYS, " Only consider the specified patch list for apply operations\n"); sqlpatch_log(LOG_ALWAYS, "-rollback \n"); sqlpatch_log(LOG_ALWAYS, " Only consider the specified patch list for rollback operations\n"); sqlpatch_log(LOG_ALWAYS, "-upgrade_mode_only\n"); sqlpatch_log(LOG_ALWAYS, " Only consider patches that require upgrade mode\n"); sqlpatch_log(LOG_ALWAYS, "-force\n"); sqlpatch_log(LOG_ALWAYS, " Run the apply and/or rollback scripts even if not necessary\n"); sqlpatch_log(LOG_ALWAYS, " per the SQL registry\n"); sqlpatch_log(LOG_ALWAYS, "-pdbs \n"); sqlpatch_log(LOG_ALWAYS, " Only consider the specified list of PDBs for patching. All\n"); sqlpatch_log(LOG_ALWAYS, " other PDBs will not be patched\n"); sqlpatch_log(LOG_ALWAYS, "-prereq\n"); sqlpatch_log(LOG_ALWAYS, " Run prerequisite checks only, do not actually run any scripts\n"); sqlpatch_log(LOG_ALWAYS, "-oh \n"); sqlpatch_log(LOG_ALWAYS, " Use the specified directory to check for installed patches\n"); sqlpatch_log(LOG_ALWAYS, "-verbose\n"); sqlpatch_log(LOG_ALWAYS, " Output additional information used for debugging\n"); sqlpatch_log(LOG_ALWAYS, "-help\n"); sqlpatch_log(LOG_ALWAYS, " Output usage information and exit\n"); sqlpatch_log(LOG_ALWAYS, "-version\n"); sqlpatch_log(LOG_ALWAYS, " Output build information and exit\n"); sqlpatch_log(LOG_ALWAYS, "\n"); } 1;