[P2604] Fase 15: Optimización IA e integración de herramienta candados. Sincronización de bitácoras y actualización de planes de proyecto.

This commit is contained in:
Ricardo Monla
2026-03-19 09:52:29 -03:00
parent 183dbf6fb6
commit 5aa37e7221
138 changed files with 8777 additions and 464 deletions
+384
View File
@@ -0,0 +1,384 @@
# 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 <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