Source code for twindb_backup

# -*- coding: utf-8 -*-
"""TwinDB Backup module.

 The module is a core of twindb-backup tool. It includes backup and restore
 functionality. The module takes a backup from something defined in a source
 class and saves the backup copy in something defined in a destination class.

 The source class inherits from BaseSource() from
 twindb_backup.source.base_source.py. The source class must define get_stream()
 method that yields a file object that is used for next classes.
 Typical classes are FileSource() to backup files and directories,
 MySQLSource() to backup MySQL.

 The destination class inherits from BaseDestination(). This is where you
 store backups. The destination class must define save() method that
 takes an input stream and saves it somewhere. Examples of
 the destination class are S3(), Ssh().

 There are modifier classes. The modifier class sits in the middle between
 the source and the destination and does something with a stream before
 the stream is saved. The modifier class may save a local copy (KeepLocal())
 or encrypt the stream or else. The modifier class inherits Modifier()

 The backup process may be depicted as a chain of modifiers with the source
 in the head and the destination in the tail.

::

 +--------+    +------------+    +------------+    +-------------+
 | source | -- | modifier 1 | -- | modifier 2 | -- | destination |
 +--------+    +------------+    +------------+    +-------------+

 """
import glob
import json
import logging
import os
import sys
from collections import namedtuple

__author__ = "TwinDB Development Team"
__email__ = "dev@twindb.com"
__version__ = "3.3.0"
STATUS_FORMAT_VERSION = 1
LOCK_FILE = "/var/run/twindb-backup.lock"
LOG_FILE = "/var/log/twindb-backup-measures.log"
INTERVALS = ["hourly", "daily", "weekly", "monthly", "yearly"]
MEDIA_TYPES = ["files", "mysql", "binlog"]
XTRABACKUP_BINARY = "xtrabackup"
XBSTREAM_BINARY = "xbstream"
MARIABACKUP_BINARY = "mariabackup"
MBSTREAM_BINARY = "mbstream"
MY_CNF_COMMON_PATHS = ["/etc/my.cnf", "/etc/mysql/my.cnf"]
DEFAULT_FILE_ENCODING = "utf-8"
GLOBAL_INIT_LOG_LEVEL = logging.DEBUG

LOG = logging.getLogger(__name__)
LOG.setLevel(GLOBAL_INIT_LOG_LEVEL)

DestTypes = namedtuple("DestinationTypes", "ssh,local,s3,gcs,azure")
QueryTypes = namedtuple("QueryTypes", ["mysql"])
SUPPORTED_DESTINATION_TYPES = DestTypes("ssh", "local", "s3", "gcs", "azure")
SUPPORTED_QUERY_LANGUAGES = QueryTypes("mysql")


[docs]class LessThanFilter(logging.Filter): # pylint: disable=too-few-public-methods """Filters out log messages of a lower level.""" def __init__(self, exclusive_maximum, name=""): super().__init__(name) self.max_level = exclusive_maximum
[docs] def filter(self, record): # non-zero return means we log this message return 1 if record.levelno < self.max_level else 0
[docs]def setup_logging(logger, debug=False): # pragma: no cover """Configures logging for the module""" fmt_str = "%(asctime)s: %(levelname)s: %(module)s.%(funcName)s():%(lineno)d: %(message)s" console_handler = logging.StreamHandler(stream=sys.stdout) console_handler.addFilter(LessThanFilter(logging.WARNING)) console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter(fmt_str)) # Log errors and warnings to stderr console_handler_err = logging.StreamHandler(stream=sys.stderr) console_handler_err.setLevel(logging.WARNING) console_handler_err.setFormatter(logging.Formatter(fmt_str)) # Log debug to stderr console_handler_debug = logging.StreamHandler(stream=sys.stderr) console_handler_debug.addFilter(LessThanFilter(logging.INFO)) console_handler_debug.setLevel(logging.DEBUG) console_handler_debug.setFormatter(logging.Formatter(fmt_str)) logger.handlers = [] logger.addHandler(console_handler) logger.addHandler(console_handler_err) if debug: logger.addHandler(console_handler_debug) logger.debug_enabled = True logger.setLevel(logging.DEBUG)
[docs]def get_files_to_delete(all_files, keep_copies): """If you give it a list of files and number of how many you'd like to keep the function will return files that need to be deleted :param all_files: list of strings :type all_files: list :param keep_copies: number of copied to keep :type keep_copies: int :return: list of strings (files) to delete :rtype: list """ LOG.debug("Retain %d files", keep_copies) if keep_copies == 0: return all_files else: return all_files[:-keep_copies]
[docs]def delete_local_files(dir_backups, keep_copies): """Deletes local backup copies based on given retention number. :param dir_backups: directory with backup copies :type dir_backups: str :param keep_copies: how many to keep :type keep_copies: int :return: None """ local_files = sorted(glob.glob(dir_backups)) LOG.debug("Local copies: %r", local_files) for local_file in get_files_to_delete(local_files, keep_copies): LOG.debug("Deleting: %s", local_file) os.unlink(local_file)
[docs]def get_timeout(run_type): """Get timeout for a each run type - daily, hourly etc :param run_type: Run type :type run_type: str :return: Number of seconds the tool allowed to wait until other instances finish :rtype: int """ timeouts = { "hourly": 3600 / 2, "daily": 24 * 3600 / 2, "weekly": 7 * 24 * 3600 / 2, "monthly": 30 * 24 * 3600 / 2, "yearly": 365 * 24 * 3600 / 2, } return int(timeouts[run_type])
[docs]def save_measures(start_time, end_time, log_path=LOG_FILE): """Save backup measures to log file""" data = { "start": start_time, "finish": end_time, "duration": end_time - start_time, } try: with open(log_path, "r", encoding=DEFAULT_FILE_ENCODING) as data_fp: log = json.load(data_fp) log["measures"].append(data) if len(log["measures"]) > 100: del log["measures"][0] except (IOError, ValueError): log = {"measures": [data]} with open(log_path, "w", encoding=DEFAULT_FILE_ENCODING) as file_pt: json.dump(log, file_pt)