From d124c656391789ed10d6e75896597bfd7a0c91dc Mon Sep 17 00:00:00 2001 From: Soporte ZH Consultores Date: Thu, 4 Jun 2026 22:46:43 -0500 Subject: [PATCH] =?UTF-8?q?fix(backup):=20respaldar=20TODO=20(DB=20+=20sec?= =?UTF-8?q?rets=20+=20caddy=20+=20systemd)=20at=C3=B3micamente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 32 +++++++++++++++++++--- bin/donna-backup.sh | 66 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1640885..4b7d2a7 100644 --- a/README.md +++ b/README.md @@ -336,11 +336,37 @@ Colores por tipo de evento manual: ## Backups -`donna-backup.timer` ejecuta diariamente a las 03:00 (jitter ±5min) y vuelca `donna` comprimido a `/var/backups/donna/donna-YYYY-MM-DD_HHMMSS.sql.gz`. Retención 14 días. +`donna-backup.timer` ejecuta diariamente a las 03:00 (jitter ±5min) y genera `/var/backups/donna/donna-YYYY-MM-DD_HHMMSS.tar.gz`. Retención 14 días. Atómico: el `.tmp` se renombra al final solo si todo salió bien (no quedan archivos parciales). -Restaurar: +Contenido del tarball: +``` +donna-YYYY-MM-DD_HHMMSS/ +├── MANIFEST.txt ← inventario + tamaños + SHA-256 de cada archivo +├── db/ +│ ├── schema-and-data.sql.gz ← mariadb-dump completo (schema + data + routines + triggers + events) +│ └── grants.sql ← SHOW GRANTS del usuario donna_app +└── etc/ + ├── donna/donna.env ← secrets (OAuth, DB pass, APP_SECRET) + ├── caddy/Caddyfile ← config del Caddy local + └── systemd/system/ + ├── donna-backup.service + └── donna-backup.timer +``` + +Restaurar en un server nuevo: ```bash -gunzip -c /var/backups/donna/donna-YYYY-MM-DD_HHMMSS.sql.gz | mariadb donna +TAR=/path/to/donna-YYYY-MM-DD_HHMMSS.tar.gz +tar -xzf "$TAR" -C /tmp && cd /tmp/donna-* +# 1. Configs +cp etc/donna/donna.env /etc/donna/donna.env && chmod 640 /etc/donna/donna.env +cp etc/caddy/Caddyfile /etc/caddy/Caddyfile && systemctl reload caddy +cp etc/systemd/system/* /etc/systemd/system/ && systemctl daemon-reload +# 2. DB (crea DB y usuario primero) +mariadb -e "CREATE DATABASE donna CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" +mariadb < db/grants.sql # recrea el usuario y sus permisos +gunzip -c db/schema-and-data.sql.gz | mariadb donna +# 3. Verificar +sha256sum -c <(grep -E '^[a-f0-9]{64}' MANIFEST.txt) # checksums OK? ``` --- diff --git a/bin/donna-backup.sh b/bin/donna-backup.sh index b11881f..89774e0 100755 --- a/bin/donna-backup.sh +++ b/bin/donna-backup.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# Donna — daily MariaDB backup with 14-day retention +# 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" @@ -13,8 +14,25 @@ mkdir -p "$BACKUP_DIR" chmod 750 "$BACKUP_DIR" STAMP=$(date +%F_%H%M%S) -OUT="$BACKUP_DIR/donna-$STAMP.sql.gz" +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}" \ @@ -26,10 +44,48 @@ MYSQL_PWD="$DB_PASS" mariadb-dump \ --events \ --default-character-set=utf8mb4 \ "$DB_NAME" \ - | gzip -9 > "$OUT" + | gzip -9 > "$ROOT/db/schema-and-data.sql.gz" -chmod 640 "$OUT" +# 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 ($(du -h "$OUT" | cut -f1))" +echo "[donna-backup] $(date -Is) wrote $OUT_FINAL ($(du -h "$OUT_FINAL" | cut -f1))"