[ADN] Commit final DB-First - Actualización completa de documentación, herramientas y componentes
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user