# frozen_string_literal: true # ========================================================== # adn/tools/bkps/lib/proc_linux.rb — Procesador Linux (apt) # # Fase 2: Soporta orquestación 1-dron-por-nodo # listar(tarea) → lista nodos Linux pendientes # ejecutar_uno(tarea, nodo) → actualiza UN solo nodo # ejecutar(tarea) → actualiza TODOS (legacy) # ========================================================== require 'shellwords' require 'net/ssh' module ADN module BKPs module Procesador class Linux def initialize(log) @log = log end # Scanner: devuelve lista de nodos Linux a actualizar # Lee los archivos .md de nodos/ y filtra los Linux (Debian/Ubuntu) def listar(tarea) nodos_dir = File.join(ADN::NODOS_DIR || 'nodos', '') unless Dir.exist?(nodos_dir) @log.error("Directorio de nodos no existe: #{nodos_dir}") return [] end # Filtrar nodos Linux (excluye Windows y otros) nodos_linux = Dir.glob(File.join(nodos_dir, '*.md')) .select { |f| es_nodo_linux?(f) } .map { |f| extraer_info_nodo(f) } .compact .reject { |n| n[:ip].nil? || n[:ip].empty? } nodos_linux.map do |nodo| { archivo: nodo[:hostname], # Identificador para ejecutar_uno vm: nodo[:hostname], # Nombre visible (nodo) ruta: nodo[:ip], # IP del nodo destino: nodo[:ip], # Mismo destino (es SSH) tamaño: 0, # No aplicable para updates pendiente: true, # Siempre verificar metadata: nodo # Info adicional (usuario, etc.) } end end # Atómico: actualiza UN solo nodo Linux vía SSH def ejecutar_uno(tarea, nombre_nodo) ruta_md = File.join(ADN::NODOS_DIR || 'nodos', "#{nombre_nodo}.md") nodo_info = File.exist?(ruta_md) ? extraer_info_nodo(ruta_md) : (tarea[:metadata] || {}) ip = nodo_info[:ip] || tarea[:origen] usuario = nodo_info[:usuario] || tarea[:usuario] || 'root' @log.info("🐧 Actualización Linux: #{nombre_nodo}") @log.info(" SSH: #{usuario}@#{ip}") begin Net::SSH.start(ip, usuario, keys_only: true, timeout: 10) do |ssh| # Variables de entorno para evitar interactividad env_vars = "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical" apt_opts = "-o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'" # Paso 1: apt update @log.info(" [1/3] Ejecutando apt update...") output_update = ssh.exec!("sudo #{env_vars} apt-get update 2>&1") @log.info(" #{output_update.lines.last&.strip || 'OK'}") # Paso 2: apt upgrade -y @log.info(" [2/3] Ejecutando apt upgrade -y...") output_upgrade = ssh.exec!("sudo #{env_vars} apt-get upgrade -y #{apt_opts} 2>&1") @log.info(" #{output_upgrade.lines.last&.strip || 'OK'}") # Paso 3: apt autoremove -y (limpieza) @log.info(" [3/3] Ejecutando apt autoremove -y...") output_autoremove = ssh.exec!("sudo #{env_vars} apt-get autoremove -y 2>&1") @log.info(" #{output_autoremove.lines.last&.strip || 'OK'}") @log.info("✔ Nodo #{nombre_nodo} actualizado exitosamente.") true end rescue => e @log.error("✗ Error actualizando #{nombre_nodo}: #{e.message}") false end end # Legacy: actualiza TODOS los nodos (compatibilidad) def ejecutar(tarea) @log.info("--- #{tarea[:texto]} ---") @log.info("Origen: #{tarea[:origen]}") nodos = listar(tarea) if nodos.empty? @log.info("Sin nodos Linux encontrados.") return true end @log.info("Encontrados #{nodos.length} nodo(s) Linux.") actualizados = 0 fallidos = 0 nodos.each do |nodo| @log.info("\n" + "=" * 50) if ejecutar_uno(tarea, nodo[:vm]) actualizados += 1 else fallidos += 1 end end @log.info("\n" + "=" * 50) @log.info("✔ #{actualizados} nodo(s) actualizado(s), #{fallidos} fallido(s).") fallidos == 0 end private # Verifica si el nodo es Linux (busca Debian, Ubuntu, Linux en el .md) def es_nodo_linux?(archivo_md) contenido = File.read(archivo_md) # Excluir explícitamente Windows return false if contenido.match?(/Windows\s+(Server\s+)?\d+/i) return false if contenido.match?(/Windows\s+10/i) return false if contenido.match?(/Windows\s+11/i) # Incluir Linux/Debian/Ubuntu return true if contenido.match?(/Debian/i) return true if contenido.match?(/Ubuntu/i) return true if contenido.match?(/Linux/i) # Incluir si tiene IP LAN y es srvv-* o srv-* (convención de nombres) hostname = File.basename(archivo_md, '.md') return true if hostname.match?(/^srvv?-/) false end # Extrae información del nodo desde el archivo .md def extraer_info_nodo(archivo_md) contenido = File.read(archivo_md) hostname = File.basename(archivo_md, '.md') # Extraer IP LAN ip = contenido.match(/\*\*IP(?:\s+LAN)?\*\*:\s*`?(\d+\.\d+\.\d+\.\d+)`?/i)&.[](1) || contenido.match(/\|\s*IP(?:\s+LAN)?[^\|]*\|\s*`?(\d+\.\d+\.\d+\.\d+)`?\s*\|/i)&.[](1) # Extraer usuario SSH (si está especificado) usuario = contenido.match(/\*\*Usuario\s*SSH\*\*:\s*`?(\w+)`?/i)&.[](1) || contenido.match(/\*\*SSH\*\*:\s*`?ssh\s+(\w+)@/i)&.[](1) || 'root' # Extraer estado (para filtrar nodos offline) estado = contenido.match(/\*\*Estado\*\*:\s*(🟢|🔴|⚫|🚧|🟡)/i)&.[](1) { hostname: hostname, ip: ip, usuario: usuario, estado: estado || '🟢' } end end end end end