diff --git a/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb b/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb index 2b523f44..8985baaf 100755 --- a/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb +++ b/adn/tools/cli/drones/dasuten_download_drive-dasu-sql4.rb @@ -11,14 +11,12 @@ require 'time' require 'json' -require 'shellwords' require 'optparse' -require 'base64' -require 'tempfile' +require_relative 'lib/dasu_executor' opciones = { test_file: nil, - file_id: nil # Google Drive file ID + file_id: nil } OptionParser.new do |opts| @@ -29,83 +27,55 @@ 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" +JSON_PATH = "C:\temp\dron_download_output.json" # Determinar archivo a descargar if opciones[:test_file] + puts "[*] Dron: Download desde Google Drive" + puts " Nodo: dasu-sql4" 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}" + backup_name = opciones[:test_file] puts " File ID: #{file_id}" - puts " Destino: #{LOCAL_BACKUP}" + puts " Destino: #{BACKUP_DIR_WINDOWS}\\#{backup_name}" elsif opciones[:file_id] + puts "[*] Dron: Download desde Google Drive" + puts " Nodo: dasu-sql4" puts " Download por file ID" download_url = "https://drive.google.com/uc?export=download&id=#{opciones[:file_id]}" + backup_name = File.basename(opciones[:file_id]) puts " File ID: #{opciones[:file_id]}" - puts " Destino: #{LOCAL_BACKUP}" + puts " Destino: #{BACKUP_DIR_WINDOWS}\\#{backup_name}" 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 "[*] Dron: Download desde Google Drive" + puts " Nodo: dasu-sql4" + puts " Drive ruta: #{drive_ruta}" + puts " File ID: #{file_id}" 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" + puts "[!] Error: Se requiere --test-file o --file-id" 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' +ps_script = <<~PSCMD.strip $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" + $BACKUP_DIR = "F:\\BACKUP" + $DOWNLOAD_URL = "#{download_url}" + $BACKUP_NAME = "#{backup_name}" + $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 "C:\\temp" $null = New-Item -ItemType Directory -Force -Path $BACKUP_DIR Write-Host "[1/3] Iniciando descarga..." @@ -114,12 +84,10 @@ ps_script = <<~'PSCMD' 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 = "" @@ -131,16 +99,13 @@ ps_script = <<~'PSCMD' 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 @@ -176,93 +141,17 @@ ps_script = <<~'PSCMD' 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) +puts "\n[1/2] Ejecutando download en dasu-sql4..." +DasuExecutor.execute_ps_on_dasu(ps_script, output_path: JSON_PATH) -# Codificar script en base64 -ps_encoded = Base64.strict_encode64(ps_script) +puts "\n[2/2] Leyendo output JSON..." +resultado = DasuExecutor.read_json_from_dasu(JSON_PATH) -# 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 +puts " Output: #{resultado.to_json}" -# Guardar script bash en archivo temporal -bash_file = Tempfile.new(['download', '.sh']) -bash_file.write(bash_script) -bash_file.chmod(0755) -bash_file.close +puts "\n✅ Download completado" +puts " Backup: #{resultado['backup_local']}" +puts " Duración: #{resultado['duracion']}s" -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 +File.write('/tmp/dron_download_output.json', resultado.to_json) +puts " JSON guardado en: /tmp/dron_download_output.json" diff --git a/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb b/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb index 3aacc09d..50292dd6 100644 --- a/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb +++ b/adn/tools/cli/drones/dasuten_exportar_diferencial_srvv-fenix.rb @@ -5,13 +5,14 @@ # 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 +# Flujo: srvv-fenix → Backup diferencial en X:\ (share srv-ns8) +# Salida: export_differential_complete con ruta del backup # ========================================================== require 'tiny_tds' require 'time' require 'json' +require_relative 'lib/dasu_executor' # Nombre del archivo con timestamp TIMESTAMP = Time.now.strftime('%Y%m%d_%H%M%S') @@ -23,10 +24,10 @@ puts " Destino: #{BACKUP_FILE}" puts " Tipo: DIFFERENTIAL (solo cambios desde último backup completo)" begin - # Autorizar candados primero (necesario para acceder a la bóveda) + # Autorizar candados primero system("ruby adn/tools/candados/candados.rb authorize >/dev/null 2>&1") - # Obtener credencial de candados (patrón usado en otros drones) + # Obtener credencial de candados pass_cmd = "ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null" sql_pass = `#{pass_cmd}`.strip @@ -34,7 +35,7 @@ begin sql_host = '10.0.10.200' if sql_pass.empty? - puts "[!] Error: No se pudo obtener credencial de candados (utnlarioja.intranet:monlaricardo)" + puts "[!] Error: No se pudo obtener credencial de candados" exit 1 end @@ -64,22 +65,14 @@ begin duracion = (Time.now - start).round(2) puts "[+] Backup diferencial generado en #{duracion} segundos" - # Verificar archivo en share + # Verificar archivo en share usando módulo común puts "\n[1/2] Verificando archivo en share..." - begin - verify_ps = <<~PSCMD.strip - if (Test-Path "#{BACKUP_FILE}") { - $size = (Get-Item "#{BACKUP_FILE}").Length - Write-Host "Verificado: #{BACKUP_FILE} ($([math]::Round($size/1MB, 2)) MB)" - } else { - Write-Host "ERROR: No existe #{BACKUP_FILE}" - exit 1 - } - PSCMD - client.execute("EXEC xp_cmdshell 'powershell -Command \"#{verify_ps.gsub('"', '\"')}\"'") - puts "[+] Archivo verificado" - rescue => e - puts "[!] Error al verificar en X: - #{e.message}" + file_info = DasuExecutor.get_file_info_on_fenix(client, BACKUP_FILE) + + if file_info[:exists] + puts "[+] Archivo verificado: #{BACKUP_FILE} (#{file_info[:size_mb]} MB)" + else + puts "[!] Error: No existe el archivo en #{BACKUP_FILE}" 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 index cb887b48..6512183b 100755 --- a/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb +++ b/adn/tools/cli/drones/dasuten_exportar_srvv-fenix.rb @@ -5,19 +5,18 @@ # 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 +# Flujo: srvv-fenix → Backup comprimido en X:\ (share srv-ns8) +# Salida: export_complete con ruta del backup # ========================================================== require 'tiny_tds' require 'time' require 'json' +require_relative 'lib/dasu_executor' # Nombre del archivo con timestamp TIMESTAMP = Time.now.strftime('%Y%m%d_%H%M%S') BACKUP_FILE = "sysdasuten_FULL_#{TIMESTAMP}.bak" - -# Unidad de red mapeada en srvv-fenix (share de srv-ns8) - UNICO DESTINO BACKUP_NS8 = "X:\\#{BACKUP_FILE}" puts "[*] Dron: Exportar backup COMPLETO DASUTEN" @@ -26,10 +25,10 @@ puts " Destino: #{BACKUP_NS8}" puts " Tipo: FULL (completo)" begin - # Autorizar candados primero (necesario para acceder a la bóveda) + # Autorizar candados primero system("ruby adn/tools/candados/candados.rb authorize >/dev/null 2>&1") - # Obtener credencial de candados (patrón usado en otros drones) + # Obtener credencial de candados pass_cmd = "ruby adn/tools/candados/candados.rb get utnlarioja.intranet:monlaricardo 2>/dev/null" sql_pass = `#{pass_cmd}`.strip @@ -37,7 +36,7 @@ begin sql_host = '10.0.10.200' if sql_pass.empty? - puts "[!] Error: No se pudo obtener credencial de candados (utnlarioja.intranet:monlaricardo)" + puts "[!] Error: No se pudo obtener credencial de candados" exit 1 end @@ -51,8 +50,7 @@ begin # Crear directorio en share si no existe puts "\n[0/2] Verificando directorio en share..." - mkdir_sql = "EXEC master.sys.xp_create_subdir 'X:\\'" - client.execute(mkdir_sql) + client.execute("EXEC master.sys.xp_create_subdir 'X:\\'") puts "[+] Directorio share asegurado: X:\\" # Generar backup comprimido directamente en share X: @@ -65,21 +63,14 @@ begin duracion = (Time.now - start).round(2) puts "[+] Backup generado en #{duracion} segundos" - # Verificar archivo en share + # Verificar archivo en share usando módulo común puts "[2/2] Verificando archivo en share..." - begin - verify_ps = <<~PSCMD.strip - if (Test-Path "#{BACKUP_NS8}") { - $size = (Get-Item "#{BACKUP_NS8}").Length - Write-Host "Verificado: #{BACKUP_NS8} ($([math]::Round($size/1MB, 2)) MB)" - } else { - Write-Host "ERROR: No existe #{BACKUP_NS8}" - exit 1 - } - PSCMD - client.execute("EXEC xp_cmdshell 'powershell -Command \"#{verify_ps.gsub('"', '\"')}\"'") - rescue => e - puts "[!] Error al verificar en X: - #{e.message}" + file_info = DasuExecutor.get_file_info_on_fenix(client, BACKUP_NS8) + + if file_info[:exists] + puts "[+] Archivo verificado: #{BACKUP_NS8} (#{file_info[:size_mb]} MB)" + else + puts "[!] Error: No existe el archivo en #{BACKUP_NS8}" exit 1 end @@ -101,7 +92,6 @@ begin puts "\n✅ Export completado en share" 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 diff --git a/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb b/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb index f5b53e3f..f9e8f3d8 100644 --- a/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb +++ b/adn/tools/cli/drones/dasuten_restaurar_dasu-sql4.rb @@ -12,18 +12,16 @@ require 'time' require 'json' -require 'base64' -require 'tempfile' -require 'shellwords' +require_relative 'lib/dasu_executor' DOWNLOAD_OUTPUT = '/tmp/dron_download_output.json' BACKUP_DIR_WINDOWS = 'F:\\BACKUP' +JSON_PATH = 'C:\temp\dron_restore_output.json' # 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" @@ -32,24 +30,18 @@ else puts "[*] Dron: Restaurar backup DASUTEN (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' +# PowerShell script para restore +ps_script = <<~'PSCMD'.strip $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) { @@ -63,10 +55,8 @@ ps_script = <<~'PSCMD' 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' @@ -77,7 +67,6 @@ ps_script = <<~'PSCMD' 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) @@ -90,14 +79,11 @@ ps_script = <<~'PSCMD' 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_total = $duracionRestore duracion_restore = $duracionRestore estado_integridad = "PENDING_CHECKDB" timestamp = (Get-Date -Format "o") @@ -110,90 +96,18 @@ ps_script = <<~'PSCMD' $res | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 PSCMD -# Codificar script en base64 -ps_encoded = Base64.strict_encode64(ps_script) +puts "\n[1/2] Ejecutando restore en dasu-sql4..." +DasuExecutor.execute_ps_on_dasu(ps_script, output_path: JSON_PATH) -# 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 +puts "\n[2/2] Leyendo output JSON..." +resultado = DasuExecutor.read_json_from_dasu(JSON_PATH) -# Guardar script bash en archivo temporal -bash_file = Tempfile.new(['restore', '.sh']) -bash_file.write(bash_script) -bash_file.chmod(0755) -bash_file.close +puts " Output: #{resultado.to_json}" -remote_bash_path = "/tmp/dron_restore_#{Time.now.to_i}.sh" +puts "\n✅ Restauración completada" +puts " Backup: #{resultado['backup_ruta']}" +puts " Estado: #{resultado['estado_integridad']}" +puts " Duración: #{resultado['duracion_total']}s" -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 +File.write('/tmp/dron_restore_output.json', resultado.to_json) +puts " JSON guardado en: /tmp/dron_restore_output.json" diff --git a/adn/tools/cli/drones/dasuten_restaurar_diferencial_dasu-sql4.rb b/adn/tools/cli/drones/dasuten_restaurar_diferencial_dasu-sql4.rb new file mode 100644 index 00000000..be1c4f25 --- /dev/null +++ b/adn/tools/cli/drones/dasuten_restaurar_diferencial_dasu-sql4.rb @@ -0,0 +1,119 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/drones/dasuten_restaurar_diferencial_dasu-sql4.rb +# ========================================================== +# Dron: Restaurar backup DIFERENCIAL en dasu-sql4 +# Flujo: RESTORE DATABASE ... WITH DIFFERENTIAL (sobre BD existente) +# Entrada: Backup diferencial en F:\BACKUP\ +# Salida: differential_restore_complete +# ========================================================== + +require 'time' +require 'json' +require_relative 'lib/dasu_executor' + +JSON_PATH = 'C:\temp\dron_restore_dif_output.json' + +puts "[*] Dron: Restaurar backup DIFERENCIAL DASUTEN" +puts " Nodo: dasu-sql4" +puts " Backup: sysdasuten_DIF_*.bak (más reciente)" +puts " Tipo: DIFFERENTIAL (aplica cambios sobre BD existente)" + +# PowerShell script para restore diferencial +ps_script = <<~'PSCMD'.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\temp\dron_restore_dif_output.json" + + $null = New-Item -ItemType Directory -Force -Path "C:\temp" + + Write-Host "[1/3] Buscando backup diferencial más reciente..." + $backupFile = Get-ChildItem "F:\BACKUP\sysdasuten_DIF_*.bak" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + + if (-not $backupFile) { + Write-Host "[!] No se encontró backup diferencial en F:\BACKUP\" + exit 1 + } + + $LOCAL_BACKUP = $backupFile.FullName + Write-Host " Archivo: $LOCAL_BACKUP" + Write-Host " Tamaño: $([math]::Round($backupFile.Length / 1MB, 2)) MB" + + $sqlcmd = "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\sqlcmd.exe" + + Write-Host "`n[2/3] Ejecutando RESTORE DATABASE ... WITH DIFFERENTIAL..." + $start = Get-Date + + $restoreSql = @" + RESTORE DATABASE sysdasuten + FROM DISK = '$LOCAL_BACKUP' + WITH NORECOVERY, REPLACE + "@ + + Write-Host " SQL: $restoreSql" + + $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 diferencial completado en $duracionRestore segundos" + + Write-Host "`n[3/3] Poniendo base de datos ONLINE..." + + $recoverySql = "RESTORE DATABASE sysdasuten WITH RECOVERY" + $recoveryOutput = & $sqlcmd -S localhost -Q $recoverySql 2>&1 + $exitoRecovery = $LASTEXITCODE -eq 0 + $duracionRecovery = [math]::Round((New-TimeSpan -Start (Get-Date) -End (Get-Date)).TotalSeconds, 2) + + if ($exitoRecovery) { + Write-Host "[+] Base de datos ONLINE" + } else { + Write-Host "[!] Error en recovery:" + Write-Host $recoveryOutput + exit 1 + } + + $duracionTotal = [math]::Round($duracionRestore + $duracionRecovery, 2) + + $res = @{ + paso = "differential_restore_complete" + backup_ruta = $LOCAL_BACKUP + backup_tipo = "differential" + nodo = "dasu-sql4" + duracion_total = $duracionTotal + duracion_restore = $duracionRestore + duracion_recovery = $duracionRecovery + estado_integridad = "PENDING_CHECKDB" + timestamp = (Get-Date -Format "o") + siguiente_paso = "verificar_integridad" + } + + Write-Host "`n✅ Restauración diferencial completada" + Write-Host " Estado: BD ONLINE" + Write-Host " Output: $($res | ConvertTo-Json -Compress)" + + $res | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +puts "\n[1/2] Ejecutando restore diferencial en dasu-sql4..." +DasuExecutor.execute_ps_on_dasu(ps_script, output_path: JSON_PATH) + +puts "\n[2/2] Leyendo output JSON..." +resultado = DasuExecutor.read_json_from_dasu(JSON_PATH) + +puts " Output: #{resultado.to_json}" + +puts "\n✅ Restauración diferencial completada" +puts " Backup: #{resultado['backup_ruta']}" +puts " Estado: #{resultado['estado_integridad']}" +puts " Duración total: #{resultado['duracion_total']}s" + +File.write('/tmp/dron_restore_dif_output.json', resultado.to_json) +puts " JSON guardado en: /tmp/dron_restore_dif_output.json" diff --git a/adn/tools/cli/drones/dasuten_verificar-integridad_dasu-sql4.rb b/adn/tools/cli/drones/dasuten_verificar-integridad_dasu-sql4.rb new file mode 100644 index 00000000..cab3f895 --- /dev/null +++ b/adn/tools/cli/drones/dasuten_verificar-integridad_dasu-sql4.rb @@ -0,0 +1,96 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/drones/dasuten_verificar-integridad_dasu-sql4.rb +# ========================================================== +# Dron: Verificar integridad de BD en dasu-sql4 +# Flujo: DBCC CHECKDB + validaciones post-restore +# Entrada: BD restaurada (pendiente verificación) +# Salida: verify_complete con estado de integridad +# ========================================================== + +require 'time' +require 'json' +require_relative 'lib/dasu_executor' + +JSON_PATH = 'C:\temp\dron_verify_output.json' + +puts "[*] Dron: Verificar integridad BD DASUTEN" +puts " Nodo: dasu-sql4" + +# PowerShell script para DBCC CHECKDB +ps_script = <<~'PSCMD'.strip + $ErrorActionPreference = "Stop" + $JSON_PATH = "C:\temp\dron_verify_output.json" + + $null = New-Item -ItemType Directory -Force -Path "C:\temp" + + Write-Host "[1/2] Ejecutando DBCC CHECKDB..." + $start = Get-Date + + $sqlcmd = "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\sqlcmd.exe" + + $checkdbSql = "DBCC CHECKDB('sysdasuten') WITH NO_INFOMSGS, ALL_ERRORMSGS" + Write-Host " SQL: $checkdbSql" + + $checkdbOutput = & $sqlcmd -S localhost -Q $checkdbSql 2>&1 + $exitoCheckdb = $LASTEXITCODE -eq 0 + $duracionCheckdb = [math]::Round((New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds, 2) + + if ($exitoCheckdb) { + Write-Host "[+] CHECKDB completado en $duracionCheckdb segundos" + + $tieneErrores = $checkdbOutput -match 'ERROR|corrupt|damaged|inconsistency' + $estadoIntegridad = if ($tieneErrores) { 'ERROR_DETECTED' } else { 'OK' } + + $paginasVerificadas = 0 + if ($checkdbOutput -match '(\d+)\s+pages') { + $paginasVerificadas = [int]$matches[1] + } + } else { + $estadoIntegridad = 'CHECKDB_FAILED' + Write-Host "[!] CHECKDB falló:" + Write-Host $checkdbOutput + } + + Write-Host "`n[2/2] Validaciones adicionales..." + + $bdStatusSql = "SELECT state_desc, recovery_model_desc FROM sys.databases WHERE name = 'sysdasuten'" + $bdStatus = & $sqlcmd -S localhost -Q $bdStatusSql -h -1 + + Write-Host " Estado BD: $bdStatus" + + $res = @{ + paso = "verify_complete" + nodo = "dasu-sql4" + duracion_checkdb = $duracionCheckdb + estado_integridad = $estadoIntegridad + paginas_verificadas = $paginasVerificadas + bd_estado = $bdStatus + timestamp = (Get-Date -Format "o") + pipeline_completo = $true + } + + Write-Host "`n✅ Verificación completada" + Write-Host " Estado integridad: $estadoIntegridad" + Write-Host " Output: $($res | ConvertTo-Json -Compress)" + + $res | ConvertTo-Json | Out-File -FilePath $JSON_PATH -Encoding utf8 +PSCMD + +puts "\n[1/2] Ejecutando DBCC CHECKDB en dasu-sql4..." +DasuExecutor.execute_ps_on_dasu(ps_script, output_path: JSON_PATH) + +puts "\n[2/2] Leyendo output JSON..." +resultado = DasuExecutor.read_json_from_dasu(JSON_PATH) + +puts " Output: #{resultado.to_json}" + +puts "\n✅ Verificación completada" +puts " Estado integridad: #{resultado['estado_integridad']}" +puts " Páginas verificadas: #{resultado['paginas_verificadas'] || 'N/A'}" +puts " Duración: #{resultado['duracion_checkdb']}s" + +File.write('/tmp/dron_verify_output.json', resultado.to_json) +puts " JSON guardado en: /tmp/dron_verify_output.json" diff --git a/adn/tools/cli/drones/lib/dasu_executor.rb b/adn/tools/cli/drones/lib/dasu_executor.rb new file mode 100644 index 00000000..e468fb09 --- /dev/null +++ b/adn/tools/cli/drones/lib/dasu_executor.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +# ========================================================== +# adn/tools/cli/drones/lib/dasu_executor.rb +# ========================================================== +# Módulo helper para ejecutar PowerShell en dasu-sql4 vía srv-dasu +# Centraliza: SSH, sshpass, base64 encoding, lectura de JSON +# ========================================================== + +require 'base64' +require 'tempfile' + +module DasuExecutor + # Configuración del target dasu-sql4 + TARGET_IP = '192.168.1.11' + TARGET_USER = 'Administrador' + TARGET_PASS = 'UTNlarioja00DASU' + TARGET_PORT = '7022' + + # Path donde se guardan los JSON output en dasu-sql4 + DEFAULT_JSON_PATH = 'C:\temp\output.json' + + module_function + + # Ejecuta un script PowerShell en dasu-sql4 vía srv-dasu + # + # @param ps_script [String] Script PowerShell a ejecutar + # @param output_path [String] Path donde PowerShell guardará el JSON (default: C:\temp\output.json) + # @return [Hash] JSON parseado del output + # + # Ejemplo: + # result = DasuExecutor.execute_ps_on_dasu(<<~PS) + # @{ status = "ok" } | ConvertTo-Json | Out-File C:\temp\out.json + # Get-Content C:\temp\out.json -Raw + # PS + def execute_ps_on_dasu(ps_script, output_path: DEFAULT_JSON_PATH) + ps_encoded = Base64.strict_encode64(ps_script) + + bash_script = <<~BASHHEREDOC + #!/bin/bash + export SSHPASS="#{TARGET_PASS}" + sshpass -e ssh -o StrictHostKeyChecking=no -p #{TARGET_PORT} #{TARGET_USER}@#{TARGET_IP} \ + "powershell -Command \\\"Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{ps_encoded}')))\\\"" + BASHHEREDOC + + execute_bash_temporary(bash_script) + end + + # Ejecuta un script PowerShell y lee el JSON resultante + # Asume que el script escribe un JSON en output_path + # + # @param ps_script [String] Script PowerShell a ejecutar + # @param output_path [String] Path del JSON en dasu-sql4 + # @return [Hash] JSON parseado + def execute_ps_with_json_output(ps_script, output_path: DEFAULT_JSON_PATH) + # Ejecutar el script principal + execute_ps_on_dasu(ps_script, output_path: output_path) + + # Leer el JSON resultante + read_json_from_dasu(output_path) + end + + # Lee un JSON desde dasu-sql4 + # + # @param output_path [String] Path del JSON en dasu-sql4 (ej: C:\temp\output.json) + # @return [Hash] JSON parseado + def read_json_from_dasu(output_path) + json_ps = "Get-Content #{output_path} -Raw" + json_encoded = Base64.strict_encode64(json_ps) + + json_script = <<~BASHHEREDOC.strip + #!/bin/bash + export SSHPASS="#{TARGET_PASS}" + sshpass -e ssh -o StrictHostKeyChecking=no -p #{TARGET_PORT} #{TARGET_USER}@#{TARGET_IP} \ + "powershell -Command \\\"Invoke-Expression ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('#{json_encoded}')))\\\"" + BASHHEREDOC + + resultado = execute_bash_temporary(json_script) + resultado = resultado.strip + + # Extraer JSON del output (puede tener ruido) + json_match = resultado.match(/\{.*\}/m) + resultado = json_match ? json_match[0] : resultado + + require 'json' + JSON.parse(resultado) + end + + # Ejecuta un script bash temporal y lo limpia después + # + # @param bash_script [String] Script bash a ejecutar + # @param remote_base [String] Directorio remoto base (default: /tmp) + # @return [String] Output del comando + def execute_bash_temporary(bash_script, remote_base: '/tmp') + bash_file = Tempfile.new(['dasu', '.sh']) + bash_file.write(bash_script) + bash_file.chmod(0755) + bash_file.close + + remote_path = "#{remote_base}/dron_#{Time.now.to_i}.sh" + + # Copiar script a srv-dasu + scp_cmd = "scp -o StrictHostKeyChecking=no #{bash_file.path} srv-dasu:#{remote_path}" + unless system(scp_cmd) + bash_file.unlink + raise "Error al copiar script a srv-dasu" + end + + # Ejecutar script + ssh_cmd = "ssh srv-dasu 'bash #{remote_path}'" + output = `#{ssh_cmd}` + output = output.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?') if output + + # Limpieza + bash_file.unlink + system("ssh srv-dasu 'rm -f #{remote_path}' 2>/dev/null") + + output + end + + # Helper para ejecutar PowerShell en fenix vía xp_cmdshell + # Útil para verificar archivos en shares mapeados + # + # @param client [TinyTds::Client] Conexión SQL Server + # @param ps_script [String] Script PowerShell a ejecutar + # @return [Array] Resultado de la ejecución + def execute_ps_on_fenix(client, ps_script) + escaped = ps_script.gsub('"', '\"') + client.execute("EXEC xp_cmdshell 'powershell -Command \"#{escaped}\"'") + end + + # Verifica si un archivo existe en un path Windows/fenix + # + # @param client [TinyTds::Client] Conexión SQL Server + # @param path [String] Path del archivo (ej: X:\backup.bak) + # @return [Boolean] true si existe + def file_exists_on_fenix?(client, path) + result = client.execute("EXEC master.sys.xp_fileexist '#{path}'") + result.to_a.any? { |f| f['File Exists'] == 1 } + end + + # Obtiene información de tamaño de un archivo en fenix + # + # @param client [TinyTds::Client] Conexión SQL Server + # @param path [String] Path del archivo + # @return [Hash] Con :exists, :size_bytes, :size_mb + def get_file_info_on_fenix(client, path) + ps_script = <<~PSCMD.strip + if (Test-Path "#{path}") { + $item = Get-Item "#{path}" + Write-Host "EXISTS:1" + Write-Host "SIZE:$($item.Length)" + } else { + Write-Host "EXISTS:0" + } + PSCMD + + result = execute_ps_on_fenix(client, ps_script) + output = result.map { |r| r['output'] }.compact.join("\n") + + exists = output.include?('EXISTS:1') + size = output.match(/SIZE:(\d+)/) ? $1.to_i : 0 + + { + exists: exists, + size_bytes: size, + size_mb: (size / 1024.0 / 1024.0).round(2) + } + end +end