187d7735b9
将模型定价改为 SpacetimeDB 强类型持久化并兼容旧配置种子迁移 修复快速编辑支付弹窗期间框选显示和交互冻结 补齐后台图片放大以及视频音频资源预览 收紧主站和 External 资源换签授权并记录管理员跨用户审计 固化外部生成入队价格、attempt 钱包结算和最终 lease 失败收口 加固运行时身份轮换、bootstrap secret 与生产构建发布门禁 同步生成绑定、定向测试、运维脚本和项目文档
1103 lines
43 KiB
Bash
Executable File
1103 lines
43 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
PROVISION_TOOLS_DIR="${PROVISION_TOOLS_DIR:-provision-tools}"
|
|
SPACETIME_BIN_SOURCE="${SPACETIME_BIN_SOURCE:-${PROVISION_TOOLS_DIR}/spacetime/spacetime}"
|
|
OTELCOL_BIN_SOURCE="${OTELCOL_BIN_SOURCE:-${PROVISION_TOOLS_DIR}/otelcol-contrib}"
|
|
WORKER_ENV_FILE="${WORKER_ENV_FILE:-/etc/genarrative/external-generation-worker.env}"
|
|
CONTROLLER_ENV_FILE="${CONTROLLER_ENV_FILE:-/etc/genarrative/external-generation-controller.env}"
|
|
GENARRATIVE_OPENSSL_VERSION="${GENARRATIVE_OPENSSL_VERSION:-3.2.0}"
|
|
GENARRATIVE_OPENSSL_PREFIX="${GENARRATIVE_OPENSSL_PREFIX:-/opt/genarrative/openssl-3.2.0}"
|
|
GENARRATIVE_OPENSSL_SOURCE_URL="${GENARRATIVE_OPENSSL_SOURCE_URL:-https://github.com/openssl/openssl/releases/download/openssl-${GENARRATIVE_OPENSSL_VERSION}/openssl-${GENARRATIVE_OPENSSL_VERSION}.tar.gz}"
|
|
GENARRATIVE_OPENSSL_SOURCE_SHA256="${GENARRATIVE_OPENSSL_SOURCE_SHA256:-14c826f07c7e433706fb5c69fa9e25dab95684844b4c962a2cf1bf183eb4690e}"
|
|
|
|
require_non_root_relative_path() {
|
|
local label="$1"
|
|
local path="$2"
|
|
|
|
if [[ -z "${path}" ]]; then
|
|
echo "[server-provision] ${label} 不能为空。" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "${path}" == /* || "${path}" == *..* ]]; then
|
|
echo "[server-provision] ${label} 只能是工作区内的相对路径: ${path}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_path() {
|
|
local path="$1"
|
|
if [[ ! -e "${path}" ]]; then
|
|
echo "[server-provision] 缺少必要文件: ${path}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_cmd() {
|
|
local name="$1"
|
|
if ! command -v "${name}" >/dev/null 2>&1; then
|
|
echo "[server-provision] 缺少命令: ${name}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
normalize_server_aliases() {
|
|
printf "%s" "${SERVER_ALIASES:-}" | tr ',' ' ' | xargs
|
|
}
|
|
|
|
validate_server_names() {
|
|
local alias_name
|
|
if [[ -z "${SERVER_NAME:-}" ]]; then
|
|
echo "[server-provision] SERVER_NAME 不能为空。" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "${SERVER_NAME}" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]]; then
|
|
echo "[server-provision] SERVER_NAME 只能填写单个域名或 IP,不能包含空格、路径或协议: ${SERVER_NAME}" >&2
|
|
exit 1
|
|
fi
|
|
for alias_name in $(normalize_server_aliases); do
|
|
if [[ ! "${alias_name}" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]]; then
|
|
echo "[server-provision] SERVER_ALIASES 只能填写域名或 IP,多个用空格或逗号分隔: ${alias_name}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
}
|
|
|
|
run_cmd() {
|
|
echo "+ $*"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
require_root_for_real_provision() {
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
return
|
|
fi
|
|
|
|
if [[ "$(id -u)" != "0" ]]; then
|
|
echo "[server-provision] 非 dry-run 会安装系统包、写入 systemd/Nginx 和创建系统用户,必须在 root agent 上执行。" >&2
|
|
echo "[server-provision] 当前用户: $(id -un) uid=$(id -u)。请确认 DEPLOY_TARGET=${DEPLOY_TARGET:-} 对应的目标服务器 agent 以 root 运行,或保持 DRY_RUN=true。" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
install_file() {
|
|
local source="$1"
|
|
local target="$2"
|
|
local mode="$3"
|
|
echo "+ install -m ${mode} ${source} ${target}"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
install -m "${mode}" "${source}" "${target}"
|
|
fi
|
|
}
|
|
|
|
install_nginx_brotli_modules() {
|
|
echo "[server-provision] 安装 Nginx Brotli 动态模块依赖"
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
run_cmd apt-get install -y libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
|
|
else
|
|
echo "[server-provision] 当前系统未使用 apt,无法自动安装 Nginx Brotli 动态模块;将继续通过 nginx -t 能力探测决定是否启用 Brotli。"
|
|
fi
|
|
}
|
|
|
|
download_file() {
|
|
local url="$1"
|
|
local output="$2"
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -fsSL --retry 3 --retry-delay 2 "${url}" -o "${output}"
|
|
elif command -v wget >/dev/null 2>&1; then
|
|
wget -O "${output}" "${url}"
|
|
else
|
|
echo "[server-provision] 需要 curl 或 wget 下载: ${url}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
openssl_lib_dir_candidates() {
|
|
printf "%s\n" \
|
|
"${GENARRATIVE_OPENSSL_PREFIX}/lib64" \
|
|
"${GENARRATIVE_OPENSSL_PREFIX}/lib"
|
|
}
|
|
|
|
find_genarrative_openssl_lib_dir() {
|
|
local lib_dir
|
|
while IFS= read -r lib_dir; do
|
|
if [[ -f "${lib_dir}/libssl.so.3" && -f "${lib_dir}/libcrypto.so.3" ]]; then
|
|
printf "%s" "${lib_dir}"
|
|
return 0
|
|
fi
|
|
done < <(openssl_lib_dir_candidates)
|
|
return 1
|
|
}
|
|
|
|
genarrative_openssl_has_required_symbol() {
|
|
local lib_dir
|
|
lib_dir="$(find_genarrative_openssl_lib_dir 2>/dev/null || true)"
|
|
if [[ -z "${lib_dir}" ]]; then
|
|
return 1
|
|
fi
|
|
grep -a -q "OPENSSL_${GENARRATIVE_OPENSSL_VERSION}" "${lib_dir}/libssl.so.3"
|
|
}
|
|
|
|
verify_genarrative_openssl_install() {
|
|
local lib_dir
|
|
lib_dir="$(find_genarrative_openssl_lib_dir 2>/dev/null || true)"
|
|
if [[ -z "${lib_dir}" ]]; then
|
|
echo "[server-provision] OpenSSL ${GENARRATIVE_OPENSSL_VERSION} 安装后缺少 libssl.so.3/libcrypto.so.3: ${GENARRATIVE_OPENSSL_PREFIX}" >&2
|
|
exit 1
|
|
fi
|
|
if ! grep -a -q "OPENSSL_${GENARRATIVE_OPENSSL_VERSION}" "${lib_dir}/libssl.so.3"; then
|
|
echo "[server-provision] OpenSSL 动态库缺少 OPENSSL_${GENARRATIVE_OPENSSL_VERSION} 符号: ${lib_dir}/libssl.so.3" >&2
|
|
exit 1
|
|
fi
|
|
if ! env "LD_LIBRARY_PATH=${lib_dir}" "${GENARRATIVE_OPENSSL_PREFIX}/bin/openssl" version | grep -q "OpenSSL ${GENARRATIVE_OPENSSL_VERSION}"; then
|
|
echo "[server-provision] OpenSSL ${GENARRATIVE_OPENSSL_VERSION} 安装后命令验证失败: ${GENARRATIVE_OPENSSL_PREFIX}/bin/openssl" >&2
|
|
exit 1
|
|
fi
|
|
echo "[server-provision] OpenSSL ${GENARRATIVE_OPENSSL_VERSION} 已就绪: ${lib_dir}"
|
|
}
|
|
|
|
install_genarrative_openssl_runtime() {
|
|
local tmp_dir archive source_dir jobs lib_dir
|
|
|
|
echo "[server-provision] 检查 api-server/libcurl 运行时 OpenSSL ${GENARRATIVE_OPENSSL_VERSION}"
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ install OpenSSL ${GENARRATIVE_OPENSSL_VERSION} into ${GENARRATIVE_OPENSSL_PREFIX}"
|
|
echo "+ verify OPENSSL_${GENARRATIVE_OPENSSL_VERSION} symbol for api-server/libcurl"
|
|
return
|
|
fi
|
|
|
|
if genarrative_openssl_has_required_symbol; then
|
|
verify_genarrative_openssl_install
|
|
return
|
|
fi
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
run_cmd apt-get install -y build-essential ca-certificates curl perl tar
|
|
else
|
|
echo "[server-provision] 当前系统未使用 apt,无法自动构建 OpenSSL ${GENARRATIVE_OPENSSL_VERSION};请手动安装到 ${GENARRATIVE_OPENSSL_PREFIX}。" >&2
|
|
exit 1
|
|
fi
|
|
require_cmd sha256sum
|
|
require_cmd tar
|
|
|
|
tmp_dir="$(mktemp -d)"
|
|
archive="${tmp_dir}/openssl-${GENARRATIVE_OPENSSL_VERSION}.tar.gz"
|
|
echo "[server-provision] 下载 OpenSSL ${GENARRATIVE_OPENSSL_VERSION}: ${GENARRATIVE_OPENSSL_SOURCE_URL}"
|
|
download_file "${GENARRATIVE_OPENSSL_SOURCE_URL}" "${archive}"
|
|
printf "%s %s\n" "${GENARRATIVE_OPENSSL_SOURCE_SHA256}" "${archive}" | sha256sum -c -
|
|
|
|
tar -xzf "${archive}" -C "${tmp_dir}"
|
|
source_dir="${tmp_dir}/openssl-${GENARRATIVE_OPENSSL_VERSION}"
|
|
jobs="$(nproc 2>/dev/null || echo 2)"
|
|
(
|
|
cd "${source_dir}"
|
|
./config --prefix="${GENARRATIVE_OPENSSL_PREFIX}" --openssldir="${GENARRATIVE_OPENSSL_PREFIX}/ssl" shared
|
|
make -j "${jobs}"
|
|
make install_sw
|
|
)
|
|
rm -rf "${tmp_dir}"
|
|
|
|
lib_dir="$(find_genarrative_openssl_lib_dir 2>/dev/null || true)"
|
|
if [[ -n "${lib_dir}" ]]; then
|
|
chmod 0755 "${GENARRATIVE_OPENSSL_PREFIX}" "${lib_dir}" || true
|
|
chmod 0644 "${lib_dir}/libssl.so.3" "${lib_dir}/libcrypto.so.3" || true
|
|
fi
|
|
verify_genarrative_openssl_install
|
|
}
|
|
|
|
sync_otelcol_install() {
|
|
local target_bin="/usr/local/bin/otelcol-contrib"
|
|
local source_bin="${OTELCOL_BIN_SOURCE}"
|
|
local version="${OTELCOL_VERSION:-0.151.0}"
|
|
local resolved_source="${source_bin}"
|
|
|
|
if [[ "${ENABLE_OTELCOL:-true}" != "true" ]]; then
|
|
echo "[server-provision] ENABLE_OTELCOL=${ENABLE_OTELCOL:-},跳过 otelcol-contrib 配置。"
|
|
return
|
|
fi
|
|
|
|
if command -v readlink >/dev/null 2>&1; then
|
|
resolved_source="$(readlink -f "${source_bin}" 2>/dev/null || echo "${source_bin}")"
|
|
fi
|
|
|
|
if [[ ! -x "${resolved_source}" ]]; then
|
|
echo "[server-provision] otelcol-contrib 不存在或不可执行: ${source_bin}" >&2
|
|
echo "[server-provision] 请确认 Prepare Provision Tools 已在目标 agent 生成 otelcol-contrib ${version}: ${source_bin}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ install -m 0755 ${resolved_source} ${target_bin}"
|
|
return
|
|
fi
|
|
|
|
install -m 0755 "${resolved_source}" "${target_bin}"
|
|
if ! "${target_bin}" --version >/dev/null 2>&1; then
|
|
echo "[server-provision] otelcol-contrib 安装后无法执行: ${target_bin}" >&2
|
|
exit 1
|
|
fi
|
|
if ! "${target_bin}" --version 2>/dev/null | grep -q "${version}"; then
|
|
echo "[server-provision] 警告: otelcol-contrib 版本不是期望的 ${version}: $("${target_bin}" --version 2>/dev/null || true)" >&2
|
|
fi
|
|
}
|
|
|
|
ensure_otelcol_runtime() {
|
|
if [[ "${ENABLE_OTELCOL:-true}" != "true" ]]; then
|
|
return
|
|
fi
|
|
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ ensure system user/group otelcol"
|
|
echo "+ install -d -m 0755 -o otelcol -g otelcol /var/lib/otelcol"
|
|
echo "+ install -d -m 0755 -o root -g root /etc/otelcol"
|
|
echo "+ install -d -m 0755 -o genarrative -g genarrative /var/log/genarrative"
|
|
echo "+ install -m 0644 deploy/otelcol/genarrative-debug.yaml /etc/otelcol/genarrative-debug.yaml"
|
|
return
|
|
fi
|
|
|
|
if ! getent group otelcol >/dev/null 2>&1; then
|
|
groupadd --system otelcol
|
|
fi
|
|
if ! id otelcol >/dev/null 2>&1; then
|
|
useradd --system --gid otelcol --home-dir /var/lib/otelcol --shell /usr/sbin/nologin otelcol
|
|
fi
|
|
|
|
install -d -m 0755 -o otelcol -g otelcol /var/lib/otelcol
|
|
install -d -m 0755 -o root -g root /etc/otelcol
|
|
install -d -m 0755 -o genarrative -g genarrative /var/log/genarrative
|
|
install -m 0644 deploy/otelcol/genarrative-debug.yaml /etc/otelcol/genarrative-debug.yaml
|
|
chown root:root /etc/otelcol/genarrative-debug.yaml
|
|
}
|
|
|
|
stamp_database_backup_timer_now() {
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ install -d -m 0755 /var/lib/systemd/timers"
|
|
echo "+ touch /var/lib/systemd/timers/stamp-genarrative-database-backup.timer"
|
|
return
|
|
fi
|
|
|
|
install -d -m 0755 /var/lib/systemd/timers
|
|
# 避免 provision 在当天 03:20 之后启动 timer 时因 Persistent=true 立刻补跑冷备份、
|
|
# 进而在初始化/发布窗口中意外停止 spacetimedb.service。
|
|
touch /var/lib/systemd/timers/stamp-genarrative-database-backup.timer
|
|
}
|
|
|
|
sync_spacetime_install() {
|
|
local root_dir="$1"
|
|
local target_bin_dir="${root_dir}/bin/current"
|
|
local target_cli="${target_bin_dir}/spacetimedb-cli"
|
|
local target_standalone="${target_bin_dir}/spacetimedb-standalone"
|
|
local resolved_command="${SPACETIME_BIN_SOURCE}"
|
|
local install_dir=""
|
|
local root_bin="${root_dir}/bin"
|
|
|
|
echo "[server-provision] 同步 SpacetimeDB current 目录到 ${target_bin_dir}"
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ mkdir -p ${target_bin_dir}"
|
|
echo "+ copy spacetimedb-cli and spacetimedb-standalone into ${target_bin_dir}"
|
|
return
|
|
fi
|
|
|
|
if command -v readlink >/dev/null 2>&1; then
|
|
resolved_command="$(readlink -f "${SPACETIME_BIN_SOURCE}" 2>/dev/null || echo "${SPACETIME_BIN_SOURCE}")"
|
|
fi
|
|
install_dir="$(cd -- "$(dirname -- "${resolved_command}")" && pwd)"
|
|
mkdir -p "${root_bin}"
|
|
|
|
if [[ -d "${install_dir}/bin" ]]; then
|
|
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir}/bin -> ${root_bin}"
|
|
rm -rf "${root_bin}"
|
|
mkdir -p "${root_bin}"
|
|
cp -a "${install_dir}/bin/." "${root_bin}/"
|
|
elif [[ -x "${install_dir}/spacetimedb-cli" && -x "${install_dir}/spacetimedb-standalone" ]]; then
|
|
echo "[server-provision] 同步 SpacetimeDB 安装: ${install_dir} -> ${target_bin_dir}"
|
|
rm -rf "${target_bin_dir}"
|
|
mkdir -p "${target_bin_dir}"
|
|
cp -f "${install_dir}/spacetimedb-cli" "${target_cli}"
|
|
cp -f "${install_dir}/spacetimedb-standalone" "${target_standalone}"
|
|
chmod +x "${target_cli}" "${target_standalone}"
|
|
else
|
|
echo "[server-provision] 未能从 SpacetimeDB 交付包推断完整安装目录: ${resolved_command}" >&2
|
|
fi
|
|
|
|
if [[ ! -x "${target_cli}" || ! -x "${target_standalone}" ]]; then
|
|
echo "[server-provision] 同步 SpacetimeDB 安装后仍缺少 current 目录。" >&2
|
|
echo "[server-provision] 需要同时存在: ${target_cli} 与 ${target_standalone}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
chown -R spacetimedb:spacetimedb "${root_bin}"
|
|
}
|
|
|
|
is_spacetimedb_ready() {
|
|
local server_url="http://127.0.0.1:3101"
|
|
|
|
if command -v curl >/dev/null 2>&1 && curl -fsS "${server_url}/v1/ping" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
wait_for_spacetimedb_service() {
|
|
local deadline=$((SECONDS + 60))
|
|
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ wait for spacetimedb.service on http://127.0.0.1:3101"
|
|
return
|
|
fi
|
|
|
|
while ((SECONDS < deadline)); do
|
|
if is_spacetimedb_ready; then
|
|
echo "[server-provision] spacetimedb.service 已就绪: http://127.0.0.1:3101"
|
|
return
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
echo "[server-provision] 等待 spacetimedb.service 就绪超时。" >&2
|
|
systemctl status spacetimedb.service --no-pager -l >&2 || true
|
|
journalctl -u spacetimedb.service --no-pager -n 80 >&2 || true
|
|
ss -ltnp >&2 || true
|
|
exit 1
|
|
}
|
|
|
|
read_env_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local line value quote_char
|
|
quote_char='"'
|
|
|
|
if [[ ! -f "${file}" ]]; then
|
|
return
|
|
fi
|
|
|
|
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
if [[ "${line}" == "${key}="* ]]; then
|
|
value="${line#*=}"
|
|
value="$(printf "%s" "${value}" | tr -d "\r")"
|
|
if [[ ${#value} -ge 2 && "${value:0:1}" == "${quote_char}" && "${value: -1}" == "${quote_char}" ]]; then
|
|
value="${value:1:${#value}-2}"
|
|
fi
|
|
printf "%s" "${value}"
|
|
return
|
|
fi
|
|
done <"${file}"
|
|
}
|
|
|
|
write_env_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local value="$3"
|
|
local tmp updated line
|
|
|
|
tmp="$(mktemp)"
|
|
updated="false"
|
|
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
if [[ "${line}" == "${key}="* ]]; then
|
|
if [[ "${updated}" != "true" ]]; then
|
|
printf "%s=%s\\n" "${key}" "${value}" >>"${tmp}"
|
|
updated="true"
|
|
fi
|
|
else
|
|
printf "%s\\n" "${line}" >>"${tmp}"
|
|
fi
|
|
done <"${file}"
|
|
if [[ "${updated}" != "true" ]]; then
|
|
printf "%s=%s\\n" "${key}" "${value}" >>"${tmp}"
|
|
fi
|
|
|
|
cat "${tmp}" >"${file}"
|
|
rm -f "${tmp}"
|
|
chmod 0600 "${file}"
|
|
chown root:root "${file}"
|
|
}
|
|
|
|
ensure_env_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local default_value="$3"
|
|
local current_value
|
|
|
|
current_value="$(read_env_value "${file}" "${key}")"
|
|
if [[ -n "${current_value}" ]]; then
|
|
return
|
|
fi
|
|
|
|
echo "[server-provision] 补齐运行态环境变量: ${key} -> ${file}"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
write_env_value "${file}" "${key}" "${default_value}"
|
|
fi
|
|
}
|
|
|
|
ensure_env_value_migrates_old_default() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local old_default="$3"
|
|
local new_default="$4"
|
|
local current_value
|
|
|
|
current_value="$(read_env_value "${file}" "${key}")"
|
|
if [[ -z "${current_value}" ]]; then
|
|
ensure_env_value "${file}" "${key}" "${new_default}"
|
|
return
|
|
fi
|
|
if [[ "${current_value}" != "${old_default}" ]]; then
|
|
return
|
|
fi
|
|
|
|
echo "[server-provision] 迁移运行态环境变量旧默认值: ${key} ${old_default} -> ${new_default} (${file})"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
write_env_value "${file}" "${key}" "${new_default}"
|
|
fi
|
|
}
|
|
|
|
ensure_runtime_bootstrap_secret_file_env() {
|
|
local file="$1"
|
|
local key="GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET_FILE"
|
|
local direct_key="GENARRATIVE_SPACETIME_RUNTIME_SERVICE_BOOTSTRAP_SECRET"
|
|
local canonical_path="/var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt"
|
|
local current_value direct_value
|
|
|
|
direct_value="$(read_env_value "${file}" "${direct_key}")"
|
|
if [[ -n "${direct_value}" ]]; then
|
|
unset direct_value
|
|
echo "[server-provision] ${file} 不得保存 ${direct_key} 明文;生产环境只允许使用 ${key}。" >&2
|
|
exit 1
|
|
fi
|
|
unset direct_value
|
|
|
|
current_value="$(read_env_value "${file}" "${key}")"
|
|
if [[ -z "${current_value}" ]]; then
|
|
ensure_env_value "${file}" "${key}" "${canonical_path}"
|
|
return
|
|
fi
|
|
if [[ "${current_value}" != "${canonical_path}" ]]; then
|
|
echo "[server-provision] ${key} 必须使用 Stdb publish 同步写入的固定路径 ${canonical_path}: ${file}" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
ensure_api_runtime_env_defaults() {
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ ensure api-server runtime env defaults in ${API_ENV_FILE}"
|
|
return
|
|
fi
|
|
if [[ ! -f "${API_ENV_FILE}" ]]; then
|
|
echo "[server-provision] 环境文件不存在,无法补齐 api-server 运行态目录变量: ${API_ENV_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 已存在的生产 env 会被保留,不会整文件覆盖;这里仅补后续版本新增的运行态写入路径。
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_ENABLED" "true"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_DIR" "/var/lib/genarrative/tracking-outbox"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_BATCH_SIZE" "500"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_FLUSH_INTERVAL_MS" "1000"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_TRACKING_OUTBOX_MAX_BYTES" "268435456"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_WALLET_REFUND_OUTBOX_ENABLED" "true"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_WALLET_REFUND_OUTBOX_DIR" "/var/lib/genarrative/wallet-refund-outbox"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_WALLET_REFUND_OUTBOX_BATCH_SIZE" "100"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_WALLET_REFUND_OUTBOX_FLUSH_INTERVAL_MS" "1000"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_WALLET_REFUND_OUTBOX_MAX_BYTES" "67108864"
|
|
ensure_env_value_migrates_old_default "${API_ENV_FILE}" "GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS" "3600" "600"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS" "900"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS" "1800"
|
|
ensure_runtime_bootstrap_secret_file_env "${API_ENV_FILE}"
|
|
ensure_env_value_migrates_old_default "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS" "45000" "180000"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_FAILURE_THRESHOLD" "3"
|
|
ensure_env_value "${API_ENV_FILE}" "GENARRATIVE_EDITOR_BGFILTER_CIRCUIT_COOLDOWN_SECONDS" "300"
|
|
}
|
|
|
|
ensure_worker_runtime_env_defaults() {
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ ensure external-generation worker runtime env defaults in ${WORKER_ENV_FILE}"
|
|
return
|
|
fi
|
|
if [[ ! -f "${WORKER_ENV_FILE}" ]]; then
|
|
echo "[server-provision] worker 环境文件不存在,无法补齐运行态变量: ${WORKER_ENV_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
ensure_env_value_migrates_old_default "${WORKER_ENV_FILE}" "GENARRATIVE_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS" "3600" "600"
|
|
ensure_env_value "${WORKER_ENV_FILE}" "GENARRATIVE_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS" "900"
|
|
ensure_env_value "${WORKER_ENV_FILE}" "GENARRATIVE_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS" "1800"
|
|
ensure_runtime_bootstrap_secret_file_env "${WORKER_ENV_FILE}"
|
|
}
|
|
|
|
parse_json_string_field() {
|
|
local json="$1"
|
|
local key="$2"
|
|
|
|
printf "%s" "${json}" | sed -n "s/.*\\\"${key}\\\"[[:space:]]*:[[:space:]]*\\\"\\([^\\\"]*\\)\\\".*/\\1/p" | head -n 1
|
|
}
|
|
|
|
ensure_spacetime_owner_client_token() {
|
|
local server_url="http://127.0.0.1:3101"
|
|
local cli_path="${SPACETIME_ROOT}/bin/current/spacetimedb-cli"
|
|
local token identity response login_output existing_token identity_preview
|
|
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ ensure GENARRATIVE_SPACETIME_TOKEN in ${API_ENV_FILE}"
|
|
echo "+ generate SpacetimeDB client identity when token is missing"
|
|
echo "+ runuser -u spacetimedb -- ${cli_path} --root-dir ${SPACETIME_ROOT} login --token [REDACTED]"
|
|
return
|
|
fi
|
|
|
|
if [[ ! -f "${API_ENV_FILE}" ]]; then
|
|
echo "[server-provision] 环境文件不存在,无法写入 GENARRATIVE_SPACETIME_TOKEN: ${API_ENV_FILE}" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -x "${cli_path}" ]]; then
|
|
echo "[server-provision] SpacetimeDB CLI 不存在或不可执行: ${cli_path}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
existing_token="$(read_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_TOKEN")"
|
|
if [[ -n "${existing_token}" ]]; then
|
|
token="${existing_token}"
|
|
echo "[server-provision] GENARRATIVE_SPACETIME_TOKEN 已存在,保留并同步 SpacetimeDB CLI 登录态。"
|
|
else
|
|
response="$(curl -fsS -X POST "${server_url}/v1/identity")"
|
|
identity="$(parse_json_string_field "${response}" "identity")"
|
|
identity="${identity:-$(parse_json_string_field "${response}" "Identity")}"
|
|
identity="${identity:-$(parse_json_string_field "${response}" "identity_hex")}"
|
|
identity="${identity:-$(parse_json_string_field "${response}" "identityHex")}"
|
|
token="$(parse_json_string_field "${response}" "token")"
|
|
token="${token:-$(parse_json_string_field "${response}" "Token")}"
|
|
if [[ -z "${identity}" || -z "${token}" ]]; then
|
|
echo "[server-provision] 生成 SpacetimeDB client identity 失败,响应缺少 identity/token。" >&2
|
|
exit 1
|
|
fi
|
|
|
|
write_env_value "${API_ENV_FILE}" "GENARRATIVE_SPACETIME_TOKEN" "${token}"
|
|
identity_preview="${identity:0:12}"
|
|
echo "[server-provision] 已生成 SpacetimeDB client identity 并写入 GENARRATIVE_SPACETIME_TOKEN: ${identity_preview}..."
|
|
fi
|
|
|
|
# 中文注释:这里是 provision 内部为 spacetimedb 运行用户隔离 CLI 登录态的受控用法,不作为人工 spacetime 命令示例。
|
|
if ! login_output="$(runuser -u spacetimedb -- "${cli_path}" --root-dir "${SPACETIME_ROOT}" login --token "${token}" 2>&1)"; then
|
|
echo "[server-provision] 使用 GENARRATIVE_SPACETIME_TOKEN 登录 SpacetimeDB CLI 失败。" >&2
|
|
printf "%s\\n" "${login_output}" | sed -E "s/[A-Za-z0-9_.=-]{24,}/[REDACTED]/g" >&2
|
|
exit 1
|
|
fi
|
|
echo "[server-provision] 已同步 SpacetimeDB CLI 登录态;后续首次 publish 将使用同一 client identity。"
|
|
}
|
|
|
|
render_nginx_brotli_directives() {
|
|
if ! command -v nginx >/dev/null 2>&1; then
|
|
echo " # Brotli 未启用:目标服务器未找到 nginx 命令。"
|
|
return
|
|
fi
|
|
|
|
local brotli_snippet
|
|
brotli_snippet="$(mktemp)"
|
|
cat >"${brotli_snippet}" <<'EOF'
|
|
include /etc/nginx/modules-enabled/*.conf;
|
|
events {}
|
|
http {
|
|
brotli on;
|
|
brotli_comp_level 4;
|
|
brotli_min_length 1024;
|
|
brotli_types application/json;
|
|
}
|
|
EOF
|
|
if nginx -t -c "${brotli_snippet}" >/dev/null 2>&1; then
|
|
cat <<'EOF'
|
|
brotli on;
|
|
brotli_comp_level 4;
|
|
brotli_min_length 1024;
|
|
brotli_types
|
|
text/plain
|
|
text/css
|
|
text/javascript
|
|
application/javascript
|
|
application/json
|
|
application/xml
|
|
application/xml+rss
|
|
image/svg+xml;
|
|
EOF
|
|
else
|
|
echo " # Brotli 未启用:nginx -t 不接受 brotli 指令。"
|
|
fi
|
|
rm -f "${brotli_snippet}"
|
|
}
|
|
|
|
render_nginx_template() {
|
|
local template="$1"
|
|
local rendered_brotli server_names
|
|
rendered_brotli="$(render_nginx_brotli_directives)"
|
|
server_names="${SERVER_NAME}"
|
|
if [[ -n "${SERVER_ALIASES:-}" ]]; then
|
|
server_names="${server_names} $(normalize_server_aliases)"
|
|
fi
|
|
sed \
|
|
-e "s/server_name genarrative.example.com;/server_name ${server_names};/g" \
|
|
-e "s|/etc/letsencrypt/live/genarrative.example.com/|/etc/letsencrypt/live/${SERVER_NAME}/|g" \
|
|
-e "/# __GENARRATIVE_BROTLI_DIRECTIVES__/r /dev/stdin" \
|
|
-e "/# __GENARRATIVE_BROTLI_DIRECTIVES__/d" \
|
|
"${template}" <<<"${rendered_brotli}"
|
|
}
|
|
|
|
render_nginx_https_config() {
|
|
render_nginx_template deploy/nginx/genarrative.conf
|
|
}
|
|
|
|
render_nginx_development_http_config() {
|
|
render_nginx_template deploy/nginx/genarrative-dev-http.conf
|
|
}
|
|
|
|
render_api_env_example() {
|
|
sed \
|
|
-e "s|^GENARRATIVE_API_PORT=.*|GENARRATIVE_API_PORT=${API_PORT}|" \
|
|
-e "s|^GENARRATIVE_SPACETIME_SERVER_URL=.*|GENARRATIVE_SPACETIME_SERVER_URL=http://127.0.0.1:3101|" \
|
|
deploy/env/api-server.env.example
|
|
}
|
|
|
|
render_external_generation_worker_env_example() {
|
|
cat deploy/env/external-generation-worker.env.example
|
|
}
|
|
|
|
render_external_generation_controller_env_example() {
|
|
cat deploy/env/external-generation-controller.env.example
|
|
}
|
|
|
|
render_otelcol_service() {
|
|
cat deploy/systemd/otelcol-contrib.service
|
|
}
|
|
|
|
validate_nginx_tls() {
|
|
local cert_dir="/etc/letsencrypt/live/${SERVER_NAME}"
|
|
if [[ "${SERVER_NAME}" == "genarrative.example.com" ]]; then
|
|
echo "[server-provision] SERVER_NAME 仍是占位域名,拒绝写入 Nginx HTTPS 配置。请填写真实域名,或先设置 NGINX_CONFIG_MODE=none。" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "${cert_dir}/fullchain.pem" || ! -f "${cert_dir}/privkey.pem" ]]; then
|
|
echo "[server-provision] 未找到 Nginx HTTPS 证书: ${cert_dir}/fullchain.pem 或 ${cert_dir}/privkey.pem" >&2
|
|
echo "[server-provision] 请先完成证书申请,或首次初始化时设置 NGINX_CONFIG_MODE=none,避免写入无法通过 nginx -t 的配置。" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
disable_nginx_default_sites_enabled() {
|
|
local moves_file="$1"
|
|
local sites_enabled="/etc/nginx/sites-enabled"
|
|
local sites_disabled="/etc/nginx/sites-disabled"
|
|
local stamp source target base
|
|
local candidates=("${sites_enabled}/default" "${sites_enabled}/default."*)
|
|
|
|
stamp="$(date +%Y%m%d%H%M%S)"
|
|
for source in "${candidates[@]}"; do
|
|
if [[ ! -e "${source}" && ! -L "${source}" ]]; then
|
|
continue
|
|
fi
|
|
|
|
base="$(basename "${source}")"
|
|
target="${sites_disabled}/${base}.disabled-${stamp}"
|
|
echo "[server-provision] 禁用 Debian 默认 Nginx 站点,避免与 Genarrative server_name 冲突: ${source} -> ${target}"
|
|
mkdir -p "${sites_disabled}"
|
|
mv "${source}" "${target}"
|
|
printf "%s\t%s\n" "${target}" "${source}" >>"${moves_file}"
|
|
done
|
|
}
|
|
|
|
restore_nginx_default_sites_enabled() {
|
|
local moves_file="$1"
|
|
local target source
|
|
|
|
if [[ ! -f "${moves_file}" ]]; then
|
|
return
|
|
fi
|
|
|
|
while IFS=$'\t' read -r target source || [[ -n "${target:-}" ]]; do
|
|
if [[ -z "${target:-}" || -z "${source:-}" ]]; then
|
|
continue
|
|
fi
|
|
if [[ -e "${target}" || -L "${target}" ]]; then
|
|
mkdir -p "$(dirname "${source}")"
|
|
if [[ ! -e "${source}" && ! -L "${source}" ]]; then
|
|
echo "[server-provision] 恢复 Debian 默认 Nginx 站点: ${target} -> ${source}"
|
|
mv "${target}" "${source}"
|
|
fi
|
|
fi
|
|
done <"${moves_file}"
|
|
}
|
|
|
|
install_nginx_config_with_rollback() {
|
|
local config_target="/etc/nginx/conf.d/genarrative.conf"
|
|
local snippet_target="/etc/nginx/snippets/genarrative-maintenance.conf"
|
|
local pingora_canary_snippet_target="/etc/nginx/snippets/genarrative-pingora-canary.conf"
|
|
local pingora_realpath_canary_snippet_target="/etc/nginx/snippets/genarrative-pingora-realpath-canary.conf"
|
|
local config_source
|
|
local rendered_config rendered_snippet rendered_pingora_canary_snippet rendered_pingora_realpath_canary_snippet
|
|
local config_backup snippet_backup pingora_canary_snippet_backup pingora_realpath_canary_snippet_backup disabled_sites
|
|
local had_config="false"
|
|
local had_snippet="false"
|
|
local had_pingora_canary_snippet="false"
|
|
local had_pingora_realpath_canary_snippet="false"
|
|
|
|
run_cmd mkdir -p /etc/nginx/snippets /etc/nginx/conf.d
|
|
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
|
|
config_source="deploy/nginx/genarrative.conf"
|
|
elif [[ "${NGINX_CONFIG_MODE}" == "development-http" ]]; then
|
|
config_source="deploy/nginx/genarrative-dev-http.conf"
|
|
else
|
|
echo "[server-provision] NGINX_CONFIG_MODE=${NGINX_CONFIG_MODE} 不需要安装 Nginx 配置。"
|
|
return
|
|
fi
|
|
|
|
echo "+ render ${config_source} -> ${config_target}"
|
|
echo "+ install -m 0644 deploy/nginx/snippets/genarrative-maintenance.conf ${snippet_target}"
|
|
echo "+ install -m 0644 deploy/nginx/snippets/genarrative-pingora-canary.conf ${pingora_canary_snippet_target}"
|
|
echo "+ install -m 0644 deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf ${pingora_realpath_canary_snippet_target}"
|
|
|
|
if [[ "${DRY_RUN}" == "true" ]]; then
|
|
echo "+ disable /etc/nginx/sites-enabled/default* if present"
|
|
echo "+ nginx -t"
|
|
echo "+ nginx -s reload"
|
|
return
|
|
fi
|
|
|
|
rendered_config="$(mktemp)"
|
|
rendered_snippet="$(mktemp)"
|
|
rendered_pingora_canary_snippet="$(mktemp)"
|
|
rendered_pingora_realpath_canary_snippet="$(mktemp)"
|
|
config_backup="$(mktemp)"
|
|
snippet_backup="$(mktemp)"
|
|
pingora_canary_snippet_backup="$(mktemp)"
|
|
pingora_realpath_canary_snippet_backup="$(mktemp)"
|
|
disabled_sites="$(mktemp)"
|
|
if [[ "${NGINX_CONFIG_MODE}" == "production-https" ]]; then
|
|
validate_nginx_tls
|
|
render_nginx_https_config >"${rendered_config}"
|
|
else
|
|
render_nginx_development_http_config >"${rendered_config}"
|
|
fi
|
|
cp deploy/nginx/snippets/genarrative-maintenance.conf "${rendered_snippet}"
|
|
cp deploy/nginx/snippets/genarrative-pingora-canary.conf "${rendered_pingora_canary_snippet}"
|
|
cp deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf "${rendered_pingora_realpath_canary_snippet}"
|
|
|
|
if [[ -f "${config_target}" ]]; then
|
|
cp -p "${config_target}" "${config_backup}"
|
|
had_config="true"
|
|
fi
|
|
if [[ -f "${snippet_target}" ]]; then
|
|
cp -p "${snippet_target}" "${snippet_backup}"
|
|
had_snippet="true"
|
|
fi
|
|
if [[ -f "${pingora_canary_snippet_target}" ]]; then
|
|
cp -p "${pingora_canary_snippet_target}" "${pingora_canary_snippet_backup}"
|
|
had_pingora_canary_snippet="true"
|
|
fi
|
|
if [[ -f "${pingora_realpath_canary_snippet_target}" ]]; then
|
|
cp -p "${pingora_realpath_canary_snippet_target}" "${pingora_realpath_canary_snippet_backup}"
|
|
had_pingora_realpath_canary_snippet="true"
|
|
fi
|
|
|
|
install -m 0644 "${rendered_config}" "${config_target}"
|
|
install -m 0644 "${rendered_snippet}" "${snippet_target}"
|
|
install -m 0644 "${rendered_pingora_canary_snippet}" "${pingora_canary_snippet_target}"
|
|
install -m 0644 "${rendered_pingora_realpath_canary_snippet}" "${pingora_realpath_canary_snippet_target}"
|
|
disable_nginx_default_sites_enabled "${disabled_sites}"
|
|
|
|
if ! nginx -t; then
|
|
echo "[server-provision] nginx -t 失败,恢复写入前的 Nginx 配置。" >&2
|
|
if [[ "${had_config}" == "true" ]]; then
|
|
cp -p "${config_backup}" "${config_target}"
|
|
else
|
|
rm -f "${config_target}"
|
|
fi
|
|
if [[ "${had_snippet}" == "true" ]]; then
|
|
cp -p "${snippet_backup}" "${snippet_target}"
|
|
else
|
|
rm -f "${snippet_target}"
|
|
fi
|
|
if [[ "${had_pingora_canary_snippet}" == "true" ]]; then
|
|
cp -p "${pingora_canary_snippet_backup}" "${pingora_canary_snippet_target}"
|
|
else
|
|
rm -f "${pingora_canary_snippet_target}"
|
|
fi
|
|
if [[ "${had_pingora_realpath_canary_snippet}" == "true" ]]; then
|
|
cp -p "${pingora_realpath_canary_snippet_backup}" "${pingora_realpath_canary_snippet_target}"
|
|
else
|
|
rm -f "${pingora_realpath_canary_snippet_target}"
|
|
fi
|
|
restore_nginx_default_sites_enabled "${disabled_sites}"
|
|
rm -f "${rendered_config}" "${rendered_snippet}" "${rendered_pingora_canary_snippet}" "${rendered_pingora_realpath_canary_snippet}" "${config_backup}" "${snippet_backup}" "${pingora_canary_snippet_backup}" "${pingora_realpath_canary_snippet_backup}" "${disabled_sites}"
|
|
exit 1
|
|
fi
|
|
echo "+ nginx -s reload"
|
|
nginx -s reload
|
|
|
|
rm -f "${rendered_config}" "${rendered_snippet}" "${rendered_pingora_canary_snippet}" "${rendered_pingora_realpath_canary_snippet}" "${config_backup}" "${snippet_backup}" "${pingora_canary_snippet_backup}" "${pingora_realpath_canary_snippet_backup}" "${disabled_sites}"
|
|
}
|
|
|
|
cleanup_placeholder_nginx_config() {
|
|
local config_target="/etc/nginx/conf.d/genarrative.conf"
|
|
local disabled_target
|
|
|
|
if [[ ! -f "${config_target}" ]]; then
|
|
return
|
|
fi
|
|
|
|
if ! grep -q "/etc/letsencrypt/live/genarrative.example.com/" "${config_target}"; then
|
|
return
|
|
fi
|
|
|
|
disabled_target="${config_target}.disabled-placeholder-$(date +%Y%m%d%H%M%S)"
|
|
echo "[server-provision] 发现上一轮初始化留下的占位域名 Nginx 配置,禁用: ${config_target} -> ${disabled_target}"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
mv "${config_target}" "${disabled_target}"
|
|
if command -v nginx >/dev/null 2>&1; then
|
|
if ! nginx -t; then
|
|
echo "[server-provision] 占位配置已禁用,但 nginx -t 仍失败;请检查其他 Nginx 配置。" >&2
|
|
else
|
|
echo "+ nginx -s reload"
|
|
nginx -s reload
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
escape_sed_replacement() {
|
|
printf "%s" "$1" | sed "s/[&|]/\\\\&/g"
|
|
}
|
|
|
|
render_spacetimedb_service() {
|
|
local root_escaped
|
|
root_escaped="$(escape_sed_replacement "${SPACETIME_ROOT}")"
|
|
sed \
|
|
-e "s|/stdb|${root_escaped}|g" \
|
|
deploy/systemd/spacetimedb.service
|
|
}
|
|
|
|
render_api_service() {
|
|
local current_escaped env_escaped
|
|
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
|
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
|
sed \
|
|
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
|
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
|
|
deploy/systemd/genarrative-api.service
|
|
}
|
|
|
|
render_external_generation_worker_service() {
|
|
local current_escaped api_env_escaped worker_env_escaped
|
|
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
|
api_env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
|
worker_env_escaped="$(escape_sed_replacement "${WORKER_ENV_FILE}")"
|
|
sed \
|
|
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
|
-e "s|/etc/genarrative/api-server.env|${api_env_escaped}|g" \
|
|
-e "s|/etc/genarrative/external-generation-worker.env|${worker_env_escaped}|g" \
|
|
deploy/systemd/genarrative-external-generation-worker@.service
|
|
}
|
|
|
|
render_external_generation_controller_service() {
|
|
local current_escaped api_env_escaped controller_env_escaped
|
|
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
|
api_env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
|
controller_env_escaped="$(escape_sed_replacement "${CONTROLLER_ENV_FILE}")"
|
|
sed \
|
|
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
|
-e "s|/etc/genarrative/api-server.env|${api_env_escaped}|g" \
|
|
-e "s|/etc/genarrative/external-generation-controller.env|${controller_env_escaped}|g" \
|
|
deploy/systemd/genarrative-external-generation-controller.service
|
|
}
|
|
|
|
render_database_backup_service() {
|
|
local current_escaped env_escaped
|
|
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
|
env_escaped="$(escape_sed_replacement "${API_ENV_FILE}")"
|
|
sed \
|
|
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
|
-e "s|/etc/genarrative/api-server.env|${env_escaped}|g" \
|
|
deploy/systemd/genarrative-database-backup.service
|
|
}
|
|
|
|
render_health_patrol_service() {
|
|
local current_escaped
|
|
current_escaped="$(escape_sed_replacement "${CURRENT_LINK}")"
|
|
sed \
|
|
-e "s|/opt/genarrative/current|${current_escaped}|g" \
|
|
deploy/systemd/genarrative-health-patrol.service
|
|
}
|
|
|
|
require_path deploy/systemd/spacetimedb.service
|
|
require_path deploy/systemd/genarrative-api.service
|
|
require_path deploy/systemd/genarrative-external-generation-worker@.service
|
|
require_path deploy/systemd/genarrative-external-generation-controller.service
|
|
require_path deploy/systemd/genarrative-database-backup.service
|
|
require_path deploy/systemd/genarrative-database-backup.timer
|
|
require_path deploy/systemd/genarrative-health-patrol.service
|
|
require_path deploy/systemd/genarrative-health-patrol.timer
|
|
require_path deploy/systemd/genarrative-pingora-gateway.service
|
|
require_path deploy/systemd/genarrative-pingora-gateway-direct-entry.conf
|
|
require_path deploy/systemd/otelcol-contrib.service
|
|
require_path deploy/otelcol/genarrative-debug.yaml
|
|
require_path deploy/nginx/genarrative.conf
|
|
require_path deploy/nginx/genarrative-dev-http.conf
|
|
require_path deploy/nginx/snippets/genarrative-maintenance.conf
|
|
require_path deploy/nginx/snippets/genarrative-pingora-canary.conf
|
|
require_path deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf
|
|
require_path deploy/logrotate/genarrative-pingora-gateway
|
|
require_path deploy/env/api-server.env.example
|
|
require_path deploy/env/external-generation-worker.env.example
|
|
require_path deploy/env/external-generation-controller.env.example
|
|
require_path scripts/deploy/maintenance-on.sh
|
|
require_path scripts/deploy/maintenance-off.sh
|
|
require_path scripts/deploy/maintenance-status.sh
|
|
|
|
validate_server_names
|
|
require_non_root_relative_path "PROVISION_TOOLS_DIR" "${PROVISION_TOOLS_DIR}"
|
|
|
|
echo "[server-provision] target=${DEPLOY_TARGET}, dry_run=${DRY_RUN}, nginx_config_mode=${NGINX_CONFIG_MODE}, source_commit=$(cat .jenkins-source-commit)"
|
|
|
|
run_cmd id
|
|
require_root_for_real_provision
|
|
install_nginx_brotli_modules
|
|
run_cmd mkdir -p "${SPACETIME_ROOT}" "${RELEASE_ROOT}" "$(dirname "${CURRENT_LINK}")" "$(dirname "${WEB_LINK}")" /etc/genarrative /etc/genarrative/pingora /var/lib/genarrative/maintenance /var/lib/genarrative/auth /var/lib/genarrative/tracking-outbox /var/lib/genarrative/wallet-refund-outbox /var/lib/genarrative/editor-generation-pricing /var/lib/genarrative/spacetime /var/lib/genarrative/database-backups /var/lib/genarrative/health-patrol /var/log/genarrative
|
|
|
|
if ! id spacetimedb >/dev/null 2>&1; then
|
|
run_cmd useradd --system --home-dir "${SPACETIME_ROOT}" --shell /usr/sbin/nologin spacetimedb
|
|
else
|
|
echo "[server-provision] 用户已存在: spacetimedb"
|
|
fi
|
|
|
|
if ! id genarrative >/dev/null 2>&1; then
|
|
run_cmd useradd --system --home-dir /opt/genarrative --shell /usr/sbin/nologin genarrative
|
|
else
|
|
echo "[server-provision] 用户已存在: genarrative"
|
|
fi
|
|
|
|
run_cmd chown -R spacetimedb:spacetimedb "${SPACETIME_ROOT}"
|
|
run_cmd chown -R genarrative:genarrative /opt/genarrative /var/lib/genarrative /srv/genarrative
|
|
run_cmd chown -R genarrative:genarrative /var/log/genarrative
|
|
if [[ -L /var/lib/genarrative/spacetime || -L /var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt ]]; then
|
|
echo "[server-provision] 运行时服务身份引导密钥路径不能是符号链接。" >&2
|
|
exit 1
|
|
fi
|
|
run_cmd install -d -o root -g genarrative -m 0750 /var/lib/genarrative/spacetime
|
|
if [[ -f /var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt ]]; then
|
|
run_cmd chown root:genarrative /var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt
|
|
run_cmd chmod 0440 /var/lib/genarrative/spacetime/runtime-service-bootstrap-secret.txt
|
|
fi
|
|
install_genarrative_openssl_runtime
|
|
|
|
if [[ ! -x "${SPACETIME_BIN_SOURCE}" ]]; then
|
|
echo "[server-provision] spacetime CLI 不存在或不可执行: ${SPACETIME_BIN_SOURCE}" >&2
|
|
exit 1
|
|
fi
|
|
echo "+ install -m 0755 ${SPACETIME_BIN_SOURCE} ${SPACETIME_ROOT}/spacetime"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
install -m 0755 "${SPACETIME_BIN_SOURCE}" "${SPACETIME_ROOT}/spacetime"
|
|
chown spacetimedb:spacetimedb "${SPACETIME_ROOT}/spacetime"
|
|
fi
|
|
sync_spacetime_install "${SPACETIME_ROOT}"
|
|
|
|
spacetimedb_service="$(mktemp)"
|
|
api_service="$(mktemp)"
|
|
external_generation_worker_service="$(mktemp)"
|
|
external_generation_controller_service="$(mktemp)"
|
|
database_backup_service="$(mktemp)"
|
|
health_patrol_service="$(mktemp)"
|
|
render_spacetimedb_service >"${spacetimedb_service}"
|
|
render_api_service >"${api_service}"
|
|
render_external_generation_worker_service >"${external_generation_worker_service}"
|
|
render_external_generation_controller_service >"${external_generation_controller_service}"
|
|
render_database_backup_service >"${database_backup_service}"
|
|
render_health_patrol_service >"${health_patrol_service}"
|
|
install_file "${spacetimedb_service}" /etc/systemd/system/spacetimedb.service 0644
|
|
install_file "${api_service}" /etc/systemd/system/genarrative-api.service 0644
|
|
install_file "${external_generation_worker_service}" /etc/systemd/system/genarrative-external-generation-worker@.service 0644
|
|
install_file "${external_generation_controller_service}" /etc/systemd/system/genarrative-external-generation-controller.service 0644
|
|
install_file "${database_backup_service}" /etc/systemd/system/genarrative-database-backup.service 0644
|
|
install_file deploy/systemd/genarrative-database-backup.timer /etc/systemd/system/genarrative-database-backup.timer 0644
|
|
install_file "${health_patrol_service}" /etc/systemd/system/genarrative-health-patrol.service 0644
|
|
install_file deploy/systemd/genarrative-health-patrol.timer /etc/systemd/system/genarrative-health-patrol.timer 0644
|
|
install_file deploy/systemd/genarrative-pingora-gateway.service /etc/systemd/system/genarrative-pingora-gateway.service 0644
|
|
install_file deploy/systemd/genarrative-pingora-gateway-direct-entry.conf /etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf 0644
|
|
install_file deploy/logrotate/genarrative-pingora-gateway /etc/logrotate.d/genarrative-pingora-gateway 0644
|
|
rm -f "${spacetimedb_service}" "${api_service}" "${external_generation_worker_service}" "${external_generation_controller_service}" "${database_backup_service}" "${health_patrol_service}"
|
|
|
|
if [[ ! -f "${API_ENV_FILE}" ]]; then
|
|
echo "+ create ${API_ENV_FILE} from example"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
render_api_env_example >"${API_ENV_FILE}"
|
|
chmod 0600 "${API_ENV_FILE}"
|
|
chown root:root "${API_ENV_FILE}"
|
|
fi
|
|
else
|
|
echo "[server-provision] 已存在环境文件,保留不覆盖: ${API_ENV_FILE}"
|
|
fi
|
|
ensure_api_runtime_env_defaults
|
|
|
|
if [[ ! -f "${WORKER_ENV_FILE}" ]]; then
|
|
echo "+ create ${WORKER_ENV_FILE} from example"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
render_external_generation_worker_env_example >"${WORKER_ENV_FILE}"
|
|
chmod 0600 "${WORKER_ENV_FILE}"
|
|
chown root:root "${WORKER_ENV_FILE}"
|
|
fi
|
|
else
|
|
echo "[server-provision] 已存在 worker 环境文件,保留不覆盖: ${WORKER_ENV_FILE}"
|
|
fi
|
|
ensure_worker_runtime_env_defaults
|
|
|
|
if [[ ! -f "${CONTROLLER_ENV_FILE}" ]]; then
|
|
echo "+ create ${CONTROLLER_ENV_FILE} from example"
|
|
if [[ "${DRY_RUN}" != "true" ]]; then
|
|
render_external_generation_controller_env_example >"${CONTROLLER_ENV_FILE}"
|
|
chmod 0600 "${CONTROLLER_ENV_FILE}"
|
|
chown root:root "${CONTROLLER_ENV_FILE}"
|
|
fi
|
|
else
|
|
echo "[server-provision] 已存在 controller 环境文件,保留不覆盖: ${CONTROLLER_ENV_FILE}"
|
|
fi
|
|
|
|
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
|
|
sync_otelcol_install
|
|
ensure_otelcol_runtime
|
|
otelcol_service="$(mktemp)"
|
|
render_otelcol_service >"${otelcol_service}"
|
|
install_file "${otelcol_service}" /etc/systemd/system/otelcol-contrib.service 0644
|
|
rm -f "${otelcol_service}"
|
|
else
|
|
echo "[server-provision] ENABLE_OTELCOL=${ENABLE_OTELCOL:-},跳过 otelcol-contrib service 安装。"
|
|
fi
|
|
|
|
if [[ "${NGINX_CONFIG_MODE}" != "none" ]]; then
|
|
install_nginx_config_with_rollback
|
|
else
|
|
cleanup_placeholder_nginx_config
|
|
fi
|
|
|
|
run_cmd systemctl daemon-reload
|
|
if [[ "${ENABLE_SERVICES}" == "true" ]]; then
|
|
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
|
|
run_cmd systemctl enable otelcol-contrib.service
|
|
fi
|
|
stamp_database_backup_timer_now
|
|
run_cmd systemctl enable spacetimedb.service genarrative-api.service genarrative-database-backup.timer genarrative-external-generation-worker@1.service genarrative-external-generation-controller.service genarrative-health-patrol.timer
|
|
run_cmd systemctl start genarrative-database-backup.timer
|
|
if [[ "${ENABLE_OTELCOL:-true}" == "true" ]]; then
|
|
run_cmd systemctl restart otelcol-contrib.service
|
|
fi
|
|
run_cmd systemctl restart spacetimedb.service
|
|
wait_for_spacetimedb_service
|
|
ensure_spacetime_owner_client_token
|
|
if [[ -x "${CURRENT_LINK}/api-server" ]]; then
|
|
run_cmd systemctl restart genarrative-api.service
|
|
run_cmd systemctl enable --now genarrative-external-generation-worker@1.service
|
|
run_cmd systemctl restart genarrative-external-generation-worker@1.service
|
|
run_cmd systemctl enable --now genarrative-external-generation-controller.service
|
|
run_cmd systemctl restart genarrative-external-generation-controller.service
|
|
else
|
|
echo "[server-provision] 尚未发现 ${CURRENT_LINK}/api-server,跳过 api-server、外部生成 worker 和 controller 首次启动。后续 API deploy 会启用并启动默认 worker 与 controller。"
|
|
fi
|
|
fi
|
|
|
|
echo "[server-provision] 完成。若是首次初始化,请补齐 ${API_ENV_FILE} 的真实密钥后再启动 api-server。"
|