[Core/Infra] Provisionamiento SSH en pc-dasu0 y unificación Nginx tailscale

This commit is contained in:
Ricardo Monla
2026-03-14 10:29:54 -03:00
parent e554bb5d0d
commit b13a389568
50 changed files with 2702 additions and 561 deletions
+12
View File
@@ -34,6 +34,7 @@ class SubcomandoAyuda
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
generar nodo <nombre> Generar ficha de nodo
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
#{Color::YELLOW}tailscale <acción>#{Color::RESET} Gestión de identidades Tailscale
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
#{Color::YELLOW}db <subcomando>#{Color::RESET} Operaciones de base de datos
#{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers
@@ -149,6 +150,17 @@ class SubcomandoAyuda
puts ""
puts "Consulta la bitácora web para listar todos los items con estado ⏳ (En proceso)."
puts "Ideal para monitorear procesos automatizados o jornadas abiertas."
when 'tailscale', 'ts'
puts "Uso: ./adn/tools/run tailscale <acción> [perfil]"
puts ""
puts "Gestión de identidades y perfiles de Tailscale:"
puts " • switch <perfil> Alternar entre cuentas (dasuten, personal)"
puts " • status Ver estado y cuenta activa"
puts " • list Listar perfiles en caché local"
puts ""
puts "Ejemplos:"
puts " ./adn/tools/run tailscale switch dasuten"
puts " ./adn/tools/run tailscale status"
else
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
+6 -7
View File
@@ -89,17 +89,16 @@ module ADN
# 2. Registrar inicio en bitácora (Bitácora-First)
registrar_inicio_bitacora(nodo, datos)
# 3. Ejecución segura vía ns8-candados
# 3. Ejecución segura vía candados
@logger.info("Solicitando autorización y ejecutando backup remoto...")
comando_vzdump = "vzdump #{datos[:vmid]} --mode #{@opciones[:modo]} --storage #{@opciones[:storage]} --compress #{@opciones[:compress]}"
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
# Usamos el binario de ruby y la ruta absoluta para ns8-candados
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
# Ruta absoluta a candados
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
# Ejecutar con ns8-candados run
# Ejecutar con ns8-candados autorizando primero
# Ejecutar con candados autorizando primero
system("ruby #{candados_path} authorize > /dev/null 2>&1")
task_id = nil
@@ -285,7 +284,7 @@ module ADN
puts "\n#{Color::BOLD}🔍 Estado de Backups en Tiempo Real (Proxmox)#{Color::RESET}"
puts "" * 60
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
# Asegurar autorización general para el lote
system("ruby #{candados_path} authorize > /dev/null 2>&1")
@@ -372,7 +371,7 @@ module ADN
# 1. Obtener lista de VMs directamente de Proxmox para nombres precisos
host = { nombre: 'srv-pmox1', ip: '10.0.10.201' }
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb')
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
system("ruby #{candados_path} authorize > /dev/null 2>&1")
@logger.info("Consultando inventario de VMs en Proxmox...")
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ------------------------------------------------------------------------------
# CLI: commit.rb - Herramienta para gestión de commits según principios ADN
# ------------------------------------------------------------------------------
# Principios aplicados:
# 1. Menos es Más: Componente atómico y reutilizable para gestión de commits
# 2. Armonía Integral: Val
+74 -2
View File
@@ -107,6 +107,8 @@ module ADN
puts " #{Color::BOLD}Modo:#{Color::RESET} #{entrada['modo'] == 'P' ? 'Presencial' : 'Remoto'}"
puts " #{Color::BOLD}IA:#{Color::RESET} #{entrada['es_ia'] ? 'Sí' : 'No'}"
puts " #{Color::BOLD}Bitácora:#{Color::RESET} #{fecha_hoy} (ID: #{bitacora_id})"
mostrar_pendientes(db)
end
end
@@ -114,7 +116,7 @@ module ADN
def comando_evento_actualizar(args)
if args.length < 1 || args[0].start_with?('-')
puts "#{Color::YELLOW}Uso: adn db evento:actualizar <ID> [OPCIONES]#{Color::RESET}"
puts "Opciones soportadas: --fin \"HH:MM\", --inicio \"HH:MM\", --estado \"\", --descripcion \"Texto\""
puts "Opciones soportadas: --fin \"HH:MM\", --inicio \"HH:MM\", --estado \"\", --descripcion \"Texto\", --nodo \"Nombre\""
return
end
@@ -127,6 +129,7 @@ module ADN
opt.on("--fin HH:MM", "Nueva hora de fin") { |f| opts[:fin] = f }
opt.on("--descripcion TEXTO", "Nueva descripción") { |d| opts[:descripcion] = d }
opt.on("--estado ESTADO", "Nuevo estado (⏳, ✅, ❌, ⚠️, ⏸️)") { |e| opts[:estado] = e }
opt.on("--nodo NOMBRE", "Nuevo nodo") { |n| opts[:nodo_raw] = n }
opt.on("-h", "--help", "Mostrar ayuda") do
puts opt
exit
@@ -141,13 +144,27 @@ module ADN
end
if opts.empty?
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --fin, --inicio, --descripcion o --estado#{Color::RESET}"
puts "#{Color::YELLOW}⚠ No se especificó nada para actualizar. Use --fin, --inicio, --descripcion, --estado o --nodo#{Color::RESET}"
return
end
puts "#{Color::CYAN}🔄 Actualizando evento #{id_evento}...#{Color::RESET}"
BitacorasDB::BitacoraDB.with_connection do |db|
if opts.key?(:nodo_raw)
if opts[:nodo_raw].nil? || opts[:nodo_raw].empty?
opts[:nodo_id] = nil
else
nodo = db.find_nodo_by_nombre(opts[:nodo_raw])
if nodo
opts[:nodo_id] = nodo['id']
else
puts "#{Color::YELLOW}⚠ Nodo '#{opts[:nodo_raw]}' no encontrado#{Color::RESET}"
end
end
opts.delete(:nodo_raw)
end
begin
res = db.update_entrada(id_evento, opts)
if res
@@ -160,6 +177,8 @@ module ADN
rescue => e
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
end
mostrar_pendientes(db)
end
end
@@ -277,6 +296,59 @@ module ADN
rescue => e
puts "#{Color::RED}✗ Error al eliminar: #{e.message}#{Color::RESET}"
end
mostrar_pendientes(db)
end
end
private
def mostrar_pendientes(db)
puts ""
puts "#{Color::CYAN}⏳ Eventos pendientes de cierre:#{Color::RESET}"
# Filtrar por bitácora de hoy y estado '⏳' o '⏸️'
fecha_hoy = Date.today.to_s
bitacora = db.find_bitacora_by_fecha(fecha_hoy)
unless bitacora
puts " #{Color::DIM}No hay bitácora para hoy.#{Color::RESET}"
return
end
# Buscar todos los pendientes de hoy
entradas_pendientes = db.list_entradas(
bitacora_id: bitacora['id'],
estado: '⏳',
limit: 20
)
# Sumamos pausados si los hay
entradas_pausadas = db.list_entradas(
bitacora_id: bitacora['id'],
estado: '⏸️',
limit: 20
)
entradas = entradas_pendientes + entradas_pausadas
if entradas.empty?
puts " #{Color::GREEN}¡Excelente! No hay eventos pendientes de cierre.#{Color::RESET}"
return
end
# Ordenar por hora de inicio (más antiguos primero)
entradas = entradas.sort_by { |e| e['inicio'] }
entradas.each do |e|
id = e['id'].to_s
inicio = e['inicio'][0..4] # HH:MM:SS -> HH:MM
nodo = (e['nodo_nombre'] || '(sin nodo)')[0..14]
desc = e['descripcion']
desc = desc[0..47] + '...' if desc.length > 50
estado_icon = e['estado']
puts " #{estado_icon} #{Color::YELLOW}ID #{id.ljust(5)}#{Color::RESET} #{inicio.ljust(5)} #{nodo.ljust(15)} #{desc}"
end
end
+2 -2
View File
@@ -104,7 +104,7 @@ module ADN
# Obtener passphrase de la bóveda
@passphrase = nil
begin
candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb')
candados_path = File.join(File.dirname(__FILE__), '../seguridad/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')
@@ -212,7 +212,7 @@ module ADN
end
def system_ssh_status_win(ip, remote_cmd)
candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb')
candados_path = File.join(File.dirname(__FILE__), '../seguridad/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"
+202
View File
@@ -0,0 +1,202 @@
# frozen_string_literal: true
# adn/tools/cli/tailscale.rb — Subcomando para gestión de identidades Tailscale
# ==============================================================================
# Este subcomando permite alternar entre diferentes perfiles de Tailscale
# (ej. DASUTEN vs Personal) de forma segura utilizando candados.
#
# Usa `candados.rb sudo` como primitiva reutilizable para ejecutar
# comandos con privilegios (no implementa sudo propio).
#
# Perfiles:
# - dasuten: pcdasu0@frlr.utn.edu.ar (ecosistema DASUTEN)
# - personal: rmonla@gmail.com (otros ecosistemas/topologías)
require_relative '../core/colores'
module ADN
class SubcomandoTailscale
CANDADOS_PATH = File.expand_path('../seguridad/candados.rb', __dir__)
def initialize(args, logger)
@args = args
@logger = logger
end
def ejecutar
if @args.empty? || @args[0] == 'ayuda'
mostrar_ayuda
return
end
accion = @args.shift
case accion
when 'switch'
ejecutar_switch
when 'status', 'estado'
ejecutar_status
when 'list', 'listar'
ejecutar_list
when 'setup'
ejecutar_setup
else
puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
def mostrar_ayuda
puts <<~AYUDA
#{Color::CYAN}🌐 Gestión de Identidades Tailscale#{Color::RESET}
#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run tailscale <acción> [perfil]
#{Color::YELLOW}Acciones:#{Color::RESET}
switch <perfil> Alternar a un perfil (dasuten, personal)
status Ver estado actual de Tailscale y cuenta activa
list Listar perfiles disponibles en Tailscale
setup Configurar operador (una vez, evita pedir sudo)
#{Color::YELLOW}Perfiles:#{Color::RESET}
dasuten Ecosistema DASUTEN (pcdasu0@frlr.utn.edu.ar)
personal Otros ecosistemas (rmonla@gmail.com)
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run tailscale setup
./adn/tools/run tailscale switch dasuten
./adn/tools/run tailscale switch personal
./adn/tools/run tailscale status
AYUDA
end
# ─── Status ───────────────────────────────────────────────────────
def ejecutar_status
puts "#{Color::CYAN}🔍 Estado actual de Tailscale:#{Color::RESET}"
puts "#{Color::DIM}#{'─' * 60}#{Color::RESET}"
system('tailscale status')
puts ""
cuenta = `tailscale status --json 2>/dev/null | jq -r '.User[].LoginName' 2>/dev/null`.strip
if cuenta.empty? || cuenta == 'null'
puts "#{Color::YELLOW}⚠ No se pudo determinar la cuenta activa.#{Color::RESET}"
else
puts "#{Color::BOLD}Cuenta activa:#{Color::RESET} #{Color::GREEN}#{cuenta}#{Color::RESET}"
end
operador_ok = system('tailscale switch --list > /dev/null 2>&1')
if operador_ok
puts "#{Color::GREEN}✓ Operador configurado (switch sin sudo)#{Color::RESET}"
else
puts "#{Color::YELLOW}⚠ Operador NO configurado. Ejecutá: ./adn/tools/run tailscale setup#{Color::RESET}"
end
end
# ─── List ─────────────────────────────────────────────────────────
def ejecutar_list
puts "#{Color::CYAN}📋 Perfiles disponibles en Tailscale:#{Color::RESET}"
# Intentar sin sudo (operador configurado)
output = `tailscale switch --list 2>&1`
if $?.success?
puts output
else
# Fallback: usar candados sudo (reutilizable)
candados_sudo('tailscale', 'switch', '--list')
end
end
# ─── Setup ────────────────────────────────────────────────────────
def ejecutar_setup
@logger.info("Configurando operador Tailscale para #{ENV['USER']}...")
puts "#{Color::CYAN}⚙️ Configurando operador Tailscale...#{Color::RESET}"
puts "#{Color::DIM}Esto permite gestionar Tailscale sin sudo en el futuro.#{Color::RESET}"
success = candados_sudo('tailscale', 'set', "--operator=#{ENV['USER']}")
if success
puts "#{Color::GREEN}✅ Operador configurado exitosamente.#{Color::RESET}"
@logger.exito("Operador Tailscale configurado para #{ENV['USER']}")
else
puts "#{Color::RED}✗ No se pudo configurar el operador.#{Color::RESET}"
puts " Verificá la clave sudo en la bóveda: ruby candados.rb ayuda"
end
end
# ─── Switch ───────────────────────────────────────────────────────
def ejecutar_switch
perfil = @args.shift
if perfil.nil?
puts "#{Color::RED}✗ Falta nombre del perfil (dasuten|personal)#{Color::RESET}"
return
end
case perfil.downcase
when 'dasuten', 'd'
switch_to('dasuten')
when 'personal', 'p'
switch_to('personal')
else
puts "#{Color::YELLOW}⚠ Perfil no reconocido. Perfiles válidos: dasuten, personal#{Color::RESET}"
end
end
def switch_to(tipo)
@logger.info("Cambiando perfil Tailscale a: #{tipo}")
# 1. Obtener usuario de la bóveda
clave_user = tipo == 'dasuten' ? 'tailscale_dasuten_user' : 'tailscale_personal_user'
user = obtener_secreto(clave_user)
if user.nil? || user.empty?
@logger.error("No se encontró '#{clave_user}' en la bóveda.")
puts "#{Color::RED}✗ Credencial '#{clave_user}' no encontrada en candados.#{Color::RESET}"
return
end
puts "#{Color::CYAN}🔄 Cambiando a perfil: #{Color::BOLD}#{user}#{Color::RESET}..."
# 2. Intentar sin sudo (operador configurado)
if system("tailscale switch '#{user}' > /dev/null 2>&1")
exito_switch(tipo, user)
return
end
# 3. Fallback: usar candados sudo (primitiva reutilizable)
puts "#{Color::DIM}Usando candados sudo...#{Color::RESET}"
if candados_sudo('tailscale', 'switch', user)
exito_switch(tipo, user)
return
end
# 4. Perfil sin sesión → login manual
@logger.advertencia("El perfil #{tipo} no tiene sesión activa.")
puts "#{Color::YELLOW}⚠ El perfil '#{user}' no tiene sesión en este equipo.#{Color::RESET}"
puts " Necesitás iniciar sesión manualmente:"
puts " #{Color::BOLD}ruby candados.rb sudo tailscale login#{Color::RESET}"
end
def exito_switch(tipo, user)
puts "#{Color::GREEN}✅ Perfil Tailscale cambiado a '#{tipo}' (#{user}).#{Color::RESET}"
@logger.exito("Perfil Tailscale cambiado a #{tipo} (#{user})")
puts ""
ejecutar_status
end
# ─── Utilidades ───────────────────────────────────────────────────
# Obtiene un secreto de la bóveda (requiere sesión abierta)
def obtener_secreto(clave)
output = `ruby #{CANDADOS_PATH} get #{clave} 2>/dev/null`.strip
return nil if output.empty? || output.include?('Error') || output.include?('✗')
output
end
# Ejecuta un comando con sudo usando candados.rb sudo (primitiva reutilizable)
def candados_sudo(*cmd_parts)
system("ruby #{CANDADOS_PATH} sudo #{cmd_parts.join(' ')}")
end
end
end