Private
Public Access
1
0
Files
linux_patch_api/src/api/handlers/system.rs
Echo 8ffe2068d7
Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 11s
CI/CD Pipeline / Clippy Lints (push) Failing after 5m34s
CI/CD Pipeline / Unit Tests (push) Successful in 11m4s
CI/CD Pipeline / Security Audit (push) Successful in 1m46s
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 2s
CI/CD Pipeline / Create Release (push) Has been skipped
Fix clippy compilation errors: restore required imports, prefix unused variables
2026-04-12 15:52:08 +00:00

272 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
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"));
}
}