#!/usr/bin/env ruby # frozen_string_literal: true # ========================================================== # adn/tools/cli/drones/orquestador_pipeline_diferencial.rb # ========================================================== # Orquestador de Pipeline DIFERENCIAL DASUTEN # ========================================================== # Flujo: exportar_dif → transferir → upload → download → restaurar_dif → verificar # Para refrescos diarios (solo cambios desde último backup completo) # ========================================================== require 'optparse' require 'json' require 'time' require 'shellwords' CLI_DIR = File.expand_path(__dir__) ADN_RUN = File.join(File.dirname(File.dirname(File.dirname(CLI_DIR))), 'tools', 'run') # Configuración opciones = { paso_inicial: 'exportar_dif', dry_run: false, timeout: 900, # 15 min por dron (suficiente para diferencial) nodo_bitacora: 'dasuten-pipeline-dif' } OptionParser.new do |opts| opts.banner = "Uso: #{ADN_RUN} dron lanzar --nota 'Pipeline Diferencial DASUTEN' -- ruby adn/tools/cli/drones/orquestador_pipeline_diferencial.rb [opciones]" opts.on("--paso PASO", %w[exportar_dif transferir upload download restaurar_dif verificar], "Comenzar desde este paso") { |p| opciones[:paso_inicial] = p } opts.on("--dry-run", "Simulación sin ejecutar drones") { opciones[:dry_run] = true } opts.on("--timeout N", Integer, "Timeout por dron en segundos") { |t| opciones[:timeout] = t } end.parse!(ARGV) # Estados del pipeline diferencial (6 drones atómicos) ESTADOS = { exportar_dif: { script: 'dasuten_exportar_diferencial_srvv-fenix.rb', nodo: 'srvv-fenix', nota: '📤 Exportar backup DIFERENCIAL DASUTEN', output_file: '/tmp/dron_export_dif_output.json', siguiente: :transferir }, transferir: { script: 'dasuten_transferir_srvv-fenix-srv-ns8.rb', nodo: 'srv-ns8', nota: '🔄 Transferir SMB (fenix → ns8)', output_file: '/tmp/dron_transfer_output.json', siguiente: :upload }, upload: { script: 'dasuten_upload_srv-ns8-drive.rb', nodo: 'srv-ns8', nota: '☁️ Upload a Google Drive', output_file: '/tmp/dron_upload_output.json', siguiente: :download }, download: { script: 'dasuten_download_drive-dasu-sql4.rb', nodo: 'dasu-sql4', nota: '📥 Download desde Google Drive', output_file: '/tmp/dron_download_output.json', siguiente: :restaurar_dif }, restaurar_dif: { script: 'dasuten_restaurar_diferencial_dasu-sql4.rb', nodo: 'dasu-sql4', nota: '💾 Restaurar backup DIFERENCIAL', output_file: '/tmp/dron_restore_dif_output.json', siguiente: :verificar }, verificar: { script: 'dasuten_verificar-integridad_dasu-sql4.rb', nodo: 'dasu-sql4', nota: '✅ Verificar integridad BD', output_file: '/tmp/dron_verify_output.json', siguiente: nil # Final } }.freeze puts "\n" + "="*70 puts "🛸 ORQUESTADOR PIPELINE DIFERENCIAL DASUTEN (Refresco diario)" puts "="*70 puts " Paso inicial: #{opciones[:paso_inicial]}" puts " Dry-run: #{opciones[:dry_run]}" puts " Timeout: #{opciones[:timeout]}s por dron" puts " Drones: #{ESTADOS.keys.join(' → ')}" puts "="*70 puts "" puts " ℹ️ Este pipeline usa backup DIFERENCIAL (solo cambios)" puts " Esperado: ~5-10 min total (vs ~35 min del completo)" puts "="*70 # ────────────────────────────────────────────────────────────── # Función para lanzar un dron y esperar su resultado # ────────────────────────────────────────────────────────────── def lanzar_dron(script, nodo, nota, timeout, dry_run) cmd = "ruby #{File.join(File.dirname(__FILE__), script)}" puts "\n" + "-"*70 puts "🛸 Lanzando dron: #{nota}" puts " Nodo: #{nodo}" puts " Script: #{script}" puts "-"*70 # Lanzar dron ejecutor dron_cmd = "#{ADN_RUN} dron lanzar --nota #{Shellwords.escape(nota)} --nodo #{Shellwords.escape(nodo)} --timeout #{timeout} -- #{cmd}" puts " Comando: #{dron_cmd}" if dry_run puts " [DRY-RUN] No ejecutado" return true, {} end exito = system(dron_cmd) codigo = $?.exitstatus if exito puts "\n✅ Dron completado exitosamente" # Leer output del dron (buscar en todos los archivos posibles) output_files = ['/tmp/dron_export_dif_output.json', '/tmp/dron_transfer_output.json', '/tmp/dron_upload_output.json', '/tmp/dron_download_output.json', '/tmp/dron_restore_dif_output.json', '/tmp/dron_verify_output.json'] output_file = output_files.find { |f| File.exist?(f) } if output_file && File.exist?(output_file) resultado = JSON.parse(File.read(output_file)) puts " Output: #{resultado.inspect}" return true, resultado else puts " [!] No se encontró archivo de output" return true, {} end else puts "\n❌ Dron fallido (código: #{codigo})" return false, {} end end # ────────────────────────────────────────────────────────────── # Ejecutar pipeline # ────────────────────────────────────────────────────────────── paso_actual = opciones[:paso_inicial].to_sym contexto = {} # Para pasar datos entre drones duraciones = [] begin loop do break unless ESTADOS[paso_actual] config = ESTADOS[paso_actual] # Lanzar dron exito, resultado = lanzar_dron( config[:script], config[:nodo], config[:nota], opciones[:timeout], opciones[:dry_run] ) unless exito puts "\n❌ Pipeline fallido en paso: #{paso_actual}" exit 1 end # Guardar contexto para siguiente paso contexto.merge!(resultado) if resultado duraciones << resultado[:duracion] if resultado[:duracion] # Decidir siguiente paso if config[:siguiente] puts "\n➡️ Pasando a: #{config[:siguiente]}" paso_actual = config[:siguiente] else puts "\n✅ Pipeline completado exitosamente!" break end end # Resumen final puts "\n" + "="*70 puts "📊 RESUMEN DEL PIPELINE DIFERENCIAL" puts "="*70 if contexto[:pipeline_completo] || contexto[:estado_integridad] == 'OK' puts " Estado: ✅ COMPLETADO" puts " Integridad BD: #{contexto[:estado_integridad] || 'N/A'}" else puts " Estado: ⏳ EN PROGRESO" end puts " Backup Drive: #{contexto[:drive_ruta] || contexto[:drive_origen] || 'N/A'}" puts " Duración total: #{duraciones.sum.round(2)}s (#{duraciones.count} drones)" puts " Tipo: #{contexto[:backup_tipo] || 'differential'}" puts "="*70 rescue => e puts "\n❌ Error en orquestador: #{e.message}" puts e.backtrace.first(5).join("\n") exit 1 end