Files
dtic-DIIAA/adn/tools/core/conciliador.rb
T
Ricardo Monla a5f71df15d [P2604-S13] Consolidar core/ — lógica de dominio a submódulos CLI
Arquitectura antes:
  core/nodos_info.rb    → usado por 4 CLIs
  core/ejecutor_remoto.rb → usado por proceso.rb
  core/triggers.rb      → usado por triggers.rb
  core/eventos.rb       → usado por triggers.rb
  core/configurador.rb  → usado por backup.rb

Arquitectura después:
  cli/nodos/info.rb       ← submódulo de nodos
  cli/ssh/ejecutor.rb     ← submódulo de ssh
  cli/triggers/motor.rb   ← submódulo de triggers
  cli/triggers/eventos.rb ← submódulo de triggers
  cli/backup/configurador.rb ← submódulo de backup

core/ queda limpio con solo infraestructura genuina:
  colores, constants, error_handler, logger, help_formatter,
  conciliador (transversal), validador (obsoleto)

- 15 tests pasan sin regresiones
- Evento 1078 (17:14→17:17)
2026-03-15 17:19:14 -03:00

208 lines
7.2 KiB
Ruby

# frozen_string_literal: true
require 'date'
require 'time'
require 'json'
require_relative '../cli/backup/configurador'
module ADN
class Conciliador
def initialize(logger)
@logger = logger
@candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
end
def ejecutar(silencioso: false)
@silencioso = silencioso
@logger.info("Iniciando saneamiento preventivo de pendientes...") unless @silencioso
# 1. Reconciliar backups de ayer y hoy
conciliar_backups_recientes
# 2. Traspasar pendientes no resueltos de ayer a hoy
traspasar_pendientes_ayer_a_hoy
end
private
def conciliar_backups_recientes
# Revisamos ayer y hoy
[Date.today - 1, Date.today].each do |fecha_obj|
fecha = fecha_obj.iso8601
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
next unless File.exist?(ruta)
# Si el archivo tiene relojes de arena (⏳)
contenido = File.read(ruta)
if contenido.include?('⏳')
@logger.info(" Revisando tareas pendientes del #{fecha}...") unless @silencioso
procesar_reconciliacion(fecha, ruta)
end
end
end
def procesar_reconciliacion(fecha, ruta)
# Obtener tareas de Proxmox
tasks = obtener_tareas_proxmox
return if tasks.empty?
vmid_map = cargar_mapa_vmid_nodos
contenido = File.read(ruta)
lineas = contenido.lines
nodo_actual = nil
cambio = false
lineas.each do |linea|
if linea.start_with?('### ') && !linea.include?('|')
nodo_actual = linea.sub('### ', '').strip
next
end
if nodo_actual && linea.include?('|') && linea.include?('⏳')
match_hora = linea.match(/\|\s*(\d{2}:\d{2})\s*\|/)
next unless match_hora
hora_inicio_str = match_hora[1]
vmid = vmid_map.key(nodo_actual)
next unless vmid
# Buscar tarea coincidente
hora_inicio_t = Time.parse("#{fecha} #{hora_inicio_str}") rescue nil
tarea = tasks.select { |t|
vmid_task = (t['vmid'] || t['id']).to_s
t['type'] == 'vzdump' &&
vmid_task == vmid &&
(t['status'] == 'OK' || !t['endtime'].nil?) &&
(hora_inicio_t ? Time.at(t['starttime']) >= (hora_inicio_t - 3600) : true)
}.max_by { |t| t['endtime'] || 0 }
if tarea && tarea['endtime']
exito = (tarea['status'] == 'OK')
hora_fin = Time.at(tarea['endtime']).strftime('%H:%M')
task_id = tarea['id']
@logger.exito(" ✨ Tarea auto-resuelta: #{nodo_actual} (#{hora_inicio_str} -> #{hora_fin})") unless @silencioso
# Aplicar cambio en el contenido
estado_ico = exito ? '✅' : '⚠️'
desc = exito ? 'Backup completado exitosamente.' : 'Fallo en la ejecución del backup.'
desc = "[task:#{task_id[0..7]}] #{desc}"
linea_nueva = "| #{hora_inicio_str} | #{hora_fin} | #{desc} | R | #{estado_ico} |\n"
# Reemplazo cuidadoso
regex_fila = /^\|\s*#{Regexp.escape(hora_inicio_str)}\s*\|\s*\|\s*[^|]*\|\s*[^|]*\|\s*[^|]*⏳[^|]*\|/
# Buscar el bloque del nodo
regex_seccion = /(### #{Regexp.escape(nodo_actual)}.*?)(?=\n###\s|<!--|\z)/m
contenido.sub!(regex_seccion) do |seccion|
nueva_seccion = seccion.sub(regex_fila, linea_nueva)
# Si ya no quedan ⏳ en esta sección, actualizamos su icono principal
unless nueva_seccion.include?('⏳')
nueva_seccion.gsub!(/#### [⏳⏳] - BKP-ADN/, "#### #{estado_ico} - BKP-ADN")
end
nueva_seccion
end
cambio = true
end
end
end
if cambio
File.write(ruta, contenido)
# Sincronizar con DB
system("#{File.join(ADN::PROJECT_ROOT, 'adn/tools/run')} db migrar:md #{fecha} --sobrescribir > /dev/null 2>&1")
end
end
def traspasar_pendientes_ayer_a_hoy
ayer = (Date.today - 1).iso8601
hoy = Date.today.iso8601
ruta_ayer = File.join(ADN::BITACORAS_DIR, "#{ayer}.md")
ruta_hoy = File.join(ADN::BITACORAS_DIR, "#{hoy}.md")
return unless File.exist?(ruta_ayer) && File.exist?(ruta_hoy)
cont_ayer = File.read(ruta_ayer)
cont_hoy = File.read(ruta_hoy)
# 1. Extraer sección Pendientes de ayer
pendientes_ayer = extraer_tabla_seccion(cont_ayer, '### Pendientes')
return if pendientes_ayer.empty?
# 2. Filtrar los que ya están en hoy (evitar duplicados)
nuevos_pendientes = pendientes_ayer.reject do |p|
cont_hoy.include?(p[:detalle])
end
return if nuevos_pendientes.empty?
# 3. Insertar en hoy
filas_nuevas = nuevos_pendientes.map { |p| "| #{p[:id]} | #{p[:nodo]} | #{p[:detalle]} |" }.join("\n")
# Regex más flexible para encontrar la tabla de pendientes en hoy
regex_tabla = /(### Pendientes\s*\n\s*\| ID \| Nodo \| Detalle \|\s*\n\s*\| :--- \| :--- \| :--- \|)/
if cont_hoy.match?(regex_tabla)
@logger.exito(" 🚀 Traspasando #{nuevos_pendientes.length} pendientes de ayer a hoy...")
cont_hoy.sub!(regex_tabla) { "#{$1}\n#{filas_nuevas}" }
File.write(ruta_hoy, cont_hoy)
system("#{File.join(ADN::PROJECT_ROOT, 'adn/tools/run')} db migrar:md #{hoy} --sobrescribir > /dev/null 2>&1")
end
end
def extraer_tabla_seccion(contenido, encabezado)
# Busca el bloque que empieza con el encabezado y captura todas las líneas que empiezan con |
regex = /#{Regexp.escape(encabezado)}[^\n]*\n+((?:\|[^\n]*\n?)+)/m
match = contenido.match(regex)
return [] unless match
filas = match[1].strip.split("\n")
return [] if filas.length < 3 # Necesitamos al menos Cabecera, Separador y 1 Fila
# Saltamos las primeras dos líneas (Cabecera y Separador)
filas[2..-1].map do |f|
parts = f.split('|').map(&:strip).reject(&:empty?)
next if parts.length < 3
{ id: parts[0], nodo: parts[1], detalle: parts[2] }
end.compact
end
def obtener_tareas_proxmox
return @tasks if @tasks
system("ruby #{@candados_path} authorize > /dev/null 2>&1")
@tasks = []
[ '10.0.10.201', '10.0.10.202', '10.0.10.203' ].each do |ip|
cmd = "pvesh get /cluster/tasks --output-format json"
output = `ruby #{@candados_path} run admindasu SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{ip} "#{cmd}"' 2>/dev/null`
if output.include?('[')
begin
json_str = output[output.index('[')..output.rindex(']')]
@tasks = JSON.parse(json_str)
break
rescue
next
end
end
end
system("ruby #{@candados_path} cerrar > /dev/null 2>&1")
@tasks
end
def cargar_mapa_vmid_nodos
mapa = {}
Dir.glob(File.join(ADN::NODOS_DIR, "*.md")).each do |f|
nombre = File.basename(f, ".md")
cont = File.read(f)
vmid = cont.match(/(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i)&.[](1)
mapa[vmid.to_s] = nombre if vmid
end
mapa
end
end
end