356 lines
9.1 KiB
Bash
356 lines
9.1 KiB
Bash
#!/bin/bash
|
|
# patch_rollback.sh - 企业级补丁包回滚脚本
|
|
|
|
set -euo pipefail
|
|
shopt -s nullglob extglob
|
|
|
|
# 脚本目录
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
CONFIG_FILE="${SCRIPT_DIR}/patch_config.sh"
|
|
|
|
# 颜色定义
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
PURPLE='\033[0;35m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m'
|
|
|
|
# 日志函数
|
|
log() {
|
|
local level="$1"
|
|
local message="$2"
|
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
|
|
case "$level" in
|
|
"INFO") color="$GREEN" ;;
|
|
"WARN") color="$YELLOW" ;;
|
|
"ERROR") color="$RED" ;;
|
|
"DEBUG") color="$BLUE" ;;
|
|
*) color="$NC" ;;
|
|
esac
|
|
|
|
echo -e "${color}[$timestamp] [$level]${NC} $message" | tee -a "$LOG_FILE"
|
|
}
|
|
|
|
info() { log "INFO" "$1"; }
|
|
warn() { log "WARN" "$1"; }
|
|
error() { log "ERROR" "$1"; }
|
|
debug() { log "DEBUG" "$1"; }
|
|
|
|
# 配置和初始化
|
|
load_config() {
|
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
error "配置文件不存在: $CONFIG_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
source "$CONFIG_FILE"
|
|
info "配置文件加载完成"
|
|
|
|
: ${LOG_LEVEL:="INFO"}
|
|
: ${LOG_FILE:="/tmp/patch_rollback_$(date +%Y%m%d_%H%M%S).log"}
|
|
: ${BACKUP_DIR:="/var/backups/patch"}
|
|
: ${TEMP_DIR:="/tmp/patch_rollback_$$"}
|
|
: ${ROLLBACK_INFO_FILE:="$BACKUP_DIR/rollback_info.json"}
|
|
}
|
|
|
|
setup_environment() {
|
|
mkdir -p "$(dirname "$LOG_FILE")"
|
|
mkdir -p "$TEMP_DIR"
|
|
|
|
info "日志文件: $LOG_FILE"
|
|
info "临时目录: $TEMP_DIR"
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ -d "$TEMP_DIR" ]]; then
|
|
rm -rf "$TEMP_DIR"
|
|
info "临时目录已清理: $TEMP_DIR"
|
|
fi
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
# 回滚点管理
|
|
get_available_rollback_points() {
|
|
info "可用的回滚点:"
|
|
|
|
if [[ ! -f "$ROLLBACK_INFO_FILE" ]]; then
|
|
warn "未找到回滚点信息文件"
|
|
return 1
|
|
fi
|
|
|
|
# 读取回滚点信息
|
|
local patch_name=$(jq -r '.patch_name' "$ROLLBACK_INFO_FILE" 2>/dev/null || echo "")
|
|
local apply_time=$(jq -r '.apply_time' "$ROLLBACK_INFO_FILE" 2>/dev/null || echo "")
|
|
local backup_path=$(jq -r '.backup_path' "$ROLLBACK_INFO_FILE" 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$patch_name" && -n "$backup_path" && -f "$backup_path" ]]; then
|
|
echo "1. $patch_name (应用时间: $apply_time)"
|
|
echo " 备份文件: $backup_path"
|
|
return 0
|
|
else
|
|
warn "回滚点信息不完整或备份文件不存在"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# 验证回滚包
|
|
verify_rollback_package() {
|
|
local rollback_path="$1"
|
|
|
|
info "验证回滚包: $rollback_path"
|
|
|
|
if [[ ! -f "$rollback_path" ]]; then
|
|
error "回滚包不存在: $rollback_path"
|
|
return 1
|
|
fi
|
|
|
|
if ! tar -tzf "$rollback_path" >/dev/null 2>&1; then
|
|
error "回滚包损坏或格式错误"
|
|
return 1
|
|
fi
|
|
|
|
info "✅ 回滚包验证通过"
|
|
return 0
|
|
}
|
|
|
|
# 回滚执行函数
|
|
perform_rollback() {
|
|
local rollback_path="$1"
|
|
local dry_run="${2:-false}"
|
|
|
|
info "开始执行回滚: $rollback_path"
|
|
|
|
# 验证回滚包
|
|
if ! verify_rollback_package "$rollback_path"; then
|
|
return 1
|
|
fi
|
|
|
|
# 解压回滚包
|
|
local extract_dir="$TEMP_DIR/rollback_extract"
|
|
if ! tar -xzf "$rollback_path" -C "$extract_dir"; then
|
|
error "回滚包解压失败"
|
|
return 1
|
|
fi
|
|
info "✅ 回滚包解压完成"
|
|
|
|
# 如果是干跑模式,只显示将要回滚的文件
|
|
if [[ "$dry_run" == "true" ]]; then
|
|
info "干跑模式: 显示将要回滚的文件"
|
|
find "$extract_dir" -type f | while read file; do
|
|
local relative_path="${file#$extract_dir/}"
|
|
local target_file="/$relative_path"
|
|
echo "📁 将回滚: $relative_path -> $target_file"
|
|
done
|
|
return 0
|
|
fi
|
|
|
|
# 执行实际回滚
|
|
if execute_file_rollback "$extract_dir"; then
|
|
info "✅ 回滚执行完成"
|
|
|
|
# 清理回滚点
|
|
cleanup_rollback_point
|
|
|
|
send_rollback_notification "success" "$rollback_path" "回滚成功"
|
|
return 0
|
|
else
|
|
error "❌ 回滚执行失败"
|
|
send_rollback_notification "failure" "$rollback_path" "回滚失败"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
execute_file_rollback() {
|
|
local extract_dir="$1"
|
|
|
|
info "开始执行文件回滚..."
|
|
|
|
local rollback_count=0
|
|
local error_count=0
|
|
|
|
# 遍历回滚包中的所有文件
|
|
find "$extract_dir" -type f | while read backup_file; do
|
|
local relative_path="${backup_file#$extract_dir/}"
|
|
local target_file="/$relative_path"
|
|
local target_dir=$(dirname "$target_file")
|
|
|
|
# 创建目标目录
|
|
mkdir -p "$target_dir"
|
|
|
|
# 备份当前文件(如果存在)
|
|
if [[ -f "$target_file" ]]; then
|
|
local current_backup="$TEMP_DIR/current_backup/$relative_path"
|
|
mkdir -p "$(dirname "$current_backup")"
|
|
cp -p "$target_file" "$current_backup"
|
|
debug "备份当前文件: $target_file"
|
|
fi
|
|
|
|
# 恢复文件
|
|
if cp -p "$backup_file" "$target_file"; then
|
|
info "✅ 回滚文件: $relative_path"
|
|
((rollback_count++))
|
|
else
|
|
error "❌ 回滚失败: $relative_path"
|
|
((error_count++))
|
|
fi
|
|
done
|
|
|
|
info "回滚完成: $rollback_count 个文件成功, $error_count 个文件失败"
|
|
|
|
if [[ $error_count -gt 0 ]]; then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# 清理回滚点
|
|
cleanup_rollback_point() {
|
|
if [[ -f "$ROLLBACK_INFO_FILE" ]]; then
|
|
mv "$ROLLBACK_INFO_FILE" "$ROLLBACK_INFO_FILE.bak"
|
|
info "回滚点已清理: $ROLLBACK_INFO_FILE -> $ROLLBACK_INFO_FILE.bak"
|
|
fi
|
|
}
|
|
|
|
# 通知函数
|
|
send_rollback_notification() {
|
|
local status="$1"
|
|
local rollback_path="$2"
|
|
local message="$3"
|
|
|
|
if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
case "$status" in
|
|
"success")
|
|
local subject="补丁回滚成功"
|
|
local color="#2ecc71"
|
|
;;
|
|
"failure")
|
|
local subject="补丁回滚失败"
|
|
local color="#e74c3c"
|
|
;;
|
|
*)
|
|
local subject="补丁回滚通知"
|
|
local color="#95a5a6"
|
|
;;
|
|
esac
|
|
|
|
# Slack通知
|
|
if [[ -n "$SLACK_WEBHOOK" ]]; then
|
|
send_slack_rollback_notification "$subject" "$message" "$color" "$rollback_path"
|
|
fi
|
|
}
|
|
|
|
send_slack_rollback_notification() {
|
|
local subject="$1"
|
|
local message="$2"
|
|
local color="$3"
|
|
local rollback_path="$4"
|
|
|
|
local payload=$(cat << EOF
|
|
{
|
|
"attachments": [
|
|
{
|
|
"color": "$color",
|
|
"title": "$subject",
|
|
"text": "$message",
|
|
"fields": [
|
|
{
|
|
"title": "回滚包",
|
|
"value": "$(basename "$rollback_path")",
|
|
"short": true
|
|
},
|
|
{
|
|
"title": "回滚时间",
|
|
"value": "$(date)",
|
|
"short": true
|
|
},
|
|
{
|
|
"title": "回滚主机",
|
|
"value": "$(hostname)",
|
|
"short": true
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
)
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -s -X POST -H 'Content-type: application/json' \
|
|
--data "$payload" "$SLACK_WEBHOOK" >/dev/null && \
|
|
info "Slack通知发送成功"
|
|
fi
|
|
}
|
|
|
|
# 主回滚函数
|
|
rollback_patch() {
|
|
local rollback_path="${1:-}"
|
|
local dry_run="${2:-false}"
|
|
|
|
# 如果没有指定回滚包,使用最近的
|
|
if [[ -z "$rollback_path" ]]; then
|
|
info "未指定回滚包,查找最近的回滚点..."
|
|
if get_available_rollback_points; then
|
|
if [[ -f "$ROLLBACK_INFO_FILE" ]]; then
|
|
rollback_path=$(jq -r '.backup_path' "$ROLLBACK_INFO_FILE")
|
|
info "使用回滚包: $rollback_path"
|
|
else
|
|
error "未找到可用的回滚点"
|
|
return 1
|
|
fi
|
|
else
|
|
error "无法获取回滚点信息"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# 执行回滚
|
|
if perform_rollback "$rollback_path" "$dry_run"; then
|
|
info "🎉 回滚操作成功完成"
|
|
return 0
|
|
else
|
|
error "💥 回滚操作失败"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# 主函数
|
|
main() {
|
|
local rollback_path="${1:-}"
|
|
local dry_run="${2:-false}"
|
|
|
|
# 加载配置
|
|
load_config
|
|
|
|
# 设置环境
|
|
setup_environment
|
|
|
|
# 显示使用信息
|
|
if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
|
echo "用法: $0 [回滚包路径] [dry-run]"
|
|
echo "示例:"
|
|
echo " $0 # 使用最近的回滚点"
|
|
echo " $0 /var/backups/patch/backup_xxx.tar.gz # 指定回滚包"
|
|
echo " $0 /path/to/backup.tar.gz dry-run # 干跑模式"
|
|
exit 0
|
|
fi
|
|
|
|
# 执行回滚
|
|
if rollback_patch "$rollback_path" "$dry_run"; then
|
|
exit 0
|
|
else
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# 异常处理
|
|
trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM
|
|
|
|
main "$@" |