[ADN] Commit final DB-First - Actualización completa de documentación, herramientas y componentes
This commit is contained in:
@@ -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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -52,7 +52,7 @@ ruby adn/tools/seguridad/ns8-candados.rb <comando> [args]
|
|||||||
```bash
|
```bash
|
||||||
# Obtener password para uso en comando remoto
|
# Obtener password para uso en comando remoto
|
||||||
PASS=$(ruby adn/tools/seguridad/ns8-candados.rb get admindasu)
|
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
|
### 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`. |
|
| `domain_admin` | Credenciales del Administrador del dominio `dasuten.utnlr`. |
|
||||||
| `admindasu` | Credenciales del usuario `admindasu` (Domain Admin). |
|
| `admindasu` | Credenciales del usuario `admindasu` (Domain Admin). |
|
||||||
| `win_domain` | Contraseña de `monlaricardo` en dominio UTNLARIOJA (SSH a VMs Windows). |
|
| `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
|
## 3. Autorización SSH
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
- **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.
|
- **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 🧬
|
## 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**:
|
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**:
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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 <nombre-nodo>
|
|
||||||
|
|
||||||
# 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`.*
|
|
||||||
+49
-2
@@ -28,14 +28,18 @@ class SubcomandoAyuda
|
|||||||
#{Color::YELLOW}Subcomandos disponibles:#{Color::RESET}
|
#{Color::YELLOW}Subcomandos disponibles:#{Color::RESET}
|
||||||
validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
|
validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
|
||||||
contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
|
contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
|
||||||
|
nodos <acción> [opciones] Gestión de nodos (agrupar, listar, buscar)
|
||||||
|
jornada <acción> [modo] [hora] Gestión de jornada (iniciar, cerrar, estado)
|
||||||
salud [--dashboard] [--json] Ver salud general del sistema ADN
|
salud [--dashboard] [--json] Ver salud general del sistema ADN
|
||||||
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
|
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
|
||||||
generar nodo <nombre> Generar ficha de nodo
|
generar nodo <nombre> Generar ficha de nodo
|
||||||
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
|
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
|
||||||
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
|
#{Color::YELLOW}backup <nodo> [--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}db <subcomando>#{Color::RESET} Operaciones de base de datos
|
||||||
#{Color::YELLOW}cierre de jornada [presencial|remoto]#{Color::RESET} Cierre de jornada laboral
|
|
||||||
#{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers
|
#{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 <nombre>#{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}ayuda [subcomando]#{Color::RESET} Mostrar esta ayuda o ayuda específica
|
||||||
|
|
||||||
#{Color::YELLOW}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}
|
#{Color::YELLOW}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}
|
||||||
@@ -79,6 +83,35 @@ class SubcomandoAyuda
|
|||||||
puts "Ejemplo:"
|
puts "Ejemplo:"
|
||||||
puts " ./adn/tools/run contexto srv-pmox1"
|
puts " ./adn/tools/run contexto srv-pmox1"
|
||||||
puts " ./adn/tools/run contexto srvv-fenix --cmd"
|
puts " ./adn/tools/run contexto srvv-fenix --cmd"
|
||||||
|
when 'nodos'
|
||||||
|
puts "Uso: ./adn/tools/run nodos <acción> [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 <término> 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 <acción> [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'
|
when 'salud'
|
||||||
puts "Uso: ./adn/tools/run salud [--dashboard] [--json]"
|
puts "Uso: ./adn/tools/run salud [--dashboard] [--json]"
|
||||||
puts ""
|
puts ""
|
||||||
@@ -102,6 +135,20 @@ class SubcomandoAyuda
|
|||||||
puts " ./adn/tools/run generar bitacora 2026-03-08"
|
puts " ./adn/tools/run generar bitacora 2026-03-08"
|
||||||
puts " ./adn/tools/run generar nodo srv-nuevo"
|
puts " ./adn/tools/run generar nodo srv-nuevo"
|
||||||
puts " ./adn/tools/run generar proyecto P2604"
|
puts " ./adn/tools/run generar proyecto P2604"
|
||||||
|
when 'proceso'
|
||||||
|
puts "Uso: ./adn/tools/run proceso <nombre_proceso> [argumentos]"
|
||||||
|
puts ""
|
||||||
|
puts "Ejecuta procesos técnicos automatizados sobre la infraestructura:"
|
||||||
|
puts " • nodos:actualizar_so <nodo> 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
|
else
|
||||||
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
|
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
|
||||||
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
|
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
|
||||||
|
|||||||
+16
-12
@@ -96,7 +96,7 @@ module ADN
|
|||||||
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
|
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
|
# 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 run
|
||||||
# Ejecutar con ns8-candados autorizando primero
|
# Ejecutar con ns8-candados autorizando primero
|
||||||
@@ -108,7 +108,8 @@ module ADN
|
|||||||
# Usar popen para leer la salida en tiempo real
|
# Usar popen para leer la salida en tiempo real
|
||||||
@logger.info("Conectando y monitoreando salida de Proxmox...")
|
@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|
|
pf.each_line do |line|
|
||||||
# Imprimir flujo para el usuario
|
# Imprimir flujo para el usuario
|
||||||
puts " #{Color::DIM}#{line.strip}#{Color::RESET}"
|
puts " #{Color::DIM}#{line.strip}#{Color::RESET}"
|
||||||
@@ -278,13 +279,13 @@ module ADN
|
|||||||
# Lista de hosts a consultar
|
# Lista de hosts a consultar
|
||||||
hosts = [
|
hosts = [
|
||||||
{ nombre: 'srv-pmox1', ip: '10.0.10.201' },
|
{ 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 "\n#{Color::BOLD}🔍 Estado de Backups en Tiempo Real (Proxmox)#{Color::RESET}"
|
||||||
puts "─" * 60
|
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
|
# Asegurar autorización general para el lote
|
||||||
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||||
|
|
||||||
@@ -295,10 +296,11 @@ module ADN
|
|||||||
|
|
||||||
# Intentamos obtener tareas
|
# Intentamos obtener tareas
|
||||||
cmd = "/usr/bin/pvesh get /cluster/tasks --output-format json"
|
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
|
# Ejecutamos vía candados run usando la clave adecuada
|
||||||
output = `ruby #{candados_path} run admindasu SSHPASS '#{cmd_seguro}' 2>&1`
|
clave = (h[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||||
|
output = `ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1`
|
||||||
|
|
||||||
if output.include?('[')
|
if output.include?('[')
|
||||||
begin
|
begin
|
||||||
@@ -370,12 +372,13 @@ module ADN
|
|||||||
|
|
||||||
# 1. Obtener lista de VMs directamente de Proxmox para nombres precisos
|
# 1. Obtener lista de VMs directamente de Proxmox para nombres precisos
|
||||||
host = { nombre: 'srv-pmox1', ip: '10.0.10.201' }
|
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")
|
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||||
|
|
||||||
@logger.info("Consultando inventario de VMs en Proxmox...")
|
@logger.info("Consultando inventario de VMs en Proxmox...")
|
||||||
cmd_vms = "/usr/bin/pvesh get /cluster/resources --type vm --output-format json"
|
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
|
vmid_map = cargar_mapa_vmid_nodos # Cargar local primero
|
||||||
|
|
||||||
@@ -400,7 +403,7 @@ module ADN
|
|||||||
output_ls = ""
|
output_ls = ""
|
||||||
[ '10.0.10.201', '10.0.10.202', '10.0.10.203' ].each do |ip|
|
[ '10.0.10.201', '10.0.10.202', '10.0.10.203' ].each do |ip|
|
||||||
cmd_ls = "ls -1 --full-time #{ruta_dump}/vzdump-*"
|
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')
|
unless res.include?('No such file') || res.include?('Connection refused')
|
||||||
output_ls = res
|
output_ls = res
|
||||||
break
|
break
|
||||||
@@ -535,8 +538,9 @@ module ADN
|
|||||||
b[:archivos].each do |a|
|
b[:archivos].each do |a|
|
||||||
print " Eliminando #{a[:nombre]}... "
|
print " Eliminando #{a[:nombre]}... "
|
||||||
del_cmd = "rm -f '#{a[:ruta]}'"
|
del_cmd = "rm -f '#{a[:ruta]}'"
|
||||||
del_seguro = "ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'"
|
del_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'"
|
||||||
system("ruby #{candados_path} run admindasu SSHPASS '#{del_seguro}' > /dev/null 2>&1")
|
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}"
|
puts "#{Color::GREEN}OK#{Color::RESET}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ module ADN
|
|||||||
comando_evento_listar(sub_args)
|
comando_evento_listar(sub_args)
|
||||||
when 'evento:actualizar'
|
when 'evento:actualizar'
|
||||||
comando_evento_actualizar(sub_args)
|
comando_evento_actualizar(sub_args)
|
||||||
|
when 'evento:eliminar'
|
||||||
|
comando_evento_eliminar(sub_args)
|
||||||
when 'nodo:crear'
|
when 'nodo:crear'
|
||||||
comando_nodo_crear(sub_args)
|
comando_nodo_crear(sub_args)
|
||||||
when 'nodo:listar'
|
when 'nodo:listar'
|
||||||
|
|||||||
@@ -256,5 +256,29 @@ module ADN
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Comando: evento:eliminar <ID> - Eliminar una entrada existente
|
||||||
|
def comando_evento_eliminar(args)
|
||||||
|
if args.empty? || args[0].start_with?('-')
|
||||||
|
puts "#{Color::YELLOW}Uso: adn db evento:eliminar <ID>#{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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 <acción> [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
|
||||||
@@ -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 <término>
|
||||||
|
|
||||||
|
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 <acción> [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 <término> 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 <término>"
|
||||||
|
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
|
||||||
@@ -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 <nombre_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
|
||||||
@@ -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
|
||||||
@@ -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 <clave> <var> <cmd_array>
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -431,8 +431,8 @@ module BitacorasDB
|
|||||||
i = 1
|
i = 1
|
||||||
|
|
||||||
params.each do |k, v|
|
params.each do |k, v|
|
||||||
if [:fin, :descripcion, :estado, :modo, :es_ia].include?(k)
|
if [:inicio, :fin, :descripcion, :estado, :modo, :es_ia].include?(k)
|
||||||
val = (k == :fin) ? parse_time(v) : v
|
val = ([:inicio, :fin].include?(k)) ? parse_time(v) : v
|
||||||
set_clauses << "#{k} = $#{i}"
|
set_clauses << "#{k} = $#{i}"
|
||||||
values << val
|
values << val
|
||||||
i += 1
|
i += 1
|
||||||
@@ -453,6 +453,20 @@ module BitacorasDB
|
|||||||
result.first&.to_h
|
result.first&.to_h
|
||||||
end
|
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
|
# Lista entradas con filtros avanzados
|
||||||
#
|
#
|
||||||
# @param bitacora_id [Integer, nil] Filtrar por bitácora
|
# @param bitacora_id [Integer, nil] Filtrar por bitácora
|
||||||
|
|||||||
@@ -158,8 +158,11 @@ ejecutar_validacion() {
|
|||||||
|
|
||||||
if [[ $exit_code -eq 0 ]]; then
|
if [[ $exit_code -eq 0 ]]; then
|
||||||
# El validador exitó con éxito
|
# El validador exitó con éxito
|
||||||
if echo "$output" | grep -q "Con errores: 0"; then
|
# Remover códigos de color ANSI para búsqueda más robusta
|
||||||
if echo "$output" | grep -q "Con advertencias: 0"; then
|
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 "✅ Validación del ADN completada sin errores ni advertencias."
|
||||||
log_exito "Principio aplicado: Armonía Integral - todos los componentes son coherentes"
|
log_exito "Principio aplicado: Armonía Integral - todos los componentes son coherentes"
|
||||||
else
|
else
|
||||||
|
|||||||
+22
-13
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env ruby
|
#!/usr/bin/env ruby
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
ENV['TZ'] = 'America/Argentina/Buenos_Aires'
|
||||||
|
|
||||||
# tools/adn.rb — Interfaz de línea de comandos unificada para el sistema ADN
|
# tools/adn.rb — Interfaz de línea de comandos unificada para el sistema ADN
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
# Subcomandos disponibles:
|
# Subcomandos disponibles:
|
||||||
# validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
|
# validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
|
||||||
# contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
|
# contexto <nodo> [--cmd] Obtener contexto topológico de un nodo
|
||||||
|
# nodos <acción> [opciones] Gestión de nodos (agrupar, listar, buscar)
|
||||||
|
# jornada <acción> [modo] [hora] Gestión de jornada (iniciar, cerrar, estado)
|
||||||
# salud [--dashboard] [--json] Ver salud general del sistema ADN
|
# salud [--dashboard] [--json] Ver salud general del sistema ADN
|
||||||
# generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
|
# generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
|
||||||
# generar nodo <nombre> Generar ficha de nodo
|
# generar nodo <nombre> Generar ficha de nodo
|
||||||
@@ -28,15 +31,9 @@ require 'fileutils'
|
|||||||
require 'date'
|
require 'date'
|
||||||
require 'time'
|
require 'time'
|
||||||
|
|
||||||
module ADN
|
require_relative 'core/constants'
|
||||||
# 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')
|
|
||||||
|
|
||||||
|
module ADN
|
||||||
# Aseguramos que los directorios existan
|
# Aseguramos que los directorios existan
|
||||||
FileUtils.mkdir_p(LOGS_DIR) unless Dir.exist?(LOGS_DIR)
|
FileUtils.mkdir_p(LOGS_DIR) unless Dir.exist?(LOGS_DIR)
|
||||||
FileUtils.mkdir_p(BITACORAS_DIR) unless Dir.exist?(BITACORAS_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/backup'
|
||||||
require_relative 'cli/triggers'
|
require_relative 'cli/triggers'
|
||||||
require_relative 'cli/conocimiento'
|
require_relative 'cli/conocimiento'
|
||||||
require_relative 'cli/cierre'
|
require_relative 'cli/jornada'
|
||||||
require_relative 'cli/inicio'
|
require_relative 'cli/proceso'
|
||||||
|
require_relative 'cli/estados'
|
||||||
require_relative 'core/conciliador'
|
require_relative 'core/conciliador'
|
||||||
|
|
||||||
require_relative 'core/colores'
|
require_relative 'core/colores'
|
||||||
@@ -66,6 +64,7 @@ require_relative 'cli/contexto'
|
|||||||
require_relative 'cli/generar'
|
require_relative 'cli/generar'
|
||||||
require_relative 'cli/salud'
|
require_relative 'cli/salud'
|
||||||
require_relative 'cli/ayuda'
|
require_relative 'cli/ayuda'
|
||||||
|
require_relative 'cli/nodos'
|
||||||
|
|
||||||
# ─── CLI Principal ──────────────────────────────────────────────────────
|
# ─── CLI Principal ──────────────────────────────────────────────────────
|
||||||
class ADNCLI
|
class ADNCLI
|
||||||
@@ -95,6 +94,8 @@ class ADNCLI
|
|||||||
SubcomandoSalud.new(args, @logger).ejecutar
|
SubcomandoSalud.new(args, @logger).ejecutar
|
||||||
when 'generar'
|
when 'generar'
|
||||||
SubcomandoGenerar.new(args, @logger).ejecutar
|
SubcomandoGenerar.new(args, @logger).ejecutar
|
||||||
|
when 'nodos'
|
||||||
|
ADN::SubcomandoNodos.new(args, @logger).ejecutar
|
||||||
when 'conocimiento'
|
when 'conocimiento'
|
||||||
ADN::SubcomandoConocimiento.new(args, @logger).ejecutar
|
ADN::SubcomandoConocimiento.new(args, @logger).ejecutar
|
||||||
when 'db'
|
when 'db'
|
||||||
@@ -103,10 +104,18 @@ class ADNCLI
|
|||||||
ADN::SubcomandoBackup.new(args, @logger).ejecutar
|
ADN::SubcomandoBackup.new(args, @logger).ejecutar
|
||||||
when 'triggers'
|
when 'triggers'
|
||||||
ADN::SubcomandoTriggers.new(args, @logger).ejecutar
|
ADN::SubcomandoTriggers.new(args, @logger).ejecutar
|
||||||
when 'cierre'
|
when 'jornada'
|
||||||
ADN::SubcomandoCierre.new(args, @logger).ejecutar
|
ADN::SubcomandoJornada.new(args, @logger).ejecutar
|
||||||
|
when 'proceso'
|
||||||
|
ADN::SubcomandoProceso.new(args, @logger).ejecutar
|
||||||
|
when 'estados'
|
||||||
|
ADN::SubcomandoEstados.new(args, @logger).ejecutar
|
||||||
when 'inicio'
|
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'
|
when 'ayuda', 'help', '-h', '--help'
|
||||||
SubcomandoAyuda.new(args, @logger).ejecutar
|
SubcomandoAyuda.new(args, @logger).ejecutar
|
||||||
when 'evolucion'
|
when 'evolucion'
|
||||||
|
|||||||
@@ -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 <<EOF > /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."
|
||||||
@@ -4,5 +4,7 @@
|
|||||||
"domain_admin": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
"domain_admin": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
||||||
"admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
"admindasu": "eyJpdiI6IjlYV0JRSVA1MzNLblFycUEiLCJ0YWciOiJ3aHI4a1cwanpLWU5OUmVHby9pd3ZRPT0iLCJkYXRhIjoiT1B3VGNHNkpUSWI0SnJ5NW5adHNVZz09In0=",
|
||||||
"win_domain": "eyJpdiI6IitrY2V0eEQxektEMFk2cGgiLCJ0YWciOiJtQmdIYU90ZTFFWUlGZElsMXBIOTJBPT0iLCJkYXRhIjoiNVVFanhaN2tIdHViZDRDRnlnPT0ifQ==",
|
"win_domain": "eyJpdiI6IitrY2V0eEQxektEMFk2cGgiLCJ0YWciOiJtQmdIYU90ZTFFWUlGZElsMXBIOTJBPT0iLCJkYXRhIjoiNVVFanhaN2tIdHViZDRDRnlnPT0ifQ==",
|
||||||
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0="
|
"deepseek_key": "eyJpdiI6InRCc2IvL3J4SGNsejgvL04iLCJ0YWciOiJBbjRlMTNXNlZzUGhNSWowSUU1a01nPT0iLCJkYXRhIjoiQXR4azk1aU9sb2QydzNwa0FMTHpjVkJKQTIvZU4wY1U2V2crK3NBbVd1UnRmUGh6In0=",
|
||||||
|
"srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==",
|
||||||
|
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ=="
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
1773303097
|
||||||
@@ -207,7 +207,7 @@ end
|
|||||||
def cmd_run(cripto, boveda, clave, env_var, comando)
|
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 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 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!
|
verificar_sesion!
|
||||||
token = boveda.get(clave)
|
token = boveda.get(clave)
|
||||||
@@ -219,9 +219,14 @@ def cmd_run(cripto, boveda, clave, env_var, comando)
|
|||||||
# Ejecutar el comando inyectando la variable
|
# Ejecutar el comando inyectando la variable
|
||||||
ENV[env_var] = secret
|
ENV[env_var] = secret
|
||||||
begin
|
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)
|
if comando.is_a?(Array)
|
||||||
system(*comando)
|
if comando.length == 1 && comando[0].include?(' ')
|
||||||
|
system(comando[0])
|
||||||
|
else
|
||||||
|
system(*comando)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
system(comando)
|
system(comando)
|
||||||
end
|
end
|
||||||
@@ -305,7 +310,7 @@ when 'encrypt', 'decrypt', 'get', 'run', 'set', 'rm', 'list'
|
|||||||
when 'encrypt' then cmd_encrypt(cripto, arg)
|
when 'encrypt' then cmd_encrypt(cripto, arg)
|
||||||
when 'decrypt' then cmd_decrypt(cripto, arg)
|
when 'decrypt' then cmd_decrypt(cripto, arg)
|
||||||
when 'get' then cmd_get(cripto, boveda, 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 'set' then cmd_set(cripto, boveda, arg)
|
||||||
when 'rm' then cmd_rm(boveda, arg)
|
when 'rm' then cmd_rm(boveda, arg)
|
||||||
when 'list' then cmd_list(boveda)
|
when 'list' then cmd_list(boveda)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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*
|
||||||
@@ -39,7 +39,7 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de
|
|||||||
### Nodos Exclusivos (Core DASUTEN)
|
### Nodos Exclusivos (Core DASUTEN)
|
||||||
| Nodo | IP | Rol en el Proyecto |
|
| 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 |
|
| **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) |
|
| **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) |
|
| **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 | 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 | 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. |
|
| 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. |
|
| — | P02 | 📍 | Instalación del sistema DASUTEN sobre el motor SQL. |
|
||||||
| — | S02 | 📍 | Hardenización SSH (llaves, passphrases, auditoría). |
|
| — | S02 | 📍 | Hardenización SSH (llaves, passphrases, auditoría). |
|
||||||
| — | MIG | 📍 | Migración de datos del sistema actual a la nueva infraestructura. |
|
| — | 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.
|
**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
|
## 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] │
|
│ [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 │ │
|
│ │ 10.0.100.x │ │
|
||||||
│ │ (Aislada) │ │
|
│ │ (Segmento │ │
|
||||||
|
│ │ DASUTEN) │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ dc-dasuten │ │
|
│ │ dc-dasuten │ │
|
||||||
│ │ (.10) [AD DS]│ │
|
│ │ (.10) [AD DS]│ │
|
||||||
@@ -113,6 +115,7 @@ Actualmente, el sistema de gestión administrativa de DASUTeN funciona dentro de
|
|||||||
│ │ pcv-dasu0 │ │
|
│ │ pcv-dasu0 │ │
|
||||||
│ │ (.12) [Test] │ pc-dasu0 │
|
│ │ (.12) [Test] │ pc-dasu0 │
|
||||||
│ └──────────────┘ [Cliente] │
|
│ └──────────────┘ [Cliente] │
|
||||||
|
│ (AD-Join) │
|
||||||
└────────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- **Estado**: ✅ Operativo a nivel servicio
|
- **Estado**: ✅ Operativo a nivel servicio
|
||||||
- **Padre/Host**: `srv-dasu`
|
- **Padre/Host**: `srv-dasu`
|
||||||
- **VMID**: `100`
|
- **VMID**: `100`
|
||||||
|
- **OS**: Windows Server Core
|
||||||
|
|
||||||
## Hardware (Virtual)
|
## Hardware (Virtual)
|
||||||
- **Hypervisor**: Proxmox VE 9.1
|
- **Hypervisor**: Proxmox VE 9.1
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
- **Hostname**: `PCV-DASU0`
|
- **Hostname**: `PCV-DASU0`
|
||||||
- **Rol**: VM de Pruebas — Emulación de cliente `pc-dasu0`
|
- **Rol**: VM de Pruebas — Emulación de cliente `pc-dasu0`
|
||||||
- **Estado**: ✅ Operativo (Unida al dominio `dasuten.utnlr`)
|
- **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)
|
- **Proyecto**: [P2601](../proyectos/P2601_dasuten.md)
|
||||||
|
|
||||||
## Hardware (Virtual)
|
## Hardware (Virtual)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
## Identidad
|
## Identidad
|
||||||
- **Hostname**: `sql-dasuten.dasuten.utnlr`
|
- **Hostname**: `sql-dasuten.dasuten.utnlr`
|
||||||
- **Rol Inicial**: Motor de Base de Datos (SQL Server 2019 Core)
|
- **Rol Inicial**: Motor de Base de Datos (SQL Server 2019 Core)
|
||||||
|
- **Sistema Operativo**: Windows Server 2019 Core
|
||||||
- **Estado**: ⏳ En configuración (Fase P02)
|
- **Estado**: ⏳ En configuración (Fase P02)
|
||||||
- **Padre/Host**: `srv-dasu`
|
- **Padre/Host**: `srv-dasu`
|
||||||
- **VMID**: `101`
|
- **VMID**: `101`
|
||||||
@@ -23,7 +24,8 @@
|
|||||||
- **Gateway**: `10.0.100.1` (IP de ruteo del host Proxmox)
|
- **Gateway**: `10.0.100.1` (IP de ruteo del host Proxmox)
|
||||||
- **Servidores DNS**: `10.0.100.10` (dc-dasuten) / `8.8.8.8`
|
- **Servidores DNS**: `10.0.100.10` (dc-dasuten) / `8.8.8.8`
|
||||||
- **Acceso Remoto**:
|
- **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 Browser (UDP 1434)
|
||||||
- SQL Instance (TCP 1433)
|
- SQL Instance (TCP 1433)
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -3,6 +3,7 @@
|
|||||||
## Identidad
|
## Identidad
|
||||||
- **Hostname**: `srv-dasu.utnlarioja`
|
- **Hostname**: `srv-dasu.utnlarioja`
|
||||||
- **Rol Inicial**: Hypervisor Proxmox VE (Standalone - DASUTEN)
|
- **Rol Inicial**: Hypervisor Proxmox VE (Standalone - DASUTEN)
|
||||||
|
- **OS**: Debian / Proxmox VE
|
||||||
- **Estado**: ⏳ Instalación Base
|
- **Estado**: ⏳ Instalación Base
|
||||||
|
|
||||||
## Hardware (Físico)
|
## Hardware (Físico)
|
||||||
@@ -15,13 +16,16 @@
|
|||||||
- **Almacenamiento**: 1x 480GB SSD ADATA SU630 (`sda` - LVM-Thin Proxmox Base)
|
- **Almacenamiento**: 1x 480GB SSD ADATA SU630 (`sda` - LVM-Thin Proxmox Base)
|
||||||
|
|
||||||
## Red
|
## 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
|
## Servicios Críticos
|
||||||
1. **Virtualización**
|
1. **Virtualización**
|
||||||
- **Motor**: Proxmox VE
|
- **Motor**: Proxmox VE
|
||||||
- **Estado**: ✅ Operativo
|
- **Estado**: ✅ Operativo (Híbrido)
|
||||||
- **Propósito**: Hospedar servidores virtuales exclusivos para la oficina de DASUTEN.
|
- **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)
|
### Huéspedes (VMs)
|
||||||
- **[VM 100: dc-dasuten](dc-dasuten.md)** (Controlador de Dominio / Windows Server Core)
|
- **[VM 100: dc-dasuten](dc-dasuten.md)** (Controlador de Dominio / Windows Server Core)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
- **Rol**: Base de Datos / Almacenamiento
|
- **Rol**: Base de Datos / Almacenamiento
|
||||||
- **Estado**: 🟢 Online
|
- **Estado**: 🟢 Online
|
||||||
- **Tipo**: Contenedor LXC (Proxmox)
|
- **Tipo**: Contenedor LXC (Proxmox)
|
||||||
|
- **Padre/Host**: `srv-pmox3` (VM ID: 107)
|
||||||
- **OS**: Debian 12 (Bookworm)
|
- **OS**: Debian 12 (Bookworm)
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
| Atributo | Valor |
|
| Atributo | Valor |
|
||||||
| :--- | :--- |
|
| :--- | :--- |
|
||||||
| **Hostname** | `srvv-maurik` |
|
| **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) |
|
| **Sistema Operativo** | Windows Server 2019 Standard (1809) |
|
||||||
| **Rol Principal** | Controlador de Dominio (Domain Controller) |
|
| **Rol Principal** | Controlador de Dominio (Domain Controller) |
|
||||||
| **Dominio** | `utnlarioja.intranet` |
|
| **Dominio** | `utnlarioja.intranet` |
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ services:
|
|||||||
POSTGRES_USER: dtic_user
|
POSTGRES_USER: dtic_user
|
||||||
POSTGRES_PASSWORD: dtic_pass_2026
|
POSTGRES_PASSWORD: dtic_pass_2026
|
||||||
POSTGRES_DB: dtic_bitacoras
|
POSTGRES_DB: dtic_bitacoras
|
||||||
|
TZ: America/Argentina/Buenos_Aires
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: ["postgres", "-c", "timezone=America/Argentina/Buenos_Aires"]
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -28,6 +30,7 @@ services:
|
|||||||
DATABASE_URL: postgres://dtic_user:dtic_pass_2026@postgres:5432/dtic_bitacoras
|
DATABASE_URL: postgres://dtic_user:dtic_pass_2026@postgres:5432/dtic_bitacoras
|
||||||
JWT_SECRET: dtic_bitacoras_secret_2026
|
JWT_SECRET: dtic_bitacoras_secret_2026
|
||||||
CORS_ORIGIN: "*"
|
CORS_ORIGIN: "*"
|
||||||
|
TZ: America/Argentina/Buenos_Aires
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -41,6 +44,7 @@ services:
|
|||||||
- "5174:5173"
|
- "5174:5173"
|
||||||
environment:
|
environment:
|
||||||
VITE_API_URL: /bitacoras/api
|
VITE_API_URL: /bitacoras/api
|
||||||
|
TZ: America/Argentina/Buenos_Aires
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Reference in New Issue
Block a user