Files
llb-oracle/rsc_clone.sh

401 lines
11 KiB
Bash
Executable File

#!/bin/bash
#
# Example RSC API call script
# v0.1 - James Pattinson - August 2025
#
# Perfoms a database clone operation
#
# usage: rsc_clone.sh -n <newname> -o <optionsfile> -h <targethost> [-s sourcehost] [-t "YYYY-MM-DD HH:MM:SS"] <srcdb>
#
# Options:
# -n <newname> : db_name / SID of the new cloned database
# -o <optionsfile> : Path to the options file containing advanced cloning options
# -h <targethost> : Target host where the cloned database will be created
# -s <sourcehost> : 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
# <srcdb> : 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 <newname> -o <optionsfile> -h <targethost> [-s sourcehost] [-t "YYYY-MM-DD HH:MM:SS"] <srcdb>" 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 | test("^" + $NODE + "(\\.|$)")) | .id')
cdmId=$(cat /tmp/rbkresponse.$$ | jq -r --arg NODE "$node_name" '.data.oracleDatabases.nodes[] | select(.logicalPath[]?.name | test("^" + $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) \(.logicalPath[0].name) \(.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