# frozen_string_literal: true # ========================================================== # dtic-BKPs - Automatic Backup Processor # ---------------------------------------------------------- # APP: dtic-BKPs # FILE: fxs/proc_syncPmox.rb # VERSION: v5.5.1 # AUTHOR: Ricardo MONLA (rmonla@) # LICENSE: MIT License # ========================================================== require 'tmpdir' require 'shellwords' require 'open3' require 'fileutils' # Carga las constantes, logger y helpers globales # require_relative '../core/globals' # --- Helpers para montaje rclone --- def montar_rclone(origen_remote, tarea_id) dir_tmp = Dir.mktmpdir("bkps_#{tarea_id}_") log :info, "#{tarea_id}: Montando origen remoto #{origen_remote} en #{dir_tmp}" # Ejecutar en background pid = spawn("rclone mount #{Shellwords.escape(origen_remote)} #{dir_tmp} --daemon --vfs-cache-mode writes") Process.detach(pid) # Esperar un poco a que monte sleep 2 if Dir.exist?(dir_tmp) && !(Dir.entries(dir_tmp) - %w[. ..]).empty? return dir_tmp else msg = "#{tarea_id}: Error montando remote #{origen_remote}" log :error, msg puts "#{ICONO_ERROR} #{msg}" sleep 5 return nil end end def desmontar_rclone(dir_tmp, tarea_id) return unless dir_tmp && Dir.exist?(dir_tmp) log :info, "#{tarea_id}: Desmontando #{dir_tmp}" system("fusermount -u #{dir_tmp} 2>/dev/null || umount #{dir_tmp} 2>/dev/null") FileUtils.remove_entry(dir_tmp) if Dir.exist?(dir_tmp) end # --- Función Principal del Procesador Proxmox --- def procesar_backups_tipo_proxmox(tarea_cfg) system 'clear' or system 'cls' # 1. --- Extraer configuración e inicializar variables --- origen_remote = tarea_cfg[:origen] dir_destino_base = tarea_cfg[:destino] eliminar_original = tarea_cfg[:eliminar_origen] sobrescribir_destino = tarea_cfg[:sobrescribir_destino] backups_procesados_llamada = 0 puts "\n#{NEGRITA}#{CIAN}--- #{tarea_cfg[:menu_texto]} ---#{RESET}" log :info, "#{tarea_cfg[:id]}: Iniciado. Origen remoto: #{origen_remote}, Eliminar: #{eliminar_original}, Sobrescribir: #{sobrescribir_destino}" # 2. --- Montar origen remoto --- dir_origen = montar_rclone(origen_remote, tarea_cfg[:id]) return unless dir_origen begin @pv_disponible ||= !`which pv`.to_s.strip.empty? unless @pv_disponible if @pv_notificado.nil? msg = "#{tarea_cfg[:id]}: Comando 'pv' no encontrado. Compresión sin barra de progreso." log :warn, msg puts "#{ICONO_WARN} #{msg}" @pv_notificado = true end end # 3. --- Buscar archivos .log --- archivos_log = Dir.glob(File.join(dir_origen, '*.log')) if archivos_log.empty? msg = "#{tarea_cfg[:id]}: No se encontraron archivos .log en '#{dir_origen}'." log :warn, msg puts "#{ICONO_WARN} #{msg}" sleep 5 $estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: 0, estado: "OK (Sin archivos)" } return end puts "#{ICONO_INFO} Encontrados #{archivos_log.length} set(s) de backup Proxmox a procesar." # 4. --- Iterar y procesar cada set --- archivos_log.each do |ruta_log| nombre_base_log = File.basename(ruta_log, '.log') begin archivos_fuente_del_backup = Dir.glob(File.join(dir_origen, "#{nombre_base_log}*")) ruta_notes = archivos_fuente_del_backup.find { |f| f.end_with?('.notes') } nombre_vm = nil if ruta_notes nombre_vm = File.read(ruta_notes).strip end if nombre_vm.nil? || nombre_vm.empty? # Fallback 1: Extraer el nombre de la VM desde dentro del propio archivo .log begin File.foreach(ruta_log) do |line| if line =~ /INFO: VM Name:\s*(.+)$/ nombre_vm = $1.strip log :info, "#{tarea_cfg[:id]}: Nombre '#{nombre_vm}' extraído del log '#{nombre_base_log}'." break end end rescue StandardError => e log :warn, "#{tarea_cfg[:id]}: Error leyendo log para extraer VM Name: #{e.message}" end end if nombre_vm.nil? || nombre_vm.empty? # Fallback 2: extraemos el ID de la VM del nombre de archivo (ej. vzdump-qemu-101-...) regex_match = nombre_base_log.match(/vzdump-(?:qemu|lxc)-(\d+)-/) if regex_match nombre_vm = "VM-#{regex_match[1]}" log :info, "#{tarea_cfg[:id]}: Sin .notes ni nombre en log para '#{nombre_base_log}'. Respaldo asignado a: #{nombre_vm}" else log :warn, "#{tarea_cfg[:id]}: No se pudo determinar nombre de VM para '#{nombre_base_log}'. Saltando." next end end nombre_final_tar = "#{nombre_vm}_#{File.mtime(ruta_log).strftime('%Y%m%d_%H%M%S')}.tar.gz" dir_destino_vm = File.join(dir_destino_base, nombre_vm) FileUtils.mkdir_p(dir_destino_vm) ruta_salida_tar = File.join(dir_destino_vm, nombre_final_tar) if File.exist?(ruta_salida_tar) if sobrescribir_destino msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_tar)}' existe. Se sobrescribirá." log :info, msg puts "#{ICONO_WARN} #{msg}" else msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_tar)}' existe. No se sobrescribe. Saltando." log :warn, msg puts "#{ICONO_WARN} #{msg}" next end end # --- Realizar compresión --- puts "#{CIAN}→ Comprimiendo set #{NEGRITA}#{nombre_base_log}#{RESET} (VM: #{nombre_vm}) a #{File.basename(ruta_salida_tar)}..." log :info, "#{tarea_cfg[:id]}: Comprimiendo Proxmox set '#{nombre_vm}' (base: #{nombre_base_log})" nombres_para_tar = archivos_fuente_del_backup.map { |f| File.basename(f) } if @pv_disponible cmd_tar = ['tar', '-cf', '-', '-C', dir_origen] + nombres_para_tar cmd_pv = ['pv', '-s', archivos_fuente_del_backup.sum { |f| File.size(f) rescue 0 }.to_s] cmd_gzip = ['gzip'] File.open(ruta_salida_tar, 'wb') do |f_out| Open3.pipeline(cmd_tar, cmd_pv, cmd_gzip, out: f_out).each_with_index do |status, i| unless status.success? cmd_name = ['tar', 'pv', 'gzip'][i] raise "Error en compresión (falló #{cmd_name} con código: #{status.exitstatus})" end end end else archivos_escapados = nombres_para_tar.map { |f| Shellwords.escape(f) }.join(' ') cmd_tar_directo = "tar -czf #{Shellwords.escape(ruta_salida_tar)} -C #{Shellwords.escape(dir_origen)} #{archivos_escapados}" system(cmd_tar_directo) raise "Error en compresión (falló tar con código: #{$?.exitstatus})" unless $?.success? end # --- Finalizar --- log :info, "#{tarea_cfg[:id]}: Compresión OK → #{ruta_salida_tar}" puts "\n#{ICONO_OK} Compresión OK: #{File.basename(ruta_salida_tar)}" if eliminar_original log :info, "#{tarea_cfg[:id]}: Eliminando fuentes Proxmox '#{nombre_base_log}'..." archivos_fuente_del_backup.each { |f| FileUtils.rm_f(f) } end backups_procesados_llamada += 1 rescue StandardError => e err_msg = "#{tarea_cfg[:id]}: Error procesando set '#{nombre_base_log}': #{e.message}" log :error, err_msg puts "\n#{ICONO_ERROR} #{err_msg}" sleep 5 FileUtils.rm_f(ruta_salida_tar) if ruta_salida_tar next end end # 5. --- Reportar estadísticas finales --- $estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: backups_procesados_llamada, estado: "OK" } log :info, "#{tarea_cfg[:id]}: #{backups_procesados_llamada} sets Proxmox procesados." puts "#{ICONO_OK} Tarea '#{tarea_cfg[:menu_texto]}' completada. Procesados: #{backups_procesados_llamada}." sleep 1.5 ensure # Siempre desmontar aunque haya error desmontar_rclone(dir_origen, tarea_cfg[:id]) end end