#!/bin/bash # # Name : backup_all.sh # Description : 备份源服务器数据并传输到目标机器执行数据恢复 # Version : 2.5 # Author :James # Date : 2025年12月18日 17:30 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # Update # 重构了大部分逻辑,更新细节如下 # ● 使用rsync进行数据传输 # ● 使用pgiz进行全核压缩 # ● 增加对于密钥连接的使用 # ● 添加迁移时间及时长统计 # ● 添加双方系统的版本检查 # ● 使用ssh保活参数防止连接中断 # ● 迁移失败时重启系统程序 # ● 增强脚本对错误处理的健壮性 # ● 优化tar压缩data子目录逻辑 # ● 添加crm-import目录 # ● 迁移日志保存至文件(/home/migration.log) # ● 数据库备份添加删库参数 # ● 只对业务库进行导出 # ● 兼容5.0版本的服务启停 # ● 去除不必要的数据库重启 # ● 数据恢复在后台异步执行 # ● 一步执行数据和录音迁移 # ● 迁移录音前进行硬盘余量检查 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # store_file="/root/restore_data.sh" prikey="/root/.ssh/migration" pubkey="/root/.ssh/migration.pub" ver_file="/ipcc/etc/pub/version" dbpw="sg6d9ybbnMv41SKT" db_list="ccdata ccsys syslog userdata" ssh_opts='-o ServerAliveInterval=60 -o ServerAliveCountMax=3' copy_id_opts='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -o ConnectionAttempts=1' running_flag=1 all_tmp="" sql_tmp="" is_ver5=0 version="" # 检测版本 [ -f "$ver_file" ] && version=$(cat "$ver_file") case "$version" in 5.*) is_ver5=1 ;; 3.*) is_ver5=0 ;; *) # 保底判断手段:如果版本为空,判断是否存在 /dipcc 目录 if [ -d "/dipcc" ]; then is_ver5=1 else is_ver5=0 fi ;; esac function confirm() { if [ ! -t 0 ]; then echo "非交互环境,脚本退出!" exit 1 fi while true; do read -p "确认退出?[y/n] " -n 1 choice echo case $choice in [yY]) echo "脚本已退出!" exit 1 ;; [nN]) echo "脚本继续运行..." break ;; *) echo "无效值,请重新输入!" ;; esac done } function cleanup() { local exit_code=$? local delete_flag=0 if [ $exit_code -ne 0 ] && [ $running_flag -eq 0 ]; then echo "脚本非正常退出,重启程序..." start_service echo "删除tmp文件..." rm -rf $all_tmp rm -rf $sql_tmp fi [ -f $prikey ] && rm -rf $prikey && delete_flag=1 [ -f $pubkey ] && rm -rf $pubkey && delete_flag=1 [ "$delete_flag" -eq 1 ] && echo "删除密钥对..." } function timelen() { local start_time=$(date +"%m-%d %H:%M") local start_second=$SECONDS "$@" local stop_second=$SECONDS local stop_time=$(date +"%m-%d %H:%M") local duration=$(($stop_second - $start_second)) # 特殊处理总耗时为0秒的情况 if [ $duration -eq 0 ]; then echo "$* 迁移耗时 0 秒" return fi # 计算小时、分钟和秒 local hours=$(($duration / 3600)) local minutes=$((($duration / 60) % 60)) local seconds=$(($duration % 60)) # 根据时长拼接字符 local time_string="" if [ $hours -gt 0 ]; then time_string+=" ${hours} 小时" fi if [ $minutes -gt 0 ]; then time_string+=" ${minutes} 分" fi if [ $seconds -gt 0 ]; then time_string+=" ${seconds} 秒" fi # 输出拼接的时间 echo "$* 开始执行时间:$start_time,结束执行时间:$stop_time,耗时$time_string" } #停止服务 function stop_service() { if [ "$is_ver5" -eq 1 ]; then service monitor stop service bs stop service sc stop service mc stop service ma stop service dipcc stop service task-manager stop service freeswitch stop service nginx stop service php-fpm stop service redis stop else /etc/init.d/ipccd stop /etc/init.d/freeswitch stop /etc/init.d/nginx stop /etc/init.d/php-fpm stop /etc/init.d/redis stop fi running_flag=0 } #启用服务 function start_service() { if [ "$is_ver5" -eq 1 ]; then service freeswitch start service nginx start service php-fpm start service redis start service dipcc start service task-manager start service bs start service sc start service mc start service ma start service monitor start else /etc/init.d/freeswitch start /etc/init.d/nginx start /etc/init.d/php-fpm start /etc/init.d/redis start /etc/init.d/ipccd start fi running_flag=1 } #安装必要软件 function install_pkg() { local pkgs=("pigz" "rsync") local pkg local role [ "$1" == 0 ] && role="本机" # shellcheck disable=SC2034 [ "$1" == 1 ] && role="目标机器" for pkg in "${pkgs[@]}"; do command -v $pkg &>/dev/null && echo "$role$pkg已安装" || yum install "$pkg" -y if [ $? -ne 0 ]; then echo "${pkg}未成功安装,脚本退出!" exit 1 fi done } function check_version() { [ -f $ver_file ] && master_ver=$(cat $ver_file) slave_ver=$(ssh $ssh_opts -p $port root@$new_host -i $prikey -- "[ -f $ver_file ]&&cat $ver_file") if [ "$master_ver" == "$slave_ver" ]; then echo "版本检查通过,开始执行迁移。" else echo -e "本机版本:$master_ver\n待迁移机器版本:$slave_ver\n系统版本不一致,迁移退出!" exit 1 fi } function check_dirs() { local base_path="/home/ipcc/data" local sub_dirs=("audio" "blacklist" "callee" "call-script" "certification" "electronic" "hide-number-tmp-dir" "inbound-plot" "ivraudio" "ivraudio1" "knowledge-file" "satisfaction" "theTxtModel" "upload-file" "voice-notify-audio" "crm-import") local existing=() for dir in "${sub_dirs[@]}"; do local full_path="$base_path/$dir" if [ ! -d "$full_path" ]; then continue fi if [ -z "$(ls -A "$full_path")" ]; then continue fi existing+=("$dir") done printf '%s\n' "${existing[@]}" } function check_voicerecord_space() { local voicerecord="/home/ipcc/data/voicerecord" if [ ! -d "$voicerecord" ]; then echo -e "\n[提示] 本机不存在录音目录 ($voicerecord),跳过容量预检。" return 0 fi echo "正在扫描录音文件大小,请稍候..." local src_size_kb=$(du -sk "$voicerecord" | awk '{print $1}') # 兜底:如果获取为空,则赋值为0 src_size_kb=${src_size_kb:-0} local dst_avail_kb=$(ssh $ssh_opts -p $port root@$new_host -i $prikey -- "df -k /home | awk 'NR==2 {print \$4}'") # 兜底:如果获取为空,则赋值为0 dst_avail_kb=${dst_avail_kb:-0} local required_kb=$(awk "BEGIN {print int($src_size_kb * 2 + 20971520)}") echo "--------------------------------------------------------" echo "本机录音总占用: $((src_size_kb / 1024 / 1024)) GB" echo "对端 /home 剩余空间: $((dst_avail_kb / 1024 / 1024)) GB" echo "预估安全迁移需要空间: $((required_kb / 1024 / 1024)) GB" echo "--------------------------------------------------------" if [ -z "$dst_avail_kb" ] || [ "$dst_avail_kb" -lt "$required_kb" ]; then echo -e "\033[1;31m[警告] 目标服务器磁盘空间不足!\033[0m" echo "为防止远端服务器磁盘耗尽,录音迁移流程已终止!" return 1 else echo -e "\033[1;32m磁盘容量安全检查通过,准备开始自动迁移录音...\033[0m" return 0 fi } function migrate_data() { local sql_file="/home/alldatabase.sql" local all_file="/home/backup_data.tar.gz" all_tmp="$all_file.tmp" sql_tmp="$sql_file.tmp" local backup_dirs=() mapfile -t backup_dirs < <(check_dirs) check_version #检测是否存在旧的备份文件 if [ -f $sql_file ] || [ -f $all_file ]; then echo "检测到存在之前的备份文件:" [ -f $sql_file ] && echo " - $sql_file" [ -f $all_file ] && echo " - $all_file" while true; do read -p "是否使用该文件继续迁移?[y/n] " -n 1 use_old echo case $use_old in [yY]) echo "使用旧备份文件继续..." break ;; [nN]) echo "删除旧备份文件,重新备份..." rm -f $sql_file $all_file break ;; *) echo "无效值,请重新输入!" ;; esac done fi stop_service #数据库备份 [ -f $sql_file ] || { # /usr/local/mysql/bin/mysqldump -uroot -p$dbpw --add-drop-database --databases $db_list >$sql_tmp /usr/local/mysql/bin/mysqldump -uroot -p$dbpw --databases $db_list >$sql_tmp if [ $? -ne 0 ]; then echo "数据库备份失败!" exit 1 else mv $sql_tmp $sql_file fi } #将所有备份数据进行打包压缩 [ -f $all_file ] || { local tar_args=(-cf -) # 动态组装 tar 参数,防止因为部分文件/目录不存在导致报错退出 [ ${#backup_dirs[@]} -gt 0 ] && tar_args+=(-C /home/ipcc/data "${backup_dirs[@]}") for d in conf db sounds; do [ -d "/usr/local/freeswitch/$d" ] && tar_args+=(-C /usr/local/freeswitch "$d") done [ -f "/usr/local/redis-5.0.4/run/dump.rdb" ] && tar_args+=(-C /usr/local/redis-5.0.4/run dump.rdb) [ -d "/var/www/web-v2/oem" ] && tar_args+=(-C /var/www/web-v2 oem) tar_args+=(-C /home alldatabase.sql) # 开启 pipefail 确保只要 tar 报错,整个管道的 $? 就会返回非 0 set -o pipefail tar "${tar_args[@]}" | pigz >$all_tmp local pkg_status=$? set +o pipefail if [ $pkg_status -ne 0 ]; then echo "备份文件打包失败,脚本退出!" rm -f $all_tmp exit 1 else mv $all_tmp $all_file fi } #仅将备份数据和脚本拷贝至新服务器 rsync -avPh -e "ssh $ssh_opts -p $port -i $prikey" $all_file $store_file root@$new_host:/home if [ $? -ne 0 ]; then echo "数据或脚本传输失败,脚本退出!" exit 1 fi #异步触发对端恢复,获取进程PID echo "正在触发对端自动恢复,防断连模式已开启..." RESTORE_PID=$(ssh $ssh_opts -p $port root@$new_host -i $prikey -- "nohup env is_ver5=$is_ver5 bash /home/restore_data.sh > /home/restore.log 2>&1 & echo \$!") if [ $? -ne 0 ] || [ -z "$RESTORE_PID" ]; then echo "触发对端恢复脚本失败,脚本退出!" exit 1 fi echo "对端恢复脚本已在后台安全运行 (PID: $RESTORE_PID)。" echo "正在实时同步对端执行日志:" echo "--------------------------------------------------------" ssh $ssh_opts -p $port root@$new_host -i $prikey -- "tail --pid=$RESTORE_PID -n +1 -f /home/restore.log" ssh_exit=$? echo "--------------------------------------------------------" if [ $ssh_exit -eq 0 ]; then #检查远端唯一PID的成功标记文件是否存在 ssh $ssh_opts -p $port root@$new_host -i $prikey -- "[ -f /tmp/.restore_${RESTORE_PID}_ok ]" is_ok=$? #清理该临时标记文件 ssh $ssh_opts -p $port root@$new_host -i $prikey -- "rm -f /tmp/.restore_${RESTORE_PID}_ok" #判定输出 if [ $is_ok -eq 0 ]; then echo -e "恢复进程执行成功!详细日志保存在对端的 /home/restore.log" else echo -e "\033[1;31m[错误] 对端恢复任务发生异常,执行失败!\033[0m" echo "请登录对端主机查看 /home/restore.log 排查原因。" exit 1 fi else echo -e "[警告] 日志实时同步中断!" echo "由于无法确认对端任务是否成功,脚本终止!请自行登录对端主机确认。" exit 1 fi #删除备份压缩包 rm -rf $sql_file $all_file start_service echo "***** 数据迁移完成,下一步请对${new_host}进行授权!*****" } function migrate_voicerecord() { local voicerecord="/home/ipcc/data/voicerecord" local zip_record="/home/voicerecord.tar.gz" if [ ! -d "$voicerecord" ]; then echo -e "\n[提示] 本机不存在录音目录 ($voicerecord),跳过录音迁移步骤!" return 0 fi #将录音目录打包 tar -cf - -C $voicerecord . | pigz >$zip_record #将录音包传至新服务器并执行录音恢复操作 rsync -avPh -e "ssh $ssh_opts -p $port -i $prikey" $zip_record root@$new_host:/home/ && ssh $ssh_opts -p $port root@$new_host -i $prikey -- " \ mkdir -p $voicerecord && \ mkdir -p /home/ipcc/data/record-tmp && \ pigz -dc /home/voicerecord.tar.gz | tar -xf - -C $voicerecord && \ chown -R nobody:nobody $voicerecord && \ chown -R nobody:nobody /home/ipcc/data/record-tmp && \ rm -f /home/voicerecord.tar.gz" if [ $? -ne 0 ]; then echo "录音恢复执行失败,脚本退出!" exit 1 fi #删除录音压缩文件 rm -rf $zip_record echo "***** 录音迁移完成,请检查录音文件夹是否创建! *****" } # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 保存迁移日志到文件 exec > >(tee -a /home/migration.log) 2>&1 # 捕获信号执行对应函数 trap confirm INT trap cleanup EXIT # restore脚本限定在/root目录下 if [ ! -f "$store_file" ]; then echo "必须存放在/root目录下!" exit 1 fi # 本机安装软件包 install_pkg 0 # 输入服务器地址和端口 read -p "请输入新服务器的IP地址:" new_host read -p "请输入新服务器的 SSH 端口(默认22):" port port=${port:-22} # 使用公私钥实现ssh免密登录 yes | ssh-keygen -t rsa -P "" -f $prikey ssh -p $port $copy_id_opts root@$new_host "mkdir -p ~/.ssh && cat > ~/.ssh/authorized_keys" <$pubkey || { echo "连接失败,脚本退出!" exit 1 } # ssh-copy-id -f -i $pubkey -p $port $copy_id_opts root@$new_host || { # echo "连接失败,脚本退出!" # exit 1 # } # 对端安装软件包 max_retries=3 for ((i = 0; i <= $max_retries; i++)); do ssh $ssh_opts -p $port root@$new_host -i $prikey -- "$(declare -f install_pkg); install_pkg 1" if [ $? -eq 0 ]; then break else if [ $i -eq $max_retries ]; then echo "安装软件包失败,请检查后手动安装!" exit 1 fi echo "安装失败,重试第[$(($i + 1))/$max_retries]次..." fi done # 迁移类型选择 echo "< 1.将除语音文件外的所有数据迁移到新服务器 >" echo "< 2.仅将语音文件迁移到新服务器 >" echo "< 3.一键执行:先迁数据,后迁语音 >" while true; do read -p "请输入您的选择[1/2/3]:" choice case $choice in 1) timelen migrate_data break ;; 2) if check_voicerecord_space; then timelen migrate_voicerecord fi break ;; 3) timelen migrate_data if check_voicerecord_space; then timelen migrate_voicerecord fi break ;; *) echo "请输入选项1、2或3!" ;; esac done