Files
oracle-clone-standalone/oracle_clone.sh
2025-10-16 05:45:19 -04:00

470 lines
17 KiB
Bash
Executable File

#!/bin/bash
ORIG_ARGS="$*"
#
# Perform a Clone of an Oracle DB
# v1.3 - James Pattinson - October 2025
#
# usage: oracle_clone.sh [options] <srcSID> <tgtHOSTNAME>
#
# Options:
# -h <dbhost> Source DB hostname
# -t <timestamp> Recovery point timestamp "YYYY-MM-DD HH:MM:SS" (uses latest if not specified)
# -n <newsid> New database SID for the clone
# -p <pfilepath> Custom pfile for the clone
# -a <key,value> Advanced Cloning Options (can be used multiple times)
# -b <pdb1,pdb2> Comma-separated list of PDBs to clone (include only these PDBs. PDB$SEED is always included)
# -c <channels> Number of RMAN channels to use
# -d Dry run mode - show API payload without executing
# -l <logfile> Log all API calls (endpoints, payloads, responses) to <logfile>
# --refresh Refresh target database before cloning (if it exists)
#
# Time is passed to 'date' on THIS machine, will use local timezone
MYDIR="$(dirname "$(realpath "$0")")"
# Argument parsing and RBK_API_LOG assignment happens below
# After argument parsing, write the log header if enabled
# Debugging: Confirm if log header code is executed
if [ -n "$RBK_API_LOG" ]; then
echo "Debug: Writing log header to $RBK_API_LOG" >> /tmp/debug_oracle_clone.log
export RBK_API_LOG
{
echo "==== Rubrik Oracle Clone Log ===="
date
echo "Script: $0"
echo "Parameters: $ORIG_ARGS"
echo
} >> "$RBK_API_LOG"
fi
source $MYDIR/oracle_funcs.sh
# Set up cleanup trap to ensure temporary files are removed
trap 'cleanup' EXIT INT TERM
usage() { echo "Usage: $0 [options] <srcSID> <tgtHOSTNAME>" 1>&2
echo "Options:" 1>&2
echo " -h <dbhost> Source DB hostname" 1>&2
echo " -t <timestamp> Recovery point \"YYYY-MM-DD HH:MM:SS\"" 1>&2
echo " -n <newsid> New database SID for clone" 1>&2
echo " -p <pfilepath> Custom pfile for the clone" 1>&2
echo " -a <key,value> Advanced Cloning Options (can be used multiple times)" 1>&2
echo " -b <pdb1,pdb2> Comma-separated list of PDBs to clone (include only these PDBs)" 1>&2
echo " -c <channels> Number of RMAN channels to use" 1>&2
echo " -d Dry run mode" 1>&2
echo " -l <logfile> Log all API calls (endpoints, payloads, responses) to <logfile>" 1>&2
echo " --refresh Refresh target database before cloning (if it exists)" 1>&2
exit 1; }
declare -a config_pairs
declare -a pdb_list
dryrun=false
refresh_host=false
num_channels=""
# Handle long options first and rebuild argument array
args=()
for arg in "$@"; do
case $arg in
--refresh)
refresh_host=true
;;
*)
args+=("$arg")
;;
esac
done
set -- "${args[@]}"
RBK_API_LOG=""
while getopts "h:dt:n:p:a:b:c:l:" o; do
case "${o}" in
h)
RBK_HOST=${OPTARG}
;;
t)
datestring=${OPTARG}
;;
n)
newsid=${OPTARG}
;;
p)
custompfile=${OPTARG}
;;
a)
config_pairs+=("${OPTARG}")
;;
b)
IFS=',' read -ra pdb_list <<< "${OPTARG}"
;;
l)
RBK_API_LOG="${OPTARG}"
;;
c)
num_channels=${OPTARG}
;;
d)
dryrun=true
;;
*)
usage
;;
esac
done
shift $((OPTIND-1))
RBK_SID=$1
RBK_TGT=$2
if [ -z "${RBK_SID}" ] || [ -z "${RBK_TGT}" ]; then
usage
fi
echo "Connecting to Rubrik with IP $RUBRIK_IP"
# API call to list Oracle DBs
echo "Finding source database: $RBK_SID"
if ! find_database; then
echo "ERROR: Failed to find source database $RBK_SID"
exit 1
fi
if [ -z "$db_id" ]; then
echo "ERROR: Could not determine database ID for $RBK_SID"
exit 1
fi
echo "Source database ID: $db_id"
# API call to get the host ID of the target
echo "Finding target host: $RBK_TGT"
ENDPOINT="https://$RUBRIK_IP/api/internal/oracle/host?name=$RBK_TGT"
if ! rest_api_get; then
echo "ERROR: Failed to query target host information"
exit 1
fi
# Check if API response exists and is valid
if [ ! -s /tmp/rbkresponse.$$ ]; then
echo "ERROR: Empty response when querying target host"
exit 1
fi
total=$(cat /tmp/rbkresponse.$$ | jq -r .total 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$total" ]; then
echo "ERROR: Invalid response format when querying target host"
exit 1
fi
if [ "$total" -ne 1 ]; then
echo "ERROR: Target host name '$RBK_TGT' does not map to a single host (found: $total)"
echo "Available hosts:"
cat /tmp/rbkresponse.$$ | jq -r '.data[].name' 2>/dev/null || echo "Could not parse host list"
exit 1
fi
target_id=$(cat /tmp/rbkresponse.$$ | jq -r '.data[0].id' 2>/dev/null)
if [ -z "$target_id" ] || [ "$target_id" = "null" ]; then
echo "ERROR: Could not determine target host ID"
exit 1
fi
echo "Target host ID: $target_id"
# Refresh target database if requested and it exists
if [ "$refresh_host" = true ]; then
echo "=== DATABASE REFRESH REQUESTED ==="
# Determine the target database name (use newsid if specified, otherwise use source SID)
if [ -n "$newsid" ]; then
target_db_name="$newsid"
echo "Target database name: $target_db_name (specified with -n option)"
else
target_db_name="$RBK_SID"
echo "Target database name: $target_db_name (using source SID)"
fi
echo "Searching for existing database '$target_db_name' on host '$RBK_TGT'..."
# Search for existing database on target host
ENDPOINT="https://$RUBRIK_IP/api/v1/oracle/db?name=$target_db_name"
if ! rest_api_get; then
echo "WARNING: Failed to query existing databases via API"
echo "Continuing without refresh"
else
if [ ! -s /tmp/rbkresponse.$$ ]; then
echo "WARNING: Empty API response when querying existing databases"
echo "Continuing without refresh"
else
# Show all databases found with this name (for informational purposes)
db_count=$(cat /tmp/rbkresponse.$$ | jq -r --arg TARGET_NAME "$target_db_name" '[.data[] | select(.name==$TARGET_NAME)] | length' 2>/dev/null)
echo "Found $db_count database(s) with name '$target_db_name' in Rubrik inventory"
if [ "$db_count" -gt 0 ]; then
echo "Database details found:"
cat /tmp/rbkresponse.$$ | jq -r --arg TARGET_NAME "$target_db_name" '.data[] | select(.name==$TARGET_NAME) | " - Host: \(.instances[0].hostName), Relic: \(.isRelic), ID: \(.id)"' 2>/dev/null
fi
# Look for database with matching name on target host (not in relic state)
# Handle both short hostname and FQDN matching
echo "Filtering for databases on target host '$RBK_TGT' (non-relic)..."
existing_db_id=$(cat /tmp/rbkresponse.$$ | jq -r --arg HOST "$RBK_TGT" --arg TARGET_NAME "$target_db_name" '
.data[] |
select(.name==$TARGET_NAME and (.instances[0].hostName==$HOST or (.instances[0].hostName | startswith($HOST + "."))) and .isRelic==false) |
.id' 2>/dev/null)
if [ -z "$existing_db_id" ] || [ "$existing_db_id" = "null" ]; then
echo "RESULT: No matching non-relic database found on target host"
echo " - This is normal when cloning to a new database name or host"
echo " - Refresh skipped, proceeding with clone operation"
else
echo "RESULT: Match found! Database ID: $existing_db_id"
echo "Initiating refresh of database '$target_db_name' on host '$RBK_TGT'..."
# Perform database refresh
ENDPOINT="https://$RUBRIK_IP/api/v1/oracle/db/$existing_db_id/refresh"
if ! rest_api_post_empty; then
echo "ERROR: Failed to refresh database via API call"
echo "Continuing with clone operation despite refresh failure"
else
echo "SUCCESS: Database refresh initiated for '$target_db_name'"
echo "Monitoring database status - waiting for it to become relic..."
echo "Press Ctrl+C to abort and exit script"
# Set up signal handler for monitoring loop
monitoring_interrupted=false
trap 'monitoring_interrupted=true; echo ""; echo "Monitoring interrupted by user - exiting script"; cleanup; exit 130' INT
# Monitor the database until it becomes relic or timeout
timeout_seconds=300 # 2 minutes
check_interval=10 # Check every 10 seconds
elapsed_time=0
while [ $elapsed_time -lt $timeout_seconds ] && [ "$monitoring_interrupted" = false ]; do
# Check current database status
remaining=$((timeout_seconds - elapsed_time))
ENDPOINT="https://$RUBRIK_IP/api/v1/oracle/db/$existing_db_id"
if rest_api_get 2>/dev/null; then
# Extract isRelic status
is_relic=$(cat /tmp/rbkresponse.$$ | jq -r '.isRelic' 2>/dev/null)
if [ "$is_relic" = "true" ]; then
echo "SUCCESS: Database '$target_db_name' is now in relic mode"
echo "Safe to proceed with clone operation"
break
else
echo "Status check: Database still active, waiting... (${remaining}s remaining)"
fi
else
echo "WARNING: Failed to check database status, continuing anyway..."
break
fi
# Use a loop with 1-second sleeps to make Ctrl+C more responsive
sleep_count=0
while [ $sleep_count -lt $check_interval ] && [ "$monitoring_interrupted" = false ]; do
sleep 1
sleep_count=$((sleep_count + 1))
done
elapsed_time=$((elapsed_time + check_interval))
done
# Restore original signal handler
trap 'cleanup' EXIT INT TERM
# Check if we were interrupted
if [ "$monitoring_interrupted" = true ]; then
exit 130
fi
# Check if we timed out
if [ $elapsed_time -ge $timeout_seconds ]; then
echo "ERROR: Timeout after 2 minutes waiting for database to become relic"
echo "The target database '$target_db_name' is still running on host '$RBK_TGT'"
echo "Please shut down Oracle database '$target_db_name' before attempting clone"
echo "Clone operation aborted to prevent conflicts"
echo "=== SCRIPT ABORTED DUE TO TIMEOUT ==="
exit 1
fi
fi
fi
fi
fi
echo "=== REFRESH PROCESSING COMPLETE ==="
echo ""
fi
# Convert datestamp from string into milliseconds
if [ -z "$datestring" ]; then
echo "No timestamp specified, determining latest recoverable point"
ENDPOINT="https://$RUBRIK_IP/api/internal/oracle/db/$db_id/recoverable_range"
if ! rest_api_get; then
echo "ERROR: Failed to get recoverable range for database"
exit 1
fi
if [ ! -s /tmp/rbkresponse.$$ ]; then
echo "ERROR: Empty response when getting recoverable range"
exit 1
fi
datestring=$(cat /tmp/rbkresponse.$$ | jq -r '[.data[].endTime] | max' 2>/dev/null)
if [ $? -ne 0 ] || [ -z "$datestring" ] || [ "$datestring" = "null" ]; then
echo "ERROR: Could not determine latest recoverable time"
exit 1
fi
fi
echo "Requested timestamp: $datestring"
# Validate and convert timestamp
if ! ts=$(date -d "$datestring" +%s 2>/dev/null); then
echo "ERROR: Invalid timestamp format: $datestring"
echo "Expected format: YYYY-MM-DD HH:MM:SS"
exit 1
fi
((millis = ts * 1000))
echo "Recovery point (milliseconds): $millis"
configmap=""
# Build configmap from -a options
echo "Processing advanced configuration options:"
for pair in "${config_pairs[@]}"; do
if [[ "$pair" != *","* ]]; then
echo "WARNING: Skipping malformed config pair (missing comma): $pair"
continue
fi
key=$(echo "$pair" | cut -d',' -f1)
value=$(echo "$pair" | cut -d',' -f2-)
# Validate key is not empty
if [ -z "$key" ]; then
echo "WARNING: Skipping config pair with empty key: $pair"
continue
fi
echo " $key = $value"
if [ -n "$configmap" ]; then
configmap="$configmap,"
fi
# Escape quotes in the value for JSON
escaped_value=$(echo "$value" | sed 's/"/\\"/g')
configmap="$configmap\"$key\":\"$escaped_value\""
done
# Build the payload
echo "Building API payload..."
PAYLOAD="{\"recoveryPoint\":{\"timestampMs\":$millis},\"targetOracleHostOrRacId\":\"$target_id\",\"shouldRestoreFilesOnly\":false"
if [ -n "$newsid" ]; then
echo " Clone DB name: $newsid"
PAYLOAD="$PAYLOAD,\"cloneDbName\":\"$newsid\""
fi
if [ -n "$custompfile" ]; then
echo " Custom pfile path: $custompfile"
PAYLOAD="$PAYLOAD,\"customPfilePath\":\"$custompfile\""
fi
if [ -n "$num_channels" ]; then
echo " Number of channels: $num_channels"
# Validate num_channels is numeric
if ! [[ "$num_channels" =~ ^[0-9]+$ ]]; then
echo "ERROR: Number of channels must be numeric: $num_channels"
exit 1
fi
PAYLOAD="$PAYLOAD,\"numChannels\":$num_channels"
fi
# Add pdbsToClone array if -b specified
if [ ${#pdb_list[@]} -gt 0 ]; then
pdbs_json="\"PDB\$SEED\""
for pdb in "${pdb_list[@]}"; do
# Validate PDB name is not empty
if [ -n "$pdb" ]; then
pdbs_json="$pdbs_json,\"$pdb\""
fi
done
echo " Including PDBs in clone: PDB\$SEED ${pdb_list[*]}"
PAYLOAD="$PAYLOAD,\"pdbsToClone\":[$pdbs_json]"
fi
# Add advanced configuration if any
if [ -n "$configmap" ]; then
PAYLOAD="$PAYLOAD,\"advancedRecoveryConfigMap\":{$configmap}"
else
PAYLOAD="$PAYLOAD,\"advancedRecoveryConfigMap\":{}"
fi
PAYLOAD="$PAYLOAD}"
ENDPOINT="https://$RUBRIK_IP/api/internal/oracle/db/$db_id/export"
# Validate JSON payload before sending
if ! echo "$PAYLOAD" | jq empty 2>/dev/null; then
echo "ERROR: Invalid JSON payload generated"
echo "Payload: $PAYLOAD"
exit 1
fi
echo "$PAYLOAD" > /tmp/payload.$$
if [ "$dryrun" = true ]; then
echo "Dry run mode - API payload that would be sent:"
echo "$PAYLOAD" | jq .
exit 0
fi
echo "Initiating clone operation..."
if ! rest_api_post_file; then
echo "ERROR: Failed to submit clone request"
exit 1
fi
# Check if we got a valid response with a status link
if [ ! -s /tmp/rbkresponse.$$ ]; then
echo "ERROR: Empty response from clone request"
exit 1
fi
ENDPOINT=$(cat /tmp/rbkresponse.$$ | jq -r '.links[0].href' 2>/dev/null)
if [ -z "$ENDPOINT" ] || [ "$ENDPOINT" = "null" ]; then
echo "ERROR: No status link returned from clone request"
cat /tmp/rbkresponse.$$ | jq . 2>/dev/null || cat /tmp/rbkresponse.$$
exit 1
fi
echo "Monitoring clone progress..."
while true; do
if ! rest_api_get; then
echo "WARNING: Failed to check status, retrying..."
sleep 10
continue
fi
status=$(cat /tmp/rbkresponse.$$ | jq -r '.status' 2>/dev/null)
if [ -z "$status" ] || [ "$status" = "null" ]; then
echo "WARNING: Could not determine status, retrying..."
sleep 10
continue
fi
case "$status" in
"SUCCEEDED")
echo "CLONE OPERATION SUCCEEDED"
cat /tmp/rbkresponse.$$ | jq . 2>/dev/null || cat /tmp/rbkresponse.$$
exit 0
;;
"FAILED"|"CANCELED"|"UNDOING")
echo "CLONE OPERATION FAILED WITH STATUS: $status"
cat /tmp/rbkresponse.$$ | jq . 2>/dev/null || cat /tmp/rbkresponse.$$
exit 1
;;
*)
echo "Status: $status, checking again in 10 seconds..."
;;
esac
sleep 10
done