Задача — набросать скрипт, который умел бы создавать бекап нескольких сайтов и загружать их в 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(), если параметр не найден в конфиге - пропускать попытку загрузки файла, если параметр не найден в конфиге и файл бекапа не был создан

