[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:
Ricardo Monla
2026-03-19 09:52:29 -03:00
parent 183dbf6fb6
commit 5aa37e7221
138 changed files with 8777 additions and 464 deletions
+150 -17
View File
@@ -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}"