Files
Ricardo Monla 892019fa22 [A04.P005] DASUTEN: Pipeline refactorizado + Documentación + Informes
Refactorización del pipeline de backups DASUTEN:

1. Flujo simplificado (un solo destino):
   - Backup directo a X:\ (share srv-ns8)
   - Eliminado paso intermedio E:\BK_SQL\sysdasuten\
   - Reducido de 3 ubicaciones a 2 (X: → /var/tmp/)

2. Módulo común DasuExecutor (lib/dasu_executor.rb):
   - Centraliza ejecución PowerShell en dasu-sql4
   - Centraliza verificación de archivos en fenix
   - Elimina código repetido en 6 drones (-52% líneas)

3. Drones refactorizados:
   - dasuten_exportar_srvv-fenix.rb (FULL → X:)
   - dasuten_exportar_diferencial_srvv-fenix.rb (DIF → X:)
   - dasuten_transferir_srvv-fenix-srv-ns8.rb (simplificado)
   - dasuten_restaurar_dasu-sql4.rb (usa DasuExecutor)
   - dasuten_restaurar_diferencial_dasu-sql4.rb (usa DasuExecutor)
   - dasuten_download_drive-dasu-sql4.rb (usa DasuExecutor)
   - dasuten_verificar-integridad_dasu-sql4.rb (usa DasuExecutor)

4. Integración BKPs mantenida:
   - T8-T15: Tareas individuales
   - C7: dasuten_full (pipeline semanal)
   - C8: dasuten_diferencial (pipeline diario)

5. Documentación actualizada:
   - A04.P006_Backups-DASUTEN.md: flujo simplificado + módulo común
   - Lecciones aprendidas agregadas

6. Nuevos archivos:
   - Informes técnicos DASUTEN (docs/informes/2026-04_DASUTEN-migracion/)
   - A01.P011/P012: Documentación de drones y pipeline
   - proc_linux.rb, ops.rb: Utilitarios del ecosistema

7. Scripts legacy (para referencia):
   - orquestador_pipeline.rb, orquestador_pipeline_diferencial.rb
   - dasuten_upload_srv-ns8-drive.rb
2026-04-14 20:06:06 -03:00

294 lines
12 KiB
Ruby

# frozen_string_literal: true
# ==========================================================
# Dron::Ops — Operaciones de Infraestructura via AtomicDrones
# ----------------------------------------------------------
# Responsabilidad: Orquestar tareas de mantenimiento de nodos
# (update_os, etc.) usando 1 dron ejecutor por nodo.
#
# Reutiliza:
# - NodosInfo (scanner de fichas .md)
# - Dron::Ejecutor (ejecución atómica con auto-bitácora)
#
# Uso:
# ./adn/tools/run dron ops update_os --target linux
# ./adn/tools/run dron ops update_os --nodo srvv-dns
# ./adn/tools/run dron ops update_os --target linux --dry-run
#
# Plan: A01.P011 — Drones de Operaciones de Infraestructura
# ==========================================================
require 'optparse'
require 'shellwords'
require_relative 'base'
require_relative 'ejecutor'
require_relative '../nodos/info'
require_relative '../../core/colores'
require_relative '../../core/constants'
module Dron
class Ops
# Comandos SSH para actualización desatendida
APT_ENV = 'DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical'
APT_OPTS = "-o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold'"
APT_CMD = "sudo #{APT_ENV} apt-get update 2>&1 && " \
"sudo #{APT_ENV} apt-get upgrade -y #{APT_OPTS} 2>&1 && " \
"sudo #{APT_ENV} apt-get autoremove -y 2>&1"
class << self
# ─── Punto de entrada CLI ──────────────────────────────────
def ejecutar(args)
accion = args.shift
case accion
when 'update_os'
cmd_update_os(args)
when 'ayuda', 'help', '--help', '-h', nil
mostrar_ayuda
else
Base.logger.error("❌ Acción desconocida para ops: #{accion}")
mostrar_ayuda
exit 1
end
end
private
# ─── update_os ─────────────────────────────────────────────
def cmd_update_os(args)
opciones = parsear_opciones(args)
# Obtener nodos candidatos
nodos = escanear_nodos(opciones)
if nodos.empty?
puts "#{Color::YELLOW}⚠ Sin nodos Linux encontrados para actualizar.#{Color::RESET}"
return
end
mostrar_resumen(nodos, opciones)
return if opciones[:dry_run]
# Orquestar: 1 dron ejecutor por nodo
resultados = orquestar_nodos(nodos, opciones)
# Resumen final
mostrar_resultados(resultados, nodos.length)
exit 1 if resultados[:failed] > 0 && opciones[:on_error] == 'abort'
end
# ─── Scanner: reutiliza NodosInfo ──────────────────────────
def escanear_nodos(opciones)
nodos_dir = ADN::NODOS_DIR
unless Dir.exist?(nodos_dir)
Base.logger.error("Directorio de nodos no existe: #{nodos_dir}")
return []
end
candidatos = Dir.glob(File.join(nodos_dir, '*.md'))
.reject { |f| File.basename(f).start_with?('_') }
.map { |f| ADN::NodosInfo.extraer_metadata(File.basename(f, '.md')) }
.compact
# Filtrar por OS
if opciones[:target] == 'linux'
candidatos.select! { |n| n[:so] == :linux }
end
# Excluir nodos sin IP
candidatos.reject! { |n| n[:ip].nil? || n[:ip].to_s.strip.empty? }
# Excluir nodos offline
candidatos.reject! { |n| n[:estado]&.to_s&.include?('🔴') }
# Filtrar nodo específico
if opciones[:nodo]
candidatos.select! { |n| n[:nombre] == opciones[:nodo] }
end
# Filtrar por ring
if opciones[:ring]
candidatos.select! { |n| n[:ring] == opciones[:ring] }
end
candidatos
end
# ─── Orquestación: 1 dron por nodo ─────────────────────────
def orquestar_nodos(nodos, opciones)
resultados = { ok: 0, failed: 0, skipped: 0 }
abortado = false
nodos.each_with_index do |nodo, i|
break if abortado
puts " #{Color::BOLD}[#{i + 1}/#{nodos.length}]#{Color::RESET} " \
"#{Color::CYAN}#{nodo[:nombre]}#{Color::RESET} (#{nodo[:ip]})"
# Construir comando SSH para este nodo
usuario = nodo[:usuario] || 'root'
puerto = nodo[:puerto_ssh] || 22
ssh_cmd = construir_ssh_cmd(nodo, usuario, puerto)
# Lanzar dron ejecutor (auto-bitácora incluida)
nota = "🐧 Actualizar #{nodo[:nombre]}"
resultado = Dron::Ejecutor.lanzar(
cmd: ssh_cmd,
nota: nota,
timeout: opciones[:timeout],
nodo: nodo[:nombre]
)
if resultado[:exit_code] == 0
puts " #{Color::GREEN}✔ Completado#{Color::RESET} (#{resultado[:duration]}s)"
resultados[:ok] += 1
else
resultados[:failed] += 1
case opciones[:on_error]
when 'abort'
puts " #{Color::RED}✖ Falló — deteniendo orquestación#{Color::RESET}"
abortado = true
resultados[:skipped] = nodos.length - i - 1
when 'continue'
puts " #{Color::YELLOW}⚠ Falló — continuando con el siguiente#{Color::RESET}"
end
end
puts ""
end
resultados
end
# ─── Construcción de comando SSH ───────────────────────────
def construir_ssh_cmd(nodo, usuario, puerto)
ssh_opts = "-o StrictHostKeyChecking=no -o ConnectTimeout=10"
# Puerto no estándar
ssh_opts += " -p #{puerto}" if puerto != 22
# Opciones especiales (legacy servers como XenServer)
ssh_opts += " #{nodo[:ssh_opts]}" if nodo[:ssh_opts]
# Clave SSH específica
if nodo[:clave_ssh]
clave = nodo[:clave_ssh].gsub('~', ENV['HOME'] || '/root')
ssh_opts += " -i #{clave}"
end
remote_cmd = APT_CMD
# Determinar método de autenticación
if nodo[:clave_boveda]
# AUTH POR PASSWORD: Usar candados run + sshpass
# candados inyecta $PASS en el entorno, sshpass lo consume
adn_run = File.join(ADN::PROJECT_ROOT, 'adn/tools/run')
"#{adn_run} candados run #{nodo[:clave_boveda]} " \
"'sshpass -p $PASS ssh #{ssh_opts} #{usuario}@#{nodo[:ip]} \"#{remote_cmd}\"'"
else
# AUTH POR CLAVE SSH: Conexión directa
"ssh #{ssh_opts} #{usuario}@#{nodo[:ip]} '#{remote_cmd}'"
end
end
# ─── Presentación ─────────────────────────────────────────
def mostrar_resumen(nodos, opciones)
puts ""
puts "#{Color::BOLD}#{Color::CYAN}🐧 Dron Ops — Actualización de OS Linux#{Color::RESET}"
puts "═" * 60
puts " Nodos detectados: #{nodos.length}"
puts " Target: #{opciones[:target]}"
puts " Ring: #{opciones[:ring] || 'todos'}" if opciones[:ring]
puts " On-error: #{opciones[:on_error]}"
puts " Timeout por nodo: #{opciones[:timeout]}s"
if opciones[:dry_run]
puts " Modo: #{Color::YELLOW}DRY-RUN (sin ejecutar)#{Color::RESET}"
end
puts ""
if opciones[:dry_run]
nodos.each_with_index do |nodo, i|
auth = nodo[:clave_boveda] ? "🔐 #{nodo[:clave_boveda]}" : "🔑 key"
ring_tag = nodo[:ring] ? " [#{nodo[:ring]}]" : ""
puts " #{Color::DIM}[#{i + 1}]#{Color::RESET} #{nodo[:nombre].ljust(18)} " \
"#{Color::DIM}#{nodo[:ip].ljust(16)} #{(nodo[:usuario] || 'root').ljust(8)} #{auth}#{ring_tag}#{Color::RESET}"
end
puts ""
puts "#{Color::YELLOW}[DRY-RUN] No se lanzaron drones.#{Color::RESET}"
end
end
def mostrar_resultados(resultados, total)
puts "#{Color::BOLD}🐧 Resumen de Operaciones#{Color::RESET}"
puts "═" * 40
puts " ✅ Exitosos: #{resultados[:ok]}"
puts " ❌ Fallidos: #{resultados[:failed]}" if resultados[:failed] > 0
puts " ⏭️ Saltados: #{resultados[:skipped]}" if resultados[:skipped] > 0
puts "#{Color::BOLD}🐧 Operación #{resultados[:failed] > 0 ? 'con errores' : 'finalizada'}.#{Color::RESET}"
end
# ─── Parser de opciones ────────────────────────────────────
def parsear_opciones(args)
opciones = {
target: 'linux',
nodo: nil,
ring: nil,
on_error: 'continue',
timeout: 600,
dry_run: false
}
OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run dron ops update_os [opciones]"
opts.on("--target OS", "OS a actualizar (linux)") { |t| opciones[:target] = t }
opts.on("--nodo NOMBRE", "Actualizar solo este nodo") { |n| opciones[:nodo] = n }
opts.on("--ring RING", %w[test standard core], "Solo nodos de este ring (test|standard|core)") { |r| opciones[:ring] = r }
opts.on("--on-error MODE", %w[abort continue], "Qué hacer si un dron falla (abort|continue)") { |m| opciones[:on_error] = m }
opts.on("--timeout N", Integer, "Timeout por nodo en segundos (default: 600)") { |t| opciones[:timeout] = t }
opts.on("--dry-run", "Solo listar nodos sin ejecutar") { opciones[:dry_run] = true }
end.parse!(args)
opciones
end
def mostrar_ayuda
puts "#{Color::BOLD}🐧 Dron Ops — Operaciones de Infraestructura#{Color::RESET}"
puts "#{Color::DIM}#{'=' * 50}#{Color::RESET}"
puts ""
puts "#{Color::CYAN}Operaciones de mantenimiento sobre nodos vía AtomicDrones#{Color::RESET}"
puts ""
puts "Uso: #{Color::YELLOW}./adn/tools/run dron ops <acción> [opciones]#{Color::RESET}"
puts ""
puts "#{Color::BOLD}Acciones:#{Color::RESET}"
puts " #{Color::GREEN}update_os#{Color::RESET} Actualizar sistema operativo de nodos"
puts ""
puts "#{Color::BOLD}Opciones:#{Color::RESET}"
puts " #{Color::GREEN}--target OS#{Color::RESET} OS a actualizar: linux (default: linux)"
puts " #{Color::GREEN}--nodo NOMBRE#{Color::RESET} Actualizar solo un nodo específico"
puts " #{Color::GREEN}--on-error MODE#{Color::RESET} abort|continue (default: continue)"
puts " #{Color::GREEN}--timeout N#{Color::RESET} Timeout por nodo en segundos (default: 600)"
puts " #{Color::GREEN}--dry-run#{Color::RESET} Solo listar sin ejecutar"
puts ""
puts "#{Color::BOLD}Ejemplos:#{Color::RESET}"
puts " #{Color::DIM}$ ./adn/tools/run dron ops update_os --target linux --dry-run#{Color::RESET}"
puts " #{Color::CYAN}Listar nodos Linux sin actualizar#{Color::RESET}"
puts " #{Color::DIM}$ ./adn/tools/run dron ops update_os --nodo srvv-dns#{Color::RESET}"
puts " #{Color::CYAN}Actualizar solo srvv-dns#{Color::RESET}"
puts " #{Color::DIM}$ ./adn/tools/run dron ops update_os --on-error continue#{Color::RESET}"
puts " #{Color::CYAN}Actualizar toda la flota Linux continuando ante fallos#{Color::RESET}"
puts ""
end
end
end
end