xpeditis2.0/infra/prod/data-node/backup/pg-restore.sh
2026-09-07 21:40:50 +02:00

193 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# Restauration PostgreSQL - production Xpeditis
# =============================================================================
# Trois modes, du moins au plus destructeur :
#
# verify <fichier.dump.age> Restaure dans une base jetable et controle
# la coherence. AUCUN impact sur la prod.
# C'est ce que lance le timer hebdomadaire.
#
# logical <fichier.dump.age> Restaure un dump logique dans une base
# nommee (par defaut une base de secours).
#
# pitr <YYYY-MM-DD HH:MM:SS> 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 <fichier.dump.age> [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 <<BANNER
############################################################
# RESTAURATION A UN INSTANT T (PITR) #
# #
# Cette operation DETRUIT le repertoire de donnees actuel #
# et rejoue les WAL jusqu'a : #
# ${TARGET_TIME}
# #
# Prerequis : #
# - backend arrete (replicas=0) : sinon ecritures perdues #
# - un dump logique frais pris AVANT (voir plus bas) #
############################################################
BANNER
confirm "Confirmez-vous la destruction de ${PGDATA_HOST} ?" "DETRUIRE ET RESTAURER"
log "Sauvegarde de securite du repertoire courant (au cas ou)"
SAFETY="/var/lib/xpeditis/pgdata-avant-pitr-$(date -u +%Y%m%dT%H%M%SZ)"
dc stop postgres
mv "${PGDATA_HOST}/pgdata" "$SAFETY" || fail "impossible de deplacer le repertoire courant"
mkdir -p "${PGDATA_HOST}/pgdata"
chown 999:999 "${PGDATA_HOST}/pgdata"
chmod 700 "${PGDATA_HOST}/pgdata"
log "Ancien repertoire conserve dans ${SAFETY} (a supprimer une fois la restauration validee)"
log "wal-g backup-fetch (derniere sauvegarde complete)"
dc run --rm --no-deps -u postgres postgres \
wal-g backup-fetch /var/lib/postgresql/data/pgdata LATEST \
|| fail "backup-fetch a echoue"
log "Configuration du rejeu des WAL jusqu'a ${TARGET_TIME}"
dc run --rm --no-deps -u postgres postgres bash -c "
set -e
D=/var/lib/postgresql/data/pgdata
touch \$D/recovery.signal
cat >> \$D/postgresql.auto.conf <<CONF
restore_command = 'wal-g wal-fetch \"%f\" \"%p\"'
recovery_target_time = '${TARGET_TIME}'
recovery_target_action = 'promote'
recovery_target_timeline = 'latest'
CONF
" || fail "configuration de la restauration echouee"
log "Redemarrage de PostgreSQL : le rejeu des WAL commence"
dc up -d postgres
log "Suivez la progression : docker compose logs -f postgres"
log "La base sort du mode restauration quand 'database system is ready to accept connections' apparait."
log "Verifiez ENSUITE les donnees avant de relancer le backend."
;;
# ---------------------------------------------------------------------------
*)
cat <<USAGE
Usage:
$0 verify [fichier.dump.age] Test de restauration sans impact
$0 logical <fichier.dump.age> [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