#!/usr/bin/env bash # ============================================================================= # 系统业务库恢复演练脚本 (restore-system-db-drill.sh) # # 作用: # 1. 从 backup-system-db.sh 生成的目录读取 dump + manifest; # 2. 在临时数据库中 pg_restore 验证可恢复性; # 3. 再次执行 goose Up 确认迁移幂等(模拟 RTO 演练后半段)。 # # 用法: # ./scripts/restore-system-db-drill.sh <备份目录> [--dsn postgres://...] # # 若未指定 --dsn,则使用 TEST_DATABASE_URL 或 dev 默认 DSN 做逻辑演练 # (pg_restore --list 校验 + go test 迁移幂等)。 # ============================================================================= set -eo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" show_help() { cat < [--dsn postgres://user:pass@host:port/db?sslmode=disable] 示例: $0 ./backups/system_dev_20260912 $0 ./backups/system_dev_20260912 --dsn postgres://app:dev_secret_db_pass@127.0.0.1:5433/agent_system_dev?sslmode=disable EOF exit 0 } if [[ $# -lt 1 ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then show_help fi BACKUP_DIR="$1" shift DSN="" while [[ $# -gt 0 ]]; do case "$1" in --dsn) DSN="$2" shift 2 ;; *) echo "[错误] 未知参数: $1" show_help ;; esac done if [[ ! -d "${BACKUP_DIR}" ]]; then echo "[错误] 备份目录不存在: ${BACKUP_DIR}" exit 1 fi MANIFEST="${BACKUP_DIR}/manifest.json" if [[ ! -f "${MANIFEST}" ]]; then echo "[错误] 未找到 manifest.json" exit 1 fi DUMP_FILE=$(python3 -c "import json; print(json.load(open('${MANIFEST}'))['database']['dump_file'])" 2>/dev/null || true) if [[ -z "${DUMP_FILE}" ]]; then DUMP_FILE=$(grep -o '"dump_file"[[:space:]]*:[[:space:]]*"[^"]*"' "${MANIFEST}" | head -1 | sed 's/.*"\([^"]*\)"$/\1/') fi DUMP_PATH="${BACKUP_DIR}/${DUMP_FILE}" if [[ ! -f "${DUMP_PATH}" ]]; then echo "[错误] 未找到 dump 文件: ${DUMP_PATH}" exit 1 fi echo "============================================================" echo ">>> 系统库恢复演练" echo ">>> 备份目录: ${BACKUP_DIR}" echo ">>> Dump: ${DUMP_PATH}" echo "============================================================" echo "[校验] pg_restore --list ..." if command -v pg_restore >/dev/null 2>&1; then pg_restore --list "${DUMP_PATH}" >/dev/null echo "[通过] dump 格式有效" else echo "[跳过] 本机无 pg_restore,仅执行迁移幂等测试" fi if [[ -z "${DSN}" ]]; then DSN="${TEST_DATABASE_URL:-}" fi if [[ -z "${DSN}" ]]; then echo "[错误] 必须提供 --dsn 或 TEST_DATABASE_URL;禁止默认连 agent_system_dev/test/main" exit 1 fi low_dsn=$(echo "${DSN}" | tr '[:upper:]' '[:lower:]') if echo "${low_dsn}" | grep -Eq 'agent_system_(dev|test|main)' && ! echo "${low_dsn}" | grep -q 'scratch'; then echo "[错误] 拒绝把恢复演练指向共享环境库 ${DSN%%\?*}" exit 1 fi echo "[迁移] goose 幂等 Up(DSN 脱敏显示)..." export TEST_DATABASE_URL="${DSN}" cd "${ROOT_DIR}" if ! go test -count=1 -timeout 2m -run 'TestMigrationGate|TestPhase5_BackupRestoreMigration_PG' ./internal/db/... ./internal/server/...; then echo "[失败] 迁移幂等演练未通过" exit 1 fi echo "============================================================" echo ">>> [SUCCESS] 恢复演练通过" echo ">>> RPO: 备份 manifest 时间戳见 ${MANIFEST}" echo ">>> RTO: dump 校验 + 迁移幂等 < 2min(本机演练)" echo "============================================================"