- P2606: Plan v1.1 EN EJECUCIÓN. Fases 1-3 completadas. - Ficha nodo srvv-nginx-rm.md. P2606 en adn/07_proyectos.md. - Incluye cambios acumulados de sesiones anteriores.
316 lines
11 KiB
Ruby
316 lines
11 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
|
|
ayuda = ADN::HelpFormatter.generar_help(
|
|
titulo: "Gestión de Nodos",
|
|
descripcion: "Gestión y consulta de nodos de infraestructura",
|
|
uso: "./adn/tools/run nodos <acción> [opciones]",
|
|
opciones: [
|
|
{ names: ['agrupar [--por ambitos|servidor]'], desc: "Agrupar nodos de la red" },
|
|
{ names: ['listar [--tipo TIPO] [--ambito AMB]'], desc: "Listar fichas de nodos (.md)" },
|
|
{ names: ['buscar <término>'], desc: "Buscar nodos por nombre o contenido" }
|
|
],
|
|
ejemplos: [
|
|
{ cmd: "./adn/tools/run nodos agrupar --por ambitos", desc: "Agrupar nodos estructurados desde la DB" },
|
|
{ cmd: "./adn/tools/run nodos agrupar --por servidor --servidor srv-pmox1", desc: "Agrupar VMs del servidor en base a fichas" },
|
|
{ cmd: "./adn/tools/run nodos listar --ambito dtic-DASUTEN", desc: "Listar nodos MD cruzados con ámbito DB" },
|
|
{ cmd: "./adn/tools/run nodos buscar dasuten", desc: "Buscar nodos relacionados con dasuten" }
|
|
]
|
|
)
|
|
|
|
puts 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, por: 'servidor' }
|
|
parser = OptionParser.new do |opt|
|
|
opt.on('--servidor SRV', 'Filtrar por servidor anfitrión') { |s| opts[:servidor] = s }
|
|
opt.on('--por CAMPO', 'Agrupar por: servidor, ambitos') { |c| opts[:por] = c }
|
|
end
|
|
parser.order!(@args)
|
|
|
|
if opts[:por] == 'ambitos'
|
|
agrupar_por_ambitos
|
|
else
|
|
agrupar_por_servidor(opts)
|
|
end
|
|
end
|
|
|
|
def agrupar_por_ambitos
|
|
puts "#{Color::CYAN}📋 Agrupando nodos por Ámbitos (Desde DB)#{Color::RESET}"
|
|
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
|
|
|
require_relative '../db/core/bitacora_db'
|
|
|
|
ambitos_hash = Hash.new { |h, k| h[k] = [] }
|
|
sin_ambito = []
|
|
|
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
nodos = db.list_nodos(limit: 1000)
|
|
nodos.each do |n|
|
|
if n['ambito_nombre']
|
|
ambitos_hash[n['ambito_nombre']] << n['nombre']
|
|
else
|
|
sin_ambito << n['nombre']
|
|
end
|
|
end
|
|
end
|
|
|
|
ambitos_hash.keys.sort.each do |amb|
|
|
vms = ambitos_hash[amb]
|
|
puts "#{Color::GREEN}🏢 #{amb}#{Color::RESET} (#{vms.length} nodos)"
|
|
vms.sort.each { |vm| puts " ├── 💻 #{vm}" }
|
|
puts ""
|
|
end
|
|
|
|
if sin_ambito.any?
|
|
puts "#{Color::YELLOW}❓ Nodos sin ámbito asignado:#{Color::RESET}"
|
|
sin_ambito.sort.each { |nodo| puts " ├── 💻 #{nodo}" }
|
|
puts ""
|
|
end
|
|
|
|
total_vms = ambitos_hash.values.flatten.length + sin_ambito.length
|
|
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
|
puts "#{Color::DIM}Total: #{ambitos_hash.keys.length} ámbitos, #{total_vms} nodos#{Color::RESET}"
|
|
end
|
|
|
|
def agrupar_por_servidor(opts)
|
|
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, ambito: nil }
|
|
parser = OptionParser.new do |opt|
|
|
opt.on('--tipo TIPO', 'Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)') { |t| opts[:tipo] = t }
|
|
opt.on('--ambito NOMBRE', 'Cruzar nombres de base de datos para filtrar') { |a| opts[:ambito] = a }
|
|
end
|
|
parser.order!(@args)
|
|
|
|
puts "#{Color::CYAN}📋 Listando fichas de nodos#{Color::RESET}"
|
|
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
|
|
|
nodos_validos = nil
|
|
if opts[:ambito]
|
|
require_relative '../db/core/bitacora_db'
|
|
BitacorasDB::BitacoraDB.with_connection do |db|
|
|
ambito = db.get_ambito(opts[:ambito])
|
|
if ambito
|
|
nodos_db = db.list_nodos(ambito_id: ambito['id'], limit: 1000)
|
|
nodos_validos = nodos_db.map { |n| n['nombre'] }
|
|
else
|
|
puts "#{Color::RED}✗ Error: Ámbito '#{opts[:ambito]}' no encontrado en la DB.#{Color::RESET}"
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
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_validos
|
|
nodos.select! { |n| nodos_validos.include?(n) }
|
|
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
|