#!/usr/bin/env ruby # script: alta_nodo.rb # Uso: ruby automatizacion/alta_nodo.rb [USUARIO] # Descripción: Automatiza la configuración SSH y la documentación del ADN para un nuevo nodo. require 'fileutils' require 'date' require 'json' require 'time' # --- Configuración --- ADN_DIR = File.expand_path('../adn', __dir__) NODOS_DIR = File.join(ADN_DIR, 'nodos') ONTOLOGIA_FILE = File.join(ADN_DIR, '01_ontologia.md') SSH_CONFIG_FILE = File.expand_path('~/.ssh/config') SSH_PUB_KEY = File.expand_path('~/.ssh/id_rsa.pub') # --- Argumentos --- ip = ARGV[0] user = ARGV[1] || 'rmonla' unless ip puts "❌ Error: Falta la IP." puts "Uso correcto: ruby automatizacion/alta_nodo.rb [USUARIO]" puts "Ejemplo: ruby automatizacion/alta_nodo.rb 10.0.10.205 rmonla" exit 1 end puts "--- 🚀 Iniciando Alta de Nodo: #{ip} con Usuario: #{user} ---" # --- 🚨 PROTOCOLO DE SEGURIDAD (CANDADO JIT) --- TOKEN_FILE = File.expand_path('../.agent_token', __dir__) def verificar_autorizacion! unless File.exist?(TOKEN_FILE) puts "\n⛔ ACCESO DENEGADO: No se encontró token de autorización." puts "🔒 Protocolo de seguridad activo. El Agente requiere permiso explícito." puts "👉 Ejecuta: ruby automatizacion/candado.rb" exit 1 end begin token = JSON.parse(File.read(TOKEN_FILE)) expires_at = Time.parse(token['expires_at']) if Time.now > expires_at puts "\n⛔ ACCESO DENEGADO: El token de autorización ha EXPIRADO." puts "💀 Expiró a las: #{expires_at.strftime('%H:%M:%S')}" puts "👉 Ejecuta nuevamente: ruby automatizacion/candado.rb" exit 1 end puts "🔓 Autorización Verificada (Válida hasta #{expires_at.strftime('%H:%M:%S')})" rescue => e puts "\n❌ Error al verificar token: #{e.message}" exit 1 end end verificar_autorizacion! # ----------------------------------------------- # Variable para trackear si tenemos acceso sin password/interactivo ssh_access_ok = false # Detectamos si necesitamos flags legacy # Intentamos primero con flags modernos, si falla, probamos con legacy ssh_flags = "-o ConnectTimeout=5 -o StrictHostKeyChecking=no" ssh_legacy_flags = "#{ssh_flags} -o PubkeyAcceptedAlgorithms=+ssh-rsa -o HostKeyAlgorithms=+ssh-rsa" # --- 0. Detección de Hostname y Verificación de Acceso --- puts "\n🔍 0. Detectando Hostname y Verificando Acceso..." begin # Probamos conexión con BatchMode para ver si ya entramos sin password check_cmd = "ssh -o BatchMode=yes #{ssh_legacy_flags} #{user}@#{ip} hostname 2>/dev/null" hostname = `#{check_cmd}`.strip if $?.success? && !hostname.empty? puts "✅ Acceso SSH verificado (Sin contraseña)." puts "✅ Hostname detectado: #{hostname}" ssh_access_ok = true else puts "⚠️ Acceso directo no disponible. Intentando conexión interactiva (pedirá password)..." # Usamos flags legacy aquí también por si acaso hostname = `ssh #{ssh_legacy_flags} #{user}@#{ip} hostname`.strip if $?.success? && !hostname.empty? puts "✅ Hostname detectado: #{hostname}" else puts "❌ No se pudo obtener el hostname. Verifica la IP y credenciales." exit 1 end end rescue => e puts "❌ Error al intentar conectar: #{e.message}" exit 1 end # --- 1. Configuración SSH Local --- puts "\n🔧 1. Configurando SSH Local..." if File.read(SSH_CONFIG_FILE).include?("Host #{hostname}") puts "ℹ️ Host '#{hostname}' ya existe en #{SSH_CONFIG_FILE}." else puts "📝 Agregando '#{hostname}' a #{SSH_CONFIG_FILE}..." File.open(SSH_CONFIG_FILE, 'a') do |f| f.puts "\nHost #{hostname}" f.puts " HostName #{ip}" f.puts " User #{user}" f.puts " StrictHostKeyChecking no" f.puts " PubkeyAcceptedAlgorithms +ssh-rsa" f.puts " HostKeyAlgorithms +ssh-rsa" end puts "✅ Configuración SSH actualizada (con soporte legacy)." end # --- 2. Copia de Claves SSH --- if ssh_access_ok puts "\n🔑 2. Copia de Claves SSH: OMITIDO (Ya tienes acceso)." else puts "\n🔑 2. Instalando Clave SSH en #{user}@#{ip}..." if File.exist?(SSH_PUB_KEY) # ssh-copy-id pedirá password una única vez aquí # Pasamos opciones legacy explícitamente system("ssh-copy-id #{ssh_legacy_flags} -i #{SSH_PUB_KEY} #{user}@#{ip}") if $?.success? puts "✅ Clave instalada exitosamente." ssh_access_ok = true else puts "⚠️ Falló ssh-copy-id. Quizás debas hacerlo manualmente o verificar password." end else puts "❌ No se encontró clave pública en #{SSH_PUB_KEY}." end end require 'json' # --- 3. Relevamiento de Hardware (Extracción Cruda & Procesamiento Local) --- puts "\n🔍 3. Extrayendo Datos Crudos del Nodo (Procesamiento Local)..." # Script remoto minimalista: Solo "cat" de archivos clave y lsblk # Usamos delimitadores únicos para separar bloques # Agregamos recolección de evidencia para detección de roles remote_script = <<~SHELL echo "---START_HOSTNAME---"; cat /etc/hostname echo "---START_CPUINFO---"; cat /proc/cpuinfo echo "---START_MEMINFO---"; cat /proc/meminfo echo "---START_OSRELEASE---"; cat /etc/os-release echo "---START_LSBLK---"; lsblk -d -o NAME,SIZE,MODEL,TYPE -J 2>/dev/null || lsblk -d -o NAME,SIZE,MODEL,TYPE echo "---START_PROCESSES---"; ps -ef --sort=-%mem | head -n 20 echo "---START_PORTS---"; ss -tuln 2>/dev/null || netstat -tuln 2>/dev/null echo "---START_MARKERS---" [ -d "/etc/xen" ] && echo "xen_config_dir_exists" [ -d "/etc/pve" ] && echo "pve_config_dir_exists" [ -d "/var/lib/docker" ] && echo "docker_lib_dir_exists" [ -f "/usr/bin/dockerd" ] && echo "dockerd_bin_exists" [ -f "/opt/xensource/bin/xe" ] && echo "citrix_xe_bin_exists" SHELL safe_script = remote_script.gsub('"', '\\"') # Usamos flags legacy para la sonda también raw_output = `ssh #{ssh_legacy_flags} #{hostname} "#{safe_script}"` # Parsers Locales (Host-Side Logic) def parse_block(output, key) return "" unless output.include?("---START_#{key}---") output.split("---START_#{key}---")[1].split("---START_")[0].strip rescue "" end hostname_raw = parse_block(raw_output, 'HOSTNAME') cpuinfo_raw = parse_block(raw_output, 'CPUINFO') meminfo_raw = parse_block(raw_output, 'MEMINFO') osrelease_raw = parse_block(raw_output, 'OSRELEASE') lsblk_raw = parse_block(raw_output, 'LSBLK') processes_raw = parse_block(raw_output, 'PROCESSES') ports_raw = parse_block(raw_output, 'PORTS') markers_raw = parse_block(raw_output, 'MARKERS') # Analizador de CPU (Local) cpu_model = cpuinfo_raw.scan(/model name\s+:\s+(.*)/).flatten.first || "Unknown" cpu_cores = cpuinfo_raw.scan(/^processor\s+:/).count cpu_arch = "x86_64" # Default (podría sacarse de flags en cpuinfo 'lm' = long mode) # Analizador de RAM (Local) mem_total_kb = meminfo_raw.scan(/MemTotal:\s+(\d+)\s+kB/).flatten.first.to_i mem_total_human = "#{(mem_total_kb / 1024.0 / 1024.0).round(2)} GiB" # Analizador de OS (Local) os_distro = osrelease_raw.scan(/PRETTY_NAME="(.*)"/).flatten.first || "Unknown Linux" os_kernel_ver = "Linux (Kernel ver. unknown)" # Kernel version requiring 'uname -r', keeping simple for now or parsing from /proc/version if needed later params = {} params[:timestamp] = Time.now.to_s params[:connector] = { ip: ip, user: user, hostname_detected: hostname_raw.empty? ? hostname : hostname_raw } params[:cpu] = { model: cpu_model, cores: cpu_cores, arch: cpu_arch } params[:ram] = { total: mem_total_human, raw: mem_total_kb * 1024 } params[:os] = { distro: os_distro, kernel: os_kernel_ver } # Simplified # Procesamiento de Discos (Local) if lsblk_raw.strip.start_with?("{") begin params[:disks] = JSON.parse(lsblk_raw) rescue params[:disks] = { format: 'raw', content: lsblk_raw } end else params[:disks] = { format: 'text', content: lsblk_raw } end # Evidencia para Inteligencia de Agente params[:evidence] = { processes: processes_raw.lines.map(&:strip), ports: ports_raw.lines.map(&:strip), file_markers: markers_raw.lines.map(&:strip) } puts " ✅ Datos crudos extraídos y procesados localmente." # --- 4. Salida para el Agente (STDOUT) --- puts "\n💾 4. Telemetría Final (Para consumo del Agente)..." puts "---START_PAYLOAD---" puts JSON.pretty_generate(params) puts "---END_PAYLOAD---" puts "\n✅ Proceso finalizado. Copia el bloque JSON anterior si necesitas procesarlo manualmente, o deja que el Agente lo lea."