chore: 针对备份及还原处理
This commit is contained in:
587
scripts/patch_tools/patch_applier.sh
Normal file
587
scripts/patch_tools/patch_applier.sh
Normal file
@@ -0,0 +1,587 @@
|
||||
#!/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 "$@"
|
||||
Reference in New Issue
Block a user