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:
@@ -39,3 +39,6 @@ WEBHOOK_SECRET= # подпись вебхуков
|
||||
LIVEKIT_API_KEY= # ключ LiveKit (фаза 3)
|
||||
LIVEKIT_API_SECRET= # секрет LiveKit (фаза 3)
|
||||
AGE_RECIPIENT= # публичный age-ключ для шифрования бэкапов
|
||||
BACKUP_KEEP_DAYS= # сколько дней хранить бэкапы (профиль задаёт значение)
|
||||
BACKUP_OFFSITE_TARGET= # необязательно: rsync-цель (user@host:/path) или rclone:remote:path
|
||||
BACKUP_OFFSITE_KEEP_DAYS= # необязательно: чистить копии на приёмнике старше N дней (rclone)
|
||||
|
||||
@@ -193,6 +193,17 @@ install-dry-run: ## Проверить установщик без измене
|
||||
install-test: ## Прогон install.sh в чистом контейнере Ubuntu 24.04
|
||||
bash scripts/test-install.sh
|
||||
|
||||
.PHONY: profiles-test
|
||||
profiles-test: ## Проверить таблицу профилей производительности (AGENT.md 10.10)
|
||||
bash scripts/test-profiles.sh
|
||||
|
||||
.PHONY: backup-test
|
||||
backup-test: ## Проверить бэкап и offsite-выгрузку на синтетическом инстансе
|
||||
bash scripts/test-backup.sh
|
||||
|
||||
.PHONY: ops-test
|
||||
ops-test: profiles-test backup-test ## Проверки эксплуатационных скриптов
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Удалить артефакты сборки
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
+73
-1
@@ -9,6 +9,7 @@ BACKUP_DIR="${GLCHAT_DATA_DIR}/backups"
|
||||
KEEP_DAYS_DEFAULT=7
|
||||
|
||||
log() { printf '[backup] %s\n' "$*"; }
|
||||
warn() { printf '[backup] внимание: %s\n' "$*" >&2; }
|
||||
die() {
|
||||
printf '[backup] ошибка: %s\n' "$*" >&2
|
||||
exit 1
|
||||
@@ -20,10 +21,35 @@ set -a && . "$ENV_FILE" && set +a
|
||||
|
||||
KEEP_DAYS="${BACKUP_KEEP_DAYS:-$KEEP_DAYS_DEFAULT}"
|
||||
AGE_RECIPIENT="${AGE_RECIPIENT:-}"
|
||||
# Выгрузка копии наружу: rsync-цель (user@host:/path) или `rclone:remote:path`
|
||||
# (AGENT.md 10.8, опциональный offsite). Ошибка выгрузки не отменяет локальный
|
||||
# бэкап: о ней пишем предупреждение.
|
||||
OFFSITE_TARGET="${BACKUP_OFFSITE_TARGET:-}"
|
||||
# Ротация на принимающей стороне — только по явной настройке: пусто значит
|
||||
# «копим копии, чистим вручную».
|
||||
OFFSITE_KEEP_DAYS="${BACKUP_OFFSITE_KEEP_DAYS:-}"
|
||||
case "$OFFSITE_KEEP_DAYS" in
|
||||
"") ;;
|
||||
*[!0-9]*)
|
||||
warn "BACKUP_OFFSITE_KEEP_DAYS='${OFFSITE_KEEP_DAYS}' не число — offsite-ротация выключена"
|
||||
OFFSITE_KEEP_DAYS=""
|
||||
;;
|
||||
esac
|
||||
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
# Два запуска в одну секунду не должны затирать друг друга: имя с суффиксом.
|
||||
SUFFIX=""
|
||||
suffix_n=1
|
||||
while [ -e "${BACKUP_DIR}/glchat-${STAMP}${SUFFIX}.tar.gz" ] ||
|
||||
[ -e "${BACKUP_DIR}/glchat-${STAMP}${SUFFIX}.tar.gz.age" ]; do
|
||||
SUFFIX="-${suffix_n}"
|
||||
suffix_n=$((suffix_n + 1))
|
||||
done
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
ARCHIVE="${BACKUP_DIR}/glchat-${STAMP}.tar.gz"
|
||||
ARCHIVE="${BACKUP_DIR}/glchat-${STAMP}${SUFFIX}.tar.gz"
|
||||
TARGET="${ARCHIVE}.age"
|
||||
# ARCHIVE_OR_TARGET — готовый файл бэкапа (с шифрованием или без): его выгружает
|
||||
# offsite-копирование.
|
||||
ARCHIVE_OR_TARGET=""
|
||||
|
||||
cleanup() { rm -rf "$WORK_DIR"; }
|
||||
trap cleanup EXIT
|
||||
@@ -71,13 +97,59 @@ if [ -n "$AGE_RECIPIENT" ]; then
|
||||
age -r "$AGE_RECIPIENT" -o "$TARGET" "$ARCHIVE" || die "age-шифрование не удалось"
|
||||
rm -f "$ARCHIVE"
|
||||
chmod 600 "$TARGET"
|
||||
ARCHIVE_OR_TARGET="$TARGET"
|
||||
log "готово: ${TARGET} ($(du -h "$TARGET" | cut -f1))"
|
||||
else
|
||||
chmod 600 "$ARCHIVE"
|
||||
ARCHIVE_OR_TARGET="$ARCHIVE"
|
||||
log "готово (БЕЗ шифрования, AGE_RECIPIENT не задан): ${ARCHIVE}"
|
||||
fi
|
||||
|
||||
log "ротация: оставляем ${KEEP_DAYS} дн."
|
||||
find "$BACKUP_DIR" -maxdepth 1 -name 'glchat-*.tar.gz*' -mtime "+${KEEP_DAYS}" -print -delete || true
|
||||
|
||||
# Offsite идёт после локальной ротации: наружу уезжает уже актуальный набор.
|
||||
# Неудача выгрузки не отменяет локальный бэкап — только предупреждение.
|
||||
if [ -n "$OFFSITE_TARGET" ]; then
|
||||
log "выгружаем копию: ${OFFSITE_TARGET}"
|
||||
offsite_ok=0
|
||||
case "$OFFSITE_TARGET" in
|
||||
rclone:*)
|
||||
remote="${OFFSITE_TARGET#rclone:}"
|
||||
if command -v rclone >/dev/null 2>&1; then
|
||||
rclone copy "$ARCHIVE_OR_TARGET" "$remote" && offsite_ok=1
|
||||
else
|
||||
warn "rclone не установлен: offsite-копия не сделана"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
# Цель — каталог на приёмнике; без --delete копии только копятся.
|
||||
rsync -az --partial "$ARCHIVE_OR_TARGET" "${OFFSITE_TARGET%/}/" && offsite_ok=1
|
||||
else
|
||||
warn "rsync не установлен: offsite-копия не сделана"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
if [ "$offsite_ok" = "1" ]; then
|
||||
log "offsite-копия отправлена"
|
||||
else
|
||||
warn "offsite-копия не отправлена (локальный бэкап на месте)"
|
||||
fi
|
||||
if [ -n "$OFFSITE_KEEP_DAYS" ]; then
|
||||
case "$OFFSITE_TARGET" in
|
||||
rclone:*)
|
||||
if [ "$offsite_ok" = "1" ]; then
|
||||
rclone delete --min-age "${OFFSITE_KEEP_DAYS}d" "${OFFSITE_TARGET#rclone:}" &&
|
||||
log "offsite-ротация: удалены копии старше ${OFFSITE_KEEP_DAYS} дн." ||
|
||||
warn "offsite-ротация не удалась"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
warn "offsite-ротация по возрасту поддержана только для rclone — почистите приёмник вручную"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
log "бэкапов в каталоге: $(find "$BACKUP_DIR" -maxdepth 1 -name 'glchat-*.tar.gz*' | wc -l)"
|
||||
|
||||
+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)
|
||||
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env bash
|
||||
# Проверка deploy/backup.sh без docker: синтетический инстанс во временном
|
||||
# каталоге, локальный offsite-приёмник (rsync) и поведение при недоступной
|
||||
# выгрузке. Запуск: make backup-test (AGENT.md 10.8, 11.3).
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/glchat-backup-test.XXXXXX")"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
checks=0
|
||||
errors=0
|
||||
|
||||
ok() {
|
||||
checks=$((checks + 1))
|
||||
printf 'ok %s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
checks=$((checks + 1))
|
||||
errors=$((errors + 1))
|
||||
printf 'ОШИБКА %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
expect() { # expect <описание> <ожидаемое> <фактическое>
|
||||
if [ "$2" = "$3" ]; then
|
||||
ok "$1 = $3"
|
||||
else
|
||||
fail "$1: ожидалось '$2', получено '$3'"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- синтетический инстанс -------------------------------------------------
|
||||
|
||||
ETC="${WORK}/etc"
|
||||
DATA="${ETC}/data"
|
||||
mkdir -p "${DATA}/db" "${DATA}/files/attachments" "${ETC}/livekit"
|
||||
sqlite3 "${DATA}/db/glchat.db" <<'SQL'
|
||||
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL);
|
||||
CREATE TABLE messages (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
|
||||
INSERT INTO users (email) VALUES ('admin@example.test'), ('guest@example.test');
|
||||
INSERT INTO messages (body) VALUES ('привет'), ('как дела');
|
||||
SQL
|
||||
printf 'файл вложения' >"${DATA}/files/attachments/a.txt"
|
||||
printf 'service: glchat\n' >"${ETC}/compose.yaml"
|
||||
printf 'port: 7880\n' >"${ETC}/livekit/livekit.yaml"
|
||||
cat >"${ETC}/.env" <<'ENV'
|
||||
DOMAIN=gl.example.test
|
||||
APP_VERSION=v0.0.0-test
|
||||
PROFILE=standard
|
||||
BACKUP_KEEP_DAYS=30
|
||||
ENV
|
||||
|
||||
run_backup() { # run_backup <offsite-цель> [keep-days]
|
||||
BACKUP_OFFSITE_TARGET="$1" BACKUP_OFFSITE_KEEP_DAYS="${2:-}" \
|
||||
GLCHAT_ETC_DIR="$ETC" "${REPO_DIR}/deploy/backup.sh"
|
||||
}
|
||||
|
||||
# --- 1. без offsite --------------------------------------------------------
|
||||
|
||||
log_out="$(run_backup "")"
|
||||
expect "локальный бэкап создан" "1" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
||||
case "$log_out" in
|
||||
*"offsite"*) fail "offsite упомянут, хотя цель не задана" ;;
|
||||
*) ok "без BACKUP_OFFSITE_TARGET выгрузки нет" ;;
|
||||
esac
|
||||
|
||||
# --- 2. offsite в локальный каталог (rsync) -------------------------------
|
||||
|
||||
OFFSITE="${WORK}/offsite"
|
||||
mkdir -p "$OFFSITE"
|
||||
run_backup "$OFFSITE" >/dev/null
|
||||
expect "offsite-копия доехала" "1" "$(find "$OFFSITE" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
||||
local_newest="$(ls -t "${DATA}/backups"/glchat-*.tar.gz | head -1)"
|
||||
local_base="$(basename "$local_newest")"
|
||||
if [ -f "${OFFSITE}/${local_base}" ] && cmp -s "$local_newest" "${OFFSITE}/${local_base}"; then
|
||||
ok "offsite-копия ${local_base} побайтово совпадает с локальной"
|
||||
else
|
||||
fail "offsite-копия ${local_base} отсутствует или отличается"
|
||||
fi
|
||||
|
||||
# --- 3. недоступный приёмник: предупреждение, но не авария ----------------
|
||||
|
||||
before="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
||||
marker="${WORK}/marker"
|
||||
touch "$marker"
|
||||
if log_out="$(run_backup "no-such-host.invalid:/nonexistent" 2>&1)"; then
|
||||
ok "недоступный offsite не роняет бэкап (код 0)"
|
||||
else
|
||||
fail "недоступный offsite привёл к ошибке"
|
||||
fi
|
||||
case "$log_out" in
|
||||
*"offsite-копия не отправлена"*) ok "предупреждение о неудачной выгрузке есть" ;;
|
||||
*) fail "нет предупреждения о неудачной выгрузке" ;;
|
||||
esac
|
||||
after="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
||||
expect "локальные бэкапы продолжают создаваться" "$((before + 1))" "$after"
|
||||
|
||||
# --- 4. нечисловой BACKUP_OFFSITE_KEEP_DAYS отключает ротацию -------------
|
||||
|
||||
if log_out="$(run_backup "$OFFSITE" "пять" 2>&1)"; then
|
||||
ok "нечисловая offsite-ротация не роняет бэкап"
|
||||
else
|
||||
fail "нечисловая offsite-ротация привела к ошибке"
|
||||
fi
|
||||
case "$log_out" in
|
||||
*"не число"*) ok "предупреждение о нечисловом значении есть" ;;
|
||||
*) fail "нет предупреждения о нечисловом значении" ;;
|
||||
esac
|
||||
|
||||
# --- 5. AGE_RECIPIENT: шифрование при наличии age, внятная ошибка без него --
|
||||
|
||||
# AGE_RECIPIENT дописываем во временную копию .env и возвращаем исходный файл:
|
||||
# дальше проверяется offsite через rclone, где шифрование только мешает.
|
||||
cp "${ETC}/.env" "${ETC}/env.keep"
|
||||
if command -v age >/dev/null 2>&1 && command -v age-keygen >/dev/null 2>&1; then
|
||||
recipient="$(age-keygen 2>/dev/null | awk '/public key:/ {print $4}')"
|
||||
printf 'AGE_RECIPIENT=%s\n' "$recipient" >>"${ETC}/.env"
|
||||
plain_before="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
||||
run_backup "" >/dev/null
|
||||
expect "с AGE_RECIPIENT появился .age-файл" "1" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.age' | wc -l | tr -d ' ')"
|
||||
expect "новых незашифрованных архивов нет" "$plain_before" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')"
|
||||
else
|
||||
printf 'AGE_RECIPIENT=age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\n' >>"${ETC}/.env"
|
||||
if run_backup "" >/dev/null 2>&1; then
|
||||
fail "без установленного age бэкап не должен проходить молча"
|
||||
else
|
||||
ok "без age установщик бэкапа падает с ошибкой"
|
||||
fi
|
||||
fi
|
||||
|
||||
mv "${ETC}/env.keep" "${ETC}/.env"
|
||||
|
||||
# --- 6. rclone-приёмник: проверяем фактические команды rclone --------------
|
||||
|
||||
SHIM="${WORK}/bin"
|
||||
mkdir -p "$SHIM"
|
||||
cat >"${SHIM}/rclone" <<'SHIMEOF'
|
||||
#!/usr/bin/env bash
|
||||
printf 'rclone %s\n' "$*" >>"${RCLONE_LOG}"
|
||||
exit 0
|
||||
SHIMEOF
|
||||
chmod +x "${SHIM}/rclone"
|
||||
rclone_log="${WORK}/rclone.log"
|
||||
: >"$rclone_log"
|
||||
(
|
||||
export PATH="${SHIM}:${PATH}" RCLONE_LOG="$rclone_log"
|
||||
run_backup "rclone:remote:glchat" 7 >/dev/null
|
||||
)
|
||||
if grep -q "^rclone copy .*glchat-.*\.tar\.gz remote:glchat$" "$rclone_log"; then
|
||||
ok "rclone copy вызван с целью remote:glchat"
|
||||
else
|
||||
fail "rclone copy не вызван: $(cat "$rclone_log")"
|
||||
fi
|
||||
if grep -q "^rclone delete --min-age 7d remote:glchat$" "$rclone_log"; then
|
||||
ok "rclone delete --min-age 7d вызван для ротации"
|
||||
else
|
||||
fail "offsite-ротация не вызвана: $(cat "$rclone_log")"
|
||||
fi
|
||||
|
||||
printf '\n=== бэкап/offsite: %d проверок, %d ошибок ===\n' "$checks" "$errors"
|
||||
[ "$errors" -eq 0 ]
|
||||
@@ -99,6 +99,17 @@ if [ "$secrets_before" != "$secrets_after" ]; then
|
||||
fi
|
||||
echo "секреты сохранены при повторной установке: ok"
|
||||
|
||||
# Offsite-бэкапы: ключи должны присутствовать в .env сразу после установки
|
||||
# (пустыми — цель задаёт владелец), иначе backup.sh не увидит настройку.
|
||||
log "offsite-настройки в .env"
|
||||
docker exec "$CONTAINER" bash -lc '
|
||||
set -e
|
||||
for key in BACKUP_KEEP_DAYS BACKUP_OFFSITE_TARGET BACKUP_OFFSITE_KEEP_DAYS AGE_RECIPIENT; do
|
||||
grep -q "^${key}=" /opt/glchat/.env || { echo "нет ${key} в .env" >&2; exit 1; }
|
||||
echo "${key}: ok"
|
||||
done
|
||||
'
|
||||
|
||||
# Сборка образа внутри DinD невозможна (overlay поверх overlay), поэтому образ
|
||||
# собирается и проверяется отдельно на хосте (make docker-build + docker compose).
|
||||
log "проверки состояния установки"
|
||||
|
||||
Executable
+116
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-profiles.sh — проверка таблицы профилей производительности (AGENT.md 10.10).
|
||||
# Значения из профиля обязаны совпадать со спецификацией: профиль меняет только
|
||||
# лимиты и тюнинг, поэтому расхождение — дефект установщика.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=../deploy/lib/common.sh
|
||||
source "${REPO_DIR}/deploy/lib/common.sh"
|
||||
|
||||
failures=0
|
||||
checks=0
|
||||
|
||||
# expect <профиль> <параметр> <ожидаемое>
|
||||
expect() {
|
||||
local tier="$1" name="$2" want="$3"
|
||||
local got="${!name:-}"
|
||||
checks=$((checks + 1))
|
||||
if [ "$got" != "$want" ]; then
|
||||
printf 'FAIL %s: %s = %s, ожидалось %s\n' "$tier" "$name" "$got" "$want" >&2
|
||||
failures=$((failures + 1))
|
||||
else
|
||||
printf 'ok %s: %s = %s\n' "$tier" "$name" "$got"
|
||||
fi
|
||||
}
|
||||
|
||||
check_lite() {
|
||||
profile_values lite
|
||||
expect lite PROFILE_SQLITE_CACHE_MB 8
|
||||
expect lite PROFILE_SQLITE_MMAP_MB 128
|
||||
expect lite PROFILE_SQLITE_READ_POOL 2
|
||||
expect lite PROFILE_GOMAXPROCS 1
|
||||
expect lite PROFILE_LRU_ENTRIES 2000
|
||||
expect lite PROFILE_LIVEKIT_MAX_PARTICIPANTS 10
|
||||
expect lite PROFILE_LIVEKIT_MAX_VIDEO 4
|
||||
expect lite PROFILE_LIVEKIT_SIMULCAST 0
|
||||
expect lite PROFILE_UPLOAD_MB 25
|
||||
expect lite PROFILE_APP_LIMIT 384m
|
||||
expect lite PROFILE_LIVEKIT_LIMIT 640m
|
||||
expect lite PROFILE_SWAP_GB 2
|
||||
expect lite PROFILE_BACKUP_KEEP 7
|
||||
expect lite PROFILE_SHARE_SOFT 1440p/30
|
||||
}
|
||||
|
||||
check_standard() {
|
||||
profile_values standard
|
||||
expect standard PROFILE_SQLITE_CACHE_MB 16
|
||||
expect standard PROFILE_SQLITE_MMAP_MB 256
|
||||
expect standard PROFILE_SQLITE_READ_POOL 4
|
||||
expect standard PROFILE_GOMAXPROCS 2
|
||||
expect standard PROFILE_LRU_ENTRIES 10000
|
||||
expect standard PROFILE_LIVEKIT_MAX_PARTICIPANTS 25
|
||||
expect standard PROFILE_LIVEKIT_MAX_VIDEO 8
|
||||
expect standard PROFILE_LIVEKIT_SIMULCAST 2
|
||||
expect standard PROFILE_UPLOAD_MB 50
|
||||
expect standard PROFILE_APP_LIMIT 768m
|
||||
expect standard PROFILE_LIVEKIT_LIMIT 1536m
|
||||
expect standard PROFILE_SWAP_GB 2
|
||||
expect standard PROFILE_BACKUP_KEEP 14
|
||||
expect standard PROFILE_SHARE_SOFT 1440p/30
|
||||
}
|
||||
|
||||
check_plus_and_pro() {
|
||||
profile_values plus
|
||||
expect plus PROFILE_SQLITE_CACHE_MB 32
|
||||
expect plus PROFILE_SQLITE_MMAP_MB 512
|
||||
expect plus PROFILE_UPLOAD_MB 100
|
||||
expect plus PROFILE_APP_LIMIT 1536m
|
||||
expect plus PROFILE_LIVEKIT_LIMIT 3g
|
||||
expect plus PROFILE_BACKUP_KEEP 30
|
||||
expect plus PROFILE_SHARE_SOFT 4K/30
|
||||
|
||||
profile_values pro
|
||||
expect pro PROFILE_SQLITE_CACHE_MB 64
|
||||
expect pro PROFILE_SQLITE_MMAP_MB 1024
|
||||
expect pro PROFILE_UPLOAD_MB 250
|
||||
expect pro PROFILE_APP_LIMIT 4g
|
||||
expect pro PROFILE_LIVEKIT_LIMIT 6g
|
||||
expect pro PROFILE_BACKUP_KEEP 60
|
||||
expect pro PROFILE_SHARE_SOFT 4K/60
|
||||
}
|
||||
|
||||
check_none() {
|
||||
profile_values none
|
||||
# none — только обязательный минимум: системные лимиты не трогаем.
|
||||
expect none PROFILE_SWAP_GB 0
|
||||
expect none PROFILE_GOMAXPROCS 0
|
||||
expect none PROFILE_SHARE_SOFT 1080p/30
|
||||
}
|
||||
|
||||
# Профиль меняет только лимиты: набор параметров у всех профилей одинаков, иначе
|
||||
# переключение профиля оставило бы часть значений от прежнего.
|
||||
check_same_keys() {
|
||||
profile_values lite
|
||||
local lite_keys
|
||||
lite_keys="$(compgen -v PROFILE_ | sort | tr '\n' ' ')"
|
||||
profile_values pro
|
||||
local pro_keys
|
||||
pro_keys="$(compgen -v PROFILE_ | sort | tr '\n' ' ')"
|
||||
checks=$((checks + 1))
|
||||
if [ "$lite_keys" != "$pro_keys" ]; then
|
||||
printf 'FAIL набор параметров профилей различается:\nlite: %s\npro: %s\n' "$lite_keys" "$pro_keys" >&2
|
||||
failures=$((failures + 1))
|
||||
else
|
||||
printf 'ok набор параметров одинаков: %s\n' "$lite_keys"
|
||||
fi
|
||||
}
|
||||
|
||||
check_lite
|
||||
check_standard
|
||||
check_plus_and_pro
|
||||
check_none
|
||||
check_same_keys
|
||||
|
||||
printf '\n=== профили: %d проверок, %d ошибок ===\n' "$checks" "$failures"
|
||||
[ "$failures" -eq 0 ] || exit 1
|
||||
Reference in New Issue
Block a user