Difference between revisions of "Rotating LVM snapshots for shadow copy"

 
Line 67: Line 67:
 
#
 
#
 
# SnapVolumes=('/dev/GROUP/foo,2000,500,1000' '/dev/GROUP/bar,3000,1000,2000')
 
# SnapVolumes=('/dev/GROUP/foo,2000,500,1000' '/dev/GROUP/bar,3000,1000,2000')
# SnapSets=(3 5 25)
+
# SnapSets=(2 5 20)
 
# OffDays="Sat Sun"
 
# OffDays="Sat Sun"
 
#
 
#

Revision as of 23:41, 24 November 2006

#!/bin/bash
#
# written by Christian Schwamborn
# bugs and suggestions to:
# christian.schwamborn[you-know-what-comes-here]nswit.de
#
# published under GNU General Public License v.2
# version 1.0.1 (2006-11-21)
#
# You are using this scrip at your own risk, the author is not responsible
# for data damage or losses in any way.
#
# What this is:
# You can use this script to create and manage snapshots for the Samba
# VFS module shadow_copy.
#
# How to use:
# The script provides some commanline parameters which are usefull for
# start/stop scrips (i.e. mount and unmount). Other parameters are usefull
# for cronjobs - add this, for a usual snapshot scenario (without trailing #)
# to your crontab:
#
# 0 12 * * 1-5 root /usr/local/sbin/smbsnap autosnap 0 0
# 0 7 * * 2-5 root /usr/local/sbin/smbsnap autosnap 0 1
# 0 7 * * 1 root /usr/local/sbin/smbsnap autosnap 0 2
# 3,33 * * * * root /usr/local/sbin/smbsnap autoresize all
#
# This takes snapshots at 7:00 and 12:00 every workday and checks every hour
# if a snapshot needs to be resized.
#
# The script has some flaws:
#   -This script currently works only with LVM2, no EVMS support yet
#   -XFS should be easy to implement, but it isn't yet
#   -You must not use dashes in your volumegroups or logical volumes
#   -Be carefull with the configuration, the parameters are not completely
#    checked right now, as the same for the command line parameters
#   -You have to keep track of the freespace of your volumegroups
#   -Be aware, that if your snapshots grow faster than you assumed, they will
#    become unusable. With the configuration shown above, this script checks
#    every 30 minutes if the snapshots are in the need of a resize. If
#    someone has a better idea how to check the snapshots than periodical,
#    let me know plaese.
#
# This script is written for the bash, other shells might work, it also uses
# some external commands: mount, umount, grep, date, bc, logger, lvcreate,
# lvremove, lvresize
#
# There are currently three variables that have to be configured:
#   -SnapVolumes is an array, every element of that array represents a logical
#    volume that is configured for snapshots. Each element is a comma seperated
#    list, which consists of the logical volume itself (i.e. /dev/GROUP1/foo),
#    the start size of the snapshot (in megabytes), the freespace which should
#    be maintained (in megabytes) and the space added, when a snapshot is
#    resized (also in megabytes).
#    The number of an element is used as a reference when calling the script
#   -SnapSets is also an array, currently every element just represents the
#    age (in days) of a snapshot of the specific snapshot-set.
#   -OffDays is a simple string with the none work days.
#
# The script will figure out by itself where to mount the snapshots, but the
# original logical volumes has to be mounted fist.
#
# Copy and adjust the following three variables (without #) to a blank file in
# /etc/samba and name it smbsnap.conf. If you place the configuration file
# elsewhere, make sure to adjust the path below.
#
# SnapVolumes=('/dev/GROUP/foo,2000,500,1000' '/dev/GROUP/bar,3000,1000,2000')
# SnapSets=(2 5 20)
# OffDays="Sat Sun"
#
###############################################################################

	. /etc/samba/smbsnap.conf

	export LANG=en_US.UTF-8
	export LANGUAGE=en_US:en
	SnapDate=$(date +%Y.%m.%d-%H.%M).00

	[ -z "${1}" ] || Command=${1}
	[ -z "${2}" ] || LVolume=${2}
	[ -z "${3}" ] || SnapSet=${3}


	# process a single snapshot
	# arguments: Command
	# needs:     SnapShot, VolumePath, SnapSets, OffDays, FreeSize, ReSize
	# provides:  na.
	# local:     cmd, SnapShotPath, CurrSnapSets, Count, Expire, Parameters, SnapState, CurrSize, FillPercet, CurrFreeSize
	function DoSnap()
	{
		cmd=${1}
		SnapShotPath=${VolumePath}/@GMT-$(echo ${SnapShot##*/} | cut -f3-4 -d\-)

		case ${cmd} in
			# to mount snapshots
			mount)
				[ -d ${SnapShotPath} ] || mkdir ${SnapShotPath} || \
					logger "${0}: ***error*** - unable to create mountpoint for ${SnapShot}"
				if mount | grep -q ${SnapShotPath}; then
					logger "${0}: ***error*** - snapshot ${SnapShot} is allready mounted to ${SnapShotPath}"
				else
					mount ${SnapShot} ${SnapShotPath} -o ro >/dev/null 2>&1 || \
						logger "${0}: ***error*** - can not mount ${SnapShot} to ${SnapShotPath}"
				fi
			;;

			# to unmount a snapshots
			umount)
				if mount | grep -q ${SnapShotPath}; then
					umount -f -l ${SnapShotPath} >/dev/null 2>&1 || \
						logger "${0}: ***error*** - can not unmount ${SnapShot} mounted to ${SnapShotPath}"
				else
					logger "${0}: ***error*** - snapshot ${SnapShot} is not mounted to ${SnapShotPath}"
				fi
			;;

			# to remove expired snapshots
			clean)
				CurrSnapSet=$(echo ${SnapShot##*/} | cut -f2 -d\-)
				if [ ${CurrSnapSet} -ge 0 ] && [ ${CurrSnapSet} -lt ${#SnapSets[@]} ]; then				
					Expire=$(echo ${SnapSets[${CurrSnapSet}]} | cut -f1 -d,)

					# add off-days, if any, to the expire time; we just count work-days
					declare -i Count=1
					while [ ${Expire} -ge ${Count} ];do
						echo ${OffDays} | grep -q $(date -d "-${Count} day" +%a) && Expire=$((${Expire} + 1))
						Count=$((${Count} + 1))
					done

					# compare date now minus expire-time with the snapshot-date
					if [ $(( $(date +%s) - ${Expire}*24*60*60 - 12*60*60)) -gt \
							$(date -d "$(echo ${SnapShot##*/} | cut -f3 -d\- |  tr \. \-) \
							$(echo ${SnapShot##*/} | cut -f4 -d\- |  tr \. \:)" +%s) ]; then
						# unmount snapshot
						if mount | grep -q ${SnapShotPath}; then
							umount -f ${SnapShotPath} >/dev/null 2>&1 || \
								logger "${0}: ***error*** - can not unmount ${SnapShot} mounted to ${SnapShotPath}"
						fi
						if ! mount | grep -q ${SnapShotPath}; then
							# remove mount-directory
							if [ -d ${SnapShotPath} ]; then
								rmdir ${SnapShotPath} || \
									logger "${0}: ***error*** - unable to remove mountpoint for ${SnapShot}"
							fi
							# finally remove snapshot
							if lvremove -f ${SnapShot} >/dev/null 2>&1;then
								logger "${0}: successfully removed outdated snapshot ${SnapShot}"
							else
								logger "${0}: ***error*** - can not remove logical volume ${SnapShot}"
							fi
						fi
					fi
				else
					logger "${0}: ***error*** - snapshot-set #${CurrSnapSet} of snaphot ${SnapShot} is not configured"
				fi
			;;

			# to check periodical if the snapshots have to be resized
			autoresize)
				Parameters="--options lv_size,snap_percent --noheadings --nosuffix --separator , --unbuffered --units m"
				SnapState=$(lvs ${Parameters} ${SnapShot})
				CurrSize=$(echo ${SnapState} | cut -f1 -d,)
				FillPercet=$(echo ${SnapState} | cut -f2 -d,)
				CurrFreeSize=$(echo "${CurrSize}-${CurrSize}/100*${FillPercet}" | bc)

				if ! [ $(echo "${CurrFreeSize} > ${FreeSize}" | bc) -eq 1 ]; then
					if lvresize -L +${ReSize}M ${SnapShot} >/dev/null 2>&1; then
						logger "${0}: successfully resized snapshot ${SnapShot}"
					else
						logger "${0}: ***error*** - an error occurred while resizing ${SnapShot}"
					fi
				fi
			;;
		esac
	}


	# invoked if all snapshots of a volume are processed
	# arguments: Command
	# needs:     VolumeDevice, VolumePath, SnapSet & functions: DoSnap
	# provides:  SnapShot
	# local:     snapset_tmp, cmd
	function DoAllSnaps()
	{
		cmd=${1}
		[ -z "${SnapSet}" ] || snapset_tmp="${SnapSet}-"
		# checkout if the configured volume exists and is mounted
		if [ -b ${VolumeDevice} ]; then
			if mount | grep -q "${VolumePath} "; then
				# process all snapshots of the volume and, if given, of a specific snapshot-set
				for SnapShot in ${VolumeDevice}-${snapset_tmp}*; do
					if [ ${SnapShot} = "${VolumeDevice}-${snapset_tmp}*" ]; then
						logger "${0}: ***error*** - no backupset #${SnapSet} found for ${VolumeDevice}"
					else
						DoSnap ${cmd}
					fi
				done
			else
				logger "${0}: ***error*** - logical volume ${VolumeDevice} not mounted to ${VolumePath}"
			fi
		else
			logger "${0}: ***error*** - logical volume ${VolumeDevice} does not exist"
		fi
	}


	# creates a new snapshot and mounts it
	# arguments: na.
	# needs:     VolumeDevice, VolumePath, SnapSet, SnapSize, SnapDate & functions: DoAllSnaps, DoSnap
	# provides:  SnapShot
	# local:     na.
	function MakeSnap ()
	{
		case ${SnapSet} in

			[0-9])
				if [ "${Command}" = "autosnap" ]; then DoAllSnaps "clean"; fi
				SnapShot=${VolumeDevice}-${SnapSet}-${SnapDate}
				if lvcreate -L${SnapSize}M -s -n ${SnapShot##*/} ${VolumeDevice} >/dev/null 2>&1; then
					logger "${0}: successfully created new snapshot ${SnapShot}"
				else
					logger "${0}: ***error*** - an error occurred while creating snapshot ${SnapShot}"
				fi
				DoSnap "mount"
			;;

			*)
				echo "usage: ${0} snap|autosnap <LV number | all> <Snap-Set Number>"
			;;
		esac
	}


	# sets some variables and splits the way for certain commands
	# arguments: one object of the array SnapVolumes
	# needs:     Command, & functions: DoAllSnaps MakeSnap
	# provides:  SnapVolume, VolumeDevice, PVGroupName, LVolumeName, VolumePath, SnapSize, FreeSize, ReSize
	# local:     na.
	function SecondChoice ()
	{
		SnapVolume=${1}
		VolumeDevice=$(echo ${SnapVolume} | cut -f1 -d,)
		PVGroupName=$(echo ${VolumeDevice} | cut -f3 -d/)
		LVolumeName=$(echo ${VolumeDevice} | cut -f4 -d/)
 		VolumePath=$(mount | grep  ^/dev[[:alnum:]/]*${PVGroupName}.${LVolumeName}[\ ] | cut -f3 -d' ')
		SnapSize=$(echo ${SnapVolume} | cut -f2 -d,)
		FreeSize=$(echo ${SnapVolume} | cut -f3 -d,)
		ReSize=$(echo ${SnapVolume} | cut -f4 -d,)

		case ${Command} in

			mount|umount|clean|autoresize)
				DoAllSnaps ${Command}
			;;

			snap|autosnap)
				MakeSnap
			;;
		esac
	}


# decides if all configured volumes are processed or just a specific one
# arguments: na.
# needs:     Command, LVolume, SnapVolumes & functions: SecondChoice
# provides:  na.
# local:     snp
case ${Command} in

	mount|umount|snap|clean|autosnap|autoresize)
		case ${LVolume} in

			all)
				for snp in ${SnapVolumes[@]}; do
					SecondChoice ${snp}
				done
			;;

			[0-9])
				if [ ${LVolume} -ge 0 ] && [ ${LVolume} -lt ${#SnapVolumes[@]} ]; then
					SecondChoice ${SnapVolumes[LVolume]}
				else
					logger "${0}: ***error*** - there is no configured logical volume #${LVolume} for snapshots"
				fi
			;;

			*)
			echo "usage: ${0} <command> <LV number | all> [<Snap-Set Number>]"
			;;
		esac
	;;

	*)
		echo "usage: ${0} <command> <LV number | all> [<Snap-Set Number>]"
		echo
		echo "       valid commands are:"
		echo "       mount      - to mount snapshots"
		echo "       umount     - to unmount snapshots"
		echo "       snap       - to make a new snapshot"
		echo "       clean      - to cleanup outdated snapshots"
		echo "       autosnap   - normally used for cronjobs to cleanup"
		echo "                    outdates snapshots an create a new one"
		echo "       autoresize - for a periodical check if snapshots"
		echo "                    needs to be resized"
		echo
		echo "       <LV number> is the number of a logical volume, configured for"
		echo "         snapshots in SnapVolumes, or simply 'all' for all volumes"
		echo "       <Snap-Set Number> is the number of the snapshot-set, configured"
		echo "         in SnapSet. It is optional, except for the commands 'snap' and"
		echo "         'autosnap'"
		echo
	;;
esac