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:
86
src/api/dto.rs
Normal file
86
src/api/dto.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct RegisterAppRequest {
|
||||
pub app_name: String,
|
||||
pub script_path: String,
|
||||
pub working_directory: String,
|
||||
pub user: String,
|
||||
#[serde(default)]
|
||||
pub environment: HashMap<String, String>,
|
||||
#[serde(default = "default_restart_policy")]
|
||||
pub restart_policy: String,
|
||||
pub app_type: String,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
fn default_restart_policy() -> String {
|
||||
"always".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ApiResponse<T> {
|
||||
pub success: bool,
|
||||
pub data: Option<T>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
ApiResponse {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(error: String) -> Self {
|
||||
ApiResponse {
|
||||
success: false,
|
||||
data: None,
|
||||
error: Some(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AppStatusResponse {
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
pub pid: Option<i32>,
|
||||
pub cpu_usage: f32,
|
||||
pub memory_usage: String,
|
||||
pub systemd_status: String,
|
||||
pub last_updated: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AppListResponse {
|
||||
pub apps: Vec<String>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct OperationResponse {
|
||||
pub app_name: String,
|
||||
pub operation: String,
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DetectedProcess {
|
||||
pub pid: i32,
|
||||
pub name: String,
|
||||
pub user: Option<String>,
|
||||
pub cpu_usage: f64,
|
||||
pub memory_mb: f64,
|
||||
pub process_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ProcessScanResponse {
|
||||
pub processes: Vec<DetectedProcess>,
|
||||
pub total: usize,
|
||||
}
|
||||
195
src/api/handlers.rs
Normal file
195
src/api/handlers.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use sysinfo::System;
|
||||
use crate::orchestrator::{AppManager, LifecycleManager};
|
||||
use crate::models::{ServiceConfig, RestartPolicy, AppType};
|
||||
use super::dto::*;
|
||||
|
||||
pub struct ApiState {
|
||||
pub app_manager: Arc<AppManager>,
|
||||
pub lifecycle_manager: Arc<LifecycleManager>,
|
||||
}
|
||||
|
||||
pub async fn register_app_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Json(payload): Json<RegisterAppRequest>,
|
||||
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
|
||||
|
||||
// Parsear tipo de aplicación
|
||||
let app_type = match payload.app_type.to_lowercase().as_str() {
|
||||
"nodejs" | "node" => AppType::NodeJs,
|
||||
"python" | "py" => AppType::Python,
|
||||
_ => return Ok(Json(ApiResponse::error(
|
||||
"Tipo de aplicación inválido. Use 'nodejs' o 'python'".to_string()
|
||||
))),
|
||||
};
|
||||
|
||||
// Parsear política de reinicio
|
||||
let restart_policy = match payload.restart_policy.to_lowercase().as_str() {
|
||||
"always" => RestartPolicy::Always,
|
||||
"on-failure" | "onfailure" => RestartPolicy::OnFailure,
|
||||
"no" | "never" => RestartPolicy::No,
|
||||
_ => RestartPolicy::Always,
|
||||
};
|
||||
|
||||
let config = ServiceConfig {
|
||||
app_name: payload.app_name.clone(),
|
||||
script_path: payload.script_path,
|
||||
working_directory: payload.working_directory,
|
||||
user: payload.user,
|
||||
environment: payload.environment,
|
||||
restart_policy,
|
||||
app_type,
|
||||
description: payload.description,
|
||||
};
|
||||
|
||||
match state.app_manager.register_app(config) {
|
||||
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
|
||||
app_name: payload.app_name,
|
||||
operation: "register".to_string(),
|
||||
success: true,
|
||||
message: "Aplicación registrada exitosamente".to_string(),
|
||||
}))),
|
||||
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn unregister_app_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Path(app_name): Path<String>,
|
||||
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
|
||||
|
||||
match state.app_manager.unregister_app(&app_name) {
|
||||
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
|
||||
app_name: app_name.clone(),
|
||||
operation: "unregister".to_string(),
|
||||
success: true,
|
||||
message: "Aplicación eliminada exitosamente".to_string(),
|
||||
}))),
|
||||
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_app_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Path(app_name): Path<String>,
|
||||
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
|
||||
|
||||
match state.lifecycle_manager.start_app(&app_name) {
|
||||
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
|
||||
app_name: app_name.clone(),
|
||||
operation: "start".to_string(),
|
||||
success: true,
|
||||
message: "Aplicación iniciada exitosamente".to_string(),
|
||||
}))),
|
||||
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn stop_app_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Path(app_name): Path<String>,
|
||||
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
|
||||
|
||||
match state.lifecycle_manager.stop_app(&app_name) {
|
||||
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
|
||||
app_name: app_name.clone(),
|
||||
operation: "stop".to_string(),
|
||||
success: true,
|
||||
message: "Aplicación detenida exitosamente".to_string(),
|
||||
}))),
|
||||
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn restart_app_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Path(app_name): Path<String>,
|
||||
) -> Result<Json<ApiResponse<OperationResponse>>, StatusCode> {
|
||||
|
||||
match state.lifecycle_manager.restart_app(&app_name) {
|
||||
Ok(_) => Ok(Json(ApiResponse::success(OperationResponse {
|
||||
app_name: app_name.clone(),
|
||||
operation: "restart".to_string(),
|
||||
success: true,
|
||||
message: "Aplicación reiniciada exitosamente".to_string(),
|
||||
}))),
|
||||
Err(e) => Ok(Json(ApiResponse::error(e.to_string()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_app_status_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
Path(app_name): Path<String>,
|
||||
) -> Result<Json<ApiResponse<AppStatusResponse>>, StatusCode> {
|
||||
|
||||
match state.app_manager.get_app_status(&app_name) {
|
||||
Some(managed_app) => {
|
||||
let response = AppStatusResponse {
|
||||
name: managed_app.name,
|
||||
status: managed_app.status.as_str().to_string(),
|
||||
pid: managed_app.pid,
|
||||
cpu_usage: managed_app.cpu_usage,
|
||||
memory_usage: format!("{:.2} MB", managed_app.memory_usage as f64 / 1024.0 / 1024.0),
|
||||
systemd_status: managed_app.systemd_status.as_str().to_string(),
|
||||
last_updated: managed_app.last_updated,
|
||||
};
|
||||
Ok(Json(ApiResponse::success(response)))
|
||||
}
|
||||
None => Ok(Json(ApiResponse::error(
|
||||
format!("Aplicación '{}' no encontrada", app_name)
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_apps_handler(
|
||||
State(state): State<Arc<ApiState>>,
|
||||
) -> Result<Json<ApiResponse<AppListResponse>>, StatusCode> {
|
||||
|
||||
let apps = state.app_manager.list_apps();
|
||||
let total = apps.len();
|
||||
|
||||
Ok(Json(ApiResponse::success(AppListResponse { apps, total })))
|
||||
}
|
||||
|
||||
pub async fn scan_processes_handler() -> Result<Json<ApiResponse<ProcessScanResponse>>, StatusCode> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
let mut detected_processes = Vec::new();
|
||||
|
||||
for (pid, process) in sys.processes() {
|
||||
let process_name = process.name().to_lowercase();
|
||||
let cmd = process.cmd().join(" ").to_lowercase();
|
||||
|
||||
let process_type = if process_name.contains("node") || cmd.contains("node") {
|
||||
Some("nodejs")
|
||||
} else if process_name.contains("python") || cmd.contains("python") {
|
||||
Some("python")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(ptype) = process_type {
|
||||
detected_processes.push(DetectedProcess {
|
||||
pid: pid.as_u32() as i32,
|
||||
name: process.name().to_string(),
|
||||
user: process.user_id().map(|u| u.to_string()),
|
||||
cpu_usage: process.cpu_usage() as f64,
|
||||
memory_mb: process.memory() as f64 / 1024.0 / 1024.0,
|
||||
process_type: ptype.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let total = detected_processes.len();
|
||||
|
||||
Ok(Json(ApiResponse::success(ProcessScanResponse {
|
||||
processes: detected_processes,
|
||||
total,
|
||||
})))
|
||||
}
|
||||
7
src/api/mod.rs
Normal file
7
src/api/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod handlers;
|
||||
pub mod websocket;
|
||||
pub mod dto;
|
||||
|
||||
pub use handlers::*;
|
||||
pub use websocket::*;
|
||||
pub use dto::*;
|
||||
188
src/api/websocket.rs
Normal file
188
src/api/websocket.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message, WebSocket, WebSocketUpgrade},
|
||||
Path, State,
|
||||
},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use futures::{sink::SinkExt, stream::StreamExt};
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use crate::logger::get_logger;
|
||||
use dashmap::DashMap;
|
||||
|
||||
pub struct WebSocketManager {
|
||||
active_connections: Arc<DashMap<String, usize>>,
|
||||
max_connections_per_app: usize,
|
||||
}
|
||||
|
||||
impl WebSocketManager {
|
||||
pub fn new() -> Self {
|
||||
WebSocketManager {
|
||||
active_connections: Arc::new(DashMap::new()),
|
||||
max_connections_per_app: 5,
|
||||
}
|
||||
}
|
||||
|
||||
fn can_connect(&self, app_name: &str) -> bool {
|
||||
let count = self.active_connections
|
||||
.get(app_name)
|
||||
.map(|v| *v)
|
||||
.unwrap_or(0);
|
||||
|
||||
count < self.max_connections_per_app
|
||||
}
|
||||
|
||||
fn increment_connection(&self, app_name: &str) {
|
||||
self.active_connections
|
||||
.entry(app_name.to_string())
|
||||
.and_modify(|c| *c += 1)
|
||||
.or_insert(1);
|
||||
}
|
||||
|
||||
fn decrement_connection(&self, app_name: &str) {
|
||||
self.active_connections
|
||||
.entry(app_name.to_string())
|
||||
.and_modify(|c| {
|
||||
if *c > 0 {
|
||||
*c -= 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WebSocketManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn logs_websocket_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
Path(app_name): Path<String>,
|
||||
State(ws_manager): State<Arc<WebSocketManager>>,
|
||||
) -> impl IntoResponse {
|
||||
let logger = get_logger();
|
||||
|
||||
// Verificar límite de conexiones
|
||||
if !ws_manager.can_connect(&app_name) {
|
||||
logger.warning(
|
||||
"WebSocket",
|
||||
"Límite de conexiones excedido",
|
||||
Some(&app_name)
|
||||
);
|
||||
return ws.on_upgrade(move |socket| async move {
|
||||
let mut socket = socket;
|
||||
let _ = socket.send(Message::Text(
|
||||
"Error: Límite de conexiones simultáneas excedido".to_string()
|
||||
)).await;
|
||||
let _ = socket.close().await;
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("WebSocket", &format!("Nueva conexión para logs de: {}", app_name));
|
||||
ws_manager.increment_connection(&app_name);
|
||||
|
||||
ws.on_upgrade(move |socket| handle_logs_socket(socket, app_name, ws_manager))
|
||||
}
|
||||
|
||||
async fn handle_logs_socket(
|
||||
socket: WebSocket,
|
||||
app_name: String,
|
||||
ws_manager: Arc<WebSocketManager>,
|
||||
) {
|
||||
let logger = get_logger();
|
||||
let service_name = format!("{}.service", app_name);
|
||||
|
||||
// Iniciar journalctl
|
||||
let mut child = match TokioCommand::new("journalctl")
|
||||
.arg("-u")
|
||||
.arg(&service_name)
|
||||
.arg("-f")
|
||||
.arg("--output=json")
|
||||
.arg("-n")
|
||||
.arg("50") // Últimas 50 líneas
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => child,
|
||||
Err(e) => {
|
||||
logger.error("WebSocket", "Error iniciando journalctl", Some(&e.to_string()));
|
||||
ws_manager.decrement_connection(&app_name);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let stdout = child.stdout.take().unwrap();
|
||||
let reader = BufReader::new(stdout);
|
||||
let mut lines = reader.lines();
|
||||
|
||||
let (mut sender, mut receiver) = socket.split();
|
||||
|
||||
// Enviar mensaje de bienvenida
|
||||
let welcome = format!("📡 Conectado a logs de: {}", app_name);
|
||||
let _ = sender.send(Message::Text(welcome)).await;
|
||||
|
||||
// Task para enviar logs
|
||||
let send_task = tokio::spawn(async move {
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
// Parsear JSON de journalctl
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) {
|
||||
let message = json.get("MESSAGE")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or(&line);
|
||||
|
||||
let timestamp = json.get("__REALTIME_TIMESTAMP")
|
||||
.and_then(|t| t.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let formatted = if !timestamp.is_empty() {
|
||||
format!("[{}] {}", timestamp, message)
|
||||
} else {
|
||||
message.to_string()
|
||||
};
|
||||
|
||||
if sender.send(Message::Text(formatted)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Si no es JSON, enviar la línea tal cual
|
||||
if sender.send(Message::Text(line)).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Task para recibir mensajes del cliente (para detectar desconexión)
|
||||
let receive_task = tokio::spawn(async move {
|
||||
while let Some(msg) = receiver.next().await {
|
||||
if let Ok(msg) = msg {
|
||||
match msg {
|
||||
Message::Close(_) => break,
|
||||
Message::Ping(_) => {
|
||||
// Los pings se manejan automáticamente
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Esperar a que termine alguna de las dos tasks
|
||||
tokio::select! {
|
||||
_ = send_task => {},
|
||||
_ = receive_task => {},
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
let _ = child.kill().await;
|
||||
ws_manager.decrement_connection(&app_name);
|
||||
|
||||
logger.info("WebSocket", &format!("Conexión cerrada para: {}", app_name));
|
||||
}
|
||||
214
src/interface.rs
214
src/interface.rs
@@ -4,10 +4,8 @@ use axum::{
|
||||
Router,
|
||||
extract::Form,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use sysinfo::System;
|
||||
use serde::Deserialize;
|
||||
use crate::logger::{get_logger, LogLevel};
|
||||
use crate::logger::get_logger;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ProcessForm {
|
||||
@@ -15,20 +13,16 @@ struct ProcessForm {
|
||||
port: String,
|
||||
}
|
||||
|
||||
pub async fn start_web_server(port: u16) {
|
||||
let app = Router::new()
|
||||
pub fn create_web_router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(index_handler))
|
||||
.route("/scan", get(scan_processes_handler))
|
||||
.route("/select", get(select_processes_handler))
|
||||
.route("/register", get(register_handler))
|
||||
.route("/add-process", post(add_process_handler))
|
||||
.route("/logs", get(logs_handler))
|
||||
.route("/clear-logs", post(clear_logs_handler));
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
println!("🖥️ Interface Web en: http://localhost:{}", port);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
.route("/clear-logs", post(clear_logs_handler))
|
||||
.route("/api-docs", get(api_docs_handler))
|
||||
}
|
||||
|
||||
async fn index_handler() -> Html<String> {
|
||||
@@ -38,105 +32,13 @@ async fn index_handler() -> Html<String> {
|
||||
}
|
||||
|
||||
async fn scan_processes_handler() -> Html<String> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
let template = include_str!("../web/scan.html");
|
||||
let mut content = String::new();
|
||||
let mut node_count = 0;
|
||||
|
||||
for (pid, process) in sys.processes() {
|
||||
let process_name = process.name();
|
||||
|
||||
if process_name.contains("node") {
|
||||
node_count += 1;
|
||||
let cpu = process.cpu_usage();
|
||||
let mem_mb = process.memory() as f64 / 1024.0 / 1024.0;
|
||||
|
||||
let cwd = if let Some(path) = process.cwd() {
|
||||
path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
};
|
||||
|
||||
content.push_str(&format!(
|
||||
r#"
|
||||
<div class="process">
|
||||
<div><span class="pid">PID: {}</span> | <span class="name">{}</span></div>
|
||||
<div><span class="cpu">CPU: {:.2}%</span> | <span class="mem">RAM: {:.2} MB</span></div>
|
||||
<div class="path">📁 {}</div>
|
||||
</div>
|
||||
"#,
|
||||
pid.as_u32(),
|
||||
process_name,
|
||||
cpu,
|
||||
mem_mb,
|
||||
cwd
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if node_count == 0 {
|
||||
content = r#"<div class="no-results">⚠️ No se detectaron procesos Node.js en ejecución</div>"#.to_string();
|
||||
} else {
|
||||
let summary = format!(r#"<p class="summary">✅ Total: {} proceso(s) Node.js detectado(s)</p>"#, node_count);
|
||||
content = summary + &content;
|
||||
}
|
||||
|
||||
let html = template.replace("{{CONTENT}}", &content);
|
||||
Html(html)
|
||||
Html(template.to_string())
|
||||
}
|
||||
|
||||
async fn select_processes_handler() -> Html<String> {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
let template = include_str!("../web/select.html");
|
||||
let mut processes_list = String::new();
|
||||
let mut node_processes = Vec::new();
|
||||
|
||||
for (pid, process) in sys.processes() {
|
||||
let process_name = process.name();
|
||||
|
||||
if process_name.contains("node") {
|
||||
let cwd = if let Some(path) = process.cwd() {
|
||||
path.to_string_lossy().to_string()
|
||||
} else {
|
||||
"N/A".to_string()
|
||||
};
|
||||
|
||||
node_processes.push((pid.as_u32(), process_name.to_string(), cwd));
|
||||
}
|
||||
}
|
||||
|
||||
if node_processes.is_empty() {
|
||||
processes_list = r#"<div class="no-results">⚠️ No se detectaron procesos Node.js en ejecución</div>"#.to_string();
|
||||
} else {
|
||||
for (pid, name, cwd) in node_processes {
|
||||
let suggested_name = cwd.split(&['/', '\\'][..])
|
||||
.filter(|s| !s.is_empty())
|
||||
.last()
|
||||
.unwrap_or("app");
|
||||
|
||||
processes_list.push_str(&format!(
|
||||
r#"
|
||||
<div class="process-item">
|
||||
<div class="process-info">
|
||||
<div><span class="pid">PID: {}</span> | {}</div>
|
||||
<div class="path">📁 {}</div>
|
||||
</div>
|
||||
<button class="select-btn" onclick="fillForm('{}', {})">
|
||||
✅ Seleccionar
|
||||
</button>
|
||||
</div>
|
||||
"#,
|
||||
pid, name, cwd, suggested_name, pid
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let html = template.replace("{{PROCESSES_LIST}}", &processes_list);
|
||||
Html(html)
|
||||
Html(template.to_string())
|
||||
}
|
||||
|
||||
async fn add_process_handler(Form(form): Form<ProcessForm>) -> Html<String> {
|
||||
@@ -154,94 +56,8 @@ async fn add_process_handler(Form(form): Form<ProcessForm>) -> Html<String> {
|
||||
}
|
||||
|
||||
async fn logs_handler() -> Html<String> {
|
||||
let logger = get_logger();
|
||||
let template = include_str!("../web/logs.html");
|
||||
|
||||
let logs = logger.read_logs(Some(100)); // Últimos 100 logs
|
||||
|
||||
// Calcular estadísticas
|
||||
let mut info_count = 0;
|
||||
let mut warning_count = 0;
|
||||
let mut error_count = 0;
|
||||
let mut critical_count = 0;
|
||||
|
||||
for log in &logs {
|
||||
match log.level {
|
||||
LogLevel::Info => info_count += 1,
|
||||
LogLevel::Warning => warning_count += 1,
|
||||
LogLevel::Error => error_count += 1,
|
||||
LogLevel::Critical => critical_count += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let stats = format!(
|
||||
r#"
|
||||
<div class="stat-item stat-info">
|
||||
<div class="stat-number" style="color: #3b82f6;">{}</div>
|
||||
<div class="stat-label">Info</div>
|
||||
</div>
|
||||
<div class="stat-item stat-warning">
|
||||
<div class="stat-number" style="color: #f59e0b;">{}</div>
|
||||
<div class="stat-label">Warnings</div>
|
||||
</div>
|
||||
<div class="stat-item stat-error">
|
||||
<div class="stat-number" style="color: #ef4444;">{}</div>
|
||||
<div class="stat-label">Errors</div>
|
||||
</div>
|
||||
<div class="stat-item stat-critical">
|
||||
<div class="stat-number" style="color: #dc2626;">{}</div>
|
||||
<div class="stat-label">Critical</div>
|
||||
</div>
|
||||
"#,
|
||||
info_count, warning_count, error_count, critical_count
|
||||
);
|
||||
|
||||
let mut logs_html = String::new();
|
||||
|
||||
if logs.is_empty() {
|
||||
logs_html = r#"<div class="no-logs">📭 No hay logs registrados</div>"#.to_string();
|
||||
} else {
|
||||
for log in logs {
|
||||
let level_class = match log.level {
|
||||
LogLevel::Info => "info",
|
||||
LogLevel::Warning => "warning",
|
||||
LogLevel::Error => "error",
|
||||
LogLevel::Critical => "critical",
|
||||
};
|
||||
|
||||
let details_html = if let Some(details) = &log.details {
|
||||
format!(r#"<div class="log-details">📝 {}</div>"#, details)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
logs_html.push_str(&format!(
|
||||
r#"
|
||||
<div class="log-entry log-{}" data-level="{}">
|
||||
<div class="log-header">
|
||||
<span class="log-module">[{}]</span>
|
||||
<span class="log-timestamp">{}</span>
|
||||
</div>
|
||||
<div class="log-message">{} {}</div>
|
||||
{}
|
||||
</div>
|
||||
"#,
|
||||
level_class,
|
||||
level_class,
|
||||
log.module,
|
||||
log.timestamp,
|
||||
log.level.emoji(),
|
||||
log.message,
|
||||
details_html
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let html = template
|
||||
.replace("{{STATS}}", &stats)
|
||||
.replace("{{LOGS}}", &logs_html);
|
||||
|
||||
Html(html)
|
||||
Html(template.to_string())
|
||||
}
|
||||
|
||||
async fn clear_logs_handler() -> Html<&'static str> {
|
||||
@@ -257,4 +73,14 @@ async fn clear_logs_handler() -> Html<&'static str> {
|
||||
Html("ERROR")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn register_handler() -> Html<String> {
|
||||
let template = include_str!("../web/register.html");
|
||||
Html(template.to_string())
|
||||
}
|
||||
|
||||
async fn api_docs_handler() -> Html<String> {
|
||||
let template = include_str!("../web/api-docs.html");
|
||||
Html(template.to_string())
|
||||
}
|
||||
|
||||
13
src/lib.rs
Normal file
13
src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod models;
|
||||
pub mod systemd;
|
||||
pub mod orchestrator;
|
||||
pub mod api;
|
||||
pub mod logger;
|
||||
pub mod config;
|
||||
pub mod monitor;
|
||||
pub mod interface;
|
||||
|
||||
// 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};
|
||||
70
src/main.rs
70
src/main.rs
@@ -2,9 +2,21 @@ mod monitor;
|
||||
mod interface;
|
||||
mod logger;
|
||||
mod config;
|
||||
mod models;
|
||||
mod systemd;
|
||||
mod orchestrator;
|
||||
mod api;
|
||||
|
||||
use logger::get_logger;
|
||||
use config::get_config_manager;
|
||||
use orchestrator::{AppManager, LifecycleManager};
|
||||
use api::{ApiState, WebSocketManager};
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
routing::{get, post, delete},
|
||||
Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
@@ -21,19 +33,63 @@ async fn main() {
|
||||
let api_key = "ak_VVeNzGxK2mCq8s7YpFtHjL3b9dR4TuZ6".to_string();
|
||||
let cloud_url = "https://api.siax-system.net/api/apps_servcs/apps".to_string();
|
||||
|
||||
// 1. Iniciamos el Monitor
|
||||
// Inicializar orchestrator
|
||||
let app_manager = Arc::new(AppManager::new());
|
||||
let lifecycle_manager = Arc::new(LifecycleManager::new());
|
||||
let ws_manager = Arc::new(WebSocketManager::new());
|
||||
|
||||
// Estado compartido para la API
|
||||
let api_state = Arc::new(ApiState {
|
||||
app_manager: app_manager.clone(),
|
||||
lifecycle_manager: lifecycle_manager.clone(),
|
||||
});
|
||||
|
||||
// 1. Iniciamos el Monitor en background
|
||||
let monitor_handle = tokio::spawn(async move {
|
||||
monitor::run_monitoring(server_name, api_key, cloud_url).await;
|
||||
});
|
||||
|
||||
// 2. Iniciamos la Interface Web
|
||||
let web_handle = tokio::spawn(async move {
|
||||
interface::start_web_server(8080).await;
|
||||
// 2. Servidor unificado en puerto 8080 (Web UI + API REST + WebSocket)
|
||||
let logger_clone = get_logger();
|
||||
let web_api_handle = tokio::spawn(async move {
|
||||
// Router para la API REST
|
||||
let api_router = Router::new()
|
||||
.route("/api/apps", get(api::list_apps_handler).post(api::register_app_handler))
|
||||
.route("/api/apps/:name", delete(api::unregister_app_handler))
|
||||
.route("/api/apps/:name/status", get(api::get_app_status_handler))
|
||||
.route("/api/apps/:name/start", post(api::start_app_handler))
|
||||
.route("/api/apps/:name/stop", post(api::stop_app_handler))
|
||||
.route("/api/apps/:name/restart", post(api::restart_app_handler))
|
||||
.route("/api/scan", get(api::scan_processes_handler))
|
||||
.with_state(api_state);
|
||||
|
||||
// Router para WebSocket
|
||||
let ws_router = Router::new()
|
||||
.route("/api/apps/:name/logs", get(api::logs_websocket_handler))
|
||||
.with_state(ws_manager);
|
||||
|
||||
// Router para la Interface Web (UI estática)
|
||||
let web_router = interface::create_web_router();
|
||||
|
||||
// Combinar todos los routers
|
||||
let app = Router::new()
|
||||
.merge(api_router)
|
||||
.merge(ws_router)
|
||||
.merge(web_router);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
|
||||
println!("✅ Sistema SIAX operativo en: http://localhost:8080");
|
||||
println!(" 📊 Interface Web: http://localhost:8080");
|
||||
println!(" 🔌 API REST: http://localhost:8080/api");
|
||||
println!(" 📡 WebSocket Logs: ws://localhost:8080/api/apps/:name/logs");
|
||||
logger_clone.info("Sistema", "Sistema SIAX completamente operativo en puerto 8080");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
|
||||
println!("✅ Sistema SIAX operativo. Monitor en segundo plano e Interface en puerto 8080.");
|
||||
logger.info("Sistema", "Sistema SIAX completamente operativo");
|
||||
logger.info("Sistema", "Iniciando servidor unificado en puerto 8080");
|
||||
|
||||
// Esperamos a ambos
|
||||
let _ = tokio::join!(monitor_handle, web_handle);
|
||||
let _ = tokio::join!(monitor_handle, web_api_handle);
|
||||
}
|
||||
|
||||
95
src/models/app.rs
Normal file
95
src/models/app.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ServiceStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Failed,
|
||||
Activating,
|
||||
Deactivating,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ServiceStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ServiceStatus::Active => "active",
|
||||
ServiceStatus::Inactive => "inactive",
|
||||
ServiceStatus::Failed => "failed",
|
||||
ServiceStatus::Activating => "activating",
|
||||
ServiceStatus::Deactivating => "deactivating",
|
||||
ServiceStatus::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
match s.to_lowercase().as_str() {
|
||||
"active" => ServiceStatus::Active,
|
||||
"inactive" => ServiceStatus::Inactive,
|
||||
"failed" => ServiceStatus::Failed,
|
||||
"activating" => ServiceStatus::Activating,
|
||||
"deactivating" => ServiceStatus::Deactivating,
|
||||
_ => ServiceStatus::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ManagedApp {
|
||||
pub name: String,
|
||||
pub status: AppStatus,
|
||||
pub pid: Option<i32>,
|
||||
pub cpu_usage: f32,
|
||||
pub memory_usage: u64,
|
||||
pub systemd_status: ServiceStatus,
|
||||
pub last_updated: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AppStatus {
|
||||
Running,
|
||||
Stopped,
|
||||
Failed,
|
||||
Crashed,
|
||||
Zombie,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl AppStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
AppStatus::Running => "running",
|
||||
AppStatus::Stopped => "stopped",
|
||||
AppStatus::Failed => "failed",
|
||||
AppStatus::Crashed => "crashed",
|
||||
AppStatus::Zombie => "zombie",
|
||||
AppStatus::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reconcile(process_detected: bool, systemd_status: &ServiceStatus) -> Self {
|
||||
match (process_detected, systemd_status) {
|
||||
(true, ServiceStatus::Active) => AppStatus::Running,
|
||||
(false, ServiceStatus::Active) => AppStatus::Crashed,
|
||||
(false, ServiceStatus::Failed) => AppStatus::Failed,
|
||||
(true, ServiceStatus::Inactive) => AppStatus::Zombie,
|
||||
(false, ServiceStatus::Inactive) => AppStatus::Stopped,
|
||||
(_, ServiceStatus::Activating) => AppStatus::Unknown,
|
||||
(_, ServiceStatus::Deactivating) => AppStatus::Unknown,
|
||||
_ => AppStatus::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emoji(&self) -> &str {
|
||||
match self {
|
||||
AppStatus::Running => "✅",
|
||||
AppStatus::Stopped => "⏹️",
|
||||
AppStatus::Failed => "❌",
|
||||
AppStatus::Crashed => "💥",
|
||||
AppStatus::Zombie => "👻",
|
||||
AppStatus::Unknown => "❓",
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/models/mod.rs
Normal file
5
src/models/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod app;
|
||||
pub mod service_config;
|
||||
|
||||
pub use app::*;
|
||||
pub use service_config::*;
|
||||
104
src/models/service_config.rs
Normal file
104
src/models/service_config.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServiceConfig {
|
||||
pub app_name: String,
|
||||
pub script_path: String,
|
||||
pub working_directory: String,
|
||||
pub user: String,
|
||||
pub environment: HashMap<String, String>,
|
||||
pub restart_policy: RestartPolicy,
|
||||
pub app_type: AppType,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AppType {
|
||||
NodeJs,
|
||||
Python,
|
||||
}
|
||||
|
||||
impl AppType {
|
||||
pub fn get_executable(&self) -> &str {
|
||||
match self {
|
||||
AppType::NodeJs => "/usr/bin/node",
|
||||
AppType::Python => "/usr/bin/python3",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_script_path(path: &str) -> Option<Self> {
|
||||
if path.ends_with(".js") || path.ends_with(".mjs") {
|
||||
Some(AppType::NodeJs)
|
||||
} else if path.ends_with(".py") {
|
||||
Some(AppType::Python)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RestartPolicy {
|
||||
Always,
|
||||
OnFailure,
|
||||
No,
|
||||
}
|
||||
|
||||
impl RestartPolicy {
|
||||
pub fn as_systemd_str(&self) -> &str {
|
||||
match self {
|
||||
RestartPolicy::Always => "always",
|
||||
RestartPolicy::OnFailure => "on-failure",
|
||||
RestartPolicy::No => "no",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ServiceConfig {
|
||||
fn default() -> Self {
|
||||
ServiceConfig {
|
||||
app_name: String::new(),
|
||||
script_path: String::new(),
|
||||
working_directory: String::new(),
|
||||
user: "root".to_string(),
|
||||
environment: HashMap::new(),
|
||||
restart_policy: RestartPolicy::Always,
|
||||
app_type: AppType::NodeJs,
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServiceConfig {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.app_name.is_empty() {
|
||||
return Err("app_name no puede estar vacío".to_string());
|
||||
}
|
||||
|
||||
if self.script_path.is_empty() {
|
||||
return Err("script_path no puede estar vacío".to_string());
|
||||
}
|
||||
|
||||
if self.working_directory.is_empty() {
|
||||
return Err("working_directory no puede estar vacío".to_string());
|
||||
}
|
||||
|
||||
if self.user.is_empty() {
|
||||
return Err("user no puede estar vacío".to_string());
|
||||
}
|
||||
|
||||
// Validar que el nombre solo contenga caracteres válidos para systemd
|
||||
if !self.app_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
|
||||
return Err("app_name solo puede contener letras, números, guiones y guiones bajos".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn service_name(&self) -> String {
|
||||
format!("{}.service", self.app_name)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT};
|
||||
use std::time::Duration;
|
||||
use crate::logger::get_logger;
|
||||
use crate::config::get_config_manager;
|
||||
use crate::systemd::SystemCtl;
|
||||
use crate::models::{AppStatus, ServiceStatus};
|
||||
|
||||
// User-Agent dinámico
|
||||
fn generate_user_agent() -> String {
|
||||
@@ -24,6 +26,9 @@ struct AppStatusUpdate {
|
||||
memory_usage: String,
|
||||
cpu_usage: String,
|
||||
last_check: String,
|
||||
systemd_status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
discrepancy: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: String) {
|
||||
@@ -47,7 +52,16 @@ pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: Str
|
||||
}
|
||||
|
||||
for app in apps_to_monitor {
|
||||
let data = collect_metrics(&sys, &app.name, app.port, &server_name);
|
||||
let data = collect_metrics_with_systemd(&sys, &app.name, app.port, &server_name);
|
||||
|
||||
// Reportar discrepancias
|
||||
if let Some(ref disc) = data.discrepancy {
|
||||
logger.warning(
|
||||
"Monitor",
|
||||
&format!("Discrepancia detectada en {}", app.name),
|
||||
Some(disc)
|
||||
);
|
||||
}
|
||||
|
||||
match send_to_cloud(data, &api_key, &cloud_url, &user_agent).await {
|
||||
Ok(_) => {},
|
||||
@@ -66,41 +80,63 @@ pub async fn run_monitoring(server_name: String, api_key: String, cloud_url: Str
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_metrics(sys: &System, name: &str, port: i32, server: &str) -> AppStatusUpdate {
|
||||
fn collect_metrics_with_systemd(sys: &System, name: &str, port: i32, server: &str) -> AppStatusUpdate {
|
||||
let mut pid_encontrado = 0;
|
||||
let mut cpu = 0.0;
|
||||
let mut mem = 0.0;
|
||||
let mut status = "stopped".to_string();
|
||||
let mut process_detected = false;
|
||||
|
||||
// 1. Detección por proceso (método original)
|
||||
for (pid, process) in sys.processes() {
|
||||
let process_name = process.name();
|
||||
|
||||
if process_name.contains("node") {
|
||||
// Soportar node y python
|
||||
if process_name.contains("node") || process_name.contains("python") {
|
||||
if let Some(cwd) = process.cwd() {
|
||||
let cwd_str = cwd.to_string_lossy();
|
||||
if cwd_str.contains(name) {
|
||||
pid_encontrado = pid.as_u32() as i32;
|
||||
cpu = process.cpu_usage();
|
||||
mem = process.memory() as f64 / 1024.0 / 1024.0;
|
||||
status = "running".to_string();
|
||||
process_detected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Consultar systemd
|
||||
let service_name = format!("{}.service", name);
|
||||
let systemd_status = SystemCtl::status(&service_name);
|
||||
|
||||
// 3. Reconciliar estados
|
||||
let final_status = AppStatus::reconcile(process_detected, &systemd_status);
|
||||
|
||||
// 4. Detectar discrepancias
|
||||
let discrepancy = match (&final_status, process_detected, &systemd_status) {
|
||||
(AppStatus::Crashed, false, ServiceStatus::Active) => {
|
||||
Some(format!("Systemd reporta activo pero proceso no detectado"))
|
||||
}
|
||||
(AppStatus::Zombie, true, ServiceStatus::Inactive) => {
|
||||
Some(format!("Proceso detectado pero systemd reporta inactivo"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let now = chrono::Local::now();
|
||||
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
|
||||
AppStatusUpdate {
|
||||
app_name: name.to_string(),
|
||||
server: server.to_string(),
|
||||
status,
|
||||
status: final_status.as_str().to_string(),
|
||||
port,
|
||||
pid: pid_encontrado,
|
||||
pid: if pid_encontrado > 0 { pid_encontrado } else { 0 },
|
||||
memory_usage: format!("{:.2}MB", mem),
|
||||
cpu_usage: format!("{:.2}%", cpu),
|
||||
last_check: timestamp,
|
||||
systemd_status: systemd_status.as_str().to_string(),
|
||||
discrepancy,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,4 +193,4 @@ async fn send_to_cloud(
|
||||
eprintln!("⚠️ Error HTTP {}: {}", status, error_text);
|
||||
Err(format!("HTTP {}: {}", status, error_text).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
128
src/orchestrator/app_manager.rs
Normal file
128
src/orchestrator/app_manager.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use super::{Result, OrchestratorError};
|
||||
use crate::models::{ServiceConfig, ManagedApp, AppStatus};
|
||||
use crate::systemd::{ServiceGenerator, SystemCtl};
|
||||
use crate::logger::get_logger;
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct AppManager {
|
||||
apps: Arc<DashMap<String, ServiceConfig>>,
|
||||
}
|
||||
|
||||
impl AppManager {
|
||||
pub fn new() -> Self {
|
||||
AppManager {
|
||||
apps: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_app(&self, config: ServiceConfig) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
|
||||
// Validar configuración
|
||||
config.validate()
|
||||
.map_err(|e| OrchestratorError::ValidationError(e))?;
|
||||
|
||||
// Verificar si ya existe
|
||||
if self.apps.contains_key(&config.app_name) {
|
||||
logger.warning("AppManager", "Aplicación ya registrada", Some(&config.app_name));
|
||||
return Err(OrchestratorError::AppAlreadyExists(config.app_name.clone()));
|
||||
}
|
||||
|
||||
// Verificar si el servicio ya existe en systemd
|
||||
if SystemCtl::is_service_exists(&config.service_name()) {
|
||||
logger.warning("AppManager", "Servicio systemd ya existe", Some(&config.service_name()));
|
||||
return Err(OrchestratorError::AppAlreadyExists(
|
||||
format!("El servicio {} ya existe en systemd", config.service_name())
|
||||
));
|
||||
}
|
||||
|
||||
logger.info("AppManager", &format!("Registrando aplicación: {}", config.app_name));
|
||||
|
||||
// Generar archivo de servicio
|
||||
let service_content = ServiceGenerator::create_service(&config)?;
|
||||
ServiceGenerator::write_service_file(&config, &service_content)?;
|
||||
|
||||
// Recargar daemon de systemd
|
||||
SystemCtl::daemon_reload()?;
|
||||
|
||||
// Habilitar el servicio
|
||||
SystemCtl::enable(&config.service_name())?;
|
||||
|
||||
// Guardar en memoria
|
||||
self.apps.insert(config.app_name.clone(), config.clone());
|
||||
|
||||
logger.info("AppManager", &format!("Aplicación {} registrada exitosamente", config.app_name));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unregister_app(&self, app_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
|
||||
logger.info("AppManager", &format!("Desregistrando aplicación: {}", app_name));
|
||||
|
||||
// Obtener configuración
|
||||
let config = self.apps.get(app_name)
|
||||
.ok_or_else(|| OrchestratorError::AppNotFound(app_name.to_string()))?;
|
||||
|
||||
let service_name = config.service_name();
|
||||
drop(config); // Liberar el lock
|
||||
|
||||
// Detener el servicio si está corriendo
|
||||
let _ = SystemCtl::stop(&service_name);
|
||||
|
||||
// Deshabilitar el servicio
|
||||
let _ = SystemCtl::disable(&service_name);
|
||||
|
||||
// Eliminar archivo de servicio
|
||||
ServiceGenerator::delete_service_file(&service_name)?;
|
||||
|
||||
// Recargar daemon
|
||||
SystemCtl::daemon_reload()?;
|
||||
|
||||
// Eliminar de memoria
|
||||
self.apps.remove(app_name);
|
||||
|
||||
logger.info("AppManager", &format!("Aplicación {} desregistrada exitosamente", app_name));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_apps(&self) -> Vec<String> {
|
||||
self.apps.iter()
|
||||
.map(|entry| entry.key().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_app(&self, app_name: &str) -> Option<ServiceConfig> {
|
||||
self.apps.get(app_name).map(|entry| entry.clone())
|
||||
}
|
||||
|
||||
pub fn app_exists(&self, app_name: &str) -> bool {
|
||||
self.apps.contains_key(app_name)
|
||||
}
|
||||
|
||||
pub fn get_app_status(&self, app_name: &str) -> Option<ManagedApp> {
|
||||
let config = self.get_app(app_name)?;
|
||||
let systemd_status = SystemCtl::status(&config.service_name());
|
||||
|
||||
// Por ahora retornamos información básica
|
||||
// El monitor.rs se encargará de enriquecer con PID, CPU, RAM
|
||||
Some(ManagedApp {
|
||||
name: app_name.to_string(),
|
||||
status: AppStatus::reconcile(false, &systemd_status),
|
||||
pid: None,
|
||||
cpu_usage: 0.0,
|
||||
memory_usage: 0,
|
||||
systemd_status,
|
||||
last_updated: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
126
src/orchestrator/lifecycle.rs
Normal file
126
src/orchestrator/lifecycle.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use super::{Result, OrchestratorError};
|
||||
use crate::systemd::SystemCtl;
|
||||
use crate::logger::get_logger;
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct LifecycleManager {
|
||||
rate_limiter: Arc<DashMap<String, Instant>>,
|
||||
rate_limit_duration: Duration,
|
||||
}
|
||||
|
||||
impl LifecycleManager {
|
||||
pub fn new() -> Self {
|
||||
LifecycleManager {
|
||||
rate_limiter: Arc::new(DashMap::new()),
|
||||
rate_limit_duration: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_app(&self, app_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
|
||||
// Verificar rate limit
|
||||
self.check_rate_limit(app_name)?;
|
||||
|
||||
logger.info("Lifecycle", &format!("Iniciando aplicación: {}", app_name));
|
||||
|
||||
let service_name = format!("{}.service", app_name);
|
||||
SystemCtl::start(&service_name)?;
|
||||
|
||||
// Actualizar rate limiter
|
||||
self.update_rate_limiter(app_name);
|
||||
|
||||
logger.info("Lifecycle", &format!("Aplicación {} iniciada", app_name));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop_app(&self, app_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
|
||||
// Verificar rate limit
|
||||
self.check_rate_limit(app_name)?;
|
||||
|
||||
logger.info("Lifecycle", &format!("Deteniendo aplicación: {}", app_name));
|
||||
|
||||
let service_name = format!("{}.service", app_name);
|
||||
SystemCtl::stop(&service_name)?;
|
||||
|
||||
// Actualizar rate limiter
|
||||
self.update_rate_limiter(app_name);
|
||||
|
||||
logger.info("Lifecycle", &format!("Aplicación {} detenida", app_name));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restart_app(&self, app_name: &str) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
|
||||
// Verificar rate limit
|
||||
self.check_rate_limit(app_name)?;
|
||||
|
||||
logger.info("Lifecycle", &format!("Reiniciando aplicación: {}", app_name));
|
||||
|
||||
let service_name = format!("{}.service", app_name);
|
||||
SystemCtl::restart(&service_name)?;
|
||||
|
||||
// Actualizar rate limiter
|
||||
self.update_rate_limiter(app_name);
|
||||
|
||||
logger.info("Lifecycle", &format!("Aplicación {} reiniciada", app_name));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_rate_limit(&self, app_name: &str) -> Result<()> {
|
||||
if let Some(last_action) = self.rate_limiter.get(app_name) {
|
||||
let elapsed = last_action.elapsed();
|
||||
if elapsed < self.rate_limit_duration {
|
||||
let logger = get_logger();
|
||||
logger.warning(
|
||||
"Lifecycle",
|
||||
"Rate limit excedido",
|
||||
Some(&format!("App: {}, Espera: {:?}", app_name, self.rate_limit_duration - elapsed))
|
||||
);
|
||||
return Err(OrchestratorError::RateLimitExceeded(app_name.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_rate_limiter(&self, app_name: &str) {
|
||||
self.rate_limiter.insert(app_name.to_string(), Instant::now());
|
||||
}
|
||||
|
||||
pub fn recover_inconsistent_state(&self, app_name: &str, expected_running: bool) -> Result<()> {
|
||||
let logger = get_logger();
|
||||
logger.warning(
|
||||
"Lifecycle",
|
||||
"Intentando recuperar estado inconsistente",
|
||||
Some(app_name)
|
||||
);
|
||||
|
||||
let service_name = format!("{}.service", app_name);
|
||||
|
||||
if expected_running {
|
||||
// Se espera que esté corriendo pero no está
|
||||
logger.info("Lifecycle", &format!("Intentando reiniciar {}", app_name));
|
||||
SystemCtl::start(&service_name)?;
|
||||
} else {
|
||||
// Se espera que esté detenido pero está corriendo
|
||||
logger.info("Lifecycle", &format!("Intentando detener {}", app_name));
|
||||
SystemCtl::stop(&service_name)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LifecycleManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
27
src/orchestrator/mod.rs
Normal file
27
src/orchestrator/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
pub mod app_manager;
|
||||
pub mod lifecycle;
|
||||
|
||||
pub use app_manager::*;
|
||||
pub use lifecycle::*;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum OrchestratorError {
|
||||
#[error("Error de systemd: {0}")]
|
||||
SystemdError(#[from] crate::systemd::SystemdError),
|
||||
|
||||
#[error("Aplicación ya existe: {0}")]
|
||||
AppAlreadyExists(String),
|
||||
|
||||
#[error("Aplicación no encontrada: {0}")]
|
||||
AppNotFound(String),
|
||||
|
||||
#[error("Rate limit excedido para: {0}")]
|
||||
RateLimitExceeded(String),
|
||||
|
||||
#[error("Error de validación: {0}")]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, OrchestratorError>;
|
||||
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