Files
shop-platform/scripts/patch_tools/patch_applier.sh

587 lines
15 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# patch_applier.sh - 企业级补丁包应用脚本
set -euo pipefail
shopt -s nullglob extglob
# 脚本目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/patch_config.sh"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m'
# 日志函数
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
"INFO") color="$GREEN" ;;
"WARN") color="$YELLOW" ;;
"ERROR") color="$RED" ;;
"DEBUG") color="$BLUE" ;;
*) color="$NC" ;;
esac
echo -e "${color}[$timestamp] [$level]${NC} $message" | tee -a "$LOG_FILE"
}
info() { log "INFO" "$1"; }
warn() { log "WARN" "$1"; }
error() { log "ERROR" "$1"; }
debug() { log "DEBUG" "$1"; }
# 配置和初始化
load_config() {
if [[ ! -f "$CONFIG_FILE" ]]; then
error "配置文件不存在: $CONFIG_FILE"
exit 1
fi
source "$CONFIG_FILE"
info "配置文件加载完成"
# 设置默认值
: ${LOG_LEVEL:="INFO"}
: ${LOG_FILE:="/tmp/patch_apply_$(date +%Y%m%d_%H%M%S).log"}
: ${BACKUP_DIR:="/var/backups/patch"}
: ${TEMP_DIR:="/tmp/patch_apply_$$"}
}
setup_environment() {
mkdir -p "$(dirname "$LOG_FILE")"
mkdir -p "$BACKUP_DIR"
mkdir -p "$TEMP_DIR"
info "日志文件: $LOG_FILE"
info "备份目录: $BACKUP_DIR"
info "临时目录: $TEMP_DIR"
}
cleanup() {
if [[ -d "$TEMP_DIR" ]]; then
rm -rf "$TEMP_DIR"
info "临时目录已清理: $TEMP_DIR"
fi
}
trap cleanup EXIT
# 验证函数
verify_package() {
local package_path="$1"
info "开始验证补丁包: $package_path"
# 检查文件存在性
if [[ ! -f "$package_path" ]]; then
error "补丁包不存在: $package_path"
return 1
fi
# 检查文件大小
local size=$(stat -c%s "$package_path" 2>/dev/null || echo "0")
if [[ $size -eq 0 ]]; then
error "补丁包为空: $package_path"
return 1
fi
# 验证校验和
if [[ -f "${package_path}.sha256" ]]; then
info "验证校验和..."
if sha256sum -c "${package_path}.sha256" >/dev/null 2>&1; then
info "✅ 校验和验证通过"
else
error "❌ 校验和验证失败"
return 1
fi
fi
# 验证签名
if [[ -f "${package_path}.sig" ]]; then
info "验证签名..."
if command -v gpg >/dev/null 2>&1; then
if gpg --verify "${package_path}.sig" "$package_path" >/dev/null 2>&1; then
info "✅ 签名验证通过"
else
error "❌ 签名验证失败"
return 1
fi
else
warn "GPG未安装跳过签名验证"
fi
fi
# 验证压缩包完整性
if ! tar -tzf "$package_path" >/dev/null 2>&1; then
error "❌ 压缩包损坏或格式错误"
return 1
fi
info "✅ 补丁包验证通过"
return 0
}
# 备份函数
create_backup() {
local backup_name="backup_$(date +%Y%m%d_%H%M%S)"
local backup_path="$BACKUP_DIR/$backup_name.tar.gz"
info "创建应用前备份: $backup_path"
# 获取需要备份的文件列表
local files_to_backup=()
while IFS='|' read -r change_type path old_info new_info; do
case "$change_type" in
"MODIFIED"|"DELETED")
local target_file="$path"
if [[ -f "$target_file" ]]; then
files_to_backup+=("$target_file")
fi
;;
esac
done < "$TEMP_DIR/changes.txt"
if [[ ${#files_to_backup[@]} -eq 0 ]]; then
info "无需备份文件"
return 0
fi
# 创建备份
tar -czf "$backup_path" "${files_to_backup[@]}"
local backup_size=$(du -h "$backup_path" | cut -f1)
info "✅ 备份创建完成: $backup_path ($backup_size)"
echo "$backup_path"
}
# 补丁应用函数
extract_package() {
local package_path="$1"
local extract_dir="$2"
info "解压补丁包到: $extract_dir"
mkdir -p "$extract_dir"
if tar -xzf "$package_path" -C "$extract_dir"; then
info "✅ 补丁包解压完成"
return 0
else
error "❌ 补丁包解压失败"
return 1
fi
}
apply_file_changes() {
local patch_dir="$1"
info "开始应用文件变更..."
local applied_count=0
local failed_count=0
# 读取变更清单
local manifest_file="$patch_dir/MANIFEST.MF"
if [[ ! -f "$manifest_file" ]]; then
error "清单文件不存在: $manifest_file"
return 1
fi
# 处理每个变更
while IFS='|' read -r change_type path extra_info; do
case "$change_type" in
"ADDED"|"MODIFIED")
apply_file_addition_modification "$patch_dir" "$path" "$change_type"
result=$?
;;
"DELETED")
apply_file_deletion "$path"
result=$?
;;
*)
warn "未知变更类型: $change_type"
result=1
;;
esac
if [[ $result -eq 0 ]]; then
((applied_count++))
else
((failed_count++))
fi
done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file")
info "文件变更应用完成: $applied_count 成功, $failed_count 失败"
if [[ $failed_count -gt 0 ]]; then
return 1
fi
return 0
}
apply_file_addition_modification() {
local patch_dir="$1"
local file_path="$2"
local change_type="$3"
local source_file="$patch_dir/files/$file_path"
local target_file="/$file_path" # 绝对路径
local target_dir=$(dirname "$target_file")
# 检查源文件是否存在
if [[ ! -f "$source_file" ]]; then
error "源文件不存在: $source_file"
return 1
fi
# 创建目标目录
if [[ ! -d "$target_dir" ]]; then
mkdir -p "$target_dir"
debug "创建目录: $target_dir"
fi
# 备份原文件(如果是修改操作)
if [[ "$change_type" == "MODIFIED" ]] && [[ -f "$target_file" ]]; then
local backup_file="$TEMP_DIR/backup/$file_path"
mkdir -p "$(dirname "$backup_file")"
cp -p "$target_file" "$backup_file"
debug "备份原文件: $target_file -> $backup_file"
fi
# 复制文件
if cp -p "$source_file" "$target_file"; then
# 设置权限(从清单中读取)
set_file_permissions "$target_file" "$change_type"
info "✅ 应用文件: $file_path ($change_type)"
return 0
else
error "❌ 文件应用失败: $file_path"
return 1
fi
}
apply_file_deletion() {
local file_path="$1"
local target_file="/$file_path"
if [[ ! -f "$target_file" ]]; then
warn "文件不存在,无需删除: $target_file"
return 0
fi
# 备份文件
local backup_file="$TEMP_DIR/backup/$file_path"
mkdir -p "$(dirname "$backup_file")"
cp -p "$target_file" "$backup_file"
# 删除文件
if rm -f "$target_file"; then
info "✅ 删除文件: $file_path"
# 尝试删除空目录
local parent_dir=$(dirname "$target_file")
while [[ "$parent_dir" != "/" ]] && rmdir "$parent_dir" 2>/dev/null; do
debug "删除空目录: $parent_dir"
parent_dir=$(dirname "$parent_dir")
done
return 0
else
error "❌ 文件删除失败: $file_path"
return 1
fi
}
set_file_permissions() {
local file_path="$1"
local change_type="$2"
# 根据文件类型设置权限
case "$file_path" in
*.sh|*.bash)
chmod 755 "$file_path"
;;
*.php|*.py|*.pl)
chmod 644 "$file_path"
;;
/etc/*|/var/www/*)
chmod 644 "$file_path"
;;
*)
chmod 644 "$file_path"
;;
esac
debug "设置权限: $file_path -> $(stat -c %a "$file_path")"
}
# 验证应用结果
verify_application() {
local patch_dir="$1"
info "开始验证应用结果..."
local manifest_file="$patch_dir/MANIFEST.MF"
local verification_passed=true
while IFS='|' read -r change_type path extra_info; do
case "$change_type" in
"ADDED"|"MODIFIED")
if ! verify_file_application "$path" "$change_type" "$patch_dir"; then
verification_passed=false
fi
;;
"DELETED")
if ! verify_file_deletion "$path"; then
verification_passed=false
fi
;;
esac
done < <(grep -E "^(ADDED|MODIFIED|DELETED)" "$manifest_file")
if $verification_passed; then
info "✅ 应用验证通过"
return 0
else
error "❌ 应用验证失败"
return 1
fi
}
verify_file_application() {
local file_path="$1"
local change_type="$2"
local patch_dir="$3"
local target_file="/$file_path"
local source_file="$patch_dir/files/$file_path"
# 检查文件是否存在
if [[ ! -f "$target_file" ]]; then
error "❌ 文件不存在: $target_file"
return 1
fi
# 检查文件内容
local source_hash=$(sha256sum "$source_file" | cut -d' ' -f1)
local target_hash=$(sha256sum "$target_file" | cut -d' ' -f1)
if [[ "$source_hash" != "$target_hash" ]]; then
error "❌ 文件内容不匹配: $file_path"
return 1
fi
debug "✅ 文件验证通过: $file_path"
return 0
}
verify_file_deletion() {
local file_path="$1"
local target_file="/$file_path"
if [[ -f "$target_file" ]]; then
error "❌ 文件未成功删除: $target_file"
return 1
fi
debug "✅ 文件删除验证通过: $file_path"
return 0
}
# 回滚准备
create_rollback_point() {
local patch_path="$1"
local backup_path="$2"
local rollback_info_file="$BACKUP_DIR/rollback_info.json"
local patch_name=$(basename "$patch_path")
local timestamp=$(date +%Y%m%d_%H%M%S)
local rollback_info=$(cat << EOF
{
"patch_name": "$patch_name",
"apply_time": "$(date -Iseconds)",
"backup_path": "$backup_path",
"rollback_path": "$BACKUP_DIR/rollback_${timestamp}.tar.gz",
"status": "applied"
}
EOF
)
echo "$rollback_info" > "$rollback_info_file"
info "回滚点创建完成: $rollback_info_file"
}
# 通知函数
send_application_notification() {
local status="$1"
local patch_path="$2"
local message="$3"
if [[ "$NOTIFICATIONS_ENABLED" != "true" ]]; then
return 0
fi
case "$status" in
"start")
local subject="补丁应用开始"
local color="#3498db"
;;
"success")
local subject="补丁应用成功"
local color="#2ecc71"
;;
"failure")
local subject="补丁应用失败"
local color="#e74c3c"
;;
*)
local subject="补丁应用通知"
local color="#95a5a6"
;;
esac
# Slack通知
if [[ -n "$SLACK_WEBHOOK" ]]; then
send_slack_notification "$subject" "$message" "$color"
fi
}
send_slack_notification() {
local subject="$1"
local message="$2"
local color="$3"
local payload=$(cat << EOF
{
"attachments": [
{
"color": "$color",
"title": "$subject",
"text": "$message",
"fields": [
{
"title": "补丁文件",
"value": "$(basename "$PATCH_PATH")",
"short": true
},
{
"title": "应用时间",
"value": "$(date)",
"short": true
},
{
"title": "应用主机",
"value": "$(hostname)",
"short": true
}
]
}
]
}
EOF
)
if command -v curl >/dev/null 2>&1; then
curl -s -X POST -H 'Content-type: application/json' \
--data "$payload" "$SLACK_WEBHOOK" >/dev/null && \
info "Slack通知发送成功"
fi
}
# 主应用函数
apply_patch() {
local patch_path="$1"
local dry_run="${2:-false}"
info "开始应用补丁包: $patch_path"
send_application_notification "start" "$patch_path" "开始应用补丁"
# 验证补丁包
if ! verify_package "$patch_path"; then
error "补丁包验证失败"
send_application_notification "failure" "$patch_path" "补丁包验证失败"
return 1
fi
# 解压补丁包
local extract_dir="$TEMP_DIR/extract"
if ! extract_package "$patch_path" "$extract_dir"; then
send_application_notification "failure" "$patch_path" "补丁包解压失败"
return 1
fi
# 如果是干跑模式,只验证不应用
if [[ "$dry_run" == "true" ]]; then
info "干跑模式: 只验证不应用"
verify_application "$extract_dir"
return 0
fi
# 创建备份
local backup_path=$(create_backup)
# 应用变更
if ! apply_file_changes "$extract_dir"; then
error "文件变更应用失败"
send_application_notification "failure" "$patch_path" "文件应用失败"
return 1
fi
# 验证应用结果
if ! verify_application "$extract_dir"; then
error "应用验证失败"
send_application_notification "failure" "$patch_path" "应用验证失败"
return 1
fi
# 创建回滚点
create_rollback_point "$patch_path" "$backup_path"
info "✅ 补丁应用完成"
send_application_notification "success" "$patch_path" "补丁应用成功"
return 0
}
# 主函数
main() {
local patch_path="${1:-}"
local dry_run="${2:-false}"
if [[ -z "$patch_path" ]]; then
echo "用法: $0 <补丁包路径> [dry-run]"
echo "示例:"
echo " $0 /opt/patches/patch-security-hotfix-1.2.3.tar.gz"
echo " $0 /opt/patches/patch-security-hotfix-1.2.3.tar.gz dry-run"
exit 1
fi
# 加载配置
load_config
# 设置环境
setup_environment
# 应用补丁
if apply_patch "$patch_path" "$dry_run"; then
info "🎉 补丁应用成功完成"
exit 0
else
error "💥 补丁应用失败"
exit 1
fi
}
# 异常处理
trap 'error "脚本执行中断"; cleanup; exit 1' INT TERM
main "$@"