Python: скрипт бекапа файлов и баз MySQL в AWS S3

By | 01/03/2018
 

PythonЗадача – набросать скрипт, который умел бы создавать бекап нескольких сайтов и загружать их в AWS S3 корзину.

Ниже описан процесс написания такого скрипта (или, скорее, уже даже “утилиты”, т.к. имеются модули и файл настроек), без особых деталей по работе и реализации самих функций – больше информации о процессе создания утилиты, её структуре и логике выполнения.

Скрипт писался на Python 3, работает и на Python 2 (только потребует установки вручную configparserи pip), работает под Linux и, теоретически – под Windows, ибо Python.

UPD:  уже вечером перечитывал код – нашёл пару Bugs, надо будет обновить. Ну и вообще тут есть поле для #ToDo/improvements.

Структура каталогов будет следующая:

  • lib: для файлов модулей
  • conf: для дефолтного файла настроек

Сейчас всё вместе выглядит так:

tree simple-site-backup/ | grep -v pyc
simple-site-backup/
├── conf
│   └── simple-site-backup.ini
├── lib
│   ├── backup.py
│   ├── common.py
│   ├── __init__.py
│   └── s3sync.py
├── README.md
├── sitebackup.py

Полностью скрипт доступен в Github.

Python argparse

Начнём с первого скрипта, в котором определим опции, и который далее будет вызывать функции из других модулей.

В корне каталога проекта создаём файл sitebackup.py, в котором используем модуль Python argparse для получения опций. Сейчас опция только одна, которая позволит переопределить файл настроек (к нему перейдём позже).

Создаём функцию getopts():

...
def getopts():

    """Use '-c' to specify non-default configuration file."""

    parser = argparse.ArgumentParser()

    parser.add_argument('-c',
                              '--config',
                              action='store',
                              default='conf/simple-site-backup.ini'
                        )

    return parser.parse_args()
...

И её вызов:

...
if __name__ == '__main__':

    # get options set from argparse() and pass them to backup()
    options = vars(getopts())
    print(options)
    exit()

В результате полностью скрипт сейчас выглядит так:

#!/usr/bin/env python

"""Personal backup script"""

import argparse

__author__ = "Arseny Zinchenko"
__license__ = "GPL"
__version__ = "0.1"
__maintainer__ = "Arseny Zinchenko"
__status__ = "Development"

def getopts():

    """Use '-c' to specify non-default configuration file."""

    parser = argparse.ArgumentParser()

    parser.add_argument('-c',
                              '--config',
                              action='store',
                              default='conf/simple-site-backup.ini'
                        )

    return parser.parse_args()


if __name__ == '__main__':

    # get options set from argparse() and pass them to backup()
    options = vars(getopts())
    print(options)

Проверяем:

./sitebackup.py
{'config': 'conf/simple-site-backup.ini'}
./sitebackup.py -c testconf
{'config': 'testconf'}

Модуль backup

Теперь можно приступать к добавлению модулей. Что бы не загромождать код основного файла и корневой каталог – создаём в нём каталог lib:

mkdir lib

Что бы Python принимал каталог lib как модуль – добавляем пустой файл __init__.py:

touch lib/__init__.py

В каталоге lib создаём файл модуля backup.py, в котором будем хранить основные функции, связанные с созданием бекапов.

Главной функцией будет backup(), которую будем вызывать из sitebackup.py, передавать ей первым параметром путь к файлу настроек, а она будет выполнять остальные  действия:

#!/usr/bin/env python

def backup(config):

    print('backup() with {}'.format(config))

Добавляем в sitebackup.py импорт модуля backup:

...
from lib import backup
...

И добавляем вызов функции backup() из импортированного модуля:

...
if __name__ == '__main__':

    # get options set from argparse() and pass them to backup()
    options = vars(getopts())

    # start here - run backup() with path to config file passed
    backup.backup(options['config'])

Запускаем:

./sitebackup.py
backup() with conf/simple-site-backup.ini

ConfigParser и файл настроек скрипта

Идея заключается в том, что бы иметь единый файл настроек, в котором будут описываться сайты, бекапы которых мы хотим делать.

При этом – должна быть возможность делать только бекап файлов и/или только бекап базы данных.

Создаём каталог conf:

mkdir conf

И в нём – файл simple-site-backup.ini, в котором будет минимум три секции:

  1. [backup-settings] – тут указываем настройки для самого скрипта – в каком каталоге создавать локальные копии
  2. [defaults] – тут зададим некоторые значения по умолчанию, которые можно будет переопределять в секциях сайтов
  3. [example] – тестовый сайт

Выглядит он так:

[backup-settings]
backup_root_path = /backups
backup_files_path = files
backup_db_path = databases

[defaults]
[example]
www_data_path = /tmp/testbak/
mysql_db = example_db
mysql_host = localhost
mysql_user = example_db_user
mysql_pass = example_db_pass

Тут в секции [backup-settings]:

  • backup_root_path = /backups: /backups у меня смонтирован из EBS раздела, подключенного к EC2, это корневой каталог для бекапов
  • backup_files_dir = files: каталог в backup_root_path для хранения копий файлов
  • backup_db_dir = databases: каталог для дампов баз

И [example]:

  • www_data_path = /tmp/testbak/: каталог, из содержимого которого будет создан архив
  • mysql_db, mysql_host, mysql_user, mysql_pass – данные базы данных, дамп которой будет выполняться

[defaults] – рассмотрим позже, пока она не используется.

Функции, не связанные непосредственно с бекапами – вынесем в отдельный модуль.

В каталоге lib добавим файл common.py, в котором добавим функцию для вызова ConfigParser:

#!/usr/bin/env python

def get_config(config):

    """Create ConfigParcer object with configuration file.
       File can be passed with -c."""

    parser = configparser.ConfigParser()

    if len(parser.read(config)) == 0:
        raise Exception('ERROR: No config file {} found!'.format(config))

    return parser

Возвращаемся к backup(), добавляем импорт common и вызов get_config():

...
from lib import common
...
def backup(config):

    # create parser object to pass to functions
    parser = common.get_config(config)

    # set own settings
    # "/backups"
    backup_root_path = parser.get('backup-settings', 'backup_root_path')
    # "/backups" + "files"
    files_destination_dir = os.path.join(backup_root_path, parser.get('backup-settings', 'backup_files_dir'))
    # "/backups" + "databases"
    db_destination_dir = os.path.join(backup_root_path, parser.get('backup-settings', 'backup_db_dir'))

    print('\nGot own settings:\n\n'
          'backup_root_path = {}\n'
          'backup_files_dir = {}\n'
          'backup_db_dir = {}\n'
          .format(backup_root_path, files_destination_dir, db_destination_dir)
         )

Вызываем, проверяем:

./sitebackup.py
Got own settings:
backup_root_path = /backups
backup_files_dir = /backups/files
backup_db_dir = /backups/databases

ОК, следующим шагом надо проверить – имеются ли локальные директории, в которые мы будем складывать бекапы, и если их нет – создать их.

Используем модуль common, в котором добавим функцию check_dirs(), которая аргументом будет принимать список каталогов для проверки:

...
def check_dirs(dirs):

    # check for backup directories, create if not
    print('Checking directories:\n')

    for dir in dirs:
        if not os.path.isdir(dir):
            print('{} - not found, creating...'.format(dir))
            os.mkdir(dir)
        else:
            print('{} - found, OK.'.format(dir))
...

Добавляем её вызов из функции backup():

...
    print('\nGot own settings:\n\n'
          'backup_root_path = {}\n'
          'backup_files_dir = {}\n'
          'backup_db_dir = {}\n'
          .format(backup_root_path, files_destination_dir, db_destination_dir)
         )

    # check for backup directories, create if not
    common.check_dirs([backup_root_path, files_destination_dir, db_destination_dir])

Запускаем:

sudo ./sitebackup.py
Got own settings:
backup_root_path = /backups
backup_files_dir = /backups/files
backup_db_dir = /backups/databases
Checking directories:
/backups - not found, creating...
/backups/files - not found, creating...
/backups/databases - not found, creating...

Проверяем:

tree /backups/
/backups/
├── databases
└── files

Бекап файлов

Теперь можно приступать к созданию бекапов. Для этого создадим две функции – одна будет создавать бекапы файлов – www_backup(), вторая будет дампить базы данных – db_backup().

Для www_backup() нам надо будет передать три параметра – имя сайта (секции) из файла настроек, имя и путь файла бекапа, а третьим аргументом передадим объект parser(), что бы функция www_backup() определила исходный каталог, который будет бекапить (www_source_dir).

Выглядит она так:

...
def www_backup(site, www_backup_file, parser):

    # if 'www_data_path' doesn't set in config for $site - skip it
    try:
        www_source_dir = parser.get(site, 'www_data_path')
    except configparser.NoOptionError as e:
        print('\nWARNING: {}.\nSkipping WWW backup for the {}.\n'.format(e, site))
        return

    print('\nCreating WWW backup for:\nsite: {}\nfrom: {}\nto: {}'.format(site, www_source_dir, www_backup_file))

    with tarfile.open(www_backup_file, "w:gz") as tar:
        tar.add(www_source_dir, arcname=os.path.basename(www_source_dir))

    print('\nWWW backup done.\n')
...

В блоке:

...
    try:
        www_source_dir = common.parser.get(site, 'www_data_path')
...

пытаемся получить параметр www_data_path из файла настроек секции сайта, если его нет – значит создавать копию файлов не требуется, пропускаем (return в начало).

Если она есть – создаём tar-архив (with tarfile.open [...]), путь и имя которого переданы  через аргумент www_backup_file.

Перед тем, как вызывать функцию www_backup() – надо определить имя создаваемого файла.

В функции backup(), добавляем:

...
    # day - month - year - hours - minutes
    # 02-01-2018-13-58
    today = datetime.datetime.now().strftime('%d-%m-%Y-%H-%M')

Далее – запускаем цикл, в котором перебираем все сайты/секции в файле настроек:

...
    # start sites backup here
    # for [backup-settings] etc
    for site in parser.sections():
        # skip own settings section 'backup-settings' and 'defaults'
        if all ([site != 'backup-settings', site != 'defaults']):
...

Т.к. секции [backup-settings] и [defaults] не относятся к созданию бекапов – пропускаем их:

...
    if all ([site != 'backup-settings', site != 'defaults']):
...

Для всех остальных секций – сначала задаём имя файла бекапа:

...
            # WWW backup section
            www_backup_file = os.path.join(files_destination_dir,
                                           today + '_'+ site + '_' + '.gz')
...

Причём в имени файла дату лучше задавать в начале, что бы потом проще было определяться в папке с бекапами – всё будет отсортировано по датам.

И затем – вызываем функцию www_backup(), которой передаём все параметры:

...
            # exec www files tar
            www_backup(site, www_backup_file, parser)
...

Тепер функция backup() полностью выглядит так:

...
def backup(config):

    # create parser object to pass to functions
    parser = common.get_config(config)

    # set own settings
    # "/backups"
    backup_root_path = parser.get('backup-settings', 'backup_root_path')
    # "/backups" + "files"
    files_destination_dir = os.path.join(backup_root_path, parser.get('backup-settings', 'backup_files_dir'))
    # "/backups" + "databases"
    db_destination_dir = os.path.join(backup_root_path, parser.get('backup-settings', 'backup_db_dir'))

    print('\nGot own settings:\n\n'
          'backup_root_path = {}\n'
          'backup_files_dir = {}\n'
          'backup_db_dir = {}\n'
          .format(backup_root_path, files_destination_dir, db_destination_dir)
         )

    # check for backup directories, create if not
    common.check_dirs([backup_root_path, files_destination_dir, db_destination_dir])

    # day - month - year - hours - minutes
    # 02-01-2018-13-58
    today = datetime.datetime.now().strftime('%d-%m-%Y-%H-%M')

    # start sites backup here
    # for [backup-settings] etc sections
    for site in parser.sections():
        # skip own settings section 'backup-settings' and 'defaults'
        if all ([site != 'backup-settings', site != 'defaults']):

            # WWW backup section
            # /backups/files/test-02-01-2018-13-58.gz
            www_backup_file = os.path.join(files_destination_dir,
                                           today + '_'+ site + '_' + '.gz')
            # exec www files tar
            www_backup(site, www_backup_file, parser)

ОК – пробуем.

Создаём тестовый каталог, который указан в файле настроек для [example] в опции www_data_path:

mkdir /tmp/testbak/

Тестовый файлик:

touch /tmp/testbak/testfile

Запускаем скрипт:

./sitebackup.py
Got own settings:
backup_root_path = /backups
backup_files_dir = /backups/files
backup_db_dir = /backups/databases
Checking directories:
/backups - found, OK.
/backups/files - found, OK.
/backups/databases - found, OK.
Creating WWW backup for:
site: example
from: /tmp/testbak/
to: /backups/files/03-01-2018-14-03_example_.gz
WWW backup done.

С этим – всё, переходим к базам данных.

Бекап баз MySQL

Тут всё аналогично бекапу файлов – задаём имя и путь будущего файла дампа БД:

...
            # DB backup section
            # /backups/databases/example_bkp_test-02-01-2018-15-59.sql
            db_backup_file = os.path.join(db_destination_dir,
                                          today + '_' + site + '_' + parser.get(site, 'mysql_db') + '.sql')

И добавляем функцию db_backup(), которой так же передаём имя сайта/секции, путь к файлу и объект parser для получения остальных параметров:

....
def db_backup(site, db_backup_file, parser):

    # if 'mysql_*' doesn't set in config for $site - skip it
    try:
        mysql_host = parser.get(site, 'mysql_host')
        mysql_db = parser.get(site, 'mysql_db')
        mysql_user = parser.get(site, 'mysql_user')
        mysql_pass = parser.get(site, 'mysql_pass')
    except configparser.NoOptionError as e:
        print('\nWARNING: {}.\nSkipping DB backup for the {}.\n'.format(e, site))
        return

    print ('Creating DB backup for:\n'
           'site: {}\n'
           'host: {}\n'
           'database: {}\n'
           'user: {}\n'
           'to: {}'.format(site, mysql_host, mysql_db, mysql_user, db_backup_file))

    dump_cmd = ['mysqldump ' +
                '--user={mysql_user} '.format(mysql_user=mysql_user) +
                '--password={db_pw} '.format(db_pw=mysql_pass) +
                '--host={db_host} '.format(db_host=mysql_host) +
                '{db_name} '.format(db_name=mysql_db) +
                '> ' +
                '{filepath}'.format(filepath=db_backup_file)]

    dump = subprocess.Popen(dump_cmd, shell=True)
    dump.wait()

    print('\nDB backup done.\n')
...

В начале функции, в блоке:

...
    # if 'mysql_*' doesn't set in config for $site - skip it
    try:
        mysql_host = parser.get(site, 'mysql_host')
...

Проверяем – есть ли параметры для MySQL вообще, если их нет – значит создавать бекап БД не надо.

В функции backup() модуля backup.py добавляем вызов db_backup(), аналогично www_backup():

...
            # exec mysql database dump
            db_backup(site, db_backup_file, parser)
...

База example_db, пользователь и пароль на рабочей машине уже созданы, запускаем скрипт:

./sitebackup.py
Got own settings:
backup_root_path = /backups
backup_files_dir = /backups/files
backup_db_dir = /backups/databases
Checking directories:
/backups - found, OK.
/backups/files - found, OK.
/backups/databases - found, OK.
Creating WWW backup for:
site: example
from: /tmp/testbak/
to: /backups/files/03-01-2018-14-10_example_.gz
WWW backup done.
Creating DB backup for:
site: example
host: localhost
database: example_db
user: example_db_user
to: /backups/databases/03-01-2018-14-10_example_example_db.sql
DB backup done.

Копирование в AWS S3

Следующий шаг – скопировать полученные файлы в корзину S3, если это указано в параметрах сайта в файле настроек.

В секцию [example] файла simple-site-backup.ini добавим параметр aws_s3_sync:

...
aws_s3_sync = yes
...

И заодно – имя корзины и данные доступа, в результате секция [example] выглядит так:

...
[example]
www_data_path = /tmp/testbak/
mysql_db = example_db
mysql_host = localhost
mysql_user = example_db_user
mysql_pass = example_db_pass
aws_s3_sync = yes
aws_s3_bucket = example-bucket
aws_access_key = EXAMPLEKEY
aws_secret_key = EXAMPLESECRET

Что бы скрипт смог загрузить данные в корзину – ему потребуется модуль boto3, добавим проверку его наличия в системе.

Вернёмся к нашему модулю common.py, в котором добавим функцию проверки check_deps() и заодно pip_install():

...
def pip_install(pkg_name):
    pip.main(['install', pkg_name])


def check_deps():

    print('\nChecking for dependencies:')

    try:
        import boto3

        print('boto3 library already installed - OK.\n')
    except ImportError:
        print('boto3 not found - going to install it now...\n')
        pip_install('boto3')
        print('\nDone - please, restart script nnow.\n')
        exit(0)
...

Теперь в функции backup() модуля backup.py можно добавить проверки – сначала проверим надо ли копировать бекапы в S3 ('aws_s3_sync' == 'yes'), затем – проверим зависимости:

...

            # exec mysql database dump
            db_backup(site, db_backup_file, parser)

            # check for S3 sync first
            # if section/site have 'aws_s3_sync' = 'yes' then check and install dependencies
            try:
                if parser.get(site, 'aws_s3_sync') == 'yes':
                    common.check_deps()
                else:
                    print('Site {} doesn\'t marked as to be synced with AWS S3, skipping.\n'.format(site))
            except configparser.NoOptionError:
                pass

Запускаем:

./sitebackup.py
...
user: example_db_user
to: /backups/databases/03-01-2018-14-17_example_example_db.sql
DB backup done.
Checking for dependencies:
boto3 library already installed - OK.

Тут всё хорошо – можно добавлять копирование данных в корзину.

boto3 client

Вынесем всю логику, связанную с AWS  в отдельный модуль, назовём его s3sync.py, разместим в каталоге lib.

В модуле создаём две функции – одна, create_client() – для авторизации boto3 и создания объекта botocore.client.S3, вторая – непосредственно будет загружать переданный аргументом файл в корзину, которая указана в файле настроек для секции/сайта.

Сначала обновим сам файл настроек – добавим новые параметры:

...
aws_s3_sync = yes
aws_s3_bucket = setevoy-example-bucket
aws_access_key = EXAMPLEKEY
aws_secret_key = EXAMPLESECRET

Теперь в s3sync.py создадим функцию create_client():

...
def create_client(access_key, secret_key):

    s3_client = boto3.client('s3',
                             aws_access_key_id=access_key,
                             aws_secret_access_key=secret_key,)

    return s3_client

И вторую – upload(), которая принимает три аргумента – имя сайта (для поиска секции в файле настроек), список файлов для загрузки (в виде Python списка) и объект parser для работы с файлом настроек:

...
def upload(site, files, parser):

    try:
        aws_access_key = parser.get(site, 'aws_access_key')
        aws_secret_key = parser.get(site, 'aws_secret_key')
        aws_s3_bucket = parser.get(site, 'aws_s3_bucket')
        s3 = create_client(aws_access_key, aws_secret_key)
    except configparser.NoOptionError as e:
        print('ERROR: no "aws_access_key" and "aws_secret_key" options found'
              'in the configuration file for the {} site: {}.'.format(site, e))
        exit(1)

    for file in files:

        print('Uploading {} to S3 bucket {} as {}'.format(file, aws_s3_bucket, os.path.basename(file)))
        s3.upload_file(file, aws_s3_bucket, Key=os.path.basename(file))

    response = s3.list_objects(Bucket=aws_s3_bucket)
    print('\nExisting data in the {} bucket:\n'.format(aws_s3_bucket))
    for file in response['Contents']:
        print(file['Key'])

В функции backup() модуля backup.py добавляем её вызов – s3sync.upload():

...
            # check for S3 sync first
            # if section/site have 'aws_s3_sync' = 'yes' then check and install dependencies
            try:
                if parser.get(site, 'aws_s3_sync') == 'yes':
                    common.check_deps()
                    s3sync.upload(site, [www_backup_file, db_backup_file], parser)
                else:
                    print('Site {} doesn\'t marked as to be synced with AWS S3, skipping.\n'.format(site))
            except configparser.NoOptionError:
                pass
...

Тут в списке [www_backup_file, db_backup_file] мы передаём файлы, имена которых были заданы чуть выше, при создании бекапа файлов и базы данных.

Примечание: создайте отдельного пользователя в AWS IAM для работы с бекапами, у которого будет доступ только к определённой корзине.

Обновим файл настроек, указываем реальные aws_access_key и aws_secret_key, пробуем:

./sitebackup.py
...
to: /backups/databases/03-01-2018-14-51_example_example_db.sql
DB backup done.
Checking for dependencies:
boto3 library already installed - OK.
Uploading /backups/files/03-01-2018-14-51_example_.gz to S3 bucket setevoy-example-bucket as 03-01-2018-14-51_example_.gz
Uploading /backups/databases/03-01-2018-14-51_example_example_db.sql to S3 bucket setevoy-example-bucket as 03-01-2018-14-51_example_example_db.sql
Existing data in the setevoy-example-bucket bucket:
03-01-2018-14-51_example_.gz
03-01-2018-14-51_example_example_db.sql

Удаление старых бекапов

Последний шаг – добавить возможность удаления старых архивов из локального хранилища в /backups (S3 корзина может иметь свои собственные политики).

Возвращаемся к модулю common.py, добавляем функцию bkps_cleanup(), которая принимает три аргумента – имя сайта, каталоги для поиска старых файлов и parser:

...
def bkps_cleanup(site, dirs, parser):

    now = time.time()

    try:
        keep_days = parser.get(site, 'bkps_keep_days')
    except configparser.NoOptionError as e:
        print('WARNING: {}.'.format(e))
        keep_days = parser.get('defaults', 'bkps_keep_days')
        print('Using default value: {}.\n'.format(keep_days))

    for d in dirs:
        for f in os.listdir(d):
            if os.stat(os.path.join(d, f)).st_mtime < now - int(keep_days) * 86400:
                print('Deleting file: {}'.format(os.path.join(d, f)))
                os.remove(os.path.join(d, f))
            else:
                print('Keeping local data: {}'.format(os.path.join(d, f)))

В файле настроек для [defaults] и [example] добавляем параметр bkps_keep_days, который указывает кол-во дней, которое необходимо хранить копии:

...
[defaults]
bkps_keep_days = 10
aws_s3_sync = no

[example]
www_data_path = /tmp/testbak/
mysql_db = example_db
mysql_host = localhost
mysql_user = example_db_user
mysql_pass = example_db_pass
bkps_keep_days = 1
...

И вызов common.bkps_cleanup() в backup() модуля backup.py:

...
            # Cleanup section
            # delete files older then "bkps_keep_days" param
            common.bkps_cleanup(site, [files_destination_dir, db_destination_dir], parser)

Всё готово – запускаем:

./sitebackup.py
Got own settings:
backup_root_path = /backups
backup_files_dir = /backups/files
backup_db_dir = /backups/databases
Checking directories:
/backups - found, OK.
/backups/files - found, OK.
/backups/databases - found, OK.
Creating WWW backup for:
site: example
from: /tmp/testbak/
to: /backups/files/03-01-2018-16-01_example_.gz
WWW backup done.
Creating DB backup for:
site: example
host: localhost
database: example_db
user: example_db_user
to: /backups/databases/03-01-2018-16-01_example_example_db.sql
DB backup done.
Checking for dependencies:
boto3 library already installed - OK.
Uploading /backups/files/03-01-2018-16-01_example_.gz to S3 bucket setevoy-example-bucket as 03-01-2018-16-01_example_.gz
Uploading /backups/databases/03-01-2018-16-01_example_example_db.sql to S3 bucket setevoy-example-bucket as 03-01-2018-16-01_example_example_db.sql
Existing data in the setevoy-example-bucket bucket:
03-01-2018-14-51_example_.gz
03-01-2018-14-51_example_example_db.sql
03-01-2018-16-01_example_.gz
03-01-2018-16-01_example_example_db.sql
Keeping data: /backups/files/03-01-2018-14-03_example_.gz
...
Keeping data: /backups/databases/03-01-2018-16-01_example_example_db.sql

Т.к. файлов старше одного дня пока нет – то ничего не удалилось.

Вроде всё готово? Запускаем на проде – сервере с RTFM.

Установка и запуск

Клонируем репозиторий:

14:10:52 [root@ip-172-31-43-63 /opt] # git clone https://github.com/setevoy2/simple-site-backup.git
Cloning into 'simple-site-backup'...
remote: Counting objects: 33, done.
remote: Compressing objects: 100% (18/18), done.
remote: Total 33 (delta 9), reused 30 (delta 9), pack-reused 0
Unpacking objects: 100% (33/33), done.
Checking connectivity... done.

Создаём каталог для своего файла настроек:

14:11:42 [root@ip-172-31-43-63 /opt] # mkdir /usr/local/etc/simple-site-backup

В нём сам файл /usr/local/etc/simple-site-backup/rtfm-prod-backups.ini:

[backup-settings]
backup_root_path = /backups
backup_files_dir = files
backup_db_dir = databases

[defaults]
bkps_keep_days = 7
aws_s3_sync = no

[rtfm]
www_data_path = /var/www/vhosts/rtfm/
mysql_db = rtfm_db1
mysql_host = 172.***.***.60
mysql_user = dbUser
mysql_pass = dbPass
bkps_keep_days = 7
aws_s3_sync = yes
aws_s3_bucket = setevoy-rtfm-simple-backups
aws_access_key = AKI***ROA
aws_secret_key = Q1r***255

Проверяем:

14:21:09 [root@ip-172-31-43-63 /opt] # ./simple-site-backup/sitebackup.py -c /usr/local/etc/simple-site-backup/rtfm-prod-backups.ini
Got own settings:
backup_root_path = /backups
backup_files_dir = /backups/files
backup_db_dir = /backups/databases
Checking directories:
/backups - found, OK.
/backups/files - not found, creating...
/backups/databases - not found, creating...
Creating WWW backup for:
site: rtfm
from: /var/www/vhosts/rtfm/
to: /backups/files/03-01-2018-14-21_rtfm_.gz
WWW backup done.
Creating DB backup for:
site: rtfm
host: 172.***.***.60
database: rtfm_db1
user: setevoy
to: /backups/databases/03-01-2018-14-21_rtfm_rtfm_db1.sql
DB backup done.
Checking for dependencies:
boto3 library already installed - OK.
Uploading /backups/files/03-01-2018-14-21_rtfm_.gz to S3 bucket setevoy-rtfm-simple-backups as 03-01-2018-14-21_rtfm_.gz
Uploading /backups/databases/03-01-2018-14-21_rtfm_rtfm_db1.sql to S3 bucket setevoy-rtfm-simple-backups as 03-01-2018-14-21_rtfm_rtfm_db1.sql
Existing data in the setevoy-rtfm-simple-backups bucket:
03-01-2018-14-21_rtfm_.gz
03-01-2018-14-21_rtfm_rtfm_db1.sql
Starting local backups storage cleanup...
Keeping local data: /backups/files/03-01-2018-14-21_rtfm_.gz
Keeping local data: /backups/databases/03-01-2018-14-21_rtfm_rtfm_db1.sql

Всё хорошо.

Глянем локальные файлы:

14:23:04 [root@ip-172-31-43-63 /opt] # ls -l /backups/{files,databases}
/backups/databases:
total 70844
-rw-r--r-- 1 root root 72540175 Jan  3 14:22 03-01-2018-14-21_rtfm_rtfm_db1.sql
/backups/files:
total 1002408
-rw-r--r-- 1 root root 1026461663 Jan  3 14:22 03-01-2018-14-21_rtfm_.gz

И корзина:

Последним шагом – добавляем задачу в крон для запуска раз в сутки ,в час ночи:

0 1 * * * /opt/simple-site-backup/sitebackup.py -c /usr/local/etc/simple-site-backup/rtfm-prod-backups.ini >> /var/log/simple-backup.log

Создаём файл лога:

touch /var/log/simple-backup.log

Готово.

#ToDo/improvements

  • дописать README.md
  • бекап файлов создаётся полный – можно было бы добавить инкрементальное создание (пример тут>>>)
  • бекап базы создаётся в виде sql-файла, без сжатия – можно было добавить (но у меня базы сравнительно небольшие)
  • Logger() для ведения своего лога, без перенаправления вывода через >> в кроне
  • удаление старых бекапов из корзины самим скриптом, а не политиками
  • вынести создание имени файла бекапа в соответствующую функцию (www_backup() или db_backup()) для корректной обработки ошибок в backup()

#Bugs

  • добавить правильную обработку ошибок в backup(), если параметр не найден в конфиге
  • пропускать попытку загрузки файла, если параметр не найден в конфиге и файл бекапа не был создан