🐝 Fase 2: Orquestador 1-dron-por-archivo (bkps orquestar)

- proc_xen.rb: listar() scanner + ejecutar_uno() atómico
- bkps.rb: --archivo flag + subcomando orquestar
- Patrón: scan → lanza drones secuenciales → 1 VM por dron
- Filosofía: Menos es Más
This commit is contained in:
Ricardo Monla
2026-04-09 12:13:17 -03:00
parent 6d7394b22e
commit 7d0842c25d
2 changed files with 202 additions and 38 deletions
+119 -10
View File
@@ -49,6 +49,8 @@ module ADN
cmd_list
when 'run'
cmd_run(@args)
when 'orquestar'
cmd_orquestar(@args)
when 'menu'
cmd_menu
when 'status'
@@ -199,6 +201,7 @@ module ADN
opts.banner = "Uso: ./adn/tools/run bkps run <T1|C1|ID> [opciones]"
opts.on("--batch", "Modo batch (sin interacción, para cron)") { options[:batch] = true }
opts.on("--dry-run", "Simulación sin ejecutar") { options[:dry_run] = true }
opts.on("--archivo NOMBRE", "Procesar solo este archivo (1-dron-por-archivo)") { |a| options[:archivo] = a }
end.parse!(args)
ref = args.shift
@@ -359,6 +362,7 @@ module ADN
options = { dias: retencion['dias_default'] || 6 }
OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run bkps sanear [opciones]"
opts.on("--batch", "Modo batch (sin confirmación)") { options[:batch] = true }
opts.on("--dias N", Integer, "Días de retención (default: #{options[:dias]})") { |d| options[:dias] = d }
opts.on("--tier N", Integer, "Solo tier N (1=proxmox, 2=local, 3=nube)") { |t| options[:tier] = t }
opts.on("--dry-run", "Solo listar, no borrar") { options[:dry_run] = true }
@@ -408,14 +412,15 @@ module ADN
storage = tier['storage']
hosts = tier['hosts'] || []
ruta_dump = "/mnt/pve/#{storage}/dump"
candado_clave = tier['candado'] || 'srv-dasu:root'
system("ruby #{candados_path} authorize > /dev/null 2>&1")
output_ls = ""
hosts.each do |h|
cmd_ls = "ls -1 --full-time #{ruta_dump}/vzdump-*"
res = `ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{h['ip']} "#{cmd_ls}"' 2>&1`
unless res.include?('No such file') || res.include?('Connection refused')
res = `ruby #{candados_path} run #{candado_clave} SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{h['ip']} "#{cmd_ls}"' 2>&1`
if $?.success? || res.include?('No such file')
output_ls = res
break
end
@@ -455,9 +460,10 @@ module ADN
# Borrar via SSH
if elim > 0 && !options[:dry_run]
todos.select { |fp, _| !protegido?(fp, por_nodo, todos) && todos[fp][:fecha] < limite }.each_value do |info|
candidatos = calcular_candidatos(todos, por_nodo, limite)
candidatos.each_value do |info|
info[:archivos].each do |ruta|
system("ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{hosts.first['ip']} \"rm -f '#{ruta}'\"' > /dev/null 2>&1")
system("ruby #{candados_path} run #{candado_clave} SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{hosts.first['ip']} \"rm -f '#{ruta}'\"' > /dev/null 2>&1")
end
end
end
@@ -598,11 +604,15 @@ module ADN
return [0, protegidos]
end
print "\n¿Borrar todos? (S/N): "
resp = STDIN.gets&.chomp&.strip&.downcase
unless resp == 's'
puts "Cancelado."
return [0, protegidos]
if options[:batch]
puts "\n#{Color::YELLOW}[BATCH] Borrando automáticamente sin pedir confirmación.#{Color::RESET}"
else
print "\n¿Borrar todos? (S/N): "
resp = STDIN.gets&.chomp&.strip&.downcase
unless resp == 's'
puts "Cancelado."
return [0, protegidos]
end
end
lista.each_with_index do |b, i|
@@ -738,7 +748,15 @@ module ADN
t_inicio = Time.now
@logger.info("#{tarea[:texto]} (#{tipo})")
procesador = factory.call(@logger)
resultado = procesador.ejecutar(tarea)
# Modo atómico: procesar un solo archivo
resultado = if options[:archivo] && procesador.respond_to?(:ejecutar_uno)
@logger.info("🎯 Modo atómico: #{options[:archivo]}")
procesador.ejecutar_uno(tarea, options[:archivo])
else
procesador.ejecutar(tarea)
end
duracion = (Time.now - t_inicio).to_i
mins = duracion / 60
segs = duracion % 60
@@ -746,6 +764,97 @@ module ADN
resultado
end
# ─── Orquestador: 1 dron por archivo ──────────────────────────
def cmd_orquestar(args)
options = {}
OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run bkps orquestar <T1|ID> [opciones]"
opts.on("--timeout N", Integer, "Timeout por dron en segundos (default: 7200)") { |t| options[:timeout] = t }
opts.on("--dry-run", "Solo listar archivos sin lanzar drones") { options[:dry_run] = true }
end.parse!(args)
ref = args.shift
unless ref
puts "#{Color::RED}✗ Falta referencia de tarea (T1, T2, o ID)#{Color::RESET}"
return
end
# Resolver tarea
tarea = case ref.upcase
when /^T(\d+)$/ then @config.tareas[$1.to_i - 1]
else @config.find_tarea(ref)
end
unless tarea
puts "#{Color::RED}✗ Tarea '#{ref}' no encontrada#{Color::RESET}"
return
end
tipo = tarea[:tipo].to_s
factory = TIPOS[tipo]
unless factory
puts "#{Color::RED}✗ Tipo '#{tipo}' no soporta orquestación#{Color::RESET}"
return
end
procesador = factory.call(@logger)
unless procesador.respond_to?(:listar)
puts "#{Color::RED}✗ Procesador '#{tipo}' no soporta listar (orquestación no disponible)#{Color::RESET}"
return
end
# Escanear archivos pendientes
archivos = procesador.listar(tarea)
pendientes = archivos.select { |a| a[:pendiente] }
puts "\n#{Color::BOLD}#{Color::CYAN}🐝 Orquestador AtomicDrones — #{tarea[:texto]}#{Color::RESET}"
puts "" * 60
puts " Total archivos: #{archivos.length}"
puts " Pendientes: #{pendientes.length}"
puts " Ya procesados: #{archivos.length - pendientes.length}"
puts ""
if pendientes.empty?
puts "#{Color::GREEN}✔ Todos los archivos ya están procesados.#{Color::RESET}"
return
end
adn_run = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run')
timeout = options[:timeout] || 7200
pendientes.each_with_index do |arch, i|
tamaño_gb = (arch[:tamaño].to_f / 1024**3).round(1)
puts " #{Color::BOLD}[#{i+1}/#{pendientes.length}]#{Color::RESET} #{arch[:vm]}#{arch[:archivo]} (#{tamaño_gb}GB)"
if options[:dry_run]
puts " #{Color::YELLOW}[DRY-RUN] Lanzaría dron para: #{arch[:archivo]}#{Color::RESET}"
next
end
# Lanzar dron atómico para este archivo
nota = "Comprimir #{arch[:vm]} (#{tamaño_gb}GB)"
cmd_dron = "#{adn_run} dron lanzar" \
" --nota \"#{nota}\"" \
" --timeout #{timeout}" \
" -- #{adn_run} bkps run #{ref} --batch --archivo #{Shellwords.escape(arch[:archivo])}"
puts " 🛸 Lanzando dron..."
resultado = system(cmd_dron)
if resultado
puts " #{Color::GREEN}✔ Dron completado#{Color::RESET}"
else
puts " #{Color::RED}✖ Dron falló — deteniendo orquestación#{Color::RESET}"
break
end
puts ""
end
puts "#{Color::BOLD}🐝 Orquestación finalizada.#{Color::RESET}"
end
def ejecutar_comando(comando, options = {})
t_inicio = Time.now
@logger.info("=== #{comando[:texto]} ===")