diff --git a/bitacoras/2026-02-28.md b/bitacoras/2026-02-28.md index ee9a2530..86284aeb 100644 --- a/bitacoras/2026-02-28.md +++ b/bitacoras/2026-02-28.md @@ -32,6 +32,13 @@ | :--- | :--- | | βœ… 08:34 | (IA) πŸ” RefactorizaciΓ³n completada. Nuevo script unificado con subcomandos (`abrir`, `get`, `set`, `encrypt`, `decrypt`, `list`, `rm`). BΓ³veda migrada. ADN actualizado. Scripts legacy eliminados. `[FΓ­sico: 0:30 hs]` | +#### βœ… - BKP01 - RefactorizaciΓ³n ns8-bkps +βš™οΈ RefactorizaciΓ³n de `dtic-BKPs` v5.5.1 a `ns8-bkps` v6.0 con arquitectura modular. + +| Tiempo | DescripciΓ³n | +| :--- | :--- | +| βœ… 08:57 | (IA) βš™οΈ RefactorizaciΓ³n de `dtic-BKPs` (7 archivos) β†’ `ns8-bkps` (8 archivos, modular). Config YAML reemplaza `load .rb`. Procesadores como clases (`Proxmox`, `Xen`, `Rclone`). Se eliminΓ³ loop subdirectories-first en rclone (redundante con `--delete-before`). Verificado: `--help` y `list` OK. `[FΓ­sico: 0:23 hs]` | + ### Operaciones Centrales #### πŸ‘οΈ - SINC01 - SincronizaciΓ³n de Inicio de Jornada diff --git a/tools/ns8-bkps/lib/config.rb b/tools/ns8-bkps/lib/config.rb new file mode 100644 index 00000000..206f0feb --- /dev/null +++ b/tools/ns8-bkps/lib/config.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# ========================================================== +# ns8-bkps/lib/config.rb β€” Carga y guardado de config YAML +# ========================================================== + +require 'yaml' +require 'fileutils' + +module NS8BKPs + class Config + attr_reader :tareas, :comandos, :auto_avanzar + + def initialize(path) + @path = path + reload! + end + + def reload! + unless File.exist?(@path) + @tareas = [] + @comandos = [] + @auto_avanzar = true + return + end + + data = YAML.safe_load(File.read(@path), permitted_classes: [Symbol], symbolize_names: true) || {} + @auto_avanzar = data[:auto_avanzar] != false + @tareas = (data[:tareas] || []).map { |t| simbolizar(t) } + @comandos = (data[:comandos] || []).map { |c| simbolizar(c) } + end + + def find_tarea(id) + @tareas.find { |t| t[:id].to_s == id.to_s } + end + + def save! + data = { + 'auto_avanzar' => @auto_avanzar, + 'tareas' => @tareas.map { |t| stringify(t) }, + 'comandos' => @comandos.map { |c| stringify(c) } + } + File.write(@path, YAML.dump(data)) + FileUtils.chmod(0644, @path) + end + + def add_tarea(tarea) + @tareas << simbolizar(tarea) + end + + def remove_tarea(index) + @tareas.delete_at(index) + end + + def add_comando(comando) + @comandos << simbolizar(comando) + end + + def remove_comando(index) + @comandos.delete_at(index) + end + + private + + def simbolizar(hash) + hash.transform_keys(&:to_sym) + end + + def stringify(hash) + hash.transform_keys(&:to_s).tap do |h| + h['tareas'] = h['tareas'].map(&:to_s) if h['tareas'] + end + end + end +end diff --git a/tools/ns8-bkps/lib/logger.rb b/tools/ns8-bkps/lib/logger.rb new file mode 100644 index 00000000..39cda35c --- /dev/null +++ b/tools/ns8-bkps/lib/logger.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# ========================================================== +# ns8-bkps/lib/logger.rb β€” Logger con colores y rotaciΓ³n +# ========================================================== + +require 'logger' +require 'fileutils' + +module NS8BKPs + # Colores ANSI + module C + RESET = "\e[0m" + BOLD = "\e[1m" + DIM = "\e[2m" + RED = "\e[91m" + GREEN = "\e[92m" + YELLOW = "\e[93m" + CYAN = "\e[96m" + end + + # Iconos semΓ‘nticos + OK = "#{C::GREEN}βœ”#{C::RESET}" + ERR = "#{C::RED}βœ–#{C::RESET}" + WARN = "#{C::YELLOW}⚠#{C::RESET}" + INFO = "#{C::CYAN}β„Ή#{C::RESET}" + + class AppLogger + def initialize(log_dir) + FileUtils.mkdir_p(log_dir) + log_path = File.join(log_dir, 'ns8-bkps.log') + @file_logger = Logger.new(log_path, 'daily') + @file_logger.level = Logger::INFO + @file_logger.formatter = proc { |sev, dt, _, msg| + "[#{dt.strftime('%Y-%m-%d %H:%M:%S')}] [#{sev}] #{msg}\n" + } + end + + def info(msg) + @file_logger.info(strip_ansi(msg)) + puts "#{INFO} #{msg}" + end + + def warn(msg) + @file_logger.warn(strip_ansi(msg)) + puts "#{WARN} #{msg}" + end + + def error(msg) + @file_logger.error(strip_ansi(msg)) + puts "#{ERR} #{msg}" + end + + def ok(msg) + @file_logger.info(strip_ansi(msg)) + puts "#{OK} #{msg}" + end + + def titulo(msg) + @file_logger.info(strip_ansi(msg)) + puts "\n#{C::BOLD}#{C::CYAN}--- #{msg} ---#{C::RESET}" + end + + def paso(msg) + @file_logger.info(strip_ansi(msg)) + puts "#{C::CYAN}β†’ #{msg}#{C::RESET}" + end + + private + + def strip_ansi(s) + s.to_s.gsub(/\e\[[\d;]*[mK]/, '') + end + end +end diff --git a/tools/ns8-bkps/lib/menu.rb b/tools/ns8-bkps/lib/menu.rb new file mode 100644 index 00000000..fe490f28 --- /dev/null +++ b/tools/ns8-bkps/lib/menu.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +# ========================================================== +# ns8-bkps/lib/menu.rb β€” MenΓΊ TUI interactivo + editor CRUD +# ========================================================== + +module NS8BKPs + class Menu + def initialize(config, dispatcher, log) + @config = config + @dispatcher = dispatcher + @log = log + end + + def ejecutar + loop do + system('clear') || system('cls') + mostrar_encabezado + opciones = construir_opciones + + opciones.each { |op| puts op[:linea] } + puts "#{C::CYAN}────────────────────────#{C::RESET}" + + print "\n#{C::YELLOW}Selecciona > #{C::RESET}" + $stdout.flush + entrada = $stdin.gets&.chomp&.strip&.upcase + next if entrada.nil? || entrada.empty? + + op = opciones.find { |o| o[:id] == entrada } + unless op + @log.error("OpciΓ³n no vΓ‘lida: #{entrada}") + sleep 1 + next + end + + case op[:tipo] + when :tarea + @dispatcher.ejecutar_tarea(op[:config]) + when :comando + @dispatcher.ejecutar_comando(op[:config]) + when :editor + editor_interactivo + @config.reload! + when :salir + puts "Saliendo..." + exit 0 + end + + unless [:editor, :salir].include?(op[:tipo]) || @config.auto_avanzar + print "\nPresiona Enter para continuar..." + $stdin.gets + end + end + end + + private + + def mostrar_encabezado + puts "#{C::BOLD}#{C::CYAN}╔══════════════════════════════════════════╗#{C::RESET}" + puts "#{C::BOLD}#{C::CYAN}β•‘ ns8-bkps β€” Procesador de Backups v6.0 β•‘#{C::RESET}" + puts "#{C::BOLD}#{C::CYAN}β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•#{C::RESET}" + end + + def construir_opciones + opciones = [] + + puts "\n#{C::BOLD}Tareas Individuales:#{C::RESET}" + @config.tareas.each_with_index do |t, i| + id = "T#{i + 1}" + opciones << { id: id, tipo: :tarea, config: t, linea: " #{C::YELLOW}#{id}.#{C::RESET} #{t[:texto]}" } + end + + puts "\n#{C::BOLD}Comandos (Secuencias):#{C::RESET}" + @config.comandos.each_with_index do |c, i| + id = "C#{i + 1}" + opciones << { id: id, tipo: :comando, config: c, linea: " #{C::YELLOW}#{id}.#{C::RESET} #{c[:texto]}" } + end + + puts "\n#{C::CYAN}────────────────────────#{C::RESET}" + opciones << { id: 'E', tipo: :editor, linea: " #{C::YELLOW}E.#{C::RESET} Editar tareas y comandos" } + opciones << { id: 'S', tipo: :salir, linea: " #{C::YELLOW}S.#{C::RESET} Salir" } + + opciones.each { |op| puts op[:linea] if op[:tipo].is_a?(Symbol) && [:editor, :salir].include?(op[:tipo]) } + opciones + end + + # ─── Editor CRUD ────────────────────────────────────────────── + + def editor_interactivo + loop do + system('clear') || system('cls') + puts "#{C::BOLD}#{C::CYAN}--- Editor de Tareas y Comandos ---#{C::RESET}\n\n" + + puts "#{C::BOLD}Tareas:#{C::RESET}" + @config.tareas.each_with_index do |t, i| + puts " #{C::YELLOW}T#{i+1}.#{C::RESET} #{t[:texto]} #{C::DIM}(#{t[:id]})#{C::RESET}" + end + + puts "\n#{C::BOLD}Comandos:#{C::RESET}" + @config.comandos.each_with_index do |c, i| + puts " #{C::YELLOW}C#{i+1}.#{C::RESET} #{c[:texto]} #{C::DIM}(#{c[:id]}, tareas: #{c[:tareas]&.join(', ')})#{C::RESET}" + end + + puts "\n#{C::CYAN}[Tx]=Editar, [NT]=Nueva Tarea, [BT]=Borrar Tarea, [G]=Guardar y Volver#{C::RESET}" + print "> " + $stdout.flush + op = $stdin.gets&.chomp&.strip&.downcase + return if op.nil? + + case op + when /^t(\d+)$/ + idx = $1.to_i - 1 + editar_tarea(idx) if @config.tareas[idx] + when 'nt' + nueva_tarea + when 'bt' + print "NΓΊmero a borrar (ej: 1): " + idx = $stdin.gets&.chomp.to_i - 1 + @config.remove_tarea(idx) if idx >= 0 && @config.tareas[idx] + when 'g' + @config.save! + @log.ok("ConfiguraciΓ³n guardada.") + sleep 1 + return + end + end + end + + def editar_tarea(idx) + tarea = @config.tareas[idx] + puts "\n#{C::CYAN}Editando: #{tarea[:texto]}#{C::RESET}" + puts "#{C::DIM}(Dejar vacΓ­o = mantener valor actual)#{C::RESET}\n" + + tarea.each do |k, v| + next if k == :id + tipo_hint = [true, false].include?(v) ? " (true/false)" : "" + print " #{C::YELLOW}#{k}#{C::RESET}#{tipo_hint} [#{v}]: " + $stdout.flush + nuevo = $stdin.gets&.chomp&.strip + next if nuevo.nil? || nuevo.empty? + + tarea[k] = case v + when Symbol then nuevo.to_sym + when TrueClass, FalseClass then nuevo.downcase == 'true' + else nuevo + end + end + @log.ok("Tarea actualizada.") + end + + def nueva_tarea + tarea = { + id: "tarea_#{Time.now.to_i}", + texto: "Nueva Tarea", + tipo: "rclone", + origen: "", + destino: "", + eliminar_origen: false, + sobrescribir: false + } + editar_tarea_hash(tarea) + @config.add_tarea(tarea) + end + + def editar_tarea_hash(tarea) + tarea.each do |k, v| + print " #{C::YELLOW}#{k}#{C::RESET} [#{v}]: " + $stdout.flush + nuevo = $stdin.gets&.chomp&.strip + tarea[k] = nuevo unless nuevo.nil? || nuevo.empty? + end + end + end +end diff --git a/tools/ns8-bkps/lib/proc_proxmox.rb b/tools/ns8-bkps/lib/proc_proxmox.rb new file mode 100644 index 00000000..0d766cb6 --- /dev/null +++ b/tools/ns8-bkps/lib/proc_proxmox.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +# ========================================================== +# ns8-bkps/lib/proc_proxmox.rb β€” Procesador de backups Proxmox +# ========================================================== + +require 'tmpdir' +require 'shellwords' +require 'open3' +require 'fileutils' + +module NS8BKPs + module Procesador + class Proxmox + def initialize(log) + @log = log + @pv = system('which pv > /dev/null 2>&1') + end + + def ejecutar(tarea) + @log.titulo(tarea[:texto]) + @log.info("#{tarea[:id]}: Origen remoto: #{tarea[:origen]}") + + dir_tmp = montar(tarea[:origen], tarea[:id]) + return false unless dir_tmp + + procesados = 0 + + begin + logs = Dir.glob(File.join(dir_tmp, '*.log')) + if logs.empty? + @log.warn("#{tarea[:id]}: Sin archivos .log en origen.") + return true + end + + @log.info("Encontrados #{logs.length} set(s) de backup Proxmox.") + + logs.each do |ruta_log| + base = File.basename(ruta_log, '.log') + begin + fuentes = Dir.glob(File.join(dir_tmp, "#{base}*")) + nombre_vm = extraer_nombre_vm(fuentes, ruta_log, base, tarea[:id]) + next unless nombre_vm + + nombre_tar = "#{nombre_vm}_#{File.mtime(ruta_log).strftime('%Y%m%d_%H%M%S')}.tar.gz" + dir_dest = File.join(tarea[:destino], nombre_vm) + FileUtils.mkdir_p(dir_dest) + ruta_tar = File.join(dir_dest, nombre_tar) + + if File.exist?(ruta_tar) + if tarea[:sobrescribir] + @log.warn("#{File.basename(ruta_tar)} existe. Sobrescribiendo.") + else + @log.warn("#{File.basename(ruta_tar)} existe. Saltando.") + next + end + end + + @log.paso("Comprimiendo #{base} (VM: #{nombre_vm})") + comprimir(dir_tmp, fuentes, ruta_tar) + @log.ok("CompresiΓ³n OK: #{File.basename(ruta_tar)}") + + if tarea[:eliminar_origen] + fuentes.each { |f| FileUtils.rm_f(f) } + @log.info("Fuentes eliminadas de origen.") + end + + procesados += 1 + rescue => e + @log.error("#{tarea[:id]}: Error procesando '#{base}': #{e.message}") + FileUtils.rm_f(ruta_tar) if ruta_tar && File.exist?(ruta_tar) + end + end + + @log.ok("#{procesados} sets Proxmox procesados.") + true + ensure + desmontar(dir_tmp, tarea[:id]) + end + end + + private + + def extraer_nombre_vm(fuentes, ruta_log, base, tarea_id) + # 1. Intentar .notes + notes = fuentes.find { |f| f.end_with?('.notes') } + nombre = notes ? File.read(notes).strip : nil + + # 2. Intentar desde el .log (INFO: VM Name: ...) + if nombre.nil? || nombre.empty? + File.foreach(ruta_log) do |line| + if line =~ /INFO: VM Name:\s*(.+)$/ + nombre = $1.strip + break + end + end rescue nil + end + + # 3. Fallback: ID de VM del nombre de archivo + if nombre.nil? || nombre.empty? + match = base.match(/vzdump-(?:qemu|lxc)-(\d+)-/) + nombre = match ? "VM-#{match[1]}" : nil + end + + @log.warn("#{tarea_id}: Sin nombre de VM para '#{base}'. Saltando.") unless nombre + nombre + end + + def comprimir(dir_origen, fuentes, ruta_salida) + nombres = fuentes.map { |f| File.basename(f) } + if @pv + cmd_tar = ['tar', '-cf', '-', '-C', dir_origen] + nombres + cmd_pv = ['pv', '-s', fuentes.sum { |f| File.size(f) rescue 0 }.to_s] + cmd_gz = ['gzip'] + File.open(ruta_salida, 'wb') do |f| + Open3.pipeline(cmd_tar, cmd_pv, cmd_gz, out: f).each_with_index do |s, i| + raise "FallΓ³ #{%w[tar pv gzip][i]} (#{s.exitstatus})" unless s.success? + end + end + else + args = nombres.map { |f| Shellwords.escape(f) }.join(' ') + cmd = "tar -czf #{Shellwords.escape(ruta_salida)} -C #{Shellwords.escape(dir_origen)} #{args}" + system(cmd) or raise "tar fallΓ³ (#{$?.exitstatus})" + end + end + + def montar(remoto, tarea_id) + dir = Dir.mktmpdir("bkps_#{tarea_id}_") + @log.info("Montando #{remoto} en #{dir}") + pid = spawn("rclone mount #{Shellwords.escape(remoto)} #{dir} --daemon --vfs-cache-mode writes") + Process.detach(pid) + sleep 2 + + if Dir.exist?(dir) && !(Dir.entries(dir) - %w[. ..]).empty? + dir + else + @log.error("Error montando #{remoto}") + nil + end + end + + def desmontar(dir, tarea_id) + return unless dir && Dir.exist?(dir) + @log.info("Desmontando #{dir}") + system("fusermount -u #{dir} 2>/dev/null || umount #{dir} 2>/dev/null") + FileUtils.remove_entry(dir) rescue nil + end + end + end +end diff --git a/tools/ns8-bkps/lib/proc_xen.rb b/tools/ns8-bkps/lib/proc_xen.rb new file mode 100644 index 00000000..e62dc2cb --- /dev/null +++ b/tools/ns8-bkps/lib/proc_xen.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +# ========================================================== +# ns8-bkps/lib/proc_xen.rb β€” Procesador de backups XEN (.xva) +# ========================================================== + +require 'shellwords' +require 'open3' +require 'fileutils' + +module NS8BKPs + module Procesador + class Xen + def initialize(log) + @log = log + @pv = system('which pv > /dev/null 2>&1') + end + + def ejecutar(tarea) + @log.titulo(tarea[:texto]) + @log.info("#{tarea[:id]}: Origen: #{tarea[:origen]}") + + archivos = Dir.glob(File.join(tarea[:origen], '*.xva')) + if archivos.empty? + @log.warn("#{tarea[:id]}: Sin archivos .xva en origen.") + return true + end + + @log.info("Encontrados #{archivos.length} archivo(s) .xva.") + procesados = 0 + + archivos.each do |ruta_xva| + nombre_xva = File.basename(ruta_xva) + nombre_vm = nombre_xva.split('_').first + dir_dest = File.join(tarea[:destino], nombre_vm) + FileUtils.mkdir_p(dir_dest) + + nombre_sin_ext = File.basename(nombre_xva, '.xva') + ruta_tar = File.join(dir_dest, "#{nombre_sin_ext}.tar.gz") + + if File.exist?(ruta_tar) + if tarea[:sobrescribir] + @log.warn("#{File.basename(ruta_tar)} existe. Sobrescribiendo.") + else + @log.warn("#{File.basename(ruta_tar)} existe. Saltando.") + next + end + end + + begin + @log.paso("Comprimiendo #{nombre_xva}") + + if @pv + cmd_tar = ['tar', '-czf', '-', '-C', tarea[:origen], nombre_xva] + cmd_pv = ['pv', '-s', File.size(ruta_xva).to_s] + File.open(ruta_tar, 'wb') do |f| + Open3.pipeline(cmd_tar, cmd_pv, out: f).each_with_index do |s, i| + raise "FallΓ³ #{%w[tar pv][i]} (#{s.exitstatus})" unless s.success? + end + end + else + cmd = "tar -czf #{Shellwords.escape(ruta_tar)} -C #{Shellwords.escape(tarea[:origen])} #{Shellwords.escape(nombre_xva)}" + system(cmd) or raise "tar fallΓ³ (#{$?.exitstatus})" + end + + @log.ok("CompresiΓ³n OK: #{File.basename(ruta_tar)}") + + if tarea[:eliminar_origen] + FileUtils.rm_f(ruta_xva) + @log.info("XVA original eliminado.") + end + + procesados += 1 + rescue => e + @log.error("#{tarea[:id]}: Error comprimiendo #{nombre_xva}: #{e.message}") + FileUtils.rm_f(ruta_tar) + end + end + + @log.ok("#{procesados} archivos .xva procesados.") + true + end + end + end +end diff --git a/tools/ns8-bkps/lib/sync_rclone.rb b/tools/ns8-bkps/lib/sync_rclone.rb new file mode 100644 index 00000000..a2473d19 --- /dev/null +++ b/tools/ns8-bkps/lib/sync_rclone.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# ========================================================== +# ns8-bkps/lib/sync_rclone.rb β€” SincronizaciΓ³n rclone +# ========================================================== +# +# NOTA: Se eliminΓ³ la estrategia subdirectories-first que existΓ­a +# en dtic-BKPs. Con --delete-before rclone ya libera el espacio +# remoto antes de transferir, haciendo innecesario el loop por +# subdirectorios. El sync root ΓΊnico es suficiente y mΓ‘s eficiente. + +require 'shellwords' + +module NS8BKPs + module Procesador + class Rclone + RCLONE_OPTS = %w[ + --delete-before + --retries 5 + --retries-sleep 10s + --timeout 30m + --contimeout 10m + --tpslimit 10 + --progress + -v + ].join(' ').freeze + + def initialize(log) + @log = log + end + + def ejecutar(tarea) + @log.titulo(tarea[:texto]) + origen = tarea[:origen] + destino = tarea[:destino] + + @log.paso("Origen: #{origen}") + @log.paso("Destino: #{destino}") + + cmd = "rclone sync #{Shellwords.escape(origen)} #{Shellwords.escape(destino)} #{RCLONE_OPTS}" + @log.info("#{tarea[:id]}: Ejecutando β†’ #{cmd}") + + exito = system(cmd) + codigo = $?.exitstatus + + if exito + @log.ok("SincronizaciΓ³n rclone completada.") + else + @log.error("#{tarea[:id]}: Fallo en sincronizaciΓ³n (cΓ³digo: #{codigo}).") + end + + exito + end + end + end +end diff --git a/tools/ns8-bkps/ns8-bkps.rb b/tools/ns8-bkps/ns8-bkps.rb new file mode 100755 index 00000000..a2441a24 --- /dev/null +++ b/tools/ns8-bkps/ns8-bkps.rb @@ -0,0 +1,170 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# ========================================================== +# ns8-bkps β€” Procesador de Backups Automatizado v6.0 +# ---------------------------------------------------------- +# RefactorizaciΓ³n de dtic-BKPs v5.5.1 +# AUTOR: Ricardo MONLA (rmonla@) +# ========================================================== +# +# Uso: +# ruby ns8-bkps.rb # MenΓΊ interactivo (auto-screen) +# ruby ns8-bkps.rb T1 # Ejecutar tarea 1 directamente +# ruby ns8-bkps.rb C1 # Ejecutar comando 1 directamente +# ruby ns8-bkps.rb list # Listar tareas y comandos +# ruby ns8-bkps.rb --help # Ayuda + +require 'shellwords' + +# ─── Rutas ─────────────────────────────────────────────────── +APP_DIR = File.dirname(File.expand_path(__FILE__)) +LIB_DIR = File.join(APP_DIR, 'lib') + +require_relative 'lib/logger' +require_relative 'lib/config' +require_relative 'lib/proc_proxmox' +require_relative 'lib/proc_xen' +require_relative 'lib/sync_rclone' +require_relative 'lib/menu' + +include NS8BKPs + +# ─── Dispatcher ────────────────────────────────────────────── +class Dispatcher + TIPOS = { + 'proxmox' => -> (log) { Procesador::Proxmox.new(log) }, + 'xva' => -> (log) { Procesador::Xen.new(log) }, + 'rclone' => -> (log) { Procesador::Rclone.new(log) }, + }.freeze + + def initialize(config, log) + @config = config + @log = log + end + + def ejecutar_tarea(tarea) + tipo = tarea[:tipo].to_s + factory = TIPOS[tipo] + unless factory + @log.error("Tipo desconocido: #{tipo}") + return + end + procesador = factory.call(@log) + procesador.ejecutar(tarea) + end + + def ejecutar_comando(comando) + @log.titulo(comando[:texto]) + tareas_ids = comando[:tareas] || [] + tareas_ids.each_with_index do |id, i| + tarea = @config.find_tarea(id) + if tarea + @log.paso("Paso #{i + 1}/#{tareas_ids.length}: #{tarea[:texto]}") + ejecutar_tarea(tarea) + else + @log.error("Tarea '#{id}' no encontrada en comando '#{comando[:id]}'.") + end + end + @log.ok("Comando '#{comando[:texto]}' completado.") + end +end + +# ─── Auto-Screen Wrapper ───────────────────────────────────── +def auto_screen! + return if ENV['TERM']&.start_with?('screen') || ENV['STY'] + puts "\n#{C::CYAN}β„Ή Relanzando en sesiΓ³n screen para permitir desatenciΓ³n...#{C::RESET}" + + cmd = ([$PROGRAM_NAME] + ARGV).map { |a| Shellwords.escape(a) }.join(' ') + nombre = "ns8bkps_#{Time.now.strftime('%H%M%S')}" + + puts "Para reconectarte: #{C::YELLOW}screen -r #{nombre}#{C::RESET}" + sleep 1 + + system('screen', '-dmS', nombre, 'bash', '-c', "#{cmd}; echo ''; read -p 'Enter para cerrar...'") + exit 0 +end + +# ─── Ctrl+C Handler ────────────────────────────────────────── +Signal.trap('INT') do + puts "\n#{C::RED}Ctrl+C β€” Saliendo...#{C::RESET}" + exit 130 +end + +# ─── Ayuda ─────────────────────────────────────────────────── +def mostrar_ayuda(config) + puts <<~HELP + #{C::BOLD}#{C::CYAN}ns8-bkps β€” Procesador de Backups v6.0#{C::RESET} + + #{C::YELLOW}Uso:#{C::RESET} + ruby ns8-bkps.rb MenΓΊ interactivo + ruby ns8-bkps.rb T Ejecutar tarea N + ruby ns8-bkps.rb C Ejecutar comando N + ruby ns8-bkps.rb list Listar tareas/comandos + ruby ns8-bkps.rb --help Esta ayuda + + #{C::YELLOW}Tareas configuradas:#{C::RESET} + HELP + + config.tareas.each_with_index do |t, i| + puts " #{C::GREEN}T#{i+1}#{C::RESET} #{t[:texto]} #{C::DIM}(#{t[:tipo]})#{C::RESET}" + end + + puts "\n #{C::YELLOW}Comandos configurados:#{C::RESET}" + config.comandos.each_with_index do |c, i| + puts " #{C::GREEN}C#{i+1}#{C::RESET} #{c[:texto]}" + end +end + +def cmd_list(config) + puts "#{C::BOLD}Tareas:#{C::RESET}" + config.tareas.each_with_index do |t, i| + puts " T#{i+1} #{t[:id]} #{t[:texto]} [#{t[:tipo]}] #{t[:origen]} β†’ #{t[:destino]}" + end + puts "\n#{C::BOLD}Comandos:#{C::RESET}" + config.comandos.each_with_index do |c, i| + puts " C#{i+1} #{c[:id]} #{c[:texto]} β†’ #{c[:tareas]&.join(', ')}" + end +end + +# ─── Main ──────────────────────────────────────────────────── + +config_path = File.join(APP_DIR, 'ns8-bkps.yml') +log = AppLogger.new(File.join(APP_DIR, 'logs')) +config = Config.new(config_path) +dispatcher = Dispatcher.new(config, log) + +arg = ARGV[0]&.strip + +case arg +when nil + # MenΓΊ interactivo con auto-screen + auto_screen! + log.info("===== ns8-bkps v6.0 iniciado (Screen) =====") + Menu.new(config, dispatcher, log).ejecutar + +when '--help', '-h', 'help' + mostrar_ayuda(config) + +when 'list' + cmd_list(config) + +when /^[TC]\d+$/i + # EjecuciΓ³n directa desde CLI + tipo = arg[0].upcase + idx = arg[1..].to_i - 1 + + if tipo == 'T' && config.tareas[idx] + dispatcher.ejecutar_tarea(config.tareas[idx]) + elsif tipo == 'C' && config.comandos[idx] + dispatcher.ejecutar_comando(config.comandos[idx]) + else + puts "#{ERR} OpciΓ³n no vΓ‘lida: #{arg}" + exit 1 + end + +else + puts "#{ERR} Comando desconocido: #{arg}" + mostrar_ayuda(config) + exit 1 +end diff --git a/tools/ns8-bkps/ns8-bkps.yml b/tools/ns8-bkps/ns8-bkps.yml new file mode 100644 index 00000000..c3050d1b --- /dev/null +++ b/tools/ns8-bkps/ns8-bkps.yml @@ -0,0 +1,70 @@ +# ConfiguraciΓ³n ns8-bkps β€” Tareas y Comandos de Backup +# Migrado desde dtic-BKPs_tasks.rb + +auto_avanzar: true + +tareas: + - id: proc_syncXen1 + texto: "βš™οΈ Procesar archivos de BKPs de VMs: syncs-XEN01" + tipo: xva + origen: "/mnt/ns8Disco2/syncs-XEN01/" + destino: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/" + eliminar_origen: true + sobrescribir: false + + - id: proc_syncPmox + texto: "βš™οΈ Procesar archivos de BKPs de VMs desde zfsDISCO1" + tipo: proxmox + origen: "pve_PMOX3:/mnt/pve/zfsDISCO1/dump/" + destino: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/" + eliminar_origen: false + sobrescribir: false + + - id: sinc_pmox_ns8 + texto: "πŸ“₯ Descargar archivos de BKPs de VMs: zfsDISCO1" + tipo: rclone + origen: "pve_PMOX3:/mnt/pve/zfsDISCO1/dump/" + destino: "/mnt/ns8Disco2/syncs-PMOX/" + eliminar_origen: false + sobrescribir: false + + - id: sync_BkpAntiguos_nube + texto: "☁️ Upload de BKPs a rmOneDrive: bkps-ANTIGUOS" + tipo: rclone + origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-ANTIGUOS/" + destino: "rmOneDrive:/dtic-BACKUPS/bkps-ANTIGUOS/" + eliminar_origen: false + sobrescribir: false + + - id: sync_BkpsSERVIDORes_nube + texto: "☁️ Upload de bkps-SERVIDORes a rmOneDrive" + tipo: rclone + origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/" + destino: "rmOneDrive:/dtic-BACKUPS/bkps-SERVIDORes/" + eliminar_origen: false + sobrescribir: false + + - id: sync_BkpUsers_nube + texto: "☁️ Upload de bkps-USERs a rmOneDrive" + tipo: rclone + origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-USERs/" + destino: "rmOneDrive:/dtic-BACKUPS/bkps-USERs/" + eliminar_origen: false + sobrescribir: false + +comandos: + - id: descargar_y_procesar_pmox + texto: "πŸ“₯βš™οΈ Descargar y Procesar BKPs de PMOXs" + tareas: [sinc_pmox_ns8, proc_syncPmox] + + - id: full_procesamiento + texto: "βš™οΈ Procesar TODOS los BKPs de NS8" + tareas: [proc_syncXen1, proc_syncPmox] + + - id: full_upload + texto: "πŸš€ Subir a la Nube todos los BKPs" + tareas: [sync_BkpAntiguos_nube, sync_BkpsSERVIDORes_nube, sync_BkpUsers_nube] + + - id: full_full + texto: "πŸš€ Procesar todo y Subir todo" + tareas: [proc_syncXen1, proc_syncPmox, sync_BkpsSERVIDORes_nube, sync_BkpAntiguos_nube, sync_BkpUsers_nube]