Refactor(ADN): Migración de tools/adn a adn/tools en busca de la Armonía Integral del DIIAA
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'date'
|
||||
require 'time'
|
||||
require 'json'
|
||||
require_relative 'configurador'
|
||||
|
||||
module ADN
|
||||
class Conciliador
|
||||
def initialize(logger)
|
||||
@logger = logger
|
||||
@candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-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
|
||||
Reference in New Issue
Block a user