feat: Descubrimiento automático de servicios systemd existentes

Implementa escaneo automático de servicios siax-app-*.service al iniciar
el agente, sincronizando automáticamente con monitored_apps.json.

PROBLEMA RESUELTO:
- Servicios systemd creados manualmente no eran detectados
- monitored_apps.json desincronizado con servicios reales
- app_IDEAS.service existía pero NO era monitoreada
- Requería agregar manualmente cada app al JSON

SOLUCIÓN IMPLEMENTADA:

Nuevo módulo src/discovery.rs:

1. discover_services():
   - Escanea /etc/systemd/system/
   - Busca archivos siax-app-*.service
   - Parsea configuración (User, WorkingDirectory, ExecStart, PORT)
   - Retorna lista de DiscoveredService

2. parse_service_file():
   - Lee archivo .service línea por línea
   - Extrae: WorkingDirectory, User, ExecStart
   - Extrae PORT de Environment="PORT=XXXX"
   - Maneja múltiples formatos

3. sync_discovered_services():
   - Compara con monitored_apps.json actual
   - Agrega solo servicios nuevos (evita duplicados)
   - Detecta puerto automáticamente:
     - Desde Environment=PORT=XXXX
     - Desde nombre de app (fallback)
   - Guarda en monitored_apps.json

FLUJO AL INICIAR:
1. Logger inicia
2. 🔍 Discovery escanea /etc/systemd/system/
3.  Encuentra: siax-app-IDEAS, siax-app-TAREAS, siax-app-fidelizacion
4. 📝 Sincroniza con monitored_apps.json
5. ConfigManager carga JSON actualizado
6. Monitor comienza vigilancia de TODAS las apps

LOGS GENERADOS:
📡 "Escaneando servicios systemd existentes..."
 "Encontrado: siax-app-IDEAS.service"
📊 "App: IDEAS, User: Some("user_apps"), WorkDir: Some("/path")"
 "Agregando IDEAS (puerto: 2000)"
 "IDEAS agregado exitosamente"
📊 "Resumen: 1 agregadas, 2 ya existían"

DETECCIÓN DE PUERTO:
1. Lee Environment=PORT=XXXX del .service
2. Si no existe, usa detección por nombre:
   - tareas → 3000
   - fidelizacion → 3001
   - ideas → 2000
   - otros → 8080 (default)

BENEFICIOS:
 Auto-descubre servicios existentes al iniciar
 Sincroniza monitored_apps.json automáticamente
 No requiere intervención manual
 Evita duplicados (compara antes de agregar)
 Extrae configuración del .service
 Logging detallado de proceso
 Tests unitarios incluidos

EJEMPLO DE USO:
# Crear servicio manualmente
sudo nano /etc/systemd/system/siax-app-NUEVA.service
sudo systemctl daemon-reload
sudo systemctl start siax-app-NUEVA

# Reiniciar agente → Auto-descubre la nueva app
sudo systemctl restart siax-agent

# Verificar que fue detectada
cat /opt/siax-agent/config/monitored_apps.json
# Ahora incluye: NUEVA

Archivos modificados:
- src/discovery.rs: +265 líneas (nuevo módulo)
- src/lib.rs: +2 líneas (export discovery)
- src/main.rs: +9 líneas (ejecuta discovery al inicio)
This commit is contained in:
2026-01-18 03:03:22 -05:00
parent b6fa1fa472
commit ad9b46bdc5
3 changed files with 238 additions and 1 deletions

226
src/discovery.rs Normal file
View File

@@ -0,0 +1,226 @@
/// Módulo para descubrir servicios systemd existentes
use std::fs;
use std::path::Path;
use crate::logger::get_logger;
use crate::config::{get_config_manager, MonitoredApp};
const SYSTEMD_DIR: &str = "/etc/systemd/system";
const SERVICE_PREFIX: &str = "siax-app-";
/// Descubre servicios systemd existentes con prefijo siax-app-*
pub fn discover_services() -> Vec<DiscoveredService> {
let logger = get_logger();
logger.info("Discovery", "Escaneando servicios systemd existentes...");
let mut services = Vec::new();
// Leer directorio de systemd
let entries = match fs::read_dir(SYSTEMD_DIR) {
Ok(entries) => entries,
Err(e) => {
logger.error("Discovery", "No se pudo leer directorio systemd", Some(&e.to_string()));
return services;
}
};
// Buscar archivos siax-app-*.service
for entry in entries.flatten() {
let path = entry.path();
if let Some(filename) = path.file_name() {
let filename_str = filename.to_string_lossy();
// Verificar que sea un archivo .service con nuestro prefijo
if filename_str.starts_with(SERVICE_PREFIX) && filename_str.ends_with(".service") {
logger.info("Discovery", &format!("Encontrado: {}", filename_str));
// Extraer nombre de la app
let app_name = extract_app_name(&filename_str);
// Leer configuración del servicio
if let Some(service) = parse_service_file(&path, &app_name) {
services.push(service);
}
}
}
}
logger.info("Discovery", &format!("✅ Descubiertos {} servicios", services.len()));
services
}
/// Extrae el nombre de la app desde el nombre del archivo
/// Ejemplo: "siax-app-IDEAS.service" -> "app_IDEAS"
fn extract_app_name(filename: &str) -> String {
// Remover "siax-app-" del inicio y ".service" del final
filename
.trim_start_matches(SERVICE_PREFIX)
.trim_end_matches(".service")
.to_string()
}
/// Servicio descubierto en systemd
#[derive(Debug, Clone)]
pub struct DiscoveredService {
pub app_name: String,
pub service_file: String,
pub working_directory: Option<String>,
pub user: Option<String>,
pub exec_start: Option<String>,
pub port: Option<i32>,
}
/// Parsea un archivo .service para extraer configuración básica
fn parse_service_file(path: &Path, app_name: &str) -> Option<DiscoveredService> {
let logger = get_logger();
let content = match fs::read_to_string(path) {
Ok(content) => content,
Err(e) => {
logger.error("Discovery", &format!("Error leyendo {}", path.display()), Some(&e.to_string()));
return None;
}
};
let mut service = DiscoveredService {
app_name: app_name.to_string(),
service_file: path.to_string_lossy().to_string(),
working_directory: None,
user: None,
exec_start: None,
port: None,
};
// Parsear líneas del archivo
for line in content.lines() {
let line = line.trim();
// WorkingDirectory
if line.starts_with("WorkingDirectory=") {
service.working_directory = Some(line.trim_start_matches("WorkingDirectory=").to_string());
}
// User
if line.starts_with("User=") {
service.user = Some(line.trim_start_matches("User=").to_string());
}
// ExecStart
if line.starts_with("ExecStart=") {
service.exec_start = Some(line.trim_start_matches("ExecStart=").to_string());
}
// Environment con PORT
if line.starts_with("Environment=") && line.contains("PORT") {
if let Some(port) = extract_port_from_env(line) {
service.port = Some(port);
}
}
}
logger.info("Discovery", &format!(" App: {}, User: {:?}, WorkDir: {:?}",
service.app_name,
service.user,
service.working_directory
));
Some(service)
}
/// Extrae el puerto de una línea Environment
/// Ejemplo: Environment="PORT=3000" -> Some(3000)
fn extract_port_from_env(line: &str) -> Option<i32> {
// Buscar PORT=número
if let Some(start) = line.find("PORT=") {
let after_port = &line[start + 5..];
// Extraer números
let port_str: String = after_port.chars()
.take_while(|c| c.is_numeric())
.collect();
port_str.parse::<i32>().ok()
} else {
None
}
}
/// Sincroniza los servicios descubiertos con monitored_apps.json
pub fn sync_discovered_services(services: Vec<DiscoveredService>) {
let logger = get_logger();
let config_manager = get_config_manager();
logger.info("Discovery", "Sincronizando servicios descubiertos con configuración...");
let mut added_count = 0;
let mut skipped_count = 0;
for service in services {
// Intentar detectar el puerto si no se encontró en Environment
let port = service.port.unwrap_or_else(|| {
// Intentar extraer del nombre o usar puerto por defecto
detect_port_from_name(&service.app_name)
});
// Verificar si ya existe en la configuración
let existing_apps = config_manager.get_apps();
let already_exists = existing_apps.iter().any(|app| app.name == service.app_name);
if already_exists {
logger.info("Discovery", &format!("⏭️ {} ya existe en configuración", service.app_name));
skipped_count += 1;
continue;
}
// Agregar a monitored_apps.json
logger.info("Discovery", &format!(" Agregando {} (puerto: {})", service.app_name, port));
match config_manager.add_app(service.app_name.clone(), port) {
Ok(_) => {
logger.info("Discovery", &format!("{} agregado exitosamente", service.app_name));
added_count += 1;
}
Err(e) => {
logger.error("Discovery", &format!("Error agregando {}", service.app_name), Some(&e));
}
}
}
logger.info("Discovery", &format!("📊 Resumen: {} agregadas, {} ya existían", added_count, skipped_count));
}
/// Intenta detectar el puerto desde el nombre de la app
/// Esto es un fallback simple si no se encuentra en el .service
fn detect_port_from_name(app_name: &str) -> i32 {
// Algunos puertos conocidos por nombre
match app_name.to_lowercase().as_str() {
name if name.contains("tareas") => 3000,
name if name.contains("fidelizacion") => 3001,
name if name.contains("ideas") => 2000,
_ => 8080, // Puerto por defecto genérico
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_app_name() {
assert_eq!(extract_app_name("siax-app-IDEAS.service"), "IDEAS");
assert_eq!(extract_app_name("siax-app-TAREAS.service"), "TAREAS");
assert_eq!(extract_app_name("siax-app-fidelizacion.service"), "fidelizacion");
}
#[test]
fn test_extract_port_from_env() {
assert_eq!(extract_port_from_env("Environment=PORT=3000"), Some(3000));
assert_eq!(extract_port_from_env("Environment=\"PORT=8080\""), Some(8080));
assert_eq!(extract_port_from_env("Environment=NODE_ENV=production"), None);
}
#[test]
fn test_detect_port_from_name() {
assert_eq!(detect_port_from_name("app_tareas"), 3000);
assert_eq!(detect_port_from_name("IDEAS"), 2000);
assert_eq!(detect_port_from_name("unknown_app"), 8080);
}
}

View File

@@ -6,8 +6,10 @@ pub mod logger;
pub mod config;
pub mod monitor;
pub mod interface;
pub mod discovery;
// Re-exportar solo lo necesario para evitar conflictos
pub use models::{ServiceConfig, ManagedApp, AppStatus, ServiceStatus, AppType, RestartPolicy};
pub use logger::{Logger, LogEntry, LogLevel, get_logger};
pub use config::{ConfigManager, MonitoredApp, AppConfig, get_config_manager};
pub use discovery::{discover_services, sync_discovered_services, DiscoveredService};

View File

@@ -6,11 +6,13 @@ mod models;
mod systemd;
mod orchestrator;
mod api;
mod discovery;
use logger::get_logger;
use config::get_config_manager;
use orchestrator::{AppManager, LifecycleManager};
use api::{ApiState, WebSocketManager};
use discovery::{discover_services, sync_discovered_services};
use std::sync::Arc;
use axum::{
routing::{get, post, delete},
@@ -24,7 +26,14 @@ async fn main() {
let logger = get_logger();
logger.info("Sistema", "Iniciando SIAX Agent");
// Inicializar config manager
// 🔍 Descubrir servicios systemd existentes
logger.info("Sistema", "Escaneando servicios systemd existentes...");
let discovered = discover_services();
if !discovered.is_empty() {
sync_discovered_services(discovered);
}
// Inicializar config manager (ahora con servicios descubiertos)
let config_manager = get_config_manager();
let apps = config_manager.get_apps();
println!("📋 Apps a monitorear: {:?}", apps);