# 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/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) caso 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 #{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