#!/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 "$@"