Files

232 lines
8.2 KiB
Ruby
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env ruby
# script: alta_nodo.rb
# Uso: ruby automatizacion/alta_nodo.rb <IP> <HOSTNAME> [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 <IP> [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."