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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b5e89144e3
commit
3543e62263
@@ -0,0 +1,278 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user