Files
codex-app-server/deploy/restore-drill.sh
host 64995fafab P2-ops: migration alignment, deploy test list, monitor alerts/single-host, backup/rollback closure, Caddy template
- migrations: MIGRATION_TABLES now equals POSTGRES_SCHEMA tables
  (uploads, task_attachments, task_followups, environments added);
  SEQUENCE_TABLES matches IDENTITY tables; task_followups
  sender_task_id column ensured before insert; UNMIGRATED_TABLES
  and migration_coverage() report tables with no PG schema instead
  of silently skipping them; CLI prints the coverage report.
- deploy: DEFAULT_TEST_SUITES gains deploy/tests (discover -s only,
  never package path); regression tests pin the default list.
- monitor: MONITOR_SINGLE_HOST=1 makes the server check single-host;
  default stays dual to avoid silent downgrade; server_mode is in
  the JSON. Optional fail-only alert outlet (webhook or mail,
  default off, alert body is pass/fail JSON only, no secrets).
- backup/rollback: README and restore-drill document what is
  recoverable (DB, code) vs not (worktrees, traces, uploads, host
  configs); rollback.sh shares the server-unit list and dual-port
  health success standard with host_deploy.sh (sync test guards it).
- public entry: Caddyfile uses native {$DOMAIN}/{$ACME_EMAIL}
  variables plus a caddy.service template; README documents the
  steps and the current BT-nginx production status.
2026-10-27 16:37:28 +00:00

113 lines
4.6 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.
#!/bin/sh
# 备份恢复演练:主机侧运行(root,或能读 /etc/codex-app-server/env 的用户)。
#
# 用法(主机上):sh /opt/codex-app-server/deploy/restore-drill.sh
# 可选环境变量:BACKUP_DIR(默认 /var/lib/codex-app-server/backups)。
#
# 演练范围(P2-ops,诚实边界):本演练只证明“数据库备份可恢复”
# (行数比对通过)。它不证明 worktree / 链路 / 上传文件可恢复,
# 因为那些根本不在备份里(见 deploy/README.md“备份制度”一节):
# 代码备份 tar 明确排除 work/,pg-backup 只备数据库。不要把
# “恢复演练 PASS”读成“整机可恢复”。
#
# 步骤:
# 1. 跑一次 backup_postgres 生成新鲜备份(.sql)。
# 2. 新建一个测试库 codex_control_restore_test_<时间戳>,用
# cloud.backup.restore_postgres 把备份恢复进去。
# 3. 比对生产库与测试库的 tasks / projects / users / audit_log 行数,
# 行数相等即通过;只比行数,不比对具体内容,不打印任何行内容。
# 4. 比对完立即删掉测试库(trap 保证即使中途失败也删)。
#
# 安全红线(写进脚本逻辑里,不是只写在注释里):
# - 恢复目标永远是新生成的测试库名,绝不等于生产库名;restore_postgres
# 本身也会拒绝目标库名等于 CLOUD_DB_NAME(allow_production 绝不在
# 本脚本里设为 True),双重保险。
# - 本脚本绝不恢复到生产库,绝不删生产库;删的只会是本次生成的测试库。
# - 全程只打印 PASS/FAIL 与行数,不打印密码,不打印备份内容。
# - 本脚本以 root 身份读 env 文件是为了拿到数据库连接参数与密码,
# 密码只经 PGPASSWORD 环境变量传给 psql/python 子进程,绝不出现在
# 命令行参数里,也绝不被 echo 出来。
set -eu
APP_DIR=${APP_DIR:-/opt/codex-app-server}
BACKUP_DIR=${BACKUP_DIR:-/var/lib/codex-app-server/backups}
ENV_FILE=${ENV_FILE:-/etc/codex-app-server/env}
if [ ! -f "$ENV_FILE" ]; then
echo "FAIL:找不到 env 文件:$ENV_FILE" >&2
exit 1
fi
# shellcheck disable=SC1090
set -a
. "$ENV_FILE"
set +a
if [ -z "${CLOUD_DB_NAME:-}" ] || [ -z "${CLOUD_DB_USER:-}" ]; then
echo "FAIL:env 文件里缺少 CLOUD_DB_NAME 或 CLOUD_DB_USER" >&2
exit 1
fi
export PGPASSWORD="${CLOUD_DB_PASSWORD:-}"
HOST_ARG=${CLOUD_DB_HOST:-127.0.0.1}
PORT_ARG=${CLOUD_DB_PORT:-5432}
STAMP=$(date +%Y%m%d%H%M%S)
TEST_DB="codex_control_restore_test_$STAMP"
drop_test_db() {
# trap 保证:无论演练成功失败,测试库都会被删掉,不留垃圾库。
psql -h "$HOST_ARG" -p "$PORT_ARG" -U "$CLOUD_DB_USER" \
-d postgres -c "DROP DATABASE IF EXISTS \"$TEST_DB\"" >/dev/null 2>&1 || true
}
trap drop_test_db EXIT
# 1. 新鲜备份(backup_postgres 的连接参数与密码都走环境变量,
# 密码只经 PGPASSWORD 传给子进程,绝不出现在命令行或输出里)。
mkdir -p "$BACKUP_DIR"
BACKUP_FILE=$(python3 - "$APP_DIR" "$BACKUP_DIR" <<'PY'
import sys
sys.path.insert(0, sys.argv[1])
from cloud.backup import backup_postgres
print(backup_postgres(sys.argv[2]))
PY
)
if [ ! -f "$BACKUP_FILE" ]; then
echo "FAIL:备份生成失败" >&2
exit 1
fi
echo "备份已生成(只打印路径,不打印内容):$BACKUP_FILE"
# 2. 建测试库并恢复
psql -h "$HOST_ARG" -p "$PORT_ARG" -U "$CLOUD_DB_USER" \
-d postgres -c "CREATE DATABASE \"$TEST_DB\"" >/dev/null
python3 - "$APP_DIR" "$BACKUP_FILE" "$TEST_DB" <<'PY'
import sys
sys.path.insert(0, sys.argv[1])
from cloud.backup import restore_postgres
# allow_production 故意不传(默认 False):目标库名绝不能等于生产库名。
print(restore_postgres(sys.argv[2], sys.argv[3]))
PY
echo "恢复完成,目标测试库:$TEST_DB"
# 3. 行数比对(只比行数,不打印行内容)
FAIL=0
for TABLE in tasks projects users audit_log; do
PROD_N=$(psql -h "$HOST_ARG" -p "$PORT_ARG" -U "$CLOUD_DB_USER" \
-d "$CLOUD_DB_NAME" -t -A -c "SELECT COUNT(*) FROM $TABLE" | tr -d ' \n')
REST_N=$(psql -h "$HOST_ARG" -p "$PORT_ARG" -U "$CLOUD_DB_USER" \
-d "$TEST_DB" -t -A -c "SELECT COUNT(*) FROM $TABLE" | tr -d ' \n')
if [ "$PROD_N" = "$REST_N" ]; then
echo "PASS $TABLE:生产库=$PROD_N 恢复库=$REST_N"
else
echo "FAIL $TABLE:生产库=$PROD_N 恢复库=$REST_N" >&2
FAIL=1
fi
done
# 4. 删测试库由 trap 执行;这里先显式删一次,trap 再删是空操作。
drop_test_db
trap - EXIT
if [ "$FAIL" = "1" ]; then
echo "恢复演练 FAIL:有表行数不一致" >&2
exit 1
fi
echo "恢复演练 PASS:全部 4 张表行数一致,测试库已删除"