Python: скрипт бекапа баз данных MySQL/MariaDB

Автор: | 16/10/2014
 

PythonВ дополнение к скриптуPython: скрипт инкрементального или полного бекапа файлов – второй, который выполняет полное копирование баз данных.

В целях “совместимости” с первым скриптом – данные так же хранятся в отдельных daily и weekly директориях, хотя в отличии от файлов тут не выполняется инкрементальный бекап – базы небольшие, а после сжатия bzip2 совсем маленькие:

# ls -l /home/setevoy/backups/setevoy/daily/
total 16
drwxr-xr-x 2 root root 4096 Oct 13 23:15 2014-10-13-files
drwxr-xr-x 2 root root 4096 Oct 14 04:00 2014-10-14-files
drwxr-xr-x 2 root root 4096 Oct 15 17:49 2014-10-15-database
drwxr-xr-x 2 root root 4096 Oct 15 08:44 2014-10-15-files

Скрипт выполняет “обход” по каждой базе, к которой есть доступ у пользователя (за исключением баз, перечисленных в переменной EXCLUDE):

# ls -lh /home/setevoy/backups/setevoy/weekly/2014-10-15-database/
total 3.1M
-rw-r--r-- 1 root root  805 Oct 15 18:02 autocomtestdb.sql.bz2
-rw-r--r-- 1 root root  417 Oct 15 18:02 mysqlslap.sql.bz2
-rw-r--r-- 1 root root 4.9K Oct 15 18:02 old_money_db1.sql.bz2
-rw-r--r-- 1 root root 3.0M Oct 15 18:02 setevoy_forum.sql.bz2
-rw-r--r-- 1 root root 5.1K Oct 15 18:02 setevoy_money_db1.sql.bz2
-rw-r--r-- 1 root root  89K Oct 15 18:02 setevoyorgua_db1.sql.bz2
-rw-r--r-- 1 root root  843 Oct 15 18:02 setevoy_test.sql.bz2
-rw-r--r-- 1 root root 1.8K Oct 15 18:02 themega_chars.sql.bz2
MariaDB [(none)]> select user, db from mysql.db;
+-------------+-------------------+
| user        | db                |
+-------------+-------------------+
| setevoy     | setevoy_test      |
| setevoy     | autocomtestdb     |
...
| vexim       | vexim             |
| worlddesign | worlddesign_db1   |
| zabbix      | zabbix            |
+-------------+-------------------+
16 rows in set (0.00 sec)

К примеру, база RTFM занимает:

# du -sh /var/lib/mysql/rtfm_db1/
47M     /var/lib/mysql/rtfm_db1/

А её бекап после сжатия:

# ls -lh ../backups/rtfm/daily/2014-10-16-database/
total 3.2M
-rw-r--r-- 1 root root 3.2M Oct 16 03:00 rtfm_db1.sql.bz2

Так же, тут нет функции для удаления старых каталогов с данными – этим занимается функция clear_old_dirs() из скрипта files_backup.py:

    clear_old_dirs(user, day, count, host, bkptype)
Deletes old directories. Depending on [count] – can store 7
(for daily) or 4 (for weekly) copies, including today’s archive.

Поэтому, скрипт запускается по cron-у перед выполнением бекапа файлов:

# crontab -l
...
0 03 * * * /home/setevoy/opt/mysql_backup.py
0 04 * * * /home/setevoy/opt/files_backup.py

Лог выглядит так:

mysql_backup.py - INFO [2014-10-15 18:29:47,028] Backup started at: 2014-10-15 18:29:47
mysql_backup.py - INFO [2014-10-15 18:29:47,041] Today is Wed, will use backuptype daily.
mysql_backup.py - INFO [2014-10-15 18:29:47,042] Directory /home/setevoy/backups/setevoy/daily/2014-10-15-database/ created
mysql_backup.py - INFO [2014-10-15 18:29:47,055] Database backup complete: database setevoy_test saved to /home/setevoy/backups/setevoy/daily/2014-10-15-database/setevoy_test.sql.bz2
mysql_backup.py - INFO [2014-10-15 18:29:47,060] Database mysqlslap in exclude list, skip
...
mysql_backup.py - INFO [2014-10-15 18:29:47,068] Backup /home/setevoy/backups/hudeem/daily/2014-10-15-database/hudeem_db.sql.bz2 already present, skip
....
mysql_backup.py - INFO [2014-10-15 18:29:57,337] Email sent from ['[email protected]'] to ['[email protected]'].
mysql_backup.py - INFO [2014-10-15 18:29:57,337] Backup finished at: 2014-10-15 18:29:57

И сам скрипт:

#!/usr/bin/env python

'''
Create full backup every user's database.

Creation date: 15.10.2014
Last modify date: 16.10.2014
'''

import MySQLdb
import socket
import os
import logging
import time
import subprocess
import sys
import smtplib
import mimetypes
import email
import email.mime.application
import socket

BACKDIR = '/home/setevoy/backups/'
LOGPATH = '/var/log/database_backup.log'
DBROOT = 'root'
_DBROOTPW = 'PasswordHere'
DBHOST = 'localhost'

# exclude databases list - will not be added to backup
EXCLUDE = 'mysqlslap'
HOST = socket.gethostname()

logging.basicConfig(format = '%(filename)s - %(levelname)-3s [%(asctime)s] %(message)s ', filename=LOGPATH, level=logging.DEBUG)


def dir_bkptype(day):

    '''On Sunday - create full weekly backup with 'full_backup()';
       other days - incremental backup with inc_backup()';
       also used in 'back_dir_create()' to create BACKDIR/weekly or daily;
       also used in 'clear_old_dirs()' for deleting old directories.'''

    if day == 'Mon':
        bkptype = 'weekly'
    else:
        bkptype = 'daily'

    logging.info('Today is %s, will use backuptype %s.' % (day, bkptype))

    return(bkptype)


def back_dir_create(user, bkptype):

    '''Creates new directory for each day;
       depending on data recieved from 'dir_bkptype()'
       can create 'weekly' or 'daily' directory.'''

    dirname = os.path.join(BACKDIR, user, bkptype, '%s-database/' % time.strftime('%Y-%m-%d'))

    if not os.path.exists(dirname):
        os.makedirs(dirname)
        logging.info('Directory %s created' % dirname)

    return(dirname)

def mysql_connect(dbroot, dbhost, dbrootpw):

    '''Calls in 'run_backup()' with [DBROOT], [_DBROOTPW] and [DBHOST]
       in arguments.
       Creates object [cursor] for use in select_user_dbs()'''

    db = MySQLdb.connect(dbhost, dbroot, dbrootpw)
    cursor = db.cursor()

    return(cursor)


def select_user_dbs(cursor):

    '''Calls with [cursor] from 'mysql_connect()';
       returns two lists like:
       for users - ['setevoy', 'setevoy', 'hudeem', [...] 'vexim', 'worlddesign', 'zabbix']
       and their databases: ['setevoy_test', 'autocomtestdb', 'hudeem_db', [...] 'vexim', 'worlddesign_db1', 'zabbix']'''

    users_list = []
    database_list = []

    cursor.execute('select user, db from mysql.db')

    for line in cursor.fetchall():
        users_list.append(line[0])
        database_list.append(line[1])

    return(users_list, database_list)

    cursor.close()


def mysql_dump(dbroot, dbrootpw, database, archname):

    '''Calls in 'run_backup()' to create dump'''

    subprocess.call('mysqldump -u %s -p%s %s | bzip2 > %s' % (dbroot, dbrootpw, database, archname), shell=True)
    logging.info('Database backup complete: database %s saved to %s' % (database, archname))


def sendmail():

    '''Send email report to [to] with used disk space information.'''

    sender = '[email protected]'
    to = ['[email protected]']

    msg = email.mime.Multipart.MIMEMultipart()

    msg['Subject'] = ('Database backup report')
    msg['From'] = 'Backup Manager <[email protected]>'
    msg['To'] = 'root <[email protected]>'

    body = email.mime.Text.MIMEText("""

    Databases backup on %s finished.

    """ % (HOST))

    msg.attach(body)

    smtpconnect = smtplib.SMTP('localhost:25')
    #smtpconnect.set_debuglevel(1)
    smtpconnect.sendmail(sender, to, msg.as_string())
    smtpconnect.quit()
    logging.info('Email sent from ['%s'] to %s.' % (sender, to))

def run_backup():

    '''Main function.
       cursor - creates 'cursor' object from 'mysql_connect()' first;
       user, database - receives lists (users_list[] and database_list[]) from 'select_user_dbs()';
       bkptype - determines backup type (weekly, daily) from 'dir_bkptype()'.

       For each user looks for it's databases, creates directory,
       and if there is no backup ('archname') yet - calls 'mysql_dump()' function.'''

    cursor = mysql_connect(DBROOT, DBHOST, _DBROOTPW)
    user, database = select_user_dbs(cursor)
    bkptype = dir_bkptype(curday)

    curday = time.strftime('%a')

    for user, database in zip(user, database):

        if database not in EXCLUDE:

            backup_dir = back_dir_create(user, bkptype)

            if backup_dir:

                archname = (backup_dir + database + '.sql.bz2')

                if not os.path.isfile(archname):
                    mysql_dump(DBROOT, _DBROOTPW, database, archname)
                else:
                    logging.info('Backup %s already present, skip' % archname)

                else:
            logging.info('Database %s in exclude list, skip' % database)

    sendmail()

if __name__ == '__main__':
    starttime = time.strftime('%Y-%m-%d %H:%M:%S')
    logging.info('Backup started at: %s' % starttime)
    run_backup()
    finishtime = time.strftime('%Y-%m-%d %H:%M:%S')
    logging.info('Backup finished at: %s' % finishtime)