first commit
This commit is contained in:
400
rsc_clone.sh
Executable file
400
rsc_clone.sh
Executable file
@@ -0,0 +1,400 @@
|
||||
#!/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 == $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
|
||||
|
||||
Reference in New Issue
Block a user