#!/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" LOG_FILE="/var/log/patch_system/rollback_$(date +%Y%m%d_%H%M%S).log" LOCK_FILE="/tmp/patch_rollback.lock" ROLLBACK_INFO_DIR="/var/lib/patch_system/rollback_info" ROLLBACK_POINTS=() # 存储回滚点数组 ROLLBACK_COUNT=0 # 回滚点数量 # 颜色定义 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 "配置文件加载完成" # 设置默认值 : ${TEMP_DIR:="/tmp/patch_rollback_$$"} : ${BACKUP_DIR:="/var/backups/patch"} : ${ROLLBACK_ENABLED:="true"} : ${ROLLBACK_INFO_DIR:="/var/lib/patch_system/rollback_info"} : ${MAX_ROLLBACK_POINTS:=10} : ${VALIDATION_ENABLED:="true"} : ${NOTIFICATIONS_ENABLED:="true"} } # 环境设置 setup_environment() { mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$ROLLBACK_INFO_DIR" mkdir -p "$BACKUP_DIR" mkdir -p "/tmp/patch_rollback" info "环境设置完成" info "日志文件: $LOG_FILE" info "回滚信息目录: $ROLLBACK_INFO_DIR" } # 锁管理 acquire_lock() { local max_retries=5 local retry_count=0 while [[ $retry_count -lt $max_retries ]]; do if (set -C; echo $$ > "$LOCK_FILE") 2>/dev/null; then info "🔒 获取回滚锁" return 0 fi local lock_pid=$(cat "$LOCK_FILE" 2>/dev/null) if kill -0 "$lock_pid" 2>/dev/null; then warn "回滚操作正在进行中 (PID: $lock_pid),等待..." sleep 5 retry_count=$((retry_count+1)) else warn "清理过期的锁文件" rm -f "$LOCK_FILE" fi done error "无法获取锁,可能已有回滚操作在进行中" exit 1 } release_lock() { rm -f "$LOCK_FILE" info "🔓 释放回滚锁" } # 路径规范化函数 - 去除多余的斜杠 normalize_path() { local path="$1" # 使用 sed 去除重复的斜杠 echo "$path" | sed 's|/\+|/|g' } # 清理函数 cleanup() { local exit_code=$? if [[ -d "$TEMP_DIR" ]]; then # rm -rf "$TEMP_DIR" info "临时目录已清理: $TEMP_DIR" fi release_lock if [[ $exit_code -eq 0 ]]; then info "✅ 回滚流程完成" else error "💥 回滚流程失败 (退出码: $exit_code)" fi exit $exit_code } trap cleanup EXIT INT TERM # 依赖检查 check_dependencies() { local deps=("tar" "gzip" "jq" "sha256sum" "cp" "mkdir" "find") local missing=() for dep in "${deps[@]}"; do if ! command -v "$dep" >/dev/null 2>&1; then missing+=("$dep") fi done if [[ ${#missing[@]} -gt 0 ]]; then error "缺少依赖工具: ${missing[*]}" return 1 fi info "依赖检查通过" return 0 } # 获取可用的回滚点 list_rollback_points() { info "📋 可用的回滚点:" >&2 info "回滚还原点信息目录: $ROLLBACK_INFO_DIR" >&2 if [[ ! -d "$ROLLBACK_INFO_DIR" ]]; then warn "回滚信息目录不存在: $ROLLBACK_INFO_DIR" >&2 return 1 fi local count=0 local temp_file="/tmp/rollback_list_$$" local found_files=0 local valid_files=0 # 安全检查 if [[ ! -d "$ROLLBACK_INFO_DIR" ]]; then error "回滚信息目录不存在: $ROLLBACK_INFO_DIR" >&2 return 1 fi info "在 $ROLLBACK_INFO_DIR 中搜索回滚信息文件..." >&2 # 清空临时文件 > "$temp_file" || { error "无法创建临时文件: $temp_file" return 1 } # 使用数组存储文件,避免管道问题 local files=() while IFS= read -r -d '' file; do files+=("$file") done < <(find "$ROLLBACK_INFO_DIR" -maxdepth 1 -name "*.json" -type f -print0 2>/dev/null) # 使用 ls -t 按时间排序 IFS=$'\n' sorted_files=($(ls -t "${files[@]}")) unset IFS found_files=${#sorted_files[@]} info "找到 $found_files 个JSON文件" >&2 for info_file in "${sorted_files[@]}"; do debug "处理文件: $info_file" # 文件可读性检查 if [[ ! -r "$info_file" ]]; then warn "文件不可读: $info_file" >&2 continue fi # 文件大小检查 if [[ ! -s "$info_file" ]]; then warn "空文件: $info_file" >&2 continue fi # 解析JSON if ! jq -e '.' "$info_file" >/dev/null 2>&1; then warn "无效的JSON文件: $info_file" >&2 continue fi # 提取字段(带默认值) local patch_name apply_time backup_path status patch_name=$(jq -r '.patch_name // "unknown_patch"' "$info_file" 2>/dev/null) apply_time=$(jq -r '.apply_time // "unknown_time"' "$info_file" 2>/dev/null) backup_path=$(jq -r '.backup_path // ""' "$info_file" 2>/dev/null) status=$(jq -r '.status // "unknown"' "$info_file" 2>/dev/null) # 验证必要字段 if [[ -z "$backup_path" ]]; then debug "缺少backup_path字段: $info_file" >&2 continue fi # 验证备份文件 if [[ ! -f "$backup_path" ]]; then warn "备份文件不存在: $backup_path (来自 $info_file)" >&2 continue fi # 验证状态 if [[ "$status" != "applied" ]]; then debug "状态不是applied: $status (来自 $info_file)" >&2 continue fi # 有效的可回滚文件 debug "有效的可回滚文件: $info_file" >&2 count=$((count+1)) valid_files=$((valid_files+1)) echo "$info_file" >> "$temp_file" echo " $count. $patch_name" echo " 应用时间: $apply_time" echo " 备份文件: $(basename "$backup_path")" echo " 状态: $status" echo " 信息文件: $(basename "$info_file")" done info "扫描完成: 找到 $found_files 个文件,其中 $valid_files 个可回滚" >&2 if [[ $valid_files -eq 0 ]]; then rm -f "$temp_file" return 1 fi export ROLLBACK_LIST_FILE="$temp_file" export ROLLBACK_COUNT=$valid_files return 0 } # 选择回滚点 select_rollback_point() { local rollback_id="${1:-}" if [[ -z "$rollback_id" ]]; then info "交互式,请选择要回滚的点:" # 交互式选择 local points=() local count=0 # 先获取回滚点列表 local temp_file=$(mktemp) if ! list_rollback_points > "$temp_file"; then rm -f "$temp_file" return 1 fi # 从临时文件中读取回滚点数组 if [[ -f "$temp_file" ]]; then cat "$temp_file" # 使用临时文件存储回滚点数量 local count_file=$(mktemp) echo "0" > "$count_file" # 注意:这里我们重新加载 points 数组 local temp_points_file=$(mktemp) while IFS= read -r -d '' info_file; do if [[ -f "$info_file" ]]; then local backup_path=$(jq -r '.backup_path // ""' "$info_file" 2>/dev/null) local status=$(jq -r '.status // "unknown"' "$info_file" 2>/dev/null) if [[ -n "$backup_path" && -f "$backup_path" && "$status" == "applied" ]]; then echo "$info_file" >> "$temp_points_file" local current_count=$(cat "$count_file") echo $((current_count + 1)) > "$count_file" fi fi done < <(find "$ROLLBACK_INFO_DIR" -name "*.json" -print0 2>/dev/null | sort -z) # 读取计数 count=$(cat "$count_file") rm -f "$count_file" if [[ -f "$temp_points_file" ]]; then mapfile -t points < "$temp_points_file" rm -f "$temp_points_file" fi rm -f "$temp_file" fi if [[ $count -eq 0 ]]; then error "无可用的回滚点" return 1 fi echo "" read -p "📝 请选择要回滚的补丁编号 (1-${count}): " selected_num if [[ ! "$selected_num" =~ ^[0-9]+$ ]] || [[ "$selected_num" -lt 1 ]] || [[ "$selected_num" -gt "$count" ]]; then error "无效的选择: $selected_num" return 1 fi rollback_id="${points[$((selected_num-1))]}" else # 直接指定回滚点 if [[ "$rollback_id" == "latest" ]]; then # 选择最新的回滚点 rollback_id=$(find "$ROLLBACK_INFO_DIR" -name "*.json" -exec jq -r '.apply_time + "|" + input_filename' {} \; 2>/dev/null | \ sort -r | head -1 | cut -d'|' -f2) if [[ -z "$rollback_id" ]]; then error "未找到回滚点" return 1 fi elif [[ ! -f "$rollback_id" ]]; then # 检查是否是文件路径 if [[ -f "$ROLLBACK_INFO_DIR/$rollback_id" ]]; then rollback_id="$ROLLBACK_INFO_DIR/$rollback_id" elif [[ -f "$ROLLBACK_INFO_DIR/${rollback_id}.json" ]]; then rollback_id="$ROLLBACK_INFO_DIR/${rollback_id}.json" else error "回滚点不存在: $rollback_id" return 1 fi fi fi if [[ ! -f "$rollback_id" ]]; then error "回滚信息文件不存在: $rollback_id" return 1 fi echo "$rollback_id" return 0 } # 解析回滚信息 parse_rollback_info() { local info_file="$1" info "📖 解析回滚信息: $(basename "$info_file")" >&2 if [[ ! -f "$info_file" ]]; then error "回滚信息文件不存在: $info_file" >&2 return 1 fi # 使用jq解析JSON local patch_name=$(jq -r '.patch_name // "unknown"' "$info_file" 2>/dev/null) local apply_time=$(jq -r '.apply_time // "unknown"' "$info_file" 2>/dev/null) local backup_path=$(jq -r '.backup_path // ""' "$info_file" 2>/dev/null) local rollback_dir=$(jq -r '.rollback_dir // ""' "$info_file" 2>/dev/null) local manifest_info=$(jq -r '.manifest_info // ""' "$info_file" 2>/dev/null) local status=$(jq -r '.status // "unknown"' "$info_file" 2>/dev/null) # 验证必要字段 if [[ -z "$backup_path" ]]; then error "回滚信息缺少备份路径" >&2 return 1 fi if [[ ! -f "$backup_path" ]]; then error "备份文件不存在: $backup_path" >&2 return 1 fi if [[ "$status" != "applied" ]]; then warn "回滚点状态为 $status,可能已回滚或无效" >&2 fi # 创建回滚信息结构 cat << EOF { "info_file": "$info_file", "patch_name": "$patch_name", "apply_time": "$apply_time", "backup_path": "$backup_path", "rollback_dir": "$rollback_dir", "manifest_info": "$manifest_info", "status": "$status" } EOF return 0 } # 验证备份文件 verify_backup_file() { local backup_file="$1" info "🔍 验证备份文件: $(basename "$backup_file")" if [[ ! -f "$backup_file" ]]; then error "备份文件不存在: $backup_file" return 1 fi # 检查文件大小 local size=$(stat -c%s "$backup_file" 2>/dev/null || echo "0") if [[ $size -eq 0 ]]; then error "备份文件为空: $backup_file" return 1 fi # 验证压缩包完整性 if ! tar -tzf "$backup_file" >/dev/null 2>&1; then error "备份文件损坏或格式错误: $backup_file" return 1 fi info "✅ 备份文件验证通过" return 0 } # 提取备份文件 extract_backup() { local backup_file="$1" local extract_dir="$2" info "📦 提取备份文件到: $extract_dir" if [[ ! -d "$extract_dir" ]]; then mkdir -p "$extract_dir" fi if tar -xzf "$backup_file" -C "$extract_dir"; then info "✅ 备份文件提取完成" # 验证提取内容 # 查找 changes.txt 文件是否存在, 如果存在,则说明找到了,如果不存在,则说明备份文件提取失败 if [[ -f "$extract_dir/changes.txt" ]]; then info "找到 changes.txt 文件" else warn "❌ 备份文件中未找到 changes.txt 文件" return 1 fi # 查找解压目录下的contents目录,获得文件数量 local contents_dir=$(normalize_path "$extract_dir/contents") local file_count=$(find "$contents_dir" -type f | wc -l) if [[ $file_count -gt 0 ]]; then info "找到 $file_count 个备份文件" return 0 else warn "⚠️ 备份文件中未找到文件" return 0 fi else error "❌ 备份文件提取失败" return 1 fi } # 执行回滚操作 perform_rollback() { local extract_dir="$1" local dry_run="${2:-false}" local rollback_info_file="${3:-}" info "🔄 开始执行回滚操作 (干跑模式: $dry_run)" if [[ ! -d "$extract_dir" ]]; then error "提取目录不存在: $extract_dir" return 1 fi local rollback_count=0 local error_count=0 local skip_count=0 local deleted_count=0 # 首先处理新增文件的删除 handle_added_files_deletion "$extract_dir" "$dry_run" deleted_count=$? # 查找所有备份文件并恢复, 备份文件存放于contents目录下 local contents_dir="$extract_dir/contents" contents_dir=$(normalize_path "$contents_dir") find "$contents_dir" -type f | while read backup_file; do local relative_path="${backup_file#$contents_dir/}" relative_path=$(normalize_path "$relative_path") local target_file="$relative_path" target_file=$(normalize_path "$target_file") local target_dir=$(dirname "$target_file") target_dir=$(normalize_path "$target_dir") if [[ "$dry_run" == "true" ]]; then info "📋 [干跑] 回滚文件: $relative_path → $target_file" >&2 rollback_count=$((rollback_count+1)) continue fi # 创建目标目录 if [[ ! -d "$target_dir" ]]; then if ! mkdir -p "$target_dir"; then error "❌ 创建目录失败: $target_dir" >&2 error_count=$((error_count+1)) continue fi debug "创建目录: $target_dir" fi # 备份当前文件(如果存在) if [[ -f "$target_file" ]]; then local current_backup="$TEMP_DIR/current_backup/$relative_path" local current_dir=$(dirname "$current_backup") mkdir -p "$current_dir" if ! cp -p "$target_file" "$current_backup"; then warn "⚠️ 当前文件备份失败: $target_file" >&2 fi fi # 恢复文件 if cp -p "$backup_file" "$target_file"; then info "✅ 回滚文件: $relative_path | $backup_file -> $target_file" >&2 ((rollback_count++)) else error "❌ 文件回滚失败: $relative_path" >&2 error_count=$((error_count+1)) fi done info "📊 回滚操作统计:" info " 恢复文件: $rollback_count" info " 删除新增: $deleted_count" info " 失败: $error_count" info " 跳过: $skip_count" if [[ $error_count -gt 0 ]]; then error "❌ 回滚操作存在失败项" return 1 fi if [[ "$dry_run" == "true" ]]; then info "✅ 干跑模式完成 - 可安全执行回滚" return 0 else info "✅ 回滚操作完成" return 0 fi } # 处理新增文件的删除 handle_added_files_deletion() { local extract_dir="$1" local dry_run="$2" info "🗑️ 处理新增文件删除..." local changes_file="$extract_dir/changes.txt" if [[ ! -f "$changes_file" ]]; then warn "⚠️ changes.txt 文件不存在,无法处理新增文件删除" return 0 fi local error_count=0 local skip_count=0 local deleted_count=0 # 处理ADDED类型的文件 while IFS='|' read -r change_type path extra_info; do if [[ "$change_type" == "ADDED" ]]; then local target_file=$(normalize_path "$path") if [[ "$dry_run" == "true" ]]; then info "📋 [干跑] 删除新增文件: $target_file" >&2 ((deleted_count++)) continue fi # 检查文件是否存在 if [[ -f "$target_file" ]]; then # 备份即将删除的文件 local backup_target="$TEMP_DIR/added_files_backup/$path" backup_target=$(normalize_path "$backup_target") local backup_dir=$(dirname "$backup_target") backup_dir=$(normalize_path "$backup_dir") mkdir -p "$backup_dir" if cp -p "$target_file" "$backup_target" 2>/dev/null; then debug "备份新增文件: $target_file" fi # 删除新增文件 if rm -f "$target_file"; then info "✅ 删除新增文件: $target_file" >&2 ((deleted_count++)) # 尝试清理空目录 local target_dir=$(dirname "$target_file") target_dir=$(normalize_path "$target_dir") clean_empty_directories_rollback "$target_dir" else error_count=$((error_count+1)) error "❌ 删除新增文件失败: $target_file" >&2 fi else warn "⚠️ 新增文件不存在,无需删除: $target_file" >&2 ((skip_count++)) fi fi done < "$changes_file" return $deleted_count } # 清理空目录(回滚版本) clean_empty_directories_rollback() { local dir="$1" while [[ "$dir" != "/" ]] && [[ -d "$dir" ]]; do # 检查目录是否为空(只检查文件,不考虑隐藏文件) if [[ -z "$(ls -A "$dir" 2>/dev/null)" ]]; then if rmdir "$dir" 2>/dev/null; then debug "清理空目录: $dir" dir=$(dirname "$dir") else break # 目录删除失败,停止清理 fi else break # 目录非空,停止清理 fi done } # 验证回滚结果 verify_rollback() { local extract_dir="$1" local rollback_info="$2" if [[ "$VALIDATION_ENABLED" != "true" ]]; then info "验证功能已禁用" return 0 fi info "🔍 验证回滚结果..." local verification_passed=true local verified_count=0 local failed_count=0 # 查找所有备份文件并恢复, 备份文件存放于contents目录下 local contents_dir="$extract_dir/contents" contents_dir=$(normalize_path "$contents_dir") # 检查所有备份文件是否已正确恢复 find "$contents_dir" -type f | while read backup_file; do local relative_path="${backup_file#$contents_dir/}" local target_file="$relative_path" if [[ ! -f "$target_file" ]]; then error "❌ 目标文件不存在: $target_file" verification_passed=false failed_count=$((failed_count+1)) continue fi # 比较文件内容 if ! cmp -s "$backup_file" "$target_file"; then error "❌ 文件内容不匹配: $relative_path" verification_passed=false failed_count=$((failed_count+1)) continue fi verified_count=$((verified_count+1)) done info "📊 回滚验证统计:" info " 成功: $verified_count" info " 失败: $failed_count" if $verification_passed; then info "✅ 回滚验证通过" return 0 else error "❌ 回滚验证失败" return 1 fi } # 更新回滚点状态 update_rollback_status() { local info_file="$1" local new_status="$2" if [[ ! -f "$info_file" ]]; then error "回滚信息文件不存在: $info_file" return 1 fi # 使用jq更新状态 if jq ".status = \"$new_status\" | .rollback_time = \"$(date -Iseconds)\"" "$info_file" > "$info_file.tmp"; then mv "$info_file.tmp" "$info_file" info "✅ 回滚点状态更新为: $new_status" return 0 else error "❌ 回滚点状态更新失败" return 1 fi } # 清理回滚点 cleanup_rollback_point() { local info_file="$1" local extract_dir="$2" info "🧹 清理回滚资源..." # 删除提取目录 if [[ -d "$extract_dir" ]]; then rm -rf "$extract_dir" debug "清理提取目录: $extract_dir" fi # 可选:删除备份文件(谨慎操作) local delete_backup="${3:-false}" if [[ "$delete_backup" == "true" ]]; then local backup_path=$(jq -r '.backup_path // ""' "$info_file") if [[ -n "$backup_path" && -f "$backup_path" ]]; then rm -f "$backup_path" info "🗑️ 删除备份文件: $(basename "$backup_path")" fi fi # 删除回滚信息文件 if [[ -f "$info_file" ]]; then rm -f "$info_file" info "🗑️ 删除回滚信息文件: $(basename "$info_file")" fi info "✅ 回滚资源清理完成" } # 发送通知 send_rollback_notification() { local status="$1" local rollback_info="$2" local message="$3" if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then return 0 fi local patch_name=$(echo "$rollback_info" | jq -r '.patch_name') local apply_time=$(echo "$rollback_info" | jq -r '.apply_time') local subject="" local color="" case "$status" in "start") subject="回滚操作开始" color="#3498db" ;; "success") subject="回滚操作成功" color="#2ecc71" ;; "failure") subject="回滚操作失败" color="#e74c3c" ;; *) subject="回滚操作通知" color="#95a5a6" ;; esac # Slack通知 if [[ -n "${SLACK_WEBHOOK:-}" ]]; then send_slack_rollback_notification "$subject" "$message" "$color" "$patch_name" "$apply_time" fi log "NOTIFICATION" "$subject: $message" } send_slack_rollback_notification() { local subject="$1" local message="$2" local color="$3" local patch_name="$4" local apply_time="$5" local payload=$(cat << EOF { "attachments": [ { "color": "$color", "title": "$subject", "text": "$message", "fields": [ { "title": "补丁名称", "value": "$patch_name", "short": true }, { "title": "应用时间", "value": "$apply_time", "short": true }, { "title": "回滚时间", "value": "$(date)", "short": true }, { "title": "回滚主机", "value": "$(hostname)", "short": true } ] } ] } EOF ) if command -v curl >/dev/null 2>&1; then if curl -s -X POST -H 'Content-type: application/json' \ --data "$payload" "$SLACK_WEBHOOK" >/dev/null; then info "✅ Slack通知发送成功" else warn "⚠️ Slack通知发送失败" fi fi } # 主回滚函数 rollback_patch() { local rollback_point="${1:-}" local dry_run="${2:-false}" local cleanup_after="${3:-false}" info "🚀 开始回滚操作, 回滚点: ${rollback_point}" # 发送开始通知 local rollback_info_json="" local rollback_info_file="" # 选择回滚点 info "定位到回滚点 ${rollback_point}" rollback_info_file=$(select_rollback_point "$rollback_point") if [[ $? -ne 0 ]]; then send_rollback_notification "failure" "{}" "回滚点选择失败" return 1 fi # 解析回滚信息 info "解析回滚信息 ${rollback_info_file}" rollback_info_json=$(parse_rollback_info "$rollback_info_file") if [[ $? -ne 0 ]]; then send_rollback_notification "failure" "{}" "回滚信息解析失败" return 1 fi local patch_name=$(echo "$rollback_info_json" | jq -r '.patch_name') local backup_path=$(echo "$rollback_info_json" | jq -r '.backup_path') send_rollback_notification "start" "$rollback_info_json" "开始回滚补丁: $patch_name" # 验证备份文件 if ! verify_backup_file "$backup_path"; then send_rollback_notification "failure" "$rollback_info_json" "备份文件验证失败" return 1 fi # 创建临时目录 TEMP_DIR=$(mktemp -d "/tmp/patch_rollback_XXXXXX") local extract_dir="$TEMP_DIR/extract" # 提取备份文件 if ! extract_backup "$backup_path" "$extract_dir"; then send_rollback_notification "failure" "$rollback_info_json" "备份文件提取失败" return 1 fi # 执行回滚操作 if ! perform_rollback "$extract_dir" "$dry_run" "$rollback_info_file"; then send_rollback_notification "failure" "$rollback_info_json" "回滚操作失败" return 1 fi # 验证回滚结果(非干跑模式) if [[ "$dry_run" != "true" ]]; then if ! verify_rollback "$extract_dir" "$rollback_info_json"; then send_rollback_notification "failure" "$rollback_info_json" "回滚验证失败" return 1 fi # 更新回滚点状态 if ! update_rollback_status "$rollback_info_file" "rolled_back"; then warn "⚠️ 回滚点状态更新失败" fi # 清理资源 if [[ "$cleanup_after" == "true" ]]; then cleanup_rollback_point "$rollback_info_file" "$extract_dir" "true" else cleanup_rollback_point "$rollback_info_file" "$extract_dir" "false" fi fi send_rollback_notification "success" "$rollback_info_json" "回滚操作成功完成" info "✅ 回滚流程完成" return 0 } # 批量清理旧回滚点 cleanup_old_rollback_points() { local max_points="${1:-$MAX_ROLLBACK_POINTS}" info "🧹 清理旧回滚点 (保留最近 $max_points 个)" if [[ ! -d "$ROLLBACK_INFO_DIR" ]]; then warn "回滚信息目录不存在" return 0 fi # 获取所有回滚点并按时间排序 local points=() while IFS= read -r -d '' info_file; do if [[ -f "$info_file" ]]; then points+=("$info_file") fi done < <(find "$ROLLBACK_INFO_DIR" -name "*.json" -print0 2>/dev/null) local total_points=${#points[@]} local points_to_remove=$((total_points - max_points)) if [[ $points_to_remove -le 0 ]]; then info "无需清理,当前 $total_points 个回滚点" return 0 fi info "需要清理 $points_to_remove 个回滚点" # 按修改时间排序,删除最旧的 for ((i=0; i 回滚点编号(使用-l查看) <文件名> 回滚信息文件路径 <补丁名> 补丁名称 示例: $0 -l # 列出所有回滚点 $0 latest # 回滚最新的补丁 $0 1 # 回滚编号为1的补丁 $0 --dry-run latest # 干跑模式测试回滚 $0 --cleanup latest # 回滚并清理资源 $0 --purge # 清理旧回滚点 环境变量: PATCH_CONFIG 配置文件路径 BACKUP_DIR 备份目录 MAX_ROLLBACK_POINTS 最大保留回滚点数 EOF } # 显示版本信息 show_version() { echo "patch_rollback.sh v1.0.0" echo "企业级补丁回滚系统" echo "支持原子性回滚、多回滚点管理、验证等功能" } # 主函数 main() { local rollback_point="" local dry_run="false" local cleanup_after="false" local list_only="false" local purge_all="false" local rollback_all="false" local verbose_mode="false" # 解析命令行参数 while [[ $# -gt 0 ]]; do case $1 in -l|--list) list_only="true" shift ;; -d|--dry-run) dry_run="true" shift ;; -c|--cleanup) cleanup_after="true" shift ;; -p|--purge) purge_all="true" shift ;; -a|--all) rollback_all="true" shift ;; -v|--verbose) verbose_mode="true" shift ;; -q|--quiet) exec >/dev/null 2>&1 shift ;; -h|--help) usage exit 0 ;; -V|--version) show_version exit 0 ;; --) shift break ;; -*) error "未知选项: $1" usage exit 1 ;; *) rollback_point="$1" shift ;; esac done # 初始化 acquire_lock load_config setup_environment check_dependencies # 处理特殊操作 if [[ "$list_only" == "true" ]]; then list_rollback_points exit $? fi if [[ "$purge_all" == "true" ]]; then cleanup_old_rollback_points 0 exit $? fi if [[ "$rollback_all" == "true" ]]; then rollback_all_patches "$dry_run" "$cleanup_after" exit $? fi # 执行回滚操作 if rollback_patch "$rollback_point" "$dry_run" "$cleanup_after"; then if [[ "$dry_run" == "true" ]]; then info "🎉 干跑模式完成 - 可安全执行回滚" else info "🎉 回滚操作成功完成" fi exit 0 else error "💥 回滚操作失败" exit 1 fi } # 批量回滚所有补丁 rollback_all_patches() { local dry_run="$1" local cleanup_after="$2" info "🔄 开始批量回滚所有补丁" if ! list_rollback_points; then error "没有可用的回滚点" return 1 fi local total_count=${#points[@]} local success_count=0 local fail_count=0 info "📊 找到 $total_count 个回滚点,开始批量回滚..." # 按时间倒序回滚(最新的先回滚) for ((i=total_count-1; i>=0; i--)); do local info_file="${points[$i]}" local patch_name=$(basename "$info_file" .json) info "🔄 回滚 ($((total_count-i))/$total_count): $patch_name" if rollback_patch "$info_file" "$dry_run" "$cleanup_after"; then success_count=$((success_count+1)) info "✅ 回滚成功: $patch_name" else fail_count=$((fail_count+1)) error "❌ 回滚失败: $patch_name" # 继续尝试下一个 fi echo "---" done info "📊 批量回滚完成:" info " 成功: $success_count" info " 失败: $fail_count" info " 总计: $total_count" if [[ $fail_count -eq 0 ]]; then info "✅ 所有回滚操作成功" return 0 else error "⚠️ 部分回滚操作失败" return 1 fi } # 异常处理 trap 'error "脚本执行中断"; exit 1' INT TERM # 执行主函数 main "$@"