[P2604] Fase 15: Optimización IA e integración de herramienta candados. Sincronización de bitácoras y actualización de planes de proyecto.
This commit is contained in:
+150
-17
@@ -47,10 +47,10 @@ module ADN
|
||||
listar_nodos
|
||||
exit 0
|
||||
end
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
puts opts
|
||||
exit 0
|
||||
end
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
mostrar_ayuda
|
||||
exit 0
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
@@ -82,6 +82,27 @@ module ADN
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Ejecución Remota Segura SSH",
|
||||
descripcion: "Ejecuta comandos remotos de forma segura via SSH + Candados",
|
||||
uso: "./adn/tools/run ssh <nodo> [comando] [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['-i', '--interactivo'], desc: "Abrir sesión SSH interactiva" },
|
||||
{ names: ['-v', '--verbose'], desc: "Mostrar detalles de conexión" },
|
||||
{ names: ['--listar'], desc: "Listar nodos configurados" },
|
||||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run ssh srv-dasu \"hostname; uptime\"", desc: "Ejecutar comando remoto en srv-dasu" },
|
||||
{ cmd: "./adn/tools/run ssh dc-dasuten \"whoami\"", desc: "Ejecutar comando remoto en dc-dasuten" },
|
||||
{ cmd: "./adn/tools/run ssh srv-ns8 --interactivo", desc: "Abrir sesión SSH interactiva en srv-ns8" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
|
||||
def conectar(nodo_nombre, comando_remoto)
|
||||
config = obtener_config(nodo_nombre)
|
||||
unless config
|
||||
@@ -90,7 +111,16 @@ module ADN
|
||||
exit 1
|
||||
end
|
||||
|
||||
@logger.info("Conectando a #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]})")
|
||||
# Mostrar contexto enriquecido
|
||||
contexto = []
|
||||
if config[:ip_lan]
|
||||
contexto << "LAN:#{config[:ip_lan]}"
|
||||
end
|
||||
if config[:ip] && config[:ip] != config[:ip_lan]
|
||||
contexto << "VPN:#{config[:ip]}"
|
||||
end
|
||||
contexto_str = contexto.any? ? " [#{contexto.join(' ')}]" : ""
|
||||
@logger.info("Conectando a #{nodo_nombre}#{contexto_str} (#{config[:ip]}:#{config[:puerto_ssh]})")
|
||||
|
||||
# Autorizar candados
|
||||
autorizar_candados
|
||||
@@ -100,6 +130,8 @@ module ADN
|
||||
ejecutar_con_passphrase(nodo_nombre, config, comando_remoto)
|
||||
when :password
|
||||
ejecutar_con_password(nodo_nombre, config, comando_remoto)
|
||||
when :rsa
|
||||
ejecutar_con_rsa(nodo_nombre, config, comando_remoto)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -107,6 +139,9 @@ module ADN
|
||||
meta = ADN::NodosInfo.extraer_metadata(nodo_nombre)
|
||||
return nil unless meta && meta[:ip] && meta[:usuario] && meta[:clave_boveda]
|
||||
|
||||
# Extraer IP de LAN si está disponible en la ficha
|
||||
meta[:ip_lan] = ADN::NodosInfo.extraer_ip_lan(nodo_nombre)
|
||||
|
||||
# Inferir proxy: si el nodo tiene host (es VM), usar el host como proxy
|
||||
if meta[:host] && meta[:host] != nodo_nombre
|
||||
proxy_meta = ADN::NodosInfo.extraer_metadata(meta[:host])
|
||||
@@ -133,34 +168,74 @@ module ADN
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
def imprimir_salida(nodo_nombre, texto, es_error=false)
|
||||
return if texto.nil? || texto.empty?
|
||||
prefix = es_error ? "#{Color::RED}[#{nodo_nombre}-ERR]#{Color::RESET} " : "#{Color::CYAN}[#{nodo_nombre}]#{Color::RESET} "
|
||||
texto.lines.each do |linea|
|
||||
if es_error
|
||||
$stderr.puts "#{prefix}#{linea}"
|
||||
else
|
||||
puts "#{prefix}#{linea}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_con_passphrase(nodo_nombre, config, comando_remoto)
|
||||
passphrase = obtener_secreto(config[:clave_boveda])
|
||||
proxy_config = config[:proxy] ? ADN::NodosInfo.extraer_metadata(config[:proxy]) : nil
|
||||
|
||||
ssh_opts = "-o ConnectTimeout=15 -o StrictHostKeyChecking=no"
|
||||
ssh_target = "#{config[:usuario]}@#{config[:ip]}"
|
||||
|
||||
if @opciones[:interactivo]
|
||||
if proxy_config
|
||||
@logger.info("Via ProxyJump #{config[:proxy]} (#{proxy_config[:ip]})")
|
||||
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
|
||||
|
||||
script = <<~BASH
|
||||
sshpass -P passphrase -p '#{passphrase}' 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]}" \
|
||||
-p #{config[:puerto_ssh]} \
|
||||
'#{ssh_target}' \
|
||||
"#{comando_remoto.gsub('"', '\\"')}"
|
||||
BASH
|
||||
|
||||
tmpscript = "/tmp/.adn_ssh_pass_#{Process.pid}.sh"
|
||||
File.write(tmpscript, script)
|
||||
File.chmod(0700, tmpscript)
|
||||
|
||||
stdout, stderr, status = Open3.capture3(tmpscript)
|
||||
File.delete(tmpscript) if File.exist?(tmpscript)
|
||||
|
||||
if status.success?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
|
||||
elsif @opciones[:interactivo]
|
||||
# Sesión interactiva: usar sshpass con passphrase prompt
|
||||
@logger.info("Abriendo sesión interactiva a #{nodo_nombre}...")
|
||||
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target}"
|
||||
exec(cmd)
|
||||
else
|
||||
# Comando remoto
|
||||
if @opciones[:verbose]
|
||||
puts "#{Color::DIM} [SSH] #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]}) > #{comando_remoto[0..60]}...#{Color::RESET}"
|
||||
end
|
||||
|
||||
# Comando remoto directo
|
||||
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target} '#{comando_remoto.gsub("'", "'\\''")}'"
|
||||
|
||||
stdout, stderr, status = Open3.capture3(cmd)
|
||||
|
||||
if status.success?
|
||||
puts stdout unless stdout.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
puts stdout unless stdout.empty?
|
||||
$stderr.puts stderr unless stderr.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
@@ -218,14 +293,72 @@ module ADN
|
||||
success = defined?(status_success) ? status_success : status.success?
|
||||
|
||||
if success
|
||||
puts stdout unless stdout.nil? || stdout.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
puts stdout unless stdout.nil? || stdout.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota")
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_con_rsa(nodo_nombre, config, comando_remoto)
|
||||
proxy_config = config[:proxy] ? ADN::NodosInfo.extraer_metadata(config[:proxy]) : nil
|
||||
|
||||
ssh_opts = "-o StrictHostKeyChecking=no -o ConnectTimeout=15"
|
||||
ssh_target = "#{config[:usuario]}@#{config[:ip]}"
|
||||
|
||||
if proxy_config
|
||||
@logger.info("Via ProxyJump #{config[:proxy]} (#{proxy_config[:ip]})")
|
||||
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
|
||||
|
||||
script = <<~BASH
|
||||
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]}" \
|
||||
-p #{config[:puerto_ssh]} \
|
||||
'#{ssh_target}' \
|
||||
"#{comando_remoto.gsub('"', '\\"')}"
|
||||
BASH
|
||||
|
||||
tmpscript = "/tmp/.adn_ssh_rsa_#{Process.pid}.sh"
|
||||
File.write(tmpscript, script)
|
||||
File.chmod(0700, tmpscript)
|
||||
|
||||
stdout, stderr, status = Open3.capture3(tmpscript)
|
||||
File.delete(tmpscript) if File.exist?(tmpscript)
|
||||
|
||||
if status.success?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
|
||||
elsif @opciones[:interactivo]
|
||||
@logger.info("Abriendo sesión interactiva a #{nodo_nombre}...")
|
||||
cmd = "ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target}"
|
||||
exec(cmd)
|
||||
else
|
||||
cmd_parts = ["ssh"] + ssh_opts.split(' ') + ["-p", config[:puerto_ssh].to_s, ssh_target, comando_remoto]
|
||||
stdout, stderr, status = Open3.capture3(*cmd_parts)
|
||||
|
||||
if status.success?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def listar_nodos
|
||||
puts "#{Color::CYAN}📡 Nodos SSH disponibles (leídos de fichas nodos/*.md):#{Color::RESET}"
|
||||
|
||||
Reference in New Issue
Block a user