401 lines
11 KiB
Bash
Executable File
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
|
|
|