diff --git a/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb b/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb new file mode 100755 index 00000000..2b523f44 --- /dev/null +++ b/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb @@ -0,0 +1,268 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb +# ========================================================== +# Dron: Download desde Google Drive a dasu-sql4 +# Flujo: Google Drive → F:\BACKUP\sysdasuten_compressed_ADN.bak +# Usa: Invoke-WebRequest (sin rclone) +# ========================================================== + +require 'time' +require 'json' +require 'shellwords' +require 'optparse' +require 'base64' +require 'tempfile' + +opciones = { + test_file: nil, + file_id: nil # Google Drive file ID +} + +OptionParser.new do |opts| + opts.banner = "Uso: dron lanzar --nota 'Download' --nodo dasu-sql4 -- ruby dasuten_download_drive.rb [opciones]" + opts.on("--test-file ARCHIVO", "Archivo específico para test (ej: test_Backup.bak)") { |f| opciones[:test_file] = f } + opts.on("--file-id ID", "Google Drive file ID") { |id| opciones[:file_id] = id } +end.parse!(ARGV) + +BACKUP_DIR_WINDOWS = "F:\\BACKUP" +BACKUP_NAME_DEFAULT = "sysdasuten_compressed_ADN.bak" +TARGET_IP = "192.168.1.11" +TARGET_USER = "Administrador" +TARGET_PASS = "UTNlarioja00DASU" + +# Determinar nombre del archivo backup +LOCAL_BACKUP = "#{BACKUP_DIR_WINDOWS}\\#{BACKUP_NAME_DEFAULT}" + +puts "[*] Dron: Download desde Google Drive" +puts " Nodo: dasu-sql4" + +# Determinar archivo a descargar +if opciones[:test_file] + puts " 🧪 MODO TEST: Descargando archivo específico" + # File ID conocido para test_Backup.bak + file_id = opciones[:file_id] || "1P0cioDV9kZNXhIJhhdRMhWWjIRsVGcxQ" + download_url = "https://drive.google.com/uc?export=download&id=#{file_id}" + puts " File ID: #{file_id}" + puts " Destino: #{LOCAL_BACKUP}" +elsif opciones[:file_id] + puts " Download por file ID" + download_url = "https://drive.google.com/uc?export=download&id=#{opciones[:file_id]}" + puts " File ID: #{opciones[:file_id]}" + puts " Destino: #{LOCAL_BACKUP}" +else + # Intentar leer file_id desde el output del upload + UPLOAD_OUTPUT = '/tmp/dron_upload_output.json' + if File.exist?(UPLOAD_OUTPUT) + upload_data = JSON.parse(File.read(UPLOAD_OUTPUT)) + drive_ruta = upload_data['drive_ruta'] + + # Extraer file_id de la ruta (último componente después del slash) + file_id = File.basename(drive_ruta) + + # Si es un nombre de archivo con timestamp, buscar el file_id real via rclone + # Primero intentamos obtener el file_id desde srv-ns8 + puts " Obteniendo file_id desde srv-ns8..." + puts " Drive ruta: #{drive_ruta}" + + # Usar rclone para obtener el file_id del archivo específico + backup_name = File.basename(drive_ruta) + drive_folder = drive_ruta.rpartition('/').first + rclone_cmd = "ssh srv-ns8 'rclone lsjson #{drive_folder} 2>/dev/null' | grep -o '\"ID\":\"[^\"]*\".*\"Name\":\"#{backup_name}\"' | head -1 | grep -o '\"ID\":\"[^\"]*\"' | cut -d'\"' -f4" + file_id_result = `#{rclone_cmd}`.strip + + if file_id_result && !file_id_result.empty? + file_id = file_id_result + puts " File ID obtenido: #{file_id}" + else + puts " [!] No se pudo obtener file_id, usando nombre de archivo" + file_id = File.basename(drive_ruta) + end + + download_url = "https://drive.google.com/uc?export=download&id=#{file_id}" + backup_name = file_id # Usar el nombre del archivo desde drive_ruta + puts " Download URL: #{download_url}" + puts " Backup nombre: #{backup_name}" + puts " Destino: #{BACKUP_DIR_WINDOWS}\\#{backup_name}" + else + puts " [!] Error: Se requiere --test-file o --file-id" + puts " Para produccion: obtener file_id con rclone lsjson desde srv-ns8" + exit 1 + end +end + +# PowerShell script para download +# Para archivos grandes, Google Drive muestra página de confirmación de virus. +# Extraemos los parámetros del formulario y hacemos POST para descargar. +ps_script = <<~'PSCMD' + $ErrorActionPreference = "Stop" + $BACKUP_DIR = "F:\BACKUP" + $DOWNLOAD_URL = "DOWNLOAD_URL_PLACEHOLDER" + $BACKUP_NAME = "BACKUP_NAME_PLACEHOLDER" + $LOCAL_BACKUP = "$BACKUP_DIR\$BACKUP_NAME" + $JSON_PATH = "C:\temp\dron_download_output.json" + + # Asegurar directorios + $null = New-Item -ItemType Directory -Force -Path "C:\temp" + $null = New-Item -ItemType Directory -Force -Path $BACKUP_DIR + + Write-Host "[1/3] Iniciando descarga..." + Write-Host " URL: $DOWNLOAD_URL" + + Write-Host "[2/3] Descargando..." + $start = Get-Date + try { + # Paso 1: Obtener página de confirmación + $response = Invoke-WebRequest -Uri $DOWNLOAD_URL -UseBasicParsing -Headers @{ + "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + } + + # Extraer parámetros del formulario de confirmación + $html = $response.Content + $fileId = "" + $uuid = "" + $confirm = "t" + if ($html -match 'name="id" value="([^"]+)"') { $fileId = $matches[1] } + if ($html -match 'name="uuid" value="([^"]+)"') { $uuid = $matches[1] } + if ($html -match 'name="confirm" value="([^"]+)"') { $confirm = $matches[1] } + + Write-Host " File ID: $fileId" + Write-Host " UUID: $uuid" + + # Paso 2: Construir URL de descarga con confirmación + $downloadConfirmUrl = "https://drive.usercontent.google.com/download?id=$fileId&export=download&confirm=$confirm&uuid=$uuid" + Write-Host " URL confirmada: $downloadConfirmUrl" + + # Paso 3: Descargar archivo real + $finalResponse = Invoke-WebRequest -Uri $downloadConfirmUrl -UseBasicParsing -Headers @{ + "User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + } + + # Guardar contenido + [System.IO.File]::WriteAllBytes($LOCAL_BACKUP, $finalResponse.Content) + + $dur = (New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds + Write-Host "[+] Descarga en $([math]::Round($dur, 2))s" + } catch { + Write-Host "[!] Error: $_" + exit 1 + } + + Write-Host "[3/3] Verificando..." + if (Test-Path $LOCAL_BACKUP) { + $tam = (Get-Item $LOCAL_BACKUP).Length + Write-Host " Tamano: $([math]::Round($tam / 1MB, 2)) MB" + if ($tam -eq 0) { + Write-Host "[!] Archivo vacío" + exit 1 + } + } else { + Write-Host "[!] No existe" + exit 1 + } + + $res = @{ + paso = "download_complete" + backup_local = $LOCAL_BACKUP + drive_origen = "Google Drive" + nodo = "dasu-sql4" + duracion = [math]::Round($dur, 2) + timestamp = (Get-Date -Format "o") + siguiente_paso = "restaurar_backup" + } + $res | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 + Write-Host " Output: $($res | ConvertTo-Json -Compress)" +PSCMD + +# Reemplazar placeholders con valores reales +ps_script = ps_script.gsub('DOWNLOAD_URL_PLACEHOLDER', download_url) +ps_script = ps_script.gsub('BACKUP_NAME_PLACEHOLDER', backup_name) + +# Codificar script en base64 +ps_encoded = Base64.strict_encode64(ps_script) + +# Script bash que se ejecuta en srv-dasu +bash_script = <<~BASHHEREDOC + #!/bin/bash + export SSHPASS="#{TARGET_PASS}" + sshpass -e ssh -o StrictHostKeyChecking=no -p 7022 #{TARGET_USER}@#{TARGET_IP} \ + "powershell -Command \\\"Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{ps_encoded}')))\\\"" +BASHHEREDOC + +# Guardar script bash en archivo temporal +bash_file = Tempfile.new(['download', '.sh']) +bash_file.write(bash_script) +bash_file.chmod(0755) +bash_file.close + +remote_bash_path = "/tmp/dron_download_#{Time.now.to_i}.sh" + +puts "\n[1/3] Copiando script a srv-dasu..." +scp_cmd = "scp -o StrictHostKeyChecking=no #{bash_file.path} srv-dasu:#{remote_bash_path}" +unless system(scp_cmd) + puts "[!] Error al copiar script" + bash_file.unlink + exit 1 +end + +puts "\n[2/3] Ejecutando en dasu-sql4 vía srv-dasu..." +ssh_cmd = "ssh srv-dasu 'bash #{remote_bash_path}'" +output = `#{ssh_cmd}` +output = output.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') if output +puts output + +# Limpieza +bash_file.unlink +system("ssh srv-dasu 'rm -f #{remote_bash_path}' 2>/dev/null") + +if $?.success? + puts "\n[3/3] Leyendo output JSON..." + + json_ps = 'Get-Content C:\temp\dron_download_output.json -Raw' + json_encoded = Base64.strict_encode64(json_ps) + + json_script = <<~JSONBASH.strip + #!/bin/bash + export SSHPASS="#{TARGET_PASS}" + sshpass -e ssh -o StrictHostKeyChecking=no -p 7022 #{TARGET_USER}@#{TARGET_IP} \ + "powershell -Command \\\"Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{json_encoded}')))\\\"" + JSONBASH + + json_file = Tempfile.new(['json', '.sh']) + json_file.write(json_script) + json_file.chmod(0755) + json_file.close + + remote_json_path = "/tmp/dron_json_#{Time.now.to_i}.sh" + system("scp -o StrictHostKeyChecking=no #{json_file.path} srv-dasu:#{remote_json_path}") + + json_cmd = "ssh srv-dasu 'bash #{remote_json_path}'" + resultado_json = `#{json_cmd}`.strip + resultado_json = resultado_json.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') if resultado_json + + json_file.unlink + system("ssh srv-dasu 'rm -f #{remote_json_path}' 2>/dev/null") + + if resultado_json.empty? + puts "[!] No se pudo leer output JSON" + exit 1 + end + + json_match = resultado_json.match(/\{.*\}/m) + resultado_json = json_match ? json_match[0] : resultado_json + + puts " Output: #{resultado_json}" + resultado = JSON.parse(resultado_json) + + puts "\n✅ Download completado" + puts " Backup: #{resultado['backup_local']}" + puts " Duración: #{resultado['duracion']}s" + + File.write('/tmp/dron_download_output.json', resultado_json) + puts " JSON guardado en: /tmp/dron_download_output.json" +else + puts "[!] Error en ejecución (código: #{$?.exitstatus})" + exit 1 +end diff --git a/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb b/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb new file mode 100644 index 00000000..e243186b --- /dev/null +++ b/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb @@ -0,0 +1,116 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb +# ========================================================== +# Dron: Exportar backup DIFERENCIAL de DASUTEN en srvv-fenix +# Flujo: srvv-fenix → Backup diferencial en E:\BK_SQL\ +# Salida: Marca evento como 'export_complete' con ruta del backup +# ========================================================== + +require 'tiny_tds' +require 'time' +require 'json' + +# Nombre del archivo con timestamp +TIMESTAMP = Time.now.strftime('%Y%m%d_%H%M%S') +BACKUP_DIR = "E:\\BK_SQL\\sysdasuten" +BACKUP_FILE = "#{BACKUP_DIR}\\sysdasuten_DIF_#{TIMESTAMP}.bak" + +puts "[*] Dron: Exportar backup DIFERENCIAL DASUTEN" +puts " Nodo: srvv-fenix" +puts " Destino: #{BACKUP_FILE}" +puts " Tipo: DIFFERENTIAL (solo cambios desde último backup completo)" +puts " Directorio: #{BACKUP_DIR}" + +begin + # Autorizar candados primero (necesario para acceder a la bóveda) + system("ruby adn/tools/candados/candados.rb authorize >/dev/null 2>&1") + + # Obtener credencial de candados (patrón usado en otros drones) + pass_cmd = "ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null" + sql_pass = `#{pass_cmd}`.strip + + sql_user = 'UTNlarioja\\monlaricardo' + sql_host = '10.0.10.200' + + if sql_pass.empty? + puts "[!] Error: No se pudo obtener credencial de candados (utnlarioja.intranet:monlaricardo)" + exit 1 + end + + client = TinyTds::Client.new( + username: sql_user, + password: sql_pass, + host: sql_host, + timeout: 600 + ) + puts "[+] Conectado a SQL Server en #{sql_host} (auth: Windows)" + + # Crear directorio si no existe + puts "\n[0/3] Verificando directorio..." + mkdir_sql = "EXEC master.sys.xp_create_subdir '#{BACKUP_DIR}'" + client.execute(mkdir_sql) + puts "[+] Directorio asegurado: #{BACKUP_DIR}" + + # Generar backup diferencial + puts "\n[1/2] Generando backup diferencial..." + start = Time.now + + sql = <<~SQL.strip + BACKUP DATABASE sysdasuten + TO DISK = '#{BACKUP_FILE}' + WITH DIFFERENTIAL, COMPRESSION, STATS=10 + SQL + + puts " SQL: #{sql}" + + result = client.execute(sql) + result.each { |r| puts " #{r['message']}" if r['message'] } + + duracion = (Time.now - start).round(2) + puts "[+] Backup diferencial generado en #{duracion} segundos" + + # Obtener tamaño del archivo (vía PowerShell remoto o xp_fileexist) + puts "\n[2/2] Verificando archivo..." + + begin + file_result = client.execute("EXEC master.sys.xp_fileexist '#{BACKUP_FILE}'") + file_info = file_result.first + + if file_info && file_result.to_a.any? { |f| f['File Exists'] == 1 } + puts "[+] Archivo verificado: #{BACKUP_FILE}" + + # Obtener tamaño aproximado + size_info = client.execute("SELECT size_bytes = (SELECT size * 8192 FROM sys.database_files WHERE name = 'sysdasuten')") + puts " Tamaño BD base: #{size_info.to_a.first['size_bytes'].to_i / 1024 / 1024} MB" + else + puts "[!] Advertencia: No se pudo verificar existencia del archivo" + end + rescue => e + puts "[!] Advertencia: #{e.message}" + end + + # Output para orquestador + output = { + paso: 'export_differential_complete', + backup_ruta: BACKUP_FILE, + backup_tipo: 'differential', + nodo: 'srvv-fenix', + duracion: duracion, + timestamp: Time.now.iso8601, + siguiente_paso: 'transferir' + } + + puts "\n✅ Export diferencial completado" + puts " Output: #{output.to_json}" + + # Escribir output para que el orquestador lo lea + File.write('/tmp/dron_export_dif_output.json', output.to_json) + +rescue => e + puts "[!] Error fatal: #{e.message}" + puts e.backtrace.first(5).join("\n") + exit 1 +end diff --git a/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb b/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb new file mode 100755 index 00000000..c5f009d4 --- /dev/null +++ b/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb @@ -0,0 +1,93 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb +# ========================================================== +# Dron: Exportar backup en srvv-fenix +# Flujo: srvv-fenix → Backup comprimido en E:\BK_SQL\ +# Salida: Marca evento como 'export_complete' con ruta del backup +# ========================================================== + +require 'tiny_tds' +require 'time' +require 'json' + +# Nombre del archivo con timestamp +TIMESTAMP = Time.now.strftime('%Y%m%d_%H%M%S') +BACKUP_DIR = "E:\\BK_SQL\\sysdasuten" +BACKUP_FILE = "#{BACKUP_DIR}\\sysdasuten_FULL_#{TIMESTAMP}.bak" + +puts "[*] Dron: Exportar backup COMPLETO DASUTEN" +puts " Nodo: srvv-fenix" +puts " Destino: #{BACKUP_FILE}" +puts " Tipo: FULL (completo)" + +begin + # Autorizar candados primero (necesario para acceder a la bóveda) + system("ruby adn/tools/candados/candados.rb authorize >/dev/null 2>&1") + + # Obtener credencial de candados (patrón usado en otros drones) + pass_cmd = "ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null" + sql_pass = `#{pass_cmd}`.strip + + sql_user = 'UTNlarioja\\monlaricardo' + sql_host = '10.0.10.200' + + if sql_pass.empty? + puts "[!] Error: No se pudo obtener credencial de candados (utnlarioja.intranet:monlaricardo)" + exit 1 + end + + client = TinyTds::Client.new( + username: sql_user, + password: sql_pass, + host: sql_host, + timeout: 600 + ) + puts "[+] Conectado a SQL Server en #{sql_host} (auth: Windows)" + + # Crear directorio si no existe + puts "\n[0/3] Verificando directorio..." + mkdir_sql = "EXEC master.sys.xp_create_subdir '#{BACKUP_DIR}'" + client.execute(mkdir_sql) + puts "[+] Directorio asegurado: #{BACKUP_DIR}" + + # Generar backup comprimido + puts "[1/2] Generando backup comprimido..." + start = Time.now + + result = client.execute("BACKUP DATABASE sysdasuten TO DISK = '#{BACKUP_FILE}' WITH INIT, COMPRESSION, STATS=10") + result.each { |r| puts " #{r['message']}" if r['message'] } + + duracion = (Time.now - start).round(2) + puts "[+] Backup generado en #{duracion} segundos" + + # Obtener tamaño del archivo (vía PowerShell remoto) + puts "[2/2] Verificando archivo..." + size_result = client.execute("EXEC master.sys.xp_fileexist '#{BACKUP_FILE}'") + file_info = client.execute("SELECT size = (SELECT size FROM sys.database_files WHERE name = 'sysdasuten')") + + puts "[+] Archivo verificado: #{BACKUP_FILE}" + + # Output para orquestador + output = { + paso: 'export_full_complete', + backup_ruta: BACKUP_FILE, + backup_tipo: 'full', + nodo: 'srvv-fenix', + duracion: duracion, + timestamp: Time.now.iso8601, + siguiente_paso: 'transferir' + } + + puts "\n✅ Export completado" + puts " Output: #{output.to_json}" + + # Escribir output para que el orquestador lo lea + File.write('/tmp/dron_export_output.json', output.to_json) + +rescue => e + puts "[!] Error fatal: #{e.message}" + exit 1 +end diff --git a/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb b/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb new file mode 100644 index 00000000..dd82a49c --- /dev/null +++ b/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb @@ -0,0 +1,199 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb +# ========================================================== +# Dron: Restaurar backup en dasu-sql4 +# Flujo: RESTORE DATABASE (sin DBCC CHECKDB) +# Entrada: Backup en F:\BACKUP\ +# Salida: restore_complete (listo para verificación) +# ========================================================== + +require 'time' +require 'json' +require 'base64' +require 'tempfile' +require 'shellwords' + +DOWNLOAD_OUTPUT = '/tmp/dron_download_output.json' +BACKUP_DIR_WINDOWS = 'F:\\BACKUP' + +# Leer ruta del backup desde el output del download +if File.exist?(DOWNLOAD_OUTPUT) + output = JSON.parse(File.read(DOWNLOAD_OUTPUT)) + backup_local = output['backup_local'] + # Convertir path Unix a Windows si es necesario + backup_name = File.basename(backup_local) + LOCAL_BACKUP = "#{BACKUP_DIR_WINDOWS}\\#{backup_name}" + puts "[*] Dron: Restaurar backup DASUTEN (COMPLETO)" +else + LOCAL_BACKUP = "#{BACKUP_DIR_WINDOWS}\\sysdasuten_FULL_*.bak" + puts "[*] Dron: Restaurar backup DASUTEN (COMPLETO - buscar más reciente)" +end + +TARGET_IP = "192.168.1.11" +TARGET_USER = "Administrador" +TARGET_PASS = "UTNlarioja00DASU" + +puts " Nodo: dasu-sql4" +puts " Backup: #{LOCAL_BACKUP}" + +# PowerShell script para restore (sin checkdb) +ps_script = <<~'PSCMD' + $ErrorActionPreference = "Stop" + $BACKUP_DIR = "F:\BACKUP" + $BACKUP_PATTERN = "sysdasuten_FULL_*.bak" + $JSON_PATH = "C:\temp\dron_restore_output.json" + + # Asegurar directorio temp + $null = New-Item -ItemType Directory -Force -Path "C:\temp" + + # Buscar el backup completo más reciente + Write-Host "[0/1] Buscando backup completo más reciente..." + $backupFile = Get-ChildItem "$BACKUP_DIR\$BACKUP_PATTERN" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if (-not $backupFile) { + Write-Host "[!] No se encontró backup completo en $BACKUP_DIR" + exit 1 + } + $LOCAL_BACKUP = $backupFile.FullName + Write-Host " Archivo: $LOCAL_BACKUP" + Write-Host " Tamaño: $([math]::Round($backupFile.Length / 1MB, 2)) MB" + + Write-Host "[1/1] Ejecutando RESTORE DATABASE..." + $start = Get-Date + + # Ruta completa a sqlcmd (SQL Server 2019) + $sqlcmd = "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\sqlcmd.exe" + + # Comando RESTORE + $restoreSql = @" + RESTORE DATABASE sysdasuten + FROM DISK = '$LOCAL_BACKUP' + WITH REPLACE, + MOVE 'sysdasuten' TO 'F:\DATA\sysdasuten.mdf', + MOVE 'sysdasuten_log' TO 'F:\LOG\sysdasuten_log.ldf' + "@ + + Write-Host " SQL: $restoreSql" + + # Ejecutar restore + $restoreOutput = & $sqlcmd -S localhost -Q $restoreSql 2>&1 + $exitoRestore = $LASTEXITCODE -eq 0 + $duracionRestore = [math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds, 2) + + if (-not $exitoRestore) { + Write-Host "[!] Error en restore:" + Write-Host $restoreOutput + exit 1 + } + + Write-Host "[+] Restore completado en $duracionRestore segundos" + + $duracionTotal = $duracionRestore + + # Output JSON + $res = @{ + paso = "restore_complete" + backup_ruta = $LOCAL_BACKUP + nodo = "dasu-sql4" + duracion_total = $duracionTotal + duracion_restore = $duracionRestore + estado_integridad = "PENDING_CHECKDB" + timestamp = (Get-Date -Format "o") + siguiente_paso = "verificar_integridad" + } + + Write-Host "`n✅ Restauración completada (pendiente CHECKDB)" + Write-Host " Output: $($res | ConvertTo-Json -Compress)" + + $res | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +# Codificar script en base64 +ps_encoded = Base64.strict_encode64(ps_script) + +# Script bash que se ejecuta en srv-dasu +bash_script = <<~BASHHEREDOC + #!/bin/bash + export SSHPASS="#{TARGET_PASS}" + sshpass -e ssh -o StrictHostKeyChecking=no -p 7022 #{TARGET_USER}@#{TARGET_IP} \ + "powershell -Command \\\"Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{ps_encoded}')))\\\"" +BASHHEREDOC + +# Guardar script bash en archivo temporal +bash_file = Tempfile.new(['restore', '.sh']) +bash_file.write(bash_script) +bash_file.chmod(0755) +bash_file.close + +remote_bash_path = "/tmp/dron_restore_#{Time.now.to_i}.sh" + +puts "\n[1/3] Copiando script a srv-dasu..." +scp_cmd = "scp -o StrictHostKeyChecking=no #{bash_file.path} srv-dasu:#{remote_bash_path}" +unless system(scp_cmd) + puts "[!] Error al copiar script" + bash_file.unlink + exit 1 +end + +puts "\n[2/3] Ejecutando en dasu-sql4 vía srv-dasu..." +ssh_cmd = "ssh srv-dasu 'bash #{remote_bash_path}'" +output = `#{ssh_cmd}` +output = output.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') if output +puts output + +# Limpieza +bash_file.unlink +system("ssh srv-dasu 'rm -f #{remote_bash_path}' 2>/dev/null") + +if $?.success? + puts "\n[3/3] Leyendo output JSON..." + + json_ps = 'Get-Content C:\temp\dron_restore_output.json -Raw' + json_encoded = Base64.strict_encode64(json_ps) + + json_script = <<~JSONBASH.strip + #!/bin/bash + export SSHPASS="#{TARGET_PASS}" + sshpass -e ssh -o StrictHostKeyChecking=no -p 7022 #{TARGET_USER}@#{TARGET_IP} \ + "powershell -Command \\\"Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{json_encoded}')))\\\"" + JSONBASH + + json_file = Tempfile.new(['json', '.sh']) + json_file.write(json_script) + json_file.chmod(0755) + json_file.close + + remote_json_path = "/tmp/dron_json_#{Time.now.to_i}.sh" + system("scp -o StrictHostKeyChecking=no #{json_file.path} srv-dasu:#{remote_json_path}") + + json_cmd = "ssh srv-dasu 'bash #{remote_json_path}'" + resultado_json = `#{json_cmd}`.strip + resultado_json = resultado_json.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') if resultado_json + + json_file.unlink + system("ssh srv-dasu 'rm -f #{remote_json_path}' 2>/dev/null") + + if resultado_json.empty? + puts "[!] No se pudo leer output JSON" + exit 1 + end + + json_match = resultado_json.match(/\{.*\}/m) + resultado_json = json_match ? json_match[0] : resultado_json + + puts " Output: #{resultado_json}" + resultado = JSON.parse(resultado_json) + + puts "\n✅ Restauración completada" + puts " Backup: #{resultado['backup_ruta']}" + puts " Estado: #{resultado['estado_integridad']}" + puts " Duración: #{resultado['duracion_total']}s" + + File.write('/tmp/dron_restore_output.json', resultado_json) + puts " JSON guardado en: /tmp/dron_restore_output.json" +else + puts "[!] Error en ejecución (código: #{$?.exitstatus})" + exit 1 +end