#!/bin/bash ORIG_ARGS="$*" # # Perform a Clone of an Oracle DB # v1.3 - James Pattinson - October 2025 # # usage: oracle_clone.sh [options] # # Options: # -h Source DB hostname # -t Recovery point timestamp "YYYY-MM-DD HH:MM:SS" (uses latest if not specified) # -n New database SID for the clone # -p Custom pfile for the clone # -a Advanced Cloning Options (can be used multiple times) # -b Comma-separated list of PDBs to clone (include only these PDBs. PDB$SEED is always included) # -c Number of RMAN channels to use # -d Dry run mode - show API payload without executing # -l Log all API calls (endpoints, payloads, responses) to # --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] " 1>&2 echo "Options:" 1>&2 echo " -h Source DB hostname" 1>&2 echo " -t Recovery point \"YYYY-MM-DD HH:MM:SS\"" 1>&2 echo " -n New database SID for clone" 1>&2 echo " -p Custom pfile for the clone" 1>&2 echo " -a Advanced Cloning Options (can be used multiple times)" 1>&2 echo " -b Comma-separated list of PDBs to clone (include only these PDBs)" 1>&2 echo " -c Number of RMAN channels to use" 1>&2 echo " -d Dry run mode" 1>&2 echo " -l Log all API calls (endpoints, payloads, responses) to " 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