docs & tools: Actualización de informes y scripts de migración DASUTEN

This commit is contained in:
Ricardo Monla
2026-04-20 16:25:51 -03:00
parent faf46be84b
commit 65dd92b769
44 changed files with 3822 additions and 568 deletions
@@ -5,8 +5,8 @@
# 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)
# Flujo: Google Drive → F:\BACKUP\sysdasuten_FULL_YYYYMMDD_HHMMSS.bak
# Usa: rclone copy (mantiene nombre original del backup)
# ==========================================================
require 'time'
@@ -27,9 +27,12 @@ end.parse!(ARGV)
BACKUP_DIR_WINDOWS = "F:\\BACKUP"
BACKUP_NAME_DEFAULT = "sysdasuten_compressed_ADN.bak"
JSON_PATH = "C:\temp\dron_download_output.json"
JSON_PATH = "C:\\temp\\dron_download_output.json"
# Determinar archivo a descargar
backup_name = nil
file_id = nil
if opciones[:test_file]
puts "[*] Dron: Download desde Google Drive"
puts " Nodo: dasu-sql4"
@@ -52,10 +55,20 @@ else
if File.exist?(UPLOAD_OUTPUT)
upload_data = JSON.parse(File.read(UPLOAD_OUTPUT))
drive_ruta = upload_data['drive_ruta']
file_id = File.basename(drive_ruta)
# Extraer el nombre EXACTO del archivo desde drive_ruta (ej: "rmonla-GDrive:drive_bkps-dasu/sysdasuten_FULL_20260415_121943.bak")
backup_name = File.basename(drive_ruta)
puts "[*] Dron: Buscando Google Drive ID real (rclone)..."
raw_json = `rclone lsjson '#{drive_ruta}' 2>/dev/null`.strip
begin
parsed = JSON.parse(raw_json).first
file_id = parsed['ID']
rescue
puts "[!] Error al parsear ID de Google Drive. rclone regresó vacio."
exit 1
end
download_url = "https://drive.google.com/uc?export=download&id=#{file_id}"
puts "[*] Dron: Download desde Google Drive"
puts " Nodo: dasu-sql4"
puts " Drive ruta: #{drive_ruta}"
puts " File ID: #{file_id}"
@@ -66,55 +79,44 @@ else
end
end
# Validar que backup_name tenga el formato correcto (FULL o DIF con fecha)
unless backup_name =~ /sysdasuten_(FULL|DIF)_\d{8}_\d{6}\.bak/
puts "[!] Advertencia: El nombre del backup '#{backup_name}' no sigue el formato esperado (sysdasuten_FULL_YYYYMMDD_HHMMSS.bak)"
end
# PowerShell script para download
ps_script = <<~PSCMD.strip
$ErrorActionPreference = "Stop"
$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"
$REMOTE_RCLONE_PATH = "#{drive_ruta}"
$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 "[1/3] Iniciando descarga con Rclone Nativo..."
Write-Host " Origen: $REMOTE_RCLONE_PATH"
Write-Host " Destino: $LOCAL_BACKUP"
Write-Host "[2/3] Descargando..."
$start = Get-Date
try {
$response = Invoke-WebRequest -Uri $DOWNLOAD_URL -UseBasicParsing -Headers @{
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
$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] }
# Limpiamos el ultimo archivo varado (por si el anterior dejo su esqueleto de 0 bytes)
if (Test-Path $LOCAL_BACKUP) { Remove-Item $LOCAL_BACKUP -Force }
Write-Host " File ID: $fileId"
Write-Host " UUID: $uuid"
# Llamamos al rclone binario local apuntando al config
& C:\\rclone\\rclone.exe copy "$REMOTE_RCLONE_PATH" "$BACKUP_DIR\\" --config "C:\\rclone\\rclone.conf" --stats-one-line --stats 1s
$downloadConfirmUrl = "https://drive.usercontent.google.com/download?id=$fileId&export=download&confirm=$confirm&uuid=$uuid"
Write-Host " URL confirmada: $downloadConfirmUrl"
$finalResponse = Invoke-WebRequest -Uri $downloadConfirmUrl -UseBasicParsing -Headers @{
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
[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: $_"
if ($LASTEXITCODE -ne 0) {
Write-Host "[!] Error: Falló la descarga de rclone (Código exit: $LASTEXITCODE)"
exit 1
}
$dur = (New-TimeSpan -Start $start -End (Get-Date)).TotalSeconds
Write-Host "[+] Descarga en $([math]::Round($dur, 2))s"
Write-Host "[3/3] Verificando..."
if (Test-Path $LOCAL_BACKUP) {
$tam = (Get-Item $LOCAL_BACKUP).Length
@@ -131,6 +133,7 @@ ps_script = <<~PSCMD.strip
$res = @{
paso = "download_complete"
backup_local = $LOCAL_BACKUP
backup_name = $BACKUP_NAME
drive_origen = "Google Drive"
nodo = "dasu-sql4"
duracion = [math]::Round($dur, 2)
@@ -12,6 +12,7 @@
require 'tiny_tds'
require 'time'
require 'json'
require 'base64'
require_relative 'lib/dasu_executor'
# Nombre del archivo con timestamp
@@ -33,6 +34,10 @@ begin
sql_user = 'UTNlarioja\\monlaricardo'
sql_host = '10.0.10.200'
smb_user = 'rmonla'
smb_server = 'srv-ns8'
smb_share = 'bkps-dasuten'
smb_path = "\\\\#{smb_server}\\#{smb_share}"
if sql_pass.empty?
puts "[!] Error: No se pudo obtener credencial de candados"
@@ -47,8 +52,24 @@ begin
)
puts "[+] Conectado a SQL Server en #{sql_host} (auth: Windows)"
# Mapear share SMB con credenciales usando PowerShell
puts "\n[0/3] Mapeando share SMB #{smb_path}..."
# Escapar password para PowerShell
sql_pass_escaped = sql_pass.gsub("'", "''")
ps_map = "net use X: #{smb_path} #{sql_pass_escaped} /user:#{smb_user} /persistent:no"
map_result = client.execute("EXEC xp_cmdshell '#{ps_map}'")
map_output = []
map_result.each { |r| map_output << r['output'] if r['output'] && !r['output'].strip.empty? }
puts " #{map_output.join("\n ")}"
puts "[+] Share mapeado en X: -> #{smb_path}"
# Crear directorio en share si no existe
puts "\n[1/3] Verificando directorio en share..."
client.execute("EXEC master.sys.xp_create_subdir 'X:\\'").each { |r| }
puts "[+] Directorio share asegurado: X:\\"
# Generar backup diferencial directamente en share X:
puts "\n[0/2] Generando backup diferencial en share X:..."
puts "[2/3] Generando backup diferencial en share X:..."
start = Time.now
sql = <<~SQL.strip
@@ -65,19 +86,22 @@ begin
duracion = (Time.now - start).round(2)
puts "[+] Backup diferencial generado en #{duracion} segundos"
# Verificar archivo en share usando módulo común
puts "\n[1/2] Verificando archivo en share..."
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)"
# Verificar archivo en share directamente en srv-ns8 vía SSH
puts "\n[3/3] Verificando archivo en share..."
backup_unix = BACKUP_FILE.gsub('X:\\', '/mnt/ns8Disco2/bkps-dasuten/')
ls_result = `ssh rmonla@10.0.10.8 "ls -l '#{backup_unix}' 2>&1"`.strip
if $?.success? && ls_result.include?(File.basename(backup_unix))
# Extraer tamaño del output de ls -l
file_size = ls_result.split[4].to_i
puts "[+] Archivo verificado: #{backup_unix} (#{file_size.to_i / 1024 / 1024} MB)"
else
puts "[!] Error: No existe el archivo en #{BACKUP_FILE}"
puts "[!] Error: No existe el archivo en #{backup_unix}"
puts " SSH output: #{ls_result}"
exit 1
end
# Tamaño aproximado de la BD base
puts "\n[2/2] Información de tamaño..."
puts "\n[4/3] Información de tamaño..."
begin
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"
@@ -12,6 +12,7 @@
require 'tiny_tds'
require 'time'
require 'json'
require 'base64'
require_relative 'lib/dasu_executor'
# Nombre del archivo con timestamp
@@ -34,6 +35,10 @@ begin
sql_user = 'UTNlarioja\\monlaricardo'
sql_host = '10.0.10.200'
smb_user = 'rmonla'
smb_server = 'srv-ns8'
smb_share = 'bkps-dasuten'
smb_path = "\\\\#{smb_server}\\#{smb_share}"
if sql_pass.empty?
puts "[!] Error: No se pudo obtener credencial de candados"
@@ -48,13 +53,23 @@ begin
)
puts "[+] Conectado a SQL Server en #{sql_host} (auth: Windows)"
# Mapear share SMB con credenciales usando net use
puts "\n[0/3] Mapeando share SMB #{smb_path}..."
sql_pass_escaped = sql_pass.gsub("'", "''")
ps_map = "net use X: #{smb_path} #{sql_pass_escaped} /user:#{smb_user} /persistent:no"
map_result = client.execute("EXEC xp_cmdshell '#{ps_map}'")
map_output = []
map_result.each { |r| map_output << r['output'] if r['output'] && !r['output'].strip.empty? }
puts " #{map_output.join("\n ")}"
puts "[+] Share mapeado en X: -> #{smb_path}"
# Crear directorio en share si no existe
puts "\n[0/2] Verificando directorio en share..."
client.execute("EXEC master.sys.xp_create_subdir 'X:\\'")
puts "\n[1/3] Verificando directorio en share..."
client.execute("EXEC master.sys.xp_create_subdir 'X:\\'").each { |r| }
puts "[+] Directorio share asegurado: X:\\"
# Generar backup comprimido directamente en share X:
puts "[1/2] Generando backup en share X: (srv-ns8)..."
puts "[2/3] Generando backup en share X: (srv-ns8)..."
start = Time.now
result = client.execute("BACKUP DATABASE sysdasuten TO DISK = '#{BACKUP_NS8}' WITH INIT, COMPRESSION, STATS=10")
@@ -63,14 +78,17 @@ begin
duracion = (Time.now - start).round(2)
puts "[+] Backup generado en #{duracion} segundos"
# Verificar archivo en share usando módulo común
puts "[2/2] Verificando archivo en share..."
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)"
# Verificar archivo en share directamente en srv-ns8 vía SSH
puts "[3/3] Verificando archivo en share..."
backup_unix = BACKUP_NS8.gsub('X:\\', '/mnt/ns8Disco2/bkps-dasuten/')
ls_result = `ssh rmonla@10.0.10.8 "ls -l '#{backup_unix}' 2>&1"`.strip
if $?.success? && ls_result.include?(File.basename(backup_unix))
# Extraer tamaño del output de ls -l
file_size = ls_result.split[4].to_i
puts "[+] Archivo verificado: #{backup_unix} (#{file_size.to_i / 1024 / 1024} MB)"
else
puts "[!] Error: No existe el archivo en #{BACKUP_NS8}"
puts "[!] Error: No existe el archivo en #{backup_unix}"
puts " SSH output: #{ls_result}"
exit 1
end
@@ -22,48 +22,60 @@ JSON_PATH = 'C:\temp\dron_restore_output.json'
if File.exist?(DOWNLOAD_OUTPUT)
output = JSON.parse(File.read(DOWNLOAD_OUTPUT))
backup_local = output['backup_local']
backup_name = File.basename(backup_local)
LOCAL_BACKUP = "#{BACKUP_DIR_WINDOWS}\\#{backup_name}"
backup_name = output['backup_name'] || File.basename(backup_local)
LOCAL_BACKUP = backup_local # Usar la ruta EXACTA que viene del download
puts "[*] Dron: Restaurar backup DASUTEN"
puts " Nodo: dasu-sql4"
puts " Backup: #{LOCAL_BACKUP} (desde download)"
puts " Nombre: #{backup_name}"
else
LOCAL_BACKUP = "#{BACKUP_DIR_WINDOWS}\\sysdasuten_FULL_*.bak"
puts "[*] Dron: Restaurar backup DASUTEN (buscar más reciente)"
puts " Nodo: dasu-sql4"
puts " Backup: #{LOCAL_BACKUP} (búsqueda por patrón)"
end
puts " Nodo: dasu-sql4"
puts " Backup: #{LOCAL_BACKUP}"
# PowerShell script para restore
ps_script = <<~'PSCMD'.strip
ps_script = <<~PSCMD.strip
$ErrorActionPreference = "Stop"
$BACKUP_DIR = "F:\BACKUP"
$BACKUP_PATTERN = "sysdasuten_FULL_*.bak"
$JSON_PATH = "C:\temp\dron_restore_output.json"
$BACKUP_DIR = "F:\\BACKUP"
$BACKUP_NAME = "#{backup_name}"
$LOCAL_BACKUP = "$BACKUP_DIR\\$BACKUP_NAME"
$JSON_PATH = "C:\\temp\\dron_restore_output.json"
$null = New-Item -ItemType Directory -Force -Path "C:\temp"
$null = New-Item -ItemType Directory -Force -Path "C:\\temp"
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
Write-Host "[0/1] Verificando backup específico..."
Write-Host " Archivo esperado: $LOCAL_BACKUP"
if (-not (Test-Path $LOCAL_BACKUP)) {
Write-Host "[!] No se encontró backup en $LOCAL_BACKUP"
Write-Host " Buscando backup más reciente como fallback..."
$BACKUP_PATTERN = "sysdasuten_FULL_*.bak"
$backupFile = Get-ChildItem "$BACKUP_DIR\\$BACKUP_PATTERN" | Sort-Object Name -Descending | Select-Object -First 1
if (-not $backupFile) {
Write-Host "[!] No se encontró ningún backup en $BACKUP_DIR"
exit 1
}
$LOCAL_BACKUP = $backupFile.FullName
Write-Host " Fallback: $LOCAL_BACKUP"
}
$LOCAL_BACKUP = $backupFile.FullName
Write-Host " Archivo: $LOCAL_BACKUP"
$backupFile = Get-Item $LOCAL_BACKUP
Write-Host " Tamaño: $([math]::Round($backupFile.Length / 1MB, 2)) MB"
Write-Host "[1/1] Ejecutando RESTORE DATABASE..."
$start = Get-Date
$sqlcmd = "C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\sqlcmd.exe"
$sqlcmd = "C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\170\\Tools\\Binn\\sqlcmd.exe"
$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'
"@
MOVE 'sysdasuten' TO 'F:\\DATA\\sysdasuten.mdf',
MOVE 'sysdasuten_log' TO 'F:\\LOG\\sysdasuten_log.ldf'
"@
Write-Host " SQL: $restoreSql"
@@ -82,6 +94,7 @@ ps_script = <<~'PSCMD'.strip
$res = @{
paso = "restore_complete"
backup_ruta = $LOCAL_BACKUP
backup_name = $BACKUP_NAME
nodo = "dasu-sql4"
duracion_total = $duracionRestore
duracion_restore = $duracionRestore
@@ -16,17 +16,28 @@ require 'shellwords'
REMOTE_DRIVE = "rmonla-GDrive:drive_bkps-dasu"
# Leer ruta del backup desde el output de la transferencia
# Archivos de output de los pasos anteriores
TRANSFER_OUTPUT = '/tmp/dron_transfer_output.json'
if File.exist?(TRANSFER_OUTPUT)
EXPORT_OUTPUT = '/tmp/dron_export_output.json'
# Determinar cuál es el más reciente (si existen)
archivos_output = [TRANSFER_OUTPUT, EXPORT_OUTPUT].select { |f| File.exist?(f) }
output_reciente = archivos_output.max_by { |f| File.mtime(f) }
if output_reciente == TRANSFER_OUTPUT
output = JSON.parse(File.read(TRANSFER_OUTPUT))
BACKUP_LOCAL = output['backup_local']
BACKUP_LOCAL = output['backup_local'] || output['backup_ruta_unix']
BACKUP_NAME = File.basename(BACKUP_LOCAL)
puts "[*] Dron: Upload a Google Drive (DIFERENCIAL)"
puts "[*] Dron: Upload a Google Drive (Desde TRANSFERENCIA)"
elsif output_reciente == EXPORT_OUTPUT
output = JSON.parse(File.read(EXPORT_OUTPUT))
BACKUP_LOCAL = output['backup_ruta_unix']
BACKUP_NAME = File.basename(BACKUP_LOCAL)
puts "[*] Dron: Upload a Google Drive (Desde EXPORTACIÓN DIRECTA)"
else
BACKUP_LOCAL = "/var/tmp/sysdasuten_compressed_ADN.bak"
BACKUP_NAME = "sysdasuten_compressed_ADN_#{Time.now.strftime('%Y%m%d_%H%M%S')}.bak"
puts "[*] Dron: Upload a Google Drive (COMPLETO)"
puts "[*] Dron: Upload a Google Drive (COMPLETO FALLBACK)"
end
puts " Nodo: srv-ns8"
+9 -2
View File
@@ -108,8 +108,15 @@ module DasuExecutor
# 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
output_buffer = ""
IO.popen(ssh_cmd, err: [:child, :out]) do |io|
io.each_line do |line|
safe_line = line.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
print safe_line
output_buffer += safe_line
end
end
output = output_buffer
# Limpieza
bash_file.unlink