Compare commits

6 Commits

Author SHA1 Message Date
marco.locatelli@steamware.net 27faed361a fix redmine backup 2026-04-02 13:09:42 +02:00
marco.locatelli@steamware.net 8e89c6c7bc riscrittura redmine files backup 2026-04-02 12:12:28 +02:00
marco.locatelli@steamware.net a0c3fad69b rimozione old logic e creazione redmine files backup 2026-04-02 11:43:32 +02:00
marco.locatelli@steamware.net f09555f933 minor fix logic backup 2026-03-31 12:31:57 +02:00
marco.locatelli@steamware.net c81f3c4d53 minor fix 2026-03-31 12:17:51 +02:00
marco.locatelli@steamware.net db31f111d3 gestione errore non-retryable nei backup logici 2026-03-31 12:06:05 +02:00
5 changed files with 215 additions and 192 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ set -euo pipefail
ulimit -n 65536
# Directory base dei backup
BACKUP_BASE=/var/backups/tscale01
BACKUP_BASE=/var/backups/tscale01/mariadb
# Timestamp per la directory del backup
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
+1 -1
View File
@@ -9,7 +9,7 @@ set -euo pipefail
ulimit -n 65536
# Directory base dei backup
BACKUP_BASE=/var/backups/tscale01
BACKUP_BASE=/var/backups/tscale01/mariadb
# Prefissi per full e incremental
FULL_PREFIX=backup-full-
@@ -1,79 +0,0 @@
#!/bin/bash
set -euo pipefail
ulimit -n 65536
BACKUP_BASE=/var/backups/tscale01
PREFIX=backup-logic-
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
TARGET="$BACKUP_BASE/${PREFIX}${TIMESTAMP}"
DUMPFILE="$TARGET/all-dbs.sql"
LOGFILE=/var/log/mariadb-backup.log
LOCKFILE=/var/lock/mariadb-backup.lock
# mysqldump bin e opzioni (root senza password: non passiamo --defaults-file)
MYSQLDUMP_BIN=$(command -v mysqldump || true)
MYSQLDUMP_OPTS="--user=root --all-databases --single-transaction --quick --lock-tables=FALSE"
# assicurati che il logfile esista e abbia permessi restrittivi
touch "$LOGFILE"
chown root:root "$LOGFILE"
chmod 600 "$LOGFILE"
#verifico file lock per non sovrapporre backup
(
flock -n 9 || { echo "[$(date '+%F %T')] SKIP: another backup is running" >> "$LOGFILE"; exit 0; }
if [ -z "$MYSQLDUMP_BIN" ]; then
echo "[$(date '+%F %T')] ERROR: mysqldump not found in PATH" >> "$LOGFILE"
exit 1
fi
mkdir -p "$TARGET"
chown mysql:mysql "$TARGET"
chmod 750 "$TARGET"
TMPLOG=$(mktemp /tmp/mariadb-backup-logic.XXXXXX)
echo "---------------------" >> "$LOGFILE"
echo "[$(date '+%F %T')] START logic backup $TARGET" >> "$LOGFILE"
# esegui il dump: stdout -> file, stderr -> TMPLOG
if $MYSQLDUMP_BIN $MYSQLDUMP_OPTS >"$DUMPFILE" 2>"$TMPLOG"; then
echo "[$(date '+%F %T')] END logic backup $TARGET" >> "$LOGFILE"
# misura dimensione apparente (byte logici)
size_apparent_bytes=$(find "$TARGET" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END{print s+0}')
size_apparent_human=$(numfmt --to=iec --suffix=B "$size_apparent_bytes" 2>/dev/null || echo "${size_apparent_bytes}B")
# misura spazio su disco effettivo (byte allocati)
size_disk_bytes=$(du -s --block-size=1 "$TARGET" 2>/dev/null | cut -f1 || echo 0)
size_disk_human=$(numfmt --to=iec --suffix=B "$size_disk_bytes" 2>/dev/null || echo "${size_disk_bytes}B")
echo "[$(date '+%F %T')] SIZE apparent: $size_apparent_human ($size_apparent_bytes bytes) for $TARGET" >> "$LOGFILE"
echo "[$(date '+%F %T')] SIZE on-disk: $size_disk_human ($size_disk_bytes bytes) for $TARGET" >> "$LOGFILE"
echo "[$(date '+%F %T')] RESULT: OK, no errors" >> "$LOGFILE"
rm -f "$TMPLOG"
else
echo "[$(date '+%F %T')] ERROR during logic backup for $TARGET. See $TMPLOG" >> "$LOGFILE"
tail -n 200 "$TMPLOG" >> "$LOGFILE"
rm -f "$TMPLOG"
exit 1
fi
# rotazione: mantieni 7 giorni per i logic (log dettagliato)
TO_DELETE=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "${PREFIX}*" -mtime +7 -print 2>/dev/null || true)
if [ -n "$TO_DELETE" ]; then
echo "[$(date '+%F %T')] ROTATE: removing old logic backups:" >> "$LOGFILE"
echo "$TO_DELETE" >> "$LOGFILE"
echo "$TO_DELETE" | tr '\n' '\0' | xargs -0 -r rm -rf --
else
echo "[$(date '+%F %T')] ROTATE: no logic backups to remove (<=7 days)" >> "$LOGFILE"
fi
echo " " >> "$LOGFILE"
) 9>"$LOCKFILE"
@@ -1,163 +1,209 @@
#!/bin/bash
# Backup logico per-database con retry e cleanup automatico
# Backup logico per-database con retry globale e retry per-database
# Sicuro per cluster Galera ad alto carico
# Autore: Marco + Copilot
# Uscita immediata in caso di:
# - comando con exit code != 0 (set -e)
# - uso di variabile non definita (set -u)
# - errore in una pipe (set -o pipefail)
set -euo pipefail
# Aumenta i file descriptor disponibili (mysqldump può aprirne molti)
# Aumenta i file descriptor disponibili (mariadb-dump può aprirne molti)
ulimit -n 65536
# Directory base dei backup
BACKUP_BASE=/var/backups/tscale01
# Directory base dei backup logici
BACKUP_BASE=/var/backups/tscale01/mariadb
# Prefisso per distinguere i backup logici di questo nodo
PREFIX=eqn-bck-logic-
# Prefisso per distinguere i backup logici
PREFIX=backup-logic-
# Timestamp per la directory del backup
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# Directory finale del backup
TARGET="$BACKUP_BASE/${PREFIX}${TIMESTAMP}"
# File di log generale
# File di log condiviso
LOGFILE=/var/log/mariadb-backup.log
# Lockfile per evitare esecuzioni concorrenti
LOCKFILE=/var/lock/mariadb-backup.lock
# Nome del server per i log (utile in ambienti multi-nodo)
# Nome breve del server per i log
SERVER_NAME=$(hostname -s)
# Binari necessari
# Numero massimo di tentativi globali dellintero backup
GLOBAL_MAX_RETRIES=5
# Attesa tra un tentativo globale e il successivo
GLOBAL_RETRY_SLEEP=30
# Numero massimo di tentativi per ogni database
DB_MAX_RETRIES=10
# Attesa tra un tentativo e laltro per lo stesso database
DB_RETRY_SLEEP=20
# Percorso del client mariadb
MYSQL_BIN=$(command -v mariadb || true)
# Percorso di mariadb-dump
MYSQLDUMP_BIN=$(command -v mariadb-dump || true)
# Opzioni dump ottimizzate per Galera (no lock, snapshot consistente)
# Opzioni ottimizzate per Galera
MYSQLDUMP_OPTS="--user=root --single-transaction --quick --skip-lock-tables"
# Numero massimo di retry per ogni database (deadlock = retry)
MAX_RETRIES=10
# Secondi di attesa tra un retry e l'altro
RETRY_SLEEP=30
# Assicura che il logfile esista e abbia permessi sicuri
touch "$LOGFILE"
chown root:root "$LOGFILE"
chmod 600 "$LOGFILE"
(
# FLOCK: evita che due backup partano insieme
flock -n 9 || { echo "[$(date '+%F %T')] [$SERVER_NAME] SKIP: another backup is running" >> "$LOGFILE"; exit 0; }
###############################################################################
# FUNZIONE PRINCIPALE: esegue un singolo tentativo completo di backup logico
###############################################################################
run_backup() {
# Controllo binari
if [ -z "$MYSQL_BIN" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR: mariadb client not found in PATH" >> "$LOGFILE"
exit 1
fi
# Timestamp per la directory del backup
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
if [ -z "$MYSQLDUMP_BIN" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR: mariadb-dump not found in PATH" >> "$LOGFILE"
exit 1
fi
# Directory di destinazione del backup
TARGET="$BACKUP_BASE/${PREFIX}${TIMESTAMP}"
# Crea directory del backup
mkdir -p "$TARGET"
chown mysql:mysql "$TARGET"
chmod 750 "$TARGET"
# File temporaneo per catturare errori di mariadb-dump
TMPLOG=$(mktemp /tmp/mariadb-backup-logic.XXXXXX)
# File temporaneo per catturare errori del dump
TMPLOG=$(mktemp /tmp/mariadb-backup-logic.XXXXXX)
# Crea la directory del backup con permessi sicuri
mkdir -p "$TARGET"
chown mysql:mysql "$TARGET"
chmod 750 "$TARGET"
echo "---------------------" >> "$LOGFILE"
echo "[$(date '+%F %T')] [$SERVER_NAME] START logic backup (per-database) $TARGET" >> "$LOGFILE"
# Log di inizio backup
echo "[$(date '+%F %T')] [$SERVER_NAME] START logic backup $TARGET" >> "$LOGFILE"
# Elenco database utente (escludiamo schemi di sistema)
DB_LIST=$($MYSQL_BIN -N -e "SHOW DATABASES" | grep -vE '^(information_schema|performance_schema|mysql|sys)$' || true)
# Elenco dei database utente (escludiamo schemi di sistema)
DB_LIST=$($MYSQL_BIN -N -e "SHOW DATABASES" | grep -vE '^(information_schema|performance_schema|mysql|sys)$' || true)
if [ -z "$DB_LIST" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] WARNING: no user databases found to dump" >> "$LOGFILE"
fi
# Flag di fallimento globale
FAILED=0
FAILED_DBS=""
FAILED=0
FAILED_DBS=""
# Ciclo su ogni database
for DB in $DB_LIST; do
# Loop su ogni database
for db in $DB_LIST; do
DUMPFILE="$TARGET/${db}.sql"
echo "[$(date '+%F %T')] [$SERVER_NAME] START dump database '$db' -> $DUMPFILE" >> "$LOGFILE"
# File di destinazione del dump
DUMPFILE="$TARGET/${DB}.sql"
attempt=1
success=0
# Log di inizio dump
echo "[$(date '+%F %T')] [$SERVER_NAME] START dump database '$DB' -> $DUMPFILE" >> "$LOGFILE"
# Retry intelligente in caso di deadlock (errore 1213)
while [ "$attempt" -le "$MAX_RETRIES" ]; do
: >"$TMPLOG" # pulisce il file temporaneo
# Tentativi per questo database
ATTEMPT=1
SUCCESS=0
if $MYSQLDUMP_BIN $MYSQLDUMP_OPTS "$db" >"$DUMPFILE" 2>"$TMPLOG"; then
echo "[$(date '+%F %T')] [$SERVER_NAME] END dump database '$db' (attempt $attempt)" >> "$LOGFILE"
success=1
break
else
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR dumping database '$db' (attempt $attempt). See $TMPLOG" >> "$LOGFILE"
tail -n 50 "$TMPLOG" >> "$LOGFILE"
# Ciclo di retry per il singolo database
while [ "$ATTEMPT" -le "$DB_MAX_RETRIES" ]; do
# Se è un deadlock, riproviamo
if grep -q "Error 1213" "$TMPLOG"; then
echo "[$(date '+%F %T')] [$SERVER_NAME] RETRY for database '$db' in ${RETRY_SLEEP}s due to deadlock (1213)" >> "$LOGFILE"
sleep "$RETRY_SLEEP"
else
# Errori non recuperabili → stop retry
echo "[$(date '+%F %T')] [$SERVER_NAME] NON-RETRYABLE error for database '$db'" >> "$LOGFILE"
break
# Pulisce il file temporaneo
: >"$TMPLOG"
# Esegue il dump
if $MYSQLDUMP_BIN $MYSQLDUMP_OPTS "$DB" >"$DUMPFILE" 2>"$TMPLOG"; then
# Dump riuscito
echo "[$(date '+%F %T')] [$SERVER_NAME] END dump database '$DB' (attempt $ATTEMPT)" >> "$LOGFILE"
SUCCESS=1
break
else
# Dump fallito
echo "[$(date '+%F %T')] [$SERVER_NAME] ERROR dumping database '$DB' (attempt $ATTEMPT). See $TMPLOG" >> "$LOGFILE"
tail -n 50 "$TMPLOG" >> "$LOGFILE"
# Deadlock 1213 → retry
if grep -q "Error 1213" "$TMPLOG"; then
echo "[$(date '+%F %T')] [$SERVER_NAME] RETRY for '$DB' in ${DB_RETRY_SLEEP}s due to deadlock (1213)" >> "$LOGFILE"
sleep "$DB_RETRY_SLEEP"
else
# Errore non recuperabile → interrompe retry
echo "[$(date '+%F %T')] [$SERVER_NAME] NON-RETRYABLE error for '$DB'" >> "$LOGFILE"
break
fi
fi
# Incrementa tentativo
ATTEMPT=$((ATTEMPT + 1))
done
# Se il database non è stato dumpato correttamente
if [ "$SUCCESS" -ne 1 ]; then
FAILED=1
FAILED_DBS="$FAILED_DBS $DB"
fi
fi
attempt=$((attempt + 1))
done
# Se dopo i retry non è andata → segna come fallito
if [ "$success" -ne 1 ]; then
FAILED=1
FAILED_DBS="$FAILED_DBS $db"
# Calcolo dimensioni apparenti e su disco
SIZE_APPARENT_BYTES=$(find "$TARGET" -type f -printf '%s\n' | awk '{s+=$1} END{print s+0}')
SIZE_APPARENT_HUMAN=$(numfmt --to=iec --suffix=B "$SIZE_APPARENT_BYTES")
SIZE_DISK_BYTES=$(du -s --block-size=1 "$TARGET" | cut -f1)
SIZE_DISK_HUMAN=$(numfmt --to=iec --suffix=B "$SIZE_DISK_BYTES")
# Log delle dimensioni
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE apparent: $SIZE_APPARENT_HUMAN ($SIZE_APPARENT_BYTES bytes)" >> "$LOGFILE"
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE on-disk: $SIZE_DISK_HUMAN ($SIZE_DISK_BYTES bytes)" >> "$LOGFILE"
# Se tutto è andato bene
if [ "$FAILED" -eq 0 ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: OK" >> "$LOGFILE"
rm -f "$TMPLOG"
return 0
else
# Fallimento → pulizia directory
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: FAILED, databases with errors:$FAILED_DBS" >> "$LOGFILE"
echo "[$(date '+%F %T')] [$SERVER_NAME] CLEANUP: removing incomplete backup directory $TARGET" >> "$LOGFILE"
rm -rf "$TARGET"
rm -f "$TMPLOG"
return 1
fi
done
}
# Calcolo dimensioni del backup
size_apparent_bytes=$(find "$TARGET" -type f -printf '%s\n' 2>/dev/null | awk '{s+=$1} END{print s+0}')
size_apparent_human=$(numfmt --to=iec --suffix=B "$size_apparent_bytes" 2>/dev/null || echo "${size_apparent_bytes}B")
###############################################################################
# BLOCCO PRINCIPALE CON FLOCK E RETRY GLOBALE
###############################################################################
(
# Evita esecuzioni concorrenti
flock -n 9 || {
echo "[$(date '+%F %T')] [$SERVER_NAME] SKIP: another backup is running" >> "$LOGFILE"
exit 0
}
size_disk_bytes=$(du -s --block-size=1 "$TARGET" 2>/dev/null | cut -f1 || echo 0)
size_disk_human=$(numfmt --to=iec --suffix=B "$size_disk_bytes" 2>/dev/null || echo "${size_disk_bytes}B")
# Ciclo di retry globale
for GLOBAL_ATTEMPT in $(seq 1 $GLOBAL_MAX_RETRIES); do
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE apparent: $size_apparent_human ($size_apparent_bytes bytes) for $TARGET" >> "$LOGFILE"
echo "[$(date '+%F %T')] [$SERVER_NAME] SIZE on-disk: $size_disk_human ($size_disk_bytes bytes) for $TARGET" >> "$LOGFILE"
# Separatore visivo per ogni tentativo globale
echo "" >> "$LOGFILE"
echo "---------------------" >> "$LOGFILE"
# Risultato finale con cleanup in caso di fallimento
if [ "$FAILED" -eq 0 ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: OK, all databases dumped successfully" >> "$LOGFILE"
else
echo "[$(date '+%F %T')] [$SERVER_NAME] RESULT: FAILED, databases with errors:$FAILED_DBS" >> "$LOGFILE"
echo "[$(date '+%F %T')] [$SERVER_NAME] CLEANUP: removing incomplete backup directory $TARGET" >> "$LOGFILE"
rm -rf "$TARGET"
rm -f "$TMPLOG"
exit 1
fi
# Log del tentativo globale
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL attempt $GLOBAL_ATTEMPT/$GLOBAL_MAX_RETRIES" >> "$LOGFILE"
rm -f "$TMPLOG"
# Esegue un tentativo completo
if run_backup; then
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RESULT: SUCCESS" >> "$LOGFILE"
break
else
# Se non è lultimo tentativo → retry globale
if [ "$GLOBAL_ATTEMPT" -lt "$GLOBAL_MAX_RETRIES" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RETRY in ${GLOBAL_RETRY_SLEEP}s" >> "$LOGFILE"
sleep "$GLOBAL_RETRY_SLEEP"
else
# Fallimento definitivo
echo "[$(date '+%F %T')] [$SERVER_NAME] GLOBAL RESULT: FAILED after $GLOBAL_MAX_RETRIES attempts" >> "$LOGFILE"
exit 1
fi
fi
done
# Rotazione: elimina backup più vecchi di 7 giorni
TO_DELETE=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "${PREFIX}*" -mtime +7 -print 2>/dev/null || true)
# Rotazione: elimina backup logici più vecchi di 7 giorni
TO_DELETE=$(find "$BACKUP_BASE" -maxdepth 1 -type d -name "${PREFIX}*" -mtime +7)
if [ -n "$TO_DELETE" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: removing old logic backups:" >> "$LOGFILE"
echo "$TO_DELETE" >> "$LOGFILE"
echo "$TO_DELETE" | tr '\n' '\0' | xargs -0 -r rm -rf --
else
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: no logic backups to remove (<=7 days)" >> "$LOGFILE"
fi
echo " " >> "$LOGFILE"
if [ -n "$TO_DELETE" ]; then
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: removing old logic backups:" >> "$LOGFILE"
echo "$TO_DELETE" | tr '\n' '\0' | xargs -0 -r rm -rf --
else
echo "[$(date '+%F %T')] [$SERVER_NAME] ROTATE: no logic backups to remove (<=7 days)" >> "$LOGFILE"
fi
) 9>"$LOCKFILE"
@@ -0,0 +1,56 @@
#!/bin/bash
set -euo pipefail
SRC="/opt/redmine/redmine_files"
BASE="/var/backups/tscale01/redmine_files"
PREFIX="redmine_files"
KEEP=3
LOG="/var/log/backup-redmine-files.log"
LOCK="/var/lock/redmine-files-backup.lock"
timestamp="$(date '+%Y%m%d_%H%M%S')"
DEST="${BASE}/${PREFIX}-${timestamp}"
log() {
printf "%s %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOG"
}
cleanup() {
rm -f "$LOCK"
}
trap cleanup EXIT
# Lock semplice
if ! ( set -o noclobber; echo "$$" > "$LOCK" ) 2>/dev/null; then
log "ERRORE: un'altra istanza è in esecuzione."
exit 1
fi
# Verifiche base
[ -d "$SRC" ] || { log "ERRORE: sorgente $SRC inesistente"; exit 1; }
mkdir -p "$BASE"
mkdir -p "$DEST"
log "Inizio snapshot → $DEST"
rsync -a --delete "$SRC"/ "$DEST"/
log "Snapshot completato."
# Retention per numero: tieni gli ultimi $KEEP, cancella il resto
log "Avvio retention: mantengo ultimi $KEEP snapshot."
# Lista directory in ordine dal più recente al più vecchio
snapshots=( $(ls -1dt "${BASE}/${PREFIX}-"* 2>/dev/null || true) )
if [ "${#snapshots[@]}" -gt "$KEEP" ]; then
to_delete=( "${snapshots[@]:$KEEP}" )
for dir in "${to_delete[@]}"; do
log "Elimino snapshot vecchio: $dir"
rm -rf -- "$dir"
done
fi
log "Retention completata."
exit 0