Files
weichenandCursor d957b05569 chore(ops): add system DB backup and restore drill scripts.
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>
2026-09-13 02:50:23 +00:00

154 lines
5.1 KiB
Bash
Executable File
Raw Permalink 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.
#!/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 "============================================================"