linux系统备份脚本

时间:2023-03-08 15:47:15

前言

  之前写过<<linux系统简单备份的脚本>>, 最开始一直用着,后来觉得有必要改进下它,不管是从操作方式上还是脚本的工作方式上。之所以这么看重备份,是因为我经历过磁盘损坏的痛苦,花了1500元才勉强将数据拯救回来。我期望尽量每周备份,期望备份的目的地存储了当前系统的完整镜像,也就是说我能够从备份盘启动,且启动后系统的操作方式、已安装的软件及软件的配置以及文件的布局和当前的一模一样,每周我只需要增量备份当前的修改到备份盘。我不喜欢造*,但是在linux下没有现成的适合我的,于是就有了下面的备份脚本(其实只是基于rsync进行了一个封装啦)。

备份准备

注意: 当前还没有加入自动镜像系统的功能,所以如果想镜像系统,需要手动这样操作(这里只是简要描述):

基本步骤:

  1. 准备一块大于或等于当前系统所在盘的硬盘,然后给它分区,分区的结构要和原系统的分区结构保持一模一样,这个可以先通过fdisk -l记录下源磁盘的分区起始分区和结束分区;
  2. 分区后再格式化每个分区,每个分区的文件系统也尽量和要备份的磁盘分区相同;
  3. 挂载相应的分区
  4. 启动backup备份,备份的时候主要有些目录需要跳过,比如sys proc,另外如果/home目录是独立分区,那么备份/分区的时候也要跳过它,这些需要跳过的目录可以在backup启动的时候设置!
  5. 当备份完需要的分区后(这里假设备份了/分区和/home分区)后,在备份磁盘的/所挂载的分区下创建home sys proc目录
  6. 将备份磁盘home分区挂载到备份磁盘/分区的home下,并将当前的虚拟文件系统挂载到备份根分区下相应的目录,执行:

    mount –o bind /proc /mnt/proc

    mount –o bind /sys /mnt/sys (这里假设备份磁盘的/分区挂载在/m下)

    然后执行chroot 备份磁盘的/分区所挂载目录, 进入后执行grub_install grub_update来安装bootloader
  7. 做一些额外的配置文件修改,比如/etc/fstab文件里面可能是用uuid来挂载的,但是因为我们是从源系统备份过来的,所以需要修改成备份磁盘分区的uuid

btw: 备份的时候,尽量在第三方系统里面去备份想备份的磁盘

最新的代码请通过git访问:

git@bitbucket.org:rongpmcu/backup-script-shell.git

备份源码

这是脚本程序backup:

#!/bin/bash
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# Author: rongp
# email: rongpmcu#gmail.com
# Date: 2014/10/26
# 备份程序
# 特点:
# 主要支持unix类系统下, 支持符号链接
# 支持增量备份
# 添加备份条目时,支持文件名含有空格 但是不能出现含有@#@的文件名条目
# 添加备份条目时,支持直接托拽文件或者文件夹作为文件名路径
# 支持每次备份的变更备份,方便用于人工操作失误后的修复
# 支持添加规则用于剔除某些文件 格式请参考rsync的PATTERN部分 # 准备加入特性:
# 增加备份条目添加别名,这样方便查看
# 增加备份日志
# 增加备份时添加用户日志
# 参数解析还是需要加入多参数同时解析(bb bf等选项应该分开化,提供机制,而非策略)
# 网络备份(由于基于rsync, 很容易加入该功能,但暂时没加入) SHELL=/bin/bash
backup_cfg_path=/etc
backup_cfg=$backup_cfg_path/backup.cfg
db_path=
db_pathname=
inc_path= XECHO=1 _help()
{
echo -e "$0 [option]\n"\
"\tOption:\n"\
"\t-h show this help.\n"\
"\t-i perform the installation, and you should use this option\n"\
"\t before using the backup to do something else.\n"\
"\t-u perform the un-installation.\n"
} help()
{
echo -e "Command action\n"\
"\th show this help.\n"\
"\ta add any file that you want to backup to the database.\n"\
"\td delete any file that you no longer want from the database.\n"\
"\tb start backup.\n"\
"\tbf assume \"yes\" as answer to all prompts and run non-interactively.\n"\
"\tbb start backup but without incremental backup.\n"\
"\tn perform a trial backup with no changes made.\n"\
"\tp print the file record.\n"\
"\tc do some configurations, such as, modifying the path to the\n"\
"\t database or to the incremental backup directory.\n"\
"\ts show the current configuration.\n"\
"\ti perform the installation, and you should use this option\n"\
"\t before using the backup to do something else.\n"\
"\tu perform the un-installation.\n"\
"\tq quit"
} color_echo()
{
case "$1" in
g)
shift
echo -e "\033[32m"$@"\033[0m"
;;
gn)
shift
echo -e -n "\033[32m"$@"\033[0m"
;;
r)
shift
echo -e "\033[31m"$@"\033[0m"
;;
y)
shift
echo -e "\033[33m"$@"\033[0m"
;;
yn)
shift
echo -e -n "\033[33m"$@"\033[0m"
;;
*)
shift
echo $@
;;
esac
} XECHO()
{
if [ "$XECHO" = 1 ]; then
echo $@
fi
} check_src_dst()
{
if ! test -e "$1" || ! test -e "$2"; then
color_echo r "$1" or "$2" does not exist! ignore it!
return 2
fi local src_part1=`df "$1" | cut -d ' ' -f 1`
local src_part2=`df "$2" | cut -d ' ' -f 1`
local nsrc_inode=`ls -lid "$1" | cut -d ' ' -f 1`
local ndst_inode=`ls -lid "$2" | cut -d ' ' -f 1`
XECHO nsrc_inode:$nsrc_inode ndst_inode:$ndst_inode if [ "$src_part1" != "$src_part2" ]; then
return 1
fi if [ "$nsrc_inode" = "$ndst_inode" ]; then
color_echo r "$src is equivalent to $dst. ignore it!"
return 2
fi if [ ! -e $db_pathname ]; then
return 1
fi while read -r tsrc tdst tex_src;
do
tsrc="${tsrc//@#@/ }"
tdst="${tdst//@#@/ }"
tex_src="${tex_src//@#@/ }"
XECHO tsrc:"$tsrc" tdst:"$tdst"
osrc_inode=`ls -lid "$tsrc" | cut -d ' ' -f 1`
odst_inode=`ls -lid "$tdst" | cut -d ' ' -f 1`
XECHO osrc_inode:$osrc_inode odst_inode:$odst_inode
if [ "$nsrc_inode" = "$osrc_inode" -a "$ndst_inode" = "$odst_inode" ]; then
if [ ${1:((${#1}-1))} = '/' -a ${tsrc:((${#tsrc}-1))} != '/' ] \
|| [ ${1:((${#1}-1))} != '/' -a ${tsrc:((${#tsrc}-1))} = '/' ]; then #/home and /home/ is very different!
echo -n "";
else
return 0
fi
fi
done < $db_pathname return 1
} extract_src_dst()
{
XECHO "extract src dst from $1" src="${1%#*}"
dst="${1#$src}"
dst="${dst#\#}"
XECHO "src: $src"
XECHO "dst: $dst" if [ "$src" = "" -o "$dst" = "" ]; then
return 1
else
return 0
fi
} fix_path()
{
local srcpath="$1" if [ "${srcpath:0:1}" = '/' ]; then
echo $srcpath
elif [ "${srcpath:0:2}" = './' ]; then
echo `pwd`/${srcpath:2}
else
echo `pwd`/$srcpath
fi
} insert_new_item()
{
XECHO add item src:"$1" dst:"$2" exclude:"$3"
tmp1="${1// /@#@}"
tmp2="${2// /@#@}"
tmp3="${3// /@#@}"
echo "$tmp1" "$tmp2" "$tmp3" >> $db_pathname
return $?
} parse_item()
{
if ! extract_src_dst "$1"; then
color_echo r "src:$src or dst:$dst is illegal!"
return 1
fi src=`fix_path "$src"`
dst=`fix_path "$dst"` XECHO after fixed, src:"$src"
XECHO after fixed, src:"$dst" return 0
} do_add()
{
local item color_echo g "Enter the mode of adding files! Some patterns are available, as follows:"
color_echo g "eg: /home/#/tmp/ means we want to backup the whole things which "
color_echo g "are under home directory to /tmp directory."
color_echo g "eg: /home/#/tmp/:/etc/#/tmp/ means we want to backup the whole "
color_echo g "things which are under the home directory and the /etc/ directory "
color_echo g "to the /tmp directory, you can append any item with ':'."
color_echo r "Note: /home and /home/ are quite different, because /home just means "
color_echo r "/home itself while /home/ means the whole contents of /home." read -p "Please type in file items: " items
items="`echo "$items" | sed "s/'//g"`" flag=0
while [ $flag = 0 ];
do
item=${items%%:*}
items=${items#$item:}
ex_src=""
if [ "$items" = "$item" ]; then
flag=1
fi
if parse_item "$item"; then
check_src_dst "$src" "$dst"
ret=$?
if [ "$ret" = 0 ]; then
color_echo y "Warning! ""$src#$dst"" is already existed! do not re-submit!"
continue
elif [ "$ret" = 2 ]; then
continue
fi read -p "Would you like to add some excluding conditions to $src: (y/n)[n] " yn
if [ "$yn" = y ]; then
color_echo r "Note: this is an expert mode, and we don't check your pattern"
color_echo r "is valid or not. Some patterns are available, as follows:"
color_echo r "eg: if your src directory is /home, and your want to exclude"
color_echo r "the directory /home/rongp, then you should type in \"rongp\"."
color_echo r "eg: if your src directory is /home, and your want to exclude"
color_echo r "the directory /home/rongp and /home/test, then you should"
color_echo r "type in \"rongp:test\", and you can append any item with ':' ." read -p "Please type in paths to the excluding files: " exitem
ex_src="$exitem"
fi if insert_new_item "$src" "$dst" "$ex_src"; then
echo ""$src"#"$dst" add successed!"
else
echo ""$src"#"$dst" add failed!"
fi
else
read -p "skip it? Yy/Nn:[n] " yn
if [ "$yn" = "y" -o "$yn" = "Y" ]; then
continue
fi
return 1
fi
done return 0 } get_choices()
{
local total_line=`wc -l $db_pathname | cut -d ' ' -f 1` unset select_tab
color_echo g "Enter the mode of "$1"! some patterns are available, as follows:"
color_echo g "eg: 1-3 means select no 1 2 3 item"
color_echo g "eg: 1:3:5 means select no 1 3 5 item"
color_echo g "you can append any no with ':' or '-', but don't mix use it."
color_echo g "no 0 means select all."
do_print
read -p "Please type in the number: " NO
if [ "${NO/-/ }" != "$NO" ]; then
num_tab=(${NO//-/ })
[ ${#num_tab[@]} -gt 2 ] && \
echo "Select failed, argument $NO is illegal!" && return 1 num0=${num_tab[0]}
num1=${num_tab[1]}
XECHO num0:$num0 num1:$num1 if [ -z "${num0//[0-9]/}" -a "$num0" -le "$total_line" -a "$num0" -gt "0" ]\
&& [ -z "${num1//[0-9]/}" -a "$num1" -le "$total_line" -a "$num1" -gt "0" ]\
&& [ "$num0" -lt "$num1" ];
then
select_tab=(`seq $num0 $num1`)
else
echo "Select failed, argument $NO is illegal!" && return 1
fi
elif [ "${NO/:/ }" != "$NO" ]; then
for num in ${NO//:/ }
do
if [ -z "${num//[0-9]/}" ]&&[ "$num" -le "$total_line" ]\
&&[ "$num" -gt "0" ]; then
continue
else
echo "Select failed, argument $num is illegal!" && return 1
fi
done j=0
for i in ${NO//:/ }
do
select_tab[j]=$i
XECHO num:$i
((j++))
done
else
if [ "$NO" = 0 ]; then
XECHO num0:${NO}
select_tab=(`seq 1 $total_line`)
elif [ -z "${NO//[0-9]/}" ]&&[ "$NO" -le "$total_line" ]\
&&[ "$NO" -gt "0" ]; then
select_tab[0]=${NO}
XECHO select_tab[0]:${NO}
else
echo "Select failed, argument $NO is illegal!" && return 1
fi
fi return 0
} do_del()
{
if ! get_choices "deleting files"; then
return 1
fi local total_num=${#select_tab[@]} if [ "$total_num" = 1 ]; then
nums=${select_tab[0]}d
elif [ "$total_num" = 2 ]; then
nums=${select_tab[0]},${select_tab[1]}d
else
for ((i=0; i<$total_num; ++i))
do
nums+="${select_tab[i]}d;"
done
fi sed -i "$nums" $db_pathname >/dev/null 2>&1
[ "$?" = 0 ] && echo "$NO delete successed!" || echo "$NO delete failed, delete failed!"
} do_print()
{
[ ! -s $db_pathname ] && color_echo y "Warning, no record found!" && return 1
echo " no source destination action"
cat -n $db_pathname | sed 's/@#@/ /g'
} check_in_select_tab()
{
local i for ((i=0; i<${#select_tab[@]}; ++i))
do
XECHO $1:select_tab[$i]:${select_tab[i]}
if [ "${select_tab[i]}" = "$1" ]; then
return 0
fi
done return 1
} do_backup()
{
local ex_file=`mktemp`
local fake=
local yes=
#local wib="${1/without-incremental-backup/"--backup --backup-dir=$inc_path/$(date +%Y-%m-%d_%H:%M:%S_$(basename ${tmpsrc})_$(basename $tmpdst))"}"
local wib [ ! -f "$db_pathname" ] && color_echo r "$db_pathname does not exist!" && return 1 if ! get_choices "backup"; then
return 1
fi if [ "$1" = fake ]; then
fake="-n"
elif [ "$1" = yes ]; then
yes=y
fi local i=0
local k=0
while read -r src dst ex_src;
do
if check_in_select_tab $((i+1)); then
XECHO "$i in select table"
src="${src//@#@/ }"
dst="${dst//@#@/ }"
XECHO src:$src dst:$dst ex_src:$ex_src ex_file:$ex_file
src_tab[k]="$src"
dst_tab[k]="$dst"
ex_src_tab[k]="$ex_src"
((k++))
fi
((i++))
done < $db_pathname for ((j=0; j<$k; ++j))
do
echo src:${src_tab[j]} dst:${dst_tab[j]} ex_src:${ex_src_tab[j]}
src="${src_tab[j]}"
dst="${dst_tab[j]}"
ex_src="${ex_src_tab[j]}"
echo "$ex_src" | awk -F ':' '{for (i=1;i<=NF;++i)print $i}' | sed 's/@#@/ /g' > $ex_file if [ "$src" = "/" ]; then
tmpsrc=$(blkid `mount | grep "/ " | cut -d ' ' -f 1` | awk -F "\"" '{print $2}')
else
tmpsrc="$src"
fi if [ "$dst" = "/" ]; then
tmpdst=$(blkid `mount | grep "/ " | cut -d ' ' -f 1` | awk -F "\"" '{print $2}')
else
tmpdst="$dst"
fi if [ "$1" = "wib" ]; then
wib=
else
wib="--backup --backup-dir=$inc_path/$(date +%Y-%m-%d_%H:%M:%S_$(basename ${tmpsrc})_$(basename $tmpdst))"
fi color_echo g "We will start backup from "
color_echo r "$src"
color_echo g to
color_echo r "$dst"
color_echo g "with excluding file or directory"
color_echo r "${ex_src//@#@/ }"
color_echo gn "continue or not? y/n: " if [ "$yes" = y ]; then
yn=y
else
read yn
fi
if [ "$yn" = y ]; then
echo Start backup "$src" to "$dst" with excluding file or directory "$ex_src"
(
flock -x 200 echo rsync -aAXvzrtopg $fake --progress --delete --exclude-from=$ex_file \
"$src" "$dst" "$wib"
eval rsync -aAXvzrtopg $fake --progress --delete --exclude-from=$ex_file \
"$src" "$dst" "$wib"
) 200>/var/lock/abc echo Backup $src to $dst with excluding file or directory $ex_src finished!
else
echo Skip backup $src to $dst with excluding file or directory $ex_src!
fi
done } get_answer()
{
local ret if [ "$4" != "" ]; then
tmpbackup="$4"
else
tmpbackup=backup.cfg.bak
fi while :
do
read -p "Type in $1 path of the backup(default is $2, q for exit): " ans_tmp
if [ "$ans_tmp" = q ]; then
ret=1
break
elif [ "$ans_tmp" != "" ]; then
if [ ! -d "$ans_tmp" ]; then
echo "$1: $ans_tmp is invalid!"
read -p "Would you like to create it now? y/n [y]: " yn
if [ "$yn" = y ]; then
mkdir -p $ans_tmp
else
continue
fi
fi sed -i "s,$3.*,$3$ans_tmp,g" $tmpbackup
ret=$?
break
else
ans_tmp="$2"
ret=0
break
fi
done return $ret
} already_install()
{
if load_cfg $backup_cfg s; then
XECHO "already install"
return 0 #has install
fi return 1
} do_install()
{
color_echo g start install
if already_install; then
color_echo y "We check that you have already installed, you should"
color_echo yn "uninstall first, would you want to uninstall it first?y/n[n] "
read yn
if [ "$yn" != y ]; then
color_echo g install finished!
color_echo r install failed!
return 1
else
do_uninstall
fi
fi cp -f backup.cfg backup.cfg.bak
load_cfg backup.cfg.bak s
if [ "$?" = 1 ]; then
exit
fi if ! get_answer "executable file backup" "$bin_path" "INSTALL_PATH=";then
color_echo g install finished!
color_echo r install failed!
return 1
fi
install_path=$ans_tmp
color_echo g install path is $install_path if ! get_answer "database" "$db_path" "DB_PATH=";then
color_echo g install finished!
color_echo r install failed!
return 1
fi
db_path=$ans_tmp
color_echo g database path is $db_path if ! get_answer "incremental backup" "$inc_path" "INCREMENTAL_BACKUP_PATH=";then
color_echo g install finished!
color_echo r install failed!
return 1
fi
inc_path=$ans_tmp
color_echo g incremental backup path is $inc_path echo
who=`whoami`
cp backup $install_path
color_echo g install backup to $install_path
ret=$?
mv backup.cfg.bak $backup_cfg
color_echo g install $backup_cfg
ret=$((ret+$?))
mkdir -p $db_path
color_echo g install $db_path
ret=$((ret+$?))
mkdir -p $inc_path
color_echo g install $inc_path
ret=$((ret+$?))
ln -s $db_path $inc_path/db
color_echo g install $inc_path/db
ret=$((ret+$?))
color_echo g install finished!
if [ $ret -gt 0 ]; then
color_echo r install failed!
[ -e $bin_path/backup ] && rm_print $bin_path/backup
[ -e $backup_cfg ] && rm_print $backup_cfg
[ -e $inc_path/db ] && rm_print $inc_path/db && rm_print -rf $inc_path
[ -e $db_pathname ] && rm_print $db_pathname
rm_print -d $db_path
return 1
fi echo
echo
color_echo y "The installation work is done, and you can remove this package now!"
color_echo y "Note: you should put the executable file \"backup\""
color_echo y "into \$PATH and you need to get \"root\" privileges to execute it."
color_echo y "for example, you can execute it like this in ubuntu: sudo backup"
return 0
} rm_print()
{
color_echo g remove $@
eval rm $@
} do_uninstall()
{
XECHO "Perform the un-installation."
color_echo g perform the un-installation...
if ! load_cfg $backup_cfg; then
color_echo g uninstall failed!
fi
[ -e $bin_path/backup ] && rm_print $bin_path/backup
[ -e $backup_cfg ] && rm_print $backup_cfg
[ -e $inc_path/db ] && rm_print $inc_path/db && rm_print -rf $inc_path
[ -e $db_pathname ] && rm_print $db_pathname
rm_print -d $db_path
color_echo g uninstall finished!
color_echo g uninstall ok!
} load_cfg()
{
if [ ! -e "$1" ]; then
[ "$2" != "s" ] && color_echo r "Error, we can't find the configure file $1, exit now!"
return 1
fi bin_path=`sed -n 's/INSTALL_PATH=\(.*\)/\1/p' $1`
db_path=`sed -n 's/DB_PATH=\(.*\)/\1/p' $1`
db_pathname=$db_path/backup.db
inc_path=`sed -n 's/INCREMENTAL_BACKUP_PATH=\(.*\)/\1/p' $1` if [ ! -d "$inc_path" ]; then
[ "$2" != "s" ] && color_echo r Load configuration file failed! your should
[ "$2" != "s" ] && color_echo r check the directory $db_pathname is valid or not!
return 2
fi XECHO database path is $db_path
XECHO database file path is $db_pathname
XECHO incremental backup path is $inc_path
return 0
} show_configure()
{
color_echo g executable backup is in $bin_path
color_echo g database directory is in $db_path
color_echo g incremental backup directory is in $inc_path
} do_modify_inc_backup_path()
{
if ! get_answer "incremental backup" "$inc_path" \
"INCREMENTAL_BACKUP_PATH=" $backup_cfg;then
return 1
fi inc_path=$ans_tmp
XECHO incremental backup is $inc_path
return 0
} do_configure()
{
color_echo g [1] modify incremental backup path
color_echo g [2] ...
read -p "Please type in the no which you are expecting to: " no if [ "$no" = 1 ]; then
do_modify_inc_backup_path
else
color_echo r Unsupported operator!
fi
} backup_start()
{
if ! load_cfg $backup_cfg; then
exit
fi while :
do
read -p "Command (h for help): " cmd
case "$cmd" in
a)
do_add
;;
d)
do_del
;;
p)
do_print
;;
c)
do_configure
;;
b)
do_backup
;;
bf)
do_backup yes
;;
bb)
do_backup wib
;;
n)
do_backup fake
;;
s)
show_configure
;;
i)
do_install
;;
u)
do_uninstall
exit
;;
q)
break
;;
h | *)
help
;;
esac
done
} username=`echo $USER` if [ "$username" != root ]; then
color_echo r "Error, you need to have \"root\" privileges to execute this program."
exit
fi if [ "$1" = "-i" ]; then
if ! do_install; then
color_echo y "Sorry, We can't continue any more. Exit now!"
fi
exit
elif [ "$1" = "-u" ]; then
do_uninstall
exit
elif [ "$1" = "-h" ]; then
_help
exit
fi if [ ! -e $backup_cfg ]; then
color_echo r "$backup_cfg does not exist! "
read -p "You need to install the backup first. perform the installation? y/n?[y]: " yn
if [ "$yn" != n ]; then
do_install
else
echo Sorry, we can\'t continue any more. Exit now!
fi
exit
fi backup_start

下面是默认配置backup.cfg

#############################
######
#############################
AUTHOR=rongp
EMAIL=rongpmcu@gmail.com
VERSION=1.0 INSTALL_PATH=/usr/bin/
DB_PATH=/var/lib/backup/
INCREMENTAL_BACKUP_PATH=/var/lib/backup/incr_backup

基本操作流程演示

  1. 下载backup和backup.cfg,给backup添加可执行权限
  2. 从usb起linux系统(推荐ubuntu)
  3. 挂载所有要备份的源和目的磁盘的分区
  4. 执行sudo ./backup -i进行安装,安装过程设置好这次备份相关的配置文件路径,可以将路径设置到永久存储的分区,这样下次可以直接使用
  5. 执行sudo backup进入备份程序,操作界面和fdisk类似,h调出帮助界面,先执行a添加要备份的条目,在备份的过程中,可以添加过滤的文件或者目录
  6. 执行bf进入正式的备份,也可以先执行n来模拟备份,执行bb的话,会只备份,而不保留之前的文件(bf会自动保存覆盖的文件,用于以后的恢复)

总结

  该脚本一直作为我个人备份的工具,从ubuntu到arch,完全满足了我的需求,希望对其他人也有用,也希望大家多提意见_

引用

完!

2012年6月