From f902b4aa891955d55e0f396d93959825703b4ed2 Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Sun, 15 Mar 2026 17:03:32 -0300 Subject: [PATCH] [P2604-S12] Refactorizar ssh.rb: datos desde fichas via NodosInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- adn/tools/cli/ssh.rb | 102 +++++++----------- adn/tools/core/nodos_info.rb | 50 ++++++++- adn/tools/seguridad/.session | 2 +- .../p2604_mejoras_ADN/P2604_mejoras_ADN.md | 3 +- nodos/pcv-dasu0.md | 1 + nodos/sql-dasuten.md | 4 +- nodos/srv-dasu.md | 3 +- 7 files changed, 91 insertions(+), 74 deletions(-) diff --git a/adn/tools/cli/ssh.rb b/adn/tools/cli/ssh.rb index 47426622..d9d7e731 100644 --- a/adn/tools/cli/ssh.rb +++ b/adn/tools/cli/ssh.rb @@ -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 diff --git a/adn/tools/core/nodos_info.rb b/adn/tools/core/nodos_info.rb index 3aca5c3b..fc55dfa0 100644 --- a/adn/tools/core/nodos_info.rb +++ b/adn/tools/core/nodos_info.rb @@ -19,21 +19,31 @@ module ADN so: detectar_so(contenido), vmid: detectar_vmid(contenido), puerto_ssh: detectar_puerto_ssh(contenido) || 22, - usuario: detectar_usuario(contenido) + usuario: detectar_usuario(contenido), + clave_boveda: detectar_clave_boveda(contenido), + auth_type: detectar_auth_type(contenido) } end def self.detectar_usuario(contenido) - # | SSH | user@domain@ip:7022 | o user@ip:7022 o user@domain:7022 - if contenido =~ /(?:SSH|Acceso)\*\*?:?\s*`?([a-zA-Z0-9_\-\.\@]+)/i + # **SSH**: user@ip:port o DOMAIN\user@ip:port + if contenido =~ /\*\*SSH\*\*:\s*`?([^`\s(]+)/i + full = $1 + # Extraer usuario de formato user@ip:port o DOMAIN\user@ip:port + if full.include?('@') + return full.split('@')[0...-1].join('@') if full.split('@').last =~ /^[\d\.]+/ + return full.split(':')[0] + end + return full + end + # Fallback: | SSH | user@domain@ip:7022 | + if contenido =~ /(?:SSH|Acceso)\*\*?:?\s*`?([a-zA-Z0-9_\-\.\\@]+)/i full = $1 if full.include?('@') parts = full.split('@') - # Si la última parte es una IP, el usuario es el resto if parts.last =~ /^\d{1,3}(\.\d{1,3}){3}/ return parts[0...-1].join('@') else - # Probablemente sea user@domain or user@domain:port return full.split(':')[0] end end @@ -43,6 +53,10 @@ module ADN end def self.detectar_ip(contenido) + # Prioridad 1: IP de línea SSH (es la IP operativa para conexión) + if contenido =~ /\*\*SSH\*\*:\s*`?[^@]+@([\d\.]+)/i + return $1 + end # | IP / Ubicación | 10.0.10.10 | # | IP | 10.0.100.11/24 | if contenido =~ /\|\s*IP\s*(?:\/\s*Ubicación)?\s*\|\s*`?([\d\.]+)/i @@ -80,6 +94,10 @@ module ADN end def self.detectar_puerto_ssh(contenido) + # **SSH**: user@ip:7022 + if contenido =~ /\*\*SSH\*\*:\s*`?[^`\s]+:(\d+)/i + return $1.to_i + end # | SSH | user@ip:7022 | if contenido =~ /(?:SSH|Puerto|Puerto SSH)\*\*?:?\s*.*:(\d+)/i return $1.to_i @@ -90,5 +108,27 @@ module ADN end nil end + + # Detecta la clave de la bóveda de candados desde la ficha + # Formato esperado: (Bóveda: `clave`) o (Bóveda: clave) + def self.detectar_clave_boveda(contenido) + if contenido =~ /\*\*SSH\*\*:.*Bóveda:\s*`?([a-zA-Z0-9_\-:]+)`?\)?/i + return $1 + end + # Fallback: buscar en cualquier línea de credenciales + if contenido =~ /[Bb]óveda:\s*`?([a-zA-Z0-9_\-:]+)`?\)?/ + return $1 + end + nil + end + + # Detecta tipo de autenticación SSH + # Formato: (Passphrase RSA, ...) o (Password, ...) + def self.detectar_auth_type(contenido) + if contenido =~ /\*\*SSH\*\*:.*\((Passphrase|Password)/i + return $1.downcase == 'passphrase' ? :passphrase : :password + end + :password # Default para VMs Windows + end end end diff --git a/adn/tools/seguridad/.session b/adn/tools/seguridad/.session index 7ffcef86..404de029 100644 --- a/adn/tools/seguridad/.session +++ b/adn/tools/seguridad/.session @@ -1 +1 @@ -1773604236 \ No newline at end of file +1773604966 \ No newline at end of file diff --git a/docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md b/docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md index bf2c9be9..eaa5cd7e 100644 --- a/docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md +++ b/docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md @@ -154,6 +154,7 @@ Mejoras atómicas, incrementales y reutilizables. Una herramienta por vez. | S9 | Limpiar progreso duplicado en este plan | Doc | ✅ | | S10 | Migrar `SubcomandoSalud` al namespace `ADN::` | Refactor | ⏳ | | S11 | Crear CLI `ssh` — ejecución remota reutilizable via `candados` + `EjecutorRemoto` | Herramienta | ✅ | +| S12 | Refactorizar `ssh.rb` — leer datos de fichas via `NodosInfo`, eliminar hash hardcodeado | Refactor | ✅ | --- @@ -169,7 +170,7 @@ Fase 6: ██████████ 100% (H1: HelpFormatter) Fase 7: ██████████ 100% (DOC1-DOC3: docs/tecnica/) Fase 8: ██████████ 100% (CI1-CI2: GitHub Actions) Fase 9: ██████████ 100% (M1-M5: MSP CLI) -Fase 10: ████████░░ 80% (S1-S9 done, S10-S11 pendientes) +Fase 10: █████████░ 90% (S1-S9,S11-S12 done, S10 pendiente) ``` --- diff --git a/nodos/pcv-dasu0.md b/nodos/pcv-dasu0.md index f50b36e4..7e574ce7 100644 --- a/nodos/pcv-dasu0.md +++ b/nodos/pcv-dasu0.md @@ -39,6 +39,7 @@ Esta VM replica la configuración prevista para la PC física [`pc-dasu0`](pc-da - **Fecha de Unión**: 27/02/2026 ## Servicios +- **SSH**: `DASUTEN\admindasu@10.0.100.12:7022` (Password, Bóveda: `admindasu`) - **OpenSSH Server**: Puerto 7022 (inicio automático, hardeado) ## Dependencias diff --git a/nodos/sql-dasuten.md b/nodos/sql-dasuten.md index 7bb00edf..4f17605c 100644 --- a/nodos/sql-dasuten.md +++ b/nodos/sql-dasuten.md @@ -24,8 +24,8 @@ - **Gateway**: `10.0.100.1` (IP de ruteo del host Proxmox) - **Servidores DNS**: `10.0.100.10` (dc-dasuten) / `8.8.8.8` - **Acceso Remoto**: - - **SSH**: `admindasu@dasuten.utnlr:7022` - - OpenSSH Server (Puerto 7022) - ✅ Habilitado con Bóveda de Secretos + - **SSH**: `DASUTEN\admindasu@10.0.100.11:7022` (Password, Bóveda: `admindasu`) + - OpenSSH Server (Puerto 7022) - ✅ Habilitado - SQL Browser (UDP 1434) - SQL Instance (TCP 1433) diff --git a/nodos/srv-dasu.md b/nodos/srv-dasu.md index e009a984..59be1240 100644 --- a/nodos/srv-dasu.md +++ b/nodos/srv-dasu.md @@ -16,8 +16,9 @@ - **Almacenamiento**: 1x 480GB SSD ADATA SU630 (`sda` - LVM-Thin Proxmox Base) ## Red -- **IP VPN**: `100.116.210.36` (Tailscale) +- **IP VPN**: `100.112.46.104` (Tailscale) - **IP Física (Bridge vmbr0)**: DHCP (WAN) + `10.0.100.1` (Alias LAN DASUTEN) +- **SSH**: `rmonla@100.112.46.104:22` (Passphrase RSA, Bóveda: `rsa`) - **Acceso**: SSH Key + Sudo (Usuario `rmonla` - Credenciales en bóveda: `srv-dasu:rmonla`) ## Servicios Críticos