From 5867e7029f21f7f24573f79d689ee3460c2861b5 Mon Sep 17 00:00:00 2001 From: James Pattinson Date: Tue, 25 Nov 2025 10:51:26 -0500 Subject: [PATCH] Getting there --- clone_oracle_database.py | 0 introspect_schema.py | 0 list_oracle_databases.py | 12 ++-- ...le_live_mounts.py => list_oracle_mounts.py | 8 ++- ...b_snapshots.py => list_oracle_snapshots.py | 19 ++++-- mount_oracle_filesonly.py | 29 ++++++--- rsc.py | 61 +++++++++++++++++++ 7 files changed, 108 insertions(+), 21 deletions(-) mode change 100644 => 100755 clone_oracle_database.py mode change 100644 => 100755 introspect_schema.py mode change 100644 => 100755 list_oracle_databases.py rename list_oracle_live_mounts.py => list_oracle_mounts.py (92%) rename list_db_snapshots.py => list_oracle_snapshots.py (93%) mode change 100644 => 100755 mode change 100644 => 100755 mount_oracle_filesonly.py diff --git a/clone_oracle_database.py b/clone_oracle_database.py old mode 100644 new mode 100755 diff --git a/introspect_schema.py b/introspect_schema.py old mode 100644 new mode 100755 diff --git a/list_oracle_databases.py b/list_oracle_databases.py old mode 100644 new mode 100755 index e643ec4..22f9ba7 --- a/list_oracle_databases.py +++ b/list_oracle_databases.py @@ -20,6 +20,7 @@ def list_oracle_databases(): nodes { dbUniqueName id + isRelic cluster { id name @@ -34,13 +35,9 @@ def list_oracle_databases(): } """ - # Variables: exclude relics and replicated databases + # Variables: exclude replicated databases only variables = { "filter": [ - { - "texts": ["false"], - "field": "IS_RELIC" - }, { "texts": ["false"], "field": "IS_REPLICATED" @@ -71,11 +68,12 @@ def list_oracle_databases(): db['dbUniqueName'], db['id'], cluster_name, - host_name + host_name, + 'Yes' if db['isRelic'] else 'No' ]) # Print tabulated output - headers = ['Database Name', 'ID', 'Cluster', 'Host'] + headers = ['Database Name', 'ID', 'Cluster', 'Host', 'Relic'] print(tabulate(table_data, headers=headers, tablefmt='grid')) if __name__ == "__main__": diff --git a/list_oracle_live_mounts.py b/list_oracle_mounts.py similarity index 92% rename from list_oracle_live_mounts.py rename to list_oracle_mounts.py index 8879ccc..9638184 100755 --- a/list_oracle_live_mounts.py +++ b/list_oracle_mounts.py @@ -120,7 +120,7 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description='List Oracle Live Mounts') parser.add_argument('--name', help='Filter by name (mounted database name)') parser.add_argument('--cluster-uuid', help='Filter by cluster UUID') - parser.add_argument('--source-db-id', help='Filter by source database ID') + parser.add_argument('--source-db-id', help='Filter by source database ID (optional, defaults to local database)') parser.add_argument('--org-id', help='Filter by organization ID') parser.add_argument('--sort-field', choices=['NAME', 'CREATION_DATE', 'STATUS'], help='Sort field') parser.add_argument('--sort-order', choices=['ASC', 'DESC'], default='ASC', help='Sort order') @@ -128,6 +128,12 @@ if __name__ == "__main__": args = parser.parse_args() + if not args.source_db_id and not (args.name or args.cluster_uuid or args.org_id): + auth = RSCAuth() + gql = RSCGraphQL(auth) + args.source_db_id = gql.get_local_database_id() + print(f"INFO: No filters specified, using local database ID: {args.source_db_id}") + try: list_oracle_live_mounts( name=args.name, diff --git a/list_db_snapshots.py b/list_oracle_snapshots.py old mode 100644 new mode 100755 similarity index 93% rename from list_db_snapshots.py rename to list_oracle_snapshots.py index 75f2cca..39f4874 --- a/list_db_snapshots.py +++ b/list_oracle_snapshots.py @@ -86,7 +86,7 @@ def find_database_by_name_or_id(identifier): def format_timestamp(timestamp): """Format ISO timestamp to readable format""" try: - dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) + dt = datetime.strptime(timestamp.replace('Z', '+0000'), '%Y-%m-%dT%H:%M:%S.%f%z') return dt.strftime('%Y-%m-%d %H:%M:%S UTC') except: return timestamp @@ -217,7 +217,7 @@ def list_snapshots(identifier): format_timestamp(snap['date']), 'On-Demand' if snap['isOnDemandSnapshot'] else 'Policy', snap['slaDomain']['name'] if snap['slaDomain'] else 'None', - 'Local + Replica' if snap['snapshotRetentionInfo']['replicationInfos'] else '✅ Local only' + 'Local + Replica' if snap['snapshotRetentionInfo']['replicationInfos'] else 'Local only' ]) headers = ['Snapshot ID', 'Date', 'Type', 'SLA Domain', 'Location Status'] @@ -245,14 +245,23 @@ def list_snapshots(identifier): sys.exit(1) def main(): - if len(sys.argv) != 2: - print("Usage: python list_db_snapshots.py ") + if len(sys.argv) == 2: + identifier = sys.argv[1] + elif len(sys.argv) == 1: + # No argument, find local database + auth = RSCAuth() + gql = RSCGraphQL(auth) + identifier = gql.get_local_database_id() + print(f"INFO: Using local database ID: {identifier}") + else: + print("Usage: python list_db_snapshots.py []") + print("If no database is specified, uses the local database.") print("Examples:") print(" python list_db_snapshots.py SCLONE") print(" python list_db_snapshots.py 2cb7e201-9da0-53f2-8c69-8fc21f82e0d2") + print(" python list_db_snapshots.py") sys.exit(1) - identifier = sys.argv[1] list_snapshots(identifier) if __name__ == "__main__": diff --git a/mount_oracle_filesonly.py b/mount_oracle_filesonly.py old mode 100644 new mode 100755 index eb2d3e7..f528bfb --- a/mount_oracle_filesonly.py +++ b/mount_oracle_filesonly.py @@ -5,6 +5,7 @@ import sys import os import argparse import time +import socket from datetime import datetime from rsc import RSCAuth, RSCGraphQL @@ -58,7 +59,6 @@ def find_database_by_name_or_id(identifier): variables = { "filter": [ {"texts": [identifier], "field": "NAME_EXACT_MATCH"}, - {"texts": ["false"], "field": "IS_RELIC"}, {"texts": ["false"], "field": "IS_REPLICATED"} ] } @@ -160,7 +160,7 @@ def get_latest_pit(db_id): 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')) + dt = datetime.strptime(latest_endtime.replace('Z', '+0000'), '%Y-%m-%dT%H:%M:%S.%f%z') unixtime_ms = int(dt.timestamp() * 1000) print(f"INFO: Latest PIT unixtime (ms): {unixtime_ms}") @@ -304,26 +304,39 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: + python mount_oracle_filesonly.py --mountpath /tmp/mount + python mount_oracle_filesonly.py --mountpath /tmp/mount SHED 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("--targethost", required=False, + help="Target host where the files will be mounted (defaults to local hostname)") 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") + parser.add_argument("srcdb", nargs='?', + help="Source database name or RSC database ID (optional, defaults to local database)") args = parser.parse_args() + if not args.targethost: + args.targethost = socket.gethostname() + print(f"INFO: No target host specified, defaulting to local hostname: {args.targethost}") + try: # Find the source database - print(f"INFO: Finding source database: {args.srcdb}") - db = find_database_by_name_or_id(args.srcdb) + if args.srcdb: + print(f"INFO: Finding source database: {args.srcdb}") + db = find_database_by_name_or_id(args.srcdb) + else: + print("INFO: No source database specified, finding local database") + auth = RSCAuth() + gql = RSCGraphQL(auth) + db_id = gql.get_local_database_id() + db = find_database_by_name_or_id(db_id) print(f"INFO: Found database: {db['dbUniqueName']} (ID: {db['id']})") print(f"INFO: Cluster: {db['cluster']['name']} (ID: {db['cluster']['id']})") diff --git a/rsc.py b/rsc.py index 09505fd..23191d0 100644 --- a/rsc.py +++ b/rsc.py @@ -2,6 +2,7 @@ import json import os import time import requests +import socket class RSCAuth: def __init__(self, config_file='rsc.json'): @@ -102,6 +103,66 @@ class RSCGraphQL: return data + def get_local_database_id(self): + """Get the ID of the local database on this host, preferring one protected by SLA""" + hostname = socket.gethostname() + + query = """ + query OracleDatabases($filter: [Filter!]) { + oracleDatabases(filter: $filter) { + nodes { + id + dbUniqueName + isRelic + effectiveSlaDomain { + id + name + } + cluster { + id + name + } + logicalPath { + fid + name + objectType + } + } + } + } + """ + + variables = { + "filter": [ + {"texts": ["false"], "field": "IS_REPLICATED"} + ] + } + + response = self.query(query, variables) + all_dbs = response['data']['oracleDatabases']['nodes'] + + # Filter databases on this host + dbs = [db for db in all_dbs if db['logicalPath'] and db['logicalPath'][0]['name'] == hostname] + + if not dbs: + raise ValueError(f"No databases found on host {hostname}") + + # Filter databases with SLA protection + protected_dbs = [db for db in dbs if db.get('effectiveSlaDomain')] + + if protected_dbs: + if len(protected_dbs) == 1: + return protected_dbs[0]['id'] + else: + # Multiple protected, use the first one with a warning + print(f"WARN: Multiple protected databases on {hostname}, using {protected_dbs[0]['dbUniqueName']}") + return protected_dbs[0]['id'] + else: + if len(dbs) == 1: + return dbs[0]['id'] + else: + raise ValueError(f"Multiple databases on {hostname}, none protected by SLA") + def introspect_schema(self): """Introspect the GraphQL schema to get type information""" introspection_query = """