diff --git a/README.md b/README.md new file mode 100644 index 0000000..58ac24d --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# RSC Oracle Database Clone Script + +## Overview + +The `rsc_clone.sh` script performs Oracle database clone operations using the Rubrik Security Cloud (RSC) GraphQL API. It allows you to create a clone of an existing Oracle database to a target host with customizable recovery points and advanced Oracle configuration options. + +## Prerequisites + +- Rubrik Security Cloud (RSC) access with valid credentials +- Source Oracle database protected by Rubrik +- Target Oracle host configured and registered in RSC +- Required configuration files: + - `rbk_api.conf` - RSC API credentials + - Oracle cloning options file + +## Configuration + +### RSC API Configuration (`rbk_api.conf`) + +Create a configuration file with your RSC credentials: + +```bash +# Rubrik Security Cloud (RSC) Configuration +RSC_HOST=your-organization.my.rubrik.com +RSC_ID="client|your-client-id-here" +RSC_SECRET=your-secret-key-here +``` + +### Oracle Cloning Options File + +Create a text file with Oracle-specific cloning parameters. Each line should contain `KEY=VALUE` pairs for Oracle initialization parameters: + +```bash +# Example: SHED_to_SCLONE.txt +CONTROL_FILES='/u01/app/oracle/oradata/SCLONE/control01.ctl, /u01/app/oracle/fast_recovery_area/SCLONE/control02.ctl' +DB_CREATE_FILE_DEST=/u01/app/oracle/oradata/SCLONE/ +AUDIT_FILE_DEST='/u01/app/oracle/admin/SCLONE/adump' +DB_FILE_NAME_CONVERT='SHED','SCLONE' +``` + +## Usage + +```bash +./rsc_clone.sh -n -o -h [-s sourcehost] [-t "YYYY-MM-DD HH:MM:SS"] [-d] +``` + +### Required Parameters + +- `-n ` - Database name/SID for the cloned database +- `-o ` - Path to Oracle cloning options file +- `-h ` - Target host where the database will be cloned +- `` - Source database name to clone + +### Optional Parameters + +- `-s ` - Source host name (use when there are multiple databases with the same name on different hosts) +- `-t "YYYY-MM-DD HH:MM:SS"` - Recovery point timestamp (defaults to latest point-in-time) +- `-d` - Dry-run mode (shows mutation variables without executing) + +## Examples + +### Basic Clone Operation + +Clone database `SHED` to `SCLONE` on target host `pve-ora19c-3`: + +```bash +./rsc_clone.sh -n SCLONE -o SHED_to_SCLONE.txt -h pve-ora19c-3 SHED +``` + +### Dry-Run Mode + +Preview the clone operation without executing it: + +```bash +./rsc_clone.sh -n SCLONE -o SHED_to_SCLONE.txt -h pve-ora19c-3 -d SHED +``` + +**Output:** +``` +DEBUG: DB ID is b4194205-b7d6-5f0b-8360-e6f349b9fd82 +INFO: No time specified, using latest PIT +Latest PIT (ISO8601): 2025-10-15T08:32:49.000Z +Latest PIT unixtime (ms): 1760517169000 +Target Host ID is 26008fd4-9a96-582e-86b7-4da31584a7ad + +=== DRY-RUN MODE === +Would execute the following GraphQL mutation: + +QUERY: +mutation OracleDatabaseExportMutation($input: ExportOracleDatabaseInput!) { + exportOracleDatabase(input: $input) { + id + links { + href + rel + __typename + } + __typename + } +} + +VARIABLES: +{ + "input": { + "request": { + "id": "b4194205-b7d6-5f0b-8360-e6f349b9fd82", + "config": { + "targetOracleHostOrRacId": "26008fd4-9a96-582e-86b7-4da31584a7ad", + "shouldRestoreFilesOnly": false, + "recoveryPoint": { + "timestampMs": 1760517169000 + }, + "cloneDbName": "SCLONE", + "shouldAllowRenameToSource": true, + "shouldSkipDropDbInUndo": false + } + }, + "advancedRecoveryConfigMap": [ + { + "key": "CONTROL_FILES", + "value": "'/u01/app/oracle/oradata/SCLONE/control01.ctl, /u01/app/oracle/fast_recovery_area/SCLONE/control02.ctl'" + }, + { + "key": "DB_CREATE_FILE_DEST", + "value": "/u01/app/oracle/oradata/SCLONE/" + }, + { + "key": "AUDIT_FILE_DEST", + "value": "'/u01/app/oracle/admin/SCLONE/adump'" + }, + { + "key": "DB_FILE_NAME_CONVERT", + "value": "'SHED','SCLONE'" + } + ] + } +} + +=== END DRY-RUN === +``` + +### Point-in-Time Recovery + +Clone database to a specific point in time: + +```bash +./rsc_clone.sh -n SCLONE -o SHED_to_SCLONE.txt -h pve-ora19c-3 -t "2025-10-15 08:00:00" SHED +``` + +### Specify Source Host + +When multiple databases with the same name exist on different hosts: + +```bash +./rsc_clone.sh -n SCLONE -o SHED_to_SCLONE.txt -h pve-ora19c-3 -s pve-ora19c-1 SHED +``` + +## Script Workflow + +1. **Parameter Validation** - Validates required parameters and options file +2. **Database Discovery** - Locates source database in RSC +3. **Host Resolution** - Resolves target host ID in RSC +4. **Recovery Point** - Determines recovery timestamp (latest or specified) +5. **Options Processing** - Converts options file to JSON format +6. **Clone Execution** - Submits GraphQL mutation to RSC API +7. **Job Monitoring** - Tracks clone job status until completion + +## Error Handling + +The script includes comprehensive error handling for: + +- Missing required parameters +- Database not found or ambiguous matches +- Target host not found +- Invalid recovery timestamps +- RSC API errors +- Job failures + +## Dependencies + +- `rsc_ops.sh` - RSC API operations and utility functions +- `rbk_api.conf` - RSC credentials configuration +- `jq` - JSON processing +- `curl` - HTTP requests +- `date`/`gdate` - Date/time operations + +## Troubleshooting + +### Common Issues + +1. **Database not found** - Verify database name and use `-s` if multiple databases exist +2. **Host not found** - Check target host name is registered in RSC +3. **Permission errors** - Ensure RSC service account has sufficient privileges +4. **Invalid timestamp** - Use format "YYYY-MM-DD HH:MM:SS" for `-t` parameter + +### Debug Information + +The script provides debug output including: +- Source database ID +- Target host ID +- Recovery point details +- Job status updates + +Use dry-run mode (`-d`) to validate configuration before executing actual clones. + +## Version History + +- v0.1 - Initial release with basic clone functionality +- Added dry-run mode for validation +- Improved error handling and database selection logic \ No newline at end of file diff --git a/rbk_api.conf.example b/rbk_api.conf.example index f072b39..a60e39f 100644 --- a/rbk_api.conf.example +++ b/rbk_api.conf.example @@ -1,9 +1,6 @@ # Example Rubrik API Configuration File # Copy this file to rbk_api.conf and fill in your actual values -# IP Address (or DNS name) of Rubrik CDM -RUBRIK_IP=your.rubrik.cluster.ip - # Rubrik Security Cloud (RSC) Configuration RSC_HOST=your-organization.my.rubrik.com RSC_ID="client|your-client-id-here" diff --git a/rsc_clone.sh b/rsc_clone.sh index 515067b..0fdc4a0 100755 --- a/rsc_clone.sh +++ b/rsc_clone.sh @@ -5,7 +5,7 @@ # # Perfoms a database clone operation # -# usage: rsc_clone.sh -n -o -h [-s sourcehost] [-t "YYYY-MM-DD HH:MM:SS"] +# usage: rsc_clone.sh -n -o -h [-s sourcehost] [-t "YYYY-MM-DD HH:MM:SS"] [-d] # # Options: # -n : db_name / SID of the new cloned database @@ -13,6 +13,7 @@ # -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 +# -d : Dry-run mode - show mutation variables without executing the clone # : Source database name or RSC dbid (if known, can be used directly) # # Example options file content: @@ -22,14 +23,13 @@ # 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; } +usage() { echo "Usage: $0 -n -o -h [-s sourcehost] [-t \"YYYY-MM-DD HH:MM:SS\"] [-d] " 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 +while getopts "n:o:t:h:s:d" o; do case "${o}" in n) newName=${OPTARG} @@ -46,6 +46,9 @@ while getopts "n:o:t:h:s:" o; do s) node_name=${OPTARG} ;; + d) + dryrun=true + ;; *) usage ;; @@ -258,15 +261,30 @@ oracleDatabases(filter: $filter) { 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 + if [[ -n "$node_name" ]]; then + # Filter by source host if specified + 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 + else + # No source host specified, get all matching databases + dbid=$(cat /tmp/rbkresponse.$$ | jq -r '.data.oracleDatabases.nodes[].id') + cdmId=$(cat /tmp/rbkresponse.$$ | jq -r '.data.oracleDatabases.nodes[].cluster.id') + + dbid_count=$(echo "$dbid" | grep -c .) + if [[ "$dbid_count" -ne 1 || -z "$dbid" ]]; then + echo "ERROR: Expected exactly one database with name '$1', found $dbid_count:" + cat /tmp/rbkresponse.$$ | jq -r '.data.oracleDatabases.nodes[] | "\(.dbUniqueName) \(.logicalPath[0].name) \(.id)"' + cleanup + exit 4 + fi fi echo "DEBUG: DB ID is $dbid" @@ -334,6 +352,22 @@ gqlClone='mutation OracleDatabaseExportMutation($input: ExportOracleDatabaseInpu gqlQuery="$(echo $gqlClone)" gqlVars="$(echo $variables)" + +if [ "$dryrun" = true ]; then + echo "=== DRY-RUN MODE ===" + echo "Would execute the following GraphQL mutation:" + echo + echo "QUERY:" + echo "$gqlQuery" + echo + echo "VARIABLES:" + echo "$gqlVars" | jq . + echo + echo "=== END DRY-RUN ===" + cleanup + exit 0 +fi + rsc_gql_query cat /tmp/rbkresponse.$$ | jq diff --git a/rsc_host_refresh.sh b/rsc_host_refresh.sh index cc6750a..4ab8553 100755 --- a/rsc_host_refresh.sh +++ b/rsc_host_refresh.sh @@ -18,7 +18,6 @@ fi MYDIR="$(dirname "$(realpath "$0")")" -source $MYDIR/oracle_funcs.sh source $MYDIR/rsc_ops.sh RBK_HOST=$1 diff --git a/rsc_list_oracle_slas.sh b/rsc_list_oracle_slas.sh index cd021db..b4e4814 100755 --- a/rsc_list_oracle_slas.sh +++ b/rsc_list_oracle_slas.sh @@ -11,7 +11,6 @@ usage() { echo "Usage: $0 [filter]" 1>&2; exit 1; } MYDIR="$(dirname "$(realpath "$0")")" # source $MYDIR/rbk_api.conf -source $MYDIR/oracle_funcs.sh source $MYDIR/rsc_ops.sh gql_SLAListQuery='query SLAListQuery($after: String, $first: Int, $filter: [GlobalSlaFilterInput!], $sortBy: SlaQuerySortByField, $sortOrder: SortOrder) { diff --git a/rsc_log_backup.sh b/rsc_log_backup.sh index c85ced1..c397813 100755 --- a/rsc_log_backup.sh +++ b/rsc_log_backup.sh @@ -16,7 +16,6 @@ fi MYDIR="$(dirname "$(realpath "$0")")" # source $MYDIR/rbk_api.conf -source $MYDIR/oracle_funcs.sh source $MYDIR/rsc_ops.sh gql_DBListQuery='query OracleDatabases($filter: [Filter!]) { diff --git a/rsc_ops.sh b/rsc_ops.sh index dce6153..c981ff6 100755 --- a/rsc_ops.sh +++ b/rsc_ops.sh @@ -8,7 +8,35 @@ #-------------------------------------------------------------------------------------------------------- MYDIR="$(dirname "$(realpath "$0")")" -#source $MYDIR/rbk_api.conf +source $MYDIR/rbk_api.conf + +# Set DATE command based on OS +if [[ "$OSTYPE" == "darwin"* ]]; then + DATE=gdate +else + DATE=date +fi + +# Utility functions +exit_with_error () { + rm -f /tmp/rbkresponse.$$ + echo Aborting Script! + exit 1 +} + +cleanup () { + rm -f /tmp/mountedDBs.$$ + rm -f /tmp/rbkresponse.$$ + rm -f /tmp/payload.$$ +} + +check_http_error () { + # All good responses start with a 2 + if [ ${http_response:0:1} != "2" ]; then + echo FATAL: HTTP error from API call: $http_response. The server responded with: + cat /tmp/rbkresponse.$$ ; echo ; exit_with_error + fi +} check_get_rsc_token () {