Files
dtic-DIIAA/adn/tools/cli/network.rb
T

384 lines
13 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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