????

Your IP : 18.188.245.152


Current Path : /lib/Acronis/PyShell/site-tools/
Upload File :
Current File : //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))