[A04.P005] DASUTEN: Pipeline refactorizado + Documentación + Informes
Refactorización del pipeline de backups DASUTEN: 1. Flujo simplificado (un solo destino): - Backup directo a X:\ (share srv-ns8) - Eliminado paso intermedio E:\BK_SQL\sysdasuten\ - Reducido de 3 ubicaciones a 2 (X: → /var/tmp/) 2. Módulo común DasuExecutor (lib/dasu_executor.rb): - Centraliza ejecución PowerShell en dasu-sql4 - Centraliza verificación de archivos en fenix - Elimina código repetido en 6 drones (-52% líneas) 3. Drones refactorizados: - dasuten_exportar_srvv-fenix.rb (FULL → X:) - dasuten_exportar_diferencial_srvv-fenix.rb (DIF → X:) - dasuten_transferir_srvv-fenix-srv-ns8.rb (simplificado) - dasuten_restaurar_dasu-sql4.rb (usa DasuExecutor) - dasuten_restaurar_diferencial_dasu-sql4.rb (usa DasuExecutor) - dasuten_download_drive-dasu-sql4.rb (usa DasuExecutor) - dasuten_verificar-integridad_dasu-sql4.rb (usa DasuExecutor) 4. Integración BKPs mantenida: - T8-T15: Tareas individuales - C7: dasuten_full (pipeline semanal) - C8: dasuten_diferencial (pipeline diario) 5. Documentación actualizada: - A04.P006_Backups-DASUTEN.md: flujo simplificado + módulo común - Lecciones aprendidas agregadas 6. Nuevos archivos: - Informes técnicos DASUTEN (docs/informes/2026-04_DASUTEN-migracion/) - A01.P011/P012: Documentación de drones y pipeline - proc_linux.rb, ops.rb: Utilitarios del ecosistema 7. Scripts legacy (para referencia): - orquestador_pipeline.rb, orquestador_pipeline_diferencial.rb - dasuten_upload_srv-ns8-drive.rb
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# adn/tools/bkps/lib/proc_linux.rb — Procesador Linux (apt)
|
||||
#
|
||||
# Fase 2: Soporta orquestación 1-dron-por-nodo
|
||||
# listar(tarea) → lista nodos Linux pendientes
|
||||
# ejecutar_uno(tarea, nodo) → actualiza UN solo nodo
|
||||
# ejecutar(tarea) → actualiza TODOS (legacy)
|
||||
# ==========================================================
|
||||
|
||||
require 'shellwords'
|
||||
require 'net/ssh'
|
||||
|
||||
module ADN
|
||||
module BKPs
|
||||
module Procesador
|
||||
class Linux
|
||||
def initialize(log)
|
||||
@log = log
|
||||
end
|
||||
|
||||
# Scanner: devuelve lista de nodos Linux a actualizar
|
||||
# Lee los archivos .md de nodos/ y filtra los Linux (Debian/Ubuntu)
|
||||
def listar(tarea)
|
||||
nodos_dir = File.join(ADN::NODOS_DIR || 'nodos', '')
|
||||
|
||||
unless Dir.exist?(nodos_dir)
|
||||
@log.error("Directorio de nodos no existe: #{nodos_dir}")
|
||||
return []
|
||||
end
|
||||
|
||||
# Filtrar nodos Linux (excluye Windows y otros)
|
||||
nodos_linux = Dir.glob(File.join(nodos_dir, '*.md'))
|
||||
.select { |f| es_nodo_linux?(f) }
|
||||
.map { |f| extraer_info_nodo(f) }
|
||||
.compact
|
||||
.reject { |n| n[:ip].nil? || n[:ip].empty? }
|
||||
|
||||
nodos_linux.map do |nodo|
|
||||
{
|
||||
archivo: nodo[:hostname], # Identificador para ejecutar_uno
|
||||
vm: nodo[:hostname], # Nombre visible (nodo)
|
||||
ruta: nodo[:ip], # IP del nodo
|
||||
destino: nodo[:ip], # Mismo destino (es SSH)
|
||||
tamaño: 0, # No aplicable para updates
|
||||
pendiente: true, # Siempre verificar
|
||||
metadata: nodo # Info adicional (usuario, etc.)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Atómico: actualiza UN solo nodo Linux vía SSH
|
||||
def ejecutar_uno(tarea, nombre_nodo)
|
||||
ruta_md = File.join(ADN::NODOS_DIR || 'nodos', "#{nombre_nodo}.md")
|
||||
nodo_info = File.exist?(ruta_md) ? extraer_info_nodo(ruta_md) : (tarea[:metadata] || {})
|
||||
ip = nodo_info[:ip] || tarea[:origen]
|
||||
usuario = nodo_info[:usuario] || tarea[:usuario] || 'root'
|
||||
|
||||
@log.info("🐧 Actualización Linux: #{nombre_nodo}")
|
||||
@log.info(" SSH: #{usuario}@#{ip}")
|
||||
|
||||
begin
|
||||
Net::SSH.start(ip, usuario, keys_only: true, timeout: 10) do |ssh|
|
||||
# Variables de entorno para evitar interactividad
|
||||
env_vars = "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical"
|
||||
apt_opts = "-o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
|
||||
# Paso 1: apt update
|
||||
@log.info(" [1/3] Ejecutando apt update...")
|
||||
output_update = ssh.exec!("sudo #{env_vars} apt-get update 2>&1")
|
||||
@log.info(" #{output_update.lines.last&.strip || 'OK'}")
|
||||
|
||||
# Paso 2: apt upgrade -y
|
||||
@log.info(" [2/3] Ejecutando apt upgrade -y...")
|
||||
output_upgrade = ssh.exec!("sudo #{env_vars} apt-get upgrade -y #{apt_opts} 2>&1")
|
||||
@log.info(" #{output_upgrade.lines.last&.strip || 'OK'}")
|
||||
|
||||
# Paso 3: apt autoremove -y (limpieza)
|
||||
@log.info(" [3/3] Ejecutando apt autoremove -y...")
|
||||
output_autoremove = ssh.exec!("sudo #{env_vars} apt-get autoremove -y 2>&1")
|
||||
@log.info(" #{output_autoremove.lines.last&.strip || 'OK'}")
|
||||
|
||||
@log.info("✔ Nodo #{nombre_nodo} actualizado exitosamente.")
|
||||
true
|
||||
end
|
||||
rescue => e
|
||||
@log.error("✗ Error actualizando #{nombre_nodo}: #{e.message}")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy: actualiza TODOS los nodos (compatibilidad)
|
||||
def ejecutar(tarea)
|
||||
@log.info("--- #{tarea[:texto]} ---")
|
||||
@log.info("Origen: #{tarea[:origen]}")
|
||||
|
||||
nodos = listar(tarea)
|
||||
if nodos.empty?
|
||||
@log.info("Sin nodos Linux encontrados.")
|
||||
return true
|
||||
end
|
||||
|
||||
@log.info("Encontrados #{nodos.length} nodo(s) Linux.")
|
||||
actualizados = 0
|
||||
fallidos = 0
|
||||
|
||||
nodos.each do |nodo|
|
||||
@log.info("\n" + "=" * 50)
|
||||
if ejecutar_uno(tarea, nodo[:vm])
|
||||
actualizados += 1
|
||||
else
|
||||
fallidos += 1
|
||||
end
|
||||
end
|
||||
|
||||
@log.info("\n" + "=" * 50)
|
||||
@log.info("✔ #{actualizados} nodo(s) actualizado(s), #{fallidos} fallido(s).")
|
||||
fallidos == 0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Verifica si el nodo es Linux (busca Debian, Ubuntu, Linux en el .md)
|
||||
def es_nodo_linux?(archivo_md)
|
||||
contenido = File.read(archivo_md)
|
||||
|
||||
# Excluir explícitamente Windows
|
||||
return false if contenido.match?(/Windows\s+(Server\s+)?\d+/i)
|
||||
return false if contenido.match?(/Windows\s+10/i)
|
||||
return false if contenido.match?(/Windows\s+11/i)
|
||||
|
||||
# Incluir Linux/Debian/Ubuntu
|
||||
return true if contenido.match?(/Debian/i)
|
||||
return true if contenido.match?(/Ubuntu/i)
|
||||
return true if contenido.match?(/Linux/i)
|
||||
|
||||
# Incluir si tiene IP LAN y es srvv-* o srv-* (convención de nombres)
|
||||
hostname = File.basename(archivo_md, '.md')
|
||||
return true if hostname.match?(/^srvv?-/)
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# Extrae información del nodo desde el archivo .md
|
||||
def extraer_info_nodo(archivo_md)
|
||||
contenido = File.read(archivo_md)
|
||||
hostname = File.basename(archivo_md, '.md')
|
||||
|
||||
# Extraer IP LAN
|
||||
ip = contenido.match(/\*\*IP(?:\s+LAN)?\*\*:\s*`?(\d+\.\d+\.\d+\.\d+)`?/i)&.[](1) ||
|
||||
contenido.match(/\|\s*IP(?:\s+LAN)?[^\|]*\|\s*`?(\d+\.\d+\.\d+\.\d+)`?\s*\|/i)&.[](1)
|
||||
|
||||
# Extraer usuario SSH (si está especificado)
|
||||
usuario = contenido.match(/\*\*Usuario\s*SSH\*\*:\s*`?(\w+)`?/i)&.[](1) ||
|
||||
contenido.match(/\*\*SSH\*\*:\s*`?ssh\s+(\w+)@/i)&.[](1) ||
|
||||
'root'
|
||||
|
||||
# Extraer estado (para filtrar nodos offline)
|
||||
estado = contenido.match(/\*\*Estado\*\*:\s*(🟢|🔴|⚫|🚧|🟡)/i)&.[](1)
|
||||
|
||||
{
|
||||
hostname: hostname,
|
||||
ip: ip,
|
||||
usuario: usuario,
|
||||
estado: estado || '🟢'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -17,5 +17,6 @@
|
||||
"dasu-sql4:Administrador": "eyJpdiI6ImQzc09sRmo2NEZBbHZmN2IiLCJ0YWciOiJxK3Z3RU9uZG1QMXNUV1hJbEFmV3d3PT0iLCJkYXRhIjoicXd5dDI4bHBkeFNhRk1yTzluczlxdz09In0=",
|
||||
"dasu-sql4:sa": "eyJpdiI6Ik43aWR5d2RacFVFR3pyNysiLCJ0YWciOiJxeVAyTmxtYXloNTdFTVlvMFphQ2hRPT0iLCJkYXRhIjoiT0lrR1FGMXRVZ0czYU5PK1ZFcG8wUT09In0=",
|
||||
"dasu-pcv:rustdesk": "eyJpdiI6ImRwRFRCYjNmc1htZUJMZ1ciLCJ0YWciOiJRYTdpcy9wRTdlTnpLemQ0Y1VqMTF3PT0iLCJkYXRhIjoiL3hEczU5OVRrRzAralFNPSJ9",
|
||||
"dasu-pcv:adminpcv": "eyJpdiI6IlVUK3JOamxCeFhGUUlCMlUiLCJ0YWciOiI0MC81bE10Y0FJM1BqMTQ1NUIxWjhBPT0iLCJkYXRhIjoiUzZuOUIxaTVISmJpclNrREZYSTkwM1k9In0="
|
||||
"dasu-pcv:adminpcv": "eyJpdiI6IlVUK3JOamxCeFhGUUlCMlUiLCJ0YWciOiI0MC81bE10Y0FJM1BqMTQ1NUIxWjhBPT0iLCJkYXRhIjoiUzZuOUIxaTVISmJpclNrREZYSTkwM1k9In0=",
|
||||
"dasu-pc:UTNLR": "eyJpdiI6IjhLejRXUjNaTGU1RkRmaUgiLCJ0YWciOiJMRi9DdUhKWXg4VjJNL3MvajNqK1V3PT0iLCJkYXRhIjoicmJmdDZjZjNScXhFcVNzUC9ZSXc3SzA9In0="
|
||||
}
|
||||
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# adn/tools/cli/dasuten_backup_download.rb — Download desde Google Drive
|
||||
# ==========================================================
|
||||
# Flujo: Google Drive (drive_bkps-dasu) → dasu-sql4
|
||||
# Uso: ruby adn/tools/cli/dasuten_backup_download.rb
|
||||
# ==========================================================
|
||||
|
||||
require 'shellwords'
|
||||
|
||||
REMOTE_DRIVE = "rmonla-GDrive:drive_bkps-dasu"
|
||||
LOCAL_BACKUP = "F:\\BACKUP\\sysdasuten_compressed_ADN.bak"
|
||||
|
||||
puts "[*] Iniciando descarga de backup desde Google Drive..."
|
||||
|
||||
begin
|
||||
# Paso 1: Listar backups disponibles
|
||||
puts "[1/4] Buscando backup más reciente en Drive..."
|
||||
cmd_list = "rclone lsf '#{REMOTE_DRIVE}' --files-only --order '-modified' 2>/dev/null | grep sysdasuten | head -1"
|
||||
backup_remoto = `#{cmd_list}`.strip
|
||||
|
||||
if backup_remoto.empty?
|
||||
puts "[!] No se encontraron backups en #{REMOTE_DRIVE}"
|
||||
exit 1
|
||||
end
|
||||
|
||||
puts " Backup encontrado: #{backup_remoto}"
|
||||
|
||||
# Paso 2: Descargar backup
|
||||
puts "[2/4] Descargando backup..."
|
||||
puts " Origen: #{REMOTE_DRIVE}/#{backup_remoto}"
|
||||
puts " Destino: #{LOCAL_BACKUP}"
|
||||
puts " (Esto puede tomar 2-5 minutos)"
|
||||
|
||||
cmd_download = "rclone copy '#{REMOTE_DRIVE}/#{backup_remoto}' '#{LOCAL_BACKUP}' --progress -v"
|
||||
start_download = Time.now
|
||||
system(cmd_download)
|
||||
|
||||
if $?.success?
|
||||
puts "[+] Descarga completada en #{(Time.now - start_download).round(2)} segundos."
|
||||
else
|
||||
puts "[!] Error en descarga (código: #{$?.exitstatus})"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Paso 3: Verificar archivo descargado
|
||||
puts "[3/4] Verificando archivo descargado..."
|
||||
cmd_verify = "powershell -Command \"(Get-Item '#{LOCAL_BACKUP}').Length\""
|
||||
tamaño = `#{cmd_verify}`.strip.to_i
|
||||
|
||||
if tamaño > 0
|
||||
puts "[+] Archivo verificado: #{(tamaño / 1024.0 / 1024.0).round(2)} MB"
|
||||
else
|
||||
puts "[!] Archivo descargado parece estar vacío"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Paso 4: Restaurar en SQL Server
|
||||
puts "[4/4] Preparando para restauración..."
|
||||
puts " Backup disponible en: #{LOCAL_BACKUP}"
|
||||
puts ""
|
||||
puts " Para restaurar, ejecutar:"
|
||||
puts " sqlcmd -S localhost -Q \"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'\""
|
||||
|
||||
puts "\n✅ Descarga completada exitosamente."
|
||||
|
||||
rescue => e
|
||||
puts "[!] Error fatal: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# adn/tools/cli/dasuten_backup_upload.rb — Upload a Google Drive
|
||||
# ==========================================================
|
||||
# Flujo: srv-ns8 (backup local) → Google Drive
|
||||
# Uso: ruby adn/tools/cli/dasuten_backup_upload.rb
|
||||
# ==========================================================
|
||||
|
||||
require 'time'
|
||||
|
||||
REMOTE_DRIVE = "rmonla-GDrive:drive_bkps-dasu"
|
||||
BACKUP_LOCAL = "/var/tmp/sysdasuten_compressed_ADN.bak"
|
||||
BACKUP_NAME = "sysdasuten_compressed_ADN_#{Time.now.strftime('%Y%m%d_%H%M%S')}.bak"
|
||||
|
||||
puts "[*] Iniciando upload de backup a Google Drive..."
|
||||
|
||||
# Verificar archivo local
|
||||
unless File.exist?(BACKUP_LOCAL)
|
||||
puts "[!] Backup no encontrado en #{BACKUP_LOCAL}"
|
||||
puts " Primero generar backup en srvv-fenix y descargar a srv-ns8"
|
||||
exit 1
|
||||
end
|
||||
|
||||
tamaño_mb = (File.size(BACKUP_LOCAL) / 1024.0 / 1024.0).round(2)
|
||||
puts " Archivo local: #{BACKUP_LOCAL}"
|
||||
puts " Tamaño: #{tamaño_mb} MB"
|
||||
puts " Destino: #{REMOTE_DRIVE}/#{BACKUP_NAME}"
|
||||
|
||||
begin
|
||||
# Upload a Google Drive
|
||||
puts "\n[1/2] Subiendo a Google Drive..."
|
||||
puts " (Esto puede tomar 3-10 minutos dependiendo del ancho de banda)"
|
||||
|
||||
cmd_upload = "rclone copy '#{BACKUP_LOCAL}' '#{REMOTE_DRIVE}/#{BACKUP_NAME}' --progress -v"
|
||||
start_upload = Time.now
|
||||
system(cmd_upload)
|
||||
|
||||
if $?.success?
|
||||
duracion = (Time.now - start_upload).round(2)
|
||||
puts "[+] Upload completado en #{duracion} segundos."
|
||||
else
|
||||
puts "[!] Error en upload (código: #{$?.exitstatus})"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Verificar archivo en Drive
|
||||
puts "\n[2/2] Verificando archivo en Drive..."
|
||||
cmd_verify = "rclone size '#{REMOTE_DRIVE}/#{BACKUP_NAME}' 2>/dev/null"
|
||||
output = `#{cmd_verify}`.strip
|
||||
puts " Tamaño en Drive: #{output}"
|
||||
|
||||
puts "\n✅ Backup subido exitosamente a Google Drive:"
|
||||
puts " #{REMOTE_DRIVE}/#{BACKUP_NAME}"
|
||||
|
||||
rescue => e
|
||||
puts "[!] Error fatal: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# adn/tools/cli/dasuten_orquestador.rb — Orquestador Fase 8b
|
||||
# ==========================================================
|
||||
# Flujo completo: srvv-fenix → Google Drive → dasu-sql4
|
||||
# Uso: ruby adn/tools/cli/dasuten_orquestador.rb [upload|download|restore]
|
||||
# ==========================================================
|
||||
|
||||
require 'optparse'
|
||||
require_relative 'dron/dispatcher'
|
||||
|
||||
PROJECT_ROOT = File.expand_path('../..', __dir__)
|
||||
CLI_DIR = File.join(PROJECT_ROOT, 'adn', 'tools', 'cli')
|
||||
ADN_RUN = File.join(PROJECT_ROOT, 'adn', 'tools', 'run')
|
||||
|
||||
opciones = {
|
||||
paso: 'all',
|
||||
dry_run: false,
|
||||
nodo: nil
|
||||
}
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Uso: #{ADN_RUN} dasuten_orquestador [opciones]"
|
||||
opts.on("--paso PASO", %w[upload download restore all], "Ejecutar solo este paso (upload|download|restore|all)") { |p| opciones[:paso] = p }
|
||||
opts.on("--nodo NOMBRE", "Nodo para bitácora del dron") { |n| opciones[:nodo] = n }
|
||||
opts.on("--dry-run", "Simulación sin ejecutar") { opciones[:dry_run] = true }
|
||||
end.parse!(ARGV)
|
||||
|
||||
puts "\n#{'='*60}"
|
||||
puts "🛸 ORQUESTADOR DASUTEN — Fase 8b (Refresco BD vía Google Drive)"
|
||||
puts '='*60
|
||||
puts " Paso seleccionado: #{opciones[:paso]}"
|
||||
puts " Dry-run: #{opciones[:dry_run]}"
|
||||
puts " Nodo: #{opciones[:nodo] || 'auto-detect'}"
|
||||
puts '='*60
|
||||
|
||||
def lanzar_dron(cmd, nota, nodo: nil)
|
||||
puts "\n🛸 Lanzando dron: #{nota}"
|
||||
puts " Comando: #{cmd}"
|
||||
|
||||
args = ["lanzar", "--nota", nota]
|
||||
args << "--nodo" << nodo if nodo
|
||||
args << "--" << cmd
|
||||
|
||||
# Ejecutar dron directamente
|
||||
require_relative 'dron/ejecutor'
|
||||
resultado = Dron::Ejecutor.lanzar(
|
||||
cmd: cmd,
|
||||
nota: nota,
|
||||
nodo: nodo,
|
||||
timeout: 1800 # 30 minutos max
|
||||
)
|
||||
|
||||
resultado
|
||||
end
|
||||
|
||||
caso = opciones[:paso]
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# PASO 1: UPLOAD desde srvv-fenix a Google Drive
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
if caso == 'all' || caso == 'upload'
|
||||
puts "\n" + "─"*60
|
||||
puts "📤 PASO 1: Upload desde srvv-fenix → Google Drive"
|
||||
puts "─"*60
|
||||
|
||||
cmd_upload = "ruby #{CLI_DIR}/dasuten_backup_upload.rb"
|
||||
|
||||
if opciones[:dry_run]
|
||||
puts "[DRY-RUN] Ejecutaría: #{cmd_upload}"
|
||||
else
|
||||
resultado = lanzar_dron(cmd_upload, "📤 Upload backup DASUTEN a Drive", nodo: 'srvv-fenix')
|
||||
if resultado[:exit_code] != 0
|
||||
puts "\n❌ Upload fallido. Deteniendo orquestación."
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# PASO 2: DOWNLOAD desde Google Drive a dasu-sql4
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
if caso == 'all' || caso == 'download'
|
||||
puts "\n" + "─"*60
|
||||
puts "📥 PASO 2: Download desde Google Drive → dasu-sql4"
|
||||
puts "─"*60
|
||||
|
||||
cmd_download = "ruby #{CLI_DIR}/dasuten_backup_download.rb"
|
||||
|
||||
if opciones[:dry_run]
|
||||
puts "[DRY-RUN] Ejecutaría: #{cmd_download}"
|
||||
else
|
||||
resultado = lanzar_dron(cmd_download, "📥 Download backup DASUTEN desde Drive", nodo: 'dasu-sql4')
|
||||
if resultado[:exit_code] != 0
|
||||
puts "\n❌ Download fallido. Deteniendo orquestación."
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# PASO 3: RESTORE en dasu-sql4
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
if caso == 'all' || caso == 'restore'
|
||||
puts "\n" + "─"*60
|
||||
puts "💾 PASO 3: Restore + DBCC CHECKDB en dasu-sql4"
|
||||
puts "─"*60
|
||||
|
||||
cmd_restore = <<~SQL.gsub("\n", ' ').strip
|
||||
powershell -Command "
|
||||
$query = @'
|
||||
RESTORE DATABASE sysdasuten FROM DISK = 'F:\\BACKUP\\sysdasuten_compressed_ADN.bak'
|
||||
WITH REPLACE,
|
||||
MOVE 'sysdasuten' TO 'F:\\DATA\\sysdasuten.mdf',
|
||||
MOVE 'sysdasuten_log' TO 'F:\\LOG\\sysdasuten_log.ldf';
|
||||
DBCC CHECKDB('sysdasuten');
|
||||
'@
|
||||
sqlcmd -S localhost -Q $query
|
||||
"
|
||||
SQL
|
||||
|
||||
if opciones[:dry_run]
|
||||
puts "[DRY-RUN] Ejecutaría: #{cmd_restore}"
|
||||
else
|
||||
resultado = lanzar_dron(cmd_restore, "💾 Restore + CHECKDB DASUTEN", nodo: 'dasu-sql4')
|
||||
if resultado[:exit_code] != 0
|
||||
puts "\n❌ Restore fallido. Verificar logs."
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n" + "═"*60
|
||||
puts "✅ Orquestación completada exitosamente!"
|
||||
puts "═"*60
|
||||
@@ -36,6 +36,8 @@ module Dron
|
||||
SanadorComando.reintentar(args)
|
||||
when 'estado', 'status'
|
||||
VigilanteComando.estado(args)
|
||||
when 'ops', 'operaciones'
|
||||
OpsComando.ejecutar(args)
|
||||
when 'ayuda', 'help', '--help', '-h'
|
||||
mostrar_ayuda
|
||||
else
|
||||
@@ -59,6 +61,7 @@ module Dron
|
||||
lines << " #{Color::GREEN}flota#{Color::RESET} Dashboard compacto de la flota"
|
||||
lines << " #{Color::GREEN}salud#{Color::RESET} Health check activo (detecta zombies)"
|
||||
lines << " #{Color::GREEN}estado <ID>#{Color::RESET} Estado detallado de un dron"
|
||||
lines << " #{Color::GREEN}ops <acción>#{Color::RESET} Operaciones de infraestructura (update_os)"
|
||||
lines << " #{Color::GREEN}sanear#{Color::RESET} Sanear flota (reintentar + limpiar)"
|
||||
lines << " #{Color::GREEN}limpiar#{Color::RESET} Limpiar drones antiguos"
|
||||
lines << " #{Color::GREEN}reintentar#{Color::RESET} Re-intentar drones fallidos"
|
||||
@@ -74,6 +77,8 @@ module Dron
|
||||
lines << "\n#{Color::BOLD}Ejemplos:#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron lanzar --nota 'Backup SQL' -- vzdump 103#{Color::RESET}"
|
||||
lines << " #{Color::CYAN}Lanzar backup como dron#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron ops update_os --target linux#{Color::RESET}"
|
||||
lines << " #{Color::CYAN}Actualizar OS de todos los nodos Linux#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron flota#{Color::RESET}"
|
||||
lines << " #{Color::CYAN}Ver estado de la flota#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron salud#{Color::RESET}"
|
||||
@@ -279,4 +284,11 @@ module Dron
|
||||
opciones
|
||||
end
|
||||
end
|
||||
|
||||
class OpsComando
|
||||
def self.ejecutar(args)
|
||||
require_relative 'ops'
|
||||
Dron::Ops.ejecutar(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# Dron::Ops — Operaciones de Infraestructura via AtomicDrones
|
||||
# ----------------------------------------------------------
|
||||
# Responsabilidad: Orquestar tareas de mantenimiento de nodos
|
||||
# (update_os, etc.) usando 1 dron ejecutor por nodo.
|
||||
#
|
||||
# Reutiliza:
|
||||
# - NodosInfo (scanner de fichas .md)
|
||||
# - Dron::Ejecutor (ejecución atómica con auto-bitácora)
|
||||
#
|
||||
# Uso:
|
||||
# ./adn/tools/run dron ops update_os --target linux
|
||||
# ./adn/tools/run dron ops update_os --nodo srvv-dns
|
||||
# ./adn/tools/run dron ops update_os --target linux --dry-run
|
||||
#
|
||||
# Plan: A01.P011 — Drones de Operaciones de Infraestructura
|
||||
# ==========================================================
|
||||
|
||||
require 'optparse'
|
||||
require 'shellwords'
|
||||
require_relative 'base'
|
||||
require_relative 'ejecutor'
|
||||
require_relative '../nodos/info'
|
||||
require_relative '../../core/colores'
|
||||
require_relative '../../core/constants'
|
||||
|
||||
module Dron
|
||||
class Ops
|
||||
# Comandos SSH para actualización desatendida
|
||||
APT_ENV = 'DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical'
|
||||
APT_OPTS = "-o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
|
||||
APT_CMD = "sudo #{APT_ENV} apt-get update 2>&1 && " \
|
||||
"sudo #{APT_ENV} apt-get upgrade -y #{APT_OPTS} 2>&1 && " \
|
||||
"sudo #{APT_ENV} apt-get autoremove -y 2>&1"
|
||||
|
||||
class << self
|
||||
# ─── Punto de entrada CLI ──────────────────────────────────
|
||||
|
||||
def ejecutar(args)
|
||||
accion = args.shift
|
||||
|
||||
case accion
|
||||
when 'update_os'
|
||||
cmd_update_os(args)
|
||||
when 'ayuda', 'help', '--help', '-h', nil
|
||||
mostrar_ayuda
|
||||
else
|
||||
Base.logger.error("❌ Acción desconocida para ops: #{accion}")
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# ─── update_os ─────────────────────────────────────────────
|
||||
|
||||
def cmd_update_os(args)
|
||||
opciones = parsear_opciones(args)
|
||||
|
||||
# Obtener nodos candidatos
|
||||
nodos = escanear_nodos(opciones)
|
||||
|
||||
if nodos.empty?
|
||||
puts "#{Color::YELLOW}⚠ Sin nodos Linux encontrados para actualizar.#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
mostrar_resumen(nodos, opciones)
|
||||
|
||||
return if opciones[:dry_run]
|
||||
|
||||
# Orquestar: 1 dron ejecutor por nodo
|
||||
resultados = orquestar_nodos(nodos, opciones)
|
||||
|
||||
# Resumen final
|
||||
mostrar_resultados(resultados, nodos.length)
|
||||
|
||||
exit 1 if resultados[:failed] > 0 && opciones[:on_error] == 'abort'
|
||||
end
|
||||
|
||||
# ─── Scanner: reutiliza NodosInfo ──────────────────────────
|
||||
|
||||
def escanear_nodos(opciones)
|
||||
nodos_dir = ADN::NODOS_DIR
|
||||
|
||||
unless Dir.exist?(nodos_dir)
|
||||
Base.logger.error("Directorio de nodos no existe: #{nodos_dir}")
|
||||
return []
|
||||
end
|
||||
|
||||
candidatos = Dir.glob(File.join(nodos_dir, '*.md'))
|
||||
.reject { |f| File.basename(f).start_with?('_') }
|
||||
.map { |f| ADN::NodosInfo.extraer_metadata(File.basename(f, '.md')) }
|
||||
.compact
|
||||
|
||||
# Filtrar por OS
|
||||
if opciones[:target] == 'linux'
|
||||
candidatos.select! { |n| n[:so] == :linux }
|
||||
end
|
||||
|
||||
# Excluir nodos sin IP
|
||||
candidatos.reject! { |n| n[:ip].nil? || n[:ip].to_s.strip.empty? }
|
||||
|
||||
# Excluir nodos offline
|
||||
candidatos.reject! { |n| n[:estado]&.to_s&.include?('🔴') }
|
||||
|
||||
# Filtrar nodo específico
|
||||
if opciones[:nodo]
|
||||
candidatos.select! { |n| n[:nombre] == opciones[:nodo] }
|
||||
end
|
||||
|
||||
# Filtrar por ring
|
||||
if opciones[:ring]
|
||||
candidatos.select! { |n| n[:ring] == opciones[:ring] }
|
||||
end
|
||||
|
||||
candidatos
|
||||
end
|
||||
|
||||
# ─── Orquestación: 1 dron por nodo ─────────────────────────
|
||||
|
||||
def orquestar_nodos(nodos, opciones)
|
||||
resultados = { ok: 0, failed: 0, skipped: 0 }
|
||||
abortado = false
|
||||
|
||||
nodos.each_with_index do |nodo, i|
|
||||
break if abortado
|
||||
|
||||
puts " #{Color::BOLD}[#{i + 1}/#{nodos.length}]#{Color::RESET} " \
|
||||
"#{Color::CYAN}#{nodo[:nombre]}#{Color::RESET} (#{nodo[:ip]})"
|
||||
|
||||
# Construir comando SSH para este nodo
|
||||
usuario = nodo[:usuario] || 'root'
|
||||
puerto = nodo[:puerto_ssh] || 22
|
||||
ssh_cmd = construir_ssh_cmd(nodo, usuario, puerto)
|
||||
|
||||
# Lanzar dron ejecutor (auto-bitácora incluida)
|
||||
nota = "🐧 Actualizar #{nodo[:nombre]}"
|
||||
resultado = Dron::Ejecutor.lanzar(
|
||||
cmd: ssh_cmd,
|
||||
nota: nota,
|
||||
timeout: opciones[:timeout],
|
||||
nodo: nodo[:nombre]
|
||||
)
|
||||
|
||||
if resultado[:exit_code] == 0
|
||||
puts " #{Color::GREEN}✔ Completado#{Color::RESET} (#{resultado[:duration]}s)"
|
||||
resultados[:ok] += 1
|
||||
else
|
||||
resultados[:failed] += 1
|
||||
case opciones[:on_error]
|
||||
when 'abort'
|
||||
puts " #{Color::RED}✖ Falló — deteniendo orquestación#{Color::RESET}"
|
||||
abortado = true
|
||||
resultados[:skipped] = nodos.length - i - 1
|
||||
when 'continue'
|
||||
puts " #{Color::YELLOW}⚠ Falló — continuando con el siguiente#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
puts ""
|
||||
end
|
||||
|
||||
resultados
|
||||
end
|
||||
|
||||
# ─── Construcción de comando SSH ───────────────────────────
|
||||
|
||||
def construir_ssh_cmd(nodo, usuario, puerto)
|
||||
ssh_opts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10"
|
||||
|
||||
# Puerto no estándar
|
||||
ssh_opts += " -p #{puerto}" if puerto != 22
|
||||
|
||||
# Opciones especiales (legacy servers como XenServer)
|
||||
ssh_opts += " #{nodo[:ssh_opts]}" if nodo[:ssh_opts]
|
||||
|
||||
# Clave SSH específica
|
||||
if nodo[:clave_ssh]
|
||||
clave = nodo[:clave_ssh].gsub('~', ENV['HOME'] || '/root')
|
||||
ssh_opts += " -i #{clave}"
|
||||
end
|
||||
|
||||
remote_cmd = APT_CMD
|
||||
|
||||
# Determinar método de autenticación
|
||||
if nodo[:clave_boveda]
|
||||
# AUTH POR PASSWORD: Usar candados run + sshpass
|
||||
# candados inyecta $PASS en el entorno, sshpass lo consume
|
||||
adn_run = File.join(ADN::PROJECT_ROOT, 'adn/tools/run')
|
||||
"#{adn_run} candados run #{nodo[:clave_boveda]} " \
|
||||
"'sshpass -p $PASS ssh #{ssh_opts} #{usuario}@#{nodo[:ip]} \"#{remote_cmd}\"'"
|
||||
else
|
||||
# AUTH POR CLAVE SSH: Conexión directa
|
||||
"ssh #{ssh_opts} #{usuario}@#{nodo[:ip]} '#{remote_cmd}'"
|
||||
end
|
||||
end
|
||||
|
||||
# ─── Presentación ─────────────────────────────────────────
|
||||
|
||||
def mostrar_resumen(nodos, opciones)
|
||||
puts ""
|
||||
puts "#{Color::BOLD}#{Color::CYAN}🐧 Dron Ops — Actualización de OS Linux#{Color::RESET}"
|
||||
puts "═" * 60
|
||||
puts " Nodos detectados: #{nodos.length}"
|
||||
puts " Target: #{opciones[:target]}"
|
||||
puts " Ring: #{opciones[:ring] || 'todos'}" if opciones[:ring]
|
||||
puts " On-error: #{opciones[:on_error]}"
|
||||
puts " Timeout por nodo: #{opciones[:timeout]}s"
|
||||
if opciones[:dry_run]
|
||||
puts " Modo: #{Color::YELLOW}DRY-RUN (sin ejecutar)#{Color::RESET}"
|
||||
end
|
||||
puts ""
|
||||
|
||||
if opciones[:dry_run]
|
||||
nodos.each_with_index do |nodo, i|
|
||||
auth = nodo[:clave_boveda] ? "🔐 #{nodo[:clave_boveda]}" : "🔑 key"
|
||||
ring_tag = nodo[:ring] ? " [#{nodo[:ring]}]" : ""
|
||||
puts " #{Color::DIM}[#{i + 1}]#{Color::RESET} #{nodo[:nombre].ljust(18)} " \
|
||||
"#{Color::DIM}#{nodo[:ip].ljust(16)} #{(nodo[:usuario] || 'root').ljust(8)} #{auth}#{ring_tag}#{Color::RESET}"
|
||||
end
|
||||
puts ""
|
||||
puts "#{Color::YELLOW}[DRY-RUN] No se lanzaron drones.#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
def mostrar_resultados(resultados, total)
|
||||
puts "#{Color::BOLD}🐧 Resumen de Operaciones#{Color::RESET}"
|
||||
puts "═" * 40
|
||||
puts " ✅ Exitosos: #{resultados[:ok]}"
|
||||
puts " ❌ Fallidos: #{resultados[:failed]}" if resultados[:failed] > 0
|
||||
puts " ⏭️ Saltados: #{resultados[:skipped]}" if resultados[:skipped] > 0
|
||||
puts "#{Color::BOLD}🐧 Operación #{resultados[:failed] > 0 ? 'con errores' : 'finalizada'}.#{Color::RESET}"
|
||||
end
|
||||
|
||||
# ─── Parser de opciones ────────────────────────────────────
|
||||
|
||||
def parsear_opciones(args)
|
||||
opciones = {
|
||||
target: 'linux',
|
||||
nodo: nil,
|
||||
ring: nil,
|
||||
on_error: 'continue',
|
||||
timeout: 600,
|
||||
dry_run: false
|
||||
}
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Uso: ./adn/tools/run dron ops update_os [opciones]"
|
||||
opts.on("--target OS", "OS a actualizar (linux)") { |t| opciones[:target] = t }
|
||||
opts.on("--nodo NOMBRE", "Actualizar solo este nodo") { |n| opciones[:nodo] = n }
|
||||
opts.on("--ring RING", %w[test standard core], "Solo nodos de este ring (test|standard|core)") { |r| opciones[:ring] = r }
|
||||
opts.on("--on-error MODE", %w[abort continue], "Qué hacer si un dron falla (abort|continue)") { |m| opciones[:on_error] = m }
|
||||
opts.on("--timeout N", Integer, "Timeout por nodo en segundos (default: 600)") { |t| opciones[:timeout] = t }
|
||||
opts.on("--dry-run", "Solo listar nodos sin ejecutar") { opciones[:dry_run] = true }
|
||||
end.parse!(args)
|
||||
|
||||
opciones
|
||||
end
|
||||
|
||||
def mostrar_ayuda
|
||||
puts "#{Color::BOLD}🐧 Dron Ops — Operaciones de Infraestructura#{Color::RESET}"
|
||||
puts "#{Color::DIM}#{'=' * 50}#{Color::RESET}"
|
||||
puts ""
|
||||
puts "#{Color::CYAN}Operaciones de mantenimiento sobre nodos vía AtomicDrones#{Color::RESET}"
|
||||
puts ""
|
||||
puts "Uso: #{Color::YELLOW}./adn/tools/run dron ops <acción> [opciones]#{Color::RESET}"
|
||||
puts ""
|
||||
puts "#{Color::BOLD}Acciones:#{Color::RESET}"
|
||||
puts " #{Color::GREEN}update_os#{Color::RESET} Actualizar sistema operativo de nodos"
|
||||
puts ""
|
||||
puts "#{Color::BOLD}Opciones:#{Color::RESET}"
|
||||
puts " #{Color::GREEN}--target OS#{Color::RESET} OS a actualizar: linux (default: linux)"
|
||||
puts " #{Color::GREEN}--nodo NOMBRE#{Color::RESET} Actualizar solo un nodo específico"
|
||||
puts " #{Color::GREEN}--on-error MODE#{Color::RESET} abort|continue (default: continue)"
|
||||
puts " #{Color::GREEN}--timeout N#{Color::RESET} Timeout por nodo en segundos (default: 600)"
|
||||
puts " #{Color::GREEN}--dry-run#{Color::RESET} Solo listar sin ejecutar"
|
||||
puts ""
|
||||
puts "#{Color::BOLD}Ejemplos:#{Color::RESET}"
|
||||
puts " #{Color::DIM}$ ./adn/tools/run dron ops update_os --target linux --dry-run#{Color::RESET}"
|
||||
puts " #{Color::CYAN}Listar nodos Linux sin actualizar#{Color::RESET}"
|
||||
puts " #{Color::DIM}$ ./adn/tools/run dron ops update_os --nodo srvv-dns#{Color::RESET}"
|
||||
puts " #{Color::CYAN}Actualizar solo srvv-dns#{Color::RESET}"
|
||||
puts " #{Color::DIM}$ ./adn/tools/run dron ops update_os --on-error continue#{Color::RESET}"
|
||||
puts " #{Color::CYAN}Actualizar toda la flota Linux continuando ante fallos#{Color::RESET}"
|
||||
puts ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# adn/tools/cli/drones/dasuten_upload_srv-ns8-drive.rb
|
||||
# ==========================================================
|
||||
# Dron: Upload a Google Drive desde srv-ns8
|
||||
# Flujo: /var/tmp/sysdasuten_compressed_ADN.bak → Google Drive
|
||||
# Entrada: Backup ya descargado en /var/tmp/
|
||||
# Salida: URL de Google Drive
|
||||
# ==========================================================
|
||||
|
||||
require 'time'
|
||||
require 'json'
|
||||
require 'shellwords'
|
||||
|
||||
REMOTE_DRIVE = "rmonla-GDrive:drive_bkps-dasu"
|
||||
|
||||
# Leer ruta del backup desde el output de la transferencia
|
||||
TRANSFER_OUTPUT = '/tmp/dron_transfer_output.json'
|
||||
if File.exist?(TRANSFER_OUTPUT)
|
||||
output = JSON.parse(File.read(TRANSFER_OUTPUT))
|
||||
BACKUP_LOCAL = output['backup_local']
|
||||
BACKUP_NAME = File.basename(BACKUP_LOCAL)
|
||||
puts "[*] Dron: Upload a Google Drive (DIFERENCIAL)"
|
||||
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)"
|
||||
end
|
||||
|
||||
puts " Nodo: srv-ns8"
|
||||
puts " Origen: #{BACKUP_LOCAL}"
|
||||
puts " Destino: #{REMOTE_DRIVE}/#{BACKUP_NAME}"
|
||||
|
||||
# Verificar archivo local
|
||||
unless File.exist?(BACKUP_LOCAL)
|
||||
puts "[!] Backup no encontrado en #{BACKUP_LOCAL}"
|
||||
puts " Ejecutar primero: dasuten_transferir_srvv-fenix-srv-ns8.rb"
|
||||
exit 1
|
||||
end
|
||||
|
||||
tamaño_mb = (File.size(BACKUP_LOCAL) / 1024.0 / 1024.0).round(2)
|
||||
puts "[+] Archivo local: #{tamaño_mb} MB"
|
||||
|
||||
begin
|
||||
# Upload a Google Drive
|
||||
puts "\n[1/2] Subiendo a Google Drive..."
|
||||
start = Time.now
|
||||
|
||||
cmd_upload = "rclone copy '#{BACKUP_LOCAL}' '#{REMOTE_DRIVE}/#{BACKUP_NAME}' --progress -v 2>&1"
|
||||
system(cmd_upload)
|
||||
|
||||
if $?.success?
|
||||
duracion = (Time.now - start).round(2)
|
||||
puts "[+] Upload completado en #{duracion} segundos"
|
||||
else
|
||||
puts "[!] Error en upload (código: #{$?.exitstatus})"
|
||||
exit 1
|
||||
end
|
||||
|
||||
# Verificar en Drive
|
||||
puts "\n[2/2] Verificando archivo en Drive..."
|
||||
cmd_verify = "rclone size '#{REMOTE_DRIVE}/#{BACKUP_NAME}' 2>/dev/null"
|
||||
output = `#{cmd_verify}`.strip
|
||||
|
||||
puts " Tamaño en Drive: #{output}"
|
||||
|
||||
# Output para orquestador
|
||||
resultado = {
|
||||
paso: 'upload_complete',
|
||||
drive_ruta: "#{REMOTE_DRIVE}/#{BACKUP_NAME}",
|
||||
drive_url: "https://drive.google.com/drive/folders/1Tgrn2QYyWf0v0eyY1bSGCLlhiqhtIA2n",
|
||||
nodo: 'srv-ns8',
|
||||
duracion: duracion,
|
||||
timestamp: Time.now.iso8601,
|
||||
siguiente_paso: 'download_drive'
|
||||
}
|
||||
|
||||
puts "\n✅ Upload completado"
|
||||
puts " Output: #{resultado.to_json}"
|
||||
|
||||
File.write('/tmp/dron_upload_output.json', resultado.to_json)
|
||||
|
||||
rescue => e
|
||||
puts "[!] Error fatal: #{e.message}"
|
||||
exit 1
|
||||
end
|
||||
Executable
+207
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# adn/tools/cli/drones/orquestador_pipeline.rb
|
||||
# ==========================================================
|
||||
# Orquestador de Pipeline DASUTEN — Intercomunicación vía JSON
|
||||
# ==========================================================
|
||||
# Flujo: exportar → transferir SMB → upload → download → restaurar → verificar
|
||||
# Cada dron escribe su output en JSON y el orquestador lo lee
|
||||
# para decidir el siguiente paso.
|
||||
# ==========================================================
|
||||
|
||||
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',
|
||||
dry_run: false,
|
||||
timeout: 1800, # 30 min por dron
|
||||
nodo_bitacora: 'dasuten-pipeline'
|
||||
}
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Uso: #{ADN_RUN} dron lanzar --nota 'Pipeline DASUTEN' -- ruby adn/tools/cli/drones/orquestador_pipeline.rb [opciones]"
|
||||
opts.on("--paso PASO", %w[exportar transferir upload download restaurar 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 (6 drones atómicos)
|
||||
ESTADOS = {
|
||||
exportar: {
|
||||
script: 'dasuten_exportar_srvv-fenix.rb',
|
||||
nodo: 'srvv-fenix',
|
||||
nota: '📤 Exportar backup DASUTEN',
|
||||
output_file: '/tmp/dron_export_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
|
||||
},
|
||||
restaurar: {
|
||||
script: 'dasuten_restaurar_dasu-sql4.rb',
|
||||
nodo: 'dasu-sql4',
|
||||
nota: '💾 Restaurar backup',
|
||||
output_file: '/tmp/dron_restore_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 DASUTEN (Fase 8b — 6 drones atómicos)"
|
||||
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
|
||||
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
# Función para lanzar un dron y esperar su resultado
|
||||
# ──────────────────────────────────────────────────────────────
|
||||
def lanzar_dron(script, nodo, nota, timeout)
|
||||
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 opciones[: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
|
||||
output_file = ESTADOS[nodo.to_sym][:output_file]
|
||||
if 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
|
||||
duracioniones = []
|
||||
|
||||
begin
|
||||
loop do
|
||||
break unless ESTADOS[paso_actual]
|
||||
|
||||
config = ESTADOS[paso_actual]
|
||||
|
||||
# Inyectar contexto del paso anterior si es necesario
|
||||
case paso_actual
|
||||
when :upload
|
||||
# El dron de upload ya sabe dónde está el archivo (hardcodeado en /var/tmp/)
|
||||
when :download
|
||||
# El dron de download usa file_id o test_file
|
||||
when :restaurar
|
||||
# El dron de restaurar usa la ruta hardcodeada F:\BACKUP\
|
||||
end
|
||||
|
||||
# Lanzar dron
|
||||
exito, resultado = lanzar_dron(
|
||||
config[:script],
|
||||
config[:nodo],
|
||||
config[:nota],
|
||||
opciones[:timeout]
|
||||
)
|
||||
|
||||
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"
|
||||
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 "="*70
|
||||
|
||||
rescue => e
|
||||
puts "\n❌ Error en orquestador: #{e.message}"
|
||||
puts e.backtrace.first(5).join("\n")
|
||||
exit 1
|
||||
end
|
||||
@@ -0,0 +1,206 @@
|
||||
#!/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
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
# adn/tools/cli/nodos/info.rb — Utilidades para extracción de metadatos de fichas de nodos
|
||||
# ======================================================================================
|
||||
#
|
||||
# Fase 2.5: Soporte dual — Tabla canónica (META:BEGIN/META:END) + fallback regex
|
||||
#
|
||||
# Prioridad de lectura:
|
||||
# 1. Tabla canónica entre <!-- META:BEGIN --> y <!-- META:END -->
|
||||
# 2. Regex heurísticas (legacy, para fichas no migradas)
|
||||
|
||||
require_relative '../../core/constants'
|
||||
|
||||
@@ -12,6 +18,30 @@ module ADN
|
||||
return nil unless File.exist?(ruta)
|
||||
|
||||
contenido = File.read(ruta, encoding: 'UTF-8')
|
||||
|
||||
# ─── Intentar tabla canónica primero ──────────────────────
|
||||
meta = extraer_meta_canonica(contenido)
|
||||
if meta
|
||||
return {
|
||||
nombre: nodo_nombre,
|
||||
ip: meta['IP'],
|
||||
ip_lan: detectar_ip_lan(contenido),
|
||||
host: meta['Host'],
|
||||
so: detectar_so_desde_valor(meta['OS']),
|
||||
vmid: meta['VMID'],
|
||||
puerto_ssh: extraer_puerto_de_ssh(meta['SSH']),
|
||||
usuario: extraer_usuario_de_ssh(meta['SSH']),
|
||||
clave_boveda: meta['Bóveda'],
|
||||
auth_type: meta['Auth']&.to_sym,
|
||||
clave_ssh: meta['Clave SSH'],
|
||||
ssh_opts: meta['SSH Opts'],
|
||||
ring: meta['Ring'],
|
||||
estado: meta['Estado'],
|
||||
_fuente: :canonica
|
||||
}
|
||||
end
|
||||
|
||||
# ─── Fallback: Regex heurísticas (legacy) ─────────────────
|
||||
{
|
||||
nombre: nodo_nombre,
|
||||
ip: detectar_ip(contenido),
|
||||
@@ -24,7 +54,10 @@ module ADN
|
||||
clave_boveda: detectar_clave_boveda(contenido),
|
||||
auth_type: detectar_auth_type(contenido),
|
||||
clave_ssh: detectar_clave_ssh(contenido),
|
||||
ssh_opts: detectar_ssh_opts(contenido)
|
||||
ssh_opts: detectar_ssh_opts(contenido),
|
||||
ring: nil,
|
||||
estado: nil,
|
||||
_fuente: :regex
|
||||
}
|
||||
end
|
||||
|
||||
@@ -35,6 +68,53 @@ module ADN
|
||||
detectar_ip_lan(contenido)
|
||||
end
|
||||
|
||||
# ─── Parser de tabla canónica ──────────────────────────────
|
||||
|
||||
def self.extraer_meta_canonica(contenido)
|
||||
match = contenido.match(/<!-- META:BEGIN -->(.*?)<!-- META:END -->/m)
|
||||
return nil unless match
|
||||
|
||||
meta = {}
|
||||
match[1].each_line do |linea|
|
||||
# | **Campo** | `valor` | o | **Campo** | valor |
|
||||
if linea =~ /\|\s*\*\*(.+?)\*\*\s*\|\s*`?([^`|]+?)`?\s*\|/
|
||||
campo = $1.strip
|
||||
valor = $2.strip
|
||||
meta[campo] = valor unless valor.empty?
|
||||
end
|
||||
end
|
||||
|
||||
meta.empty? ? nil : meta
|
||||
end
|
||||
|
||||
def self.extraer_usuario_de_ssh(ssh_str)
|
||||
return nil unless ssh_str
|
||||
# Formato: user@ip:port
|
||||
if ssh_str =~ /^([a-zA-Z0-9_\-\.]+)@/
|
||||
return $1
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def self.extraer_puerto_de_ssh(ssh_str)
|
||||
return 22 unless ssh_str
|
||||
# Formato: user@ip:port
|
||||
if ssh_str =~ /:(\d+)$/
|
||||
return $1.to_i
|
||||
end
|
||||
22
|
||||
end
|
||||
|
||||
def self.detectar_so_desde_valor(os_str)
|
||||
return :linux unless os_str
|
||||
return :linux if os_str =~ /linux|ubuntu|debian|centos|rocky|proxmox|xen/i
|
||||
return :windows if os_str =~ /windows|server|win/i
|
||||
return :firmware if os_str =~ /firmware|hikvision|dvr|mikrotik|routeros/i
|
||||
:linux
|
||||
end
|
||||
|
||||
# ─── Regex heurísticas (legacy — para fichas sin META) ─────
|
||||
|
||||
def self.detectar_usuario(contenido)
|
||||
# **SSH**: user@ip:port o DOMAIN\user@ip:port
|
||||
if contenido =~ /\*\*SSH\*\*:\s*`?([^`\s(]+)/i
|
||||
@@ -182,3 +262,4 @@ module ADN
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+16
-5
@@ -21,7 +21,7 @@ require_relative 'nodos/info'
|
||||
|
||||
module ADN
|
||||
class SubcomandoSSH
|
||||
CANDADOS_PATH = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
|
||||
CANDADOS_PATH = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'candados', 'candados.rb')
|
||||
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@@ -261,17 +261,28 @@ module ADN
|
||||
|
||||
if proxy_config
|
||||
@logger.info("Via ProxyJump #{config[:proxy]} (#{proxy_config[:ip]})")
|
||||
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
|
||||
|
||||
# ProxyCommand con sshpass: el proxy usa passphrase RSA,
|
||||
# el destino usa password. Todo se ejecuta localmente via bash.
|
||||
# Determinar comando ProxyCommand según auth_type del proxy
|
||||
proxy_cmd = case proxy_config[:auth_type]
|
||||
when :passphrase
|
||||
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
|
||||
"sshpass -P passphrase -p '#{proxy_passphrase}' ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}"
|
||||
when :key, :rsa
|
||||
# Proxy usa clave SSH sin passphrase
|
||||
"ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}"
|
||||
else
|
||||
# Default: ssh sin passphrase
|
||||
"ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}"
|
||||
end
|
||||
|
||||
# ProxyCommand con sshpass: el proxy usa clave/passphrase, el destino usa password
|
||||
script = <<~BASH
|
||||
export SSHPASS=#{password.shellescape}
|
||||
sshpass -e ssh \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o ConnectTimeout=30 \
|
||||
-o ServerAliveInterval=5 \
|
||||
-o ProxyCommand="sshpass -P passphrase -p '#{proxy_passphrase}' ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}" \
|
||||
-o ProxyCommand="#{proxy_cmd}" \
|
||||
-p #{config[:puerto_ssh]} \
|
||||
'#{ssh_target}' \
|
||||
"#{comando_remoto.gsub('"', '\\"')}"
|
||||
|
||||
Reference in New Issue
Block a user