[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
This commit is contained in:
@@ -36,6 +36,8 @@ module Dron
|
||||
SanadorComando.reintentar(args)
|
||||
when 'estado', 'status'
|
||||
VigilanteComando.estado(args)
|
||||
when 'ops', 'operaciones'
|
||||
OpsComando.ejecutar(args)
|
||||
when 'ayuda', 'help', '--help', '-h'
|
||||
mostrar_ayuda
|
||||
else
|
||||
@@ -59,6 +61,7 @@ module Dron
|
||||
lines << " #{Color::GREEN}flota#{Color::RESET} Dashboard compacto de la flota"
|
||||
lines << " #{Color::GREEN}salud#{Color::RESET} Health check activo (detecta zombies)"
|
||||
lines << " #{Color::GREEN}estado <ID>#{Color::RESET} Estado detallado de un dron"
|
||||
lines << " #{Color::GREEN}ops <acción>#{Color::RESET} Operaciones de infraestructura (update_os)"
|
||||
lines << " #{Color::GREEN}sanear#{Color::RESET} Sanear flota (reintentar + limpiar)"
|
||||
lines << " #{Color::GREEN}limpiar#{Color::RESET} Limpiar drones antiguos"
|
||||
lines << " #{Color::GREEN}reintentar#{Color::RESET} Re-intentar drones fallidos"
|
||||
@@ -74,6 +77,8 @@ module Dron
|
||||
lines << "\n#{Color::BOLD}Ejemplos:#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron lanzar --nota 'Backup SQL' -- vzdump 103#{Color::RESET}"
|
||||
lines << " #{Color::CYAN}Lanzar backup como dron#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron ops update_os --target linux#{Color::RESET}"
|
||||
lines << " #{Color::CYAN}Actualizar OS de todos los nodos Linux#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron flota#{Color::RESET}"
|
||||
lines << " #{Color::CYAN}Ver estado de la flota#{Color::RESET}"
|
||||
lines << " #{Color::DIM}$ ./adn/tools/run dron salud#{Color::RESET}"
|
||||
@@ -279,4 +284,11 @@ module Dron
|
||||
opciones
|
||||
end
|
||||
end
|
||||
|
||||
class OpsComando
|
||||
def self.ejecutar(args)
|
||||
require_relative 'ops'
|
||||
Dron::Ops.ejecutar(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user