From 87c05d684d3910534c9c8318a89d68bf78fc9f9c Mon Sep 17 00:00:00 2001 From: ZF sun <34314687@qq.com> Date: Tue, 18 Nov 2025 10:38:19 +0800 Subject: [PATCH] init --- docker-compose.yml | 18 + install_patch_system.sh | 298 +++++++++++ patch_applier.sh | 1038 ++++++++++++++++++++++++++++++++++++ patch_config.sh | 196 +++++++ patch_config.sh.example | 202 +++++++ patch_fnc.sh | 126 +++++ patch_generator.sh | 1099 +++++++++++++++++++++++++++++++++++++++ patch_rollback.sh | 1085 ++++++++++++++++++++++++++++++++++++++ patch_verifier.sh | 541 +++++++++++++++++++ patch_workflow.sh | 159 ++++++ 10 files changed, 4762 insertions(+) create mode 100644 docker-compose.yml create mode 100644 install_patch_system.sh create mode 100644 patch_applier.sh create mode 100644 patch_config.sh create mode 100644 patch_config.sh.example create mode 100644 patch_fnc.sh create mode 100644 patch_generator.sh create mode 100644 patch_rollback.sh create mode 100644 patch_verifier.sh create mode 100644 patch_workflow.sh diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d6e4c9a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +# 基于alpine的docker镜像,用于运行patch工具 +# 镜像名称:patch-tools-alpine +# 基础镜像:alpine:3.18 +# 安装依赖:bash, tar, gzip, zip, diffutils, patch +# 复制脚本:patch_config.sh, patch.sh +# 入口点:/bin/bash /patch.sh + +services: + patch-tools: + image: ubuntu/squid:latest + container_name: patch-tools-alpine + volumes: + # 脚本目录,用于安装,挂载当前目录到容器的/working_dir/scripts目录下 + - ./:/working_dir/scripts + + # 新旧项目 + - ../shop-projects/online-backend-src/ftp-src/:/working_dir/old-src + - ../shop-projects/backend/src:/working_dir/new-src diff --git a/install_patch_system.sh b/install_patch_system.sh new file mode 100644 index 0000000..b206bda --- /dev/null +++ b/install_patch_system.sh @@ -0,0 +1,298 @@ +#!/bin/bash +# install_patch_system.sh - 补丁管理系统安装脚本 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +INSTALL_DIR="/opt/patch-management" +CONFIG_FILE="${SCRIPT_DIR}/patch_config.sh" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; } +info() { log "INFO: $1"; } +error() { log "ERROR: $1"; exit 1; } +warn() { log "WARNING: $1"; } + +# 检测是否在Docker容器中运行 +is_docker_environment() { + # 检查多种Docker环境标识 + if [[ -f /.dockerenv ]]; then + return 0 + fi + + if grep -q docker /proc/1/cgroup 2>/dev/null; then + return 0 + fi + + if grep -q lxc /proc/1/cgroup 2>/dev/null; then + return 0 + fi + + if [[ -n "${container:-}" ]]; then + return 0 + fi + + if [[ -n "${DOCKER_CONTAINER:-}" ]]; then + return 0 + fi + + # 检查容器相关的环境变量组合 + local env_vars=( + "HOSTNAME" + "HOME" + "USER" + ) + + for var in "${env_vars[@]}"; do + if [[ -n "${!var:-}" ]] && [[ "${!var}" =~ ^[a-f0-9]{12,}$ ]]; then + # 检查环境变量值是否像容器ID + if [[ ${!var} =~ ^[a-f0-9]{12,64}$ ]]; then + return 0 + fi + fi + done + + return 1 +} + +# 获取适当的命令前缀(在Docker中使用空前缀,否则使用sudo) +get_cmd_prefix() { + if is_docker_environment; then + echo "" + else + echo "sudo" + fi +} + +# 配置加载 +load_config() { + if [[ ! -f "$CONFIG_FILE" ]]; then + error "配置文件不存在: $CONFIG_FILE" + exit 1 + fi + + source "$CONFIG_FILE" + info "配置文件加载完成" +} + +install_dependencies() { + info "安装系统依赖..." + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + + local will_install_dependencies=false + local dependencies=( + "tar" + "gzip" + "bzip2" + "jq" + "gpg" + "bc" + ) + + for dep in "${dependencies[@]}"; do + if command -v "$dep" >/dev/null 2>&1; then + info "系统依赖 $dep 已安装" + else + warn "系统依赖 $dep 未安装" + will_install_dependencies=true + fi + done + + if ! $will_install_dependencies; then + info "系统依赖已安装" + return 0 + fi + + # 关键依赖 + local keys_deps=( + "coreutils" + "findutils" + "util-linux" + ) + + if command -v apt-get >/dev/null 2>&1; then + # Debian/Ubuntu + $sudo_prefix apt-get update + $sudo_prefix apt-get install -y $(printf "%s " "${keys_deps[@]}") $(printf "%s " "${dependencies[@]}") + elif command -v yum >/dev/null 2>&1; then + # CentOS/RHEL + $sudo_prefix yum install -y $(printf "%s " "${keys_deps[@]}") $(printf "%s " "${dependencies[@]}") + else + warn "无法自动安装依赖,请手动安装: $(printf "%s " "${keys_deps[@]}") $(printf "%s " "${dependencies[@]}")" + fi + + # 安装GPG(用于签名验证) + if command -v apt-get >/dev/null 2>&1; then + $sudo_prefix apt-get install -y gnupg + elif command -v yum >/dev/null 2>&1; then + $sudo_prefix yum install -y gnupg + fi +} + +create_directories() { + info "创建目录结构..." + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + $sudo_prefix mkdir -p "$INSTALL_DIR" + $sudo_prefix mkdir -p "/var/backups/patch" + $sudo_prefix mkdir -p "/var/log/patch_system" + $sudo_prefix mkdir -p "/etc/patch/keys" + + # 设置权限 + $sudo_prefix chmod 755 "$INSTALL_DIR" + $sudo_prefix chmod 755 "/var/backups/patch" + $sudo_prefix chmod 755 "/var/log/patch_system" + $sudo_prefix chmod 700 "/etc/patch/keys" +} + +install_scripts() { + info "安装脚本文件..." + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + # 复制脚本文件, 存在则覆盖 + $sudo_prefix cp --force "$SCRIPT_DIR/patch_generator.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_fnc.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_applier.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_rollback.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_verifier.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_workflow.sh" "$INSTALL_DIR/" + $sudo_prefix cp --force "$SCRIPT_DIR/patch_config.sh" "$INSTALL_DIR/" + + info "脚本文件已安装在: $INSTALL_DIR/" + + # 设置执行权限 + $sudo_prefix chmod +x "$INSTALL_DIR"/*.sh + + # 创建符号链接 + $sudo_prefix ln -sf "$INSTALL_DIR/patch_workflow.sh" "/usr/local/bin/patch-mgmt" + info "符号链接已创建: /usr/local/bin/patch-mgmt" +} + +setup_cron() { + info "设置定时任务..." + + if is_docker_environment; then + info "Docker环境,跳过系统定时任务设置" + info "如需定时任务,请考虑使用宿主机的crontab或Docker运行参数" + return 0 + fi + + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + local cron_job="0 2 * * * $INSTALL_DIR/patch_verifier.sh /opt/patches batch > /var/log/patch_system/batch_verify.log 2>&1" + + if ! crontab -l 2>/dev/null | grep -q "patch_verifier.sh"; then + (crontab -l 2>/dev/null; echo "$cron_job") | crontab - + info "定时任务已添加" + else + info "定时任务已存在" + fi +} + +generate_gpg_key() { + local name="${1:-John Doe}" + local email="${2:-johndoe@example.com}" + local key_type="${3:-RSA}" + local key_length="${4:-4096}" + + cat > /tmp/gpg_batch << EOF +Key-Type: $key_type +Key-Length: $key_length +Subkey-Type: $key_type +Subkey-Length: $key_length +Name-Real: $name +Name-Email: $email +Expire-Date: 0 +%commit +EOF + + gpg --batch --generate-key /tmp/gpg_batch + rm -f /tmp/gpg_batch + + echo "✅ 密钥生成完成" + gpg --list-secret-keys --keyid-format LONG "$email" +} + +generate_keys() { + info "生成签名密钥..." + + local key_dir="/etc/patch/keys" + local sudo_prefix + sudo_prefix=$(get_cmd_prefix) + + if [[ ! -f "$key_dir/private.pem" ]]; then + $sudo_prefix mkdir -p "$key_dir" + + # 生成GPG密钥对 + generate_gpg_key "$PATCH_AUTHOR" "$PATCH_EMAIL" "RSA" "4096" + + # 生成RSA密钥对 + openssl genrsa -out "$key_dir/private.pem" 4096 + openssl rsa -in "$key_dir/private.pem" -pubout -out "$key_dir/public.pem" + + $sudo_prefix chmod 600 "$key_dir/private.pem" + $sudo_prefix chmod 644 "$key_dir/public.pem" + + info "密钥对已生成: $key_dir/" + else + info "密钥对已存在" + fi +} + +main() { + info "开始安装企业级补丁管理系统" + echo "========================================" + echo "📋 安装配置文件: $INSTALL_DIR/patch_config.sh" + + # 加载配置 + load_config + + # 检查运行环境 + if is_docker_environment; then + info "检测到Docker容器环境,将以root权限执行(不适用sudo)" + SUDO_CMD="" + else + info "检测到主机环境,将使用sudo执行管理员操作" + SUDO_CMD="sudo" + + # 在非Docker环境中,建议非root用户运行 + if [[ $EUID -eq 0 ]]; then + warn "检测到以root用户运行,建议在非Docker环境中使用具备sudo权限的普通用户" + fi + fi + + # 安装依赖 + install_dependencies + + # 创建目录 + create_directories + + # 安装脚本 + install_scripts + + # 设置定时任务 + setup_cron + + # 生成密钥 + generate_keys + + info "🎉 补丁管理系统安装完成!" + echo "" + echo "📁 安装目录: $INSTALL_DIR" + echo "🔧 使用命令: patch-mgmt" + echo "📋 配置文件: $INSTALL_DIR/patch_config.sh" + echo "" + echo "💡 下一步操作:" + echo " 1. 编辑配置文件: $INSTALL_DIR/patch_config.sh" + echo " 2. 测试系统: patch-mgmt --help" + echo " 3. 配置通知: 修改SLACK_WEBHOOK等设置" +} + +main "$@" \ No newline at end of file diff --git a/patch_applier.sh b/patch_applier.sh new file mode 100644 index 0000000..7b38339 --- /dev/null +++ b/patch_applier.sh @@ -0,0 +1,1038 @@ +#!/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 "$@" diff --git a/patch_config.sh b/patch_config.sh new file mode 100644 index 0000000..549f39a --- /dev/null +++ b/patch_config.sh @@ -0,0 +1,196 @@ +#!/bin/bash +# patch_config.sh - 企业级补丁配置 + +# ============================================================================== +# 基础配置 - 定义补丁的基本信息 +# ============================================================================== + +# 基础配置 +PATCH_NAME="upgrade-hotfix" +PATCH_VERSION="1.0.0" +PATCH_DESCRIPTION="紧急升级修复" +PATCH_AUTHOR="devops" +PATCH_EMAIL="devops@aigc-quickapp.com" + +# ============================================================================== +# 文件筛选配置 - 定义哪些文件需要被包含或排除 +# ============================================================================== + +## 包含的文件模式 +INCLUDE_PATTERNS=( + ".+/.*" # 匹配所有子目录下的所有文件 + ".env" + ".htaccess" + '.user.ini' + '404.html' + "composer.json" + "index.html" + "index.php" + "install.lock" + "install.php" + "nginx.htaccess" + "think" + "LICENSE" + "README.md" + "robots.txt" +) + +## 排除的文件模式 +EXCLUDE_PATTERNS=( + "*.log" + "*.tmp" + "*.bak" + "*.swp" + "*.swx" + "^.github/" + # 排除任何位置的node_modules文件夹及其所有子目录和文件 + ".*/node_modules($|/.*)" + # 排除任何位置的__pycache__文件夹及其所有子目录和文件 + ".*/__pycache__($|/.*)" + # 排除根目录下的一级文件夹及其所有子目录和文件 + "^.git/" + "^cache($|/.*)" + "^temp($|/.*)" + "^tmp($|/.*)" + "^logs($|/.*)" + "^runtime($|/.*)" + "^uploads($|/.*)" + "^attachment($|/.*)" + "^h5($|/.*)" + "^hwapp($|/.*)" + ".DS_Store" + "Thumbs.db" +) + +# ============================================================================== +# 文件大小限制 - 定义补丁处理的文件大小范围 +# ============================================================================== + +# 文件大小限制 +MAX_FILE_SIZE="20MB" # 最大文件大小 +MIN_FILE_SIZE="0KB" # 最小文件大小 + +# ============================================================================== +# 时间过滤 - 定义补丁处理的文件时间范围 +# ============================================================================== + +# 时间过滤 +MODIFIED_AFTER="2000-01-01T00:00:00Z" # 仅包含修改时间在该时间之后的文件 +CREATED_AFTER="2000-01-01T00:00:00Z" # 仅包含创建时间在该时间之后的文件 + +# ============================================================================== +# 比较方法配置 - 定义补丁处理的比较方式 +# ============================================================================== + +# 比较方法配置 +# 比较内容和时间 +COMPARISON_METHOD="content" # 比较方法: content, time, both +HASH_ALGORITHM="sha256" # 哈希算法,用于文件内容比较 +TIME_PRECISION="second" # 时间精度,用于文件时间比较 +COMPARE_PERMISSIONS=false # 是否比较文件权限 +COMPARE_OWNERSHIP=false # 是否比较文件所有者 +IGNORE_LINE_ENDINGS=true # 是否忽略换行符格式不同时(比如Windows的 \r\n vs Linux的 \n) + +# ============================================================================== +# 增量配置 - 定义是否启用增量补丁 +# ============================================================================== + +# 增量配置 +INCREMENTAL_ENABLED=true # 是否启用增量补丁 +BASE_VERSION="1.0.0" # 基础版本,用于增量比较 +DETECT_RENAMES=true # 是否检测文件重命名 +BINARY_DIFF=true # 是否启用二进制差异比较 +CHUNK_SIZE="8KB" # 二进制差异比较的块大小 + + + +# ============================================================================== +# 压缩配置 - 定义补丁压缩方式和级别 +# ============================================================================== + +# 压缩配置 +COMPRESSION_ENABLED=true # 是否启用压缩 +COMPRESSION_ALGORITHM="gzip" # gzip, bzip2, xz +COMPRESSION_LEVEL=6 # 压缩级别,1-9 +PER_FILE_OPTIMIZATION=true # 是否对每个文件单独压缩 + +# ============================================================================== +# 安全配置 - 定义补丁签名和加密方式 +# ============================================================================== + +# 安全配置 +SIGNING_ENABLED=false # 是否启用签名 +SIGNING_ALGORITHM="rsa" # 签名算法,rsa, ecdsa +PRIVATE_KEY="/etc/patch/keys/private.pem" # 私钥文件路径 +PUBLIC_KEY="/etc/patch/keys/public.pem" # 公钥文件路径 + +ENCRYPTION_ENABLED=false # 是否启用加密 +ENCRYPTION_ALGORITHM="aes-256-gcm" # 加密算法,aes-256-gcm, aes-256-cbc +ENCRYPTION_KEY="/etc/patch/keys/encryption.key" # 加密密钥文件路径 +ENCRYPTION_IV="/etc/patch/keys/encryption.iv" # 加密初始化向量文件路径 + +# ============================================================================== +# 备份配置 - 定义是否启用备份和备份策略 +# ============================================================================== + +# 备份配置 +BACKUP_ENABLED=true # 是否启用备份 +BACKUP_STRATEGY="full" # 备份策略,full, incremental, differential +BACKUP_RETENTION_DAYS=30 # 备份保留天数 +BACKUP_DIR="/var/backups/patch" # 备份目录 + +# ============================================================================== +# 回滚配置 - 定义是否启用自动回滚和回滚策略 +# ============================================================================== + +# 回滚配置 +ROLLBACK_ENABLED=true # 是否启用自动回滚 +ROLLBACK_AUTO_GENERATE=true # 是否自动生成回滚脚本 +ROLLBACK_STRATEGY="snapshot" # 回滚策略,snapshot, restore +ROLLBACK_INFO_DIR="/var/lib/patch_system/rollback_info" + +# ============================================================================== +# 通知配置 - 定义是否启用通知和通知渠道 +# ============================================================================== + +# 通知配置 +NOTIFICATIONS_ENABLED=false # 是否启用通知 +SLACK_WEBHOOK="https://hooks.slack.com/services/..." # Slack Webhook URL +EMAIL_RECIPIENTS="devops@company.com,team@company.com" # 邮箱接收人列表 + +# ============================================================================== +# 性能配置 - 定义并行处理和资源限制 +# ============================================================================== + +# 性能配置 +PARALLEL_PROCESSING=false # 是否启用并行处理 +MAX_WORKERS=4 # 最大工作线程数 +MEMORY_LIMIT="2GB" # 内存限制 +IO_BUFFER_SIZE="64KB" # IO缓冲区大小 + +# ============================================================================== +# 输出配置 - 定义补丁输出格式和目录 +# ============================================================================== + +# 输出配置 +OUTPUT_FORMAT="tar.gz" # 输出格式,tar.gz, zip +OUTPUT_DIRECTORY="/opt/patches" # 输出目录 +NAMING_PATTERN="patch-{name}-{version}-{timestamp}-{git_commit}.{format}" # 文件名命名模式,包含占位符,举例:patch-app-1.0.0-20250101T000000Z-abc123.tar.gz + +# ============================================================================== +# 日志配置 - 定义日志级别、文件路径和大小 +# ============================================================================== + +# 日志配置 +LOG_LEVEL="INFO" # 日志级别,DEBUG, INFO, WARN, ERROR, TRACE; DEBUG 会开启终端调试输出,TRACE 只会开启详细日志输出 +LOG_FILE="/var/log/patch_system/patch.log" # 日志文件路径 +LOG_MAX_SIZE="10MB" # 日志文件最大大小 +LOG_BACKUP_COUNT=10 # 日志文件备份数量 + +# ============================================================================== +# 长路径支持 - 定义是否启用长路径支持 +# ============================================================================== + +# 长路径支持 +LONG_PATH_SUPPORT=true # 是否启用长路径支持 +TAR_OPTIONS="--force-local" # tar命令选项,--force-local 强制使用本地文件系统 diff --git a/patch_config.sh.example b/patch_config.sh.example new file mode 100644 index 0000000..6181fd0 --- /dev/null +++ b/patch_config.sh.example @@ -0,0 +1,202 @@ +#!/bin/bash +# patch_config.sh - 企业级补丁配置 + +# ============================================================================== +# 基础配置 - 定义补丁的基本信息 +# ============================================================================== + +# 基础配置 +PATCH_NAME="security-hotfix-2025" +PATCH_VERSION="1.0.0" +PATCH_DESCRIPTION="紧急安全漏洞修复" +PATCH_AUTHOR="企业DevOps团队" +PATCH_EMAIL="devops@aigc-quickapp.com" + +# ============================================================================== +# 文件筛选配置 - 定义哪些文件需要被包含或排除 +# ============================================================================== + +## 包含的文件模式 +INCLUDE_PATTERNS=( + ".+/.*" # 匹配所有子目录下的文件 + "^.well-known/" + "^addon/" + "^addons/" + "^app/" + "^config/" + "^extend/" + "^public/" + "^templates/" + "^vendor/" + "^web/" + "^webapi/" + "^.404.html" + "^index.php" + "^install.php" + "^install.lock" + "^.env" + "^.env.test" + "^.env.production" + "^.env.staging" + "^.env.development" + "^.env.local" + "^.htaccess" + "^.user.ini" + "^composer.json" + "^composer.lock" +) + +## 排除的文件模式 +EXCLUDE_PATTERNS=( + "*.log" + "*.tmp" + "*.bak" + "*.swp" + "*.swx" + "^.github/" + "/node_modules/*" + "/__pycache__/*" + # 排除以下一级文件夹 + "^.git/" + "^cache/" + "^temp/" + "^tmp/" + "^logs/" + "^runtime/" + "^uploads/" + "^attachment/" + "^h5/" + "^hwapp/" + ".DS_Store" + "Thumbs.db" +) + +# ============================================================================== +# 文件大小限制 - 定义补丁处理的文件大小范围 +# ============================================================================== + +# 文件大小限制 +MAX_FILE_SIZE="20MB" # 最大文件大小 +MIN_FILE_SIZE="0KB" # 最小文件大小 + +# ============================================================================== +# 时间过滤 - 定义补丁处理的文件时间范围 +# ============================================================================== + +# 时间过滤 +MODIFIED_AFTER="2000-01-01T00:00:00Z" # 仅包含修改时间在该时间之后的文件 +CREATED_AFTER="2000-01-01T00:00:00Z" # 仅包含创建时间在该时间之后的文件 + +# ============================================================================== +# 比较方法配置 - 定义补丁处理的比较方式 +# ============================================================================== + +# 比较方法配置 +# 比较内容和时间 +COMPARISON_METHOD="content" # 比较方法: content, time, both +HASH_ALGORITHM="sha256" # 哈希算法,用于文件内容比较 +TIME_PRECISION="second" # 时间精度,用于文件时间比较 +COMPARE_PERMISSIONS=false # 是否比较文件权限 +COMPARE_OWNERSHIP=false # 是否比较文件所有者 + +# ============================================================================== +# 增量配置 - 定义是否启用增量补丁 +# ============================================================================== + +# 增量配置 +INCREMENTAL_ENABLED=true # 是否启用增量补丁 +BASE_VERSION="1.0.0" # 基础版本,用于增量比较 +DETECT_RENAMES=true # 是否检测文件重命名 +BINARY_DIFF=true # 是否启用二进制差异比较 +CHUNK_SIZE="8KB" # 二进制差异比较的块大小 + + + +# ============================================================================== +# 压缩配置 - 定义补丁压缩方式和级别 +# ============================================================================== + +# 压缩配置 +COMPRESSION_ENABLED=true # 是否启用压缩 +COMPRESSION_ALGORITHM="gzip" # gzip, bzip2, xz +COMPRESSION_LEVEL=6 # 压缩级别,1-9 +PER_FILE_OPTIMIZATION=true # 是否对每个文件单独压缩 + +# ============================================================================== +# 安全配置 - 定义补丁签名和加密方式 +# ============================================================================== + +# 安全配置 +SIGNING_ENABLED=true # 是否启用签名 +SIGNING_ALGORITHM="rsa" # 签名算法,rsa, ecdsa +PRIVATE_KEY="/etc/patch/keys/private.pem" # 私钥文件路径 +PUBLIC_KEY="/etc/patch/keys/public.pem" # 公钥文件路径 + +ENCRYPTION_ENABLED=false # 是否启用加密 +ENCRYPTION_ALGORITHM="aes-256-gcm" # 加密算法,aes-256-gcm, aes-256-cbc +ENCRYPTION_KEY="/etc/patch/keys/encryption.key" # 加密密钥文件路径 +ENCRYPTION_IV="/etc/patch/keys/encryption.iv" # 加密初始化向量文件路径 + +# ============================================================================== +# 备份配置 - 定义是否启用备份和备份策略 +# ============================================================================== + +# 备份配置 +BACKUP_ENABLED=true # 是否启用备份 +BACKUP_STRATEGY="full" # 备份策略,full, incremental, differential +BACKUP_RETENTION_DAYS=30 # 备份保留天数 + +# ============================================================================== +# 回滚配置 - 定义是否启用自动回滚和回滚策略 +# ============================================================================== + +# 回滚配置 +ROLLBACK_ENABLED=true # 是否启用自动回滚 +ROLLBACK_AUTO_GENERATE=true # 是否自动生成回滚脚本 +ROLLBACK_STRATEGY="snapshot" # 回滚策略,snapshot, restore + +# ============================================================================== +# 通知配置 - 定义是否启用通知和通知渠道 +# ============================================================================== + +# 通知配置 +NOTIFICATIONS_ENABLED=false # 是否启用通知 +SLACK_WEBHOOK="https://hooks.slack.com/services/..." # Slack Webhook URL +EMAIL_RECIPIENTS="devops@company.com,team@company.com" # 邮箱接收人列表 + +# ============================================================================== +# 性能配置 - 定义并行处理和资源限制 +# ============================================================================== + +# 性能配置 +PARALLEL_PROCESSING=false # 是否启用并行处理 +MAX_WORKERS=4 # 最大工作线程数 +MEMORY_LIMIT="2GB" # 内存限制 +IO_BUFFER_SIZE="64KB" # IO缓冲区大小 + +# ============================================================================== +# 输出配置 - 定义补丁输出格式和目录 +# ============================================================================== + +# 输出配置 +OUTPUT_FORMAT="tar.gz" # 输出格式,tar.gz, zip +OUTPUT_DIRECTORY="/opt/patches" # 输出目录 +NAMING_PATTERN="patch-{name}-{version}-{timestamp}-{git_commit}.{format}" # 文件名命名模式,包含占位符,举例:patch-app-1.0.0-20250101T000000Z-abc123.tar.gz + +# ============================================================================== +# 日志配置 - 定义日志级别、文件路径和大小 +# ============================================================================== + +# 日志配置 +LOG_LEVEL="INFO" # 日志级别,DEBUG, INFO, WARN, ERROR +LOG_FILE="/var/log/patch_system/patch.log" # 日志文件路径 +LOG_MAX_SIZE="100MB" # 日志文件最大大小 +LOG_BACKUP_COUNT=10 # 日志文件备份数量 + +# ============================================================================== +# 长路径支持 - 定义是否启用长路径支持 +# ============================================================================== + +# 长路径支持 +LONG_PATH_SUPPORT=true # 是否启用长路径支持 +TAR_OPTIONS="--force-local" # tar命令选项,--force-local 强制使用本地文件系统 diff --git a/patch_fnc.sh b/patch_fnc.sh new file mode 100644 index 0000000..a44e2a6 --- /dev/null +++ b/patch_fnc.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# patch_fnc.sh - 企业级增量补丁包生成脚本公共函数 +# 用于并行查找,提供给xargs使用 + + +# 文件工具函数 +parse_size() { + local size_str="$1" + local unit=$(echo "$size_str" | sed 's/[0-9.]//g' | tr 'a-z' 'A-Z') + local value=$(echo "$size_str" | sed 's/[^0-9.]//g') + + case "$unit" in + "KB") echo "$(echo "$value * 1024" | bc)" ;; + "MB") echo "$(echo "$value * 1024 * 1024" | bc)" ;; + "GB") echo "$(echo "$value * 1024 * 1024 * 1024" | bc)" ;; + *) echo "$value" ;; + esac +} + +get_file_hash() { + local file_path="$1" + local algorithm="${2:-sha256}" + + case "$algorithm" in + "md5") md5sum "$file_path" | cut -d' ' -f1 ;; + "sha1") sha1sum "$file_path" | cut -d' ' -f1 ;; + "sha256") sha256sum "$file_path" | cut -d' ' -f1 ;; + *) sha256sum "$file_path" | cut -d' ' -f1 ;; + esac +} + +get_file_info() { + local file_path="$1" + + if [[ ! -f "$file_path" ]]; then + error "文件不存在: $file_path" + return 1 + fi + + local stat_output + if stat --version 2>&1 | grep -q GNU; then + # GNU stat + stat_output=$(stat -c "%s|%Y|%Z|%a|%u|%g" "$file_path") + else + # BSD stat + stat_output=$(stat -f "%z|%m|%c|%p|%u|%g" "$file_path") + fi + + echo "$stat_output" +} + +# 文件筛选 +should_include_file() { + local file_path="$1" + local file_info="$2" + + IFS='|' read -r size mtime ctime permissions uid gid <<< "$file_info" + + # 文件大小过滤 + local max_size=$(parse_size "$MAX_FILE_SIZE") + local min_size=$(parse_size "$MIN_FILE_SIZE") + + if [[ $size -gt $max_size ]] || [[ $size -lt $min_size ]]; then + warn "文件大小超出范围: $file_path ($size bytes, 限制:$min_size-$max_size bytes)" + return 1 + fi + + # 包含模式匹配 + local include_match=false + for pattern in "${INCLUDE_PATTERNS[@]}"; do + if [[ "$file_path" == $pattern ]] || [[ "$file_path" =~ $pattern ]]; then + include_match=true + debug "文件匹配包含模式<$pattern>: $file_path" + break + fi + done + + if [[ ${#INCLUDE_PATTERNS[@]} -gt 0 ]] && ! $include_match; then + warn "文件不匹配任何包含模式: $file_path" + return 1 + fi + + # 排除模式匹配 + for pattern in "${EXCLUDE_PATTERNS[@]}"; do + if [[ "$file_path" == $pattern ]] || [[ "$file_path" =~ $pattern ]]; then + warn "文件匹配排除模式<$pattern>: $file_path" + return 1 + fi + done + + # 时间过滤 + if [[ -n "$MODIFIED_AFTER" ]]; then + local filter_time=$(date -d "$MODIFIED_AFTER" +%s 2>/dev/null || date +%s) + if [[ $mtime -lt $filter_time ]]; then + local mtime_str=$(date -d @$mtime +"%Y-%m-%d %H:%M:%S") + warn "文件修改时间过早: $file_path ($mtime_str)" + return 1 + fi + fi + + # 权限过滤 + if [[ "$PERMISSIONS_FILTER" == "true" ]]; then + if [[ $permissions -ne 644 ]] && [[ $permissions -ne 755 ]]; then + warn "文件权限不符合要求: $file_path ($permissions)" + return 1 + fi + fi + + # 用户过滤 + if [[ "$UID_FILTER" == "true" ]]; then + if [[ $uid -ne 0 ]] && [[ $uid -ne 1000 ]]; then + warn "文件所有者不符合要求: $file_path ($uid)" + return 1 + fi + fi + + # 组过滤 + if [[ "$GID_FILTER" == "true" ]]; then + if [[ $gid -ne 0 ]] && [[ $gid -ne 1000 ]]; then + warn "文件组不符合要求: $file_path ($gid)" + return 1 + fi + fi + + return 0 +} \ No newline at end of file diff --git a/patch_generator.sh b/patch_generator.sh new file mode 100644 index 0000000..6f3ccf7 --- /dev/null +++ b/patch_generator.sh @@ -0,0 +1,1099 @@ +#!/bin/bash +# patch_generator.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/generate_$(date +%Y%m%d_%H%M%S).log" +TEMP_DIR=$(mktemp -d "/tmp/patch_gen_XXXXXX") + +# 颜色定义 +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" ;; + "TRACE") color="$PURPLE" ;; + *) color="$NC" ;; + esac + + + # 日志级别过滤 + case "$LOG_LEVEL" in + "DEBUG") + # DEBUG级别:输出所有日志 + ;; + "TRACE") + # TRACE级别:输出所有日志 + ;; + "INFO") + # INFO级别:只输出INFO、WARN和ERROR日志 + if [[ "$level" == "DEBUG" ]] || [[ "$level" == "TRACE" ]]; then + return 0 + fi + ;; + "WARN") + # WARN级别:只输出WARN和ERROR日志 + if [[ "$level" == "DEBUG" ]] || [[ "$level" == "TRACE" ]] || [[ "$level" == "INFO" ]]; then + return 0 + fi + ;; + "ERROR") + # ERROR级别:只输出ERROR日志 + if [[ "$level" != "ERROR" ]]; then + return 0 + fi + ;; + *) + # 其他级别:不输出任何日志 + return 0 + ;; + esac + + echo -e "${color}[$timestamp] [$level]${NC} $message" | tee -a "$LOG_FILE" +} + +debug() { log "DEBUG" "$1"; } +trace() { log "TRACE" "$1"; } +info() { log "INFO" "$1"; } +warn() { log "WARN" "$1"; } +error() { log "ERROR" "$1"; } + + +# 配置加载 +load_config() { + if [[ ! -f "$CONFIG_FILE" ]]; then + error "配置文件不存在: $CONFIG_FILE" + exit 1 + fi + + # 设置默认值 + : ${PATCH_NAME:="unnamed-patch"} + : ${PATCH_VERSION:="1.0.0"} + : ${OUTPUT_DIRECTORY:="/opt/patches"} + : ${LOG_LEVEL:="INFO"} + : ${MAX_WORKERS:=4} + : ${MAX_FILE_SIZE:="100MB"} + : ${COMPRESSION_LEVEL:=6} + : ${PERMISSIONS_FILTER:="false"} + : ${UID_FILTER:="false"} + : ${GID_FILTER:="false"} + : ${CHECKSUM_ENABLED:="true"} + : ${SIGNING_ENABLED:="false"} + : ${COMPRESSION_ALGORITHM:="gzip"} + : ${HASH_ALGORITHM:="sha256"} + : ${COMPARISON_METHOD:="both"} + : ${TIME_PRECISION:="second"} + : ${LONG_PATH_SUPPORT:="true"} + : ${IGNORE_LINE_ENDINGS:="false"} + + source "$CONFIG_FILE" + info "配置文件加载完成" +} + +# 环境设置 +setup_environment() { + # 创建必要目录 + mkdir -p "$(dirname "$LOG_FILE")" + mkdir -p "$OUTPUT_DIRECTORY" + mkdir -p "$TEMP_DIR" + + # 注意:不在环境设置中启用调试模式,以免影响日志输出 + # 如果需要详细调试,可以在脚本开头手动设置 set -x + + info "环境设置完成" + info "日志文件: $LOG_FILE" + info "输出目录: $OUTPUT_DIRECTORY" + info "临时目录: $TEMP_DIR" +} + +# 清理函数 +cleanup() { + if [[ -d "$TEMP_DIR" ]]; then + rm -rf "$TEMP_DIR" + info "临时目录已清理: $TEMP_DIR" + fi +} + +trap cleanup EXIT + +# 依赖检查 +check_dependencies() { + local deps=("tar" "gzip" "jq" "find" "stat" "sha256sum" "date" "mkdir" "cp" "bc") + 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 + + # 检查可选依赖 + if [[ "$SIGNING_ENABLED" == "true" ]] && ! command -v gpg >/dev/null 2>&1; then + warn "GPG未安装,签名功能将禁用" + SIGNING_ENABLED="false" + fi + + info "依赖检查通过" + return 0 +} + +# 文件工具函数 +parse_size() { + local size_str="$1" + local unit=$(echo "$size_str" | sed 's/[0-9.]//g' | tr 'a-z' 'A-Z') + local value=$(echo "$size_str" | sed 's/[^0-9.]//g') + + case "$unit" in + "KB") echo "$(echo "$value * 1024" | bc)" ;; + "MB") echo "$(echo "$value * 1024 * 1024" | bc)" ;; + "GB") echo "$(echo "$value * 1024 * 1024 * 1024" | bc)" ;; + *) echo "$value" ;; + esac +} + + +get_file_hash() { + local file_path="$1" + local algorithm="${2:-sha256}" + + # 根据 $IGNORE_LINE_ENDINGS 是否为true,决定是否忽略行尾的换行符 + local cmd_tr="" + if [[ "$IGNORE_LINE_ENDINGS" == "true" ]]; then + cmd_tr="tr -d '\r\n'" + fi + + case "$algorithm" in + "md5") + if [[ -n "$cmd_tr" ]]; then + cat "$file_path" | $cmd_tr | md5sum | cut -d' ' -f1 + else + cat "$file_path" | md5sum | cut -d' ' -f1 + fi;; + "sha1") + if [[ -n "$cmd_tr" ]]; then + cat "$file_path" | $cmd_tr | sha1sum | cut -d' ' -f1 + else + cat "$file_path" | sha1sum | cut -d' ' -f1 + fi;; + "sha256") + if [[ -n "$cmd_tr" ]]; then + cat "$file_path" | $cmd_tr | sha256sum | cut -d' ' -f1 + else + cat "$file_path" | sha256sum | cut -d' ' -f1 + fi;; + *) + if [[ -n "$cmd_tr" ]]; then + cat "$file_path" | $cmd_tr | sha256sum | cut -d' ' -f1 + else + cat "$file_path" | sha256sum | cut -d' ' -f1 + fi;; + esac +} + +get_file_info() { + local file_path="$1" + + if [[ ! -f "$file_path" ]]; then + error "文件不存在: $file_path" + return 1 + fi + + local stat_output + if stat --version 2>&1 | grep -q GNU; then + # GNU stat + stat_output=$(stat -c "%s|%Y|%Z|%a|%u|%g" "$file_path") + else + # BSD stat + stat_output=$(stat -f "%z|%m|%c|%p|%u|%g" "$file_path") + fi + + echo "$stat_output" +} + +# 文件筛选 +should_include_file() { + local file_path="$1" + local file_info="$2" + + IFS='|' read -r size mtime ctime permissions uid gid <<< "$file_info" + + # 文件大小过滤 + local max_size=$(parse_size "$MAX_FILE_SIZE") + local min_size=$(parse_size "$MIN_FILE_SIZE") + + if [[ $size -gt $max_size ]] || [[ $size -lt $min_size ]]; then + warn "文件大小超出范围: $file_path ($size bytes, 限制:$min_size-$max_size bytes)" + return 1 + fi + + # 包含模式匹配 + local include_match=false + for pattern in "${INCLUDE_PATTERNS[@]}"; do + if [[ "$file_path" == $pattern ]] || [[ "$file_path" =~ $pattern ]]; then + include_match=true + trace "文件匹配包含模式<$pattern>: $file_path" + break + fi + done + + if [[ ${#INCLUDE_PATTERNS[@]} -gt 0 ]] && ! $include_match; then + warn "文件不匹配任何包含模式: $file_path" + return 1 + fi + + # 排除模式匹配 + for pattern in "${EXCLUDE_PATTERNS[@]}"; do + if [[ "$file_path" == $pattern ]] || [[ "$file_path" =~ $pattern ]]; then + warn "文件匹配排除模式<$pattern>: $file_path" + return 1 + fi + done + + # 时间过滤 + if [[ -n "$MODIFIED_AFTER" ]]; then + local filter_time=$(date -d "$MODIFIED_AFTER" +%s 2>/dev/null || date +%s) + if [[ $mtime -lt $filter_time ]]; then + local mtime_str=$(date -d @$mtime +"%Y-%m-%d %H:%M:%S") + warn "文件修改时间过早: $file_path ($mtime_str)" + return 1 + fi + fi + + # 权限过滤 + if [[ "$PERMISSIONS_FILTER" == "true" ]]; then + if [[ $permissions -ne 644 ]] && [[ $permissions -ne 755 ]]; then + warn "文件权限不符合要求: $file_path ($permissions)" + return 1 + fi + fi + + # 用户过滤 + if [[ "$UID_FILTER" == "true" ]]; then + if [[ $uid -ne 0 ]] && [[ $uid -ne 1000 ]]; then + warn "文件所有者不符合要求: $file_path ($uid)" + return 1 + fi + fi + + # 组过滤 + if [[ "$GID_FILTER" == "true" ]]; then + if [[ $gid -ne 0 ]] && [[ $gid -ne 1000 ]]; then + warn "文件组不符合要求: $file_path ($gid)" + return 1 + fi + fi + + return 0 +} + +# 目录扫描 +scan_directory() { + local base_dir="$1" + local output_file="$2" + + info "开始扫描目录: $base_dir" + + if [[ ! -d "$base_dir" ]]; then + error "目录不存在: $base_dir" + return 1 + fi + + # 清空输出文件 + > "$output_file" + + local file_count=0 + local scanned_count=0 + + # 使用find扫描文件 + while IFS= read -r -d '' file_path; do + ((scanned_count++)) + + # 获取文件信息 + local file_info + if file_info=$(get_file_info "$file_path" 2>/dev/null); then + # 检查是否应该包含该文件 + local relative_path="${file_path#$base_dir/}" # 这里一定要使用相对路径,方便在config中配置匹配模式 + if should_include_file "$relative_path" "$file_info"; then + local file_hash=$(get_file_hash "$file_path" "$HASH_ALGORITHM") + + echo "$relative_path|$file_info|$file_hash" >> "$output_file" + ((file_count++)) + + if [[ $((file_count % 500)) -eq 0 ]]; then + info "已扫描 $file_count 个文件..." + fi + fi + else + warn "无法获取文件信息: $file_path" + fi + done < <(find "$base_dir" -type f -print0 2>/dev/null) + + info "目录扫描完成: $base_dir (扫描: $scanned_count, 包含: $file_count)" + info "-------------------------------------------------------------- " + return 0 +} + +# 并行目录扫描 +scan_directory_parallel() { + local base_dir="$1" + local output_file="$2" + + info "开始并行扫描目录: $base_dir" + + if [[ ! -d "$base_dir" ]]; then + error "目录不存在: $base_dir" + return 1 + fi + + # 清空输出文件 + > "$output_file" + + # 创建临时文件存储处理结果 + local temp_result="$TEMP_DIR/parallel_result_$$" + + # 加载函数文件 + local func_file="$SCRIPT_DIR/patch_fnc.sh" + + # 使用xargs进行并行处理 + find "$base_dir" -type f -print0 2>/dev/null | \ + xargs -0 -P "$MAX_WORKERS" -I {} bash -c ' + source "'"$func_file"'" + file_path="{}" + file_info=$(get_file_info "$file_path" 2>/dev/null) + if [[ $? -eq 0 ]] && should_include_file "$file_path" "$file_info"; then + file_hash=$(get_file_hash "$file_path" "'"$HASH_ALGORITHM"'") + relative_path="${file_path#'"$base_dir"'/}" + echo "$relative_path|$file_info|$file_hash" + fi + ' 2>>"$LOG_FILE" | \ + tee -a "$temp_result" >> "$output_file" + + # 计算文件数量 + local file_count=$(wc -l < "$temp_result") + local scanned_count=$(find "$base_dir" -type f 2>/dev/null | wc -l) + + # 清理临时文件 + rm -f "$temp_result" + + info "并行扫描完成: $base_dir (扫描: $scanned_count, 包含: $file_count)" + return 0 +} + +# 差异比较 +compare_files() { + local old_file="$1" + local new_file="$2" + local output_file="$3" + + info "开始比较文件差异" + + # 输出差异比较规则 + info "比较规则: " + info "比较方法配置: $COMPARISON_METHOD" + info "哈希算法配置: $HASH_ALGORITHM" + info "时间精度:$TIME_PRECISION" + info "是否忽略换行符不同: $IGNORE_LINE_ENDINGS" + + + + declare -A old_files + declare -A new_files + + # 加载旧版本文件信息 + while IFS='|' read -r path file_info hash; do + old_files["$path"]="$file_info|$hash" + done < "$old_file" + + # 加载新版本文件信息 + while IFS='|' read -r path file_info hash; do + new_files["$path"]="$file_info|$hash" + done < "$new_file" + + # 清空输出文件 + > "$output_file" + + local changes=() + + # 检测新增文件 + for path in "${!new_files[@]}"; do + if [[ -z "${old_files[$path]:-}" ]]; then + changes+=("ADDED|$path|${new_files[$path]}") + warn "检测新增文件: $path" + fi + done + + # 检测删除文件 + for path in "${!old_files[@]}"; do + if [[ -z "${new_files[$path]:-}" ]]; then + changes+=("DELETED|$path|${old_files[$path]}") + warn "检测到删除文件: $path" + fi + done + + # 检测修改文件 + for path in "${!new_files[@]}"; do + if [[ -n "${old_files[$path]:-}" ]]; then + IFS='|' read -r old_info old_hash <<< "${old_files[$path]}" + IFS='|' read -r new_info new_hash <<< "${new_files[$path]}" + + local is_modified=false + local old_short_hash="${old_hash##*|}" # 使用短哈希值,不使用复合哈希值,因为复合哈希值包含权限,用户和组信息, + local new_short_hash="${new_hash##*|}" # 使用短哈希值,不使用复合哈希值,因为复合哈希值包含权限,用户和组信息, + + case "$COMPARISON_METHOD" in + "content") + [[ "$old_short_hash" != "$new_short_hash" ]] && is_modified=true + if $is_modified; then + trace "检测到修改文件: $path | 哈希值变化: <$old_short_hash> => <$new_short_hash>" + fi + ;; + "time") + IFS='|' read -r old_size old_mtime old_ctime old_perm old_uid old_gid <<< "$old_info" + IFS='|' read -r new_size new_mtime new_ctime new_perm new_uid new_gid <<< "$new_info" + + if [[ "$TIME_PRECISION" == "second" ]]; then + [[ $old_mtime -ne $new_mtime ]] && is_modified=true + if $is_modified; then + trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" + fi + else + [[ $(echo "$old_mtime != $new_mtime" | bc) -eq 1 ]] && is_modified=true + if $is_modified; then + trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" + fi + fi + ;; + "both") + IFS='|' read -r old_size old_mtime old_ctime old_perm old_uid old_gid <<< "$old_info" + IFS='|' read -r new_size new_mtime new_ctime new_perm new_uid new_gid <<< "$new_info" + + if [[ "$old_short_hash" != "$new_short_hash" ]]; then + is_modified=true + trace "检测到修改文件: $path | 哈希值变化: <$old_short_hash> => <$new_short_hash>" + elif [[ "$TIME_PRECISION" == "second" ]] && [[ $old_mtime -ne $new_mtime ]]; then + is_modified=true + trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" + elif [[ "$TIME_PRECISION" == "millisecond" ]] && [[ $(echo "$old_mtime != $new_mtime" | bc) -eq 1 ]]; then + is_modified=true + trace "检测到修改文件: $path | 时间变化: <$old_mtime> => <$new_mtime>" + fi + ;; + esac + + if $is_modified; then + changes+=("MODIFIED|$path|${old_files[$path]}|${new_files[$path]}") + + fi + fi + done + + # 写入变更到文件 + for change in "${changes[@]}"; do + echo "$change" >> "$output_file" + done + + local change_count=${#changes[@]} + info "差异比较完成: $change_count 个变更" + + return 0 +} + +# 处理长路径复制 +handle_long_path_copy() { + local source="$1" + local dest="$2" + + local max_path_length=200 + local dest_length=${#dest} + + if [[ $dest_length -le $max_path_length ]]; then + cp -p "$source" "$dest" + return $? + fi + + # 方法1: 使用相对路径 + local source_dir=$(dirname "$source") + local dest_dir=$(dirname "$dest") + local filename=$(basename "$source") + + if pushd "$source_dir" >/dev/null 2>&1; then + if cp -p "$filename" "$dest" 2>/dev/null; then + popd >/dev/null 2>&1 + return 0 + fi + popd >/dev/null 2>&1 + fi + + # 方法2: 使用临时短路径 + local temp_file="$TEMP_DIR/short_$(basename "$source")" + if cp -p "$source" "$temp_file" && mv "$temp_file" "$dest"; then + return 0 + fi + + # 方法3: 使用rsync(如果可用) + if command -v rsync >/dev/null 2>&1; then + if rsync -a "$source" "$dest"; then + return 0 + fi + fi + + error "无法复制长路径文件: $source -> $dest" + return 1 +} + +# 生成清单文件 +generate_manifest() { + local changes_file="$1" + local patch_dir="$2" + + local manifest_file="$patch_dir/MANIFEST.MF" + + info "生成清单文件: $manifest_file" + + cat > "$manifest_file" << EOF +# 补丁包清单 +名称: $PATCH_NAME +版本: $PATCH_VERSION +描述: $PATCH_DESCRIPTION +作者: $PATCH_AUTHOR +邮箱: $PATCH_EMAIL +生成时间: $(date) +生成主机: $(hostname) +比较方法: $COMPARISON_METHOD +哈希算法: $HASH_ALGORITHM +包含目录: ${TARGET_DIRECTORIES[*]} + +# 变更统计 +EOF + + # 统计变更类型 + local added_count=$(grep -c "^ADDED" "$changes_file") + local modified_count=$(grep -c "^MODIFIED" "$changes_file") + local deleted_count=$(grep -c "^DELETED" "$changes_file") + + cat >> "$manifest_file" << EOF +新增文件: $added_count +修改文件: $modified_count +删除文件: $deleted_count +总变更数: $((added_count + modified_count + deleted_count)) + +# 变更详情 +EOF + + # 写入变更详情 + while IFS='|' read -r change_type path extra_info; do + case "$change_type" in + "ADDED") + IFS='|' read -r size mtime ctime perm uid gid hash <<< "$extra_info" + printf "ADDED|%s|%d|%s|%s\n" "$path" "$size" "$hash" "$(date -d "@$mtime")" >> "$manifest_file" + ;; + "MODIFIED") + IFS='|' read -r old_info new_info <<< "$extra_info" + IFS='|' read -r old_size old_mtime old_ctime old_perm old_uid old_gid old_hash <<< "$old_info" + IFS='|' read -r new_size new_mtime new_ctime new_perm new_uid new_gid new_hash <<< "$new_info" + printf "MODIFIED|%s|%d->%d|%s->%s\n" "$path" "$old_size" "$new_size" "$old_hash" "$new_hash" >> "$manifest_file" + ;; + "DELETED") + IFS='|' read -r size mtime ctime perm uid gid hash <<< "$extra_info" + printf "DELETED|%s|%d|%s\n" "$path" "$size" "$hash" >> "$manifest_file" + ;; + esac + done < "$changes_file" + + info "清单文件生成完成" +} + +# 创建补丁包 +create_patch_package() { + local patch_dir="$1" + local output_path="$2" + + info "创建补丁包: $output_path" + info "压缩算法: $COMPRESSION_ALGORITHM" + info "长路径支持: $LONG_PATH_SUPPORT" + info "补丁内容目录: $patch_dir" + + # 确定压缩选项 + local compression_flag="" + case "$COMPRESSION_ALGORITHM" in + "gzip") compression_flag="z" ;; + "bzip2") compression_flag="j" ;; + "xz") compression_flag="J" ;; + *) compression_flag="" ;; + esac + + # 处理长路径支持 + local tar_options="-c${compression_flag}f" + if [[ "$LONG_PATH_SUPPORT" == "true" ]]; then + tar_options="--force-local $tar_options" + fi + + # 创建补丁包 + if pushd "$patch_dir" >/dev/null 2>&1; then + if tar $tar_options "$output_path" .; then + local size=$(du -h "$output_path" | cut -f1) + info "✅ 补丁包创建成功: $output_path ($size)" + popd >/dev/null 2>&1 + return 0 + else + error "❌ 补丁包创建失败" + popd >/dev/null 2>&1 + return 1 + fi + else + error "无法进入补丁目录: $patch_dir" + return 1 + fi +} + +# 安全签名 +sign_package() { + local package_path="$1" + + if [[ "$SIGNING_ENABLED" != "true" ]]; then + info "签名功能已禁用" + return 0 + fi + + if [[ ! -f "$PRIVATE_KEY" ]]; then + warn "私钥文件不存在: $PRIVATE_KEY" + return 1 + fi + + info "开始签名补丁包" + + if command -v gpg >/dev/null 2>&1; then + if gpg --homedir "/etc/patch/keys" \ + --batch --yes --detach-sign \ + --local-user "$PATCH_AUTHOR" \ + --output "${package_path}.sig" \ + "$package_path"; then + info "✅ 补丁包签名完成: ${package_path}.sig" + return 0 + else + error "❌ 签名失败" + return 1 + fi + else + warn "GPG未安装,跳过签名" + return 1 + fi +} + +# 生成校验和 +generate_checksum() { + local package_path="$1" + + if [[ "$CHECKSUM_ENABLED" != "true" ]]; then + info "校验和生成已禁用" + return 0 + fi + + info "生成校验和文件" + + local checksum_file="${package_path}.sha256" + if sha256sum "$package_path" > "$checksum_file"; then + info "✅ 校验和生成完成: $checksum_file" + return 0 + else + error "❌ 校验和生成失败" + return 1 + fi +} + +# 生成回滚包 +generate_rollback_package() { + if [[ "$ROLLBACK_ENABLED" != "true" ]]; then + info "回滚包生成已禁用" + return 0 + fi + + local source_dir="$1" + local changes_file="$2" + local patch_path="$3" + + info "开始生成回滚包" + + local rollback_dir="$TEMP_DIR/rollback_content" + mkdir -p "$rollback_dir" + + local rollback_files=() + + # 收集需要回滚的文件 + while IFS='|' read -r change_type path extra_info; do + case "$change_type" in + "MODIFIED"|"DELETED") + local source_file="$source_dir/$path" + if [[ -f "$source_file" ]]; then + local dest_file="$rollback_dir/$path" + local dest_dir=$(dirname "$dest_file") + + mkdir -p "$dest_dir" + if handle_long_path_copy "$source_file" "$dest_file"; then + rollback_files+=("$path") + trace "回滚文件: $source_file" + else + warn "无法复制回滚文件: $source_file" + fi + fi + ;; + esac + done < "$changes_file" + + if [[ ${#rollback_files[@]} -eq 0 ]]; then + warn "没有需要回滚的文件" + return 0 + fi + + # 创建回滚包 + local rollback_path="${patch_path%.*}.rollback.${patch_path##*.}" + if create_patch_package "$rollback_dir" "$rollback_path"; then + local size=$(du -h "$rollback_path" | cut -f1) + info "✅ 回滚包生成完成: $rollback_path ($size)" + echo "$rollback_path" + return 0 + else + error "❌ 回滚包生成失败" + return 1 + fi +} + +# 通知系统 +send_notification() { + local status="$1" + local message="$2" + local patch_path="${3:-}" + + if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then + return 0 + fi + + 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_notification "$subject" "$message" "$color" "$patch_path" + fi + + # 邮件通知(简化实现) + if [[ -n "$EMAIL_RECIPIENTS" ]]; then + send_email_notification "$subject" "$message" "$patch_path" + fi +} + +send_slack_notification() { + local subject="$1" + local message="$2" + local color="$3" + local patch_path="$4" + + local payload=$(cat << EOF +{ + "attachments": [ + { + "color": "$color", + "title": "$subject", + "text": "$message", + "fields": [ + { + "title": "补丁名称", + "value": "$PATCH_NAME", + "short": true + }, + { + "title": "版本", + "value": "$PATCH_VERSION", + "short": true + }, + { + "title": "文件", + "value": "$(basename "$patch_path")", + "short": true + }, + { + "title": "时间", + "value": "$(date)", + "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 +} + +send_email_notification() { + local subject="$1" + local message="$2" + local patch_path="$3" + + # 简化实现 - 实际环境中应使用sendmail或mail命令 + info "邮件通知: $subject - $message" + + local email_content=" +补丁生成通知 +======================================== +主题: $subject +时间: $(date) +主机: $(hostname) +补丁: $PATCH_NAME v$PATCH_VERSION +文件: $(basename "$patch_path") +描述: $message +======================================== +" + + # 这里可以添加实际的邮件发送逻辑 + echo "$email_content" > "$TEMP_DIR/email_notification.txt" + info "邮件内容已保存: $TEMP_DIR/email_notification.txt" +} + +# 主生成函数 +generate_patch() { + local source_dir="$1" + local target_dir="$2" + local output_path="$3" + + info "开始生成补丁包" + send_notification "start" "开始生成补丁包" "$output_path" + + # 扫描目录 + local old_manifest="$TEMP_DIR/old_manifest.txt" + local new_manifest="$TEMP_DIR/new_manifest.txt" + + if [[ "$PARALLEL_PROCESSING" == "true" ]]; then + scan_directory_parallel "$source_dir" "$old_manifest" + scan_directory_parallel "$target_dir" "$new_manifest" + else + scan_directory "$source_dir" "$old_manifest" + scan_directory "$target_dir" "$new_manifest" + fi + + # 比较差异 + local changes_file="$TEMP_DIR/changes.txt" + compare_files "$old_manifest" "$new_manifest" "$changes_file" + + # 检查是否有变更 + local change_count=$(wc -l < "$changes_file" 2>/dev/null || echo "0") + if [[ $change_count -eq 0 ]]; then + warn "未发现文件变更,无需生成补丁" + send_notification "success" "未发现文件变更" "$output_path" + return 0 + fi + + # 创建补丁内容目录 + local patch_content_dir="$TEMP_DIR/patch_content" + mkdir -p "$patch_content_dir/files" + + # 复制变更文件 + info "复制变更文件到补丁目录" + local copied_count=0 + + while IFS='|' read -r change_type path extra_info; do + case "$change_type" in + "ADDED"|"MODIFIED") + local source_file="$target_dir/$path" + local dest_file="$patch_content_dir/files/$path" + local dest_dir=$(dirname "$dest_file") + + mkdir -p "$dest_dir" + if handle_long_path_copy "$source_file" "$dest_file"; then + ((copied_count++)) + trace "复制文件: $source_file -> $dest_file" + else + warn "文件复制失败: $source_file" + fi + ;; + esac + done < "$changes_file" + + info "文件复制完成: $copied_count 个文件" + + # 生成清单 + generate_manifest "$changes_file" "$patch_content_dir" + + # 创建补丁包 + if create_patch_package "$patch_content_dir" "$output_path"; then + # 生成校验和 + generate_checksum "$output_path" + + # 签名补丁包 + sign_package "$output_path" + + # 生成回滚包 + generate_rollback_package "$source_dir" "$changes_file" "$output_path" + + send_notification "success" "补丁包生成成功" "$output_path" + return 0 + else + send_notification "failure" "补丁包生成失败" "$output_path" + return 1 + fi +} + +# 批量处理 +batch_generate() { + local config_dir="$1" + + if [[ ! -d "$config_dir" ]]; then + error "配置目录不存在: $config_dir" + return 1 + fi + + info "开始批量生成补丁包" + + local success_count=0 + local total_count=0 + + # 处理每个配置文件 + for config_file in "$config_dir"/*.sh; do + if [[ -f "$config_file" ]]; then + ((total_count++)) + info "处理配置: $(basename "$config_file")" + + # 备份当前配置 + cp "$CONFIG_FILE" "$TEMP_DIR/original_config.sh" + + # 使用新配置 + cp "$config_file" "$CONFIG_FILE" + load_config + + # 生成补丁包 + if main; then + ((success_count++)) + fi + + # 恢复原配置 + cp "$TEMP_DIR/original_config.sh" "$CONFIG_FILE" + load_config + fi + done + + info "批量生成完成: $success_count/$total_count 成功" + + if [[ $success_count -eq $total_count ]]; then + return 0 + else + return 1 + fi +} + +# 主函数 +main() { + local source_dir="${1:-}" + local target_dir="${2:-}" + local output_path="${3:-}" + + # 参数验证 + if [[ -z "$source_dir" ]] || [[ -z "$target_dir" ]]; then + error "必须指定源目录和目标目录" + echo "用法: $0 <源目录> <目标目录> [输出路径]" + echo "示例:" + echo " $0 /old/version /new/version" + echo " $0 /old/version /new/version /opt/patches/patch.tar.gz" + echo " $0 batch /path/to/configs" + exit 1 + fi + + # 批量处理模式 + if [[ "$source_dir" == "batch" ]]; then + batch_generate "$target_dir" + return $? + fi + + # 设置默认输出路径 + if [[ -z "$output_path" ]]; then + local timestamp=$(date +%Y%m%d_%H%M%S) + output_path="$OUTPUT_DIRECTORY/patch-${PATCH_NAME}-${PATCH_VERSION}-${timestamp}.tar.gz" + fi + + # 验证目录存在性 + if [[ ! -d "$source_dir" ]]; then + error "源目录不存在: $source_dir" + exit 1 + fi + + if [[ ! -d "$target_dir" ]]; then + error "目标目录不存在: $target_dir" + exit 1 + fi + + # 执行补丁生成 + if generate_patch "$source_dir" "$target_dir" "$output_path"; then + info "🎉 补丁包生成成功完成!" + info "📦 补丁文件: $output_path" + info "📋 日志文件: $LOG_FILE" + return 0 + else + error "💥 补丁包生成失败" + return 1 + fi +} + +# 异常处理 +trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM + +# 加载配置 +load_config + +# 设置环境 +setup_environment + +# 检查依赖 +check_dependencies + +# 执行主函数 +main "$@" \ No newline at end of file diff --git a/patch_rollback.sh b/patch_rollback.sh new file mode 100644 index 0000000..680b157 --- /dev/null +++ b/patch_rollback.sh @@ -0,0 +1,1085 @@ +#!/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 "🔓 释放回滚锁" +} + +# 清理函数 +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 + warning "文件不可读: $info_file" >&2 + continue + fi + + # 文件大小检查 + if [[ ! -s "$info_file" ]]; then + warning "空文件: $info_file" >&2 + continue + fi + + # 解析JSON + if ! jq -e '.' "$info_file" >/dev/null 2>&1; then + warning "无效的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 + warning "备份文件不存在: $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 "✅ 备份文件提取完成" + + # 验证提取内容 + local file_count=$(find "$extract_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}" + + 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 + + # 查找所有备份文件 + 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") + + 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 " 失败: $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 +} + +# 验证回滚结果 +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 + + # 检查所有备份文件是否已正确恢复 + find "$extract_dir" -type f | while read backup_file; do + local relative_path="${backup_file#$extract_dir/}" + local target_file="$relative_path" + + if [[ ! -f "$target_file" ]]; then + error "❌ 目标文件不存在: $target_file" + verification_passed=false + fail_count=$((fail_count+1)) + continue + fi + + # 比较文件内容 + if ! cmp -s "$backup_file" "$target_file"; then + error "❌ 文件内容不匹配: $relative_path" + verification_passed=false + fail_count=$((fail_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"; 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 "$@" \ No newline at end of file diff --git a/patch_verifier.sh b/patch_verifier.sh new file mode 100644 index 0000000..ffc05ee --- /dev/null +++ b/patch_verifier.sh @@ -0,0 +1,541 @@ +#!/bin/bash +# patch_verifier.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_verify_$(date +%Y%m%d_%H%M%S).log"} + : ${TEMP_DIR:="/tmp/patch_verify_$$"} +} + +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 + +# 验证函数 +verify_package_integrity() { + local package_path="$1" + local verify_type="$2" # pre-apply, post-apply, standalone + + info "开始验证补丁包: $package_path (类型: $verify_type)" + + local overall_result=true + + # 1. 基础验证 + if ! verify_basic_integrity "$package_path"; then + overall_result=false + fi + + # 2. 安全验证 + if ! verify_security "$package_path"; then + overall_result=false + fi + + # 3. 内容验证 + if ! verify_content "$package_path" "$verify_type"; then + overall_result=false + fi + + # 4. 系统状态验证 + if [[ "$verify_type" == "post-apply" ]]; then + if ! verify_system_state "$package_path"; then + overall_result=false + fi + fi + + # 生成验证报告 + generate_verification_report "$package_path" "$overall_result" "$verify_type" + + if $overall_result; then + info "✅ 补丁包验证通过" + return 0 + else + error "❌ 补丁包验证失败" + return 1 + fi +} + +verify_basic_integrity() { + local package_path="$1" + + info "执行基础完整性验证..." + local result=true + + # 文件存在性检查 + if [[ ! -f "$package_path" ]]; then + error "❌ 补丁包不存在: $package_path" + result=false + fi + + # 文件大小检查 + local size=$(stat -c%s "$package_path" 2>/dev/null || echo "0") + if [[ $size -eq 0 ]]; then + error "❌ 补丁包为空" + result=false + else + info "✅ 文件大小: $(numfmt --to=iec-i --suffix=B $size)" + fi + + # 压缩包完整性检查 + if ! tar -tzf "$package_path" >/dev/null 2>&1; then + error "❌ 压缩包损坏或格式错误" + result=false + else + info "✅ 压缩包完整性验证通过" + fi + + $result +} + +verify_security() { + local package_path="$1" + + info "执行安全验证..." + local result=true + + # 校验和验证 + if [[ -f "${package_path}.sha256" ]]; then + if sha256sum -c "${package_path}.sha256" >/dev/null 2>&1; then + info "✅ 校验和验证通过" + else + error "❌ 校验和验证失败" + result=false + fi + else + warn "⚠️ 未找到校验和文件" + fi + + # 签名验证 + if [[ "$SIGNING_ENABLED" == "true" ]] ;then + if [[ -f "${package_path}.sig" ]]; then + 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 "❌ 签名验证失败" + result=false + fi + else + warn "⚠️ GPG未安装,跳过签名验证" + fi + else + warn "⚠️ 未找到签名文件" + fi + fi + + $result +} + +verify_content() { + local package_path="$1" + local verify_type="$2" + + info "执行内容验证..." + local result=true + + # 解压补丁包 + local extract_dir="$TEMP_DIR/extract" + mkdir -p "$extract_dir" + if ! tar -xzf "$package_path" -C "$extract_dir"; then + error "❌ 补丁包解压失败" + return false + fi + + # 检查清单文件 + local manifest_file="$extract_dir/MANIFEST.MF" + if [[ ! -f "$manifest_file" ]]; then + error "❌ 清单文件不存在" + result=false + else + info "✅ 清单文件存在" + + # 验证清单格式 + if ! validate_manifest_format "$manifest_file"; then + result=false + fi + + # 验证文件完整性 + if ! validate_file_integrity "$extract_dir" "$manifest_file" "$verify_type"; then + result=false + fi + fi + + $result +} + +validate_manifest_format() { + local manifest_file="$1" + local result=true + + # 检查必需字段 + local required_fields=("名称" "版本" "生成时间") + for field in "${required_fields[@]}"; do + if ! grep -q "^$field:" "$manifest_file"; then + error "❌ 清单缺少必需字段: $field" + result=false + fi + done + + # 检查变更记录格式 + local change_count=$(grep -c -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file" || true) + if [[ $change_count -eq 0 ]]; then + warn "⚠️ 清单中没有变更记录" + else + info "✅ 变更记录数量: $change_count" + fi + + $result +} + +validate_file_integrity() { + local extract_dir="$1" + local manifest_file="$2" + local verify_type="$3" + + info "验证文件完整性..." + local result=true + local verified_count=0 + local error_count=0 + + # 处理清单中的每个变更记录 + while IFS='|' read -r change_type path extra_info; do + case "$change_type" in + "ADDED"|"MODIFIED") + if ! verify_patch_file "$extract_dir" "$path" "$change_type" "$verify_type"; then + error_count=$((error_count + 1)) + result=false + else + verified_count=$((verified_count + 1)) + fi + ;; + "DELETED") + if ! verify_deleted_file "$path" "$verify_type"; then + error_count=$((error_count + 1)) + result=false + else + verified_count=$((verified_count + 1)) + fi + ;; + esac + done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file") + + info "文件验证完成: $verified_count 个文件成功, $error_count 个文件失败" + $result +} + +verify_patch_file() { + local extract_dir="$1" + local file_path="$2" + local change_type="$3" + local verify_type="$4" + + local patch_file="$extract_dir/files/$file_path" + local target_file="/$file_path" + + # 检查补丁包中的文件是否存在 + if [[ ! -f "$patch_file" ]]; then + error "❌ 补丁包中文件不存在: $file_path" + return 1 + fi + + # 对于应用后验证,检查目标文件 + if [[ "$verify_type" == "post-apply" ]]; then + if [[ ! -f "$target_file" ]]; then + error "❌ 目标文件不存在: $target_file" + return 1 + fi + + # 比较文件内容 + local patch_hash=$(sha256sum "$patch_file" | cut -d' ' -f1) + local target_hash=$(sha256sum "$target_file" | cut -d' ' -f1) + + if [[ "$patch_hash" != "$target_hash" ]]; then + error "❌ 文件内容不匹配: $file_path" + return 1 + fi + + info "✅ 文件验证通过: $file_path" + else + info "✅ 补丁文件存在: $file_path" + fi + + return 0 +} + +verify_deleted_file() { + local file_path="$1" + local verify_type="$2" + + local target_file="/$file_path" + + # 对于应用后验证,检查文件是否已删除 + if [[ "$verify_type" == "post-apply" ]]; then + if [[ -f "$target_file" ]]; then + error "❌ 文件未成功删除: $target_file" + return 1 + fi + info "✅ 文件删除验证通过: $file_path" + else + info "✅ 删除操作记录存在: $file_path" + fi + + return 0 +} + +verify_system_state() { + local package_path="$1" + + info "验证系统状态..." + local result=true + + # 检查关键服务状态 + if ! verify_services; then + result=false + fi + + # 检查磁盘空间 + if ! verify_disk_space; then + result=false + fi + + # 检查系统负载 + if ! verify_system_load; then + result=false + fi + + $result +} + +verify_services() { + local result=true + + # 检查Web服务 + if systemctl is-active --quiet nginx || systemctl is-active --quiet apache2; then + info "✅ Web服务运行正常" + else + warn "⚠️ Web服务未运行" + fi + + # 检查数据库服务 + if systemctl is-active --quiet mysql || systemctl is-active --quiet postgresql; then + info "✅ 数据库服务运行正常" + else + warn "⚠️ 数据库服务未运行" + fi + + $result +} + +verify_disk_space() { + local result=true + local threshold=90 # 90% 使用率阈值 + + # 检查根分区使用率 + local usage=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') + if [[ $usage -gt $threshold ]]; then + error "❌ 磁盘空间不足: / 分区使用率 $usage%" + result=false + else + info "✅ 磁盘空间正常: / 分区使用率 $usage%" + fi + + $result +} + +verify_system_load() { + local result=true + local load_threshold=10.0 # 负载阈值 + + # 获取系统负载 + local load=$(awk '{print $1}' /proc/loadavg) + local cores=$(nproc) + + if (( $(echo "$load > $cores" | bc -l) )); then + warn "⚠️ 系统负载较高: $load (CPU核心数: $cores)" + else + info "✅ 系统负载正常: $load (CPU核心数: $cores)" + fi + + $result +} + +# 报告生成函数 +generate_verification_report() { + local package_path="$1" + local overall_result="$2" + local verify_type="$3" + + local report_file="$TEMP_DIR/verification_report_$(date +%Y%m%d_%H%M%S).txt" + + cat > "$report_file" << EOF +补丁包验证报告 +======================================== +补丁包: $(basename "$package_path") +验证类型: $verify_type +验证时间: $(date) +验证主机: $(hostname) +总体结果: $(if $overall_result; then echo "通过"; else echo "失败"; fi) + +详细验证结果: +EOF + + # 添加详细验证信息 + { + echo "1. 基础完整性验证: $(if verify_basic_integrity "$package_path"; then echo "通过"; else echo "失败"; fi)" + echo "2. 安全验证: $(if verify_security "$package_path"; then echo "通过"; else echo "失败"; fi)" + echo "3. 内容验证: $(if verify_content "$package_path" "$verify_type"; then echo "通过"; else echo "失败"; fi)" + if [[ "$verify_type" == "post-apply" ]]; then + echo "4. 系统状态验证: $(if verify_system_state "$package_path"; then echo "通过"; else echo "失败"; fi)" + fi + } >> "$report_file" + + info "验证报告生成完成: $report_file" + + # 显示报告摘要 + echo + echo "验证报告摘要:" + cat "$report_file" + echo +} + +# 批量验证函数 +batch_verify() { + local patch_dir="${1:-$OUTPUT_DIRECTORY}" + local verify_type="${2:-standalone}" + + info "开始批量验证补丁包目录: $patch_dir" + + local total_count=0 + local success_count=0 + local fail_count=0 + + # 查找所有补丁包文件 + for patch_file in "$patch_dir"/*.tar.gz; do + if [[ -f "$patch_file" ]]; then + ((total_count++)) + info "验证补丁包: $(basename "$patch_file")" + + if verify_package_integrity "$patch_file" "$verify_type"; then + ((success_count++)) + else + ((fail_count++)) + fi + + echo "----------------------------------------" + fi + done + + info "批量验证完成: $success_count/$total_count 成功, $fail_count/$total_count 失败" + + if [[ $fail_count -eq 0 ]]; then + return 0 + else + return 1 + fi +} + +# 主验证函数 +main_verify() { + local patch_path="${1:-}" + local verify_type="${2:-standalone}" + local batch_mode="${3:-false}" + + # 加载配置 + load_config + + # 设置环境 + setup_environment + + # 批量验证模式 + if [[ "$batch_mode" == "true" ]] || [[ -d "$patch_path" ]]; then + batch_verify "$patch_path" "$verify_type" + return $? + fi + + # 单个文件验证 + if [[ -z "$patch_path" ]]; then + echo "用法: $0 <补丁包路径|目录> [verify-type] [batch]" + echo "验证类型:" + echo " standalone - 独立验证(默认)" + echo " pre-apply - 应用前验证" + echo " post-apply - 应用后验证" + echo "示例:" + echo " $0 /opt/patches/patch.tar.gz" + echo " $0 /opt/patches/patch.tar.gz pre-apply" + echo " $0 /opt/patches/ batch" + exit 1 + fi + + # 执行验证 + if verify_package_integrity "$patch_path" "$verify_type"; then + info "🎉 补丁包验证成功" + exit 0 + else + error "💥 补丁包验证失败" + exit 1 + fi +} + +# 异常处理 +trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM + +main_verify "$@" \ No newline at end of file diff --git a/patch_workflow.sh b/patch_workflow.sh new file mode 100644 index 0000000..1ea2bbf --- /dev/null +++ b/patch_workflow.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# patch_workflow.sh - 完整补丁管理工作流 + +set -euo pipefail + +# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT_DIR="/opt/patch-management" + +# 颜色定义 +GREEN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +log() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# 完整工作流 +full_patch_workflow() { + local source_dir="$1" + local target_dir="$2" + local patch_name="$3" # 补丁文件路径 + + log "开始完整补丁管理工作流" + echo "========================================" + + # 1. 生成补丁包 + log "步骤1: 生成补丁包" + if ! "$SCRIPT_DIR/patch_generator.sh" "$source_dir" "$target_dir" "$patch_name"; then + error "补丁包生成失败" + return 1 + fi + + # 获取生成的补丁包路径 + local patch_file=$(find "/opt/patches" -name "*${patch_name}*" -type f | head -1) + if [[ -z "$patch_file" ]]; then + error "未找到补丁包文件" + return 1 + fi + + # 2. 验证补丁包 + log "步骤2: 验证补丁包" + if ! "$SCRIPT_DIR/patch_verifier.sh" "$patch_file" "pre-apply"; then + error "补丁包验证失败" + return 1 + fi + + # 3. 应用补丁包(干跑模式) + log "步骤3: 干跑模式应用补丁" + if ! "$SCRIPT_DIR/patch_applier.sh" "$patch_file" "dry-run"; then + error "干跑模式应用失败" + return 1 + fi + + # 4. 实际应用补丁包 + read -p "是否继续实际应用补丁? (y/N): " confirm + if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then + log "步骤4: 实际应用补丁" + if ! "$SCRIPT_DIR/patch_applier.sh" "$patch_file"; then + error "补丁应用失败" + return 1 + fi + else + log "操作取消" + return 0 + fi + + # 5. 应用后验证 + log "步骤5: 应用后验证" + if ! "$SCRIPT_DIR/patch_verifier.sh" "$patch_file" "post-apply"; then + error "应用后验证失败" + return 1 + fi + + log "🎉 完整补丁管理工作流完成" + return 0 +} + +# 回滚工作流 +rollback_workflow() { + local rollback_file="${1:-}" + + log "开始回滚工作流" + echo "========================================" + + # 1. 干跑模式回滚 + log "步骤1: 干跑模式回滚" + if ! "$SCRIPT_DIR/patch_rollback.sh" "$rollback_file" "dry-run"; then + error "干跑模式回滚失败" + return 1 + fi + + # 2. 实际回滚 + read -p "是否继续实际回滚? (y/N): " confirm + if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then + log "步骤2: 实际回滚" + if ! "$SCRIPT_DIR/patch_rollback.sh" "$rollback_file"; then + error "回滚失败" + return 1 + fi + else + log "操作取消" + return 0 + fi + + log "✅ 回滚工作流完成" + return 0 +} + +# 主函数 +main() { + case "${1:-}" in + "generate-full") + shift + full_patch_workflow "$@" + ;; + "generate") + shift + "$SCRIPT_DIR/patch_generator.sh" "$@" + ;; + "apply") + shift + "$SCRIPT_DIR/patch_applier.sh" "$@" + ;; + "rollback") + shift + rollback_workflow "$@" + ;; + "verify") + shift + "$SCRIPT_DIR/patch_verifier.sh" "$@" + ;; + "batch-verify") + shift + "$SCRIPT_DIR/patch_verifier.sh" "$1" "standalone" "batch" + ;; + *) + echo "用法: $0 [args]" + echo "命令:" + echo " generate-full <旧目录> <新目录> [补丁名称] # 生成补丁" + echo " generate <旧目录> <新目录> [补丁名称] # 生成补丁" + echo " apply <补丁包路径> [dry-run] # 应用补丁" + echo " rollback [回滚包路径] # 回滚补丁" + echo " verify <补丁包路径> [验证类型] # 验证补丁" + echo " batch-verify <目录> # 批量验证" + echo "" + echo "示例:" + echo " 进入项目目录,然后执行" + echo " $0 generate /old/version /new/version" + echo " $0 apply /opt/patches/patch.tar.gz dry-run" + echo " $0 rollback /var/backups/patch/backup.tar.gz" + exit 1 + ;; + esac +} + +# 异常处理 +trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM + +main "$@" \ No newline at end of file