#!/usr/bin/env bash # ============================================================================= # Restauration PostgreSQL - production Xpeditis # ============================================================================= # Trois modes, du moins au plus destructeur : # # verify Restaure dans une base jetable et controle # la coherence. AUCUN impact sur la prod. # C'est ce que lance le timer hebdomadaire. # # logical Restaure un dump logique dans une base # nommee (par defaut une base de secours). # # pitr Restauration a un instant T via WAL-G. # DETRUIT le repertoire de donnees courant. # Reserve aux incidents majeurs. # # Lire docs/mise-en-prod/12-sauvegardes-restauration.md AVANT d'utiliser `pitr`. set -euo pipefail COMPOSE_DIR="${COMPOSE_DIR:-/opt/xpeditis/data-node}" ENV_FILE="${ENV_FILE:-${COMPOSE_DIR}/.env.data}" PGDATA_HOST="${PGDATA_HOST:-/var/lib/xpeditis/pgdata}" # shellcheck disable=SC1090 set -a; source "$ENV_FILE"; set +a log() { printf '\n[%s] %s\n' "$(date -Is)" "$*"; } fail() { printf '\nECHEC: %s\n' "$*" >&2; exit 1; } dc() { docker compose -f "${COMPOSE_DIR}/docker-compose.data.yml" --env-file "$ENV_FILE" "$@"; } confirm() { local prompt="$1" expected="$2" answer printf '\n%s\nTapez exactement "%s" pour confirmer : ' "$prompt" "$expected" read -r answer [[ "$answer" == "$expected" ]] || fail "confirmation refusee" } decrypt_dump() { local encrypted="$1" out="$2" [[ -f "$encrypted" ]] || fail "fichier introuvable : $encrypted" [[ -f "${BACKUP_AGE_KEY_FILE:-/root/.config/xpeditis/backup-age.key}" ]] \ || fail "cle de dechiffrement absente (BACKUP_AGE_KEY_FILE)" age -d -i "${BACKUP_AGE_KEY_FILE:-/root/.config/xpeditis/backup-age.key}" \ -o "$out" "$encrypted" || fail "dechiffrement age echoue" } MODE="${1:-}" case "$MODE" in # --------------------------------------------------------------------------- verify) ENCRYPTED="${2:-$(ls -1t /var/lib/xpeditis/dumps/xpeditis-*.dump.age 2>/dev/null | head -1)}" [[ -n "$ENCRYPTED" ]] || fail "aucun dump trouve" SCRATCH_DB="xpeditis_restore_check" TMP_DUMP="/tmp/xpeditis-verify-$$.dump" log "Verification de : $ENCRYPTED" decrypt_dump "$ENCRYPTED" "$TMP_DUMP" trap 'rm -f "$TMP_DUMP"' EXIT log "Creation de la base jetable ${SCRATCH_DB}" dc exec -T -u postgres postgres psql -q -c "DROP DATABASE IF EXISTS ${SCRATCH_DB};" dc exec -T -u postgres postgres psql -q -c "CREATE DATABASE ${SCRATCH_DB};" log "pg_restore dans la base jetable" dc exec -T -u postgres postgres pg_restore \ --no-owner --no-privileges --exit-on-error -d "$SCRATCH_DB" < "$TMP_DUMP" \ || fail "pg_restore a echoue : LA SAUVEGARDE EST INEXPLOITABLE" log "Controles de coherence" # Une sauvegarde qui se restaure mais ne contient rien ne vaut rien : on # verifie que les tables metier critiques sont peuplees. dc exec -T -u postgres postgres psql -d "$SCRATCH_DB" -v ON_ERROR_STOP=1 <<'SQL' \pset pager off SELECT 'tables' AS objet, count(*) AS n FROM information_schema.tables WHERE table_schema='public'; SELECT 'users' AS objet, count(*) AS n FROM users; SELECT 'organizations' AS objet, count(*) AS n FROM organizations; SELECT 'csv_bookings' AS objet, count(*) AS n FROM csv_bookings; SELECT 'migrations' AS objet, count(*) AS n FROM migrations; DO $$ DECLARE t int; BEGIN SELECT count(*) INTO t FROM information_schema.tables WHERE table_schema='public'; IF t < 10 THEN RAISE EXCEPTION 'Sauvegarde suspecte : seulement % tables restaurees', t; END IF; END $$; SQL log "Nettoyage" dc exec -T -u postgres postgres psql -q -c "DROP DATABASE ${SCRATCH_DB};" log "SAUVEGARDE VALIDE : $ENCRYPTED" ;; # --------------------------------------------------------------------------- logical) ENCRYPTED="${2:?usage: $0 logical [base_cible]}" TARGET_DB="${3:-${POSTGRES_DB}_restored}" TMP_DUMP="/tmp/xpeditis-restore-$$.dump" confirm "Restauration logique de $ENCRYPTED dans la base '${TARGET_DB}'. La base cible sera SUPPRIMEE si elle existe." "RESTAURER ${TARGET_DB}" decrypt_dump "$ENCRYPTED" "$TMP_DUMP" trap 'rm -f "$TMP_DUMP"' EXIT dc exec -T -u postgres postgres psql -q -c "DROP DATABASE IF EXISTS ${TARGET_DB};" dc exec -T -u postgres postgres psql -q -c "CREATE DATABASE ${TARGET_DB};" dc exec -T -u postgres postgres pg_restore \ --no-owner --no-privileges --exit-on-error -j 2 -d "$TARGET_DB" < "$TMP_DUMP" \ || fail "pg_restore a echoue" log "Restauration terminee dans '${TARGET_DB}'." log "La base de production n'a PAS ete touchee. Pour basculer :" log " 1. arreter le backend : kubectl -n xpeditis-prod scale deploy/xpeditis-backend --replicas=0" log " 2. renommer les bases : ALTER DATABASE ${POSTGRES_DB} RENAME TO ${POSTGRES_DB}_old;" log " ALTER DATABASE ${TARGET_DB} RENAME TO ${POSTGRES_DB};" log " 3. relancer le backend : kubectl -n xpeditis-prod scale deploy/xpeditis-backend --replicas=2" ;; # --------------------------------------------------------------------------- pitr) TARGET_TIME="${2:?usage: $0 pitr \"YYYY-MM-DD HH:MM:SS\"}" cat <> \$D/postgresql.auto.conf < [base] Restauration logique dans une base a part $0 pitr "YYYY-MM-DD HH:MM:SS" Restauration a un instant T (DESTRUCTIF) Lister les sauvegardes disponibles : ls -lht /var/lib/xpeditis/dumps/ docker compose -f ${COMPOSE_DIR}/docker-compose.data.yml exec -u postgres postgres wal-g backup-list --detail USAGE exit 1 ;; esac