Created
April 9, 2015 20:41
Rsync wrapper that snapshots an lvm volume and redirects rsync to use the snapshot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# This script is a wrapper around rsync that will snapshot the volume | |
# being synced and 'serve' that snapshot instead of the live data. | |
# This prevents errors generated by moved/deleted files (eg: maildir | |
# new/ -> cur/) | |
# It assumes that all volumes being synced by this script are in | |
# /my/base/path/... and that the path represents the root of a | |
# volume. It would need more intelligence if this isn't true. | |
# Call the script from the machine that will be the destination of the | |
# data like: | |
# rsync --rsync-path=/path/to/this/script \ | |
# remoteuser@remotehost:/my/base/path/foo/ /local/path/to/store/copy/ | |
# Shouldn't be necessary, but make sure. | |
export PATH=$PATH:/bin:/usr/bin:/sbin:/usr/sbin | |
PROG=$(basename $0) | |
LOGFILE=/path/to/logs/rsync-snap.$(date +%Y%m%d).$$.log | |
MAILTO=you@yourdomain.org | |
HOST=$(hostname -s) | |
#build a new list of args for rsync, with an alternate source (eg: a snapshot) | |
ARGS="" | |
# This is the volume we want to snapshot. if we look at all args and | |
# don't find a volume we like, we'll bail. | |
snap="" | |
# So we don't have to do this for everything. All stderr to logfile. | |
exec 2>>${LOGFILE} | |
#log a few bookkeeping items in the case of error/mailout | |
echo "Date: $(date +%c)" >&2 | |
echo "Host: ${HOST}" >&2 | |
echo "Pid: $$ (grep '$PROG' /var/log/misc | grep $$)" >&2 | |
echo "Logfile is: ${LOGFILE}" >&2 | |
# On exit trap, mail the log, etc... | |
trap "cat ${LOGFILE} | mail -s 'Error during Rsync Snapshot' ${MAILTO}" EXIT | |
# Bail on error, now that we've got a trap setup... | |
set -e | |
function dosyslog { | |
logger -p user.notice -t $PROG "(pid: $$) $1" | |
} | |
function log { | |
dosyslog "$1" | |
echo "$(date +%c): $1" >&2 | |
} | |
function logdie { | |
dosyslog "$1" | |
echo $1 >&2 | |
# Since we have the EXIT handler in place, this should trigger mail | |
# message. | |
exit 1 | |
} | |
# On with the show... | |
# Look at all the args and find one that matches volumes we want to | |
# snapshot instead of syning directly. Ignore all other args. | |
for arg in "$@"; do | |
case "$arg" in | |
# /tmp allows for testing without using critical volumes. | |
/my/base/path/*|/tmp*) | |
snap="$arg" | |
case "$arg" in | |
/tmp*) | |
devbase=/dev/${HOST} | |
base=temp | |
;; | |
*) | |
devbase=/dev/homes | |
base=$(basename $snap) | |
;; | |
esac | |
SNAPNAME=snap_${base} | |
# We need an individual name for the snap dev in case one volume | |
# bails before the mount and snapshot are cleaned up. this | |
# allows the script using this to still have a reasonable chance | |
# at syncing any remaining volumes. | |
SNAPDEV=${devbase}/${SNAPNAME} | |
ORIGDEV=${devbase}/${base} | |
# This is where we'll mount the snapshot that is actually taken. | |
SNAPMNT=/mnt/${SNAPNAME} | |
# Instead of just appending SNAPMNT, we munge the original | |
# volume name in a way that will preservce an existing trailing | |
# / if it was there. | |
ARGS="${ARGS}${SNAPMNT}" | |
# Requires bash v3+, but that's bog standard now. | |
if [[ $arg =~ .*/ ]]; then | |
ARGS="${ARGS}/" | |
fi | |
ARGS="${ARGS} " | |
;; | |
# Pass other args untouched. | |
*) ARGS="${ARGS}${arg} ";; | |
esac | |
done | |
# We're feeling helpful today. | |
cat << EOM >&2 | |
If you recieve this by mail, it is quite likely that there is a | |
leftover mountpoint (${SNAPMNT}) or snapshotted Logical Volume | |
(${SNAPDEV}). | |
To remedy: | |
if [[ \$(mount | grep ${SNAPMNT}) ]]; then | |
umount ${SNAPMNT} | |
fi | |
if [ -L "${SNAPDEV}" ]; then | |
lvremove ${SNAPDEV} | |
fi | |
The following was logged during the rsync run: | |
EOM | |
# Log this before testing $snap so that we have more to work with in | |
# the log file. | |
log "Original Arguments: $*" >&2 | |
if [ -z "$snap" ]; then | |
# It's _VERY_ important to die here as we don't want to set about | |
# creating/deleting snapshots of things we're unsure of!! | |
logdie "We didn't find a device to snapshot in the args passed to rsync." | |
fi | |
log "Modified Arguments: ${ARGS}" | |
# Our friendlier name for the dev is actually a symlink into | |
# /dev/mapper/... | |
if [ -L "${SNAPDEV}" ]; then | |
logdie "Snapshot device already exists. Aborting." | |
fi | |
if [ -n "$(mount | grep "${SNAPMNT}")" ]; then | |
logdie "Something is already mounted on ${SNAPMNT}. Aborting." | |
fi | |
[ -d "${SNAPMNT}" ] || mkdir -p ${SNAPMNT} >&2 | |
# Any non-rsync stdout must be redirected or else the rsync | |
# transaction will fail (typically with protocol mismatch errors, | |
# etc). | |
lvcreate -L 10G -s -n ${SNAPNAME} ${ORIGDEV} >&2 | |
log "Created lvm snapshot ${SNAPDEV} from ${ORIGDEV}" | |
mount ${SNAPDEV} ${SNAPMNT} >&2 | |
log "Mounted lvm snapshot ${SNAPDEV} on ${SNAPMNT}." | |
# It is imperative that stdout for rsync _not_ be touched/redirected | |
# or the transaction will stall and eventually timeout. | |
rsync ${ARGS} | |
log "Finished rsync of snapshotted volume." | |
umount ${SNAPMNT} >&2 | |
log "Unmounted lvm snapshot ${SNAPDEV} from ${SNAPMNT}." | |
lvremove -f ${SNAPDEV} >&2 | |
log "Removed lvm snapshot ${SNAPDEV}." | |
# For debugging, enable errors at various points. Uncomment the | |
# command below to get a dump of full logs, etc. | |
# logdie "Forced error...there are still active snapshots and mounts." | |
# Turn off this trap so we don't get mailed every time. | |
trap - EXIT | |
rm -f ${LOGFILE} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment