#!/usr/bin/env bash set -euo pipefail ############################################################################### # backup-bdd-recette.sh # # Ce script réalise une sauvegarde logique de plusieurs bases PostgreSQL # définies dans le fichier .env, exporte également la liste des rôles/users, # puis transfère l’ensemble vers une machine distante de stockage. # # Fonctionnement global : # 1. charge la configuration depuis le fichier .env ; # 2. prépare les chemins, logs et variables de connexion ; # 3. empêche l’exécution simultanée grâce à un verrou ; # 4. crée les dossiers de destination sur la machine distante ; # 5. exporte les rôles PostgreSQL ; # 6. dump chaque base au format personnalisé PostgreSQL ; # 7. transfère chaque fichier vers le serveur distant ; # 8. applique une rotation distante sur 10 jours ; # 9. envoie un bilan sur Discord : # - 1 message global si tout est OK ; # - en cas d’erreur partielle : # * USERS OK -> message simple ; # * USERS KO -> message détaillé ; # * DB OK -> message simple ; # * DB KO -> message détaillé. ############################################################################### ####################################### # Chargement du .env ####################################### SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env" if [[ ! -f "$ENV_FILE" ]]; then echo "ERROR: fichier .env introuvable : $ENV_FILE" >&2 exit 1 fi set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a ####################################### # Vérification des variables requises ####################################### : "${ENV_NAME:?Variable ENV_NAME manquante}" : "${PGHOST:?Variable PGHOST manquante}" : "${PGPORT:?Variable PGPORT manquante}" : "${PGUSER:?Variable PGUSER manquante}" : "${PGPASSWORD:?Variable PGPASSWORD manquante}" : "${DBS:?Variable DBS manquante}" : "${BACKUP_REMOTE_USER:?Variable BACKUP_REMOTE_USER manquante}" : "${BACKUP_REMOTE_HOST:?Variable BACKUP_REMOTE_HOST manquante}" : "${BACKUP_REMOTE_DIR:?Variable BACKUP_REMOTE_DIR manquante}" : "${SSH_KEY:?Variable SSH_KEY manquante}" : "${SSH_TIMEOUT:?Variable SSH_TIMEOUT manquante}" : "${BACKUP_LOG_DIR:?Variable BACKUP_LOG_DIR manquante}" ####################################### # Configuration principale ####################################### read -r -a DBS_ARRAY <<< "$DBS" IA_SSH="${BACKUP_REMOTE_USER}@${BACKUP_REMOTE_HOST}" IA_BASE_DIR="${BACKUP_REMOTE_DIR}" RETENTION_DAYS=10 SSH_OPTS=( -i "$SSH_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout="${SSH_TIMEOUT}" ) LOG_DIR="${BACKUP_LOG_DIR}" mkdir -p "$LOG_DIR" TS="$(date +'%Y-%m-%d_%H-%M-%S')" BACKUP_DIR_NAME="backup_${TS}" LOG_FILE="${LOG_DIR}/${BACKUP_DIR_NAME}.log" TMP_DIR="/tmp/pg_dump_${BACKUP_DIR_NAME}" mkdir -p "$TMP_DIR" exec > >(tee -a "$LOG_FILE") 2>&1 log() { echo "---- $(date +'%Y-%m-%d %H:%M:%S') ---- $*"; } export PGPASSWORD ####################################### # Configuration Discord ####################################### DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}" DISCORD_PING="${DISCORD_PING:-@here}" discord_send() { local msg="$1" [[ -z "${DISCORD_WEBHOOK_URL:-}" ]] && return curl -fsS -H "Content-Type: application/json" \ -d "{\"content\":\"$msg\"}" \ "$DISCORD_WEBHOOK_URL" >/dev/null || true } ####################################### # Message global OK ####################################### discord_msg_global_ok() { local msg="**BACKUP BDD ${ENV_NAME} 🟢**\n" msg+="Name: ${BACKUP_DIR_NAME}\n" msg+="Dumps transfer: ✅\n" msg+="Users transfer: ✅" discord_send "$msg" } ####################################### # Messages USERS ####################################### discord_msg_users_ok_simple() { local msg="**BACKUP BDD ${ENV_NAME} 🟢**\n" msg+="Users backup validé" discord_send "$msg" } discord_msg_users_error() { local export_ok="$1" local transfer_ok="$2" local details="$3" local export_disp transfer_disp export_disp=$([[ -n "$export_ok" ]] && echo "✅" || echo "❌") transfer_disp=$([[ -n "$transfer_ok" ]] && echo "✅" || echo "❌") local msg="**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**\n" msg+="Name: ${BACKUP_DIR_NAME}\n" msg+="Users export: ${export_disp}\n" msg+="Users transfer: ${transfer_disp}" [[ -n "$details" ]] && msg+="\nDetails: ${details}" discord_send "$msg" } ####################################### # Messages DB ####################################### discord_msg_db_ok_simple() { local db="$1" local msg="**BACKUP BDD ${ENV_NAME} 🟢**\n" msg+="Backup validé : ${db}" discord_send "$msg" } discord_msg_db_error() { local db="$1" local dump_ok="$2" local transfer_ok="$3" local details="$4" local dump_disp transfer_disp dump_disp=$([[ -n "$dump_ok" ]] && echo "✅" || echo "❌") transfer_disp=$([[ -n "$transfer_ok" ]] && echo "✅" || echo "❌") local msg="**${DISCORD_PING} BACKUP BDD ${ENV_NAME} 🔴**\n" msg+="Name: ${BACKUP_DIR_NAME}\n" msg+="Database: ${db}\n" msg+="Dump: ${dump_disp}\n" msg+="Transfer: ${transfer_disp}" [[ -n "$details" ]] && msg+="\nDetails: ${details}" discord_send "$msg" } ####################################### # Variables de statut globales ####################################### DUMPS_OK=true USERS_OK=true USERS_EXPORT_OK=true USERS_TRANSFER_OK=true USERS_DETAILS="" declare -A DB_DUMP_OK declare -A DB_TRANSFER_OK declare -A DB_DETAILS ####################################### # Verrou d’exécution ####################################### LOCK_DIR="/tmp/pg_multi_dump_stream.lock.d" if ! mkdir "$LOCK_DIR" 2>/dev/null; then log "ERROR: Backup déjà en cours" discord_msg_users_error "" "" "Lock already exists" exit 1 fi trap 'rm -rf "$LOCK_DIR"' EXIT ####################################### # Préparation du dossier distant ####################################### REMOTE_DIR="${IA_BASE_DIR}" log "Creating remote directories" if ! ssh "${SSH_OPTS[@]}" "$IA_SSH" "mkdir -p '${REMOTE_DIR}/ferme' '${REMOTE_DIR}/sirh' '${REMOTE_DIR}/inventory' '${REMOTE_DIR}/user'"; then log "ERROR: remote mkdir failed" discord_msg_users_error "" "" "Remote mkdir failed" exit 1 fi ####################################### # Export des rôles PostgreSQL ####################################### ROLES_FILE="${TMP_DIR}/user_${TS}.dump" set +e psql -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -d postgres -Atq <<'SQL' > "$ROLES_FILE" SELECT rolname FROM pg_roles WHERE rolname !~ '^pg_'; SQL RET=$? if [[ $RET -ne 0 ]]; then USERS_OK= USERS_EXPORT_OK= USERS_DETAILS="roles export failed" fi scp "${SSH_OPTS[@]}" "$ROLES_FILE" "$IA_SSH:${REMOTE_DIR}/user/" RET=$? if [[ $RET -ne 0 ]]; then USERS_OK= USERS_TRANSFER_OK= if [[ -n "$USERS_DETAILS" ]]; then USERS_DETAILS+=" | roles transfer failed" else USERS_DETAILS="roles transfer failed" fi fi set -e ####################################### # Dump des bases ####################################### set +e for DB in "${DBS_ARRAY[@]}"; do FILE="${TMP_DIR}/${DB}_${TS}.dump" DB_DUMP_OK["$DB"]=true DB_TRANSFER_OK["$DB"]=true DB_DETAILS["$DB"]="" log "Dump $DB" pg_dump -h "$PGHOST" -p "$PGPORT" -U "$PGUSER" -Fc -d "$DB" -f "$FILE" RET=$? if [[ $RET -ne 0 ]]; then DUMPS_OK= DB_DUMP_OK["$DB"]= DB_TRANSFER_OK["$DB"]= DB_DETAILS["$DB"]="dump failed" continue fi scp "${SSH_OPTS[@]}" "$FILE" "$IA_SSH:${REMOTE_DIR}/${DB}/" RET=$? if [[ $RET -ne 0 ]]; then DUMPS_OK= DB_TRANSFER_OK["$DB"]= DB_DETAILS["$DB"]="transfer failed" fi done set -e ####################################### # Rotation distante ####################################### log "Starting remote rotation: delete backups older than ${RETENTION_DAYS} days" set +e ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${REMOTE_DIR}/user' -type f -name 'user_*.dump' -mtime +${RETENTION_DAYS} -delete" RET=$? if [[ $RET -ne 0 ]]; then log "ERROR: remote rotation failed for users" fi for DB in "${DBS_ARRAY[@]}"; do ssh "${SSH_OPTS[@]}" "$IA_SSH" "find '${REMOTE_DIR}/${DB}' -type f -name '${DB}_*.dump' -mtime +${RETENTION_DAYS} -delete" RET=$? if [[ $RET -ne 0 ]]; then log "ERROR: remote rotation failed for ${DB}" else log "Remote rotation OK for ${DB}" fi done set -e log "Remote rotation finished" ####################################### # Nettoyage local ####################################### rm -rf "$TMP_DIR" ####################################### # Bilan final Discord ####################################### MODE_KO= [[ -z "${DUMPS_OK:-}" ]] && MODE_KO=true [[ -z "${USERS_OK:-}" ]] && MODE_KO=true if [[ -z "${MODE_KO:-}" ]]; then discord_msg_global_ok exit 0 fi if [[ -n "${USERS_EXPORT_OK:-}" && -n "${USERS_TRANSFER_OK:-}" ]]; then discord_msg_users_ok_simple else discord_msg_users_error "${USERS_EXPORT_OK:+true}" "${USERS_TRANSFER_OK:+true}" "$USERS_DETAILS" fi for DB in "${DBS_ARRAY[@]}"; do if [[ -n "${DB_DUMP_OK[$DB]:-}" && -n "${DB_TRANSFER_OK[$DB]:-}" ]]; then discord_msg_db_ok_simple "$DB" else discord_msg_db_error "$DB" "${DB_DUMP_OK[$DB]:+true}" "${DB_TRANSFER_OK[$DB]:+true}" "${DB_DETAILS[$DB]}" fi done exit 2