proxmox-backup/src/server/email_notifications.rs

487 lines
12 KiB
Rust
Raw Normal View History

use anyhow::Error;
use serde_json::json;
use handlebars::{Handlebars, Helper, Context, RenderError, RenderContext, Output, HelperResult};
use proxmox::tools::email::sendmail;
use proxmox::api::schema::parse_property_string;
use crate::{
config::datastore::DataStoreConfig,
config::verify::VerificationJobConfig,
2020-10-29 11:07:46 +00:00
config::sync::SyncJobConfig,
api2::types::{
APTUpdateInfo,
GarbageCollectionStatus,
Userid,
Notify,
DatastoreNotify,
},
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}}
Removed bad chunks: {{status.removed-bad}}
Leftover bad chunks: {{status.still-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}}
Deduplication Factor: {{deduplication-factor}}
Garbage collection successful.
2020-11-02 08:11:08 +00:00
Please visit the web interface for futher details:
<https://{{fqdn}}:{{port}}/#DataStore-{{datastore}}>
"###;
const GC_ERR_TEMPLATE: &str = r###"
Datastore: {{datastore}}
Garbage collection failed: {{error}}
2020-11-02 08:11:08 +00:00
Please visit the web interface for futher details:
<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
"###;
const VERIFY_OK_TEMPLATE: &str = r###"
Job ID: {{job.id}}
Datastore: {{job.store}}
Verification successful.
2020-11-02 08:11:08 +00:00
Please visit the web interface for futher details:
<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
"###;
const VERIFY_ERR_TEMPLATE: &str = r###"
Job ID: {{job.id}}
Datastore: {{job.store}}
Verification failed on these snapshots/groups:
{{#each errors}}
{{this~}}
{{/each}}
2020-11-02 08:11:08 +00:00
Please visit the web interface for futher details:
<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
"###;
2020-10-29 11:07:46 +00:00
const SYNC_OK_TEMPLATE: &str = r###"
Job ID: {{job.id}}
Datastore: {{job.store}}
Remote: {{job.remote}}
Remote Store: {{job.remote-store}}
Synchronization successful.
2020-11-02 08:11:08 +00:00
Please visit the web interface for futher details:
<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
2020-10-29 11:07:46 +00:00
"###;
const SYNC_ERR_TEMPLATE: &str = r###"
Job ID: {{job.id}}
Datastore: {{job.store}}
Remote: {{job.remote}}
Remote Store: {{job.remote-store}}
Synchronization failed: {{error}}
2020-11-02 08:11:08 +00:00
Please visit the web interface for futher details:
<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
2020-10-29 11:07:46 +00:00
"###;
const PACKAGE_UPDATES_TEMPLATE: &str = r###"
Proxmox Backup Server has the following updates available:
{{#each updates }}
{{Package}}: {{OldVersion}} -> {{Version~}}
{{/each }}
2020-11-02 08:11:08 +00:00
To upgrade visit the web interface:
<https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates>
"###;
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();
2020-10-29 11:07:46 +00:00
hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE).unwrap();
hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE).unwrap();
hb.register_template_string("package_update_template", PACKAGE_UPDATES_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!("<html><body><pre>\n{}\n<pre>", handlebars::html_escape(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,
notify: DatastoreNotify,
datastore: &str,
status: &GarbageCollectionStatus,
result: &Result<(), Error>,
) -> Result<(), Error> {
match notify.gc {
None => { /* send notifications by default */ },
Some(notify) => {
if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
return Ok(());
}
}
}
2020-11-02 08:11:08 +00:00
let (fqdn, port) = get_server_url();
let mut data = json!({
"datastore": datastore,
"fqdn": fqdn,
"port": port,
});
let text = match result {
Ok(()) => {
let deduplication_factor = if status.disk_bytes > 0 {
(status.index_data_bytes as f64)/(status.disk_bytes as f64)
} else {
1.0
};
2020-11-02 08:11:08 +00:00
data["status"] = json!(status);
data["deduplication-factor"] = format!("{:.2}", deduplication_factor).into();
HANDLEBARS.render("gc_ok_template", &data)?
}
Err(err) => {
2020-11-02 08:11:08 +00:00
data["error"] = err.to_string().into();
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,
notify: DatastoreNotify,
job: VerificationJobConfig,
result: &Result<Vec<String>, Error>,
) -> Result<(), Error> {
2020-11-02 08:11:08 +00:00
let (fqdn, port) = get_server_url();
let mut data = json!({
"job": job,
"fqdn": fqdn,
"port": port,
});
let mut result_is_ok = false;
let text = match result {
Ok(errors) if errors.is_empty() => {
result_is_ok = true;
HANDLEBARS.render("verify_ok_template", &data)?
}
Ok(errors) => {
2020-11-02 08:11:08 +00:00
data["errors"] = json!(errors);
HANDLEBARS.render("verify_err_template", &data)?
}
Err(_) => {
// aborted job - do not send any email
return Ok(());
}
};
match notify.verify {
None => { /* send notifications by default */ },
Some(notify) => {
if notify == Notify::Never || (result_is_ok && notify == Notify::Error) {
return Ok(());
}
}
}
let subject = match result {
Ok(errors) if errors.is_empty() => format!(
"Verify Datastore '{}' successful",
job.store,
),
_ => format!(
"Verify Datastore '{}' failed",
job.store,
),
};
send_job_status_mail(email, &subject, &text)?;
Ok(())
}
2020-10-29 11:07:46 +00:00
pub fn send_sync_status(
email: &str,
notify: DatastoreNotify,
2020-10-29 11:07:46 +00:00
job: &SyncJobConfig,
result: &Result<(), Error>,
) -> Result<(), Error> {
match notify.sync {
None => { /* send notifications by default */ },
Some(notify) => {
if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
return Ok(());
}
}
}
2020-11-02 08:11:08 +00:00
let (fqdn, port) = get_server_url();
let mut data = json!({
"job": job,
"fqdn": fqdn,
"port": port,
});
2020-10-29 11:07:46 +00:00
let text = match result {
Ok(()) => {
HANDLEBARS.render("sync_ok_template", &data)?
}
Err(err) => {
2020-11-02 08:11:08 +00:00
data["error"] = err.to_string().into();
2020-10-29 11:07:46 +00:00
HANDLEBARS.render("sync_err_template", &data)?
}
};
let subject = match result {
Ok(()) => format!(
"Sync remote '{}' datastore '{}' successful",
job.remote,
job.remote_store,
),
Err(_) => format!(
"Sync remote '{}' datastore '{}' failed",
job.remote,
job.remote_store,
),
};
send_job_status_mail(email, &subject, &text)?;
Ok(())
}
2020-11-02 08:11:08 +00:00
fn get_server_url() -> (String, usize) {
// user will surely request that they can change this
let nodename = proxmox::tools::nodename();
let mut fqdn = nodename.to_owned();
if let Ok(resolv_conf) = crate::api2::node::dns::read_etc_resolv_conf() {
if let Some(search) = resolv_conf["search"].as_str() {
fqdn.push('.');
fqdn.push_str(search);
}
}
let port = 8007;
(fqdn, port)
}
pub fn send_updates_available(
updates: &[&APTUpdateInfo],
) -> Result<(), Error> {
// update mails always go to the root@pam configured email..
if let Some(email) = lookup_user_email(Userid::root_userid()) {
let nodename = proxmox::tools::nodename();
let subject = format!("New software packages available ({})", nodename);
2020-11-02 08:11:08 +00:00
let (fqdn, port) = get_server_url();
let text = HANDLEBARS.render("package_update_template", &json!({
2020-11-02 08:11:08 +00:00
"fqdn": fqdn,
"port": port,
"updates": updates,
}))?;
send_job_status_mail(&email, &subject, &text)?;
}
Ok(())
}
/// Lookup users email address
fn lookup_user_email(userid: &Userid) -> Option<String> {
use crate::config::user::{self, User};
if let Ok(user_config) = user::cached_config() {
if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
return user.email;
}
}
None
}
/// Lookup Datastore notify settings
pub fn lookup_datastore_notify_settings(
store: &str,
) -> (Option<String>, DatastoreNotify) {
let mut email = None;
let notify = DatastoreNotify { gc: None, verify: None, sync: None };
let (config, _digest) = match crate::config::datastore::config() {
Ok(result) => result,
Err(_) => return (email, notify),
};
let config: DataStoreConfig = match config.lookup("datastore", store) {
Ok(result) => result,
Err(_) => return (email, notify),
};
email = match config.notify_user {
Some(ref userid) => lookup_user_email(userid),
2020-11-05 13:03:15 +00:00
None => lookup_user_email(Userid::root_userid()),
};
let notify_str = config.notify.unwrap_or_default();
if let Ok(value) = parse_property_string(&notify_str, &DatastoreNotify::API_SCHEMA) {
if let Ok(notify) = serde_json::from_value(value) {
return (email, notify);
}
}
(email, notify)
}
// 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_else(|| 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_else(|| RenderError::new("relative-percentage: param0 not found"))?;
let param1 = h.param(1).map(|v| v.value().as_f64())
.flatten()
.ok_or_else(|| RenderError::new("relative-percentage: param1 not found"))?;
if param1 == 0.0 {
out.write("-")?;
} else {
out.write(&format!("{:.2}%", (param0*100.0)/param1))?;
}
Ok(())
}