Some checks failed
CI/CD Pipeline / Code Format (push) Failing after 12s
CI/CD Pipeline / Clippy Lints (push) Failing after 5m34s
CI/CD Pipeline / Unit Tests (push) Failing after 10m51s
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
CI/CD Pipeline / Security Audit (push) Failing after 15m40s
208 lines
6.9 KiB
Rust
208 lines
6.9 KiB
Rust
//! 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<crate::packages::Patch>,
|
|
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<Vec<String>>,
|
|
#[serde(default)]
|
|
pub reboot: bool,
|
|
#[serde(default)]
|
|
pub reboot_delay_seconds: u64,
|
|
}
|
|
|
|
/// List available patches
|
|
pub async fn list_patches(
|
|
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, "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<PatchApplyRequest>,
|
|
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 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;
|
|
// In production, would trigger actual reboot via system handler
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|