[P2604] Fase 15: Optimización IA e integración de herramienta candados. Sincronización de bitácoras y actualización de planes de proyecto.
This commit is contained in:
+1
-1
@@ -74,7 +74,7 @@ Facultad Regional La Rioja
|
||||
| Nodo | Ubicación | Descripción |
|
||||
| :--- | :--- | :--- |
|
||||
| **[cam-hikvision-24](../nodos/cam-hikvision-24.md)** | `10.0.10.24` | Cámara / DVR |
|
||||
| **[pc-dasu0](../nodos/pc-dasu0.md)** | Oficina DASUTEN | Win10 Físico (Ryzen 5) |
|
||||
| **[dasu-pc-0](../nodos/dasu-pc-0.md)** | Oficina DASUTEN | Win10 Físico (Ryzen 5) |
|
||||
| **[srv-dasu](../nodos/srv-dasu.md)** | Oficina DASUTEN | Proxmox VE Físico (Standalone) |
|
||||
|
||||
---
|
||||
|
||||
@@ -33,6 +33,9 @@ Esta hebra define el formato, estructura y reglas de sincronización de las bit
|
||||
- **E (Estado [Modo])**: Combinación de Modo (`[P]` Presencial / `[R]` Remoto) y Estado Semántico (`✅`, `⏳`, `👁️`, `⚠️`). Ejemplo: `[P] ✅`.
|
||||
- **Estados (Tema)**: Estrictamente delimitados para trazabilidad (✅ Cerrado, ⏳ En Proceso, ⚠️ Fallo).
|
||||
- **Semántica (General)**: Uso de una taxonomía visual rica para escaneo visual ultrarrápido (ver hebra [04_iconografia.md](04_iconografia.md)).
|
||||
- **Trazabilidad de Proyectos (Prefijos)**: Cuando una actividad forme parte de un plan formalizado en `docs/proy/`, la descripción del evento en la bitácora DEBE comenzar con el código del plan entre corchetes.
|
||||
- **Formato**: `[PROY.FASE.ITEM] Descripción de la actividad`
|
||||
- **Ejemplo**: `[P2601.07.01] Configuración de DNS en dasu-pc-0`
|
||||
- **Auto-Registro (IA)**: Cualquier acción técnica autónoma de la IA **DEBE** registrarse explícitamente en las tablas cronológicas indicando "(IA)" y el respectivo emoji semántico.
|
||||
|
||||
### 3.1 Ejemplo Estructural
|
||||
|
||||
@@ -35,3 +35,15 @@ Representan la **naturaleza técnica** de la acción dentro de las tablas cronol
|
||||
| 📝 | **Documentación** | Actualización del ADN, manuales, reportes, logs manuales. |
|
||||
| 🧬 | **ADN** | Refactorización o evolución de las hebras del ADN. |
|
||||
| 🛡️ | **Seguridad** | Hardening, auditoría de accesos, bóveda de secretos. |
|
||||
|
||||
## 3. Indicadores de Progreso (Manifiestos y Planes)
|
||||
|
||||
Utilizados para representar el porcentaje de completitud de fases en proyectos y planes detallados.
|
||||
|
||||
| Elemento | Carácter | Uso |
|
||||
| :--- | :---: | :--- |
|
||||
| Bloque Completo | `█` | Representa un 10% de avance. |
|
||||
| Bloque Pendiente | `░` | Representa un 10% de carencia. |
|
||||
|
||||
### Formato Estándar
|
||||
`Fase N: ██████░░░░ 60% Nombre de la Fase` (Longitud fija de 10 bloques).
|
||||
|
||||
@@ -22,6 +22,9 @@ Sincronización en cascada entre secciones de la bitácora. Ver hebra [`02_bitac
|
||||
### 5. Iconografía
|
||||
Título Principal = Estado (ej. ✅, ⏳, ➡️). Eventos cronológicos = Emojis semánticos al inicio de la celda de tiempo. Ver hebra [`04_iconografia.md`](04_iconografia.md).
|
||||
|
||||
### 5.1 Trazabilidad de Proyectos
|
||||
Toda entrada que responda a un plan de proyecto formalizado DEBE llevar el código identificador al inicio de la descripción: `[CODIGO] Detalle...`. Esto vincula inequívocamente el registro diario con la documentación técnica en `docs/proy/`.
|
||||
|
||||
### 6. Armonía
|
||||
Consultar y seguir siempre las normativas vivas del ADN. Ver hebra [`06_gobernanza.md`](06_gobernanza.md).
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Esta hebra establece las reglas fundamentales de idioma, nomenclatura, coherenci
|
||||
## 1. Idioma y Nomenclatura
|
||||
|
||||
- **Directiva**: **Todo el contenido debe estar en español.**
|
||||
- **Nomenclatura de Nodos**: Estrictamente **minúsculas** en nombres de archivo y referencias (ej: `srv-ns8`, `pc-dasu0`, `srvv-maurik`).
|
||||
- **Nomenclatura de Nodos**: Estrictamente **minúsculas** en nombres de archivo y referencias (ej: `srv-ns8`, `dasu-pc-0`, `srvv-maurik`).
|
||||
- **Alcance**:
|
||||
- Interacciones con el asistente (chat).
|
||||
- Documentación (archivos `.md`).
|
||||
|
||||
@@ -75,6 +75,7 @@ Cada proyecto debe tener un archivo manifiesto con las siguientes secciones:
|
||||
| **Nodos Involucrados** | Qué infraestructura participa (links a `nodos/`). Categorizar en **Core** y **Soporte**. |
|
||||
| **Herramientas** | Software, scripts y servicios utilizados. |
|
||||
| **Hitos Clave** | Timeline de alto nivel con estados y fechas. |
|
||||
| **Progreso** | Indicadores visuales mediante barras Unicode de completitud de fases. |
|
||||
| **Referencias** | Links a bitácoras, nodos, documentos técnicos. |
|
||||
|
||||
### Secciones Opcionales
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env ruby
|
||||
require_relative '../lib/wzombi/config'
|
||||
require_relative '../lib/wzombi/server'
|
||||
require_relative '../lib/wzombi/command_store'
|
||||
|
||||
base = File.expand_path("..", __dir__)
|
||||
config = WZombi::Config.new(base_path: base)
|
||||
|
||||
def help
|
||||
puts <<~HELP
|
||||
Uso:
|
||||
w-zombi start
|
||||
w-zombi cmd "<comando>"
|
||||
w-zombi ayuda | -h | -help
|
||||
|
||||
Ejemplo ADN:
|
||||
adn tools w-zombi cmd "ipconfig"
|
||||
HELP
|
||||
end
|
||||
|
||||
cmd = ARGV[0]
|
||||
|
||||
case cmd
|
||||
when "start"
|
||||
WZombi::Server.new(config).start
|
||||
|
||||
when "cmd"
|
||||
command = ARGV[1]
|
||||
if command.nil?
|
||||
puts "Falta comando"
|
||||
exit
|
||||
end
|
||||
store = WZombi::CommandStore.new(config.cmd_path)
|
||||
store.update(command)
|
||||
puts "Comando actualizado: #{command}"
|
||||
|
||||
when "ayuda", "-h", "-help", nil
|
||||
help
|
||||
|
||||
else
|
||||
puts "Comando desconocido"
|
||||
help
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"1773805876","cmd":"whoami","status":"pending"}
|
||||
@@ -0,0 +1 @@
|
||||
NO_CMD
|
||||
@@ -0,0 +1 @@
|
||||
{"id":0,"cmd":""}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
require 'json'
|
||||
|
||||
module WZombi
|
||||
class CommandStore
|
||||
def initialize(path)
|
||||
@path = path
|
||||
init_file
|
||||
end
|
||||
|
||||
def init_file
|
||||
File.write(@path, { id: 0, cmd: "" }.to_json) unless File.exist?(@path)
|
||||
end
|
||||
|
||||
def current
|
||||
JSON.parse(File.read(@path))
|
||||
end
|
||||
|
||||
def update(cmd)
|
||||
data = current
|
||||
data["id"] += 1
|
||||
data["cmd"] = cmd
|
||||
File.write(@path, data.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
module WZombi
|
||||
class Config
|
||||
attr_reader :puerto, :base_path
|
||||
|
||||
def initialize(base_path:)
|
||||
@base_path = base_path
|
||||
@puerto = ENV.fetch("WZ_PORT", 8000).to_i
|
||||
end
|
||||
|
||||
def log_path
|
||||
File.join(base_path, "data/telemetria.log")
|
||||
end
|
||||
|
||||
def cmd_path
|
||||
File.join(base_path, "data/cmd.json")
|
||||
end
|
||||
|
||||
def payloads_path
|
||||
File.join(base_path, "payloads")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
module WZombi
|
||||
module Engine
|
||||
class PS1Builder
|
||||
def self.build(host:)
|
||||
<<~PS1
|
||||
# W-ZOMBI ENGINE
|
||||
$WZ_HOST = "#{host}"
|
||||
$LAST_CMD_ID = 0
|
||||
|
||||
function Get-Cmd {
|
||||
try {
|
||||
return Invoke-RestMethod -Uri "http://$WZ_HOST/cmd"
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Send-Log {
|
||||
param([string]$msg)
|
||||
try {
|
||||
Invoke-RestMethod -Uri "http://$WZ_HOST/log" -Method Post -Body $msg -ContentType "text/plain"
|
||||
} catch {}
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
$cmdData = Get-Cmd
|
||||
if ($cmdData.id -ne $LAST_CMD_ID) {
|
||||
$LAST_CMD_ID = $cmdData.id
|
||||
$result = Invoke-Expression $cmdData.cmd 2>&1 | Out-String
|
||||
Send-Log $result
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
PS1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
module WZombi
|
||||
class Logger
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
line = "[#{Time.now.strftime('%H:%M:%S')}] #{msg}"
|
||||
File.open(@path, "a") { |f| f.puts(line) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
require 'json'
|
||||
|
||||
module WZombi
|
||||
module Routes
|
||||
class Cmd
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(_req, res)
|
||||
res['Content-Type'] = 'application/json'
|
||||
res.body = File.read(@config.cmd_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module WZombi
|
||||
module Routes
|
||||
class Log
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(req, res)
|
||||
File.open(@config.log_path, "a") { |f| f.puts(req.body) }
|
||||
res.body = "OK"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
require_relative '../engine/ps1_builder'
|
||||
|
||||
module WZombi
|
||||
module Routes
|
||||
class Zombi
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(_req, res)
|
||||
# Priorizamos host desde variable de entorno, sino usamos la IP Tailscale conocida
|
||||
host = ENV.fetch("WZ_HOST", "100.111.195.4:#{@config.puerto}")
|
||||
res.body = Engine::PS1Builder.build(host: host)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'webrick'
|
||||
require_relative 'routes/zombi'
|
||||
require_relative 'routes/cmd'
|
||||
require_relative 'routes/log'
|
||||
|
||||
module WZombi
|
||||
class Server
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def start
|
||||
server = WEBrick::HTTPServer.new(Port: @config.puerto)
|
||||
|
||||
server.mount_proc('/zombi.ps1', &Routes::Zombi.new(@config).method(:call))
|
||||
server.mount_proc('/cmd', &Routes::Cmd.new(@config).method(:call))
|
||||
server.mount_proc('/log', &Routes::Log.new(@config).method(:call))
|
||||
|
||||
trap("INT") { server.shutdown }
|
||||
|
||||
puts "[W-ZOMBI] Servidor corriendo en puerto #{@config.puerto}"
|
||||
server.start
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
$pub_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCxCHrqnaVrkihb/0yBAxzcb9knhCAWSFFC9J6rEKHE4Yu5DW3XZ/aoWKFq1NEZS6yNQqBqFGvAe32RBwnX7RiJEkhYgS/xr2SJorIxTWWJUOmV5i7OopQQQoZ7YUoY56tdVrYaulJCDyJkvnmGZwGdUS782BEb9nWDwXd5t1qDZJtCWCV/xK34zRuTy7DqHPU3sx+PUwj0KWy8rQBeRK2KNvHCpcVTQvx4u1QS9Cbbqsic+BrZfaU1LBa70GdJL8GY9p1ZIlSqqPMyzcpILh7CwrnyNr7hj+kaKVIdzeo+0WIf97XsjD0dNNt5VPP3kBrnTA62EQ6eraZl7V01Hx6gK178K9C3fcgPHsxZASf8/Vo+zOeg3ChCYQFPF9YzSyQpAJE2yT7ptuzXulhifpRBIs9d8HS5+6mWYgaLPqwDpZRvYB5NsX96m2OX3LBIeeJm4ZEc4531jsvOJNPiok9SVg083aAUbflFKtWV5M01X54rT4+uU6Uy4LRQGXPr5Mor+BBZyZ91bTtX8nGOVZrQ3Cxm4jc0Q7Aj902SX4yqrImLuoM7XIbVTHGQt+gsgVSxEbb4KaF4jOvRZKYrDcSFrJN9KWcx5SLCDZmN4TlGqgbVPrfxHzF7BqLrzq+bcG+BVm3DP3ROBwAjbXPcztiaDHpPjWDiZByy5nHnzqmmCw== rmonla@srv-ns8`n"
|
||||
|
||||
$config = @"
|
||||
Port 7022
|
||||
PubkeyAuthentication yes
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
PasswordAuthentication yes
|
||||
StrictModes yes
|
||||
|
||||
# Logging
|
||||
#SyslogFacility LOCAL0
|
||||
LogLevel VERBOSE
|
||||
|
||||
Subsystem sftp sftp-server.exe
|
||||
|
||||
Match Group administrators
|
||||
AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
|
||||
"@
|
||||
|
||||
$ssh_dir = "C:\ProgramData\ssh"
|
||||
$admin_keys = "$ssh_dir\administrators_authorized_keys"
|
||||
$sshd_config = "$ssh_dir\sshd_config"
|
||||
|
||||
# Ensure dir exists
|
||||
if (-not (Test-Path $ssh_dir)) {
|
||||
New-Item -ItemType Directory -Force -Path $ssh_dir | Out-Null
|
||||
}
|
||||
|
||||
# Write files as UTF-8 without BOM (Safe cross-platform)
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($False)
|
||||
[System.IO.File]::WriteAllText($admin_keys, $pub_key, $utf8NoBom)
|
||||
[System.IO.File]::WriteAllText($sshd_config, $config, $utf8NoBom)
|
||||
|
||||
# ACL helper function
|
||||
function Set-SecureAcl($path) {
|
||||
if (Test-Path $path) {
|
||||
$acl = Get-Acl $path
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
$acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) | Out-Null }
|
||||
|
||||
$systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule("NT AUTHORITY\SYSTEM", "FullControl", "Allow")
|
||||
# S-1-5-32-544 is Builtin\Administrators
|
||||
$adminSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")
|
||||
$adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule($adminSid, "FullControl", "Allow")
|
||||
|
||||
$acl.AddAccessRule($systemRule)
|
||||
$acl.AddAccessRule($adminRule)
|
||||
$acl.SetOwner($adminSid)
|
||||
|
||||
Set-Acl $path $acl
|
||||
}
|
||||
}
|
||||
|
||||
# Apply correct Windows OpenSSH permissions to the key file specifically
|
||||
# `sshd_config` needs to be readable but `administrators_authorized_keys` MUST be strict
|
||||
Set-SecureAcl $admin_keys
|
||||
|
||||
# Start service automatically & Restart
|
||||
Set-Service sshd -StartupType Automatic
|
||||
Restart-Service sshd
|
||||
|
||||
# Firewall Rule
|
||||
New-NetFirewallRule -Name "OpenSSH-7022" -DisplayName "OpenSSH Server (sshd) 7022" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 7022 -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
Write-Output "SSH fully configured for port 7022 with RSA Public Key."
|
||||
@@ -0,0 +1,41 @@
|
||||
$AuthorizedKeysPath = "$env:ProgramData\ssh\administrators_authorized_keys"
|
||||
$Keys = @"
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvPSt/gxq2AujLe5IMdybgyTADQbdVKQsvJBmAIU1WKWDz/2Ak+PUXgDXQPxfKipjQhEvZ7NkBzdvGBKsHWZ0DrEc01rUtoUT+U3CdajPyTCW7ZGQgk0R+Vg/4vA/ywRKCRCCSEa7t5ZT0xCQe8B75Gw7MqRL56u9ltUQgLuFcFB5zgqoW9WcpvLGOXVOj8s6BttA0CH5slWBPkYwNpywK5XztHwYZDUzX4BARC+JgCGWeatKBGUvD5WenKgsmAMWr+j0wvdm6EZumHMrMoizIq53Ylnw12MlwUtKAaCNqk72vZ7PkBVaiZEZphFL0KZxCCFGdXB9qF9C1PCRcIogq8ooiGUn5DVcTotEKDA3U1bT3bpN+9soBOoRmnRoIhUtIMlsiM0p3HaiFZJERcsQeal8c0SnA3vzV+8GbEBffm4maVac3u9cr4/Qh9VExf4ecs+c6QDPxq0CQDBZH24r4o+jVpQPcRszTSVHGWAntbotmLc2FNVnX8jQ6EHYIqFv0kqUT4g03QpzTZfW4DyQnED/7jvscIhiXMl7gFqh9UpRIqjy5BGxkz16VkFB3Qs6oKyKpXpU/Rxq0cEVnB6wgAga1kbexaMVfl78Q0mVS/J0+MnfxBkb9eYJ7a52NbWRtFQKS+hnfW459lsKjZs9vn7uG6f6XSZmQYWBZZpRG1Q== rmonla@srvNS8
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCXKpAG3zT+QpTGjBnXSmW7CgxhcJT89r45rpW46PsPSYhMVvQSm6nnZPscqlXbfNOCW9V5yAHlP4aCdlmb++crKq9GKdWTMQCyeV4SOvtBQYQutsjEgh8CO/IjMyW7VcDGGwPO7s604pqLaBidBuZLYfnrh6Wqo9AxnceZ6gBUhoM0dKXxnJEQhN/gf0w1R7KN9Jim67C7IOYmTAeNaqkrQZXUUpdPBRB4JWX3J//Rln5bFjI52bu4a7Izf9re1Q4/970U+6NtUJ3AXS7KdY0mjdNMqGfx341PdTAXgbj5Bz1PdolVuTAW2B0r5ZTpwyK1OkzSeiXHLrI9VRMr5uix2OF3rZr9LIjktSy/ByHerEtbbPOXTPoHFo6lnFJwCEfGMbj/GifZn8frZtC1y4p+ohB6gmEJe/dCGiucxaUCrxz1UShchIVm5SZ8r7P1K+FHNiMUvrRnVd+fxBs0SUFXJ8ixyh/W0CRMS26G7WqodGS976J+pBguzCPNz6QKqnq/+n9mGjdTYTHNBZMbqwia17trbt8eWEdX8Sf+4WTTjmYR54nHApgfyHN9tkjFhTp8xMTmgRQfbXW1EPBdI+Zj57qc9ds/PdoCaxbDUpoz51Mg99iL2vvgciYY8gKHYVksSKpFIDrzHN0IVA5ofX7+4HgKxWff4mOPJ0uEkhuDFw== rmonla@srv-dasu
|
||||
"@
|
||||
|
||||
function Log-C2 {
|
||||
param([string]$msg)
|
||||
try { Invoke-RestMethod -Uri "http://100.111.195.4:8000/log" -Method Post -Body "[SSH-KEY] $msg" -ContentType "text/plain" -UseBasicParsing -ErrorAction SilentlyContinue } catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
Log-C2 "Iniciando despliegue de llaves..."
|
||||
if (!(Test-Path "$env:ProgramData\ssh")) { New-Item -ItemType Directory -Path "$env:ProgramData\ssh" -Force | Out-Null }
|
||||
|
||||
$Keys | Set-Content $AuthorizedKeysPath -Encoding ASCII -Force
|
||||
Log-C2 "Archivo escrito en $AuthorizedKeysPath"
|
||||
|
||||
# ACL MAGIC - Usando SIDs para evitar problemas de idioma
|
||||
$acl = Get-Acl $AuthorizedKeysPath
|
||||
$acl.SetOwner([System.Security.Principal.SecurityIdentifier]"S-1-5-18") # SYSTEM
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
$rule1 = New-Object System.Security.AccessControl.FileSystemAccessRule([System.Security.Principal.SecurityIdentifier]"S-1-5-18", "FullControl", "Allow")
|
||||
$rule2 = New-Object System.Security.AccessControl.FileSystemAccessRule([System.Security.Principal.SecurityIdentifier]"S-1-5-32-544", "FullControl", "Allow")
|
||||
$acl.SetAccessRule($rule1)
|
||||
$acl.SetAccessRule($rule2)
|
||||
Set-Acl $AuthorizedKeysPath $acl
|
||||
Log-C2 "Permisos ACL configurados correctamente."
|
||||
|
||||
# Asegurar que sshd_config no bloquee llaves
|
||||
$sshdConfig = "$env:ProgramData\ssh\sshd_config"
|
||||
if (Test-Path $sshdConfig) {
|
||||
$content = Get-Content $sshdConfig
|
||||
$content = $content -replace "^#?PubkeyAuthentication.*", "PubkeyAuthentication yes"
|
||||
$content = $content -replace "^#?PasswordAuthentication.*", "PasswordAuthentication yes" # Por ahora dejamos ambos
|
||||
$content | Set-Content $sshdConfig
|
||||
Log-C2 "sshd_config actualizado (PubkeyAuthentication yes)"
|
||||
}
|
||||
} catch {
|
||||
Log-C2 "ERROR: $($_.Exception.Message)"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
$keySource = "C:\Users\admindasu\.ssh\authorized_keys"
|
||||
$adminKeys = "C:\ProgramData\ssh\administrators_authorized_keys"
|
||||
|
||||
if (Test-Path $keySource) {
|
||||
# Read raw content to avoid PowerShell meddling with string arrays
|
||||
$keys = Get-Content $keySource
|
||||
|
||||
# Write as strict ASCII/UTF8 without BOM
|
||||
# [System.IO.File]::WriteAllText is the safest way to ensure no UTF-16 LE BOM is added.
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($False)
|
||||
[System.IO.File]::WriteAllLines($adminKeys, $keys, $utf8NoBom)
|
||||
|
||||
Write-Output "administrators_authorized_keys re-written as UTF-8 (No BOM)"
|
||||
|
||||
# Also fix permissions just in case
|
||||
$acl = Get-Acl $adminKeys
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
$systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule("NT AUTHORITY\SYSTEM", "FullControl", "Allow")
|
||||
$adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule("BUILTIN\Administradores", "FullControl", "Allow")
|
||||
$acl.SetAccessRule($systemRule)
|
||||
$acl.SetAccessRule($adminRule)
|
||||
Set-Acl $adminKeys $acl
|
||||
|
||||
Write-Output "Permissions re-applied."
|
||||
} else {
|
||||
Write-Output "Source key not found."
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
$path = "C:\ProgramData\ssh\sshd_config"
|
||||
$content = Get-Content $path -Raw
|
||||
$content = $content -replace "(?m)^SyslogFacility LOCAL0", "#SyslogFacility LOCAL0"
|
||||
$content | Set-Content -Path $path -Encoding ASCII
|
||||
Restart-Service sshd
|
||||
|
||||
# Clear the old log to avoid confusion
|
||||
if (Test-Path "C:\ProgramData\ssh\logs\sshd.log") {
|
||||
Clear-Content "C:\ProgramData\ssh\logs\sshd.log"
|
||||
}
|
||||
Write-Output "sshd_config updated for file logging and restarted."
|
||||
@@ -0,0 +1,28 @@
|
||||
$path = "C:\ProgramData\ssh\administrators_authorized_keys"
|
||||
|
||||
if (Test-Path $path) {
|
||||
$acl = Get-Acl $path
|
||||
|
||||
# Disable inheritance
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
|
||||
# Remove all existing access rules (we're starting fresh)
|
||||
$acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) | Out-Null }
|
||||
|
||||
# Add SYSTEM and builtin Administrators full control
|
||||
$systemRule = New-Object System.Security.AccessControl.FileSystemAccessRule("NT AUTHORITY\SYSTEM", "FullControl", "Allow")
|
||||
# Using well-known SID for Builtin Administrators (S-1-5-32-544) to avoid localization issues (Administradores vs Administrators)
|
||||
$adminSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")
|
||||
$adminRule = New-Object System.Security.AccessControl.FileSystemAccessRule($adminSid, "FullControl", "Allow")
|
||||
|
||||
$acl.AddAccessRule($systemRule)
|
||||
$acl.AddAccessRule($adminRule)
|
||||
|
||||
# Set Owner to Builtin Administrators
|
||||
$acl.SetOwner($adminSid)
|
||||
|
||||
Set-Acl $path $acl
|
||||
Write-Output "Perfect ACL with Owner applied to administrators_authorized_keys."
|
||||
} else {
|
||||
Write-Output "File not found."
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
$path = "C:\ProgramData\ssh\sshd_config"
|
||||
$content = Get-Content $path
|
||||
$newContent = $content | ForEach-Object {
|
||||
if ($_ -match "Match Group administrators") {
|
||||
"#$_"
|
||||
} elseif ($_ -match "AuthorizedKeysFile __PROGRAMDATA__") {
|
||||
"#$_"
|
||||
} elseif ($_ -match "#PubkeyAuthentication yes") {
|
||||
"PubkeyAuthentication yes"
|
||||
} else {
|
||||
$_
|
||||
}
|
||||
}
|
||||
$newContent | Set-Content $path
|
||||
Restart-Service sshd
|
||||
@@ -0,0 +1,33 @@
|
||||
# Temporary fix to test if StrictModes is blocking the key
|
||||
$path = "C:\ProgramData\ssh\sshd_config"
|
||||
|
||||
$sshdConfig = @"
|
||||
Port 7022
|
||||
|
||||
# Authentication
|
||||
PubkeyAuthentication yes
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
PasswordAuthentication yes
|
||||
StrictModes no
|
||||
|
||||
# Host keys
|
||||
HostKey __PROGRAMDATA__/ssh/ssh_host_rsa_key
|
||||
HostKey __PROGRAMDATA__/ssh/ssh_host_ecdsa_key
|
||||
HostKey __PROGRAMDATA__/ssh/ssh_host_ed25519_key
|
||||
|
||||
# Logging
|
||||
SyslogFacility LOCAL0
|
||||
LogLevel DEBUG3
|
||||
|
||||
# Subsystem
|
||||
Subsystem sftp sftp-server.exe
|
||||
|
||||
Match Group administrators
|
||||
AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
|
||||
"@
|
||||
|
||||
$sshdConfig | Set-Content -Path $path -Encoding ASCII
|
||||
Write-Output ">>> sshd_config reescrito con StrictModes no para prueba"
|
||||
|
||||
Restart-Service sshd
|
||||
Write-Output ">>> sshd reiniciado OK"
|
||||
@@ -0,0 +1,74 @@
|
||||
function Log-Msg {
|
||||
param([string]$Message)
|
||||
Write-Host $Message -ForegroundColor Cyan
|
||||
try {
|
||||
Invoke-RestMethod -Uri "http://100.111.195.4:8000/log" -Method Post -Body "TELEMETRIA: $Message" -ContentType "text/plain" -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$TARGET_PORT = 7022
|
||||
|
||||
Log-Msg "=========================================================="
|
||||
Log-Msg "OPT ADN: GESTION INTELIGENTE DE OPENSSH SERVER (PORT $TARGET_PORT)"
|
||||
Log-Msg "=========================================================="
|
||||
|
||||
try {
|
||||
# 1. Verificar Instalación
|
||||
$sshCheck = Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'
|
||||
if ($sshCheck.State -ne 'Installed') {
|
||||
Log-Msg "Estado: NO INSTALADO. Iniciando instalacion..."
|
||||
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 | Out-Null
|
||||
Log-Msg "Caracteristica instalada con exito."
|
||||
} else {
|
||||
Log-Msg "Estado: INSTALADO. Verificando configuracion..."
|
||||
}
|
||||
|
||||
# 2. Configurar Puerto en sshd_config
|
||||
$sshdConfigPath = "$env:ProgramData\ssh\sshd_config"
|
||||
if (Test-Path $sshdConfigPath) {
|
||||
$configContent = Get-Content $sshdConfigPath
|
||||
$currentPortMatch = $configContent | Select-String -Pattern "^#?Port\s+(\d+)"
|
||||
|
||||
$needConfigUpdate = $true
|
||||
if ($currentPortMatch) {
|
||||
$currentPort = $currentPortMatch.Matches[0].Groups[1].Value
|
||||
if ($currentPort -eq $TARGET_PORT.ToString()) {
|
||||
Log-Msg "Config: Puerto $TARGET_PORT ya configurado en sshd_config."
|
||||
$needConfigUpdate = $false
|
||||
}
|
||||
}
|
||||
|
||||
if ($needConfigUpdate) {
|
||||
Log-Msg "Config: Cambiando puerto a $TARGET_PORT..."
|
||||
if ($currentPortMatch) {
|
||||
$newContent = $configContent -replace "^#?Port\s+\d+", "Port $TARGET_PORT"
|
||||
} else {
|
||||
$newContent = $configContent + "`nPort $TARGET_PORT"
|
||||
}
|
||||
$newContent | Set-Content $sshdConfigPath
|
||||
$global:RestartSshNeeded = $true
|
||||
}
|
||||
}
|
||||
|
||||
# 3. Configurar Firewall
|
||||
$ruleName = "OpenSSH-Server-In-TCP-$TARGET_PORT"
|
||||
if (!(Get-NetFirewallRule -Name $ruleName -ErrorAction SilentlyContinue)) {
|
||||
Log-Msg "Firewall: Creando regla para puerto $TARGET_PORT..."
|
||||
New-NetFirewallRule -Name $ruleName -DisplayName "OpenSSH Server (Port $TARGET_PORT)" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort $TARGET_PORT | Out-Null
|
||||
}
|
||||
|
||||
# 4. Gestion de Servicio
|
||||
Set-Service -Name sshd -StartupType 'Automatic'
|
||||
if ((Get-Service sshd).Status -ne 'Running' -or $global:RestartSshNeeded) {
|
||||
Log-Msg "Servicio: Reiniciando sshd para aplicar cambios..."
|
||||
Restart-Service sshd -Force -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
Log-Msg "Servicio: sshd ya esta operando."
|
||||
}
|
||||
|
||||
# 5. Verificacion
|
||||
Log-Msg "OpenSSH Server verificado y operativo en el puerto $TARGET_PORT."
|
||||
} catch {
|
||||
Log-Msg "CRITICO: Fallo en gestion SSH: $_"
|
||||
}
|
||||
Log-Msg "=========================================================="
|
||||
@@ -0,0 +1 @@
|
||||
try { $pw = ConvertTo-SecureString '"$PW"' -AsPlainText -Force; $cred = New-Object System.Management.Automation.PSCredential ('DASUTEN\admindasu', $pw); Add-Computer -DomainName dasuten.utnlr -Credential $cred -Server 100.85.117.101 -Force -Restart } catch { $_ | Out-File join_error.txt }
|
||||
@@ -0,0 +1,21 @@
|
||||
# Rebuild sshd_config for dasu-srvv-dc
|
||||
$sshdConfig = @"
|
||||
Port 7022
|
||||
PubkeyAuthentication yes
|
||||
AuthorizedKeysFile .ssh/authorized_keys
|
||||
PasswordAuthentication yes
|
||||
Subsystem sftp sftp-server.exe
|
||||
LogLevel DEBUG3
|
||||
|
||||
# COMENTADO: los admins usaran su .ssh/authorized_keys personal
|
||||
#Match Group administrators
|
||||
# AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
|
||||
"@
|
||||
|
||||
$path = "C:\ProgramData\ssh\sshd_config"
|
||||
$sshdConfig | Set-Content -Path $path -Encoding UTF8
|
||||
Write-Output "sshd_config reescrito OK"
|
||||
Write-Output (Get-Content $path)
|
||||
|
||||
Restart-Service sshd
|
||||
Write-Output "sshd reiniciado"
|
||||
@@ -0,0 +1,13 @@
|
||||
# Start a temporary SSHD server on port 7023 in debug mode and capture its output
|
||||
$sshdPath = "C:\Windows\System32\OpenSSH\sshd.exe"
|
||||
$logPath = "C:\ProgramData\ssh\sshd_debug.log"
|
||||
|
||||
Remove-Item $logPath -ErrorAction SilentlyContinue
|
||||
|
||||
# Open Firewall
|
||||
New-NetFirewallRule -Name "OpenSSH-Debug" -DisplayName "OpenSSH-Debug" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 7023 -ErrorAction SilentlyContinue
|
||||
|
||||
# Start the process in the background, redirecting stderr and stdout
|
||||
$proc = Start-Process -FilePath $sshdPath -ArgumentList "-p 7023 -d" -RedirectStandardOutput $logPath -RedirectStandardError $logPath -WindowStyle Hidden -PassThru
|
||||
|
||||
Write-Output "Temporary SSHD started on port 7023."
|
||||
@@ -0,0 +1,68 @@
|
||||
function Log-C2 {
|
||||
param([string]$msg)
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] $msg"
|
||||
Write-Host "📡 $line" -ForegroundColor Cyan
|
||||
try {
|
||||
if (-not $WZ_HOST) { $WZ_HOST = "100.111.195.4:8000" }
|
||||
Invoke-RestMethod -Uri "http://$WZ_HOST/log" -Method Post -Body $line -ContentType "text/plain" -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Log-C2 "=========================================================="
|
||||
Log-C2 "🚀 INICIANDO DESPLIEGUE DE TAILSCALE"
|
||||
Log-C2 "=========================================================="
|
||||
|
||||
$tailscalePath = "C:\Program Files\Tailscale\tailscale.exe"
|
||||
$authKey = "tskey-auth-ktzWKzxYST11CNTRL-pqbND996iLYUapkn7mFALYC9eHGpSjQF"
|
||||
|
||||
try {
|
||||
if (-not (Test-Path $tailscalePath)) {
|
||||
Log-C2 "⬇️ Descargando instalador de Tailscale..."
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$installer = "$env:TEMP\tailscale-setup.exe"
|
||||
Invoke-WebRequest -Uri "https://pkgs.tailscale.com/stable/tailscale-setup.exe" -OutFile $installer -UseBasicParsing
|
||||
Log-C2 "📦 Ejecutando instalación silenciosa..."
|
||||
Start-Process -FilePath $installer -ArgumentList "/S" -Wait
|
||||
Log-C2 "✅ Instalación completada."
|
||||
} else {
|
||||
Log-C2 "ℹ️ Tailscale ya se encuentra instalado."
|
||||
}
|
||||
|
||||
Log-C2 "🔄 Reiniciando estado de Tailscale..."
|
||||
& $tailscalePath down 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
& $tailscalePath reset 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
Log-C2 "🔑 Autenticando con el nodo..."
|
||||
$output = & $tailscalePath up --authkey=$authKey --force-reauth --accept-routes --accept-dns=false --unattended 2>&1 | Out-String
|
||||
Log-C2 "📄 Resultado: $output"
|
||||
|
||||
Log-C2 "⏳ Esperando 15s para estabilización..."
|
||||
Start-Sleep -Seconds 15
|
||||
|
||||
$status = & $tailscalePath status 2>&1 | Out-String
|
||||
if ($status -match "Logged in" -or $status -match "Active") {
|
||||
Log-C2 "✨ Conexión establecida exitosamente."
|
||||
$ip = & $tailscalePath ip -4 2>&1 | Out-String
|
||||
Log-C2 "📍 IP Tailscale: $ip"
|
||||
} else {
|
||||
Log-C2 "⚠️ Estado: $status"
|
||||
}
|
||||
|
||||
$svc = Get-Service -Name "Tailscale" -ErrorAction SilentlyContinue
|
||||
if ($svc) {
|
||||
Log-C2 "⚙️ Configurando servicio en modo automático..."
|
||||
Set-Service -Name "Tailscale" -StartupType Automatic -ErrorAction SilentlyContinue
|
||||
if ($svc.Status -ne "Running") {
|
||||
Start-Service -Name "Tailscale" -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
} catch {
|
||||
Log-C2 "❌ ERROR FATAL: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Log-C2 "=========================================================="
|
||||
Log-C2 "🏁 OPERACION FINALIZADA"
|
||||
Log-C2 "=========================================================="
|
||||
@@ -0,0 +1,49 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn
|
||||
NhAAAAAwEAAQAAAgEAsQh66p2la5IoW/9MgQMc3G/ZJ4QgFkhRQvSeqxChxOGLuQ1t12f2
|
||||
qFihatTRGUusjUKgahRrwHt9kQcJ1+0YiRJIWIEv8a9kiaKyMU1liVDpleYuzqKUEEKGe2
|
||||
FKGOerXVa2GrpSQg8iZL55hmcBnVEu/NgRG/Z1g8F3ebdag2SbQlglf8St+M0bk8uw6hz1
|
||||
N7Mfj1MI9ClsvK0AXkStijbxwqXFU0L8eLtUEvQm26rInPga2X2lNSwWu9BnSS/BmPadWS
|
||||
JUqqjzMs3KSC4ewsK58ja+4Y/pGilSHc3qPtFiH/e17Iw9HTTbeVTz95Aa50wOthEOnq2m
|
||||
Ze1dNR8eoCte/CvQt33IDx7MWQEn/P1aPsznoNwoQmEBTxfWM0skKQCRNsk+6bbs17pYYn
|
||||
6UQSLPXfB0ufuplmIGiz6sA6WUb2AeTbF/eptjl9ywSHniZuGRHOOd9Y7LziTT4qJPUlYN
|
||||
PN2gFG35RSrVleTNNV+eK0+PrlOlMuC0UBlz6+TKK/gQWcmfdW07V/JxjlWa0NwsZuI3NE
|
||||
OwI/dNkl+MqqyJi7qDO1yG1UxxkLfoLIFUsRG2+CmheIzr0WSmKw3EhayTfSlnMeUiwg2Z
|
||||
jeE5RqoG1T638R8xewai686vm3BvgVZtwz90TgcAI21z3M7Ymgx6T41g4mQcsuZx586ppg
|
||||
sAAAdI+mS8bPpkvGwAAAAHc3NoLXJzYQAAAgEAsQh66p2la5IoW/9MgQMc3G/ZJ4QgFkhR
|
||||
QvSeqxChxOGLuQ1t12f2qFihatTRGUusjUKgahRrwHt9kQcJ1+0YiRJIWIEv8a9kiaKyMU
|
||||
1liVDpleYuzqKUEEKGe2FKGOerXVa2GrpSQg8iZL55hmcBnVEu/NgRG/Z1g8F3ebdag2Sb
|
||||
Qlglf8St+M0bk8uw6hz1N7Mfj1MI9ClsvK0AXkStijbxwqXFU0L8eLtUEvQm26rInPga2X
|
||||
2lNSwWu9BnSS/BmPadWSJUqqjzMs3KSC4ewsK58ja+4Y/pGilSHc3qPtFiH/e17Iw9HTTb
|
||||
eVTz95Aa50wOthEOnq2mZe1dNR8eoCte/CvQt33IDx7MWQEn/P1aPsznoNwoQmEBTxfWM0
|
||||
skKQCRNsk+6bbs17pYYn6UQSLPXfB0ufuplmIGiz6sA6WUb2AeTbF/eptjl9ywSHniZuGR
|
||||
HOOd9Y7LziTT4qJPUlYNPN2gFG35RSrVleTNNV+eK0+PrlOlMuC0UBlz6+TKK/gQWcmfdW
|
||||
07V/JxjlWa0NwsZuI3NEOwI/dNkl+MqqyJi7qDO1yG1UxxkLfoLIFUsRG2+CmheIzr0WSm
|
||||
Kw3EhayTfSlnMeUiwg2ZjeE5RqoG1T638R8xewai686vm3BvgVZtwz90TgcAI21z3M7Ymg
|
||||
x6T41g4mQcsuZx586ppgsAAAADAQABAAACADo0+5UgeD9CMxrseg8BIwAnllKz0okBBhbp
|
||||
rzG3qji9n98cVz035ZW8bnZdutKCWx2nBm9af4MuFz8T/VyNjD+lTzwqXcUtUfUfFU+4ju
|
||||
XzQJoUsNcoBV7DQMxYVLCTm3h5Bi8Li/hEWZ6eMf7K53D+PGDN+fIjusezWMEgVBZXgeTy
|
||||
boHv/PONHMAffO+8zbOepYXOW2dMJ4BxsWlNU3HaVusU7ihOIgzgKuiAdjZLlOJngQx5j1
|
||||
RIRsFB6HPby9+rVlj0PLpQCoZtu1C4OUz4EYNeFKy9UHtVkrZR+e7lkUnJfHtmMMHlmzPt
|
||||
hrmjG/VowH2EZjYuCkQBI2BP9xVOq/GwdBmQlb0rIZJPCYUnoI0Wpmy45RS2e2ix6KjktH
|
||||
3V1L3gQK6RPBR+KZZxpA6r9vaFqpgqzl0dpWCDEEbSviTv/qi5bUsExC/Tt4+OYpUmkReT
|
||||
kTC4bChp350L45gdWt2Lg/cL/6imH3WuLvPzAppTRiK3n4J+f8ImZgG96bbbY7uVkP9ALg
|
||||
vRApzeT6BzXfcMu4MEI4CnWVBAGS+f0ZnIAQiE953hjqkQ4VGjL/3lMM8qk+tphcEfD1Jv
|
||||
uqY68gRVyGVRwJ4fq/9vZ7yGxyE9rY6icdrOmZWkhtbu7mGWh3M4M6/8475MTkju41P54K
|
||||
cNcSoDYSWs82x2vKABAAABAQCzJ5b3s51ynRhVsdBN54lihZfSqVfkMJ7e3axJHNhRpxmy
|
||||
4ZVDtaoMalxvMVhI4898AwBP0E94nhjeGwxEOdvZ+jovndinvNI1kz94qYGG5qoVgHmoX5
|
||||
1YnnEHbyb57kZD1EqGKxkHGf14AwLnz5H5enAon3ILM3MmviV+orL/XWxUtZEOiMme6Aqa
|
||||
caCC5vH+98KQUbBmx/7cyGd723OCig2bHEkfSQ2Tk90lNf3qnRXU9oBP/HKQvCpfAEORUb
|
||||
pNpx22guZtwsbtJkS+Ryz3umoFpWr+p9ysUy8jWwS5lV6wcTOccCZChSxrWiqVFsV9raBT
|
||||
pgunFIJdTyM4t1u5AAABAQDXztyqzbcPO7GJnmRWFZl/J5Gfn0PQ5T7iiCT5w8ai2pNJOr
|
||||
ilgbR0aNJRCICAVEx8XWCl+1FaLPBXs59cLXdbnWcXf7mqjTU5yszEqg4PecvfSNBeVf+K
|
||||
NdAaFylHHpULP38xv4QZp0Wm2ne5UotI7GEa6GEDdufw3HbUmK7GW6advlp2YKZgrqRGr5
|
||||
YN6DG5JnYnJNtWRkLxKRt2uPK4m4+hrNEFZ+MiItSmgoF28rMIgh3Ar/AqgYYLyvw5ZPDl
|
||||
NHnGnV/X0UpYUftoJJdHo4vNDdhfbC38gP7XY+PCmHoza1mx/zuGSTunUExj2Alsunnq3l
|
||||
aMP2Wg/Jn51oILAAABAQDSAO+AkBHpZoRx+xcdRM6X7zwJdP5vp2h7qwZRYSKB02pZQvb5
|
||||
y2IxVDGUfBIkVSWMw5ozUV+h7ptZCbZSipib1WQF2yj1n9znYVQNh38Dl/FXpwgtKqL6Z+
|
||||
LiAryLzC7n0+6LBY2GqUm5ND3MwrrLAmsjxx4yvQXpTlAYa/DlFFC7qWb0brN47nRJvSXX
|
||||
rtWoAg/30lamh1VGp1glZngefp9Ekluh3v7KyEOnNIArfHL5HKkKxCAzEdMnnkoqeFEWus
|
||||
lRDdW7s0TbOWvYRt5y1Z/IEsiUbMzqVfWK2GNzCjPkOITVj5ZSSfKPk7NteGGD/Ti9jBrI
|
||||
SYtDKgM+c+wBAAAADnJtb25sYUBzcnYtbnM4AQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCxCHrqnaVrkihb/0yBAxzcb9knhCAWSFFC9J6rEKHE4Yu5DW3XZ/aoWKFq1NEZS6yNQqBqFGvAe32RBwnX7RiJEkhYgS/xr2SJorIxTWWJUOmV5i7OopQQQoZ7YUoY56tdVrYaulJCDyJkvnmGZwGdUS782BEb9nWDwXd5t1qDZJtCWCV/xK34zRuTy7DqHPU3sx+PUwj0KWy8rQBeRK2KNvHCpcVTQvx4u1QS9Cbbqsic+BrZfaU1LBa70GdJL8GY9p1ZIlSqqPMyzcpILh7CwrnyNr7hj+kaKVIdzeo+0WIf97XsjD0dNNt5VPP3kBrnTA62EQ6eraZl7V01Hx6gK178K9C3fcgPHsxZASf8/Vo+zOeg3ChCYQFPF9YzSyQpAJE2yT7ptuzXulhifpRBIs9d8HS5+6mWYgaLPqwDpZRvYB5NsX96m2OX3LBIeeJm4ZEc4531jsvOJNPiok9SVg083aAUbflFKtWV5M01X54rT4+uU6Uy4LRQGXPr5Mor+BBZyZ91bTtX8nGOVZrQ3Cxm4jc0Q7Aj902SX4yqrImLuoM7XIbVTHGQt+gsgVSxEbb4KaF4jOvRZKYrDcSFrJN9KWcx5SLCDZmN4TlGqgbVPrfxHzF7BqLrzq+bcG+BVm3DP3ROBwAjbXPcztiaDHpPjWDiZByy5nHnzqmmCw== rmonla@srv-ns8
|
||||
@@ -0,0 +1,60 @@
|
||||
# 🧟 W-Zombi: Toolkit de Inyección Lateral para Windows VMs
|
||||
|
||||
> Técnica documentada en la bitácora del [25/02/2026](../../bitacoras/2026-02-25.md) (Hito DB01).
|
||||
|
||||
## Problema
|
||||
|
||||
La consola HTML5/VNC de Proxmox **no soporta copiar/pegar** texto. Esto impide provisionar comandos complejos en VMs Windows recién creadas que aún no tienen SSH.
|
||||
|
||||
## Solución
|
||||
|
||||
Arquitectura C2 liviana que sortea la limitación:
|
||||
|
||||
```
|
||||
srv-ns8 (10.0.10.8) VM Windows (VNC)
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ servidor.rb │ ← HTTP :8000 ── │ zombi.ps1 (loop) │
|
||||
│ (Ruby / WEBrick) │ │ Consulta activo.ps1 │
|
||||
│ │ ── payload ────→ │ cada 10 seg │
|
||||
│ payloads/activo.ps1 │ │ Ejecuta y reporta │
|
||||
│ (hot-swap por IA) │ ← POST /log ── │ telemetría al server │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
## Uso
|
||||
|
||||
### 1. En srv-ns8 (la IA o el operador):
|
||||
```bash
|
||||
ruby tools/w-zombi/servidor.rb
|
||||
```
|
||||
|
||||
### 2. En la VM Windows (tipear manualmente en VNC — PowerShell Admin):
|
||||
```powershell
|
||||
iwr 10.0.10.8:8000/zombi.ps1 -useb|iex
|
||||
```
|
||||
|
||||
### 3. Cambiar payload activo:
|
||||
```bash
|
||||
# Copiar un payload pre-armado
|
||||
cp tools/w-zombi/payloads/install_ssh.ps1 tools/w-zombi/payloads/activo.ps1
|
||||
|
||||
# O la IA modifica activo.ps1 directamente
|
||||
```
|
||||
|
||||
## Estructura
|
||||
|
||||
```
|
||||
tools/w-zombi/
|
||||
├── servidor.rb ← Servidor HTTP Ruby (WEBrick)
|
||||
├── README.md ← Este archivo
|
||||
└── payloads/
|
||||
├── activo.ps1 ← Payload que ejecuta la VM (hot-swap)
|
||||
├── install_ssh.ps1 ← Instalar OpenSSH Server nativo
|
||||
└── sonda.ps1 ← Diagnóstico y relevamiento del sistema
|
||||
```
|
||||
|
||||
## Seguridad
|
||||
|
||||
- Servidor **efímero** — solo se levanta durante la operación.
|
||||
- Solo escucha en la red interna (`10.0.10.x` / `10.0.100.x`).
|
||||
- Una vez instalado SSH, este mecanismo deja de ser necesario.
|
||||
@@ -0,0 +1 @@
|
||||
OpenSSH-Win64.zip
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env ruby
|
||||
require_relative '../lib/wzombi/config'
|
||||
require_relative '../lib/wzombi/server'
|
||||
require_relative '../lib/wzombi/command_store'
|
||||
|
||||
base = File.expand_path("..", __dir__)
|
||||
config = WZombi::Config.new(base_path: base)
|
||||
|
||||
def help
|
||||
puts <<~HELP
|
||||
Uso:
|
||||
w-zombi start
|
||||
w-zombi cmd "<comando>"
|
||||
w-zombi ayuda | -h | -help
|
||||
|
||||
Ejemplo ADN:
|
||||
adn tools w-zombi cmd "ipconfig"
|
||||
HELP
|
||||
end
|
||||
|
||||
cmd = ARGV[0]
|
||||
|
||||
case cmd
|
||||
when "start"
|
||||
WZombi::Server.new(config).start
|
||||
|
||||
when "cmd"
|
||||
command = ARGV[1]
|
||||
if command.nil?
|
||||
puts "Falta comando"
|
||||
exit
|
||||
end
|
||||
store = WZombi::CommandStore.new(config.cmd_path)
|
||||
store.update(command)
|
||||
puts "Comando actualizado: #{command}"
|
||||
|
||||
when "ayuda", "-h", "-help", nil
|
||||
help
|
||||
|
||||
else
|
||||
puts "Comando desconocido"
|
||||
help
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
{"id":0,"cmd":""}
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'json'
|
||||
|
||||
module WZombi
|
||||
class CommandStore
|
||||
def initialize(path)
|
||||
@path = path
|
||||
init_file
|
||||
end
|
||||
|
||||
def init_file
|
||||
File.write(@path, { id: 0, cmd: "" }.to_json) unless File.exist?(@path)
|
||||
end
|
||||
|
||||
def current
|
||||
JSON.parse(File.read(@path))
|
||||
end
|
||||
|
||||
def update(cmd)
|
||||
data = current
|
||||
data["id"] += 1
|
||||
data["cmd"] = cmd
|
||||
File.write(@path, data.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
module WZombi
|
||||
class Config
|
||||
attr_reader :puerto, :base_path
|
||||
|
||||
def initialize(base_path:)
|
||||
@base_path = base_path
|
||||
@puerto = ENV.fetch("WZ_PORT", 8000).to_i
|
||||
end
|
||||
|
||||
def log_path
|
||||
File.join(base_path, "data/telemetria.log")
|
||||
end
|
||||
|
||||
def cmd_path
|
||||
File.join(base_path, "data/cmd.json")
|
||||
end
|
||||
|
||||
def payloads_path
|
||||
File.join(base_path, "payloads")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
module WZombi
|
||||
module Engine
|
||||
class PS1Builder
|
||||
def self.build(host:)
|
||||
<<~PS1
|
||||
# W-ZOMBI ENGINE
|
||||
$WZ_HOST = "#{host}"
|
||||
$LAST_CMD_ID = 0
|
||||
|
||||
function Get-Cmd {
|
||||
try {
|
||||
return Invoke-RestMethod -Uri "http://$WZ_HOST/cmd"
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Send-Log {
|
||||
param([string]$msg)
|
||||
try {
|
||||
Invoke-RestMethod -Uri "http://$WZ_HOST/log" -Method Post -Body $msg -ContentType "text/plain"
|
||||
} catch {}
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
$cmdData = Get-Cmd
|
||||
if ($cmdData.id -ne $LAST_CMD_ID) {
|
||||
$LAST_CMD_ID = $cmdData.id
|
||||
$result = Invoke-Expression $cmdData.cmd 2>&1 | Out-String
|
||||
Send-Log $result
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
PS1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
module WZombi
|
||||
class Logger
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
line = "[#{Time.now.strftime('%H:%M:%S')}] #{msg}"
|
||||
File.open(@path, "a") { |f| f.puts(line) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
require 'json'
|
||||
|
||||
module WZombi
|
||||
module Routes
|
||||
class Cmd
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(_req, res)
|
||||
res['Content-Type'] = 'application/json'
|
||||
res.body = File.read(@config.cmd_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module WZombi
|
||||
module Routes
|
||||
class Log
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(req, res)
|
||||
File.open(@config.log_path, "a") { |f| f.puts(req.body) }
|
||||
res.body = "OK"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
require_relative '../engine/ps1_builder'
|
||||
|
||||
module WZombi
|
||||
module Routes
|
||||
class Zombi
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(_req, res)
|
||||
res.body = Engine::PS1Builder.build(host: "127.0.0.1:#{@config.puerto}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'webrick'
|
||||
require_relative 'routes/zombi'
|
||||
require_relative 'routes/cmd'
|
||||
require_relative 'routes/log'
|
||||
|
||||
module WZombi
|
||||
class Server
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def start
|
||||
server = WEBrick::HTTPServer.new(Port: @config.puerto)
|
||||
|
||||
server.mount_proc('/zombi.ps1', &Routes::Zombi.new(@config).method(:call))
|
||||
server.mount_proc('/cmd', &Routes::Cmd.new(@config).method(:call))
|
||||
server.mount_proc('/log', &Routes::Log.new(@config).method(:call))
|
||||
|
||||
trap("INT") { server.shutdown }
|
||||
|
||||
puts "[W-ZOMBI] Servidor corriendo en puerto #{@config.puerto}"
|
||||
server.start
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,5 +7,6 @@
|
||||
"srv-dasu:rmonla": "eyJpdiI6IkFoczJZWUIwQ2piZzdJNzgiLCJ0YWciOiJJdDhCZndRc1RoZFhVMVQ4OEhaN1J3PT0iLCJkYXRhIjoiaUQycmxndGFsUkE2dVRaTEgrTT0ifQ==",
|
||||
"dominio-utnlarioja.intranet:monlaricardo": "eyJpdiI6InFRQ3VNYWlVSHlKMURNdzciLCJ0YWciOiJ1a3hESUZlaEt6enV3UXZxWUFrWXhRPT0iLCJkYXRhIjoiNVNaekdxVyt6dE5WQmhPaXF3PT0ifQ==",
|
||||
"ns8:rmonla:sudo": "eyJpdiI6IkRqbTBhVlZSYUpVdno3TFkiLCJ0YWciOiJJSnE2ei8zZHIrV2hFQks1VjNpVGN3PT0iLCJkYXRhIjoiT3JxTjRhTmlQK2R3TlJZZk9BPT0ifQ==",
|
||||
"srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0="
|
||||
"srv-dasu:root": "eyJpdiI6Ik5LY09JR1FvaFVPRXd3bjUiLCJ0YWciOiJwb0NLZ3A4N3VOQUZMQWhMS1hUVWZnPT0iLCJkYXRhIjoicEgvWTFmd0RhSFQ0YWRHRUxyZ1lBQT09In0=",
|
||||
"srv-ns8:rmonla:sudo": "eyJpdiI6InJDOUp2STdYR1crb2NkMTgiLCJ0YWciOiJHSUZVbXM5VnVmZ3d1NXQ3SFVwOUdRPT0iLCJkYXRhIjoib1ZVTzdaLzk1WGdwZDhKREM3aWpnV3c9In0="
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1773874654
|
||||
+292
-174
@@ -1,186 +1,304 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
class SubcomandoAyuda
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
mostrar_ayuda_principal
|
||||
else
|
||||
mostrar_ayuda_subcomando(@args[0])
|
||||
module ADN
|
||||
class SubcomandoAyuda
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
mostrar_ayuda_principal
|
||||
else
|
||||
mostrar_ayuda_subcomando(@args[0])
|
||||
end
|
||||
end
|
||||
|
||||
def mostrar_ayuda_principal
|
||||
puts <<~AYUDA_PRINCIPAL
|
||||
#{Color::CYAN}🔧 Herramienta ADN - Interfaz de línea de comandos unificada#{Color::RESET}
|
||||
#{Color::DIM}Versión: Fase 1 (Núcleo Autónomo)#{Color::RESET}
|
||||
private
|
||||
|
||||
#{Color::GREEN}Uso:#{Color::RESET} ./adn/tools/run <subcomando> [opciones] [argumentos]
|
||||
def mostrar_ayuda_principal
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Herramienta ADN - Interfaz de línea de comandos unificada",
|
||||
descripcion: "Versión: Fase 1 (Núcleo Autónomo)",
|
||||
uso: "./adn/tools/run <subcomando> [opciones] [argumentos]",
|
||||
opciones: [
|
||||
{ names: ['validador [--watch] [--fix]'], desc: "Validar cumplimiento del ADN en bitácoras" },
|
||||
{ names: ['contexto <nodo> [--cmd]'], desc: "Obtener contexto topológico de un nodo" },
|
||||
{ names: ['nodos <acción> [opciones]'], desc: "Gestión de nodos (agrupar, listar, buscar)" },
|
||||
{ names: ['jornada <acción> [modo] [hora] [opciones]'], desc: "Gestión de jornada (iniciar, cerrar, estado)" },
|
||||
{ names: ['salud [--dashboard] [--json]'], desc: "Ver salud general del sistema ADN" },
|
||||
{ names: ['generar bitacora [fecha]'], desc: "Generar nueva bitácora (fecha: YYYY-MM-DD)" },
|
||||
{ names: ['generar nodo <nombre>'], desc: "Generar ficha de nodo" },
|
||||
{ names: ['msp <subcomando>'], desc: "Gestión de MSPs NotebookLM (auditar, listar, registrar)" },
|
||||
{ names: ['generar proyecto <código>'], desc: "Generar manifiesto de proyecto" },
|
||||
{ names: ['tailscale <acción>'], desc: "Gestión de identidades Tailscale" },
|
||||
{ names: ['backup <nodo> [--mode MODO]'], desc: "Ejecutar backup seguro de un nodo" },
|
||||
{ names: ['db <subcomando>'], desc: "Operaciones de base de datos" },
|
||||
{ names: ['triggers [opciones]'], desc: "Gestión del motor de triggers" },
|
||||
{ names: ['plan [subcomando]'], desc: "Gestión de planes y progreso" },
|
||||
{ names: ['proceso <nombre>'], desc: "Ejecutar procesos técnicos automatizados" },
|
||||
{ names: ['estados'], desc: "Listar tareas en proceso (⏳)" },
|
||||
{ names: ['conocimiento <subcomando>'], desc: "Gestión de Base de Conocimiento" },
|
||||
{ names: ['ayuda [subcomando]'], desc: "Mostrar esta ayuda o ayuda específica" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run ayuda jornada", desc: "Ver ayuda detallada del subcomando jornada" },
|
||||
{ cmd: "./adn/tools/run ayuda nodos", desc: "Ver ayuda detallada del subcomando nodos" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
puts ""
|
||||
puts "#{Color::DIM}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}"
|
||||
puts " evolucion <propuesta.md> Gestionar propuesta de evolución del ADN"
|
||||
puts " triggers [--test] [--force] Ejecutar triggers de sincronización"
|
||||
puts ""
|
||||
puts "#{Color::DIM}Para ayuda detallada de un subcomando:#{Color::RESET}"
|
||||
puts " ./adn/tools/run ayuda <subcomando>"
|
||||
puts ""
|
||||
puts "#{Color::DIM}Documentación completa:#{Color::RESET} docs/plan/260307-1400_Plan_Mejoras_ADN.md"
|
||||
end
|
||||
|
||||
#{Color::YELLOW}Subcomandos disponibles:#{Color::RESET}
|
||||
validador [--watch] [--fix] Validar cumplimiento del ADN en bitácoras
|
||||
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
|
||||
generar bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD)
|
||||
generar nodo <nombre> Generar ficha de nodo
|
||||
msp <subcomando> Gestión de MSPs NotebookLM (auditar, listar, registrar)
|
||||
#{Color::YELLOW}generar proyecto <código>#{Color::RESET} Generar manifiesto de proyecto
|
||||
#{Color::YELLOW}tailscale <acción>#{Color::RESET} Gestión de identidades Tailscale
|
||||
#{Color::YELLOW}backup <nodo> [--mode MODO]#{Color::RESET} Ejecutar backup seguro de un nodo
|
||||
#{Color::YELLOW}db <subcomando>#{Color::RESET} Operaciones de base de datos
|
||||
#{Color::YELLOW}triggers [opciones]#{Color::RESET} Gestión del motor de triggers
|
||||
#{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}Subcomandos en desarrollo (Fases futuras):#{Color::RESET}
|
||||
evolucion <propuesta.md> Gestionar propuesta de evolución del ADN
|
||||
triggers [--test] [--force] Ejecutar triggers de sincronización
|
||||
|
||||
#{Color::DIM}Para ayuda detallada de un subcomando:#{Color::RESET}
|
||||
./adn/tools/run ayuda <subcomando>
|
||||
|
||||
#{Color::DIM}Documentación completa:#{Color::RESET} docs/plan/260307-1400_Plan_Mejoras_ADN.md
|
||||
AYUDA_PRINCIPAL
|
||||
end
|
||||
|
||||
def mostrar_ayuda_subcomando(subcomando)
|
||||
case subcomando.downcase
|
||||
when 'validador'
|
||||
puts "Uso: ./adn/tools/run validador [--watch] [--fix]"
|
||||
puts ""
|
||||
puts "Valida el cumplimiento del ADN en las bitácoras:"
|
||||
puts " • Formato de tiempo (HH:MM, sin \"hs\")"
|
||||
puts " • Iconografía de estados y semántica correcta"
|
||||
puts " • Presencia de métricas híbridas ([Físico/Remoto:])"
|
||||
puts " • Referencias bidireccionales en rollovers"
|
||||
puts " • Estructura de tablas en bitácoras"
|
||||
puts ""
|
||||
puts "Opciones:"
|
||||
puts " --watch Monitorizar cambios en tiempo real (Fase 1 completa)"
|
||||
puts " --fix Intentar corregir errores automáticamente"
|
||||
puts ""
|
||||
puts "Ejemplo:"
|
||||
puts " ./adn/tools/run validador"
|
||||
puts " ./adn/tools/run validador --fix"
|
||||
when 'contexto'
|
||||
puts "Uso: ./adn/tools/run contexto <nombre-nodo> [--cmd]"
|
||||
puts ""
|
||||
puts "Obtiene información contextual de un nodo:"
|
||||
puts " • IP y configuración de red"
|
||||
puts " • Rol en la topología"
|
||||
puts " • Comandos SSH sugeridos"
|
||||
puts ""
|
||||
puts "Ejemplo:"
|
||||
puts " ./adn/tools/run contexto srv-pmox1"
|
||||
puts " ./adn/tools/run contexto srvv-fenix --cmd"
|
||||
when 'nodos'
|
||||
puts "Uso: ./adn/tools/run nodos <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'
|
||||
puts "Uso: ./adn/tools/run salud [--dashboard] [--json]"
|
||||
puts ""
|
||||
puts "Muestra métricas de salud del sistema ADN:"
|
||||
puts " • Cantidad de hebras, bitácoras, nodos, proyectos"
|
||||
puts " • Errores recientes en logs"
|
||||
puts ""
|
||||
puts "Ejemplo:"
|
||||
puts " ./adn/tools/run salud"
|
||||
puts " ./adn/tools/run salud --json"
|
||||
when 'generar'
|
||||
puts "Uso: ./adn/tools/run generar <tipo> [argumentos]"
|
||||
puts ""
|
||||
puts "Tipos disponibles:"
|
||||
puts " bitacora [fecha] - Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)"
|
||||
puts " nodo <nombre> - Crea ficha de nodo"
|
||||
puts " proyecto <código> - Crea manifiesto de proyecto"
|
||||
puts ""
|
||||
puts "Ejemplos:"
|
||||
puts " ./adn/tools/run generar bitacora"
|
||||
puts " ./adn/tools/run generar bitacora 2026-03-08"
|
||||
puts " ./adn/tools/run generar nodo srv-nuevo"
|
||||
puts " ./adn/tools/run generar proyecto P2604"
|
||||
when 'proceso'
|
||||
puts "Uso: ./adn/tools/run proceso <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."
|
||||
when 'tailscale', 'ts'
|
||||
puts "Uso: ./adn/tools/run tailscale <acción> [perfil]"
|
||||
puts ""
|
||||
puts "Gestión de identidades y perfiles de Tailscale:"
|
||||
puts " • switch <perfil> Alternar entre cuentas (dasuten, personal)"
|
||||
puts " • status Ver estado y cuenta activa"
|
||||
puts " • list Listar perfiles en caché local"
|
||||
puts ""
|
||||
puts "Ejemplos:"
|
||||
puts " ./adn/tools/run tailscale switch dasuten"
|
||||
puts " ./adn/tools/run tailscale status"
|
||||
when 'msp'
|
||||
puts "Uso: ./adn/tools/run msp <subcomando> [opciones]"
|
||||
puts ""
|
||||
puts "Gestión de MSPs (Markdown Style Papers) de NotebookLM:"
|
||||
puts " • auditar Verificar estado de conexión MCP y NotebookLM"
|
||||
puts " • listar Listar MSPs disponibles en el sistema"
|
||||
puts " • registrar Registrar un MSP descargado"
|
||||
puts ""
|
||||
puts "Opciones:"
|
||||
puts " --archivo RUTA Ruta al archivo MSP (para registrar)"
|
||||
puts " --titulo TITULO Título del MSP (opcional)"
|
||||
puts ""
|
||||
puts "Ejemplos:"
|
||||
puts " ./adn/tools/run msp auditar"
|
||||
puts " ./adn/tools/run msp listar"
|
||||
puts " ./adn/tools/run msp registrar --archivo docs/mcps/mi-msp.md"
|
||||
else
|
||||
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
|
||||
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
|
||||
def mostrar_ayuda_subcomando(subcomando)
|
||||
case subcomando.downcase
|
||||
when 'validador'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Validador de ADN",
|
||||
descripcion: "Valida el cumplimiento del ADN en las bitácoras",
|
||||
uso: "./adn/tools/run validador [--watch] [--fix]",
|
||||
opciones: [
|
||||
{ names: ['--watch'], desc: "Monitorizar cambios en tiempo real (Fase 1 completa)" },
|
||||
{ names: ['--fix'], desc: "Intentar corregir errores automáticamente" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run validador", desc: "Ejecutar validador estándar" },
|
||||
{ cmd: "./adn/tools/run validador --fix", desc: "Ejecutar validador con corrección automática" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
puts ""
|
||||
puts "El validador verifica:"
|
||||
puts " • Formato de tiempo (HH:MM, sin \"hs\")"
|
||||
puts " • Iconografía de estados y semántica correcta"
|
||||
puts " • Presencia de métricas híbridas ([Físico/Remoto:])"
|
||||
puts " • Referencias bidireccionales en rollovers"
|
||||
puts " • Estructura de tablas en bitácoras"
|
||||
|
||||
when 'contexto'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Contexto de Nodo",
|
||||
descripcion: "Obtiene información contextual de un nodo",
|
||||
uso: "./adn/tools/run contexto <nombre-nodo> [--cmd]",
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run contexto srv-pmox1", desc: "Obtener contexto del nodo srv-pmox1" },
|
||||
{ cmd: "./adn/tools/run contexto srvv-fenix --cmd", desc: "Obtener contexto con comandos SSH sugeridos" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
puts ""
|
||||
puts "Información que proporciona:"
|
||||
puts " • IP y configuración de red"
|
||||
puts " • Rol en la topología"
|
||||
puts " • Comandos SSH sugeridos"
|
||||
|
||||
when 'nodos'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Nodos",
|
||||
descripcion: "Gestión y consulta de nodos de infraestructura",
|
||||
uso: "./adn/tools/run nodos <acción> [opciones]",
|
||||
opciones: [
|
||||
{ names: ['agrupar [--servidor SRV]'], desc: "Agrupar VMs por servidor anfitrión" },
|
||||
{ names: ['listar [--tipo TIPO]'], desc: "Listar fichas de nodos (.md)" },
|
||||
{ names: ['buscar <término>'], desc: "Buscar nodos por nombre o contenido" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run nodos agrupar", desc: "Agrupar todos los VMs por servidor" },
|
||||
{ cmd: "./adn/tools/run nodos agrupar --servidor srv-pmox1", desc: "Agrupar VMs del servidor srv-pmox1" },
|
||||
{ cmd: "./adn/tools/run nodos listar --tipo srvv", desc: "Listar nodos de tipo srvv" },
|
||||
{ cmd: "./adn/tools/run nodos buscar dasuten", desc: "Buscar nodos relacionados con dasuten" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
when 'jornada'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Jornada Laboral",
|
||||
descripcion: "Registro y control de jornada laboral",
|
||||
uso: "./adn/tools/run jornada <acción> [modo] [hora] [opciones]",
|
||||
opciones: [
|
||||
{ names: ['iniciar [presencial|remoto] [HH:MM]'], desc: "Registrar inicio de jornada" },
|
||||
{ names: ['cerrar [presencial|remoto] [HH:MM]'], desc: "Registrar cierre de jornada" },
|
||||
{ names: ['estado'], desc: "Ver estado de la jornada actual" },
|
||||
{ names: ['--nodo NOMBRE'], desc: "Nodo a registrar (default: dtic-DIIAA)" },
|
||||
{ names: ['--fecha YYYY-MM-DD'], desc: "Fecha de la bitácora (default: hoy)" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run jornada iniciar presencial 08:00", desc: "Iniciar jornada presencial a las 08:00" },
|
||||
{ cmd: "./adn/tools/run jornada cerrar remoto 23:00", desc: "Cerrar jornada remota a las 23:00" },
|
||||
{ cmd: "./adn/tools/run jornada estado", desc: "Ver estado actual de la jornada" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
when 'salud'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Salud del Sistema ADN",
|
||||
descripcion: "Muestra métricas de salud del sistema ADN",
|
||||
uso: "./adn/tools/run salud [--dashboard] [--json]",
|
||||
opciones: [
|
||||
{ names: ['--dashboard'], desc: "Mostrar dashboard de salud (por implementar)" },
|
||||
{ names: ['--json'], desc: "Salida en formato JSON" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run salud", desc: "Mostrar reporte de salud estándar" },
|
||||
{ cmd: "./adn/tools/run salud --json", desc: "Mostrar reporte de salud en formato JSON" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
puts ""
|
||||
puts "Métricas mostradas:"
|
||||
puts " • Cantidad de hebras, bitácoras, nodos, proyectos"
|
||||
puts " • Errores recientes en logs"
|
||||
|
||||
when 'generar'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Generador de Componentes ADN",
|
||||
descripcion: "Genera nuevos componentes en el ecosistema ADN",
|
||||
uso: "./adn/tools/run generar <tipo> [argumentos]",
|
||||
opciones: [
|
||||
{ names: ['bitacora [fecha]'], desc: "Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" },
|
||||
{ names: ['nodo <nombre>'], desc: "Crea ficha de nodo" },
|
||||
{ names: ['proyecto <código>'], desc: "Crea manifiesto de proyecto" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run generar bitacora", desc: "Crear bitácora para hoy" },
|
||||
{ cmd: "./adn/tools/run generar bitacora 2026-03-08", desc: "Crear bitácora para el 8 de marzo de 2026" },
|
||||
{ cmd: "./adn/tools/run generar nodo srv-nuevo", desc: "Crear ficha para el nodo srv-nuevo" },
|
||||
{ cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
when 'proceso'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Procesos Automatizados ADN",
|
||||
descripcion: "Ejecuta procesos técnicos automatizados sobre la infraestructura",
|
||||
uso: "./adn/tools/run proceso <nombre_proceso> [argumentos]",
|
||||
opciones: [
|
||||
{ names: ['nodos:actualizar_so <nodo>'], desc: "Actualiza paquetes y parches de seguridad" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run proceso nodos:actualizar_so srv-ns8", desc: "Actualizar SO en el nodo srv-ns8" },
|
||||
{ cmd: "./adn/tools/run proceso nodos:actualizar_so srv-dasu", desc: "Actualizar SO en el nodo srv-dasu" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
when 'triggers'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Triggers ADN",
|
||||
descripcion: "Gestiona el sistema de triggers para sincronización automática del ADN",
|
||||
uso: "./adn/tools/run triggers [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['--test [TRIGGER_ID]'], desc: "Probar un trigger específico (modo simulación)" },
|
||||
{ names: ['--forzar'], desc: "Ejecutar triggers incluso si sistema deshabilitado" },
|
||||
{ names: ['--listar'], desc: "Listar todos los triggers configurados" },
|
||||
{ names: ['--registrar'], desc: "Registrar triggers en el bus de eventos" },
|
||||
{ names: ['--limpiar'], desc: "Limpiar todos los triggers registrados" },
|
||||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run triggers --listar", desc: "Listar todos los triggers configurados" },
|
||||
{ cmd: "./adn/tools/run triggers --registrar", desc: "Registrar triggers en el bus de eventos" },
|
||||
{ cmd: "./adn/tools/run triggers --test bitacoras_0", desc: "Probar trigger bitacoras_0 (modo simulación)" },
|
||||
{ cmd: "./adn/tools/run triggers --forzar", desc: "Forzar ejecución de triggers" },
|
||||
{ cmd: "./adn/tools/run triggers --limpiar", desc: "Limpiar todos los triggers registrados" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
when 'estados'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Listado de Tareas en Proceso",
|
||||
descripcion: "Consulta la bitácora web para listar todos los items con estado ⏳ (En proceso)",
|
||||
uso: "./adn/tools/run estados",
|
||||
descripcion_adicional: "Ideal para monitorear procesos automatizados o jornadas abiertas."
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
when 'tailscale', 'ts'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Identidades Tailscale",
|
||||
descripcion: "Gestión de identidades y perfiles de Tailscale",
|
||||
uso: "./adn/tools/run tailscale <acción> [perfil]",
|
||||
opciones: [
|
||||
{ names: ['switch <perfil>'], desc: "Alternar entre cuentas (dasuten, personal)" },
|
||||
{ names: ['status'], desc: "Ver estado y cuenta activa" },
|
||||
{ names: ['list'], desc: "Listar perfiles en caché local" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run tailscale switch dasuten", desc: "Cambiar a la cuenta dasuten" },
|
||||
{ cmd: "./adn/tools/run tailscale status", desc: "Ver estado actual de Tailscale" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
puts ""
|
||||
puts "Perfiles disponibles:"
|
||||
puts " • dasuten: Ecosistema DASUTEN (pcdasu0@frlr.utn.edu.ar)"
|
||||
puts " • personal: Otros ecosistemas (rmonla@gmail.com)"
|
||||
|
||||
when 'msp'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de MSPs - NotebookLM",
|
||||
descripcion: "Herramientas para auditar, registrar y listar MSPs (Markdown Style Papers)",
|
||||
uso: "./adn/tools/run msp <subcomando> [opciones]",
|
||||
opciones: [
|
||||
{ names: ['auditar'], desc: "Verificar estado de conexión MCP y NotebookLM" },
|
||||
{ names: ['registrar'], desc: "Registrar un MSP descargado en la base de conocimiento", default: "--archivo <ruta>" },
|
||||
{ names: ['listar'], desc: "Listar MSPs disponibles en el sistema" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run msp auditar", desc: "Verificar estado de MCP y NotebookLM" },
|
||||
{ cmd: "./adn/tools/run msp listar", desc: "Listar MSPs disponibles" },
|
||||
{ cmd: "./adn/tools/run msp registrar --archivo /ruta/msp.md", desc: "Registrar un MSP" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
when 'conocimiento'
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Base de Conocimiento",
|
||||
descripcion: "Permite inyectar, organizar e indexar nueva documentación en el motor autónomo.",
|
||||
uso: "./adn/tools/run conocimiento <subcomando> [opciones]",
|
||||
opciones: [
|
||||
{ names: ['asimilar <archivo.md>'], desc: "Ingiere un documento Markdown a la base cognitiva del ADN." }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run conocimiento asimilar docs/configuracion_bd.md", desc: "Asimilar documentación de configuración de BD" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
|
||||
else
|
||||
puts "#{Color::RED}✗ Subcomando desconocido: #{subcomando}#{Color::RESET}"
|
||||
puts "Usa './adn/tools/run ayuda' para ver la lista de subcomandos."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+39
-35
@@ -4,13 +4,15 @@ require 'optparse'
|
||||
require 'fileutils'
|
||||
require 'date'
|
||||
require_relative 'backup/configurador'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
module ADN
|
||||
class SubcomandoBackup
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
|
||||
|
||||
# Cargar valores predeterminados desde config.yml
|
||||
defaults = Configurador.obtener('backup.defaults') || {}
|
||||
@opciones = {
|
||||
@@ -22,6 +24,12 @@ module ADN
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
# Handle help flags
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Uso: ./adn/tools/run backup <nodo|estados> [OPCIONES]"
|
||||
opts.separator ""
|
||||
@@ -107,7 +115,7 @@ module ADN
|
||||
# Usar popen para leer la salida en tiempo real
|
||||
@logger.info("Conectando y monitoreando salida de Proxmox...")
|
||||
|
||||
clave = (@nodo[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
clave = (datos[:host_name] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
IO.popen("ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1") do |pf|
|
||||
pf.each_line do |line|
|
||||
# Imprimir flujo para el usuario
|
||||
@@ -609,46 +617,42 @@ module ADN
|
||||
|
||||
@logger.info("Auto-cerrando backup de #{nodo_actual} (Task ID: #{tarea['id']})")
|
||||
cerrar_entrada_especifica(nodo_actual, hora_inicio_str, exito, tarea['id'], hora_fin)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Backup ADN",
|
||||
descripcion: "Ejecuta un backup remoto o consulta el estado actual.",
|
||||
uso: "./adn/tools/run backup <nodo|estados> [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['<nodo>'], desc: "Ejecutar backup del nodo especificado" },
|
||||
{ names: ['estados'], desc: "Ver estado actual de los backups en Proxmox" },
|
||||
{ names: ['sanear'], desc: "Limpiar backups antiguos según retención" },
|
||||
{ names: ['predeterminar'], desc: "Configurar valores por defecto" },
|
||||
{ names: ['--mode MODO'], desc: "Modo de backup (stop, snapshot, suspend)" },
|
||||
{ names: ['--storage STORAGE'], desc: "Nombre del storage en Proxmox" },
|
||||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run backup srv-dasu", desc: "Ejecutar backup del nodo srv-dasu" },
|
||||
{ cmd: "./adn/tools/run backup estados", desc: "Ver estado de backups en Proxmox" },
|
||||
{ cmd: "./adn/tools/run backup sanear", desc: "Limpiar backups antiguos" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cerrar_entrada_especifica(nodo, hora_ini, exito, task_id, hora_fin)
|
||||
fecha = Date.today.iso8601
|
||||
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
|
||||
return unless File.exist?(ruta)
|
||||
|
||||
estado_ico = exito ? '✅' : '⚠️'
|
||||
desc = exito ? 'Backup completado exitosamente.' : 'Fallo en la ejecución del backup.'
|
||||
desc = "[task:#{task_id[0..7]}] #{desc}" if task_id
|
||||
|
||||
linea_nueva = "| #{hora_ini} | #{hora_fin} | #{desc} | R | #{estado_ico} |"
|
||||
|
||||
contenido = File.read(ruta)
|
||||
# Buscamos la fila específica que coincida con el nodo Y la hora de inicio
|
||||
# Capturamos el bloque del nodo para evitar errores si otros nodos tienen la misma hora
|
||||
regex_seccion = /(### #{Regexp.escape(nodo)}.*?)(?=^###\s|<!--|\z)/m
|
||||
seccion = contenido.match(regex_seccion)&.[](0)
|
||||
unless seccion
|
||||
return
|
||||
end
|
||||
|
||||
# Regex para la fila exacta que tenga la hora de inicio Y un reloj ⏳ en la última columna
|
||||
regex_fila = /\|\s*#{Regexp.escape(hora_ini)}\s*\|\s*\|\s*[^|]*\|\s*[^|]*\|\s*[^|]*⏳[^|]*\|/
|
||||
|
||||
if seccion.match?(regex_fila)
|
||||
@logger.exito("Reconciliado: #{nodo} (#{hora_ini} -> #{hora_fin})")
|
||||
nueva_seccion = seccion.sub(regex_fila, linea_nueva)
|
||||
# Si ya no quedan ⏳ en esta sección, actualizamos su icono #### ⏳ -> ✅
|
||||
# Buscamos cualquier reloj de arena que quede en la sección
|
||||
unless nueva_seccion.include?('⏳')
|
||||
nueva_seccion.gsub!(/#### [⏳⏳] - BKP-ADN/, "#### #{estado_ico} - BKP-ADN")
|
||||
end
|
||||
|
||||
nuevo_contenido = contenido.gsub(seccion, nueva_seccion)
|
||||
File.write(ruta, nuevo_contenido)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,654 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'optparse'
|
||||
require 'fileutils'
|
||||
require 'date'
|
||||
require_relative 'backup/configurador'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
module ADN
|
||||
class SubcomandoBackup
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
|
||||
# Cargar valores predeterminados desde config.yml
|
||||
defaults = Configurador.obtener('backup.defaults') || {}
|
||||
@opciones = {
|
||||
modo: defaults['modo'] || 'stop',
|
||||
storage: defaults['storage'] || 'local',
|
||||
compress: defaults['compress'] || 'gzip',
|
||||
retencion: defaults['retencion_dias'] || 6
|
||||
}
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
# Handle help flags
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
parser = OptionParser.new do |opts|
|
||||
opts.banner = "Uso: ./adn/tools/run backup <nodo|estados> [OPCIONES]"
|
||||
opts.separator ""
|
||||
opts.separator "Ejecuta un backup remoto o consulta el estado actual."
|
||||
opts.separator ""
|
||||
opts.separator "Acciones:"
|
||||
opts.separator " <nodo> Ejecutar backup del nodo especificado"
|
||||
opts.separator " estados Ver estado actual de los backups en Proxmox"
|
||||
opts.separator " sanear Limpiar backups antiguos según retención"
|
||||
opts.separator " predeterminar Configurar valores por defecto"
|
||||
opts.separator ""
|
||||
opts.separator "Opciones:"
|
||||
opts.on("--mode MODO", "Modo de backup (stop, snapshot, suspend. Default: #{@opciones[:modo]})") do |m|
|
||||
@opciones[:modo] = m
|
||||
end
|
||||
opts.on("--storage STORAGE", "Nombre del storage en Proxmox (Default: #{@opciones[:storage]})") do |s|
|
||||
@opciones[:storage] = s
|
||||
end
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
parser.parse!(@args)
|
||||
rescue OptionParser::InvalidOption => e
|
||||
@logger.error("Opción inválida: #{e.message}")
|
||||
puts parser
|
||||
exit 1
|
||||
end
|
||||
|
||||
if @args.empty?
|
||||
@logger.error("Se requiere nombre de nodo o acción")
|
||||
puts parser
|
||||
exit 1
|
||||
end
|
||||
|
||||
case @args[0]
|
||||
when 'predeterminar'
|
||||
comando_predeterminar(@args[1..-1])
|
||||
when 'estados', 'estado'
|
||||
comando_estados
|
||||
when 'sanear'
|
||||
comando_sanear
|
||||
else
|
||||
ejecutar_backup(@args[0])
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ejecutar_backup(nodo)
|
||||
@logger.info("Iniciando flujo ADN-Backup para nodo: #{nodo}")
|
||||
|
||||
# 1. Obtener contexto del nodo
|
||||
datos = obtener_contexto_nodo(nodo)
|
||||
unless datos
|
||||
@logger.error("No se pudo determinar el contexto técnico para #{nodo}")
|
||||
exit 1
|
||||
end
|
||||
|
||||
@logger.info("Contexto validado: VM #{datos[:vmid]} en #{datos[:host_name]} (#{datos[:host_ip]})")
|
||||
|
||||
# 2. Registrar inicio en bitácora (Bitácora-First)
|
||||
registrar_inicio_bitacora(nodo, datos)
|
||||
|
||||
# 3. Ejecución segura vía candados
|
||||
@logger.info("Solicitando autorización y ejecutando backup remoto...")
|
||||
|
||||
comando_vzdump = "vzdump #{datos[:vmid]} --mode #{@opciones[:modo]} --storage #{@opciones[:storage]} --compress #{@opciones[:compress]}"
|
||||
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{datos[:host_ip]} \"#{comando_vzdump}\""
|
||||
|
||||
# Ruta absoluta a candados
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
|
||||
|
||||
# Ejecutar con candados autorizando primero
|
||||
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||
|
||||
task_id = nil
|
||||
full_output = ""
|
||||
|
||||
# Usar popen para leer la salida en tiempo real
|
||||
@logger.info("Conectando y monitoreando salida de Proxmox...")
|
||||
|
||||
clave = (datos[:host_name] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
IO.popen("ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1") do |pf|
|
||||
pf.each_line do |line|
|
||||
# Imprimir flujo para el usuario
|
||||
puts " #{Color::DIM}#{line.strip}#{Color::RESET}"
|
||||
full_output += line
|
||||
|
||||
# Si detectamos el Task ID al inicio, lo registramos de inmediato
|
||||
if task_id.nil? && match = line.match(/started backup task '([^']+)'/)
|
||||
task_id = match[1]
|
||||
@logger.info("Task ID detectado: #{task_id}. Registrando en bitácora...")
|
||||
registrar_task_id_bitacora(nodo, task_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
success = $?.success?
|
||||
|
||||
# 4. Finalizar y cerrar
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
|
||||
if success
|
||||
@logger.exito("Backup de #{nodo} completado exitosamente.")
|
||||
registrar_fin_bitacora(nodo, true, task_id)
|
||||
else
|
||||
@logger.error("La ejecución del backup falló.")
|
||||
registrar_fin_bitacora(nodo, false, task_id)
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def obtener_contexto_nodo(nombre_nodo)
|
||||
ruta_nodo = File.join(ADN::NODOS_DIR, "#{nombre_nodo}.md")
|
||||
unless File.exist?(ruta_nodo)
|
||||
@logger.error("No existe el archivo de nodo: #{ruta_nodo}")
|
||||
@logger.sugerencia("./adn/tools/run db nodo:crear --nombre #{nombre_nodo} --tipo servidor --ip 0.0.0.0 --desc \"Servidor a configurar\"")
|
||||
return nil
|
||||
end
|
||||
|
||||
contenido = File.read(ruta_nodo)
|
||||
vmid = contenido.match(/(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i)&.[](1)
|
||||
host_name = contenido.match(/(?:Padre\/Host|Host Anfitrión|Anfitrión|Host)\*\*:?\s*`([^`]+)`/i)&.[](1)
|
||||
|
||||
unless vmid && host_name
|
||||
@logger.error("Contexto incompleto en #{nombre_nodo}.md: VMID=#{vmid}, Host=#{host_name}")
|
||||
@logger.sugerencia([
|
||||
"Asegurate de que #{nombre_nodo}.md tenga las siguientes líneas:",
|
||||
"- **Padre/Host**: `srv-proxmox-ejemplo`",
|
||||
"- **VMID**: `1xx`"
|
||||
])
|
||||
return nil
|
||||
end
|
||||
|
||||
# Buscar IP del host
|
||||
ruta_host = File.join(ADN::NODOS_DIR, "#{host_name}.md")
|
||||
host_ip = nil
|
||||
if File.exist?(ruta_host)
|
||||
c_host = File.read(ruta_host)
|
||||
host_ip ||= c_host.match(/\|\s*IP[^\|]*\|\s*`([^`]+)`\s*\|/)&.[](1)
|
||||
host_ip ||= c_host.match(/IP\*\*:?\s*`([^`]+)`/)&.[](1)
|
||||
host_ip ||= c_host.match(/IP:\s*([\d\.]+)/)&.[](1)
|
||||
end
|
||||
|
||||
unless host_ip
|
||||
@logger.error("No se encontró IP para el host #{host_name}")
|
||||
@logger.sugerencia([
|
||||
"Verificá que el archivo #{host_name}.md exista.",
|
||||
"Si no existe, crealo con: ./adn/tools/run db nodo:crear --nombre #{host_name} --tipo servidor --ip DIRECCION_IP",
|
||||
"Asegurate de que incluya: **IP**: `DIRECCION_IP`"
|
||||
])
|
||||
return nil
|
||||
end
|
||||
|
||||
host_ip = host_ip.split('/')[0] if host_ip.include?('/')
|
||||
{ vmid: vmid, host_name: host_name, host_ip: host_ip }
|
||||
end
|
||||
|
||||
def registrar_inicio_bitacora(nodo, datos)
|
||||
require_relative '../db/core/bitacora_db'
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
# Asegurar bitácora
|
||||
bitacora = db.find_bitacora_by_fecha(Date.today)
|
||||
bitacora ||= db.create_bitacora(fecha: Date.today)
|
||||
|
||||
# Buscar nodo en DB
|
||||
nodo_db = db.find_nodo_by_nombre(nodo)
|
||||
|
||||
hora = Time.now.strftime('%H:%M')
|
||||
entrada = db.create_entrada(
|
||||
inicio: hora,
|
||||
descripcion: "Inicio de backup remoto vía SSH para VM #{datos[:vmid]} en #{datos[:host_name]}.",
|
||||
estado: '⏳',
|
||||
modo: 'R',
|
||||
bitacora_id: bitacora['id'],
|
||||
nodo_id: nodo_db ? nodo_db['id'] : nil
|
||||
)
|
||||
|
||||
# Guardar ID para actualizar después
|
||||
@backup_entrada_id = entrada['id'].to_i
|
||||
@backup_bitacora_id = bitacora['id'].to_i
|
||||
end
|
||||
rescue => e
|
||||
@logger.error("Error registrando inicio en DB: #{e.message}")
|
||||
end
|
||||
|
||||
def registrar_task_id_bitacora(nodo, task_id)
|
||||
return unless @backup_entrada_id
|
||||
|
||||
require_relative '../db/core/bitacora_db'
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
db.update_entrada(@backup_entrada_id, {
|
||||
descripcion: "[task:#{task_id[0..7]}] Backup remoto en ejecución."
|
||||
})
|
||||
end
|
||||
rescue => e
|
||||
@logger.error("Error registrando task_id en DB: #{e.message}")
|
||||
end
|
||||
|
||||
def registrar_fin_bitacora(nodo, exito, task_id = nil)
|
||||
return unless @backup_entrada_id
|
||||
|
||||
require_relative '../db/core/bitacora_db'
|
||||
require_relative '../db/exporters/md_exporter'
|
||||
|
||||
hora_fin = Time.now.strftime('%H:%M')
|
||||
estado = exito ? '✅' : '⚠️'
|
||||
desc = exito ? 'Backup completado exitosamente.' : 'Fallo en la ejecución del backup.'
|
||||
desc = "[task:#{task_id[0..7]}] #{desc}" if task_id
|
||||
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
db.update_entrada(@backup_entrada_id, {
|
||||
fin: hora_fin,
|
||||
descripcion: desc,
|
||||
estado: estado
|
||||
})
|
||||
end
|
||||
rescue => e
|
||||
@logger.error("Error registrando fin en DB: #{e.message}")
|
||||
end
|
||||
|
||||
|
||||
def comando_predeterminar(args)
|
||||
if args.length < 2
|
||||
puts "#{Color::YELLOW}Uso: adn backup predeterminar <parametro> <valor>#{Color::RESET}"
|
||||
puts "Ej: adn backup predeterminar modo snapshot"
|
||||
exit 1
|
||||
end
|
||||
|
||||
param = args[0].downcase
|
||||
valor = args[1]
|
||||
param = 'modo' if param == 'mode'
|
||||
param = 'retencion_dias' if param == 'edad'
|
||||
|
||||
unless ['modo', 'storage', 'compress', 'retencion_dias'].include?(param)
|
||||
@logger.error("Parámetro no predeterminable: #{param}")
|
||||
exit 1
|
||||
end
|
||||
|
||||
Configurador.establecer("backup.defaults.#{param}", valor)
|
||||
@logger.exito("Valor predeterminado establecido: backup.#{param} = #{valor}")
|
||||
puts "#{Color::GREEN}✓ Se ha predeterminado el #{param} a '#{valor}' para futuros backups.#{Color::RESET}"
|
||||
end
|
||||
|
||||
def comando_estados
|
||||
@logger.info("Consultando estado de backups en cluster Proxmox")
|
||||
|
||||
# Lista de hosts a consultar
|
||||
hosts = [
|
||||
{ nombre: 'srv-pmox1', ip: '10.0.10.201' },
|
||||
{ nombre: 'srv-dasu', ip: '100.116.210.36' },
|
||||
]
|
||||
|
||||
puts "\n#{Color::BOLD}🔍 Estado de Backups en Tiempo Real (Proxmox)#{Color::RESET}"
|
||||
puts "─" * 60
|
||||
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
|
||||
# Asegurar autorización general para el lote
|
||||
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||
|
||||
all_tasks_accumulated = []
|
||||
|
||||
hosts.each do |h|
|
||||
print "📡 Conectando a #{h[:nombre]} (#{h[:ip]})... "
|
||||
|
||||
# Intentamos obtener tareas
|
||||
cmd = "/usr/bin/pvesh get /cluster/tasks --output-format json"
|
||||
cmd_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{h[:ip]} '#{cmd}'"
|
||||
|
||||
# Ejecutamos vía candados run usando la clave adecuada
|
||||
clave = (h[:nombre] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
output = `ruby #{candados_path} run #{clave} SSHPASS '#{cmd_seguro}' 2>&1`
|
||||
|
||||
if output.include?('[')
|
||||
begin
|
||||
# Extraer el JSON
|
||||
json_str = output[output.index('[')..output.rindex(']')]
|
||||
tasks_host = JSON.parse(json_str)
|
||||
all_tasks_accumulated.concat(tasks_host)
|
||||
|
||||
# Filtrar tareas vzdump activas de ESTE host para mostrar
|
||||
tasks_activas = tasks_host.select { |t| t['type'] == 'vzdump' && t['endtime'].nil? }
|
||||
|
||||
puts "#{Color::GREEN}OK#{Color::RESET}"
|
||||
if tasks_activas.empty?
|
||||
puts " #{Color::DIM}Sin tareas de backup activas en este host.#{Color::RESET}"
|
||||
else
|
||||
tasks_activas.each do |t|
|
||||
msg = t['status'] || 'ejecutando'
|
||||
print " ▶ #{Color::CYAN}#{t['id']}#{Color::RESET} "
|
||||
puts "[#{msg}] (Iniciado: #{Time.at(t['starttime']).strftime('%H:%M:%S')})"
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
puts "#{Color::YELLOW}JSON ERROR#{Color::RESET}"
|
||||
puts " #{Color::DIM}Error parseando respuesta: #{e.message}#{Color::RESET}"
|
||||
end
|
||||
else
|
||||
puts "#{Color::RED}ERROR#{Color::RESET}"
|
||||
if output.include?('Permission denied')
|
||||
puts " #{Color::DIM}Error de permisos/candado puesto.#{Color::RESET}"
|
||||
else
|
||||
puts " #{Color::DIM}No se detectaron tareas activas o el host no respondió.#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
|
||||
# 2. Sincronización Automática: Auto-reconciliación y Migración
|
||||
fecha_hoy = Date.today.iso8601
|
||||
archivo_md = File.join(ADN::BITACORAS_DIR, "#{fecha_hoy}.md")
|
||||
|
||||
if File.exist?(archivo_md)
|
||||
print "#{Color::DIM}🔄 Reconciliando tareas pendientes... #{Color::RESET}"
|
||||
auto_reconciliar_bitacora(all_tasks_accumulated)
|
||||
puts "#{Color::DIM}OK#{Color::RESET}"
|
||||
|
||||
print "#{Color::DIM}🔄 Sincronizando bitácora digital... #{Color::RESET}"
|
||||
# Ejecutamos la migración de forma silenciosa
|
||||
cmd_migrar = "#{File.join(ADN::PROJECT_ROOT, 'adn/tools/run')} db migrar:md #{fecha_hoy} --sobrescribir > /dev/null 2>&1"
|
||||
system(cmd_migrar)
|
||||
puts "#{Color::DIM}OK#{Color::RESET}"
|
||||
end
|
||||
|
||||
# 3. Historial de la DB (Sincronizado)
|
||||
puts "\n#{Color::BOLD}📅 Historial de hoy (Sincronizado)#{Color::RESET}"
|
||||
puts "─" * 60
|
||||
|
||||
db_tool = "./adn/tools/run"
|
||||
# Buscamos por "backup" o "BKP"
|
||||
system("#{db_tool} db evento:listar --desde '#{fecha_hoy}' --buscar 'ackup'")
|
||||
|
||||
puts "\n#{Color::DIM}Para historial completo use: ./adn/tools/run db evento:listar --desde '#{fecha_hoy}'#{Color::RESET}"
|
||||
end
|
||||
|
||||
def comando_sanear
|
||||
dias = @opciones[:retencion].to_i
|
||||
storage = @opciones[:storage]
|
||||
@logger.info("Iniciando saneamiento de backups (Retención: #{dias} días en '#{storage}')")
|
||||
|
||||
# 1. Obtener lista de VMs directamente de Proxmox para nombres precisos
|
||||
host = { nombre: 'srv-pmox1', ip: '10.0.10.201' }
|
||||
candados_path = File.join(ADN::PROJECT_ROOT, 'adn', 'tools', 'seguridad', 'candados.rb')
|
||||
system("ruby #{candados_path} authorize > /dev/null 2>&1")
|
||||
|
||||
@logger.info("Consultando inventario de VMs en Proxmox...")
|
||||
cmd_vms = "/usr/bin/pvesh get /cluster/resources --type vm --output-format json"
|
||||
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
|
||||
|
||||
begin
|
||||
if output_vms.include?('[')
|
||||
json_vms = output_vms[output_vms.index('[')..output_vms.rindex(']')]
|
||||
JSON.parse(json_vms).each do |vm|
|
||||
id = vm['vmid'].to_s
|
||||
# Priorizar nombre de Proxmox si el local no existe o es genérico
|
||||
vmid_map[id] = vm['name'] if vm['name'] && (!vmid_map[id] || vmid_map[id] == id)
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
@logger.advertencia("No se pudo completar el mapa de VMs desde Proxmox: #{e.message}")
|
||||
end
|
||||
|
||||
# 2. Obtener lista de archivos en el storage
|
||||
# Intentar con varios hosts del cluster por si uno está caído
|
||||
ruta_dump = "/mnt/pve/#{storage}/dump"
|
||||
@logger.info("Listando archivos en #{storage}...")
|
||||
|
||||
output_ls = ""
|
||||
[ '10.0.10.201', '10.0.10.202', '10.0.10.203' ].each do |ip|
|
||||
cmd_ls = "ls -1 --full-time #{ruta_dump}/vzdump-*"
|
||||
res = `ruby #{candados_path} run admindasu SSHPASS 'sshpass -e ssh -o StrictHostKeyChecking=no root@#{ip} "#{cmd_ls}"' 2>&1`
|
||||
unless res.include?('No such file') || res.include?('Connection refused')
|
||||
output_ls = res
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if output_ls.empty? || output_ls.include?('No such file')
|
||||
@logger.error("No se encontró la ruta de backups en ningún host: #{ruta_dump}")
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
exit 1
|
||||
end
|
||||
|
||||
# 3. Filtrar archivos por antigüedad
|
||||
ahora = Time.now
|
||||
limite = ahora - (dias * 24 * 60 * 60)
|
||||
|
||||
backups_agrupados = {}
|
||||
|
||||
output_ls.split("\n").each do |linea|
|
||||
next if linea.include?('OPTIONS]') || linea.empty?
|
||||
|
||||
partes = linea.split(/\s+/)
|
||||
next if partes.length < 9
|
||||
|
||||
fecha_str = "#{partes[5]} #{partes[6]}"
|
||||
ruta_archivo = partes[8..-1].join(' ')
|
||||
nombre = File.basename(ruta_archivo)
|
||||
|
||||
fingerprint = nombre.sub(/\.(log|vma\.zst|tar\.zst|vma\.zst\.notes|tar\.zst\.notes)$/, '')
|
||||
|
||||
begin
|
||||
mtime = Time.parse(fecha_str)
|
||||
if mtime < limite
|
||||
vmid = fingerprint.match(/vzdump-(?:qemu|lxc)-(\d+)-/)&.[](1)
|
||||
nodo_humano = vmid ? vmid_map[vmid] : "ID:#{vmid}"
|
||||
|
||||
backups_agrupados[fingerprint] ||= {
|
||||
fingerprint: fingerprint,
|
||||
fecha: mtime,
|
||||
vmid: vmid,
|
||||
nodo: nodo_humano,
|
||||
archivos: []
|
||||
}
|
||||
backups_agrupados[fingerprint][:archivos] << { ruta: ruta_archivo, nombre: nombre }
|
||||
end
|
||||
rescue
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
if backups_agrupados.empty?
|
||||
puts "#{Color::GREEN}✓ No se encontraron backups mayores a #{dias} días en #{storage}.#{Color::RESET}"
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
return
|
||||
end
|
||||
|
||||
# Convertir a array ordenado por fecha
|
||||
lista_final = backups_agrupados.values.sort_by { |b| b[:fecha] }
|
||||
|
||||
# 4. Mostrar lista con nuevo formato solicitado
|
||||
puts "\n#{Color::BOLD}🗑️ Propuesta de Saneamiento de Backups#{Color::RESET} (Retención: #{dias} días)"
|
||||
puts "─" * 90
|
||||
puts "#{Color::BOLD}ID Nombre_VMID_Fecha Componentes detectados#{Color::RESET}"
|
||||
puts "─" * 90
|
||||
|
||||
lista_final.each_with_index do |b, idx|
|
||||
num = (idx + 1).to_s.rjust(2, '0')
|
||||
fecha_corta = b[:fecha].strftime('%Y-%m-%d')
|
||||
|
||||
# Formato solicitado: XX - VMNombre_VMid_Fecha
|
||||
entrada_label = "#{num} - #{Color::BOLD}#{Color::CYAN}#{b[:nodo]}_#{b[:vmid]}_#{fecha_corta}#{Color::RESET}".ljust(60)
|
||||
|
||||
tipos = b[:archivos].map { |a| a[:nombre].split('.').last }.uniq.join(", ")
|
||||
total_archivos = "(#{b[:archivos].length} arch.)"
|
||||
|
||||
puts "#{entrada_label} #{Color::DIM}#{tipos.ljust(15)} #{total_archivos}#{Color::RESET}"
|
||||
end
|
||||
puts "─" * 90
|
||||
puts "Total detectado: #{lista_final.length} backups completos (#{backups_agrupados.values.sum { |b| b[:archivos].length }} archivos en total)."
|
||||
|
||||
puts "\n#{Color::BOLD}Acciones:#{Color::RESET}"
|
||||
puts " • Escriba #{Color::BOLD}'S'#{Color::RESET} para borrar TODOS los backups listados."
|
||||
puts " • Escriba #{Color::BOLD}excluir 1, 3-5#{Color::RESET} para conservar backups específicos."
|
||||
puts " • Escriba #{Color::BOLD}'N'#{Color::RESET} para cancelar."
|
||||
|
||||
print "\n👉 #{Color::BOLD}¿Qué deseas hacer?#{Color::RESET}: "
|
||||
input = STDIN.gets.chomp.strip.downcase
|
||||
|
||||
if input == 'n' || input == ''
|
||||
puts "#{Color::YELLOW}Operación cancelada.#{Color::RESET}"
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
exit
|
||||
end
|
||||
|
||||
# Procesar exclusiones
|
||||
seleccionados = lista_final.dup
|
||||
if input.start_with?('excluir')
|
||||
exclusiones = []
|
||||
patron = input.sub('excluir', '').strip
|
||||
|
||||
patron.split(/[\s,]+/).each do |p|
|
||||
next if p.empty?
|
||||
if p.include?('-')
|
||||
inicio, fin = p.split('-').map(&:to_i)
|
||||
exclusiones.concat((inicio..fin).to_a)
|
||||
else
|
||||
exclusiones << p.to_i
|
||||
end
|
||||
end
|
||||
|
||||
seleccionados = lista_final.reject.with_index { |_, idx| exclusiones.include?(idx + 1) }
|
||||
|
||||
if seleccionados.empty?
|
||||
puts "#{Color::YELLOW}Has excluido todos los backups. Nada que hacer.#{Color::RESET}"
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
exit
|
||||
end
|
||||
|
||||
puts "#{Color::CYAN}ℹ Has excluido #{lista_final.length - seleccionados.length} backups. Procediendo con #{seleccionados.length}...#{Color::RESET}"
|
||||
elsif input != 's'
|
||||
puts "#{Color::RED}Opción no válida. Cancelando.#{Color::RESET}"
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
exit
|
||||
end
|
||||
|
||||
# 5. Borrado final
|
||||
total_a_borrar = seleccionados.sum { |b| b[:archivos].length }
|
||||
puts "\n#{Color::BOLD}🗑️ Borrando #{total_a_borrar} archivos asociados a #{seleccionados.length} backups...#{Color::RESET}"
|
||||
|
||||
seleccionados.each_with_index do |b, i|
|
||||
puts "\n #{Color::BOLD}[#{i+1}/#{seleccionados.length}] Procesando #{b[:nodo]}...#{Color::RESET}"
|
||||
b[:archivos].each do |a|
|
||||
print " Eliminando #{a[:nombre]}... "
|
||||
del_cmd = "rm -f '#{a[:ruta]}'"
|
||||
del_seguro = "sshpass -e ssh -o StrictHostKeyChecking=no root@#{host[:ip]} '#{del_cmd}'"
|
||||
clave = (b[:nodo] == 'srv-dasu' ? 'srv-dasu:rmonla' : 'admindasu')
|
||||
system("ruby #{candados_path} run #{clave} SSHPASS '#{del_seguro}' > /dev/null 2>&1")
|
||||
puts "#{Color::GREEN}OK#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
|
||||
@logger.exito("Saneamiento completado. Se limpiaron #{seleccionados.length} sets de backup.")
|
||||
system("ruby #{candados_path} cerrar > /dev/null 2>&1")
|
||||
end
|
||||
|
||||
def cargar_mapa_vmid_nodos
|
||||
mapa = {}
|
||||
Dir.glob(File.join(ADN::NODOS_DIR, "*.md")).each do |f|
|
||||
next if File.directory?(f)
|
||||
begin
|
||||
contenido = File.read(f)
|
||||
# Buscar nombre en h1 o metadatos
|
||||
nombre = File.basename(f, ".md")
|
||||
vmid = contenido.match(/(?:VM ID|vmid|VMID)\*\*?:?\s*`?(\d+)`?/i)&.[](1)
|
||||
mapa[vmid.to_s] = nombre if vmid
|
||||
rescue
|
||||
next
|
||||
end
|
||||
end
|
||||
mapa
|
||||
end
|
||||
|
||||
def auto_reconciliar_bitacora(all_tasks)
|
||||
fecha = Date.today.iso8601
|
||||
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
|
||||
return unless File.exist?(ruta)
|
||||
|
||||
contenido = File.read(ruta)
|
||||
vmid_map = cargar_mapa_vmid_nodos
|
||||
|
||||
# Encontrar todas las filas que tienen un reloj de arena
|
||||
lineas = contenido.lines
|
||||
nodo_actual = nil
|
||||
|
||||
lineas.each do |linea|
|
||||
if linea.start_with?('### srvv-')
|
||||
nodo_actual = linea.sub('### ', '').strip
|
||||
next
|
||||
end
|
||||
|
||||
# Si es una fila de tabla en progreso (⏳)
|
||||
if nodo_actual && linea.include?('|') && linea.include?('⏳')
|
||||
match_hora = linea.match(/\|\s*(\d{2}:\d{2})\s*\|/)
|
||||
next unless match_hora
|
||||
|
||||
hora_inicio_str = match_hora[1]
|
||||
vmid = vmid_map.key(nodo_actual)
|
||||
next unless vmid
|
||||
|
||||
# Buscar última tarea vzdump de este VMID que ya terminó
|
||||
hora_inicio_t = Time.parse("#{fecha} #{hora_inicio_str}") rescue nil
|
||||
|
||||
tarea = all_tasks.select { |t|
|
||||
vmid_task = (t['vmid'] || t['id']).to_s
|
||||
t['type'] == 'vzdump' &&
|
||||
vmid_task == vmid &&
|
||||
!t['endtime'].nil? &&
|
||||
(hora_inicio_t ? Time.at(t['starttime']) >= (hora_inicio_t - 3600) : true)
|
||||
}.max_by { |t| t['endtime'] }
|
||||
|
||||
if tarea
|
||||
exito = (tarea['status'] == 'OK')
|
||||
hora_fin = Time.at(tarea['endtime']).strftime('%H:%M')
|
||||
|
||||
@logger.info("Auto-cerrando backup de #{nodo_actual} (Task ID: #{tarea['id']})")
|
||||
cerrar_entrada_especifica(nodo_actual, hora_inicio_str, exito, tarea['id'], hora_fin)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Backup ADN",
|
||||
descripcion: "Ejecuta un backup remoto o consulta el estado actual.",
|
||||
uso: "./adn/tools/run backup <nodo|estados> [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['<nodo>'], desc: "Ejecutar backup del nodo especificado" },
|
||||
{ names: ['estados'], desc: "Ver estado actual de los backups en Proxmox" },
|
||||
{ names: ['sanear'], desc: "Limpiar backups antiguos según retención" },
|
||||
{ names: ['predeterminar'], desc: "Configurar valores por defecto" },
|
||||
{ names: ['--mode MODO'], desc: "Modo de backup (stop, snapshot, suspend)" },
|
||||
{ names: ['--storage STORAGE'], desc: "Nombre del storage en Proxmox" },
|
||||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run backup srv-dasu", desc: "Ejecutar backup del nodo srv-dasu" },
|
||||
{ cmd: "./adn/tools/run backup estados", desc: "Ver estado de backups en Proxmox" },
|
||||
{ cmd: "./adn/tools/run backup sanear", desc: "Limpiar backups antiguos" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cerrar_entrada_especifica(nodo, hora_ini, exito, task_id, hora_fin)
|
||||
fecha = Date.today.iso8601
|
||||
ruta = File.join(ADN::BITACORAS_DIR, "#{fecha}.md")
|
||||
return unless File.exist?(ruta)
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../candados/candados'
|
||||
|
||||
module ADN
|
||||
class SubcomandoCandados
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
# Llamamos al help interno de candados.rb
|
||||
system("ruby #{File.join(File.dirname(__FILE__), '../candados/candados.rb')} help")
|
||||
return
|
||||
end
|
||||
|
||||
# Ejecutamos el script de candados pasando los argumentos
|
||||
# Nota: candados.rb maneja internamente la seguridad y auditoría
|
||||
script_path = File.join(File.dirname(__FILE__), '../candados/candados.rb')
|
||||
system("ruby #{script_path} #{@args.join(' ')}")
|
||||
|
||||
# Propagamos el exit status si es un comando de ejecución (run/sudo)
|
||||
if ['run', 'sudo'].include?(@args[0])
|
||||
exit $?.exitstatus || 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,6 +8,8 @@
|
||||
require 'optparse'
|
||||
require 'fileutils'
|
||||
require 'date'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
module ADN
|
||||
class SubcomandoConocimiento
|
||||
@@ -19,7 +21,8 @@ module ADN
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty? || @args[0] == 'help'
|
||||
# Handle help flags
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
@@ -164,14 +167,19 @@ module ADN
|
||||
end
|
||||
|
||||
def mostrar_ayuda
|
||||
puts "#{Color::CYAN}🧠 Herramienta ADN - Gestión de Base de Conocimiento#{Color::RESET}"
|
||||
puts "Permite inyectar, organizar e indexar nueva documentación en el motor autónomo."
|
||||
puts ""
|
||||
puts "#{Color::BOLD}SUBCOMANDOS:#{Color::RESET}"
|
||||
puts " #{Color::CYAN}asimilar <archivo.md>#{Color::RESET} Ingiere un documento Markdown a la base cognitiva del ADN."
|
||||
puts ""
|
||||
puts "#{Color::BOLD}EJEMPLOS:#{Color::RESET}"
|
||||
puts " ./adn/tools/run conocimiento asimilar docs/configuracion_bd.md"
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Base de Conocimiento",
|
||||
descripcion: "Permite inyectar, organizar e indexar nueva documentación en el motor autónomo.",
|
||||
uso: "./adn/tools/run conocimiento <subcomando> [opciones]",
|
||||
opciones: [
|
||||
{ names: ['asimilar <archivo.md>'], desc: "Ingiere un documento Markdown a la base cognitiva del ADN." }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run conocimiento asimilar docs/configuracion_bd.md", desc: "Asimilar documentación de configuración de BD" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
require 'optparse'
|
||||
require_relative '../core/colores'
|
||||
require_relative 'nodos/info'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
class SubcomandoContexto
|
||||
def initialize(args, logger)
|
||||
@@ -12,10 +13,10 @@ class SubcomandoContexto
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
@logger.error("Se requiere nombre de nodo")
|
||||
puts "Uso: ./adn/tools/run contexto <nombre-nodo> [--cmd]"
|
||||
exit 1
|
||||
# Handle help flags
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
nodo = @args[0]
|
||||
@@ -30,10 +31,6 @@ class SubcomandoContexto
|
||||
opts.on("--cmd", "Generar comando SSH con ProxyJump si es necesario") do
|
||||
@opciones[:cmd] = true
|
||||
end
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
@@ -49,6 +46,25 @@ class SubcomandoContexto
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Contexto de Nodo",
|
||||
descripcion: "Obtiene información contextual de un nodo",
|
||||
uso: "./adn/tools/run contexto <nombre-nodo> [--cmd]",
|
||||
opciones: [
|
||||
{ names: ['--cmd'], desc: "Generar comando SSH con ProxyJump si es necesario" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run contexto srv-pmox1", desc: "Obtener contexto del nodo srv-pmox1" },
|
||||
{ cmd: "./adn/tools/run contexto srvv-fenix --cmd", desc: "Obtener contexto con comandos SSH sugeridos" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def obtener_contexto(nombre_nodo)
|
||||
@logger.info("Buscando contexto para nodo: #{nombre_nodo}")
|
||||
|
||||
|
||||
+140
-1
@@ -23,6 +23,8 @@
|
||||
require 'optparse'
|
||||
require 'date'
|
||||
require 'time'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
# Cargar módulo BitacorasDB
|
||||
begin
|
||||
@@ -59,7 +61,144 @@ module ADN
|
||||
|
||||
# Ejecuta el subcomando según los argumentos proporcionados
|
||||
def ejecutar
|
||||
if @args.empty? || @args[0] == 'help'
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
subcomando = @args[0]
|
||||
sub_args = @args[1..-1] || []
|
||||
|
||||
case subcomando
|
||||
when 'salud:bd'
|
||||
comando_salud_bd
|
||||
when 'bitacora:crear'
|
||||
comando_bitacora_crear(sub_args)
|
||||
when 'bitacora:listar'
|
||||
comando_bitacora_listar(sub_args)
|
||||
when 'evento:crear'
|
||||
comando_evento_crear(sub_args)
|
||||
when 'evento:listar'
|
||||
comando_evento_listar(sub_args)
|
||||
when 'evento:actualizar'
|
||||
comando_evento_actualizar(sub_args)
|
||||
when 'evento:eliminar'
|
||||
comando_evento_eliminar(sub_args)
|
||||
when 'nodo:crear'
|
||||
comando_nodo_crear(sub_args)
|
||||
when 'nodo:listar'
|
||||
comando_nodo_listar(sub_args)
|
||||
when 'hito:listar'
|
||||
comando_hito_listar(sub_args)
|
||||
when 'hito:actualizar'
|
||||
comando_hito_actualizar(sub_args)
|
||||
when 'hito:renombrar'
|
||||
comando_hito_renombrar(sub_args)
|
||||
when 'hito:ordenar'
|
||||
comando_hito_ordenar(sub_args)
|
||||
when 'estadisticas'
|
||||
comando_estadisticas
|
||||
when 'backup'
|
||||
comando_backup(sub_args)
|
||||
when 'archivar:md'
|
||||
comando_archivar_md
|
||||
|
||||
else
|
||||
@logger.error("Subcomando desconocido: #{subcomando}")
|
||||
puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}"
|
||||
puts ""
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
end
|
||||
rescue Interrupt
|
||||
puts "\n#{Color::YELLOW}⚠ Operación interrumpida por el usuario#{Color::RESET}"
|
||||
@logger.info("Operación de base de datos interrumpida por usuario")
|
||||
exit 130
|
||||
rescue => e
|
||||
@logger.error("Error en operación de base de datos: #{e.message}", { backtrace: e.backtrace })
|
||||
puts "#{Color::RED}✗ Error: #{e.message}#{Color::RESET}"
|
||||
puts "#{Color::DIM}#{e.backtrace.first}#{Color::RESET}" if ENV['ADN_DEBUG']
|
||||
exit 1
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# ─── COMANDOS PRINCIPALES ──────────────────────────────────────────────
|
||||
|
||||
# ─── MÉTODOS AUXILIARES ──────────────────────────────────────────────
|
||||
|
||||
# Carga la configuración desde config.yml
|
||||
def cargar_configuracion
|
||||
config_path = File.join(__dir__, 'config.yml')
|
||||
return {} unless File.exist?(config_path)
|
||||
|
||||
begin
|
||||
YAML.load_file(config_path)
|
||||
rescue => e
|
||||
@logger.error("Error cargando configuración: #{e.message}")
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
# Muestra la ayuda general del subcomando
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Operaciones de Base de Datos ADN",
|
||||
descripcion: "Integra las operaciones de base de datos PostgreSQL con la CLI ADN",
|
||||
uso: "./adn/tools/run db [SUBCOMANDO] [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['salud:bd'], desc: "Verificar conexión y estado de la BD" },
|
||||
{ names: ['backup [OPCIONES]'], desc: "Crear backup de la base de datos" },
|
||||
{ names: ['bitacora:crear [FECHA]'], desc: "Crear nueva bitácora diaria" },
|
||||
{ names: ['bitacora:listar [OPCIONES]'], desc: "Listar bitácoras existentes" },
|
||||
{ names: ['evento:crear [OPCIONES]'], desc: "Crear nueva entrada cronológica" },
|
||||
{ names: ['evento:listar [OPCIONES]'], desc: "Listar entradas con filtros" },
|
||||
{ names: ['nodo:crear [OPCIONES]'], desc: "Crear nuevo nodo (servidor/VM/PC)" },
|
||||
{ names: ['nodo:listar [OPCIONES]'], desc: "Listar nodos del sistema" },
|
||||
{ names: ['estadisticas'], desc: "Mostrar estadísticas del sistema" },
|
||||
{ names: ['archivar:md'], desc: "Mueve los archivos .md migrados al archivo histórico" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run db salud:bd", desc: "Verificar conexión y estado de la BD" },
|
||||
{ cmd: "./adn/tools/run db bitacora:crear --resumen \"Inicio de proyecto\"", desc: "Crear bitácora con resumen" },
|
||||
{ cmd: "./adn/tools/run db evento:crear --inicio 09:00 --descripcion \"Reunión\" --nodo srv-ns8", desc: "Crear entrada de evento" },
|
||||
{ cmd: "./adn/tools/run db nodo:listar --tipo servidor", desc: "Listar nodos de tipo servidor" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
puts ""
|
||||
puts "#{Color::YELLOW}⚠ Nota:#{Color::RESET} Requiere PostgreSQL en ejecución (puerto 5433)"
|
||||
puts " Iniciar contenedor: #{Color::DIM}cd dtic-BITACORAs && docker-compose up -d#{Color::RESET}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ADN
|
||||
class SubcomandoDB
|
||||
require_relative 'db/bitacora'
|
||||
require_relative 'db/evento'
|
||||
require_relative 'db/nodo'
|
||||
require_relative 'db/hito'
|
||||
require_relative 'db/misc'
|
||||
include SubcomandoDBBitacora
|
||||
include SubcomandoDBEvento
|
||||
include SubcomandoDBNodo
|
||||
include SubcomandoDBHito
|
||||
include SubcomandoDBMisc
|
||||
# Inicializa el subcomando con argumentos y logger
|
||||
#
|
||||
# @param args [Array<String>] Argumentos de línea de comandos
|
||||
# @param logger [ADNLogger] Instancia de logger para registro
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
@config = cargar_configuracion
|
||||
end
|
||||
|
||||
# Ejecuta el subcomando según los argumentos proporcionados
|
||||
def ejecutar
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
require 'date'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
class SubcomandoGenerar
|
||||
def initialize(args, logger)
|
||||
@@ -38,12 +39,24 @@ class SubcomandoGenerar
|
||||
private
|
||||
|
||||
def mostrar_ayuda_generar
|
||||
puts "Uso: ./adn/tools/run generar <tipo> [argumentos]"
|
||||
puts ""
|
||||
puts "Tipos disponibles:"
|
||||
puts " bitacora [fecha] Generar nueva bitácora (fecha: YYYY-MM-DD, default: hoy)"
|
||||
puts " nodo <nombre> Generar ficha de nodo"
|
||||
puts " proyecto <código> Generar manifiesto de proyecto"
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Generador de Componentes ADN",
|
||||
descripcion: "Genera nuevos componentes en el ecosistema ADN",
|
||||
uso: "./adn/tools/run generar <tipo> [argumentos]",
|
||||
opciones: [
|
||||
{ names: ['bitacora [fecha]'], desc: "Crea nueva bitácora (fecha: YYYY-MM-DD, default: hoy)" },
|
||||
{ names: ['nodo <nombre>'], desc: "Crea ficha de nodo" },
|
||||
{ names: ['proyecto <código>'], desc: "Crea manifiesto de proyecto" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run generar bitacora", desc: "Crear bitácora para hoy" },
|
||||
{ cmd: "./adn/tools/run generar bitacora 2026-03-08", desc: "Crear bitácora para el 8 de marzo de 2026" },
|
||||
{ cmd: "./adn/tools/run generar nodo srv-nuevo", desc: "Crear ficha para el nodo srv-nuevo" },
|
||||
{ cmd: "./adn/tools/run generar proyecto P2604", desc: "Crear manifiesto para el proyecto P2604" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
|
||||
def generar_bitacora
|
||||
@@ -210,6 +223,16 @@ class SubcomandoGenerar
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| 1 | *Primer hito* | 📍 | *Fecha* |
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progreso
|
||||
|
||||
```
|
||||
Fase 1: ░░░░░░░░░░ 0% Nombre de la Fase
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard
|
||||
*URL del panel de seguimiento web (si aplica)*
|
||||
|
||||
|
||||
+20
-22
@@ -13,6 +13,7 @@ require 'time'
|
||||
require 'optparse'
|
||||
require_relative '../db/core/bitacora_db'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
module ADN
|
||||
class SubcomandoJornada
|
||||
@@ -57,28 +58,25 @@ module ADN
|
||||
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
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Jornada Laboral",
|
||||
descripcion: "Registro y control de jornada laboral",
|
||||
uso: "./adn/tools/run jornada <acción> [modo] [hora] [opciones]",
|
||||
opciones: [
|
||||
{ names: ['iniciar [presencial|remoto] [HH:MM]'], desc: "Registrar inicio de jornada" },
|
||||
{ names: ['cerrar [presencial|remoto] [HH:MM]'], desc: "Registrar cierre de jornada" },
|
||||
{ names: ['estado'], desc: "Ver estado de la jornada actual" },
|
||||
{ names: ['--nodo NOMBRE'], desc: "Nodo a registrar (default: #{NODO_DEFAULT})" },
|
||||
{ names: ['--fecha YYYY-MM-DD'], desc: "Fecha de la bitácora (default: hoy)" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run jornada iniciar presencial 08:00", desc: "Iniciar jornada presencial a las 08:00" },
|
||||
{ cmd: "./adn/tools/run jornada cerrar remoto 23:00", desc: "Cerrar jornada remota a las 23:00" },
|
||||
{ cmd: "./adn/tools/run jornada estado", desc: "Ver estado actual de la jornada" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
|
||||
def parsear_modo_y_hora
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# adn/tools/cli/network.rb — Herramienta para topología de red estandarizada
|
||||
# ==========================================================================
|
||||
# Esta herramienta provee una vista consolidada de la topología de red del
|
||||
# sistema ADN, eliminando la necesidad de búsquedas exploratorias en /nodos.
|
||||
#
|
||||
# Uso:
|
||||
# ./adn/tools/run network show
|
||||
# ./adn/tools/run network show --proyecto P2604
|
||||
# ./adn/tools/run network show --nodo srv-ns8
|
||||
# ./adn/tools/run network show --json
|
||||
#
|
||||
# Proporciona información estructurada sobre:
|
||||
# - Nodos y sus IPs, roles, saltos SSH
|
||||
# - Proyectos activos y sus hitos
|
||||
# - Dependencias entre componentes
|
||||
# - Estado de candados necesarios
|
||||
|
||||
require 'optparse'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/constants'
|
||||
require_relative '../core/help_formatter'
|
||||
require_relative 'nodos/info'
|
||||
require_relative 'db/db'
|
||||
|
||||
module ADN
|
||||
class SubcomandoNetwork
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
@opciones = {
|
||||
formato: :texto, # :texto, :json, :markdown
|
||||
proyecto: nil,
|
||||
nodo: nil,
|
||||
detalle: false
|
||||
}
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
accion = @args.shift
|
||||
|
||||
case accion
|
||||
when 'show'
|
||||
mostrar_topologia
|
||||
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 crear_parser
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Uso: ./adn/tools/run network show [OPCIONES]"
|
||||
opts.separator ""
|
||||
opts.separator "Muestra la topología de red estandarizada del sistema ADN"
|
||||
opts.separator ""
|
||||
opts.separator "Opciones:"
|
||||
|
||||
opts.on("--formato FORMATO", [:texto, :json, :markdown], "Formato de salida (texto, json, markdown)") do |formato|
|
||||
@opciones[:formato] = formato
|
||||
end
|
||||
|
||||
opts.on("--proyecto CODIGO", "Filtrar por código de proyecto (ej: P2604)") do |codigo|
|
||||
@opciones[:proyecto] = codigo.upcase
|
||||
end
|
||||
|
||||
opts.on("--nodo NOMBRE", "Mostrar detalles de un nodo específico") do |nombre|
|
||||
@opciones[:nodo] = nombre
|
||||
end
|
||||
|
||||
opts.on("--detalle", "Mostrar información detallada incluyendo métricas") do
|
||||
@opciones[:detalle] = true
|
||||
end
|
||||
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
mostrar_ayuda
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Topología de Red ADN",
|
||||
descripcion: "Muestra la topología de red estandarizada del sistema ADN",
|
||||
uso: "./adn/tools/run network show [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['--formato FORMATO'], desc: "Formato de salida (texto, json, markdown)" },
|
||||
{ names: ['--proyecto CODIGO'], desc: "Filtrar por código de proyecto (ej: P2604)" },
|
||||
{ names: ['--nodo NOMBRE'], desc: "Mostrar detalles de un nodo específico" },
|
||||
{ names: ['--detalle'], desc: "Mostrar información detallada incluyendo métricas" },
|
||||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run network show", desc: "Mostrar topología completa en formato texto" },
|
||||
{ cmd: "./adn/tools/run network show --json", desc: "Mostrar topología en formato JSON" },
|
||||
{ cmd: "./adn/tools/run network show --proyecto P2604", desc: "Mostrar topología filtrada por proyecto P2604" },
|
||||
{ cmd: "./adn/tools/run network show --nodo srv-ns8 --detalle", desc: "Mostrar detalles del nodo srv-ns8" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
|
||||
def mostrar_topologia
|
||||
parser = crear_parser
|
||||
parser.parse!(@args)
|
||||
|
||||
datos = recopilar_datos_topologia
|
||||
|
||||
case @opciones[:formato]
|
||||
when :json
|
||||
puts JSON.pretty_generate(datos)
|
||||
when :markdown
|
||||
puts generar_markdown(datos)
|
||||
else
|
||||
puts generar_texto(datos)
|
||||
end
|
||||
end
|
||||
|
||||
def recopilar_datos_topologia
|
||||
{
|
||||
timestamp: Time.now.iso8601,
|
||||
nodos: obtener_informacion_nodos,
|
||||
proyectos: obtener_informacion_proyectos,
|
||||
estadisticas: obtener_estadisticas_sistema
|
||||
}
|
||||
end
|
||||
|
||||
def obtener_informacion_nodos
|
||||
nodos = []
|
||||
|
||||
# Obtener nodos desde fichas .md
|
||||
Dir.glob(File.join(ADN::NODOS_DIR, '*.md')).each do |archivo|
|
||||
nodo_nombre = File.basename(archivo, '.md')
|
||||
next if nodo_nombre.start_with?('_')
|
||||
|
||||
# Filtrar por nodo específico si se indicó
|
||||
next if @opciones[:nodo] && @opciones[:nodo] != nodo_nombre
|
||||
|
||||
metadata = ADN::NodosInfo.extraer_metadata(nodo_nombre)
|
||||
next unless metadata && metadata[:ip] # Solo nodos con IP configurada
|
||||
|
||||
nodo_info = {
|
||||
nombre: nodo_nombre,
|
||||
ip: metadata[:ip],
|
||||
ip_lan: metadata[:ip_lan],
|
||||
hostname: metadata[:nombre],
|
||||
so: metadata[:so]&.to_s,
|
||||
puerto_ssh: metadata[:puerto_ssh],
|
||||
usuario: metadata[:usuario],
|
||||
auth_type: metadata[:auth_type]&.to_s,
|
||||
clave_boveda: metadata[:clave_boveda],
|
||||
host: metadata[:host], # Para identificar VMs y su hypervisor
|
||||
rol: determinar_rol_nodo(metadata),
|
||||
proxysalto: metadata[:host] # Si tiene host, necesita proxy jump
|
||||
}
|
||||
|
||||
# Añadir información de proyectos si se filtró por proyecto
|
||||
if @opciones[:proyecto]
|
||||
nodo_info[:proyectos_relacionados] = obtener_proyectos_relacionados_nodo(nodo_nombre)
|
||||
end
|
||||
|
||||
nodos << nodo_info
|
||||
end
|
||||
|
||||
nodos.sort_by { |n| n[:nombre] }
|
||||
end
|
||||
|
||||
def determinar_rol_nodo(metadata)
|
||||
caso metadata[:so]&.to_s
|
||||
when 'windows'
|
||||
if metadata[:nombre]&.start_with?('srvv')
|
||||
'Servidor Windows Virtualizado'
|
||||
elsif metadata[:nombre]&.start_with?('dasu-srvv')
|
||||
'Controlador de Dominio / Servicios'
|
||||
else
|
||||
'Workstation Windows'
|
||||
end
|
||||
when 'linux'
|
||||
if metadata[:nombre]&.start_with?('srv-') && !metadata[:nombre]&.start_with?('srvv')
|
||||
'Servidor Linux Físico'
|
||||
elsif metadata[:nombre]&.start_with?('srv-pmox')
|
||||
'Hipertensor Proxmox'
|
||||
elsif metadata[:nombre]&.start_with?('pcv')
|
||||
'Máquina Virtual Linux'
|
||||
elsif metadata[:nombre]&.start_with?('dtic-bitacoras')
|
||||
'Servidor de Base de Datos y Bitácoras'
|
||||
else
|
||||
'Servicio Linux'
|
||||
end
|
||||
else
|
||||
'Nodo Desconocido'
|
||||
end
|
||||
end
|
||||
|
||||
def obtener_informacion_proyectos
|
||||
return [] unless @opciones[:proyecto]
|
||||
|
||||
proyectos = []
|
||||
begin
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
proyecto = db.find_proyecto_by_codigo(@opciones[:proyecto])
|
||||
if proyecto
|
||||
hitos = db.list_hitos(@opciones[:proyecto])
|
||||
|
||||
proyectos << {
|
||||
codigo: proyecto['codigo'],
|
||||
nombre: proyecto['nombre'],
|
||||
descripcion: proyecto['descripcion'],
|
||||
estado: proyecto['estado'],
|
||||
hitos: hitos.map do |hito|
|
||||
{
|
||||
id: hito['id_hito'],
|
||||
titulo: hito['titulo'],
|
||||
estado: hito['estado'],
|
||||
horas_presencial: hito['horas_presencial'],
|
||||
horas_remoto: hito['horas_remoto']
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
@logger&.warn("Error obteniendo información de proyectos: #{e.message}")
|
||||
end
|
||||
|
||||
proyectos
|
||||
end
|
||||
|
||||
def obtener_estadisticas_sistema
|
||||
estadisticas = {}
|
||||
|
||||
begin
|
||||
BitacorasDB::BitacoraDB.with_connection do |db|
|
||||
stats = db.statistics
|
||||
estadisticas = {
|
||||
bitacoras: stats[:bitacoras] || 0,
|
||||
entradas: stats[:entradas] || 0,
|
||||
nodos: stats[:nodos] || 0,
|
||||
temas: stats[:temas] || 0,
|
||||
entradas_hoy: obtener_entradas_hoy(db)
|
||||
}
|
||||
end
|
||||
rescue => e
|
||||
@logger&.warn("Error obteniendo estadísticas: #{e.message}")
|
||||
estadisticas = { error: "No se pudieron obtener estadísticas" }
|
||||
end
|
||||
|
||||
estadisticas
|
||||
end
|
||||
|
||||
def obtener_entradas_hoy(db)
|
||||
hoy = Date.today.to_s
|
||||
resultado = db.execute("SELECT COUNT(*) as total FROM bitacoras.entradas e JOIN bitacoras.bitacoras b ON e.bitacora_id = b.id WHERE b.fecha = $1", [hoy])
|
||||
resultado.first['total'].to_i
|
||||
end
|
||||
|
||||
def obtener_proyectos_relacionados_nodo(nodo_nombre)
|
||||
# En una implementación completa, esto consultaría la base de datos
|
||||
# para encontrar proyectos asociados a este nodo
|
||||
# Por ahora, retornamos una lista vacía
|
||||
[]
|
||||
end
|
||||
|
||||
def generar_texto(datos)
|
||||
salida = []
|
||||
salida << "#{Color::CYAN}🌐 TOPOLOGÍA DE RED ADN - #{datos[:timestamp]}#{Color::RESET}"
|
||||
salida << "#{Color::DIM}#{'-' * 80}#{Color::RESET}"
|
||||
|
||||
# Estadísticas del sistema
|
||||
salida << "#{Color::YELLOW}📊 ESTADÍSTICAS DEL SISTEMA#{Color::RESET}"
|
||||
stats = datos[:estadisticas]
|
||||
salida << " Bitácoras: #{stats[:bitacoras]}"
|
||||
salida << " Entradas: #{stats[:entradas]}"
|
||||
salida << " Entradas hoy: #{stats[:entradas_hoy]}"
|
||||
salida << " Nodos: #{stats[:nodos]}"
|
||||
salida << " Temas: #{stats[:temas]}"
|
||||
salida << ""
|
||||
|
||||
# Nodos
|
||||
salida << "#{Color::YELLOW}🖥️ NODOS CONFIGURADOS (#{datos[:nodos].length})#{Color::RESET}"
|
||||
datos[:nodos].each do |nodo|
|
||||
estado_ssh = nodo[:ip] ? "#{Color::GREEN}✓#{Color::RESET}" : "#{Color::RED}✗#{Color::RESET}"
|
||||
salida << " #{estado_ssh} #{nodo[:nombre]} (#{nodo[:rol]})"
|
||||
salida << " IP: #{nodo[:ip] || '(no configurada)'}"
|
||||
salida << " SSH: #{nodo[:usuario]}@#{nodo[:ip]}:#{nodo[:puerto_ssh]}" if nodo[:ip]
|
||||
salida << " Auth: #{nodo[:auth_type] || '(no configurado)'}" if nodo[:ip]
|
||||
salida << " Bóveda: #{nodo[:clave_boveda] || '(no configurada)'}" if nodo[:ip]
|
||||
|
||||
if nodo[:proxysalto]
|
||||
salida << " ↳ Requiere proxy jump vía: #{nodo[:proxysalto]}"
|
||||
end
|
||||
|
||||
salida << ""
|
||||
end
|
||||
|
||||
# Proyectos (si se filtró)
|
||||
unless datos[:proyectos].empty?
|
||||
salida << "#{Color::YELLOW}📋 PROYECTOS RELACIONADOS#{Color::RESET}"
|
||||
datos[:proyectos].each do |proyecto|
|
||||
salida << " #{proyecto[:codigo]} - #{proyecto[:nombre]}"
|
||||
salida << " Estado: #{proyecto[:estado]}"
|
||||
salida << " Hitos: #{proyecto[:hitos].length}"
|
||||
|
||||
if @opciones[:detalle]
|
||||
proyecto[:hitos].each do |hito|
|
||||
salida << " [#{hito[:id]}] #{hito[:titulo]} (#{hito[:estado]})"
|
||||
end
|
||||
end
|
||||
end
|
||||
salida << ""
|
||||
end
|
||||
|
||||
salida << "#{Color::DIM}#{'-' * 80}#{Color::RESET}"
|
||||
salida << "#{Color::CYAN}💡 Para conexión SSH directa: ./adn/tools/run ssh <nodo> <comando>#{Color::RESET}"
|
||||
salida << "#{Color::DIM}Nota: Los candados se gestionan automáticamente mediante adn candados#{Color::RESET}"
|
||||
|
||||
salida.join("\n")
|
||||
end
|
||||
|
||||
def generar_markdown(datos)
|
||||
salida = []
|
||||
salida << "# Topología de Red ADN"
|
||||
salida << ""
|
||||
salida << "**Generado:** #{datos[:timestamp]}"
|
||||
salida << ""
|
||||
|
||||
salida << "## Estadísticas del Sistema"
|
||||
salida << ""
|
||||
salida << "| Métrica | Valor |"
|
||||
salida << "|---------|-------|"
|
||||
stats = datos[:estadisticas]
|
||||
salida << "| Bitácoras | #{stats[:bitacoras]} |"
|
||||
salida << "| Entradas | #{stats[:entradas]} |"
|
||||
salida << "| Entradas hoy | #{stats[:entradas_hoy]} |"
|
||||
salida << "| Nodos | #{stats[:nodos]} |"
|
||||
salida << "| Temas | #{stats[:temas]} |"
|
||||
salida << ""
|
||||
|
||||
salida << "## Nodos Configurados (#{datos[:nodos].length})"
|
||||
salida << ""
|
||||
salida << "| Nodo | Rol | IP | SSH | Auth | Bóveda | Proxy Jump |"
|
||||
salida << "|------|-----|----|-----|------|--------|------------|"
|
||||
datos[:nodos].each do |nodo|
|
||||
ssh_info = nodo[:ip] ? "#{nodo[:usuario]}@#{nodo[:ip]}:#{nodo[:puerto_ssh]}" : "(no configurado)"
|
||||
auth_info = nodo[:auth_type] || "(no configurado)"
|
||||
boveda_info = nodo[:clave_boveda] || "(no configurada)"
|
||||
proxy_info = nodo[:proxysalto] || "−"
|
||||
salida << "| #{nodo[:nombre]} | #{nodo[:rol]} | #{nodo[:ip] or '(no configurada)'} | #{ssh_info} | #{auth_info} | #{boveda_info} | #{proxy_info} |"
|
||||
end
|
||||
salida << ""
|
||||
|
||||
unless datos[:proyectos].empty?
|
||||
salida << "## Proyectos Relacionados"
|
||||
salida << ""
|
||||
datos[:proyectos].each do |proyecto|
|
||||
salida << "### #{proyecto[:codigo]} - #{proyecto[:nombre]}"
|
||||
salida << ""
|
||||
salida << "**Estado:** #{proyecto[:estado]}"
|
||||
salida << ""
|
||||
salida << "| Hito | Título | Estado | Hrs Pres. | Hrs Rem. |"
|
||||
salida << "|------|--------|--------|-----------|----------|"
|
||||
proyecto[:hitos].each do |hito|
|
||||
salida << "| #{hito[:id]} | #{hito[:titulo]} | #{hito[:estado]} | #{hito[:horas_presencial] or 0} | #{hito[:horas_remoto] or 0} |"
|
||||
end
|
||||
salida << ""
|
||||
end
|
||||
end
|
||||
|
||||
salida.join("\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
+18
-25
@@ -49,31 +49,24 @@ module ADN
|
||||
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}
|
||||
info <nodo> Mostrar metadata completa de un nodo
|
||||
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 info srv-dasu
|
||||
./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
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Nodos",
|
||||
descripcion: "Gestión y consulta de nodos de infraestructura",
|
||||
uso: "./adn/tools/run nodos <acción> [opciones]",
|
||||
opciones: [
|
||||
{ names: ['agrupar [--servidor SRV]'], desc: "Agrupar VMs por servidor anfitrión" },
|
||||
{ names: ['listar [--tipo TIPO]'], desc: "Listar fichas de nodos (.md)" },
|
||||
{ names: ['buscar <término>'], desc: "Buscar nodos por nombre o contenido" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run nodos agrupar", desc: "Agrupar todos los VMs por servidor" },
|
||||
{ cmd: "./adn/tools/run nodos agrupar --servidor srv-pmox1", desc: "Agrupar VMs del servidor srv-pmox1" },
|
||||
{ cmd: "./adn/tools/run nodos listar --tipo srvv", desc: "Listar nodos de tipo srvv" },
|
||||
{ cmd: "./adn/tools/run nodos buscar dasuten", desc: "Buscar nodos relacionados con dasuten" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
# ─── Info ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ module ADN
|
||||
{
|
||||
nombre: nodo_nombre,
|
||||
ip: detectar_ip(contenido),
|
||||
ip_lan: detectar_ip_lan(contenido),
|
||||
host: detectar_host(contenido),
|
||||
so: detectar_so(contenido),
|
||||
vmid: detectar_vmid(contenido),
|
||||
@@ -25,6 +26,13 @@ module ADN
|
||||
}
|
||||
end
|
||||
|
||||
def self.extraer_ip_lan(nodo_nombre)
|
||||
ruta = File.join(ADN::NODOS_DIR, "#{nodo_nombre}.md")
|
||||
return nil unless File.exist?(ruta)
|
||||
contenido = File.read(ruta, encoding: 'UTF-8')
|
||||
detectar_ip_lan(contenido)
|
||||
end
|
||||
|
||||
def self.detectar_usuario(contenido)
|
||||
# **SSH**: user@ip:port o DOMAIN\user@ip:port
|
||||
if contenido =~ /\*\*SSH\*\*:\s*`?([^`\s(]+)/i
|
||||
@@ -69,6 +77,18 @@ module ADN
|
||||
nil
|
||||
end
|
||||
|
||||
def self.detectar_ip_lan(contenido)
|
||||
# Buscar IP de LAN en formato: `10.0.100.1` (después de "LAN" o "Bridge" o "Alias")
|
||||
if contenido =~ /(?:LAN|Bridge vmbr0|Alias).*?`([\d\.]+)`/i
|
||||
return $1
|
||||
end
|
||||
# - **IP**: `10.0.100.12/24` (Estática) - buscar solo la IP antes de /
|
||||
if contenido =~ /\*\*IP\*\*:\s*`([\d\.]+)\//
|
||||
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
|
||||
@@ -122,11 +142,12 @@ module ADN
|
||||
nil
|
||||
end
|
||||
|
||||
# Detecta tipo de autenticación SSH
|
||||
# Formato: (Passphrase RSA, ...) o (Password, ...)
|
||||
def self.detectar_auth_type(contenido)
|
||||
if contenido =~ /\*\*SSH\*\*:.*\((Passphrase|Password)/i
|
||||
return $1.downcase == 'passphrase' ? :passphrase : :password
|
||||
if contenido =~ /\*\*SSH\*\*:.*\((Passphrase|Password|RSA)/i
|
||||
match = $1.downcase
|
||||
return :passphrase if match == 'passphrase'
|
||||
return :rsa if match == 'rsa'
|
||||
return :password
|
||||
end
|
||||
:password # Default para VMs Windows
|
||||
end
|
||||
|
||||
+17
-10
@@ -11,6 +11,7 @@ require_relative '../core/colores'
|
||||
require_relative 'nodos/info'
|
||||
require_relative 'ssh/ejecutor'
|
||||
require_relative '../db/core/bitacora_db'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
module ADN
|
||||
class SubcomandoProceso
|
||||
@@ -20,7 +21,8 @@ module ADN
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty? || @args[0] == 'help'
|
||||
# Handle help flags
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
@@ -41,15 +43,20 @@ module ADN
|
||||
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"
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Procesos Automatizados ADN",
|
||||
descripcion: "Ejecuta procesos técnicos automatizados sobre la infraestructura",
|
||||
uso: "./adn/tools/run proceso <nombre_proceso> [argumentos]",
|
||||
opciones: [
|
||||
{ names: ['nodos:migrar_backup'], desc: "Migración de backups entre nodos" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run proceso nodos:migrar_backup --desde srv-ns8 --hacia srvv-dns --ruta /backups/backup.sql", desc: "Ejecutar migración de backup (modo simulación)" },
|
||||
{ cmd: "./adn/tools/run proceso nodos:migrar_backup --desde srv-ns8 --hacia srvv-dns --ruta /backups/backup.sql --ejecutar", desc: "Ejecutar migración real de backup" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
|
||||
def comando_migrar_backup(args)
|
||||
|
||||
+150
-17
@@ -47,10 +47,10 @@ module ADN
|
||||
listar_nodos
|
||||
exit 0
|
||||
end
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
puts opts
|
||||
exit 0
|
||||
end
|
||||
opts.on("-h", "--help", "Mostrar esta ayuda") do
|
||||
mostrar_ayuda
|
||||
exit 0
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
@@ -82,6 +82,27 @@ module ADN
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Ejecución Remota Segura SSH",
|
||||
descripcion: "Ejecuta comandos remotos de forma segura via SSH + Candados",
|
||||
uso: "./adn/tools/run ssh <nodo> [comando] [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['-i', '--interactivo'], desc: "Abrir sesión SSH interactiva" },
|
||||
{ names: ['-v', '--verbose'], desc: "Mostrar detalles de conexión" },
|
||||
{ names: ['--listar'], desc: "Listar nodos configurados" },
|
||||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run ssh srv-dasu \"hostname; uptime\"", desc: "Ejecutar comando remoto en srv-dasu" },
|
||||
{ cmd: "./adn/tools/run ssh dc-dasuten \"whoami\"", desc: "Ejecutar comando remoto en dc-dasuten" },
|
||||
{ cmd: "./adn/tools/run ssh srv-ns8 --interactivo", desc: "Abrir sesión SSH interactiva en srv-ns8" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
end
|
||||
|
||||
def conectar(nodo_nombre, comando_remoto)
|
||||
config = obtener_config(nodo_nombre)
|
||||
unless config
|
||||
@@ -90,7 +111,16 @@ module ADN
|
||||
exit 1
|
||||
end
|
||||
|
||||
@logger.info("Conectando a #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]})")
|
||||
# Mostrar contexto enriquecido
|
||||
contexto = []
|
||||
if config[:ip_lan]
|
||||
contexto << "LAN:#{config[:ip_lan]}"
|
||||
end
|
||||
if config[:ip] && config[:ip] != config[:ip_lan]
|
||||
contexto << "VPN:#{config[:ip]}"
|
||||
end
|
||||
contexto_str = contexto.any? ? " [#{contexto.join(' ')}]" : ""
|
||||
@logger.info("Conectando a #{nodo_nombre}#{contexto_str} (#{config[:ip]}:#{config[:puerto_ssh]})")
|
||||
|
||||
# Autorizar candados
|
||||
autorizar_candados
|
||||
@@ -100,6 +130,8 @@ module ADN
|
||||
ejecutar_con_passphrase(nodo_nombre, config, comando_remoto)
|
||||
when :password
|
||||
ejecutar_con_password(nodo_nombre, config, comando_remoto)
|
||||
when :rsa
|
||||
ejecutar_con_rsa(nodo_nombre, config, comando_remoto)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -107,6 +139,9 @@ module ADN
|
||||
meta = ADN::NodosInfo.extraer_metadata(nodo_nombre)
|
||||
return nil unless meta && meta[:ip] && meta[:usuario] && meta[:clave_boveda]
|
||||
|
||||
# Extraer IP de LAN si está disponible en la ficha
|
||||
meta[:ip_lan] = ADN::NodosInfo.extraer_ip_lan(nodo_nombre)
|
||||
|
||||
# Inferir proxy: si el nodo tiene host (es VM), usar el host como proxy
|
||||
if meta[:host] && meta[:host] != nodo_nombre
|
||||
proxy_meta = ADN::NodosInfo.extraer_metadata(meta[:host])
|
||||
@@ -133,34 +168,74 @@ module ADN
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
def imprimir_salida(nodo_nombre, texto, es_error=false)
|
||||
return if texto.nil? || texto.empty?
|
||||
prefix = es_error ? "#{Color::RED}[#{nodo_nombre}-ERR]#{Color::RESET} " : "#{Color::CYAN}[#{nodo_nombre}]#{Color::RESET} "
|
||||
texto.lines.each do |linea|
|
||||
if es_error
|
||||
$stderr.puts "#{prefix}#{linea}"
|
||||
else
|
||||
puts "#{prefix}#{linea}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_con_passphrase(nodo_nombre, config, comando_remoto)
|
||||
passphrase = obtener_secreto(config[:clave_boveda])
|
||||
proxy_config = config[:proxy] ? ADN::NodosInfo.extraer_metadata(config[:proxy]) : nil
|
||||
|
||||
ssh_opts = "-o ConnectTimeout=15 -o StrictHostKeyChecking=no"
|
||||
ssh_target = "#{config[:usuario]}@#{config[:ip]}"
|
||||
|
||||
if @opciones[:interactivo]
|
||||
if proxy_config
|
||||
@logger.info("Via ProxyJump #{config[:proxy]} (#{proxy_config[:ip]})")
|
||||
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
|
||||
|
||||
script = <<~BASH
|
||||
sshpass -P passphrase -p '#{passphrase}' ssh \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o ConnectTimeout=30 \
|
||||
-o ServerAliveInterval=5 \
|
||||
-o ProxyCommand="sshpass -P passphrase -p '#{proxy_passphrase}' ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}" \
|
||||
-p #{config[:puerto_ssh]} \
|
||||
'#{ssh_target}' \
|
||||
"#{comando_remoto.gsub('"', '\\"')}"
|
||||
BASH
|
||||
|
||||
tmpscript = "/tmp/.adn_ssh_pass_#{Process.pid}.sh"
|
||||
File.write(tmpscript, script)
|
||||
File.chmod(0700, tmpscript)
|
||||
|
||||
stdout, stderr, status = Open3.capture3(tmpscript)
|
||||
File.delete(tmpscript) if File.exist?(tmpscript)
|
||||
|
||||
if status.success?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
|
||||
elsif @opciones[:interactivo]
|
||||
# Sesión interactiva: usar sshpass con passphrase prompt
|
||||
@logger.info("Abriendo sesión interactiva a #{nodo_nombre}...")
|
||||
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target}"
|
||||
exec(cmd)
|
||||
else
|
||||
# Comando remoto
|
||||
if @opciones[:verbose]
|
||||
puts "#{Color::DIM} [SSH] #{nodo_nombre} (#{config[:ip]}:#{config[:puerto_ssh]}) > #{comando_remoto[0..60]}...#{Color::RESET}"
|
||||
end
|
||||
|
||||
# Comando remoto directo
|
||||
cmd = "sshpass -P 'passphrase' -p '#{passphrase}' ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target} '#{comando_remoto.gsub("'", "'\\''")}'"
|
||||
|
||||
stdout, stderr, status = Open3.capture3(cmd)
|
||||
|
||||
if status.success?
|
||||
puts stdout unless stdout.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
puts stdout unless stdout.empty?
|
||||
$stderr.puts stderr unless stderr.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
@@ -218,14 +293,72 @@ module ADN
|
||||
success = defined?(status_success) ? status_success : status.success?
|
||||
|
||||
if success
|
||||
puts stdout unless stdout.nil? || stdout.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
puts stdout unless stdout.nil? || stdout.empty?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota")
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def ejecutar_con_rsa(nodo_nombre, config, comando_remoto)
|
||||
proxy_config = config[:proxy] ? ADN::NodosInfo.extraer_metadata(config[:proxy]) : nil
|
||||
|
||||
ssh_opts = "-o StrictHostKeyChecking=no -o ConnectTimeout=15"
|
||||
ssh_target = "#{config[:usuario]}@#{config[:ip]}"
|
||||
|
||||
if proxy_config
|
||||
@logger.info("Via ProxyJump #{config[:proxy]} (#{proxy_config[:ip]})")
|
||||
proxy_passphrase = obtener_secreto(proxy_config[:clave_boveda])
|
||||
|
||||
script = <<~BASH
|
||||
ssh \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o ConnectTimeout=30 \
|
||||
-o ServerAliveInterval=5 \
|
||||
-o ProxyCommand="sshpass -P passphrase -p '#{proxy_passphrase}' ssh -o StrictHostKeyChecking=no -p #{proxy_config[:puerto_ssh]} -W %h:%p #{proxy_config[:usuario]}@#{proxy_config[:ip]}" \
|
||||
-p #{config[:puerto_ssh]} \
|
||||
'#{ssh_target}' \
|
||||
"#{comando_remoto.gsub('"', '\\"')}"
|
||||
BASH
|
||||
|
||||
tmpscript = "/tmp/.adn_ssh_rsa_#{Process.pid}.sh"
|
||||
File.write(tmpscript, script)
|
||||
File.chmod(0700, tmpscript)
|
||||
|
||||
stdout, stderr, status = Open3.capture3(tmpscript)
|
||||
File.delete(tmpscript) if File.exist?(tmpscript)
|
||||
|
||||
if status.success?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
|
||||
elsif @opciones[:interactivo]
|
||||
@logger.info("Abriendo sesión interactiva a #{nodo_nombre}...")
|
||||
cmd = "ssh #{ssh_opts} -p #{config[:puerto_ssh]} #{ssh_target}"
|
||||
exec(cmd)
|
||||
else
|
||||
cmd_parts = ["ssh"] + ssh_opts.split(' ') + ["-p", config[:puerto_ssh].to_s, ssh_target, comando_remoto]
|
||||
stdout, stderr, status = Open3.capture3(*cmd_parts)
|
||||
|
||||
if status.success?
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
@logger.exito("Comando ejecutado exitosamente en #{nodo_nombre}")
|
||||
else
|
||||
imprimir_salida(nodo_nombre, stdout)
|
||||
imprimir_salida(nodo_nombre, stderr, true)
|
||||
ADN::ErrorHandler.error("Fallo en ejecución remota (exit: #{status.exitstatus})")
|
||||
exit status.exitstatus || 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def listar_nodos
|
||||
puts "#{Color::CYAN}📡 Nodos SSH disponibles (leídos de fichas nodos/*.md):#{Color::RESET}"
|
||||
|
||||
+31
-14
@@ -14,6 +14,8 @@
|
||||
require 'optparse'
|
||||
require_relative 'triggers/motor'
|
||||
require_relative 'triggers/eventos'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../core/help_formatter'
|
||||
|
||||
module ADN
|
||||
class SubcomandoTriggers
|
||||
@@ -33,6 +35,12 @@ module ADN
|
||||
|
||||
# Ejecuta el subcomando con las opciones especificadas
|
||||
def ejecutar
|
||||
# Handle help flags
|
||||
if @args.empty? || @args[0] == 'help' || @args[0] == '-h' || @args[0] == '--help'
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
parser = crear_parser
|
||||
|
||||
begin
|
||||
@@ -43,11 +51,6 @@ module ADN
|
||||
exit 1
|
||||
end
|
||||
|
||||
if @opciones[:ayuda] || @args.empty? && !@opciones[:listar] && !@opciones[:limpiar] && !@opciones[:registrar]
|
||||
mostrar_ayuda(parser)
|
||||
return
|
||||
end
|
||||
|
||||
# Cargar configuración primero
|
||||
cargar_configuracion
|
||||
|
||||
@@ -104,8 +107,29 @@ module ADN
|
||||
end
|
||||
|
||||
# Muestra la ayuda del subcomando
|
||||
def mostrar_ayuda(parser)
|
||||
puts parser
|
||||
def mostrar_ayuda
|
||||
ayuda = ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Triggers ADN",
|
||||
descripcion: "Gestiona el sistema de triggers para sincronización automática del ADN",
|
||||
uso: "./adn/tools/run triggers [OPCIONES]",
|
||||
opciones: [
|
||||
{ names: ['--test [TRIGGER_ID]'], desc: "Probar un trigger específico (modo simulación)" },
|
||||
{ names: ['--forzar'], desc: "Ejecutar triggers incluso si sistema deshabilitado" },
|
||||
{ names: ['--listar'], desc: "Listar todos los triggers configurados" },
|
||||
{ names: ['--registrar'], desc: "Registrar triggers en el bus de eventos" },
|
||||
{ names: ['--limpiar'], desc: "Limpiar todos los triggers registrados" },
|
||||
{ names: ['-h', '--help'], desc: "Mostrar esta ayuda" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run triggers --listar", desc: "Listar todos los triggers configurados" },
|
||||
{ cmd: "./adn/tools/run triggers --registrar", desc: "Registrar triggers en el bus de eventos" },
|
||||
{ cmd: "./adn/tools/run triggers --test bitacoras_0", desc: "Probar trigger bitacoras_0 (modo simulación)" },
|
||||
{ cmd: "./adn/tools/run triggers --forzar", desc: "Forzar ejecución de triggers" },
|
||||
{ cmd: "./adn/tools/run triggers --limpiar", desc: "Limpiar todos los triggers registrados" }
|
||||
]
|
||||
)
|
||||
|
||||
puts ayuda
|
||||
puts ""
|
||||
puts "Descripción:"
|
||||
puts " El sistema de triggers implementa el Principio de Armonía Integral al propagar"
|
||||
@@ -115,13 +139,6 @@ module ADN
|
||||
puts " Bus de eventos: adn/tools/eventos.rb"
|
||||
puts " Motor de triggers: adn/tools/triggers.rb"
|
||||
puts ""
|
||||
puts "Ejemplos:"
|
||||
puts " ./adn/tools/run triggers --listar"
|
||||
puts " ./adn/tools/run triggers --registrar"
|
||||
puts " ./adn/tools/run triggers --test bitacoras_0"
|
||||
puts " ./adn/tools/run triggers --forzar"
|
||||
puts " ./adn/tools/run triggers --limpiar"
|
||||
puts ""
|
||||
puts "📚 Documentación: docs/plan/260307-1400_Plan_Mejoras_ADN.md"
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'optparse'
|
||||
require 'json'
|
||||
require_relative '../core/colores'
|
||||
require_relative '../w-zombi/lib/wzombi/config'
|
||||
require_relative '../w-zombi/lib/wzombi/server'
|
||||
require_relative '../w-zombi/lib/wzombi/command_store'
|
||||
|
||||
module ADN
|
||||
class SubcomandoWZombi
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
@base_path = File.expand_path('../w-zombi', __dir__)
|
||||
@config = WZombi::Config.new(base_path: @base_path)
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
ayuda
|
||||
return
|
||||
end
|
||||
|
||||
accion = @args[0]
|
||||
case accion
|
||||
when 'start'
|
||||
iniciar_servidor
|
||||
when 'cmd'
|
||||
actualizar_comando(@args[1])
|
||||
when 'log'
|
||||
ver_logs
|
||||
when 'ayuda', '-h', '--help'
|
||||
ayuda
|
||||
else
|
||||
@logger.error("Acción desconocida: #{accion}")
|
||||
ayuda
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def iniciar_servidor
|
||||
@logger.info("Iniciando servidor W-ZOMBI v2.0")
|
||||
begin
|
||||
WZombi::Server.new(@config).start
|
||||
rescue Errno::EADDRINUSE
|
||||
@logger.error("El puerto #{@config.puerto} ya está en uso.")
|
||||
exit 1
|
||||
rescue => e
|
||||
@logger.error("Error al iniciar el servidor: #{e.message}")
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def actualizar_comando(comando)
|
||||
if comando.nil? || comando.empty?
|
||||
@logger.error("Debe especificar un comando. Uso: wzombi cmd \"comando\"")
|
||||
exit 1
|
||||
end
|
||||
store = WZombi::CommandStore.new(@config.cmd_path)
|
||||
store.update(comando)
|
||||
@logger.exito("Comando actualizado: #{comando}")
|
||||
end
|
||||
|
||||
def ver_logs
|
||||
log_path = @config.log_path
|
||||
unless File.exist?(log_path)
|
||||
@logger.advertencia("No hay logs disponibles en #{log_path}")
|
||||
return
|
||||
end
|
||||
puts "#{Color::CYAN}🛰️ Telemetría W-ZOMBI (tail -f)#{Color::RESET}"
|
||||
# Usamos exec para ceder el control al proceso tail si el usuario lo desea,
|
||||
# o system para volver después.
|
||||
system("tail -f #{log_path}")
|
||||
end
|
||||
|
||||
def ayuda
|
||||
puts <<~HELP
|
||||
#{Color::CYAN}🧟 Herramienta W-ZOMBI v2.0#{Color::RESET}
|
||||
|
||||
Uso: ./adn/tools/run wzombi <acción> [argumentos]
|
||||
|
||||
Acciones:
|
||||
start Levanta el servidor C2 (WEBrick)
|
||||
cmd "<comando>" Actualiza el comando para los agentes
|
||||
log Muestra la telemetría en tiempo real (tail -f)
|
||||
ayuda Muestra esta ayuda
|
||||
|
||||
Ejemplos:
|
||||
./adn/tools/run wzombi cmd "ipconfig /all"
|
||||
./adn/tools/run wzombi start
|
||||
HELP
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -412,7 +412,12 @@ module BitacorasDB
|
||||
[inicio_time, fin_time, descripcion, estado, modo, es_ia,
|
||||
tema_id, bitacora_id, nodo_id]
|
||||
)
|
||||
result.first
|
||||
entrada = result.first
|
||||
|
||||
# Ejecutar hooks después de la creación
|
||||
ejecutar_hooks_entrada(entrada, :crear) if entrada
|
||||
|
||||
entrada
|
||||
end
|
||||
|
||||
# Actualiza una entrada existente
|
||||
@@ -450,7 +455,12 @@ module BitacorasDB
|
||||
|
||||
values << id
|
||||
result = @connection.exec_params(query, values)
|
||||
result.first&.to_h
|
||||
entrada = result.first&.to_h
|
||||
|
||||
# Ejecutar hooks después de la actualización
|
||||
ejecutar_hooks_entrada(entrada, :actualizar) if entrada
|
||||
|
||||
entrada
|
||||
end
|
||||
|
||||
# Elimina una entrada existente
|
||||
@@ -844,10 +854,38 @@ module BitacorasDB
|
||||
end
|
||||
end
|
||||
|
||||
# Convierte un resultado PG a Hash
|
||||
def result_to_hash(result)
|
||||
return {} if result.ntuples.zero?
|
||||
result.first.to_h
|
||||
end
|
||||
# Convierte un resultado PG a Hash
|
||||
def result_to_hash(result)
|
||||
return {} if result.ntuples.zero?
|
||||
result.first.to_h
|
||||
end
|
||||
|
||||
# Ejecuta hooks personalizados después de operaciones de entrada
|
||||
# Esto implementa el concepto de DB Hooks para migrar triggers.yml a capa de datos
|
||||
#
|
||||
# @param entrada [Hash] La entrada que fue creada o actualizada
|
||||
# @param accion [Symbol] :crear o :actualizar
|
||||
def ejecutar_hooks_entrada(entrada, accion)
|
||||
# Hook para proyectos con etiqueta [P2604]
|
||||
if entrada[:descripcion]&.match?(/\[P2604\]/)
|
||||
proyecto_codigo = entrada[:descripcion].match(/\[P2604\.(\d+)\.(\d+)\.(\d+)\]/)&.to_s
|
||||
if proyecto_codigo
|
||||
# Normalizar formato: P2604.05.01 -> P2604
|
||||
proyecto_base = proyecto_codigo.split('.')[0..1].join('.')
|
||||
|
||||
# Actualizar el manifiesto del proyecto en la base de datos
|
||||
proyecto = find_proyecto_by_codigo(proyecto_base)
|
||||
if proyecto
|
||||
# Aquí podríamos actualizar algún campo del proyecto basado en la entrada
|
||||
# Por ahora, solo registramos que se debería actualizar
|
||||
@logger&.info("Hook ejecutado: Entrada #{accion} para proyecto #{proyecto_base} detectada")
|
||||
# En una implementación completa, aquí actualizaríamos el manifiesto del proyecto
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Otros hooks podrían ir aquí...
|
||||
# Por ejemplo, hooks para sincronización con nodos, etc.
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'plantillas'
|
||||
|
||||
module ADN
|
||||
class GeneradorProy
|
||||
def self.nuevo(tipo, titulo, opciones)
|
||||
unless Plantillas.existe?(tipo)
|
||||
puts "✗ Tipo inválido: #{tipo}"
|
||||
return
|
||||
end
|
||||
|
||||
titulo_base = titulo
|
||||
titulo_base = tipo.capitalize if titulo_base.nil? || titulo_base.empty?
|
||||
|
||||
# Remover espacios para el filename manteniendo legibilidad
|
||||
titulo_archivo = titulo_base.gsub(/\s+/, '-')
|
||||
|
||||
proy_id = opciones[:proy]
|
||||
fase_id = opciones[:fase]
|
||||
|
||||
# Construir el prefijo del nombre del archivo
|
||||
prefijos = []
|
||||
prefijos << proy_id if proy_id && proy_id.is_a?(String) && !proy_id.empty?
|
||||
prefijos << fase_id if fase_id && fase_id.is_a?(String) && !fase_id.empty?
|
||||
|
||||
prefijo_str = prefijos.empty? ? "" : "#{prefijos.join('.')}_"
|
||||
|
||||
nombre_archivo = "#{prefijo_str}#{titulo_archivo}.md"
|
||||
|
||||
# Determinar el directorio de destino
|
||||
dir_base = File.expand_path('../../../docs/proy', __dir__)
|
||||
FileUtils.mkdir_p(dir_base) unless Dir.exist?(dir_base)
|
||||
|
||||
destino_dir = dir_base
|
||||
|
||||
if proy_id && proy_id.is_a?(String) && !proy_id.empty?
|
||||
# Buscar carpetas que coincidan con el patrón P2601*
|
||||
require 'fileutils'
|
||||
coincidencias = Dir.glob(File.join(dir_base, "#{proy_id}*")).select { |f| File.directory?(f) }
|
||||
|
||||
if coincidencias.any?
|
||||
destino_dir = coincidencias.first
|
||||
else
|
||||
# Crear directorio P2601_Nombre (si hay titulo le ponemos el titulo, sino solo P2601)
|
||||
destino_dir = File.join(dir_base, "#{proy_id}_#{titulo_archivo}")
|
||||
FileUtils.mkdir_p(destino_dir)
|
||||
end
|
||||
end
|
||||
|
||||
ruta_completa = File.join(destino_dir, nombre_archivo)
|
||||
contenido = Plantillas.obtener(tipo)
|
||||
|
||||
File.write(ruta_completa, contenido)
|
||||
puts "✓ Archivo generado: #{ruta_completa}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,109 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ADN
|
||||
module Plantillas
|
||||
|
||||
def self.existe?(tipo)
|
||||
plantillas.key?(tipo)
|
||||
end
|
||||
|
||||
def self.obtener(tipo)
|
||||
plantillas[tipo]
|
||||
end
|
||||
|
||||
def self.plantillas
|
||||
{
|
||||
"plan" => template_plan,
|
||||
"subplan" => template_subplan,
|
||||
"mejora" => template_mejora,
|
||||
"incidente" => template_incidente,
|
||||
"tarea" => template_tarea
|
||||
}
|
||||
end
|
||||
|
||||
def self.template_plan
|
||||
<<~MD
|
||||
# [Código] - Plan
|
||||
|
||||
## Objetivo
|
||||
...
|
||||
|
||||
## Fases
|
||||
- [ ] Tarea
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progreso
|
||||
```
|
||||
Fase 1: ██████░░░░ 60% Nombre de la Fase
|
||||
```
|
||||
---
|
||||
MD
|
||||
end
|
||||
|
||||
def self.template_subplan
|
||||
<<~MD
|
||||
# [Código] - Subplan Técnico
|
||||
|
||||
## Contexto Técnico
|
||||
...
|
||||
|
||||
## Procedimiento
|
||||
...
|
||||
MD
|
||||
end
|
||||
|
||||
def self.template_mejora
|
||||
<<~MD
|
||||
# [Código] - Mejora
|
||||
|
||||
## Objetivo
|
||||
...
|
||||
|
||||
## Implementación
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progreso
|
||||
```
|
||||
En curso: ██████░░░░ 60% Implementación
|
||||
```
|
||||
---
|
||||
MD
|
||||
end
|
||||
|
||||
def self.template_incidente
|
||||
<<~MD
|
||||
# [Código] - Incidente
|
||||
|
||||
## Descripción
|
||||
...
|
||||
|
||||
## Resolución
|
||||
...
|
||||
MD
|
||||
end
|
||||
|
||||
def self.template_tarea
|
||||
<<~MD
|
||||
# [Código] - Tarea
|
||||
|
||||
## Descripción
|
||||
...
|
||||
|
||||
## Estado
|
||||
⏳
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progreso
|
||||
```
|
||||
Fase 1: ██████░░░░ 60% Nombre de la Fase
|
||||
```
|
||||
---
|
||||
MD
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative '../core/help_formatter'
|
||||
require_relative 'generador'
|
||||
|
||||
module ADN
|
||||
class SubcomandoProy
|
||||
def initialize(args, logger)
|
||||
@args = args
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def ejecutar
|
||||
if @args.empty?
|
||||
mostrar_ayuda
|
||||
return
|
||||
end
|
||||
|
||||
accion = @args.shift
|
||||
|
||||
case accion
|
||||
when 'nuevo'
|
||||
tipo = @args.shift
|
||||
titulo = nil
|
||||
opciones = {}
|
||||
|
||||
while (arg = @args.shift)
|
||||
if arg =~ /^--(\w+)[:=](.+)$/
|
||||
opciones[$1.to_sym] = $2.gsub(/^['"]|['"]$/, '')
|
||||
elsif arg =~ /^--(\w+)$/
|
||||
opciones[$1.to_sym] = true
|
||||
else
|
||||
titulo = arg unless titulo
|
||||
end
|
||||
end
|
||||
|
||||
GeneradorProy.nuevo(tipo, titulo, opciones)
|
||||
when 'ayuda', 'help', '-h', '--help'
|
||||
mostrar_ayuda
|
||||
else
|
||||
puts "✗ Acción desconocida: #{accion}"
|
||||
mostrar_ayuda
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mostrar_ayuda
|
||||
puts ADN::HelpFormatter.generar_help(
|
||||
titulo: "Gestión de Proyectos",
|
||||
descripcion: "Creación de estructuras basadas en plantillas",
|
||||
uso: "./adn/tools/run proy nuevo [tipo]",
|
||||
opciones: [
|
||||
{ names: ['nuevo [plan|subplan|mejora|incidente|tarea]'], desc: "Crear estructura desde plantilla" }
|
||||
],
|
||||
ejemplos: [
|
||||
{ cmd: "./adn/tools/run proy nuevo plan", desc: "Crear un plan nuevo" },
|
||||
{ cmd: "./adn/tools/run proy nuevo mejora", desc: "Crear mejora" }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
+12
-1
@@ -23,6 +23,8 @@ ENV['TZ'] = 'America/Argentina/Buenos_Aires'
|
||||
# conocimiento asimilar <archivo> Asimilar documentación al ADN
|
||||
# evolucion <propuesta.md> Gestionar propuesta de evolución del ADN
|
||||
# msp <subcomando> Gestión de MSPs (auditar, registrar, listar)
|
||||
# proy <subcomando> Generación y gestión de proyectos
|
||||
# candados <acción> [opciones] Gestión de secretos y autorización SSH
|
||||
# ayuda [subcomando] Mostrar ayuda detallada
|
||||
#
|
||||
# Documentación completa: docs/proy/p2604_mejoras_ADN/P2604_mejoras_ADN.md
|
||||
@@ -54,6 +56,7 @@ require_relative 'cli/proceso'
|
||||
require_relative 'cli/estados'
|
||||
require_relative 'cli/tailscale'
|
||||
require_relative 'cli/ssh'
|
||||
require_relative 'cli/candados'
|
||||
require_relative 'core/conciliador'
|
||||
|
||||
require_relative 'core/colores'
|
||||
@@ -69,6 +72,8 @@ require_relative 'cli/salud'
|
||||
require_relative 'cli/ayuda'
|
||||
require_relative 'cli/nodos'
|
||||
require_relative 'cli/msp'
|
||||
require_relative 'cli/wzombi'
|
||||
require_relative 'proy/proy'
|
||||
|
||||
# ─── CLI Principal ──────────────────────────────────────────────────────
|
||||
class ADNCLI
|
||||
@@ -123,7 +128,7 @@ class ADNCLI
|
||||
# Compatibilidad: redirigir a jornada cerrar
|
||||
ADN::SubcomandoJornada.new(['cerrar'] + args, @logger).ejecutar
|
||||
when 'ayuda', 'help', '-h', '--help'
|
||||
SubcomandoAyuda.new(args, @logger).ejecutar
|
||||
ADN::SubcomandoAyuda.new(args, @logger).ejecutar
|
||||
when 'evolucion'
|
||||
@logger.info("Subcomando 'evolucion' será implementado en Fase 2")
|
||||
puts "#{Color::YELLOW}⚠ El subcomando 'evolucion' será implementado en Fase 2#{Color::RESET}"
|
||||
@@ -132,8 +137,14 @@ class ADNCLI
|
||||
ADN::SubcomandoPlan.new(args, @logger).ejecutar
|
||||
when 'msp'
|
||||
SubcomandoMsp.new(args, @logger).ejecutar
|
||||
when 'proy'
|
||||
ADN::SubcomandoProy.new(args, @logger).ejecutar
|
||||
when 'ssh'
|
||||
ADN::SubcomandoSSH.new(args, @logger).ejecutar
|
||||
when 'candados', 'secrets'
|
||||
ADN::SubcomandoCandados.new(args, @logger).ejecutar
|
||||
when 'wzombi', 'w-zombi'
|
||||
ADN::SubcomandoWZombi.new(args, @logger).ejecutar
|
||||
else
|
||||
@logger.error("Subcomando desconocido: #{subcomando}")
|
||||
puts "#{Color::RED}✗ Subcomando desconocido: '#{subcomando}'#{Color::RESET}"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
1773690569
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env ruby
|
||||
require_relative '../lib/wzombi/config'
|
||||
require_relative '../lib/wzombi/server'
|
||||
require_relative '../lib/wzombi/command_store'
|
||||
|
||||
base = File.expand_path("..", __dir__)
|
||||
config = WZombi::Config.new(base_path: base)
|
||||
|
||||
def help
|
||||
puts <<~HELP
|
||||
Uso:
|
||||
w-zombi start
|
||||
w-zombi cmd "<comando>"
|
||||
w-zombi ayuda | -h | -help
|
||||
|
||||
Ejemplo ADN:
|
||||
adn tools w-zombi cmd "ipconfig"
|
||||
HELP
|
||||
end
|
||||
|
||||
cmd = ARGV[0]
|
||||
|
||||
case cmd
|
||||
when "start"
|
||||
WZombi::Server.new(config).start
|
||||
|
||||
when "cmd"
|
||||
command = ARGV[1]
|
||||
if command.nil?
|
||||
puts "Falta comando"
|
||||
exit
|
||||
end
|
||||
store = WZombi::CommandStore.new(config.cmd_path)
|
||||
store.update(command)
|
||||
puts "Comando actualizado: #{command}"
|
||||
|
||||
when "ayuda", "-h", "-help", nil
|
||||
help
|
||||
|
||||
else
|
||||
puts "Comando desconocido"
|
||||
help
|
||||
end
|
||||
@@ -0,0 +1 @@
|
||||
{"id":8,"cmd":"Powershell -Command \"cat C:\\wz_rename.txt\""}
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'json'
|
||||
|
||||
module WZombi
|
||||
class CommandStore
|
||||
def initialize(path)
|
||||
@path = path
|
||||
init_file
|
||||
end
|
||||
|
||||
def init_file
|
||||
File.write(@path, { id: 0, cmd: "" }.to_json) unless File.exist?(@path)
|
||||
end
|
||||
|
||||
def current
|
||||
JSON.parse(File.read(@path))
|
||||
end
|
||||
|
||||
def update(cmd)
|
||||
data = current
|
||||
data["id"] += 1
|
||||
data["cmd"] = cmd
|
||||
File.write(@path, data.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
module WZombi
|
||||
class Config
|
||||
attr_reader :puerto, :base_path
|
||||
|
||||
def initialize(base_path:)
|
||||
@base_path = base_path
|
||||
@puerto = ENV.fetch("WZ_PORT", 8000).to_i
|
||||
end
|
||||
|
||||
def log_path
|
||||
File.join(base_path, "data/telemetria.log")
|
||||
end
|
||||
|
||||
def cmd_path
|
||||
File.join(base_path, "data/cmd.json")
|
||||
end
|
||||
|
||||
def payloads_path
|
||||
File.join(base_path, "payloads")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
module WZombi
|
||||
module Engine
|
||||
class PS1Builder
|
||||
def self.build(host:)
|
||||
<<~PS1
|
||||
# W-ZOMBI ENGINE v2.0
|
||||
$WZ_HOST = "#{host}"
|
||||
$LAST_CMD_ID = 0
|
||||
|
||||
function Get-Cmd {
|
||||
try {
|
||||
return Invoke-RestMethod -Uri "http://$WZ_HOST/cmd" -UseBasicParsing -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Send-Log {
|
||||
param([string]$msg)
|
||||
$line = "[$(Get-Date -Format 'HH:mm:ss')] $msg"
|
||||
Write-Host "📡 $line" -ForegroundColor Cyan
|
||||
try {
|
||||
Invoke-RestMethod -Uri "http://$WZ_HOST/log" -Method Post -Body $line -ContentType "text/plain" -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null
|
||||
} catch {}
|
||||
}
|
||||
|
||||
Write-Host "🧟 ゾンビ (Zombi) v2.0 ONLINE - Esperando ordenes..." -ForegroundColor Green
|
||||
|
||||
while ($true) {
|
||||
try {
|
||||
$cmdData = Get-Cmd
|
||||
if ($cmdData -and $cmdData.id -ne $LAST_CMD_ID -and $cmdData.cmd -ne "NO_CMD") {
|
||||
$LAST_CMD_ID = $cmdData.id
|
||||
Write-Host ">>> EJECUTANDO [ID:$($cmdData.id)]: $($cmdData.cmd)" -ForegroundColor Yellow
|
||||
$result = Invoke-Expression $cmdData.cmd 2>&1 | Out-String
|
||||
Send-Log $result
|
||||
}
|
||||
} catch {
|
||||
Send-Log "!!! EXCEPCION FATAL EN LOOP: $($_.Exception.Message)"
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
PS1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module WZombi
|
||||
class Logger
|
||||
def initialize(path)
|
||||
@path = path
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
line = "[#{Time.now.strftime('%H:%M:%S')}] #{msg}"
|
||||
File.open(@path, "a") { |f| f.puts(line) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
require 'json'
|
||||
|
||||
module WZombi
|
||||
module Routes
|
||||
class Cmd
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(_req, res)
|
||||
res['Content-Type'] = 'application/json'
|
||||
res.body = File.read(@config.cmd_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
module WZombi
|
||||
module Routes
|
||||
class Log
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(req, res)
|
||||
File.open(@config.log_path, "a") { |f| f.puts(req.body) }
|
||||
res.body = "OK"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
require_relative '../engine/ps1_builder'
|
||||
|
||||
module WZombi
|
||||
module Routes
|
||||
class Zombi
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def call(_req, res)
|
||||
# Priorizamos host desde variable de entorno, sino usamos la IP Tailscale conocida
|
||||
host = ENV.fetch("WZ_HOST", "100.111.195.4:#{@config.puerto}")
|
||||
res.body = Engine::PS1Builder.build(host: host)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
require 'webrick'
|
||||
require_relative 'routes/zombi'
|
||||
require_relative 'routes/cmd'
|
||||
require_relative 'routes/log'
|
||||
|
||||
module WZombi
|
||||
class Server
|
||||
def initialize(config)
|
||||
@config = config
|
||||
end
|
||||
|
||||
def start
|
||||
server = WEBrick::HTTPServer.new(Port: @config.puerto)
|
||||
|
||||
server.mount_proc('/zombi.ps1', &Routes::Zombi.new(@config).method(:call))
|
||||
server.mount_proc('/cmd', &Routes::Cmd.new(@config).method(:call))
|
||||
server.mount_proc('/log', &Routes::Log.new(@config).method(:call))
|
||||
|
||||
trap("INT") { server.shutdown }
|
||||
|
||||
puts "[W-ZOMBI] Servidor corriendo en puerto #{@config.puerto}"
|
||||
server.start
|
||||
end
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -163,7 +163,7 @@ adn:
|
||||
# ─── Configuración de Ejecución ──────────────────────────────────────────
|
||||
configuracion:
|
||||
# Habilitar/deshabilitar todo el sistema de triggers
|
||||
sistema_habilitado: false # Cambiar a true en Fase 2
|
||||
sistema_habilitado: true # Habilitado en Fase 2
|
||||
|
||||
# Modo de ejecución
|
||||
modo: "batch" # batch, instantaneo, cron
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
PROMPT
|
||||
|
||||
En el plan docs/proy/p2601_dasuten/plan/P2601_6.1.B_AD-Join:_pc-dasu0_al_Dominio.md hay que agregar items y subitems de cambios de nombres de host de los nodos sql-dasuten a dasu-srvv-sql, dc-dasuten a dasu-srvv-dc y pcv-dasu0 a dasu-pcv0 que se deben realizar.
|
||||
Genera un nuevo plan con sus respectivos elementos necesarios como fases y demas para agregar el nodo pc-dasu0 a la red de dasu-srvv-dc
|
||||
|
||||
Continúa el trabajo priorizando siempre el uso y la optimización de las herramientas definidas en el ecosistema ADN, con el objetivo de mejorarlas y hacerlas reutilizables en el futuro. Si es necesario crear nuevas herramientas, asegúrate de que sigan los mismos principios de sostenibilidad y evita soluciones temporales o de un solo uso.
|
||||
Continua con el trabajo de la creacion de los usurios aalmiron (Andrea ALMIRON) y rmolina (Romina MOLINA) en el dominio dasuten.utnlr para que puedn acceder a la pc-dasu0. Puedes usar como contraseña $DASU123
|
||||
|
||||
Realiza el trabajo priorizando siempre el uso y la optimización de las herramientas definidas en el ecosistema ADN, con el objetivo de mejorarlas y hacerlas reutilizables en el futuro. Si es necesario crear nuevas herramientas, asegúrate de que sigan los mismos principios de sostenibilidad y evita soluciones temporales o de un solo uso.
|
||||
|
||||
Para más contexto, consulta los lineamientos en `adn/README.md`, así como el índice actualizado de herramientas disponibles en ADN.
|
||||
|
||||
@@ -15,4 +17,8 @@ Sigue el flujo de trabajo establecido:
|
||||
3. **Ejecutar las tareas correspondientes.**
|
||||
4. **Actualizar nuevamente la bitácora** con los resultados y avances.
|
||||
|
||||
**Menos es más, mejora continua y armonía integral.**
|
||||
# Principio rector
|
||||
|
||||
**Menos es más.**
|
||||
**Mejora continua.**
|
||||
**Armonía integral.**
|
||||
@@ -0,0 +1,49 @@
|
||||
# P2601.07.01 - Plan: Integración dasu-pc-0 a Dominio DASUTEN
|
||||
|
||||
**Fecha:** 18 de marzo de 2026
|
||||
**Autor:** Antigravity (IA ADN)
|
||||
**Estado:** ✅ FINALIZADO
|
||||
**Proyecto Padre:** [P2601 - Red DASUTEN](../P2601_dasuten.md)
|
||||
|
||||
## 📋 Objetivo
|
||||
Integrar exitosamente el nodo físico `dasu-pc-0` al dominio `dasu-srvv-dc` para centralizar la gestión de usuarios, aplicar GPOs y unificar la red operativa de DASUTEN.
|
||||
|
||||
## 📅 Fases de Implementación
|
||||
|
||||
### **Fase 7.1: Preparación y Validación (Completada)**
|
||||
- [x] **1.A:** Verificación de conectividad IP con `dasu-srvv-dc` (vía Tailscale 100.85.117.101).
|
||||
- [x] **1.B:** Configuración de DNS primario en `dasu-pc-0` apuntando a DC.
|
||||
- [x] **1.C:** Validación de credenciales de Administrador de Dominio.
|
||||
|
||||
### **Fase 7.2: Ejecución de AD-Join (Completada)**
|
||||
- [x] **2.A:** Unión al dominio `dasuten.utnlr`.
|
||||
- [x] **2.B:** Reinicio de la máquina y comprobación de persistencia.
|
||||
- [x] **2.C:** Confirmación de visibilidad en el contenedor de Computers del DC.
|
||||
|
||||
### **Fase 7.3: Post-Configuración y Seguridad (Completada)**
|
||||
- [x] **3.A:** Verificación de aplicación de GPOs básicas.
|
||||
- [x] **3.B:** Renombramiento definitivo a `dasu-pc-0` inyectado vía SSH (RSA).
|
||||
- [x] **3.C:** Registro final en la ontología del ADN y ficha del nodo.
|
||||
|
||||
### **Fase 7.4: Creación de Usuarios de Dominio (Completada)**
|
||||
- [x] **4.A:** Crear usuario `aalmiron` (Andrea ALMIRON) en AD.
|
||||
- [x] **4.B:** Crear usuario `rmolina` (Romina MOLINA) en AD.
|
||||
- [x] **4.C:** Otorgar acceso a `dasu-pc-0` para estos usuarios (Verificado).
|
||||
|
||||
---
|
||||
|
||||
## 📊 Progreso
|
||||
|
||||
```
|
||||
Fase 1: ██████████ 100% Preparación y Validación
|
||||
Fase 2: ██████████ 100% Ejecución de AD-Join
|
||||
Fase 3: ██████████ 100% Post-Configuración
|
||||
Fase 4: ██████████ 100% Creación de Usuarios
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Referencias
|
||||
- **DC:** `dasu-srvv-dc` (100.85.117.101)
|
||||
- **Host:** `dasu-pc-0` (Ex `pc-dasu0`)
|
||||
- **Ontología:** [01_ontologia.md](../../../adn/01_ontologia.md)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user