170 lines
7.4 KiB
Bash
Executable File
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"
|