//! Patch Management API Handlers //! //! Implements REST endpoints for patch management operations: //! - GET /api/v1/patches - List available patches //! - POST /api/v1/patches/apply - Apply patches - async use actix_web::{web, HttpRequest, HttpResponse, Responder}; use chrono::Utc; use serde::{Deserialize, Serialize}; use tracing::{error, info}; use uuid::Uuid; use crate::jobs::manager::{JobManager, JobOperation, JobStatus}; use crate::packages::PackageManagerBackend; use super::packages::{ApiResponse, JobResponseData}; /// Patch list response data #[derive(Debug, Serialize)] pub struct PatchListData { pub patches: Vec, pub total: usize, pub security_updates: usize, pub requires_reboot: bool, } /// Patch apply request #[derive(Debug, Deserialize, Clone)] pub struct PatchApplyRequest { #[serde(default)] pub packages: Option>, #[serde(default)] pub reboot: bool, #[serde(default)] pub reboot_delay_seconds: u64, } /// List available patches pub async fn list_patches( backend: web::Data>, _req: HttpRequest, ) -> impl Responder { let request_id = Uuid::new_v4().to_string(); let _timestamp = Utc::now().to_rfc3339(); info!(request_id = %request_id, "Listing available patches"); match backend.list_patches() { Ok(patches) => { let total = patches.len(); let security_updates = patches .iter() .filter(|p| p.severity == "critical" || p.severity == "high") .count(); let requires_reboot = patches.iter().any(|p| p.name.contains("kernel")); let response = ApiResponse::success(PatchListData { patches, total, security_updates, requires_reboot, }); HttpResponse::Ok().json(response) } Err(e) => { error!(request_id = %request_id, error = %e, "Failed to list patches"); let response = ApiResponse::<()>::error( "PKG_MANAGER_ERROR", &format!("Failed to list patches: {}", e), None, true, ); HttpResponse::InternalServerError().json(response) } } } /// Apply patches (async operation) pub async fn apply_patches( body: web::Json, backend: web::Data>, job_manager: web::Data, _req: HttpRequest, ) -> impl Responder { let request_id = Uuid::new_v4().to_string(); let _timestamp = Utc::now().to_rfc3339(); let packages_count = body.packages.as_ref().map(|p| p.len()).unwrap_or(0); info!( request_id = %request_id, packages = ?body.packages, reboot = body.reboot, "Applying patches" ); // Create async job let package_list = body.packages.clone().unwrap_or_default(); match job_manager .create_job(JobOperation::PatchApply, package_list) .await { Ok(job_id) => { // Spawn background task to execute the patching let backend_clone = backend.clone(); let job_manager_clone = job_manager.clone(); let request = body.clone(); 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("Starting patch application...".to_string()), ) .await; let _ = job_manager_clone .add_job_log(&job_id_clone, "Job started".to_string()) .await; // Execute patching match backend_clone.apply_patches(request.packages.as_deref()) { Ok(_) => { let _ = job_manager_clone.complete_job(&job_id_clone).await; info!(job_id = %job_id_clone, "Patch application completed"); // Handle reboot if requested if request.reboot { let _ = job_manager_clone .add_job_log( &job_id_clone, format!( "Reboot scheduled in {} seconds", request.reboot_delay_seconds ), ) .await; // Trigger actual reboot via system handler match backend_clone.reboot_system(request.reboot_delay_seconds) { Ok(_) => { let _ = job_manager_clone .add_job_log( &job_id_clone, "Reboot command executed".to_string(), ) .await; } Err(e) => { let _ = job_manager_clone .add_job_log(&job_id_clone, format!("Reboot failed: {}", e)) .await; } } } } Err(e) => { let _ = job_manager_clone .fail_job(&job_id_clone, e.to_string()) .await; error!(job_id = %job_id_clone, error = %e, "Patch application failed"); } } }); let response = ApiResponse::success(JobResponseData { job_id: job_id.to_string(), status: "pending".to_string(), operation: "patch_apply".to_string(), packages: Some(vec![format!("{} packages", packages_count)]), package: None, }); HttpResponse::Accepted().json(response) } Err(e) => { error!(request_id = %request_id, error = %e, "Failed to create job"); let response = ApiResponse::<()>::error( "JOB_CREATE_ERROR", &format!("Failed to create job: {}", e), None, true, ); HttpResponse::InternalServerError().json(response) } } } /// Configure routes for patch endpoints pub fn configure_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/patches") .route("", web::get().to(list_patches)) .route("/apply", web::post().to(apply_patches)), ); } #[cfg(test)] mod tests { use super::*; #[test] fn test_patch_apply_request_default() { let json = r#"{}"#; let request: PatchApplyRequest = serde_json::from_str(json).unwrap(); assert!(request.packages.is_none()); assert!(!request.reboot); assert_eq!(request.reboot_delay_seconds, 0); } #[test] fn test_patch_apply_request_full() { let json = r#"{"packages": ["pkg1", "pkg2"], "reboot": true, "reboot_delay_seconds": 60}"#; let request: PatchApplyRequest = serde_json::from_str(json).unwrap(); assert_eq!(request.packages.unwrap().len(), 2); assert!(request.reboot); assert_eq!(request.reboot_delay_seconds, 60); } }