Files
dh-inspeccion-v2/scripts/backup-inventario-a-revisar.sh
T

170 lines
7.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
ROOT="${ROOT:-${1:-/var/www/dhv2.korexlabs.com}}"
BACKUP_ROOT="${BACKUP_ROOT:-/root/DH_V2_BACKUPS}"
STAMP="${STAMP:-$(date +%Y%m%d_%H%M%S)}"
BACKUP_DIR="${BACKUP_DIR:-$BACKUP_ROOT/INVENTARIO_A_REVISAR_$STAMP}"
MARKER="$BACKUP_DIR/INVENTARIO A REVISAR.txt"
MIN_FREE_KB=$((12 * 1024 * 1024))
fail() { echo "ERROR BACKUP INVENTARIO A REVISAR: $*" >&2; exit 1; }
cd "$ROOT"
[[ -f docker-compose.yml ]] || fail "No existe docker-compose.yml en $ROOT"
[[ -f api-v3/package.json ]] || fail "No se encontró api-v3/package.json"
FREE_KB="$(df -Pk "$ROOT" | awk 'NR==2 {print $4}')"
[[ "$FREE_KB" =~ ^[0-9]+$ ]] || fail "No se pudo determinar espacio libre"
(( FREE_KB >= MIN_FREE_KB )) || fail "Se requieren al menos 12 GB libres para el backup fuerte; hay $(( FREE_KB / 1024 / 1024 )) GB"
DB_HEALTH="$(docker inspect dhv2-db --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' 2>/dev/null || true)"
[[ "$DB_HEALTH" == "healthy" ]] || fail "PostgreSQL no está healthy (estado: $DB_HEALTH)"
docker compose exec -T db sh -lc 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"' >/dev/null
umask 077
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
API_VERSION="$(node -p "require('./api-v3/package.json').version")"
WEB_VERSION="$(node -p "require('./web-v2/package.json').version")"
PHASE="$(sed -n "s/.*API_PHASE = '\([^']*\)'.*/\1/p" api-v3/src/version.ts | head -n1)"
cat > "$MARKER" <<TXT
INVENTARIO A REVISAR
Backup fuerte previo al cambio conceptual de inventarios.
Objetivo: poder volver al estado exacto anterior si la definición cambia después de la reunión.
Fecha: $(date --iso-8601=seconds)
Servidor: $(hostname)
Proyecto: $ROOT
API: $API_VERSION
WEB: $WEB_VERSION
Fase: $PHASE
Este backup contiene:
- dump PostgreSQL en formato custom;
- código fuente del proyecto;
- volumen completo de asset-media/importaciones;
- imágenes Docker actuales de api/web/migrate;
- docker-compose y .env (si existe, permisos 600);
- estado de contenedores, migraciones y hashes SHA-256.
TXT
chmod 600 "$MARKER"
printf '%s\n' "[1/8] PostgreSQL"
docker compose exec -T db sh -lc 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' > "$BACKUP_DIR/database.dump"
[[ -s "$BACKUP_DIR/database.dump" ]] || fail "database.dump quedó vacío"
docker compose exec -T db sh -lc 'pg_restore -l' < "$BACKUP_DIR/database.dump" > "$BACKUP_DIR/database-list.txt"
[[ -s "$BACKUP_DIR/database-list.txt" ]] || fail "No se pudo validar database.dump"
cat <<'SQL' | docker compose exec -T db sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1' > "$BACKUP_DIR/inventory-state.txt"
\echo '========== INVENTARIO A REVISAR · ESTADO DEL MAESTRO =========='
SELECT count(*) AS assets_total FROM assets;
SELECT type.code,type.name,count(asset.id) AS cantidad FROM asset_types type LEFT JOIN assets asset ON asset.asset_type_id=type.id AND asset.information_status<>'INACTIVE' GROUP BY type.id,type.code,type.name ORDER BY type.code;
\echo ''
\echo '========== IMPORTACIONES =========='
SELECT batch.id,batch.original_name,batch.status,plan.revision,plan.status AS plan_status,plan.applied_at FROM asset_import_batches batch LEFT JOIN LATERAL (SELECT revision,status,applied_at FROM asset_import_plans p WHERE p.batch_id=batch.id ORDER BY revision DESC LIMIT 1) plan ON true ORDER BY batch.created_at DESC;
SQL
printf '%s\n' "[2/8] Source"
tar \
--exclude='api-v3/node_modules' \
--exclude='api-v3/dist' \
--exclude='web-v2/node_modules' \
--exclude='web-v2/dist' \
--exclude='.git' \
--exclude='DH_V2_FASE_*.zip' \
-czf "$BACKUP_DIR/source.tar.gz" \
api-v3 web-v2 docs scripts infra docker-compose.yml README.md .env.example
[[ -s "$BACKUP_DIR/source.tar.gz" ]] || fail "source.tar.gz quedó vacío"
tar -tzf "$BACKUP_DIR/source.tar.gz" >/dev/null
printf '%s\n' "[3/8] Configuración privada"
cp -a docker-compose.yml "$BACKUP_DIR/docker-compose.yml"
if [[ -f .env ]]; then
cp -a .env "$BACKUP_DIR/.env"
chmod 600 "$BACKUP_DIR/.env"
fi
printf '%s\n' "[4/8] Asset media / archivos importados"
docker volume inspect dhv2_asset_media >/dev/null
# nginx:alpine ya forma parte del stack web y aporta tar/gzip BusyBox sin instalar nada en el host.
docker run --rm \
-v dhv2_asset_media:/source:ro \
-v "$BACKUP_DIR:/backup" \
--entrypoint sh nginx:alpine \
-c 'cd /source && tar -czf /backup/asset-media.tar.gz .'
[[ -s "$BACKUP_DIR/asset-media.tar.gz" ]] || fail "asset-media.tar.gz quedó vacío"
tar -tzf "$BACKUP_DIR/asset-media.tar.gz" >/dev/null
printf '%s\n' "[5/8] Imágenes Docker exactas"
for image in dhv2-api:latest dhv2-web:latest dhv2-migrate:latest; do
docker image inspect "$image" >/dev/null || fail "Falta la imagen $image"
done
docker image save dhv2-api:latest dhv2-web:latest dhv2-migrate:latest | gzip -1 > "$BACKUP_DIR/docker-images.tar.gz"
[[ -s "$BACKUP_DIR/docker-images.tar.gz" ]] || fail "docker-images.tar.gz quedó vacío"
gzip -t "$BACKUP_DIR/docker-images.tar.gz"
printf '%s\n' "[6/8] Estado operativo"
docker compose ps -a > "$BACKUP_DIR/docker-status.txt"
for image in dhv2-api:latest dhv2-web:latest dhv2-migrate:latest; do docker image inspect "$image" --format '{{.RepoTags}} {{.Id}} {{.Created}}'; done > "$BACKUP_DIR/docker-images.txt"
docker compose --profile tools run --rm migrate npm run migration:show > "$BACKUP_DIR/migrations.txt" 2>&1
grep -Fq 'Pending migrations: no' "$BACKUP_DIR/migrations.txt" || fail "Hay migraciones pendientes; backup no se marca como aceptado"
curl -fsS http://127.0.0.1:3101/api/v3/health > "$BACKUP_DIR/health.json"
printf '%s\n' "[7/8] Runbook de restauración"
cat > "$BACKUP_DIR/RESTORE_INVENTARIO_A_REVISAR.txt" <<'TXT'
RESTAURACIÓN — INVENTARIO A REVISAR
No ejecutar estas acciones durante operación normal. Este paquete es un punto de retorno.
Contenido principal:
- database.dump PostgreSQL completo
- source.tar.gz código fuente previo
- asset-media.tar.gz volumen de archivos/importaciones
- docker-images.tar.gz imágenes exactas api/web/migrate
- .env secretos/configuración previa (si existía)
- docker-compose.yml compose previo
Orden recomendado de recuperación:
1) detener api/web/migrate y mantener una copia del estado actual;
2) restaurar source.tar.gz y .env/docker-compose.yml;
3) cargar docker-images.tar.gz con: gzip -dc docker-images.tar.gz | docker image load;
4) restaurar PostgreSQL desde database.dump con el usuario owner;
5) restaurar dhv2_asset_media desde asset-media.tar.gz;
6) recrear api/web y verificar /api/v3/health y migration:show.
La restauración destructiva debe hacerse sólo después de revisar el incidente y conservar un backup del estado que se reemplazará.
TXT
chmod 600 "$BACKUP_DIR/RESTORE_INVENTARIO_A_REVISAR.txt"
printf '%s\n' "[8/8] Integridad SHA-256"
cat > "$BACKUP_DIR/BACKUP_OK.txt" <<TXT
INVENTARIO A REVISAR — BACKUP OK
Fecha: $(date --iso-8601=seconds)
API: $API_VERSION
WEB: $WEB_VERSION
Fase: $PHASE
DB: validada con pg_restore -l
Asset media: tar validado
Docker images: gzip validado
SHA-256: OK
TXT
chmod 600 "$BACKUP_DIR/BACKUP_OK.txt"
(
cd "$BACKUP_DIR"
find . -maxdepth 1 -type f ! -name 'SHA256SUMS.txt' -printf '%P\0' \
| sort -z \
| xargs -0 sha256sum
) > "$BACKUP_DIR/SHA256SUMS.txt"
chmod 600 "$BACKUP_DIR/SHA256SUMS.txt"
(
cd "$BACKUP_DIR"
sha256sum -c SHA256SUMS.txt >/dev/null
) || fail "Falló la validación SHA-256"
printf '\nBACKUP FUERTE INVENTARIO A REVISAR: OK\n'
printf 'Ruta: %s\n' "$BACKUP_DIR"
du -sh "$BACKUP_DIR"