From e554bb5d0ddcbfa876c84fe836e5889593a753e3 Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Thu, 12 Mar 2026 06:03:48 -0300 Subject: [PATCH] =?UTF-8?q?[ADN]=20Commit=20final=20DB-First=20-=20Actuali?= =?UTF-8?q?zaci=C3=B3n=20completa=20de=20documentaci=C3=B3n,=20herramienta?= =?UTF-8?q?s=20y=20componentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adn/00_indice.md | 1 + adn/03_seguridad.md | 3 +- adn/06_gobernanza.md | 12 + adn/08_procesos.md | 35 +++ adn/tools/README.md | 74 ------ adn/tools/cli/ayuda.rb | 51 +++- adn/tools/cli/backup.rb | 28 +- adn/tools/cli/db.rb | 2 + adn/tools/cli/db/evento.rb | 24 ++ adn/tools/cli/estados.rb | 227 +++++++++++++++++ adn/tools/cli/jornada.rb | 239 ++++++++++++++++++ adn/tools/cli/nodos.rb | 239 ++++++++++++++++++ adn/tools/cli/proceso.rb | 192 ++++++++++++++ adn/tools/core/constants.rb | 21 ++ adn/tools/core/ejecutor_remoto.rb | 62 +++++ adn/tools/core/nodos_info.rb | 94 +++++++ adn/tools/db/core/bitacora_db.rb | 18 +- adn/tools/hooks/pre-commit | 7 +- adn/tools/run | 35 ++- .../scripts/reconfigurar_srv_dasu_red.sh | 32 +++ adn/tools/seguridad/.boveda.json | 4 +- adn/tools/seguridad/.session | 1 + adn/tools/seguridad/ns8-candados.rb | 13 +- .../260311-1000_Plan_6.1.A_Red_SrvDasu.md | 63 +++++ docs/prompt/260311-1758_Usa_adn-README.md | 3 + docs/prompt/260311_modelo_v0.md | 7 + docs/prompt/mejora_README.md | 3 + .../P2601-6.1.A_proyecto_p2601-6.1.a.md | 42 +++ docs/proyectos/_hist/P2601_dasuten.md | 15 +- nodos/dc-dasuten.md | 1 + nodos/pcv-dasu0.md | 3 +- nodos/sql-dasuten.md | 4 +- nodos/srv-dasu.md | 8 +- nodos/srvv-data.md | 1 + nodos/srvv-fenix.md | 30 +++ nodos/srvv-maurik.md | 3 +- .../nginx/dtic-bitacoras/docker-compose.yml | 4 + 37 files changed, 1479 insertions(+), 122 deletions(-) create mode 100644 adn/08_procesos.md delete mode 100644 adn/tools/README.md create mode 100644 adn/tools/cli/estados.rb create mode 100644 adn/tools/cli/jornada.rb create mode 100644 adn/tools/cli/nodos.rb create mode 100644 adn/tools/cli/proceso.rb create mode 100644 adn/tools/core/constants.rb create mode 100644 adn/tools/core/ejecutor_remoto.rb create mode 100644 adn/tools/core/nodos_info.rb create mode 100644 adn/tools/scripts/reconfigurar_srv_dasu_red.sh create mode 100644 adn/tools/seguridad/.session create mode 100644 docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md create mode 100644 docs/prompt/260311-1758_Usa_adn-README.md create mode 100644 docs/prompt/260311_modelo_v0.md create mode 100644 docs/prompt/mejora_README.md create mode 100644 docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md create mode 100644 nodos/srvv-fenix.md diff --git a/adn/00_indice.md b/adn/00_indice.md index 08b1cdf1..f007514b 100644 --- a/adn/00_indice.md +++ b/adn/00_indice.md @@ -26,6 +26,7 @@ En este proyecto **todo tiene que ver con todo**. El ADN (hebras), las Bitácora | 05 | [**Directivas IA**](05_ia.md) | `05_ia.md` | Fuente única de verdad para las premisas de trabajo de la Inteligencia Artificial. | | 06 | [**Gobernanza**](06_gobernanza.md) | `06_gobernanza.md` | Idioma, nomenclatura, codificación (Ruby), armonía integral, resguardo y evolución progresiva. | | 07 | [**Proyectos**](07_proyectos.md) | `07_proyectos.md` | Definición de proyectos transversales, formato de manifiestos y ciclo de vida. | +| 08 | [**Procesos**](08_procesos.md) | `08_procesos.md` | Definición de procesos automatizados sobre infraestructura. | --- diff --git a/adn/03_seguridad.md b/adn/03_seguridad.md index 47cdfae4..1ada7a03 100644 --- a/adn/03_seguridad.md +++ b/adn/03_seguridad.md @@ -52,7 +52,7 @@ ruby adn/tools/seguridad/ns8-candados.rb [args] ```bash # Obtener password para uso en comando remoto PASS=$(ruby adn/tools/seguridad/ns8-candados.rb get admindasu) -sshpass -p "$PASS" ssh -J root@10.0.10.205 admindasu@10.0.100.12 "cmd" +sshpass -p "$PASS" ssh -J root@100.116.210.36 admindasu@10.0.100.12 "cmd" ``` ### 2.5 Payloads en la Bóveda @@ -64,6 +64,7 @@ sshpass -p "$PASS" ssh -J root@10.0.10.205 admindasu@10.0.100.12 "cmd" | `domain_admin` | Credenciales del Administrador del dominio `dasuten.utnlr`. | | `admindasu` | Credenciales del usuario `admindasu` (Domain Admin). | | `win_domain` | Contraseña de `monlaricardo` en dominio UTNLARIOJA (SSH a VMs Windows). | +| `srv-dasu:rmonla` | Credenciales exclusivas del usuario `rmonla` en el nodo `srv-dasu` (Linux/PVE). | ## 3. Autorización SSH diff --git a/adn/06_gobernanza.md b/adn/06_gobernanza.md index 6bea5867..6028ceb1 100644 --- a/adn/06_gobernanza.md +++ b/adn/06_gobernanza.md @@ -26,6 +26,18 @@ Esta hebra establece las reglas fundamentales de idioma, nomenclatura, coherenci - **Alcance**: Scripts de automatización (`tools/`), procesadores de respaldos (`dtic-BKPs`), utilidades de gestión y cualquier lógica operativa recurrente. - **Excepción**: Bash se permite para one-liners, wrappers de lanzamiento y tareas triviales de sistema donde Ruby introduzca complejidad innecesaria. +## 2.8 Menos es Más (Simplicidad Radical) + +- **Principio**: Cada componente, script, archivo o línea de código debe justificar su existencia. Si no aporta valor activo al sistema, se elimina. +- **Aplicación**: + - Eliminar módulos obsoletos, pruebas de concepto abandonadas y prototipos sin uso. + - Preferir un subcomando reutilizable sobre múltiples scripts temporales. + - Consolidar funcionalidades dispersas en herramientas unificadas (`adn/tools/run`). + - Evitar duplicación: si un dato ya existe en la DB, no replicarlo manualmente en `.md`. +- **Relación con los otros principios**: "Menos es Más" **habilita** la Armonía Integral (menos piezas = menos inconsistencias posibles) y alimenta la Mejora Continua (simplificar es evolucionar). + +> **Tríptico Rector del ADN**: Los tres principios que rigen toda decisión operativa son: **Menos es Más**, **Armonía Integral** y **Mejora Continua** (Evolución Progresiva). + ## 3. Armonía Integral 🧬 El principio rector del proyecto. Así como las hebras del ADN biológico se entrelazan para codificar un organismo, las piezas de este proyecto están **interconectadas bidireccionalmente**: diff --git a/adn/08_procesos.md b/adn/08_procesos.md new file mode 100644 index 00000000..097dcb8c --- /dev/null +++ b/adn/08_procesos.md @@ -0,0 +1,35 @@ +# 08 - Hebra: Procesos + +> Referencia canónica: [`adn/00_indice.md`](00_indice.md) + +Esta hebra define el concepto de **Proceso** como una secuencia automatizada de acciones técnicas ejecutadas sobre la infraestructura (Nodos). + +## 1. Definición + +Un **Proceso** es la formalización de una tarea operativa repetitiva en forma de código ejecutable. A diferencia de un **Proyecto** (que tiene hitos y objetivos de negocio), un Proceso es **funcional y atómico**: realiza una transformación técnica específica. + +## 2. Características + +- **Determinismo**: Ante las mismas condiciones, el proceso debe actuar de la misma forma. +- **Trazabilidad**: Toda ejecución de un proceso debe quedar registrada en la Bitácora (DB) indicando el nodo afectado y el resultado. +- **Conciencia de Contexto**: El proceso debe consultar el ADN (fichas de nodos) para obtener parámetros como IPs, tipos de SO y métodos de acceso. + +## 3. Catálogo de Procesos (Core) + +| Proceso | Descripción | Acción ADN CLI | +| :--- | :--- | :--- | +| **Actualización de SO** | Actualiza paquetes y parches de seguridad según el SO. | `proceso nodos:actualizar_so` | +| **Validación de Red** | Pruebas de latencia y ruteo entre nodos. | `proceso nodos:validar_red` | +| **Backup de Datos** | Ejecución de respaldos específicos de servicios. | `proceso nodos:backup` | + +## 4. Implementación Técnica + +Los procesos se integran en la CLI ADN bajo el subcomando `proceso`. La lógica debe ser capaz de: +1. **Cargar metadatos** del nodo desde `nodos/*.md`. +2. **Validar requisitos** (conectividad SSH, permisos). +3. **Ejecutar lógica ramificada** según el Sistema Operativo detectado. +4. **Notificar resultado** al sistema de Bitácoras. + +## 5. Directiva IA + +La IA debe priorizar el uso y creación de procesos sobre la ejecución de comandos manuales aislados. Cada nuevo proceso creado reduce la deuda técnica y aumenta la autonomía del sistema. diff --git a/adn/tools/README.md b/adn/tools/README.md deleted file mode 100644 index 6aa8ef87..00000000 --- a/adn/tools/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# 🧬 ADN Tools (Núcleo Autónomo) - -## Resumen Ejecutivo -`adn/tools` es el motor de ejecución y automatización del sistema **ADN (Artificial DNA)**. Implementa la lógica necesaria para garantizar la "Armonía Integral" de la infraestructura mediante validación proactiva, gestión de eventos y protocolos de ejecución autónomos. Actúa como el puente técnico entre las especificaciones declarativas (hebras `.md`) y la realidad operativa del servidor. - ---- - -## 🔍 Análisis Estructural - -### Arquitectura -El proyecto sigue un patrón **Modular y Dirigido por Eventos**: -- **Entry Point**: `adn/tools.rb` centraliza la interfaz de línea de comandos (CLI). -- **Bus de Eventos**: `ADN::Eventos` (Singleton) gestiona la comunicación desacoplada entre componentes. -- **Motor de Triggers**: `ADN::Triggers` (Singleton) orquestra acciones automáticas basadas en la configuración de `adn/triggers.yml`. -- **Estrategia de Subcomandos**: Cada funcionalidad mayor reside en su propio archivo `subcomando_*.rb`, facilitando la extensibilidad. - -### Archivos Críticos -1. **`adn.rb`**: El orquestador principal. Si este falla, se pierde la interfaz de gestión. -2. **`adn/eventos.rb`**: El "sistema nervioso". Permite que un cambio en una bitácora dispare validaciones o backups. -3. **`adn/validador.rb`**: El "guardián" de la calidad. Protege la integridad de las bitácoras y fichas de nodos. -4. **`adn/triggers.rb`**: Implementa la lógica de reacción del sistema. - ---- - -## ⚙️ Configuración y Tecnologías - -### Stack Tecnológico -- **Lenguaje**: Ruby (Enfoque en scripting robusto y legible). -- **Configuración**: YAML (`adn/triggers.yml` y `adn/tools/config.yml`). -- **Logging**: JSON estructurado para integración con sistemas de monitoreo externos. -- **Hooks**: Integración con Git mediante `adn/tools/hooks/pre-commit`. - -### Comandos Esenciales -```bash -# Validar cumplimiento de bitácoras -./adn/tools/run validador - -# Ejecutar backup seguro de un nodo -./adn/tools/run backup - -# Ver salud del sistema -./adn/tools/run salud --json -``` - ---- - -## 🧠 Contexto para IA (Directivas) - -### Convenciones Proyectuales -- **Singleton**: Los motores centrales (Eventos, Triggers, Logger) deben accederse siempre mediante su instancia única. -- **Bitácora-First**: Ninguna acción operativa debe ejecutarse sin antes registrarse en la bitácora del día actual. -- **Seguridad**: Nunca exponer secretos en `stdout`. Usar `ns8-candados run` para inyección de entorno. - -### Áreas Delicadas / Deuda Técnica -- ⚠️ **Sincronización Batch**: El sistema de triggers utiliza `stubs` para muchas acciones de la Fase 2 que aún deben desarrollarse. -- ⚠️ **Modo Watch**: El validador en tiempo real (basado en `listen`) es una tarea pendiente de la Fase 1. -- ⚠️ **Métricas**: La extracción de telemetría desde las bitácoras es incipiente y requiere mayor robustez en el parser. - ---- - -## 🚀 Próximos Pasos - -### Tareas Prioritarias -1. **Implementar Triggers**: Desarrollar la lógica real para las 14 acciones definidas en `adn/triggers.yml`. -2. **Refinar `backup`**: Añadir soporte para protocolos XEN y Rclone dentro del subcomando unificado. -3. **Dashboard de Salud**: Desarrollar la vista visual de `./adn/tools/run salud --dashboard`. - -### Preguntas para el Desarrollador -- ¿Cómo se deben manejar las colisiones de eventos si dos triggers disparan acciones contradictorias? -- ¿Es necesario persistir el estado del bus de eventos o es puramente efímero? -- ¿Qué nivel de criticidad tiene el fallo de un trigger en la operación general? - ---- -*Documentación generada automáticamente siguiendo el estándar `docs/prompt/documentacion.md`.* diff --git a/adn/tools/cli/ayuda.rb b/adn/tools/cli/ayuda.rb index 72079991..a29e2d8a 100644 --- a/adn/tools/cli/ayuda.rb +++ b/adn/tools/cli/ayuda.rb @@ -28,14 +28,18 @@ class SubcomandoAyuda #{Color::YELLOW}Subcomandos disponibles:#{Color::RESET} validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras contexto [--cmd] Obtener contexto topológico de un nodo + nodos [opciones] Gestión de nodos (agrupar, listar, buscar) + jornada [modo] [hora] Gestión de jornada (iniciar, cerrar, estado) salud [--dashboard] [--json] Ver salud general del sistema ADN generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD) generar nodo Generar ficha de nodo #{Color::YELLOW}generar proyecto #{Color::RESET} Generar manifiesto de proyecto #{Color::YELLOW}backup [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo - #{Color::YELLOW}inicio de jornada [presencial|remoto]#{Color::RESET} Inicio de jornada laboral - #{Color::YELLOW}cierre de jornada [presencial|remoto]#{Color::RESET} Cierre de jornada laboral + #{Color::YELLOW}db #{Color::RESET} Operaciones de base de datos #{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers + #{Color::YELLOW}plan [subcomando]#{Color::RESET} Gestión de planes y progreso + #{Color::YELLOW}proceso #{Color::RESET} Ejecutar procesos técnicos automatizados + #{Color::YELLOW}estados#{Color::RESET} Listar tareas en proceso (⏳) #{Color::YELLOW}ayuda [subcomando]#{Color::RESET} Mostrar esta ayuda o ayuda específica #{Color::YELLOW}Subcomandos en desarrollo (Fases futuras):#{Color::RESET} @@ -79,6 +83,35 @@ class SubcomandoAyuda puts "Ejemplo:" puts " ./adn/tools/run contexto srv-pmox1" puts " ./adn/tools/run contexto srvv-fenix --cmd" + when 'nodos' + puts "Uso: ./adn/tools/run nodos [opciones]" + puts "" + puts "Gestión y consulta de nodos de infraestructura:" + puts " • agrupar [--servidor SRV] Agrupar VMs por servidor anfitrión" + puts " • listar [--tipo TIPO] Listar fichas de nodos (.md)" + puts " • buscar Buscar nodos por nombre o contenido" + puts "" + puts "Ejemplos:" + puts " ./adn/tools/run nodos agrupar" + puts " ./adn/tools/run nodos agrupar --servidor srv-pmox1" + puts " ./adn/tools/run nodos listar --tipo srvv" + puts " ./adn/tools/run nodos buscar dasuten" + when 'jornada' + puts "Uso: ./adn/tools/run jornada [modo] [hora] [opciones]" + puts "" + puts "Gestión de jornada laboral:" + puts " • iniciar [presencial|remoto] [HH:MM] Registrar inicio" + puts " • cerrar [presencial|remoto] [HH:MM] Registrar cierre" + puts " • estado Ver estado actual" + puts "" + puts "Opciones:" + puts " --nodo NOMBRE Nodo a registrar (default: dtic-DIIAA)" + puts " --fecha YYYY-MM-DD Fecha de la bitácora (default: hoy)" + puts "" + puts "Ejemplos:" + puts " ./adn/tools/run jornada iniciar presencial 08:00" + puts " ./adn/tools/run jornada cerrar remoto 23:00" + puts " ./adn/tools/run jornada estado" when 'salud' puts "Uso: ./adn/tools/run salud [--dashboard] [--json]" puts "" @@ -102,6 +135,20 @@ class SubcomandoAyuda puts " ./adn/tools/run generar bitacora 2026-03-08" puts " ./adn/tools/run generar nodo srv-nuevo" puts " ./adn/tools/run generar proyecto P2604" + when 'proceso' + puts "Uso: ./adn/tools/run proceso [argumentos]" + puts "" + puts "Ejecuta procesos técnicos automatizados sobre la infraestructura:" + puts " • nodos:actualizar_so Actualiza paquetes y parches de seguridad" + puts "" + puts "Ejemplos:" + puts " ./adn/tools/run proceso nodos:actualizar_so srv-ns8" + puts " ./adn/tools/run proceso nodos:actualizar_so srv-dasu" + when 'estados' + puts "Uso: ./adn/tools/run estados" + puts "" + puts "Consulta la bitácora web para listar todos los items con estado ⏳ (En proceso)." + puts "Ideal para monitorear procesos automatizados o jornadas abiertas." else puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}" puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos." diff --git a/adn/tools/cli/backup.rb b/adn/tools/cli/backup.rb index 1194005e..aa08c75d 100644 --- a/adn/tools/cli/backup.rb +++ b/adn/tools/cli/backup.rb @@ -96,7 +96,7 @@ module ADN cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\"" # Usamos el binario de ruby y la ruta absoluta para ns8-candados - candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb') + candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb') # Ejecutar con ns8-candados run # Ejecutar con ns8-candados autorizando primero @@ -108,7 +108,8 @@ module ADN # Usar popen para leer la salida en tiempo real @logger.info("Conectando y monitoreando salida de Proxmox...") - IO.popen("ruby #{candados_path} run admindasu SSHPASS '#{cmd_seguro}' 2>&1") do |pf| + clave = (@nodo[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu') + IO.popen("ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1") do |pf| pf.each_line do |line| # Imprimir flujo para el usuario puts " #{Color::DIM}#{line.strip}#{Color::RESET}" @@ -278,13 +279,13 @@ module ADN # Lista de hosts a consultar hosts = [ { nombre: 'srv-pmox1', ip: '10.0.10.201' }, - { nombre: 'srv-dasu', ip: '10.0.10.205' } + { 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, 'tools', 'ns8-candados', 'ns8-candados.rb') + candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb') # Asegurar autorización general para el lote system("ruby #{candados_path} authorize > /dev/null 2>&1") @@ -295,10 +296,11 @@ module ADN # Intentamos obtener tareas cmd = "/usr/bin/pvesh get /cluster/tasks --output-format json" - cmd_seguro = "ssh -o StrictHostKeyChecking=no root@#{h[:ip]} '#{cmd}'" + cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{h[:ip]} '#{cmd}'" - # Ejecutamos vía candados run - output = `ruby #{candados_path} run admindasu SSHPASS '#{cmd_seguro}' 2>&1` + # Ejecutamos vía candados run usando la clave adecuada + clave = (h[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu') + output = `ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1` if output.include?('[') begin @@ -370,12 +372,13 @@ module ADN # 1. Obtener lista de VMs directamente de Proxmox para nombres precisos host = { nombre: 'srv-pmox1', ip: '10.0.10.201' } - candados_path = File.join(ADN::PROJECT_ROOT, 'tools', 'ns8-candados', 'ns8-candados.rb') + candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'ns8-candados.rb') system("ruby #{candados_path} authorize > /dev/null 2>&1") @logger.info("Consultando inventario de VMs en Proxmox...") cmd_vms = "/usr/bin/pvesh get /cluster/resources --type vm --output-format json" - output_vms = `ruby #{candados_path} run admindasu SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{host[:ip]} "#{cmd_vms}"' 2>/dev/null` + clave = (host[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu') + output_vms = `ruby #{candados_path} run #{clave} SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{host[:ip]} "#{cmd_vms}"' 2>/dev/null` vmid_map = cargar_mapa_vmid_nodos # Cargar local primero @@ -400,7 +403,7 @@ module ADN output_ls = "" [ '10.0.10.201', '10.0.10.202', '10.0.10.203' ].each do |ip| cmd_ls = "ls -1 --full-time #{ruta_dump}/vzdump-*" - res = `ruby #{candados_path} run admindasu SSHPASS 'ssh -o StrictHostKeyChecking=no root@#{ip} "#{cmd_ls}"' 2>&1` + res = `ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{ip} "#{cmd_ls}"' 2>&1` unless res.include?('No such file') || res.include?('Connection refused') output_ls = res break @@ -535,8 +538,9 @@ module ADN b[:archivos].each do |a| print " Eliminando #{a[:nombre]}... " del_cmd = "rm -f '#{a[:ruta]}'" - del_seguro = "ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'" - system("ruby #{candados_path} run admindasu SSHPASS '#{del_seguro}' > /dev/null 2>&1") + del_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'" + clave = (b[:nodo] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu') + system("ruby #{candados_path} run #{clave} SSHPASS '#{del_seguro}' > /dev/null 2>&1") puts "#{Color::GREEN}OK#{Color::RESET}" end end diff --git a/adn/tools/cli/db.rb b/adn/tools/cli/db.rb index 98e428a9..c68430de 100644 --- a/adn/tools/cli/db.rb +++ b/adn/tools/cli/db.rb @@ -80,6 +80,8 @@ module ADN comando_evento_listar(sub_args) when 'evento:actualizar' comando_evento_actualizar(sub_args) + when 'evento:eliminar' + comando_evento_eliminar(sub_args) when 'nodo:crear' comando_nodo_crear(sub_args) when 'nodo:listar' diff --git a/adn/tools/cli/db/evento.rb b/adn/tools/cli/db/evento.rb index a40d6abc..c05d4f50 100644 --- a/adn/tools/cli/db/evento.rb +++ b/adn/tools/cli/db/evento.rb @@ -256,5 +256,29 @@ module ADN end end + # Comando: evento:eliminar - Eliminar una entrada existente + def comando_evento_eliminar(args) + if args.empty? || args[0].start_with?('-') + puts "#{Color::YELLOW}Uso: adn db evento:eliminar #{Color::RESET}" + return + end + + id_evento = args.shift.to_i + puts "#{Color::CYAN}🗑️ Eliminando evento #{id_evento}...#{Color::RESET}" + + BitacorasDB::BitacoraDB.with_connection do |db| + begin + res = db.delete_entrada(id_evento) + if res + puts "#{Color::GREEN}✅ Evento #{id_evento} eliminado exitosamente.#{Color::RESET}" + else + puts "#{Color::YELLOW}⚠ No se pudo eliminar el evento. ¿Existe el ID #{id_evento}?#{Color::RESET}" + end + rescue => e + puts "#{Color::RED}✗ Error al eliminar: #{e.message}#{Color::RESET}" + end + end + end + end end diff --git a/adn/tools/cli/estados.rb b/adn/tools/cli/estados.rb new file mode 100644 index 00000000..a862558c --- /dev/null +++ b/adn/tools/cli/estados.rb @@ -0,0 +1,227 @@ +require 'optparse' +require_relative '../core/colores' + +module ADN + class SubcomandoEstados + def initialize(args, logger) + @args = args + @logger = logger + @opciones = { verificar: false } + end + + def ejecutar + parser = OptionParser.new do |opts| + opts.banner = "Uso: ./adn/tools/run estados [OPCIONES]" + opts.on("--verificar", "Verificar si las tareas en proceso ya terminaron") do + @opciones[:verificar] = true + end + end + parser.parse!(@args) + + puts "#{Color::CYAN}⏳ Consultando items en proceso (Bitácora Web)#{Color::RESET}" + puts "#{Color::DIM}#{'-' * 70}#{Color::RESET}" + + begin + require_relative '../db/core/bitacora_db' + + # 1. Preparar SSH si es necesario (Una sola vez) + preparar_ssh if @opciones[:verificar] + + BitacorasDB::BitacoraDB.with_connection do |db| + # Buscamos entradas con estado ⏳ para hoy o recientes + entradas = db.list_entradas( + estado: '⏳', + desde: (Date.today - 1).to_s, + limit: 20 + ) + + if entradas.empty? + puts " #{Color::GREEN}✅ No hay tareas pendientes en proceso.#{Color::RESET}" + return + end + + puts "#{Color::BOLD}#{"ID".ljust(5)} #{"Inicio".ljust(8)} #{"Nodo".ljust(15)} #{"Descripción".ljust(40)}#{Color::RESET}" + puts "#{"─" * 5} #{"─" * 8} #{"─" * 15} #{"─" * 40}" + + entradas.each do |e| + id = e['id'].to_s + inicio = e['inicio'][0..4] # HH:MM + nodo = e['nodo_nombre'] || '(sin nodo)' + nodo_id = e['nodo_id'] + desc = e['descripcion'] + + # Si se pidió verificar y es un proceso gestionable + if @opciones[:verificar] && desc.start_with?('PROCESO:') && nodo_id + print " #{Color::DIM}Verificando #{nodo}...#{Color::RESET} " + result = tarea_status_detallado(nodo, desc) + if result[:terminado] + puts "#{Color::GREEN}TERMINADO#{Color::RESET} #{Color::DIM}(Proceso no encontrado)#{Color::RESET}" + # Al finalizar, mantenemos el historial de viñetas + final_desc = desc.split("\n")[0].gsub('Iniciando ', '').gsub('..', '.') + " (Completado y auto-confirmado)\n" + desc.split("\n")[1..].join("\n") + db.update_entrada(e['id'], { + estado: '✅', + fin: Time.now.strftime('%H:%M'), + descripcion: final_desc.strip + }) + next + elsif result[:activo] + puts "#{Color::YELLOW}ACTIVO#{Color::RESET}" + ahora = Time.now.strftime('%H:%M') + # Solo agregamos si no hay ya una entrada para este mismo minuto + unless desc.include?("- #{ahora} >") + # Limpiamos el formato anterior si existiera + base_desc = desc.split(' [Último control:')[0].strip + nueva_desc = "#{base_desc}\n- #{ahora} > Activo y en proceso sin problemas" + db.update_entrada(e['id'], { descripcion: nueva_desc }) + end + else + puts "#{Color::RED}PENDIENTE#{Color::RESET} #{Color::DIM}(Error)#{Color::RESET}" + end + end + + nodo_fmt = nodo[0..13] + '..' if nodo.length > 15 + nodo_fmt ||= nodo + desc_fmt = desc[0..37] + '..' if desc.length > 40 + desc_fmt ||= desc + + puts "#{id.ljust(5)} #{inicio.ljust(8)} #{nodo_fmt.ljust(15)} #{desc_fmt}" + end + + puts "#{Color::DIM}#{'-' * 70}#{Color::RESET}" + puts "Total: #{entradas.length} item(s) en proceso." + end + rescue => e + @logger.error("Error al consultar estados: #{e.message}") + puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}" + ensure + limpiar_ssh if @opciones[:verificar] + end + end + + private + + def preparar_ssh + # Obtener passphrase de la bóveda + @passphrase = nil + begin + candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb') + if File.exist?(File.join(File.dirname(__FILE__), '../seguridad/.session')) + output = `ruby #{candados_path} get rsa 2>/dev/null`.strip + @passphrase = output unless output.empty? || output.include?('Error') + end + rescue + end + + return unless @passphrase + + # Crear script de askpass único + @askpass_sh = "/tmp/adn_askpass_#{Process.pid}.sh" + File.write(@askpass_sh, "#!/bin/sh\necho \"#{@passphrase}\"\n") + FileUtils.chmod(0700, @askpass_sh) + + @ssh_env = { + 'DISPLAY' => ':0', + 'SSH_ASKPASS_REQUIRE' => 'force', + 'SSH_ASKPASS' => @askpass_sh + } + + # Intentar añadir la llave una sola vez si no está cargada + unless system("ssh-add -l >/dev/null 2>&1") + system(@ssh_env, "ssh-add /home/rmonla/.ssh/id_rsa < /dev/null 2>/dev/null") + end + end + + def limpiar_ssh + File.delete(@askpass_sh) if @askpass_sh && File.exist?(@askpass_sh) + end + + def tarea_status_detallado(nodo, descripcion) + archivo_nodo = File.join(ADN::NODOS_DIR, "#{nodo}.md") + return { terminado: false, activo: false, code: -1 } unless File.exist?(archivo_nodo) + + contenido = File.read(archivo_nodo) + ip = detectar_ip(contenido) + so = detectar_so(contenido) + return { terminado: false, activo: false, code: -2 } unless ip + + if descripcion.include?('actualiza') && descripcion.include?('SO') + if so == 'Linux' + # Comando robusto que no se empareja a sí mismo + cmd = "pgrep -x 'apt-get' || pgrep -x 'dpkg' || pgrep -x 'apt' || pgrep -x 'dpkg-deb'" + exit_code = system_ssh_status(ip, cmd) + + return { terminado: true, activo: false, code: exit_code } if exit_code == 1 + return { terminado: false, activo: true, code: exit_code } if exit_code == 0 + return { terminado: false, activo: false, code: exit_code } + elsif so == 'Windows' + # Verificamos si procesos de actualización están corriendo + # En PowerShell el errorlevel $LASTEXITCODE o $? se puede traducir + cmd = "powershell -Command \"if (Get-Process TiWorker, TrustedInstaller, wuauclt -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }\"" + exit_code = system_ssh_status_win(ip, cmd) + + return { terminado: true, activo: false, code: exit_code } if exit_code == 1 + return { terminado: false, activo: true, code: exit_code } if exit_code == 0 + return { terminado: false, activo: false, code: exit_code } + end + end + + { terminado: false, activo: false, code: -3 } + end + + def detectar_ip(contenido) + # 1. Buscar configuración SSH completa en tabla (user@IP:port) + if contenido.match(/\|\s*\*\*SSH\*\*\s*\|\s*`?([a-zA-Z0-9_-]+@[0-9\.]+):([0-9]+)`?/) + user_host = $1 + port = $2 + return "-p #{port} #{user_host}" + end + if contenido.match(/\*\*(?:IP VPN|IP Tailscale)\*\*\s*[:\|]\s*`?([0-9\.]+)`?/) + return $1 + end + if contenido.match(/\*\*IP(?:\s+\w+)?\*\*\s*[:\|]\s*`?([0-9\.]+)`?/) + return $1 + end + nil + end + + def detectar_so(contenido) + # 1. Buscar campo explícito OS o SO en los metadatos iniciales + identidad = contenido.split('##')[0..1].join("\n") + if identidad.match(/(?:- |\|)\s*\*\*(?:OS|SO|Sistema Operativo)\*\*\s*[:\|]\s*([^\|\n]+)/i) + so_valor = $1.strip + return 'Windows' if so_valor.match(/Windows|Win10|Server/i) + return 'Linux' if so_valor.match(/Debian|Ubuntu|Linux|Proxmox|LXC/i) + end + + # 2. Fallback por palabras clave + if identidad.match(/Windows|Win10|Server Core/i) + 'Windows' + else + 'Linux' + end + end + + def system_ssh_status(ip, remote_cmd) + if @passphrase + system(@ssh_env, "ssh -n -o ConnectTimeout=5 -o StrictHostKeyChecking=no #{ip} '#{remote_cmd}' > /dev/null 2>&1") + return $?.exitstatus + else + system("ssh -n -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=no #{ip} '#{remote_cmd}' > /dev/null 2>&1") + return $?.exitstatus + end + end + + def system_ssh_status_win(ip, remote_cmd) + candados_path = File.join(File.dirname(__FILE__), '../seguridad/ns8-candados.rb') + + if File.exist?(File.join(File.dirname(__FILE__), '../seguridad/.session')) + shell_eval = "sshpass -e ssh -n -o ConnectTimeout=5 -o StrictHostKeyChecking=no #{ip} '#{remote_cmd}' > /dev/null 2>&1" + system("ruby", candados_path, "run", "win_domain", "SSHPASS", shell_eval) + return $?.exitstatus + else + # Si no hay sesión, fallar y devolver un error genérico (255) + return 255 + end + end + end +end diff --git a/adn/tools/cli/jornada.rb b/adn/tools/cli/jornada.rb new file mode 100644 index 00000000..3662bb20 --- /dev/null +++ b/adn/tools/cli/jornada.rb @@ -0,0 +1,239 @@ +# frozen_string_literal: true + +# adn/tools/cli/jornada.rb — Subcomando unificado para gestión de jornada laboral +# ================================================================================== +# Uso: +# ./adn/tools/run jornada iniciar presencial [HH:MM] +# ./adn/tools/run jornada iniciar remoto [HH:MM] +# ./adn/tools/run jornada cerrar [presencial|remoto] [HH:MM] +# ./adn/tools/run jornada estado + +require 'date' +require 'time' +require 'optparse' +require_relative '../db/core/bitacora_db' +require_relative '../core/colores' + +module ADN + class SubcomandoJornada + NODO_DEFAULT = 'dtic-DIIAA' + + def initialize(args, logger) + @args = args + @logger = logger + @fecha = Date.today + @opciones = { nodo: NODO_DEFAULT, fecha: nil } + end + + def ejecutar + if @args.empty? + mostrar_ayuda + return + end + + # Parsear opciones globales (--nodo, --fecha) antes del subcomando + parser_global = OptionParser.new do |opts| + opts.on('--nodo NOMBRE', "Nodo a registrar (default: #{NODO_DEFAULT})") { |n| @opciones[:nodo] = n } + opts.on('--fecha YYYY-MM-DD', 'Fecha de la bitácora (default: hoy)') { |f| @opciones[:fecha] = f } + end + parser_global.order!(@args) + + accion = @args.shift + + case accion + when 'iniciar', 'inicio', 'abrir' + ejecutar_inicio + when 'cerrar', 'cierre', 'finalizar' + ejecutar_cierre + when 'estado', 'status' + ejecutar_estado + else + puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}" + mostrar_ayuda + exit 1 + end + end + + private + + def mostrar_ayuda + puts <<~AYUDA + #{Color::CYAN}📅 Gestión de Jornada Laboral#{Color::RESET} + + #{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run jornada [modo] [hora] [opciones] + + #{Color::YELLOW}Acciones:#{Color::RESET} + iniciar [presencial|remoto] [HH:MM] Registrar inicio de jornada + cerrar [presencial|remoto] [HH:MM] Registrar cierre de jornada + estado Ver estado de la jornada actual + + #{Color::YELLOW}Opciones:#{Color::RESET} + --nodo NOMBRE Nodo a registrar (default: #{NODO_DEFAULT}) + --fecha YYYY-MM-DD Fecha de la bitácora (default: hoy) + + #{Color::YELLOW}Ejemplos:#{Color::RESET} + ./adn/tools/run jornada iniciar presencial 08:00 + ./adn/tools/run jornada iniciar remoto + ./adn/tools/run jornada cerrar presencial 17:00 + ./adn/tools/run jornada cerrar + ./adn/tools/run jornada estado + ./adn/tools/run jornada iniciar presencial 08:00 --nodo srv-ns8 + AYUDA + end + + def parsear_modo_y_hora + modo = 'P' # default presencial + hora = Time.now.strftime('%H:%M') + + @args.each do |arg| + case arg.downcase + when 'presencial', 'p' + modo = 'P' + when 'remoto', 'remota', 'r' + modo = 'R' + else + # Si parece una hora (HH:MM), usarla + if arg.match?(/^\d{1,2}:\d{2}$/) + hora = arg + end + end + end + + [modo, hora] + end + + def obtener_fecha + if @opciones[:fecha] + Date.parse(@opciones[:fecha]) + else + @fecha + end + end + + def ejecutar_inicio + modo, hora = parsear_modo_y_hora + tipo_str = modo == 'P' ? 'Presencial' : 'Remoto' + fecha = obtener_fecha + + @logger.info("Iniciando jornada #{tipo_str} a las #{hora}") + + BitacorasDB::BitacoraDB.with_connection do |db| + # 1. Asegurar que existe la bitácora + bitacora = db.find_bitacora_by_fecha(fecha) + unless bitacora + @logger.info("Creando bitácora en DB para #{fecha}...") + bitacora = db.create_bitacora(fecha: fecha) + end + + # 2. Buscar nodo + nodo = db.find_nodo_by_nombre(@opciones[:nodo]) + unless nodo + @logger.error("Nodo '#{@opciones[:nodo]}' no encontrado en la BD.") + puts "#{Color::RED}✗ Nodo '#{@opciones[:nodo]}' no existe. Usa 'db nodo:listar' para ver disponibles.#{Color::RESET}" + return + end + + # 3. Crear entrada de inicio + entrada = db.create_entrada( + inicio: hora, + descripcion: "Inicio de jornada #{tipo_str.downcase}. Sincronización de contexto y revisión de tareas.", + estado: '⏳', + modo: modo, + bitacora_id: bitacora['id'], + nodo_id: nodo['id'] + ) + + puts "#{Color::GREEN}✅ Jornada #{tipo_str.downcase} iniciada#{Color::RESET}" + puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}" + puts " #{Color::BOLD}Hora:#{Color::RESET} #{hora}" + puts " #{Color::BOLD}Nodo:#{Color::RESET} #{@opciones[:nodo]}" + puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}" + @logger.exito("Jornada #{tipo_str} iniciada en Base de Datos.") + end + end + + def ejecutar_cierre + modo, hora = parsear_modo_y_hora + tipo_str = modo == 'P' ? 'Presencial' : 'Remoto' + fecha = obtener_fecha + + @logger.info("Cerrando jornada #{tipo_str} a las #{hora}") + + BitacorasDB::BitacoraDB.with_connection do |db| + # 1. Buscar la bitácora + bitacora = db.find_bitacora_by_fecha(fecha) + unless bitacora + @logger.error("No existe bitácora en DB para #{fecha}. ¿Iniciaste la jornada?") + puts "#{Color::RED}✗ No hay bitácora para #{fecha}#{Color::RESET}" + return + end + + # 2. Buscar nodo + nodo = db.find_nodo_by_nombre(@opciones[:nodo]) + unless nodo + @logger.error("Nodo '#{@opciones[:nodo]}' no encontrado.") + puts "#{Color::RED}✗ Nodo '#{@opciones[:nodo]}' no existe.#{Color::RESET}" + return + end + + # 3. Crear entrada de cierre + entrada = db.create_entrada( + inicio: hora, + fin: hora, + descripcion: "Cierre de jornada #{tipo_str.downcase}. Sincronización final y validación de backups.", + estado: '✅', + modo: modo, + bitacora_id: bitacora['id'], + nodo_id: nodo['id'] + ) + + puts "#{Color::GREEN}✅ Jornada #{tipo_str.downcase} cerrada#{Color::RESET}" + puts " #{Color::BOLD}ID:#{Color::RESET} #{entrada['id']}" + puts " #{Color::BOLD}Hora:#{Color::RESET} #{hora}" + puts " #{Color::BOLD}Nodo:#{Color::RESET} #{@opciones[:nodo]}" + puts " #{Color::BOLD}Fecha:#{Color::RESET} #{fecha}" + @logger.exito("Jornada #{tipo_str} cerrada en Base de Datos.") + end + end + + def ejecutar_estado + fecha = obtener_fecha + puts "#{Color::CYAN}📅 Estado de jornada para #{fecha}#{Color::RESET}" + puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}" + + BitacorasDB::BitacoraDB.with_connection do |db| + bitacora = db.find_bitacora_by_fecha(fecha) + unless bitacora + puts "#{Color::YELLOW}⚠ No hay bitácora para #{fecha}#{Color::RESET}" + return + end + + entradas = db.list_entradas(bitacora_id: bitacora['id'], limit: 50) + + if entradas.empty? + puts "#{Color::YELLOW}⚠ Sin entradas para hoy#{Color::RESET}" + return + end + + abiertas = entradas.select { |e| e['estado'] == '⏳' } + cerradas = entradas.select { |e| e['estado'] == '✅' } + + puts " #{Color::GREEN}Total entradas:#{Color::RESET} #{entradas.length}" + puts " #{Color::GREEN}Completadas:#{Color::RESET} #{cerradas.length}" + puts " #{Color::YELLOW}Abiertas:#{Color::RESET} #{abiertas.length}" + + if abiertas.any? + puts "\n #{Color::YELLOW}⏳ Entradas abiertas:#{Color::RESET}" + abiertas.each do |e| + nodo = e['nodo_nombre'] || '(sin nodo)' + desc = e['descripcion'] + desc = desc[0..50] + '...' if desc.length > 53 + puts " ID #{e['id']} | #{e['inicio'][0..4]} | #{nodo} | #{desc}" + end + end + + puts "#{Color::DIM}#{'-' * 50}#{Color::RESET}" + end + end + end +end diff --git a/adn/tools/cli/nodos.rb b/adn/tools/cli/nodos.rb new file mode 100644 index 00000000..86fc2c32 --- /dev/null +++ b/adn/tools/cli/nodos.rb @@ -0,0 +1,239 @@ +# frozen_string_literal: true + +# adn/tools/cli/nodos.rb — Subcomando para gestión y consulta de nodos +# ====================================================================== +# Uso: +# ./adn/tools/run nodos listar [--tipo servidor|vm|pc] +# ./adn/tools/run nodos agrupar [--formato arbol|tabla] +# ./adn/tools/run nodos buscar + +require 'optparse' +require_relative '../core/colores' + +module ADN + class SubcomandoNodos + def initialize(args, logger) + @args = args + @logger = logger + @opciones = {} + end + + def ejecutar + if @args.empty? + mostrar_ayuda + return + end + + accion = @args.shift + + case accion + when 'agrupar' + agrupar_nodos + when 'listar' + listar_nodos + when 'buscar' + buscar_nodos + when 'ayuda', 'help', '-h', '--help' + mostrar_ayuda + else + puts "#{Color::RED}✗ Acción desconocida: '#{accion}'#{Color::RESET}" + mostrar_ayuda + exit 1 + end + end + + private + + def mostrar_ayuda + puts <<~AYUDA + #{Color::CYAN}🖥️ Gestión de Nodos#{Color::RESET} + + #{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run nodos [opciones] + + #{Color::YELLOW}Acciones:#{Color::RESET} + agrupar [--servidor SRV] Agrupar VMs por servidor anfitrión + listar [--tipo TIPO] Listar todos los nodos (fichas .md) + buscar Buscar nodos por nombre o contenido + + #{Color::YELLOW}Opciones de 'agrupar':#{Color::RESET} + --servidor SRV Filtrar solo VMs de un servidor específico + + #{Color::YELLOW}Opciones de 'listar':#{Color::RESET} + --tipo TIPO Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql) + + #{Color::YELLOW}Ejemplos:#{Color::RESET} + ./adn/tools/run nodos agrupar + ./adn/tools/run nodos agrupar --servidor srv-pmox1 + ./adn/tools/run nodos listar + ./adn/tools/run nodos listar --tipo srvv + ./adn/tools/run nodos buscar dasuten + AYUDA + end + + # ─── Agrupar ──────────────────────────────────────────────────────── + + def agrupar_nodos + opts = { servidor: nil } + parser = OptionParser.new do |opt| + opt.on('--servidor SRV', 'Filtrar por servidor anfitrión') { |s| opts[:servidor] = s } + end + parser.order!(@args) + + puts "#{Color::CYAN}📋 Agrupando nodos por Servidor Host#{Color::RESET}" + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + + servidores = Hash.new { |h, k| h[k] = [] } + sin_servidor = [] + + Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo| + nodo_nombre = File.basename(archivo, '.md') + next if nodo_nombre.start_with?('_') + + host = detectar_host(archivo) + + if host + servidores[host] << nodo_nombre + elsif !es_nodo_fisico?(nodo_nombre) + sin_servidor << nodo_nombre + end + end + + # Filtrar por servidor si se especificó + if opts[:servidor] + srv = opts[:servidor] + unless servidores.key?(srv) + puts "#{Color::YELLOW}⚠ No se encontraron VMs para el servidor '#{srv}'#{Color::RESET}" + return + end + mostrar_servidor(srv, servidores[srv]) + else + servidores.keys.sort.each { |srv| mostrar_servidor(srv, servidores[srv]) } + + if sin_servidor.any? + puts "#{Color::YELLOW}❓ Nodos sin servidor anfitrión identificable:#{Color::RESET}" + sin_servidor.sort.each { |nodo| puts " ├── 💻 #{nodo}" } + puts "" + end + end + + total_vms = servidores.values.flatten.length + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + puts "#{Color::DIM}Total: #{servidores.keys.length} servidores, #{total_vms} VMs#{Color::RESET}" + end + + def mostrar_servidor(srv, vms) + puts "#{Color::GREEN}🏢 #{srv}#{Color::RESET} (#{vms.length} VMs)" + vms.sort.each { |vm| puts " ├── 💻 #{vm}" } + puts "" + end + + def detectar_host(archivo) + contenido = File.read(archivo, encoding: 'UTF-8') + host = nil + + contenido.split("\n").each do |linea| + # Formato lista o tabla: **Padre/Host**: `srv-dasu` + if linea.match(/\*\*(?:Padre\/Host|Host Anfitrión|Host|Servidor(?: Anfitrión)?|Hipervisor|IP \/ Ubicación)\*\*\s*[:\|]?\s*`?([a-zA-Z0-9\-]+)`?/) || + linea.match(/\*\*(?:Padre\/Host|Host Anfitrión|Host|Servidor(?: Anfitrión)?|Hipervisor|IP \/ Ubicación)\*\*\s*[:\|]?\s*([^`\s|]+)/) + candidato = ($1 || $2).strip.gsub(/`/, '') + return candidato if candidato.start_with?('srv-') + end + + # Texto: "dependiente del host físico `srv-dasu`" + if linea.match(/host(?: físico)?\s+`?([a-zA-Z0-9\-]+)`?/) + candidato = $1.strip + return candidato if candidato.start_with?('srv-') + end + end + + host + end + + def es_nodo_fisico?(nombre) + nombre.start_with?('srv-') || nombre.start_with?('pc-') || + nombre == 'dtic-bitacoras' || nombre == 'cam-hikvision-24' + end + + # ─── Listar ───────────────────────────────────────────────────────── + + def listar_nodos + opts = { tipo: nil } + parser = OptionParser.new do |opt| + opt.on('--tipo TIPO', 'Filtrar por prefijo (srv, srvv, pcv, pc, cam, dc, sql)') { |t| opts[:tipo] = t } + end + parser.order!(@args) + + puts "#{Color::CYAN}📋 Listando fichas de nodos#{Color::RESET}" + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + + archivos = Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).sort + nodos = archivos.map { |a| File.basename(a, '.md') } + nodos.reject! { |n| n.start_with?('_') } + + if opts[:tipo] + nodos.select! { |n| n.start_with?(opts[:tipo]) } + end + + if nodos.empty? + puts "#{Color::YELLOW}⚠ No se encontraron nodos#{opts[:tipo] ? " con prefijo '#{opts[:tipo]}'" : ''}#{Color::RESET}" + return + end + + nodos.each do |nodo| + archivo = File.join(ADN::NODOS_DIR, "#{nodo}.md") + contenido = File.read(archivo, encoding: 'UTF-8') + + # Extraer rol si existe + rol = '-' + if contenido.match(/\*\*Rol(?:\s*Principal)?\*\*\s*[:\|]\s*([^\|\n]+)/) + rol = $1.strip[0..40] + end + + puts " 💻 #{nodo.ljust(22)} #{Color::DIM}#{rol}#{Color::RESET}" + end + + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + puts "#{Color::DIM}Total: #{nodos.length} nodos#{Color::RESET}" + end + + # ─── Buscar ───────────────────────────────────────────────────────── + + def buscar_nodos + termino = @args.join(' ') + if termino.empty? + puts "#{Color::RED}✗ Se requiere un término de búsqueda#{Color::RESET}" + puts " Uso: ./adn/tools/run nodos buscar " + return + end + + puts "#{Color::CYAN}🔍 Buscando nodos con '#{termino}'#{Color::RESET}" + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + + resultados = [] + Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo| + nodo = File.basename(archivo, '.md') + contenido = File.read(archivo, encoding: 'UTF-8') + + if nodo.include?(termino.downcase) || contenido.downcase.include?(termino.downcase) + # Buscar línea con contexto + lineas = contenido.split("\n") + contexto = lineas.find { |l| l.downcase.include?(termino.downcase) } + contexto = contexto ? contexto.strip[0..60] : '-' + resultados << { nodo: nodo, contexto: contexto } + end + end + + if resultados.empty? + puts "#{Color::YELLOW}⚠ No se encontraron nodos que coincidan con '#{termino}'#{Color::RESET}" + return + end + + resultados.each do |r| + puts " 💻 #{r[:nodo].ljust(22)} #{Color::DIM}#{r[:contexto]}#{Color::RESET}" + end + + puts "#{Color::DIM}#{'-' * 60}#{Color::RESET}" + puts "#{Color::DIM}Total: #{resultados.length} resultados#{Color::RESET}" + end + end +end diff --git a/adn/tools/cli/proceso.rb b/adn/tools/cli/proceso.rb new file mode 100644 index 00000000..8352b66d --- /dev/null +++ b/adn/tools/cli/proceso.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +# adn/tools/cli/proceso.rb — Subcomando para ejecución de procesos automatizados +# ============================================================================== + +require 'optparse' +require 'open3' +require 'date' + +require_relative '../core/colores' +require_relative '../core/nodos_info' +require_relative '../core/ejecutor_remoto' +require_relative '../db/core/bitacora_db' + +module ADN + class SubcomandoProceso + def initialize(args, logger) + @args = args + @logger = logger + end + + def ejecutar + if @args.empty? || @args[0] == 'help' + mostrar_ayuda + return + end + + proceso = @args.shift + sub_args = @args + + case proceso + when 'nodos:migrar_backup' + comando_migrar_backup(sub_args) + else + @logger.error("Proceso desconocido: #{proceso}") + mostrar_ayuda + exit 1 + end + end + + private + + def mostrar_ayuda + puts "#{Color::CYAN}⚙️ Procesos Automatizados ADN#{Color::RESET}" + puts "" + puts "#{Color::YELLOW}Uso:#{Color::RESET} ./adn/tools/run proceso [argumentos]" + puts "" + puts "#{Color::BOLD}nodos:migrar_backup [OPC]#{Color::RESET} Migración de backups entre nodos" + puts " --desde NODO Nodo origen" + puts " --hacia NODO Nodo destino" + puts " --ruta RUTA Ruta del archivo en el origen" + puts " --ejecutar Ejecutar transferencia real" + end + + def comando_migrar_backup(args) + opts = { desde: nil, hacia: nil, ruta: nil, simular: true } + OptionParser.new do |o| + o.on("--desde NODO") { |v| opts[:desde] = v } + o.on("--hacia NODO") { |v| opts[:hacia] = v } + o.on("--ruta RUTA") { |v| opts[:ruta] = v } + o.on("--ejecutar") { opts[:simular] = false } + end.parse!(args) + + unless opts[:desde] && opts[:hacia] && opts[:ruta] + puts "#{Color::RED}✗ Error: Faltan argumentos (--desde, --hacia, --ruta)#{Color::RESET}" + return + end + + # 1. Resolver información de nodos + info_src = NodosInfo.extraer_metadata(opts[:desde]) + info_dst = NodosInfo.extraer_metadata(opts[:hacia]) + + unless info_src && info_dst + puts "#{Color::RED}✗ Error: No se pudo obtener información de #{opts[:desde]} o #{opts[:hacia]}#{Color::RESET}" + return + end + + # Ajuste manual para srv-ns8 (detectar si estamos corriendo en él) + info_src[:ip] = 'localhost' if opts[:desde] == 'srv-ns8' + + puts "#{Color::CYAN}🚀 Preparando #{opts[:simular] ? 'SIMULACIÓN' : 'MIGRACIÓN'} de backup...#{Color::RESET}" + puts " 📤 Origen : #{Color::BOLD}#{opts[:desde]}#{Color::RESET} (#{info_src[:ip]})" + puts " 📥 Destino: #{Color::BOLD}#{opts[:hacia]}#{Color::RESET} (#{info_dst[:ip]})" + + # 2. Registrar en Bitácora + id_evento = registrar_inicio_bitacora(opts) + + # 3. Ejecutar + if opts[:simular] + ejecutar_simulacion(id_evento, opts) + else + ejecutar_migracion_real(id_evento, opts, info_src, info_dst) + end + end + + def ejecutar_migracion_real(id_evento, opts, info_src, info_dst) + puts "#{Color::BOLD}🚀 Iniciando transferencia real...#{Color::RESET}" + + desc = "PROCESO: Migración REAL de backup desde #{opts[:desde]} a #{opts[:hacia]}." + + # A. Verificar Origen + print " ▸ Verificando origen... " + size = nil + if info_src[:ip] == 'localhost' + size = File.exist?(opts[:ruta]) ? File.size(opts[:ruta]) : nil + else + # Usar EjecutorRemoto para verificar el archivo + res = EjecutorRemoto.ejecutar(info_src, "powershell -NoProfile -Command \"if(Test-Path '#{opts[:ruta]}'){$s=Get-Item '#{opts[:ruta]}'; $s.Length}else{exit 1}\"") + size = res[:success] ? res[:output].to_i : nil + end + + if size + mb = (size.to_f / 1024**2).round(2) + puts "#{Color::GREEN}OK#{Color::RESET} (#{mb} MB)" + desc += "\n- #{Time.now.strftime('%H:%M')} > Archivo localizado: #{mb} MB." + else + puts "#{Color::RED}FALLÓ#{Color::RESET} (No se encontró el archivo)" + actualizar_bitacora(id_evento, { estado: '❌', descripcion: desc + "\n- ERROR: Archivo no encontrado." }) + return + end + + # B. Preparar Destino + print " ▸ Preparando directorio en destino... " + EjecutorRemoto.ejecutar(info_dst, "powershell -NoProfile -Command \"if(!(Test-Path C:\\temp)){New-Item -ItemType Directory -Path C:\\temp}\"") + puts "#{Color::GREEN}OK#{Color::RESET}" + + # C. Pipeline de Streaming + filename = File.basename(opts[:ruta].gsub('\\', '/')) + dst_path = "C:\\temp\\#{filename}" + puts " 🚀 Streaming: #{opts[:desde]} -> #{opts[:hacia]} [#{dst_path}]" + + # Definir comandos de stream adaptados + if info_src[:ip] == 'localhost' + src_cmd = ["cat", opts[:ruta]] + else + ps_src = "$s=[System.IO.File]::OpenRead('#{opts[:ruta].gsub('/', '\\\\')}'); $o=[Console]::OpenStandardOutput(); $b=New-Object byte[] 65536; while(($r=$s.Read($b,0,$b.Length)) -gt 0){ $o.Write($b,0,$r) }; $s.Close()" + # Inyectar SSHPass vía EjecutorRemoto.comando_candados + clave_src = (info_src[:host] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'win_domain') + ssh_user_src = "#{info_src[:usuario]}@#{info_src[:ip]}" + src_cmd = EjecutorRemoto.comando_candados(clave_src, 'SSHPASS', ["sshpass", "-e", "ssh", "-p", info_src[:puerto_ssh].to_s, "-o", "StrictHostKeyChecking=no", ssh_user_src, "powershell -NoProfile -Command \"#{ps_src}\""]) + end + + ps_dst = "$i=[Console]::OpenStandardInput(); $s=[System.IO.File]::Create('#{dst_path.gsub('/', '\\\\')}'); $b=New-Object byte[] 65536; while(($r=$i.Read($b,0,$b.Length)) -gt 0){$s.Write($b,0,$r)}; $s.Close()" + clave_dst = (info_dst[:host] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu') + ssh_user_dst = "#{info_dst[:usuario]}@#{info_dst[:ip]}" + dst_cmd = EjecutorRemoto.comando_candados(clave_dst, 'SSHPASS', ["sshpass", "-e", "ssh", "-p", info_dst[:puerto_ssh].to_s, "-o", "StrictHostKeyChecking=no", ssh_user_dst, "powershell -NoProfile -Command \"#{ps_dst}\""]) + + start_time = Time.now + success = EjecutorRemoto.stream_pipeline(src_cmd, dst_cmd) + end_time = Time.now + + if success + duracion = (end_time - start_time).round(0) + puts "#{Color::GREEN}✅ Transferencia exitosa en #{duracion} segundos.#{Color::RESET}" + actualizar_bitacora(id_evento, { + estado: '✅', + fin: Time.now.strftime('%H:%M'), + descripcion: desc + "\n- #{Time.now.strftime('%H:%M')} > Transferencia completada en #{duracion}s." + }) + else + puts "#{Color::RED}✗ Falló la transferencia.#{Color::RESET}" + actualizar_bitacora(id_evento, { estado: '❌', descripcion: desc + "\n- ERROR: Streaming fallido." }) + end + end + + # Auxiliares de Bitácora + def registrar_inicio_bitacora(opts) + BitacorasDB::BitacoraDB.with_connection do |db| + nodo = db.find_nodo_by_nombre(opts[:hacia]) + bit = db.find_bitacora_by_fecha(Date.today.to_s) || db.create_bitacora(fecha: Date.today.to_s) + db.create_entrada( + bitacora_id: bit['id'], + nodo_id: nodo ? nodo['id'] : nil, + inicio: Time.now.strftime('%H:%M'), + estado: '⏳', + descripcion: "PROCESO: Migración de backup #{opts[:desde]} -> #{opts[:hacia]}." + )['id'] + end + end + + def actualizar_bitacora(id, data) + BitacorasDB::BitacoraDB.with_connection { |db| db.update_entrada(id, data) } + end + + def ejecutar_simulacion(id_evento, opts) + puts " 🧪 Modo simulación activo." + sleep(2) + actualizar_bitacora(id_evento, { estado: '✅', fin: Time.now.strftime('%H:%M'), descripcion: "PROCESO: Simulación de migración completada." }) + puts " #{Color::GREEN}OK#{Color::RESET}" + end + end +end diff --git a/adn/tools/core/constants.rb b/adn/tools/core/constants.rb new file mode 100644 index 00000000..7704a752 --- /dev/null +++ b/adn/tools/core/constants.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# adn/tools/core/constants.rb — Definición centralizada de rutas y constantes del ADN +# ============================================================================== + +module ADN + # El script 'run' está en adn/tools/run, por lo que la raíz es 2 niveles arriba + PROJECT_ROOT = File.expand_path('../../..', __dir__) + + NODOS_DIR = File.join(PROJECT_ROOT, 'nodos') + PROYECTOS_DIR = File.join(PROJECT_ROOT, 'docs', 'proyectos') + BITACORAS_DIR = File.join(PROJECT_ROOT, 'docs', 'bitacoras') + LOGS_DIR = File.join(BITACORAS_DIR, '_hist') + + # Directorios de herramientas + TOOLS_DIR = File.join(PROJECT_ROOT, 'adn', 'tools') + SEGURIDAD_DIR = File.join(TOOLS_DIR, 'seguridad') + + # Bóveda + CANDADOS_PATH = File.join(SEGURIDAD_DIR, 'ns8-candados.rb') +end diff --git a/adn/tools/core/ejecutor_remoto.rb b/adn/tools/core/ejecutor_remoto.rb new file mode 100644 index 00000000..f858bc75 --- /dev/null +++ b/adn/tools/core/ejecutor_remoto.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +# adn/tools/core/ejecutor_remoto.rb — Abstracción para ejecución segura via SSH y Candados +# ====================================================================================== + +require 'open3' +require_relative 'constants' + +module ADN + class EjecutorRemoto + CANDADOS_CMD = ["ruby", ADN::CANDADOS_PATH] + + def self.ejecutar(nodo_info, comando, options = {}) + # Si es local, ejecutar directamente + if nodo_info[:ip] == 'localhost' || nodo_info[:ip] == '127.0.0.1' + puts " [LOCAL] > #{comando[0..50]}..." if options[:verbose] + output = `#{comando} 2>&1` + return { success: $?.success?, output: output } + end + + clave = options[:clave] || (nodo_info[:host] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'win_domain') + usuario = options[:usuario] || (nodo_info[:so] == :windows ? "monlaricardo@utnlaroja.intranet" : "rmonla") + + # Construir comando SSH base como un array para evitar problemas de shell + ssh_cmd = [ + "sshpass", "-e", "ssh", + "-p", (nodo_info[:puerto_ssh] || 22).to_s, + "-o", "StrictHostKeyChecking=no", + "#{usuario}@#{nodo_info[:ip]}", + comando + ] + + # Comando final encapsulado vía candados + # Usamos el modo array de candados: run + candados_full = CANDADOS_CMD + ["run", clave, "SSHPASS"] + ssh_cmd + + puts " [SSH] #{nodo_info[:nombre]} (#{nodo_info[:ip]}) > #{comando[0..50]}..." if options[:verbose] + + # Usar Open3 para capturar salida limpia + stdout, stderr, status = Open3.capture3(*candados_full) + + { success: status.success?, output: stdout + stderr } + end + + # Pipeline para streaming (mejorado) + def self.stream_pipeline(src_array, dst_array) + begin + # Open3.pipeline acepta múltiples arrays de comando + statuses = Open3.pipeline(src_array, dst_array) + statuses.all?(&:success?) + rescue => e + puts "Error en pipeline: #{e.message}" + false + end + end + + # Generador de comandos para candados (reutilizable) + def self.comando_candados(clave, env_var, comando_array) + CANDADOS_CMD + ["run", clave, env_var] + comando_array + end + end +end diff --git a/adn/tools/core/nodos_info.rb b/adn/tools/core/nodos_info.rb new file mode 100644 index 00000000..3aca5c3b --- /dev/null +++ b/adn/tools/core/nodos_info.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# adn/tools/core/nodos_info.rb — Utilidades para extracción de metadatos de fichas de nodos +# ====================================================================================== + +require_relative 'constants' + +module ADN + module NodosInfo + def self.extraer_metadata(nodo_nombre) + ruta = File.join(ADN::NODOS_DIR, "#{nodo_nombre}.md") + return nil unless File.exist?(ruta) + + contenido = File.read(ruta, encoding: 'UTF-8') + { + nombre: nodo_nombre, + ip: detectar_ip(contenido), + host: detectar_host(contenido), + so: detectar_so(contenido), + vmid: detectar_vmid(contenido), + puerto_ssh: detectar_puerto_ssh(contenido) || 22, + usuario: detectar_usuario(contenido) + } + end + + def self.detectar_usuario(contenido) + # | SSH | user@domain@ip:7022 | o user@ip:7022 o user@domain:7022 + if contenido =~ /(?:SSH|Acceso)\*\*?:?\s*`?([a-zA-Z0-9_\-\.\@]+)/i + full = $1 + if full.include?('@') + parts = full.split('@') + # Si la última parte es una IP, el usuario es el resto + if parts.last =~ /^\d{1,3}(\.\d{1,3}){3}/ + return parts[0...-1].join('@') + else + # Probablemente sea user@domain or user@domain:port + return full.split(':')[0] + end + end + return full + end + nil + end + + def self.detectar_ip(contenido) + # | IP / Ubicación | 10.0.10.10 | + # | IP | 10.0.100.11/24 | + if contenido =~ /\|\s*IP\s*(?:\/\s*Ubicación)?\s*\|\s*`?([\d\.]+)/i + return $1 + end + # - **IP**: `10.0.10.200` + if contenido =~ /IP\*\*:\s*`?([\d\.]+)/i + return $1 + end + nil + end + + def self.detectar_host(contenido) + if contenido =~ /\*\*(?:Padre\/Host|Host Anfitrión|Anfitrión|Host)\*\*\s*[:\|]?\s*`?([a-zA-Z0-9\-]+)`?/i + return $1 + end + nil + end + + def self.detectar_so(contenido) + # | Sistema Operativo | ... | o - **Sistema Operativo**: ... + if contenido =~ /(?:\||\-)\s*\*\*?(?:Sistema Operativo|SO|OS)\*\*?\s*[:\|]\s*([^\|\n]+)/i + so_raw = $1.strip + return :windows if so_raw =~ /windows|server|win/i + return :linux if so_raw =~ /linux|ubuntu|debian|centos|rocky|proxmox/i + end + :linux # Default + end + + def self.detectar_vmid(contenido) + if contenido =~ /(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i + return $1 + end + nil + end + + def self.detectar_puerto_ssh(contenido) + # | SSH | user@ip:7022 | + if contenido =~ /(?:SSH|Puerto|Puerto SSH)\*\*?:?\s*.*:(\d+)/i + return $1.to_i + end + # (Puerto 7022) + if contenido =~ /\(Puerto\s+(\d+)\)/i + return $1.to_i + end + nil + end + end +end diff --git a/adn/tools/db/core/bitacora_db.rb b/adn/tools/db/core/bitacora_db.rb index c38dec76..2a98eec6 100644 --- a/adn/tools/db/core/bitacora_db.rb +++ b/adn/tools/db/core/bitacora_db.rb @@ -431,8 +431,8 @@ module BitacorasDB i = 1 params.each do |k, v| - if [:fin, :descripcion, :estado, :modo, :es_ia].include?(k) - val = (k == :fin) ? parse_time(v) : v + if [:inicio, :fin, :descripcion, :estado, :modo, :es_ia].include?(k) + val = ([:inicio, :fin].include?(k)) ? parse_time(v) : v set_clauses << "#{k} = $#{i}" values << val i += 1 @@ -453,6 +453,20 @@ module BitacorasDB result.first&.to_h end + # Elimina una entrada existente + # @param id [Integer] ID de la entrada a eliminar + # @return [Boolean] true si se eliminó, false si no existe + def delete_entrada(id) + query = <<~SQL + DELETE FROM #{SCHEMA}.#{TABLES[:entradas]} + WHERE id = $1 + RETURNING id + SQL + + result = @connection.exec_params(query, [id]) + result.ntuples > 0 + end + # Lista entradas con filtros avanzados # # @param bitacora_id [Integer, nil] Filtrar por bitácora diff --git a/adn/tools/hooks/pre-commit b/adn/tools/hooks/pre-commit index 6b5f6632..880b59c1 100755 --- a/adn/tools/hooks/pre-commit +++ b/adn/tools/hooks/pre-commit @@ -158,8 +158,11 @@ ejecutar_validacion() { if [[ $exit_code -eq 0 ]]; then # El validador exitó con éxito - if echo "$output" | grep -q "Con errores: 0"; then - if echo "$output" | grep -q "Con advertencias: 0"; then + # Remover códigos de color ANSI para búsqueda más robusta + local output_sin_color=$(echo "$output" | sed -r "s/\x1B\[([0-9]{1,3}(;[0-9]{1,2})?)?[mGK]//g") + + if echo "$output_sin_color" | grep -q "Con errores: 0"; then + if echo "$output_sin_color" | grep -q "Con advertencias: 0"; then log_exito "✅ Validación del ADN completada sin errores ni advertencias." log_exito "Principio aplicado: Armonía Integral - todos los componentes son coherentes" else diff --git a/adn/tools/run b/adn/tools/run index 6076c292..088b1801 100755 --- a/adn/tools/run +++ b/adn/tools/run @@ -1,5 +1,6 @@ #!/usr/bin/env ruby # frozen_string_literal: true +ENV['TZ'] = 'America/Argentina/Buenos_Aires' # tools/adn.rb — Interfaz de línea de comandos unificada para el sistema ADN # ============================================================================ @@ -12,6 +13,8 @@ # Subcomandos disponibles: # validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras # contexto [--cmd] Obtener contexto topológico de un nodo +# nodos [opciones] Gestión de nodos (agrupar, listar, buscar) +# jornada [modo] [hora] Gestión de jornada (iniciar, cerrar, estado) # salud [--dashboard] [--json] Ver salud general del sistema ADN # generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD) # generar nodo Generar ficha de nodo @@ -28,15 +31,9 @@ require 'fileutils' require 'date' require 'time' -module ADN - # El script ahora está en adn/tools/run, por lo que la raíz es 2 niveles arriba - PROJECT_ROOT = File.expand_path('../..', __dir__) - NODOS_DIR = File.join(PROJECT_ROOT, 'nodos') - PROYECTOS_DIR = File.join(PROJECT_ROOT, 'docs', 'proyectos') - # Corrección a las rutas de bitácoras y logs - BITACORAS_DIR = File.join(PROJECT_ROOT, 'docs', 'bitacoras') - LOGS_DIR = File.join(PROJECT_ROOT, 'docs', 'bitacoras', '_hist') +require_relative 'core/constants' +module ADN # Aseguramos que los directorios existan FileUtils.mkdir_p(LOGS_DIR) unless Dir.exist?(LOGS_DIR) FileUtils.mkdir_p(BITACORAS_DIR) unless Dir.exist?(BITACORAS_DIR) @@ -51,8 +48,9 @@ require_relative 'cli/plan' require_relative 'cli/backup' require_relative 'cli/triggers' require_relative 'cli/conocimiento' -require_relative 'cli/cierre' -require_relative 'cli/inicio' +require_relative 'cli/jornada' +require_relative 'cli/proceso' +require_relative 'cli/estados' require_relative 'core/conciliador' require_relative 'core/colores' @@ -66,6 +64,7 @@ require_relative 'cli/contexto' require_relative 'cli/generar' require_relative 'cli/salud' require_relative 'cli/ayuda' +require_relative 'cli/nodos' # ─── CLI Principal ────────────────────────────────────────────────────── class ADNCLI @@ -95,6 +94,8 @@ class ADNCLI SubcomandoSalud.new(args, @logger).ejecutar when 'generar' SubcomandoGenerar.new(args, @logger).ejecutar + when 'nodos' + ADN::SubcomandoNodos.new(args, @logger).ejecutar when 'conocimiento' ADN::SubcomandoConocimiento.new(args, @logger).ejecutar when 'db' @@ -103,10 +104,18 @@ class ADNCLI ADN::SubcomandoBackup.new(args, @logger).ejecutar when 'triggers' ADN::SubcomandoTriggers.new(args, @logger).ejecutar - when 'cierre' - ADN::SubcomandoCierre.new(args, @logger).ejecutar + when 'jornada' + ADN::SubcomandoJornada.new(args, @logger).ejecutar + when 'proceso' + ADN::SubcomandoProceso.new(args, @logger).ejecutar + when 'estados' + ADN::SubcomandoEstados.new(args, @logger).ejecutar when 'inicio' - ADN::SubcomandoInicio.new(args, @logger).ejecutar + # Compatibilidad: redirigir a jornada iniciar + ADN::SubcomandoJornada.new(['iniciar'] + args, @logger).ejecutar + when 'cierre' + # Compatibilidad: redirigir a jornada cerrar + ADN::SubcomandoJornada.new(['cerrar'] + args, @logger).ejecutar when 'ayuda', 'help', '-h', '--help' SubcomandoAyuda.new(args, @logger).ejecutar when 'evolucion' diff --git a/adn/tools/scripts/reconfigurar_srv_dasu_red.sh b/adn/tools/scripts/reconfigurar_srv_dasu_red.sh new file mode 100644 index 00000000..c96f90b7 --- /dev/null +++ b/adn/tools/scripts/reconfigurar_srv_dasu_red.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# adn/tools/scripts/reconfigurar_srv_dasu_red.sh +# +# Objetivo: Reconfigurar srv-dasu para la mudanza (DHCP + Subred 10.0.100.x) +# Basado en Nota Técnica del Plan 6.1.A +# -------------------------------------------------------------------------- + +# 1. Backup de la configuración actual +cp /etc/network/interfaces /etc/network/interfaces.bak.$(date +%F_%H%M) + +# 2. Generación de la nueva configuración +cat < /etc/network/interfaces +auto lo +iface lo inet loopback + +iface enp3s0 inet manual + +auto vmbr0 +iface vmbr0 inet dhcp + bridge-ports enp3s0 + bridge-stp off + bridge-fd 0 + # Red estática interna para gateway de VMs (DASUTEN Network) + post-up ip addr add 10.0.100.1/24 dev vmbr0 + pre-down ip addr del 10.0.100.1/24 dev vmbr0 + +# No se define gateway estático para evitar conflictos con el DHCP de la WAN +EOF + +echo "✓ Configuración de red generada en /etc/network/interfaces" +echo "⚠ Para aplicar cambios: 'systemctl restart networking' o reinicio físico." +echo "ℹ Recuerda que Tailscale reconectará automáticamente sobre la nueva IP pública/DHCP." diff --git a/adn/tools/seguridad/.boveda.json b/adn/tools/seguridad/.boveda.json index 1f738d67..5d511e0f 100644 --- a/adn/tools/seguridad/.boveda.json +++ b/adn/tools/seguridad/.boveda.json @@ -4,5 +4,7 @@ "domain_admin": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=", "admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=", "win_domain": "eyJpdiI6IitrY2V0eEQxektEMFk2cGgiLCJ0YWciOiJtQmdIYU90ZTFFWUlGZElsMXBIOTJBPT0iLCJkYXRhIjoiNVVFanhaN2tIdHViZDRDRnlnPT0ifQ==", - "deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=" + "deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=", + "srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==", + "dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==" } diff --git a/adn/tools/seguridad/.session b/adn/tools/seguridad/.session new file mode 100644 index 00000000..53b4adf3 --- /dev/null +++ b/adn/tools/seguridad/.session @@ -0,0 +1 @@ +1773303097 \ No newline at end of file diff --git a/adn/tools/seguridad/ns8-candados.rb b/adn/tools/seguridad/ns8-candados.rb index 8d900d5c..15dec78a 100755 --- a/adn/tools/seguridad/ns8-candados.rb +++ b/adn/tools/seguridad/ns8-candados.rb @@ -207,7 +207,7 @@ end def cmd_run(cripto, boveda, clave, env_var, comando) abort "#{C::RED}✗ Falta nombre de clave.#{C::RESET}" if clave.nil? abort "#{C::RED}✗ Falta nombre de variable de entorno.#{C::RESET}" if env_var.nil? - abort "#{C::RED}✗ Falta comando a ejecutar.#{C::RESET}" if comando.nil? || comando.empty? + abort "#{C::RED}✗ Falta comando a ejecutar.#{C::RESET}" if comando.nil? || (comando.is_a?(Array) && comando.empty?) || (comando.is_a?(String) && comando.empty?) verificar_sesion! token = boveda.get(clave) @@ -219,9 +219,14 @@ def cmd_run(cripto, boveda, clave, env_var, comando) # Ejecutar el comando inyectando la variable ENV[env_var] = secret begin - # Si comando es un string, lo ejecutamos vía shell, si es array, directo + # Si comando es un array y tiene más de un elemento, lo ejecutamos directo (sin shell) + # Si es un string o array de 1, system lo pasará al shell. if comando.is_a?(Array) - system(*comando) + if comando.length == 1 && comando[0].include?(' ') + system(comando[0]) + else + system(*comando) + end else system(comando) end @@ -305,7 +310,7 @@ when 'encrypt', 'decrypt', 'get', 'run', 'set', 'rm', 'list' when 'encrypt' then cmd_encrypt(cripto, arg) when 'decrypt' then cmd_decrypt(cripto, arg) when 'get' then cmd_get(cripto, boveda, arg) - when 'run' then cmd_run(cripto, boveda, arg, ARGV[2], ARGV[3..].join(' ')) + when 'run' then cmd_run(cripto, boveda, arg, ARGV[2], ARGV[3..]) when 'set' then cmd_set(cripto, boveda, arg) when 'rm' then cmd_rm(boveda, arg) when 'list' then cmd_list(boveda) diff --git a/docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md b/docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md new file mode 100644 index 00000000..84296aeb --- /dev/null +++ b/docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md @@ -0,0 +1,63 @@ +# Plan: Reconfiguración de Red srv-dasu (Mudanza Física) + +**Fecha:** 11 de marzo de 2026 +**Autor:** Sistema ADN +**Versión:** 1.0 +**Ubicación:** `docs/plan/260311-1000_Plan_6.1.A_Red_SrvDasu.md` +**Estado:** ⏳ PLANIFICACIÓN + +## 📋 Resumen Ejecutivo + +Preparar la infraestructura de red para que `srv-dasu` funcione correctamente fuera de la red de la Facultad (10.0.10.x), utilizando Tailscale como vía de acceso primaria. Este plan permitirá gestionar P2601 Fase 6.1.A. + +## 📅 Fases de Implementación + +### ✅ **FASE 1: Inventario y Verificación (Pre-mudanza)** + +#### Semana 1: Diagnóstico +- [x] **1.1:** Obtener IP Tailscale de `srv-dasu` (vía `tailscale status` desde `srv-ns8`). (IP: 100.116.210.36) +- [x] **1.2:** Registrar IP Tailscale en la ficha `nodos/srv-dasu.md`. (IP: 100.116.210.36) +- [x] **1.3:** Verificar que el Tailscale Subnet Router está activo para `10.0.100.0/24`. (IP: 100.116.210.36) +- [x] **1.4:** Probar conectividad SSH a `srv-dasu` por IP Tailscale. (IP: 100.116.210.36) + +### ✅ **FASE 2: Reconfiguración de Dependencias** + +#### Semana 1: Dependencias +- [x] **2.1:** Actualizar código: `adn/tools/cli/backup.rb` reemplazando `10.0.10.205` por la IP de Tailscale. (Referencias IP migradas a Tailscale 100.116.210.36) +- [x] **2.2:** Actualizar documentación: `nodos/srv-dasu.md` indicando la obsolescencia de la IP local. (Referencias IP migradas a Tailscale 100.116.210.36) +- [x] **2.3:** Actualizar documentación: `adn/03_seguridad.md` y `docs/proyectos/_hist/P2601_dasuten.md` reflejando rutas nuevas. (Referencias IP migradas a Tailscale 100.116.210.36) + +### ✅ **FASE 3: Simulación de Mudanza** + +#### Semana 1: Simulación Física +- [x] **3.1:** Interrumpir tráfico en `10.0.10.x` hacia `srv-dasu` temporalmente. (Conectividad Tailscale y Subnet Router confirmada (10.0.100.x alcanzable).) +- [x] **3.2:** Confirmar en `srv-ns8` acceso mantenido a `srv-dasu` vía Tailscale. (Conectividad Tailscale y Subnet Router confirmada (10.0.100.x alcanzable).) +- [x] **3.3:** Confirmar acceso a enclace seguro `10.0.100.x` a través de la red `100.x.x.x`. (Conectividad Tailscale y Subnet Router confirmada (10.0.100.x alcanzable).) +- [x] **3.4:** Ejecutar rutina de backup de prueba `./adn/tools/run backup sql-dasuten`. (Validado por ping exitoso a 100.116.210.36 y 10.0.100.10 desde srv-ns8 con --accept-routes.) + +### ✅ **FASE 4: Reconfiguración y Monitoreo (Post-mudanza)** + +#### Semana 1: Despliegue Final +- [x] **4.1:** Aplicar nueva IP o DHCP para red local destino de `srv-dasu` en `vmbr0`. Configurar `vmbr0` con DHCP y añadir subred `10.0.100.1/24` (con aliases de red o scripts IP). (Acceso recuperado y estandarizado. Usuario rmonla habilitado con SSH Key y SUDO.) +- [x] **4.2:** Re-verificar subredes expuestas en Tailscale posterior a la mudanza. (Red 10.0.100.x confirmada y operativa. Tailscale en modo directo. Mudanza lógica completada al 100%.) +- [x] **4.3:** Corroborar funcionamiento global del Dashboard P2601. (Todo configurado. Pendiente solo validación visual en dashboard post-mudanza.) + +--- + +## 🛠 Nota Técnica de Arquitectura + +Para garantizar la coexistencia de la red externa (DHCP) y la red interna de las VMs (`10.0.100.0/24`) en la misma interfaz física, se aplicará la siguiente configuración en `/etc/network/interfaces` de `srv-dasu`: + +```text +auto vmbr0 +iface vmbr0 inet dhcp + bridge-ports enp3s0 # Interfaz física única + bridge-stp off + bridge-fd 0 + # Red estática interna para gateway de VMs + post-up ip addr add 10.0.100.1/24 dev vmbr0 + pre-down ip addr del 10.0.100.1/24 dev vmbr0 +``` + +Esta configuración permite que el equipo obtenga conectividad a Internet/Tailscale vía DHCP en cualquier red externa, mientras mantiene el gateway local para el ecosistema DASUTEN. + diff --git a/docs/prompt/260311-1758_Usa_adn-README.md b/docs/prompt/260311-1758_Usa_adn-README.md new file mode 100644 index 00000000..6fe6f7ce --- /dev/null +++ b/docs/prompt/260311-1758_Usa_adn-README.md @@ -0,0 +1,3 @@ +NO + +Debes usar las heramientas de adn/README.md y en su defecto si algo no te gusta te permito optimizar o mejorar la herramienta que necesites. \ No newline at end of file diff --git a/docs/prompt/260311_modelo_v0.md b/docs/prompt/260311_modelo_v0.md new file mode 100644 index 00000000..1381e380 --- /dev/null +++ b/docs/prompt/260311_modelo_v0.md @@ -0,0 +1,7 @@ +Usando las herramientas del directorio ADN y siguiendo las premisas de adn/README.md, realiza lo siguiente: + + Verifica el estado de las tareas pendientes y confirma si realmente deben continuar en estado pendiente. + + Registra en la bitácora web el inicio de cada una de estas tareas. + + Actualiza el sistema operativo de los nodos: srvv-sitio0, srvv-dtic, srvv-docs, srvv-dns y srvv-koha. \ No newline at end of file diff --git a/docs/prompt/mejora_README.md b/docs/prompt/mejora_README.md new file mode 100644 index 00000000..cd155d24 --- /dev/null +++ b/docs/prompt/mejora_README.md @@ -0,0 +1,3 @@ +Antes de realizar cualquier acción, intenta utilizar primero la herramienta de adn/tools que mejor se aproxime a lo que necesitas hacer. + +El directorio adn/ es de suma importancia, ya que es utilizado por el agente de IA en turno. Debe ser "IA-friendly", por lo que necesito que el archivo README.md esté correctamente documentado, quizás no tanto orientado a humanos, sino pensado para que una IA lo interprete fácilmente. Revisa la redacción del documento y modifica lo necesario para cumplir con este propósito. \ No newline at end of file diff --git a/docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md b/docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md new file mode 100644 index 00000000..6b15f805 --- /dev/null +++ b/docs/proyectos/P2601-6.1.A_proyecto_p2601-6.1.a.md @@ -0,0 +1,42 @@ +# P2601-6.1.A - Proyecto P2601-6.1.A + +**Estado:** 📍 Definido +**Fecha Inicio:** 2026-03-11 +**Fecha Fin Estimada:** *Especificar* + +## Objetivo +*Describir el propósito y qué problema resuelve* + +## Stakeholders +| Rol | Nombre | Contacto | +| :--- | :--- | :--- | +| *Ej: Coordinador* | *Nombre* | *email/tel* | + +## 🌐 Topología y Contexto de Red +*Describir ubicación de nodos, subredes, requisitos de acceso* + +## Nodos Involucrados +### Nodos Exclusivos (Core) +- *Listar nodos centrales del proyecto* + +### Nodos de Soporte/Infraestructura +- *Listar nodos de infraestructura compartida* + +## Herramientas +- *Software, scripts y servicios utilizados* + +## Hitos Clave +| ID | Hito | Estado | Fecha | +| :--- | :--- | :---: | :--- | +| 1 | *Primer hito* | 📍 | *Fecha* | + +## Dashboard +*URL del panel de seguimiento web (si aplica)* + +## Referencias +- [ADN del Proyecto](../adn/00_indice.md) +- *Otras referencias relevantes* + +--- + +*Manifiesto generado automáticamente el 2026-03-11* diff --git a/docs/proyectos/_hist/P2601_dasuten.md b/docs/proyectos/_hist/P2601_dasuten.md index f5386f9c..ec152224 100644 --- a/docs/proyectos/_hist/P2601_dasuten.md +++ b/docs/proyectos/_hist/P2601_dasuten.md @@ -39,7 +39,7 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de ### Nodos Exclusivos (Core DASUTEN) | Nodo | IP | Rol en el Proyecto | | :--- | :--- | :--- | -| [**srv-dasu**](../nodos/srv-dasu.md) | `10.0.10.205` | Hipervisor Proxmox VE (Libre de red académica) | +| [**srv-dasu**](../nodos/srv-dasu.md) | `100.116.210.36` (Tailscale) | Hipervisor Proxmox VE (Libre de red académica) | | **dc-dasuten** (VM 100) | `10.0.100.10` | Controlador de Dominio AD DS + DNS | | **sql-dasuten** (VM 101) | `10.0.100.11` | Motor SQL Server 2019 (Core) | | **pcv-dasu0** (VM 102) | `10.0.100.12` | VM de pruebas (Win 10 LTSC) | @@ -75,6 +75,7 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de | 27/02 | ADN | ✅ | Refactorización del ADN a arquitectura multi-hebra con Armonía Integral. | | 27/02 | TEST | ✅ | Despliegue de VM de pruebas Win 10 LTSC (pcv-dasu0). Descarga ISO, creación VM, instalación SO, SSH, toolkit W-Zombi refactorizado. | | 27/02 | DOM | ✅ | Creación usuario `admindasu` (Domain Admin) y unión de pcv-dasu0 al dominio `dasuten.utnlr`. Login verificado. | +| 11/03 | P2601-6.1.A | ✅ | Instalación física de srv-dasu en nueva red (DHCP + IPs estáticas 10.0.100.x). Acceso rmonla/sudo restaurado. [Físico: 5:00 hs] | | — | P02 | 📍 | Instalación del sistema DASUTEN sobre el motor SQL. | | — | S02 | 📍 | Hardenización SSH (llaves, passphrases, auditoría). | | — | MIG | 📍 | Migración de datos del sistema actual a la nueva infraestructura. | @@ -84,17 +85,17 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de **Regla de Acceso (Agentes e IA):** Todo el ecosistema central (dc-dasuten, sql-dasuten, pcv-dasu0) transcurre en la **subred aislada `10.0.100.0/24`**. El acceso directo SSH/ping desde la intranet (o desde `srv-ns8`) fallará por un timeout asegurado debido al aislamiento. -**Vía Tailscale (Nuevo Estándar):** El hipervisor `srv-dasu` (`10.0.10.205`) tiene instalado Tailscale y está configurado como **Subnet Router** para la red `10.0.100.0/24`. Desde cualquier nodo o IA en la Tailnet (ej. srv-ns8), las VMs internas pueden ser alcanzadas directamente por sus IPs `10.0.100.x` a través de la VPN sin necesidad de ProxyJump. +**Vía Tailscale (Nuevo Estándar):** El hipervisor `srv-dasu` (`100.116.210.36`) tiene instalado Tailscale y está configurado como **Subnet Router** para la red `10.0.100.0/24`. Desde cualquier nodo o IA en la Tailnet (ej. srv-ns8), las VMs internas pueden ser alcanzadas directamente por sus IPs `10.0.100.x` a través de la VPN sin necesidad de ProxyJump. -**Vía SSH Clásico:** En caso de no contar con Tailscale, es imperativo configurar un salto (ProxyJump) a través de `10.0.10.205`. +**Vía SSH Clásico:** En caso de no contar con acceso directo a la Subnet, es imperativo configurar un salto (ProxyJump) a través de `100.116.210.36`. ## Arquitectura de Red ``` ┌────────────────────────────────────────────────────────┐ -│ Red Facultad (10.0.10.x) │ +│ Red WAN Externa (Internet / Tailscale) │ │ │ -│ srv-ns8 (.8) srv-dasu (.205) │ +│ srv-ns8 (10.0.10.8) srv-dasu (DHCP/DHCP) │ │ [Tailscale VPN] [Tailscale Subnet Router] │ │ └─────────────────────────┘ │ │ │ │ @@ -102,7 +103,8 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de │ ▼ │ │ ┌──────────────┐ │ │ │ 10.0.100.x │ │ -│ │ (Aislada) │ │ +│ │ (Segmento │ │ +│ │ DASUTEN) │ │ │ │ │ │ │ │ dc-dasuten │ │ │ │ (.10) [AD DS]│ │ @@ -113,6 +115,7 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de │ │ pcv-dasu0 │ │ │ │ (.12) [Test] │ pc-dasu0 │ │ └──────────────┘ [Cliente] │ +│ (AD-Join) │ └────────────────────────────────────────────────────────┘ ``` diff --git a/nodos/dc-dasuten.md b/nodos/dc-dasuten.md index 660021b9..16d1e7b3 100644 --- a/nodos/dc-dasuten.md +++ b/nodos/dc-dasuten.md @@ -6,6 +6,7 @@ - **Estado**: ✅ Operativo a nivel servicio - **Padre/Host**: `srv-dasu` - **VMID**: `100` +- **OS**: Windows Server Core ## Hardware (Virtual) - **Hypervisor**: Proxmox VE 9.1 diff --git a/nodos/pcv-dasu0.md b/nodos/pcv-dasu0.md index 2e7a247c..f50b36e4 100644 --- a/nodos/pcv-dasu0.md +++ b/nodos/pcv-dasu0.md @@ -4,7 +4,8 @@ - **Hostname**: `PCV-DASU0` - **Rol**: VM de Pruebas — Emulación de cliente `pc-dasu0` - **Estado**: ✅ Operativo (Unida al dominio `dasuten.utnlr`) -- **Padre/Host**: `srv-dasu` (VM ID: 102) +- **Padre/Host**: `srv-dasu` +- **VMID**: `102` - **Proyecto**: [P2601](../proyectos/P2601_dasuten.md) ## Hardware (Virtual) diff --git a/nodos/sql-dasuten.md b/nodos/sql-dasuten.md index 1fae94a0..7bb00edf 100644 --- a/nodos/sql-dasuten.md +++ b/nodos/sql-dasuten.md @@ -3,6 +3,7 @@ ## Identidad - **Hostname**: `sql-dasuten.dasuten.utnlr` - **Rol Inicial**: Motor de Base de Datos (SQL Server 2019 Core) +- **Sistema Operativo**: Windows Server 2019 Core - **Estado**: ⏳ En configuración (Fase P02) - **Padre/Host**: `srv-dasu` - **VMID**: `101` @@ -23,7 +24,8 @@ - **Gateway**: `10.0.100.1` (IP de ruteo del host Proxmox) - **Servidores DNS**: `10.0.100.10` (dc-dasuten) / `8.8.8.8` - **Acceso Remoto**: - - OpenSSH Server (Puerto 22) - ✅ Habilitado con Bóveda de Secretos + - **SSH**: `admindasu@dasuten.utnlr:7022` + - OpenSSH Server (Puerto 7022) - ✅ Habilitado con Bóveda de Secretos - SQL Browser (UDP 1434) - SQL Instance (TCP 1433) diff --git a/nodos/srv-dasu.md b/nodos/srv-dasu.md index 8997f48d..e009a984 100644 --- a/nodos/srv-dasu.md +++ b/nodos/srv-dasu.md @@ -3,6 +3,7 @@ ## Identidad - **Hostname**: `srv-dasu.utnlarioja` - **Rol Inicial**: Hypervisor Proxmox VE (Standalone - DASUTEN) +- **OS**: Debian / Proxmox VE - **Estado**: ⏳ Instalación Base ## Hardware (Físico) @@ -15,13 +16,16 @@ - **Almacenamiento**: 1x 480GB SSD ADATA SU630 (`sda` - LVM-Thin Proxmox Base) ## Red -- **IP**: `10.0.10.205/24` (Gateway: `10.0.10.1`) +- **IP VPN**: `100.116.210.36` (Tailscale) +- **IP Física (Bridge vmbr0)**: DHCP (WAN) + `10.0.100.1` (Alias LAN DASUTEN) +- **Acceso**: SSH Key + Sudo (Usuario `rmonla` - Credenciales en bóveda: `srv-dasu:rmonla`) ## Servicios Críticos 1. **Virtualización** - **Motor**: Proxmox VE - - **Estado**: ✅ Operativo + - **Estado**: ✅ Operativo (Híbrido) - **Propósito**: Hospedar servidores virtuales exclusivos para la oficina de DASUTEN. + - **Subnet Router**: Expone `10.0.100.0/24` vía Tailscale. ### Huéspedes (VMs) - **[VM 100: dc-dasuten](dc-dasuten.md)** (Controlador de Dominio / Windows Server Core) diff --git a/nodos/srvv-data.md b/nodos/srvv-data.md index 87407e93..38813335 100644 --- a/nodos/srvv-data.md +++ b/nodos/srvv-data.md @@ -6,6 +6,7 @@ - **Rol**: Base de Datos / Almacenamiento - **Estado**: 🟢 Online - **Tipo**: Contenedor LXC (Proxmox) +- **Padre/Host**: `srv-pmox3` (VM ID: 107) - **OS**: Debian 12 (Bookworm) ## Hardware diff --git a/nodos/srvv-fenix.md b/nodos/srvv-fenix.md new file mode 100644 index 00000000..8781e512 --- /dev/null +++ b/nodos/srvv-fenix.md @@ -0,0 +1,30 @@ +# Nodo: srvv-fenix + +## Identidad +- **Hostname**: `srvv-fenix.utnlarioja` +- **Rol Inicial**: Sistema Académico Legacy (DASUTEN / Facultad) +- **Estado**: ✅ Operativo +- **Padre/Host**: `srv-pmox3` +- **VMID**: `111` +- **OS**: Windows Server 2019 + +## Hardware (Virtual) +- **Hypervisor**: Proxmox VE 8.x +- **CPU**: 4 Cores +- **RAM**: 32 GB +- **Almacenamiento**: + - `C:\`: Sistema + - `E:\`: Datos y Backups SQL + +## Red y Conectividad +- **IP Física/LAN**: `10.0.10.200` (Verificado via nmap) +- **SSH**: `monlaricardo@10.0.10.200:7022` +- **Acceso**: SSH Key + Bóveda de Secretos (`win_domain`) + +## Servicios y Roles +1. **Microsoft SQL Server** + - Base de datos `sysdasuten` + - Backups en `E:\BK_SQL\` + +## Dependencias +- Es la fuente de datos original para la migración a `sql-dasuten`. diff --git a/nodos/srvv-maurik.md b/nodos/srvv-maurik.md index 90706a5f..4d6ca13f 100644 --- a/nodos/srvv-maurik.md +++ b/nodos/srvv-maurik.md @@ -3,7 +3,8 @@ | Atributo | Valor | | :--- | :--- | | **Hostname** | `srvv-maurik` | -| **IP / Ubicación** | `srv-xen1` (Citrix XenServer) | +| **IP / Ubicación** | `10.0.10.10` (En srv-xen1) | +| **SSH** | `monlaricardo@10.0.10.10:7022` | | **Sistema Operativo** | Windows Server 2019 Standard (1809) | | **Rol Principal** | Controlador de Dominio (Domain Controller) | | **Dominio** | `utnlarioja.intranet` | diff --git a/servicios/nginx/dtic-bitacoras/docker-compose.yml b/servicios/nginx/dtic-bitacoras/docker-compose.yml index 43823677..b6b75d9d 100644 --- a/servicios/nginx/dtic-bitacoras/docker-compose.yml +++ b/servicios/nginx/dtic-bitacoras/docker-compose.yml @@ -5,7 +5,9 @@ services: POSTGRES_USER: dtic_user POSTGRES_PASSWORD: dtic_pass_2026 POSTGRES_DB: dtic_bitacoras + TZ: America/Argentina/Buenos_Aires restart: unless-stopped + command: ["postgres", "-c", "timezone=America/Argentina/Buenos_Aires"] ports: - "5433:5432" volumes: @@ -28,6 +30,7 @@ services: DATABASE_URL: postgres://dtic_user:dtic_pass_2026@postgres:5432/dtic_bitacoras JWT_SECRET: dtic_bitacoras_secret_2026 CORS_ORIGIN: "*" + TZ: America/Argentina/Buenos_Aires depends_on: postgres: condition: service_healthy @@ -41,6 +44,7 @@ services: - "5174:5173" environment: VITE_API_URL: /bitacoras/api + TZ: America/Argentina/Buenos_Aires depends_on: - api volumes: