Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 11s
CI/CD Pipeline / Clippy Lints (push) Failing after 5m33s
CI/CD Pipeline / Unit Tests (push) Failing after 5m52s
CI/CD Pipeline / Security Audit (push) Successful in 1m49s
CI/CD Pipeline / Build Debian Package (push) Failing after 1s
CI/CD Pipeline / Build RPM Package (push) Failing after 1s
CI/CD Pipeline / Build Alpine Package (push) Failing after 2s
CI/CD Pipeline / Build Arch Package (push) Failing after 1s
CI/CD Pipeline / Create Release (push) Has been skipped
273 lines
8.7 KiB
Rust
273 lines
8.7 KiB
Rust
//! System Management API Handlers
|
|
//!
|
|
//! Implements REST endpoints for system management operations:
|
|
//! - GET /api/v1/system/info - OS version, kernel, last update time
|
|
//! - GET /api/v1/health - Health check endpoint
|
|
//! - POST /api/v1/system/reboot - System reboot - async
|
|
|
|
use actix_web::{web, HttpRequest, HttpResponse, Responder};
|
|
use chrono::Utc;
|
|
use serde::{Deserialize, Serialize};
|
|
use tracing::{error, info, warn};
|
|
use uuid::Uuid;
|
|
|
|
use super::packages::ApiResponse;
|
|
use crate::jobs::manager::{JobManager, JobOperation, JobStatus};
|
|
use crate::packages::PackageManagerBackend;
|
|
|
|
/// Normalize and validate file paths to prevent path traversal attacks (VULN-002)
|
|
/// Returns None if path contains traversal patterns
|
|
#[allow(dead_code)]
|
|
fn validate_path_no_traversal(path: &str) -> bool {
|
|
// Validate path - check for traversal patterns
|
|
if path.contains("..") || path.contains("//") {
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
|
|
/// System info response data
|
|
#[derive(Debug, Serialize)]
|
|
pub struct SystemInfoData {
|
|
pub hostname: String,
|
|
pub os: String,
|
|
pub os_version: String,
|
|
pub kernel: String,
|
|
pub architecture: String,
|
|
pub last_update_check: Option<String>,
|
|
pub last_update_apply: Option<String>,
|
|
pub pending_reboot: bool,
|
|
}
|
|
|
|
/// Health check response data
|
|
#[derive(Debug, Serialize)]
|
|
pub struct HealthData {
|
|
pub status: String,
|
|
pub uptime_seconds: u64,
|
|
pub version: String,
|
|
}
|
|
|
|
/// Reboot request
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
pub struct RebootRequest {
|
|
#[serde(default)]
|
|
pub delay_seconds: u64,
|
|
#[serde(default)]
|
|
pub force: bool,
|
|
}
|
|
|
|
/// Get system information
|
|
pub async fn get_system_info(
|
|
backend: web::Data<Box<dyn PackageManagerBackend>>,
|
|
_req: HttpRequest,
|
|
) -> impl Responder {
|
|
let request_id = Uuid::new_v4().to_string();
|
|
let _timestamp = Utc::now().to_rfc3339();
|
|
|
|
info!(request_id = %request_id, "Getting system information");
|
|
|
|
match backend.get_system_info() {
|
|
Ok(sys_info) => {
|
|
let response = ApiResponse::success(SystemInfoData {
|
|
hostname: sys_info.hostname,
|
|
os: sys_info.os,
|
|
os_version: sys_info.os_version,
|
|
kernel: sys_info.kernel,
|
|
architecture: sys_info.architecture,
|
|
last_update_check: sys_info.last_update_check,
|
|
last_update_apply: sys_info.last_update_apply,
|
|
pending_reboot: sys_info.pending_reboot,
|
|
});
|
|
|
|
HttpResponse::Ok().json(response)
|
|
}
|
|
Err(e) => {
|
|
error!(request_id = %request_id, error = %e, "Failed to get system info");
|
|
let response = ApiResponse::<()>::error(
|
|
"SYSTEM_INFO_ERROR",
|
|
&format!("Failed to get system info: {}", e),
|
|
None,
|
|
true,
|
|
);
|
|
HttpResponse::InternalServerError().json(response)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Health check endpoint
|
|
pub async fn health_check(_req: HttpRequest) -> impl Responder {
|
|
let _request_id = Uuid::new_v4().to_string();
|
|
let _timestamp = Utc::now().to_rfc3339();
|
|
|
|
// Calculate uptime from /proc/uptime
|
|
let uptime_seconds = std::fs::read_to_string("/proc/uptime")
|
|
.ok()
|
|
.and_then(|content| {
|
|
content
|
|
.split_whitespace()
|
|
.next()
|
|
.and_then(|s| s.parse::<f64>().ok())
|
|
.map(|f| f as u64)
|
|
})
|
|
.unwrap_or(0);
|
|
|
|
let version = env!("CARGO_PKG_VERSION").to_string();
|
|
|
|
let response = ApiResponse::success(HealthData {
|
|
status: "healthy".to_string(),
|
|
uptime_seconds,
|
|
version,
|
|
});
|
|
|
|
HttpResponse::Ok().json(response)
|
|
}
|
|
|
|
/// Reboot the system (async operation)
|
|
pub async fn reboot_system(
|
|
body: web::Json<RebootRequest>,
|
|
backend: web::Data<Box<dyn PackageManagerBackend>>,
|
|
job_manager: web::Data<JobManager>,
|
|
_req: HttpRequest,
|
|
) -> impl Responder {
|
|
let request_id = Uuid::new_v4().to_string();
|
|
let _timestamp = Utc::now().to_rfc3339();
|
|
let delay = body.delay_seconds;
|
|
let force = body.force;
|
|
|
|
info!(
|
|
request_id = %request_id,
|
|
delay_seconds = delay,
|
|
force = force,
|
|
"Initiating system reboot"
|
|
);
|
|
|
|
// Check for running jobs unless force is true
|
|
if !force {
|
|
let running_count = job_manager.running_count().await;
|
|
if running_count > 0 {
|
|
warn!(request_id = %request_id, running_jobs = running_count, "Reboot blocked by running jobs");
|
|
let response = ApiResponse::<()>::error(
|
|
"REBOOT_BLOCKED",
|
|
"Cannot reboot while jobs are running. Use force=true to override.",
|
|
Some(serde_json::json!({"running_jobs": running_count})),
|
|
false,
|
|
);
|
|
return HttpResponse::Conflict().json(response);
|
|
}
|
|
}
|
|
|
|
// Create async job for reboot
|
|
match job_manager.create_job(JobOperation::Reboot, vec![]).await {
|
|
Ok(job_id) => {
|
|
// Spawn background task to execute the reboot
|
|
let backend_clone = backend.clone();
|
|
let job_manager_clone = job_manager.clone();
|
|
let delay_clone = delay;
|
|
|
|
tokio::spawn(async move {
|
|
let job_id_clone = job_id;
|
|
|
|
// Update job to running
|
|
let _ = job_manager_clone
|
|
.update_job(
|
|
&job_id_clone,
|
|
JobStatus::Running,
|
|
Some(0),
|
|
Some("Preparing system reboot...".to_string()),
|
|
)
|
|
.await;
|
|
let _ = job_manager_clone
|
|
.add_job_log(&job_id_clone, "Job started".to_string())
|
|
.await;
|
|
|
|
// Execute reboot
|
|
match backend_clone.reboot_system(delay_clone) {
|
|
Ok(_) => {
|
|
let _ = job_manager_clone
|
|
.add_job_log(&job_id_clone, "Reboot command executed".to_string())
|
|
.await;
|
|
// Note: Job won't complete normally since system reboots
|
|
info!(job_id = %job_id_clone, "System reboot initiated");
|
|
}
|
|
Err(e) => {
|
|
let _ = job_manager_clone
|
|
.fail_job(&job_id_clone, e.to_string())
|
|
.await;
|
|
error!(job_id = %job_id_clone, error = %e, "System reboot failed");
|
|
}
|
|
}
|
|
});
|
|
|
|
let scheduled_at = if delay > 0 {
|
|
Utc::now() + chrono::Duration::seconds(delay as i64)
|
|
} else {
|
|
Utc::now()
|
|
};
|
|
|
|
let response = ApiResponse::success(serde_json::json!({
|
|
"job_id": job_id.to_string(),
|
|
"status": "pending",
|
|
"operation": "reboot",
|
|
"scheduled_at": scheduled_at.to_rfc3339(),
|
|
"delay_seconds": delay,
|
|
"force": force,
|
|
}));
|
|
|
|
HttpResponse::Accepted().json(response)
|
|
}
|
|
Err(e) => {
|
|
error!(request_id = %request_id, error = %e, "Failed to create reboot job");
|
|
let response = ApiResponse::<()>::error(
|
|
"JOB_CREATE_ERROR",
|
|
&format!("Failed to create job: {}", e),
|
|
None,
|
|
true,
|
|
);
|
|
HttpResponse::InternalServerError().json(response)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configure routes for system endpoints
|
|
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
|
cfg.service(
|
|
web::scope("/system")
|
|
.route("/info", web::get().to(get_system_info))
|
|
.route("/reboot", web::post().to(reboot_system)),
|
|
)
|
|
.route("/health", web::get().to(health_check));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_reboot_request_default() {
|
|
let json = r#"{}"#;
|
|
let request: RebootRequest = serde_json::from_str(json).unwrap();
|
|
assert_eq!(request.delay_seconds, 0);
|
|
assert!(!request.force);
|
|
}
|
|
|
|
#[test]
|
|
fn test_reboot_request_full() {
|
|
let json = r#"{"delay_seconds": 60, "force": true}"#;
|
|
let request: RebootRequest = serde_json::from_str(json).unwrap();
|
|
assert_eq!(request.delay_seconds, 60);
|
|
assert!(request.force);
|
|
}
|
|
|
|
#[test]
|
|
fn test_health_data_serialization() {
|
|
let health = HealthData {
|
|
status: "healthy".to_string(),
|
|
uptime_seconds: 12345,
|
|
version: "0.1.0".to_string(),
|
|
};
|
|
let json = serde_json::to_string(&health).unwrap();
|
|
assert!(json.contains("healthy"));
|
|
assert!(json.contains("12345"));
|
|
}
|
|
}
|