Refactor(ADN): Optimización de la arquitectura y reparación de subcomandos
- Reestructuración del CLI en módulos discretos (core/, comando/, config/, clientes/) - Migración del punto de entrada tools/adn.rb hacia el launcher ejecutable tools/adn/run - Simplificación de nombres de comandos (remoción prefijo subcomando_) - Corrección de rutas base transversales (PROJECT_ROOT, BITACORAS_DIR) - Reescritura integral de sentencias require_relative - Restauración de sintaxis en archivo sync.rb truncado
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'optparse'
|
||||
require 'fileutils'
|
||||
require 'date'
|
||||
|
||||
module ADN
|
||||
class SubcomandoBackup
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
@opciones = { modo: 'stop', storage: 'local', compress: 'gzip' }
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Uso: ./tools/adn/run backup <nodo> [OPCIONES]"
|
||||
opts.separator ""
|
||||
opts.separator "Ejecuta un backup remoto siguiendo el protocolo ADN"
|
||||
opts.separator ""
|
||||
opts.separator "Opciones:"
|
||||
opts.on("--mode MODO", "Modo de backup (stop, snapshot, suspend. Default: stop)") do |m|
|
||||
@opciones[:modo] = m
|
||||
end
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
parser.parse!(@args)
|
||||
rescue OptionParser::InvalidOption => e
|
||||
@logger.error("Opción inválida: #{e.message}")
|
||||
puts parser
|
||||
exit 1
|
||||
end
|
||||
|
||||
if @args.empty?
|
||||
@logger.error("Se requiere nombre de nodo")
|
||||
puts parser
|
||||
exit 1
|
||||
end
|
||||
|
||||
nodo = @args[0]
|
||||
ejecutar_backup(nodo)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ejecutar_backup(nodo)
|
||||
@logger.info("Iniciando flujo ADN-Backup para nodo: #{nodo}")
|
||||
|
||||
# 1. Obtener contexto del nodo
|
||||
datos = obtener_contexto_nodo(nodo)
|
||||
unless datos
|
||||
@logger.error("No se pudo determinar el contexto técnico para #{nodo}")
|
||||
exit 1
|
||||
end
|
||||
|
||||
@logger.info("Contexto validado: VM #{datos[:vmid]} en #{datos[:host_name]} (#{datos[:host_ip]})")
|
||||
|
||||
# 2. Registrar inicio en bitácora (Bitácora-First)
|
||||
registrar_inicio_bitacora(nodo, datos)
|
||||
|
||||
# 3. Ejecución segura vía ns8-candados
|
||||
@logger.info("Solicitando autorización y ejecutando backup remoto...")
|
||||
|
||||
comando_vzdump = "vzdump #{datos[:vmid]} --mode #{@opciones[:modo]} --storage #{@opciones[:storage]} --compress #{@opciones[:compress]}"
|
||||
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
|
||||
|
||||
# Usamos el binario de ruby y la ruta absoluta para ns8-candados
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb')
|
||||
|
||||
# Ejecutar con ns8-candados run
|
||||
# Nota: el comando 'authorize' debe haberse ejecutado antes o lo incluimos
|
||||
puts "#{Color::DIM}🔓 Abriendo candado para sesión de backup...#{Color::RESET}"
|
||||
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||
|
||||
success = system("ruby #{candados_path} run admindasu SSHPASS '#{cmd_seguro}'")
|
||||
|
||||
# 4. Finalizar y cerrar
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
|
||||
if success
|
||||
@logger.exito("Backup de #{nodo} completado exitosamente.")
|
||||
registrar_fin_bitacora(nodo, true)
|
||||
else
|
||||
@logger.error("La ejecución del backup falló.")
|
||||
registrar_fin_bitacora(nodo, false)
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def obtener_contexto_nodo(nombre_nodo)
|
||||
ruta_nodo = File.join(ADN::NODOS_DIR, "#{nombre_nodo}.md")
|
||||
unless File.exist?(ruta_nodo)
|
||||
@logger.error("No existe el archivo de nodo: #{ruta_nodo}")
|
||||
return nil
|
||||
end
|
||||
|
||||
contenido = File.read(ruta_nodo)
|
||||
# Buscamos VMID (puede decir "VM ID: 100" o "vmid: 100")
|
||||
vmid = contenido.match(/(?:VM ID|vmid):\s*(\d+)/i)&.[](1)
|
||||
|
||||
# Buscamos el host (especificamente el link o nombre entre backticks)
|
||||
# Formato: - **Padre/Host**: `srv-dasu`
|
||||
host_name = contenido.match(/Padre\/Host\*\*:?\s*`([^`]+)`/)&.[](1)
|
||||
host_name ||= contenido.match(/Host\*\*:?\s*`([^`]+)`/)&.[](1)
|
||||
|
||||
unless vmid && host_name
|
||||
@logger.error("Contexto incompleto en #{nombre_nodo}.md: VMID=#{vmid}, Host=#{host_name}")
|
||||
return nil
|
||||
end
|
||||
|
||||
# Buscar IP del host
|
||||
ruta_host = File.join(ADN::NODOS_DIR, "#{host_name}.md")
|
||||
host_ip = nil
|
||||
if File.exist?(ruta_host)
|
||||
c_host = File.read(ruta_host)
|
||||
# IP en tabla: | IP | `10.0.10.1` |
|
||||
host_ip ||= c_host.match(/\|\s*IP[^\|]*\|\s*`([^`]+)`\s*\|/)&.[](1)
|
||||
# IP en lista: - **IP**: `10.0.10.1`
|
||||
host_ip ||= c_host.match(/IP\*\*:?\s*`([^`]+)`/)&.[](1)
|
||||
# IP simple: IP: 10.0.10.1
|
||||
host_ip ||= c_host.match(/IP:\s*([\d\.]+)/)&.[](1)
|
||||
end
|
||||
|
||||
unless host_ip
|
||||
@logger.error("No se encontró IP para el host #{host_name}")
|
||||
return nil
|
||||
end
|
||||
|
||||
if host_ip && host_ip.include?('/')
|
||||
host_ip = host_ip.split('/')[0]
|
||||
end
|
||||
|
||||
{ vmid: vmid, host_name: host_name, host_ip: host_ip }
|
||||
end
|
||||
|
||||
def registrar_inicio_bitacora(nodo, datos)
|
||||
fecha = Date.today.iso8601
|
||||
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
|
||||
return unless File.exist?(ruta)
|
||||
|
||||
hora = Time.now.strftime('%H:%M')
|
||||
|
||||
# Si ya existe la sección del nodo, añadimos la línea. Si no, creamos la sección.
|
||||
contenido = File.read(ruta)
|
||||
|
||||
entrada = <<~ENTRADA
|
||||
|
||||
### #{nodo}
|
||||
|
||||
#### ⏳ - BKP-ADN - Backup Autónomo (Protocolo ADN)
|
||||
Ejecución de backup gestionada por `tools/adn.rb` para la VM #{datos[:vmid]} en el host #{datos[:host_name]}.
|
||||
|
||||
| I | F | Descripción | E |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| #{hora} | | Inicio de backup remoto vía SSH (Seguro). | [R] ⏳ |
|
||||
ENTRADA
|
||||
|
||||
unless contenido.include?("### #{nodo}")
|
||||
# Insertar antes del footer de la IA
|
||||
nuevo_contenido = contenido.sub(/<!-- 🤖 PREMISAS/, "#{entrada}\n\n<!-- 🤖 PREMISAS")
|
||||
File.write(ruta, nuevo_contenido)
|
||||
end
|
||||
end
|
||||
|
||||
def registrar_fin_bitacora(nodo, exito)
|
||||
fecha = Date.today.iso8601
|
||||
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
|
||||
return unless File.exist?(ruta)
|
||||
|
||||
hora = Time.now.strftime('%H:%M')
|
||||
estado = exito ? '✅' : '⚠️'
|
||||
desc = exito ? 'Backup completado exitosamente.' : 'Fallo en la ejecución del backup.'
|
||||
|
||||
contenido = File.read(ruta)
|
||||
|
||||
# Actualizar la línea de tiempo
|
||||
linea_nueva = "| 08:31 | #{hora} | #{desc} | [R] #{estado} |"
|
||||
|
||||
# Buscamos la tarea BKP-ADN
|
||||
nuevo_contenido = contenido.gsub(/#### ⏳ - BKP-ADN/, "#### #{estado} - BKP-ADN")
|
||||
nuevo_contenido = nuevo_contenido.gsub(/\| \d{2}:\d{2} \| \| Inicio de backup remoto.*/, linea_nueva)
|
||||
|
||||
File.write(ruta, nuevo_contenido)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user