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:
226
src/discovery.rs
Normal file
226
src/discovery.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
11
src/main.rs
11
src/main.rs
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user