About Me

My photo
I know the last digit of PI

Friday, August 28, 2009

Как да си направим backup на диска под Линукс

Въпреки, че хардовете в днешно време са доста надеждни (поне на мен лично не ми е горял хард все още), не е зле човек да се потдотви и да си прави бакъпи редовно.
Понеже основната ми машинка която ползвам за SCM, Continuous integration, Issue trackig и т.н. е под линукс, затова реших да си напиша няколко скриптчета с които да си правя бакъпите. Изискванията ми не са кой знае какви - просто трябва да мога да си възтановя SCM и issue tracking даните без загуби и много труд. При евентуален срив и загуба на хард не очаквам да мога да възтановя цялата система. Пък и какъв е смисълът? Така никога няма да седна да инсталирам нова версия на софтуера! Всъщност от тази гледна точка сривовете са даже нещо много полезно... помагат ти да си разчистиш старите данни и боклуци, които са се насъбрали с години, кара те да качиш нови програмки и т.н. :-D Просто трябва да се случват в подходящ момент :-) когато имаш достатъчно време да си сетъпнеш нова система!...
Та да се върнем на въпроса - скриптът за бакъп трябва да може да копира някои директории, да ги архивира и да пази копия на тях. Също така е хубаво да може да се следи и история...случайно, ако се прецакат някакви настройки да могат да се проверят кога са променени, и да се възтановят правилните конфигурации. Та скрипта трябва да може да пази архив на старите бекъпи. В основата на всичко е rsync, която се използва за създаване на самите файлове. За да сработи скрипта трябва да се създаде една директория /etc/backup където са всички конфигурации включително и самият скрипт:

#!/bin/sh

VERBOSE=0
SCOPE=""
CONFIG="/etc/backup/backup.conf"

# Outputs the help message how to use the script
print_help()
{
echo "Usage: backup [-v | --verbose] <-s | --scope> " >&2
echo " -v or --verbose switch verbose mode on" >&2
echo " -s or --scope reads the list of directories to backup for " >&2
echo " the from the /etc/backup.conf " >&2
}

# Parses the passed command line and defines the backup scope, verbose mode, etc.
parse_command_line()
{
PREV_SCOPE=0
for p in "$@"; do
if [ $PREV_SCOPE -eq 1 ]; then
SCOPE=$p
PREV_SCOPE=0
else
if [ '--verbose' == $p -o '-v' == $p ]; then
VERBOSE=1
fi

if [ '--scope' == $p -o '-s' == $p ]; then
PREV_SCOPE=1
fi
fi
done
}

# Outputs message to the console if the verbose mode is selected
print()
{
if [ $VERBOSE -eq 1 ]; then
echo "$1"
fi
}


read_configuration()
{
# @author Michael Klier
match=0

while read line; do
# skip comments
[[ ${line:0:1} == "#" ]] && continue

# skip empty lines
[[ -z "$line" ]] && continue

# still no match? lets check again
if [ $match == 0 ]; then

# do we have an opening tag ?
if [[ ${line:$((${#line}-1))} == "{" ]]; then

# strip "{"
group=${line:0:$((${#line}-1))}
# strip whitespace
group=${group// /}

# do we have a match ?
if [[ "$group" == "$1" ]]; then
match=1
continue
fi

continue
fi

# found closing tag after config was read - exit loop
elif [[ ${line:0} == "}" && $match == 1 ]]; then
break

# got a config line eval it
else
eval $line
fi

done < "$CONFIG"
}


#
#
# The main script starts here
#
#

START_TIME=`date +%s`

# Parse the command line parameters
parse_command_line $@

if [ -z $SCOPE ]; then
print_help
echo $"Please enter the name of the backup scope" >&2
exit 1
fi

# Read LIST variable from the configuration file
read_configuration $SCOPE

# Example how the LIST variable should looks like
# LIST="etc root usr home var"

# Waits at 1 seconds, so if the user starts the script too fast the TIME_STAMP directory will be different
sleep 1

BACKUP_DIR="/backup/$SCOPE"

# Output the configuration information
print "Backup scope: $SCOPE"
print "Backup directory list: $LIST"
print "Exclude file: $EXCLUDE_FILE"
print "Backup dir is $BACKUP_DIR"

# Creates the backup directory
if [ ! -d $BACKUP_DIR ]; then
mkdir $BACKUP_DIR
fi

#
#
# Rotate backups, so we keep only the last 100
#
#

# Maximum number of backups to keep
maxInd=20

# Gets the oldest backup canonical path (it is a symbolic link to a time-stamped tar file)
REAL_PATH=`readlink -f $BACKUP_DIR/b.$maxInd.tar.gz`

# Remove the oldest backup
if [ -e $REAL_PATH ]; then
print "Removing tar file $REAL_PATH"
rm -rf $REAL_PATH
fi

# Remove the symbolic link
rm -rf $BACKUP_DIR/b.$maxInd.tar.gz


# Rotate backups from $ind to $ind+1
for ((ind = $maxInd-1; ind >= 0; ind--)); do
let nextInd=$ind+1
if [ -e $BACKUP_DIR/b.$ind.tar.gz ]; then
print "Rotating $BACKUP_DIR/b.$ind.tar.gz to $BACKUP_DIR/b.$nextInd.tar.gz"
mv $BACKUP_DIR/b.$ind.tar.gz $BACKUP_DIR/b.$nextInd.tar.gz
fi
done

# Delete the first backup (it must already be rotated, but just to be sure that the file doesn't exists)
rm -f $BACKUP_DIR/b.0.tar.gz

# Generate new timestamp for the new backup
TIME_STAMP=`date "+%F_%H-%M-%S"`

# Create the backup folder using the timestamp
mkdir $BACKUP_DIR/$TIME_STAMP

# Backup all files in the backup-list
for d in $LIST; do
print "Backuping /$d/ to $BACKUP_DIR/$TIME_STAMP/$d/"
mkdir -p $BACKUP_DIR/$TIME_STAMP/$d/
if [ $VERBOSE -eq 1 ]; then
rsync -vv -a --delete --exclude-from=$EXCLUDE_FILE /$d/ $BACKUP_DIR/$TIME_STAMP/$d/
else
rsync -a --delete --exclude-from=$EXCLUDE_FILE /$d/ $BACKUP_DIR/$TIME_STAMP/$d/
fi


done

# Go to the backup directory, so we could tar the content
pushd $BACKUP_DIR/$TIME_STAMP > /dev/null

# Archive the entire directory into single tar file
tar -c -z -f $BACKUP_DIR/$TIME_STAMP.tar.gz .

# Restore the current directory
popd > /dev/null

# Remove the backup directory
rm -rf $BACKUP_DIR/$TIME_STAMP

# Create a symbolic link to the directory
ln -s $BACKUP_DIR/$TIME_STAMP.tar.gz $BACKUP_DIR/b.0.tar.gz

FINISH_TIME=`date +%s`

FILE_SIZE=`ls -lrt $BACKUP_DIR/$TIME_STAMP.tar.gz | awk '{print $5}'`

printf "Completed! Execution time: $((FINISH_TIME - START_TIME)) seconds. Backup file size: $FILE_SIZE bytes."
printf "\n"




И така този скрипт се ползва по следния начин:
/etc/backup -s SCOPE_NAME

SCOPE_NAME се чете от конфигурационият файл /etc/backup.conf в който се описват кои директории да се копират. Ето и примерен такъв:
#
# In order to define a new scope of backup items uncomment following lines
#
#
#
# scope_name {
# LIST="etc root usr home var"
# EXCLUDE_FILE="/etc/backup/backup.excludes"
# }
#
# No leading ot trailing / in the directory names.
# The directories are sub directories of the /


weekly {
LIST="etc root usr home var"
EXCLUDE_FILE="/etc/backup/backup.excludes"
}

daily {
LIST="etc usr/red5/conf usr/red5/webapps usr/share/tomcat6/conf usr/share/tomcat6/webapps var/svn var/trac"
EXCLUDE_FILE="/etc/backup/backup.excludes"
}


Всеки scope си има списък от директории (LIST) и EXCLUDE_FILE в който се описват, коит файлове от тези директории да се пропуснат. Примерен backup.excludes:
#Excluding subdirs from /usr/

/bin/***
/java/***
/games/***
/kerberos/***
/sbin/***
/libexec/***
/lib/***
/tmp
/tmp/***
/include/***

# Excluding subdirs from /var/

/tmp/***


Така целият скрипт очаква, че ще има директория /backup (която е добре да моунтната на друг хард диск)

Хубаво е да се направят и cron task-ове които да правят дневен или седмичен бекъп.
Ето няколко примерни такива:

/etc/cron.daily/backup_daily
#!/bin/bash

TIME_STAMP=`date "+%F_%H-%M-%S"`

echo "Starting daily backup $TIME_STAMP" > /var/log/backup/$TIME_STAMP.log

/usr/bin/backup --verbose --scope daily > /var/log/backup/$TIME_STAMP.log 2> /var/log/backup/$TIME_STAMP.err



/etc/cron.weekly/backup_weekly
#!/bin/bash

TIME_STAMP=`date "+%F_%H-%M-%S"`

echo "Starting weekly backup $TIME_STAMP" > /var/log/backup/$TIME_STAMP.log

/usr/bin/backup --verbose --scope weekly > /var/log/backup/$TIME_STAMP.log 2> /var/log/backup/$TIME_STAMP.err



И така това е целият скрипт. Успех!

~Киро :-)

Many thanks to Michael Klier for his article
Parsing Simple Config Files In Bash