feat: Automating node scanning (scripts/alta_nodo.rb, candado.rb) and updated ontology

This commit is contained in:
Ricardo Monla
2026-02-18 00:56:32 -03:00
parent 07a5bbb6c9
commit a56ab03d9d
7 changed files with 344 additions and 0 deletions
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env ruby
# script: alta_nodo.rb
# Uso: ruby scripts/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 scripts/alta_nodo.rb <IP> [USUARIO]"
puts "Ejemplo: ruby scripts/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 scripts/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 scripts/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."