diff --git a/src/bin/proxmox-backup-proxy.rs b/src/bin/proxmox-backup-proxy.rs index ce290171..78f366db 100644 --- a/src/bin/proxmox-backup-proxy.rs +++ b/src/bin/proxmox-backup-proxy.rs @@ -240,11 +240,19 @@ async fn schedule_tasks() -> Result<(), Error> { async fn schedule_datastore_garbage_collection() { - use proxmox_backup::config::datastore::{ - self, - DataStoreConfig, + use proxmox_backup::config::{ + datastore::{ + self, + DataStoreConfig, + }, + user::{ + self, + User, + }, }; + let email = server::lookup_user_email(Userid::root_userid()); + let config = match datastore::config() { Err(err) => { eprintln!("unable to read datastore config - {}", err); @@ -325,6 +333,7 @@ async fn schedule_datastore_garbage_collection() { }; let store2 = store.clone(); + let email2 = email.clone(); if let Err(err) = WorkerTask::new_thread( worker_type, @@ -345,6 +354,13 @@ async fn schedule_datastore_garbage_collection() { eprintln!("could not finish job state for {}: {}", worker_type, err); } + if let Some(email2) = email2 { + let gc_status = datastore.last_gc_status(); + if let Err(err) = crate::server::send_gc_status(&email2, datastore.name(), &gc_status, &result) { + eprintln!("send gc notification failed: {}", err); + } + } + result } ) { diff --git a/src/server.rs b/src/server.rs index dbaec645..9a18c56b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -34,3 +34,6 @@ pub mod jobstate; mod verify_job; pub use verify_job::*; + +mod email_notifications; +pub use email_notifications::*; diff --git a/src/server/email_notifications.rs b/src/server/email_notifications.rs new file mode 100644 index 00000000..bad9f09f --- /dev/null +++ b/src/server/email_notifications.rs @@ -0,0 +1,242 @@ +use anyhow::Error; +use serde_json::json; + +use handlebars::{Handlebars, Helper, Context, RenderError, RenderContext, Output, HelperResult}; + +use proxmox::tools::email::sendmail; + +use crate::{ + config::verify::VerificationJobConfig, + api2::types::{ + Userid, + GarbageCollectionStatus, + }, + tools::format::HumanByte, +}; + +const GC_OK_TEMPLATE: &str = r###" + +Datastore: {{datastore}} +Task ID: {{status.upid}} +Index file count: {{status.index-file-count}} + +Removed garbage: {{human-bytes status.removed-bytes}} +Removed chunks: {{status.removed-chunks}} +Remove bad files: {{status.removed-bad}} + +Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks) + +Original Data usage: {{human-bytes status.index-data-bytes}} +On Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}}) +On Disk chunks: {{status.disk-chunks}} + +Garbage collection successful. + +"###; + + +const GC_ERR_TEMPLATE: &str = r###" + +Datastore: {{datastore}} + +Garbage collection failed: {{error}} + +"###; + +const VERIFY_OK_TEMPLATE: &str = r###" + +Job ID: {{job.id}} +Datastore: {{job.store}} + +Verification successful. + +"###; + +const VERIFY_ERR_TEMPLATE: &str = r###" + +Job ID: {{job.id}} +Datastore: {{job.store}} + +Verification failed: {{error}} + +"###; + +lazy_static::lazy_static!{ + + static ref HANDLEBARS: Handlebars<'static> = { + let mut hb = Handlebars::new(); + + hb.set_strict_mode(true); + + hb.register_helper("human-bytes", Box::new(handlebars_humam_bytes_helper)); + hb.register_helper("relative-percentage", Box::new(handlebars_relative_percentage_helper)); + + hb.register_template_string("gc_ok_template", GC_OK_TEMPLATE).unwrap(); + hb.register_template_string("gc_err_template", GC_ERR_TEMPLATE).unwrap(); + + hb.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE).unwrap(); + hb.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE).unwrap(); + + hb + }; +} + +fn send_job_status_mail( + email: &str, + subject: &str, + text: &str, +) -> Result<(), Error> { + + // Note: OX has serious problems displaying text mails, + // so we include html as well + let html = format!("
\n{}\n", text); + + let nodename = proxmox::tools::nodename(); + + let author = format!("Proxmox Backup Server - {}", nodename); + + sendmail( + &[email], + &subject, + Some(&text), + Some(&html), + None, + Some(&author), + )?; + + Ok(()) +} + +pub fn send_gc_status( + email: &str, + datastore: &str, + status: &GarbageCollectionStatus, + result: &Result<(), Error>, +) -> Result<(), Error> { + + let text = match result { + Ok(()) => { + let data = json!({ + "status": status, + "datastore": datastore, + }); + HANDLEBARS.render("gc_ok_template", &data)? + } + Err(err) => { + let data = json!({ + "error": err.to_string(), + "datastore": datastore, + }); + HANDLEBARS.render("gc_err_template", &data)? + } + }; + + let subject = match result { + Ok(()) => format!( + "Garbage Collect Datastore '{}' successful", + datastore, + ), + Err(_) => format!( + "Garbage Collect Datastore '{}' failed", + datastore, + ), + }; + + send_job_status_mail(email, &subject, &text)?; + + Ok(()) +} + +pub fn send_verify_status( + email: &str, + job: VerificationJobConfig, + result: &Result<(), Error>, +) -> Result<(), Error> { + + + let text = match result { + Ok(()) => { + let data = json!({ "job": job }); + HANDLEBARS.render("verify_ok_template", &data)? + } + Err(err) => { + let data = json!({ "job": job, "error": err.to_string() }); + HANDLEBARS.render("verify_err_template", &data)? + } + }; + + let subject = match result { + Ok(()) => format!( + "Verify Datastore '{}' successful", + job.store, + ), + Err(_) => format!( + "Verify Datastore '{}' failed", + job.store, + ), + }; + + send_job_status_mail(email, &subject, &text)?; + + Ok(()) +} + +/// Lookup users email address +/// +/// For "backup@pam", this returns the address from "root@pam". +pub fn lookup_user_email(userid: &Userid) -> Option{ + + use crate::config::user::{self, User}; + + if userid == Userid::backup_userid() { + return lookup_user_email(Userid::root_userid()); + } + + if let Ok(user_config) = user::cached_config() { + if let Ok(user) = user_config.lookup:: ("user", userid.as_str()) { + return user.email.clone(); + } + } + + None +} + +// Handlerbar helper functions + +fn handlebars_humam_bytes_helper( + h: &Helper, + _: &Handlebars, + _: &Context, + _rc: &mut RenderContext, + out: &mut dyn Output +) -> HelperResult { + let param = h.param(0).map(|v| v.value().as_u64()) + .flatten() + .ok_or(RenderError::new("human-bytes: param not found"))?; + + out.write(&HumanByte::from(param).to_string())?; + + Ok(()) +} + +fn handlebars_relative_percentage_helper( + h: &Helper, + _: &Handlebars, + _: &Context, + _rc: &mut RenderContext, + out: &mut dyn Output +) -> HelperResult { + let param0 = h.param(0).map(|v| v.value().as_f64()) + .flatten() + .ok_or(RenderError::new("relative-percentage: param0 not found"))?; + let param1 = h.param(1).map(|v| v.value().as_f64()) + .flatten() + .ok_or(RenderError::new("relative-percentage: param1 not found"))?; + + if param1 == 0.0 { + out.write("-")?; + } else { + out.write(&format!("{:.2}%", (param0*100.0)/param1))?; + } + Ok(()) +} diff --git a/src/server/verify_job.rs b/src/server/verify_job.rs index 42a9efff..064fb2b7 100644 --- a/src/server/verify_job.rs +++ b/src/server/verify_job.rs @@ -46,6 +46,8 @@ pub fn do_verification_job( }) } + let email = crate::server::lookup_user_email(userid); + let job_id = job.jobname().to_string(); let worker_type = job.jobtype().to_string(); let upid_str = WorkerTask::new_thread( @@ -103,6 +105,12 @@ pub fn do_verification_job( Ok(_) => (), } + if let Some(email) = email { + if let Err(err) = crate::server::send_verify_status(&email, verification_job, &result) { + eprintln!("send verify notification failed: {}", err); + } + } + result }, )?;