4 # Full and Incremental backup script with tar and LVM snapshots.
6 # Copyright (C) 2008, 2009 Amand Tihon <amand.tihon@alrj.org>
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 # Note: This script relies on GNU tar specific options,
22 # do not attempt to use it as-is with other tar implementations.
26 CONFIG_FILE="/etc/brioche.conf"
29 #######################################################################
30 # Default config values. Do not change, edit $CONFIG_FILE
31 #######################################################################
33 BACKUPTAB="/etc/briochetab"
36 TAR_OPTS="--one-file-system -S "
39 SNAPSHOT_MOUNTPOINT="/mnt/backup-snapshot"
40 SNAPSHOT_NAME="backup-snap"
45 FTP_HOST="ftpback.example.com"
49 # Ensure that we have a minimal PATH
50 PATH=/sbin:/bin:/usr/sbin:/usr/bin
51 FINAL_STATUS="SUCCESS"
52 SUMMARY="/tmp/backup.sumary"
55 #######################################################################
57 #######################################################################
59 trap 'interrupted ctrl-C' INT
60 trap 'interrupted KILL' TERM
64 echo -n `date "+%Y-%m-%d %H:%M:%S"`
67 # Log a line with timestamp.
73 # The summary that will be sent by email
80 # Set final status to CRITICAL
83 FINAL_STATUS="CRITICAL"
88 if [ "$FINAL_STATUS" != "CRITICAL" ]; then
95 if [ "$FINAL_STATUS" == "SUCCESS" ]; then
96 FINAL_STATUS="WARNING"
103 local usage=`df -hP "${REPODIR}" | tail -n 1 | tr -s [:space:] | cut -d" " -f5 | tr -d '%'`
104 log "${REPODIR} usage after backup: ${usage}%."
106 if [ "$usage" -ge "$USAGE_WARN" ]; then
108 summary "Warning : Filesystem ${REPODIR} is ${usage}% full."
111 summary "Backup procedure ended on `NOW`"
113 SUBJECT="Backup report for `hostname` (${FINAL_STATUS})."
114 mail -s "${SUBJECT}" "${MAILTO}" < "$SUMMARY"
122 FINAL_STATUS="INTERRUPTED"
123 summary "Backup procedure interrupted by user ($1)."
124 summary "Take care, an LVM snapshot may still be present !"
128 #######################################################################
130 #######################################################################
132 # Make a snapshot of a logical volume.
133 # Usage: make_snapshot vg lv
136 log "Creating a snapshot volume of /dev/$1/$2"
137 lvcreate --snapshot -L ${SNAPSHOT_SIZE} -n ${SNAPSHOT_NAME} /dev/$1/$2
139 if [ "$RETVAL" != "0" ]; then
140 log "Error ${RETVAL}: Unable to create a snapshot of /dev/$1/$2"
145 # Mount the snapshot volume.
146 # On error, tries to remove the snapshot.
147 # Usage: mount_snapshot vg
150 log "Mounting the snapshot volume ${SNAPSHOT_NAME}."
151 if [ ! -d "${SNAPSHOT_MOUNTPOINT}" ]; then
152 log "Creating mountpoint ${SNAPSHOT_MOUNTPOINT}."
153 mkdir -p "${SNAPSHOT_MOUNTPOINT}"
156 mount /dev/$1/${SNAPSHOT_NAME} ${SNAPSHOT_MOUNTPOINT}
158 if [ "$RETVAL" != "0" ]; then
159 log "Error ${RETVAL}: Unable to mount /dev/$1/${SNAPSHOT_NAME} on ${SNAPSHOT_MOUNTPOINT}"
160 remove_snapshot $1 || return 100
165 # Remove a previously created snapshot. It must be unmounted.
166 # Usage: remove_snapshot vg
169 log "Removing the snapshot volume /dev/$1/${SNAPSHOT_NAME}."
170 lvremove -f /dev/$1/${SNAPSHOT_NAME}
172 if [ "$RETVAL" != "0" ]; then
173 log "Error ${RETVAL}: Unable to remove the snapshot volume /dev/$1/${SNAPSHOT_NAME}"
178 # Unmount the previously mounted snapshot.
179 # Usage: unmount_snapshot vg
182 log "Unmounting the snapshot volume /dev/$1/${SNAPSHOT_NAME}."
183 umount /dev/$1/${SNAPSHOT_NAME}
185 if [ "$RETVAL" != "0" ]; then
186 log "Error ${RETVAL}: Unable to unmount the snapshot volume /dev/$1/${SNAPSHOT_NAME}"
191 #######################################################################
192 # Main backup functions
193 #######################################################################
195 # Make a full backup of a directory, with snar file for subsequent incremental
196 # Usage: make_full_backup source_dir hostname volumename
197 # Returns 0 on success, 1 on error
200 log "Making full backup of $2 - ${3}."
202 local destdir="${REPODIR}/$2"
203 local today=`date "+%Y%m%d"`
204 local destfile="${destdir}/${3}.full.${today}.tar.${COMPRESS}"
205 local destsnar="${destdir}/${3}.full.snar"
207 # Move previous run to the "undo" directory
208 if [ ! -d "${destdir}/undo" ]; then
209 log "Creating undo/ directory."
210 mkdir -p "${destdir}/undo"
212 log "Moving old run into undo/ directory."
213 mv ${destdir}/${3}.* ${destdir}/undo
215 # Do the actual backup. Destination file name and snar are like
216 # /backup/valeron/root.full.20090105.tar.bz2
217 # /backup/valeron/root.full.snar
219 tar -cf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1
221 if [ "$?" = "0" ]; then
222 log "Removing undo/ directory."
223 rm -rf "${destdir}/undo"
225 log "Error $?: Could not archive $2 - $3"
232 # Make an incremental backup of a directory, from full's snar file
233 # Usage: make_incr_backup source_dir hostname volumename
234 # Returns 0 on success, 1 on error, 2 if no previous full is found.
237 log "Making incremental backup of $2 - ${3}."
239 local destdir="${REPODIR}/$2"
240 local today=`date "+%Y%m%d"`
241 local destfile="${destdir}/${3}.incr.${today}.tar.${COMPRESS}"
242 local destsnar="${destdir}/${3}.incr.${today}.snar"
243 local fullsnar="${destdir}/${3}.full.snar"
245 # Test existence of full backup
246 if [ ! -e $fullsnar ]; then
247 log "Could not find catalog ${fullsnar}."
251 # Prepare the copy of the snar file
252 cp "$fullsnar" "$destsnar"
254 # Do the actual backup. Destination file name and snar are like
255 # /backup/valeron/root.incr.20090105.tar.bz2
256 # /backup/valeron/root.incr.20090105.snar
258 tar -cf ${destfile} ${TAR_OPTS} ${COMPRESS_OPT} -g ${destsnar} $1
260 if [ ! "$?" = "0" ]; then
261 log "Error $?: Could not archive $2 - $3"
268 #######################################################################
270 #######################################################################
272 # Push everything in the directory given in $1 to the FTP server, under
276 log "Mirror $1 on FTP (${FTP_HOST})."
278 local target="${FTP_DIR}/${2}/latest"
279 local command="mkdir -p ${target}; cd ${target}"
280 command="${command}; mirror --reverse --only-newer --verbose ${source}"
282 lftp -e "${command}; exit" ${FTP_HOST}
285 # Rotate old files on FTP
286 # Usage: ftp_rotate group
289 log "Rotating backups of $1 on FTP (${FTP_HOST})."
290 local lastrun="run-${FTP_KEEP}"
291 local target="${FTP_DIR}/${1}"
292 local commands="mkdir -p ${target}; cd ${target}"
296 if [ "$FTP_KEEP" != "0" ]; then
297 commands="$commands; rmdir ${lastrun}"
299 # Move everything back
300 for run in `seq $FTP_KEEP -1 2`; do
303 commands="$commands; mv run-$newer run-$run"
305 # Move "old latest" to run-1
306 commands="$commands; mv latest run-1"
308 commands="$commands; rmdir latest"
311 # Create "new latest" directory
312 commands="$commands; mkdir latest; exit"
314 # Run the commands on the FTP server
315 lftp -e "$commands" $FTP_HOST
317 #######################################################################
319 #######################################################################
321 # Truncate summary file, start header blurb
323 summary "Backup procedure started on `NOW`"
325 if [ -r "${CONFIG_FILE}" ]
327 source "${CONFIG_FILE}"
329 summary "Error: Unable to read configuration file ${CONFIG_FILE}. Aborting."
334 if [ ! -r "${BACKUPTAB}" ]
336 summary "Error: Unable to read ${BACKUPTAB}. Aborting."
341 # TODO: Something cleaner, here...
348 echo "Usage: ${0} [-f]"
353 # Discover which COMPRESS_OPT to use, from COMPRESS
356 COMPRESS_OPT="--gzip"
359 COMPRESS_OPT="--bzip2"
362 COMPRESS_OPT="--lzma"
365 summary "Unknown compression method: ${COMPRESS}. Falling back to gzip."
367 COMPRESS_OPT="--gzip"
371 #######################################################################
372 # Parse backuptab file, call backup functions for each line
373 #######################################################################
375 # Rotate FTP groups on full
376 if [ "$USE_FTP" = "yes" -a "$DO_FULL_BACKUP" = "yes" ]; then
377 for group in `grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | awk '{print $3}' | sort -u`
383 # Ignore empty and commented lines
384 grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | tr -s [:space:]| while read line
386 # split line in fields
387 device=`echo $line|cut -d" " -f1`
388 dosnap=`echo $line|cut -d" " -f2`
389 group=`echo $line|cut -d" " -f3`
390 volume=`echo $line|cut -d" " -f4`
392 # Make and mount snapshot if needed.
393 if [ "$dosnap" = "yes" ]; then
394 # Split the device to find the VG and LV
395 vg=`echo $device|cut -d"/" -f3`
396 lv=`echo $device|cut -d"/" -f4`
397 make_snapshot $vg $lv
398 if [ "$?" != "0" ]; then
399 summary "Could not take a snapshot of $device"
407 if [ "$RETVAL" != "0" ]; then
408 summary "Could not mount the snapshot of $device"
410 if [ "$RETVAL" = "100" ]; then
411 summary "Could not remove the snapshot !"
419 BACKUP_SOURCE="${SNAPSHOT_MOUNTPOINT}"
421 BACKUP_SOURCE="$device"
425 if [ "$DO_FULL_BACKUP" = "no" ]; then
426 make_incr_backup ${BACKUP_SOURCE} $group $volume
428 if [ "$RETVAL" = "0" ]; then
429 summary "INCREMENTAL backup of $device done on `NOW`."
430 elif [ "$RETVAL" = "1" ]; then
431 summary "Error during incremental backup of $device"
433 elif [ "$RETVAL" = "2" ]; then
434 summary "Can't do an incremental backup without a full one being present."
435 summary "Switching to full backup for $device"
436 make_full_backup ${BACKUP_SOURCE} $group $volume
437 if [ "$?" = "0" ]; then
438 summary "FULL backup of $device done on `NOW`."
440 summary "Error during full backup of $device"
444 summary "Unknown error during incremental backup of $device"
447 else # Do a full backup
448 make_full_backup ${BACKUP_SOURCE} $group $volume
449 if [ "$?" = "0" ]; then
450 summary "FULL backup of $device done on `NOW`."
452 summary "Error during full backup of $device"
457 # Time to unmount and destroy the snapshot, if needed.
458 # Any error here is critical, since the snapshot and the mountpoint are
459 # the same for each device...
460 if [ "$dosnap" = "yes" ]; then
462 if [ "$?" != "0" ]; then
463 summary "Could not unmount snapshot !"
469 if [ "$?" != "0" ]; then
470 summary "Could not destroy the snapshot !"
478 # Push everything on the FTP
479 if [ "$USE_FTP" = "yes" ]; then
481 for group in `grep -v -E '^[[:space:]]*(#.*)?$' $BACKUPTAB | awk '{print $3}' | sort -u`
483 ftp_push "$REPODIR/$group" $group
484 summary "Mirrored ${group} to ${FTP_HOST}."