feat(ops): offsite-бэкапы и проверки профилей
- deploy/backup.sh: BACKUP_OFFSITE_TARGET (rsync или rclone:remote:path) выгружает готовый архив после локальной ротации; неудача выгрузки только предупреждает. Ротация на приёмнике — опционально (BACKUP_OFFSITE_KEEP_DAYS, rclone delete --min-age). Имя архива получает суффикс, если бэкап запущен дважды в одну секунду. - deploy/install.sh: offsite-настройки в .env (BACKUP_OFFSITE_TARGET, BACKUP_OFFSITE_KEEP_DAYS) отдельным разделом; детектор незаполненных плейсхолдеров учитывает цифры в именах (FAIL2BAN_ENABLED); dry-run больше не требует прав на /opt/glchat — явность GLCHAT_ETC_DIR фиксируется до подключения common.sh, иначе ветка временного каталога была недостижима. - scripts/test-profiles.sh: сверка таблицы профилей 10.10 и одинакового набора PROFILE_* (46 проверок), make profiles-test / ops-test. - scripts/test-backup.sh: бэкап и offsite на синтетическом инстансе — локальный rsync-приёмник, шим rclone, недоступный хост, нечисловой срок (12 проверок), make backup-test. - scripts/test-install.sh: проверка offsite-ключей в .env после установки.
This commit is contained in:
+31
-5
@@ -37,6 +37,13 @@ if [ -z "$DEPLOY_DIR" ] || [ ! -f "${DEPLOY_DIR}/lib/common.sh" ]; then
|
||||
exec bash "${TMP_REPO}/deploy/install.sh" "$@"
|
||||
fi
|
||||
|
||||
# Путь установки задан явно? Спрашиваем до common.sh: он подставляет дефолт
|
||||
# /opt/glchat, после чего отличить явное значение от дефолтного нельзя.
|
||||
GLCHAT_ETC_DIR_EXPLICIT=0
|
||||
if [ -n "${GLCHAT_ETC_DIR:-}" ]; then
|
||||
GLCHAT_ETC_DIR_EXPLICIT=1
|
||||
fi
|
||||
|
||||
# shellcheck source=lib/common.sh
|
||||
. "${DEPLOY_DIR}/lib/common.sh"
|
||||
|
||||
@@ -111,9 +118,6 @@ GLCHAT_FORCE_OS="${GLCHAT_FORCE_OS:-0}"
|
||||
GLCHAT_ASSUME_YES="${GLCHAT_ASSUME_YES:-0}"
|
||||
|
||||
TARGET_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}"
|
||||
if [ "$DRY_RUN" = "1" ] && [ -z "${GLCHAT_ETC_DIR:-}" ]; then
|
||||
TARGET_DIR="$(mktemp -d /tmp/glchat-dry-run.XXXXXX)"
|
||||
fi
|
||||
GLCHAT_ETC_DIR="$TARGET_DIR"
|
||||
GLCHAT_DATA_DIR="${TARGET_DIR}/data"
|
||||
GLCHAT_INSTALL_LOG="${TARGET_DIR}/install.log"
|
||||
@@ -256,6 +260,18 @@ parse_args() {
|
||||
*) die "неизвестный аргумент: $1 (см. --help)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Dry-run обязан быть безвредным: если /opt/glchat недоступен для записи и
|
||||
# путь не задан явно — пишем .env и конфиги во временный каталог.
|
||||
if [ "$DRY_RUN" = "1" ] && [ "$GLCHAT_ETC_DIR_EXPLICIT" = "0" ] && [ ! -w "$TARGET_DIR" ]; then
|
||||
local unwritable="$TARGET_DIR"
|
||||
TARGET_DIR="$(mktemp -d /tmp/glchat-dry-run.XXXXXX)"
|
||||
GLCHAT_ETC_DIR="$TARGET_DIR"
|
||||
GLCHAT_DATA_DIR="${TARGET_DIR}/data"
|
||||
GLCHAT_INSTALL_LOG="${TARGET_DIR}/install.log"
|
||||
GLCHAT_ENV_FILE="${TARGET_DIR}/.env"
|
||||
info "каталог ${unwritable} недоступен для записи: dry-run работает во временном ${TARGET_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
interactive_defaults() {
|
||||
@@ -768,7 +784,6 @@ LIVEKIT_API_KEY=@LIVEKIT_API_KEY@
|
||||
LIVEKIT_API_SECRET=@LIVEKIT_API_SECRET@
|
||||
TURN_RELAY_USER=@TURN_RELAY_USER@
|
||||
TURN_RELAY_PASSWORD=@TURN_RELAY_PASSWORD@
|
||||
AGE_RECIPIENT=@AGE_RECIPIENT@
|
||||
|
||||
# --- профиль производительности ---
|
||||
SQLITE_READ_POOL=@SQLITE_READ_POOL@
|
||||
@@ -793,6 +808,13 @@ SHARE_MAX_LIMIT=@SHARE_MAX@
|
||||
BACKUP_KEEP_DAYS=@BACKUP_KEEP@
|
||||
SWAP_SIZE_GB=@SWAP_SIZE@
|
||||
|
||||
# --- бэкапы: offsite-выгрузка (опция, AGENT.md 10.8) ---
|
||||
# Цель: rsync (user@host:/path) или rclone:remote:path; пусто — только локально.
|
||||
BACKUP_OFFSITE_TARGET=@BACKUP_OFFSITE_TARGET@
|
||||
# Ротация на приёмнике (дни, только rclone); пусто — копим без ротации.
|
||||
BACKUP_OFFSITE_KEEP_DAYS=@BACKUP_OFFSITE_KEEP_DAYS@
|
||||
AGE_RECIPIENT=@AGE_RECIPIENT@
|
||||
|
||||
# --- функции ---
|
||||
REGISTRATION_ENABLED=@REGISTRATION_ENABLED@
|
||||
ANTI_BOT_ENABLED=@ANTI_BOT_ENABLED@
|
||||
@@ -861,6 +883,9 @@ LIVEKIT_API_KEY=${LIVEKIT_API_KEY}
|
||||
LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}
|
||||
LIVEKIT_PUBLIC_URL=${LIVEKIT_PUBLIC_URL}
|
||||
|
||||
# Offsite-бэкапы: цель и срок хранения задаются вручную после установки.
|
||||
BACKUP_OFFSITE_TARGET=${BACKUP_OFFSITE_TARGET:-}
|
||||
BACKUP_OFFSITE_KEEP_DAYS=${BACKUP_OFFSITE_KEEP_DAYS:-}
|
||||
AGE_RECIPIENT=${AGE_RECIPIENT:-}
|
||||
SQLITE_READ_POOL=${PROFILE_SQLITE_READ_POOL}
|
||||
SQLITE_CACHE_SIZE=-${PROFILE_SQLITE_CACHE_MB}000
|
||||
@@ -907,7 +932,8 @@ for line in open(pairs_path, encoding="utf-8"):
|
||||
for key, value in values.items():
|
||||
template = template.replace("@" + key + "@", value)
|
||||
|
||||
leftovers = [t for t in __import__("re").findall(r"@[A-Z_]{2,}@", template)]
|
||||
# В именах плейсхолдеров есть цифры (FAIL2BAN_ENABLED) — их тоже ловим.
|
||||
leftovers = [t for t in __import__("re").findall(r"@[A-Z0-9_]{2,}@", template)]
|
||||
open(template_path, "w", encoding="utf-8").write(template)
|
||||
if leftovers:
|
||||
print("незаполненные плейсхолдеры: " + ", ".join(sorted(set(leftovers))), file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user