# A01.P012 — Pipeline de Drones para Backup y Restauración de DASUTEN > **Estado**: 🟢 Pipeline implementado (2026-04-14) > **Pertenece a**: [A01 — Ecosistema ADN](./A01_dtic-ADN.md) > **Relacionado**: [A01.P009 — Evolución Dron ADN](./A01.P009_Evolucion-Dron-ADN.md) > **Caso de Uso**: [A04.P005 — DASUTEN sin DC](../dtic-DASUTEN/A04.P005_DASUTEN-sin-DC.md) > **Fecha**: 2026-04-14 > **Responsable**: Lic. Ricardo MONLA > **Versión**: 2.0 — Con verificación de consistencia (DBCC CHECKDB) de BD --- ## 📈 Progreso ``` Fase 0: ██████████ 100% Diseño de Arquitectura de Pipeline ✅ Fase 1: ██████████ 100% Drones Atómicos Fragmentados ✅ Fase 1.5: ██████████ 100% Orquestador con Intercomunicación JSON ✅ Fase 1.7: ██████████ 100% Implementación Real — Pipeline DASUTEN ✅ (2026-04-14) Fase 1.8: ██████████ 100% Verificación de Consistencia (DBCC CHECKDB) ✅ Fase 1.9: ██████████ 100% Pipeline Diferencial (Refresco diario) ✅ Fase 2: ░░░░░░░░░░ 0% Persistencia en Bitácora — Outputs en DB Fase 3: ░░░░░░░░░░ 0% Reintentos Automáticos — Retry con backoff Fase 4: ░░░░░░░░░░ 0% Condicionales — Skip si output indica "ya hecho" ``` --- ## 📋 Resumen Ejecutivo Este plan documenta el **Pipeline de Drones para Backup y Restauración de DASUTEN** — una implementación real donde múltiples drones atómicos se encadenan para formar el flujo de refresco de base de datos (Fase 8b), comunicándose entre sí mediante **archivos JSON** que actúan como medio de intercambio de contexto. **Caso de uso:** Refresco de la base de datos `sysdasuten` desde `srvv-fenix` (producción) hasta `dasu-sql4` (nuevo servidor), pasando por Google Drive como intermediario. **Problema resuelto:** Transferir 1.3 GB de backup entre servidores sin dependencia de Tailscale inestable, usando Google Drive como puente y verificando integridad con DBCC CHECKDB. **Solución:** Un **orquestador** que: 1. Lanza 6 drones atómicos secuencialmente 2. Lee el output JSON de cada dron completado 3. Inyecta el contexto en el siguiente dron 4. Decide continuar o abortar según el resultado ### Convención de Nombres (desde 2026-04-14) ``` dasuten__.rb Ejemplos: - dasuten_exportar_srvv-fenix.rb (acción en nodo específico) - dasuten_transferir_srvv-fenix-srv-ns8.rb (transferencia entre nodos) - dasuten_upload_srv-ns8-drive.rb (upload desde nodo a Drive) ``` --- ## 🎯 Objetivo Implementar un **Pipeline de Drones para el refresco de BD de DASUTEN** que permita: 1. **Fragmentar el flujo de backup** en 6 drones atómicos independientes (1 dron = 1 paso) 2. **Intercomunicar drones** mediante archivos JSON estandarizados (`/tmp/dron_*_output.json`) 3. **Orquestar el refresco completo** con un solo comando desde `srv-ns8` 4. **Registrar trazabilidad** de cada paso en la Bitácora Web 5. **Permitir re-ejecución** de pasos individuales sin re-correr todo el pipeline 6. **Verificar integridad** de la BD restaurada con `DBCC CHECKDB` --- ## 🏗️ Arquitectura del Pipeline ### Diagrama de Flujo (6 Drones Atómicos) ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ ORQUESTADOR (orquestador_pipeline.rb) │ │ │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ Contexto Compartido (Hash Ruby) │ │ │ │ - backup_ruta: ruta del archivo .bak │ │ │ │ - drive_ruta: URL de Google Drive │ │ │ │ - estado_integridad: resultado de DBCC CHECKDB │ │ │ │ - duracion_total: suma de duraciones de drones │ │ │ └────────────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘ │ │ 1. Lanza dron ▼ ┌─────────────────┐ │ DRON: │ Nodo: srvv-fenix │ exportar │ Tarea: BACKUP DATABASE WITH COMPRESSION │ │ Output: /tmp/dron_export_output.json └─────────────────┘ │ │ 2. Orquestador lee JSON, extrae backup_ruta ▼ ┌─────────────────┐ │ DRON: │ Nodo: srv-ns8 │ transferir │ Tarea: smbclient //fenix/BK_SQL → /var/tmp/ │ │ Output: /tmp/dron_transfer_output.json └─────────────────┘ │ │ 3. Orquestador lee JSON, extrae backup_local ▼ ┌─────────────────┐ │ DRON: │ Nodo: srv-ns8 │ upload │ Tarea: rclone copy → Google Drive │ │ Output: /tmp/dron_upload_output.json └─────────────────┘ │ │ 4. Orquestador lee JSON, extrae drive_ruta ▼ ┌─────────────────┐ │ DRON: │ Nodo: dasu-sql4 │ download │ Tarea: Invoke-WebRequest ← Google Drive │ │ Output: /tmp/dron_download_output.json └─────────────────┘ │ │ 5. Orquestador lee JSON, extrae backup_local ▼ ┌─────────────────┐ │ DRON: │ Nodo: dasu-sql4 │ restaurar │ Tarea: RESTORE DATABASE (sin CHECKDB) │ │ Output: /tmp/dron_restore_output.json └─────────────────┘ │ │ 6. Orquestador lee JSON, decide verificar ▼ ┌─────────────────┐ │ DRON: │ Nodo: dasu-sql4 │ verificar │ Tarea: DBCC CHECKDB + validaciones │ │ Output: /tmp/dron_verify_output.json └─────────────────┘ │ ▼ ✅ PIPELINE COMPLETADO (6 drones) ``` --- ## 🧩 Drones Atómicos del Pipeline (6 drones) Cada dron es **independiente**, **autocontenido** y **auto-bitácorado**. ### Dron 1: `dasuten_exportar_srvv-fenix.rb` | Campo | Valor | |-------|-------| | **Nodo** | `srvv-fenix` | | **Tarea** | Generar backup comprimido en SQL Server | | **Comando** | `BACKUP DATABASE sysdasuten TO DISK = ... WITH COMPRESSION` | | **Output File** | `/tmp/dron_export_output.json` | | **Output Clave** | `backup_ruta` | **Output JSON:** ```json { "paso": "export_complete", "backup_ruta": "E:\\BK_SQL\\sysdasuten_compressed_ADN.bak", "nodo": "srvv-fenix", "duracion": 45.2, "timestamp": "2026-04-14T10:00:00-03:00", "siguiente_paso": "transferir" } ``` --- ### Dron 2: `dasuten_transferir_srvv-fenix-srv-ns8.rb` | Campo | Valor | |-------|-------| | **Nodo** | `srv-ns8` | | **Tarea** | Descargar backup desde srvv-fenix vía SMB | | **Comando** | `smbclient //10.0.10.200/BK_SQL -c "get sysdasuten_compressed_ADN.bak"` | | **Output File** | `/tmp/dron_transfer_output.json` | | **Output Clave** | `backup_local` | **Output JSON:** ```json { "paso": "transfer_complete", "backup_local": "/var/tmp/sysdasuten_compressed_ADN.bak", "nodo": "srv-ns8", "duracion": 120.5, "timestamp": "2026-04-14T10:02:00-03:00", "siguiente_paso": "upload" } ``` --- ### Dron 3: `dasuten_upload_srv-ns8-drive.rb` | Campo | Valor | |-------|-------| | **Nodo** | `srv-ns8` | | **Tarea** | Subir backup a Google Drive vía rclone | | **Comando** | `rclone copy /var/tmp/... rmonla-GDrive:drive_bkps-dasu/...` | | **Output File** | `/tmp/dron_upload_output.json` | | **Output Clave** | `drive_ruta` | **Output JSON:** ```json { "paso": "upload_complete", "drive_ruta": "rmonla-GDrive:drive_bkps-dasu/sysdasuten_compressed_ADN_20260414_100500.bak", "drive_url": "https://drive.google.com/drive/folders/1Tgrn2QYyWf0v0eyY1bSGCLlhiqhtIA2n", "nodo": "srv-ns8", "duracion": 180.5, "timestamp": "2026-04-14T10:05:00-03:00", "siguiente_paso": "download" } ``` --- ### Dron 4: `dasuten_download_drive-dasu-sql4.rb` | Campo | Valor | |-------|-------| | **Nodo** | `dasu-sql4` | | **Tarea** | Descargar backup desde Google Drive vía HTTP | | **Comando** | `Invoke-WebRequest -Uri "https://drive.usercontent.google.com/download?id=..."` | | **Output File** | `/tmp/dron_download_output.json` | | **Output Clave** | `backup_local` | **Output JSON:** ```json { "paso": "download_complete", "backup_local": "F:\\BACKUP\\sysdasuten_compressed_ADN.bak", "drive_origen": "rmonla-GDrive:drive_bkps-dasu/sysdasuten_compressed_ADN_20260414_100500.bak", "nodo": "dasu-sql4", "duracion": 150.3, "timestamp": "2026-04-14T10:08:00-03:00", "siguiente_paso": "restaurar_backup" } ``` --- ### Dron 5: `dasuten_restaurar_dasu-sql4.rb` | Campo | Valor | |-------|-------| | **Nodo** | `dasu-sql4` | | **Tarea** | Restaurar backup (solo RESTORE, sin CHECKDB) | | **Comando** | `RESTORE DATABASE ... WITH REPLACE, MOVE ...` | | **Output File** | `/tmp/dron_restore_output.json` | | **Output Clave** | `estado_integridad=PENDING_CHECKDB` | **Output JSON:** ```json { "paso": "restore_complete", "backup_ruta": "F:\\BACKUP\\sysdasuten_compressed_ADN.bak", "nodo": "dasu-sql4", "duracion_restore": 0.23, "estado_integridad": "PENDING_CHECKDB", "timestamp": "2026-04-14T10:10:00-03:00", "siguiente_paso": "verificar_integridad" } ``` --- ### Dron 6: `dasuten_verificar-integridad_dasu-sql4.rb` | Campo | Valor | |-------|-------| | **Nodo** | `dasu-sql4` | | **Tarea** | Verificar integridad con DBCC CHECKDB | | **Comando** | `DBCC CHECKDB('sysdasuten') WITH NO_INFOMSGS, ALL_ERRORMSGS` | | **Output File** | `/tmp/dron_verify_output.json` | | **Output Clave** | `estado_integridad`, `pipeline_completo` | **Output JSON:** ```json { "paso": "verify_complete", "nodo": "dasu-sql4", "duracion_checkdb": 248.56, "estado_integridad": "OK", "paginas_verificadas": 1186793, "timestamp": "2026-04-14T10:14:00-03:00", "pipeline_completo": true } ``` --- ## 🎼 Orquestador: `orquestador_pipeline.rb` El orquestador es el **cerebro** del pipeline. No ejecuta tareas directamente, sino que: 1. **Lanza drones** secuencialmente vía `dron lanzar` 2. **Lee outputs JSON** de cada dron completado 3. **Inyecta contexto** en el siguiente dron (vía ENV o argumentos) 4. **Decide continuar** o abortar según exito/fracaso 5. **Registra estado** en la Bitácora Web ### Código del Orquestador (simplificado) ```ruby #!/usr/bin/env ruby # adn/tools/cli/drones/orquestador_pipeline.rb ESTADOS = { exportar: { script: 'dasuten_exportar.rb', nodo: 'srvv-fenix', nota: '📤 Exportar backup DASUTEN', output_file: '/tmp/dron_export_output.json', siguiente: :upload }, upload: { script: 'dasuten_upload_drive.rb', nodo: 'srv-ns8', nota: '☁️ Upload a Google Drive', output_file: '/tmp/dron_upload_output.json', siguiente: :download }, # ... más pasos }.freeze def lanzar_dron(script, nodo, nota, timeout) cmd = "#{ADN_RUN} dron lanzar --nota #{nota} --nodo #{nodo} --timeout #{timeout} -- ruby #{script}" exito = system(cmd) if exito # Leer output del dron output_file = ESTADOS[nodo.to_sym][:output_file] resultado = JSON.parse(File.read(output_file)) if File.exist?(output_file) return true, resultado else return false, {} end end # Ejecutar pipeline paso_actual = :exportar contexto = {} loop do break unless ESTADOS[paso_actual] config = ESTADOS[paso_actual] exito, resultado = lanzar_dron(config[:script], config[:nodo], config[:nota], timeout) unless exito puts "❌ Pipeline fallido en paso: #{paso_actual}" exit 1 end contexto.merge!(resultado) if config[:siguiente] paso_actual = config[:siguiente] else puts "✅ Pipeline completado!" break end end ``` --- ## 🚀 Uso del Pipeline ### Ejecutar Pipeline Completo ```bash # Pipeline completo (todos los pasos encadenados) ./adn/tools/run dron lanzar --nota "Pipeline DASUTEN 8b" -- \ ruby adn/tools/cli/drones/orquestador_pipeline.rb ``` ### Ejecutar Drones Individualmente ```bash # Paso 1: Exportar backup ./adn/tools/run dron lanzar --nota "Exportar backup" --nodo srvv-fenix -- \ ruby adn/tools/cli/drones/dasuten_exportar.rb # Paso 2: Upload a Drive ./adn/tools/run dron lanzar --nota "Upload Drive" --nodo srv-ns8 -- \ ruby adn/tools/cli/drones/dasuten_upload_drive.rb # Paso 3: Download desde Drive ./adn/tools/run dron lanzar --nota "Download Drive" --nodo dasu-sql4 -- \ ruby adn/tools/cli/drones/dasuten_download_drive.rb # Paso 4: Restaurar backup ./adn/tools/run dron lanzar --nota "Restaurar backup" --nodo dasu-sql4 -- \ ruby adn/tools/cli/drones/dasuten_restaurar.rb ``` ### Ejecutar desde un Paso Específico ```bash # Comenzar desde upload (saltear export) ./adn/tools/run dron lanzar --nota "Pipeline DASUTEN" -- \ ruby adn/tools/cli/drones/orquestador_pipeline.rb --paso upload ``` ### Dry-Run (Simulación) ```bash # Ver qué haría sin ejecutar ./adn/tools/run dron lanzar --nota "Pipeline DASUTEN" -- \ ruby adn/tools/cli/drones/orquestador_pipeline.rb --dry-run ``` --- ## 📊 Ventajas de esta Arquitectura | Ventaja | Descripción | |---------|-------------| | **🧩 Atomicidad** | Cada dron hace UNA cosa. Fácil de testear, debuggear, reemplazar. | | **🔗 Intercomunicación** | JSON como contrato entre drones. Lenguaje-agnóstico. | | **🔄 Re-ejecución** | Si falla el paso 3, re-ejecutar solo paso 3 (no todo el pipeline). | | **👁️ Observabilidad** | Cada dron se registra individualmente en Bitácora Web. | | **🛡️ Tolerancia a Fallos** | Si un dron falla, el pipeline se detiene (fail-fast). | | **📈 Escalabilidad** | Agregar nuevos pasos = agregar nuevo dron + registrar en ESTADOS. | --- ## 📁 Estructura de Archivos ``` adn/tools/cli/drones/ ├── orquestador_pipeline.rb # Orquestador principal ├── dasuten_exportar.rb # Dron 1: Exportar backup ├── dasuten_upload_drive.rb # Dron 2: Upload a Drive ├── dasuten_download_drive.rb # Dron 3: Download desde Drive └── dasuten_restaurar.rb # Dron 4: Restaurar backup /tmp/ ├── dron_export_output.json # Output del dron 1 ├── dron_upload_output.json # Output del dron 2 ├── dron_download_output.json # Output del dron 3 └── dron_restore_output.json # Output del dron 4 ``` --- ## 🔮 Futuras Mejoras (Fases Pendientes) ### Fase 2: Persistencia en Bitácora Actualmente los outputs JSON son efímeros (`/tmp/`). La **Fase 2** persistirá los outputs en la Bitácora Web: ```ruby # En dron_db.rb CREATE TABLE drone_outputs ( id SERIAL PRIMARY KEY, dron_id VARCHAR(50) REFERENCES drones(dron_id), output JSONB NOT NULL, created_at TIMESTAMP DEFAULT NOW() ); -- El orquestador puede leer outputs de la DB en lugar de archivos output = DronDB.obtener_output(dron_id) ``` ### Fase 3: Reintentos Automáticos ```ruby # Orquestador con retry def lanzar_dron_con_retry(script, nodo, nota, timeout, max_retries: 3) max_retries.times do |intento| exito, resultado = lanzar_dron(script, nodo, nota, timeout) return true, resultado if exito puts "⚠ Dron falló (intento #{intento + 1}/#{max_retries})" sleep(2 ** intento) # Backoff exponencial end return false, {} end ``` ### Fase 4: Condicionales de Skip ```ruby # Skip si ya está hecho def deberia_ejecutar?(paso, contexto) case paso when :upload # Skip si ya hay backup reciente en Drive !backup_reciente_en_drive?(contexto[:backup_ruta]) when :restaurar # Skip si BD ya está actualizada !bd_actualizada?(contexto[:backup_local]) else true end end ``` --- ## 📝 Lecciones Aprendidas ### ✅ Lo que Funcionó | Lección | Descripción | |---------|-------------| | **JSON como contrato** | Simple, legible, lenguaje-agnóstico. Funciona mejor que DB compartida. | | **Outputs en /tmp** | Fácil de limpiar, no requiere permisos especiales. | | **Orquestador stateless** | El estado está en los JSONs, no en el orquestador. | | **Drones auto-bitácora** | Cada dron se registra individualmente en la Bitácora Web. | | **Google Drive como intermediario** | Elimina dependencia de Tailscale inestable. Download HTTP directo desde dasu-sql4. | | **Base64 encoding para PowerShell** | Evita problemas de escaping UTF-8 al transportar scripts sobre SSH. | | **sshpass + ProxyCommand** | Patrón efectivo para acceder a VMs detrás de relay (dasu-sql4 detrás de srv-dasu). | ### 📊 Resultados del Pipeline DASUTEN (2026-04-14) | Métrica | Valor | |---------|-------| | **Archivo transferido** | 1.3 GB (`sysdasuten_compressed_ADN.bak`) | | **Tiempo total de pipeline** | ~35 minutos | | **Upload a Drive** | 29 minutos (1736s) — limitado por ancho de banda | | **Download HTTP** | ~5 minutos — con confirmación de virus manejada | | **Restore SQL** | 0.23 segundos — MOVE instantáneo | | **DBCC CHECKDB** | 248.56 segundos — **Integridad: OK** | | **Throughput efectivo** | ~650 KB/s (limitado por subida a Drive) | ### ⚠️ Lo que Requiere Atención | Lección | Descripción | |---------|-------------| | **Limpieza de archivos** | Los JSONs en `/tmp/` deben limpiarse post-pipeline. | | **Timeouts por dron** | Cada dron debe tener timeout individual (no todos duran lo mismo). | | **Manejo de errores** | El orquestador debe capturar stderr de cada dron para debugging. | ### 🔧 Desafíos Superados | Desafío | Solución | |---------|----------| | **Virus confirmation page** | Parseo de HTML con regex PowerShell para extraer `id`, `uuid`, `confirm` | | **dasu-sql4 sleep mode** | Acceso vía relay SSH desde srv-dasu con `sshpass -e` + ProxyCommand | | **PowerShell escaping** | Base64 encoding del script completo + heredoc con `'PSCMD'` (sin interpolación) | | **sqlcmd no disponible en PATH** | Usar ruta completa: `C:\Program Files\...\sqlcmd.exe` | --- ## 🔗 Referencias - **Ámbito**: [A01_dtic-ADN.md](./A01_dtic-ADN.md) - **Evolución Dron**: [A01.P009_Evolucion-Dron-ADN.md](./A01.P009_Evolucion-Dron-ADN.md) - **Drones Ops**: [A01.P011_Drones-Operaciones-Infra.md](./A01.P011_Drones-Operaciones-Infra.md) - **Caso de Uso**: [A04.P005_DASUTEN-sin-DC.md](../dtic-DASUTEN/A04.P005_DASUTEN-sin-DC.md) — Pipeline implementado para Fase 8b --- ## 📊 Estado del Pipeline DASUTEN (6 drones atómicos) | Paso | Estado | Dron | Nodo | Output | Duración | |------|--------|------|------|--------|----------| | **8b.1** | ✅ Completado | `dasuten_exportar_srvv-fenix.rb` | srvv-fenix | `backup_ruta` | 44.8s | | **8b.2** | ✅ Completado | `dasuten_transferir_srvv-fenix-srv-ns8.rb` | srv-ns8 | `backup_local` | ~2 min | | **8b.3** | ✅ Completado | `dasuten_upload_srv-ns8-drive.rb` | srv-ns8 | `drive_ruta` | 1736s (29 min) | | **8b.4** | ✅ Completado | `dasuten_download_drive-dasu-sql4.rb` | dasu-sql4 | `backup_local` | ~5 min | | **8b.5** | ✅ Completado | `dasuten_restaurar_dasu-sql4.rb` | dasu-sql4 | `estado_integridad=PENDING` | 0.23s | | **8b.6** | ✅ Completado | `dasuten_verificar-integridad_dasu-sql4.rb` | dasu-sql4 | `estado_integridad=OK` | 248.56s | **Pipeline completado exitosamente el 2026-04-14 a las 17:21.** ### Resultados Finales - **Backup comprimido:** 1.3 GB (`sysdasuten_compressed_ADN.bak`) - **Google Drive URL:** `rmonla-GDrive:drive_bkps-dasu/sysdasuten_compressed_ADN_20260414_*.bak` - **Download HTTP directo:** Con confirmación de virus manejada (formulario HTML parsing) - **Restore:** 0.23 segundos (MOVE a F:\DATA y F:\LOG) - **DBCC CHECKDB:** 248.56 segundos — **Integridad: OK** (1,186,793 páginas verificadas, sin errores) - **Duración total del pipeline:** ~35 minutos --- ## 🔄 Pipeline Diferencial (Refresco Diario) Para refrescos diarios de la base de datos, se recomienda usar **backup diferencial** que solo transfiere los cambios desde el último backup completo. ### Ventajas del Pipeline Diferencial | Métrica | Completo | Diferencial | |---------|----------|-------------| | **Tamaño backup** | 1.3 GB | ~50-100 MB* | | **Tiempo export** | 45 seg | ~5-10 seg | | **Tiempo upload** | 29 min | ~2-3 min | | **Tiempo download** | ~5 min | ~30 seg | | **Tiempo total** | ~35 min | ~5-10 min | \* Depende de la cantidad de cambios diarios ### Drones del Pipeline Diferencial | Paso | Dron | Nodo | Tarea | |------|------|------|-------| | **1** | `dasuten_exportar_diferencial_srvv-fenix.rb` | srvv-fenix | `BACKUP DATABASE ... WITH DIFFERENTIAL` | | **2** | `dasuten_transferir_srvv-fenix-srv-ns8.rb` | srv-ns8 | SMB → `/var/tmp/` | | **3** | `dasuten_upload_srv-ns8-drive.rb` | srv-ns8 | rclone → Google Drive | | **4** | `dasuten_download_drive-dasu-sql4.rb` | dasu-sql4 | HTTP ← Google Drive | | **5** | `dasuten_restaurar_diferencial_dasu-sql4.rb` | dasu-sql4 | `RESTORE ... WITH DIFFERENTIAL` + `RECOVERY` | | **6** | `dasuten_verificar-integridad_dasu-sql4.rb` | dasu-sql4 | `DBCC CHECKDB` | ### Orquestador Diferencial ```bash # Ejecutar pipeline diferencial completo ./adn/tools/run dron lanzar --nota "Refresco Diario DASUTEN" -- \ ruby adn/tools/cli/drones/orquestador_pipeline_diferencial.rb ``` ### Estrategia Recomendada | Día | Tipo de Backup | Duración Estimada | |-----|----------------|-------------------| | **Lunes** | Completo | ~35 min | | **Martes** | Diferencial | ~5-10 min | | **Miércoles** | Diferencial | ~5-10 min | | **Jueves** | Diferencial | ~5-10 min | | **Viernes** | Diferencial | ~5-10 min | | **Sábado** | Diferencial | ~5-10 min | | **Domingo** | — (sin cambios) | — | ### Consideraciones Importantes 1. **El backup diferencial se basa en el último backup completo**, no en el diferencial anterior 2. **Cada diferencial es acumulativo** — el martes tiene cambios desde lunes, el miércoles tiene cambios desde lunes (no desde martes) 3. **Restaurar diferencial requiere:** - Último backup completo (ya restaurado en dasu-sql4) - Backup diferencial más reciente - `WITH NORECOVERY` para aplicar diferencial, luego `WITH RECOVERY` para poner BD online --- *Documento creado: 2026-04-14* *Última actualización: 2026-04-14* *Versión: 2.1 — Con pipeline diferencial*