#!/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" # 颜色定义 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_apply_$(date +%Y%m%d_%H%M%S).log"} : ${BACKUP_DIR:="/var/backups/patch"} : ${TEMP_DIR:="/tmp/patch_apply_$$"} } setup_environment() { mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$BACKUP_DIR" mkdir -p "$TEMP_DIR" info "日志文件: $LOG_FILE" info "备份目录: $BACKUP_DIR" info "临时目录: $TEMP_DIR" } cleanup() { if [[ -d "$TEMP_DIR" ]]; then rm -rf "$TEMP_DIR" info "临时目录已清理: $TEMP_DIR" fi } trap cleanup EXIT # 验证函数 verify_package() { local package_path="$1" info "开始验证补丁包: $package_path" # 检查文件存在性 if [[ ! -f "$package_path" ]]; then error "补丁包不存在: $package_path" return 1 fi # 检查文件大小 local size=$(stat -c%s "$package_path" 2>/dev/null || echo "0") if [[ $size -eq 0 ]]; then error "补丁包为空: $package_path" return 1 fi # 验证校验和 if [[ -f "${package_path}.sha256" ]]; then info "验证校验和..." if sha256sum -c "${package_path}.sha256" >/dev/null 2>&1; then info "✅ 校验和验证通过" else error "❌ 校验和验证失败" return 1 fi fi # 验证签名 if [[ -f "${package_path}.sig" ]]; then info "验证签名..." if command -v gpg >/dev/null 2>&1; then if gpg --verify "${package_path}.sig" "$package_path" >/dev/null 2>&1; then info "✅ 签名验证通过" else error "❌ 签名验证失败" return 1 fi else warn "GPG未安装,跳过签名验证" fi fi # 验证压缩包完整性 if ! tar -tzf "$package_path" >/dev/null 2>&1; then error "❌ 压缩包损坏或格式错误" return 1 fi info "✅ 补丁包验证通过" return 0 } # 备份函数 create_backup() { local backup_name="backup_$(date +%Y%m%d_%H%M%S)" local backup_path="$BACKUP_DIR/$backup_name.tar.gz" info "创建应用前备份: $backup_path" # 获取需要备份的文件列表 local files_to_backup=() while IFS='|' read -r change_type path old_info new_info; do case "$change_type" in "MODIFIED"|"DELETED") local target_file="$path" if [[ -f "$target_file" ]]; then files_to_backup+=("$target_file") fi ;; esac done < "$TEMP_DIR/changes.txt" if [[ ${#files_to_backup[@]} -eq 0 ]]; then info "无需备份文件" return 0 fi # 创建备份 tar -czf "$backup_path" "${files_to_backup[@]}" local backup_size=$(du -h "$backup_path" | cut -f1) info "✅ 备份创建完成: $backup_path ($backup_size)" echo "$backup_path" } # 补丁应用函数 extract_package() { local package_path="$1" local extract_dir="$2" info "解压补丁包到: $extract_dir" mkdir -p "$extract_dir" if tar -xzf "$package_path" -C "$extract_dir"; then info "✅ 补丁包解压完成" return 0 else error "❌ 补丁包解压失败" return 1 fi } apply_file_changes() { local patch_dir="$1" info "开始应用文件变更..." local applied_count=0 local failed_count=0 # 读取变更清单 local manifest_file="$patch_dir/MANIFEST.MF" if [[ ! -f "$manifest_file" ]]; then error "清单文件不存在: $manifest_file" return 1 fi # 处理每个变更 while IFS='|' read -r change_type path extra_info; do case "$change_type" in "ADDED"|"MODIFIED") apply_file_addition_modification "$patch_dir" "$path" "$change_type" result=$? ;; "DELETED") apply_file_deletion "$path" result=$? ;; *) warn "未知变更类型: $change_type" result=1 ;; esac if [[ $result -eq 0 ]]; then ((applied_count++)) else ((failed_count++)) fi done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file") info "文件变更应用完成: $applied_count 成功, $failed_count 失败" if [[ $failed_count -gt 0 ]]; then return 1 fi return 0 } apply_file_addition_modification() { local patch_dir="$1" local file_path="$2" local change_type="$3" local source_file="$patch_dir/files/$file_path" local target_file="/$file_path" # 绝对路径 local target_dir=$(dirname "$target_file") # 检查源文件是否存在 if [[ ! -f "$source_file" ]]; then error "源文件不存在: $source_file" return 1 fi # 创建目标目录 if [[ ! -d "$target_dir" ]]; then mkdir -p "$target_dir" debug "创建目录: $target_dir" fi # 备份原文件(如果是修改操作) if [[ "$change_type" == "MODIFIED" ]] && [[ -f "$target_file" ]]; then local backup_file="$TEMP_DIR/backup/$file_path" mkdir -p "$(dirname "$backup_file")" cp -p "$target_file" "$backup_file" debug "备份原文件: $target_file -> $backup_file" fi # 复制文件 if cp -p "$source_file" "$target_file"; then # 设置权限(从清单中读取) set_file_permissions "$target_file" "$change_type" info "✅ 应用文件: $file_path ($change_type)" return 0 else error "❌ 文件应用失败: $file_path" return 1 fi } apply_file_deletion() { local file_path="$1" local target_file="/$file_path" if [[ ! -f "$target_file" ]]; then warn "文件不存在,无需删除: $target_file" return 0 fi # 备份文件 local backup_file="$TEMP_DIR/backup/$file_path" mkdir -p "$(dirname "$backup_file")" cp -p "$target_file" "$backup_file" # 删除文件 if rm -f "$target_file"; then info "✅ 删除文件: $file_path" # 尝试删除空目录 local parent_dir=$(dirname "$target_file") while [[ "$parent_dir" != "/" ]] && rmdir "$parent_dir" 2>/dev/null; do debug "删除空目录: $parent_dir" parent_dir=$(dirname "$parent_dir") done return 0 else error "❌ 文件删除失败: $file_path" return 1 fi } set_file_permissions() { local file_path="$1" local change_type="$2" # 根据文件类型设置权限 case "$file_path" in *.sh|*.bash) chmod 755 "$file_path" ;; *.php|*.py|*.pl) chmod 644 "$file_path" ;; /etc/*|/var/www/*) chmod 644 "$file_path" ;; *) chmod 644 "$file_path" ;; esac debug "设置权限: $file_path -> $(stat -c %a "$file_path")" } # 验证应用结果 verify_application() { local patch_dir="$1" info "开始验证应用结果..." local manifest_file="$patch_dir/MANIFEST.MF" local verification_passed=true while IFS='|' read -r change_type path extra_info; do case "$change_type" in "ADDED"|"MODIFIED") if ! verify_file_application "$path" "$change_type" "$patch_dir"; then verification_passed=false fi ;; "DELETED") if ! verify_file_deletion "$path"; then verification_passed=false fi ;; esac done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file") if $verification_passed; then info "✅ 应用验证通过" return 0 else error "❌ 应用验证失败" return 1 fi } verify_file_application() { local file_path="$1" local change_type="$2" local patch_dir="$3" local target_file="/$file_path" local source_file="$patch_dir/files/$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" 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_path="$1" local backup_path="$2" local rollback_info_file="$BACKUP_DIR/rollback_info.json" local patch_name=$(basename "$patch_path") local timestamp=$(date +%Y%m%d_%H%M%S) local rollback_info=$(cat << EOF { "patch_name": "$patch_name", "apply_time": "$(date -Iseconds)", "backup_path": "$backup_path", "rollback_path": "$BACKUP_DIR/rollback_${timestamp}.tar.gz", "status": "applied" } EOF ) echo "$rollback_info" > "$rollback_info_file" info "回滚点创建完成: $rollback_info_file" } # 通知函数 send_application_notification() { local status="$1" local patch_path="$2" local message="$3" if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then return 0 fi case "$status" in "start") local subject="补丁应用开始" local color="#3498db" ;; "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_notification "$subject" "$message" "$color" fi } send_slack_notification() { local subject="$1" local message="$2" local color="$3" local payload=$(cat << EOF { "attachments": [ { "color": "$color", "title": "$subject", "text": "$message", "fields": [ { "title": "补丁文件", "value": "$(basename "$PATCH_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 } # 主应用函数 apply_patch() { local patch_path="$1" local dry_run="${2:-false}" info "开始应用补丁包: $patch_path" send_application_notification "start" "$patch_path" "开始应用补丁" # 验证补丁包 if ! verify_package "$patch_path"; then error "补丁包验证失败" send_application_notification "failure" "$patch_path" "补丁包验证失败" return 1 fi # 解压补丁包 local extract_dir="$TEMP_DIR/extract" if ! extract_package "$patch_path" "$extract_dir"; then send_application_notification "failure" "$patch_path" "补丁包解压失败" return 1 fi # 如果是干跑模式,只验证不应用 if [[ "$dry_run" == "true" ]]; then info "干跑模式: 只验证不应用" verify_application "$extract_dir" return 0 fi # 创建备份 local backup_path=$(create_backup) # 应用变更 if ! apply_file_changes "$extract_dir"; then error "文件变更应用失败" send_application_notification "failure" "$patch_path" "文件应用失败" return 1 fi # 验证应用结果 if ! verify_application "$extract_dir"; then error "应用验证失败" send_application_notification "failure" "$patch_path" "应用验证失败" return 1 fi # 创建回滚点 create_rollback_point "$patch_path" "$backup_path" info "✅ 补丁应用完成" send_application_notification "success" "$patch_path" "补丁应用成功" return 0 } # 主函数 main() { local patch_path="${1:-}" local dry_run="${2:-false}" if [[ -z "$patch_path" ]]; then echo "用法: $0 <补丁包路径> [dry-run]" echo "示例:" echo " $0 /opt/patches/patch-security-hotfix-1.2.3.tar.gz" echo " $0 /opt/patches/patch-security-hotfix-1.2.3.tar.gz dry-run" exit 1 fi # 加载配置 load_config # 设置环境 setup_environment # 应用补丁 if apply_patch "$patch_path" "$dry_run"; then info "🎉 补丁应用成功完成" exit 0 else error "💥 补丁应用失败" exit 1 fi } # 异常处理 trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM main "$@"