games-development-ai/deploy/import-newapi-pool.sh

212 lines
9.1 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.

#!/usr/bin/env bash
# 在 mini-infra 校验离线 emit SQL并通过 Tailscale 受控导入 mini-desktop staging MySQL。
# SQL 仅经 stdin 传输;日志与证据只保留哈希、用户名和聚合计数,不输出 token。
set -euo pipefail
readonly EXPECTED_ACK='IMPORT_NEWAPI_POOL_TO_STAGING'
readonly EXPECTED_SOURCE_HOST='mini-infra'
readonly EXPECTED_SOURCE_RUNTIME_HOST='minione-ubuntu-infra'
readonly EXPECTED_TARGET_HOST='root@100.64.0.7'
readonly MYSQL_CONTAINER='game-staging-mysql'
readonly MYSQL_DATABASE='ruoyi-vue-pro'
readonly POOL_TABLE='newapi_quota_pool'
NEWAPI_POOL_IMPORT_ACK="${NEWAPI_POOL_IMPORT_ACK:-}"
NEWAPI_POOL_SQL_FILE="${NEWAPI_POOL_SQL_FILE:-}"
NEWAPI_POOL_IMPORT_HOST="${NEWAPI_POOL_IMPORT_HOST:-$EXPECTED_TARGET_HOST}"
NEWAPI_POOL_IMPORT_EVIDENCE="${NEWAPI_POOL_IMPORT_EVIDENCE:-./newapi-pool-import.evidence}"
HOSTNAME_BIN="${HOSTNAME_BIN:-hostname}"
SSH_BIN="${SSH_BIN:-ssh}"
die() { printf 'NEWAPI_POOL_IMPORT_FAIL %s\n' "$*" >&2; exit 1; }
mode_of() { stat -f '%Lp' "$1" 2>/dev/null || stat -c '%a' "$1"; }
[ "$NEWAPI_POOL_IMPORT_ACK" = "$EXPECTED_ACK" ] || \
die "NEWAPI_POOL_IMPORT_ACK 必须精确设为 $EXPECTED_ACK"
[ -f "$NEWAPI_POOL_SQL_FILE" ] && [ ! -L "$NEWAPI_POOL_SQL_FILE" ] || \
die 'NEWAPI_POOL_SQL_FILE 必须是本机普通文件'
case "$(mode_of "$NEWAPI_POOL_SQL_FILE")" in
400|600) ;;
*) die 'emit SQL 含 token文件权限必须是 400 或 600' ;;
esac
source_hostname="$("$HOSTNAME_BIN" -s 2>/dev/null || "$HOSTNAME_BIN")"
source_hostname="${source_hostname%%.*}"
case "$source_hostname" in
"$EXPECTED_SOURCE_HOST"|"$EXPECTED_SOURCE_RUNTIME_HOST") ;;
*) die "只允许在 mini-infra 离线执行actual=$source_hostname" ;;
esac
[ "$NEWAPI_POOL_IMPORT_HOST" = "$EXPECTED_TARGET_HOST" ] || \
die "导入目标只允许 $EXPECTED_TARGET_HOST"
[ ! -e "$NEWAPI_POOL_IMPORT_EVIDENCE" ] || \
die "证据文件已存在,拒绝覆盖:$NEWAPI_POOL_IMPORT_EVIDENCE"
umask 077
validation_file="$(mktemp)"
remote_output_file="$(mktemp)"
cleanup() { rm -f "$validation_file" "$remote_output_file"; }
trap cleanup EXIT
# 只接受预置工具生成的四条幂等 INSERT解析仅输出非敏感 username/user id。
python3 - "$NEWAPI_POOL_SQL_FILE" >"$validation_file" <<'PY'
import re
import sys
path = sys.argv[1]
comment_re = re.compile(r"^-- pool_entry username=(neice_[0-9]{3}) newapi_user_id=([0-9]+)$")
insert_re = re.compile(
r"^INSERT INTO newapi_quota_pool "
r"\(newapi_user_id, newapi_token_id, newapi_token_key, grant_quota, "
r"quota_per_unit_snapshot, usd_rate_snapshot, status\) VALUES "
r"\(([0-9]+), [0-9]+, '[A-Za-z0-9_-]{16,128}', [0-9]+, [0-9]+, "
r"[0-9]+(?:\.[0-9]+)?, 'FREE'\) ON DUPLICATE KEY UPDATE "
r"newapi_token_id=VALUES\(newapi_token_id\), "
r"newapi_token_key=VALUES\(newapi_token_key\), "
r"grant_quota=VALUES\(grant_quota\), "
r"quota_per_unit_snapshot=VALUES\(quota_per_unit_snapshot\), "
r"usd_rate_snapshot=VALUES\(usd_rate_snapshot\);$"
)
entries = []
pending = None
with open(path, encoding="utf-8") as handle:
for line_number, raw in enumerate(handle, 1):
line = raw.strip()
if not line:
continue
comment = comment_re.fullmatch(line)
if comment:
if pending is not None:
raise SystemExit(f"第 {line_number} 行前一条 pool_entry 缺 INSERT")
pending = (comment.group(1), int(comment.group(2)))
continue
if line.startswith("--"):
continue
insert = insert_re.fullmatch(line)
if not insert or pending is None:
raise SystemExit(f"第 {line_number} 行不是受控额度池 INSERT")
sql_user_id = int(insert.group(1))
if sql_user_id != pending[1]:
raise SystemExit(f"第 {line_number} 行 user id 与 pool_entry 不一致")
entries.append(pending)
pending = None
if pending is not None:
raise SystemExit("最后一条 pool_entry 缺 INSERT")
expected_names = [f"neice_{number:03d}" for number in range(51, 55)]
names = [entry[0] for entry in entries]
user_ids = [entry[1] for entry in entries]
if names != expected_names:
raise SystemExit(f"只允许导入 neice_051..054actual={names}")
if len(set(user_ids)) != 4:
raise SystemExit("四条 pool_entry 的 newapi_user_id 必须唯一")
print("4\t" + ",".join(str(user_id) for user_id in user_ids))
PY
IFS=$'\t' read -r source_row_count target_user_ids <"$validation_file"
[ "$source_row_count" = 4 ] && [[ "$target_user_ids" =~ ^[0-9]+(,[0-9]+){3}$ ]] || \
die 'emit SQL 校验结果非法'
source_sql_sha256="$(sha256sum "$NEWAPI_POOL_SQL_FILE" | awk '{print $1}')"
# 远端脚本固定容器、数据库和表SQL 由 stdin 送入临时 mode600 文件,事务执行后立即销毁。
remote_script="$(cat <<'REMOTE'
set -euo pipefail
expected_hash="$1"
target_user_ids="$2"
sql_file="$(mktemp)"
mysql_error=''
cleanup_remote() {
rm -f "$sql_file"
[ -z "$mysql_error" ] || rm -f "$mysql_error"
}
trap cleanup_remote EXIT
umask 077
cat >"$sql_file"
chmod 600 "$sql_file"
[ "$(sha256sum "$sql_file" | awk '{print $1}')" = "$expected_hash" ] || {
printf '远端 SQL 哈希不匹配\n' >&2
exit 4
}
docker inspect game-staging-mysql >/dev/null
aggregate() {
local query
query="SELECT COUNT(*), COALESCE(SUM(status='FREE'),0), COALESCE(SUM(status='CLAIMED'),0), COALESCE(SUM(status='INVALID'),0), (SELECT COUNT(*) FROM newapi_quota_pool WHERE status='FREE' AND deleted=b'0') FROM newapi_quota_pool WHERE newapi_user_id IN (${target_user_ids}) AND deleted=b'0';"
docker exec game-staging-mysql sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B "$1" -e "$2"' \
sh 'ruoyi-vue-pro' "$query"
}
before="$(aggregate)"
mysql_error="$(mktemp)"
set +e
{
printf 'START TRANSACTION;\n'
cat "$sql_file"
printf '\nCOMMIT;\n'
} | docker exec -i game-staging-mysql sh -lc \
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B "$1"' sh 'ruoyi-vue-pro' \
>/dev/null 2>"$mysql_error"
mysql_rc="${PIPESTATUS[1]}"
set -e
if [ "$mysql_rc" -ne 0 ]; then
# mysql stderr 可能回显含 token 的 SQL仅保留返回码并让临时错误文件随 EXIT 清理。
printf 'staging MySQL 事务导入失败 rc=%s\n' "$mysql_rc" >&2
exit "$mysql_rc"
fi
rm -f "$mysql_error"
mysql_error=''
after="$(aggregate)"
printf 'BEFORE\t%s\n' "$before"
printf 'AFTER\t%s\n' "$after"
REMOTE
)"
remote_script_base64="$(printf '%s' "$remote_script" | base64 | tr -d '\n')"
remote_command="bash -c 'script_b64=\$1; shift; eval \"\$(printf %s \"\$script_b64\" | base64 -d)\"' _ '$remote_script_base64' '$source_sql_sha256' '$target_user_ids'"
if ! "$SSH_BIN" -o ProxyCommand=none "$NEWAPI_POOL_IMPORT_HOST" "$remote_command" \
<"$NEWAPI_POOL_SQL_FILE" >"$remote_output_file"; then
die '远端事务导入失败;未签发通过证据'
fi
before_line="$(awk -F '\t' '$1=="BEFORE" {print; count++} END {exit count!=1}' "$remote_output_file")" || \
die '远端缺唯一 BEFORE 聚合'
after_line="$(awk -F '\t' '$1=="AFTER" {print; count++} END {exit count!=1}' "$remote_output_file")" || \
die '远端缺唯一 AFTER 聚合'
IFS=$'\t' read -r _ before_total before_free before_claimed before_invalid before_global_free <<<"$before_line"
IFS=$'\t' read -r _ after_total after_free after_claimed after_invalid after_global_free <<<"$after_line"
for value in "$before_total" "$before_free" "$before_claimed" "$before_invalid" "$before_global_free" \
"$after_total" "$after_free" "$after_claimed" "$after_invalid" "$after_global_free"; do
[[ "$value" =~ ^[0-9]+$ ]] || die '远端聚合包含非整数'
done
[ "$after_total" -eq 4 ] || die "导入后四个目标 user id 未全部存在actual=$after_total"
[ $((after_free + after_claimed + after_invalid)) -eq 4 ] || die '导入后目标状态聚合不守恒'
[ "$before_total" -le "$after_total" ] || die '导入后目标行数倒退'
mkdir -p "$(dirname "$NEWAPI_POOL_IMPORT_EVIDENCE")"
{
printf 'schema=newapi-pool-import/1\n'
printf 'captured_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'source_host=%s\n' "$source_hostname"
printf 'target_host=100.64.0.7\n'
printf 'database=%s\n' "$MYSQL_DATABASE"
printf 'table=%s\n' "$POOL_TABLE"
printf 'source_sql_sha256=%s\n' "$source_sql_sha256"
printf 'source_row_count=%s\n' "$source_row_count"
printf 'source_usernames=neice_051,neice_052,neice_053,neice_054\n'
printf 'before_target_total=%s\n' "$before_total"
printf 'before_target_free=%s\n' "$before_free"
printf 'before_target_claimed=%s\n' "$before_claimed"
printf 'before_target_invalid=%s\n' "$before_invalid"
printf 'before_global_free=%s\n' "$before_global_free"
printf 'after_target_total=%s\n' "$after_total"
printf 'after_target_free=%s\n' "$after_free"
printf 'after_target_claimed=%s\n' "$after_claimed"
printf 'after_target_invalid=%s\n' "$after_invalid"
printf 'after_global_free=%s\n' "$after_global_free"
printf 'inserted_target_rows=%s\n' "$((after_total - before_total))"
printf 'status=PASS\n'
} >"$NEWAPI_POOL_IMPORT_EVIDENCE"
chmod 400 "$NEWAPI_POOL_IMPORT_EVIDENCE"
printf 'NEWAPI_POOL_IMPORT_PASS rows=4 sql_sha256=%s before=%s after=%s evidence=%s\n' \
"$source_sql_sha256" "$before_total" "$after_total" "$NEWAPI_POOL_IMPORT_EVIDENCE"