[ADN] Commit final DB-First - Actualización completa de documentación, herramientas y componentes
This commit is contained in:
+49
-2
@@ -28,14 +28,18 @@ class SubcomandoAyuda
|
||||
#{Color::YELLOW}Subcomandos disponibles:#{Color::RESET}
|
||||
validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
|
||||
contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
|
||||
nodos <acción> [opciones] Gestión de nodos (agrupar, listar, buscar)
|
||||
jornada <acción> [modo] [hora] Gestión de jornada (iniciar, cerrar, estado)
|
||||
salud [--dashboard] [--json] Ver salud general del sistema ADN
|
||||
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
|
||||
generar nodo <nombre> Generar ficha de nodo
|
||||
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
|
||||
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
|
||||
#{Color::YELLOW}inicio de jornada [presencial|remoto]#{Color::RESET} Inicio de jornada laboral
|
||||
#{Color::YELLOW}cierre de jornada [presencial|remoto]#{Color::RESET} Cierre de jornada laboral
|
||||
#{Color::YELLOW}db <subcomando>#{Color::RESET} Operaciones de base de datos
|
||||
#{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers
|
||||
#{Color::YELLOW}plan [subcomando]#{Color::RESET} Gestión de planes y progreso
|
||||
#{Color::YELLOW}proceso <nombre>#{Color::RESET} Ejecutar procesos técnicos automatizados
|
||||
#{Color::YELLOW}estados#{Color::RESET} Listar tareas en proceso (⏳)
|
||||
#{Color::YELLOW}ayuda [subcomando]#{Color::RESET} Mostrar esta ayuda o ayuda específica
|
||||
|
||||
#{Color::YELLOW}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}
|
||||
@@ -79,6 +83,35 @@ class SubcomandoAyuda
|
||||
puts "Ejemplo:"
|
||||
puts " ./adn/tools/run contexto srv-pmox1"
|
||||
puts " ./adn/tools/run contexto srvv-fenix --cmd"
|
||||
when 'nodos'
|
||||
puts "Uso: ./adn/tools/run nodos <acción> [opciones]"
|
||||
puts ""
|
||||
puts "Gestión y consulta de nodos de infraestructura:"
|
||||
puts " • agrupar [--servidor SRV] Agrupar VMs por servidor anfitrión"
|
||||
puts " • listar [--tipo TIPO] Listar fichas de nodos (.md)"
|
||||
puts " • buscar <término> Buscar nodos por nombre o contenido"
|
||||
puts ""
|
||||
puts "Ejemplos:"
|
||||
puts " ./adn/tools/run nodos agrupar"
|
||||
puts " ./adn/tools/run nodos agrupar --servidor srv-pmox1"
|
||||
puts " ./adn/tools/run nodos listar --tipo srvv"
|
||||
puts " ./adn/tools/run nodos buscar dasuten"
|
||||
when 'jornada'
|
||||
puts "Uso: ./adn/tools/run jornada <acción> [modo] [hora] [opciones]"
|
||||
puts ""
|
||||
puts "Gestión de jornada laboral:"
|
||||
puts " • iniciar [presencial|remoto] [HH:MM] Registrar inicio"
|
||||
puts " • cerrar [presencial|remoto] [HH:MM] Registrar cierre"
|
||||
puts " • estado Ver estado actual"
|
||||
puts ""
|
||||
puts "Opciones:"
|
||||
puts " --nodo NOMBRE Nodo a registrar (default: dtic-DIIAA)"
|
||||
puts " --fecha YYYY-MM-DD Fecha de la bitácora (default: hoy)"
|
||||
puts ""
|
||||
puts "Ejemplos:"
|
||||
puts " ./adn/tools/run jornada iniciar presencial 08:00"
|
||||
puts " ./adn/tools/run jornada cerrar remoto 23:00"
|
||||
puts " ./adn/tools/run jornada estado"
|
||||
when 'salud'
|
||||
puts "Uso: ./adn/tools/run salud [--dashboard] [--json]"
|
||||
puts ""
|
||||
@@ -102,6 +135,20 @@ class SubcomandoAyuda
|
||||
puts " ./adn/tools/run generar bitacora 2026-03-08"
|
||||
puts " ./adn/tools/run generar nodo srv-nuevo"
|
||||
puts " ./adn/tools/run generar proyecto P2604"
|
||||
when 'proceso'
|
||||
puts "Uso: ./adn/tools/run proceso <nombre_proceso> [argumentos]"
|
||||
puts ""
|
||||
puts "Ejecuta procesos técnicos automatizados sobre la infraestructura:"
|
||||
puts " • nodos:actualizar_so <nodo> Actualiza paquetes y parches de seguridad"
|
||||
puts ""
|
||||
puts "Ejemplos:"
|
||||
puts " ./adn/tools/run proceso nodos:actualizar_so srv-ns8"
|
||||
puts " ./adn/tools/run proceso nodos:actualizar_so srv-dasu"
|
||||
when 'estados'
|
||||
puts "Uso: ./adn/tools/run estados"
|
||||
puts ""
|
||||
puts "Consulta la bitácora web para listar todos los items con estado ⏳ (En proceso)."
|
||||
puts "Ideal para monitorear procesos automatizados o jornadas abiertas."
|
||||
else
|
||||
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
|
||||
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
|
||||
|
||||
+16
-12
@@ -96,7 +96,7 @@ module ADN
|
||||
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
|
||||
|
||||
# Usamos el binario de ruby y la ruta absoluta para ns8-candados
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb')
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
|
||||
|
||||
# Ejecutar con ns8-candados run
|
||||
# Ejecutar con ns8-candados autorizando primero
|
||||
@@ -108,7 +108,8 @@ module ADN
|
||||
# Usar popen para leer la salida en tiempo real
|
||||
@logger.info("Conectando y monitoreando salida de Proxmox...")
|
||||
|
||||
IO.popen("ruby #{candados_path} run admindasu SSHPASS '#{cmd_seguro}' 2>&1") do |pf|
|
||||
clave = (@nodo[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
IO.popen("ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1") do |pf|
|
||||
pf.each_line do |line|
|
||||
# Imprimir flujo para el usuario
|
||||
puts " #{Color::DIM}#{line.strip}#{Color::RESET}"
|
||||
@@ -278,13 +279,13 @@ module ADN
|
||||
# Lista de hosts a consultar
|
||||
hosts = [
|
||||
{ nombre: 'srv-pmox1', ip: '10.0.10.201' },
|
||||
{ nombre: 'srv-dasu', ip: '10.0.10.205' }
|
||||
{ nombre: 'srv-dasu', ip: '100.116.210.36' },
|
||||
]
|
||||
|
||||
puts "\n#{Color::BOLD}🔍 Estado de Backups en Tiempo Real (Proxmox)#{Color::RESET}"
|
||||
puts "─" * 60
|
||||
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb')
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
|
||||
# Asegurar autorización general para el lote
|
||||
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||
|
||||
@@ -295,10 +296,11 @@ module ADN
|
||||
|
||||
# Intentamos obtener tareas
|
||||
cmd = "/usr/bin/pvesh get /cluster/tasks --output-format json"
|
||||
cmd_seguro = "ssh -o StrictHostKeyChecking=no root@#{h[:ip]} '#{cmd}'"
|
||||
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{h[:ip]} '#{cmd}'"
|
||||
|
||||
# Ejecutamos vía candados run
|
||||
output = `ruby #{candados_path} run admindasu SSHPASS '#{cmd_seguro}' 2>&1`
|
||||
# Ejecutamos vía candados run usando la clave adecuada
|
||||
clave = (h[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
output = `ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1`
|
||||
|
||||
if output.include?('[')
|
||||
begin
|
||||
@@ -370,12 +372,13 @@ module ADN
|
||||
|
||||
# 1. Obtener lista de VMs directamente de Proxmox para nombres precisos
|
||||
host = { nombre: 'srv-pmox1', ip: '10.0.10.201' }
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb')
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
|
||||
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||
|
||||
@logger.info("Consultando inventario de VMs en Proxmox...")
|
||||
cmd_vms = "/usr/bin/pvesh get /cluster/resources --type vm --output-format json"
|
||||
output_vms = `ruby #{candados_path} run admindasu SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{host[:ip]} "#{cmd_vms}"' 2>/dev/null`
|
||||
clave = (host[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
output_vms = `ruby #{candados_path} run #{clave} SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{host[:ip]} "#{cmd_vms}"' 2>/dev/null`
|
||||
|
||||
vmid_map = cargar_mapa_vmid_nodos # Cargar local primero
|
||||
|
||||
@@ -400,7 +403,7 @@ module ADN
|
||||
output_ls = ""
|
||||
[ '10.0.10.201', '10.0.10.202', '10.0.10.203' ].each do |ip|
|
||||
cmd_ls = "ls -1 --full-time #{ruta_dump}/vzdump-*"
|
||||
res = `ruby #{candados_path} run admindasu SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{ip} "#{cmd_ls}"' 2>&1`
|
||||
res = `ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{ip} "#{cmd_ls}"' 2>&1`
|
||||
unless res.include?('No such file') || res.include?('Connection refused')
|
||||
output_ls = res
|
||||
break
|
||||
@@ -535,8 +538,9 @@ module ADN
|
||||
b[:archivos].each do |a|
|
||||
print " Eliminando #{a[:nombre]}... "
|
||||
del_cmd = "rm -f '#{a[:ruta]}'"
|
||||
del_seguro = "ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'"
|
||||
system("ruby #{candados_path} run admindasu SSHPASS '#{del_seguro}' > /dev/null 2>&1")
|
||||
del_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'"
|
||||
clave = (b[:nodo] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
system("ruby #{candados_path} run #{clave} SSHPASS '#{del_seguro}' > /dev/null 2>&1")
|
||||
puts "#{Color::GREEN}OK#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -80,6 +80,8 @@ module ADN
|
||||
comando_evento_listar(sub_args)
|
||||
when 'evento:actualizar'
|
||||
comando_evento_actualizar(sub_args)
|
||||
when 'evento:eliminar'
|
||||
comando_evento_eliminar(sub_args)
|
||||
when 'nodo:crear'
|
||||
comando_nodo_crear(sub_args)
|
||||
when 'nodo:listar'
|
||||
|
||||
@@ -256,5 +256,29 @@ module ADN
|
||||
end
|
||||
end
|
||||
|
||||
# Comando: evento:eliminar <ID> - Eliminar una entrada existente
|
||||
def comando_evento_eliminar(args)
|
||||
if args.empty? || args[0].start_with?('-')
|
||||
puts "#{Color::YELLOW}Uso: adn db evento:eliminar <ID>#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
id_evento = args.shift.to_i
|
||||
puts "#{Color::CYAN}🗑️ Eliminando evento #{id_evento}...#{Color::RESET}"
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
begin
|
||||
res = db.delete_entrada(id_evento)
|
||||
if res
|
||||
puts "#{Color::GREEN}✅ Evento #{id_evento} eliminado exitosamente.#{Color::RESET}"
|
||||
else
|
||||
puts "#{Color::YELLOW}⚠ No se pudo eliminar el evento. ¿Existe el ID #{id_evento}?#{Color::RESET}"
|
||||
end
|
||||
rescue => e
|
||||
puts "#{Color::RED}✗ Error al eliminar: #{e.message}#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
require 'optparse'
|
||||
require_relative '../core/colores'
|
||||
|
||||
module ADN
|
||||
class SubcomandoEstados
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
@opciones = { verificar: false }
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Uso: ./adn/tools/run estados [OPCIONES]"
|
||||
opts.on("--verificar", "Verificar si las tareas en proceso ya terminaron") do
|
||||
@opciones[:verificar] = true
|
||||
end
|
||||
end
|
||||
parser.parse!(@args)
|
||||
|
||||
puts "#{Color::CYAN}⏳ Consultando items en proceso (Bitácora Web)#{Color::RESET}"
|
||||
puts "#{Color::DIM}#{'-' * 70}#{Color::RESET}"
|
||||
|
||||
begin
|
||||
require_relative '../db/core/bitacora_db'
|
||||
|
||||
# 1. Preparar SSH si es necesario (Una sola vez)
|
||||
preparar_ssh if @opciones[:verificar]
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
# Buscamos entradas con estado ⏳ para hoy o recientes
|
||||
entradas = db.list_entradas(
|
||||
estado: '⏳',
|
||||
desde: (Date.today - 1).to_s,
|
||||
limit: 20
|
||||
)
|
||||
|
||||
if entradas.empty?
|
||||
puts " #{Color::GREEN}✅ No hay tareas pendientes en proceso.#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(8)} #{"Nodo".ljust(15)} #{"Descripción".ljust(40)}#{Color::RESET}"
|
||||
puts "#{"─" * 5} #{"─" * 8} #{"─" * 15} #{"─" * 40}"
|
||||
|
||||
entradas.each do |e|
|
||||
id = e['id'].to_s
|
||||
inicio = e['inicio'][0..4] # HH:MM
|
||||
nodo = e['nodo_nombre'] || '(sin nodo)'
|
||||
nodo_id = e['nodo_id']
|
||||
desc = e['descripcion']
|
||||
|
||||
# Si se pidió verificar y es un proceso gestionable
|
||||
if @opciones[:verificar] && desc.start_with?('PROCESO:') && nodo_id
|
||||
print " #{Color::DIM}Verificando #{nodo}...#{Color::RESET} "
|
||||
result = tarea_status_detallado(nodo, desc)
|
||||
if result[:terminado]
|
||||
puts "#{Color::GREEN}TERMINADO#{Color::RESET} #{Color::DIM}(Proceso no encontrado)#{Color::RESET}"
|
||||
# Al finalizar, mantenemos el historial de viñetas
|
||||
final_desc = desc.split("\n")[0].gsub('Iniciando ', '').gsub('..', '.') + " (Completado y auto-confirmado)\n" + desc.split("\n")[1..].join("\n")
|
||||
db.update_entrada(e['id'], {
|
||||
estado: '✅',
|
||||
fin: Time.now.strftime('%H:%M'),
|
||||
descripcion: final_desc.strip
|
||||
})
|
||||
next
|
||||
elsif result[:activo]
|
||||
puts "#{Color::YELLOW}ACTIVO#{Color::RESET}"
|
||||
ahora = Time.now.strftime('%H:%M')
|
||||
# Solo agregamos si no hay ya una entrada para este mismo minuto
|
||||
unless desc.include?("- #{ahora} >")
|
||||
# Limpiamos el formato anterior si existiera
|
||||
base_desc = desc.split(' [Último control:')[0].strip
|
||||
nueva_desc = "#{base_desc}\n- #{ahora} > Activo y en proceso sin problemas"
|
||||
db.update_entrada(e['id'], { descripcion: nueva_desc })
|
||||
end
|
||||
else
|
||||
puts "#{Color::RED}PENDIENTE#{Color::RESET} #{Color::DIM}(Error)#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
nodo_fmt = nodo[0..13] + '..' if nodo.length > 15
|
||||
nodo_fmt ||= nodo
|
||||
desc_fmt = desc[0..37] + '..' if desc.length > 40
|
||||
desc_fmt ||= desc
|
||||
|
||||
puts "#{id.ljust(5)} #{inicio.ljust(8)} #{nodo_fmt.ljust(15)} #{desc_fmt}"
|
||||
end
|
||||
|
||||
puts "#{Color::DIM}#{'-' * 70}#{Color::RESET}"
|
||||
puts "Total: #{entradas.length} item(s) en proceso."
|
||||
end
|
||||
rescue => e
|
||||
@logger.error("Error al consultar estados: #{e.message}")
|
||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||
ensure
|
||||
limpiar_ssh if @opciones[:verificar]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preparar_ssh
|
||||
# Obtener passphrase de la bóveda
|
||||
@passphrase = nil
|
||||
begin
|
||||
candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb')
|
||||
if File.exist?(File.join(File.dirname(__FILE__), '../seguridad/.session'))
|
||||
output = `ruby #{candados_path} get rsa 2>/dev/null`.strip
|
||||
@passphrase = output unless output.empty? || output.include?('Error')
|
||||
end
|
||||
rescue
|
||||
end
|
||||
|
||||
return unless @passphrase
|
||||
|
||||
# Crear script de askpass único
|
||||
@askpass_sh = "/tmp/adn_askpass_#{Process.pid}.sh"
|
||||
File.write(@askpass_sh, "#!/bin/sh\necho \"#{@passphrase}\"\n")
|
||||
FileUtils.chmod(0700, @askpass_sh)
|
||||
|
||||
@ssh_env = {
|
||||
'DISPLAY' => ':0',
|
||||
'SSH_ASKPASS_REQUIRE' => 'force',
|
||||
'SSH_ASKPASS' => @askpass_sh
|
||||
}
|
||||
|
||||
# Intentar añadir la llave una sola vez si no está cargada
|
||||
unless system("ssh-add -l >/dev/null 2>&1")
|
||||
system(@ssh_env, "ssh-add /home/rmonla/.ssh/id_rsa < /dev/null 2>/dev/null")
|
||||
end
|
||||
end
|
||||
|
||||
def limpiar_ssh
|
||||
File.delete(@askpass_sh) if @askpass_sh && File.exist?(@askpass_sh)
|
||||
end
|
||||
|
||||
def tarea_status_detallado(nodo, descripcion)
|
||||
archivo_nodo = File.join(ADN::NODOS_DIR, "#{nodo}.md")
|
||||
return { terminado: false, activo: false, code: -1 } unless File.exist?(archivo_nodo)
|
||||
|
||||
contenido = File.read(archivo_nodo)
|
||||
ip = detectar_ip(contenido)
|
||||
so = detectar_so(contenido)
|
||||
return { terminado: false, activo: false, code: -2 } unless ip
|
||||
|
||||
if descripcion.include?('actualiza') && descripcion.include?('SO')
|
||||
if so == 'Linux'
|
||||
# Comando robusto que no se empareja a sí mismo
|
||||
cmd = "pgrep -x 'apt-get' || pgrep -x 'dpkg' || pgrep -x 'apt' || pgrep -x 'dpkg-deb'"
|
||||
exit_code = system_ssh_status(ip, cmd)
|
||||
|
||||
return { terminado: true, activo: false, code: exit_code } if exit_code == 1
|
||||
return { terminado: false, activo: true, code: exit_code } if exit_code == 0
|
||||
return { terminado: false, activo: false, code: exit_code }
|
||||
elsif so == 'Windows'
|
||||
# Verificamos si procesos de actualización están corriendo
|
||||
# En PowerShell el errorlevel $LASTEXITCODE o $? se puede traducir
|
||||
cmd = "powershell -Command \"if (Get-Process TiWorker, TrustedInstaller, wuauclt -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }\""
|
||||
exit_code = system_ssh_status_win(ip, cmd)
|
||||
|
||||
return { terminado: true, activo: false, code: exit_code } if exit_code == 1
|
||||
return { terminado: false, activo: true, code: exit_code } if exit_code == 0
|
||||
return { terminado: false, activo: false, code: exit_code }
|
||||
end
|
||||
end
|
||||
|
||||
{ terminado: false, activo: false, code: -3 }
|
||||
end
|
||||
|
||||
def detectar_ip(contenido)
|
||||
# 1. Buscar configuración SSH completa en tabla (user@IP:port)
|
||||
if contenido.match(/\|\s*\*\*SSH\*\*\s*\|\s*`?([a-zA-Z0-9_-]+@[0-9\.]+):([0-9]+)`?/)
|
||||
user_host = $1
|
||||
port = $2
|
||||
return "-p #{port} #{user_host}"
|
||||
end
|
||||
if contenido.match(/\*\*(?:IP VPN|IP Tailscale)\*\*\s*[:\|]\s*`?([0-9\.]+)`?/)
|
||||
return $1
|
||||
end
|
||||
if contenido.match(/\*\*IP(?:\s+\w+)?\*\*\s*[:\|]\s*`?([0-9\.]+)`?/)
|
||||
return $1
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def detectar_so(contenido)
|
||||
# 1. Buscar campo explícito OS o SO en los metadatos iniciales
|
||||
identidad = contenido.split('##')[0..1].join("\n")
|
||||
if identidad.match(/(?:- |\|)\s*\*\*(?:OS|SO|Sistema Operativo)\*\*\s*[:\|]\s*([^\|\n]+)/i)
|
||||
so_valor = $1.strip
|
||||
return 'Windows' if so_valor.match(/Windows|Win10|Server/i)
|
||||
return 'Linux' if so_valor.match(/Debian|Ubuntu|Linux|Proxmox|LXC/i)
|
||||
end
|
||||
|
||||
# 2. Fallback por palabras clave
|
||||
if identidad.match(/Windows|Win10|Server Core/i)
|
||||
'Windows'
|
||||
else
|
||||
'Linux'
|
||||
end
|
||||
end
|
||||
|
||||
def system_ssh_status(ip, remote_cmd)
|
||||
if @passphrase
|
||||
system(@ssh_env, "ssh -n -o ConnectTimeout=5 -o StrictHostKeyChecking=no #{ip} '#{remote_cmd}' > /dev/null 2>&1")
|
||||
return $?.exitstatus
|
||||
else
|
||||
system("ssh -n -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no #{ip} '#{remote_cmd}' > /dev/null 2>&1")
|
||||
return $?.exitstatus
|
||||
end
|
||||
end
|
||||
|
||||
def system_ssh_status_win(ip, remote_cmd)
|
||||
candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb')
|
||||
|
||||
if File.exist?(File.join(File.dirname(__FILE__), '../seguridad/.session'))
|
||||
shell_eval = "sshpass -e ssh -n -o ConnectTimeout=5 -o StrictHostKeyChecking=no #{ip} '#{remote_cmd}' > /dev/null 2>&1"
|
||||
system("ruby", candados_path, "run", "win_domain", "SSHPASS", shell_eval)
|
||||
return $?.exitstatus
|
||||
else
|
||||
# Si no hay sesión, fallar y devolver un error genérico (255)
|
||||
return 255
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,239 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# adn/tools/cli/jornada.rb — Subcomando unificado para gestión de jornada laboral
|
||||
# ==================================================================================
|
||||
# Uso:
|
||||
# ./adn/tools/run jornada iniciar presencial [HH:MM]
|
||||
# ./adn/tools/run jornada iniciar remoto [HH:MM]
|
||||
# ./adn/tools/run jornada cerrar [presencial|remoto] [HH:MM]
|
||||
# ./adn/tools/run jornada estado
|
||||
|
||||
require 'date'
|
||||
require 'time'
|
||||
require 'optparse'
|
||||
require_relative '../db/core/bitacora_db'
|
||||
require_relative '../core/colores'
|
||||
|
||||
module ADN
|
||||
class SubcomandoJornada
|
||||
NODO_DEFAULT = 'dtic-DIIAA'
|
||||
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
@fecha = Date.today
|
||||
@opciones = { nodo: NODO_DEFAULT, fecha: nil }
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
# Parsear opciones globales (--nodo, --fecha) antes del subcomando
|
||||
parser_global = OptionParser.new do |opts|
|
||||
opts.on('--nodo NOMBRE', "Nodo a registrar (default: #{NODO_DEFAULT})") { |n| @opciones[:nodo] = n }
|
||||
opts.on('--fecha YYYY-MM-DD', 'Fecha de la bitácora (default: hoy)') { |f| @opciones[:fecha] = f }
|
||||
end
|
||||
parser_global.order!(@args)
|
||||
|
||||
accion = @args.shift
|
||||
|
||||
case accion
|
||||
when 'iniciar', 'inicio', 'abrir'
|
||||
ejecutar_inicio
|
||||
when 'cerrar', 'cierre', 'finalizar'
|
||||
ejecutar_cierre
|
||||
when 'estado', 'status'
|
||||
ejecutar_estado
|
||||
else
|
||||
puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}"
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
puts <<~AYUDA
|
||||
#{Color::CYAN}📅 Gestión de Jornada Laboral#{Color::RESET}
|
||||
|
||||
#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run jornada <acción> [modo] [hora] [opciones]
|
||||
|
||||
#{Color::YELLOW}Acciones:#{Color::RESET}
|
||||
iniciar [presencial|remoto] [HH:MM] Registrar inicio de jornada
|
||||
cerrar [presencial|remoto] [HH:MM] Registrar cierre de jornada
|
||||
estado Ver estado de la jornada actual
|
||||
|
||||
#{Color::YELLOW}Opciones:#{Color::RESET}
|
||||
--nodo NOMBRE Nodo a registrar (default: #{NODO_DEFAULT})
|
||||
--fecha YYYY-MM-DD Fecha de la bitácora (default: hoy)
|
||||
|
||||
#{Color::YELLOW}Ejemplos:#{Color::RESET}
|
||||
./adn/tools/run jornada iniciar presencial 08:00
|
||||
./adn/tools/run jornada iniciar remoto
|
||||
./adn/tools/run jornada cerrar presencial 17:00
|
||||
./adn/tools/run jornada cerrar
|
||||
./adn/tools/run jornada estado
|
||||
./adn/tools/run jornada iniciar presencial 08:00 --nodo srv-ns8
|
||||
AYUDA
|
||||
end
|
||||
|
||||
def parsear_modo_y_hora
|
||||
modo = 'P' # default presencial
|
||||
hora = Time.now.strftime('%H:%M')
|
||||
|
||||
@args.each do |arg|
|
||||
case arg.downcase
|
||||
when 'presencial', 'p'
|
||||
modo = 'P'
|
||||
when 'remoto', 'remota', 'r'
|
||||
modo = 'R'
|
||||
else
|
||||
# Si parece una hora (HH:MM), usarla
|
||||
if arg.match?(/^\d{1,2}:\d{2}$/)
|
||||
hora = arg
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[modo, hora]
|
||||
end
|
||||
|
||||
def obtener_fecha
|
||||
if @opciones[:fecha]
|
||||
Date.parse(@opciones[:fecha])
|
||||
else
|
||||
@fecha
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_inicio
|
||||
modo, hora = parsear_modo_y_hora
|
||||
tipo_str = modo == 'P' ? 'Presencial' : 'Remoto'
|
||||
fecha = obtener_fecha
|
||||
|
||||
@logger.info("Iniciando jornada #{tipo_str} a las #{hora}")
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
# 1. Asegurar que existe la bitácora
|
||||
bitacora = db.find_bitacora_by_fecha(fecha)
|
||||
unless bitacora
|
||||
@logger.info("Creando bitácora en DB para #{fecha}...")
|
||||
bitacora = db.create_bitacora(fecha: fecha)
|
||||
end
|
||||
|
||||
# 2. Buscar nodo
|
||||
nodo = db.find_nodo_by_nombre(@opciones[:nodo])
|
||||
unless nodo
|
||||
@logger.error("Nodo '#{@opciones[:nodo]}' no encontrado en la BD.")
|
||||
puts "#{Color::RED}✗ Nodo '#{@opciones[:nodo]}' no existe. Usa 'db nodo:listar' para ver disponibles.#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
# 3. Crear entrada de inicio
|
||||
entrada = db.create_entrada(
|
||||
inicio: hora,
|
||||
descripcion: "Inicio de jornada #{tipo_str.downcase}. Sincronización de contexto y revisión de tareas.",
|
||||
estado: '⏳',
|
||||
modo: modo,
|
||||
bitacora_id: bitacora['id'],
|
||||
nodo_id: nodo['id']
|
||||
)
|
||||
|
||||
puts "#{Color::GREEN}✅ Jornada #{tipo_str.downcase} iniciada#{Color::RESET}"
|
||||
puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}"
|
||||
puts " #{Color::BOLD}Hora:#{Color::RESET} #{hora}"
|
||||
puts " #{Color::BOLD}Nodo:#{Color::RESET} #{@opciones[:nodo]}"
|
||||
puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}"
|
||||
@logger.exito("Jornada #{tipo_str} iniciada en Base de Datos.")
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_cierre
|
||||
modo, hora = parsear_modo_y_hora
|
||||
tipo_str = modo == 'P' ? 'Presencial' : 'Remoto'
|
||||
fecha = obtener_fecha
|
||||
|
||||
@logger.info("Cerrando jornada #{tipo_str} a las #{hora}")
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
# 1. Buscar la bitácora
|
||||
bitacora = db.find_bitacora_by_fecha(fecha)
|
||||
unless bitacora
|
||||
@logger.error("No existe bitácora en DB para #{fecha}. ¿Iniciaste la jornada?")
|
||||
puts "#{Color::RED}✗ No hay bitácora para #{fecha}#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
# 2. Buscar nodo
|
||||
nodo = db.find_nodo_by_nombre(@opciones[:nodo])
|
||||
unless nodo
|
||||
@logger.error("Nodo '#{@opciones[:nodo]}' no encontrado.")
|
||||
puts "#{Color::RED}✗ Nodo '#{@opciones[:nodo]}' no existe.#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
# 3. Crear entrada de cierre
|
||||
entrada = db.create_entrada(
|
||||
inicio: hora,
|
||||
fin: hora,
|
||||
descripcion: "Cierre de jornada #{tipo_str.downcase}. Sincronización final y validación de backups.",
|
||||
estado: '✅',
|
||||
modo: modo,
|
||||
bitacora_id: bitacora['id'],
|
||||
nodo_id: nodo['id']
|
||||
)
|
||||
|
||||
puts "#{Color::GREEN}✅ Jornada #{tipo_str.downcase} cerrada#{Color::RESET}"
|
||||
puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}"
|
||||
puts " #{Color::BOLD}Hora:#{Color::RESET} #{hora}"
|
||||
puts " #{Color::BOLD}Nodo:#{Color::RESET} #{@opciones[:nodo]}"
|
||||
puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}"
|
||||
@logger.exito("Jornada #{tipo_str} cerrada en Base de Datos.")
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_estado
|
||||
fecha = obtener_fecha
|
||||
puts "#{Color::CYAN}📅 Estado de jornada para #{fecha}#{Color::RESET}"
|
||||
puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}"
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
bitacora = db.find_bitacora_by_fecha(fecha)
|
||||
unless bitacora
|
||||
puts "#{Color::YELLOW}⚠ No hay bitácora para #{fecha}#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
entradas = db.list_entradas(bitacora_id: bitacora['id'], limit: 50)
|
||||
|
||||
if entradas.empty?
|
||||
puts "#{Color::YELLOW}⚠ Sin entradas para hoy#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
abiertas = entradas.select { |e| e['estado'] == '⏳' }
|
||||
cerradas = entradas.select { |e| e['estado'] == '✅' }
|
||||
|
||||
puts " #{Color::GREEN}Total entradas:#{Color::RESET} #{entradas.length}"
|
||||
puts " #{Color::GREEN}Completadas:#{Color::RESET} #{cerradas.length}"
|
||||
puts " #{Color::YELLOW}Abiertas:#{Color::RESET} #{abiertas.length}"
|
||||
|
||||
if abiertas.any?
|
||||
puts "\n #{Color::YELLOW}⏳ Entradas abiertas:#{Color::RESET}"
|
||||
abiertas.each do |e|
|
||||
nodo = e['nodo_nombre'] || '(sin nodo)'
|
||||
desc = e['descripcion']
|
||||
desc = desc[0..50] + '...' if desc.length > 53
|
||||
puts " ID #{e['id']} | #{e['inicio'][0..4]} | #{nodo} | #{desc}"
|
||||
end
|
||||
end
|
||||
|
||||
puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,239 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# adn/tools/cli/nodos.rb — Subcomando para gestión y consulta de nodos
|
||||
# ======================================================================
|
||||
# Uso:
|
||||
# ./adn/tools/run nodos listar [--tipo servidor|vm|pc]
|
||||
# ./adn/tools/run nodos agrupar [--formato arbol|tabla]
|
||||
# ./adn/tools/run nodos buscar <término>
|
||||
|
||||
require 'optparse'
|
||||
require_relative '../core/colores'
|
||||
|
||||
module ADN
|
||||
class SubcomandoNodos
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
@opciones = {}
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
accion = @args.shift
|
||||
|
||||
case accion
|
||||
when 'agrupar'
|
||||
agrupar_nodos
|
||||
when 'listar'
|
||||
listar_nodos
|
||||
when 'buscar'
|
||||
buscar_nodos
|
||||
when 'ayuda', 'help', '-h', '--help'
|
||||
mostrar_ayuda
|
||||
else
|
||||
puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}"
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
puts <<~AYUDA
|
||||
#{Color::CYAN}🖥️ Gestión de Nodos#{Color::RESET}
|
||||
|
||||
#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run nodos <acción> [opciones]
|
||||
|
||||
#{Color::YELLOW}Acciones:#{Color::RESET}
|
||||
agrupar [--servidor SRV] Agrupar VMs por servidor anfitrión
|
||||
listar [--tipo TIPO] Listar todos los nodos (fichas .md)
|
||||
buscar <término> Buscar nodos por nombre o contenido
|
||||
|
||||
#{Color::YELLOW}Opciones de 'agrupar':#{Color::RESET}
|
||||
--servidor SRV Filtrar solo VMs de un servidor específico
|
||||
|
||||
#{Color::YELLOW}Opciones de 'listar':#{Color::RESET}
|
||||
--tipo TIPO Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)
|
||||
|
||||
#{Color::YELLOW}Ejemplos:#{Color::RESET}
|
||||
./adn/tools/run nodos agrupar
|
||||
./adn/tools/run nodos agrupar --servidor srv-pmox1
|
||||
./adn/tools/run nodos listar
|
||||
./adn/tools/run nodos listar --tipo srvv
|
||||
./adn/tools/run nodos buscar dasuten
|
||||
AYUDA
|
||||
end
|
||||
|
||||
# ─── Agrupar ────────────────────────────────────────────────────────
|
||||
|
||||
def agrupar_nodos
|
||||
opts = { servidor: nil }
|
||||
parser = OptionParser.new do |opt|
|
||||
opt.on('--servidor SRV', 'Filtrar por servidor anfitrión') { |s| opts[:servidor] = s }
|
||||
end
|
||||
parser.order!(@args)
|
||||
|
||||
puts "#{Color::CYAN}📋 Agrupando nodos por Servidor Host#{Color::RESET}"
|
||||
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
||||
|
||||
servidores = Hash.new { |h, k| h[k] = [] }
|
||||
sin_servidor = []
|
||||
|
||||
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo|
|
||||
nodo_nombre = File.basename(archivo, '.md')
|
||||
next if nodo_nombre.start_with?('_')
|
||||
|
||||
host = detectar_host(archivo)
|
||||
|
||||
if host
|
||||
servidores[host] << nodo_nombre
|
||||
elsif !es_nodo_fisico?(nodo_nombre)
|
||||
sin_servidor << nodo_nombre
|
||||
end
|
||||
end
|
||||
|
||||
# Filtrar por servidor si se especificó
|
||||
if opts[:servidor]
|
||||
srv = opts[:servidor]
|
||||
unless servidores.key?(srv)
|
||||
puts "#{Color::YELLOW}⚠ No se encontraron VMs para el servidor '#{srv}'#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
mostrar_servidor(srv, servidores[srv])
|
||||
else
|
||||
servidores.keys.sort.each { |srv| mostrar_servidor(srv, servidores[srv]) }
|
||||
|
||||
if sin_servidor.any?
|
||||
puts "#{Color::YELLOW}❓ Nodos sin servidor anfitrión identificable:#{Color::RESET}"
|
||||
sin_servidor.sort.each { |nodo| puts " ├── 💻 #{nodo}" }
|
||||
puts ""
|
||||
end
|
||||
end
|
||||
|
||||
total_vms = servidores.values.flatten.length
|
||||
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
||||
puts "#{Color::DIM}Total: #{servidores.keys.length} servidores, #{total_vms} VMs#{Color::RESET}"
|
||||
end
|
||||
|
||||
def mostrar_servidor(srv, vms)
|
||||
puts "#{Color::GREEN}🏢 #{srv}#{Color::RESET} (#{vms.length} VMs)"
|
||||
vms.sort.each { |vm| puts " ├── 💻 #{vm}" }
|
||||
puts ""
|
||||
end
|
||||
|
||||
def detectar_host(archivo)
|
||||
contenido = File.read(archivo, encoding: 'UTF-8')
|
||||
host = nil
|
||||
|
||||
contenido.split("\n").each do |linea|
|
||||
# Formato lista o tabla: **Padre/Host**: `srv-dasu`
|
||||
if linea.match(/\*\*(?:Padre\/Host|Host Anfitrión|Host|Servidor(?: Anfitrión)?|Hipervisor|IP \/ Ubicación)\*\*\s*[:\|]?\s*`?([a-zA-Z0-9\-]+)`?/) ||
|
||||
linea.match(/\*\*(?:Padre\/Host|Host Anfitrión|Host|Servidor(?: Anfitrión)?|Hipervisor|IP \/ Ubicación)\*\*\s*[:\|]?\s*([^`\s|]+)/)
|
||||
candidato = ($1 || $2).strip.gsub(/`/, '')
|
||||
return candidato if candidato.start_with?('srv-')
|
||||
end
|
||||
|
||||
# Texto: "dependiente del host físico `srv-dasu`"
|
||||
if linea.match(/host(?: físico)?\s+`?([a-zA-Z0-9\-]+)`?/)
|
||||
candidato = $1.strip
|
||||
return candidato if candidato.start_with?('srv-')
|
||||
end
|
||||
end
|
||||
|
||||
host
|
||||
end
|
||||
|
||||
def es_nodo_fisico?(nombre)
|
||||
nombre.start_with?('srv-') || nombre.start_with?('pc-') ||
|
||||
nombre == 'dtic-bitacoras' || nombre == 'cam-hikvision-24'
|
||||
end
|
||||
|
||||
# ─── Listar ─────────────────────────────────────────────────────────
|
||||
|
||||
def listar_nodos
|
||||
opts = { tipo: nil }
|
||||
parser = OptionParser.new do |opt|
|
||||
opt.on('--tipo TIPO', 'Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)') { |t| opts[:tipo] = t }
|
||||
end
|
||||
parser.order!(@args)
|
||||
|
||||
puts "#{Color::CYAN}📋 Listando fichas de nodos#{Color::RESET}"
|
||||
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
||||
|
||||
archivos = Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).sort
|
||||
nodos = archivos.map { |a| File.basename(a, '.md') }
|
||||
nodos.reject! { |n| n.start_with?('_') }
|
||||
|
||||
if opts[:tipo]
|
||||
nodos.select! { |n| n.start_with?(opts[:tipo]) }
|
||||
end
|
||||
|
||||
if nodos.empty?
|
||||
puts "#{Color::YELLOW}⚠ No se encontraron nodos#{opts[:tipo] ? " con prefijo '#{opts[:tipo]}'" : ''}#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
nodos.each do |nodo|
|
||||
archivo = File.join(ADN::NODOS_DIR, "#{nodo}.md")
|
||||
contenido = File.read(archivo, encoding: 'UTF-8')
|
||||
|
||||
# Extraer rol si existe
|
||||
rol = '-'
|
||||
if contenido.match(/\*\*Rol(?:\s*Principal)?\*\*\s*[:\|]\s*([^\|\n]+)/)
|
||||
rol = $1.strip[0..40]
|
||||
end
|
||||
|
||||
puts " 💻 #{nodo.ljust(22)} #{Color::DIM}#{rol}#{Color::RESET}"
|
||||
end
|
||||
|
||||
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
||||
puts "#{Color::DIM}Total: #{nodos.length} nodos#{Color::RESET}"
|
||||
end
|
||||
|
||||
# ─── Buscar ─────────────────────────────────────────────────────────
|
||||
|
||||
def buscar_nodos
|
||||
termino = @args.join(' ')
|
||||
if termino.empty?
|
||||
puts "#{Color::RED}✗ Se requiere un término de búsqueda#{Color::RESET}"
|
||||
puts " Uso: ./adn/tools/run nodos buscar <término>"
|
||||
return
|
||||
end
|
||||
|
||||
puts "#{Color::CYAN}🔍 Buscando nodos con '#{termino}'#{Color::RESET}"
|
||||
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
||||
|
||||
resultados = []
|
||||
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo|
|
||||
nodo = File.basename(archivo, '.md')
|
||||
contenido = File.read(archivo, encoding: 'UTF-8')
|
||||
|
||||
if nodo.include?(termino.downcase) || contenido.downcase.include?(termino.downcase)
|
||||
# Buscar línea con contexto
|
||||
lineas = contenido.split("\n")
|
||||
contexto = lineas.find { |l| l.downcase.include?(termino.downcase) }
|
||||
contexto = contexto ? contexto.strip[0..60] : '-'
|
||||
resultados << { nodo: nodo, contexto: contexto }
|
||||
end
|
||||
end
|
||||
|
||||
if resultados.empty?
|
||||
puts "#{Color::YELLOW}⚠ No se encontraron nodos que coincidan con '#{termino}'#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
resultados.each do |r|
|
||||
puts " 💻 #{r[:nodo].ljust(22)} #{Color::DIM}#{r[:contexto]}#{Color::RESET}"
|
||||
end
|
||||
|
||||
puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}"
|
||||
puts "#{Color::DIM}Total: #{resultados.length} resultados#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,192 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# adn/tools/cli/proceso.rb — Subcomando para ejecución de procesos automatizados
|
||||
# ==============================================================================
|
||||
|
||||
require 'optparse'
|
||||
require 'open3'
|
||||
require 'date'
|
||||
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/nodos_info'
|
||||
require_relative '../core/ejecutor_remoto'
|
||||
require_relative '../db/core/bitacora_db'
|
||||
|
||||
module ADN
|
||||
class SubcomandoProceso
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty? || @args[0] == 'help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
proceso = @args.shift
|
||||
sub_args = @args
|
||||
|
||||
case proceso
|
||||
when 'nodos:migrar_backup'
|
||||
comando_migrar_backup(sub_args)
|
||||
else
|
||||
@logger.error("Proceso desconocido: #{proceso}")
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
puts "#{Color::CYAN}⚙️ Procesos Automatizados ADN#{Color::RESET}"
|
||||
puts ""
|
||||
puts "#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run proceso <nombre_proceso> [argumentos]"
|
||||
puts ""
|
||||
puts "#{Color::BOLD}nodos:migrar_backup [OPC]#{Color::RESET} Migración de backups entre nodos"
|
||||
puts " --desde NODO Nodo origen"
|
||||
puts " --hacia NODO Nodo destino"
|
||||
puts " --ruta RUTA Ruta del archivo en el origen"
|
||||
puts " --ejecutar Ejecutar transferencia real"
|
||||
end
|
||||
|
||||
def comando_migrar_backup(args)
|
||||
opts = { desde: nil, hacia: nil, ruta: nil, simular: true }
|
||||
OptionParser.new do |o|
|
||||
o.on("--desde NODO") { |v| opts[:desde] = v }
|
||||
o.on("--hacia NODO") { |v| opts[:hacia] = v }
|
||||
o.on("--ruta RUTA") { |v| opts[:ruta] = v }
|
||||
o.on("--ejecutar") { opts[:simular] = false }
|
||||
end.parse!(args)
|
||||
|
||||
unless opts[:desde] && opts[:hacia] && opts[:ruta]
|
||||
puts "#{Color::RED}✗ Error: Faltan argumentos (--desde, --hacia, --ruta)#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
# 1. Resolver información de nodos
|
||||
info_src = NodosInfo.extraer_metadata(opts[:desde])
|
||||
info_dst = NodosInfo.extraer_metadata(opts[:hacia])
|
||||
|
||||
unless info_src && info_dst
|
||||
puts "#{Color::RED}✗ Error: No se pudo obtener información de #{opts[:desde]} o #{opts[:hacia]}#{Color::RESET}"
|
||||
return
|
||||
end
|
||||
|
||||
# Ajuste manual para srv-ns8 (detectar si estamos corriendo en él)
|
||||
info_src[:ip] = 'localhost' if opts[:desde] == 'srv-ns8'
|
||||
|
||||
puts "#{Color::CYAN}🚀 Preparando #{opts[:simular] ? 'SIMULACIÓN' : 'MIGRACIÓN'} de backup...#{Color::RESET}"
|
||||
puts " 📤 Origen : #{Color::BOLD}#{opts[:desde]}#{Color::RESET} (#{info_src[:ip]})"
|
||||
puts " 📥 Destino: #{Color::BOLD}#{opts[:hacia]}#{Color::RESET} (#{info_dst[:ip]})"
|
||||
|
||||
# 2. Registrar en Bitácora
|
||||
id_evento = registrar_inicio_bitacora(opts)
|
||||
|
||||
# 3. Ejecutar
|
||||
if opts[:simular]
|
||||
ejecutar_simulacion(id_evento, opts)
|
||||
else
|
||||
ejecutar_migracion_real(id_evento, opts, info_src, info_dst)
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_migracion_real(id_evento, opts, info_src, info_dst)
|
||||
puts "#{Color::BOLD}🚀 Iniciando transferencia real...#{Color::RESET}"
|
||||
|
||||
desc = "PROCESO: Migración REAL de backup desde #{opts[:desde]} a #{opts[:hacia]}."
|
||||
|
||||
# A. Verificar Origen
|
||||
print " ▸ Verificando origen... "
|
||||
size = nil
|
||||
if info_src[:ip] == 'localhost'
|
||||
size = File.exist?(opts[:ruta]) ? File.size(opts[:ruta]) : nil
|
||||
else
|
||||
# Usar EjecutorRemoto para verificar el archivo
|
||||
res = EjecutorRemoto.ejecutar(info_src, "powershell -NoProfile -Command \"if(Test-Path '#{opts[:ruta]}'){$s=Get-Item '#{opts[:ruta]}'; $s.Length}else{exit 1}\"")
|
||||
size = res[:success] ? res[:output].to_i : nil
|
||||
end
|
||||
|
||||
if size
|
||||
mb = (size.to_f / 1024**2).round(2)
|
||||
puts "#{Color::GREEN}OK#{Color::RESET} (#{mb} MB)"
|
||||
desc += "\n- #{Time.now.strftime('%H:%M')} > Archivo localizado: #{mb} MB."
|
||||
else
|
||||
puts "#{Color::RED}FALLÓ#{Color::RESET} (No se encontró el archivo)"
|
||||
actualizar_bitacora(id_evento, { estado: '❌', descripcion: desc + "\n- ERROR: Archivo no encontrado." })
|
||||
return
|
||||
end
|
||||
|
||||
# B. Preparar Destino
|
||||
print " ▸ Preparando directorio en destino... "
|
||||
EjecutorRemoto.ejecutar(info_dst, "powershell -NoProfile -Command \"if(!(Test-Path C:\\temp)){New-Item -ItemType Directory -Path C:\\temp}\"")
|
||||
puts "#{Color::GREEN}OK#{Color::RESET}"
|
||||
|
||||
# C. Pipeline de Streaming
|
||||
filename = File.basename(opts[:ruta].gsub('\\', '/'))
|
||||
dst_path = "C:\\temp\\#{filename}"
|
||||
puts " 🚀 Streaming: #{opts[:desde]} -> #{opts[:hacia]} [#{dst_path}]"
|
||||
|
||||
# Definir comandos de stream adaptados
|
||||
if info_src[:ip] == 'localhost'
|
||||
src_cmd = ["cat", opts[:ruta]]
|
||||
else
|
||||
ps_src = "$s=[System.IO.File]::OpenRead('#{opts[:ruta].gsub('/', '\\\\')}'); $o=[Console]::OpenStandardOutput(); $b=New-Object byte[] 65536; while(($r=$s.Read($b,0,$b.Length)) -gt 0){ $o.Write($b,0,$r) }; $s.Close()"
|
||||
# Inyectar SSHPass vía EjecutorRemoto.comando_candados
|
||||
clave_src = (info_src[:host] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'win_domain')
|
||||
ssh_user_src = "#{info_src[:usuario]}@#{info_src[:ip]}"
|
||||
src_cmd = EjecutorRemoto.comando_candados(clave_src, 'SSHPASS', ["sshpass", "-e", "ssh", "-p", info_src[:puerto_ssh].to_s, "-o", "StrictHostKeyChecking=no", ssh_user_src, "powershell -NoProfile -Command \"#{ps_src}\""])
|
||||
end
|
||||
|
||||
ps_dst = "$i=[Console]::OpenStandardInput(); $s=[System.IO.File]::Create('#{dst_path.gsub('/', '\\\\')}'); $b=New-Object byte[] 65536; while(($r=$i.Read($b,0,$b.Length)) -gt 0){$s.Write($b,0,$r)}; $s.Close()"
|
||||
clave_dst = (info_dst[:host] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
ssh_user_dst = "#{info_dst[:usuario]}@#{info_dst[:ip]}"
|
||||
dst_cmd = EjecutorRemoto.comando_candados(clave_dst, 'SSHPASS', ["sshpass", "-e", "ssh", "-p", info_dst[:puerto_ssh].to_s, "-o", "StrictHostKeyChecking=no", ssh_user_dst, "powershell -NoProfile -Command \"#{ps_dst}\""])
|
||||
|
||||
start_time = Time.now
|
||||
success = EjecutorRemoto.stream_pipeline(src_cmd, dst_cmd)
|
||||
end_time = Time.now
|
||||
|
||||
if success
|
||||
duracion = (end_time - start_time).round(0)
|
||||
puts "#{Color::GREEN}✅ Transferencia exitosa en #{duracion} segundos.#{Color::RESET}"
|
||||
actualizar_bitacora(id_evento, {
|
||||
estado: '✅',
|
||||
fin: Time.now.strftime('%H:%M'),
|
||||
descripcion: desc + "\n- #{Time.now.strftime('%H:%M')} > Transferencia completada en #{duracion}s."
|
||||
})
|
||||
else
|
||||
puts "#{Color::RED}✗ Falló la transferencia.#{Color::RESET}"
|
||||
actualizar_bitacora(id_evento, { estado: '❌', descripcion: desc + "\n- ERROR: Streaming fallido." })
|
||||
end
|
||||
end
|
||||
|
||||
# Auxiliares de Bitácora
|
||||
def registrar_inicio_bitacora(opts)
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
nodo = db.find_nodo_by_nombre(opts[:hacia])
|
||||
bit = db.find_bitacora_by_fecha(Date.today.to_s) || db.create_bitacora(fecha: Date.today.to_s)
|
||||
db.create_entrada(
|
||||
bitacora_id: bit['id'],
|
||||
nodo_id: nodo ? nodo['id'] : nil,
|
||||
inicio: Time.now.strftime('%H:%M'),
|
||||
estado: '⏳',
|
||||
descripcion: "PROCESO: Migración de backup #{opts[:desde]} -> #{opts[:hacia]}."
|
||||
)['id']
|
||||
end
|
||||
end
|
||||
|
||||
def actualizar_bitacora(id, data)
|
||||
BitacorasDB::BitacoraDB.with_connection { |db| db.update_entrada(id, data) }
|
||||
end
|
||||
|
||||
def ejecutar_simulacion(id_evento, opts)
|
||||
puts " 🧪 Modo simulación activo."
|
||||
sleep(2)
|
||||
actualizar_bitacora(id_evento, { estado: '✅', fin: Time.now.strftime('%H:%M'), descripcion: "PROCESO: Simulación de migración completada." })
|
||||
puts " #{Color::GREEN}OK#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user