[Core/Infra] Provisionamiento SSH en pc-dasu0 y unificación Nginx tailscale
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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...")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user