Задача – набросать скрипт, который умел бы создавать бекап нескольких сайтов и загружать их в AWS S3 корзину.
Ниже описан процесс написания такого скрипта (или, скорее, уже даже “утилиты”, т.к. имеются модули и файл настроек), без особых деталей по работе и реализации самих функций – больше информации о процессе создания утилиты, её структуре и логике выполнения.
Скрипт писался на Python 3, работает и на Python 2 (только потребует установки вручную configparser
и pip
), работает под Linux и, теоретически – под Windows, ибо Python.
UPD: уже вечером перечитывал код – нашёл пару Bugs, надо будет обновить. Ну и вообще тут есть поле для #ToDo/improvements.
Структура каталогов будет следующая:
lib
: для файлов модулейconf
: для дефолтного файла настроек
Сейчас всё вместе выглядит так:
[simterm]
$ 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
[/simterm]
Полностью скрипт доступен в 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)
Проверяем:
[simterm]
$ ./sitebackup.py {'config': 'conf/simple-site-backup.ini'} $ ./sitebackup.py -c testconf {'config': 'testconf'}
[/simterm]
Модуль backup
Теперь можно приступать к добавлению модулей. Что бы не загромождать код основного файла и корневой каталог – создаём в нём каталог lib
:
[simterm]
$ mkdir lib
[/simterm]
Что бы Python принимал каталог lib
как модуль – добавляем пустой файл __init__.py
:
[simterm]
$ touch lib/__init__.py
[/simterm]
В каталоге 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'])
Запускаем:
[simterm]
$ ./sitebackup.py backup() with conf/simple-site-backup.ini
[/simterm]
ConfigParser
и файл настроек скрипта
Идея заключается в том, что бы иметь единый файл настроек, в котором будут описываться сайты, бекапы которых мы хотим делать.
При этом – должна быть возможность делать только бекап файлов и/или только бекап базы данных.
Создаём каталог conf
:
[simterm]
$ mkdir conf
[/simterm]
И в нём – файл simple-site-backup.ini
, в котором будет минимум три секции:
[backup-settings]
– тут указываем настройки для самого скрипта – в каком каталоге создавать локальные копии[defaults]
– тут зададим некоторые значения по умолчанию, которые можно будет переопределять в секциях сайтов[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) )
Вызываем, проверяем:
[simterm]
$ ./sitebackup.py Got own settings: backup_root_path = /backups backup_files_dir = /backups/files backup_db_dir = /backups/databases
[/simterm]
ОК, следующим шагом надо проверить – имеются ли локальные директории, в которые мы будем складывать бекапы, и если их нет – создать их.
Используем модуль 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])
Запускаем:
[simterm]
$ 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...
[/simterm]
Проверяем:
[simterm]
$ tree /backups/ /backups/ ├── databases └── files
[/simterm]
Бекап файлов
Теперь можно приступать к созданию бекапов. Для этого создадим две функции – одна будет создавать бекапы файлов – 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
:
[simterm]
$ mkdir /tmp/testbak/
[/simterm]
Тестовый файлик:
[simterm]
$ touch /tmp/testbak/testfile
[/simterm]
Запускаем скрипт:
[simterm]
$ ./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.
[/simterm]
С этим – всё, переходим к базам данных.
Бекап баз 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, пользователь и пароль на рабочей машине уже созданы, запускаем скрипт:
[simterm]
$ ./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.
[/simterm]
Копирование в 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
Запускаем:
[simterm]
$ ./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.
[/simterm]
Тут всё хорошо – можно добавлять копирование данных в корзину.
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
, пробуем:
[simterm]
$ ./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)
Всё готово – запускаем:
[simterm]
$ ./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
[/simterm]
Т.к. файлов старше одного дня пока нет – то ничего не удалилось.
Вроде всё готово? Запускаем на проде – сервере с RTFM.
Установка и запуск
Клонируем репозиторий:
[simterm]
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.
[/simterm]
Создаём каталог для своего файла настроек:
[simterm]
14:11:42 [root@ip-172-31-43-63 /opt] # mkdir /usr/local/etc/simple-site-backup
[/simterm]
В нём сам файл /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
Проверяем:
[simterm]
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
[/simterm]
Всё хорошо.
Глянем локальные файлы:
[simterm]
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
[/simterm]
И корзина:
Последним шагом – добавляем задачу в крон для запуска раз в сутки ,в час ночи:
0 1 * * * /opt/simple-site-backup/sitebackup.py -c /usr/local/etc/simple-site-backup/rtfm-prod-backups.ini >> /var/log/simple-backup.log
Создаём файл лога:
[simterm]
# touch /var/log/simple-backup.log
[/simterm]
Готово.
#ToDo/improvements
- дописать
README.md
- бекап файлов создаётся полный – можно было бы добавить инкрементальное создание (пример тут>>>)
- бекап базы создаётся в виде sql-файла, без сжатия – можно было добавить (но у меня базы сравнительно небольшие)
Logger()
для ведения своего лога, без перенаправления вывода через>>
в кроне- удаление старых бекапов из корзины самим скриптом, а не политиками
- вынести создание имени файла бекапа в соответствующую функцию (
www_backup()
илиdb_backup()
) для корректной обработки ошибок вbackup()
#Bugs
- добавить правильную обработку ошибок в
backup()
, если параметр не найден в конфиге - пропускать попытку загрузки файла, если параметр не найден в конфиге и файл бекапа не был создан