Files
codex-app-server/deploy/restore-drill.sh
T
host 89a918b543 Ops batch: deploy automation, backup restore drill, monitoring, multi-user verification
Deploy: deploy.sh builds tarball, backs up current code (keep 3),
deploys, restarts, health-gates 60s (ok/backend=postgres/db_ok),
auto-rolls back on failure; rollback.sh restores latest backup.
Rollback is code-only, DB migrations are additive-only so old
code can still read new columns.
Backup: restore_postgres refuses to restore into the production
DB name unless explicitly allowed (drill never allows it);
restore-drill.sh backs up fresh, restores to a test DB, compares
tasks/projects/users/audit row counts, drops the test DB.
Monitoring: worker writes heartbeat file every loop (atomic
replace, failure only logged); GET /health adds
worker_last_seen_seconds_ago + worker_ok (180s default, env
tunable, still HTTP 200 when worker is stale); monitor.sh
checks server/worker/health/pg_isready/docker-postgres/disk/
app dir as one JSON line, exit non-zero on any fail, plus
systemd timer every 5 minutes. Push alerting to chat still
needs a separate channel decision (SSH key or webhook URL).
Multi-user: verify-multiuser.py creates test users/projects
in memory-key only, checks RBAC via real HTTP API (owner/
maintainer/contributor/viewer, cross-project isolation,
merge 200/403/409, no-key 401), test tasks carry a never-
satisfiable dependency so the production worker never claims
them, cleanup deletes only multiuser-test- rows (audit log
is append-only and keeps test entries by design).
Tests: host 27 pass, cloud 298 pass (8 skipped are
environment-dependent real-Postgres tests).
2026-10-26 04:37:40 +08:00

107 lines
4.2 KiB
Bash
Executable File
Raw 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)。
#
# 步骤:
# 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 张表行数一致,测试库已删除"