????
Current Path : /proc/self/root/usr/lib/Acronis/PyShell/site-tools/ |
Current File : //proc/self/root/usr/lib/Acronis/PyShell/site-tools/cleanup_backups.py |
# Script allows to cleanup target machine's DML DB from foreign archives and backups # # Requirements: # - Installed Acronis Cyber Protection agent # - Running of the script requires admin rights # # Notes: # By default script # - Will be running in report mode, to display statistics related to archives and backups that are bounded to a machine # - Uses agent's DML database, so script will try to stop MMS service first, apply required actions, then start MMS # override path with --db-path option (or -d), in that case MMS service will not be touched # - Uses machine ID's from registry, override it with --machine-id option (or -m) # - Cleanup mode is turned off, override it with --cleanup option (or -c) # To run script in report mode for current machine # "C:\Program Files\Acronis\PyShell\python.exe" cleanup_backups.py # To run script in cleanup mode for current machine # "C:\Program Files\Acronis\PyShell\python.exe" cleanup_backups.py -c # To run script in cleanup mode for current machine and for specified location only # "C:\Program Files\Acronis\PyShell\python.exe" cleanup_backups.py -c -l 8F44EB8E-E15E-4B3E-BBC4-40924F7EE303 # To run script in report mode for a specific machine # "C:\Program Files\Acronis\PyShell\python.exe" cleanup_backups.py -m D6E857EF-6781-4AB2-97A5-AD20A15D3E27 # To run script in cleanup mode for a specific machine (all foreign archives and backups will be removed except specified machine) # "C:\Program Files\Acronis\PyShell\python.exe" cleanup_backups.py -c -m D6E857EF-6781-4AB2-97A5-AD20A15D3E27 # To run script in cleanup mode for custom DB path # "C:\Program Files\Acronis\PyShell\python.exe" cleanup_backups.py -c -d D:\TTASK-50248\var\lib\Acronis\BackupAndRecovery\MMSData\DML\F4CEEE47-042C-4828-95A0-DE44EC267A28.db3 # If everything is good then script should be finished with output "Successfully finished." import acrort import argparse import os import platform import re import subprocess import sys import time OS_WINDOWS = 'Windows' OS_LINUX = 'Linux' OS_MAC = 'Darwin' CHUNK_SIZE = 250 data = { OS_WINDOWS: { 'product_path': lambda x: get_windows_product_installation_path(), 'start_mms_args': ['sc', 'start', 'mms'], 'stop_mms_args': ['sc', 'stop', 'mms'], }, OS_LINUX: { 'product_path': '/usr/lib/' + acrort.common.BRAND_NAME, 'start_mms_args': ['service', 'acronis_mms', 'start'], 'stop_mms_args': ['service', 'acronis_mms', 'stop'], }, OS_MAC: { 'product_path': '/Library/Application Support/BackupClient/' + acrort.common.BRAND_NAME, 'start_mms_args': ['launchctl', 'start', 'acronis_mms'], 'stop_mms_args': ['launchctl', 'stop', 'acronis_mms'], } } def is_guid(key): RE_UUID = re.compile("[0-F]{8}-[0-F]{4}-[0-F]{4}-[0-F]{4}-[0-F]{12}", re.I) return bool(RE_UUID.match(key)) def get_settings_key(): return r'SOFTWARE\{}\BackupAndRecovery\Settings'.format(acrort.common.BRAND_NAME) def get_machine_settings_key(): return get_settings_key() + r'\MachineManager' def get_current_machine_id(): return registry_read_string(get_machine_settings_key(), 'MMSCurrentMachineID') def get_windows_product_installation_path(): key = r'SOFTWARE\{}\Installer'.format(acrort.common.BRAND_NAME) return registry_read_string(key, 'TargetDir') def get_product_installation_path(): value = data[platform.system()]['product_path'] if isinstance(value, str): return value return value(1) def get_product_data_path(): return os.path.join(acrort.fs.APPDATA_COMMON, acrort.common.BRAND_NAME) def registry_read_string(key_name, value_name, open_hive=None): root_reg = acrort.registry.open_system_hive(hive=open_hive) if key_name not in root_reg.subkeys: acrort.common.make_logic_error( "Key '{}' not found. May be MMS service is not installed".format(key_name)).throw() key = root_reg.subkeys.open(key_name=key_name) if value_name not in key.values: acrort.common.make_logic_error( "Value '{}' not found. May be MMS service is not installed".format(value_name)).throw() value = key.values.open(value_name=value_name) return value.get(acrort.registry.TYPE_SZ) def is_service_running(service_name): system = platform.system() if system == OS_WINDOWS: args = ['sc', 'query', service_name] ps = subprocess.Popen(args, stdout=subprocess.PIPE) output = ps.communicate()[0] return 'STOPPED' not in str(output) elif system in [OS_MAC, OS_LINUX]: ps = subprocess.Popen(('ps', 'aux'), stdout=subprocess.PIPE) output = ps.communicate()[0] return ('/' + service_name) in str(output) else: acrort.common.make_logic_error('Unsupported operating system: ' + system).throw() def start_service(args, service_name): try: print('Executing command: {}'.format(' '.join(args))) subprocess.run(args, stdout=subprocess.DEVNULL, check=True) except Exception as e: print('Can\'t start %s service: %s', service_name, str(e)) def stop_service(args, service_name, is_service_running): try: print('Executing command: {}'.format(' '.join(args))) subprocess.run(args, stdout=subprocess.DEVNULL, check=True, timeout=60) except subprocess.CalledProcessError as e: acrort.common.make_logic_error( 'Can\'t stop {} service with error: {}'.format(service_name, str(e))).throw() else: # Lookup for target process, wait if it is still here wait_reattempts = 10 while wait_reattempts: time.sleep(10) if not is_service_running(): break wait_reattempts = wait_reattempts - 1 if not wait_reattempts: acrort.common.make_logic_error( 'Can\'t stop %s service, please stop it manually.'.format(service_name)).throw() def is_mms_service_running(): return is_service_running('mms') def start_mms_service(): start_service(data[platform.system()]['start_mms_args'], 'MMS') def stop_mms_service(): stop_service(data[platform.system()]['stop_mms_args'], 'MMS', is_mms_service_running) def get_default_dml_database_path(): return os.path.join(get_product_data_path(), 'BackupAndRecovery', 'MMSData', 'DML', 'F4CEEE47-042C-4828-95A0-DE44EC267A28.db3') # Archives specs def make_select_all_archives_spec(): return [ ('^Is', 'string', 'DMS::Cache::Archive'), ] def make_select_archives_spec(archive_ids): ids = [[('', 'string', id)] for id in archive_ids] return [ ('^Is', 'string', 'DMS::Cache::Archive'), ('.ID', 'string',''), ('.ID^ValueIn', 'complex_trait', [('', 'array', ids)]), ] def make_select_foreign_archives_spec(machine_id, location_id, limit): options = [ ('.LimitOptions', 'dword', limit), ] result = [ ('^Is', 'string', 'DMS::Cache::Archive'), ('^Not', 'complex_trait', [ ('.Attributes.MachineID', 'string', machine_id) ]) ] if location_id: result += [('.Attributes.LocationID', 'guid', location_id)] return result, options def make_select_machine_archives_spec(machine_id, location_id): result = [ ('.Attributes.MachineID', 'string', ''), ('.Attributes.MachineID^Like', 'string', machine_id), ('^Is', 'string', 'DMS::Cache::Archive'), ] if location_id: result += [('.Attributes.LocationID', 'guid', location_id)] return result # Backups specs def make_select_all_backups_spec(): return [ ('^Is', 'string', 'DMS::Cache::Slice'), ] def make_select_backups_spec(backup_ids): ids = [[('', 'string', id)] for id in backup_ids] return [ ('^Is', 'string', 'DMS::Cache::Slice'), ('.ID', 'string',''), ('.ID^ValueIn', 'complex_trait', [('', 'array', ids)]), ] def make_select_machine_backups_spec(machine_id, location_id): result = [ ('^Is', 'string', 'DMS::Cache::Slice'), ('.Attributes.Location.LocationMachineID', 'string', ''), ('.Attributes.Location.LocationMachineID^Like', 'string', machine_id) ] if location_id: result += [('.Attributes.LocationID', 'guid', location_id)] return result def make_select_foreign_backups_spec(machine_id, location_id, limit): options = [ ('.LimitOptions', 'dword', limit), ] result = [ ('^Is', 'string', 'DMS::Cache::Slice'), ('^Not', 'complex_trait', [ ('.Attributes.Location.LocationMachineID', 'string', machine_id), ]) ] if location_id: result += [('.Attributes.LocationID', 'guid', location_id)] return result, options # Zmq acks specs def make_select_channel_zmqgw_acks_spec(channel): return [ ('^Is', 'string', 'ZmqGw::Ack'), ('.ID.Channel', 'string', channel), ] def make_select_zmq_acks_spec_for_deletion(zmq_acks): ids = [[('', 'string', id)] for id in zmq_acks] return [ ('^Is', 'string', 'ZmqGw::Ack'), ('.ID.Key', 'string',''), ('.ID.Key^ValueIn', 'complex_trait', [('', 'array', ids)]), ] def make_select_zmq_acks_spec(channel, limit): options = [ ('.LimitOptions', 'dword', limit), ] return [ ('^Is', 'string', 'ZmqGw::Ack'), ('.ID.Channel', 'string', channel), ], options def get_objects(dml, pattern, options=None): if options: return dml.select(acrort.dml.ViewSpec(acrort.plain.Unit(flat=pattern), acrort.plain.Unit(flat=options))) return dml.select(acrort.dml.ViewSpec(acrort.plain.Unit(flat=pattern))) def get_objects_count(dml, pattern): options = [ ('.Counter.CounterObjectTemplate.ID', 'string', 'Count'), ('.Counter.CounterObjectTemplate.ID^PrimaryKey', 'nil', None) ] return dml.select1(acrort.dml.ViewSpec(acrort.plain.Unit(flat=pattern), acrort.plain.Unit(flat=options))).CounterValue.ref def print_progress(current, max): sys.stdout.write('{} of {}\r'.format(current, max)) def get_archives_count(dml, machine_id, location_id): all_archives = get_objects_count(dml, make_select_all_archives_spec()) machine_archives = get_objects_count(dml, make_select_machine_archives_spec(machine_id, location_id)) zmq_acks = get_objects_count(dml, make_select_channel_zmqgw_acks_spec('Archive')) return all_archives, machine_archives, all_archives - machine_archives, zmq_acks def cleanup_archives(dml, machine_id, location_id, all_archives_count): counter = 0 foreign_archives = ['dummy'] while foreign_archives: print_progress(counter, all_archives_count) foreign_archives = get_objects(dml, *make_select_foreign_archives_spec(machine_id, location_id, CHUNK_SIZE)) if foreign_archives: ids = [item.ID.ref for item in foreign_archives] dml.delete(pattern=acrort.plain.Unit(flat=make_select_archives_spec(ids))) counter += len(foreign_archives) print_progress(counter, all_archives_count) def get_backups_count(dml, machine_id, location_id): all_backups = get_objects_count(dml, make_select_all_backups_spec()) machine_backups = get_objects_count(dml, make_select_machine_backups_spec(machine_id, location_id)) zmq_acks = get_objects_count(dml, make_select_channel_zmqgw_acks_spec('Slice')) return all_backups, machine_backups, all_backups - machine_backups, zmq_acks def cleanup_backups(dml, machine_id, location_id, all_backups_count): counter = 0 foreign_backups = ['dummy'] print_progress(counter, all_backups_count) while foreign_backups: foreign_backups = get_objects(dml, *make_select_foreign_backups_spec(machine_id, location_id, CHUNK_SIZE)) if foreign_backups: ids = [item.ID.ref for item in foreign_backups] dml.delete(pattern=acrort.plain.Unit(flat=make_select_backups_spec(ids))) counter += len(foreign_backups) print_progress(counter, all_backups_count) def cleanup_zmqgw_acks(dml, all_acks_count): counter = 0 print_progress(counter, all_acks_count) for channel in ['Archive', 'Slice']: acks = ['dummy'] while acks: acks = get_objects(dml, *make_select_zmq_acks_spec(channel, CHUNK_SIZE)) if acks: ids = [item.ID.Key.ref for item in acks] dml.delete(pattern=acrort.plain.Unit(flat=make_select_zmq_acks_spec_for_deletion(ids))) counter += len(acks) print_progress(counter, all_acks_count) def main(): system = platform.system() if system not in [OS_WINDOWS, OS_LINUX, OS_MAC]: acrort.common.make_logic_error('Unsupported operating system: ' + system).throw() parser = argparse.ArgumentParser(description='Cleanup DML DB from foreign archives and backups') parser.add_argument( '-c', '--cleanup', required=False, action="store_true", help='Flag whether to cleanup DML DB from foreign archives and backups') parser.add_argument( '-d', '--db-path', required=False, nargs=1, help='Full path where DML database is placed, product path will be used if it is not specified') parser.add_argument( '-m', '--machine-id', required=False, nargs=1, help='Machine identifier in form <GUID>, will be used from the current MMS installation if it is not specified') parser.add_argument( '-b', '--backups', required=False, action="store_true", help='Flag whether to report number of foreign archives and backups only') parser.add_argument( '-a', '--acks', required=False, action="store_true", help='Flag whether to report number of ZMQ acks only') parser.add_argument( '-l', '--location-id', required=False, nargs=1, help='Location identifier in form <GUID>, will be used as additional filter for archive and backups deletion') args = parser.parse_args() db_path = get_default_dml_database_path() if args.db_path: db_path = args.db_path[0] silent_mode = args.backups or args.acks if not silent_mode: print("DML database used at: {}".format(db_path)) machine_id = None if args.machine_id: machine_id = args.machine_id[0] if not machine_id: machine_id = get_current_machine_id() if not silent_mode: print("Machine to preserve archives and backups from: {}".format(machine_id)) machine_id = machine_id.upper() if not is_guid(machine_id): print("Machine identifier error: invalid GUID format: {}".format(machine_id)) return location_id = None if args.location_id: location_id = args.location_id[0] cleanup = args.cleanup if cleanup and not args.db_path and is_mms_service_running(): print("Stopping MMS service...") stop_mms_service() print("Done.\n") connection_string = 'sqlite-v2://{}#limit_statement_cache'.format(db_path) conn = acrort.dml.open_database(connection_string) archives_count, machine_archives, foreign_archives, zmq_arc_acks = get_archives_count(conn.dml, machine_id, location_id) backups_count, machine_backups, foreign_backups, zmq_bckp_acks = get_backups_count(conn.dml, machine_id, location_id) if args.backups: print(foreign_archives + foreign_backups) return elif args.acks: print(zmq_arc_acks + zmq_bckp_acks) return print("Statistics (before cleanup):") print("Total archives: {}, machine archives: {}, foreign archives: {}, zmq acks: {}".format(archives_count, machine_archives, foreign_archives, zmq_arc_acks)) print("Total backups: {}, machine backups: {}, foreign backups: {}, zmq acks: {}".format(backups_count, machine_backups, foreign_backups, zmq_bckp_acks)) if cleanup: if foreign_archives: print("\nCleanup foreign archives...") cleanup_archives(conn.dml, machine_id, location_id, archives_count) print("\nDone.\n") if foreign_backups: print("\nCleanup foreign backups...") cleanup_backups(conn.dml, machine_id, location_id, backups_count) print("\nDone.\n") if zmq_arc_acks + zmq_bckp_acks: print("\nCleanup zmq acks...") cleanup_zmqgw_acks(conn.dml, zmq_arc_acks + zmq_bckp_acks) print("\nDone.\n") print("Statistics (after cleanup):") archives_count, machine_archives, foreign_archives, zmq_arc_acks = get_archives_count(conn.dml, machine_id, location_id) backups_count, machine_backups, foreign_backups, zmq_bckp_acks = get_backups_count(conn.dml, machine_id, location_id) print("Total archives: {}, machine archives: {}, foreign archives: {}, zmq acks: {}".format(archives_count, machine_archives, foreign_archives, zmq_arc_acks)) print("Total backups: {}, machine backups: {}, foreign backups: {}, zmq acks: {}".format(backups_count, machine_backups, foreign_backups, zmq_bckp_acks)) if cleanup and not args.db_path: print("\nStarting MMS service...") start_mms_service() print("Done.") print("\nSuccessfully finished.") if __name__ == '__main__': import acrobind exit(acrobind.interruptable_safe_execute(main))