Skip to content

Instantly share code, notes, and snippets.

@cloudrck
Created June 10, 2016 02:37
Show Gist options
  • Save cloudrck/9863c05c3d0124ae094af804a138b0d2 to your computer and use it in GitHub Desktop.
Save cloudrck/9863c05c3d0124ae094af804a138b0d2 to your computer and use it in GitHub Desktop.
Bash Backup Script
#!/usr/bin/env bash
# This file:
#
# - Simple Bash script to mysqldump > Gzip > Rsync+ssh storage
# - It trys to avoid hardcoded login credentials
# - It assumes the user executing has ~/.my.cnf for mysqldump
#
# Version: 0.1
#
# Usage:
#
# ./Backup.sh -c [email protected] -p 2121 -r /dir/to/remotedir --mysql
#######
# Copyright (c) 2016 Daejuan Jacobs (https://blog.cloudrck.com)
#
# The following code utilized:
# bash3boilerplate - a template to write better bash scripts:
# https://github.com/kvz/bash3boilerplate
# Licensed under MIT
# Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io)
#
### Configuration
#####################################################################
# Exit on error. Append ||true if you expect an error.
# `set` is safer than relying on a shebang like `#!/bin/bash -e` because that is neutralized
# when someone runs your script as `bash yourscript.sh`
set -o errexit
set -o nounset
# Bash will remember & return the highest exitcode in a chain of pipes.
# This way you can catch the error in case mysqldump fails in `mysqldump |gzip`
set -o pipefail
# set -o xtrace
# Environment variables and their defaults
LOG_LEVEL="${LOG_LEVEL:-6}" # 7 = debug -> 0 = emergency
NO_COLOR="${NO_COLOR:-}" # true = disable color. otherwise autodetected
# Commandline options. This defines the usage page, and is used to parse cli
# opts & defaults from. The parsing is unforgiving so be precise in your syntax
# - A short option must be preset for every long option; but every short option
# need not have a long option
# - `--` is respected as the separator between options and arguments
read -r -d '' usage <<-'EOF' || true # exits non-zero when EOF encountered
-c --conn [arg] Connection String. Required.
-p --port [arg] SSH Port. Default is 22
-i --identity [arg] Identity file for ssh. Use full path. Default is /part/<USERNAME>/.ssh/id_rsa
-t --temp [arg] Location of directory. Default="/tmp/bq"
-v Enable verbose mode, print script as it is executed
-d --debug Enables debug mode
-b --database [arg] Database Name.
-h --help This page
-n --no-color Disable color output
-r --rsync-directory [arg] Set remote rsync direction /home/to/backupdir
-m --mysql Enable mysqldump
EOF
# Set magic variables for current file and its directory.
# BASH_SOURCE[0] is used so we can display the current file even if it is sourced by a parent script.
# If you need the script that was executed, consider using $0 instead.
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)"
__os="Linux"
if [[ "${OSTYPE:-}" == "darwin"* ]]; then
__os="OSX"
fi
### Functions
#####################################################################
function _fmt () {
local color_debug="\x1b[35m"
local color_info="\x1b[32m"
local color_notice="\x1b[34m"
local color_warning="\x1b[33m"
local color_error="\x1b[31m"
local color_critical="\x1b[1;31m"
local color_alert="\x1b[1;33;41m"
local color_emergency="\x1b[1;4;5;33;41m"
local colorvar=color_$1
local color="${!colorvar:-$color_error}"
local color_reset="\x1b[0m"
if [ "${NO_COLOR}" = "true" ] || [[ "${TERM:-}" != "xterm"* ]] || [ -t 1 ]; then
# Don't use colors on pipes or non-recognized terminals
color=""; color_reset=""
fi
echo -e "$(date -u +"%Y-%m-%d %H:%M:%S UTC") ${color}$(printf "[%9s]" ${1})${color_reset}";
}
function emergency () { echo "$(_fmt emergency) ${@}" 1>&2 || true; exit 1; }
function alert () { [ "${LOG_LEVEL}" -ge 1 ] && echo "$(_fmt alert) ${@}" 1>&2 || true; }
function critical () { [ "${LOG_LEVEL}" -ge 2 ] && echo "$(_fmt critical) ${@}" 1>&2 || true; }
function error () { [ "${LOG_LEVEL}" -ge 3 ] && echo "$(_fmt error) ${@}" 1>&2 || true; }
function warning () { [ "${LOG_LEVEL}" -ge 4 ] && echo "$(_fmt warning) ${@}" 1>&2 || true; }
function notice () { [ "${LOG_LEVEL}" -ge 5 ] && echo "$(_fmt notice) ${@}" 1>&2 || true; }
function info () { [ "${LOG_LEVEL}" -ge 6 ] && echo "$(_fmt info) ${@}" 1>&2 || true; }
function debug () { [ "${LOG_LEVEL}" -ge 7 ] && echo "$(_fmt debug) ${@}" 1>&2 || true; }
function help () {
echo "" 1>&2
echo " ${@}" 1>&2
echo "" 1>&2
echo " ${usage}" 1>&2
echo "" 1>&2
exit 1
}
function cleanup_before_exit () {
info "Cleaning up. Done"
}
trap cleanup_before_exit EXIT
### Parse commandline options
#####################################################################
# Translate usage string -> getopts arguments, and set $arg_<flag> defaults
while read line; do
# fetch single character version of option string
opt="$(echo "${line}" |awk '{print $1}' |sed -e 's#^-##')"
# fetch long version if present
long_opt="$(echo "${line}" |awk '/\-\-/ {print $2}' |sed -e 's#^--##')"
long_opt_mangled="$(sed 's#-#_#g' <<< $long_opt)"
# map long name back to short name
varname="short_opt_${long_opt_mangled}"
eval "${varname}=\"${opt}\""
# check if option takes an argument
varname="has_arg_${opt}"
if ! echo "${line}" |egrep '\[.*\]' >/dev/null 2>&1; then
init="0" # it's a flag. init with 0
eval "${varname}=0"
else
opt="${opt}:" # add : if opt has arg
init="" # it has an arg. init with ""
eval "${varname}=1"
fi
opts="${opts:-}${opt}"
varname="arg_${opt:0:1}"
if ! echo "${line}" |egrep '\. Default=' >/dev/null 2>&1; then
eval "${varname}=\"${init}\""
else
match="$(echo "${line}" |sed 's#^.*Default=\(\)#\1#g')"
eval "${varname}=\"${match}\""
fi
done <<< "${usage}"
# Allow long options like --this
opts="${opts}-:"
# Reset in case getopts has been used previously in the shell.
OPTIND=1
# start parsing command line
set +o nounset # unexpected arguments will cause unbound variables
# to be dereferenced
# Overwrite $arg_<flag> defaults with the actual CLI options
while getopts "${opts}" opt; do
[ "${opt}" = "?" ] && help "Invalid use of script: ${@} "
if [ "${opt}" = "-" ]; then
# OPTARG is long-option-name or long-option=value
if [[ "${OPTARG}" =~ .*=.* ]]; then
# --key=value format
long=${OPTARG/=*/}
long_mangled="$(sed 's#-#_#g' <<< $long)"
# Set opt to the short option corresponding to the long option
eval "opt=\"\${short_opt_${long_mangled}}\""
OPTARG=${OPTARG#*=}
else
# --key value format
# Map long name to short version of option
long_mangled="$(sed 's#-#_#g' <<< $OPTARG)"
eval "opt=\"\${short_opt_${long_mangled}}\""
# Only assign OPTARG if option takes an argument
eval "OPTARG=\"\${@:OPTIND:\${has_arg_${opt}}}\""
# shift over the argument if argument is expected
((OPTIND+=has_arg_${opt}))
fi
# we have set opt/OPTARG to the short value and the argument as OPTARG if it exists
fi
varname="arg_${opt:0:1}"
default="${!varname}"
value="${OPTARG}"
if [ -z "${OPTARG}" ] && [ "${default}" = "0" ]; then
value="1"
fi
eval "${varname}=\"${value}\""
debug "cli arg ${varname} = ($default) -> ${!varname}"
done
set -o nounset # no more unbound variable references expected
shift $((OPTIND-1))
[ "${1:-}" = "--" ] && shift
### Switches (like -d for debugmode, -h for showing helppage)
#####################################################################
# debug mode
if [ "${arg_d}" = "1" ]; then
set -o xtrace
LOG_LEVEL="7"
fi
# verbose mode
if [ "${arg_v}" = "1" ]; then
set -o verbose
fi
# help mode
if [ "${arg_h}" = "1" ]; then
# Help exists with code 1
help "Help using ${0}"
fi
### Validation (decide what's required for running your script and error out)
#####################################################################
[ -z "${arg_c:-}" ] && help "Setting a connection with -c or --conn is required"
[ -z "${LOG_LEVEL:-}" ] && emergency "Cannot continue without LOG_LEVEL. "
### Runtime
#####################################################################
debug "__file: ${__file}"
debug "__dir: ${__dir}"
debug "__base: ${__base}"
debug "__os: ${__os}"
debug "arg_d: ${arg_d}"
debug "arg_d: ${arg_b}"
debug "arg_v: ${arg_v}"
debug "arg_h: ${arg_h}"
debug "arg_m: ${arg_m}"
debug "arg_c: ${arg_c}"
debug "arg_r: ${arg_r}"
debug "arg_p: ${arg_p}"
############### Functions
# fully ISO-8601 compliant date
timestamp=$(date +%Y-%m-%dT%H:%M:%S%z)
if [ "${arg_p}" = "" ]; then
ssh_port=22
else
ssh_port=$arg_p
fi
if [ "${arg_t}" = "" ]; then
localbackup_dir="/tmp/bq"
else
localbackup_dir=$arg_t
fi
if [ "${arg_i}" = "" ]; then
# Get root directory of script
# Assumes it's the same root as the script user.
ROOT=${__dir#/} ROOT=/${ROOT%%/*}
myuser=$(whoami)
localssh_id="${ROOT}/${myuser}/.ssh/id_rsa"
else
localssh_id=$arg_i
fi
#
#
##### ToDo allow [email protected]:2121
#IFS=':' read -a hostarray <<< "$arg_c"
#echo "IP: ${hostarray[0]}"
#echo "port: ${hostarray[1]}"
#######
#
#
function mysqldb () {
db_name=${arg_b}
mkdir -p $localbackup_dir
cd $localbackup_dir
info "${arg_c}"
# This assumes you have a config file at ~/.my.cnf
# https://blog.cloudrck.com/
mysqldump $db_name | gzip > $db_name.sql.gz
sha1sum $db_name.sql.gz
# Currently you must have the remote host server fingerprint known, running via cron will
# cause this to hang for the amount of time specified by "LoginGraceTime"
# on the remote server if there is a change to the fingerprint since this
# will prompt you to proceed or halt connection.
# This will reject host keys not already in the list.
rsync -avz -e "ssh -i ${localssh_id} -p ${ssh_port} -o CheckHostIP=yes -o StrictHostKeyChecking=yes" --progress $db_name.sql.gz $arg_c:$arg_r/$db_name-$timestamp.gz
rm $db_name.sql.gz
}
#### Main
if [ "${arg_m}" = "1" ]; then
# Help exists with code 1
#help "Help using ${0}"
notice "Using MySql Dump"
mysqldb
notice "Backup Complete"
fi
info "End Backup Script"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment