Regelmäßiges inkrementelles Backup einrichten

Um seine Daten sicher zu halten, sollte man Sicherungen machen. Diese können entweder hardwaregesteuert über RAID (siehe Wikipedia - Link) oder auch softwaregesteuert gemacht werden. Hier beschäftige ich mich mit der softwaregesteuerten Variante.

Zunächst einmal werde ich ein Beispiel darstellen und es dann erläutern. Also besonderes Schmankerl hierbei bekommen die Backup-Verzeichnisse das aktuelle Datum bzw. die aktuelle Zeit in den Namen geschrieben.
---------------------------------------------------------------------------------------------------------
#!/bin/bash
# ----------------------------------------------------------------------
# Makes snapshots from configured directory. Backup is done as hard links
# in the backup directory incrementally. The parent-directories for each 
# backup is the actual hour of the day.

# This script should be called every hour.
# ----------------------------------------------------------------------

# ------------- system commands used by this script --------------------
ID=/bin/id
ECHO=/bin/echo

MOUNT=/bin/mount
RM=/bin/rm
MV=/bin/mv
CP=/bin/cp
TOUCH=/bin/touch
MKDIR='/bin/mkdir -p'      # including sub-dirs
TAR=/bin/tar
RSYNC=/usr/bin/rsync

# ------------- check running ------------------------------------------

PID=/var/run/make_snapshot.pid

if [ -e $PID ] 
then 
	echo "already running ..."
	logger "make_snapshot already running - no second instance started"
	exit 1
fi
$TOUCH $PID

# ------------- file locations -----------------------------------------

MOUNT_DEVICE=/dev/sdc1
SNAPSHOT_RW=/mnt/backup
EXCLUDES=/root/scripts/backup_exclude
DIR_TO_BACKUP=/mnt/data/storage/
LOG=/log

# ------------- the script itself --------------------------------------

HOUR=`date +%HUhr`

# make sure we're running as root
if (( `$ID -u` != 0 )); then { $ECHO "Sorry, must be root.  Exiting..."; exit; } fi

# attempt to remount the RW mount point as RW; else abort
$MOUNT -o remount,rw $MOUNT_DEVICE $SNAPSHOT_RW
if (( $? )); then
	$ECHO "snapshot: could not remount $SNAPSHOT_RW readwrite"
	exit
fi

# rotating snapshots of $DIR_TO_BACKUP (fixme: this should be more general)

# step 0: create log-dir
if [ ! -d $SNAPSHOT_RW/$DIR_TO_BACKUP ] ; then
	$MKDIR $SNAPSHOT_RW/$DIR_TO_BACKUP
fi  
if [ ! -d $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG ] ; then
	$MKDIR $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG
fi  

# step 1: delete the oldest snapshot, if it exists:
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.7 ] ; then
	OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.7 | \
				sed -e "s/^.*[/]//g" | \
				sed -e "s/[.]hourly[.][1-7]//g"`
	$RM -rf $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.7
	$RM -rf $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.7.log
fi

# step 2: shift the middle snapshots(s) back by one, if they exist
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.6 ] ; then                     
	OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.6 | \
				sed -e "s/^.*[/]//g" | \
				sed -e "s/[.]hourly[.][1-7]//g"`
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.6 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.7
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.6.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.7.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.5 ] ; then                     
	OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.5 | \
				sed -e "s/^.*[/]//g" | \
				sed -e "s/[.]hourly[.][1-7]//g"`
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.5 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.6
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.5.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.6.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.4 ] ; then                     
	OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.4 | \
				sed -e "s/^.*[/]//g" | \
				sed -e "s/[.]hourly[.][1-7]//g"`
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.4 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.5
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.4.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.5.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.3 ] ; then                     
	OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.3 | \
				sed -e "s/^.*[/]//g" | \
				sed -e "s/[.]hourly[.][1-7]//g"`
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.3 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.4
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.3.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.4.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.2 ] ; then                     
	OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.2 | \
				sed -e "s/^.*[/]//g" | \
				sed -e "s/[.]hourly[.][1-7]//g"`
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.2 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.3
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.2.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.3.log
fi

# step 3: make a hard-link-only (except for dirs) copy of the latest snapshot,
# if that exists
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.1 ] ; then
	OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.1 | \
				sed -e "s/^.*[/]//g" | \
				sed -e "s/[.]hourly[.][1-7]//g"`
	$CP -al $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.1 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.2
	$CP $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.1.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.2.log
	
	if [ $OLDHOUR != $HOUR  ] ; then
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.1 $SNAPSHOT_RW/$DIR_TO_BACKUP/$HOUR.hourly.1
	fi
fi

# step 4: rsync from the system into the latest snapshot (notice that
# rsync behaves like cp --remove-destination by default, so the destination
# is unlinked first.  If it were not so, this would copy over the other
# snapshot(s) too!
$RSYNC                                                  \
	-va --delete --delete-excluded                      \
	--exclude-from="$EXCLUDES"                          \
	$DIR_TO_BACKUP $SNAPSHOT_RW/$DIR_TO_BACKUP/$HOUR.hourly.1 \
	> $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$HOUR.hourly.1.log

# step 5: update the mtime of hourly.0 to reflect the snapshot time
$TOUCH $SNAPSHOT_RW/$DIR_TO_BACKUP/$HOUR.hourly.1
$TOUCH $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$HOUR.hourly.1.log

# now remount the RW snapshot mountpoint as readonly

$MOUNT -o remount,ro $MOUNT_DEVICE $SNAPSHOT_RW
if (( $? )); then
	$ECHO "snapshot: could not remount $SNAPSHOT_RW readonly"
	exit
fi

# remove PID in /var/run/
$RM $PID
---------------------------------------------------------------------------------------------------------

Nun, im Grunde ist das Skript schnell erlärt. Zunächst werden ein paar Konstanten definiert, die die nötigen Befehle und Tools beinhalten. Danach werden das Laufwerk und das Verzeichnis definiert, wo das Backup hineingeschrieben werden soll. Es folgt dann noch das Verzeichnis, welches gesichert wird.
Für die Überprüfung, dass nur eine Instanz des Skripts läuft, wird eine *.pid Datei in /var/run gespreichert, die signalisiert, dass das Skript läuft. Sollte die Datei existieren wird das Skript beendet.
Nun wird das Backup-Laufwerk, welches möglicherweise "readonly" gemounted ist, als "readwrite" gemounted, damit das Backup geschrieben werden kann.
Es werden die Verzeichnisse für das Backup und die zugehörige Loggingdatei angelegt, falls sie noch nicht existieren.
Nun wird das älteste Backup (das 8.-letzte) gelöscht.
Alle weiteren Backups werden jeweils um eine Stufe hochgestuft. Nummer 0 wird 1, 1 wird 2, 2 wird 3, usw.
Zuletzt wird über das Tool rsync das tatsächliche Backup ausgeführt.
Zuguterletzt wird das Backup-Laufwerk wieder "readonly" gemounted.
---------------------------------------------------------------------------------------------------------
Als täglichen Job wird das aktuellste Backup (also das mit der Nummer 0) gesichert.
---------------------------------------------------------------------------------------------------------

#!/bin/bash
# ----------------------------------------------------------------------
# Will copy the latest backup to a new dackup directory.
#
# This script should be called once a day.
# ----------------------------------------------------------------------

# ------------- system commands used by this script --------------------
ID=/bin/id
ECHO=/bin/echo

MOUNT=/bin/mount
RM=/bin/rm
MV=/bin/mv
CP=/bin/cp
MKDIR='/bin/mkdir -p'

# ------------- file locations -----------------------------------------

MOUNT_DEVICE=/dev/sdc1
SNAPSHOT_RW=/mnt/backup
DIR_TO_BACKUP=/mnt/data/storage/
LOG=/log

# ------------- the script itself --------------------------------------

WEEKDAY=`date +%A`

# make sure we're running as root
if (( `$ID -u` != 0 )); then { $ECHO "Sorry, must be root.  Exiting..."; exit; } fi

# attempt to remount the RW mount point as RW; else abort
$MOUNT -o remount,rw $MOUNT_DEVICE $SNAPSHOT_RW
if (( $? )); then
	$ECHO "snapshot: could not remount $SNAPSHOT_RW readwrite"
	exit 1
fi

# step 0: create log-dir
if [ ! -d $SNAPSHOT_RW/$DIR_TO_BACKUP ] ; then
    $MKDIR $SNAPSHOT_RW/$DIR_TO_BACKUP
fi  
if [ ! -d $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG ] ; then
    $MKDIR $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG
fi  

# step 1: delete the oldest snapshot, if it exists
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.7 ] ; then    
    OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.7 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
    $RM -rf $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.7         
    $RM -rf $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.7.log
fi

# step 2: shift the middle snapshots(s) back by one, if they exist
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.6 ] ; then                      
    OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.6 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
	$MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.6 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.7
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.6.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.7.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.5 ] ; then                      
    OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.5 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.5 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.6
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.5.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.6.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.4 ] ; then                      
    OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.4 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.4 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.5
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.4.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.5.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.3 ] ; then                      
    OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.3 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.3 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.4
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.3.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.4.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.2 ] ; then                      
    OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.2 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.2 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.3
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.2.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.3.log
fi
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.1 ] ; then                      
    OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.1 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.1 $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.2
    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.1.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDWEEKDAY.daily.2.log
fi

# step 3: make a hard-link-only (except for dirs) copy of
# $OLDWEEKDAY.daily.7, assuming that exists, into $WEEKDAY.daily.1
# copy last hourly.1 to daily.1
if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.1 ] ; then                     
    OLDHOUR=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.hourly.1 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]hourly[.][1-7]//g"`
    $CP -al $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDHOUR.hourly.1 $SNAPSHOT_RW/$DIR_TO_BACKUP/$WEEKDAY.daily.1
    $CP $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$OLDHOUR.hourly.1.log $SNAPSHOT_RW/$DIR_TO_BACKUP/$LOG/$WEEKDAY.daily.1.log

    if [ -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.7 ] ; then
	OLDWEEKDAY=`dir -d $SNAPSHOT_RW/$DIR_TO_BACKUP/*.daily.7 | \
                sed -e "s/^.*[/]//g" | \
                sed -e "s/[.]daily[.][1-7]//g"`
	if [ $OLDWEEKDAY != $WEEKDAY ] ; then
	    $MV $SNAPSHOT_RW/$DIR_TO_BACKUP/$OLDWEEKDAY.daily.1 $SNAPSHOT_RW/$DIR_TO_BACKUP/$WEEKDAY.daily.1
	fi
    fi
fi


# note: do *not* update the mtime of $WEEKDAY.daily.1; it will reflect
# when hourly.7 was made, which should be correct.

# now remount the RW snapshot mountpoint as readonly

$MOUNT -o remount,ro $MOUNT_DEVICE $SNAPSHOT_RW
if (( $? )); then
	$ECHO "snapshot: could not remount $SNAPSHOT_RW readonly"
	exit 1
fi
---------------------------------------------------------------------------------------------------------
Das aktuellste stündliche Backup wird als aktuellstes tägliches Backup kopiert. Die vorhergehenden Tage werden je um eine Stelle nach hinten kopiert. Das älteste tägliche Backup wird gelöscht.