#!/bin/bash # patch_applier.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/apply_$(date +%Y%m%d_%H%M%S).log" LOCK_FILE="/tmp/patch_apply.lock" ROLLBACK_INFO_DIR="/var/lib/patch_system/rollback_info" # 颜色定义 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"} : ${BACKUP_DIR:="/var/backups/patch"} : ${TEMP_DIR:="/tmp/patch_apply_$$"} : ${ROLLBACK_ENABLED:="true"} : ${ROLLBACK_INFO_DIR:="/var/lib/patch_system/rollback_info"} : ${VALIDATION_ENABLED:="true"} : ${NOTIFICATIONS_ENABLED:="true"} : ${ATOMIC_OPERATIONS:="true"} : ${MAX_BACKUP_COUNT:=10} : ${PATCH_STAGING_DIR:="/tmp/patch_staging"} } setup_environment() { mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$BACKUP_DIR" mkdir -p "$TEMP_DIR" mkdir -p "$ROLLBACK_INFO_DIR" mkdir -p "$PATCH_STAGING_DIR" info "环境设置完成" info "日志文件: $LOG_FILE" info "备份目录: $BACKUP_DIR" info "临时目录: $TEMP_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++)) else warn "清理过期的锁文件" rm -f "$LOCK_FILE" fi done error "无法获取锁,可能已有补丁应用在进行中" exit 1 } release_lock() { rm -f "$LOCK_FILE" info "🔓 释放应用锁" } 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" "find" "stat" "sha256sum" "date" "mkdir" "cp" "jq") 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 } # 补丁包验证 verify_patch_package() { local patch_file="$1" info "🔍 验证补丁包: $patch_file" # 检查文件存在性 if [[ ! -f "$patch_file" ]]; then error "补丁包不存在: $patch_file" return 1 fi # 检查文件大小 local size=$(stat -c%s "$patch_file" 2>/dev/null || echo "0") if [[ $size -eq 0 ]]; then error "补丁包为空: $patch_file" return 1 fi # 验证压缩包完整性 if ! tar -tzf "$patch_file" >/dev/null 2>&1; then error "补丁包损坏或格式错误: $patch_file" return 1 fi # 验证校验和(如果存在) local checksum_file="${patch_file}.sha256" if [[ -f "$checksum_file" ]]; then if sha256sum -c "$checksum_file" >/dev/null 2>&1; then info "✅ 校验和验证通过" else error "❌ 校验和验证失败" return 1 fi fi # 验证签名(如果存在) local sig_file="${patch_file}.sig" if [[ -f "$sig_file" ]] && command -v gpg >/dev/null 2>&1; then if gpg --verify "$sig_file" "$patch_file" >/dev/null 2>&1; then info "✅ 签名验证通过" else error "❌ 签名验证失败" return 1 fi fi info "✅ 补丁包验证通过" return 0 } # 提取补丁包 extract_patch() { local patch_file="$1" local extract_dir="$2" info "📦 提取补丁包到: $extract_dir" if [[ ! -d "$extract_dir" ]]; then mkdir -p "$extract_dir" fi if tar -xzf "$patch_file" -C "$extract_dir"; then info "✅ 补丁包提取完成" # 验证提取内容 if [[ -f "$extract_dir/MANIFEST.MF" ]]; then info "✅ 清单文件验证通过" return 0 else error "❌ 补丁包缺少清单文件" return 1 fi else error "❌ 补丁包提取失败" return 1 fi } # 解析清单文件 parse_manifest() { local manifest_file="$1" info "📋 解析清单文件: $manifest_file" >&2 if [[ ! -f "$manifest_file" ]]; then error "清单文件不存在: $manifest_file" return 1 fi # 读取基础信息 local patch_name=$(grep -i "名称:" "$manifest_file" | cut -d':' -f2- | sed 's/^[[:space:]]*//') local patch_version=$(grep -i "版本:" "$manifest_file" | cut -d':' -f2- | sed 's/^[[:space:]]*//') local change_count=$(grep -c -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file" 2>/dev/null || echo "0") echo "补丁信息:" echo " 名称: ${patch_name:-未知}" echo " 版本: ${patch_version:-未知}" echo " 变更数: $change_count" echo "" # 提取变更列表 local changes=() while IFS='|' read -r change_type path extra_info; do changes+=("$change_type|$path|$extra_info") done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file") if [[ ${#changes[@]} -eq 0 ]]; then warn "⚠️ 清单中没有找到变更记录" >&2 fi # 返回变更数组 printf "%s\n" "${changes[@]}" return 0 } # 创建备份 create_backup() { local changes_file="$1" local backup_dir="$2" info "💾 创建应用前备份..." >&2 if [[ ! -s "$changes_file" ]]; then warn "无变更需要备份" >&2 return 0 fi local files_to_backup=() # 分析需要备份的文件 while IFS='|' read -r change_type path extra_info; do case "$change_type" in "MODIFIED"|"DELETED") local target_file="$path" if [[ -f "$target_file" ]]; then files_to_backup+=("$target_file") debug "需要备份: $target_file" >&2 fi ;; esac done < "$changes_file" if [[ ${#files_to_backup[@]} -eq 0 ]]; then info "⚠️ 无文件需要备份" >&2 return 0 fi # 创建备份 local backup_tar="$backup_dir/backup_$(date +%Y%m%d_%H%M%S).tar.gz" mkdir -p "$(dirname "$backup_tar")" local work_dir="$(pwd)" # 默认是当前工作目录 if tar -czf "$backup_tar" -C "${work_dir}" "${files_to_backup[@]/#\//}"; then local size=$(du -h "$backup_tar" | cut -f1) info "✅ 备份创建完成: $backup_tar ($size)" >&2 echo "$backup_tar" return 0 else error "❌ 备份创建失败" >&2 return 1 fi } # 应用文件变更 apply_file_changes() { local extract_dir="$1" local changes_file="$2" local dry_run="${3:-false}" info "🔄 开始应用文件变更 (干跑模式: $dry_run)" >&2 local applied_count=0 local failed_count=0 local skip_count=0 # 读取变更列表 while IFS='|' read -r change_type path extra_info; do case "$change_type" in "ADDED") apply_file_addition "$extract_dir" "$path" "$dry_run" result=$? ;; "MODIFIED") apply_file_modification "$extract_dir" "$path" "$dry_run" result=$? ;; "DELETED") apply_file_deletion "$path" "$dry_run" result=$? ;; *) warn "⚠️ 未知变更类型: $change_type" >&2 result=99 ;; esac case $result in 0) ((applied_count++)) ;; 1) ((failed_count++)) ;; 2) ((skip_count++)) ;; esac done < "$changes_file" info "📊 变更应用统计:" >&2 info " 成功: $applied_count" >&2 info " 失败: $failed_count" >&2 info " 跳过: $skip_count" >&2 info " 总计: $((applied_count + failed_count + skip_count))" >&2 if [[ $failed_count -gt 0 ]]; then error "❌ 存在应用失败的变更" >&2 return 1 fi if [[ "$dry_run" == "true" ]]; then info "✅ 干跑模式完成 - 无实际变更" >&2 return 0 else info "✅ 文件变更应用完成" >&2 return 0 fi } # 应用文件新增 apply_file_addition() { local extract_dir="$1" local file_path="$2" local dry_run="$3" local source_file="$extract_dir/files/$file_path" local target_file="$file_path" local target_dir=$(dirname "$target_file") # 检查源文件是否存在 if [[ ! -f "$source_file" ]]; then error "源文件不存在: $source_file" >&2 return 1 fi if [[ "$dry_run" == "true" ]]; then info "📋 [干跑] 新增文件: $file_path" >&2 return 0 fi # 创建目标目录 if [[ ! -d "$target_dir" ]]; then if ! mkdir -p "$target_dir"; then error "创建目录失败: $target_dir" >&2 return 1 fi debug "创建目录: $target_dir" >&2 fi # 检查目标文件是否已存在 if [[ -f "$target_file" ]]; then warn "目标文件已存在,将被覆盖: $target_file" >&2 fi # 复制文件 if cp "$source_file" "$target_file"; then # 设置权限 set_file_permissions "$target_file" "644" info "✅ 新增文件: $file_path" >&2 return 0 else error "❌ 文件新增失败: $file_path" >&2 return 1 fi } # 应用文件修改 apply_file_modification() { local extract_dir="$1" local file_path="$2" local dry_run="$3" local source_file="$extract_dir/files/$file_path" local target_file="$file_path" info "🔄 开始应用文件修改: $file_path" >&2 info "当前工作目录: $(pwd)" >&2 info "源文件: $source_file" >&2 info "目标文件: $target_file" >&2 # 检查源文件 if [[ ! -f "$source_file" ]]; then error "源文件不存在: $source_file" >&2 return 1 fi # 检查目标文件 if [[ ! -f "$target_file" ]]; then warn "目标文件不存在,将作为新增处理: $target_file" >&2 return apply_file_addition "$extract_dir" "$file_path" "$dry_run" fi if [[ "$dry_run" == "true" ]]; then info "📋 [干跑] 修改文件: $file_path" >&2 return 0 fi # 备份原文件 local backup_file="$TEMP_DIR/backup/$file_path" local backup_dir=$(dirname "$backup_file") mkdir -p "$backup_dir" if ! cp -p "$target_file" "$backup_file"; then error "备份原文件失败: $target_file" >&2 return 1 fi # 应用修改 if cp "$source_file" "$target_file"; then # 保持原权限 local original_perm=$(stat -c%a "$backup_file" 2>/dev/null || echo "644") set_file_permissions "$target_file" "$original_perm" info "✅ 修改文件: $file_path" >&2 return 0 else error "❌ 文件修改失败: $file_path" >&2 # 恢复备份 cp "$backup_file" "$target_file" return 1 fi } # 应用文件删除 apply_file_deletion() { local file_path="$1" local dry_run="$2" local target_file="$file_path" if [[ ! -f "$target_file" ]]; then warn "文件不存在,无需删除: $target_file" >&2 return 2 # 跳过 fi if [[ "$dry_run" == "true" ]]; then info "📋 [干跑] 删除文件: $file_path" >&2 return 0 fi # 备份文件 local backup_file="$TEMP_DIR/backup/$file_path" local backup_dir=$(dirname "$backup_file") mkdir -p "$backup_dir" if ! cp -p "$target_file" "$backup_file"; then error "备份文件失败: $target_file" >&2 return 1 fi # 删除文件 if rm -f "$target_file"; then info "✅ 删除文件: $file_path" >&2 # 尝试清理空目录 clean_empty_directories "$(dirname "$target_file")" return 0 else error "❌ 文件删除失败: $file_path" >&2 return 1 fi } # 清理空目录 clean_empty_directories() { local dir="$1" while [[ "$dir" != "/" ]] && [[ -d "$dir" ]]; do if ! rmdir "$dir" 2>/dev/null; then break # 目录非空,停止清理 fi debug "清理空目录: $dir" >&2 dir=$(dirname "$dir") done } # 设置文件权限 set_file_permissions() { local file="$1" local permissions="${2:-644}" if chmod "$permissions" "$file" 2>/dev/null; then debug "设置权限: $file → $permissions" else warn "权限设置失败: $file" fi } # 验证应用结果 verify_application() { local extract_dir="$1" local changes_file="$2" if [[ "$VALIDATION_ENABLED" != "true" ]]; then info "验证功能已禁用" >&2 return 0 fi info "🔍 验证应用结果..." >&2 local manifest_file="$extract_dir/MANIFEST.MF" local verification_passed=true local verified_count=0 local failed_count=0 # 读取变更列表进行验证 while IFS='|' read -r change_type path extra_info; do case "$change_type" in "ADDED"|"MODIFIED") if verify_file_application "$extract_dir" "$path" "$change_type"; then ((verified_count++)) else ((failed_count++)) verification_passed=false fi ;; "DELETED") if verify_file_deletion "$path"; then ((verified_count++)) else ((failed_count++)) verification_passed=false fi ;; esac done < "$changes_file" info "📊 验证统计:" >&2 info " 成功: $verified_count" >&2 info " 失败: $failed_count" >&2 if $verification_passed; then info "✅ 应用验证通过" >&2 return 0 else error "❌ 应用验证失败" >&2 return 1 fi } # 验证文件应用 verify_file_application() { local extract_dir="$1" local file_path="$2" local change_type="$3" local source_file="$extract_dir/files/$file_path" local target_file="$file_path" # 检查目标文件是否存在 if [[ ! -f "$target_file" ]]; then error "❌ 目标文件不存在: $target_file" return 1 fi # 比较文件内容 local source_hash=$(sha256sum "$source_file" | cut -d' ' -f1) local target_hash=$(sha256sum "$target_file" | cut -d' ' -f1) if [[ "$source_hash" != "$target_hash" ]]; then error "❌ 文件内容不匹配: $file_path" error " 源哈希: $source_hash" error " 目标哈希: $target_hash" return 1 fi debug "✅ 文件验证通过: $file_path" return 0 } # 验证文件删除 verify_file_deletion() { local file_path="$1" local target_file="$file_path" if [[ -f "$target_file" ]]; then error "❌ 文件未成功删除: $target_file" return 1 fi debug "✅ 文件删除验证通过: $file_path" return 0 } # 创建回滚点 create_rollback_point() { local patch_file="$1" local backup_file="$2" local extract_dir="$3" local changes_file="$4" if [[ "$ROLLBACK_ENABLED" != "true" ]]; then info "回滚功能已禁用" return 0 fi info "🔄 创建回滚点..." local patch_name=$(basename "$patch_file" .tar.gz) local timestamp=$(date +%Y%m%d_%H%M%S) local rollback_info_file="$ROLLBACK_INFO_DIR/${patch_name}_${timestamp}.json" # 读取清单信息 local manifest_file="$extract_dir/MANIFEST.MF" local patch_info="" if [[ -f "$manifest_file" ]]; then patch_info=$(grep -E "^(名称|版本|描述):" "$manifest_file" | head -3 | tr '\n' ';') fi # 统计变更类型 local added_count=$(grep -c "^ADDED" "$changes_file") local modified_count=$(grep -c "^MODIFIED" "$changes_file") local deleted_count=$(grep -c "^DELETED" "$changes_file") # 创建回滚信息JSON local rollback_info=$(cat << EOF { "patch_name": "$patch_name", "apply_time": "$(date -Iseconds)", "backup_path": "$backup_file", "rollback_dir": "$BACKUP_DIR/rollback_$timestamp", "manifest_info": "$patch_info", "status": "applied", "changes": { "added": $added_count, "modified": $modified_count, "deleted": $deleted_count, "total": $((added_count + modified_count + deleted_count)) }, "system_info": { "hostname": "$(hostname)", "user": "$(whoami)", "working_directory": "$PWD" } } EOF ) echo "$rollback_info" > "$rollback_info_file" info "✅ 回滚点创建完成: $rollback_info_file" # 清理旧回滚点 cleanup_old_rollback_points } # 清理旧回滚点 cleanup_old_rollback_points() { local max_points="${1:-$MAX_BACKUP_COUNT}" info "🧹 清理旧回滚点 (保留最近 $max_points 个)" if [[ ! -d "$ROLLBACK_INFO_DIR" ]]; then return 0 fi # 获取所有回滚点并按时间排序 local points=($(find "$ROLLBACK_INFO_DIR" -name "*.json" -type f -printf "%T@|%p\n" | sort -rn | cut -d'|' -f2)) local total_points=${#points[@]} local points_to_remove=$((total_points - max_points)) if [[ $points_to_remove -le 0 ]]; then debug "无需清理,当前 $total_points 个回滚点" return 0 fi info "清理 $points_to_remove 个旧回滚点" for ((i=max_points; i/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 } # 主应用函数 apply_patch() { local patch_file="$1" local dry_run="${2:-false}" local target_dir="${3:-./}" info "🚀 开始应用补丁包: $patch_file" info "目标目录: $target_dir" info "干跑模式: $dry_run" # 发送开始通知 send_notification "start" "$patch_file" "开始应用补丁" # 验证补丁包 if ! verify_patch_package "$patch_file"; then send_notification "failure" "$patch_file" "补丁包验证失败" return 1 fi # 创建临时目录 local extract_dir="$TEMP_DIR/extract" local changes_file="$TEMP_DIR/changes.txt" # 提取补丁包 info ">>提取补丁包..." if ! extract_patch "$patch_file" "$extract_dir"; then send_notification "failure" "$patch_file" "补丁包解压失败" return 1 fi # 解析清单文件 info ">>解析清单文件..." if ! parse_manifest "$extract_dir/MANIFEST.MF" > "$changes_file"; then send_notification "failure" "$patch_file" "清单文件解析失败" return 1 fi # 检查是否有变更 info ">>检查变更记录..." if [[ ! -s "$changes_file" ]]; then warn "⚠️ 补丁包中没有变更记录" send_notification "success" "$patch_file" "补丁包无变更记录" return 0 fi # 创建备份文件(非干跑模式) local backup_file="" if [[ "$dry_run" != "true" ]]; then info ">>创建备份..." backup_file=$(create_backup "$changes_file" "$BACKUP_DIR") if [[ $? -ne 0 ]]; then send_notification "failure" "$patch_file" "备份创建失败" return 1 fi fi # 应用变更 info ">>应用变更..." if ! apply_file_changes "$extract_dir" "$changes_file" "$dry_run"; then send_notification "failure" "$patch_file" "文件应用失败" return 1 fi # 验证应用结果(非干跑模式) info ">>验证应用结果..." if [[ "$dry_run" != "true" ]]; then if ! verify_application "$extract_dir" "$changes_file"; then send_notification "failure" "$patch_file" "应用验证失败" return 1 fi # 创建回滚点 info ">>创建回滚点..." create_rollback_point "$patch_file" "$backup_file" "$extract_dir" "$changes_file" fi send_notification "success" "$patch_file" "补丁应用成功" info "✅ 补丁应用流程完成" return 0 } # 显示使用帮助 usage() { cat << EOF 用法: $0 [选项] <补丁包路径> 选项: -d, --dry-run 干跑模式(只验证不应用) -t, --target DIR 目标目录(默认: /) -c, --config FILE 配置文件路径 -v, --verbose 详细输出 -q, --quiet 安静模式 -h, --help 显示此帮助 -V, --version 显示版本 示例: $0 patch.tar.gz # 应用补丁 $0 --dry-run patch.tar.gz # 干跑模式 $0 --target /app patch.tar.gz # 应用到指定目录 $0 --config custom_config.sh patch.tar.gz 环境变量: PATCH_CONFIG 配置文件路径 BACKUP_DIR 备份目录 LOG_LEVEL 日志级别 报告问题: 请检查日志文件 $LOG_FILE EOF } # 显示版本信息 show_version() { echo "patch_applier.sh v2.0.0" echo "企业级补丁应用系统" echo "支持原子性操作、完整验证、智能回滚点创建" } # 主函数 main() { local patch_file="" local dry_run="false" local target_dir="/" local verbose_mode="false" # 初始化 load_config setup_environment check_dependencies acquire_lock # 解析命令行参数 while [[ $# -gt 0 ]]; do case $1 in -d|--dry-run) dry_run="true" shift ;; -t|--target) target_dir="$2" shift 2 ;; -c|--config) CONFIG_FILE="$2" shift 2 ;; -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 ;; -*) error "未知选项: $1" usage exit 1 ;; *) patch_file="$1" shift ;; esac done # 检查参数 if [[ -z "$patch_file" ]]; then error "必须指定补丁包路径" usage exit 1 fi if [[ ! -f "$patch_file" ]]; then error "补丁包不存在: $patch_file" exit 1 fi # 执行补丁应用 if apply_patch "$patch_file" "$dry_run" "$target_dir"; then if [[ "$dry_run" == "true" ]]; then info "🎉 干跑模式完成 - 可安全应用补丁" else info "🎉 补丁应用成功完成!" fi exit 0 else error "💥 补丁应用失败" exit 1 fi } # 异常处理 trap 'error "脚本执行中断"; exit 1' INT TERM # 执行主函数 main "$@"