# 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 [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(/