LDP/LDP/guide/docbook/abs-guide/nightly-backup.sh

365 lines
13 KiB
Bash

#!/bin/bash
# nightly-backup.sh
# http://www.richardneill.org/source.php#nightly-backup-rsync
# Copyright (c) 2005 Richard Neill <backup@richardneill.org>.
# This is Free Software licensed under the GNU GPL.
# ==> Included in ABS Guide with script author's kind permission.
# ==> (Thanks!)
# This does a backup from the host computer to a locally connected
#+ firewire HDD using rsync and ssh.
# (Script should work with USB-connected device (see lines 40-43).
# It then rotates the backups.
# Run it via cron every night at 5am.
# This only backs up the home directory.
# If ownerships (other than the user's) should be preserved,
#+ then run the rsync process as root (and re-instate the -o).
# We save every day for 7 days, then every week for 4 weeks,
#+ then every month for 3 months.
# See: http://www.mikerubel.org/computers/rsync_snapshots/
#+ for more explanation of the theory.
# Save as: $HOME/bin/nightly-backup_firewire-hdd.sh
# Known bugs:
# ----------
# i) Ideally, we want to exclude ~/.tmp and the browser caches.
# ii) If the user is sitting at the computer at 5am,
#+ and files are modified while the rsync is occurring,
#+ then the BACKUP_JUSTINCASE branch gets triggered.
# To some extent, this is a
#+ feature, but it also causes a "disk-space leak".
##### BEGIN CONFIGURATION SECTION ############################################
LOCAL_USER=rjn # User whose home directory should be backed up.
MOUNT_POINT=/backup # Mountpoint of backup drive.
# NO trailing slash!
# This must be unique (eg using a udev symlink)
# MOUNT_POINT=/media/disk # For USB-connected device.
SOURCE_DIR=/home/$LOCAL_USER # NO trailing slash - it DOES matter to rsync.
BACKUP_DEST_DIR=$MOUNT_POINT/backup/`hostname -s`.${LOCAL_USER}.nightly_backup
DRY_RUN=false #If true, invoke rsync with -n, to do a dry run.
# Comment out or set to false for normal use.
VERBOSE=false # If true, make rsync verbose.
# Comment out or set to false otherwise.
COMPRESS=false # If true, compress.
# Good for internet, bad on LAN.
# Comment out or set to false otherwise.
### Exit Codes ###
E_VARS_NOT_SET=64
E_COMMANDLINE=65
E_MOUNT_FAIL=70
E_NOSOURCEDIR=71
E_UNMOUNTED=72
E_BACKUP=73
##### END CONFIGURATION SECTION ##############################################
# Check that all the important variables have been set:
if [ -z "$LOCAL_USER" ] ||
[ -z "$SOURCE_DIR" ] ||
[ -z "$MOUNT_POINT" ] ||
[ -z "$BACKUP_DEST_DIR" ]
then
echo 'One of the variables is not set! Edit the file: $0. BACKUP FAILED.'
exit $E_VARS_NOT_SET
fi
if [ "$#" != 0 ] # If command-line param(s) . . .
then # Here document(ation).
cat <<-ENDOFTEXT
Automatic Nightly backup run from cron.
Read the source for more details: $0
The backup directory is $BACKUP_DEST_DIR .
It will be created if necessary; initialisation is no longer required.
WARNING: Contents of $BACKUP_DEST_DIR are rotated.
Directories named 'backup.\$i' will eventually be DELETED.
We keep backups from every day for 7 days (1-8),
then every week for 4 weeks (9-12),
then every month for 3 months (13-15).
You may wish to add this to your crontab using 'crontab -e'
# Back up files: $SOURCE_DIR to $BACKUP_DEST_DIR
#+ every night at 3:15 am
15 03 * * * /home/$LOCAL_USER/bin/nightly-backup_firewire-hdd.sh
Don't forget to verify the backups are working,
especially if you don't read cron's mail!"
ENDOFTEXT
exit $E_COMMANDLINE
fi
# Parse the options.
# ==================
if [ "$DRY_RUN" == "true" ]; then
DRY_RUN="-n"
echo "WARNING:"
echo "THIS IS A 'DRY RUN'!"
echo "No data will actually be transferred!"
else
DRY_RUN=""
fi
if [ "$VERBOSE" == "true" ]; then
VERBOSE="-v"
else
VERBOSE=""
fi
if [ "$COMPRESS" == "true" ]; then
COMPRESS="-z"
else
COMPRESS=""
fi
# Every week (actually of 8 days) and every month,
#+ extra backups are preserved.
DAY_OF_MONTH=`date +%d` # Day of month (01..31).
if [ $DAY_OF_MONTH = 01 ]; then # First of month.
MONTHSTART=true
elif [ $DAY_OF_MONTH = 08 \
-o $DAY_OF_MONTH = 16 \
-o $DAY_OF_MONTH = 24 ]; then
# Day 8,16,24 (use 8, not 7 to better handle 31-day months)
WEEKSTART=true
fi
# Check that the HDD is mounted.
# At least, check that *something* is mounted here!
# We can use something unique to the device, rather than just guessing
#+ the scsi-id by having an appropriate udev rule in
#+ /etc/udev/rules.d/10-rules.local
#+ and by putting a relevant entry in /etc/fstab.
# Eg: this udev rule:
# BUS="scsi", KERNEL="sd*", SYSFS{vendor}="WDC WD16",
# SYSFS{model}="00JB-00GVA0 ", NAME="%k", SYMLINK="lacie_1394d%n"
if mount | grep $MOUNT_POINT >/dev/null; then
echo "Mount point $MOUNT_POINT is indeed mounted. OK"
else
echo -n "Attempting to mount $MOUNT_POINT..."
# If it isn't mounted, try to mount it.
sudo mount $MOUNT_POINT 2>/dev/null
if mount | grep $MOUNT_POINT >/dev/null; then
UNMOUNT_LATER=TRUE
echo "OK"
# Note: Ensure that this is also unmounted
#+ if we exit prematurely with failure.
else
echo "FAILED"
echo -e "Nothing is mounted at $MOUNT_POINT. BACKUP FAILED!"
exit $E_MOUNT_FAIL
fi
fi
# Check that source dir exists and is readable.
if [ ! -r $SOURCE_DIR ] ; then
echo "$SOURCE_DIR does not exist, or cannot be read. BACKUP FAILED."
exit $E_NOSOURCEDIR
fi
# Check that the backup directory structure is as it should be.
# If not, create it.
# Create the subdirectories.
# Note that backup.0 will be created as needed by rsync.
for ((i=1;i<=15;i++)); do
if [ ! -d $BACKUP_DEST_DIR/backup.$i ]; then
if /bin/mkdir -p $BACKUP_DEST_DIR/backup.$i ; then
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ No [ ] test brackets. Why?
echo "Warning: directory $BACKUP_DEST_DIR/backup.$i is missing,"
echo "or was not initialised. (Re-)creating it."
else
echo "ERROR: directory $BACKUP_DEST_DIR/backup.$i"
echo "is missing and could not be created."
if [ "$UNMOUNT_LATER" == "TRUE" ]; then
# Before we exit, unmount the mount point if necessary.
cd
sudo umount $MOUNT_POINT &&
echo "Unmounted $MOUNT_POINT again. Giving up."
fi
exit $E_UNMOUNTED
fi
fi
done
# Set the permission to 700 for security
#+ on an otherwise permissive multi-user system.
if ! /bin/chmod 700 $BACKUP_DEST_DIR ; then
echo "ERROR: Could not set permissions on $BACKUP_DEST_DIR to 700."
if [ "$UNMOUNT_LATER" == "TRUE" ]; then
# Before we exit, unmount the mount point if necessary.
cd ; sudo umount $MOUNT_POINT \
&& echo "Unmounted $MOUNT_POINT again. Giving up."
fi
exit $E_UNMOUNTED
fi
# Create the symlink: current -> backup.1 if required.
# A failure here is not critical.
cd $BACKUP_DEST_DIR
if [ ! -h current ] ; then
if ! /bin/ln -s backup.1 current ; then
echo "WARNING: could not create symlink current -> backup.1"
fi
fi
# Now, do the rsync.
echo "Now doing backup with rsync..."
echo "Source dir: $SOURCE_DIR"
echo -e "Backup destination dir: $BACKUP_DEST_DIR\n"
/usr/bin/rsync $DRY_RUN $VERBOSE -a -S --delete --modify-window=60 \
--link-dest=../backup.1 $SOURCE_DIR $BACKUP_DEST_DIR/backup.0/
# Only warn, rather than exit if the rsync failed,
#+ since it may only be a minor problem.
# E.g., if one file is not readable, rsync will fail.
# This shouldn't prevent the rotation.
# Not using, e.g., `date +%a` since these directories
#+ are just full of links and don't consume *that much* space.
if [ $? != 0 ]; then
BACKUP_JUSTINCASE=backup.`date +%F_%T`.justincase
echo "WARNING: the rsync process did not entirely succeed."
echo "Something might be wrong."
echo "Saving an extra copy at: $BACKUP_JUSTINCASE"
echo "WARNING: if this occurs regularly, a LOT of space will be consumed,"
echo "even though these are just hard-links!"
fi
# Save a readme in the backup parent directory.
# Save another one in the recent subdirectory.
echo "Backup of $SOURCE_DIR on `hostname` was last run on \
`date`" > $BACKUP_DEST_DIR/README.txt
echo "This backup of $SOURCE_DIR on `hostname` was created on \
`date`" > $BACKUP_DEST_DIR/backup.0/README.txt
# If we are not in a dry run, rotate the backups.
[ -z "$DRY_RUN" ] &&
# Check how full the backup disk is.
# Warn if 90%. if 98% or more, we'll probably fail, so give up.
# (Note: df can output to more than one line.)
# We test this here, rather than before
#+ so that rsync may possibly have a chance.
DISK_FULL_PERCENT=`/bin/df $BACKUP_DEST_DIR |
tr "\n" ' ' | awk '{print $12}' | grep -oE [0-9]+ `
echo "Disk space check on backup partition \
$MOUNT_POINT $DISK_FULL_PERCENT% full."
if [ $DISK_FULL_PERCENT -gt 90 ]; then
echo "Warning: Disk is greater than 90% full."
fi
if [ $DISK_FULL_PERCENT -gt 98 ]; then
echo "Error: Disk is full! Giving up."
if [ "$UNMOUNT_LATER" == "TRUE" ]; then
# Before we exit, unmount the mount point if necessary.
cd; sudo umount $MOUNT_POINT &&
echo "Unmounted $MOUNT_POINT again. Giving up."
fi
exit $E_UNMOUNTED
fi
# Create an extra backup.
# If this copy fails, give up.
if [ -n "$BACKUP_JUSTINCASE" ]; then
if ! /bin/cp -al $BACKUP_DEST_DIR/backup.0 \
$BACKUP_DEST_DIR/$BACKUP_JUSTINCASE
then
echo "ERROR: Failed to create extra copy \
$BACKUP_DEST_DIR/$BACKUP_JUSTINCASE"
if [ "$UNMOUNT_LATER" == "TRUE" ]; then
# Before we exit, unmount the mount point if necessary.
cd ;sudo umount $MOUNT_POINT &&
echo "Unmounted $MOUNT_POINT again. Giving up."
fi
exit $E_UNMOUNTED
fi
fi
# At start of month, rotate the oldest 8.
if [ "$MONTHSTART" == "true" ]; then
echo -e "\nStart of month. \
Removing oldest backup: $BACKUP_DEST_DIR/backup.15" &&
/bin/rm -rf $BACKUP_DEST_DIR/backup.15 &&
echo "Rotating monthly,weekly backups: \
$BACKUP_DEST_DIR/backup.[8-14] -> $BACKUP_DEST_DIR/backup.[9-15]" &&
/bin/mv $BACKUP_DEST_DIR/backup.14 $BACKUP_DEST_DIR/backup.15 &&
/bin/mv $BACKUP_DEST_DIR/backup.13 $BACKUP_DEST_DIR/backup.14 &&
/bin/mv $BACKUP_DEST_DIR/backup.12 $BACKUP_DEST_DIR/backup.13 &&
/bin/mv $BACKUP_DEST_DIR/backup.11 $BACKUP_DEST_DIR/backup.12 &&
/bin/mv $BACKUP_DEST_DIR/backup.10 $BACKUP_DEST_DIR/backup.11 &&
/bin/mv $BACKUP_DEST_DIR/backup.9 $BACKUP_DEST_DIR/backup.10 &&
/bin/mv $BACKUP_DEST_DIR/backup.8 $BACKUP_DEST_DIR/backup.9
# At start of week, rotate the second-oldest 4.
elif [ "$WEEKSTART" == "true" ]; then
echo -e "\nStart of week. \
Removing oldest weekly backup: $BACKUP_DEST_DIR/backup.12" &&
/bin/rm -rf $BACKUP_DEST_DIR/backup.12 &&
echo "Rotating weekly backups: \
$BACKUP_DEST_DIR/backup.[8-11] -> $BACKUP_DEST_DIR/backup.[9-12]" &&
/bin/mv $BACKUP_DEST_DIR/backup.11 $BACKUP_DEST_DIR/backup.12 &&
/bin/mv $BACKUP_DEST_DIR/backup.10 $BACKUP_DEST_DIR/backup.11 &&
/bin/mv $BACKUP_DEST_DIR/backup.9 $BACKUP_DEST_DIR/backup.10 &&
/bin/mv $BACKUP_DEST_DIR/backup.8 $BACKUP_DEST_DIR/backup.9
else
echo -e "\nRemoving oldest daily backup: $BACKUP_DEST_DIR/backup.8" &&
/bin/rm -rf $BACKUP_DEST_DIR/backup.8
fi &&
# Every day, rotate the newest 8.
echo "Rotating daily backups: \
$BACKUP_DEST_DIR/backup.[1-7] -> $BACKUP_DEST_DIR/backup.[2-8]" &&
/bin/mv $BACKUP_DEST_DIR/backup.7 $BACKUP_DEST_DIR/backup.8 &&
/bin/mv $BACKUP_DEST_DIR/backup.6 $BACKUP_DEST_DIR/backup.7 &&
/bin/mv $BACKUP_DEST_DIR/backup.5 $BACKUP_DEST_DIR/backup.6 &&
/bin/mv $BACKUP_DEST_DIR/backup.4 $BACKUP_DEST_DIR/backup.5 &&
/bin/mv $BACKUP_DEST_DIR/backup.3 $BACKUP_DEST_DIR/backup.4 &&
/bin/mv $BACKUP_DEST_DIR/backup.2 $BACKUP_DEST_DIR/backup.3 &&
/bin/mv $BACKUP_DEST_DIR/backup.1 $BACKUP_DEST_DIR/backup.2 &&
/bin/mv $BACKUP_DEST_DIR/backup.0 $BACKUP_DEST_DIR/backup.1 &&
SUCCESS=true
if [ "$UNMOUNT_LATER" == "TRUE" ]; then
# Unmount the mount point if it wasn't mounted to begin with.
cd ; sudo umount $MOUNT_POINT && echo "Unmounted $MOUNT_POINT again."
fi
if [ "$SUCCESS" == "true" ]; then
echo 'SUCCESS!'
exit 0
fi
# Should have already exited if backup worked.
echo 'BACKUP FAILED! Is this just a dry run? Is the disk full?) '
exit $E_BACKUP