CreaRack-SL

Copia de seguridad y restauración completa en segundo plano (v1.56.0 · ADR T2 2/2)

Descripción

Migración del flujo de backup y restore completo del patrón síncrono (bloqueaba el worker ASGI durante ~4-10 min) al patrón AsyncJob + polling de v1.55.0 (Auto-Plan).

Cambios de arquitectura

Backup Completo: antes vs. después

  • Antes (v1.55.0): GET /api/racks/backup/full bloqueaba el worker, generaba el ZIP en memoria/disco temporal y lo servía.
  • Ahora (v1.56.0):
    • POST /api/racks/backup/full/start → encola la tarea en Huey (Redis), responde 202 + job_id inmediatamente.
    • Cliente hace polling a GET /api/jobs/{job_id} cada 2-3 seg hasta status: "done".
    • Al estar done, descarga el ZIP con GET /api/racks/backup/full/download/{job_id} → filestreaming sin cargar en RAM.
    • El ZIP se auto-limpia tras servirse (POSIX unlink + filehandle abierto = inode vivo).

Restore Completo: antes vs. después

  • Antes: POST /api/racks/restore/full (upload + form) bloqueaba el worker durante la transacción todo-o-nada (~13 modelos).
  • Ahora:
    • POST /api/racks/restore/full → guarda el ZIP en disco, encola la restauración en Huey, responde 202 + job_id.
    • Cliente hace polling a GET /api/jobs/{job_id} hasta status: "done".
    • Al terminar, result contiene los contadores (n_racks creados, n_devices, etc.) y un flag de éxito/rollback.

Funciones reutilizables extraídas

  1. _collect_backup_data(org) → dict: Serializa todo el inventario de la org (13 modelos) a dict JSON-able. Reutilizable para futuros formatos (CSV, DB migrations).

  2. write_full_backup_zip(org, zip_path: str) → None: Escribe el ZIP en disco. Punto único de generación (Huey task + descarga stream).

  3. apply_full_restore_from_zip(org, zip_data: dict) → dict: Restaura modelos desde el dict. Transacción todo-o-nada, devuelve contadores.

Datos serializados

~13 modelos por org:

  • Rack, Device, RackGroup, BoxCategory, Stencil
  • Blueprint, BlueprintPlacement, MapAnnotation
  • ConfigBackup, ScriptTemplate, Script
  • MonitoringTarget, MonitoringAlert, DeviceProfile
  • AIPrompt

+ uploads: Solo archivos referenciados por esta org (stencils, blueprints). No un walk recursivo de uploads/ (evita bloat y fuga cross-tenant).

Seguridad:

  • RLS + ORM en los endpoints → aislamiento por org al descargar.
  • Creds SNMP se exportan en claro al ZIP (T1 decision) y se re-cifran al restaurar.

Frontend

  • Backup: alpine-components.js → encola + polling + tope 6 min + toast de estado.
  • Restore: base.js → encola + polling + tope 6 min + recarga al finish.

Reuso de patterns

El patrón AsyncJob fue estrenado en v1.55.0 (Auto-Plan). Con v1.56.0 cubre:

  • ✅ Análisis de planos (v1.55.0)
  • ✅ Backup completo (v1.56.0)
  • ✅ Restore completo (v1.56.0)
  • ⏳ Future: exportación PDF masiva, reportes IA

Tests

tests/api/test_backup_restore_async.py (3 tests):

  1. Encolado: POST /backup/full/start responde 202 + job_id.
  2. Descarga con aislamiento: GET /backup/full/download/{job_id} sirve solo si la org coincide + job está done.
  3. Round-trip: restauración desde ZIP con contadores correctos.

Actualización 23-08-2026 (v1.82.3, task #245) — los dominios EXTENDIDOS y el caso de los patrones fantasma

Desde el módulo racks/api/export/backup_domains.py (collect_extended_domains) / restore_domains.py (restore_extended_domains), la copia también viaja con dominios que se añadieron después de v1.56.0: KnownIssue, MaintenanceWindow, NotificationChannel, Runbook, SLAPolicy, Signage CMS, etc. — la misma mecánica de arriba (dict JSON-able → ZIP → restore con upsert/create), pero para las entidades de ITSM y señalización en vez del inventario físico de racks.

El incidente: patrones que nunca existieron

Hasta v1.82.2, collect_extended_domains incluía también RecurringPattern (patrones de incidencias recurrentes, ver [[feature—monitoring—itsm-ciclo-cierre]]). Tras la mudanza “Dos Casas” (reasignación de organización de un cliente), el CCIB vio en su panel 68 “patrones recurrentes” con 419 incidencias asociadas que nunca ocurrieron en su organización — dos paneles de la misma pantalla contradiciéndose entre sí (los patrones decían una cosa, el histórico de incidencias real decía otra).

Causa raíz: RecurringPattern es un artefacto derivado — lo genera el detector diario (03:00) analizando las incidencias reales de una organización. Las incidencias en sí no viajan en la copia (por diseño: son datos operativos, no configuración). Al restaurar los patrones sin las incidencias que los originaron, el ZIP fabricaba estadísticas sin ningún dato real detrás.

El fix

RecurringPattern deja de serializarse en collect_extended_domains y de restaurarse en restore_extended_domains — ni en backups nuevos, ni al leer ZIPs antiguos que aún lo traigan (el bloque de restore se ignora a propósito, ver comentario en el código). El detector diario los reconstruye solos en cuanto la organización destino tiene incidencias reales que analizar — es la garantía que el propio pattern_service ya declaraba (“si reaparece, el detector lo recrea”).

Los patrones fantasma que ya existían en producción no se limpiaron con una migración expresa: caducan solos hacia el 18-09-2026 vía la purga diaria de 30 días que ya documenta [[feature—monitoring—itsm-ciclo-cierre]].

Lección: un dominio “extendido” que se añade a un backup ya en producción hereda implícitamente la promesa de portabilidad cross-org del backup — pero solo vale para datos de configuración, no para artefactos derivados de datos que el propio backup no transporta. La pregunta a hacerse antes de añadir un modelo a collect_extended_domains: ¿de qué otro dato depende este, y viaja también en la copia?

Véase también

  • [[feature—blueprints—autoplan-async-jobs-v155]]
  • [[entity—core—model—asyncjob]]
  • [[entity—racks—endpoint—backup-full-start]]
  • [[entity—racks—endpoint—backup-full-download]]
  • [[entity—racks—service—run-full-backup]]
  • [[entity—racks—service—apply-full-restore-from-zip]]
  • [[concept—saas—multi-tenancy]]
  • [[concept—backend—async-jobs]]
  • [[feature—monitoring—itsm-ciclo-cierre]]