# 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))
|