384 lines
13 KiB
Ruby
384 lines
13 KiB
Ruby
# frozen_string_literal: true
|
||
|
||
# adn/tools/cli/network.rb — Herramienta para topología de red estandarizada
|
||
# ==========================================================================
|
||
# Esta herramienta provee una vista consolidada de la topología de red del
|
||
# sistema ADN, eliminando la necesidad de búsquedas exploratorias en /nodos.
|
||
#
|
||
# Uso:
|
||
# ./adn/tools/run network show
|
||
# ./adn/tools/run network show --proyecto P2604
|
||
# ./adn/tools/run network show --nodo srv-ns8
|
||
# ./adn/tools/run network show --json
|
||
#
|
||
# Proporciona información estructurada sobre:
|
||
# - Nodos y sus IPs, roles, saltos SSH
|
||
# - Proyectos activos y sus hitos
|
||
# - Dependencias entre componentes
|
||
# - Estado de candados necesarios
|
||
|
||
require 'optparse'
|
||
require_relative '../core/colores'
|
||
require_relative '../core/constants'
|
||
require_relative '../core/help_formatter'
|
||
require_relative 'nodos/info'
|
||
require_relative 'db'
|
||
|
||
module ADN
|
||
class SubcomandoNetwork
|
||
def initialize(args, logger)
|
||
@args = args
|
||
@logger = logger
|
||
@opciones = {
|
||
formato: :texto, # :texto, :json, :markdown
|
||
proyecto: nil,
|
||
nodo: nil,
|
||
detalle: false
|
||
}
|
||
end
|
||
|
||
def ejecutar
|
||
if @args.empty?
|
||
mostrar_ayuda
|
||
return
|
||
end
|
||
|
||
accion = @args.shift
|
||
|
||
case accion
|
||
when 'show'
|
||
mostrar_topologia
|
||
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 crear_parser
|
||
OptionParser.new do |opts|
|
||
opts.banner = "Uso: ./adn/tools/run network show [OPCIONES]"
|
||
opts.separator ""
|
||
opts.separator "Muestra la topología de red estandarizada del sistema ADN"
|
||
opts.separator ""
|
||
opts.separator "Opciones:"
|
||
|
||
opts.on("--formato FORMATO", [:texto, :json, :markdown], "Formato de salida (texto, json, markdown)") do |formato|
|
||
@opciones[:formato] = formato
|
||
end
|
||
|
||
opts.on("--proyecto CODIGO", "Filtrar por código de proyecto (ej: P2604)") do |codigo|
|
||
@opciones[:proyecto] = codigo.upcase
|
||
end
|
||
|
||
opts.on("--nodo NOMBRE", "Mostrar detalles de un nodo específico") do |nombre|
|
||
@opciones[:nodo] = nombre
|
||
end
|
||
|
||
opts.on("--detalle", "Mostrar información detallada incluyendo métricas") do
|
||
@opciones[:detalle] = true
|
||
end
|
||
|
||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||
mostrar_ayuda
|
||
end
|
||
end
|
||
end
|
||
|
||
def mostrar_ayuda
|
||
ayuda = ADN::HelpFormatter.generar_help(
|
||
titulo: "Topología de Red ADN",
|
||
descripcion: "Muestra la topología de red estandarizada del sistema ADN",
|
||
uso: "./adn/tools/run network show [OPCIONES]",
|
||
opciones: [
|
||
{ names: ['--formato FORMATO'], desc: "Formato de salida (texto, json, markdown)" },
|
||
{ names: ['--proyecto CODIGO'], desc: "Filtrar por código de proyecto (ej: P2604)" },
|
||
{ names: ['--nodo NOMBRE'], desc: "Mostrar detalles de un nodo específico" },
|
||
{ names: ['--detalle'], desc: "Mostrar información detallada incluyendo métricas" },
|
||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||
],
|
||
ejemplos: [
|
||
{ cmd: "./adn/tools/run network show", desc: "Mostrar topología completa en formato texto" },
|
||
{ cmd: "./adn/tools/run network show --json", desc: "Mostrar topología en formato JSON" },
|
||
{ cmd: "./adn/tools/run network show --proyecto P2604", desc: "Mostrar topología filtrada por proyecto P2604" },
|
||
{ cmd: "./adn/tools/run network show --nodo srv-ns8 --detalle", desc: "Mostrar detalles del nodo srv-ns8" }
|
||
]
|
||
)
|
||
|
||
puts ayuda
|
||
end
|
||
|
||
def mostrar_topologia
|
||
parser = crear_parser
|
||
parser.parse!(@args)
|
||
|
||
datos = recopilar_datos_topologia
|
||
|
||
case @opciones[:formato]
|
||
when :json
|
||
puts JSON.pretty_generate(datos)
|
||
when :markdown
|
||
puts generar_markdown(datos)
|
||
else
|
||
puts generar_texto(datos)
|
||
end
|
||
end
|
||
|
||
def recopilar_datos_topologia
|
||
{
|
||
timestamp: Time.now.iso8601,
|
||
nodos: obtener_informacion_nodos,
|
||
proyectos: obtener_informacion_proyectos,
|
||
estadisticas: obtener_estadisticas_sistema
|
||
}
|
||
end
|
||
|
||
def obtener_informacion_nodos
|
||
nodos = []
|
||
|
||
# Obtener nodos desde fichas .md
|
||
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo|
|
||
nodo_nombre = File.basename(archivo, '.md')
|
||
next if nodo_nombre.start_with?('_')
|
||
|
||
# Filtrar por nodo específico si se indicó
|
||
next if @opciones[:nodo] && @opciones[:nodo] != nodo_nombre
|
||
|
||
metadata = ADN::NodosInfo.extraer_metadata(nodo_nombre)
|
||
next unless metadata && metadata[:ip] # Solo nodos con IP configurada
|
||
|
||
nodo_info = {
|
||
nombre: nodo_nombre,
|
||
ip: metadata[:ip],
|
||
ip_lan: metadata[:ip_lan],
|
||
hostname: metadata[:nombre],
|
||
so: metadata[:so]&.to_s,
|
||
puerto_ssh: metadata[:puerto_ssh],
|
||
usuario: metadata[:usuario],
|
||
auth_type: metadata[:auth_type]&.to_s,
|
||
clave_boveda: metadata[:clave_boveda],
|
||
host: metadata[:host], # Para identificar VMs y su hypervisor
|
||
rol: determinar_rol_nodo(metadata),
|
||
proxysalto: metadata[:host] # Si tiene host, necesita proxy jump
|
||
}
|
||
|
||
# Añadir información de proyectos si se filtró por proyecto
|
||
if @opciones[:proyecto]
|
||
nodo_info[:proyectos_relacionados] = obtener_proyectos_relacionados_nodo(nodo_nombre)
|
||
end
|
||
|
||
nodos << nodo_info
|
||
end
|
||
|
||
nodos.sort_by { |n| n[:nombre] }
|
||
end
|
||
|
||
def determinar_rol_nodo(metadata)
|
||
case metadata[:so]&.to_s
|
||
when 'windows'
|
||
if metadata[:nombre]&.start_with?('srvv')
|
||
'Servidor Windows Virtualizado'
|
||
elsif metadata[:nombre]&.start_with?('dasu-srvv')
|
||
'Controlador de Dominio / Servicios'
|
||
else
|
||
'Workstation Windows'
|
||
end
|
||
when 'linux'
|
||
if metadata[:nombre]&.start_with?('srv-') && !metadata[:nombre]&.start_with?('srvv')
|
||
'Servidor Linux Físico'
|
||
elsif metadata[:nombre]&.start_with?('srv-pmox')
|
||
'Hipertensor Proxmox'
|
||
elsif metadata[:nombre]&.start_with?('pcv')
|
||
'Máquina Virtual Linux'
|
||
elsif metadata[:nombre]&.start_with?('dtic-bitacoras')
|
||
'Servidor de Base de Datos y Bitácoras'
|
||
else
|
||
'Servicio Linux'
|
||
end
|
||
else
|
||
'Nodo Desconocido'
|
||
end
|
||
end
|
||
|
||
def obtener_informacion_proyectos
|
||
return [] unless @opciones[:proyecto]
|
||
|
||
proyectos = []
|
||
begin
|
||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||
proyecto = db.find_proyecto_by_codigo(@opciones[:proyecto])
|
||
if proyecto
|
||
hitos = db.list_hitos(@opciones[:proyecto])
|
||
|
||
proyectos << {
|
||
codigo: proyecto['codigo'],
|
||
nombre: proyecto['nombre'],
|
||
descripcion: proyecto['descripcion'],
|
||
estado: proyecto['estado'],
|
||
hitos: hitos.map do |hito|
|
||
{
|
||
id: hito['id_hito'],
|
||
titulo: hito['titulo'],
|
||
estado: hito['estado'],
|
||
horas_presencial: hito['horas_presencial'],
|
||
horas_remoto: hito['horas_remoto']
|
||
}
|
||
end
|
||
}
|
||
end
|
||
end
|
||
rescue => e
|
||
@logger&.warn("Error obteniendo información de proyectos: #{e.message}")
|
||
end
|
||
|
||
proyectos
|
||
end
|
||
|
||
def obtener_estadisticas_sistema
|
||
estadisticas = {}
|
||
|
||
begin
|
||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||
stats = db.statistics
|
||
estadisticas = {
|
||
bitacoras: stats[:bitacoras] || 0,
|
||
entradas: stats[:entradas] || 0,
|
||
nodos: stats[:nodos] || 0,
|
||
temas: stats[:temas] || 0,
|
||
entradas_hoy: obtener_entradas_hoy(db)
|
||
}
|
||
end
|
||
rescue => e
|
||
@logger&.warn("Error obteniendo estadísticas: #{e.message}")
|
||
estadisticas = { error: "No se pudieron obtener estadísticas" }
|
||
end
|
||
|
||
estadisticas
|
||
end
|
||
|
||
def obtener_entradas_hoy(db)
|
||
hoy = Date.today.to_s
|
||
resultado = db.execute("SELECT COUNT(*) as total FROM bitacoras.entradas e JOIN bitacoras.bitacoras b ON e.bitacora_id = b.id WHERE b.fecha = $1", [hoy])
|
||
resultado.first['total'].to_i
|
||
end
|
||
|
||
def obtener_proyectos_relacionados_nodo(nodo_nombre)
|
||
# En una implementación completa, esto consultaría la base de datos
|
||
# para encontrar proyectos asociados a este nodo
|
||
# Por ahora, retornamos una lista vacía
|
||
[]
|
||
end
|
||
|
||
def generar_texto(datos)
|
||
salida = []
|
||
salida << "#{Color::CYAN}🌐 TOPOLOGÍA DE RED ADN - #{datos[:timestamp]}#{Color::RESET}"
|
||
salida << "#{Color::DIM}#{'-' * 80}#{Color::RESET}"
|
||
|
||
# Estadísticas del sistema
|
||
salida << "#{Color::YELLOW}📊 ESTADÍSTICAS DEL SISTEMA#{Color::RESET}"
|
||
stats = datos[:estadisticas]
|
||
salida << " Bitácoras: #{stats[:bitacoras]}"
|
||
salida << " Entradas: #{stats[:entradas]}"
|
||
salida << " Entradas hoy: #{stats[:entradas_hoy]}"
|
||
salida << " Nodos: #{stats[:nodos]}"
|
||
salida << " Temas: #{stats[:temas]}"
|
||
salida << ""
|
||
|
||
# Nodos
|
||
salida << "#{Color::YELLOW}🖥️ NODOS CONFIGURADOS (#{datos[:nodos].length})#{Color::RESET}"
|
||
datos[:nodos].each do |nodo|
|
||
estado_ssh = nodo[:ip] ? "#{Color::GREEN}✓#{Color::RESET}" : "#{Color::RED}✗#{Color::RESET}"
|
||
salida << " #{estado_ssh} #{nodo[:nombre]} (#{nodo[:rol]})"
|
||
salida << " IP: #{nodo[:ip] || '(no configurada)'}"
|
||
salida << " SSH: #{nodo[:usuario]}@#{nodo[:ip]}:#{nodo[:puerto_ssh]}" if nodo[:ip]
|
||
salida << " Auth: #{nodo[:auth_type] || '(no configurado)'}" if nodo[:ip]
|
||
salida << " Bóveda: #{nodo[:clave_boveda] || '(no configurada)'}" if nodo[:ip]
|
||
|
||
if nodo[:proxysalto]
|
||
salida << " ↳ Requiere proxy jump vía: #{nodo[:proxysalto]}"
|
||
end
|
||
|
||
salida << ""
|
||
end
|
||
|
||
# Proyectos (si se filtró)
|
||
unless datos[:proyectos].empty?
|
||
salida << "#{Color::YELLOW}📋 PROYECTOS RELACIONADOS#{Color::RESET}"
|
||
datos[:proyectos].each do |proyecto|
|
||
salida << " #{proyecto[:codigo]} - #{proyecto[:nombre]}"
|
||
salida << " Estado: #{proyecto[:estado]}"
|
||
salida << " Hitos: #{proyecto[:hitos].length}"
|
||
|
||
if @opciones[:detalle]
|
||
proyecto[:hitos].each do |hito|
|
||
salida << " [#{hito[:id]}] #{hito[:titulo]} (#{hito[:estado]})"
|
||
end
|
||
end
|
||
end
|
||
salida << ""
|
||
end
|
||
|
||
salida << "#{Color::DIM}#{'-' * 80}#{Color::RESET}"
|
||
salida << "#{Color::CYAN}💡 Para conexión SSH directa: ./adn/tools/run ssh <nodo> <comando>#{Color::RESET}"
|
||
salida << "#{Color::DIM}Nota: Los candados se gestionan automáticamente mediante adn candados#{Color::RESET}"
|
||
|
||
salida.join("\n")
|
||
end
|
||
|
||
def generar_markdown(datos)
|
||
salida = []
|
||
salida << "# Topología de Red ADN"
|
||
salida << ""
|
||
salida << "**Generado:** #{datos[:timestamp]}"
|
||
salida << ""
|
||
|
||
salida << "## Estadísticas del Sistema"
|
||
salida << ""
|
||
salida << "| Métrica | Valor |"
|
||
salida << "|---------|-------|"
|
||
stats = datos[:estadisticas]
|
||
salida << "| Bitácoras | #{stats[:bitacoras]} |"
|
||
salida << "| Entradas | #{stats[:entradas]} |"
|
||
salida << "| Entradas hoy | #{stats[:entradas_hoy]} |"
|
||
salida << "| Nodos | #{stats[:nodos]} |"
|
||
salida << "| Temas | #{stats[:temas]} |"
|
||
salida << ""
|
||
|
||
salida << "## Nodos Configurados (#{datos[:nodos].length})"
|
||
salida << ""
|
||
salida << "| Nodo | Rol | IP | SSH | Auth | Bóveda | Proxy Jump |"
|
||
salida << "|------|-----|----|-----|------|--------|------------|"
|
||
datos[:nodos].each do |nodo|
|
||
ssh_info = nodo[:ip] ? "#{nodo[:usuario]}@#{nodo[:ip]}:#{nodo[:puerto_ssh]}" : "(no configurado)"
|
||
auth_info = nodo[:auth_type] || "(no configurado)"
|
||
boveda_info = nodo[:clave_boveda] || "(no configurada)"
|
||
proxy_info = nodo[:proxysalto] || "−"
|
||
salida << "| #{nodo[:nombre]} | #{nodo[:rol]} | #{nodo[:ip] or '(no configurada)'} | #{ssh_info} | #{auth_info} | #{boveda_info} | #{proxy_info} |"
|
||
end
|
||
salida << ""
|
||
|
||
unless datos[:proyectos].empty?
|
||
salida << "## Proyectos Relacionados"
|
||
salida << ""
|
||
datos[:proyectos].each do |proyecto|
|
||
salida << "### #{proyecto[:codigo]} - #{proyecto[:nombre]}"
|
||
salida << ""
|
||
salida << "**Estado:** #{proyecto[:estado]}"
|
||
salida << ""
|
||
salida << "| Hito | Título | Estado | Hrs Pres. | Hrs Rem. |"
|
||
salida << "|------|--------|--------|-----------|----------|"
|
||
proyecto[:hitos].each do |hito|
|
||
salida << "| #{hito[:id]} | #{hito[:titulo]} | #{hito[:estado]} | #{hito[:horas_presencial] or 0} | #{hito[:horas_remoto] or 0} |"
|
||
end
|
||
salida << ""
|
||
end
|
||
end
|
||
|
||
salida.join("\n")
|
||
end
|
||
end
|
||
end |