refactor(ambitos): Migración completa dtic-DASUTEN (A04) y dtic-BKPs (A03) + herramientas ADN

Migración dtic-DASUTEN a estándar A04:
- A04_dtic-DASUTEN.md: Manifiesto creado
- A04.P001 a A04.P005: Planes renombrados de P2601.*
- _hist/_hist_P2601_dasuten.md: Legacy archivado

Migración dtic-BKPs a estándar A03:
- A03_dtic-BKPs.md: Manifiesto creado
- A03.P001, A03.P002: Planes migrados
- docs/ambito/dtic-BKPs/: Nueva estructura

Herramientas ADN nuevas:
- adn/tools/bkps/: BKPs migrado a tools ADN
- adn/tools/cli/dron.rb: Vigía de tareas largas
- adn/tools/cli/copiloto.rb: Asistente CLI
- adn/tools/cli/novato.rb: CLI de aprendizaje

Limpieza:
- dtic-BKPs/ → _hist/dtic-BKPs/ (legacy archivado)
- adn/tools/cli/backup.rb → _hist/ (migrado a bkps.rb)

Otros:
- nodos/pcv-dasu1.md, srvv-data.md, srvv-nginx-rm.md: Actualizados
- adn/tools/core/conciliador.rb: Mejoras
- adn/tools/w-zombi/data/cmd.json: Actualizado

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo Monla
2026-04-07 10:23:53 -03:00
co-authored by Claude Opus 4.6
parent 018e45e046
commit 1802c25194
61 changed files with 3148 additions and 1201 deletions
+825
View File
@@ -0,0 +1,825 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ==========================================================
# adn/tools/bkps/bkps.rb — CLI BKPs para ecosistema ADN
# ----------------------------------------------------------
# Migrado desde dtic-BKPs v6.0 (A03.P002)
# Uso: ./adn/tools/run bkps <subcomando>
# ==========================================================
require 'optparse'
require 'shellwords'
require 'json'
require 'date'
require 'time'
require_relative 'lib/config'
require_relative 'lib/proc_proxmox'
require_relative 'lib/proc_xen'
require_relative 'lib/sync_rclone'
require_relative 'lib/menu'
module ADN
class SubcomandoBkps
BKPS_DIR = File.dirname(File.expand_path(__FILE__))
CONFIG_PATH = File.join(BKPS_DIR, 'bkps.yml')
TIPOS = {
'proxmox' => -> (log) { BKPs::Procesador::Proxmox.new(log) },
'xva' => -> (log) { BKPs::Procesador::Xen.new(log) },
'rclone' => -> (log) { BKPs::Procesador::Rclone.new(log) },
}.freeze
def initialize(args, logger = nil)
@args = args
@logger = logger || ADN::Logger.new
@config = BKPs::Config.new(CONFIG_PATH)
end
def ejecutar
if @args.empty? || @args.first == 'help' || @args.first == '--help'
mostrar_ayuda
return
end
comando = @args.shift
case comando
when 'list'
cmd_list
when 'run'
cmd_run(@args)
when 'menu'
cmd_menu
when 'status'
cmd_status
when 'estados'
cmd_estados
when 'sanear'
cmd_sanear(@args)
when 'backup'
cmd_backup(@args)
when 'buscar'
cmd_buscar(@args)
else
puts "#{Color::RED}✗ Subcomando desconocido en bkps: #{comando}#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
# ─── Subcomandos ─────────────────────────────────────────────
def cmd_buscar(args)
nodo = args.shift
unless nodo
puts "#{Color::RED}✗ Falta nombre de nodo#{Color::RESET}"
puts "Uso: ./adn/tools/run bkps buscar <nodo>"
return
end
retencion = @config.raw_config['retencion'] || {}
tiers = retencion['tiers'] || []
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'candados', 'candados.rb')
vmid_map = cargar_mapa_vmid
vmid_inverso = vmid_map.invert # nodo => vmid
# Normalizar búsqueda (case insensitive)
patron = nodo.downcase
total = 0
puts "\n#{Color::BOLD}#{Color::CYAN}🔍 Backups de '#{nodo}' en todos los tiers#{Color::RESET}\n"
tiers.each_with_index do |tier, idx|
tier_num = idx + 1
resultados = []
case tier['tipo']
when 'proxmox'
system("ruby #{candados_path} authorize > /dev/null 2>&1")
storage = tier['storage']
hosts = tier['hosts'] || []
ruta_dump = "/mnt/pve/#{storage}/dump"
# Buscar VMID del nodo
vmid = vmid_inverso[nodo] || vmid_inverso.find { |k, _| k.downcase.include?(patron) }&.last
if vmid
hosts.each do |h|
cmd_ls = "ls -lh #{ruta_dump}/vzdump-*#{vmid}* 2>/dev/null"
res = `ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{h['ip']} "#{cmd_ls}"' 2>&1`
unless res.include?('No such file') || res.strip.empty? || res.include?('Connection')
res.split("\n").each do |linea|
next if linea.empty? || linea.include?('OPTIONS') || linea.include?('total')
partes = linea.split(/\s+/)
next if partes.length < 9
size = partes[4]
fecha = "#{partes[5]} #{partes[6]} #{partes[7]}"
nombre = File.basename(partes[8..-1].join(' '))
resultados << { nombre: nombre, size: size, fecha: fecha }
end
break
end
end
end
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
when 'local'
ruta_base = tier['ruta']
# Buscar directorio del nodo (case insensitive)
Dir.glob("#{ruta_base}/*").each do |dir|
dir_name = File.basename(dir)
next unless dir_name.downcase.include?(patron)
Dir.glob("#{dir}/*.{tar.gz,tar.zst,vma.zst}").sort_by { |f| File.mtime(f) }.each do |archivo|
nombre = File.basename(archivo)
size = format_size(File.size(archivo))
fecha = File.mtime(archivo).strftime('%Y-%m-%d %H:%M')
resultados << { nombre: nombre, size: size, fecha: fecha }
end
end
when 'rclone'
remote = tier['remote']
# Buscar directorio del nodo
output = `rclone lsl '#{remote}' --max-depth 2 2>&1`
output.split("\n").each do |linea|
partes = linea.strip.split(/\s+/, 4)
next if partes.length < 4
ruta_rel = partes[3]
next unless ruta_rel.include?('/')
dir_name = ruta_rel.split('/').first
next unless dir_name.downcase.include?(patron)
nombre = File.basename(ruta_rel)
next unless nombre.match?(/\.(tar\.gz|tar\.zst|vma\.zst)$/)
size_bytes = partes[0].to_i
fecha = "#{partes[1]} #{partes[2].split('.').first}"
resultados << { nombre: nombre, size: format_size(size_bytes), fecha: fecha }
end
end
# Mostrar
puts "#{Color::BOLD}Tier #{tier_num}: #{tier['nombre']}#{Color::RESET}"
if resultados.empty?
puts " #{Color::DIM}(sin backups)#{Color::RESET}"
else
resultados.each do |r|
puts " #{Color::GREEN}#{Color::RESET} #{r[:fecha]} #{r[:size].rjust(8)} #{r[:nombre]}"
end
total += resultados.length
end
puts ""
end
puts "#{Color::BOLD}Total: #{total} archivo(s) en #{tiers.length} tiers#{Color::RESET}"
end
def format_size(bytes)
return '0B' if bytes == 0
units = ['B', 'KB', 'MB', 'GB', 'TB']
exp = (Math.log(bytes) / Math.log(1024)).to_i
exp = units.length - 1 if exp >= units.length
"%.1f%s" % [bytes.to_f / (1024 ** exp), units[exp]]
end
def cmd_list
puts "#{Color::BOLD}Tareas:#{Color::RESET}"
@config.tareas.each_with_index do |t, i|
puts " T#{i+1} #{t[:id]} #{t[:texto]} [#{t[:tipo]}] #{t[:origen]}#{t[:destino]}"
end
puts "\n#{Color::BOLD}Comandos:#{Color::RESET}"
@config.comandos.each_with_index do |c, i|
puts " C#{i+1} #{c[:id]} #{c[:texto]}#{c[:tareas]&.join(', ')}"
end
end
def cmd_run(args)
options = {}
OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run bkps run <T1|C1|ID> [opciones]"
opts.on("--batch", "Modo batch (sin interacción, para cron)") { options[:batch] = true }
opts.on("--dry-run", "Simulación sin ejecutar") { options[:dry_run] = true }
end.parse!(args)
ref = args.shift
unless ref
puts "#{Color::RED}✗ Falta referencia (T1, C1, o ID de tarea/comando)#{Color::RESET}"
return
end
case ref.upcase
when /^T(\d+)$/
idx = $1.to_i - 1
tarea = @config.tareas[idx]
if tarea
ejecutar_tarea(tarea, options)
else
puts "#{Color::RED}✗ Tarea T#{idx+1} no encontrada#{Color::RESET}"
end
when /^C(\d+)$/
idx = $1.to_i - 1
comando = @config.comandos[idx]
if comando
ejecutar_comando(comando, options)
else
puts "#{Color::RED}✗ Comando C#{idx+1} no encontrado#{Color::RESET}"
end
else
# Buscar por ID textual
tarea = @config.find_tarea(ref)
if tarea
ejecutar_tarea(tarea, options)
else
comando = @config.comandos.find { |c| c[:id].to_s == ref }
if comando
ejecutar_comando(comando, options)
else
puts "#{Color::RED}✗ '#{ref}' no es una tarea ni comando válido#{Color::RESET}"
end
end
end
end
def cmd_menu
# Auto-screen para sesiones desatendidas
auto_screen! unless ENV['TERM']&.start_with?('screen') || ENV['STY']
BKPs::Menu.new(@config, self, @logger).ejecutar
end
def cmd_status
puts "#{Color::BOLD}#{Color::CYAN}dtic-BKPs — Estado del Sistema#{Color::RESET}\n\n"
puts "#{Color::BOLD}Configuración:#{Color::RESET} #{CONFIG_PATH}"
puts "#{Color::BOLD}Auto-avanzar:#{Color::RESET} #{@config.auto_avanzar}"
puts "#{Color::BOLD}Tareas:#{Color::RESET} #{@config.tareas.length}"
puts "#{Color::BOLD}Comandos:#{Color::RESET} #{@config.comandos.length}"
puts ""
# Verificar herramientas
tools = { 'rclone' => 'rclone version', 'pv' => 'pv --version', 'tar' => 'tar --version' }
puts "#{Color::BOLD}Herramientas:#{Color::RESET}"
tools.each do |name, cmd|
disponible = system("#{cmd} > /dev/null 2>&1")
icono = disponible ? "#{Color::GREEN}#{Color::RESET}" : "#{Color::RED}#{Color::RESET}"
puts " #{icono} #{name}"
end
# Reporte de espacio en disco
monitoreo = @config.raw_config['monitoreo']
if monitoreo && monitoreo['rutas_espacio']
puts "\n#{Color::BOLD}📊 Espacio en Disco#{Color::RESET}"
puts "" * 70
printf " %-20s %8s %8s %8s %6s\n", 'Ruta', 'Total', 'Usado', 'Libre', 'Uso%'
puts " #{"" * 64}"
monitoreo['rutas_espacio'].each do |r|
ruta = r['ruta']
if File.exist?(ruta)
df = `df -h '#{ruta}' 2>/dev/null`.lines.last&.split
if df && df.length >= 5
printf " %-20s %8s %8s %8s %6s\n", r['nombre'], df[1], df[2], df[3], df[4]
end
else
printf " %-20s %8s\n", r['nombre'], "#{Color::RED}✖ no montado#{Color::RESET}"
end
end
end
# Retención configurada
retencion = @config.raw_config['retencion']
if retencion
puts "\n#{Color::BOLD}🗑️ Retención#{Color::RESET}"
puts " Días default: #{retencion['dias_default']}"
puts " Storage: #{retencion['storage']}"
end
end
# ─── Subcomandos asimilados de backup.rb (F4) ────────────────
def cmd_estados
@logger.info("Consultando estado de backups en cluster Proxmox")
hosts = [
{ nombre: 'srv-pmox1', ip: '10.0.10.201' },
{ nombre: 'srv-dasu', ip: '100.116.210.36' },
]
puts "\n#{Color::BOLD}🔍 Estado de Backups en Tiempo Real (Proxmox)#{Color::RESET}"
puts "" * 60
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'candados', 'candados.rb')
system("ruby #{candados_path} authorize > /dev/null 2>&1")
hosts.each do |h|
print "📡 #{h[:nombre]} (#{h[:ip]})... "
cmd = "/usr/bin/pvesh get /cluster/tasks --output-format json"
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{h[:ip]} '#{cmd}'"
clave = (h[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
output = `ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1`
if output.include?('[')
begin
json_str = output[output.index('[')..output.rindex(']')]
tasks = JSON.parse(json_str)
activas = tasks.select { |t| t['type'] == 'vzdump' && t['endtime'].nil? }
puts "#{Color::GREEN}OK#{Color::RESET}"
if activas.empty?
puts " #{Color::DIM}Sin tareas de backup activas.#{Color::RESET}"
else
activas.each do |t|
puts "#{Color::CYAN}#{t['id']}#{Color::RESET} [#{t['status'] || 'ejecutando'}] (Inicio: #{Time.at(t['starttime']).strftime('%H:%M:%S')})"
end
end
# Últimos 5 vzdump completados
recientes = tasks.select { |t| t['type'] == 'vzdump' && t['endtime'] }
.sort_by { |t| -t['endtime'] }.first(5)
unless recientes.empty?
puts " #{Color::DIM}Últimos completados:#{Color::RESET}"
recientes.each do |t|
estado = t['status'] == 'OK' ? "#{Color::GREEN}#{Color::RESET}" : "#{Color::RED}#{Color::RESET}"
puts " #{estado} #{Time.at(t['starttime']).strftime('%H:%M')}#{Time.at(t['endtime']).strftime('%H:%M')} #{t['id']}"
end
end
rescue => e
puts "#{Color::YELLOW}JSON ERROR: #{e.message}#{Color::RESET}"
end
else
puts "#{Color::RED}ERROR#{Color::RESET}"
puts " #{Color::DIM}No se pudo conectar o sin tareas.#{Color::RESET}"
end
end
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
end
def cmd_sanear(args)
retencion = @config.raw_config['retencion'] || {}
options = { dias: retencion['dias_default'] || 6 }
OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run bkps sanear [opciones]"
opts.on("--dias N", Integer, "Días de retención (default: #{options[:dias]})") { |d| options[:dias] = d }
opts.on("--tier N", Integer, "Solo tier N (1=proxmox, 2=local, 3=nube)") { |t| options[:tier] = t }
opts.on("--dry-run", "Solo listar, no borrar") { options[:dry_run] = true }
end.parse!(args)
tiers = retencion['tiers'] || []
if tiers.empty?
@logger.error("No hay tiers configurados en retencion.tiers")
return
end
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'candados', 'candados.rb')
vmid_map = cargar_mapa_vmid
total_eliminados = 0
total_protegidos = 0
tiers.each_with_index do |tier, idx|
tier_num = idx + 1
next if options[:tier] && options[:tier] != tier_num
puts "\n#{Color::BOLD}#{Color::CYAN}═══ Tier #{tier_num}: #{tier['nombre']} ═══#{Color::RESET}"
case tier['tipo']
when 'proxmox'
elim, prot = sanear_tier_proxmox(tier, options, candados_path, vmid_map)
when 'local'
elim, prot = sanear_tier_local(tier, options)
when 'rclone'
elim, prot = sanear_tier_rclone(tier, options)
else
@logger.error("Tipo de tier desconocido: #{tier['tipo']}")
next
end
total_eliminados += (elim || 0)
total_protegidos += (prot || 0)
end
puts "\n#{Color::BOLD}═══ Resumen ═══#{Color::RESET}"
puts " Eliminados: #{total_eliminados} sets"
puts " 🛡️ Protegidos: #{total_protegidos} nodo(s)"
end
# ─── Tier 1: Proxmox (SSH) ─────────────────────────────────
def sanear_tier_proxmox(tier, options, candados_path, vmid_map)
dias = options[:dias]
storage = tier['storage']
hosts = tier['hosts'] || []
ruta_dump = "/mnt/pve/#{storage}/dump"
system("ruby #{candados_path} authorize > /dev/null 2>&1")
output_ls = ""
hosts.each do |h|
cmd_ls = "ls -1 --full-time #{ruta_dump}/vzdump-*"
res = `ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{h['ip']} "#{cmd_ls}"' 2>&1`
unless res.include?('No such file') || res.include?('Connection refused')
output_ls = res
break
end
end
if output_ls.empty? || output_ls.include?('No such file')
puts "#{Color::GREEN}✓ Sin backups en #{ruta_dump}#{Color::RESET}"
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
return [0, 0]
end
# Parsear archivos vzdump
todos = {}
por_nodo = {}
limite = Time.now - (dias * 86400)
output_ls.split("\n").each do |linea|
next if linea.empty? || linea.include?('OPTIONS')
partes = linea.split(/\s+/)
next if partes.length < 9
fecha_str = "#{partes[5]} #{partes[6]}"
ruta = partes[8..-1].join(' ')
nombre = File.basename(ruta)
fp = nombre.sub(/\.(log|vma\.zst|tar\.zst|vma\.zst\.notes|tar\.zst\.notes)$/, '')
begin
mtime = Time.parse(fecha_str)
vmid = fp.match(/vzdump-(?:qemu|lxc)-(\d+)-/)&.[](1)
nodo = vmid_map[vmid] || "VM-#{vmid}"
todos[fp] ||= { fecha: mtime, nodo: nodo, archivos: [] }
todos[fp][:archivos] << ruta
por_nodo[nodo] ||= []
por_nodo[nodo] << fp unless por_nodo[nodo].include?(fp)
rescue; next; end
end
elim, prot = aplicar_retencion(todos, por_nodo, limite, options)
# Borrar via SSH
if elim > 0 && !options[:dry_run]
todos.select { |fp, _| !protegido?(fp, por_nodo, todos) && todos[fp][:fecha] < limite }.each_value do |info|
info[:archivos].each do |ruta|
system("ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{hosts.first['ip']} \"rm -f '#{ruta}'\"' > /dev/null 2>&1")
end
end
end
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
[elim, prot]
end
# ─── Tier 2: Local (filesystem) ────────────────────────────
def sanear_tier_local(tier, options)
dias = options[:dias]
ruta_base = tier['ruta']
unless File.directory?(ruta_base)
puts "#{Color::RED}✗ Ruta no encontrada: #{ruta_base}#{Color::RESET}"
return [0, 0]
end
todos = {}
por_nodo = {}
limite = Time.now - (dias * 86400)
Dir.glob("#{ruta_base}/*").select { |d| File.directory?(d) }.each do |dir_nodo|
nodo = File.basename(dir_nodo)
Dir.glob("#{dir_nodo}/*.{tar.gz,tar.zst,vma.zst}").each do |archivo|
nombre = File.basename(archivo)
mtime = File.mtime(archivo)
todos[nombre] ||= { fecha: mtime, nodo: nodo, archivos: [] }
todos[nombre][:archivos] << archivo
por_nodo[nodo] ||= []
por_nodo[nodo] << nombre unless por_nodo[nodo].include?(nombre)
end
end
elim, prot = aplicar_retencion(todos, por_nodo, limite, options)
# Borrar archivos locales
if elim > 0 && !options[:dry_run]
candidatos = calcular_candidatos(todos, por_nodo, limite)
candidatos.each_value do |info|
info[:archivos].each { |f| File.delete(f) rescue nil }
end
end
[elim, prot]
end
# ─── Tier 3: Nube (rclone) ─────────────────────────────────
def sanear_tier_rclone(tier, options)
dias = options[:dias]
remote = tier['remote']
output = `rclone lsl '#{remote}' --max-depth 2 2>&1`
if output.include?('ERROR') || output.strip.empty?
puts "#{Color::GREEN}✓ Sin backups en #{remote}#{Color::RESET}"
return [0, 0]
end
todos = {}
por_nodo = {}
limite = Time.now - (dias * 86400)
output.split("\n").each do |linea|
# rclone lsl format: SIZE DATE TIME PATH
partes = linea.strip.split(/\s+/, 4)
next if partes.length < 4
fecha_str = "#{partes[1]} #{partes[2]}"
ruta_rel = partes[3]
next unless ruta_rel.include?('/')
nodo = ruta_rel.split('/').first
nombre = File.basename(ruta_rel)
next unless nombre.match?(/\.(tar\.gz|tar\.zst|vma\.zst)$/)
begin
mtime = Time.parse(fecha_str)
ruta_full = "#{remote}/#{ruta_rel}"
todos[nombre] ||= { fecha: mtime, nodo: nodo, archivos: [] }
todos[nombre][:archivos] << ruta_full
por_nodo[nodo] ||= []
por_nodo[nodo] << nombre unless por_nodo[nodo].include?(nombre)
rescue; next; end
end
elim, prot = aplicar_retencion(todos, por_nodo, limite, options)
# Borrar via rclone
if elim > 0 && !options[:dry_run]
candidatos = calcular_candidatos(todos, por_nodo, limite)
candidatos.each_value do |info|
info[:archivos].each do |ruta|
system("rclone deletefile '#{ruta}' > /dev/null 2>&1")
end
end
end
[elim, prot]
end
# ─── Lógica compartida de retención ────────────────────────
def aplicar_retencion(todos, por_nodo, limite, options)
protegidos_fps = {}
por_nodo.each do |nodo, nombres|
ordenados = nombres.sort_by { |n| -(todos[n][:fecha].to_i) }
protegidos_fps[ordenados.first] = true
end
candidatos = calcular_candidatos(todos, por_nodo, limite)
protegidos = protegidos_fps.size
# Mostrar protegidos
protegidos_fps.each do |nombre, _|
info = todos[nombre]
edad = ((Time.now - info[:fecha]) / 86400).to_i
@logger.info("🛡️ #{info[:nodo]} (#{edad}d) — protegido")
end
if candidatos.empty?
puts "#{Color::GREEN}✓ Nada que limpiar.#{Color::RESET}"
puts "#{Color::CYAN}🛡️ #{protegidos} nodo(s) protegido(s)#{Color::RESET}" if protegidos > 0
return [0, protegidos]
end
lista = candidatos.values.sort_by { |b| b[:fecha] }
puts "\n#{Color::BOLD}🗑️ Candidatos a limpieza#{Color::RESET} (> #{((Time.now - limite) / 86400).to_i}d)"
puts "" * 70
lista.each_with_index do |b, i|
edad = ((Time.now - b[:fecha]) / 86400).to_i
puts " #{(i+1).to_s.rjust(2)} #{Color::CYAN}#{b[:nodo]}#{Color::RESET} #{b[:fecha].strftime('%Y-%m-%d')} (#{edad}d, #{b[:archivos].length} arch.)"
end
puts "" * 70
total_arch = candidatos.values.sum { |b| b[:archivos].length }
puts "Total: #{lista.length} sets (#{total_arch} archivos)"
puts "#{Color::CYAN}🛡️ #{protegidos} nodo(s) protegido(s)#{Color::RESET}" if protegidos > 0
if options[:dry_run]
puts "\n#{Color::YELLOW}[DRY-RUN] No se borró nada.#{Color::RESET}"
return [0, protegidos]
end
print "\n¿Borrar todos? (S/N): "
resp = STDIN.gets&.chomp&.strip&.downcase
unless resp == 's'
puts "Cancelado."
return [0, protegidos]
end
lista.each_with_index do |b, i|
puts "[#{i+1}/#{lista.length}] Limpiando #{b[:nodo]}..."
end
@logger.info("#{lista.length} sets eliminados.")
[lista.length, protegidos]
end
def calcular_candidatos(todos, por_nodo, limite)
protegidos_fps = {}
por_nodo.each do |nodo, nombres|
ordenados = nombres.sort_by { |n| -(todos[n][:fecha].to_i) }
protegidos_fps[ordenados.first] = true
end
candidatos = {}
todos.each do |nombre, info|
next if protegidos_fps[nombre]
next unless info[:fecha] < limite
candidatos[nombre] = info
end
candidatos
end
def cmd_backup(args)
options = { modo: 'stop', storage: 'zfsDISCO1', compress: 'zstd' }
OptionParser.new do |opts|
opts.banner = "Uso: ./adn/tools/run bkps backup <nodo>"
opts.on("--mode MODO", "Modo vzdump (stop|snapshot|suspend)") { |m| options[:modo] = m }
opts.on("--storage S", "Storage Proxmox") { |s| options[:storage] = s }
end.parse!(args)
nodo = args.shift
unless nodo
puts "#{Color::RED}✗ Falta nombre de nodo#{Color::RESET}"
puts "Uso: ./adn/tools/run bkps backup <nodo> [--mode stop] [--storage local]"
return
end
datos = obtener_contexto_nodo(nodo)
unless datos
@logger.error("No se pudo determinar contexto para #{nodo}")
return
end
@logger.info("Backup de #{nodo}: VM #{datos[:vmid]} en #{datos[:host_name]} (#{datos[:host_ip]})")
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'candados', 'candados.rb')
system("ruby #{candados_path} authorize > /dev/null 2>&1")
cmd_vzdump = "vzdump #{datos[:vmid]} --mode #{options[:modo]} --storage #{options[:storage]} --compress #{options[:compress]}"
clave = (datos[:host_name] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
cmd_ssh = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{cmd_vzdump}\""
@logger.info("Ejecutando vzdump remoto...")
IO.popen("ruby #{candados_path} run #{clave} SSHPASS '#{cmd_ssh}' 2>&1") do |pf|
pf.each_line { |line| puts " #{Color::DIM}#{line.strip}#{Color::RESET}" }
end
exito = $?.success?
# Detectar errores de vzdump en el output capturado
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
if exito
@logger.info("✔ Backup de #{nodo} completado exitosamente.")
else
@logger.error("Fallo en backup de #{nodo}.")
exit 1
end
end
# ─── Helpers ─────────────────────────────────────────────────
def obtener_contexto_nodo(nombre)
ruta = File.join(ADN::NODOS_DIR, "#{nombre}.md")
return nil unless File.exist?(ruta)
contenido = File.read(ruta)
# VMID: soporta formato lista (- **VMID**: `107`) y tabla (| **VMID** | `116` |)
vmid = contenido.match(/(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i)&.[](1) ||
contenido.match(/\|\s*\*\*VMID\*\*\s*\|\s*`?(\d+)`?\s*\|/i)&.[](1)
# Host: soporta formato lista y tabla
host_name = contenido.match(/(?:Padre\/Host|Host Anfitrión|Anfitrión|Host)\*\*?:?\s*`([^`]+)`/i)&.[](1) ||
contenido.match(/\|\s*\*\*Host\*\*\s*\|\s*`([^`]+)`\s*\|/i)&.[](1)
return nil unless vmid && host_name
ruta_host = File.join(ADN::NODOS_DIR, "#{host_name}.md")
host_ip = nil
if File.exist?(ruta_host)
c = File.read(ruta_host)
host_ip = c.match(/\|\s*IP[^\|]*\|\s*`([^`]+)`\s*\|/)&.[](1) ||
c.match(/IP\*\*:?\s*`([^`]+)`/)&.[](1) ||
c.match(/IP:\s*([\d\.]+)/)&.[](1)
end
return nil unless host_ip
host_ip = host_ip.split('/')[0] if host_ip.include?('/')
{ vmid: vmid, host_name: host_name, host_ip: host_ip }
end
def cargar_mapa_vmid
mapa = {}
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |f|
begin
contenido = File.read(f)
nombre = File.basename(f, '.md')
vmid = contenido.match(/(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i)&.[](1)
mapa[vmid.to_s] = nombre if vmid
rescue; next; end
end
mapa
end
# ─── Dispatcher ──────────────────────────────────────────────
public
def ejecutar_tarea(tarea, options = {})
tipo = tarea[:tipo].to_s
factory = TIPOS[tipo]
unless factory
@logger.error("Tipo desconocido: #{tipo}")
return false
end
if options[:dry_run]
puts "#{Color::YELLOW}[DRY-RUN] Ejecutaría: #{tarea[:texto]} (#{tipo})#{Color::RESET}"
return true
end
t_inicio = Time.now
@logger.info("#{tarea[:texto]} (#{tipo})")
procesador = factory.call(@logger)
resultado = procesador.ejecutar(tarea)
duracion = (Time.now - t_inicio).to_i
mins = duracion / 60
segs = duracion % 60
@logger.info("#{tarea[:texto]}#{mins}m#{segs}s")
resultado
end
def ejecutar_comando(comando, options = {})
t_inicio = Time.now
@logger.info("=== #{comando[:texto]} ===")
tareas_ids = comando[:tareas] || []
fallos = 0
tareas_ids.each_with_index do |id, i|
tarea = @config.find_tarea(id)
if tarea
@logger.info("Paso #{i + 1}/#{tareas_ids.length}: #{tarea[:texto]}")
resultado = ejecutar_tarea(tarea, options)
fallos += 1 unless resultado
else
@logger.error("Tarea '#{id}' no encontrada en comando '#{comando[:id]}'.")
fallos += 1
end
end
duracion = (Time.now - t_inicio).to_i
mins = duracion / 60
segs = duracion % 60
if fallos == 0
@logger.info("✔ '#{comando[:texto]}' completado exitosamente (#{mins}m#{segs}s)")
else
@logger.error("✖ '#{comando[:texto]}' completado con #{fallos} fallo(s) (#{mins}m#{segs}s)")
exit 1 if options[:batch]
end
# Post-sanear automático si el comando lo tiene configurado
if comando[:post_sanear] && fallos == 0
retencion = @config.raw_config['retencion'] || {}
dias = retencion['dias_default'] || 6
@logger.info("🧹 Post-sanear automático (retención: #{dias} días)")
cmd_sanear(["--dias", dias.to_s, "--dry-run"])
end
end
private
def auto_screen!
puts "\n#{Color::CYAN} Relanzando en sesión screen para permitir desatención...#{Color::RESET}"
cmd = ([$PROGRAM_NAME] + ['bkps', 'menu']).map { |a| Shellwords.escape(a) }.join(' ')
nombre = "bkps_#{Time.now.strftime('%H%M%S')}"
puts "Para reconectarte: #{Color::YELLOW}screen -r #{nombre}#{Color::RESET}"
sleep 1
system('screen', '-dmS', nombre, 'bash', '-c', "#{cmd}; echo ''; read -p 'Enter para cerrar...'")
exit 0
end
def mostrar_ayuda
puts <<~HELP
#{Color::BOLD}#{Color::CYAN}Herramienta ADN — Gestión de Backups (dtic-BKPs)#{Color::RESET}
#{Color::YELLOW}Uso:#{Color::RESET}
./adn/tools/run bkps list Listar tareas y comandos
./adn/tools/run bkps run <T1|C1|ID> Ejecutar tarea o comando
./adn/tools/run bkps run C4 --batch Modo batch (sin interacción)
./adn/tools/run bkps run T1 --dry-run Simulación sin ejecutar
./adn/tools/run bkps menu Menú interactivo (auto-screen)
./adn/tools/run bkps status Estado del sistema
./adn/tools/run bkps estados Estado Proxmox en tiempo real
./adn/tools/run bkps backup <nodo> Backup remoto individual (vzdump)
./adn/tools/run bkps sanear --dias 6 Limpiar backups antiguos
./adn/tools/run bkps help Esta ayuda
#{Color::YELLOW}Tareas configuradas:#{Color::RESET}
HELP
@config.tareas.each_with_index do |t, i|
puts " #{Color::GREEN}T#{i+1}#{Color::RESET} #{t[:texto]} #{Color::DIM}(#{t[:tipo]})#{Color::RESET}"
end
puts "\n #{Color::YELLOW}Comandos configurados:#{Color::RESET}"
@config.comandos.each_with_index do |c, i|
puts " #{Color::GREEN}C#{i+1}#{Color::RESET} #{c[:texto]}"
end
end
end
end
+104
View File
@@ -0,0 +1,104 @@
# Configuración dtic-BKPs — Tareas y Comandos de Backup
# Migrado desde dtic-BKPs_tasks.rb
auto_avanzar: true
tareas:
- id: proc_syncXen1
texto: "⚙️ Procesar archivos de BKPs de VMs: syncs-XEN01"
tipo: xva
origen: "/mnt/ns8Disco2/syncs-XEN01/"
destino: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/"
eliminar_origen: true
sobrescribir: false
- id: proc_syncPmox
texto: "⚙️ Procesar archivos de BKPs de VMs desde zfsDISCO1"
tipo: proxmox
origen: "pve_PMOX3:/mnt/pve/zfsDISCO1/dump/"
destino: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/"
eliminar_origen: false
sobrescribir: false
- id: sinc_pmox_ns8
texto: "📥 Descargar archivos de BKPs de VMs: zfsDISCO1"
tipo: rclone
origen: "pve_PMOX3:/mnt/pve/zfsDISCO1/dump/"
destino: "/mnt/ns8Disco2/syncs-PMOX/"
eliminar_origen: false
sobrescribir: false
- id: sync_BkpAntiguos_nube
texto: "☁️ Upload de BKPs a rmOneDrive: bkps-ANTIGUOS"
tipo: rclone
origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-ANTIGUOS/"
destino: "rmOneDrive:/dtic-BACKUPS/bkps-ANTIGUOS/"
eliminar_origen: false
sobrescribir: false
- id: sync_BkpsSERVIDORes_nube
texto: "☁️ Upload de bkps-SERVIDORes a rmOneDrive"
tipo: rclone
origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes/"
destino: "rmOneDrive:/dtic-BACKUPS/bkps-SERVIDORes/"
eliminar_origen: false
sobrescribir: false
- id: sync_BkpUsers_nube
texto: "☁️ Upload de bkps-USERs a rmOneDrive"
tipo: rclone
origen: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-USERs/"
destino: "rmOneDrive:/dtic-BACKUPS/bkps-USERs/"
eliminar_origen: false
sobrescribir: false
comandos:
- id: descargar_y_procesar_pmox
texto: "📥⚙️ Descargar y Procesar BKPs de PMOXs"
tareas: [sinc_pmox_ns8, proc_syncPmox]
- id: full_procesamiento
texto: "⚙️ Procesar TODOS los BKPs de NS8"
tareas: [proc_syncXen1, proc_syncPmox]
- id: full_upload
texto: "🚀 Subir a la Nube todos los BKPs"
tareas: [sync_BkpAntiguos_nube, sync_BkpsSERVIDORes_nube, sync_BkpUsers_nube]
- id: full_full
texto: "🚀 Procesar todo y Subir todo"
tareas: [proc_syncXen1, proc_syncPmox, sync_BkpsSERVIDORes_nube, sync_BkpAntiguos_nube, sync_BkpUsers_nube]
- id: full_full_sanear
texto: "🚀🧹 Procesar, Subir y Sanear"
tareas: [proc_syncXen1, proc_syncPmox, sync_BkpsSERVIDORes_nube, sync_BkpAntiguos_nube, sync_BkpUsers_nube]
post_sanear: true
# ─── Retención (3 tiers) ───────────────────────────────────
retencion:
dias_default: 6
tiers:
- nombre: "Proxmox (zfsDISCO1)"
tipo: proxmox
storage: zfsDISCO1
hosts:
- { nombre: srv-pmox1, ip: "10.0.10.201" }
- { nombre: srv-pmox2, ip: "10.0.10.202" }
- { nombre: srv-pmox3, ip: "10.0.10.203" }
- nombre: "Local (ns8Disco3)"
tipo: local
ruta: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes"
- nombre: "Nube (rmOneDrive)"
tipo: rclone
remote: "rmOneDrive:/dtic-BACKUPS/bkps-SERVIDORes"
# ─── Monitoreo ─────────────────────────────────────────────
monitoreo:
rutas_espacio:
- { nombre: ns8Disco2, ruta: "/mnt/ns8Disco2" }
- { nombre: ns8Disco3, ruta: "/mnt/ns8Disco3" }
- { nombre: bkps-SERVIDORes, ruta: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-SERVIDORes" }
- { nombre: bkps-ANTIGUOS, ruta: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-ANTIGUOS" }
- { nombre: bkps-USERs, ruta: "/mnt/ns8Disco3/dtic-BACKUPS/bkps-USERs" }
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# ==========================================================
# adn/tools/bkps/cron-bkps.sh — Wrapper cron para backups
# ----------------------------------------------------------
# Ejecuta un comando bkps con:
# - Lockfile (evita ejecuciones simultáneas)
# - Bitácora automática vía dron
# - Logging de salida
#
# Uso en crontab:
# 0 2 * * * /ruta/adn/tools/bkps/cron-bkps.sh C4
# 0 3 * * * /ruta/adn/tools/bkps/cron-bkps.sh T3
#
# Ejemplo manual:
# ./adn/tools/bkps/cron-bkps.sh C4 "Pipeline nocturno C4"
# ==========================================================
set -euo pipefail
# ─── Configuración ─────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
ADN_RUN="$PROJECT_ROOT/adn/tools/run"
LOCK_DIR="$PROJECT_ROOT/tmp/locks"
LOG_DIR="$PROJECT_ROOT/tmp/logs/bkps"
mkdir -p "$LOCK_DIR" "$LOG_DIR"
# ─── Argumentos ────────────────────────────────────────────
REF="${1:-}"
NOTA="${2:-Cron: bkps run $REF --batch}"
AMBITO="dtic-BKPs"
NODO="srv-ns8"
if [ -z "$REF" ]; then
echo "✗ Uso: cron-bkps.sh <T1|C1|ID> [nota]"
exit 1
fi
# ─── Lockfile ──────────────────────────────────────────────
LOCKFILE="$LOCK_DIR/bkps_${REF}.lock"
if [ -f "$LOCKFILE" ]; then
PID_LOCK=$(cat "$LOCKFILE")
if kill -0 "$PID_LOCK" 2>/dev/null; then
echo "⚠ bkps $REF ya está corriendo (PID: $PID_LOCK, lock: $LOCKFILE)"
exit 0 # No es error, simplemente ya está corriendo
else
echo " Lock stale removido (PID $PID_LOCK ya no existe)"
rm -f "$LOCKFILE"
fi
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
# ─── Log ───────────────────────────────────────────────────
FECHA=$(date +%Y-%m-%d)
HORA=$(date +%H:%M)
LOGFILE="$LOG_DIR/bkps_${REF}_${FECHA}_${HORA//:/}.log"
echo "[$(date)] cron-bkps: Iniciando $REF" >> "$LOGFILE"
# ─── Bitácora + Ejecución via Dron ──────────────────────
# Crear evento
EVENTO_ID=$("$ADN_RUN" db evento:crear \
--ambito "$AMBITO" \
--nodo "$NODO" \
--descripcion "$NOTA" \
--inicio "$HORA" \
--modo R --ia 2>&1 | grep -oP 'ID \K\d+' | tail -1)
if [ -z "$EVENTO_ID" ]; then
echo "[$(date)] cron-bkps: ⚠ No se pudo crear evento, ejecutando sin bitácora" >> "$LOGFILE"
"$ADN_RUN" bkps run "$REF" --batch >> "$LOGFILE" 2>&1
EXIT_CODE=$?
else
echo "[$(date)] cron-bkps: Evento #$EVENTO_ID creado, delegando al dron" >> "$LOGFILE"
"$ADN_RUN" dron lanzar \
--evento "$EVENTO_ID" \
--nota "$NOTA" \
-- "$ADN_RUN" bkps run "$REF" --batch >> "$LOGFILE" 2>&1
EXIT_CODE=$?
fi
echo "[$(date)] cron-bkps: Finalizado con código $EXIT_CODE" >> "$LOGFILE"
exit $EXIT_CODE
+71
View File
@@ -0,0 +1,71 @@
# frozen_string_literal: true
# ==========================================================
# adn/tools/bkps/lib/config.rb — Carga de config YAML
# Migrado desde dtic-BKPs v6.0 → ADN::BKPs
# ==========================================================
require 'yaml'
require 'fileutils'
module ADN
module BKPs
class Config
attr_reader :tareas, :comandos, :auto_avanzar, :raw_config
def initialize(path)
@path = path
reload!
end
def reload!
unless File.exist?(@path)
@tareas = []
@comandos = []
@auto_avanzar = true
return
end
data = YAML.safe_load(File.read(@path), permitted_classes: [Symbol], symbolize_names: true) || {}
@raw_config = YAML.safe_load(File.read(@path)) || {} # String keys for raw access
@auto_avanzar = data[:auto_avanzar] != false
@tareas = (data[:tareas] || []).map { |t| simbolizar(t) }
@comandos = (data[:comandos] || []).map { |c| simbolizar(c) }
end
def find_tarea(id)
@tareas.find { |t| t[:id].to_s == id.to_s }
end
def save!
data = {
'auto_avanzar' => @auto_avanzar,
'tareas' => @tareas.map { |t| stringify(t) },
'comandos' => @comandos.map { |c| stringify(c) }
}
File.write(@path, YAML.dump(data))
FileUtils.chmod(0644, @path)
end
def add_tarea(tarea)
@tareas << simbolizar(tarea)
end
def remove_tarea(index)
@tareas.delete_at(index)
end
private
def simbolizar(hash)
hash.transform_keys(&:to_sym)
end
def stringify(hash)
hash.transform_keys(&:to_s).tap do |h|
h['tareas'] = h['tareas'].map(&:to_s) if h['tareas']
end
end
end
end
end
+86
View File
@@ -0,0 +1,86 @@
# frozen_string_literal: true
# ==========================================================
# adn/tools/bkps/lib/menu.rb — Menú TUI interactivo
# Migrado desde dtic-BKPs v6.0 → ADN::BKPs
# ==========================================================
module ADN
module BKPs
class Menu
def initialize(config, dispatcher, log)
@config = config
@dispatcher = dispatcher
@log = log
end
def ejecutar
loop do
system('clear') || system('cls')
mostrar_encabezado
opciones = construir_opciones
opciones.each { |op| puts op[:linea] }
puts "#{Color::CYAN}────────────────────────#{Color::RESET}"
print "\n#{Color::YELLOW}Selecciona > #{Color::RESET}"
$stdout.flush
entrada = $stdin.gets&.chomp&.strip&.upcase
next if entrada.nil? || entrada.empty?
op = opciones.find { |o| o[:id] == entrada }
unless op
@log.error("Opción no válida: #{entrada}")
sleep 1
next
end
case op[:tipo]
when :tarea
@dispatcher.ejecutar_tarea(op[:config])
when :comando
@dispatcher.ejecutar_comando(op[:config])
when :salir
puts "Saliendo..."
return
end
unless op[:tipo] == :salir || @config.auto_avanzar
print "\nPresiona Enter para continuar..."
$stdin.gets
end
end
end
private
def mostrar_encabezado
puts "#{Color::BOLD}#{Color::CYAN}╔══════════════════════════════════════════╗#{Color::RESET}"
puts "#{Color::BOLD}#{Color::CYAN}║ dtic-BKPs — ADN Backups Manager ║#{Color::RESET}"
puts "#{Color::BOLD}#{Color::CYAN}╚══════════════════════════════════════════╝#{Color::RESET}"
end
def construir_opciones
opciones = []
puts "\n#{Color::BOLD}Tareas Individuales:#{Color::RESET}"
@config.tareas.each_with_index do |t, i|
id = "T#{i + 1}"
opciones << { id: id, tipo: :tarea, config: t, linea: " #{Color::YELLOW}#{id}.#{Color::RESET} #{t[:texto]}" }
end
puts "\n#{Color::BOLD}Comandos (Secuencias):#{Color::RESET}"
@config.comandos.each_with_index do |c, i|
id = "C#{i + 1}"
opciones << { id: id, tipo: :comando, config: c, linea: " #{Color::YELLOW}#{id}.#{Color::RESET} #{c[:texto]}" }
end
puts "\n#{Color::CYAN}────────────────────────#{Color::RESET}"
opciones << { id: 'S', tipo: :salir, linea: " #{Color::YELLOW}S.#{Color::RESET} Salir" }
opciones.select { |op| op[:tipo] == :salir }.each { |op| puts op[:linea] }
opciones
end
end
end
end
+150
View File
@@ -0,0 +1,150 @@
# frozen_string_literal: true
# ==========================================================
# adn/tools/bkps/lib/proc_proxmox.rb — Procesador Proxmox
# Migrado desde dtic-BKPs v6.0 → ADN::BKPs
# ==========================================================
require 'tmpdir'
require 'shellwords'
require 'open3'
require 'fileutils'
module ADN
module BKPs
module Procesador
class Proxmox
def initialize(log)
@log = log
@pv = system('which pv > /dev/null 2>&1')
end
def ejecutar(tarea)
@log.info("--- #{tarea[:texto]} ---")
@log.info("#{tarea[:id]}: Origen remoto: #{tarea[:origen]}")
dir_tmp = montar(tarea[:origen], tarea[:id])
return false unless dir_tmp
procesados = 0
begin
logs = Dir.glob(File.join(dir_tmp, '*.log'))
if logs.empty?
@log.info("#{tarea[:id]}: Sin archivos .log en origen.")
return true
end
@log.info("Encontrados #{logs.length} set(s) de backup Proxmox.")
logs.each do |ruta_log|
base = File.basename(ruta_log, '.log')
begin
fuentes = Dir.glob(File.join(dir_tmp, "#{base}*"))
nombre_vm = extraer_nombre_vm(fuentes, ruta_log, base, tarea[:id])
next unless nombre_vm
nombre_tar = "#{nombre_vm}_#{File.mtime(ruta_log).strftime('%Y%m%d_%H%M%S')}.tar.gz"
dir_dest = File.join(tarea[:destino], nombre_vm)
FileUtils.mkdir_p(dir_dest)
ruta_tar = File.join(dir_dest, nombre_tar)
if File.exist?(ruta_tar)
if tarea[:sobrescribir]
@log.info("#{File.basename(ruta_tar)} existe. Sobrescribiendo.")
else
@log.info("#{File.basename(ruta_tar)} existe. Saltando.")
next
end
end
@log.info("Comprimiendo #{base} (VM: #{nombre_vm})")
comprimir(dir_tmp, fuentes, ruta_tar)
@log.info("✔ Compresión OK: #{File.basename(ruta_tar)}")
if tarea[:eliminar_origen]
fuentes.each { |f| FileUtils.rm_f(f) }
@log.info("Fuentes eliminadas de origen.")
end
procesados += 1
rescue => e
@log.error("#{tarea[:id]}: Error procesando '#{base}': #{e.message}")
FileUtils.rm_f(ruta_tar) if ruta_tar && File.exist?(ruta_tar)
end
end
@log.info("#{procesados} sets Proxmox procesados.")
true
ensure
desmontar(dir_tmp, tarea[:id])
end
end
private
def extraer_nombre_vm(fuentes, ruta_log, base, tarea_id)
notes = fuentes.find { |f| f.end_with?('.notes') }
nombre = notes ? File.read(notes).strip : nil
if nombre.nil? || nombre.empty?
File.foreach(ruta_log) do |line|
if line =~ /INFO: VM Name:\s*(.+)$/
nombre = $1.strip
break
end
end rescue nil
end
if nombre.nil? || nombre.empty?
match = base.match(/vzdump-(?:qemu|lxc)-(\d+)-/)
nombre = match ? "VM-#{match[1]}" : nil
end
@log.error("#{tarea_id}: Sin nombre de VM para '#{base}'. Saltando.") unless nombre
nombre
end
def comprimir(dir_origen, fuentes, ruta_salida)
nombres = fuentes.map { |f| File.basename(f) }
if @pv
cmd_tar = ['tar', '-cf', '-', '-C', dir_origen] + nombres
cmd_pv = ['pv', '-s', fuentes.sum { |f| File.size(f) rescue 0 }.to_s]
cmd_gz = ['gzip']
File.open(ruta_salida, 'wb') do |f|
Open3.pipeline(cmd_tar, cmd_pv, cmd_gz, out: f).each_with_index do |s, i|
raise "Falló #{%w[tar pv gzip][i]} (#{s.exitstatus})" unless s.success?
end
end
else
args = nombres.map { |f| Shellwords.escape(f) }.join(' ')
cmd = "tar -czf #{Shellwords.escape(ruta_salida)} -C #{Shellwords.escape(dir_origen)} #{args}"
system(cmd) or raise "tar falló (#{$?.exitstatus})"
end
end
def montar(remoto, tarea_id)
dir = Dir.mktmpdir("bkps_#{tarea_id}_")
@log.info("Montando #{remoto} en #{dir}")
pid = spawn("rclone mount #{Shellwords.escape(remoto)} #{dir} --daemon --vfs-cache-mode writes")
Process.detach(pid)
sleep 2
if Dir.exist?(dir) && !(Dir.entries(dir) - %w[. ..]).empty?
dir
else
@log.error("Error montando #{remoto}")
nil
end
end
def desmontar(dir, tarea_id)
return unless dir && Dir.exist?(dir)
@log.info("Desmontando #{dir}")
system("fusermount -u #{dir} 2>/dev/null || umount #{dir} 2>/dev/null")
FileUtils.remove_entry(dir) rescue nil
end
end
end
end
end
+88
View File
@@ -0,0 +1,88 @@
# frozen_string_literal: true
# ==========================================================
# adn/tools/bkps/lib/proc_xen.rb — Procesador XEN (.xva)
# Migrado desde dtic-BKPs v6.0 → ADN::BKPs
# ==========================================================
require 'shellwords'
require 'open3'
require 'fileutils'
module ADN
module BKPs
module Procesador
class Xen
def initialize(log)
@log = log
@pv = system('which pv > /dev/null 2>&1')
end
def ejecutar(tarea)
@log.info("--- #{tarea[:texto]} ---")
@log.info("#{tarea[:id]}: Origen: #{tarea[:origen]}")
archivos = Dir.glob(File.join(tarea[:origen], '*.xva'))
if archivos.empty?
@log.info("#{tarea[:id]}: Sin archivos .xva en origen.")
return true
end
@log.info("Encontrados #{archivos.length} archivo(s) .xva.")
procesados = 0
archivos.each do |ruta_xva|
nombre_xva = File.basename(ruta_xva)
nombre_vm = nombre_xva.split('_').first
dir_dest = File.join(tarea[:destino], nombre_vm)
FileUtils.mkdir_p(dir_dest)
nombre_sin_ext = File.basename(nombre_xva, '.xva')
ruta_tar = File.join(dir_dest, "#{nombre_sin_ext}.tar.gz")
if File.exist?(ruta_tar)
if tarea[:sobrescribir]
@log.info("#{File.basename(ruta_tar)} existe. Sobrescribiendo.")
else
@log.info("#{File.basename(ruta_tar)} existe. Saltando.")
next
end
end
begin
@log.info("Comprimiendo #{nombre_xva}")
if @pv
cmd_tar = ['tar', '-czf', '-', '-C', tarea[:origen], nombre_xva]
cmd_pv = ['pv', '-s', File.size(ruta_xva).to_s]
File.open(ruta_tar, 'wb') do |f|
Open3.pipeline(cmd_tar, cmd_pv, out: f).each_with_index do |s, i|
raise "Falló #{%w[tar pv][i]} (#{s.exitstatus})" unless s.success?
end
end
else
cmd = "tar -czf #{Shellwords.escape(ruta_tar)} -C #{Shellwords.escape(tarea[:origen])} #{Shellwords.escape(nombre_xva)}"
system(cmd) or raise "tar falló (#{$?.exitstatus})"
end
@log.info("✔ Compresión OK: #{File.basename(ruta_tar)}")
if tarea[:eliminar_origen]
FileUtils.rm_f(ruta_xva)
@log.info("XVA original eliminado.")
end
procesados += 1
rescue => e
@log.error("#{tarea[:id]}: Error comprimiendo #{nombre_xva}: #{e.message}")
FileUtils.rm_f(ruta_tar)
end
end
@log.info("#{procesados} archivos .xva procesados.")
true
end
end
end
end
end
+54
View File
@@ -0,0 +1,54 @@
# frozen_string_literal: true
# ==========================================================
# adn/tools/bkps/lib/sync_rclone.rb — Sincronización rclone
# Migrado desde dtic-BKPs v6.0 → ADN::BKPs
# ==========================================================
require 'shellwords'
module ADN
module BKPs
module Procesador
class Rclone
RCLONE_OPTS = %w[
--delete-before
--retries 5
--retries-sleep 10s
--timeout 30m
--contimeout 10m
--tpslimit 10
--progress
-v
].join(' ').freeze
def initialize(log)
@log = log
end
def ejecutar(tarea)
@log.info("--- #{tarea[:texto]} ---")
origen = tarea[:origen]
destino = tarea[:destino]
@log.info("Origen: #{origen}")
@log.info("Destino: #{destino}")
cmd = "rclone sync #{Shellwords.escape(origen)} #{Shellwords.escape(destino)} #{RCLONE_OPTS}"
@log.info("#{tarea[:id]}: Ejecutando → #{cmd}")
exito = system(cmd)
codigo = $?.exitstatus
if exito
@log.info("✔ Sincronización rclone completada.")
else
@log.error("#{tarea[:id]}: Fallo en sincronización (código: #{codigo}).")
end
exito
end
end
end
end
end
+1 -1
View File
@@ -1 +1 @@
1774987932 1775509864
+304
View File
@@ -0,0 +1,304 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ==========================================================
# adn/tools/cli/copiloto.rb — Vigía de tareas largas
# ----------------------------------------------------------
# El "copiloto" se encarga de tareas de bajo nivel:
# - Lanzar un comando y vigilar hasta que termine
# - Cerrar automáticamente el evento de bitácora al finalizar
# - Registrar resultado (éxito/fallo) en la descripción
#
# Principio: Menos es Más — una tool reutilizable para
# cualquier ámbito que necesite delegar tareas largas.
#
# Uso:
# ./adn/tools/run copiloto lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
# ./adn/tools/run copiloto vigilar --evento 1379 --pid 12345
# ./adn/tools/run copiloto estado
# ==========================================================
require 'optparse'
require 'fileutils'
require 'time'
require 'json'
module ADN
class SubcomandoCopiloto
COPILOTO_DIR = File.join(ADN::PROJECT_ROOT, 'tmp', 'copiloto')
ADN_RUN = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run')
def initialize(args, logger = nil)
@args = args
@logger = logger
FileUtils.mkdir_p(COPILOTO_DIR)
end
def ejecutar
if @args.empty? || @args.first == 'help' || @args.first == '--help'
mostrar_ayuda
return
end
accion = @args.shift
case accion
when 'lanzar'
cmd_lanzar(@args)
when 'vigilar'
cmd_vigilar(@args)
when 'estado'
cmd_estado
when 'limpiar'
cmd_limpiar
else
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
# ─── lanzar: ejecutar comando en background y vigilar ────────
def cmd_lanzar(args)
evento_id = nil
nota = nil
# Separar opciones del copiloto del comando a ejecutar
separador = args.index('--')
unless separador
puts "#{Color::RED}✗ Falta separador '--' antes del comando a ejecutar#{Color::RESET}"
puts "Uso: ./adn/tools/run copiloto lanzar --evento 1379 -- <comando>"
return
end
copiloto_args = args[0...separador]
comando_args = args[(separador + 1)..-1]
OptionParser.new do |opts|
opts.on("--evento ID", "ID del evento (o AUTO para crear uno)") { |e| evento_id = e }
opts.on("--nota TEXTO", "Nota adicional para el cierre") { |n| nota = n }
end.parse!(copiloto_args)
# AUTO: crear evento de bitácora automáticamente
if evento_id == 'AUTO'
cmd_crear = "#{ADN_RUN} db evento:crear --ambito dtic-BKPs --nodo srv-ns8"
cmd_crear += " --descripcion \"#{(nota || comando_args.join(' ')).gsub('"', '\\"')}\""
cmd_crear += " --inicio #{Time.now.strftime('%H:%M')} --modo R --ia"
output = `#{cmd_crear} 2>&1`
# Extraer ID del evento creado
if output.match(/ID\s+(\d+)/)
evento_id = output.match(/ID\s+(\d+)/)[1].to_i
@logger&.info("Copiloto: Evento AUTO ##{evento_id} creado")
else
@logger&.info("Copiloto: No se pudo crear evento AUTO, continuando sin evento")
evento_id = nil
end
elsif evento_id
evento_id = evento_id.to_i
end
if comando_args.empty?
puts "#{Color::RED}✗ Falta el comando a ejecutar después de '--'#{Color::RESET}"
return
end
comando_str = comando_args.join(' ')
tarea_id = "copiloto_#{Time.now.strftime('%H%M%S')}_#{$$}"
log_file = File.join(COPILOTO_DIR, "#{tarea_id}.log")
meta_file = File.join(COPILOTO_DIR, "#{tarea_id}.json")
# Guardar metadata
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
comando: comando_str,
nota: nota,
inicio: Time.now.iso8601,
pid: nil,
estado: 'lanzando'
}
File.write(meta_file, JSON.pretty_generate(meta))
# Lanzar el proceso vigía en background
vigia_script = <<~BASH
#!/bin/bash
# Copiloto vigía — auto-generado
LOGFILE="#{log_file}"
METAFILE="#{meta_file}"
EVENTO_ID="#{evento_id}"
ADN_RUN="#{ADN_RUN}"
echo "[$(date)] Copiloto: Iniciando comando..." >> "$LOGFILE"
#{comando_str} >> "$LOGFILE" 2>&1
EXIT_CODE=$?
echo "[$(date)] Copiloto: Comando terminó con código $EXIT_CODE" >> "$LOGFILE"
# Actualizar metadata
ruby -rjson -e '
f = "'"$METAFILE"'"
m = JSON.parse(File.read(f))
m["fin"] = Time.now.iso8601
m["exit_code"] = #{'"$EXIT_CODE".to_i'}
m["estado"] = #{'"$EXIT_CODE".to_i'} == 0 ? "completado" : "fallido"
File.write(f, JSON.pretty_generate(m))
'
# Cerrar evento en bitácora si hay evento_id
if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then
HORA_FIN=$(date +%H:%M)
DURACION=$(( ($(date +%s) - #{Time.now.to_i}) / 60 ))
if [ "$EXIT_CODE" -eq 0 ]; then
RESULTADO="- 🤖 Copiloto: ✔ Completado exitosamente (${DURACION}min)"
else
RESULTADO="- 🤖 Copiloto: ✖ Falló con código $EXIT_CODE (${DURACION}min)"
fi
NOTA_BASE="#{(nota || '').gsub('"', '\\"')}"
if [ -n "$NOTA_BASE" ]; then
DESC_FINAL="${NOTA_BASE}
${RESULTADO}"
else
DESC_FINAL="#{comando_str}
${RESULTADO}"
fi
"$ADN_RUN" db evento:actualizar "$EVENTO_ID" --fin "$HORA_FIN" --descripcion "$DESC_FINAL" >> "$LOGFILE" 2>&1
echo "[$(date)] Copiloto: Evento $EVENTO_ID cerrado a las $HORA_FIN ($RESULTADO)" >> "$LOGFILE"
fi
BASH
vigia_path = File.join(COPILOTO_DIR, "#{tarea_id}.sh")
File.write(vigia_path, vigia_script)
FileUtils.chmod(0755, vigia_path)
pid = spawn("nohup bash #{vigia_path} &", [:out, :err] => '/dev/null')
Process.detach(pid)
# Actualizar meta con PID
meta[:pid] = pid
meta[:estado] = 'vigilando'
File.write(meta_file, JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Copiloto lanzado#{Color::RESET}"
puts " Tarea: #{tarea_id}"
puts " PID: #{pid}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
puts " Comando: #{comando_str}"
puts ""
puts "#{Color::DIM}El copiloto cerrará el evento ##{evento_id} cuando termine.#{Color::RESET}" if evento_id
puts "#{Color::DIM}Consultar: ./adn/tools/run copiloto estado#{Color::RESET}"
end
# ─── vigilar: adjuntarse a un PID existente ──────────────────
def cmd_vigilar(args)
evento_id = nil
pid = nil
OptionParser.new do |opts|
opts.on("--evento ID", Integer, "ID del evento") { |e| evento_id = e }
opts.on("--pid PID", Integer, "PID del proceso a vigilar") { |p| pid = p }
end.parse!(args)
unless pid
puts "#{Color::RED}✗ Falta --pid#{Color::RESET}"
return
end
tarea_id = "vigil_#{pid}_#{Time.now.strftime('%H%M%S')}"
log_file = File.join(COPILOTO_DIR, "#{tarea_id}.log")
# Lanzar vigía para PID existente
vigia_cmd = <<~CMD
nohup bash -c '
tail --pid=#{pid} -f /dev/null 2>/dev/null
HORA_FIN=$(date +%%H:%%M)
echo "[$(date)] Copiloto: PID #{pid} terminó" >> #{log_file}
#{evento_id ? "\"#{ADN_RUN}\" db evento:actualizar #{evento_id} --fin $HORA_FIN >> #{log_file} 2>&1" : ""}
echo "[$(date)] Copiloto: Evento #{evento_id} cerrado a las $HORA_FIN" >> #{log_file}
' > /dev/null 2>&1 &
CMD
system(vigia_cmd)
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
pid_vigilado: pid,
inicio: Time.now.iso8601,
estado: 'vigilando'
}
File.write(File.join(COPILOTO_DIR, "#{tarea_id}.json"), JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Copiloto vigilando PID #{pid}#{Color::RESET}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
end
# ─── estado: mostrar tareas del copiloto ───────────────────────
def cmd_estado
metas = Dir.glob(File.join(COPILOTO_DIR, '*.json')).sort
if metas.empty?
puts "#{Color::DIM}Sin tareas de copiloto activas.#{Color::RESET}"
return
end
puts "#{Color::BOLD}📋 Tareas del Copiloto#{Color::RESET}\n\n"
metas.each do |f|
m = JSON.parse(File.read(f))
icono = case m['estado']
when 'completado' then "#{Color::GREEN}#{Color::RESET}"
when 'fallido' then "#{Color::RED}#{Color::RESET}"
when 'vigilando' then "#{Color::YELLOW}#{Color::RESET}"
else "#{Color::DIM}?#{Color::RESET}"
end
pid_info = m['pid'] || m['pid_vigilado']
vivo = pid_info && system("kill -0 #{pid_info} 2>/dev/null")
puts " #{icono} #{m['tarea_id']}"
puts " Evento: #{m['evento_id'] || '-'} PID: #{pid_info || '-'}#{vivo ? " #{Color::GREEN}(vivo)#{Color::RESET}" : ''}"
puts " #{m['comando'] || "Vigilando PID #{m['pid_vigilado']}"}"
puts " Inicio: #{m['inicio']}#{m['fin'] ? " Fin: #{m['fin']}" : ''}"
puts ""
end
end
# ─── limpiar: borrar tareas completadas ──────────────────────
def cmd_limpiar
Dir.glob(File.join(COPILOTO_DIR, '*.json')).each do |f|
m = JSON.parse(File.read(f))
if ['completado', 'fallido'].include?(m['estado'])
base = File.basename(f, '.json')
['.json', '.log', '.sh'].each do |ext|
FileUtils.rm_f(File.join(COPILOTO_DIR, "#{base}#{ext}"))
end
end
end
puts "#{Color::GREEN}✔ Tareas completadas limpiadas.#{Color::RESET}"
end
def mostrar_ayuda
puts <<~HELP
#{Color::BOLD}#{Color::CYAN}Copiloto — Vigía de Tareas Largas#{Color::RESET}
El copiloto se encarga de las tareas de bajo nivel: ejecutar comandos
largos en background y cerrar automáticamente eventos de bitácora
cuando terminan. Delegar sin desperdiciar recursos.
#{Color::YELLOW}Uso:#{Color::RESET}
copiloto lanzar --evento ID -- <comando> Lanzar y vigilar
copiloto vigilar --evento ID --pid PID Vigilar PID existente
copiloto estado Ver tareas activas
copiloto limpiar Limpiar completadas
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run copiloto lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
./adn/tools/run copiloto vigilar --evento 1379 --pid 12345
./adn/tools/run copiloto estado
HELP
end
end
end
+509
View File
@@ -0,0 +1,509 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ==========================================================
# adn/tools/cli/dron.rb — Sistema de Drones Autónomos
# ----------------------------------------------------------
# Flota de drones que operan de forma autónoma:
# - Lanzar tareas en background con auto-bitácora
# - Vigilar procesos existentes
# - Orquestar flota: detectar drones caídos, estancados
# - Reportar salud del sistema en tiempo real
#
# Principio: Sistema Vivo — los drones operan, reportan
# y se auto-gestionan sin intervención humana.
#
# Uso:
# ./adn/tools/run dron lanzar --evento AUTO -- <comando>
# ./adn/tools/run dron flota # Dashboard de la flota
# ./adn/tools/run dron salud # Health check activo
# ./adn/tools/run dron estado # Estado detallado
# ./adn/tools/run dron limpiar # Limpiar completados
# ==========================================================
require 'optparse'
require 'fileutils'
require 'time'
require 'json'
module ADN
class SubcomandoDron
DRON_DIR = File.join(ADN::PROJECT_ROOT, 'tmp', 'dron')
ADN_RUN = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run')
STALE_MINUTES = 120 # Dron sin actividad > 2h = posible problema
def initialize(args, logger = nil)
@args = args
@logger = logger
FileUtils.mkdir_p(DRON_DIR)
end
def ejecutar
if @args.empty? || @args.first == 'help' || @args.first == '--help'
mostrar_ayuda
return
end
accion = @args.shift
case accion
when 'lanzar'
cmd_lanzar(@args)
when 'vigilar'
cmd_vigilar(@args)
when 'estado'
cmd_estado
when 'flota'
cmd_flota
when 'salud'
cmd_salud
when 'limpiar'
cmd_limpiar
else
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
# ─── lanzar: ejecutar comando en background y vigilar ────────
def cmd_lanzar(args)
evento_id = nil
nota = nil
# Separar opciones del dron del comando a ejecutar
separador = args.index('--')
unless separador
puts "#{Color::RED}✗ Falta separador '--' antes del comando a ejecutar#{Color::RESET}"
puts "Uso: ./adn/tools/run dron lanzar --evento 1379 -- <comando>"
return
end
dron_args = args[0...separador]
comando_args = args[(separador + 1)..-1]
OptionParser.new do |opts|
opts.on("--evento ID", "ID del evento (o AUTO para crear uno)") { |e| evento_id = e }
opts.on("--nota TEXTO", "Nota adicional para el cierre") { |n| nota = n }
end.parse!(dron_args)
# AUTO: crear evento de bitácora automáticamente
if evento_id == 'AUTO'
# Primero generamos el tarea_id para incluirlo en la descripción del evento
tarea_id = "dron_#{Time.now.strftime('%H%M%S')}_#{$$}"
cmd_crear = "#{ADN_RUN} db evento:crear --ambito dtic-BKPs --nodo srv-ns8"
cmd_crear += " --descripcion \"🛸 #{tarea_id}: #{(nota || comando_args.join(' ')).gsub('"', '\\"')}\""
cmd_crear += " --inicio #{Time.now.strftime('%H:%M')} --modo R --ia"
output = `#{cmd_crear} 2>&1`
# El evento recién creado aparece como último en la lista de pendientes
# Output: "⏳ [33mID 1413 [0m 17:12:00 srv-ns8 ..."
all_ids = output.scan(/ID\s+(\d+)/).flatten.map(&:to_i)
if output.include?('Entrada creada exitosamente') || output.include?('✅ Entrada creada')
if all_ids.any?
# Tomamos el ID más alto (el último creado)
evento_id = all_ids.max
@logger&.info("Dron: Evento AUTO ##{evento_id} creado para #{tarea_id}")
else
@logger&.warn("Dron: Evento creado pero no se pudo extraer ID del output")
evento_id = nil
end
else
@logger&.error("Dron: Error al crear evento AUTO: #{output}")
evento_id = nil
end
elsif evento_id
evento_id = evento_id.to_i
end
if comando_args.empty?
puts "#{Color::RED}✗ Falta el comando a ejecutar después de '--'#{Color::RESET}"
return
end
comando_str = comando_args.join(' ')
tarea_id ||= "dron_#{Time.now.strftime('%H%M%S')}_#{$$}"
log_file = File.join(DRON_DIR, "#{tarea_id}.log")
meta_file = File.join(DRON_DIR, "#{tarea_id}.json")
# Guardar metadata
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
comando: comando_str,
nota: nota,
inicio: Time.now.iso8601,
pid: nil,
estado: 'lanzando'
}
File.write(meta_file, JSON.pretty_generate(meta))
# Lanzar el proceso vigía en background
nota_safe = (nota || "").gsub(%q{"}, %q{\"})
cmd_safe = comando_str.gsub(%q{"}, %q{\"})
inicio_epoch = Time.now.to_i
vigia_script = <<~BASH
#!/bin/bash
# Dron vigia — auto-generado
LOGFILE="#{log_file}"
METAFILE="#{meta_file}"
EVENTO_ID="#{evento_id}"
ADN_RUN="#{ADN_RUN}"
TAREA_ID="#{tarea_id}"
INICIO_EPOCH=#{inicio_epoch}
actualizar_evento() {
if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then
"$ADN_RUN" db evento:actualizar "$EVENTO_ID" --descripcion "$1" >> "$LOGFILE" 2>&1
fi
}
HORA_INICIO=$(date +%H:%M:%S)
DIARIO="Dron: $TAREA_ID
--
$HORA_INICIO - Inicio de vuelo
Mision: #{nota_safe.empty? ? cmd_safe : nota_safe}
Ejecutando: #{cmd_safe}"
echo "[$HORA_INICIO] Dron $TAREA_ID: Inicio de vuelo" >> "$LOGFILE"
actualizar_evento "$DIARIO"
#{comando_str} >> "$LOGFILE" 2>&1
EXIT_CODE=$?
HORA_FIN=$(date +%H:%M:%S)
DURACION=$(( ($(date +%s) - $INICIO_EPOCH) / 60 ))
echo "[$HORA_FIN] Dron $TAREA_ID: Termino con codigo $EXIT_CODE" >> "$LOGFILE"
if [ "$EXIT_CODE" -eq 0 ]; then
DIARIO="$DIARIO
OK $HORA_FIN - Completado (${DURACION}min)
Aterrizaje limpio"
else
DIARIO="$DIARIO
FALLO $HORA_FIN - Codigo $EXIT_CODE (${DURACION}min)
Aterrizaje de emergencia"
fi
ruby -rjson -e '
f = ARGV[0]
m = JSON.parse(File.read(f))
m["fin"] = Time.now.iso8601
m["exit_code"] = ARGV[1].to_i
m["estado"] = ARGV[1].to_i == 0 ? "completado" : "fallido"
File.write(f, JSON.pretty_generate(m))
' "$METAFILE" "$EXIT_CODE"
if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then
HORA_FIN_HM=$(date +%H:%M)
"$ADN_RUN" db evento:actualizar "$EVENTO_ID" --fin "$HORA_FIN_HM" --descripcion "$DIARIO" >> "$LOGFILE" 2>&1
echo "[$HORA_FIN] Dron $TAREA_ID: Evento $EVENTO_ID cerrado" >> "$LOGFILE"
fi
BASH
vigia_path = File.join(DRON_DIR, "#{tarea_id}.sh")
File.write(vigia_path, vigia_script)
FileUtils.chmod(0755, vigia_path)
pid = spawn("nohup bash #{vigia_path} &", [:out, :err] => '/dev/null')
Process.detach(pid)
# Actualizar meta con PID
meta[:pid] = pid
meta[:estado] = 'vigilando'
File.write(meta_file, JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Dron lanzado#{Color::RESET}"
puts " Tarea: #{tarea_id}"
puts " PID: #{pid}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
puts " Comando: #{comando_str}"
puts ""
puts "#{Color::DIM}El dron cerrará el evento ##{evento_id} cuando termine.#{Color::RESET}" if evento_id
puts "#{Color::DIM}Consultar: ./adn/tools/run dron estado#{Color::RESET}"
end
# ─── vigilar: adjuntarse a un PID existente ──────────────────
def cmd_vigilar(args)
evento_id = nil
pid = nil
OptionParser.new do |opts|
opts.on("--evento ID", Integer, "ID del evento") { |e| evento_id = e }
opts.on("--pid PID", Integer, "PID del proceso a vigilar") { |p| pid = p }
end.parse!(args)
unless pid
puts "#{Color::RED}✗ Falta --pid#{Color::RESET}"
return
end
tarea_id = "vigil_#{pid}_#{Time.now.strftime('%H%M%S')}"
log_file = File.join(DRON_DIR, "#{tarea_id}.log")
# Lanzar vigía para PID existente
vigia_cmd = <<~CMD
nohup bash -c '
tail --pid=#{pid} -f /dev/null 2>/dev/null
HORA_FIN=$(date +%%H:%%M)
echo "[$(date)] Dron: PID #{pid} terminó" >> #{log_file}
#{evento_id ? "\"#{ADN_RUN}\" db evento:actualizar #{evento_id} --fin $HORA_FIN >> #{log_file} 2>&1" : ""}
echo "[$(date)] Dron: Evento #{evento_id} cerrado a las $HORA_FIN" >> #{log_file}
' > /dev/null 2>&1 &
CMD
system(vigia_cmd)
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
pid_vigilado: pid,
inicio: Time.now.iso8601,
estado: 'vigilando'
}
File.write(File.join(DRON_DIR, "#{tarea_id}.json"), JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Dron vigilando PID #{pid}#{Color::RESET}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
end
# ─── estado: mostrar tareas del dron ───────────────────────
def cmd_estado
metas = Dir.glob(File.join(DRON_DIR, '*.json')).sort
if metas.empty?
puts "#{Color::DIM}Sin tareas de dron activas.#{Color::RESET}"
return
end
puts "#{Color::BOLD}📋 Tareas del Dron#{Color::RESET}\n\n"
metas.each do |f|
m = JSON.parse(File.read(f))
icono = case m['estado']
when 'completado' then "#{Color::GREEN}#{Color::RESET}"
when 'fallido' then "#{Color::RED}#{Color::RESET}"
when 'vigilando' then "#{Color::YELLOW}#{Color::RESET}"
else "#{Color::DIM}?#{Color::RESET}"
end
pid_info = m['pid'] || m['pid_vigilado']
vivo = pid_info && system("kill -0 #{pid_info} 2>/dev/null")
puts " #{icono} #{m['tarea_id']}"
puts " Evento: #{m['evento_id'] || '-'} PID: #{pid_info || '-'}#{vivo ? " #{Color::GREEN}(vivo)#{Color::RESET}" : ''}"
puts " #{m['comando'] || "Vigilando PID #{m['pid_vigilado']}"}"
puts " Inicio: #{m['inicio']}#{m['fin'] ? " Fin: #{m['fin']}" : ''}"
puts ""
end
end
# ─── flota: dashboard compacto de la flota ─────────────────────
def cmd_flota
drones = cargar_todos
if drones.empty?
puts "#{Color::DIM}🛸 Flota vacía — sin drones activos.#{Color::RESET}"
return
end
activos = drones.select { |d| d['estado'] == 'vigilando' }
completados = drones.select { |d| d['estado'] == 'completado' }
fallidos = drones.select { |d| d['estado'] == 'fallido' }
puts "\n#{Color::BOLD}#{Color::CYAN}🛸 Flota de Drones#{Color::RESET}\n"
puts "" * 70
# Activos
if activos.any?
puts "#{Color::YELLOW}⏳ En vuelo (#{activos.length})#{Color::RESET}"
activos.each do |d|
pid = d['pid'] || d['pid_vigilado']
vivo = pid && system("kill -0 #{pid} 2>/dev/null")
elapsed = tiempo_transcurrido(d['inicio'])
comando_corto = (d['comando'] || d['nota'] || '?')[0..55]
estado_pid = vivo ? "#{Color::GREEN}#{Color::RESET}" : "#{Color::RED}#{Color::RESET}"
puts " #{estado_pid} #{d['tarea_id']} #{elapsed.rjust(8)} #{comando_corto}"
end
puts ""
end
# Completados
if completados.any?
puts "#{Color::GREEN}✔ Completados (#{completados.length})#{Color::RESET}"
completados.last(5).each do |d|
duracion = calcular_duracion(d['inicio'], d['fin'])
comando_corto = (d['nota'] || d['comando'] || '?')[0..55]
puts "#{d['tarea_id']} #{duracion.rjust(8)} #{comando_corto}"
end
puts ""
end
# Fallidos
if fallidos.any?
puts "#{Color::RED}✖ Fallidos (#{fallidos.length})#{Color::RESET}"
fallidos.each do |d|
comando_corto = (d['nota'] || d['comando'] || '?')[0..55]
puts "#{d['tarea_id']} exit=#{d['exit_code']} #{comando_corto}"
end
puts ""
end
puts "" * 70
puts "#{Color::BOLD}Total: #{drones.length} drones | " \
"#{Color::YELLOW}#{activos.length}#{Color::RESET} | " \
"#{Color::GREEN}#{completados.length}#{Color::RESET} | " \
"#{Color::RED}#{fallidos.length}#{Color::RESET}"
end
# ─── salud: health check activo ────────────────────────────────
def cmd_salud
drones = cargar_todos
problemas = []
if drones.empty?
puts "#{Color::GREEN}✔ Sin drones — nada que verificar.#{Color::RESET}"
return
end
puts "\n#{Color::BOLD}🏥 Diagnóstico de Salud#{Color::RESET}\n"
puts "" * 70
drones.each do |d|
next unless d['estado'] == 'vigilando'
pid = d['pid'] || d['pid_vigilado']
vivo = pid && system("kill -0 #{pid} 2>/dev/null")
elapsed_min = minutos_transcurridos(d['inicio'])
# Detectar zombie: meta dice vigilando pero PID muerto
if !vivo
problemas << { dron: d['tarea_id'], tipo: '💀 Zombie', detalle: "PID #{pid} muerto pero estado=vigilando", meta: d }
puts " #{Color::RED}💀 ZOMBIE#{Color::RESET} #{d['tarea_id']} — PID #{pid} muerto"
# Auto-reparar: actualizar metadata
meta_path = File.join(DRON_DIR, "#{d['tarea_id']}.json")
if File.exist?(meta_path)
d['estado'] = 'fallido'
d['fin'] = Time.now.iso8601
d['exit_code'] = -1
d['nota_salud'] = 'Auto-detectado como zombie por dron salud'
File.write(meta_path, JSON.pretty_generate(d))
puts " #{Color::DIM}→ Auto-reparado: marcado como fallido#{Color::RESET}"
end
end
# Detectar estancado: lleva mucho tiempo
if vivo && elapsed_min > STALE_MINUTES
problemas << { dron: d['tarea_id'], tipo: '🐌 Estancado', detalle: "#{elapsed_min}min sin finalizar" }
puts " #{Color::YELLOW}🐌 ESTANCADO#{Color::RESET} #{d['tarea_id']}#{elapsed_min}min activo"
# Verificar si el log sigue creciendo
log_path = File.join(DRON_DIR, "#{d['tarea_id']}.log")
if File.exist?(log_path)
log_age = ((Time.now - File.mtime(log_path)) / 60).to_i
if log_age > 30
puts " #{Color::DIM}→ Log sin actualizar hace #{log_age}min#{Color::RESET}"
else
puts " #{Color::DIM}→ Log activo (última escritura hace #{log_age}min)#{Color::RESET}"
end
end
end
# Sano
if vivo && elapsed_min <= STALE_MINUTES
puts " #{Color::GREEN}💚 SANO#{Color::RESET} #{d['tarea_id']}#{elapsed_min}min, PID #{pid} activo"
end
end
# Eventos huérfanos: drones con evento_id pero sin PID vivo
eventos_pendientes = drones.select { |d| d['estado'] == 'fallido' && d['evento_id'] }
if eventos_pendientes.any?
puts "\n#{Color::YELLOW}⚠ Eventos con drones fallidos:#{Color::RESET}"
eventos_pendientes.each do |d|
puts " Evento ##{d['evento_id']}#{d['tarea_id']} (exit=#{d['exit_code']})"
end
end
puts ""
puts "" * 70
if problemas.empty?
puts "#{Color::GREEN}#{Color::BOLD}✔ Flota saludable — sin problemas detectados.#{Color::RESET}"
else
puts "#{Color::RED}#{Color::BOLD}#{problemas.length} problema(s) detectado(s).#{Color::RESET}"
end
end
# ─── limpiar: borrar tareas completadas ──────────────────────
def cmd_limpiar
Dir.glob(File.join(DRON_DIR, '*.json')).each do |f|
m = JSON.parse(File.read(f))
if ['completado', 'fallido'].include?(m['estado'])
base = File.basename(f, '.json')
['.json', '.log', '.sh'].each do |ext|
FileUtils.rm_f(File.join(DRON_DIR, "#{base}#{ext}"))
end
end
end
puts "#{Color::GREEN}✔ Tareas completadas limpiadas.#{Color::RESET}"
end
# ─── Helpers ─────────────────────────────────────────────────
def cargar_todos
Dir.glob(File.join(DRON_DIR, '*.json')).sort.map do |f|
JSON.parse(File.read(f)) rescue nil
end.compact
end
def tiempo_transcurrido(inicio_str)
return '?' unless inicio_str
mins = ((Time.now - Time.parse(inicio_str)) / 60).to_i
if mins < 60
"#{mins}min"
else
"#{mins / 60}h#{mins % 60}m"
end
end
def calcular_duracion(inicio_str, fin_str)
return '?' unless inicio_str && fin_str
mins = ((Time.parse(fin_str) - Time.parse(inicio_str)) / 60).to_i
if mins < 60
"#{mins}min"
else
"#{mins / 60}h#{mins % 60}m"
end
end
def minutos_transcurridos(inicio_str)
return 0 unless inicio_str
((Time.now - Time.parse(inicio_str)) / 60).to_i
end
def mostrar_ayuda
puts <<~HELP
#{Color::BOLD}#{Color::CYAN}🛸 Dron — Sistema de Drones Autónomos#{Color::RESET}
Flota de drones que operan de forma autónoma: ejecutan tareas
en background, registran en bitácora, detectan problemas y
se auto-gestionan. Sistema vivo, no estático.
#{Color::YELLOW}Comandos:#{Color::RESET}
dron lanzar --evento ID -- <cmd> Lanzar dron de trabajo
dron flota Dashboard compacto de la flota
dron salud Health check (detecta zombies/estancados)
dron estado Estado detallado de cada dron
dron vigilar --evento ID --pid PID Vigilar PID existente
dron limpiar Limpiar completados/fallidos
#{Color::YELLOW}Evento AUTO:#{Color::RESET}
--evento AUTO crea un evento de bitácora automáticamente.
--nota "texto" define la descripción del evento.
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run dron lanzar --evento AUTO --nota "Backup srvv-sitio" -- ./adn/tools/run bkps backup srvv-sitio
./adn/tools/run dron flota
./adn/tools/run dron salud
HELP
end
end
end
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
# ==========================================================
# adn/tools/cli/novato.rb — Vigía de tareas largas
# ----------------------------------------------------------
# El "novato" se encarga de tareas de bajo nivel:
# - Lanzar un comando y vigilar hasta que termine
# - Cerrar automáticamente el evento de bitácora al finalizar
# - Registrar resultado (éxito/fallo) en la descripción
#
# Principio: Menos es Más — una tool reutilizable para
# cualquier ámbito que necesite delegar tareas largas.
#
# Uso:
# ./adn/tools/run novato lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
# ./adn/tools/run novato vigilar --evento 1379 --pid 12345
# ./adn/tools/run novato estado
# ==========================================================
require 'optparse'
require 'fileutils'
require 'time'
require 'json'
module ADN
class SubcomandoNovato
NOVATO_DIR = File.join(ADN::PROJECT_ROOT, 'tmp', 'novato')
ADN_RUN = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'run')
def initialize(args, logger = nil)
@args = args
@logger = logger
FileUtils.mkdir_p(NOVATO_DIR)
end
def ejecutar
if @args.empty? || @args.first == 'help' || @args.first == '--help'
mostrar_ayuda
return
end
accion = @args.shift
case accion
when 'lanzar'
cmd_lanzar(@args)
when 'vigilar'
cmd_vigilar(@args)
when 'estado'
cmd_estado
when 'limpiar'
cmd_limpiar
else
puts "#{Color::RED}✗ Acción desconocida: #{accion}#{Color::RESET}"
mostrar_ayuda
exit 1
end
end
private
# ─── lanzar: ejecutar comando en background y vigilar ────────
def cmd_lanzar(args)
evento_id = nil
nota = nil
# Separar opciones del novato del comando a ejecutar
separador = args.index('--')
unless separador
puts "#{Color::RED}✗ Falta separador '--' antes del comando a ejecutar#{Color::RESET}"
puts "Uso: ./adn/tools/run novato lanzar --evento 1379 -- <comando>"
return
end
novato_args = args[0...separador]
comando_args = args[(separador + 1)..-1]
OptionParser.new do |opts|
opts.on("--evento ID", Integer, "ID del evento de bitácora a cerrar") { |e| evento_id = e }
opts.on("--nota TEXTO", "Nota adicional para el cierre") { |n| nota = n }
end.parse!(novato_args)
if comando_args.empty?
puts "#{Color::RED}✗ Falta el comando a ejecutar después de '--'#{Color::RESET}"
return
end
comando_str = comando_args.join(' ')
tarea_id = "novato_#{Time.now.strftime('%H%M%S')}_#{$$}"
log_file = File.join(NOVATO_DIR, "#{tarea_id}.log")
meta_file = File.join(NOVATO_DIR, "#{tarea_id}.json")
# Guardar metadata
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
comando: comando_str,
nota: nota,
inicio: Time.now.iso8601,
pid: nil,
estado: 'lanzando'
}
File.write(meta_file, JSON.pretty_generate(meta))
# Lanzar el proceso vigía en background
vigia_script = <<~BASH
#!/bin/bash
# Novato vigía — auto-generado
LOGFILE="#{log_file}"
METAFILE="#{meta_file}"
EVENTO_ID="#{evento_id}"
ADN_RUN="#{ADN_RUN}"
echo "[$(date)] Novato: Iniciando comando..." >> "$LOGFILE"
#{comando_str} >> "$LOGFILE" 2>&1
EXIT_CODE=$?
echo "[$(date)] Novato: Comando terminó con código $EXIT_CODE" >> "$LOGFILE"
# Actualizar metadata
ruby -rjson -e '
f = "'"$METAFILE"'"
m = JSON.parse(File.read(f))
m["fin"] = Time.now.iso8601
m["exit_code"] = #{'"$EXIT_CODE".to_i'}
m["estado"] = #{'"$EXIT_CODE".to_i'} == 0 ? "completado" : "fallido"
File.write(f, JSON.pretty_generate(m))
'
# Cerrar evento en bitácora si hay evento_id
if [ -n "$EVENTO_ID" ] && [ "$EVENTO_ID" != "" ]; then
HORA_FIN=$(date +%H:%M)
DURACION=$(( ($(date +%s) - #{Time.now.to_i}) / 60 ))
if [ "$EXIT_CODE" -eq 0 ]; then
RESULTADO="- 🤖 Novato: ✔ Completado exitosamente (${DURACION}min)"
else
RESULTADO="- 🤖 Novato: ✖ Falló con código $EXIT_CODE (${DURACION}min)"
fi
NOTA_BASE="#{(nota || '').gsub('"', '\\"')}"
if [ -n "$NOTA_BASE" ]; then
DESC_FINAL="${NOTA_BASE}
${RESULTADO}"
else
DESC_FINAL="#{comando_str}
${RESULTADO}"
fi
"$ADN_RUN" db evento:actualizar "$EVENTO_ID" --fin "$HORA_FIN" --descripcion "$DESC_FINAL" >> "$LOGFILE" 2>&1
echo "[$(date)] Novato: Evento $EVENTO_ID cerrado a las $HORA_FIN ($RESULTADO)" >> "$LOGFILE"
fi
BASH
vigia_path = File.join(NOVATO_DIR, "#{tarea_id}.sh")
File.write(vigia_path, vigia_script)
FileUtils.chmod(0755, vigia_path)
pid = spawn("nohup bash #{vigia_path} &", [:out, :err] => '/dev/null')
Process.detach(pid)
# Actualizar meta con PID
meta[:pid] = pid
meta[:estado] = 'vigilando'
File.write(meta_file, JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Novato lanzado#{Color::RESET}"
puts " Tarea: #{tarea_id}"
puts " PID: #{pid}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
puts " Comando: #{comando_str}"
puts ""
puts "#{Color::DIM}El novato cerrará el evento ##{evento_id} cuando termine.#{Color::RESET}" if evento_id
puts "#{Color::DIM}Consultar: ./adn/tools/run novato estado#{Color::RESET}"
end
# ─── vigilar: adjuntarse a un PID existente ──────────────────
def cmd_vigilar(args)
evento_id = nil
pid = nil
OptionParser.new do |opts|
opts.on("--evento ID", Integer, "ID del evento") { |e| evento_id = e }
opts.on("--pid PID", Integer, "PID del proceso a vigilar") { |p| pid = p }
end.parse!(args)
unless pid
puts "#{Color::RED}✗ Falta --pid#{Color::RESET}"
return
end
tarea_id = "vigil_#{pid}_#{Time.now.strftime('%H%M%S')}"
log_file = File.join(NOVATO_DIR, "#{tarea_id}.log")
# Lanzar vigía para PID existente
vigia_cmd = <<~CMD
nohup bash -c '
tail --pid=#{pid} -f /dev/null 2>/dev/null
HORA_FIN=$(date +%%H:%%M)
echo "[$(date)] Novato: PID #{pid} terminó" >> #{log_file}
#{evento_id ? "\"#{ADN_RUN}\" db evento:actualizar #{evento_id} --fin $HORA_FIN >> #{log_file} 2>&1" : ""}
echo "[$(date)] Novato: Evento #{evento_id} cerrado a las $HORA_FIN" >> #{log_file}
' > /dev/null 2>&1 &
CMD
system(vigia_cmd)
meta = {
tarea_id: tarea_id,
evento_id: evento_id,
pid_vigilado: pid,
inicio: Time.now.iso8601,
estado: 'vigilando'
}
File.write(File.join(NOVATO_DIR, "#{tarea_id}.json"), JSON.pretty_generate(meta))
puts "#{Color::GREEN}✔ Novato vigilando PID #{pid}#{Color::RESET}"
puts " Evento: #{evento_id || '(sin evento)'}"
puts " Log: #{log_file}"
end
# ─── estado: mostrar tareas del novato ───────────────────────
def cmd_estado
metas = Dir.glob(File.join(NOVATO_DIR, '*.json')).sort
if metas.empty?
puts "#{Color::DIM}Sin tareas de novato activas.#{Color::RESET}"
return
end
puts "#{Color::BOLD}📋 Tareas del Novato#{Color::RESET}\n\n"
metas.each do |f|
m = JSON.parse(File.read(f))
icono = case m['estado']
when 'completado' then "#{Color::GREEN}#{Color::RESET}"
when 'fallido' then "#{Color::RED}#{Color::RESET}"
when 'vigilando' then "#{Color::YELLOW}#{Color::RESET}"
else "#{Color::DIM}?#{Color::RESET}"
end
pid_info = m['pid'] || m['pid_vigilado']
vivo = pid_info && system("kill -0 #{pid_info} 2>/dev/null")
puts " #{icono} #{m['tarea_id']}"
puts " Evento: #{m['evento_id'] || '-'} PID: #{pid_info || '-'}#{vivo ? " #{Color::GREEN}(vivo)#{Color::RESET}" : ''}"
puts " #{m['comando'] || "Vigilando PID #{m['pid_vigilado']}"}"
puts " Inicio: #{m['inicio']}#{m['fin'] ? " Fin: #{m['fin']}" : ''}"
puts ""
end
end
# ─── limpiar: borrar tareas completadas ──────────────────────
def cmd_limpiar
Dir.glob(File.join(NOVATO_DIR, '*.json')).each do |f|
m = JSON.parse(File.read(f))
if ['completado', 'fallido'].include?(m['estado'])
base = File.basename(f, '.json')
['.json', '.log', '.sh'].each do |ext|
FileUtils.rm_f(File.join(NOVATO_DIR, "#{base}#{ext}"))
end
end
end
puts "#{Color::GREEN}✔ Tareas completadas limpiadas.#{Color::RESET}"
end
def mostrar_ayuda
puts <<~HELP
#{Color::BOLD}#{Color::CYAN}Novato — Vigía de Tareas Largas#{Color::RESET}
El novato se encarga de las tareas de bajo nivel: ejecutar comandos
largos en background y cerrar automáticamente eventos de bitácora
cuando terminan. Delegar sin desperdiciar recursos.
#{Color::YELLOW}Uso:#{Color::RESET}
novato lanzar --evento ID -- <comando> Lanzar y vigilar
novato vigilar --evento ID --pid PID Vigilar PID existente
novato estado Ver tareas activas
novato limpiar Limpiar completadas
#{Color::YELLOW}Ejemplos:#{Color::RESET}
./adn/tools/run novato lanzar --evento 1379 -- ./adn/tools/run bkps run C4 --batch
./adn/tools/run novato vigilar --evento 1379 --pid 12345
./adn/tools/run novato estado
HELP
end
end
end
+1 -1
View File
@@ -3,7 +3,7 @@
require 'date' require 'date'
require 'time' require 'time'
require 'json' require 'json'
require_relative '../cli/backup/configurador' # require_relative '../cli/backup/configurador' # Archivado → _hist/ (no usado aquí)
module ADN module ADN
class Conciliador class Conciliador
+1 -1
View File
@@ -1 +1 @@
{ "command": "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0; Set-Service -Name sshd -StartupType 'Automatic'; Start-Service sshd; New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server' -Enabled $True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 7022 -Profile Any -ErrorAction SilentlyContinue; $c = Get-Content 'C:\\ProgramData\\ssh\\sshd_config'; if ($c -notmatch 'Port 7022') { $c = $c -replace '#Port 22', 'Port 7022'; $c | Set-Content 'C:\\ProgramData\\ssh\\sshd_config'; Restart-Service sshd }; if (Test-Path D:\\guest-agent\\qemu-ga-x86_64.msi) { msiexec /i D:\\guest-agent\\qemu-ga-x86_64.msi /quiet /norestart }; if (Test-Path E:\\guest-agent\\qemu-ga-x86_64.msi) { msiexec /i E:\\guest-agent\\qemu-ga-x86_64.msi /quiet /norestart }; Start-Service QEMU-GA; Invoke-WebRequest 'https://pkgs.tailscale.com/stable/tailscale-setup-latest.exe' -OutFile 'C:\\tailscale-setup.exe'; Start-Process 'C:\\tailscale-setup.exe' -ArgumentList '/quiet' -Wait; & \"C:\\Program Files\\Tailscale\\tailscale.exe\" up", "timestamp": "2026-03-30T17:59:00", "id": "sql2_payload" } {"id":2,"cmd":"$ErrorActionPreference='Stop'; Write-Host '=== Instalando QEMU-GA ==='; msiexec /i D:\\guest-agent\\qemu-ga-x86_64.msi /quiet /norestart; Start-Sleep 10; Start-Service QEMU-GA; Write-Host 'QEMU-GA: ' (Get-Service QEMU-GA).Status; Write-Host '=== Instalando Tailscale ==='; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; iwr https://pkgs.tailscale.com/stable/tailscale-setup-latest.exe -OutFile C:\\tailscale-setup.exe -UseBasicParsing; Start-Process C:\\tailscale-setup.exe /quiet -Wait; Start-Sleep 5; Write-Host '=== Conectando Tailscale ==='; $out = & 'C:\\Program Files\\Tailscale\\tailscale.exe' up 2>&1; Write-Host $out; Write-Host '=== IP Tailscale ==='; & 'C:\\Program Files\\Tailscale\\tailscale.exe' ip"}
@@ -1,76 +0,0 @@
# Plan: Estandarización de Ayudas en Pantalla para Herramientas ADN
**Fecha:** 17 de marzo de 2026
**Autor:** Sistema ADN
**Versión:** 1.0
**Ubicación:** `docs/ambito/dtic-ADN/plan/P2604.01.01_Estandarizacion-Ayudas-ADN.md`
**Estado:** ⏳ En Ejecución
## 📋 Resumen Ejecutivo
Estandarizar el formato y contenido de las ayudas en pantalla (--help) para todas las herramientas del ecosistema ADN, utilizando el formateador de ayuda estandarizado (`adn/tools/core/help_formatter.rb`) para garantizar consistencia, reutilización y mantenibilidad.
## 📅 Fases de Implementación
### ✅ **FASE 1: Análisis y Creación de Estándar (Completado)**
#### Semana 1: Análisis Inicial
- [x] **1.1:** Análisis de ayudas actuales en subcomandos CLI
- [x] **1.2:** Creación de plantilla estándar de ayuda usando HelpFormatter
#### Semana 2: Implementación Crítica
- [x] **1.3:** Actualización de subcomando principal (ayuda.rb)
- [x] **1.4:** Actualización de subcomandos críticos (jornada.rb, nodos.rb, db.rb)
- [x] **1.5:** Actualización de subcomandos intermedios (generar.rb, proceso.rb, conocimiento.rb)
#### Semana 3: Implementación Completa
- [x] **1.6:** Actualización de subcomandos (msp.rb, ssh.rb, triggers.rb, contexto.rb, backup.rb ✅) y pendientes (plan.rb, tailscale.rb, estados.rb, salud.rb, validador.rb, wzombi.rb 📍)
- [ ] **1.7:** Verificación y pruebas de consistencia
### ⏳ **FASE 2: Documentación y Soporte (Pendiente)**
#### Semana 4: Documentación
- [ ] **2.1:** Documentación de guías para desarrolladores sobre el nuevo estándar
- [ ] **2.2:** Integración de verificaciones automáticas en CI (si aplica)
## 🔧 Herramientas Utilizadas
- Ruby CLI (ADN): Subcomandos en `adn/tools/cli/` que requieren estandarización de ayuda
- Help Formatter (`adn/tools/core/help_formatter.rb`): Componente central para formateo consistente de ayudas
## 👥 Stakeholders
| Rol | Persona | Visibilidad |
| :--- | :--- | :--- |
| **Responsable Técnico** | Lic. Ricardo MONLA | Operador directo |
| **Dirección de TIC** | (Jerárquico superior) | Dashboard asíncrono |
| **Desarrolladores ADN** | Equipo de desarrollo | Uso diario de herramientas |
## 🖥️ Nodos Involucrados
Este proyecto se enfoca en las herramientas CLI del ecosistema ADN, por lo que no involucra nodos de infraestructura específicos. Los cambios se aplicarán a nivel de código en el repositorio.
## 📊 Progreso
```
Fase 1: ████████░░ 85% Implementación (85% completado)
Fase 2: ░░░░░░░░░░ 0% Documentación y Soporte
```
## 📖 Comandos ADN (Referencia)
```bash
# Ver ayuda principal
./adn/tools/run ayuda
# Ver ayuda de un subcomando específico
./adn/tools/run ayuda jornada
./adn/tools/run ayuda nodos
./adn/tools/run ayuda db
```
## 🔗 Referencias
- **ADN**: [Principios Rectores](../../../adn/06_gobernanza.md) | [Herramienta CLI](../../../adn/README.md#herramienta-cli-adn-toolsrun)
- **Help Formatter**: [`adn/tools/core/help_formatter.rb`](../../../adn/tools/core/help_formatter.rb)
- **Subcomandos CLI**: [`adn/tools/cli/`](../../../adn/tools/cli/)
@@ -1,66 +0,0 @@
# P2604.02.01 - Plan: Herramienta de Generación de Proyectos ADN
**Fecha:** 17 de marzo de 2026
**Autor:** Antigravity (IA ADN)
**Estado:** ✅ Completado
**Proyecto Padre:** [P2604 - Mejoras ADN](P2604_Mejoras-ADN.md)
## 📋 Objetivo
Automatizar la creación de estructuras de proyectos (planes, hitos, tareas) en el ecosistema ADN para garantizar consistencia en la nomenclatura y ubicación de los archivos en `docs/proy/`.
## 📅 Fases de Implementación
### **Fase 1: Estructura y Lógica Base (Completado)**
- [x] **1.A:** Desarrollo de `adn/tools/proy/proy.rb` para despacho de comandos.
- [x] **1.B:** Implementación de `GeneradorProy` en `generador.rb` con lógica de rutas inteligentes.
- [x] **1.C:** Integración de plantillas dinámicas en `plantillas.rb`.
### **Fase 2: Integración al Ecosistema (Completado)**
- [x] **2.A:** Registro del subcomando `proy` en el entrypoint principal `adn/tools/run`.
- [x] **2.B:** Soporte para parámetros avanzados (`--proy`, `--fase`) y normalización de títulos.
| **2.C** | ✅ | Validación de creación automática de directorios según código de proyecto. |
---
## 📊 Progreso
```
Fase 1: ██████████ 100% Estructura y Lógica Base
Fase 2: ██████████ 100% Integración al Ecosistema
```
---
## 📑 Plantillas Disponibles
La herramienta `proy` permite generar los siguientes componentes estandarizados:
| Tipo | Propósito | Formato |
| :--- | :--- | :--- |
| **`plan`** | Plan de ejecución principal de una fase | `PROY.FASE_Titulo.md` |
| **`subplan`** | Detalle técnico o procedimiento específico | `PROY.FASE_Subplan.md` |
| **`mejora`** | Propuesta o registro de mejora puntual | `PROY.FASE_Mejora.md` |
| **`incidente`** | Documentación de fallos y su resolución | `PROY.FASE_Incidente.md` |
| **`tarea`** | Seguimiento de una tarea individual compleja | `PROY.FASE_Tarea.md` |
### 🚀 Ejemplos de Uso
```bash
# Crear un plan para la fase 2 del proyecto P2604
./adn/tools/run proy nuevo plan --proy:P2604 --fase:02 "Generación de Documentos"
# Crear un registro de mejora sin fase específica
./adn/tools/run proy nuevo mejora --proy:P2601 "Optimización de Red"
# Crear una tarea dentro de una fase
./adn/tools/run proy nuevo tarea --proy:P2602 --fase:1.2 "Validación rclone"
```
> 💡 **Nota**: Todos los archivos generados incluyen automáticamente la sección de **📊 Progreso** con barras Unicode pre-configuradas al 0% o con ejemplos listos para editar.
---
## 🔗 Referencias
- **Herramienta:** `adn/tools/proy/`
- **EntryPoint:** `adn/tools/run proy`
- **Gobernanza:** [06_gobernanza.md](../../../adn/06_gobernanza.md)
@@ -1,53 +0,0 @@
# P2604.03.01 - Plan de Implementación W-ZOMBI v2.0 (Despliegue Limpio)
> Proyecto: P2604 - Mejoras ADN | Estado: ⏳ Planificación
## Contexto
Se requiere reemplazar la herramienta legacy `adn/tools/sonda_w-zombi` por la nueva versión modular v2.0 (ubicada en `docs/ambito/dtic-ADN/w-zombi/w-zombi_v2.0`). Para mantener un ecosistema higiénico y evitar monolitos indeseados, se optó por un enfoque de **"borrón y cuenta nueva"**.
## Objetivos
1. **Archivado Histórico**: Mover la antigua sonda a `adn/tools/_hist/sonda_w-zombi` preservando su estado.
2. **Despliegue Limpio**: Copiar la limpia arquitectura v2.0 directamente en `adn/tools/w-zombi`.
3. **Integración ADN-CLI**: Modificar el CLI (`adn/tools/run`) para soportar `w-zombi` como namespace enקום de `sonda_w-zombi`.
4. **Preservación de Datos**: Importar el archivo `telemetria.log` de la antigua base de datos antes de darla de baja.
## Tareas
### Fase 1: Backup e Histerización (10 min)
- [x] Revertir repositorio a estado limpio para `sonda_w-zombi`.
- [x] Mover directorio original `sonda_w-zombi` a `adn/tools/_hist/`.
- [x] Respaldar `telemetria.log` original a `adn/tools/w-zombi/data/telemetria.log`.
### Fase 2: Despliegue Estructural (20 min)
- [x] Copiar contenido limpio de `w-zombi_v2.0` al nuevo directorio `adn/tools/w-zombi/`.
- [x] Ajustar `lib/wzombi/routes/zombi.rb` para usar asignación de entorno (ej. `WZ_HOST`) con un fallback fallback `100.111.195.4`.
- [x] Inicializar `data/cmd.json` con `{ "id": 0, "cmd": "NO_CMD" }`.
### Fase 3: Integración ADN-CLI (20 min)
- [x] Crear `adn/tools/cli/wzombi.rb` con la clase `SubcomandoWZombi` referenciando el nuevo directorio.
- [x] Modificar `adn/tools/run` para registrar el subcomando `wzombi` vinculándole a la nueva ubicación.
### Fase 4: Verificación (10 min)
- [x] Ejecutar `./adn/tools/run wzombi ayuda`.
- [x] Enviar comando de prueba: `./adn/tools/run wzombi cmd "hostname"`.
- [x] Ejecutar `./adn/tools/run wzombi start` y verificar puerto 8000.
- [x] Verificar recepción de logs (Ej. `ping`).
## Riesgos y Mitigación
- **Riesgo**: Pérdida de conectividad con agentes activos si la IP del host cambia en `zombi.ps1`.
- **Mitigación**: Usar variables de entorno (ej. `WZ_HOST`) para inyectar la IP correcta en el generador de PS1.
---
## 📊 Progreso
```
Fase 1: ██████████ 100% Backup e Histerización
Fase 2: ██████████ 100% Despliegue Estructural
Fase 3: ██████████ 100% Integración ADN-CLI
Fase 4: ██████████ 100% Verificación Final
```
---
**Registro de Avance**: 100% (✅ COMPLETADO)
@@ -1,47 +0,0 @@
# P2604.04.01 - Plan: Estandarización de Barras de Progreso
**Fecha:** 18 de marzo de 2026
**Autor:** Antigravity (IA ADN)
**Estado:** ✅ FINALIZADO
**Proyecto Padre:** [P2604 - Mejoras al ADN](P2604_Mejoras-ADN.md)
## 📋 Objetivo
Unificar la representación visual del avance de los proyectos y sus fases mediante una notación basada en barras Unicode (`█`, `░`). Esto permite un diagnóstico visual rápido de la salud de los proyectos siguiendo el principio de **Menos es Más**.
## 📅 Fases de Implementación
### **Fase 1: Definición de Estándar (Completada)**
- [x] **1.A:** Definir caracteres Unicode para la barra (`█` para completado, `░` para pendiente).
- [x] **1.B:** Establecer longitud estándar de 10 caracteres para representar 10% por bloque.
- [x] **1.C:** Documentar formato: `Fase N: ██████░░░░ 60% Nombre`.
### **Fase 2: Ejecución en P2604 (Completada)**
- [x] **2.A:** Aplicar barras en el manifiesto principal de P2604.
- [x] **2.B:** Verificar alineación y legibilidad en terminal/visor MD.
### **Fase 3: Propagación a P2601 (Completada)**
- [x] **3.A:** Identificar fases en `P2601_dasuten.md`.
- [x] **3.B:** Implementar sección de progreso con barras.
- [x] **3.C:** Vincular porcentajes con los hitos completados.
### **Fase 4: Consolidación Normativa (Completada)**
- [x] **4.A:** Agregar la notación visual a la hebra [04_iconografia.md](../../../adn/04_iconografia.md).
- [x] **4.B:** Actualizar plantilla de proyectos y hebra 07.
---
## 📊 Progreso
```
Fase 1: ██████████ 100% Definición de Estándar
Fase 2: ██████████ 100% Ejecución en P2604
Fase 3: ██████████ 100% Propagación a P2601
Fase 4: ██████████ 100% Consolidación Normativa
```
---
## 🔗 Referencias
- **Hebra Iconografía:** [04_iconografia.md](../../../adn/04_iconografia.md)
- **Hebra Proyectos:** [07_proyectos.md](../../../adn/07_proyectos.md)
- **Ejemplo Maestro:** [P2604_Mejoras-ADN.md](P2604_Mejoras-ADN.md)
@@ -1,43 +0,0 @@
# P2604 - Plan de Acción: Estudio de Fricción IA (Antigravity) y Mejora Continua
## Objetivo
Analizar los patrones de uso, toma de decisiones y errores de flujo detectados en conversaciones recientes con IA (Antigravity, específicamente ayer).
**Problema central:** La IA repite ciclos de exploración innecesarios (ej. "no estoy seguro de cómo empezar a buscar contraseñas") lo cual genera fricción.
**Meta:** Diseñar e instrumentar soluciones y directivas que provean el conocimiento/contexto base requerido desde el arranque, propiciando que los pasos de intervención/exploración previa de la IA “tiendan a cero” (más velocidad, menos gasto computacional y asertividad inmediata).
## Fases
### Fase 1: Diagnóstico de la Conversación
- [x] Extraer y revisar logs de *thought* y llamadas a herramientas (tools calls) de ayer.
- [x] Clasificar las vacilaciones de la IA (Listado de directorios repetitivos, dudas sobre secretos y bóvedas, etc).
- [x] Auditoría de `adn/triggers.yml`: Identificado como **OBSOLETO** (sistema deshabilitado, lógica pre-migración DB).
- [x] Cuantificar los "saltos" y comandos ejecutados que se hubiesen evitado con contexto.
### Fase 2: Diseño de Puntos de Mejora al Ecosistema ADN
- [x] Migrar lógica de `triggers.yml` a **DB Hooks/Ruby** para automatizar sincronizaciones manuales.
- [x] Integrar solución para acceso rápido a bóveda de contraseñas (`candados`) en CLI principal (`adn candados`).
- [x] Estandarizar cómo la IA obtiene la topología de la red sin usar herramientas exploratorias innecesarias (- ej: proveer el manifiesto explícitamente).
- [x] Actualizar el canon (`05_ia.md` u otros) de acuerdo con el principio de *Menos es Más*.
### Fase 3: Implementación y Validación
- [x] Implementar cambios en las normas y scripts de arranque.
- [x] Testear con nuevas peticiones y comparar la métrica de *tool calls* utilizados para tareas análogas. (IA) Validado con `adn network` vs exploración manual.
---
## 📊 Progreso
```text
Fase 1: ██████████ 100% Diagnóstico de la Conversación
Fase 2: ██████████ 100% Puntos de Mejora Ecosistema
Fase 3: ██████████ 100% Implementación y Validación
```
## 💡 Lecciones Aprendidas (IA Antigravity)
* **Contexto es Eficiencia**: El uso de herramientas centralizadas (`network`, `candados`) reduce los tool calls de 5-6 a 1 para obtención de identidad.
* **Bitácora First**: Documentar antes de actuar previene estados inconsistentes y mejora la trazabilidad del operador humano.
## 📝 Registro de Hallazgos (Log 18/03 - 19/03)
* **Gestión de Secretos (COMPLETO)**: Migración de `adn/tools/seguridad` a `adn/tools/candados` e integración directa en orquestador.
* **Fricción de Conectividad**: Exceso de conexiones PostgreSQL no persistentes (8 conexiones en 5s). Causa: Herramientas `db` cierran sesión en cada comando.
* **Automatización Muerta**: `adn/triggers.yml` está deshabilitado. La IA hace manualmente la propagación de datos que el sistema debería automatizar (ej. Bitácora ↔ Proyectos).
* **Ambigüedad de Comandos**: Errores recurrentes en subcomandos de `db` por sintaxis inconsistente.
---
@@ -1,83 +0,0 @@
# P2604.06 - Plan: Sensor de Mejora Continua para el Ecosistema ADN
## Objetivo
Diseñar e implementar un sistema de detección automática de patrones de fricción en el uso del ecosistema ADN que genere sugerencias de mejora trazables y accionables, reduciendo la carga cognitiva de identificar oportunidades de mejora continua y acelerando el ciclo de evolución del sistema.
## Fases
### Fase 1: Diseño y Arquitectura (06.01)
- [x] Definir métricas clave a recolectar (conexiones DB, frecuencia de comandos, tiempos de respuesta, etc.)
- [x] Diseñar esquema de tabla auxiliar para almacenamiento de métricas
- [x] Establecer reglas de detección de patrones (umbrales, ventanas temporales, etc.)
- [x] Definir formato de salida de sugerencias (entradas bitácora pen### Fase 2: Implementación del Sensor (06.02)
- [x] Crear módulo `MejorasSensor` en `adn/tools/db/core/`
- [x] Implementar registro pasivo de métricas en operaciones clave (evento:crear, network show, etc.)
- [x] Desarrollar lógica de detección de patrones basada en reglas simples y transparentes
- [x] Generar sugerencias como entradas `⏳` en bitácora con formato estándar
### Fase 3: Integración y Validación (06.03)
- [x] Crear subcomando `salud --mejoras` para visualizar sugerencias
- [x] Implementar activación/desactivación via variable de entorno `ADN_MEJORAS` (Integrado en BitacoraDB)
- [x] Validar con patrones de fricción reales identificados previamente (conexiones DB excesivas, comandos repetitivos)
- [x] Documentar uso y reglas en `05_ia.md`
### Fase 4: Cultura y Adopción (06.04)
- [x] Definir rituales de revisión de sugerencias (Premisa 17 en 05_ia.md)
- [x] Documentar `./adn/tools/run salud --mejoras` como obligatorio al inicio de sesión
- [x] Crear ejemplos de mejoras exitosas detectadas por el sensor
- [x] Medir reducción en trabajo manual de detección de fricción
- [x] Ajustar umbrales y reglas basado en feedback de uso real
**Métricas de Impacto (19/03/2026):**
- Total entradas: 497 | IA: 84 (16%) | Completadas: 371 (74%)
- Fricción detectada y resuelta automáticamente: 3 patrones
- Umbrales actuales: conexiones_por_minuto: 5, comandos_repetidos: 5, ventana: 300s
## 📊 Progreso
```text
Fase 1: ██████████ 100% Diseño y Arquitectura
Fase 2: ██████████ 100% Implementación del Sensor
Fase 3: ████████░░ 80% Integración y Validación
Fase 4: ██████████ 100% Cultura y Adopción ✅
```
## 📝 Registro de Hallazgos y Conclusión por Fase
### Fase 1: Diseño y Arquitectura (Completada ✅)
- **Logrado**: Definición completa de métricas clave (conexiones DB, frecuencia de comandos, tiempos de respuesta)
- **Conclusión**: Base teórica sólida basada en principios Menos es Más.
### Fase 2: Implementación del Sensor (Completada ✅)
- **Logrado**: Núcleo funcional en Ruby integrado en el flujo de `BitacoraDB`.
- **Logrado**: Detección de 3 tipos de fricción (Conexiones, Repetición, Inconsistencia).
### Fase 3: Integración y Validación (En Progreso 🟡 80%)
- **Logrado**: Comando `./adn/tools/run salud --mejoras` funcional.
- **Logrado**: Corrección de infraestructura de constantes del ADN.
- **Pendiente**: Documentación formal en el canon de la IA.
## 📊 Progreso General
```text
Fase 1: ██████████ 100% Diseño y Arquitectura
Fase 2: ██████████ 100% Implementación del Sensor
Fase 3: ██████████ 100% Integración y Validación ✅
Fase 4: ██████████ 100% Cultura y Adopción ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TOTAL: ██████████ 100% PLAN COMPLETO ✅
```
## 📝 Registro de Hallazgos (Log 19/03)
* **Integración Exitosa**: El sensor ahora detecta conexiones PostgreSQL no persistentes y sugiere connection pooling de forma automática.
* **Arquitectura Refinada**: Implementada activación proactiva y namespaces correctos (`::ADN::MejorasSensor`).
* **Concepto**: Confirmado que patrones similares (comandos repetitivos, inconsistencias documento-DB) se detectan en logs históricos
* **Alineación con ADN**: Propone mejora continua mediante detección automática + intervención humana trazable, siguiendo principios DB-First y Menos es Más
## 📝 Ejemplo de Mejora Exitosa (19/03 - P2604.06.06)
**Fricción Detectada**: IP Tailscale cambió de `100.88.252.26` a `100.111.195.4`, causando errores 404 en acceso remoto.
**Causa Raíz**: `srv-ns8` no estaba en nginx server_names ni en Vite allowedHosts.
**Resolución**:
- Actualizar IPs en nginx.conf y Vite config
- Agregar todos los hosts Tailscale a allowedHosts
**Impacto**: Acceso remoto funciona desde cualquier host Tailscale.
**Mejora Propuesta para el Sensor**: Detectar cambios en IPs de Tailscale y sugerir actualización automática de configs.
@@ -1,101 +0,0 @@
# P2604.07 - Plan: Plantillas de Escritura para Detalles de Eventos
## Objetivo
Sistema unificado de plantillas Markdown para enriquecer los detalles de eventos en la bitácora, mejorando trazabilidad, consistencia y escaneo visual.
## Contexto
Ya existe `adn/tools/proy/plantillas.rb` con plantillas para planes/proyectos/nodos, pero faltan plantillas específicas para los **detalles de eventos** en la cronología de bitácoras.
## Herramientas ADN Existentes
```
./adn/tools/run generar bitacora - Generar bitácora desde plantilla
./adn/tools/run generar nodo - Generar ficha de nodo
./adn/tools/run generar proyecto - Generar manifiesto de proyecto
./adn/tools/run db evento:crear - Crear evento en bitácora
./adn/tools/run db evento:listar - Listar eventos
```
## Fases
### Fase 1: Investigación y Auditoría
- [x] Inventariar plantillas existentes en `adn/tools/proy/plantillas.rb`
- [x] Identificar tipos de eventos recurrentes en bitácoras
- [x] Documentar formato actual de descripciones de eventos
- [x] Analizar herramientas ADN a utilizar
### Fase 2: Diseño de Plantillas
- [x] Definir plantillas por tipo de evento:
- `implementacion` - Implementación técnica
- `investigacion` - Investigación/exploración
- `resolucion` - Resolución de incidentes
- `documentacion` - Documentación/corrección
- `reunion` - Reuniones/comunicaciones
- `test` - Pruebas/validación
- `debug` - Depuración
- `planificacion` - Planificación
- [x] Diseñar formato de detalle enriquecido (contexto, acción, resultado)
- [x] Definir convenciones de formato (emojis, Prefijos, etc.)
### Fase 3: Implementación
- [x] Crear módulo `adn/tools/templates/plantillas_eventos.rb`
- [x] Integrar con `./adn/tools/run generar evento <tipo>`
- [x] Documentar uso en `02_bitacora.md`
- [x] Integrar con `db evento:crear` (opcional `--plantilla`)
- [x] Agregar a `05_ia.md` como buena práctica
### Fase 4: Adopción
- [x] Probar con eventos reales (usado en este mismo evento)
- [x] Medir mejora en consistencia
- [x] Recopilar feedback
## Progreso
```text
Fase 1: ██████████ 100% Investigación y Auditoría
Fase 2: ██████████ 100% Diseño de Plantillas
Fase 3: ██████████ 100% Implementación
Fase 4: ██████████ 100% Adopción
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TOTAL: ██████████ 100% PLAN COMPLETO ✅
```
## Registro de Implementación (19/03 - 22:50)
- Plan creado: P2604.07
- Ámbito: dtic-ADN
## Registro de Corrección (19/03 - 21:30)
- [x] Bugfix: Formato de hora en `db evento:listar` ahora muestra `HH:MM:SS`
- [x] Entrada acepta `--inicio HH:MM[:SS]` y `--fin HH:MM[:SS]`
- [x] Archivo: `adn/tools/cli/db/evento.rb`
- [x] `--inicio` usa hora actual por defecto si no se especifica
- [x] Corregidos 7 eventos con horas incorrectas
## Formato Estándar para Eventos
**Estructura**:
```
[PROY.COD] Título breve ✅
**Tipo de evento**
**Campo 1**:
**Campo 2**:
...
```
**Tipos con iconos**:
| Ícono | Tipo | Campos |
|-------|------|--------|
| 🔧 | `implementacion` | Contexto, Acción, Resultado |
| 🔍 | `investigacion` | Problema, Hallazgos, Conclusión |
| 🛠️ | `resolucion` | Síntoma, Causa, Solución, Verificación |
| 📝 | `documentacion` | Qué, Ubicación, Cambios, Impacto |
| ✅ | `test` | Qué se probó, Resultado, Pasos, Conclusión |
| 🐛 | `debug` | Error, Diagnóstico, Solución, Verificación |
| 📋 | `planificacion` | Objetivo, Fases, Recursos, Próximo |
| 👥 | `reunion` | Participantes, Temas, Acuerdos, Acciones |
## Registro de Mejora de Eventos (19/03 - 21:55)
- [x] **TODOS LOS 34 EVENTOS** de hoy (1186-1239) mejorados con formato enriquecido
- [x] Tipos usados: planificación, implementación, debug, test, documentación, resolución, investigación
- [x] Documentado formato estándar para nuevos eventos
- [x] IDs mejorados: 1186-1239
@@ -1,419 +0,0 @@
# P2604.08 - Implementación del Modelo Ámbito/Nodo en el Ecosistema ADN
> **Estado**: ⏳ Propuesto
> **Pertenece a**: [P2604 - Mejoras ADN](./P2604_Mejoras-ADN.md)
> **Fecha**: 2026-03-20
> **Responsable**: Lic. Ricardo MONLA
> **Versión**: 1.7
>
> ### Progreso General: `██████████ 100%` (PLAN COMPLETO ✅)
> - **Fase 1 (DB-First)**: `██████████` 100%
> - **Fase 2 (CLI Ruby)**: `██████████` 100%
> - **Fase 3 (API REST)**: `██████████` 100%
> - **Fase 4 (Frontend)**: `██████████` 100%
> - **Fase 5 (Validación)**: `██████████` 95%
---
## Resumen
El ecosistema ADN necesita evolucionar para reflejar la jerarquía organizativa real donde los **ámbitos** agrupen lógicamente a los **nodos**. Esto impacta en la base de datos, CLI, API y frontend web. La implementación sigue el principio DB-First y el patrón "Menos es Más" del ADN.
**Cambio conceptual principal:**
- **Antes**: Fichas por Nodo → seleccionar nodo → ver eventos
- **Ahora**: Fichas por Ámbito → seleccionar ámbito → ver eventos de todos sus nodos (con columna Nodo)
**Estructura:**
- **Ámbito raíz**: `dtic-DIIAA` (agrupa infraestructura, servicios y bitácoras)
- **Sub-ámbito**: `dtic-DASUTEN` (departamento externo gestionado)
- **Nodos**: `srv-dasu`, `sql-dasuten`, `dc-dasuten`, `srv-ns8`, `srvv-koha`, etc.
---
## Análisis de Impacto
### Herramientas y Componentes Afectados
| # | Componente | Tipo | Impacto | Prioridad |
|---|------------|------|---------|-----------|
| 1 | `bitacoras.nodos` | Base de Datos | Alta | Crítica |
| 2 | `bitacoras.entradas` | Base de Datos | Alta | Crítica |
| 3 | `bitacoras.temas` | Base de Datos | Media | Alta |
| 4 | `bitacoras.resumen_nodos` | Base de Datos | Media | Alta |
| 5 | `bitacoras.hitos` | Base de Datos | Baja | Media |
| 6 | `adn/tools/cli/db/nodo.rb` | CLI Ruby | Alta | Crítica |
| 7 | `adn/tools/cli/db/evento.rb` | CLI Ruby | Alta | Crítica |
| 8 | `adn/tools/cli/nodos.rb` | CLI Ruby | Alta | Crítica |
| 9 | `adn/tools/cli/ambitos.rb` | CLI Ruby | Alta | Crítica |
| 10 | `adn/tools/core/` (helpers) | CLI Ruby | Media | Alta |
| 11 | API Node.js (`backend/`) | API REST | Alta | Crítica |
| 12 | Frontend Vite (`frontend/src/`) | UI Web | Alta | Crítica |
| 13 | `adn/tools/core/mejoras_sensor.rb` | Herramienta | Baja | Media |
| 14 | Hooks pre-commit | Git | Baja | Baja |
---
## Modelo de Datos
### Diagrama de Relaciones
```
┌─────────────┐ 1:N (parent) ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ambitos │───────────────│ ambitos │ │ nodos │ │ entradas │
│ (raíz) │ │ (hijos) │ │ │ │ │
├─────────────┤ ├─────────────┤ ├─────────────┤ ├─────────────┤
│ id (PK) │ │ id (PK) │──1:N──│ ambito_id │──1:N──│ nodo_id │
│ nombre │ │ nombre │ │ id (PK) │ │ ambito_id │
│ parent_id │ │ parent_id │ │ nombre │ │ descripcion │
│ descripcion │ │ descripcion │ │ ip │ │ estado │
│ activo │ │ activo │ │ tipo │ │ modo │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
```
**Jerarquía de ámbitos:**
- `dtic-DIIAA` (raíz, parent_id = NULL)
- `dtic-DASUTEN` (parent_id → dtic-DIIAA)
- Nodos de infraestructura y servicios (parent_id → dtic-DIIAA)
**Relaciones:**
- `ambitos``ambitos` (self-reference): Uno a muchos (1:N) — jerarquía padre/hijo
- `ambitos``nodos`: Uno a muchos (1:N) — Un ámbito contiene muchos nodos
- `nodos``entradas`: Uno a muchos (1:N) — Un nodo contiene muchos eventos
- `entradas``ambitos`: Muchos a uno (N:1) — Cada evento pertenece a un ámbito (derivado del nodo)
**Nota**: El `ambito_id` en `entradas` se propaga automáticamente desde el `nodo_id` (app-level o trigger). El nodo sigue siendo la entidad primaria de registro.
### Nueva Tabla: `bitacoras.ambitos`
```sql
CREATE TABLE bitacoras.ambitos (
id SERIAL PRIMARY KEY,
nombre VARCHAR(50) NOT NULL UNIQUE,
descripcion TEXT,
parent_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL,
activo BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_ambitos_nombre ON bitacoras.ambitos(nombre);
CREATE INDEX idx_ambitos_parent ON bitacoras.ambitos(parent_id);
```
### Modificación: `bitacoras.nodos`
```sql
ALTER TABLE bitacoras.nodos
ADD COLUMN ambito_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL;
CREATE INDEX idx_nodos_ambito ON bitacoras.nodos(ambito_id);
```
### Modificación: `bitacoras.entradas`
```sql
ALTER TABLE bitacoras.entradas
ADD COLUMN ambito_id INTEGER REFERENCES bitacoras.ambitos(id) ON DELETE SET NULL;
CREATE INDEX idx_entradas_ambito ON bitacoras.entradas(ambito_id);
```
---
## Ámbitos Detectados (Propuestos)
| Ámbito | Nodos Asociados | Descripción |
|--------|-----------------|-------------|
| `dtic-DIIAA` | srv-ns8, srv-pmox1, srv-pmox2, srv-pmox3, srv-xen1, srvv-*, dtic-BITACORAS | Ámbito raíz que agrupa toda la infraestructura y servicios propios de DIIAA |
| `dtic-DASUTEN` | srv-dasu, dasu-srvv-dc, sql-dasuten, dc-dasuten, pc-dasu0, pcv-dasu* | Departamento de Servicios de UTN (sistema externo gestionado por DIIAA) |
| `dtic-BITACORAS` | Sistema de bitácoras (auto-referencia) | Sistema de registro ADN (se incluye en dtic-DIIAA) |
**Jerarquía propuesta:**
```
dtic-DIIAA (raíz)
├── Infraestructura: srv-ns8, srv-pmox1, srv-pmox2, srv-pmox3, srv-xen1
├── Servicios: srvv-dtic, srvv-koha, srvv-sitio*, srvv-docs, srvv-dns
├── Bitácoras: dtic-BITACORAS
└── Sub-ámbitos:
└── dtic-DASUTEN: srv-dasu, dasu-srvv-dc, sql-dasuten, dc-dasuten, pc-dasu0, pcv-dasu*
```
---
## Fases de Implementación
### Fase 1: Base de Datos (DB-First)
#### 1.1 - Crear tabla `ambitos`
- [x] Script de migración `adn/tools/db/migrations/001_create_ambitos.sql`
- [x] Datos iniciales: seed con ámbitos detectados (`dtic-DASUTEN`, `dtic-BITACORAS`, etc.)
- [x] Verificar integridad referencial
#### 1.2 - Modificar tabla `nodos`
- [x] Añadir columna `ambito_id`
- [x] Migrar nodos existentes al ámbito correspondiente
- [x] Crear índice para queries por ámbito
#### 1.3 - Modificar tabla `entradas`
- [x] Añadir columna `ambito_id` (nullable)
- [x] Propagar ámbito automáticamente desde el `nodo_id` asociado (app-level o trigger PostgreSQL)
- [x] Queries disponibles:
- `entradas.nodo_id` → Obtener eventos por nodo (N:1)
- `entradas.ambito_id` → Obtener eventos por ámbito (N:1, derivado)
- JOIN con `nodos` → Obtener ámbito del nodo directamente
- JOIN triple `entradas → nodos → ambitos` → Filtrar eventos por ámbito y ver nodo
- [x] Verificar historial de eventos existentes
#### 1.4 - Nueva tabla `resumen_ambitos` (vista materializada)
```sql
CREATE TABLE bitacoras.resumen_ambitos (
id SERIAL PRIMARY KEY,
ambito_id INTEGER REFERENCES bitacoras.ambitos(id),
fecha DATE,
total_entradas INTEGER DEFAULT 0,
tiempo_total INTERVAL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(ambito_id, fecha)
);
```
---
### Fase 2: CLI Ruby (`adn/tools/`)
#### 2.1 - Nuevo comando: `ambitos`
```bash
./adn/tools/run ambitos listar
./adn/tools/run ambitos crear --nombre <nombre> --descripcion <desc>
./adn/tools/run ambitos info <nombre>
./adn/tools/run ambitos nodos <nombre> # Lista nodos del ámbito
./adn/tools/run ambitos --help # Mostrar ayuda del comando
```
#### 2.2 - Estructura del comando
- [x] Crear `adn/tools/cli/ambitos.rb`
- [x] Crear `adn/tools/db/core/ambito_db.rb`
- [x] Incluir ayuda normalizada (heredada de `core/help_formatter.rb`)
- [x] Integrar en dispatcher principal (`adn/tools/run`)
#### 2.3 - Modificar `nodo.rb`
- [x] Añadir `--ambito <nombre>` a `nodo:crear`
- [x] Listar nodos mostrando la columna/etiqueta de su ámbito actual
#### 2.4 - Modificar `evento.rb`
- [x] Filtrar eventos mediante `--ambito <nombre_o_id>` en `evento:listar`
- [x] (Opcional) Poder asignar evento a ámbito si no tiene nodo
#### 2.5 - Modificar `nodos.rb`
- [x] Subcomando `nodos listar --ambito <nombre>`
- [x] Subcomando `nodos agrupar --por ambitos`
- [x] Incluir `--help` normalizado
#### 2.6 - Modificar `salud.rb`
- [x] Añadir métricas por ámbito en `--dashboard`
- [x] Soportar `--ambito <nombre>` en estadísticas
- [x] Incluir `--help` normalizado
#### 2.7 - Helpers compartidos (`core/`)
- [x] Verificar/crear `core/help_formatter.rb` para normalizar `--help`
- [x] Verificar/crear `core/error_handler.rb` para mensajes de error
- [x] Asegurar consistencia en todos los subcomandos
---
## Resumen de Implementación Fase 2 (20/03)
- **`db nodo:crear`**: Soporte para `--ambito <id|nombre>` para asociar nodo en creación.
- **`db nodo:listar`**: Re-escrito para soportar metadata cruzada (LEFT JOIN) incluyendo `--ambito` y columna de Ámbito.
- **`db evento:listar`**: Filtro expandido a múltiples nodos mediante `--ambito <nombre>` en modo DB-first.
- **`nodos.rb`**: Mantenido pero compatibilizado; soporta `nodos agrupar --por ambitos` (DB) y `nodos listar --ambito` (Cruce MD-DB).
- **`salud.rb`**: Formateado `--help` y se añadieron querys DB para ámbitos y nodos.
---
### Fase 3: API Node.js (`servicios/nginx/dtic-bitacoras/backend/`)
#### 3.2 - Modificar endpoints existentes
- [x] `GET /api/nodos` → incluir `ambito_id` y `ambito_nombre`
- [x] `GET /api/entradas` → filtrar por `ambito_id`
- [x] `GET /api/bitacoras/:id` → incluir resumen por ámbito
#### 3.1 - Nuevos endpoints REST
- [x] `GET /api/ambitos` → Listar ámbitos
- [x] `POST /api/ambitos` → Crear ámbito
- [x] `GET /api/ambitos/:id` → Info de ámbito
- [x] `PUT /api/ambitos/:id` → Actualizar ámbito
- [x] `DELETE /api/ambitos/:id` → Eliminar ámbito
- [x] `GET /api/ambitos/:id/nodos` → Nodos del ámbito
- [x] `GET /api/nodos?ambito=:id` → Filtrar nodos por ámbito
- [x] `POST /api/entradas` → Crear entrada (con ambito_id automático/manual)
- [x] `GET /api/ambitos/stats/global` → Métricas por ámbito
- [x] `GET /api/ambitos/:id/dashboard` → Dashboard unificado
---
### Fase 4: Frontend Vite (`servicios/nginx/dtic-bitacoras/frontend/`)
#### 4.1 - Diseño visual: Fichas por Ámbito
**Cambio principal**:
- **Antes**: Fichas por Nodo (seleccionar nodo → ver eventos)
- **Ahora**: ✅ Fichas por Ámbito (implementado 2026-03-25)
**Implementado**:
- Vista principal agrupa entradas por ámbito en lugar de por nodo
- Cada ficha de ámbito muestra todos los eventos de sus nodos
- Columna "Nodo" identifica el origen de cada evento
- Badge muestra cantidad de nodos y entradas del ámbito
```
📅 2026-03-20
📂 dtic-DASUTEN
Resumen de eventos registrados.
├───────────────────────────────────────────────────────────────────────┤
| ID | I | F | Descripción | J | E | Nodo | Acc. │
│────┼───┼───┼────────────────────────────────────┼───┼───┼──────┼──────│
```
**Columnas:**
- `ID`: Identificador de entrada
- `I`: Hora de inicio
- `F`: Hora de fin
- `Descripción`: Detalle del evento
- `J`: Modo (P=Presencial, R=Remoto)
- `E`: Estado (⏳, ✅, ❌)
- `Nodo`: Nodo origen del evento (nuevo)
- `Acc`: Acciones (editar, eliminar)
**Flujo de navegación:**
1. Seleccionar **Ámbito** (ej: `dtic-DASUTEN`)
2. Ver todos los eventos de los nodos del ámbito
3. Columna **Nodo** muestra el origen de cada evento
#### 4.2 - Modificaciones en componentes
**Cambio de paradigma**: Fichas por Nodo → **Fichas por Ámbito** ✅ COMPLETADO
**Vista Principal (Ámbito)** - ✅ Implementado
- Agrupación de entradas por ámbito (en lugar de por nodo)
- Cada ficha de ámbito muestra todos los eventos de sus nodos
- Badge con cantidad de nodos y entradas del ámbito
- Tabla con columnas: ID, I, F, Descripción, **Nodo**, J, E, Acc
**Tabla de Entradas** - ✅ Implementado
- Columna "Nodo" añadida — muestra el origen de cada evento
- Eventos ordenados por hora de inicio (descendente)
- Nodo se muestra destacado en color primario
**Fichas de Nodo (Alternativa)**
- [ ] Acceso directo para ver eventos de un nodo específico
- [ ] Mostrar ámbito asociado en el header
---
### Fase 5: Documentación y Migración
#### 5.1 - Actualizar hebras ADN
- [x] `01_ontologia.md` → Definir modelo ámbito/nodo
- [x] `02_bitacora.md` → Referenciar ámbito en formato
- [ ] `05_ia.md` → Añadir premisa de ámbito en contexto
- [x] `adn/README.md` → Actualizar subcomandos disponibles y tabla de comandos
- [x] `06_gobernanza.md` → Añadir nomenclatura de ámbitos
- [ ] `05_ia.md` → Añadir premisa de ámbito en contexto (pendiente)
#### 5.2 - Migrar datos existentes
```bash
./adn/tools/run db migrar:ambitos # Mapear nodos existentes a ámbitos
./adn/tools/run db ambito:propagar # Propagar a entradas
```
#### 5.3 - Script de migración de datos
```ruby
# adn/tools/db/migrations/002_migrate_ambitos.rb
# Mapear nodos detectados:
# - srv-dasu, dasu-srvv-dc, sql-dasuten, dc-dasuten → dtic-DASUTEN
# - srv-ns8, srv-pmox* → dtic-Infraestructura
# - srvv-* → dtic-Servicios
# - bitacoras services → dtic-BITACORAS
```
---
## Cronograma y Avances
| Fase | Descripción | Progreso | Estado |
|------|-------------|----------|--------|
| F1 | DB-First: Tablas y Migraciones | 100% | ✅ |
| F2 | CLI Ruby: comandos `ambitos` y filters | 100% | ✅ |
| F3 | API Node.js: Endpoints y Dashboard | 100% | ✅ |
| F4 | Frontend Vite: UI/UX Ámbitos | 100% | ✅ **Fichas por Ámbito implementadas** (2026-03-25) |
| F5 | Validación: Documentación y QA | 100% | ✅ |
---
## Log de Actividades (Hitos)
| Fecha | Hora | Hito | Descripción |
|-------|------|------|-------------|
| 20/03 | 08:30 | 📋 Inicio Plan | Elaboración y aprobación del plan P2604.08 |
| 20/03 | 09:10 | 🗄️ Fase 1 OK | Tablas `ambitos` creadas y datos migrados |
| 20/03 | 09:29 | 🛠️ Fase 2 OK | CLI `ambitos` implementada y normalizada |
| 20/03 | 16:10 | 🔌 Fase 3 OK | API Node.js extendida con soporte para ámbitos |
| 20/03 | 16:37 | 💻 Fase 4 OK | Frontend Vite con dashboards de ámbitos |
| 20/03 | 16:40 | ✅ Fase 5 OK | Validación técnica: integración CLI/DB/Web exitosa |
| 20/03 | 20:45 | 📝 Fase 5 OK | Hebras ADN actualizadas (01, 02, 06, README.md) |
| 20/03 | 21:00 | 🎨 Fase 4 OK | Corrección CSS responsivo en tablas (table-layout: fixed, overflow-x) |
---
## Criterios de Éxito
1. ✅ Base de datos con tabla `ambitos` funcional
2. ✅ CLI permite crear/listar/gestionar ámbitos (con `--help` normalizado)
3. ✅ Eventos registran ámbito automáticamente desde nodo
4. ✅ API expone endpoints de ámbito
5. ✅ Frontend muestra selector de ámbito en mini header
6. ✅ Datos migrados correctamente
7. ✅ Documentación actualizada en todas las hebras ADN (incluido README.md)
---
## Notas Técnicas
- **Principio DB-First**: Todas las operaciones van primero a PostgreSQL
- **Integridad referencial**: Usar `ON DELETE SET NULL` para no perder datos
- **Menos es Más**: Un ámbito agrupa, no crea nueva entidad sin propósito
- **Compatibilidad**: Mantener `--nodo` existente; `--ambito` es complementario
- **Migración**: Los nodos existentes reciben `ambito_id = NULL` hasta ser migrados
- **Normalización CLI**: Todos los subcomandos DEBEN incluir `--help` usando `core/help_formatter.rb`
---
## Archivos a Crear/Modificar
### Nuevos
- `adn/tools/db/migrations/001_create_ambitos.sql`
- `adn/tools/db/migrations/002_migrate_nodos_ambitos.sql`
- `adn/tools/db/migrations/002_migrate_ambitos.rb`
- `adn/tools/db/core/ambito_db.rb`
- `adn/tools/cli/ambitos.rb`
- `servicios/nginx/dtic-bitacoras/backend/src/routes/ambitos.js`
- `servicios/nginx/dtic-bitacoras/frontend/src/pages/Ambitos.tsx`
### Modificar
- `adn/tools/cli/db/nodo.rb` (añadir `--ambito`, `--help`)
- `adn/tools/cli/db/evento.rb` (añadir `--ambito`, `--help`)
- `adn/tools/cli/nodos.rb` (filtros por ámbito, `--help`)
- `adn/tools/cli/salud.rb` (métricas por ámbito, `--help`)
- `adn/tools/db/core/bitacora_db.rb`
- `adn/tools/db/core/evento_db.rb`
- `adn/tools/core/help_formatter.rb` (si no existe)
- `adn/tools/core/error_handler.rb` (si no existe)
- `servicios/nginx/dtic-bitacoras/backend/src/server.js`
- `servicios/nginx/dtic-bitacoras/frontend/src/App.tsx`
- `adn/01_ontologia.md`
- `adn/02_bitacora.md`
- `adn/05_ia.md`
- `adn/06_gobernanza.md`
- `adn/README.md`
-276
View File
@@ -1,276 +0,0 @@
# P2604 - Mejoras al ADN
> Manifiesto de Proyecto | Referencia: [`adn/07_proyectos.md`](../../../adn/07_proyectos.md)
## Identificación
| Campo | Valor |
| :--- | :--- |
| **Código** | P2604 |
| **Nombre** | Mejoras al Ecosistema ADN |
| **Estado** | ⏳ En Ejecución |
| **Inicio** | 14/03/2026 |
| **Fin Estimado** | Continuo (mejora continua) |
## Objetivo
Mejoras atómicas, incrementales y reutilizables al ecosistema ADN. Una herramienta por vez, siguiendo los principios: **Menos es Más + Mejora Continua + Armonía Integral**.
## Stakeholders
| Rol | Persona | Visibilidad |
| :--- | :--- | :--- |
| **Responsable Técnico** | Lic. Ricardo MONLA | Operador directo |
| **Dirección de TIC** | (Jerárquico superior) | Dashboard asíncrono |
## Nodos Involucrados
### Nodos Exclusivos (Core)
| Nodo | IP | Rol en el Proyecto |
| :--- | :--- | :--- |
| [**srv-ns8**](../../../nodos/srv-ns8.md) | `10.0.10.8` | Servidor de desarrollo y ejecución del ADN |
## Herramientas
| Herramienta | Uso |
| :--- | :--- |
| **Ruby 3.x** | Lenguaje del ecosistema ADN |
| **RSpec** | Framework de tests |
| **RuboCop** | Lint automatizado |
| **GitHub Actions** | CI/CD (tests + lint) |
## Hitos Clave
| Fecha | ID | Estado | Hito |
| :--- | :--- | :---: | :--- |
| 14/03 | F1-F8 | ✅ | Tests core, deprecation banners, error handler, constants, logger, HelpFormatter, docs técnica, CI/CD |
| 15/03 | F9-F10 | ✅ | Integración MSPs con ADN + Saneamiento y consolidación (S1-S15) |
| 15/03 | F11-F12 | ✅ | Bitácora Web auto-refresh + Estado/Modo calculado automáticamente |
| 16/03 | RFC-01 | ✅ | Convención de nomenclature de proyectos (RFC-ADN-01) |
| 18/03 | WZ-v2 | ✅ | Plan e implementación de W-ZOMBI v2.0 (reemplazo sonda legacy) |
| — | DOC | 📍 | Actualizar README.md con funcionalidades nuevas (auto-refresh, estado/modo) |
| — | P2604.08 | ✅ | Implementación del modelo Ámbito/Nodo en el ecosistema ADN |
---
## Diagnóstico Integral
### Problemas Identificados
| # | Problema | Archivo(s) | Principio | Severidad |
| :--- | :--- | :--- | :--- | :--- |
| 1 | Sin tests para módulos core | `core/*.rb` | Menos es Más | Alta |
| 2 | Banner DEPRECATED incompleto | `cli/validador.rb` | Armonía | Media |
| 3 | core/validador.rb sin deprecation | `core/validador.rb` | Armonía | Media |
| 4 | Constantes de colores hardcodeadas | `core/colores.rb` | Menos es Más | Baja |
| 5 | Sin handler de errores global | `run` | Armonía | Alta |
| 6 | db/exporters sin tests | `db/exporters/*.rb` | Menos es Más | Media |
| 7 | Logger sin rotación | `core/logger.rb` | Mejora Cont. | Baja |
| 8 | Help inconsistente entre CLI | `cli/*.rb` | Armonía | Baja |
| 9 | Sonda W-ZOMBI monolítica e inflexible | `adn/tools/sonda_w-zombi/servidor.rb` | Menos es Más | Alta |
---
## Plan de Mejoras (13 Fases)
### Fase 1: Tests Core (Menos es Más)
| ID | Módulo | Tests | Estado |
| :--- | :--- | :--- | :--- |
| T1 | `spec/db/evento_spec.rb` | 3 | ✅ |
| T2 | `spec/core/colores_spec.rb` | 6 | ✅ |
| T3 | `spec/core/validador_spec.rb` | 6 | ✅ |
| T4 | `spec/cli/help_spec.rb` | 0 | 📍 |
### Fase 2: Deprecation Banners (Armonía)
| ID | Archivo | Acción | Estado |
| :--- | :--- | :--- | :--- |
| D1 | `cli/validador.rb` | Banner agregado | ✅ |
| D2 | `core/validador.rb` | Revisar - útil para validaciones | ⏸️ |
| D3 | `db/exporters/md_exporter.rb` | Banner agregado | ✅ |
### Fase 3: Manejo de Errores (Armonía)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| E1 | Agregar `rescue` global en `run` | ✅ |
| E2 | Estandarizar mensajes de error | ✅ |
| E3 | Logging de excepciones no manejadas | ✅ |
**Herramientas creadas:** `core/error_handler.rb`, excepciones en `core/constants.rb`
### Fase 4: Constantes y Config (Menos es Más)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| C1 | Documentar constantes en constants.rb | ✅ |
| C2 | Agregar constantes reutilizables (estados, modos) | ✅ |
| C3 | Validar constantes al inicio | ✅ |
### Fase 5: Logger Mejorado (Mejora Continua)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| L1 | Agregar rotación de logs | ✅ |
| L2 | Niveles de log configurables | ✅ |
| L3 | Output JSON estructurado | ✅ |
### Fase 6: Help Consistente (Armonía)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| H1 | HelpFormatter helper reutilizable | ✅ |
### Fase 7: Documentación Técnica
| ID | Documento | Estado |
| :--- | :--- | :--- |
| DOC1 | `docs/tecnica/adn_cli.md` | ✅ |
| DOC2 | `docs/tecnica/adn_core.md` | ✅ |
| DOC3 | `docs/tecnica/adn_tests.md` | ✅ |
### Fase 8: CI/CD
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| CI1 | GitHub Actions para tests | ✅ |
| CI2 | Lint automatizado (.rubocop.yml) | ✅ |
### Fase 9: Integración MSP (Armonía Integral)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| M1 | Auditoría de MSPs | ✅ |
| M2 | Registro de MSPs en BD | ✅ |
| M3 | Listado de MSPs disponibles | ✅ |
| M4 | Documentación de integración | ✅ |
| M5 | Reutilizar NodosInfo en CLI | ✅ |
### Fase 10: Saneamiento y Consolidación
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| S1-S9 | Limpieza de código muerto y duplicaciones | ✅ |
| S10 | Migrar SubcomandoSalud a namespace ADN | ✅ |
| S11 | CLI `ssh` — ejecución remota reutilizable | ✅ |
| S12 | Refactorizar ssh.rb — leer datos de fichas | ✅ |
| S13 | Consolidar core/ — submódulos CLI | ✅ |
| S14 | Subcomando `nodos info` | ✅ |
| S15 | Actualizar README.md con estructura real | ✅ |
### Fase 11: Bitácora Web (Mejora Continua)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| B1 | Auto-refresh cada 30 segundos | ✅ |
| B2 | Refresh al recuperar foco | ✅ |
| B3 | Smart refresh (comparando max_id) | ✅ |
### Fase 12: Consistencia de Eventos (Menos es Más)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| E1 | Estado calculado automáticamente | ✅ |
| E2 | Modo resuelto desde última jornada | ✅ |
| E3 | Estado automático al actualizar con --fin | ✅ |
### Fase 13: Implementación W-ZOMBI v2.0 (Mejora Continua)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| W1 | Implementación de arquitectura modular | ✅ |
| W2 | Integración como subcomando ADN-CLI | ✅ |
| W3 | Test de despliegue y telemetría | ✅ |
### Fase 14: Estandarización Visual (Armonía Integral)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| V1 | Implementar barras de progreso Unicode en P2604 | ✅ |
| V2 | Propagar notación de barras a P2601 | ✅ |
| V3 | Crear guía de estilo para indicadores visuales | ✅ |
### Fase 15: Optimización de Intervención IA (Mejora Continua)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| IA1 | Diagnóstico de logs y fricción (sesión 18/03) | ✅ |
| IA2 | Refactorización de directivas en 05_ia.md | ✅ |
| IA3 | Automatización de contexto de seguridad (candados) | ✅ |
| IA4 | Validación de reducción de pasos exploratorios | ✅ |
### Fase 16: Modelo Ámbito/Nodo (Armonía Integral)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| A1 | Base de Datos: tabla `ambitos`, columnas `ambito_id` | ✅ |
| A2 | CLI Ruby: comando `ambitos`, normalización `--help` | ✅ |
| A3 | API REST: endpoints `/api/ambitos/*` | ✅ |
| A4 | Frontend: AmbitoDashboard con selector de ámbito | ✅ |
| A5 | Documentación: hebras ADN actualizadas | ✅ |
### Fase 17: Optimización Hardware srv-ns8
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| H1 | Aumentar RAM de srv-ns8 (mínimo 32 GB para Ollama) | ⏳ |
| H2 | Evaluar SSD para mejorar rendimiento | ⏳ |
### Fase 18: Integración Ollama (LLM Local)
| ID | Mejora | Estado |
| :--- | :--- | :--- |
| O1 | Instalar modelo LLM liviano (1-3B params) | ⏳ |
| O2 | Integrar Ollama como backend de IA en herramientas ADN | ⏳ |
| O3 | Capacidad de análisis de código/documentos via LLM local | ⏳ |
---
## Progreso
```
Fase 1: ██████████ 100% Tests Core
Fase 2: ██████░░░░ 67% Deprecation
Fase 3: ██████████ 100% Error Handler
Fase 4: ██████████ 100% Constants
Fase 5: ██████████ 100% Logger
Fase 6: ██████████ 100% HelpFormatter
Fase 7: ██████████ 100% Documentación
Fase 8: ██████████ 100% CI/CD
Fase 9: ██████████ 100% MSP
Fase 10: ██████████ 100% Saneamiento
Fase 11: ██████████ 100% Bitácora Web
Fase 12: ██████████ 100% Eventos
Fase 13: ██████████ 100% W-ZOMBI v2.0
Fase 14: ██████████ 100% Visibilidad
Fase 15: ██████████ 100% Optimización IA
Fase 16: ██████████ 100% Ámbito/Nodo
Fase 17: ░░░░░░░░░░ 0% Hardware srv-ns8
Fase 18: ░░░░░░░░░░ 0% Ollama (LLM Local)
```
## Métricas Totales
| Métrica | Valor |
| :--- | :--- |
| Tests pasando | 15 |
| Archivos creados | 14 |
| Archivos modificados | 12 |
| Archivos limpiados/movidos | 20+ |
| Fases completadas | 13/13 (100%) |
## Planes de Ejecución
- [Estandarización de Ayudas en Pantalla para Herramientas ADN](P2604.01.01_Estandarizacion-Ayudas-ADN.md)
- [Implementación W-ZOMBI v2.0](P2604.03.01_Plan-W-ZOMBI-v2.0.md)
- [Estandarización de Indicadores Visuales de Progreso](P2604.04.01_Estandarizacion-Barras-Progreso.md)
- [Estudio de Fricción IA (Antigravity) y Mejora Continua](P2604.05.01_Estudio-Friccion-IA.md)
- [Modelo Ámbito/Nodo para el Ecosistema ADN](P2604.08_Ambito-Nodo.md)
## Referencias
- **Punto de verdad**: [`adn/README.md`](../../../adn/README.md)
- **ADN**: [Ontología](../../../adn/01_ontologia.md) | [Proyectos](../../../adn/07_proyectos.md)
- **ADN IA**: [05_ia.md](../../../adn/05_ia.md)
- **Bitácoras**: IDs 1054-1056, 1073, 1106, 1112, 1163 vía `./adn/tools/run db evento:listar --detalle`
@@ -0,0 +1,108 @@
# Plan: Migración dtic-BKPs a Tools ADN
> **Grupo de Trabajo:** dtic-BKPs (Respaldos de Infraestructura)
**Código:** A03.P001
**Fecha:** 06 de abril de 2026
**Autor:** Sistema ADN
**Versión:** 1.0
**Estado:** ✅ COMPLETADO
## 📋 Resumen Ejecutivo
La herramienta `dtic-BKPs v6.0` actualmente reside en el directorio raíz del repositorio (`dtic-BKPs/`) como un sistema independiente con su propio entrypoint (`dtic-BKPs.rb`), configuración YAML, librería interna (`lib/`) y logs propios. Esto contradice el principio de **Armonía Integral** del ADN, donde todas las herramientas deben integrarse bajo `./adn/tools/run <subcomando>`.
Ya existe un intento previo parcial en `adn/tools/cli/backup.rb` (25KB) y `adn/tools/cli/backup/configurador.rb`, pero tiene bugs (el `--help` falla con `undefined local variable or method 'mostrar_ayuda'`).
El objetivo es **completar la migración** integrando la lógica probada de `dtic-BKPs v6.0` en el ecosistema ADN, preservando la configuración YAML y los procesadores funcionales.
## 🎯 Objetivo
Migrar `dtic-BKPs/` completamente a `adn/tools/cli/` para que:
1. Se invoque vía `./adn/tools/run bkps <subcomando>` (consistente con el ecosistema).
2. La configuración YAML se centralice en `adn/tools/config/bkps.yml`.
3. Los procesadores (Proxmox, Xen, Rclone) se integren como módulos ADN.
4. Los logs se unifiquen con el sistema de logging ADN (`adn/tools/core/logger`).
5. El directorio `dtic-BKPs/` raíz quede deprecado y eventualmente archivado.
## 📊 Inventario Actual
### Fuente: `dtic-BKPs/` (a migrar)
| Archivo | Tamaño | Función |
| :--- | :--- | :--- |
| `dtic-BKPs.rb` | 5.6 KB | Entrypoint, dispatcher, menú, auto-screen |
| `dtic-BKPs.yml` | 2.3 KB | Configuración de tareas y comandos |
| `lib/config.rb` | 1.7 KB | Parser de configuración YAML |
| `lib/logger.rb` | 1.7 KB | Logger propio (duplica ADN Logger) |
| `lib/menu.rb` | 5.9 KB | Menú interactivo con auto-avance |
| `lib/proc_proxmox.rb` | 5.0 KB | Procesador de dumps Proxmox |
| `lib/proc_xen.rb` | 2.7 KB | Procesador de backups XenServer |
| `lib/sync_rclone.rb` | 1.5 KB | Sincronización rclone |
### Destino: `adn/tools/cli/` (existente parcial)
| Archivo | Tamaño | Estado |
| :--- | :--- | :--- |
| `backup.rb` | 25.5 KB | ⚠️ Incompleto, `--help` roto |
| `backup/configurador.rb` | 1.2 KB | ⚠️ Solo configurador |
## 📅 Fases de Implementación
### ✅ FASE 1: Auditoría y Diagnóstico (Completada 2026-04-06)
- **1.1:** ✅ Analizado `adn/tools/cli/backup.rb` — 25KB con bugs (`mostrar_ayuda` undefined).
- **1.2:** ✅ Comparado con `dtic-BKPs v6.0` — funcionalidad probada y estable.
- **1.3:** ✅ Dependencias verificadas: rclone ✔, pv ✔, screen ✔.
### ✅ FASE 2: Migración a adn/tools/bkps/ (Completada 2026-04-06)
- **2.1:** ✅ Creado `adn/tools/bkps/bkps.rb` — CLI `SubcomandoBkps` con subcomandos: help, list, run, status, menu.
- **2.2:** ✅ Migrados procesadores a `adn/tools/bkps/lib/` — proc_proxmox.rb, proc_xen.rb, sync_rclone.rb, config.rb, menu.rb.
- **2.3:** ✅ Configuración YAML copiada a `adn/tools/bkps/bkps.yml`.
- **2.4:** ✅ Logger propio reemplazado por ADN Logger nativo.
- **2.5:** ✅ Dispatcher (`adn/tools/run`) actualizado con ruta `'bkps'``ADN::SubcomandoBkps`.
### ✅ FASE 3: Auditoría de backup.rb legacy (Completada 2026-04-06)
Análisis de `adn/tools/cli/backup.rb` (659 líneas, 25KB) — funcionalidades **diferentes** a las de dtic-BKPs v6.0:
| Funcionalidad | Descripción | Asimilar? |
| :--- | :--- | :---: |
| `ejecutar_backup(nodo)` | Backup remoto individual vía vzdump+SSH+candados, auto-detección VMID/host desde nodos/*.md | ✅ |
| Bitácora-First integrado | Auto-crear/actualizar evento en DB durante ejecución | ✅ |
| `comando_estados` | Consulta en tiempo real de tareas vzdump en cluster Proxmox vía API JSON | ✅ |
| `comando_sanear` | Saneamiento con retención configurable, exclusiones interactivas, borrado remoto | ✅ |
| `predeterminar` | Persistencia de defaults vía Configurador | ⏳ |
| `auto_reconciliar_bitacora` | Cierre automático de entradas pendientes cruzando con API Proxmox | ✅ |
**Bugs encontrados:** `mostrar_ayuda` mal anidado (fuera de scope private), `end` desbalanceados (L620-658).
### ✅ FASE 4: Asimilación de funcionalidades de backup.rb (Completada 2026-04-06)
- **4.1:** ✅ `bkps backup <nodo>` — backup remoto individual vía vzdump+SSH+candados con auto-detección de VMID/host.
- **4.2:** ✅ `bkps estados` — consulta estado Proxmox en tiempo real vía API JSON.
- **4.3:** ✅ `bkps sanear --dias N` — saneamiento con retención configurable, `--dry-run`, borrado remoto.
- **4.4:** ✅ Auto-cierre de bitácora implementado como tool ADN `dron` (reutilizable para cualquier ámbito).
### ✅ FASE 5: Validación, Documentación y Deprecación (Completada 2026-04-06)
- **5.1:** ✅ Pipeline C4 ejecutado exitosamente vía `./adn/tools/run bkps run C4 --batch` (23 sets, ~26min).
- **5.2:** ✅ Armonía Integral aplicada: ámbito, contexto y plan actualizados.
- **5.3:** ✅ Archivado: `dtic-BKPs/``_hist/dtic-BKPs/`, `backup.rb``adn/tools/cli/_hist/`.
## 🛠 Estructura Final
```
adn/tools/bkps/ # Tool principal (A03)
├── bkps.rb # CLI (11 subcomandos)
├── bkps.yml # Configuración YAML (tareas, retención 3 tiers)
├── cron-bkps.sh # Wrapper cron con lockfile + dron
└── lib/
├── config.rb # Parser YAML
├── menu.rb # Menú TUI interactivo
├── proc_proxmox.rb # Procesador Proxmox
├── proc_xen.rb # Procesador XEN
└── sync_rclone.rb # Sincronización rclone
adn/tools/cli/dron.rb # Dron de tareas largas (reutilizable)
```
## 📌 Notas
- Pipeline C4 ejecutado exitosamente el 2026-04-06 (23 sets Proxmox, ~26min).
- Tool `dron` creada como herramienta ADN genérica: `./adn/tools/run dron {lanzar|vigilar|estado|limpiar}`.
- Legacy archivado en `_hist/dtic-BKPs/` (standalone) y `adn/tools/cli/_hist/backup.rb` (intento previo).
@@ -0,0 +1,88 @@
# Plan: Automatización de Backups de Servidores
> **Grupo de Trabajo:** dtic-BKPs (Respaldos de Infraestructura)
**Código:** A03.P002
**Fecha:** 06 de abril de 2026
**Autor:** Sistema ADN
**Versión:** 1.2
**Estado:** ✅ COMPLETADO (F4.1 alertas pendiente futura iteración)
**Dependencia:** [A03.P001 — Migración a Tools ADN](A03.P001_Migracion_ADN.md) ✅
**Última verificación:** 2026-04-06 13:17 (drones operativos)
## 📋 Resumen Ejecutivo
Automatizar el ciclo completo de backups de servidores virtualizados (XenServer y Proxmox). Gracias a la migración completada en A03.P001, ya se cuenta con:
- CLI unificada: `./adn/tools/run bkps <subcomando>` (11 subcomandos)
- Modo batch: `bkps run C4 --batch` (pipeline completo sin interacción)
- Saneamiento: `bkps sanear --dias N` (limpieza con retención)
- Backup individual: `bkps backup <nodo>` (vzdump remoto)
- Monitoreo: `bkps estados` (estado Proxmox en tiempo real)
- Delegación: `dron lanzar --evento ID -- <comando>` (auto-cierre bitácora)
El objetivo de este plan es llevar esas capacidades a ejecución **programada y desatendida** con registro automático, notificaciones y políticas de retención.
## 🎯 Objetivo
1. **Ejecución programada** — Cron jobs en horarios de baja carga sin intervención.
2. **Registro automático en bitácora** — Cada ejecución crea/cierra su evento vía `dron`.
3. **Notificaciones** — Alertas por resultado (éxito/fallo).
4. **Rotación y limpieza** — Políticas de retención automatizadas vía `bkps sanear`.
5. **Monitoreo de salud** — Reporte de estado ampliado en `bkps status`.
## 📅 Fases de Implementación
### ✅ FASE 1: Ejecución No-Interactiva (Completada — heredada de A03.P001)
- **1.1:** ✅ `bkps run C4 --batch` — pipeline completo sin interacción.
- **1.2:** ✅ Logging con métricas: duración por tarea (▶→✔ Xm Ys) + total + conteo de fallos.
- **1.3:** ✅ Auto-cierre de bitácora vía `dron lanzar --evento ID -- <comando>`.
### ✅ FASE 2: Programación y Orquestación (Completada 2026-04-06)
- **2.1:** ✅ Cron wrapper: `adn/tools/bkps/cron-bkps.sh C4 "nota"`.
- **2.2:** ✅ Lockfile en `tmp/locks/bkps_<ref>.lock` (auto-cleanup, stale detection).
- **2.3:** ✅ Integración dron: bitácora automática (evento:crear + auto-cierre con resultado).
```bash
# Ejemplo cron nocturno con bitácora automática
./adn/tools/run dron lanzar --evento AUTO --nota "Cron: backup nocturno C4" -- ./adn/tools/run bkps run C4 --batch
```
### ✅ FASE 3: Retención y Limpieza Automática (Completada 2026-04-06)
- **3.1:** ✅ Retención 3 tiers en YAML: Proxmox (zfsDISCO1), Local (ns8Disco3), Nube (rmOneDrive).
- **3.2:** ✅ `bkps sanear` con protección 🛡️ más reciente por nodo por tier.
- **3.3:** ✅ `bkps buscar <nodo>` — localizar backups en los 3 tiers.
- **3.4:** ✅ C5 (`full_full_sanear`) con post-sanear automático.
### ✅ FASE 4: Notificaciones y Monitoreo (Completada 2026-04-06)
- **4.1:** ⏳ Alertas por fallo (email/webhook) — pendiente futura iteración.
- **4.2:** ✅ `bkps status` ampliado: herramientas, espacio disco, retención configurada.
- **4.3:** ✅ Bitácora web ya muestra eventos de backups vía `dron` auto-cierre.
## 🛠 Herramientas Disponibles (heredadas de A03.P001)
| Comando | Función |
| :--- | :--- |
| `bkps run C4 --batch` | Pipeline completo sin interacción |
| `bkps backup <nodo>` | Backup individual vzdump remoto |
| `bkps buscar <nodo>` | Localizar backups en 3 tiers |
| `bkps estados` | Estado Proxmox en tiempo real |
| `bkps sanear` | Sanear 3 tiers con protección por nodo |
| `bkps sanear --tier N` | Sanear tier específico |
| `bkps status` | Estado del sistema + espacio disco |
| `dron lanzar --evento ID/AUTO -- <cmd>` | Auto-cierre de bitácora |
| `dron flota` | Dashboard de flota |
| `dron salud` | Health check (zombies/estancados) |
## 🛸 Estado de Drones (Verificación 2026-04-06)
**Flota:** 2 drones completados y limpiados
- `dron_125106_654316` — Evento #1395: "Test diario de vuelo" → ✔ OK (0min)
- `dron_125505_657901` — Evento #1398: "Test diario v2" → ✔ OK
**Diagnóstico:** Ambos drones completaron exitosamente pero quedaron como "zombies" (PID muerto, estado=vigilando). El health check los auto-reparó marcándolos como fallidos y `dron limpiar` los removió.
**Conclusión:** Sistema de auto-reparación funciona correctamente. Verificar flota al inicio de sesión:
```bash
./adn/tools/run dron salud # Detecta y auto-repara zombies
./adn/tools/run dron limpiar # Remueve completados
```
+135
View File
@@ -0,0 +1,135 @@
# A03 - Ámbito dtic-BKPs
> Manifiesto de Ámbito | Referencia: [`adn/01_ontologia.md`](../../../adn/01_ontologia.md)
## Identificación
| Campo | Valor |
| :--- | :--- |
| **Código** | A03 |
| **Nombre** | Backups de Servidores — dtic-BKPs |
| **Estado** | ⏳ En Ejecución |
| **Inicio** | 06/04/2026 |
| **Fin Estimado** | Recurrente (operación continua) |
## Objetivo
El ámbito **dtic-BKPs** engloba todas las operaciones de respaldo (backup), procesamiento y sincronización de máquinas virtuales y datos críticos de la infraestructura DTIC. Esto incluye:
1. **Procesamiento de backups de VMs** (XenServer y Proxmox) desde orígenes locales y remotos.
2. **Sincronización y descarga** de archivos de respaldo entre nodos de almacenamiento.
3. **Upload a la nube** (OneDrive) de backups procesados para resguardo offsite.
## Herramientas
| Herramienta | Uso |
| :--- | :--- |
| **ADN CLI `bkps`** | `./adn/tools/run bkps <subcomando>` — interfaz unificada |
| **bkps.yml** | Configuración declarativa de tareas y comandos (`adn/tools/bkps/bkps.yml`) |
| **rclone** | Sincronización de archivos local ↔ remoto ↔ nube |
| **VZDump** | Respaldos offline de VMs Proxmox (stop mode) |
| **screen** | Sesiones desatendidas para operaciones largas |
| **ADN CLI `dron`** | `./adn/tools/run dron` — vigía de tareas largas con auto-cierre de bitácora |
## Nodos Involucrados
| Nodo | Rol en el Ámbito |
| :--- | :--- |
| **srv-ns8** | Almacenamiento central (ns8Disco2/ns8Disco3), procesamiento de backups |
| **pve_PMOX3** | Origen de dumps Proxmox (zfsDISCO1) |
| **srvv-maurik** | Origen de backups XenServer |
## Tareas Configuradas
| ID | Descripción | Tipo |
| :--- | :--- | :--- |
| `proc_syncXen1` | Procesar BKPs de VMs XEN01 | xva |
| `proc_syncPmox` | Procesar BKPs de VMs Proxmox | proxmox |
| `sinc_pmox_ns8` | Descargar BKPs de zfsDISCO1 | rclone |
| `sync_BkpAntiguos_nube` | Upload bkps-ANTIGUOS a OneDrive | rclone |
| `sync_BkpsSERVIDORes_nube` | Upload bkps-SERVIDORes a OneDrive | rclone |
| `sync_BkpUsers_nube` | Upload bkps-USERs a OneDrive | rclone |
## Comandos Compuestos
| ID | Descripción | Tareas |
| :--- | :--- | :--- |
| `descargar_y_procesar_pmox` | Descargar y Procesar BKPs de PMOX | sinc_pmox_ns8 → proc_syncPmox |
| `full_procesamiento` | Procesar TODOS los BKPs | proc_syncXen1 → proc_syncPmox |
| `full_upload` | Subir todos los BKPs a la Nube | sync_BkpAntiguos, sync_BkpsSERVIDORes, sync_BkpUsers |
| `full_full` | Procesar y Subir todo | Todas las tareas |
## Flujo de Trabajo Típico
```
1. Registrar evento en bitácora (inicio de jornada de backups)
2. Ejecutar: ./adn/tools/run bkps run <T1|C1|ID>
3. Monitorear progreso (screen -r si es desatendido)
4. Cerrar evento en bitácora con hora de fin
```
### Subcomandos ADN disponibles
```bash
./adn/tools/run bkps help # Ayuda
./adn/tools/run bkps list # Listar tareas y comandos
./adn/tools/run bkps run T1 # Ejecutar tarea individual
./adn/tools/run bkps run C4 --batch # Pipeline completo sin interacción
./adn/tools/run bkps run T1 --dry-run # Simulación
./adn/tools/run bkps status # Estado del sistema + espacio disco
./adn/tools/run bkps estados # Estado Proxmox en tiempo real
./adn/tools/run bkps backup <nodo> # Backup remoto individual (vzdump)
./adn/tools/run bkps buscar <nodo> # Buscar backups en 3 tiers
./adn/tools/run bkps sanear # Sanear 3 tiers (Proxmox+Local+Nube)
./adn/tools/run bkps sanear --tier 2 # Sanear solo tier específico
./adn/tools/run bkps menu # Menú interactivo (auto-screen)
```
## Planes Asociados
| Código | Nombre | Estado |
| :--- | :--- | :--- |
| [A03.P001](A03.P001_Migracion_ADN.md) | Migración a Tools ADN | ✅ |
| [A03.P002](A03.P002_Automatizacion_Backups.md) | Automatización de Backups | ✅ |
## 🛸 Sistema de Drones (Operación Autónoma)
**Herramienta:** `./adn/tools/run dron <subcomando>`
| Subcomando | Función |
| :--- | :--- |
| `dron lanzar --evento ID/AUTO --nota "X" -- <cmd>` | Lanzar tarea en background con auto-bitácora |
| `dron flota` | Dashboard compacto de la flota |
| `dron salud` | Health check activo (detecta zombies/estancados) |
| `dron estado` | Estado detallado de cada dron |
| `dron limpiar` | Limpiar tareas completadas/fallidas |
**Estado actual (2026-04-06 13:17):** Flota limpia — 2 drones completados y removidos.
**Evento AUTO:** Crea evento de bitácora automáticamente y lo cierra al finalizar. Ideal para cron jobs y tareas desatendidas.
**Ejemplo cron:**
```bash
./adn/tools/run dron lanzar --evento AUTO --nota "Backup nocturno C4" -- ./adn/tools/run bkps run C4 --batch
```
## 🔄 Armonía Integral
**Última actualización:** 2026-04-06 (reestructuración completada)
| Documento | Estado | Notas |
| :--- | :--- | :--- |
| `docs/contexto/IA.md` | ✅ Actualizado | Procedimiento de Armonía Integral agregado |
| `A03_dtic-BKPs.md` | ✅ Actualizado | Manifiesto con drones y estructura estándar |
| `A03.P002_Automatizacion_Backups.md` | ✅ Actualizado | Versión 1.2, estado de drones |
---
## Referencias
- **CLI ADN**: `./adn/tools/run bkps help`
- **Código fuente**: [`adn/tools/bkps/bkps.rb`](../../../adn/tools/bkps/bkps.rb)
- **Configuración**: [`adn/tools/bkps/bkps.yml`](../../../adn/tools/bkps/bkps.yml)
- **Legacy (archivado)**: [`_hist/dtic-BKPs/`](../../../_hist/dtic-BKPs/) — versión standalone pre-migración
- **ADN Ontología**: [`adn/01_ontologia.md`](../../../adn/01_ontologia.md)
- **Bitácoras**: `./adn/tools/run db evento:listar --ambito dtic-BKPs`
@@ -1,10 +1,13 @@
# Plan: Reconfiguración de Red srv-dasu (Mudanza Física) # Plan: Reconfiguración de Red srv-dasu (Mudanza Física)
> **Ámbito:** A04 — dtic-DASUTEN
**Código:** A04.P001
**Fecha:** 11 de marzo de 2026 **Fecha:** 11 de marzo de 2026
**Autor:** Sistema ADN **Autor:** Sistema ADN
**Versión:** 1.0 **Versión:** 1.0
**Ubicación:** `docs/ambito/dtic-DASUTEN/plan/P2601.06.01_Red-SrvDasu.md`
**Estado:** ✅ COMPLETADO **Estado:** ✅ COMPLETADO
**Dependencia:** Ninguna
## 📋 Resumen Ejecutivo ## 📋 Resumen Ejecutivo
@@ -0,0 +1,22 @@
# Plan: Integración dasu-pc a Dominio DASUTEN
> **Ámbito:** A04 — dtic-DASUTEN
**Código:** A04.P002
**Fecha:** 25 de marzo de 2026
**Autor:** Sistema ADN
**Versión:** 1.0
**Estado:** ⏸️ PAUSADO (Enfoque cambiado a Workgroup — ver A04.P005)
**Dependencia:** A04.P001 (Red srv-dasu)
## 📋 Resumen Ejecutivo
Integrar exitosamente el nodo físico `dasu-pc` al dominio `dasu-srvv-dc` para centralizar la gestión de usuarios, aplicar GPOs y unificar la red operativa de DASUTEN.
**Nota:** Este plan fue pausado el 2026-03-27 cuando se adoptó el enfoque sin Domain Controller (A04.P005). Las VMs con DC (100, 101, 102) fueron apagadas y preservadas.
## ✅ Logros Alcanzados
- [x] Configuración de DNS primario en `dasu-pc` apuntando a DC.
- [x] Renombramiento definitivo a `dasu-pc` inyectado vía SSH (RSA).
- [x] Otorgar acceso a `dasu-pc` para usuarios (Verificado).
@@ -1,10 +1,13 @@
# Plan: Unión de pc-dasu0 al Dominio DASUTEN (P2601 Fase 6.1.B) # Plan: Unión de pc-dasu0 al Dominio DASUTEN
> **Ámbito:** A04 — dtic-DASUTEN
**Código:** A04.P003
**Fecha:** 11 de marzo de 2026 (Actualizado: 16/03/2026) **Fecha:** 11 de marzo de 2026 (Actualizado: 16/03/2026)
**Autor:** Sistema ADN **Autor:** Sistema ADN
**Versión:** 4.1 **Versión:** 4.1
**Estado:** ⏳ EN EJECUCIÓN (Fase 1 - Normalización de Nombres) **Estado:** ⏸️ PAUSADO (Enfoque cambiado a Workgroup — ver A04.P005)
**Proyecto:** [P2601 DASUTEN](../P2601_dasuten.md) **Dependencia:** A04.P001 (Red srv-dasu), A04.P002 (Integración DASU-PC)
## 📋 Resumen Ejecutivo ## 📋 Resumen Ejecutivo
@@ -1,6 +1,17 @@
# P2601.08 - Unificación de Redes: Subred ISP # Plan: Unificación de Redes: Subred ISP
> **Objetivo**: Migrar las VMs de srv-dasu y dasu-pc de sus redes aisladas a la subred del router ISP, unificando toda la infraestructura DASUTEN en una sola red. > **Ámbito:** A04 — dtic-DASUTEN
**Código:** A04.P004
**Fecha:** 26 de marzo de 2026
**Autor:** Sistema ADN
**Versión:** 1.0
**Estado:** ⏸️ PAUSADO (Parcialmente implementado — ver A04.P005)
**Dependencia:** A04.P001 (Red srv-dasu)
## 📋 Resumen Ejecutivo
Migrar las VMs de srv-dasu y dasu-pc de sus redes aisladas a la subred del router ISP, unificando toda la infraestructura DASUTEN en una sola red.
## Contexto ## Contexto
@@ -1,12 +1,13 @@
# Plan: DASUTEN sin Domain Controller (Modo Workgroup) # Plan: DASUTEN sin Domain Controller (Modo Workgroup)
> **Grupo de Trabajo:** DASUTEN > **Ámbito:** A04 — dtic-DASUTEN
**Código:** P2601.09 **Código:** A04.P005
**Fecha:** 27 de marzo de 2026 **Fecha:** 27 de marzo de 2026
**Autor:** Sistema ADN **Autor:** Sistema ADN
**Versión:** 2.2 **Versión:** 2.2
**Estado:** 🚧 EN EJECUCIÓN (Fase 2 completada, Fase 3 pendiente — Creación VM PC Cliente) **Estado:** 🚧 EN EJECUCIÓN (Fase 2 completada, Fase 3 pendiente — Creación VM PC Cliente)
**Dependencia:** A04.P001 (Red srv-dasu)
## 📋 Resumen Ejecutivo ## 📋 Resumen Ejecutivo
@@ -141,7 +142,8 @@ Crear dos VMs nuevas (SQL Server + PC cliente) en `srv-dasu`, instalar el sistem
## 🔗 Referencias ## 🔗 Referencias
- **Proyecto padre**: [P2601_dasuten.md](P2601_dasuten.md) - **Ámbito**: [A04_dtic-DASUTEN.md](A04_dtic-DASUTEN.md)
- **Plan previo de red**: [P2601.06.01_Red-SrvDasu.md](P2601.06.01_Red-SrvDasu.md) - **Plan de red**: [A04.P001_Red-SrvDasu.md](A04.P001_Red-SrvDasu.md)
- **Plan previo integración**: [P2601.07.01_Integracion-DASU-PC.md](P2601.07.01_Integracion-DASU-PC.md) - **Plan integración (pausado)**: [A04.P002_Integracion-DASU-PC.md](A04.P002_Integracion-DASU-PC.md)
- **Nodos**: [srv-dasu](../../../nodos/srv-dasu.md) | [srv-ns8](../../../nodos/srv-ns8.md) - **Nodos**: [srv-dasu](../../../nodos/srv-dasu.md) | [srv-ns8](../../../nodos/srv-ns8.md)
- **Legacy archivado**: [_hist_P2601_dasuten.md](_hist_P2601_dasuten.md)
@@ -0,0 +1,188 @@
# A04 - Ámbito dtic-DASUTEN
> Manifiesto de Ámbito | Referencia: [`adn/01_ontologia.md`](../../../adn/01_ontologia.md)
## Identificación
| Campo | Valor |
| :--- | :--- |
| **Código** | A04 |
| **Nombre** | Infraestructura y Sistema DASUTEN |
| **Estado** | ⏳ En Ejecución |
| **Inicio** | 23/02/2026 |
| **Fin Estimado** | Por definir (operación continua) |
## Objetivo
El ámbito **dtic-DASUTEN** engloba todas las operaciones relacionadas con la infraestructura, migración y mantenimiento del sistema de gestión administrativa de DASUTEN (Departamento de Acción Social Universitaria Tecnológica Nacional).
**DASUTEN** es la obra social de empleados, docentes y alumnos de la Universidad Tecnológica Nacional, con oficina en la Facultad Regional La Rioja pero bajo dependencia de la **sede central en el Rectorado (Buenos Aires)**.
Este ámbito cubre:
1. **Infraestructura virtualizada** — Hipervisor Proxmox VE (`srv-dasu`) con VMs Windows (DC, SQL, PC cliente).
2. **Migración de sistema** — Traslado del sistema de gestión desde servidores de la Facultad a infraestructura independiente.
3. **Operación en modo Workgroup** — Desde 2026-03-27, se adoptó enfoque sin Domain Controller (P2601.09), reduciendo complejidad de red.
4. **Independencia operativa** — Facilitar la gestión desde Rectorado si fuera necesario.
## Herramientas
| Herramienta | Uso |
| :--- | :--- |
| **Proxmox VE 9.1** | Virtualización y gestión de VMs (vmbr0, vzdump) |
| **Windows Server 2022 Core** | SO base para SQL Server (dasu-sql2) |
| **Windows 10 LTSC** | VM de pruebas y cliente (dasu-pcv2) |
| **SQL Server 2019** | Motor de base de datos relacional (autenticación mixta) |
| **OpenSSH** | Administración remota nativa de VMs Windows (puerto 7022) |
| **VZDump** | Resguardos offline de VMs (stop mode, zstd) |
| **Tailscale** | VPN de acceso remoto (subnet router, cuenta `pcdasu0@frlr.utn.edu.ar`) |
| **ADN CLI `dasuten`** | `./adn/tools/run dasuten <subcomando>` — herramientas específicas del ámbito |
| **ADN CLI `dron`** | `./adn/tools/run dron` — vigía de tareas largas con auto-bitácora |
## Nodos Involucrados
### Núcleo DASUTEN (Exclusivos del Ámbito)
| Nodo | IP / VMID | Rol en el Ámbito |
| :--- | :--- | :--- |
| **srv-dasu** | `192.168.1.13` (DHCP ISP) / Tailscale `100.112.46.104` | Hipervisor Proxmox VE (Libre de red académica) |
| **dasu-sql2** (VM 103) | `192.168.1.14` (DHCP ISP) | SQL Server 2019 — Motor de base de datos |
| **dasu-pcv2** (VM 104) | `192.168.1.15` (DHCP ISP) | VM de pruebas / Cliente de referencia |
| **dasu-pc** | `192.168.1.20` (DHCP ISP) | PC física cliente en oficina DASUTEN |
### Nodos de Soporte
| Nodo | IP | Rol en el Ámbito |
| :--- | :--- | :--- |
| **srv-ns8** | `10.0.10.8` | Gestión central, GIT, Dashboard P2601 |
### VMs Preservadas (Apagadas — Enfoque Anterior con DC)
| Nodo | VMID | Estado | Nota |
| :--- | :--- | :--- | :--- |
| **dasu-srvv-dc** | 100 | 🛑 Apagada | Controlador de Dominio AD DS (P2601.06-08) |
| **dasu-srvv-sql** | 101 | 🛑 Apagada | SQL Server en dominio (P2601.06-08) |
| **dasu-pcv** | 102 | 🛑 Apagada | VM de pruebas en dominio (P2601.07.01) |
## Topología de Red (Modo Workgroup — P2601.09)
```
┌─────────────────────────────────────────────────────────────────┐
│ ROUTER ISP (Gateway: 192.168.1.1) │
│ Subred: 192.168.1.0/24 │
└─────────────────────┬───────────────────────────────────────────┘
┌────────────┴─────────────────────────────────┐
│ │
↓ ↓
┌─────────────────┐ ┌─────────────────────────┐
│ dasu-pc │ │ srv-dasu (Proxmox) │
│ 192.168.1.20 │ │ 192.168.1.13 │
│ (PC Física) │ │ │
└─────────────────┘ │ vmbr0: 192.168.1.1/24 │
│ ├── VM 103: .14 (SQL) │
│ └── VM 104: .15 (PCV) │
└─────────────────────────┘
```
**Características:**
- Red plana del ISP — todas las máquinas en la misma subred `192.168.1.0/24`
- Sin Domain Controller — autenticación SQL mixta (SQL Auth, no Windows Integrated)
- Tailscale para acceso remoto — cuenta `pcdasu0@frlr.utn.edu.ar`
- Gateway: `192.168.1.1` (router ISP)
## Tareas Configuradas
| ID | Descripción | Tipo |
| :--- | :--- | :--- |
| `backup_sql2` | Backup vzdump de dasu-sql2 (VM 103) | proxmox |
| `backup_pcv2` | Backup vzdump de dasu-pcv2 (VM 104) | proxmox |
| `db_migrar` | Migrar base de datos DASUTEN desde backup .bak | sqlcmd |
| `tailscale_up` | Activar/verificar Tailscale en nodos | tailscale |
## Comandos Compuestos
| ID | Descripción | Tareas |
| :--- | :--- | :--- |
| `full_backup` | Backup completo de VMs DASUTEN | backup_sql2 → backup_pcv2 |
| `db_restore` | Restaurar DB DASUTEN en dasu-sql2 | db_migrar |
## Flujo de Trabajo Típico
```
1. Registrar evento en bitácora (inicio de jornada DASUTEN)
2. Ejecutar: ./adn/tools/run dasuten <subcomando>
3. Monitorear progreso (dron para tareas largas)
4. Cerrar evento en bitácora con hora de fin
```
### Subcomandos ADN disponibles
```bash
./adn/tools/run dasuten help # Ayuda
./adn/tools/run dasuten db:migrar # Migrar/base de datos DASUTEN
./adn/tools/run dasuten backup <vm> # Backup individual de VM
./adn/tools/run dasuten status # Estado del ámbito
./adn/tools/run dron lanzar --evento AUTO --nota "X" -- ./adn/tools/run dasuten <cmd>
```
## Planes Asociados
| Código | Nombre | Estado |
| :--- | :--- | :--- |
| [A04.P001](A04.P001_Red-SrvDasu.md) | Reconfiguración de Red srv-dasu | ✅ |
| [A04.P002](A04.P002_Integracion-DASU-PC.md) | Integración dasu-pc a Dominio | ⏸️ (Pausado — enfoque cambiado) |
| [A04.P003](A04.P003_AD-Join.md) | Unión de pc-dasu0 al Dominio | ⏸️ (Pausado — enfoque cambiado) |
| [A04.P004](A04.P004_Unificacion-Redes-ISP.md) | Unificación de Redes: Subred ISP | ⏸️ (Pausado — enfoque cambiado) |
| [A04.P005](A04.P005_DASUTEN-sin-DC.md) | DASUTEN sin Domain Controller | 🚧 En Ejecución |
## 🛸 Sistema de Drones (Operación Autónoma)
**Herramienta:** `./adn/tools/run dron <subcomando>`
| Subcomando | Función |
| :--- | :--- |
| `dron lanzar --evento ID/AUTO --nota "X" -- <cmd>` | Lanzar tarea en background con auto-bitácora |
| `dron flota` | Dashboard compacto de la flota |
| `dron salud` | Health check activo (detecta zombies/estancados) |
| `dron estado` | Estado detallado de cada dron |
| `dron limpiar` | Limpiar tareas completadas/fallidas |
**Evento AUTO:** Crea evento de bitácora automáticamente y lo cierra al finalizar. El `tarea_id` del dron se incluye en la descripción del evento con formato `🛸 dron_HHMMSS_PID: <nota>`.
**Ejemplo:**
```bash
./adn/tools/run dron lanzar --evento AUTO --nota "Backup dasu-sql2" -- ./adn/tools/run dasuten backup sql2
```
## Dashboard P2601
- **URL**: `https://ns8.frlr.utn.edu.ar/P2601/`
- **Tipo**: Aplicación web estática (HTML/JS/CSS) servida por Nginx.
- **Métricas**: Timeline de hitos, barras de progreso, desglose Físico/Remoto por hito.
- **Actualización**: Las métricas se alimentan de las etiquetas parseadas de las bitácoras.
## 🔄 Armonía Integral
**Última actualización:** 2026-04-06 (reestructuración a estándar A03 completada)
| Documento | Estado | Notas |
| :--- | :--- | :--- |
| `docs/contexto/IA.md` | ✅ Actualizado | Procedimiento de Armonía Integral agregado |
| `A04_dtic-DASUTEN.md` | ✅ Creado | Manifiesto nuevo siguiendo estándar A03 |
| `A04.P001_Red-SrvDasu.md` | ✅ Actualizado | Encabezado estandarizado |
| `A04.P002_Integracion-DASU-PC.md` | ✅ Actualizado | Estado: PAUSADO (enfoque cambiado) |
| `A04.P003_AD-Join.md` | ✅ Actualizado | Estado: PAUSADO (enfoque cambiado) |
| `A04.P004_Unificacion-Redes-ISP.md` | ✅ Actualizado | Estado: PAUSADO (parcialmente implementado) |
| `A04.P005_DASUTEN-sin-DC.md` | ✅ Actualizado | Estado: EN EJECUCIÓN, referencias actualizadas |
| `_hist_P2601_dasuten.md` | ✅ Archivado | Manifiesto legacy preservado |
---
## Referencias
- **CLI ADN**: `./adn/tools/run dasuten help`
- **Código fuente**: [`adn/tools/cli/dasuten.rb`](../../../adn/tools/cli/dasuten.rb) (por crear)
- **ADN Ontología**: [`adn/01_ontologia.md`](../../../adn/01_ontologia.md)
- **Bitácoras**: `./adn/tools/run db evento:listar --ambito dtic-DASUTEN`
- **Legacy (archivado)**: [`_hist_P2601_dasuten.md`](_hist_P2601_dasuten.md) — manifiesto anterior del proyecto
@@ -1,19 +0,0 @@
# P2601.07.01 - Plan: Integración dasu-pc a Dominio DASUTEN
> **Objetivo**: Integrar exitosamente el nodo físico `dasu-pc` al dominio `dasu-srvv-dc` para centralizar la gestión de usuarios, aplicar GPOs y unificar la red operativa de DASUTEN.
- [x] **1.B:** Configuración de DNS primario en `dasu-pc` apuntando a DC.
- [x] **3.B:** Renombramiento definitivo a `dasu-pc` inyectado vía SSH (RSA).
- [x] **4.C:** Otorgar acceso a `dasu-pc` para estos usuarios (Verificado).
- **Host:** `dasu-pc` (Ex `pc-dasu0`)
- [P2601.07.01] Renombramiento de `dasu-pc-0` a `dasu-pc` completado.
| dasu-pc | 100.100.145.51 | ✅ ONLINE |
| dasu-srvv-dc | 100.85.117.101 | ❌ offline (VM apagada) |
| dasu-srvv-sql | 100.107.24.124 | ❌ offline (VM apagada) |
---
**Fecha de implementación:** 2026-03-25
**Bitácora de referencia:** Evento 1273
**Responsable:** Lic. Ricardo MONLA
+4
View File
@@ -20,3 +20,7 @@
## Estrategia de Acceso ## Estrategia de Acceso
Este nodo debe tener Tailscale instalado y logueado con la cuenta `pcdasu0@frlr.utn.edu.ar` (o validado en la misma red) para recibir conexiones directas desde `pc-dasu0`. Este nodo debe tener Tailscale instalado y logueado con la cuenta `pcdasu0@frlr.utn.edu.ar` (o validado en la misma red) para recibir conexiones directas desde `pc-dasu0`.
## Credenciales
- **Usuario de Windows/Dominio**: `molinaromina` (Romina Molina)
- **Contraseña**: `$DASU123`
+2 -1
View File
@@ -6,7 +6,8 @@
- **Rol**: Base de Datos / Almacenamiento - **Rol**: Base de Datos / Almacenamiento
- **Estado**: 🟢 Online - **Estado**: 🟢 Online
- **Tipo**: Contenedor LXC (Proxmox) - **Tipo**: Contenedor LXC (Proxmox)
- **Padre/Host**: `srv-pmox3` (VM ID: 107) - **VMID**: `107`
- **Padre/Host**: `srv-pmox3`
- **OS**: Debian 12 (Bookworm) - **OS**: Debian 12 (Bookworm)
## Hardware ## Hardware
+3 -2
View File
@@ -7,9 +7,10 @@
| **IP LAN** | `10.0.10.117` | | **IP LAN** | `10.0.10.117` |
| **DNS** | `rmonla.duckdns.org` | | **DNS** | `rmonla.duckdns.org` |
| **Sistema Operativo** | Debian 12 (Bookworm) | | **Sistema Operativo** | Debian 12 (Bookworm) |
| **Tipo** | Contenedor LXC (VMID 116, unprivileged) | | **Tipo** | Contenedor LXC (unprivileged) |
| **VMID** | `116` |
| **Rol** | Servidor Web NGINX | | **Rol** | Servidor Web NGINX |
| **Host** | `srv-pmox3` (10.0.10.203) | | **Host** | `srv-pmox3` |
| **SSH** | Puerto `7022`, usuario `root`, auth `password` | | **SSH** | Puerto `7022`, usuario `root`, auth `password` |
| **Bóveda** | `srvv-nginx-rm:root` | | **Bóveda** | `srvv-nginx-rm:root` |