The Ultimate MySQL Backup Script in Bash

So, just as many smart people in the world do, when I needed a MySQL backup script, I first googled it. To my surprise, I couldn’t find a single script that fit my needs. They were either far too basic (e.g. simple mysqldump without any kind of error reporting) or an entire program that needed to be deployed, configured and setup (e.g. Zmanda). I set out to write a bash script that was easy enough for someone to use who didn’t have the depth and knowledge of a Linux/Bash, but also could be used by a sysadmin who wanted something simple and quick to deploy. I think that I’ve accomplished just that, but I’ll let you be the judge. The source code (at the time of writing) can be found below or the latest at github.

#!/bin/bash
# Author: Jason Barnett <[email protected]>
MYSQL_BACKUP_VERSION=1.0

############
## README ##
############
##
##  Assumptions this script makes:
##  ------------------------------
##    -The script itself is run by root
##    -root's home is /root
##    -MySQL Server version is >= 5.1.2 (That's when the whole --routines switch was added to mysqldump)
##    -crontab is configured to email you, help can be found here:
##      https://www.cyberciti.biz/faq/linux-unix-crontab-change-mailto-settings/
##
##  How to use this script:
##  -----------------------
##    Simply run this script as is once and it should ask all the right questions and place them in an easy
##    to reference config file so it never needs to ask again.
##
##    I suggest setting up a cronjob with this script once you've confirmed it's running as expected
##    and then putting it in your crontab, e.g.:
##
##      0 2 * * * /path/to/mysql-backup.sh > /dev/null
##
##    The reason behind my suggestion is that most people have (or sould have) cron setup so that it only
##    emails them when there is any output (both stdout or stderr) from the script/command/etc... I've
##    written this script to output to stderr when you need to be notified by email about a problem, e.g.
##    it's unable to grab a full list of databases, or it can't authenticate, etc... All of the stdout output
##    can be safely ignored. In order for this to work properly though, it requires cron daemon to be configured
##    properly so you can receive email from it when things go awry.
##
###########

## Functions ##
###############

function ask_question {
    question=$1
    read -p "$question: "
    echo $REPLY
}

function ask_yes_no {
    question=$1
    read -p "$question: [y/n] "
    local answer=$(echo $REPLY | tr '[:upper:]' '[:lower:]')

    while [[ "${answer}" != "yes" && "${answer}" != "no" && "${answer}" != "y" && "${answer}" != "n" ]];do
        read -p "y/n only please... $question: [y/n] "
        answer=$(echo $REPLY | tr '[:upper:]' '[:lower:]')
    done

    [[ "${answer}" == "yes" || "${answer}" == "y" ]] && echo true || echo false
}

function msg {
    echo "$1"
}

function err_msg {
    echo "$1" 1>&2
}

function fail_msg {
    echo "$1" 1>&2
    exit 1
}

function update_config {
    local config_property config_value
    config_property=$(echo $1 | awk -F= '{print $1}')
    config_value=$(echo $1 | awk -F= '{print $2}')

    [[ ! -d ${HOME}/.config/mysql-backup ]] && mkdir -p ${HOME}/.config/mysql-backup
    chmod 0700 ${HOME}/.config/mysql-backup

    [[ ! -e ${HOME}/.config/mysql-backup/config ]] && touch ${HOME}/.config/mysql-backup/config
    chmod 0600 ${HOME}/.config/mysql-backup/config

    sed -i "/^${config_property}=/d" ${HOME}/.config/mysql-backup/config
    echo "${config_property}=${config_value}" >> ${HOME}/.config/mysql-backup/config
}

function get_mysql_credentials {
    MYSQL_USER=$(ask_question "MySQL Username")
    [[ -z $MYSQL_USER ]] && exit 1
    MYSQL_HOST=$(ask_question "MySQL Host")
    MYSQL_PASS=$(ask_question "MySQL Password")
    [[ -n ${MYSQL_USER} && -n ${MYSQL_HOST} && -n ${MYSQL_PASS} ]] || {
        fail_msg "You have not specified a MySQL Username, Host and/or Password"
    }

    check_mysql_credentials
    update_config "MYSQL_USER=${MYSQL_USER}"
    update_config "MYSQL_HOST=${MYSQL_HOST}"
    update_config "MYSQL_PASS=${MYSQL_PASS}"
}

function check_mysql_credentials {
    temp_file=$(mktemp /tmp/.mysql-backup.XXXXXX)
    local good_credentials=

    mysql -u${MYSQL_USER} -h${MYSQL_HOST} -p${MYSQL_PASS} \
        -BNe 'show databases;' &> ${temp_file} && good_credentials=true

    if [[ -z $good_credentials ]];then
        fail_msg "You have a bad MySQL Username, Host and/or Password"
    fi

    rm -f ${temp_file}
}

function ask_about_routines {
    ROUTINES=$(ask_yes_no "Dump stored routines?")
    update_config "ROUTINES=${ROUTINES}"
}

function get_backup_destination {
    BACKUP_DEST=$(ask_question "Choose a backup destination, absolute path only")

    while [[ $(echo ${BACKUP_DEST} | egrep '^((\/[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*(\-[a-zA-Z0-9]+)*)+)$') == "" ]];do
        BACKUP_DEST=$(ask_question "I said absolute path only...")
    done

    update_config "BACKUP_DEST=${BACKUP_DEST}"
}

## MAIN SCRIPT ##
#################

# Check if mysql server even exists on the machine and exit if it's not.
[[ -x /usr/bin/mysqld_safe ]] || { echo "MySQL-Server is not installed on this machine."; exit 0; }

# Set HOME and load config (We set HOME here because most likely we're running from the crontab and HOME isn't set)
HOME=/root
[[ -e ${HOME}/.config/mysql-backup/config ]] && . ${HOME}/.config/mysql-backup/config

# Check for MySQL credentials
[[ -n ${MYSQL_USER} && -n ${MYSQL_HOST} && -n ${MYSQL_PASS} ]] && check_mysql_credentials || get_mysql_credentials

# Should we dump routines?
[[ -n ${ROUTINES} && (${ROUTINES} == "true" || ${ROUTINES} == "false") ]] || ask_about_routines
[[ ${ROUTINES} == "true" ]] && ROUTINES='--routines' || ROUTINES=

# Check for backup destination, create it if it doesn't exist, and set the correct permissions
[[ -n ${BACKUP_DEST} ]] || get_backup_destination
[[ ! -d ${BACKUP_DEST} ]] && mkdir -p ${BACKUP_DEST}
chown root:root -R ${BACKUP_DEST}
chmod 0700 ${BACKUP_DEST}

# Locate gzip binary and try to use pigz (multi-threaded gzip), or fallback and use gzip
GZIP=`which pigz 2> /dev/null`
[[ -z ${GZIP} ]] && { GZIP=`which gzip 2> /dev/null`; msg "INFO: You don't have pigz installed, using gzip instead."; msg "      This is not a big deal, pigz simply speeds up the backup process."; }

# Locate mysqldump binary
MYSQLDUMP=`which mysqldump 2> /dev/null`
[[ -z ${MYSQLDUMP} ]] && fail_msg "Unable to locate \"mysqldump\". Make sure it's in your \$PATH."

# Get a list of all databases
DBs="$(mysql -u${MYSQL_USER} -h${MYSQL_HOST} -p${MYSQL_PASS} -BNe 'show databases;' | egrep -v '^(information_schema)$')"
mysql_status=$?

[[ $mysql_status != "0" ]] && fail_msg "There was an issue grabbing a complete list of databases to backup."

backup_failed=
for db in ${DBs};do
    FILE="${BACKUP_DEST}/${db}.sql.gz"
    [ -f ${FILE}.2 ] && rm -f ${FILE}.2
    [ -f ${FILE}.1 ] && mv ${FILE}.1 ${FILE}.2
    [ -f ${FILE}.0 ] && mv ${FILE}.0 ${FILE}.1
    [ -f ${FILE} ] && mv ${FILE} ${FILE}.0
    echo -n "Backing up $db... "
    ${MYSQLDUMP} -u${MYSQL_USER} -h${MYSQL_HOST} -p${MYSQL_PASS} $ROUTINES -B ${db} | $GZIP -9 > ${FILE}
    if [[ $? == "0" ]];
        then
            msg Success!
        else
            err_msg Failed!
            backup_failed=true
            failed_dbs="$db $failed_dbs"
    fi
done

[[ $backup_failed == "true" ]] && fail_msg "There was an issue backing up the following databases: ${failed_dbs}"

Loading Comments...