#!/bin/bash # ============================================================= # BACKUP SCRIPT PARA HAZARD (Odoo 16 en Docker) # Respalda: Base de datos PostgreSQL + Módulos Enterprise + Módulos Custom + Config # Frecuencia: Cada 12 horas (cron) # Retención local: 15 días # Nota: Plesk sube estos archivos a Google Drive automáticamente # como parte del backup del dominio tesscorp.com.mx # ============================================================= set -euo pipefail # --- CONFIGURACIÓN --- SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_NAME="backup_hazard_$(date +%Y%m%d_%H%M%S)" BACKUP_DIR="$SCRIPT_DIR/backups" CUSTOM_ADDONS_PATH="$SCRIPT_DIR/Addons/custom" ENTERPRISE_ADDONS_PATH="$SCRIPT_DIR/Addons/enterprise" CONF_PATH="$SCRIPT_DIR/Conf" DOCKER_COMPOSE="$SCRIPT_DIR/docker-compose.yml" DOCKERFILE="$SCRIPT_DIR/Dockerfile" AGENTS_PATH="$SCRIPT_DIR/.agents" DB_CONTAINER="odoo-16-db-hazard-new" ODOO_CONTAINER="odoo-16-hazard-new" DB_NAME="hazard_new" DB_USER="odoo" LOG_FILE="$SCRIPT_DIR/logs/backup.log" MAX_RETENTION_DAYS=15 LOCK_FILE="/tmp/backup_hazard.lock" # --- FUNCIONES --- log() { echo "$1" } cleanup_on_exit() { rm -f "$LOCK_FILE" rm -f "$BACKUP_DIR/custom_$BACKUP_NAME.tar.gz" 2>/dev/null rm -f "$BACKUP_DIR/enterprise_$BACKUP_NAME.tar.gz" 2>/dev/null rm -f "$BACKUP_DIR/config_$BACKUP_NAME.tar.gz" 2>/dev/null rm -f "$BACKUP_DIR/db_$BACKUP_NAME.sql" 2>/dev/null } trap cleanup_on_exit EXIT # --- PREVENIR EJECUCIONES SIMULTÁNEAS --- if [ -f "$LOCK_FILE" ]; then LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null) if kill -0 "$LOCK_PID" 2>/dev/null; then log "⚠️ [$(date '+%Y-%m-%d %H:%M:%S')] Backup ya en ejecución (PID: $LOCK_PID). Saliendo." exit 1 else log "🔓 [$(date '+%Y-%m-%d %H:%M:%S')] Lock file huérfano encontrado. Limpiando..." rm -f "$LOCK_FILE" fi fi echo $$ > "$LOCK_FILE" # --- CREAR DIRECTORIOS --- mkdir -p "$BACKUP_DIR" mkdir -p "$(dirname "$LOG_FILE")" log "🚀 [$(date '+%Y-%m-%d %H:%M:%S')] Iniciando respaldo total de Hazard..." # --- VERIFICAR QUE LOS CONTENEDORES ESTÉN CORRIENDO --- if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then log "❌ [$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Contenedor $DB_CONTAINER no está corriendo. Abortando." exit 1 fi if ! docker ps --format '{{.Names}}' | grep -q "^${ODOO_CONTAINER}$"; then log "⚠️ [$(date '+%Y-%m-%d %H:%M:%S')] ADVERTENCIA: Contenedor $ODOO_CONTAINER no está corriendo. Continuando solo con DB..." fi # --- 1. RESPALDAR MÓDULOS CUSTOM --- log "📦 Comprimiendo módulos custom..." if [ -d "$CUSTOM_ADDONS_PATH" ]; then tar -czf "$BACKUP_DIR/custom_$BACKUP_NAME.tar.gz" -C "$(dirname "$CUSTOM_ADDONS_PATH")" "$(basename "$CUSTOM_ADDONS_PATH")" CUSTOM_SIZE=$(du -sh "$BACKUP_DIR/custom_$BACKUP_NAME.tar.gz" | cut -f1) log " ✓ Módulos custom comprimidos: $CUSTOM_SIZE" else log "⚠️ ADVERTENCIA: Ruta de módulos custom no encontrada: $CUSTOM_ADDONS_PATH" fi # --- 1b. RESPALDAR MÓDULOS ENTERPRISE --- log "📦 Comprimiendo módulos Enterprise (esto puede tardar)..." if [ -d "$ENTERPRISE_ADDONS_PATH" ]; then tar -czf "$BACKUP_DIR/enterprise_$BACKUP_NAME.tar.gz" -C "$(dirname "$ENTERPRISE_ADDONS_PATH")" "$(basename "$ENTERPRISE_ADDONS_PATH")" ENT_SIZE=$(du -sh "$BACKUP_DIR/enterprise_$BACKUP_NAME.tar.gz" | cut -f1) log " ✓ Módulos Enterprise comprimidos: $ENT_SIZE" else log "⚠️ ADVERTENCIA: Ruta de módulos Enterprise no encontrada: $ENTERPRISE_ADDONS_PATH" fi # --- 1c. RESPALDAR CONFIGURACIÓN (Docker + Odoo + Agents) --- log "📦 Comprimiendo configuración del proyecto..." tar -czf "$BACKUP_DIR/config_$BACKUP_NAME.tar.gz" \ -C "$SCRIPT_DIR" \ "$(basename "$CONF_PATH")" \ "$(basename "$DOCKER_COMPOSE")" \ "$(basename "$DOCKERFILE")" \ "$(basename "$AGENTS_PATH")" \ 2>/dev/null CONF_SIZE=$(du -sh "$BACKUP_DIR/config_$BACKUP_NAME.tar.gz" | cut -f1) log " ✓ Configuración comprimida: $CONF_SIZE" # --- 2. RESPALDAR BASE DE DATOS (SQL Dump) --- log "🗄️ Extrayendo base de datos desde el contenedor Docker..." if docker exec "$DB_CONTAINER" pg_dump -U "$DB_USER" "$DB_NAME" > "$BACKUP_DIR/db_$BACKUP_NAME.sql" 2>/dev/null; then DB_SIZE=$(du -sh "$BACKUP_DIR/db_$BACKUP_NAME.sql" | cut -f1) log " ✓ Base de datos exportada: $DB_SIZE" else log "❌ [$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Falló el dump de la base de datos." exit 1 fi # --- 3. VERIFICAR QUE EL DUMP NO ESTÉ VACÍO --- if [ ! -s "$BACKUP_DIR/db_$BACKUP_NAME.sql" ]; then log "❌ [$(date '+%Y-%m-%d %H:%M:%S')] ERROR: El dump de la base de datos está vacío. Abortando." exit 1 fi # --- 4. EMPAQUETAR TODO --- log "🎁 Creando paquete comprimido final..." tar -czf "$BACKUP_DIR/$BACKUP_NAME.tar.gz" -C "$BACKUP_DIR" \ "custom_$BACKUP_NAME.tar.gz" \ "enterprise_$BACKUP_NAME.tar.gz" \ "config_$BACKUP_NAME.tar.gz" \ "db_$BACKUP_NAME.sql" FINAL_SIZE=$(du -sh "$BACKUP_DIR/$BACKUP_NAME.tar.gz" | cut -f1) log " ✓ Paquete final: $FINAL_SIZE" # --- 5. VERIFICAR INTEGRIDAD DEL ARCHIVO --- if tar -tzf "$BACKUP_DIR/$BACKUP_NAME.tar.gz" > /dev/null 2>&1; then log " ✓ Integridad verificada: OK" else log "❌ [$(date '+%Y-%m-%d %H:%M:%S')] ERROR: El archivo de backup está corrupto." rm -f "$BACKUP_DIR/$BACKUP_NAME.tar.gz" exit 1 fi # Limpiar archivos temporales (también se limpian en trap EXIT) rm -f "$BACKUP_DIR/custom_$BACKUP_NAME.tar.gz" "$BACKUP_DIR/enterprise_$BACKUP_NAME.tar.gz" "$BACKUP_DIR/config_$BACKUP_NAME.tar.gz" "$BACKUP_DIR/db_$BACKUP_NAME.sql" # --- 6. POLÍTICA DE RETENCIÓN --- log "🧹 Ejecutando política de retención (Limpiando archivos de más de $MAX_RETENTION_DAYS días)..." DELETED_COUNT=$(find "$BACKUP_DIR" -name "backup_hazard_*.tar.gz" -type f -mtime +$MAX_RETENTION_DAYS -delete -print | wc -l) log " ✓ Archivos eliminados: $DELETED_COUNT" # --- 7. RESUMEN --- TOTAL_BACKUPS=$(find "$BACKUP_DIR" -name "backup_hazard_*.tar.gz" -type f | wc -l) TOTAL_SIZE=$(du -sh "$BACKUP_DIR" | cut -f1) echo "----------------------------------------------------" log "✅ ¡RESPALDO COMPLETADO CON ÉXITO!" log "📍 Archivo: $BACKUP_DIR/$BACKUP_NAME.tar.gz" log "📊 Tamaño: $FINAL_SIZE" log "📁 Total backups locales: $TOTAL_BACKUPS ($TOTAL_SIZE)" log "☁️ Google Drive: Plesk subirá este archivo en su próximo backup programado" echo "----------------------------------------------------"