228 lines
8.7 KiB
Ruby
228 lines
8.7 KiB
Ruby
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
|