#!/usr/bin/env python3 import json import sys import os import argparse import time from datetime import datetime from rsc import RSCAuth, RSCGraphQL def find_database_by_name_or_id(identifier): """Find database by name or ID and return its details""" auth = RSCAuth() gql = RSCGraphQL(auth) # Check if identifier looks like a UUID (contains hyphens) if '-' in identifier: # It's likely a database ID query = """ query OracleDatabase($fid: UUID!) { oracleDatabase(fid: $fid) { dbUniqueName id cluster { id name } logicalPath { fid name objectType } } } """ variables = {"fid": identifier} else: # It's a database name query = """ query OracleDatabases($filter: [Filter!]) { oracleDatabases(filter: $filter) { nodes { dbUniqueName id cluster { id name } logicalPath { fid name objectType } } } } """ variables = { "filter": [ {"texts": [identifier], "field": "NAME_EXACT_MATCH"}, {"texts": ["false"], "field": "IS_RELIC"}, {"texts": ["false"], "field": "IS_REPLICATED"} ] } response = gql.query(query, variables) if '-' in identifier: # Direct ID lookup db = response['data']['oracleDatabase'] if not db: raise ValueError(f"Database with ID '{identifier}' not found") return db else: # Name lookup databases = response['data']['oracleDatabases']['nodes'] if not databases: raise ValueError(f"No databases found with name '{identifier}'") if len(databases) > 1: print(f"Multiple databases found with name '{identifier}':") for db in databases: host_name = db['logicalPath'][0]['name'] if db['logicalPath'] else 'Unknown' print(f" - {db['dbUniqueName']} (ID: {db['id']}, Host: {host_name})") raise ValueError("Please specify the database ID instead") return databases[0] def get_oracle_host_id(host_name, cluster_id): """Get Oracle host ID by name and cluster""" auth = RSCAuth() gql = RSCGraphQL(auth) query = """ query OracleHosts($filter: [Filter!]) { oracleTopLevelDescendants(filter: $filter) { nodes { name id } } } """ variables = { "filter": [ {"texts": [host_name], "field": "NAME"}, {"texts": [cluster_id], "field": "CLUSTER_ID"} ] } response = gql.query(query, variables) hosts = response['data']['oracleTopLevelDescendants']['nodes'] if not hosts: raise ValueError(f"Host '{host_name}' not found in cluster") if len(hosts) > 1: print(f"WARN: Multiple hosts found for '{host_name}':", file=sys.stderr) for host in hosts: print(f" - {host['name']} (ID: {host['id']})", file=sys.stderr) # Use the first one print(f"WARN: Using first match: {hosts[0]['name']}", file=sys.stderr) return hosts[0]['id'] def get_latest_pit(db_id): """Get the latest Point in Time from recoverable ranges""" auth = RSCAuth() gql = RSCGraphQL(auth) query = """ 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": db_id} response = gql.query(query, variables) # Get latest endTime from recoverable ranges ranges = response['data']['oracleRecoverableRanges']['data'] if ranges: latest_endtime = max(range_item['endTime'] for range_item in ranges) print(f"INFO: Latest PIT (ISO8601): {latest_endtime}") # Convert to datetime and then to milliseconds since epoch dt = datetime.fromisoformat(latest_endtime.replace('Z', '+00:00')) unixtime_ms = int(dt.timestamp() * 1000) print(f"INFO: Latest PIT unixtime (ms): {unixtime_ms}") return unixtime_ms else: raise ValueError("No recoverable ranges found for database") """Get Oracle host ID by name and cluster""" auth = RSCAuth() gql = RSCGraphQL(auth) query = """ query OracleHosts($filter: [Filter!]) { oracleTopLevelDescendants(filter: $filter) { nodes { name id } } } """ variables = { "filter": [ {"texts": [host_name], "field": "NAME"}, {"texts": [cluster_id], "field": "CLUSTER_ID"} ] } response = gql.query(query, variables) hosts = response['data']['oracleTopLevelDescendants']['nodes'] if not hosts: raise ValueError(f"Host '{host_name}' not found in cluster") if len(hosts) > 1: print(f"WARN: Multiple hosts found for '{host_name}':", file=sys.stderr) for host in hosts: print(f" - {host['name']} (ID: {host['id']})", file=sys.stderr) # Use the first one print(f"WARN: Using first match: {hosts[0]['name']}", file=sys.stderr) return hosts[0]['id'] def mount_files_only(db_id, target_host_id, recovery_timestamp_ms, target_mount_path): """Execute files-only mount operation""" auth = RSCAuth() gql = RSCGraphQL(auth) variables = { "input": { "request": { "config": { "targetOracleHostOrRacId": target_host_id, "shouldMountFilesOnly": True, "recoveryPoint": { "timestampMs": recovery_timestamp_ms, "scn": None }, "targetMountPath": target_mount_path, "shouldAllowRenameToSource": True, "shouldSkipDropDbInUndo": False }, "id": db_id }, "advancedRecoveryConfigMap": [] } } query = """ mutation OracleDatabaseMountMutation($input: MountOracleDatabaseInput!) { mountOracleDatabase(input: $input) { id links { href rel __typename } __typename } } """ response = gql.query(query, variables) return response['data']['mountOracleDatabase']['id'] def monitor_job_status(job_id, cluster_id): """Monitor the mount job status until completion""" auth = RSCAuth() gql = RSCGraphQL(auth) query = """ query OracleDatabaseAsyncRequestDetails($input: GetOracleAsyncRequestStatusInput!) { oracleDatabaseAsyncRequestDetails(input: $input) { id nodeId status startTime endTime progress error { message } } } """ variables = { "input": { "id": job_id, "clusterUuid": cluster_id } } while True: response = gql.query(query, variables) details = response['data']['oracleDatabaseAsyncRequestDetails'] status = details['status'] progress = details.get('progress', 0) print(f"INFO: Job status: {status} ({progress}%)") if status == "FAILED": error_msg = details.get('error', {}).get('message', 'Unknown error') print(f"ERROR: Files-only mount FAILED: {error_msg}", file=sys.stderr) print(json.dumps(response, indent=2)) sys.exit(2) elif status == "CANCELLED": print("WARN: Files-only mount CANCELLED") sys.exit(3) elif status == "SUCCEEDED": print("INFO: Files-only mount SUCCEEDED") print(json.dumps(response, indent=2)) return time.sleep(15) def main(): parser = argparse.ArgumentParser( description="Mount Oracle database files-only using Rubrik Security Cloud", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python mount_oracle_filesonly.py --targethost target-host --mountpath /tmp/mount SHED python mount_oracle_filesonly.py --targethost target-host --mountpath /tmp/mount --timestamp "2025-11-25 12:00:00" SHED """ ) parser.add_argument("--targethost", required=True, help="Target host where the files will be mounted") parser.add_argument("--mountpath", required=True, help="Target mount path for the files") parser.add_argument("--timestamp", help="Optional timestamp for the recovery point in format 'YYYY-MM-DD HH:MM:SS'") parser.add_argument("srcdb", help="Source database name or RSC database ID") args = parser.parse_args() try: # Find the source database print(f"INFO: Finding source database: {args.srcdb}") db = find_database_by_name_or_id(args.srcdb) print(f"INFO: Found database: {db['dbUniqueName']} (ID: {db['id']})") print(f"INFO: Cluster: {db['cluster']['name']} (ID: {db['cluster']['id']})") # Get recovery timestamp if args.timestamp: print(f"INFO: Using specified timestamp: {args.timestamp}") try: dt = datetime.strptime(args.timestamp, '%Y-%m-%d %H:%M:%S') recovery_timestamp_ms = int(dt.timestamp() * 1000) print(f"INFO: Recovery timestamp: {recovery_timestamp_ms} ms") except ValueError as e: print(f"ERROR: Invalid timestamp format. Use 'YYYY-MM-DD HH:MM:SS': {e}", file=sys.stderr) sys.exit(1) else: print("INFO: No timestamp specified, using latest PIT") recovery_timestamp_ms = get_latest_pit(db['id']) # Get target host ID print(f"INFO: Resolving target host: {args.targethost}") target_host_id = get_oracle_host_id(args.targethost, db['cluster']['id']) print(f"INFO: Target host ID: {target_host_id}") # Execute the files-only mount print(f"INFO: Starting files-only mount to path '{args.mountpath}'") job_id = mount_files_only( db['id'], target_host_id, recovery_timestamp_ms, args.mountpath ) print(f"INFO: Mount job started with ID: {job_id}") # Monitor the job monitor_job_status(job_id, db['cluster']['id']) except Exception as e: print(f"ERROR: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()