# 
# $Header: rdbms/admin/odbsrmt.pl /main/12 2018/06/19 06:11:40 molagapp Exp $
#
# odbsrmt.pl
# 
# Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
#
#    NAME
#      odbsrmt.pl - Oracle Database Backup Service Reporting and Maintenance Tool 
#
#    DESCRIPTION
#      This script is used to generate backup reports/and perform
#      maintenance in a easy way. 
#
#
#    MODIFIED   (MM/DD/YY)
#    molagapp    04/13/18 - bug 27927431: add rman-duplicate mode 
#    molagapp    01/17/18 - add date filter
#    molagapp    10/11/17 - add json and xml output. move utility func
#                           to odbsrmt.pm
#    molagapp    08/15/17 - bug 26583495
#    molagapp    04/26/17 - add support for DEFERED_DELETE
#    molagapp    10/05/16 - bug 25745299 
#    molagapp    10/05/16 - add option to delete backups
#    molagapp    06/14/16 - Creation
# 

use strict;
use warnings;
use File::Basename qw();
BEGIN {
  my ($name, $path, $suffix) = File::Basename::fileparse($0);
  push @INC, $path;
} 
use Pod::Usage;
use Getopt::Long;     # to parse command line options
use Term::ReadKey;    # to not echo password
use IPC::Open2;       # to perform 2-way communication with SQL*Plus
use XML::Parser;      # to parse XML doc
use File::Spec ();
use Cwd;
use odbsrmt qw( log_msg valid_log_dir get_log_file_base_path 
                get_login_string opc_validation opc_get_counts
                opc_getcontainers opc_getobjects opc_report
                opc_delete_file opc_filter_delete_file
                opc_delete 
);

# these consts should be exported by odbsrmt.pm, will fix it
use constant REPORTFLAG          => 0x000001;
use constant DUPLICATEFLAG       => 0x000010;
use constant DELETEFLAG          => 0x000100;
use constant GARBAGECOLLECTFLAG  => 0x001000;

# set all option vars to 0 to avoid "Use of uninitialized value" errors
our ($cred, $help, $debug, $host, $prefix,
     $container, $dir, $base, $mode, $format, 
     $dbid, $dbname, $exclude_deferred, $untildate,
     $forcename ) = 
     (0) x 15;

# file path and base name
my $ReportFilePathBase;
# report file handle
my $REPORTOUT;
# file full path name
my $ReportFileFullName;
# script mode
my $report = 0;
my $duplicate = 0;
my $delete = 0;
my $garbagecollect = 0;
# delet list
my @deletelist;
my $rc;
my $ext = "lst";
my $modebit = 0;
my $retrycnt;
my $RETRY = 3;

$prefix = "";

# Get Options
GetOptions ("credential=s"     => \$cred,       # username/[passwd] 
            "help"             => \$help,       # help page
            "debug"            => \$debug,      # turn on debug
            "host=s"           => \$host,       # host 
            "prefix=s"         => \$prefix,     # file prefix 
            "container=s"      => \$container,  # OPC container name 
            "dir=s"            => \$dir,        # Dir to store report 
            "base=s"           => \$base,       # report file base name 
            "mode=s"           => \$mode,       # mode of the script 
            "format=s"         => \$format,     # format of report output 
            "dbid=s"           => \$dbid,       # dbid to delete 
            "dbname=s"         => \$dbname,     # dbname to delete 
            "untildate=s"      => \$untildate,  # until date 
            "forcename=s"      => \$forcename,  # force outputname 
            "exclude_deferred" => \$exclude_deferred # ignore backups 
                                                    # in heartbeat xml
           )
     or die("Error in parsing command line arguments\n");

if (!$cred or $help)
{
   pod2usage( -verbose => 99 );
   exit 0;
}

if (!$base and !$forcename)
{
   log_msg("odbsrmt.pl: output file name must be specified\n");

   pod2usage( -verbose => 99 );
   exit 0;
}

if (!$mode)
{
   log_msg("odbsrmt.pl: mode of the script name must be specified".
           ", either be report or delete\n");

   pod2usage( -verbose => 99 );
   exit 0;
}

if (lc $mode eq lc "report")
{
   $report = 1;
   $modebit = $modebit | REPORTFLAG;
}
elsif (lc $mode eq lc "rman-duplicate")
{
   # duplicate mode can be viewed as a submode of report
   # code is path is similar, just print out less columns
   $report = 1;
   $duplicate = 1;
   $modebit = $modebit | REPORTFLAG;
   $modebit = $modebit | DUPLICATEFLAG;

   $format = "xml";

   if (!$dbid)
   {
      log_msg("odbsrmt.pl: DBID must be specified\n");
      exit 0;
   }
}
elsif (lc $mode eq lc "delete")
{
   $modebit = $modebit | DELETEFLAG;
   $delete = 1;

   if (!$dbname and !$dbid)
   {
      log_msg("odbsrmt.pl: DBNAME or DBID must be specified\n");
      exit 0;
   }
}
elsif (lc $mode eq lc "garbage-collection")
{
   $garbagecollect = 1;
   $modebit = $modebit | GARBAGECOLLECTFLAG;
}
else
{
   log_msg("odbsrmt.pl: invalid mode, either be report, rman-duplicate,".
           " delete or garbage-collection\n");
   exit 0;
}

$cred = get_login_string ($cred,1); 

#clean host variable a little
chomp $host;

if (substr($host, -1) eq "/") 
{
   chop $host;
}

if ($report or $duplicate)
{
   if (($format) and (lc $format ne lc "text") 
       and (lc $format ne lc "json") and (lc $format ne lc "xml"))
   {
      log_msg("odbsrmt.pl: invalid format option");
      die;
   }

   if (lc $format eq lc "xml")
   {
      $ext = "xml";
   }
   elsif (lc $format eq lc "json")
   {
      $ext = "json";
   }
   else
   {
      $ext = "lst";
   }

   if (!valid_log_dir($dir))
   {
      log_msg("odbsrmt.pl: unexpeced error from valid_log_dir");
      die;
   }  

   if ($forcename)
   {
      $ReportFileFullName = 
        get_log_file_base_path($dir, $forcename, $debug);
   }
   else
   {
      $ReportFilePathBase = 
        get_log_file_base_path($dir, $base, $debug);

      $ReportFileFullName = $ReportFilePathBase.$$.".$ext";
   }

   # open report file handle
   if (!open($REPORTOUT, ">", $ReportFileFullName))
   {
      print STDERR "set_log_file_base_path: unable to open ".
                   "$ReportFileFullName as REPORTOUT\n";
      die;
   }

   print STDERR "odbsrmt.pl: ALL report output will be written ".
                "to [".$ReportFileFullName."]\n";

   # make $REPORTOUT"hot" so diagnostic and error message output does not get
   # buffered
   select((select($REPORTOUT), $|=1)[0]);

   # main routine to construct backup metadata report
   opc_report($REPORTOUT, $cred, $host, $container, undef, 
              $modebit, $exclude_deferred, 
              $format, $prefix,
              $dbname, $dbid, $untildate, $debug);

   log_msg("odbsrmt.pl: ALL report output has been ".
           "written to [".$ReportFileFullName."]\n");
   log_msg("odbsrmt.pl: Reporting completed\n");

   close($REPORTOUT);
}
elsif ($delete or $garbagecollect)
{
   # main routine to construct backup metadata report
   opc_report(undef, $cred, $host, $container, \@deletelist,
              $modebit, $exclude_deferred, $format,
              $prefix, $dbname, $dbid, $untildate, $debug);

   if (!@deletelist)
   {
      if ($garbagecollect)
      {
         log_msg("odbsrmt.pl: no qualifying backups found to purge\n");
         exit 0;
      }
      else
      {
         log_msg("odbsrmt.pl: no qualifying backups found to delete, ".
                 "verify whether Dbname or Dbid is correctly given\n");
      }
      die;
   }

   if (!valid_log_dir($dir))
   {
      log_msg("odbsrmt.pl: unexpeced error from valid_log_dir");
      die;
   }  

   $ReportFilePathBase = 
     get_log_file_base_path($dir, $base, $debug);

   $ext = "lst";

   if ($forcename)
   {
      $ReportFileFullName = 
        get_log_file_base_path($dir, $forcename, $debug);
   }
   else
   {
      $ReportFileFullName = $ReportFilePathBase.$$.".$ext";
   }

   # open report file handle
   if (!open($REPORTOUT, ">", $ReportFileFullName))
   {
      print STDERR "set_log_file_base_path: unable to open ".
                   "$ReportFileFullName as REPORTOUT\n";
      return 0;
   }

   print STDERR "odbsrmt.pl: ALL file names to be deleted will be written ".
                "to [".$ReportFileFullName."]\n";

   # make $REPORTOUT"hot" so diagnostic and error message output does not get
   # buffered
   select((select($REPORTOUT), $|=1)[0]);

   if ($dbid)
   {
      opc_delete_file($REPORTOUT, $cred, $host, @deletelist, 
                      $report, $exclude_deferred,
                      $prefix, $debug);

      for ($retrycnt = 0; $retrycnt < $RETRY; $retrycnt++)
      {
         $rc =  opc_delete($cred, $host, $ReportFileFullName, 
                           $debug);
         if ($rc == 0)
         {last;}
         log_msg("odbsrmt.pl: DELETE request failed, retrying...\n");
      }

      if ($rc == -1)
      {
         log_msg("odbsrmt.pl: Deletion failed\n");
         die;
      }
   }
   elsif ($dbname and !$dbid)
   {
      my @filterdeletelist = ();

      # since a dbname may correspond to mulptiple dbids, we
      # let the user decide which dbid to delete
      opc_filter_delete_file(@deletelist, @filterdeletelist); 

      if (!@filterdeletelist) 
      {
         log_msg("odbsrmt.pl: No matched file to be deleted, quit...\n");
         die;
      }

      opc_delete_file($REPORTOUT, $cred, $host, @filterdeletelist,
                      $report, $exclude_deferred, $prefix, $debug);

      for ($retrycnt = 0; $retrycnt < $RETRY; $retrycnt++)
      {
         $rc =  opc_delete($cred, $host, $ReportFileFullName,
                           $debug);
         if ($rc == 0)
         {last;}

         log_msg("odbsrmt.pl: DELETE request failed, retrying...\n");
      }

      if ($rc == -1)
      {
         log_msg("odbsrmt.pl: Deletion failed\n");
         die;
      }
   }
   elsif ($garbagecollect)
   {
      # in garbage collection mode, dbid is optional, so remove
      # all expired heartbeat metadata regardless of its dbid.
      opc_delete_file($REPORTOUT, $cred, $host, @deletelist, 
                      $report, 0, $prefix, $debug);

      for ($retrycnt = 0; $retrycnt < $RETRY; $retrycnt++)
      {
         $rc =  opc_delete($cred, $host, $ReportFileFullName,
                           $debug);
         if ($rc == 0)
         {last;}

         log_msg("odbsrmt.pl: DELETE request failed, retrying...\n");
      }

      if ($rc == -1)
      {
         log_msg("odbsrmt.pl: Deletion failed\n");
         die;
      }
   }
   else
   {
      log_msg("odbsrmt.pl: DBNAME or DBID must be specified\n");
      die;
   }

   log_msg("odbsrmt.pl: ALL file names have been ".
           "written to [".$ReportFileFullName."]\n");

   log_msg("odbsrmt.pl: Deletion completed\n");

   close($REPORTOUT);
}

exit 0;

#END OF MAIN

__END__

=pod

=head1 NAME

odbsrmt.pl - Oracle Database Backup Service Reporting and Maintenance Tool 
 
=head1 SYNOPSIS

perl odbsrmt.pl <option> 
 
=head1 DESCRIPTION

Usage: perl odbsrmt.pl  --credential=username[/password]
                        --host=opc_host
                        --base=file_base_name
                        --mode=mode_of_script
                        [--format=format_mode]
                        [--dbname=database_name]
                        [--dbid=database_id]
                        [--container=name_of_container]
                        [--dir=directory]
                        [--prefix=prefix_string]
                        [--exclude_deferred]
                        [--debug] 
                        [--help]

     --credential username (optional /password; otherwise prompts for password)
       used to connect to Oracle Public Cloud. 

     --host OPC end point. 

     --base      report/delete/rman-duplicate/garbage-collection file base name.
     --forcename report/delete/rman-duplicate/garbage-collection file name.

     NOTE: Only one of the options needs to be specified. --base will generate
           a process ID based name to prevent file conflicts.
           If a fixed output file name is preferred, use --forcename option.

     --mode be "report", "rman-duplicate", "delete" or "garbage-collection"
            report mode lists all metadata information about backup pieces stored
            in OPC.

            rman-duplicate mode generates backup location list XML file that can
            be read by RMAN to perform DUPLCIATE through OSS.

            delete mode removes certain backup pieces in one operation
            --dbname and/or --dbid must be provided. Backup pieces associated with
            the dbname and/or dbid will be deleted.
            
     Note:  you can provide either --dbname or --dbid, or both. If you provide
            --dbname only, a list of dbids associated with the dbname will be 
            prompted. Users will need to choose dbids from the list to proceed.

            garbage-collection mode purges expired hearbeat metadata in one 
            operation.
            By default no dbid is required so all expired heartbeat metadata
            will be purged. If --dbid is specified, then only expired heartbeat
            metadata associated with the dbid will be purged.

   Optional:
     --format specifies keywords from "text", "xml" or "json" to produce formatted
              scanning output.

     --dbname specifies the database name that contains the backup files, for mode=delete.

     --dbid   specifies the database ID that contains the backup files.

     --container specifies the container that contains the backup files.

     --dir directory where the report or delete file will be stored. If omitted, the current
       directory will be used.

     --prefix reports only files with the specified prefix (of the backup piece name). 
       If not specified, all files in the container will be scanned. 

     --untildate Delete backup pieces prior to the specified date. 
                 Used in delete mode only. Must be in YYYY-MM-DD format. 

     --exclude_deferred ignore backups that are deleted by RMAN when _OPC_DEFERRED_DELETE
       is set to TRUE but still exists in OPC container.
       By default, report mode skips heartbeat metadata while delete mode includes 
       heartbeat metadata unless exclude_deferred is specified.

     --help prints help information.

     --debug turns on production of debugging info while running this script.

=head1 NOTE 
     odbsrmt.pl currently supports Linux/Unix platforms only

=cut

