#!/bin/bash # # Example RSC API call script # v0.1 - James Pattinson - August 2025 # # Perfoms a database clone operation # # usage: rsc_clone.sh -n -o -h [-s sourcehost] [-t "YYYY-MM-DD HH:MM:SS"] # # Options: # -n : db_name / SID of the new cloned database # -o : Path to the options file containing advanced cloning options # -h : Target host where the cloned database will be created # -s : Source host where the original database is located (optional, use when there is ambiguity) # -t "YYYY-MM-DD HH:MM:SS" : Optional timestamp for the recovery point, defaults to latest PIT # : Source database name or RSC dbid (if known, can be used directly) # # Example options file content: # CONTROL_FILES='/u01/app/oracle/oradata/NEWNAME/control01.ctl, /u01/app/oracle/fast_recovery_area/NEWNAME/control02.ctl' # DB_FILE_NAME_CONVERT='OLDNAME','NEWNAME' # DB_CREATE_FILE_DEST=/u01/app/oracle/oradata/NEWNAME/ # AUDIT_FILE_DEST='/u01/app/oracle/admin/NEWNAME/adump' usage() { echo "Usage: $0 -n -o -h [-s sourcehost] [-t "YYYY-MM-DD HH:MM:SS"] " 1>&2; exit 1; } MYDIR="$(dirname "$(realpath "$0")")" source $MYDIR/oracle_funcs.sh source $MYDIR/rsc_ops.sh while getopts "n:o:t:h:s:" o; do case "${o}" in n) newName=${OPTARG} ;; t) datestring=${OPTARG} ;; o) optionsFile=${OPTARG} ;; h) targetHost=${OPTARG} ;; s) node_name=${OPTARG} ;; *) usage ;; esac done shift $((OPTIND-1)) # Check if required options are set if [[ -z "$1" || -z "$newName" || -z "$targetHost" || -z "$optionsFile" ]]; then usage fi # Check if optionsFile exists if [[ ! -f "$optionsFile" ]]; then echo "ERROR: Options file '$optionsFile' does not exist." exit_with_error fi template_to_json() { local input_file="${1}" local first=1 echo "[" # Use tr to remove CR characters, then process lines while IFS= read -r line; do # Remove any CR characters and then check for empty lines line=$(echo "$line" | tr -d '\r') # Ignore empty lines and lines starting with # [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue key="${line%%=*}" value="${line#*=}" # Trim whitespace from key only key="$(echo -n "$key" | xargs)" # Remove leading/trailing whitespace but preserve quotes value="$(echo -n "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" # Handle value escaping if [[ $value =~ ^\'.*\'$ ]]; then # Keep the single quotes in the value value_escaped=$(printf '%s' "$value" | sed 's/"/\\"/g') else # If no surrounding quotes, then trim and remove any single quotes value="$(echo -n "$value" | xargs)" value_escaped=$(printf '%s' "$value" | sed "s/'//g" | sed 's/"/\\"/g') fi if [[ $first -eq 0 ]]; then echo "," fi echo -n " { \"key\": \"${key}\", \"value\": \"${value_escaped}\" }" first=0 done < <(tr -d '\r' < "$input_file") echo echo "]" } get_latest_pit() { gql_getRR='query OracleDatabaseRecoverableRangesQuery($fid: String!) { oracleRecoverableRanges( input: {id: $fid, shouldIncludeDbSnapshotSummaries: false} ) { data { beginTime endTime __typename } __typename } oracleMissedRecoverableRanges(input: {id: $fid}) { data { beginTime endTime __typename } __typename } }' variables="{ \"fid\": \"$dbid\" }" gqlQuery="$(echo $gql_getRR)" gqlVars="$(echo $variables)" rsc_gql_query # Get latest endTime latest_endtime=$(cat /tmp/rbkresponse.$$ | jq -r '.data.oracleRecoverableRanges.data[] | .endTime' | sort -r | head -n 1) echo "Latest PIT (ISO8601): $latest_endtime" # Convert to unixtime in milliseconds latest_unixtime_ms=$(date -d "$latest_endtime" +%s 2>/dev/null) if [[ -z "$latest_unixtime_ms" ]]; then # Try with gdate (macOS) latest_unixtime_ms=$(gdate -d "$latest_endtime" +%s 2>/dev/null) fi if [[ -z "$latest_unixtime_ms" ]]; then echo "ERROR: Unable to convert $latest_endtime to unixtime" exit 5 fi latest_unixtime_ms=$((latest_unixtime_ms * 1000)) echo "Latest PIT unixtime (ms): $latest_unixtime_ms" export latest_unixtime_ms } get_oracle_host_id() { gql_list_targets='query ExampleQuery($filter: [Filter!]) { oracleTopLevelDescendants(filter: $filter) { nodes { name id } } }' variables="{ \"filter\": [ { \"texts\": [\"$1\"], \"field\": \"NAME\" }, { \"texts\": [\"$cdmId\"], \"field\": \"CLUSTER_ID\" } ] }" gqlQuery="$(echo $gql_list_targets)" gqlVars="$(echo $variables)" rsc_gql_query # Get all matching host IDs (portable, no mapfile) host_ids=$(cat /tmp/rbkresponse.$$ | jq -r '.data.oracleTopLevelDescendants.nodes[] | .id') host_count=$(echo "$host_ids" | grep -c .) if [[ $host_count -ne 1 ]]; then echo "ERROR: Multiple hosts found for '$1':" cat /tmp/rbkresponse.$$ | jq -r '.data.oracleTopLevelDescendants.nodes[] | "\(.name) \(.id)"' exit_with_error fi # Set the first match (or empty if none) targetHostId=$(echo "$host_ids" | head -n 1) } # If $1 looks like a dbid (contains hyphens), use it directly and skip DB lookup if [[ "$1" == *-* ]]; then dbid="$1" echo "INFO: Using provided dbid: $dbid" gql_lookupCdmId='query OracleDatabase($fid: UUID!) { oracleDatabase(fid: $fid) { cluster { id } } }' variables="{ \"fid\": \"$dbid\" }" gqlQuery="$(echo $gql_lookupCdmId)" gqlVars="$(echo $variables)" rsc_gql_query cdmId=$(cat /tmp/rbkresponse.$$ | jq -r '.data.oracleDatabase.cluster.id') if [[ -z "$cdmId" ]]; then echo "ERROR: Could not find CDM ID for dbid '$dbid'" exit 1 fi echo "CDM ID is $cdmId" else gql_DBListQuery='query OracleDatabases($filter: [Filter!]) { oracleDatabases(filter: $filter) { nodes { dbUniqueName id cluster { id } logicalPath { fid name objectType } } } }' variables="{ \"filter\": [ { \"texts\": [\"$1\"], \"field\": \"NAME_EXACT_MATCH\" }, { \"texts\": [\"false\"], \"field\": \"IS_RELIC\" }, { \"texts\": [\"false\"], \"field\": \"IS_REPLICATED\" } ] }" gqlQuery="$(echo $gql_DBListQuery)" gqlVars="$(echo $variables)" rsc_gql_query dbid=$(cat /tmp/rbkresponse.$$ | jq -r --arg NODE "$node_name" '.data.oracleDatabases.nodes[] | select(.logicalPath[]?.name == $NODE) | .id') cdmId=$(cat /tmp/rbkresponse.$$ | jq -r --arg NODE "$node_name" '.data.oracleDatabases.nodes[] | select(.logicalPath[]?.name == $NODE) | .cluster.id') dbid_count=$(echo "$dbid" | grep -c .) if [[ "$dbid_count" -ne 1 || -z "$dbid" ]]; then echo "ERROR: Expected exactly one database running on node '$node_name', found $dbid_count:" cat /tmp/rbkresponse.$$ | jq -r '.data.oracleDatabases.nodes[] | "\(.dbUniqueName) \(.id)"' cleanup exit 4 fi echo "DEBUG: DB ID is $dbid" fi # Only run UTC conversion if -t was used if [[ -n "${datestring:-}" ]]; then utctime=$($DATE -d"$datestring" +"%Y-%m-%d %H:%M:%S") if [ $? -ne 0 ]; then echo ERROR: Unable to convert supplied timestamp to UTC time exit_with_error fi unixtime=$($DATE -d"$datestring" +%s) unixtime_ms=$((unixtime * 1000)) echo INFO: Requested time is $datestring which is $utctime in UTC, unixtime is $unixtime else echo INFO: No time specified, using latest PIT get_latest_pit unixtime_ms=$latest_unixtime_ms fi # Call the function and capture the output get_oracle_host_id "$targetHost" if [[ -z "$targetHostId" ]]; then echo "ERROR: Could not resolve target host ID for '$targetHost'" exit_with_error fi echo Target Host ID is $targetHostId cloningOptions=$(template_to_json $optionsFile) variables=" { \"input\": { \"request\": { \"id\": \"$dbid\", \"config\": { \"targetOracleHostOrRacId\": \"$targetHostId\", \"shouldRestoreFilesOnly\": false, \"recoveryPoint\": { \"timestampMs\": $unixtime_ms }, \"cloneDbName\": \"$newName\", \"shouldAllowRenameToSource\": true, \"shouldSkipDropDbInUndo\": false } }, \"advancedRecoveryConfigMap\": $cloningOptions } }" gqlClone='mutation OracleDatabaseExportMutation($input: ExportOracleDatabaseInput!) { exportOracleDatabase(input: $input) { id links { href rel __typename } __typename } }' gqlQuery="$(echo $gqlClone)" gqlVars="$(echo $variables)" rsc_gql_query cat /tmp/rbkresponse.$$ | jq # Save the id from the response job_id=$(cat /tmp/rbkresponse.$$ | jq -r '.data.exportOracleDatabase.id') echo "DEBUG: Job id is $job_id" gqlCheckStatus='query OracleDatabaseAsyncRequestDetails($input: GetOracleAsyncRequestStatusInput!) { oracleDatabaseAsyncRequestDetails(input: $input) { id nodeId status startTime endTime progress error { message } } }' variables="{ \"input\": { \"id\": \"$job_id\", \"clusterUuid\": \"$cdmId\" } }" gqlQuery="$(echo $gqlCheckStatus)" gqlVars="$(echo $variables)" while true; do rsc_gql_query status=$(cat /tmp/rbkresponse.$$ | jq -r '.data.oracleDatabaseAsyncRequestDetails.status') progress=$(cat /tmp/rbkresponse.$$ | jq -r '.data.oracleDatabaseAsyncRequestDetails.progress') echo "Job status: $status $progress percent" if [[ "$status" == "FAILED" ]]; then echo "Database clone FAILED" cat /tmp/rbkresponse.$$ | jq cleanup exit 2 elif [[ "$status" == "CANCELLED" ]]; then echo "Database clone CANCELLED" exit 3 elif [[ "$status" == "SUCCEEDED" ]]; then echo "Database clone SUCCEEDED" cat /tmp/rbkresponse.$$ | jq cleanup exit 0 fi sleep 15 done echo "Database clone SUCCEEDED" cat /tmp/rbkresponse.$$ | jq cleanup exit 0 fi sleep 15 done