Files
donna/bin/donna-backup.sh
Soporte ZH Consultores d124c65639 fix(backup): respaldar TODO (DB + secrets + caddy + systemd) atómicamente
Bug: el script previo solo respaldaba la BD donna. /etc/donna/donna.env,
/etc/caddy/Caddyfile y los systemd units quedaban sin respaldo. Además,
si mariadb-dump fallaba a media corrida el .sql.gz parcial quedaba
como si fuera válido (se vio hoy con un archivo de 20 bytes).

Cambios:
- Output ahora es un solo tar.gz con DB + GRANTs + configs + MANIFEST
  (sha256 de cada archivo incluido)
- Atómico: tar se escribe a .tmp y se renombra solo al final
- Incluye SHOW GRANTS para poder recrear el usuario donna_app en un
  server nuevo sin tener que conocer el password de root del MariaDB
- Limpieza de archivos .sql.gz legacy también en la retención
- README documenta el procedimiento de restore en server nuevo
2026-06-04 22:46:43 -05:00

92 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Donna — daily full backup (DB + configs + privileges)
# Retention: 14 days. Atomic: archivo se renombra al final solo si todo salió bien.
set -euo pipefail
BACKUP_DIR="/var/backups/donna"
ENV_FILE="/etc/donna/donna.env"
RETENTION_DAYS=14
# shellcheck disable=SC1090
. <(grep -E '^(DB_NAME|DB_USER|DB_PASS|DB_HOST|DB_PORT)=' "$ENV_FILE")
mkdir -p "$BACKUP_DIR"
chmod 750 "$BACKUP_DIR"
STAMP=$(date +%F_%H%M%S)
TMP_DIR=$(mktemp -d -t donna-backup.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
OUT_FINAL="$BACKUP_DIR/donna-$STAMP.tar.gz"
OUT_TMP="$OUT_FINAL.tmp"
# Layout dentro del tarball:
# donna-$STAMP/
# ├── db/
# │ ├── schema-and-data.sql.gz ← DB completa de la app
# │ └── grants.sql ← GRANTs para recrear el usuario donna_app
# ├── etc/donna/donna.env
# ├── etc/caddy/Caddyfile
# └── etc/systemd/system/donna-backup.{service,timer}
ROOT="$TMP_DIR/donna-$STAMP"
mkdir -p "$ROOT/db" "$ROOT/etc/donna" "$ROOT/etc/caddy" "$ROOT/etc/systemd/system"
# 1) Dump completo de la BD (schema + data + routines + triggers + events)
MYSQL_PWD="$DB_PASS" mariadb-dump \
--host="${DB_HOST:-127.0.0.1}" \
--port="${DB_PORT:-3306}" \
--user="$DB_USER" \
--single-transaction \
--quick \
--routines \
--triggers \
--events \
--default-character-set=utf8mb4 \
"$DB_NAME" \
| gzip -9 > "$ROOT/db/schema-and-data.sql.gz"
# 2) GRANTs del usuario de la app (para recrear permisos en un server nuevo)
# Si corre como root con socket disponible usa el root del server,
# si no, hace SHOW GRANTS como el propio usuario de la app.
{
if [ -S /run/mysqld/mysqld.sock ] && [ "$(id -u)" -eq 0 ]; then
mariadb --batch --skip-column-names -e "SHOW GRANTS FOR '${DB_USER}'@'localhost';" 2>/dev/null \
| sed 's/$/;/' || true
else
MYSQL_PWD="$DB_PASS" mariadb --user="$DB_USER" --batch --skip-column-names \
-e "SHOW GRANTS;" 2>/dev/null | sed 's/$/;/' || true
fi
} > "$ROOT/db/grants.sql"
# 3) Archivos de configuración fuera del repo
cp -p /etc/donna/donna.env "$ROOT/etc/donna/donna.env"
cp -p /etc/caddy/Caddyfile "$ROOT/etc/caddy/Caddyfile"
cp -p /etc/systemd/system/donna-backup.service "$ROOT/etc/systemd/system/donna-backup.service"
cp -p /etc/systemd/system/donna-backup.timer "$ROOT/etc/systemd/system/donna-backup.timer"
# 4) Manifest (qué hay adentro + tamaños + checksum)
{
echo "Donna full backup — $(date -Is)"
echo "Host: $(hostname -f 2>/dev/null || hostname)"
echo "DB: $DB_NAME"
echo
echo "Contents:"
( cd "$ROOT" && find . -type f -exec ls -lh {} \; | awk '{print $5, $9}' )
echo
echo "SHA-256:"
( cd "$ROOT" && find . -type f -exec sha256sum {} \; )
} > "$ROOT/MANIFEST.txt"
# 5) Empaquetar y comprimir atómicamente
tar -C "$TMP_DIR" -czf "$OUT_TMP" "donna-$STAMP"
mv "$OUT_TMP" "$OUT_FINAL"
chmod 640 "$OUT_FINAL"
# 6) Retención
find "$BACKUP_DIR" -type f -name 'donna-*.tar.gz' -mtime "+$RETENTION_DAYS" -delete
# Limpieza de respaldos legacy (.sql.gz) más viejos que retención
find "$BACKUP_DIR" -type f -name 'donna-*.sql.gz' -mtime "+$RETENTION_DAYS" -delete
echo "[donna-backup] $(date -Is) wrote $OUT_FINAL ($(du -h "$OUT_FINAL" | cut -f1))"