- prod-backup.sh (tier hourly 48ч / daily 30д + uploads), prod-backup-offsite-pull.sh (DR на dev-vm) - prod-restore-verify.sh: restore во временную БД + сверка числа строк, дроп - prod-monitor.sh: api/web/public/БД/диск/5xx → Telegram владельцу (+ --test) - deploy-prod.sh: guard'ы (prod-postgres, .env chmod600, не dev-пароль, quick-login off, SEED_DEMO_DATA=false, issuer), бэкап ДО миграций, только --yes, НЕ в ночных - systemd таймеры (backup-hourly/daily, monitor, offsite-pull); docs/prod-runbook.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
57 lines
3.1 KiB
Bash
Executable file
57 lines
3.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
||
#
|
||
# ПРОД food-market: проверка восстановления бэкапа. Берёт последний daily-дамп
|
||
# (или путь из аргумента), ВОССТАНАВЛИВАЕТ во ВРЕМЕННУЮ БД в том же контейнере
|
||
# postgres, сверяет число строк ключевых таблиц с боевой БД, затем удаляет
|
||
# временную БД. Боевую БД НЕ трогает (только читает count'ы).
|
||
#
|
||
# Выход 0 — восстановление успешно и counts совпали; !=0 — проблема (для мониторинга).
|
||
set -uo pipefail
|
||
|
||
CONTAINER="${FM_PG_CONTAINER:-food-market-prod-postgres}"
|
||
DB="${FM_PG_DB:-food_market}"
|
||
DB_USER="${FM_PG_USER:-food_market}"
|
||
BASE_DIR="${FM_BACKUP_DIR:-/opt/food-market-data/backups}"
|
||
TMP_DB="food_market_restore_test"
|
||
DUMP="${1:-}"
|
||
|
||
log() { echo "[$(date -Is)] [restore-verify] $*"; }
|
||
fail() { log "FAIL: $*"; cleanup; exit 1; }
|
||
cleanup() { docker exec "$CONTAINER" psql -U "$DB_USER" -d "$DB" -c "DROP DATABASE IF EXISTS $TMP_DB;" >/dev/null 2>&1 || true; }
|
||
|
||
[ -n "$DUMP" ] || DUMP="$(ls -t "$BASE_DIR"/daily/db-*.dump "$BASE_DIR"/hourly/db-*.dump 2>/dev/null | head -1)"
|
||
[ -n "$DUMP" ] && [ -f "$DUMP" ] || fail "дамп не найден (ни в аргументе, ни в $BASE_DIR)"
|
||
log "дамп: $DUMP ($(du -h "$DUMP" | cut -f1))"
|
||
|
||
trap cleanup EXIT
|
||
cleanup
|
||
log "создаю временную БД $TMP_DB"
|
||
docker exec "$CONTAINER" psql -U "$DB_USER" -d "$DB" -c "CREATE DATABASE $TMP_DB;" >/dev/null 2>&1 || fail "CREATE DATABASE"
|
||
|
||
log "pg_restore → $TMP_DB"
|
||
if ! docker exec -i "$CONTAINER" pg_restore -U "$DB_USER" -d "$TMP_DB" --no-owner --no-privileges < "$DUMP" >/tmp/fm-restore.log 2>&1; then
|
||
# pg_restore может вернуть !=0 на безобидных warning'ах (owner/extension); проверяем,
|
||
# что таблицы реально появились — это решающий критерий.
|
||
log "pg_restore завершился с предупреждениями (проверяю таблицы):"
|
||
tail -5 /tmp/fm-restore.log 2>/dev/null | sed 's/^/ /' || true
|
||
fi
|
||
|
||
# Сверка числа строк ключевых таблиц: боевая vs восстановленная.
|
||
TABLES="users roles organizations products supplies retail_sales money_transactions stocks"
|
||
mismatch=0
|
||
for t in $TABLES; do
|
||
live=$(docker exec "$CONTAINER" psql -U "$DB_USER" -d "$DB" -A -t -c "SELECT count(*) FROM $t;" 2>/dev/null | tr -d '[:space:]')
|
||
rest=$(docker exec "$CONTAINER" psql -U "$DB_USER" -d "$TMP_DB" -A -t -c "SELECT count(*) FROM $t;" 2>/dev/null | tr -d '[:space:]')
|
||
live="${live:-ERR}"; rest="${rest:-ERR}"
|
||
if [ "$live" = "$rest" ] && [ "$live" != "ERR" ]; then
|
||
log " $t: $live = $rest ✓"
|
||
else
|
||
log " $t: боевая=$live восстановлено=$rest ✗"; mismatch=1
|
||
fi
|
||
done
|
||
|
||
[ "$mismatch" = 0 ] || fail "число строк не совпало (см. выше)"
|
||
log "✓ Восстановление проверено: все ключевые таблицы совпали."
|
||
# cleanup выполнит trap EXIT.
|
||
exit 0
|