[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:
+38
-64
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
1773604236
|
||||
1773604966
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user