CreaRack-SL

Servicio / Tarea Huey `run_full_backup()` — Generar ZIP de backup (v1.56.0)

{“related”: [“entity—racks—endpoint—backup-full-start”, “entity—racks—endpoint—backup-full-download”, “entity—racks—service—apply-full-restore-from-zip”, “feature—racks—backup-restore-async-v156”, “concept—backend—async-jobs”, “entity—core—service—backup-verify”, “feature—backup-restore—manifest-verificacion-v1-69-0”, “feature—backup-restore—creacion-selectiva-v1-74-0”], “content”: ”## Signature\n\npython\nfrom racks.tasks import run_full_backup\n\n@huey_task\ndef run_full_backup(job_id: str, org_id: int, include_signage_videos: bool = False, selection: dict | None = None) -> dict:\n \"\"\"Tarea Huey: genera y verifica el ZIP de backup completo de la org.\"\"\"\n\n\nLlamador: backup_full_start() endpoint (via run_full_backup(str(job.job_id), org.id, include_videos, selection)). \nQueue: Huey Redis. \nTimeout: ~5-10 minutos (variable según size org). \n\n## Descripción\n\nTarea asincrónica que genera y verifica el ZIP de backup completo de una organización, usando write_full_backup_zip() + verify_backup_zip() (task #236, v1.69.0).\n\n### Flujo\n\n1. Obtiene org = Organization.objects.get(id=org_id).\n2. Obtiene el AsyncJob por job_id.\n3. Marca el job como \"processing\" (+ progress si aplica).\n4. Llama write_full_backup_zip(org, zip_path, include_signage_videos=include_signage_videos, selection=selection):\n - _collect_backup_data(org) → serializa ~13 modelos.\n - Si selection no es None, poda el dict con backup_selection.prune_backup_data() ANTES de construir el manifest (v1.74.0).\n - Construye el manifest (build_manifest) a partir de los MISMOS bytes que se escriben.\n - Escribe backup_data.json, los uploads referenciados y manifest.json en el ZIP.\n - Devuelve el manifest (dict).\n5. Verifica verify_backup_zip(zip_path) releyendo el ZIP de disco:\n - Si ok=False: loguea los errores, borra el ZIP (os.unlink), marca el job en \"error\" (“Backup failed integrity verification. Please try again.”) y termina.\n - Si ok=True: continúa.\n6. Marca el job como \"done\" con result: { \"filename\": \"...\", \"download_url\": \"...\", \"verified\": true, \"counts\": {...} }.\n7. Si error no controlado: marca como \"error\" con el mensaje.\n\n### Datos serializados\n\nEstructura del backup_data.json (sin cambios estructurales en v1.72.1):\n\njson\n{\n \"version\": \"1.72.1\",\n \"timestamp\": \"2026-08-20T10:04:18.123456\",\n \"organization\": { \"id\": 1, \"name\": \"ACME Corp\", \"...\": \"...\" },\n \"racks\": [ { \"...\": \"...\", \"devices\": [ { \"...\": \"...\" } ] } ],\n \"groups\": [ \"...\" ], \"device_groups\": [ \"...\" ], \"box_categories\": [ \"...\" ],\n \"stencils\": [ \"...\" ], \"blueprints\": [ \"...\" ], \"config_backups\": [ \"...\" ],\n \"script_templates\": [ \"...\" ], \"scripts\": [ \"...\" ],\n \"monitoring_targets\": [ \"...\" ], \"monitoring_alerts\": [ \"...\" ],\n \"device_profiles\": [ \"...\" ], \"ai_prompts\": [ \"...\" ]\n}\n\n\n### Fix v1.72.1 — device_profiles incompleto en el ZIP (task #236, PR #400)\n\n_collect_backup_data() serializaba DeviceProfile con un subconjunto de campos anterior a la iniciativa “Ficha Central”: faltaban assigned_page (la clasificación de página — Wireless/UPS/DSM/etc.) y otros 12 campos — role, location, notes, manual_fields, group_ids (vía dp.groups, con prefetch_related), y las credenciales propias de la ficha (snmp_v3_username, snmp_v3_auth_protocol, snmp_v3_auth_key, snmp_v3_priv_protocol, snmp_v3_priv_key, ssh_username, ssh_password_encrypted, ssh_enable_password_encrypted). Efecto: al restaurar, las fichas Wireless/UPS/DSM (y cualquier ficha con campos manuales) llegaban vacías. Cazado por Edu en el click-test del ensayo, antes de release.\n\nLas credenciales viajan cifradas tal cual dentro del ZIP — solo son restaurables en la misma instalación (coherente con la decisión de no exportar secretos del credential store en claro).\n\nTest estructural anti-deriva en tests/api/test_backup_scope.py: un campo nuevo en DeviceProfile sin exportar ni excluir a conciencia rompe el CI.\n\nVer el fallback de assigned_page para ZIPs legacy (backups previos al 20-08-2026, sin este campo) en [[entity—racks—service—apply-full-restore-from-zip]].\n\n### Manifest (task #236, v1.69.0)\n\nAdemás de backup_data.json y los uploads, el ZIP incluye ahora manifest.json:\n\njson\n{\n \"manifest_version\": 1,\n \"created_at\": \"2026-08-20T10:04:18.123456\",\n \"app_version\": \"1.72.1\",\n \"organization\": \"ACME Corp\",\n \"counts\": { \"racks\": 12, \"devices\": 340, \"blueprints\": 3, \"placements\": 40, \"annotations\": 8, \"...\": \"...\" },\n \"uploads\": [\"uploads/stencils/s1.png\", \"...\"],\n \"backup_data_sha256\": \"…64 hex chars…\"\n}\n\n\nConstruido por core.services.backup_verify.build_manifest() a partir de los MISMOS bytes que se escriben a backup_data.json (evita divergencia por doble serialización). Ver [[entity—core—service—backup-verify]].\n\n### Backup parcial — selección del diálogo de creación (task #236, v1.74.0)\n\nDesde v1.74.0, selection (normalizada por backup_selection.normalize_selection(), ver [[feature—backup-restore—creacion-selectiva-v1-74-0]]) poda backup_data ANTES de calcular el manifest y escribir el ZIP, reutilizando backup_selection.prune_backup_data(). Si selection no es None:\n\n- El nombre del fichero final lleva el sufijo _partial (crearack_backup_<org>_<timestamp>_partial.zip), para que nunca se confunda con un backup completo.\n- El manifest declara partial: true + la propia selección aplicada.\n- Sin selection (caso por defecto, incluido el backup nocturno) el resultado es idéntico al histórico — byte-compatible.\n\n### Uploads incluidos\n\nCriterio: Solo archivos referenciados por esta org, no un walk recursivo de uploads/.\n\nFuentes:\n- Paths de stencils: stencil.image_path.\n- Paths de blueprints: blueprint.image_path.\n\nBeneficio: Evita:\n- 📦 Bloat: No empaquetar los uploads de todas las orgs.\n- 🔒 Fuga cross-tenant: Cada org ve solo sus archivos.\n\n### Función auxiliar: write_full_backup_zip()\n\npython\ndef write_full_backup_zip(org, zip_path: str) -> dict:\n \"\"\"Escribe el ZIP de backup en `zip_path` (datos + uploads + manifest). Devuelve el manifest.\"\"\"\n backup_data = _collect_backup_data(org)\n json_bytes = json.dumps(backup_data, indent=2).encode(\"utf-8\")\n uploads = collect_org_upload_files(org_upload_image_paths(backup_data))\n manifest = build_manifest(backup_data, json_bytes, [arcname for _, arcname in uploads])\n with zipfile.ZipFile(zip_path, \"w\", zipfile.ZIP_DEFLATED) as zipf:\n zipf.writestr(\"backup_data.json\", json_bytes)\n for abs_path, arcname in uploads:\n zipf.write(abs_path, arcname)\n zipf.writestr(MANIFEST_NAME, json.dumps(manifest, indent=2))\n return manifest\n\n\nPunto único de generación: Huey task + backup automático nocturno (core.services.backup_service.generate_backup(), desde v1.69.0) reusean esta función. Ver [[entity—core—service—backup-service]].\n\n### Verificación de integridad (task #236, v1.69.0)\n\ncore.services.backup_verify.verify_backup_zip(zip_path) relee el ZIP de disco tras escribirlo y comprueba: manifest presente, checksum de backup_data.json, conteos declarados vs. reales, uploads declarados vs. presentes, y CRC de todos los miembros. Devuelve {\"ok\": bool, \"errors\": [...], \"counts\": {...}}. Un backup que no verifica no se sirve: se borra y el job termina en error. Ver [[entity—core—service—backup-verify]].\n\n### Error handling\n\n- DB error: Marca job como \"error\" con traceback.\n- Verificación de integridad fallida (task #236): borra el ZIP, marca job como \"error\" con mensaje al usuario.\n- Disk space: Si no hay espacio en ASYNC_BACKUP_DIR, error marcado en job.\n- Timeout Huey: Si la tarea tarda >timeout config (default 5-10 min), Huey la mata y marca como error.\n\n### Performance\n\n- Típico: 2-5 min para 100 racks + 2000 devices.\n- Bottleneck: Serialización JSON + compresión ZIP + relectura de verificación. IO-bound, no CPU-bound → OK en Huey.\n- No bloquea worker ASGI: El request devuelve inmediatamente con job_id.\n\n## Véase también\n\n- [[entity—racks—endpoint—backup-full-start]]\n- [[entity—racks—endpoint—backup-full-download]]\n- [[entity—racks—service—apply-full-restore-from-zip]]\n- [[feature—racks—backup-restore-async-v156]]\n- [[concept—backend—async-jobs]]\n- [[entity—core—service—backup-verify]]\n- [[feature—backup-restore—manifest-verificacion-v1-69-0]]\n- [[feature—backup-restore—creacion-selectiva-v1-74-0]]\n”}