#!/bin/bash # Sauvegarde des bases d'owni, tirée depuis le poste. # # scripts/sauvegarder-owni.sh # sauvegarde # scripts/sauvegarder-owni.sh --essai # montre ce qui serait fait # # Tiré depuis le poste et non poussé depuis owni : une machine compromise ne # doit pas pouvoir atteindre les sauvegardes de ce qu'elle héberge. C'est le # poste qui va chercher, owni n'a aucun accès en retour. # # Les dumps sont faits ici plutôt que repris de /var/backup : ISPConfig n'en # produit qu'un sur six (seule la messagerie y est réglée), et n'en garde # qu'une copie. Ce script prend les six et garde ce qu'on lui dit. # # Ce qu'il ne fait pas : les fichiers des sites. Le code est dans Gitea, les # pièces jointes et les boîtes mail sont un autre sujet — voir docs/sauvegardes.md. set -u -o pipefail DEST="${SAUVEGARDE_DEST:-$HOME/Sauvegardes/owni}" GARDE="${SAUVEGARDE_GARDE:-30}" # jours de rétention SSH_OPTS=(-o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=20 -i "$HOME/.ssh/id_rsa_owni.alpinux.org") HOTE="abonnelc@owni.alpinux.org" ESSAI=0 [ "${1:-}" = "--essai" ] && ESSAI=1 # Les bases applicatives. dbispconfig est à part : c'est la configuration # d'ISPConfig, sans laquelle les sites ne se reconstruisent pas à l'identique. BASES=(c1_messagerie_db c1dolibarr c1gitea c1alpid c1_installparty c1_evenements dbispconfig) jour=$(date +%Y-%m-%d) cible="$DEST/$jour" echo "Sauvegarde d'owni vers $cible" [ "$ESSAI" = 1 ] && echo "(essai : rien ne sera écrit)" if [ "$ESSAI" = 0 ]; then mkdir -p "$cible" || { echo "impossible de créer $cible" >&2; exit 1; } chmod 700 "$DEST" "$cible" fi # Une base qui échoue ne doit pas interrompre les autres : mieux vaut cinq # sauvegardes sur six qu'aucune, et le compte final dit ce qui manque. faites=0 ratees=() for base in "${BASES[@]}"; do fichier="$cible/${base}_${jour}.sql.gz" printf ' %-20s ' "$base" if [ "$ESSAI" = 1 ]; then echo "→ ${fichier/#$HOME/\~}" continue fi # --single-transaction : pas de verrou sur les tables InnoDB, le site # continue de répondre pendant le dump. if ssh "${SSH_OPTS[@]}" "$HOTE" \ "sudo mysqldump --no-tablespaces --single-transaction --quick '$base'" \ 2>/dev/null | gzip -c > "$fichier"; then taille=$(du -h "$fichier" | cut -f1) # Un dump vide ou tronqué se reconnaît à sa dernière ligne. if zcat "$fichier" 2>/dev/null | tail -2 | grep -q "Dump completed"; then echo "ok $taille" faites=$((faites + 1)) else echo "INCOMPLET ($taille) — dump tronqué" ratees+=("$base") fi else echo "ÉCHEC" ratees+=("$base") rm -f "$fichier" fi done [ "$ESSAI" = 1 ] && exit 0 chmod 600 "$cible"/*.sql.gz 2>/dev/null # Rotation : on efface les jours entiers passés la rétention. Faite après, et # seulement si la sauvegarde du jour a réussi — sinon on effacerait les # anciennes sans en avoir de nouvelle. if [ "$faites" -gt 0 ]; then find "$DEST" -maxdepth 1 -type d -name '20*-*-*' -mtime "+$GARDE" \ -exec rm -rf {} + 2>/dev/null fi echo echo "$faites base(s) sauvegardée(s) sur ${#BASES[@]}, rétention $GARDE jours." if [ ${#ratees[@]} -gt 0 ]; then echo "Échec : ${ratees[*]}" >&2 exit 1 fi