[P2604-S12] Refactorizar ssh.rb: datos desde fichas via NodosInfo

- Eliminar hash NODOS hardcodeado de ssh.rb
- Leer IP, puerto, usuario, auth, bóveda de fichas nodos/*.md
- Inferir proxy automáticamente (host != nodo → proxy)
- Mejorar NodosInfo: detectar_clave_boveda, detectar_auth_type
- Actualizar fichas: srv-dasu (IP Tailscale), pcv-dasu0, sql-dasuten
- Formato SSH estándar: usuario@ip:port (Auth, Bóveda: clave)
- 15 tests pasan sin regresiones
- Evento 1077 (16:58→17:03)
This commit is contained in:
Ricardo Monla
2026-03-15 17:03:32 -03:00
parent cfb8a6ccb6
commit f902b4aa89
7 changed files with 91 additions and 74 deletions
+38 -64
View File
@@ -17,54 +17,12 @@ require 'shellwords'
require_relative '../core/colores'
require_relative '../core/constants'
require_relative '../core/error_handler'
require_relative '../core/nodos_info'
module ADN
class SubcomandoSSH
CANDADOS_PATH = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
# Mapa de nodos → configuración SSH
# Prioridad: 1) DB/fichas, 2) este mapa, 3) defaults
NODOS = {
'srv-dasu' => {
ip: '100.112.46.104',
usuario: 'rmonla',
puerto: 22,
auth: :passphrase, # Usa passphrase de clave RSA
clave_boveda: 'rsa' # Clave en candados para passphrase
},
'dc-dasuten' => {
ip: '10.0.100.10',
usuario: 'DASUTEN\admindasu',
puerto: 7022,
auth: :password,
clave_boveda: 'admindasu',
proxy: 'srv-dasu' # Requiere salto via srv-dasu
},
'sql-dasuten' => {
ip: '10.0.100.11',
usuario: 'DASUTEN\admindasu',
puerto: 7022,
auth: :password,
clave_boveda: 'admindasu',
proxy: 'srv-dasu'
},
'pcv-dasu0' => {
ip: '10.0.100.12',
usuario: 'DASUTEN\admindasu',
puerto: 7022,
auth: :password,
clave_boveda: 'admindasu',
proxy: 'srv-dasu'
},
'pc-dasu0' => {
ip: '100.100.145.51',
usuario: 'admindasu',
puerto: 7022,
auth: :password,
clave_boveda: 'admindasu'
}
}.freeze
def initialize(args, logger)
@args = args
@logger = logger
@@ -127,17 +85,17 @@ module ADN
def conectar(nodo_nombre, comando_remoto)
config = obtener_config(nodo_nombre)
unless config
ADN::ErrorHandler.error("Nodo '#{nodo_nombre}' no encontrado")
puts " Nodos disponibles: #{NODOS.keys.join(', ')}"
ADN::ErrorHandler.error("Nodo '#{nodo_nombre}' no encontrado o sin SSH")
puts " Usa: ./adn/tools/run ssh --listar"
exit 1
end
@logger.info("Conectando a #{nodo_nombre} (#{config[:ip]}:#{config[:puerto]})")
@logger.info("Conectando a #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]})")
# Autorizar candados
autorizar_candados
case config[:auth]
case config[:auth_type]
when :passphrase
ejecutar_con_passphrase(nodo_nombre, config, comando_remoto)
when :password
@@ -146,7 +104,16 @@ module ADN
end
def obtener_config(nodo_nombre)
NODOS[nodo_nombre]
meta = ADN::NodosInfo.extraer_metadata(nodo_nombre)
return nil unless meta && meta[:ip] && meta[:usuario] && meta[:clave_boveda]
# 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])
meta[:proxy] = meta[:host] if proxy_meta
end
meta
end
def autorizar_candados
@@ -176,15 +143,15 @@ module ADN
if @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_target}"
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]}) > #{comando_remoto[0..60]}...#{Color::RESET}"
puts "#{Color::DIM} [SSH] #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]}) > #{comando_remoto[0..60]}...#{Color::RESET}"
end
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto]} #{ssh_target} '#{comando_remoto.gsub("'", "'\\''")}'"
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target} '#{comando_remoto.gsub("'", "'\\''")}'"
stdout, stderr, status = Open3.capture3(cmd)
@@ -202,7 +169,7 @@ module ADN
def ejecutar_con_password(nodo_nombre, config, comando_remoto)
password = obtener_secreto(config[:clave_boveda])
proxy_config = config[:proxy] ? NODOS[config[:proxy]] : nil
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]}"
@@ -219,8 +186,8 @@ module ADN
-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]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}" \
-p #{config[:puerto]} \
-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
@@ -241,7 +208,7 @@ module ADN
ENV['SSHPASS'] = password
cmd_parts = ["sshpass", "-e", "ssh"] +
ssh_opts.split(' ') +
["-p", config[:puerto].to_s,
["-p", config[:puerto_ssh].to_s,
ssh_target,
comando_remoto]
stdout, stderr, status = Open3.capture3(*cmd_parts)
@@ -261,16 +228,23 @@ module ADN
end
def listar_nodos
puts "#{Color::CYAN}📡 Nodos SSH configurados:#{Color::RESET}"
puts "#{Color::DIM}#{'─' * 70}#{Color::RESET}"
printf " %-15s %-20s %-8s %-12s %s\n", "NODO", "IP", "PUERTO", "AUTH", "PROXY"
puts "#{Color::DIM}#{'─' * 70}#{Color::RESET}"
NODOS.each do |nombre, cfg|
proxy = cfg[:proxy] || '-'
printf " #{Color::GREEN}%-15s#{Color::RESET} %-20s %-8s %-12s %s\n",
nombre, cfg[:ip], cfg[:puerto], cfg[:auth], proxy
puts "#{Color::CYAN}📡 Nodos SSH disponibles (leídos de fichas nodos/*.md):#{Color::RESET}"
puts "#{Color::DIM}#{'─' * 75}#{Color::RESET}"
printf " %-15s %-20s %-8s %-12s %-10s %s\n", "NODO", "IP", "PUERTO", "AUTH", "BÓVEDA", "PROXY"
puts "#{Color::DIM}#{'─' * 75}#{Color::RESET}"
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).sort.each do |archivo|
nombre = File.basename(archivo, '.md')
next if nombre.start_with?('_')
config = obtener_config(nombre)
next unless config # Sin config SSH válida
proxy = config[:proxy] || '-'
printf " #{Color::GREEN}%-15s#{Color::RESET} %-20s %-8s %-12s %-10s %s\n",
nombre, config[:ip], config[:puerto_ssh], config[:auth_type], config[:clave_boveda], proxy
end
puts "#{Color::DIM}#{'─' * 70}#{Color::RESET}"
puts "#{Color::DIM}#{'─' * 75}#{Color::RESET}"
end
end
end