From 1802c25194baac0ee36f3c5f899daedbf0ceba3a Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Tue, 7 Apr 2026 10:23:53 -0300 Subject: [PATCH] =?UTF-8?q?refactor(ambitos):=20Migraci=C3=B3n=20completa?= =?UTF-8?q?=20dtic-DASUTEN=20(A04)=20y=20dtic-BKPs=20(A03)=20+=20herramien?= =?UTF-8?q?tas=20ADN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../dtic-BKPs/_hist}/dtic-BKPs.rb | 0 .../dtic-BKPs/_hist}/dtic-BKPs.yml | 0 .../_hist}/dtic-BKPs_v5.5.1/CHANGELOG.md | 0 .../dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md | 0 .../_hist}/dtic-BKPs_v5.5.1/README.md | 0 .../_hist}/dtic-BKPs_v5.5.1/app-run-sh | 0 .../_hist}/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb | 0 .../dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb | 0 .../dtic-BKPs_v5.5.1/fxs/config_handler.rb | 0 .../dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb | 0 .../dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb | 0 .../dtic-BKPs_v5.5.1/fxs/sync_rclone.rb | 0 .../dtic-BKPs_v5.5.1/fxs/ui_handlers.rb | 0 .../dtic-BKPs_v5.5.1/src/core/globals.rb | 0 .../_hist}/dtic-BKPs_v5.5.1/src/install.sh | 0 .../dtic-BKPs/_hist}/lib/config.rb | 0 .../dtic-BKPs/_hist}/lib/logger.rb | 0 .../dtic-BKPs/_hist}/lib/menu.rb | 0 .../dtic-BKPs/_hist}/lib/proc_proxmox.rb | 0 .../dtic-BKPs/_hist}/lib/proc_xen.rb | 0 .../dtic-BKPs/_hist}/lib/sync_rclone.rb | 0 adn/tools/bkps/bkps.rb | 825 ++++++++++++++++++ adn/tools/bkps/bkps.yml | 104 +++ adn/tools/bkps/cron-bkps.sh | 87 ++ adn/tools/bkps/lib/config.rb | 71 ++ adn/tools/bkps/lib/menu.rb | 86 ++ adn/tools/bkps/lib/proc_proxmox.rb | 150 ++++ adn/tools/bkps/lib/proc_xen.rb | 88 ++ adn/tools/bkps/lib/sync_rclone.rb | 54 ++ adn/tools/candados/.session | 2 +- adn/tools/cli/{ => _hist}/backup.rb | 0 adn/tools/cli/{ => _hist}/backup.rb.backup | 0 .../cli/{ => _hist}/backup/configurador.rb | 0 adn/tools/cli/copiloto.rb | 304 +++++++ adn/tools/cli/dron.rb | 509 +++++++++++ adn/tools/cli/novato.rb | 286 ++++++ adn/tools/core/conciliador.rb | 2 +- adn/tools/w-zombi/data/cmd.json | 2 +- .../P2604.01.01_Estandarizacion-Ayudas-ADN.md | 76 -- .../P2604.02.01_Generacion-Proyectos-ADN.md | 66 -- .../dtic-ADN/P2604.03.01_Plan-W-ZOMBI-v2.0.md | 53 -- ...4.04.01_Estandarizacion-Barras-Progreso.md | 47 - .../P2604.05.01_Estudio-Friccion-IA.md | 43 - ...-Mejora-Continua-para-el-Ecosistema-ADN.md | 83 -- .../P2604.07_Plantillas-Escritura-Eventos.md | 101 --- docs/ambito/dtic-ADN/P2604.08_Ambito-Nodo.md | 419 --------- docs/ambito/dtic-ADN/P2604_Mejoras-ADN.md | 276 ------ .../dtic-BKPs/A03.P001_Migracion_ADN.md | 108 +++ .../A03.P002_Automatizacion_Backups.md | 88 ++ docs/ambito/dtic-BKPs/A03_dtic-BKPs.md | 135 +++ ...Red-SrvDasu.md => A04.P001_Red-SrvDasu.md} | 5 +- .../A04.P002_Integracion-DASU-PC.md | 22 + ...1.06.02_AD-Join.md => A04.P003_AD-Join.md} | 9 +- ...P.md => A04.P004_Unificacion-Redes-ISP.md} | 15 +- ...N-sin-DC.md => A04.P005_DASUTEN-sin-DC.md} | 14 +- docs/ambito/dtic-DASUTEN/A04_dtic-DASUTEN.md | 188 ++++ .../P2601.07.01_Integracion-DASU-PC.md | 19 - .../_hist_P2601_dasuten.md} | 0 nodos/pcv-dasu1.md | 4 + nodos/srvv-data.md | 3 +- nodos/srvv-nginx-rm.md | 5 +- 61 files changed, 3148 insertions(+), 1201 deletions(-) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs.yml (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/CHANGELOG.md (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/README.md (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/app-run-sh (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/fxs/config_handler.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/src/core/globals.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/dtic-BKPs_v5.5.1/src/install.sh (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/lib/config.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/lib/logger.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/lib/menu.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/lib/proc_proxmox.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/lib/proc_xen.rb (100%) rename {dtic-BKPs => _hist/dtic-BKPs/_hist}/lib/sync_rclone.rb (100%) create mode 100644 adn/tools/bkps/bkps.rb create mode 100644 adn/tools/bkps/bkps.yml create mode 100755 adn/tools/bkps/cron-bkps.sh create mode 100644 adn/tools/bkps/lib/config.rb create mode 100644 adn/tools/bkps/lib/menu.rb create mode 100644 adn/tools/bkps/lib/proc_proxmox.rb create mode 100644 adn/tools/bkps/lib/proc_xen.rb create mode 100644 adn/tools/bkps/lib/sync_rclone.rb rename adn/tools/cli/{ => _hist}/backup.rb (100%) rename adn/tools/cli/{ => _hist}/backup.rb.backup (100%) rename adn/tools/cli/{ => _hist}/backup/configurador.rb (100%) create mode 100644 adn/tools/cli/copiloto.rb create mode 100644 adn/tools/cli/dron.rb create mode 100644 adn/tools/cli/novato.rb delete mode 100644 docs/ambito/dtic-ADN/P2604.01.01_Estandarizacion-Ayudas-ADN.md delete mode 100644 docs/ambito/dtic-ADN/P2604.02.01_Generacion-Proyectos-ADN.md delete mode 100644 docs/ambito/dtic-ADN/P2604.03.01_Plan-W-ZOMBI-v2.0.md delete mode 100644 docs/ambito/dtic-ADN/P2604.04.01_Estandarizacion-Barras-Progreso.md delete mode 100644 docs/ambito/dtic-ADN/P2604.05.01_Estudio-Friccion-IA.md delete mode 100644 docs/ambito/dtic-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md delete mode 100644 docs/ambito/dtic-ADN/P2604.07_Plantillas-Escritura-Eventos.md delete mode 100644 docs/ambito/dtic-ADN/P2604.08_Ambito-Nodo.md delete mode 100644 docs/ambito/dtic-ADN/P2604_Mejoras-ADN.md create mode 100644 docs/ambito/dtic-BKPs/A03.P001_Migracion_ADN.md create mode 100644 docs/ambito/dtic-BKPs/A03.P002_Automatizacion_Backups.md create mode 100644 docs/ambito/dtic-BKPs/A03_dtic-BKPs.md rename docs/ambito/dtic-DASUTEN/{P2601.06.01_Red-SrvDasu.md => A04.P001_Red-SrvDasu.md} (97%) create mode 100644 docs/ambito/dtic-DASUTEN/A04.P002_Integracion-DASU-PC.md rename docs/ambito/dtic-DASUTEN/{P2601.06.02_AD-Join.md => A04.P003_AD-Join.md} (98%) rename docs/ambito/dtic-DASUTEN/{P2601.08_Unificacion-Redes-ISP.md => A04.P004_Unificacion-Redes-ISP.md} (94%) rename docs/ambito/dtic-DASUTEN/{P2601.09_DASUTEN-sin-DC.md => A04.P005_DASUTEN-sin-DC.md} (96%) create mode 100644 docs/ambito/dtic-DASUTEN/A04_dtic-DASUTEN.md delete mode 100644 docs/ambito/dtic-DASUTEN/P2601.07.01_Integracion-DASU-PC.md rename docs/ambito/dtic-DASUTEN/{P2601_dasuten.md => _hist/_hist_P2601_dasuten.md} (100%) diff --git a/dtic-BKPs/dtic-BKPs.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs.rb diff --git a/dtic-BKPs/dtic-BKPs.yml b/_hist/dtic-BKPs/_hist/dtic-BKPs.yml similarity index 100% rename from dtic-BKPs/dtic-BKPs.yml rename to _hist/dtic-BKPs/_hist/dtic-BKPs.yml diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/CHANGELOG.md b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/CHANGELOG.md similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/CHANGELOG.md rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/CHANGELOG.md diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/PROYECT_dtic-BKPs.md diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/README.md b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/README.md similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/README.md rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/README.md diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/app-run-sh b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/app-run-sh similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/app-run-sh rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/app-run-sh diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/dtic-BKPs_app.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/dtic-BKPs_tasks.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/config_handler.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/config_handler.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/fxs/config_handler.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/config_handler.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/proc_syncPmox.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/proc_syncXEN.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/sync_rclone.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/fxs/ui_handlers.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/src/core/globals.rb b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/src/core/globals.rb similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/src/core/globals.rb rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/src/core/globals.rb diff --git a/dtic-BKPs/dtic-BKPs_v5.5.1/src/install.sh b/_hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/src/install.sh similarity index 100% rename from dtic-BKPs/dtic-BKPs_v5.5.1/src/install.sh rename to _hist/dtic-BKPs/_hist/dtic-BKPs_v5.5.1/src/install.sh diff --git a/dtic-BKPs/lib/config.rb b/_hist/dtic-BKPs/_hist/lib/config.rb similarity index 100% rename from dtic-BKPs/lib/config.rb rename to _hist/dtic-BKPs/_hist/lib/config.rb diff --git a/dtic-BKPs/lib/logger.rb b/_hist/dtic-BKPs/_hist/lib/logger.rb similarity index 100% rename from dtic-BKPs/lib/logger.rb rename to _hist/dtic-BKPs/_hist/lib/logger.rb diff --git a/dtic-BKPs/lib/menu.rb b/_hist/dtic-BKPs/_hist/lib/menu.rb similarity index 100% rename from dtic-BKPs/lib/menu.rb rename to _hist/dtic-BKPs/_hist/lib/menu.rb diff --git a/dtic-BKPs/lib/proc_proxmox.rb b/_hist/dtic-BKPs/_hist/lib/proc_proxmox.rb similarity index 100% rename from dtic-BKPs/lib/proc_proxmox.rb rename to _hist/dtic-BKPs/_hist/lib/proc_proxmox.rb diff --git a/dtic-BKPs/lib/proc_xen.rb b/_hist/dtic-BKPs/_hist/lib/proc_xen.rb similarity index 100% rename from dtic-BKPs/lib/proc_xen.rb rename to _hist/dtic-BKPs/_hist/lib/proc_xen.rb diff --git a/dtic-BKPs/lib/sync_rclone.rb b/_hist/dtic-BKPs/_hist/lib/sync_rclone.rb similarity index 100% rename from dtic-BKPs/lib/sync_rclone.rb rename to _hist/dtic-BKPs/_hist/lib/sync_rclone.rb diff --git a/adn/tools/bkps/bkps.rb b/adn/tools/bkps/bkps.rb new file mode 100644 index 00000000..59bc96a0 --- /dev/null +++ b/adn/tools/bkps/bkps.rb @@ -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 +# ========================================================== + +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 " + 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 [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 " + 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 [--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 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 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 diff --git a/adn/tools/bkps/bkps.yml b/adn/tools/bkps/bkps.yml new file mode 100644 index 00000000..0fe642dc --- /dev/null +++ b/adn/tools/bkps/bkps.yml @@ -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" } diff --git a/adn/tools/bkps/cron-bkps.sh b/adn/tools/bkps/cron-bkps.sh new file mode 100755 index 00000000..926f935f --- /dev/null +++ b/adn/tools/bkps/cron-bkps.sh @@ -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 [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 diff --git a/adn/tools/bkps/lib/config.rb b/adn/tools/bkps/lib/config.rb new file mode 100644 index 00000000..3ecacd1f --- /dev/null +++ b/adn/tools/bkps/lib/config.rb @@ -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 diff --git a/adn/tools/bkps/lib/menu.rb b/adn/tools/bkps/lib/menu.rb new file mode 100644 index 00000000..2b08b7fa --- /dev/null +++ b/adn/tools/bkps/lib/menu.rb @@ -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 diff --git a/adn/tools/bkps/lib/proc_proxmox.rb b/adn/tools/bkps/lib/proc_proxmox.rb new file mode 100644 index 00000000..502a11a0 --- /dev/null +++ b/adn/tools/bkps/lib/proc_proxmox.rb @@ -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 diff --git a/adn/tools/bkps/lib/proc_xen.rb b/adn/tools/bkps/lib/proc_xen.rb new file mode 100644 index 00000000..b5d1f5ba --- /dev/null +++ b/adn/tools/bkps/lib/proc_xen.rb @@ -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 diff --git a/adn/tools/bkps/lib/sync_rclone.rb b/adn/tools/bkps/lib/sync_rclone.rb new file mode 100644 index 00000000..0c16aa50 --- /dev/null +++ b/adn/tools/bkps/lib/sync_rclone.rb @@ -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 diff --git a/adn/tools/candados/.session b/adn/tools/candados/.session index f84c2983..f6cb61c2 100644 --- a/adn/tools/candados/.session +++ b/adn/tools/candados/.session @@ -1 +1 @@ -1774987932 \ No newline at end of file +1775509864 \ No newline at end of file diff --git a/adn/tools/cli/backup.rb b/adn/tools/cli/_hist/backup.rb similarity index 100% rename from adn/tools/cli/backup.rb rename to adn/tools/cli/_hist/backup.rb diff --git a/adn/tools/cli/backup.rb.backup b/adn/tools/cli/_hist/backup.rb.backup similarity index 100% rename from adn/tools/cli/backup.rb.backup rename to adn/tools/cli/_hist/backup.rb.backup diff --git a/adn/tools/cli/backup/configurador.rb b/adn/tools/cli/_hist/backup/configurador.rb similarity index 100% rename from adn/tools/cli/backup/configurador.rb rename to adn/tools/cli/_hist/backup/configurador.rb diff --git a/adn/tools/cli/copiloto.rb b/adn/tools/cli/copiloto.rb new file mode 100644 index 00000000..37dd10dd --- /dev/null +++ b/adn/tools/cli/copiloto.rb @@ -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 -- " + 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 -- 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 diff --git a/adn/tools/cli/dron.rb b/adn/tools/cli/dron.rb new file mode 100644 index 00000000..1f2fe1b1 --- /dev/null +++ b/adn/tools/cli/dron.rb @@ -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 -- +# ./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 -- " + 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 -- 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 diff --git a/adn/tools/cli/novato.rb b/adn/tools/cli/novato.rb new file mode 100644 index 00000000..5bd71c29 --- /dev/null +++ b/adn/tools/cli/novato.rb @@ -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 -- " + 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 -- 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 diff --git a/adn/tools/core/conciliador.rb b/adn/tools/core/conciliador.rb index e8121242..9c1f6eb9 100644 --- a/adn/tools/core/conciliador.rb +++ b/adn/tools/core/conciliador.rb @@ -3,7 +3,7 @@ require 'date' require 'time' require 'json' -require_relative '../cli/backup/configurador' +# require_relative '../cli/backup/configurador' # Archivado → _hist/ (no usado aquí) module ADN class Conciliador diff --git a/adn/tools/w-zombi/data/cmd.json b/adn/tools/w-zombi/data/cmd.json index 1d0635e4..7ed8577c 100644 --- a/adn/tools/w-zombi/data/cmd.json +++ b/adn/tools/w-zombi/data/cmd.json @@ -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" } \ No newline at end of file +{"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"} diff --git a/docs/ambito/dtic-ADN/P2604.01.01_Estandarizacion-Ayudas-ADN.md b/docs/ambito/dtic-ADN/P2604.01.01_Estandarizacion-Ayudas-ADN.md deleted file mode 100644 index d432e6c4..00000000 --- a/docs/ambito/dtic-ADN/P2604.01.01_Estandarizacion-Ayudas-ADN.md +++ /dev/null @@ -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/) \ No newline at end of file diff --git a/docs/ambito/dtic-ADN/P2604.02.01_Generacion-Proyectos-ADN.md b/docs/ambito/dtic-ADN/P2604.02.01_Generacion-Proyectos-ADN.md deleted file mode 100644 index b8d3ab6c..00000000 --- a/docs/ambito/dtic-ADN/P2604.02.01_Generacion-Proyectos-ADN.md +++ /dev/null @@ -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) diff --git a/docs/ambito/dtic-ADN/P2604.03.01_Plan-W-ZOMBI-v2.0.md b/docs/ambito/dtic-ADN/P2604.03.01_Plan-W-ZOMBI-v2.0.md deleted file mode 100644 index 0a3c7418..00000000 --- a/docs/ambito/dtic-ADN/P2604.03.01_Plan-W-ZOMBI-v2.0.md +++ /dev/null @@ -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) diff --git a/docs/ambito/dtic-ADN/P2604.04.01_Estandarizacion-Barras-Progreso.md b/docs/ambito/dtic-ADN/P2604.04.01_Estandarizacion-Barras-Progreso.md deleted file mode 100644 index ed8eed08..00000000 --- a/docs/ambito/dtic-ADN/P2604.04.01_Estandarizacion-Barras-Progreso.md +++ /dev/null @@ -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) diff --git a/docs/ambito/dtic-ADN/P2604.05.01_Estudio-Friccion-IA.md b/docs/ambito/dtic-ADN/P2604.05.01_Estudio-Friccion-IA.md deleted file mode 100644 index cb9a2967..00000000 --- a/docs/ambito/dtic-ADN/P2604.05.01_Estudio-Friccion-IA.md +++ /dev/null @@ -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. ---- diff --git a/docs/ambito/dtic-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md b/docs/ambito/dtic-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md deleted file mode 100644 index 013638df..00000000 --- a/docs/ambito/dtic-ADN/P2604.06_Sensor-de-Mejora-Continua-para-el-Ecosistema-ADN.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/docs/ambito/dtic-ADN/P2604.07_Plantillas-Escritura-Eventos.md b/docs/ambito/dtic-ADN/P2604.07_Plantillas-Escritura-Eventos.md deleted file mode 100644 index 1c6154a0..00000000 --- a/docs/ambito/dtic-ADN/P2604.07_Plantillas-Escritura-Eventos.md +++ /dev/null @@ -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 ` -- [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 diff --git a/docs/ambito/dtic-ADN/P2604.08_Ambito-Nodo.md b/docs/ambito/dtic-ADN/P2604.08_Ambito-Nodo.md deleted file mode 100644 index e57c46d4..00000000 --- a/docs/ambito/dtic-ADN/P2604.08_Ambito-Nodo.md +++ /dev/null @@ -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 --descripcion -./adn/tools/run ambitos info -./adn/tools/run ambitos nodos # 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 ` a `nodo:crear` -- [x] Listar nodos mostrando la columna/etiqueta de su ámbito actual - -#### 2.4 - Modificar `evento.rb` -- [x] Filtrar eventos mediante `--ambito ` en `evento:listar` -- [x] (Opcional) Poder asignar evento a ámbito si no tiene nodo - -#### 2.5 - Modificar `nodos.rb` -- [x] Subcomando `nodos listar --ambito ` -- [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 ` 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 ` 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 ` 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` diff --git a/docs/ambito/dtic-ADN/P2604_Mejoras-ADN.md b/docs/ambito/dtic-ADN/P2604_Mejoras-ADN.md deleted file mode 100644 index 82ca5d7f..00000000 --- a/docs/ambito/dtic-ADN/P2604_Mejoras-ADN.md +++ /dev/null @@ -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` diff --git a/docs/ambito/dtic-BKPs/A03.P001_Migracion_ADN.md b/docs/ambito/dtic-BKPs/A03.P001_Migracion_ADN.md new file mode 100644 index 00000000..0505da10 --- /dev/null +++ b/docs/ambito/dtic-BKPs/A03.P001_Migracion_ADN.md @@ -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 `. + +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 ` (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 ` — 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). diff --git a/docs/ambito/dtic-BKPs/A03.P002_Automatizacion_Backups.md b/docs/ambito/dtic-BKPs/A03.P002_Automatizacion_Backups.md new file mode 100644 index 00000000..ed50e6ab --- /dev/null +++ b/docs/ambito/dtic-BKPs/A03.P002_Automatizacion_Backups.md @@ -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 ` (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 ` (vzdump remoto) +- Monitoreo: `bkps estados` (estado Proxmox en tiempo real) +- Delegación: `dron lanzar --evento ID -- ` (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 -- `. + +### ✅ 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_.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 ` — 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 ` | Backup individual vzdump remoto | +| `bkps buscar ` | 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 -- ` | 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 +``` diff --git a/docs/ambito/dtic-BKPs/A03_dtic-BKPs.md b/docs/ambito/dtic-BKPs/A03_dtic-BKPs.md new file mode 100644 index 00000000..607bea0c --- /dev/null +++ b/docs/ambito/dtic-BKPs/A03_dtic-BKPs.md @@ -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 ` — 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 +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 # Backup remoto individual (vzdump) +./adn/tools/run bkps buscar # 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 | Función | +| :--- | :--- | +| `dron lanzar --evento ID/AUTO --nota "X" -- ` | 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` diff --git a/docs/ambito/dtic-DASUTEN/P2601.06.01_Red-SrvDasu.md b/docs/ambito/dtic-DASUTEN/A04.P001_Red-SrvDasu.md similarity index 97% rename from docs/ambito/dtic-DASUTEN/P2601.06.01_Red-SrvDasu.md rename to docs/ambito/dtic-DASUTEN/A04.P001_Red-SrvDasu.md index 3deecc81..b207bb7b 100644 --- a/docs/ambito/dtic-DASUTEN/P2601.06.01_Red-SrvDasu.md +++ b/docs/ambito/dtic-DASUTEN/A04.P001_Red-SrvDasu.md @@ -1,10 +1,13 @@ # Plan: Reconfiguración de Red srv-dasu (Mudanza Física) +> **Ámbito:** A04 — dtic-DASUTEN + +**Código:** A04.P001 **Fecha:** 11 de marzo de 2026 **Autor:** Sistema ADN **Versión:** 1.0 -**Ubicación:** `docs/ambito/dtic-DASUTEN/plan/P2601.06.01_Red-SrvDasu.md` **Estado:** ✅ COMPLETADO +**Dependencia:** Ninguna ## 📋 Resumen Ejecutivo diff --git a/docs/ambito/dtic-DASUTEN/A04.P002_Integracion-DASU-PC.md b/docs/ambito/dtic-DASUTEN/A04.P002_Integracion-DASU-PC.md new file mode 100644 index 00000000..961a5d1e --- /dev/null +++ b/docs/ambito/dtic-DASUTEN/A04.P002_Integracion-DASU-PC.md @@ -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). diff --git a/docs/ambito/dtic-DASUTEN/P2601.06.02_AD-Join.md b/docs/ambito/dtic-DASUTEN/A04.P003_AD-Join.md similarity index 98% rename from docs/ambito/dtic-DASUTEN/P2601.06.02_AD-Join.md rename to docs/ambito/dtic-DASUTEN/A04.P003_AD-Join.md index 932f331f..ca33435e 100644 --- a/docs/ambito/dtic-DASUTEN/P2601.06.02_AD-Join.md +++ b/docs/ambito/dtic-DASUTEN/A04.P003_AD-Join.md @@ -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) **Autor:** Sistema ADN **Versión:** 4.1 -**Estado:** ⏳ EN EJECUCIÓN (Fase 1 - Normalización de Nombres) -**Proyecto:** [P2601 DASUTEN](../P2601_dasuten.md) +**Estado:** ⏸️ PAUSADO (Enfoque cambiado a Workgroup — ver A04.P005) +**Dependencia:** A04.P001 (Red srv-dasu), A04.P002 (Integración DASU-PC) ## 📋 Resumen Ejecutivo diff --git a/docs/ambito/dtic-DASUTEN/P2601.08_Unificacion-Redes-ISP.md b/docs/ambito/dtic-DASUTEN/A04.P004_Unificacion-Redes-ISP.md similarity index 94% rename from docs/ambito/dtic-DASUTEN/P2601.08_Unificacion-Redes-ISP.md rename to docs/ambito/dtic-DASUTEN/A04.P004_Unificacion-Redes-ISP.md index 699fe3da..bbb7d691 100644 --- a/docs/ambito/dtic-DASUTEN/P2601.08_Unificacion-Redes-ISP.md +++ b/docs/ambito/dtic-DASUTEN/A04.P004_Unificacion-Redes-ISP.md @@ -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 diff --git a/docs/ambito/dtic-DASUTEN/P2601.09_DASUTEN-sin-DC.md b/docs/ambito/dtic-DASUTEN/A04.P005_DASUTEN-sin-DC.md similarity index 96% rename from docs/ambito/dtic-DASUTEN/P2601.09_DASUTEN-sin-DC.md rename to docs/ambito/dtic-DASUTEN/A04.P005_DASUTEN-sin-DC.md index 50862842..9caba07a 100644 --- a/docs/ambito/dtic-DASUTEN/P2601.09_DASUTEN-sin-DC.md +++ b/docs/ambito/dtic-DASUTEN/A04.P005_DASUTEN-sin-DC.md @@ -1,12 +1,13 @@ # 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 **Autor:** Sistema ADN **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 @@ -141,7 +142,8 @@ Crear dos VMs nuevas (SQL Server + PC cliente) en `srv-dasu`, instalar el sistem ## 🔗 Referencias -- **Proyecto padre**: [P2601_dasuten.md](P2601_dasuten.md) -- **Plan previo de red**: [P2601.06.01_Red-SrvDasu.md](P2601.06.01_Red-SrvDasu.md) -- **Plan previo integración**: [P2601.07.01_Integracion-DASU-PC.md](P2601.07.01_Integracion-DASU-PC.md) +- **Ámbito**: [A04_dtic-DASUTEN.md](A04_dtic-DASUTEN.md) +- **Plan de red**: [A04.P001_Red-SrvDasu.md](A04.P001_Red-SrvDasu.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) +- **Legacy archivado**: [_hist_P2601_dasuten.md](_hist_P2601_dasuten.md) diff --git a/docs/ambito/dtic-DASUTEN/A04_dtic-DASUTEN.md b/docs/ambito/dtic-DASUTEN/A04_dtic-DASUTEN.md new file mode 100644 index 00000000..ce38a87c --- /dev/null +++ b/docs/ambito/dtic-DASUTEN/A04_dtic-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 ` — 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 +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 # 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 +``` + +## 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 | Función | +| :--- | :--- | +| `dron lanzar --evento ID/AUTO --nota "X" -- ` | 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: `. + +**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 diff --git a/docs/ambito/dtic-DASUTEN/P2601.07.01_Integracion-DASU-PC.md b/docs/ambito/dtic-DASUTEN/P2601.07.01_Integracion-DASU-PC.md deleted file mode 100644 index ee6da63d..00000000 --- a/docs/ambito/dtic-DASUTEN/P2601.07.01_Integracion-DASU-PC.md +++ /dev/null @@ -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 diff --git a/docs/ambito/dtic-DASUTEN/P2601_dasuten.md b/docs/ambito/dtic-DASUTEN/_hist/_hist_P2601_dasuten.md similarity index 100% rename from docs/ambito/dtic-DASUTEN/P2601_dasuten.md rename to docs/ambito/dtic-DASUTEN/_hist/_hist_P2601_dasuten.md diff --git a/nodos/pcv-dasu1.md b/nodos/pcv-dasu1.md index f85f5442..d1c475f4 100644 --- a/nodos/pcv-dasu1.md +++ b/nodos/pcv-dasu1.md @@ -20,3 +20,7 @@ ## 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`. + +## Credenciales +- **Usuario de Windows/Dominio**: `molinaromina` (Romina Molina) +- **Contraseña**: `$DASU123` diff --git a/nodos/srvv-data.md b/nodos/srvv-data.md index 38813335..b205e56d 100644 --- a/nodos/srvv-data.md +++ b/nodos/srvv-data.md @@ -6,7 +6,8 @@ - **Rol**: Base de Datos / Almacenamiento - **Estado**: 🟢 Online - **Tipo**: Contenedor LXC (Proxmox) -- **Padre/Host**: `srv-pmox3` (VM ID: 107) +- **VMID**: `107` +- **Padre/Host**: `srv-pmox3` - **OS**: Debian 12 (Bookworm) ## Hardware diff --git a/nodos/srvv-nginx-rm.md b/nodos/srvv-nginx-rm.md index ab9bfd1f..b31a073e 100644 --- a/nodos/srvv-nginx-rm.md +++ b/nodos/srvv-nginx-rm.md @@ -7,9 +7,10 @@ | **IP LAN** | `10.0.10.117` | | **DNS** | `rmonla.duckdns.org` | | **Sistema Operativo** | Debian 12 (Bookworm) | -| **Tipo** | Contenedor LXC (VMID 116, unprivileged) | +| **Tipo** | Contenedor LXC (unprivileged) | +| **VMID** | `116` | | **Rol** | Servidor Web NGINX | -| **Host** | `srv-pmox3` (10.0.10.203) | +| **Host** | `srv-pmox3` | | **SSH** | Puerto `7022`, usuario `root`, auth `password` | | **Bóveda** | `srvv-nginx-rm:root` |