Files
dtic-DIIAA/adn/tools/cli/nodos.rb
T
Ricardo Monla 4ff2dc5e8e [P2604-S14] Subcomando 'nodos info' — metadata de fichas como CLI
Nuevo subcomando: ./adn/tools/run nodos info <nodo>
- Muestra IP, puerto SSH, usuario, auth, bóveda, host, SO, VMID
- Lecturas directas de fichas nodos/*.md via NodosInfo
- Campos no detectados se marcan como (no detectado)
- Completa la arquitectura: info.rb es submódulo + herramienta CLI

Ejemplo: nodos info pcv-dasu0 → IP, puerto 7022, DASUTEN\admindasu...
- 15 tests, 0 failures
- Evento 1079 (17:25→17:28)
2026-03-15 17:26:44 -03:00

256 lines
8.9 KiB
Ruby

# frozen_string_literal: true
# adn/tools/cli/nodos.rb — Subcomando para gestión y consulta de nodos
# ======================================================================
# Uso:
# ./adn/tools/run nodos listar [--tipo servidor|vm|pc]
# ./adn/tools/run nodos agrupar [--formato arbol|tabla]
# ./adn/tools/run nodos buscar <término>
require 'optparse'
require_relative '../core/colores'
require_relative 'nodos/info'
require_relative '../core/help_formatter'
module ADN
class SubcomandoNodos
def initialize(args, logger)
@args = args
@logger = logger
@opciones = {}
end
def ejecutar
if @args.empty?
mostrar_ayuda
return
end
accion = @args.shift
case accion
when 'info'
info_nodo
when 'agrupar'
agrupar_nodos
when 'listar'
listar_nodos
when 'buscar'
buscar_nodos
when 'ayuda', 'help', '-h', '--help'
mostrar_ayuda
else
puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
def mostrar_ayuda
puts <<~AYUDA
#{Color::CYAN}🖥️ Gestión de Nodos#{Color::RESET}
#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run nodos <acción> [opciones]
#{Color::YELLOW}Acciones:#{Color::RESET}
info <nodo> Mostrar metadata completa de un nodo
agrupar [--servidor SRV] Agrupar VMs por servidor anfitrión
listar [--tipo TIPO] Listar todos los nodos (fichas .md)
buscar <término> Buscar nodos por nombre o contenido
#{Color::YELLOW}Opciones de 'agrupar':#{Color::RESET}
--servidor SRV Filtrar solo VMs de un servidor específico
#{Color::YELLOW}Opciones de 'listar':#{Color::RESET}
--tipo TIPO Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run nodos info srv-dasu
./adn/tools/run nodos agrupar
./adn/tools/run nodos agrupar --servidor srv-pmox1
./adn/tools/run nodos listar
./adn/tools/run nodos listar --tipo srvv
./adn/tools/run nodos buscar dasuten
AYUDA
end
# ─── Info ───────────────────────────────────────────────────────────
def info_nodo
nombre = @args.shift
unless nombre
puts "#{Color::RED}✗ Se requiere nombre del nodo#{Color::RESET}"
puts " Uso: ./adn/tools/run nodos info <nodo>"
return
end
metadata = ADN::NodosInfo.extraer_metadata(nombre)
unless metadata
puts "#{Color::RED}✗ Nodo '#{nombre}' no encontrado en nodos/#{Color::RESET}"
return
end
puts "#{Color::CYAN}🖥️ Ficha: #{nombre}#{Color::RESET}"
puts "#{Color::DIM}#{'─' * 50}#{Color::RESET}"
campos = {
'IP' => metadata[:ip],
'Puerto SSH' => metadata[:puerto_ssh],
'Usuario' => metadata[:usuario],
'Auth' => metadata[:auth_type],
'Bóveda' => metadata[:clave_boveda],
'Host' => metadata[:host],
'SO' => metadata[:so],
'VMID' => metadata[:vmid]
}
campos.each do |label, valor|
valor_str = valor ? valor.to_s : "#{Color::DIM}(no detectado)#{Color::RESET}"
printf " #{Color::BOLD}%-14s#{Color::RESET} %s\n", label, valor_str
end
puts "#{Color::DIM}#{'─' * 50}#{Color::RESET}"
puts "#{Color::DIM} Fuente: nodos/#{nombre}.md#{Color::RESET}"
end
# ─── Agrupar ────────────────────────────────────────────────────────
def agrupar_nodos
opts = { servidor: nil }
parser = OptionParser.new do |opt|
opt.on('--servidor SRV', 'Filtrar por servidor anfitrión') { |s| opts[:servidor] = s }
end
parser.order!(@args)
puts "#{Color::CYAN}📋 Agrupando nodos por Servidor Host#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
servidores = Hash.new { |h, k| h[k] = [] }
sin_servidor = []
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo|
nodo_nombre = File.basename(archivo, '.md')
next if nodo_nombre.start_with?('_')
metadata = ADN::NodosInfo.extraer_metadata(nodo_nombre)
host = metadata ? metadata[:host] : nil
if host
servidores[host] << nodo_nombre
elsif !es_nodo_fisico?(nodo_nombre)
sin_servidor << nodo_nombre
end
end
# Filtrar por servidor si se especificó
if opts[:servidor]
srv = opts[:servidor]
unless servidores.key?(srv)
puts "#{Color::YELLOW}⚠ No se encontraron VMs para el servidor '#{srv}'#{Color::RESET}"
return
end
mostrar_servidor(srv, servidores[srv])
else
servidores.keys.sort.each { |srv| mostrar_servidor(srv, servidores[srv]) }
if sin_servidor.any?
puts "#{Color::YELLOW}❓ Nodos sin servidor anfitrión identificable:#{Color::RESET}"
sin_servidor.sort.each { |nodo| puts " ├── 💻 #{nodo}" }
puts ""
end
end
total_vms = servidores.values.flatten.length
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts "#{Color::DIM}Total: #{servidores.keys.length} servidores, #{total_vms} VMs#{Color::RESET}"
end
def mostrar_servidor(srv, vms)
puts "#{Color::GREEN}🏢 #{srv}#{Color::RESET} (#{vms.length} VMs)"
vms.sort.each { |vm| puts " ├── 💻 #{vm}" }
puts ""
end
def es_nodo_fisico?(nombre)
nombre.start_with?('srv-') || nombre.start_with?('pc-') ||
nombre == 'dtic-bitacoras' || nombre == 'cam-hikvision-24'
end
# ─── Listar ─────────────────────────────────────────────────────────
def listar_nodos
opts = { tipo: nil }
parser = OptionParser.new do |opt|
opt.on('--tipo TIPO', 'Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)') { |t| opts[:tipo] = t }
end
parser.order!(@args)
puts "#{Color::CYAN}📋 Listando fichas de nodos#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
archivos = Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).sort
nodos = archivos.map { |a| File.basename(a, '.md') }
nodos.reject! { |n| n.start_with?('_') }
if opts[:tipo]
nodos.select! { |n| n.start_with?(opts[:tipo]) }
end
if nodos.empty?
puts "#{Color::YELLOW}⚠ No se encontraron nodos#{opts[:tipo] ? " con prefijo '#{opts[:tipo]}'" : ''}#{Color::RESET}"
return
end
nodos.each do |nodo|
metadata = ADN::NodosInfo.extraer_metadata(nodo)
rol = metadata && metadata[:host] ? 'VM' : (metadata && metadata[:so] ? metadata[:so].to_s : '-')
puts " 💻 #{nodo.ljust(22)} #{Color::DIM}#{rol}#{Color::RESET}"
end
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts "#{Color::DIM}Total: #{nodos.length} nodos#{Color::RESET}"
end
# ─── Buscar ─────────────────────────────────────────────────────────
def buscar_nodos
termino = @args.join(' ')
if termino.empty?
puts "#{Color::RED}✗ Se requiere un término de búsqueda#{Color::RESET}"
puts " Uso: ./adn/tools/run nodos buscar <término>"
return
end
puts "#{Color::CYAN}🔍 Buscando nodos con '#{termino}'#{Color::RESET}"
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
resultados = []
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo|
nodo = File.basename(archivo, '.md')
contenido = File.read(archivo, encoding: 'UTF-8')
if nodo.include?(termino.downcase) || contenido.downcase.include?(termino.downcase)
# Buscar línea con contexto
lineas = contenido.split("\n")
contexto = lineas.find { |l| l.downcase.include?(termino.downcase) }
contexto = contexto ? contexto.strip[0..60] : '-'
resultados << { nodo: nodo, contexto: contexto }
end
end
if resultados.empty?
puts "#{Color::YELLOW}⚠ No se encontraron nodos que coincidan con '#{termino}'#{Color::RESET}"
return
end
resultados.each do |r|
puts " 💻 #{r[:nodo].ljust(22)} #{Color::DIM}#{r[:contexto]}#{Color::RESET}"
end
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
puts "#{Color::DIM}Total: #{resultados.length} resultados#{Color::RESET}"
end
end
end