refactor(ambitos): Migración completa dtic-DASUTEN (A04) y dtic-BKPs (A03) + herramientas ADN
Migración dtic-DASUTEN a estándar A04: - A04_dtic-DASUTEN.md: Manifiesto creado - A04.P001 a A04.P005: Planes renombrados de P2601.* - _hist/_hist_P2601_dasuten.md: Legacy archivado Migración dtic-BKPs a estándar A03: - A03_dtic-BKPs.md: Manifiesto creado - A03.P001, A03.P002: Planes migrados - docs/ambito/dtic-BKPs/: Nueva estructura Herramientas ADN nuevas: - adn/tools/bkps/: BKPs migrado a tools ADN - adn/tools/cli/dron.rb: Vigía de tareas largas - adn/tools/cli/copiloto.rb: Asistente CLI - adn/tools/cli/novato.rb: CLI de aprendizaje Limpieza: - dtic-BKPs/ → _hist/dtic-BKPs/ (legacy archivado) - adn/tools/cli/backup.rb → _hist/ (migrado a bkps.rb) Otros: - nodos/pcv-dasu1.md, srvv-data.md, srvv-nginx-rm.md: Actualizados - adn/tools/core/conciliador.rb: Mejoras - adn/tools/w-zombi/data/cmd.json: Actualizado Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
018e45e046
commit
1802c25194
Executable
+170
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs — Procesador de Backups Automatizado v6.0
|
||||
# ----------------------------------------------------------
|
||||
# Refactorización de dtic-BKPs v5.5.1
|
||||
# AUTOR: Ricardo MONLA (rmonla@)
|
||||
# ==========================================================
|
||||
#
|
||||
# Uso:
|
||||
# ruby dtic-BKPs.rb # Menú interactivo (auto-screen)
|
||||
# ruby dtic-BKPs.rb T1 # Ejecutar tarea 1 directamente
|
||||
# ruby dtic-BKPs.rb C1 # Ejecutar comando 1 directamente
|
||||
# ruby dtic-BKPs.rb list # Listar tareas y comandos
|
||||
# ruby dtic-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 DTICBKPs
|
||||
|
||||
# ─── 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 = "dticbkps_#{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}dtic-BKPs — Procesador de Backups v6.0#{C::RESET}
|
||||
|
||||
#{C::YELLOW}Uso:#{C::RESET}
|
||||
ruby dtic-BKPs.rb Menú interactivo
|
||||
ruby dtic-BKPs.rb T<N> Ejecutar tarea N
|
||||
ruby dtic-BKPs.rb C<N> Ejecutar comando N
|
||||
ruby dtic-BKPs.rb list Listar tareas/comandos
|
||||
ruby dtic-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, 'dtic-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("===== dtic-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
|
||||
@@ -0,0 +1,70 @@
|
||||
# Configuración dtic-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]
|
||||
@@ -0,0 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [5.5.1] - 2025-11-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed naming inconsistencies across the application, ensuring all references to the app name are correct.
|
||||
|
||||
## [5.5.0] - 2025-11-14
|
||||
|
||||
### Added
|
||||
|
||||
- Automatic execution via command-line parameters
|
||||
|
||||
## [5.4.1] - 2025-11-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- Mejora en el manejo de sesiones de screen en app-run.sh para adjuntar automáticamente a la sesión al ejecutar el script.
|
||||
|
||||
## [5.4] - 2025-11-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Modified synchronization process to execute in subdirectories before root.
|
||||
|
||||
## [5.3] - 2025-11-13
|
||||
|
||||
### Changed
|
||||
|
||||
- Reorganization of prompts files
|
||||
- Documentation updates
|
||||
@@ -0,0 +1,350 @@
|
||||
# Documentación del Proyecto dtic-BKPs
|
||||
|
||||
## Resumen del Proyecto
|
||||
|
||||
**Nombre del Proyecto:** dtic-BKPs
|
||||
**Versión:** v5.5.1
|
||||
**Autor:** Lic. Ricardo MONLA (rmonla@)
|
||||
**Organización:** Dirección de TIC, Facultad Regional La Rioja, Universidad Tecnológica Nacional
|
||||
**Licencia:** MIT License
|
||||
|
||||
### Propósito y Descripción
|
||||
|
||||
dtic-BKPs es un procesador automatizado de respaldos escrito en Ruby, diseñado para manejar el procesamiento, compresión y sincronización de respaldos de máquinas virtuales desde diversas plataformas de virtualización. El sistema proporciona un menú interactivo basado en terminal para gestionar operaciones de respaldo en entornos XenServer/XCP-ng y Proxmox VE.
|
||||
|
||||
La aplicación automatiza el flujo de trabajo de descarga de respaldos desde servidores remotos, procesándolos (compresión, organización) y subiéndolos a almacenamiento en la nube u otros destinos utilizando rclone para sincronización universal de archivos.
|
||||
|
||||
### Características Principales
|
||||
|
||||
- **Interfaz de Terminal Interactiva:** Sistema basado en menús para ejecutar tareas de respaldo
|
||||
- **Soporte Multiplataforma:** Procesamiento nativo para XenServer/XCP-ng (archivos .xva) y respaldos de Proxmox VE
|
||||
- **Sincronización Universal:** Integración con rclone para sincronizar a cualquier destino local o remoto
|
||||
- **Arquitectura Modular:** Estructura de código organizada con procesadores separados para diferentes tipos de respaldo
|
||||
- **Tareas Configurables:** Definiciones de tareas editables a través de la interfaz de la aplicación
|
||||
- **Logging Integral:** Logging detallado con retroalimentación visual e indicadores de progreso
|
||||
- **Secuencias de Comandos:** Soporte para flujos de trabajo de respaldo de múltiples pasos
|
||||
|
||||
## Estructura del Proyecto
|
||||
|
||||
### Diseño de Directorios
|
||||
|
||||
```
|
||||
dtic-BKPs/
|
||||
├── _vStable/ # Directorio de versión estable
|
||||
│ ├── dtic-BKPs_app.rb # Script principal de la aplicación
|
||||
│ ├── dtic-BKPs_tasks.rb # Archivo de configuración de tareas
|
||||
│ ├── runDticBKPs.sh # Script de ejecución
|
||||
│ ├── README.md # Documentación del proyecto
|
||||
│ ├── LICENSE # Licencia MIT
|
||||
│ ├── logs/ # Directorio de archivos de log
|
||||
│ │ └── dtic-BKPs.log # Log principal de la aplicación
|
||||
│ ├── fxs/ # Módulos de funciones
|
||||
│ │ ├── config_handler.rb # Gestión de configuración
|
||||
│ │ ├── ui_handlers.rb # Manejadores de interfaz de usuario
|
||||
│ │ ├── proc_syncPmox.rb # Procesador de respaldos Proxmox
|
||||
│ │ ├── proc_syncXEN.rb # Procesador de respaldos Xen
|
||||
│ │ └── sync_rclone.rb # Sincronización rclone
|
||||
│ └── src/ # Código fuente y documentación
|
||||
│ ├── install.sh # Script de instalación
|
||||
│ ├── project__v5.5.0_*.md # Documentación del proyecto
|
||||
│ └── core/ # Módulos principales
|
||||
│ └── globals.rb # Constantes globales y utilidades
|
||||
├── _vStable/ # Versiones estables anteriores
|
||||
└── _basurero/ # Directorio de archivos
|
||||
```
|
||||
|
||||
### Componentes Principales
|
||||
|
||||
#### Aplicación Principal (`dtic-BKPs_app.rb`)
|
||||
- Punto de entrada del procesador de respaldos
|
||||
- Sistema de menú interactivo
|
||||
- Motor de ejecución de tareas
|
||||
- Carga y gestión de configuración
|
||||
|
||||
#### Configuración de Tareas (`dtic-BKPs_tasks.rb`)
|
||||
- Define tareas individuales de respaldo
|
||||
- Contiene secuencias de comandos (flujos de trabajo de múltiples pasos)
|
||||
- Parámetros configurables para cada tipo de tarea
|
||||
|
||||
#### Módulos de Funciones (`fxs/`)
|
||||
- **config_handler.rb:** Carga y guarda configuraciones de tareas
|
||||
- **ui_handlers.rb:** Gestiona interfaz de usuario e interacciones de menú
|
||||
- **proc_syncPmox.rb:** Procesa conjuntos de respaldos de Proxmox VE
|
||||
- **proc_syncXEN.rb:** Procesa archivos .xva de XenServer/XCP-ng
|
||||
- **sync_rclone.rb:** Maneja operaciones de sincronización rclone
|
||||
|
||||
## Stack Tecnológico
|
||||
|
||||
### Tecnologías Principales
|
||||
|
||||
| Componente | Tecnología | Versión | Propósito |
|
||||
|------------|------------|---------|-----------|
|
||||
| **Lenguaje de Programación** | Ruby | 2.7+ | Lógica principal de la aplicación |
|
||||
| **Sincronización** | rclone | Última | Transferencia universal de archivos |
|
||||
| **Compresión** | tar/gzip | Sistema | Compresión de respaldos |
|
||||
| **Monitoreo de Progreso** | pv | Opcional | Visualización de progreso |
|
||||
| **Logging** | Ruby Logger | Integrado | Logging de la aplicación |
|
||||
|
||||
### Dependencias
|
||||
|
||||
#### Dependencias Requeridas
|
||||
- **Ruby 2.7+:** Entorno de ejecución principal
|
||||
- **rclone:** Herramienta universal de sincronización de archivos
|
||||
- **tar y gzip:** Utilidades de compresión (generalmente preinstaladas en Linux)
|
||||
|
||||
#### Dependencias Opcionales
|
||||
- **pv (Pipe Viewer):** Proporciona barras de progreso durante operaciones de compresión
|
||||
|
||||
#### Requisitos del Sistema
|
||||
- **Sistema Operativo:** Linux (Debian/Ubuntu recomendado)
|
||||
- **Permisos:** Acceso a directorios de origen y destino de respaldos
|
||||
- **Red:** Conectividad a orígenes remotos de respaldos (si aplica)
|
||||
- **Almacenamiento:** Espacio suficiente para procesamiento de respaldos y archivos temporales
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
### Tipos de Procesamiento de Respaldos
|
||||
|
||||
#### 1. Procesamiento XenServer/XCP-ng (`:xva`)
|
||||
- **Entrada:** Archivos de respaldo de máquinas virtuales .xva
|
||||
- **Proceso:** Comprime archivos .xva individuales en archivos .tar.gz
|
||||
- **Organización:** Agrupa respaldos por nombre de máquina virtual
|
||||
- **Salida:** Archivos comprimidos en estructura de directorios de destino
|
||||
|
||||
#### 2. Procesamiento Proxmox VE (`:proxmox_log_notes`)
|
||||
- **Entrada:** Conjuntos de respaldos de Proxmox (archivos .log, .notes y datos)
|
||||
- **Proceso:** Lee archivo .notes para nombre de MV, comprime conjunto completo de respaldo
|
||||
- **Organización:** Crea archivos con timestamp por máquina virtual
|
||||
- **Salida:** Archivos .tar.gz consolidados con nomenclatura apropiada
|
||||
|
||||
#### 3. Sincronización Universal (`:rclone_sync`)
|
||||
- **Entrada:** Cualquier directorio local o remoto
|
||||
- **Proceso:** Utiliza rclone para sincronizar archivos entre ubicaciones
|
||||
- **Destinos:** Rutas locales, almacenamiento en nube, servidores remotos
|
||||
- **Características:** Sincronización incremental, reporte de progreso, manejo de errores
|
||||
|
||||
### Gestión de Tareas
|
||||
|
||||
#### Tareas Individuales
|
||||
Cada tarea define:
|
||||
- **ID Único:** Identificador simbólico para la tarea
|
||||
- **Texto de Visualización:** Descripción amigable para el usuario
|
||||
- **Tipo de Proceso:** `:xva`, `:proxmox_log_notes`, o `:rclone_sync`
|
||||
- **Ruta de Origen:** Directorio origen (local o remoto rclone)
|
||||
- **Ruta de Destino:** Directorio destino (local o remoto rclone)
|
||||
- **Opciones:** Eliminar origen después del procesamiento, sobrescribir destino
|
||||
|
||||
#### Secuencias de Comandos
|
||||
Flujos de trabajo de múltiples pasos que ejecutan múltiples tareas en orden:
|
||||
- **Descargar y Procesar:** Sincronizar desde remoto, luego procesar respaldos
|
||||
- **Ciclo Completo de Respaldo:** Descargar → Procesar → Subir a nube
|
||||
- **Operaciones Masivas:** Procesar múltiples orígenes de respaldos
|
||||
|
||||
### Interfaz de Usuario
|
||||
|
||||
#### Menú Principal
|
||||
- **Tareas Individuales:** Ejecutar operaciones de respaldo únicas
|
||||
- **Secuencias de Comandos:** Ejecutar flujos de trabajo de respaldo de múltiples pasos
|
||||
- **Editor de Tareas:** Modificar tareas existentes o crear nuevas
|
||||
- **Salir:** Cierre limpio de la aplicación
|
||||
|
||||
#### Modo Editor de Tareas
|
||||
- **Editar Tareas:** Modificar parámetros de tareas existentes
|
||||
- **Crear Tareas:** Agregar nuevas operaciones de respaldo
|
||||
- **Eliminar Tareas:** Remover tareas no utilizadas
|
||||
- **Guardar Cambios:** Persistir modificaciones en archivo de configuración
|
||||
|
||||
### Sistema de Configuración
|
||||
|
||||
#### Archivo de Configuración de Tareas
|
||||
- **Formato:** Array Ruby de hashes de tareas
|
||||
- **Ubicación:** `dtic-BKPs_tasks.rb`
|
||||
- **Editable:** A través de interfaz de aplicación o manualmente
|
||||
- **Auto-generado:** Comentarios y formato preservados
|
||||
|
||||
#### Configuración de Runtime
|
||||
- **Avance Automático:** Opción para proceder automáticamente a través de tareas
|
||||
- **Nivel de Logging:** Verbose configurable
|
||||
- **Salida en Color:** Retroalimentación visual con colores ANSI
|
||||
|
||||
## Instalación y Configuración
|
||||
|
||||
### Proceso de Instalación
|
||||
|
||||
1. **Descarga:** Obtener todos los archivos del proyecto
|
||||
2. **Permisos:** Hacer ejecutable el script de instalación
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
3. **Ejecutar Instalador:** Ejecutar el script de instalación
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
4. **Verificación de Dependencias:** El script verifica instalación de Ruby y rclone
|
||||
5. **Configuración:** Genera configuración inicial de tareas
|
||||
|
||||
### Configuración de Tareas
|
||||
|
||||
#### Ejemplos de Configuración de Tareas
|
||||
|
||||
**Procesamiento de Respaldo Xen:**
|
||||
```ruby
|
||||
{
|
||||
id: :proc_syncXen1,
|
||||
menu_texto: "Procesar respaldos de MV Xen01",
|
||||
tipo_proceso: :xva,
|
||||
origen: "/mnt/respaldos/xen01/",
|
||||
destino: "/mnt/procesados/xen01/",
|
||||
eliminar_origen: true,
|
||||
sobrescribir_destino: false,
|
||||
}
|
||||
```
|
||||
|
||||
**Procesamiento de Respaldo Proxmox:**
|
||||
```ruby
|
||||
{
|
||||
id: :proc_syncPmox,
|
||||
menu_texto: "Procesar respaldos Proxmox",
|
||||
tipo_proceso: :proxmox_log_notes,
|
||||
origen: "servidor_pve:/var/lib/vz/dump/",
|
||||
destino: "/mnt/procesados/proxmox/",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false,
|
||||
}
|
||||
```
|
||||
|
||||
**Subida a Nube:**
|
||||
```ruby
|
||||
{
|
||||
id: :subir_nube,
|
||||
menu_texto: "Subir a almacenamiento en nube",
|
||||
tipo_proceso: :rclone_sync,
|
||||
origen: "/mnt/procesados/",
|
||||
destino: "proveedor_nube:/respaldos/",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false,
|
||||
}
|
||||
```
|
||||
|
||||
## Guía de Uso
|
||||
|
||||
### Operación Básica
|
||||
|
||||
1. **Iniciar Aplicación:**
|
||||
```bash
|
||||
./dtic-BKPs_app.rb
|
||||
```
|
||||
|
||||
2. **Seleccionar Operación:**
|
||||
- Ingresar número de tarea para ejecución individual
|
||||
- Ingresar número de comando para ejecución de secuencia
|
||||
- Presionar 'E' para modo de edición de tareas
|
||||
|
||||
3. **Monitorear Progreso:**
|
||||
- Indicadores visuales de progreso
|
||||
- Actualizaciones de estado en tiempo real
|
||||
- Logging detallado a archivo
|
||||
|
||||
### Características Avanzadas
|
||||
|
||||
#### Secuencias de Comandos
|
||||
Ejecutar flujos de trabajo complejos de respaldo:
|
||||
- **C1:** Descargar y procesar respaldos Proxmox
|
||||
- **C2:** Ciclo completo (descargar → procesar → subir)
|
||||
- **C3:** Sincronización masiva a nube
|
||||
|
||||
#### Personalización de Tareas
|
||||
Modificar tareas a través del editor:
|
||||
- Cambiar rutas de origen/destino
|
||||
- Ajustar opciones de procesamiento
|
||||
- Agregar nuevos orígenes de respaldo
|
||||
- Crear secuencias de comandos personalizadas
|
||||
|
||||
## Logging y Monitoreo
|
||||
|
||||
### Archivos de Log
|
||||
- **Log Principal:** `logs/dtic-BKPs.log`
|
||||
- **Formato:** Timestamp, severidad, mensaje
|
||||
- **Rotación:** Rotación diaria con limpieza automática
|
||||
|
||||
### Niveles de Log
|
||||
- **INFO:** Operaciones normales y completación
|
||||
- **WARN:** Problemas no críticos (archivos faltantes, etc.)
|
||||
- **ERROR:** Fallos críticos que requieren atención
|
||||
|
||||
### Características de Monitoreo
|
||||
- **Estadísticas de Ejecución:** Seguimiento de tareas completadas y rendimiento
|
||||
- **Indicadores de Progreso:** Retroalimentación visual durante operaciones largas
|
||||
- **Reportes de Error:** Mensajes de error detallados con contexto
|
||||
- **Rastro de Auditoría:** Registro completo de todas las operaciones
|
||||
|
||||
## Consideraciones de Seguridad
|
||||
|
||||
### Control de Acceso
|
||||
- **Permisos de Archivos:** Acceso apropiado a directorios de respaldo
|
||||
- **Acceso Remoto:** Configuraciones remotas rclone seguras
|
||||
- **Gestión de Credenciales:** Almacenamiento seguro de credenciales de acceso
|
||||
|
||||
### Protección de Datos
|
||||
- **Encriptación:** Usar almacenamiento remoto encriptado cuando sea posible
|
||||
- **Integridad de Respaldos:** Verificación de completitud de respaldos
|
||||
- **Logging de Acceso:** Rastro de auditoría de operaciones de respaldo
|
||||
|
||||
## Mantenimiento y Solución de Problemas
|
||||
|
||||
### Mantenimiento Regular
|
||||
- **Rotación de Logs:** Monitorear tamaños de archivos de log
|
||||
- **Respaldo de Configuración:** Preservar configuraciones de tareas
|
||||
- **Actualizaciones de Dependencias:** Mantener rclone y Ruby actualizados
|
||||
|
||||
### Problemas Comunes
|
||||
- **Dependencias Faltantes:** Ejecutar script de instalación para verificar
|
||||
- **Errores de Permisos:** Verificar permisos de acceso a directorios
|
||||
- **Problemas de Red:** Verificar conectividad a orígenes remotos
|
||||
- **Errores de Configuración:** Validar definiciones de tareas
|
||||
|
||||
### Optimización de Rendimiento
|
||||
- **Procesamiento Paralelo:** Considerar múltiples instancias para diferentes orígenes
|
||||
- **Planificación de Almacenamiento:** Asegurar espacio adecuado para procesamiento
|
||||
- **Ancho de Banda de Red:** Monitorear velocidades de transferencia para operaciones remotas
|
||||
|
||||
## Desarrollo y Extensión
|
||||
|
||||
### Arquitectura
|
||||
- **Diseño Modular:** Procesadores separados para diferentes tipos de respaldo
|
||||
- **Configuración Dirigida:** Definiciones de tareas en archivos externos
|
||||
- **Extensible:** Fácil adición de nuevos tipos de procesador
|
||||
|
||||
### Agregar Nuevos Procesadores
|
||||
1. Crear nuevo módulo procesador en `fxs/`
|
||||
2. Agregar mapeo de procesador en aplicación principal
|
||||
3. Definir parámetros de configuración de tareas
|
||||
4. Actualizar documentación y ejemplos
|
||||
|
||||
### Estándares de Código
|
||||
- **Estilo Ruby:** Seguir convenciones y mejores prácticas de Ruby
|
||||
- **Documentación:** Comentarios y documentación comprehensivos
|
||||
- **Manejo de Errores:** Manejo robusto de errores y logging
|
||||
- **Testing:** Testing manual de nuevas características
|
||||
|
||||
## Soporte y Recursos
|
||||
|
||||
### Documentación
|
||||
- **README.md:** Instalación y uso básico
|
||||
- **Archivos del Proyecto:** Documentación técnica detallada
|
||||
- **Ejemplos de Configuración:** Definiciones de tareas de ejemplo
|
||||
|
||||
### Comunidad y Soporte
|
||||
- **Autor:** Lic. Ricardo MONLA (rmonla@)
|
||||
- **Organización:** Dirección de TIC, Universidad Tecnológica Nacional
|
||||
- **Licencia:** MIT License (código abierto)
|
||||
|
||||
### Historial de Versiones
|
||||
- **v4.4:** Versión modular inicial con soporte Xen y Proxmox
|
||||
- **v5.2:** Sistema de configuración mejorado y editor de tareas
|
||||
- **v5.5.0:** Automatic execution via command-line parameters
|
||||
- **v5.5.1 (2025-11-14):** Fixed naming inconsistencies across the application to ensure correct app name references.
|
||||
|
||||
---
|
||||
|
||||
*Esta documentación proporciona una visión comprehensiva del procesador automatizado de respaldos dtic-BKPs. Para detalles específicos de implementación, referirse al código fuente y archivos de configuración.*
|
||||
@@ -0,0 +1,87 @@
|
||||
# dtic-BKPs - Procesador de Backups Automatizado v5.5.1
|
||||
|
||||
`dtic-BKPs` es un script de terminal interactivo escrito en Ruby, diseñado para automatizar el procesamiento, la compresión y la sincronización de copias de seguridad. Esta versión introduce una estructura de archivos modular, una configuración simplificada y la capacidad de editar tareas directamente desde la aplicación.
|
||||
|
||||
## ✨ Características Principales
|
||||
|
||||
* **Menú Interactivo y Editable:** Navega, ejecuta, y ahora también **crea, edita y elimina tareas** sin salir de la aplicación.
|
||||
* **Configuración Simplificada:** La definición de tareas se ha estandarizado con claves `origen` y `destino`, independientemente de si la ruta es local o remota (`rclone`).
|
||||
* **Estructura Modular:** El código está organizado en módulos para la aplicación principal, configuración, instalación, logging y procesadores específicos.
|
||||
* **Soporte Multiplataforma:** Procesamiento nativo para backups de **XenServer/XCP-ng** (`.xva`) y **Proxmox VE**.
|
||||
* **Sincronización Universal con `rclone`:** Sincroniza archivos entre cualquier combinación de rutas locales y remotas.
|
||||
* **Feedback Visual y Logging:** Mantiene la interfaz de usuario clara con colores, íconos, barras de progreso y un registro detallado en `dtic-BKPs.log`.
|
||||
|
||||
## 📂 Estructura del Proyecto
|
||||
|
||||
El proyecto tiene una estructura modular para facilitar su mantenimiento. Los archivos principales son:
|
||||
|
||||
* `install.sh`: Script principal de instalación que prepara el entorno.
|
||||
* `dtic-BKPs_app.rb`: El script que ejecuta la aplicación y su menú interactivo.
|
||||
* `dtic-BKPs_install.rb`: Script en Ruby para verificar dependencias y generar la configuración inicial.
|
||||
* `dtic-BKPs_conf.rb`: Aquí se guardan las tareas y preferencias. **Este archivo es modificable desde la propia aplicación**.
|
||||
* `core/globals.rb`: Define constantes, colores y el logger global.
|
||||
* `fxs/`: Contiene los módulos con las funciones principales (UI, manejo de configuración y procesadores).
|
||||
* `LICENSE`: La licencia del proyecto (MIT).
|
||||
|
||||
## 🚀 Instalación y Puesta en Marcha (Debian/Ubuntu)
|
||||
|
||||
La instalación está diseñada para ser lo más simple posible.
|
||||
|
||||
1. **Clona o descarga todos los archivos** del proyecto en un mismo directorio.
|
||||
|
||||
2. **Otorga permisos de ejecución** al script de instalación principal:
|
||||
```bash
|
||||
chmod +x install.sh
|
||||
```
|
||||
|
||||
3. **Ejecuta el instalador:**
|
||||
```bash
|
||||
./install.sh
|
||||
```
|
||||
|
||||
El script verificará si tienes `Ruby` instalado y te guiará para instalarlo si es necesario. Luego, lanzará el instalador de la aplicación (`dtic-BKPs_install.rb`), que verificará el resto de las dependencias (`rclone`, `pv`, etc.).
|
||||
|
||||
## 🔧 Configuración de Tareas
|
||||
|
||||
Puedes configurar las tareas de dos maneras:
|
||||
|
||||
1. **Editando `dtic-BKPs_conf.rb` manualmente** (método tradicional).
|
||||
2. **Usando el editor integrado** en la aplicación (opción `E` en el menú principal).
|
||||
|
||||
### Formato de Tarea Simplificado
|
||||
|
||||
| Clave | Descripción | Ejemplo |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| `id` | Símbolo único para la tarea. | `:xen_procesar` |
|
||||
| `menu_texto` | Descripción que se muestra en el menú. | `"Procesar backups Xen01 (.xva)"` |
|
||||
| `tipo_proceso` | `:xva`, `:proxmox_log_notes` o `:rclone_sync`. | `:rclone_sync` |
|
||||
| `origen` | Ruta de origen (local o remota `rclone`). | `"miRemoto:/var/lib/vz/dump/"` |
|
||||
| `destino` | Ruta de destino (local o remota `rclone`). | `"/media/backups/pmox1_sync/"` |
|
||||
| `eliminar_origen` | `true` o `false`. Borra archivos originales tras el procesamiento. (No aplica a `rclone`) | `true` |
|
||||
| `sobrescribir_destino` | `true` o `false`. Sobrescribe archivos en el destino si ya existen. | `false` |
|
||||
|
||||
## ▶️ Uso de la Aplicación
|
||||
|
||||
Ejecuta `./dtic-BKPs_app.rb` para iniciar el menú. Las opciones son:
|
||||
|
||||
* `[Enter]` / `[Número]`: Ejecuta la tarea seleccionada.
|
||||
* `[E]`: Entra al modo de edición de tareas.
|
||||
* `[S]`: Sale de la aplicación.
|
||||
|
||||
### Modo Edición
|
||||
|
||||
Dentro del modo de edición, puedes:
|
||||
* **Editar** una tarea existente (`[Número]`).
|
||||
* Añadir una **Nueva** tarea (`[N]`).
|
||||
* **Borrar** una tarea (`[B]`).
|
||||
* **Guardar** los cambios en `dtic-BKPs_conf.rb` y volver al menú principal (`[G]`).
|
||||
|
||||
## ✍️ Autor
|
||||
|
||||
* **Lic. Ricardo MONLA (rmonla@)**
|
||||
|
||||
* **Dirección de TIC, Facultad Regional La Rioja, Universidad Tecnológica Nacional**
|
||||
|
||||
## 📜 Licencia
|
||||
|
||||
Este proyecto está bajo la Licencia MIT. Consulta el archivo `LICENSE` para más detalles.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Logging functionality for dtic-BKPs execution
|
||||
LOG_FILE="./logs/runDtic-BKPs.log"
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
echo "[$TIMESTAMP] Starting dtic-BKPs application via screen" >> "$LOG_FILE"
|
||||
|
||||
# Execute the Ruby application in screen with relative path and start detached
|
||||
screen -S dtic-BKPs_session -d -m ./dtic-BKPs_app.rb "$1"
|
||||
|
||||
# Log completion (note: this will execute immediately as screen detaches)
|
||||
echo "[$TIMESTAMP] dtic-BKPs screen session launched" >> "$LOG_FILE"
|
||||
+295
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs - Automatic Backup Processor (Unified Script)
|
||||
# ----------------------------------------------------------
|
||||
# APP: dtic-BKPs
|
||||
# VERSION: v5.5.1
|
||||
# AUTHOR: Ricardo MONLA ( rmonla@ )
|
||||
# LICENSE: MIT License
|
||||
# ==========================================================
|
||||
|
||||
# --- REQUIRES GLOBALES ---
|
||||
require 'fileutils'
|
||||
require 'open3'
|
||||
require 'time'
|
||||
require 'io/console'
|
||||
require 'shellwords'
|
||||
require 'logger'
|
||||
|
||||
# --- CARGA INICIAL DE MÓDULOS ---
|
||||
# (Se asume que los otros archivos están en el mismo directorio o accesibles)
|
||||
# require_relative 'core/globals'
|
||||
require_relative 'fxs/config_handler'
|
||||
require_relative 'fxs/ui_handlers'
|
||||
require_relative 'fxs/proc_syncPmox'
|
||||
require_relative 'fxs/sync_rclone'
|
||||
require_relative 'fxs/proc_syncXEN'
|
||||
|
||||
# ==========================================================
|
||||
# CONSTANTES GLOBALES
|
||||
# ==========================================================
|
||||
|
||||
# --- CONSTANTES GLOBALES DEL SCRIPT ---
|
||||
APP_NOM_BASE = 'dtic-BKPs'
|
||||
APP_NOM = "#{APP_NOM_BASE}_app.rb"
|
||||
APP_VER = 'v5.5.1' # Actualizado
|
||||
APP_AUTOR = "Ricardo MONLA ( rmonla@ )"
|
||||
APP_DIR_SCRIPT = File.dirname(File.expand_path(__FILE__))
|
||||
APP_CFG_NOM_ARCHIVO = "#{APP_NOM_BASE}_tasks.rb"
|
||||
APP_LOG_NOM_ARCHIVO = "logs/#{APP_NOM_BASE}.log"
|
||||
APP_CFG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_CFG_NOM_ARCHIVO)
|
||||
APP_LOG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_LOG_NOM_ARCHIVO)
|
||||
|
||||
# --- COLORES E ICONOS GLOBALES ---
|
||||
RESET = "\e[0m"; NEGRITA = "\e[1m"; SUBRAYADO="\e[4m"; INTERMITENTE = "\e[5m"
|
||||
CIAN = "\e[96m"; AMARILLO = "\e[93m"; ROJO = "\e[91m"; VERDE = "\e[92m"
|
||||
ICONO_OK = "#{VERDE}✔#{RESET}"; ICONO_ERROR = "#{ROJO}✖#{RESET}"; ICONO_WARN = "#{AMARILLO}⚠#{RESET}"; ICONO_INFO = "#{CIAN}ℹ#{RESET}"
|
||||
|
||||
# --- CONFIGURACIÓN GLOBAL ---
|
||||
|
||||
# Bloque para configurar el logger directamente aquí
|
||||
require 'logger' # Asegúrate de que Logger esté cargado
|
||||
$logger = Logger.new(APP_LOG_RUTA_ARCHIVO, 'daily')
|
||||
$logger.level = Logger::INFO # Puedes ajustar el nivel de log aquí
|
||||
$logger.formatter = proc do |severity, datetime, progname, msg|
|
||||
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] [#{severity}] #{msg}\n"
|
||||
end
|
||||
|
||||
$estadisticas_ejecucion = { tareas_completadas: {} }
|
||||
|
||||
# --- FUNCIÓN DE LOGGING GLOBAL ---
|
||||
def log(nivel, mensaje)
|
||||
# Usa esta expresión regular más robusta para eliminar códigos ANSI
|
||||
msg_l = mensaje.gsub(/\e\[(?:\d{1,3}(?:;\d{1,3})*)?[m|K]/, '')
|
||||
case nivel
|
||||
when :info then $logger.info(msg_l)
|
||||
when :error then $logger.error(msg_l)
|
||||
when :warn then $logger.warn(msg_l)
|
||||
else $logger.debug(msg_l)
|
||||
end
|
||||
end
|
||||
|
||||
# Función auxiliar para verificar si un comando existe
|
||||
def which(cmd)
|
||||
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
||||
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
||||
exts.each { |ext|
|
||||
exe = File.join(path, "#{cmd}#{ext}")
|
||||
return exe if File.executable?(exe) && !File.directory?(exe)
|
||||
}
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# ==========================================================
|
||||
# (Bucle Principal)
|
||||
# ==========================================================
|
||||
|
||||
# Mapea los tipos de proceso a sus funciones correspondientes.
|
||||
PROCESSOR_MAP = {
|
||||
xva: ->(task_cfg) { procesar_backups_xva(task_cfg) },
|
||||
rclone_sync: ->(task_cfg) { sincronizar_rclone(task_cfg) },
|
||||
proxmox_log_notes: ->(task_cfg) { procesar_backups_tipo_proxmox(task_cfg) }
|
||||
}
|
||||
|
||||
def ejecutar_tarea_individual(tarea_cfg)
|
||||
processor = PROCESSOR_MAP[tarea_cfg[:tipo_proceso]]
|
||||
if processor
|
||||
processor.call(tarea_cfg)
|
||||
else
|
||||
puts "#{ICONO_ERROR} Tipo de proceso desconocido: #{tarea_cfg[:tipo_proceso]}"
|
||||
log :error, "Tipo de proceso desconocido para tarea #{tarea_cfg[:id]}: #{tarea_cfg[:tipo_proceso]}"
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_comando(comando_cfg, todas_las_tareas)
|
||||
system 'clear' or system 'cls'
|
||||
puts "\n#{NEGRITA}#{CIAN}--- Ejecutando Comando: #{comando_cfg[:menu_texto]} ---#{RESET}"
|
||||
log :info, "Iniciando ejecución del comando: #{comando_cfg[:id]}"
|
||||
|
||||
comando_cfg[:tareas].each_with_index do |id_tarea, index|
|
||||
tarea_actual = todas_las_tareas.find { |t| t[:id] == id_tarea }
|
||||
if tarea_actual
|
||||
puts "\n#{AMARILLO}--- Paso #{index + 1}/#{comando_cfg[:tareas].length}: #{tarea_actual[:menu_texto]} ---#{RESET}"
|
||||
log :info, "Ejecutando tarea '#{id_tarea}' dentro del comando '#{comando_cfg[:id]}'."
|
||||
ejecutar_tarea_individual(tarea_actual)
|
||||
# Aquí puedes añadir lógica para pausar o continuar automáticamente
|
||||
# if !AUTO_AVANZAR_TAREA_MENU # O una configuración específica para comandos
|
||||
# puts "\nPresiona Enter para continuar con la siguiente tarea del comando..."
|
||||
# $stdin.gets
|
||||
# end
|
||||
else
|
||||
puts "#{ICONO_ERROR} Tarea con ID '#{id_tarea}' no encontrada para el comando '#{comando_cfg[:id]}'. Saltando."
|
||||
log :error, "Tarea con ID '#{id_tarea}' no encontrada para el comando '#{comando_cfg[:id]}'."
|
||||
end
|
||||
end
|
||||
puts "\n#{ICONO_OK} Comando '#{comando_cfg[:menu_texto]}' completado."
|
||||
log :info, "Comando '#{comando_cfg[:id]}' completado."
|
||||
sleep 2
|
||||
end
|
||||
|
||||
def bucle_principal(tareas_config, comandos_config, auto_avanzar)
|
||||
|
||||
loop do
|
||||
opciones_menu = []
|
||||
|
||||
# Añadir tareas individuales al menú
|
||||
tareas_config.each_with_index do |tarea_cfg, index|
|
||||
opciones_menu << {
|
||||
id: "T#{index + 1}",
|
||||
texto: tarea_cfg[:menu_texto],
|
||||
tipo: :tarea,
|
||||
config: tarea_cfg
|
||||
}
|
||||
end
|
||||
|
||||
# Añadir comandos al menú
|
||||
comandos_config.each_with_index do |comando_cfg, index|
|
||||
opciones_menu << {
|
||||
id: "C#{index + 1}",
|
||||
texto: comando_cfg[:menu_texto],
|
||||
tipo: :comando,
|
||||
config: comando_cfg
|
||||
}
|
||||
end
|
||||
|
||||
opciones_menu << { id: 'E', texto: "Editar tareas y comandos programados", tipo: :especial }
|
||||
opciones_menu << { id: 'S', texto: "Salir del procesador", tipo: :especial }
|
||||
|
||||
system 'clear' or system 'cls'
|
||||
mostrar_encabezado_app
|
||||
puts "\n#{CIAN}--- Menú Principal ---#{RESET}"
|
||||
|
||||
puts "\n#{NEGRITA}Tareas Individuales: #{RESET}"
|
||||
opciones_menu.select { |op| op[:tipo] == :tarea }.each do |op|
|
||||
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
|
||||
end
|
||||
|
||||
puts "\n#{NEGRITA}Comandos (Secuencias de Tareas): #{RESET}"
|
||||
opciones_menu.select { |op| op[:tipo] == :comando }.each do |op|
|
||||
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
|
||||
end
|
||||
|
||||
puts "\n#{CIAN}--------------------#{RESET}"
|
||||
# Opciones especiales al final
|
||||
opciones_menu.select { |op| op[:tipo] == :especial }.each do |op|
|
||||
puts "#{AMARILLO}#{op[:id]}.#{RESET} #{op[:texto]}"
|
||||
end
|
||||
puts "#{CIAN}--------------------#{RESET}"
|
||||
|
||||
opcion_usuario = leer_entrada_simple("Selecciona una opción > ").strip.upcase
|
||||
opcion_encontrada = opciones_menu.find { |op| op[:id].to_s == opcion_usuario }
|
||||
|
||||
if opcion_encontrada
|
||||
case opcion_encontrada[:tipo]
|
||||
when :tarea
|
||||
ejecutar_tarea_individual(opcion_encontrada[:config])
|
||||
when :comando
|
||||
ejecutar_comando(opcion_encontrada[:config], tareas_config)
|
||||
when :especial
|
||||
case opcion_encontrada[:id]
|
||||
when 'E'
|
||||
modificar_tareas_y_comandos_menu(tareas_config, comandos_config, auto_avanzar)
|
||||
# Recargar configuración después de editar
|
||||
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
|
||||
when 'S'
|
||||
puts "Saliendo..."; exit 0
|
||||
end
|
||||
end
|
||||
|
||||
# Pausar si no es una opción de edición/salir
|
||||
unless ['E', 'S'].include?(opcion_encontrada[:id].to_s) || auto_avanzar
|
||||
puts "\nPresiona Enter para continuar..."
|
||||
$stdin.gets
|
||||
end
|
||||
else
|
||||
puts "#{ICONO_ERROR} Opción no válida."
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# --- Inicio del Script ---
|
||||
Signal.trap("INT") { puts "\n#{ROJO}Ctrl+C detectado. Saliendo...#{RESET}"; log :warn, "Script interrumpido (Ctrl+C)."; exit 130; }
|
||||
|
||||
# === AUTO-SCREEN WRAPPER ===
|
||||
# Verificamos si ya estamos dentro de una sesión de GNU Screen
|
||||
unless ENV['TERM'] == 'screen' || ENV['TERM'] =~ /^screen/ || ENV['STY']
|
||||
puts "\n#{CIAN}ℹ Relanzando dtic-BKPs en una sesión separada (screen) para permitir desatención...#{RESET}"
|
||||
|
||||
# Construimos el comando original con sus argumentos
|
||||
comando_original = [$PROGRAM_NAME] + ARGV
|
||||
comando_escapado = comando_original.map { |arg| Shellwords.escape(arg) }.join(' ')
|
||||
|
||||
# Nombre de sesión dinámico
|
||||
nombre_sesion = "dticBKPs_#{Time.now.strftime('%H%M%S')}"
|
||||
|
||||
puts "Para reconectarte luego, usa: #{AMARILLO}screen -r #{nombre_sesion}#{RESET}"
|
||||
sleep 2
|
||||
|
||||
# Reemplaza el proceso actual lanzándolo en una sesión de screen desatendida (-dmS)
|
||||
system('screen', '-dmS', nombre_sesion, 'bash', '-c', "#{comando_escapado}; echo ''; read -p 'Presiona Enter para cerrar la sesión...'")
|
||||
exit 0
|
||||
end
|
||||
|
||||
log :info, "===== Inicio script #{APP_NOM} #{APP_VER} (en Screen) ====="
|
||||
|
||||
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
|
||||
|
||||
opciones_menu = []
|
||||
|
||||
# Añadir tareas individuales al menú
|
||||
tareas_config.each_with_index do |tarea_cfg, index|
|
||||
opciones_menu << {
|
||||
id: "T#{index + 1}",
|
||||
texto: tarea_cfg[:menu_texto],
|
||||
tipo: :tarea,
|
||||
config: tarea_cfg
|
||||
}
|
||||
end
|
||||
|
||||
# Añadir comandos al menú
|
||||
comandos_config.each_with_index do |comando_cfg, index|
|
||||
opciones_menu << {
|
||||
id: "C#{index + 1}",
|
||||
texto: comando_cfg[:menu_texto],
|
||||
tipo: :comando,
|
||||
config: comando_cfg
|
||||
}
|
||||
end
|
||||
|
||||
opciones_menu << { id: 'E', texto: "Editar tareas y comandos programados", tipo: :especial }
|
||||
opciones_menu << { id: 'S', texto: "Salir del procesador", tipo: :especial }
|
||||
|
||||
if ARGV[0]
|
||||
opcion_usuario = ARGV[0].strip.upcase
|
||||
opcion_encontrada = opciones_menu.find { |op| op[:id].to_s == opcion_usuario }
|
||||
|
||||
if opcion_encontrada
|
||||
case opcion_encontrada[:tipo]
|
||||
when :tarea
|
||||
ejecutar_tarea_individual(opcion_encontrada[:config])
|
||||
when :comando
|
||||
ejecutar_comando(opcion_encontrada[:config], tareas_config)
|
||||
when :especial
|
||||
case opcion_encontrada[:id]
|
||||
when 'E'
|
||||
modificar_tareas_y_comandos_menu(tareas_config, comandos_config, auto_avanzar)
|
||||
# Recargar configuración después de editar
|
||||
tareas_config, comandos_config, auto_avanzar = cargar_configuracion
|
||||
when 'S'
|
||||
puts "Saliendo..."; exit 0
|
||||
end
|
||||
end
|
||||
log :info, "Ejecutado automáticamente desde línea de comandos: #{opcion_usuario}"
|
||||
exit 0
|
||||
else
|
||||
puts "#{ICONO_ERROR} Opción no válida: #{opcion_usuario}"
|
||||
log :error, "Opción no válida desde línea de comandos: #{opcion_usuario}"
|
||||
exit 1
|
||||
end
|
||||
else
|
||||
bucle_principal(tareas_config, comandos_config, auto_avanzar)
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
# ==========================================================
|
||||
# Archivo de Configuración para dtic-BKPs
|
||||
# Generado automáticamente el 2025-09-25 11:53:34
|
||||
# ==========================================================
|
||||
AUTO_AVANZAR_TAREA_MENU = true
|
||||
TAREAS_CONFIG = [
|
||||
{
|
||||
id: :proc_syncXen1,
|
||||
menu_texto: "⚙️ Procesar archivos de BKPs de VMs: syncs-XEN01",
|
||||
tipo_proceso: :xva,
|
||||
origen: "/mnt/ns8Disco2/syncs-XEN01/",
|
||||
destino: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/",
|
||||
eliminar_origen: true,
|
||||
sobrescribir_destino: false,
|
||||
},
|
||||
{
|
||||
id: :proc_syncPmox,
|
||||
menu_texto: "⚙️ Procesar archivos de BKPs de VMs desde zfsDISCO1",
|
||||
tipo_proceso: :proxmox_log_notes,
|
||||
origen: "pve_PMOX3:/mnt/pve/zfsDISCO1/dump/",
|
||||
destino: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false,
|
||||
},
|
||||
{
|
||||
id: :sinc_pmox_ns8,
|
||||
menu_texto: "📥 Descargar archivos de BKPs de VMs: zfsDISCO1",
|
||||
tipo_proceso: :rclone_sync,
|
||||
origen: "pve_PMOX3:/mnt/pve/zfsDISCO1/dump/",
|
||||
destino: "/mnt/ns8Disco2/syncs-PMOX/",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false,
|
||||
},
|
||||
{
|
||||
id: :sync_BkpAntiguos_nube,
|
||||
menu_texto: "☁️ Upload de BKPs a rmOneDrive: bkps-ANTIGUOS",
|
||||
tipo_proceso: :rclone_sync,
|
||||
origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-ANTIGUOS/",
|
||||
destino: "rmOneDrive:/dtic-BACKUPS/bkps-ANTIGUOS/",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false,
|
||||
},
|
||||
{
|
||||
id: :sync_BkpsSERVIDORes_nube,
|
||||
menu_texto: "☁️ Upload de bkps-SERVIDORes a rmOneDrive",
|
||||
tipo_proceso: :rclone_sync,
|
||||
origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/",
|
||||
destino: "rmOneDrive:/dtic-BACKUPS/bkps-SERVIDORes/",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false,
|
||||
},
|
||||
{
|
||||
id: :sync_BkpUsers_nube,
|
||||
menu_texto: "☁️ Upload de bkps-USERs a rmOneDrive",
|
||||
tipo_proceso: :rclone_sync,
|
||||
origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-USERs/",
|
||||
destino: "rmOneDrive:/dtic-BACKUPS/bkps-USERs/",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false,
|
||||
}
|
||||
|
||||
]
|
||||
COMANDOS_CONFIG = [
|
||||
{
|
||||
id: :descargar_y_procesar_pmox,
|
||||
menu_texto: "📥⚙️ Descargar y Procesar BKPs de PMOXs",
|
||||
tareas: [:sinc_pmox_ns8, :proc_syncPmox],
|
||||
},
|
||||
{
|
||||
id: :full_procesamiento,
|
||||
menu_texto: "⚙️ Procesar TODOS los BKPs de NS8",
|
||||
tareas: [:proc_syncXen1, :proc_syncPmox],
|
||||
},
|
||||
{
|
||||
id: :full_upload,
|
||||
menu_texto: "🚀 Subir a la Nube todos los BKPs",
|
||||
tareas: [:sync_BkpAntiguos_nube, :sync_BkpsSERVIDORes_nube, :sync_BkpUsers_nube, :sync_BkpXen01_nube],
|
||||
},
|
||||
{
|
||||
id: :full_full,
|
||||
menu_texto: "🚀 Procesar todo y Subir todo.",
|
||||
tareas: [:proc_syncXen1, :proc_syncPmox, :sync_BkpsSERVIDORes_nube, :sync_BkpAntiguos_nube, :sync_BkpUsers_nube],
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# FILE: fxs/config_handler.rb
|
||||
# ==========================================================
|
||||
|
||||
def cargar_configuracion
|
||||
unless File.exist?(APP_CFG_RUTA_ARCHIVO)
|
||||
puts "#{ICONO_WARN} Archivo de configuración de tareas no encontrado: #{APP_CFG_RUTA_ARCHIVO}."
|
||||
puts "#{ICONO_INFO} Usando configuración interna. Los cambios se guardarán en un nuevo archivo."
|
||||
# Si el archivo no existe, devuelve valores predeterminados (vacíos o un ejemplo)
|
||||
return [], [], true # Retorna tareas vacías, comandos vacíos y auto_avanzar en true
|
||||
end
|
||||
|
||||
begin
|
||||
# Carga el archivo de configuración. Esto definirá TAREAS_CONFIG y COMANDOS_CONFIG
|
||||
load APP_CFG_RUTA_ARCHIVO
|
||||
|
||||
unless defined?(TAREAS_CONFIG) && TAREAS_CONFIG.is_a?(Array)
|
||||
puts "#{ICONO_ERROR} TAREAS_CONFIG no está definida correctamente en '#{APP_CFG_RUTA_ARCHIVO}'."
|
||||
exit 1
|
||||
end
|
||||
|
||||
unless defined?(COMANDOS_CONFIG) && COMANDOS_CONFIG.is_a?(Array)
|
||||
puts "#{ICONO_ERROR} COMANDOS_CONFIG no está definida correctamente en '#{APP_CFG_RUTA_ARCHIVO}'."
|
||||
exit 1
|
||||
end
|
||||
|
||||
auto_avanzar = defined?(AUTO_AVANZAR_TAREA_MENU) ? AUTO_AVANZAR_TAREA_MENU : true
|
||||
return TAREAS_CONFIG, COMANDOS_CONFIG, auto_avanzar
|
||||
rescue LoadError, SyntaxError => e
|
||||
puts "#{ICONO_ERROR} Error cargando configuración desde '#{APP_CFG_RUTA_ARCHIVO}':\n#{e.message}"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def guardar_configuracion(tareas, comandos, auto_avanzar)
|
||||
begin
|
||||
File.open(APP_CFG_RUTA_ARCHIVO, 'w') do |f|
|
||||
f.puts "# frozen_string_literal: true\n"
|
||||
f.puts "# =========================================================="
|
||||
f.puts "# Archivo de Configuración para #{APP_NOM_BASE}"
|
||||
f.puts "# Generado automáticamente el #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
f.puts "# ==========================================================\n"
|
||||
f.puts "AUTO_AVANZAR_TAREA_MENU = #{auto_avanzar}\n"
|
||||
|
||||
f.puts "TAREAS_CONFIG = ["
|
||||
tareas.each do |tarea|
|
||||
f.puts " {"
|
||||
tarea.each { |key, value| f.puts " #{key}: #{value.inspect}," }
|
||||
f.puts " },"
|
||||
end
|
||||
f.puts "]\n"
|
||||
|
||||
f.puts "COMANDOS_CONFIG = ["
|
||||
comandos.each do |comando|
|
||||
f.puts " {"
|
||||
comando.each do |key, value|
|
||||
if key == :tareas
|
||||
f.puts " #{key}: [#{value.map(&:inspect).join(', ')}],"
|
||||
else
|
||||
f.puts " #{key}: #{value.inspect},"
|
||||
end
|
||||
end
|
||||
f.puts " },"
|
||||
end
|
||||
f.puts "]"
|
||||
end
|
||||
puts "\n#{ICONO_OK} Configuración de tareas y comandos guardada en '#{APP_CFG_NOM_ARCHIVO}'."
|
||||
log :info, "Configuración de tareas y comandos actualizada y guardada por el usuario."
|
||||
sleep 1.5
|
||||
return true
|
||||
rescue StandardError => e
|
||||
puts "\n#{ICONO_ERROR} No se pudo guardar la configuración: #{e.message}"
|
||||
log :error, "Fallo al guardar configuración de tareas y comandos: #{e.message}"
|
||||
sleep 2
|
||||
return false
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
# 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
|
||||
@@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs - Automatic Backup Processor
|
||||
# ----------------------------------------------------------
|
||||
# APP: dtic-BKPs
|
||||
# FILE: fxs/proc_syncXEN.rb
|
||||
# VERSION: v5.5.1
|
||||
# AUTHOR: Ricardo MONLA (rmonla@)
|
||||
# LICENSE: MIT License
|
||||
# ==========================================================
|
||||
|
||||
# --- Función Principal del Procesador XVA ---
|
||||
def procesar_backups_xva(tarea_cfg)
|
||||
system 'clear' or system 'cls'
|
||||
# 1. --- Extraer configuración y inicializar variables ---
|
||||
dir_origen = tarea_cfg[:origen]
|
||||
dir_destino_base = tarea_cfg[:destino]
|
||||
eliminar_original = tarea_cfg[:eliminar_origen]
|
||||
sobrescribir_destino = tarea_cfg[:sobrescribir_destino]
|
||||
archivos_procesados_llamada = 0
|
||||
|
||||
puts "\n#{NEGRITA}#{CIAN}--- #{tarea_cfg[:menu_texto]} ---#{RESET}"
|
||||
log :info, "#{tarea_cfg[:id]}: Iniciado desde #{dir_origen}. Eliminar: #{eliminar_original}, Sobrescribir: #{sobrescribir_destino}"
|
||||
|
||||
@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
|
||||
|
||||
# 2. --- Buscar archivos a procesar ---
|
||||
archivos_disponibles = Dir.glob(File.join(dir_origen, '*.xva'))
|
||||
|
||||
if archivos_disponibles.empty?
|
||||
msg = "#{tarea_cfg[:id]}: No hay archivos .xva en '#{dir_origen}'."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: 0, estado: "OK (Sin archivos)" }
|
||||
return
|
||||
end
|
||||
|
||||
puts "#{ICONO_INFO} Encontrados #{archivos_disponibles.length} archivo(s) .xva a procesar."
|
||||
|
||||
# 3. --- Iterar y procesar cada archivo ---
|
||||
archivos_disponibles.each do |ruta_completa_xva_origen|
|
||||
nombre_base_xva = File.basename(ruta_completa_xva_origen)
|
||||
nombre_vm = nombre_base_xva.split('_').first
|
||||
dir_dest_especifico_vm = File.join(dir_destino_base, nombre_vm)
|
||||
|
||||
begin
|
||||
FileUtils.mkdir_p(dir_dest_especifico_vm)
|
||||
rescue StandardError => e
|
||||
log :error, "#{tarea_cfg[:id]}: Error creando dir '#{dir_dest_especifico_vm}': #{e.message}"
|
||||
next
|
||||
end
|
||||
|
||||
nombre_sin_ext = File.basename(nombre_base_xva, '.xva')
|
||||
ruta_salida_comp = File.join(dir_dest_especifico_vm, "#{nombre_sin_ext}.tar.gz")
|
||||
|
||||
if File.exist?(ruta_salida_comp)
|
||||
if sobrescribir_destino
|
||||
msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_comp)}' existe. Se sobrescribirá."
|
||||
log :info, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
else
|
||||
msg = "#{tarea_cfg[:id]}: Destino '#{File.basename(ruta_salida_comp)}' existe. No se sobrescribe. Saltando."
|
||||
log :warn, msg
|
||||
puts "#{ICONO_WARN} #{msg}"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# 4. --- Realizar la compresión ---
|
||||
puts "#{CIAN}→ Comprimiendo #{NEGRITA}#{nombre_base_xva}#{RESET} a #{File.basename(ruta_salida_comp)}..."
|
||||
log :info, "#{tarea_cfg[:id]}: Comprimiendo XVA #{nombre_base_xva} → #{File.basename(ruta_salida_comp)}"
|
||||
|
||||
begin
|
||||
if @pv_disponible
|
||||
cmd_tar = ['tar', '-czf', '-', '-C', dir_origen, nombre_base_xva]
|
||||
cmd_pv = ['pv', '-s', File.size(ruta_completa_xva_origen).to_s]
|
||||
|
||||
File.open(ruta_salida_comp, 'wb') do |f_out|
|
||||
Open3.pipeline(cmd_tar, cmd_pv, out: f_out).each_with_index do |status, i|
|
||||
unless status.success?
|
||||
cmd_name = i == 0 ? 'tar' : 'pv'
|
||||
raise "Error en compresión (falló #{cmd_name} con código: #{status.exitstatus})"
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
cmd_tar_directo = "tar -czf #{Shellwords.escape(ruta_salida_comp)} -C #{Shellwords.escape(dir_origen)} #{Shellwords.escape(nombre_base_xva)}"
|
||||
system(cmd_tar_directo)
|
||||
raise "Error en compresión (falló tar con código: #{$?.exitstatus})" unless $?.success?
|
||||
end
|
||||
|
||||
# 5. --- Finalizar y limpiar ---
|
||||
log :info, "#{tarea_cfg[:id]}: Compresión OK → #{ruta_salida_comp}"
|
||||
puts "\n#{ICONO_OK} Compresión OK: #{File.basename(ruta_salida_comp)}"
|
||||
|
||||
if eliminar_original
|
||||
log :info, "#{tarea_cfg[:id]}: Eliminando XVA original #{nombre_base_xva}"
|
||||
FileUtils.rm_f(ruta_completa_xva_origen)
|
||||
end
|
||||
|
||||
archivos_procesados_llamada += 1
|
||||
|
||||
rescue StandardError => e
|
||||
err_msg = "#{tarea_cfg[:id]}: Error al comprimir #{nombre_base_xva}: #{e.message}"
|
||||
log :error, err_msg
|
||||
puts "\n#{ICONO_ERROR} #{err_msg}"
|
||||
FileUtils.rm_f(ruta_salida_comp)
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
# 6. --- Reportar estadísticas finales ---
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { archivos: archivos_procesados_llamada, estado: "OK" }
|
||||
log :info, "#{tarea_cfg[:id]}: #{archivos_procesados_llamada} archivos .xva procesados"
|
||||
puts "#{ICONO_OK} Tarea '#{tarea_cfg[:menu_texto]}' completada. Procesados: #{archivos_procesados_llamada}."
|
||||
sleep 1.5
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs - Automatic Backup Processor
|
||||
# ----------------------------------------------------------
|
||||
# APP: dtic-BKPs
|
||||
# FILE: fxs/sync_rclone.rb
|
||||
# VERSION: v5.5.1
|
||||
# AUTHOR: Ricardo MONLA (rmonla@)
|
||||
# LICENSE: MIT License
|
||||
# ==========================================================
|
||||
|
||||
# Carga las constantes y helpers globales
|
||||
# require_relative '../core/globals'
|
||||
|
||||
def sincronizar_rclone(tarea_cfg)
|
||||
system 'clear' or system 'cls'
|
||||
origen = tarea_cfg[:origen]
|
||||
destino = tarea_cfg[:destino]
|
||||
|
||||
puts "\n#{NEGRITA}#{CIAN}--- #{tarea_cfg[:menu_texto]} ---#{RESET}"
|
||||
log :info, "#{tarea_cfg[:id]}: Iniciado rclone sync. Origen: #{origen}, Destino: #{destino}"
|
||||
|
||||
# Get all subdirectories recursively
|
||||
subdirs = Dir.glob("#{origen}/**/*").select { |f| File.directory?(f) }.map { |d| d.sub("#{origen}/", '') }
|
||||
|
||||
all_exito = true
|
||||
|
||||
# Opciones base de rclone:
|
||||
# --delete-before: Borra origen del remoto ANTES de transferir, liberando espacio clave para respaldos grandes (Code 7 limits)
|
||||
# --retries 5 etc: Blindaje de conexión para archivos gigantescos hacia la nube.
|
||||
rclone_ops = "--delete-before --retries 5 --retries-sleep 10s --timeout 30m --contimeout 10m --tpslimit 10"
|
||||
|
||||
# Execute rclone for each subdirectory
|
||||
subdirs.each do |subdir|
|
||||
comando_sub = "rclone sync #{Shellwords.escape(File.join(origen, subdir))} #{Shellwords.escape(File.join(destino, subdir))} --progress -v #{rclone_ops}"
|
||||
log :info, "#{tarea_cfg[:id]}: Ejecutando subdir #{subdir} → #{comando_sub}"
|
||||
puts "#{CIAN}→ Ejecutando rclone sync subdir (#{subdir})#{RESET}"
|
||||
|
||||
exito_sub = system(comando_sub)
|
||||
if !exito_sub
|
||||
log :error, "#{tarea_cfg[:id]}: Fallo en sincronización subdir #{subdir}"
|
||||
puts "#{ICONO_ERROR} Fallo sincronización rclone subdir (#{subdir})."
|
||||
end
|
||||
all_exito &= exito_sub
|
||||
end
|
||||
|
||||
# Execute rclone for the root directory
|
||||
comando_rclone = "rclone sync #{Shellwords.escape(origen)} #{Shellwords.escape(destino)} --progress -v #{rclone_ops}"
|
||||
log :info, "#{tarea_cfg[:id]}: Ejecutando root → #{comando_rclone}"
|
||||
puts "#{CIAN}→ Ejecutando rclone sync root (Origen: #{origen} → Destino: #{destino})#{RESET}"
|
||||
|
||||
exito = system(comando_rclone)
|
||||
all_exito &= exito
|
||||
codigo_salida = $?.exitstatus
|
||||
estado_final = all_exito ? "Éxito" : "Fallo (cód: #{codigo_salida})"
|
||||
|
||||
$estadisticas_ejecucion[:tareas_completadas][tarea_cfg[:id]] = { estado: estado_final }
|
||||
|
||||
if all_exito
|
||||
log :info, "#{tarea_cfg[:id]}: Sincronización OK"
|
||||
puts "#{ICONO_OK} Sincronización rclone OK."
|
||||
else
|
||||
log :error, "#{tarea_cfg[:id]}: Fallo en sincronización (#{estado_final})"
|
||||
puts "#{ICONO_ERROR} Fallo sincronización rclone (#{estado_final})."
|
||||
end
|
||||
|
||||
sleep 1.5
|
||||
all_exito
|
||||
end
|
||||
@@ -0,0 +1,119 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# FILE: fxs/ui_handlers.rb
|
||||
# ==========================================================
|
||||
|
||||
def leer_entrada_simple(prompt)
|
||||
print prompt; $stdout.flush; $stdin.gets.chomp
|
||||
end
|
||||
|
||||
def mostrar_encabezado_app
|
||||
puts "#{NEGRITA}#{CIAN}--- dtic-BKPs - Procesador de Backups #{APP_VER} ---#{RESET}"
|
||||
end
|
||||
|
||||
def editar_item_config(item, type_name)
|
||||
system 'clear' or system 'cls'
|
||||
puts "#{NEGRITA}#{CIAN}--- Editando #{type_name}: #{item[:id]} ---#{RESET}"
|
||||
puts "Deja el campo en blanco para mantener el valor actual."
|
||||
|
||||
item.each do |key, value|
|
||||
next if key == :id # ID no se edita directamente
|
||||
|
||||
if key == :tareas && type_name == "Comando"
|
||||
current_tasks = value.map(&:to_s).join(', ')
|
||||
nuevo_valor_str = leer_entrada_simple("#{AMARILLO}#{key}#{RESET} (IDs de tareas separados por coma) [Actual: #{current_tasks}] > ").strip
|
||||
unless nuevo_valor_str.empty?
|
||||
item[key] = nuevo_valor_str.split(',').map(&:strip).map(&:to_sym).uniq
|
||||
end
|
||||
else
|
||||
tipo = value.is_a?(TrueClass) || value.is_a?(FalseClass) ? "(true/false)" : ""
|
||||
nuevo_valor = leer_entrada_simple("#{AMARILLO}#{key}#{RESET} #{tipo} [Actual: #{value.inspect}] > ").strip
|
||||
unless nuevo_valor.empty?
|
||||
if value.is_a?(Symbol)
|
||||
item[key] = nuevo_valor.to_sym
|
||||
elsif tipo == "(true/false)"
|
||||
item[key] = nuevo_valor.downcase == 'true'
|
||||
else
|
||||
item[key] = nuevo_valor
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
puts "#{ICONO_OK} #{type_name} actualizada."
|
||||
sleep 1
|
||||
end
|
||||
|
||||
def modificar_tareas_y_comandos_menu(tareas_actuales, comandos_actuales, auto_avanzar_actual)
|
||||
loop do
|
||||
system 'clear' or system 'cls'
|
||||
puts "#{NEGRITA}#{CIAN}--- Editor de Tareas y Comandos ---#{RESET}"
|
||||
|
||||
puts "\n#{NEGRITA}Tareas Individuales: #{RESET}"
|
||||
tareas_actuales.each_with_index do |tarea, index|
|
||||
puts "#{AMARILLO}T#{index + 1}.#{RESET} #{tarea[:menu_texto]} (#{AMARILLO}id: :#{tarea[:id]}#{RESET})"
|
||||
end
|
||||
|
||||
puts "\n#{NEGRITA}Comandos: #{RESET}"
|
||||
comandos_actuales.each_with_index do |comando, index|
|
||||
puts "#{AMARILLO}C#{index + 1}.#{RESET} #{comando[:menu_texto]} (#{AMARILLO}id: :#{comando[:id]}#{RESET}, tareas: #{comando[:tareas].join(', ')}#{RESET})"
|
||||
end
|
||||
|
||||
puts "#{CIAN}-------------------------#{RESET}"
|
||||
prompt = "[Tx]=Editar Tarea, [Cx]=Editar Comando, [NT]=Nueva Tarea, [NC]=Nuevo Comando, [BT]=Borrar Tarea, [BC]=Borrar Comando, [G]=Guardar y Volver > "
|
||||
opcion = leer_entrada_simple(prompt).downcase
|
||||
|
||||
case opcion
|
||||
when /^t(\d+)$/
|
||||
index = $1.to_i - 1
|
||||
editar_item_config(tareas_actuales[index], "Tarea") if tareas_actuales[index]
|
||||
when /^c(\d+)$/
|
||||
index = $1.to_i - 1
|
||||
editar_item_config(comandos_actuales[index], "Comando") if comandos_actuales[index]
|
||||
when 'nt'
|
||||
# Generar un ID único para la nueva tarea
|
||||
new_id = "nueva_tarea_#{Time.now.to_i}".to_sym
|
||||
nueva_tarea = {
|
||||
id: new_id,
|
||||
menu_texto: "Nueva Tarea (#{new_id})",
|
||||
tipo_proceso: :rclone_sync,
|
||||
origen: "",
|
||||
destino: "",
|
||||
eliminar_origen: false,
|
||||
sobrescribir_destino: false
|
||||
}
|
||||
editar_item_config(nueva_tarea, "Nueva Tarea")
|
||||
tareas_actuales << nueva_tarea
|
||||
when 'nc'
|
||||
# Generar un ID único para el nuevo comando
|
||||
new_id = "nuevo_comando_#{Time.now.to_i}".to_sym
|
||||
nuevo_comando = {
|
||||
id: new_id,
|
||||
menu_texto: "Nuevo Comando (#{new_id})",
|
||||
tareas: [] # Se pueden añadir tareas después
|
||||
}
|
||||
editar_item_config(nuevo_comando, "Nuevo Comando")
|
||||
comandos_actuales << nuevo_comando
|
||||
when 'bt'
|
||||
index_b = leer_entrada_simple("Número de tarea a borrar (Ej: T1) > ").downcase.gsub('t', '').to_i - 1
|
||||
if tareas_actuales[index_b]
|
||||
puts "#{ICONO_WARN} Borrando tarea '#{tareas_actuales[index_b][:menu_texto]}'."
|
||||
tareas_actuales.delete_at(index_b)
|
||||
sleep 1
|
||||
end
|
||||
when 'bc'
|
||||
index_b = leer_entrada_simple("Número de comando a borrar (Ej: C1) > ").downcase.gsub('c', '').to_i - 1
|
||||
if comandos_actuales[index_b]
|
||||
puts "#{ICONO_WARN} Borrando comando '#{comandos_actuales[index_b][:menu_texto]}'."
|
||||
comandos_actuales.delete_at(index_b)
|
||||
sleep 1
|
||||
end
|
||||
when 'g'
|
||||
guardar_configuracion(tareas_actuales, comandos_actuales, auto_avanzar_actual)
|
||||
return
|
||||
else
|
||||
puts "#{ICONO_ERROR} Opción no válida."
|
||||
sleep 1
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# FILE: core/globals.rb
|
||||
# ==========================================================
|
||||
|
||||
# --- CONSTANTES GLOBALES DEL SCRIPT ---
|
||||
APP_NOM_BASE = 'dtic-BKPs'
|
||||
APP_NOM = "#{APP_NOM_BASE}_app.rb"
|
||||
APP_VER = 'v5.5.1' # Actualizado
|
||||
APP_AUTOR = "Ricardo MONLA ( rmonla@ )"
|
||||
APP_DIR_SCRIPT = File.dirname(File.expand_path(__FILE__))
|
||||
APP_CFG_NOM_ARCHIVO = "#{APP_NOM_BASE}_tasks.rb"
|
||||
APP_LOG_NOM_ARCHIVO = "../logs/#{APP_NOM_BASE}.log"
|
||||
APP_CFG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_CFG_NOM_ARCHIVO)
|
||||
APP_LOG_RUTA_ARCHIVO = File.join(APP_DIR_SCRIPT, APP_LOG_NOM_ARCHIVO)
|
||||
|
||||
# --- COLORES E ICONOS GLOBALES ---
|
||||
RESET = "\e[0m"; NEGRITA = "\e[1m"; SUBRAYADO="\e[4m"; INTERMITENTE = "\e[5m"
|
||||
CIAN = "\e[96m"; AMARILLO = "\e[93m"; ROJO = "\e[91m"; VERDE = "\e[92m"
|
||||
ICONO_OK = "#{VERDE}✔#{RESET}"; ICONO_ERROR = "#{ROJO}✖#{RESET}"; ICONO_WARN = "#{AMARILLO}⚠#{RESET}"; ICONO_INFO = "#{CIAN}ℹ#{RESET}"
|
||||
|
||||
# --- CONFIGURACIÓN GLOBAL ---
|
||||
|
||||
# Bloque para configurar el logger directamente aquí
|
||||
require 'logger' # Asegúrate de que Logger esté cargado
|
||||
$logger = Logger.new(APP_LOG_RUTA_ARCHIVO, 'daily')
|
||||
$logger.level = Logger::INFO # Puedes ajustar el nivel de log aquí
|
||||
$logger.formatter = proc do |severity, datetime, progname, msg|
|
||||
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] [#{severity}] #{msg}\n"
|
||||
end
|
||||
|
||||
$estadisticas_ejecucion = { tareas_completadas: {} }
|
||||
|
||||
# --- FUNCIÓN DE LOGGING GLOBAL ---
|
||||
def log(nivel, mensaje)
|
||||
# Usa esta expresión regular más robusta para eliminar códigos ANSI
|
||||
msg_l = mensaje.gsub(/\e\[(?:\d{1,3}(?:;\d{1,3})*)?[m|K]/, '')
|
||||
case nivel
|
||||
when :info then $logger.info(msg_l)
|
||||
when :error then $logger.error(msg_l)
|
||||
when :warn then $logger.warn(msg_l)
|
||||
else $logger.debug(msg_l)
|
||||
end
|
||||
end
|
||||
|
||||
# Función auxiliar para verificar si un comando existe
|
||||
def which(cmd)
|
||||
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
||||
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
||||
exts.each { |ext|
|
||||
exe = File.join(path, "#{cmd}#{ext}")
|
||||
return exe if File.executable?(exe) && !File.directory?(exe)
|
||||
}
|
||||
end
|
||||
nil
|
||||
end
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# dtic-BKPs - Automatic Backup Processor
|
||||
# ----------------------------------------------------------
|
||||
# APP: dtic-BKPs
|
||||
# FILE: install.sh
|
||||
# VERSION: v5.5.0 (of the main app)
|
||||
# AUTHOR: Ricardo MONLA (rmonla@)
|
||||
# LICENSE: MIT License
|
||||
# ==========================================================
|
||||
|
||||
# --- Colores para la salida ---
|
||||
CIAN='\033[0;96m'
|
||||
AMARILLO='\033[0;93m'
|
||||
VERDE='\033[0;92m'
|
||||
ROJO='\033[0;91m'
|
||||
NEGRITA='\033[1m'
|
||||
NC='\033[0m' # Sin color
|
||||
|
||||
# --- Iconos ---
|
||||
ICONO_OK="${VERDE}✔${NC}"
|
||||
ICONO_ERROR="${ROJO}✖${NC}"
|
||||
ICONO_INFO="${CIAN}ℹ${NC}"
|
||||
ICONO_WARN="${AMARILLO}⚠${NC}"
|
||||
|
||||
echo -e "${NEGRITA}${CIAN}--- Instalador de dtic-BKPs ---${NC}"
|
||||
|
||||
# 1. VERIFICAR SI RUBY ESTÁ INSTALADO
|
||||
echo -e "\n${ICONO_INFO} Verificando si Ruby está instalado..."
|
||||
|
||||
if command -v ruby > /dev/null 2>&1
|
||||
then
|
||||
# Ruby ya está instalado
|
||||
RUBY_VERSION=$(ruby -v)
|
||||
echo -e "${ICONO_OK} Ruby ya está instalado: ${AMARILLO}${RUBY_VERSION}${NC}"
|
||||
else
|
||||
# Ruby no está instalado
|
||||
echo -e "${ICONO_WARN} Ruby no se encuentra en el sistema."
|
||||
|
||||
# Manera compatible con 'sh' para pedir entrada
|
||||
echo -n -e "${AMARILLO}¿Deseas instalar Ruby (a través de 'sudo apt install ruby-full')? (S/N): ${NC}"
|
||||
read respuesta
|
||||
|
||||
# Usamos 'case' en lugar de 'if [[ ... =~ ... ]]' para máxima compatibilidad
|
||||
case "$respuesta" in
|
||||
[Ss] | [Ss][Ii]) # Acepta S, s, Si, si
|
||||
echo -e "\n${ICONO_INFO} Se necesita contraseña de administrador para instalar paquetes."
|
||||
echo -e "${ICONO_INFO} Actualizando lista de paquetes (sudo apt-get update)..."
|
||||
sudo apt-get update
|
||||
|
||||
echo -e "${ICONO_INFO} Instalando ruby-full..."
|
||||
sudo apt-get install -y ruby-full
|
||||
|
||||
if ! command -v ruby > /dev/null 2>&1
|
||||
then
|
||||
echo -e "\n${ICONO_ERROR} La instalación de Ruby falló. Por favor, instálalo manualmente y vuelve a ejecutar este script."
|
||||
exit 1
|
||||
fi
|
||||
echo -e "\n${ICONO_OK} Ruby instalado correctamente."
|
||||
;;
|
||||
*)
|
||||
echo -e "\n${ICONO_ERROR} Instalación cancelada. Ruby es necesario para continuar."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 2. EJECUTAR EL SCRIPT DE INSTALACIÓN DE RUBY
|
||||
echo -e "\n${ICONO_INFO} Lanzando el instalador de la aplicación (dtic-BKPs_install.rb)..."
|
||||
echo -e "${CIAN}--------------------------------------------------${NC}"
|
||||
|
||||
ruby ./dtic-BKPs_install.rb
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs/lib/config.rb — Carga y guardado de config YAML
|
||||
# ==========================================================
|
||||
|
||||
require 'yaml'
|
||||
require 'fileutils'
|
||||
|
||||
module DTICBKPs
|
||||
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
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs/lib/logger.rb — Logger con colores y rotación
|
||||
# ==========================================================
|
||||
|
||||
require 'logger'
|
||||
require 'fileutils'
|
||||
|
||||
module DTICBKPs
|
||||
# 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, 'dtic-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
|
||||
@@ -0,0 +1,174 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs/lib/menu.rb — Menú TUI interactivo + editor CRUD
|
||||
# ==========================================================
|
||||
|
||||
module DTICBKPs
|
||||
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}║ dtic-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
|
||||
@@ -0,0 +1,150 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs/lib/proc_proxmox.rb — Procesador de backups Proxmox
|
||||
# ==========================================================
|
||||
|
||||
require 'tmpdir'
|
||||
require 'shellwords'
|
||||
require 'open3'
|
||||
require 'fileutils'
|
||||
|
||||
module DTICBKPs
|
||||
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
|
||||
@@ -0,0 +1,85 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-BKPs/lib/proc_xen.rb — Procesador de backups XEN (.xva)
|
||||
# ==========================================================
|
||||
|
||||
require 'shellwords'
|
||||
require 'open3'
|
||||
require 'fileutils'
|
||||
|
||||
module DTICBKPs
|
||||
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
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ==========================================================
|
||||
# dtic-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 DTICBKPs
|
||||
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
|
||||
Reference in New Issue
Block a user