Bela specifics
This commit is contained in:
368
mount_oracle_filesonly.py
Normal file
368
mount_oracle_filesonly.py
Normal file
@@ -0,0 +1,368 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user