feat: Implementación completa Fase 4 - Sistema de monitoreo con API REST y WebSocket
✨ Nuevas funcionalidades: - API REST unificada en puerto 8080 (eliminado CORS) - WebSocket para logs en tiempo real desde journalctl - Integración completa con systemd para gestión de servicios - Escaneo automático de procesos Node.js y Python - Rate limiting (1 operación/segundo por app) - Interface web moderna con Tailwind CSS (tema oscuro) - Documentación API estilo Swagger completamente en español 🎨 Interface Web (todas las páginas en español): - Dashboard con estadísticas en tiempo real - Visor de escaneo de procesos con filtros - Formulario de registro de aplicaciones con variables de entorno - Visor de logs en tiempo real con WebSocket y sidebar - Página de selección de apps detectadas - Documentación completa de API REST 🏗️ Arquitectura: - Módulo models: ServiceConfig, ManagedApp, AppStatus - Módulo systemd: wrapper de systemctl, generador de .service, parser - Módulo orchestrator: AppManager, LifecycleManager con validaciones - Módulo api: handlers REST, WebSocket manager, DTOs - Servidor unificado en puerto 8080 (Web + API + WS) 🔧 Mejoras técnicas: - Eliminación de CORS mediante servidor unificado - Separación clara frontend/backend con carga dinámica - Thread-safe con Arc<DashMap> para estado compartido - Reconciliación de estados: sysinfo vs systemd - Validaciones de paths, usuarios y configuraciones - Manejo robusto de errores con thiserror 📝 Documentación: - README.md actualizado con arquitectura completa - EJEMPLOS.md con casos de uso detallados - ESTADO_PROYECTO.md con progreso de Fase 4 - API docs interactiva en /api-docs - Script de despliegue mejorado con health checks 🚀 Producción: - Deployment script con validaciones - Health checks y rollback capability - Configuración de sudoers para systemctl - Hardening de seguridad en servicios systemd
This commit is contained in:
29
src/systemd/mod.rs
Normal file
29
src/systemd/mod.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
pub mod systemctl;
|
||||
pub mod service_generator;
|
||||
pub mod parser;
|
||||
|
||||
pub use systemctl::*;
|
||||
pub use service_generator::*;
|
||||
pub use parser::*;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SystemdError {
|
||||
#[error("Error ejecutando comando systemctl: {0}")]
|
||||
CommandError(String),
|
||||
|
||||
#[error("Permisos insuficientes: {0}")]
|
||||
PermissionError(String),
|
||||
|
||||
#[error("Servicio no encontrado: {0}")]
|
||||
ServiceNotFound(String),
|
||||
|
||||
#[error("Error de validación: {0}")]
|
||||
ValidationError(String),
|
||||
|
||||
#[error("Error de I/O: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, SystemdError>;
|
||||
34
src/systemd/parser.rs
Normal file
34
src/systemd/parser.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use crate::models::ServiceStatus;
|
||||
|
||||
pub struct SystemdParser;
|
||||
|
||||
impl SystemdParser {
|
||||
pub fn parse_status_output(output: &str) -> ServiceStatus {
|
||||
let output_lower = output.to_lowercase();
|
||||
|
||||
if output_lower.contains("active (running)") {
|
||||
ServiceStatus::Active
|
||||
} else if output_lower.contains("inactive") {
|
||||
ServiceStatus::Inactive
|
||||
} else if output_lower.contains("failed") {
|
||||
ServiceStatus::Failed
|
||||
} else if output_lower.contains("activating") {
|
||||
ServiceStatus::Activating
|
||||
} else if output_lower.contains("deactivating") {
|
||||
ServiceStatus::Deactivating
|
||||
} else {
|
||||
ServiceStatus::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_is_active_output(output: &str) -> ServiceStatus {
|
||||
match output.trim().to_lowercase().as_str() {
|
||||
"active" => ServiceStatus::Active,
|
||||
"inactive" => ServiceStatus::Inactive,
|
||||
"failed" => ServiceStatus::Failed,
|
||||
"activating" => ServiceStatus::Activating,
|
||||
"deactivating" => ServiceStatus::Deactivating,
|
||||
_ => ServiceStatus::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
152
src/systemd/service_generator.rs
Normal file
152
src/systemd/service_generator.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use super::{Result, SystemdError};
|
||||
use crate::models::ServiceConfig;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use crate::logger::get_logger;
|
||||
|
||||
pub struct ServiceGenerator;
|
||||
|
||||
impl ServiceGenerator {
|
||||
pub fn create_service(config: &ServiceConfig) -> Result<String> {
|
||||
let logger = get_logger();
|
||||
|
||||
// Validar configuración
|
||||
config.validate().map_err(|e| SystemdError::ValidationError(e))?;
|
||||
|
||||
// Validar que el script existe
|
||||
if !Path::new(&config.script_path).exists() {
|
||||
logger.error("ServiceGenerator", "Script no encontrado", Some(&config.script_path));
|
||||
return Err(SystemdError::ValidationError(
|
||||
format!("El script '{}' no existe", config.script_path)
|
||||
));
|
||||
}
|
||||
|
||||
// Validar que el directorio de trabajo existe
|
||||
if !Path::new(&config.working_directory).exists() {
|
||||
logger.error("ServiceGenerator", "Directorio de trabajo no encontrado", Some(&config.working_directory));
|
||||
return Err(SystemdError::ValidationError(
|
||||
format!("El directorio '{}' no existe", config.working_directory)
|
||||
));
|
||||
}
|
||||
|
||||
// Validar que el usuario existe
|
||||
if !Self::user_exists(&config.user) {
|
||||
logger.warning("ServiceGenerator", "Usuario podría no existir", Some(&config.user));
|
||||
}
|
||||
|
||||
// Generar contenido del servicio
|
||||
let service_content = Self::generate_service_content(config);
|
||||
|
||||
logger.info("ServiceGenerator", &format!("Servicio generado: {}", config.service_name()));
|
||||
|
||||
Ok(service_content)
|
||||
}
|
||||
|
||||
fn generate_service_content(config: &ServiceConfig) -> String {
|
||||
let default_desc = format!("SIAX Managed Service: {}", config.app_name);
|
||||
let description = config.description.as_ref()
|
||||
.map(|d| d.as_str())
|
||||
.unwrap_or(&default_desc);
|
||||
|
||||
let executable = config.app_type.get_executable();
|
||||
|
||||
// Generar variables de entorno
|
||||
let env_vars = config.environment
|
||||
.iter()
|
||||
.map(|(key, value)| format!("Environment=\"{}={}\"", key, value))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
format!(
|
||||
r#"[Unit]
|
||||
Description={}
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={}
|
||||
WorkingDirectory={}
|
||||
ExecStart={} {}
|
||||
Restart={}
|
||||
RestartSec=10
|
||||
{}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"#,
|
||||
description,
|
||||
config.user,
|
||||
config.working_directory,
|
||||
executable,
|
||||
config.script_path,
|
||||
config.restart_policy.as_systemd_str(),
|
||||
env_vars
|
||||
)
|
||||
}
|
||||
|
||||
pub fn write_service_file(config: &ServiceConfig, content: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
let service_path = format!("/etc/systemd/system/{}", config.service_name());
|
||||
|
||||
logger.info("ServiceGenerator", &format!("Escribiendo servicio en: {}", service_path));
|
||||
|
||||
match fs::write(&service_path, content) {
|
||||
Ok(_) => {
|
||||
logger.info("ServiceGenerator", &format!("Servicio {} creado exitosamente", config.service_name()));
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
logger.error("ServiceGenerator", "Permisos insuficientes", Some(&service_path));
|
||||
Err(SystemdError::PermissionError(
|
||||
"Se requieren permisos sudo para escribir en /etc/systemd/system".to_string()
|
||||
))
|
||||
} else {
|
||||
logger.error("ServiceGenerator", "Error escribiendo archivo", Some(&e.to_string()));
|
||||
Err(SystemdError::IoError(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_service_file(service_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
let service_path = format!("/etc/systemd/system/{}", service_name);
|
||||
|
||||
logger.info("ServiceGenerator", &format!("Eliminando servicio: {}", service_path));
|
||||
|
||||
match fs::remove_file(&service_path) {
|
||||
Ok(_) => {
|
||||
logger.info("ServiceGenerator", &format!("Servicio {} eliminado exitosamente", service_name));
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
logger.warning("ServiceGenerator", "Servicio no encontrado", Some(service_name));
|
||||
Ok(()) // No es un error si ya no existe
|
||||
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
logger.error("ServiceGenerator", "Permisos insuficientes", Some(&service_path));
|
||||
Err(SystemdError::PermissionError(
|
||||
"Se requieren permisos sudo para eliminar servicios".to_string()
|
||||
))
|
||||
} else {
|
||||
logger.error("ServiceGenerator", "Error eliminando archivo", Some(&e.to_string()));
|
||||
Err(SystemdError::IoError(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn user_exists(username: &str) -> bool {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("id")
|
||||
.arg(username)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => out.status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
171
src/systemd/systemctl.rs
Normal file
171
src/systemd/systemctl.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use super::{Result, SystemdError};
|
||||
use crate::models::ServiceStatus;
|
||||
use std::process::Command;
|
||||
use crate::logger::get_logger;
|
||||
|
||||
pub struct SystemCtl;
|
||||
|
||||
impl SystemCtl {
|
||||
pub fn start(service_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
logger.info("SystemCtl", &format!("Iniciando servicio: {}", service_name));
|
||||
|
||||
let output = Command::new("systemctl")
|
||||
.arg("start")
|
||||
.arg(service_name)
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
logger.info("SystemCtl", &format!("Servicio {} iniciado exitosamente", service_name));
|
||||
Ok(())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if error.contains("permission denied") || error.contains("Authentication is required") {
|
||||
logger.error("SystemCtl", "Error de permisos", Some(&error));
|
||||
Err(SystemdError::PermissionError(
|
||||
"Se requieren permisos sudo. Configura sudoers para systemctl".to_string()
|
||||
))
|
||||
} else if error.contains("not found") || error.contains("not-found") {
|
||||
logger.error("SystemCtl", "Servicio no encontrado", Some(service_name));
|
||||
Err(SystemdError::ServiceNotFound(service_name.to_string()))
|
||||
} else {
|
||||
logger.error("SystemCtl", "Error ejecutando start", Some(&error));
|
||||
Err(SystemdError::CommandError(error.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(service_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
logger.info("SystemCtl", &format!("Deteniendo servicio: {}", service_name));
|
||||
|
||||
let output = Command::new("systemctl")
|
||||
.arg("stop")
|
||||
.arg(service_name)
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
logger.info("SystemCtl", &format!("Servicio {} detenido exitosamente", service_name));
|
||||
Ok(())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if error.contains("permission denied") || error.contains("Authentication is required") {
|
||||
Err(SystemdError::PermissionError(
|
||||
"Se requieren permisos sudo. Configura sudoers para systemctl".to_string()
|
||||
))
|
||||
} else {
|
||||
Err(SystemdError::CommandError(error.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn restart(service_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
logger.info("SystemCtl", &format!("Reiniciando servicio: {}", service_name));
|
||||
|
||||
let output = Command::new("systemctl")
|
||||
.arg("restart")
|
||||
.arg(service_name)
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
logger.info("SystemCtl", &format!("Servicio {} reiniciado exitosamente", service_name));
|
||||
Ok(())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
if error.contains("permission denied") || error.contains("Authentication is required") {
|
||||
Err(SystemdError::PermissionError(
|
||||
"Se requieren permisos sudo. Configura sudoers para systemctl".to_string()
|
||||
))
|
||||
} else {
|
||||
Err(SystemdError::CommandError(error.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enable(service_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
logger.info("SystemCtl", &format!("Habilitando servicio: {}", service_name));
|
||||
|
||||
let output = Command::new("systemctl")
|
||||
.arg("enable")
|
||||
.arg(service_name)
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
logger.info("SystemCtl", &format!("Servicio {} habilitado exitosamente", service_name));
|
||||
Ok(())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(SystemdError::CommandError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disable(service_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
logger.info("SystemCtl", &format!("Deshabilitando servicio: {}", service_name));
|
||||
|
||||
let output = Command::new("systemctl")
|
||||
.arg("disable")
|
||||
.arg(service_name)
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
logger.info("SystemCtl", &format!("Servicio {} deshabilitado exitosamente", service_name));
|
||||
Ok(())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(SystemdError::CommandError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn daemon_reload() -> Result<()> {
|
||||
let logger = get_logger();
|
||||
logger.info("SystemCtl", "Recargando daemon de systemd");
|
||||
|
||||
let output = Command::new("systemctl")
|
||||
.arg("daemon-reload")
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
logger.info("SystemCtl", "Daemon recargado exitosamente");
|
||||
Ok(())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
Err(SystemdError::CommandError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(service_name: &str) -> ServiceStatus {
|
||||
let output = Command::new("systemctl")
|
||||
.arg("is-active")
|
||||
.arg(service_name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let status_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
ServiceStatus::from_str(&status_str)
|
||||
}
|
||||
Err(_) => ServiceStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_service_exists(service_name: &str) -> bool {
|
||||
let output = Command::new("systemctl")
|
||||
.arg("list-unit-files")
|
||||
.arg(service_name)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let output_str = String::from_utf8_lossy(&out.stdout);
|
||||
output_str.contains(service_name)
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user