Support dev/test/main pg_dump backups with RPO manifest metadata and a restore drill that validates dump integrity and migration idempotency. Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.1 KiB
Bash
Executable File
154 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# 系统业务库备份脚本 (backup-system-db.sh)
|
||
#
|
||
# 作用:
|
||
# 1. 按环境 (dev/test/main) 定位 Docker 容器与数据库;
|
||
# 2. 采用 pg_dump -Fc 导出系统业务库(含 task_runs、工作流治理等);
|
||
# 3. 生成 manifest.json 记录 RPO 基准元数据与 SHA256。
|
||
#
|
||
# 用法:
|
||
# ./scripts/backup-system-db.sh <dev|test|main> <目标备份目录>
|
||
# =============================================================================
|
||
|
||
set -eo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||
|
||
show_help() {
|
||
cat <<EOF
|
||
用法:
|
||
$0 <dev|test|main> <目标备份目录>
|
||
|
||
示例:
|
||
$0 dev ./backups/system_dev_20260912
|
||
$0 test /tmp/agent_system_test_backup
|
||
EOF
|
||
exit 0
|
||
}
|
||
|
||
if [[ $# -lt 2 ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
|
||
show_help
|
||
fi
|
||
|
||
ENV_NAME="$1"
|
||
TARGET_DIR="$2"
|
||
|
||
case "${ENV_NAME}" in
|
||
dev)
|
||
CONTAINER_NAME="shuimu-context-pg-dev"
|
||
DB_NAME="agent_system_dev"
|
||
DB_USER="app"
|
||
DB_PASS="dev_secret_db_pass"
|
||
;;
|
||
test)
|
||
CONTAINER_NAME="shuimu-system-pg-test"
|
||
DB_NAME="agent_system_test"
|
||
DB_USER="app"
|
||
DB_PASS="test_secret_db_pass"
|
||
;;
|
||
main)
|
||
CONTAINER_NAME="shuimu-system-pg-main"
|
||
DB_NAME="agent_system_main"
|
||
DB_USER="app"
|
||
DB_PASS="prod_highly_secure_db_pass"
|
||
;;
|
||
*)
|
||
echo "[错误] 未知环境: ${ENV_NAME}(仅支持 dev/test/main)"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
mkdir -p "${TARGET_DIR}"
|
||
TARGET_DIR="$(cd "${TARGET_DIR}" && pwd)"
|
||
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
echo "[错误] 未找到 docker 命令"
|
||
exit 1
|
||
fi
|
||
|
||
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||
echo "[警告] 容器 ${CONTAINER_NAME} 未运行,尝试启动 ${ENV_NAME} 环境..."
|
||
if [[ -f "${ROOT_DIR}/scripts/env-service.sh" ]]; then
|
||
"${ROOT_DIR}/scripts/env-service.sh" "${ENV_NAME}" start
|
||
else
|
||
docker compose -f "${ROOT_DIR}/docker-compose.${ENV_NAME}.yml" up -d system-db
|
||
fi
|
||
fi
|
||
|
||
echo "[等待] 检查 ${ENV_NAME} 系统库连接..."
|
||
for i in {1..30}; do
|
||
if docker exec -i "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
if [ "$i" -eq 30 ]; then
|
||
echo "[错误] 数据库连接超时"
|
||
exit 1
|
||
fi
|
||
done
|
||
|
||
echo "[统计] 采集 ${ENV_NAME} 系统库指标..."
|
||
TASK_RUNS=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
|
||
"SELECT count(*) FROM task_runs;" 2>/dev/null || echo "0")
|
||
PROJECTS=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
|
||
"SELECT count(*) FROM projects;" 2>/dev/null || echo "0")
|
||
DB_SIZE=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
|
||
"SELECT pg_size_pretty(pg_database_size('${DB_NAME}'));" 2>/dev/null || echo "0 MB")
|
||
GOOSE_VER=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
|
||
"SELECT version_id FROM goose_db_version ORDER BY id DESC LIMIT 1;" 2>/dev/null || echo "0")
|
||
|
||
DUMP_FILENAME="system_db_${ENV_NAME}.dump"
|
||
TMP_CONTAINER_DUMP="/tmp/${DUMP_FILENAME}"
|
||
DEST_DUMP_PATH="${TARGET_DIR}/${DUMP_FILENAME}"
|
||
|
||
echo "[导出] pg_dump -Fc ${DB_NAME} ..."
|
||
START_TIME=$(date +%s)
|
||
docker exec "${CONTAINER_NAME}" pg_dump -U "${DB_USER}" -d "${DB_NAME}" -Fc -Z 6 -f "${TMP_CONTAINER_DUMP}"
|
||
docker cp "${CONTAINER_NAME}:${TMP_CONTAINER_DUMP}" "${DEST_DUMP_PATH}"
|
||
docker exec "${CONTAINER_NAME}" rm -f "${TMP_CONTAINER_DUMP}"
|
||
DUMP_DURATION=$(( $(date +%s) - START_TIME ))
|
||
|
||
DUMP_SIZE_HUMAN=$(ls -lh "${DEST_DUMP_PATH}" | awk '{print $5}')
|
||
DUMP_SIZE_BYTES=$(stat -c "%s" "${DEST_DUMP_PATH}" 2>/dev/null || stat -f "%z" "${DEST_DUMP_PATH}" 2>/dev/null || echo "0")
|
||
|
||
if command -v sha256sum >/dev/null 2>&1; then
|
||
DUMP_SHA256=$(sha256sum "${DEST_DUMP_PATH}" | awk '{print $1}')
|
||
elif command -v shasum >/dev/null 2>&1; then
|
||
DUMP_SHA256=$(shasum -a 256 "${DEST_DUMP_PATH}" | awk '{print $1}')
|
||
else
|
||
DUMP_SHA256="unavailable"
|
||
fi
|
||
|
||
NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||
MANIFEST_FILE="${TARGET_DIR}/manifest.json"
|
||
cat <<EOF > "${MANIFEST_FILE}"
|
||
{
|
||
"manifest_version": "1.0",
|
||
"created_at": "${NOW_ISO}",
|
||
"environment": "${ENV_NAME}",
|
||
"rpo_target_minutes": 60,
|
||
"database": {
|
||
"container_name": "${CONTAINER_NAME}",
|
||
"database_name": "${DB_NAME}",
|
||
"user": "${DB_USER}",
|
||
"dump_file": "${DUMP_FILENAME}",
|
||
"dump_sha256": "${DUMP_SHA256}",
|
||
"dump_size_bytes": ${DUMP_SIZE_BYTES},
|
||
"dump_size_human": "${DUMP_SIZE_HUMAN}",
|
||
"goose_version": ${GOOSE_VER:-0},
|
||
"task_runs_count": ${TASK_RUNS:-0},
|
||
"projects_count": ${PROJECTS:-0},
|
||
"database_size": "${DB_SIZE}"
|
||
}
|
||
}
|
||
EOF
|
||
|
||
echo "============================================================"
|
||
echo ">>> [SUCCESS] 系统库备份完成 (${ENV_NAME})"
|
||
echo ">>> Dump: ${DEST_DUMP_PATH} (${DUMP_SIZE_HUMAN}, ${DUMP_DURATION}s)"
|
||
echo ">>> Manifest: ${MANIFEST_FILE}"
|
||
echo ">>> 恢复演练: ./scripts/restore-system-db-drill.sh \"${TARGET_DIR}\""
|
||
echo "============================================================"
|