Files
glchat/scripts/test-backup.sh
T
grendervill 3151327f6e 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 после установки.
2026-09-22 18:44:50 +03:00

163 lines
6.9 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
# Проверка 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 ]