Files
patch-system-tools/patch_rollback.sh

1203 lines
35 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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<points_to_remove; i++)); do
local oldest_file=$(find "$ROLLBACK_INFO_DIR" -name "*.json" -exec stat -c "%Y|%n" {} \; | \
sort -n | head -1 | cut -d'|' -f2)
if [[ -n "$oldest_file" && -f "$oldest_file" ]]; then
local backup_path=$(jq -r '.backup_path // ""' "$oldest_file")
# 删除备份文件
if [[ -n "$backup_path" && -f "$backup_path" ]]; then
rm -f "$backup_path"
debug "删除备份文件: $(basename "$backup_path")"
fi
# 删除信息文件
rm -f "$oldest_file"
info "🗑️ 清理回滚点: $(basename "$oldest_file")"
fi
done
info "✅ 回滚点清理完成"
return 0
}
# 显示使用帮助
# 显示使用帮助
usage() {
cat << EOF
用法: $0 [选项] [回滚点]
选项:
-l, --list 列出可用的回滚点
-d, --dry-run 干跑模式(只验证不回滚)
-c, --cleanup 回滚后清理资源
-p, --purge 清理所有旧回滚点
-a, --all 回滚所有补丁
-v, --verbose 详细输出
-q, --quiet 安静模式
-h, --help 显示此帮助
-V, --version 显示版本
参数:
回滚点可以是以下格式:
latest 最新的回滚点
<数字> 回滚点编号(使用-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 "$@"