From b9e7bcc2726a79e12707b5acaccf2c0f00390dbb Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Tue, 27 Oct 2020 13:36:56 +0100 Subject: [PATCH] send notification mails for GC and verify jobs --- src/bin/proxmox-backup-proxy.rs | 22 ++- src/server.rs | 3 + src/server/email_notifications.rs | 242 ++++++++++++++++++++++++++++++ src/server/verify_job.rs | 8 + 4 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 src/server/email_notifications.rs 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
         },
     )?;