Files
dtic-DIIAA/adn/tools/cli/dron/dispatcher.rb
T
Ricardo MonlaandClaude Opus 4.6 3543e62263 feat(dron): Migración a AtomicDrones con arquitectura DB-First
Implementación del sistema AtomicDrones basado en "Menos es Más":
- Refactorización de dron.rb a dispatcher + módulos atómicos
- Nuevos módulos: ejecutor, vigilante, sanador, bitacora
- DronDB para acceso a datos en PostgreSQL
- Tablas: dron_logs, dron_avances, dron_metricas
- Views: vw_dron_estado, vw_dron_zombies, vw_dron_metricas_semanal
- Health check con detección de zombies (heartbeat > 5min)
- Dashboard compacto de la flota
- Saneamiento con reintentos (backoff exponencial) y limpieza

Comandos soportados:
- dron lanzar: Ejecutar comando como dron
- dron flota: Dashboard de la flota
- dron salud: Health check activo
- dron estado <ID>: Estado detallado
- dron sanear: Saneamiento completo
- dron limpiar: Limpieza de antiguos
- dron reintentar: Re-intentar fallidos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 11:35:13 -03:00

279 lines
8.4 KiB
Ruby

# frozen_string_literal: true
# Dron::Dispatcher — Dispatcher de subcomandos para AtomicDrones
#
# Responsabilidad única: Despachar comandos a los módulos atómicos correctos
# - Parsea argumentos de línea de comandos
# - Invoca el módulo atómico correspondiente
# - Muestra ayuda formateada
#
# Uso:
# require_relative 'dron/dispatcher'
# Dron::Dispatcher.ejecutar(ARGV)
require_relative '../../core/help_formatter'
require_relative '../../core/logger'
require_relative '../../core/colores'
module Dron
class Dispatcher
def self.ejecutar(args)
@logger ||= ADN::Logger.new
subcomando = args.shift
case subcomando
when 'lanzar', 'run', nil
EjecutorComando.ejecutar(args)
when 'flota', 'list'
VigilanteComando.flota(args)
when 'salud', 'health'
VigilanteComando.salud(args)
when 'sanear', 'clean'
SanadorComando.sanear(args)
when 'limpiar', 'cleanup'
SanadorComando.limpiar(args)
when 'reintentar', 'retry'
SanadorComando.reintentar(args)
when 'estado', 'status'
VigilanteComando.estado(args)
when 'ayuda', 'help', '--help', '-h'
mostrar_ayuda
else
@logger.error("❌ Subcomando desconocido: #{subcomando}")
mostrar_ayuda
exit 1
end
end
def self.mostrar_ayuda
lines = []
lines << "#{Color::BOLD}🛸 Sistema de Drones ADN (AtomicDrones)#{Color::RESET}"
lines << "#{Color::DIM}#{'=' * 50}#{Color::RESET}"
lines << ""
lines << "#{Color::CYAN}Gestión de drones atómicos especializados para automatización de tareas#{Color::RESET}"
lines << ""
lines << "Uso: #{Color::YELLOW}./adn/tools/run dron <subcomando> [opciones]#{Color::RESET}"
lines << ""
lines << "\n#{Color::BOLD}Subcomandos:#{Color::RESET}"
lines << " #{Color::GREEN}lanzar [OPTIONS]#{Color::RESET} Lanzar tarea como dron ejecutor"
lines << " #{Color::GREEN}flota#{Color::RESET} Dashboard compacto de la flota"
lines << " #{Color::GREEN}salud#{Color::RESET} Health check activo (detecta zombies)"
lines << " #{Color::GREEN}estado <ID>#{Color::RESET} Estado detallado de un dron"
lines << " #{Color::GREEN}sanear#{Color::RESET} Sanear flota (reintentar + limpiar)"
lines << " #{Color::GREEN}limpiar#{Color::RESET} Limpiar drones antiguos"
lines << " #{Color::GREEN}reintentar#{Color::RESET} Re-intentar drones fallidos"
lines << ""
lines << "\n#{Color::BOLD}Opciones:#{Color::RESET}"
lines << " #{Color::GREEN}--nota \"texto\"#{Color::RESET} Nota descriptiva para la bitácora"
lines << " #{Color::GREEN}--evento ID#{Color::RESET} Vincular a evento existente"
lines << " #{Color::GREEN}--timeout N#{Color::RESET} Timeout en segundos (default: 3600)"
lines << " #{Color::GREEN}--dry-run#{Color::RESET} Simulación sin ejecutar"
lines << " #{Color::GREEN}--help#{Color::RESET} Mostrar esta ayuda"
lines << ""
lines << "\n#{Color::BOLD}Ejemplos:#{Color::RESET}"
lines << " #{Color::DIM}$ ./adn/tools/run dron lanzar --nota 'Backup SQL' -- vzdump 103#{Color::RESET}"
lines << " #{Color::CYAN}Lanzar backup como dron#{Color::RESET}"
lines << " #{Color::DIM}$ ./adn/tools/run dron flota#{Color::RESET}"
lines << " #{Color::CYAN}Ver estado de la flota#{Color::RESET}"
lines << " #{Color::DIM}$ ./adn/tools/run dron salud#{Color::RESET}"
lines << " #{Color::CYAN}Health check de la flota#{Color::RESET}"
lines << ""
puts lines.join("\n")
end
end
# ============================================================================
# Comandos encapsulados para cada módulo atómico
# ============================================================================
class EjecutorComando
def self.ejecutar(args)
require_relative 'ejecutor'
# Parsear argumentos
opciones = parsear_opciones(args)
if opciones[:cmd].nil?
puts "❌ Error: Debes especificar un comando a ejecutar"
puts ""
puts "Uso: ./adn/tools/run dron lanzar --nota \"Nota\" -- <comando>"
exit 1
end
# Lanzar dron ejecutor
resultado = Dron::Ejecutor.lanzar(
cmd: opciones[:cmd],
nota: opciones[:nota],
evento_id: opciones[:evento_id],
timeout: opciones[:timeout],
flujo_id: opciones[:flujo_id]
)
# Mostrar resultado
puts "\n🛸 Resultado del Dron Ejecutor"
puts "═" * 60
puts " Dron ID: #{resultado[:dron_id]}"
puts " Estado: #{resultado[:exit_code] == 0 ? '✅ Completado' : '❌ Fallido'}"
puts " Exit Code: #{resultado[:exit_code]}"
puts " Duración: #{resultado[:duration]}s"
puts "═" * 60
exit resultado[:exit_code]
end
def self.parsear_opciones(args)
opciones = {
nota: nil,
evento_id: nil,
timeout: 3600,
flujo_id: nil,
cmd: nil
}
# Parsear opciones con nombre
while args.first&.start_with?('--')
arg = args.shift
case arg
when '--nota'
opciones[:nota] = args.shift
when '--evento'
opciones[:evento_id] = args.shift.to_i
when '--timeout'
opciones[:timeout] = args.shift.to_i
when '--flujo'
opciones[:flujo_id] = args.shift
when '--help', '-h'
Dispatcher.mostrar_ayuda
exit 0
end
end
# El resto es el comando a ejecutar
opciones[:cmd] = args.join(' ') unless args.empty?
opciones
end
end
class VigilanteComando
def self.flota(args)
require_relative 'vigilante'
output = Dron::Vigilante.dashboard
puts output
end
def self.salud(args)
require_relative 'vigilante'
diagnosticos = Dron::Vigilante.salud
exit(diagnosticos[:saludable] ? 0 : 1)
end
def self.estado(args)
require_relative 'vigilante'
require_relative '../../db/core/dron_db'
dron_id = args.first
if dron_id.nil?
puts "❌ Error: Debes especificar un dron_id"
puts "Uso: ./adn/tools/run dron estado <dron_id>"
exit 1
end
dron = ADN::DB::DronDB.obtener_dron(dron_id)
if dron.nil?
puts "❌ Dron no encontrado: #{dron_id}"
exit 1
end
puts "\n📊 Estado del Dron"
puts "═" * 60
puts " ID: #{dron['dron_id']}"
puts " Tipo: #{dron['tipo']}"
puts " Estado: #{dron['estado']}"
puts " Comando: #{dron['cmd']}"
puts " Inicio: #{dron['started_at']}"
puts " Fin: #{dron['completed_at'] || '—'}"
puts " Heartbeat: #{dron['heartbeat']}"
puts " Reintentos: #{dron['reintentos']}"
puts " Exit Code: #{dron['exit_code'] || '—'}"
puts "═" * 60
# Mostrar avances si existen
require_relative '../../db/core/bitacora_db'
sql = 'SELECT * FROM bitacoras.dron_avances WHERE dron_id = $1 ORDER BY paso ASC'
avances = BitacorasDB::BitacoraDB.ejecutar(sql, [dron_id])
unless avances.empty?
puts "\n📝 Avances:"
avances.each do |avance|
puts " [#{avance['estado']}] Paso #{avance['paso']}: #{avance['descripcion']}"
end
end
end
end
class SanadorComando
def self.sanear(args)
require_relative 'sanador'
opciones = parsear_opciones(args)
resultado = Dron::Sanador.sanear(
dias_limpieza: opciones[:dias],
dry_run: opciones[:dry_run]
)
exit(0)
end
def self.limpiar(args)
require_relative 'sanador'
opciones = parsear_opciones(args)
resultado = Dron::Sanador.limpiar_antiguos(
dias: opciones[:dias],
dry_run: opciones[:dry_run]
)
exit(0)
end
def self.reintentar(args)
require_relative 'sanador'
opciones = parsear_opciones(args)
resultado = Dron::Sanador.reintentar_fallidos(
dry_run: opciones[:dry_run]
)
exit(0)
end
def self.parsear_opciones(args)
opciones = {
dias: 7,
dry_run: false
}
args.each do |arg|
case arg
when '--dry-run'
opciones[:dry_run] = true
when '--dias'
idx = args.index(arg)
opciones[:dias] = args[idx + 1].to_i if args[idx + 1]
end
end
opciones
end
end
end