backup.py

Fri, 19 Jan 2018 16:53:13 +0000

author
Tuomo Valkonen <tuomov@iki.fi>
date
Fri, 19 Jan 2018 16:53:13 +0000
changeset 6
46c89e5a219f
parent 5
4c5514b2fa76
child 7
e189d4a6cb8c
permissions
-rw-r--r--

subprocess improvements

#
# Borgend Backup instance
#

import config
import logging
import time
from instance import BorgInstance
from queue import Queue
from threading import Thread, Lock

loglevel_translation={
    'CRITICAL': logging.CRITICAL,
    'ERROR': logging.ERROR,
    'WARNING': logging.WARNING,
    'DEBUG': logging.DEBUG,
    'INFO': logging.INFO
}

def translate_loglevel(x):
    if x in loglevel_translation:
        return loglevel_translation[x]
    else:
        return logging.ERROR

class Backup:

    def __decode_config(self, cfg):
        loc0='backup target %d' % self.identifier

        self.name=config.check_string(cfg, 'name', 'Name', loc0)

        self.loc='backup target "%s"' % self.name

        self.repository=config.check_string(cfg, 'repository',
                                            'Target repository', self.loc)

        self.archive_prefix=config.check_string(cfg, 'archive_prefix',
                                                'Archive prefix', self.loc)

        self.archive_template=config.check_string(cfg, 'archive_template',
                                                  'Archive template', self.loc)

        self.backup_interval=config.check_nonneg_int(cfg, 'backup_interval',
                                                     'Backup interval', self.loc,
                                                     config.defaults['backup_interval'])

        self.retry_interval=config.check_nonneg_int(cfg, 'retry_interval',
                                                    'Retry interval', self.loc,
                                                    config.defaults['retry_interval'])

        self.paths=config.check_nonempty_list_of_strings(cfg, 'paths', 'Paths', self.loc)

        self.common_parameters=config.check_list_of_dicts(cfg, 'common_parameters',
                                                         'Borg parameters', self.loc,
                                                         default=[])

        self.create_parameters=config.check_list_of_dicts(cfg, 'create_parameters',
                                                         'Create parameters', self.loc,
                                                         default=[])

        self.prune_parameters=config.check_list_of_dicts(cfg, 'prune_parameters',
                                                         'Prune parameters', self.loc,
                                                         default=[])


    def __init__(self, identifier, cfg):
        self.identifier=identifier

        self.__decode_config(cfg)

        self.config=config
        self.lastrun=None
        self.lastrun_success=None
        self.borg_instance=None
        self.current_operation=None
        self.thread=None
        self.lock=Lock()

    def __block_when_running(self):
        with self.lock:
            not_running=self.borg_instance is None and self.thread is None
        assert(not_running)

    def __listener(self):
        success=False
        for status in iter(self.borg_instance.read, None):
            logging.info(str(status))
            t=status['type']
            if t=='progress_percent':
                pass
            elif t=='archive_progress':
                pass
            elif t=='progress_message':
                if 'finished' in status:
                    logging.info('Borg subprocess finished succesfully')
                    success=status['finished']
            elif t=='progress_percent':
                # Temporary output
                print('%d / %d', status['current'], status['total'])
                pass
            elif t=='file_status':
                pass
            elif t=='log_message':
                if 'levelname' not in status:
                    status['levelname']='ERROR'
                if 'message' not in status:
                    status['message']='UNKNOWN'
                if 'name' not in status:
                    status['name']='borg'
                logging.log(translate_loglevel(status['levelname']),
                            status['name'] + ': ' + status['message'])
            elif t=='exception':
                pass
            elif t=='unparsed_error':
                pass

        logging.info('Waiting for borg subprocess to terminate')

        self.borg_instance.wait()

        logging.info('Borg subprocess terminated; terminating listener thread')

        with self.lock:
            if self.current_operation=='create':
                self.lastrun=self.time_started
                self.lastrun_success=success
            self.borg_instance=None
            self.thread=None
            self.current_operation=None
            self.time_started=None

    def __launch(self, queue, operation, archive_or_repository, *args):

        inst=BorgInstance(operation, archive_or_repository, *args)
        inst.launch()

        t=Thread(target=self.__listener)
        t.daemon=True

        self.thread=t
        self.borg_instance=inst
        self.queue=queue
        self.current_operation=operation
        self.time_started=time.monotonic()

        t.start()

    def create(self, queue):
        self.__block_when_running()

        archive="%s::%s%s" % (self.repository,
                              self.archive_prefix,
                              self.archive_template)

        self.__launch(queue, 'create', archive,
                      self.common_parameters+self.create_parameters,
                      self.paths)

    def prune(self, queue):
        self.__block_when_running()
        self.__launch(queue, 'prune', self.repository,
                      ([{'prefix': self.archive_prefix}] + 
                       self.common_parameters +
                       self.prune_parameters))

    # TODO: Decide exact (manual) abort mechanism. Perhaps two stages
    def abort(self):
        with self.lock:
            if self.borg_instance:
                self.borg_instance.terminate()
            if self.thread:
                self.thread.terminate()

    def join(self):
        if self.thread:
            self.thread.join()

    def next_action():
        # TODO pruning as well
        now=time.monotonic()
        if not self.lastrun:
            return 'create', now+self.retry_interval
        elif not self.lastrun_success:
            return 'create', self.lastrun+self.retry_interval
        else:
            if self.backup_interval==0:
                return 'none', 0
            else:
                return 'create', self.lastrun+self.backup_interval

mercurial