4 use handlebars
::{Handlebars, Helper, Context, RenderError, RenderContext, Output, HelperResult, TemplateError}
;
6 use proxmox
::tools
::email
::sendmail
;
7 use proxmox_lang
::try_block
;
8 use proxmox_schema
::{parse_property_string, ApiType}
;
11 User
, TapeBackupJobSetup
, SyncJobConfig
, VerificationJobConfig
,
12 APTUpdateInfo
, GarbageCollectionStatus
, HumanByte
,
13 Userid
, Notify
, DatastoreNotify
, DataStoreConfig
,
16 const GC_OK_TEMPLATE
: &str = r
###"
18 Datastore: {{datastore}}
19 Task ID: {{status.upid}}
20 Index file count: {{status.index-file-count}}
22 Removed garbage: {{human-bytes status.removed-bytes}}
23 Removed chunks: {{status.removed-chunks}}
24 Removed bad chunks: {{status.removed-bad}}
26 Leftover bad chunks: {{status.still-bad}}
27 Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks)
29 Original Data usage: {{human-bytes status.index-data-bytes}}
30 On-Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}})
31 On-Disk chunks: {{status.disk-chunks}}
33 Deduplication Factor: {{deduplication-factor}}
35 Garbage collection successful.
38 Please visit the web interface for further details:
40 <https://{{fqdn}}:{{port}}/#DataStore-{{datastore}}>
45 const GC_ERR_TEMPLATE
: &str = r
###"
47 Datastore: {{datastore}}
49 Garbage collection failed: {{error}}
52 Please visit the web interface for further details:
54 <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
58 const VERIFY_OK_TEMPLATE
: &str = r
###"
61 Datastore: {{job.store}}
63 Verification successful.
66 Please visit the web interface for further details:
68 <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
72 const VERIFY_ERR_TEMPLATE
: &str = r
###"
75 Datastore: {{job.store}}
77 Verification failed on these snapshots/groups:
84 Please visit the web interface for further details:
86 <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
90 const SYNC_OK_TEMPLATE
: &str = r
###"
93 Datastore: {{job.store}}
94 Remote: {{job.remote}}
95 Remote Store: {{job.remote-store}}
97 Synchronization successful.
100 Please visit the web interface for further details:
102 <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
106 const SYNC_ERR_TEMPLATE
: &str = r
###"
109 Datastore: {{job.store}}
110 Remote: {{job.remote}}
111 Remote Store: {{job.remote-store}}
113 Synchronization failed: {{error}}
116 Please visit the web interface for further details:
118 <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
122 const PACKAGE_UPDATES_TEMPLATE
: &str = r
###"
123 Proxmox Backup Server has the following updates available:
125 {{Package}}: {{OldVersion}} -> {{Version~}}
128 To upgrade visit the web interface:
130 <https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates>
134 const TAPE_BACKUP_OK_TEMPLATE
: &str = r
###"
139 Datastore: {{job.store}}
140 Tape Pool: {{job.pool}}
141 Tape Drive: {{job.drive}}
143 {{#if snapshot-list ~}}
146 {{#each snapshot-list~}}
150 Duration: {{duration}}
152 Tape Backup successful.
155 Please visit the web interface for further details:
157 <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
161 const TAPE_BACKUP_ERR_TEMPLATE
: &str = r
###"
166 Datastore: {{job.store}}
167 Tape Pool: {{job.pool}}
168 Tape Drive: {{job.drive}}
170 {{#if snapshot-list ~}}
173 {{#each snapshot-list~}}
177 Tape Backup failed: {{error}}
180 Please visit the web interface for further details:
182 <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
186 lazy_static
::lazy_static
!{
188 static ref HANDLEBARS
: Handlebars
<'
static> = {
189 let mut hb
= Handlebars
::new();
190 let result
: Result
<(), TemplateError
> = try_block
!({
192 hb
.set_strict_mode(true);
193 hb
.register_escape_fn(handlebars
::no_escape
);
195 hb
.register_helper("human-bytes", Box
::new(handlebars_humam_bytes_helper
));
196 hb
.register_helper("relative-percentage", Box
::new(handlebars_relative_percentage_helper
));
198 hb
.register_template_string("gc_ok_template", GC_OK_TEMPLATE
)?
;
199 hb
.register_template_string("gc_err_template", GC_ERR_TEMPLATE
)?
;
201 hb
.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE
)?
;
202 hb
.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE
)?
;
204 hb
.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE
)?
;
205 hb
.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE
)?
;
207 hb
.register_template_string("tape_backup_ok_template", TAPE_BACKUP_OK_TEMPLATE
)?
;
208 hb
.register_template_string("tape_backup_err_template", TAPE_BACKUP_ERR_TEMPLATE
)?
;
210 hb
.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE
)?
;
215 if let Err(err
) = result
{
216 eprintln
!("error during template registration: {}", err
);
223 /// Summary of a successful Tape Job
225 pub struct TapeBackupJobSummary
{
226 /// The list of snaphots backed up
227 pub snapshot_list
: Vec
<String
>,
228 /// The total time of the backup job
229 pub duration
: std
::time
::Duration
,
232 fn send_job_status_mail(
236 ) -> Result
<(), Error
> {
238 // Note: OX has serious problems displaying text mails,
239 // so we include html as well
240 let html
= format
!("<html><body><pre>\n{}\n<pre>", handlebars
::html_escape(text
));
242 let nodename
= proxmox
::tools
::nodename();
244 let author
= format
!("Proxmox Backup Server - {}", nodename
);
258 pub fn send_gc_status(
260 notify
: DatastoreNotify
,
262 status
: &GarbageCollectionStatus
,
263 result
: &Result
<(), Error
>,
264 ) -> Result
<(), Error
> {
267 None
=> { /* send notifications by default */ }
,
269 if notify
== Notify
::Never
|| (result
.is_ok() && notify
== Notify
::Error
) {
275 let (fqdn
, port
) = get_server_url();
276 let mut data
= json
!({
277 "datastore": datastore
,
282 let text
= match result
{
284 let deduplication_factor
= if status
.disk_bytes
> 0 {
285 (status
.index_data_bytes
as f64)/(status
.disk_bytes
as f64)
290 data
["status"] = json
!(status
);
291 data
["deduplication-factor"] = format
!("{:.2}", deduplication_factor
).into();
293 HANDLEBARS
.render("gc_ok_template", &data
)?
296 data
["error"] = err
.to_string().into();
297 HANDLEBARS
.render("gc_err_template", &data
)?
301 let subject
= match result
{
303 "Garbage Collect Datastore '{}' successful",
307 "Garbage Collect Datastore '{}' failed",
312 send_job_status_mail(email
, &subject
, &text
)?
;
317 pub fn send_verify_status(
319 notify
: DatastoreNotify
,
320 job
: VerificationJobConfig
,
321 result
: &Result
<Vec
<String
>, Error
>,
322 ) -> Result
<(), Error
> {
324 let (fqdn
, port
) = get_server_url();
325 let mut data
= json
!({
331 let mut result_is_ok
= false;
333 let text
= match result
{
334 Ok(errors
) if errors
.is_empty() => {
336 HANDLEBARS
.render("verify_ok_template", &data
)?
339 data
["errors"] = json
!(errors
);
340 HANDLEBARS
.render("verify_err_template", &data
)?
343 // aborted job - do not send any email
348 match notify
.verify
{
349 None
=> { /* send notifications by default */ }
,
351 if notify
== Notify
::Never
|| (result_is_ok
&& notify
== Notify
::Error
) {
357 let subject
= match result
{
358 Ok(errors
) if errors
.is_empty() => format
!(
359 "Verify Datastore '{}' successful",
363 "Verify Datastore '{}' failed",
368 send_job_status_mail(email
, &subject
, &text
)?
;
373 pub fn send_sync_status(
375 notify
: DatastoreNotify
,
377 result
: &Result
<(), Error
>,
378 ) -> Result
<(), Error
> {
381 None
=> { /* send notifications by default */ }
,
383 if notify
== Notify
::Never
|| (result
.is_ok() && notify
== Notify
::Error
) {
389 let (fqdn
, port
) = get_server_url();
390 let mut data
= json
!({
396 let text
= match result
{
398 HANDLEBARS
.render("sync_ok_template", &data
)?
401 data
["error"] = err
.to_string().into();
402 HANDLEBARS
.render("sync_err_template", &data
)?
406 let subject
= match result
{
408 "Sync remote '{}' datastore '{}' successful",
413 "Sync remote '{}' datastore '{}' failed",
419 send_job_status_mail(email
, &subject
, &text
)?
;
424 pub fn send_tape_backup_status(
427 job
: &TapeBackupJobSetup
,
428 result
: &Result
<(), Error
>,
429 summary
: TapeBackupJobSummary
,
430 ) -> Result
<(), Error
> {
432 let (fqdn
, port
) = get_server_url();
433 let duration
: proxmox_time
::TimeSpan
= summary
.duration
.into();
434 let mut data
= json
!({
439 "snapshot-list": summary
.snapshot_list
,
440 "duration": duration
.to_string(),
443 let text
= match result
{
445 HANDLEBARS
.render("tape_backup_ok_template", &data
)?
448 data
["error"] = err
.to_string().into();
449 HANDLEBARS
.render("tape_backup_err_template", &data
)?
453 let subject
= match (result
, id
) {
454 (Ok(()), Some(id
)) => format
!(
455 "Tape Backup '{}' datastore '{}' successful",
459 (Ok(()), None
) => format
!(
460 "Tape Backup datastore '{}' successful",
463 (Err(_
), Some(id
)) => format
!(
464 "Tape Backup '{}' datastore '{}' failed",
468 (Err(_
), None
) => format
!(
469 "Tape Backup datastore '{}' failed",
474 send_job_status_mail(email
, &subject
, &text
)?
;
479 /// Send email to a person to request a manual media change
480 pub fn send_load_media_email(
484 reason
: Option
<String
>,
485 ) -> Result
<(), Error
> {
487 let subject
= format
!("Load Media '{}' request for drive '{}'", label_text
, drive
);
489 let mut text
= String
::new();
491 if let Some(reason
) = reason
{
492 text
.push_str(&format
!("The drive has the wrong or no tape inserted. Error:\n{}\n\n", reason
));
495 text
.push_str("Please insert the requested media into the backup drive.\n\n");
497 text
.push_str(&format
!("Drive: {}\n", drive
));
498 text
.push_str(&format
!("Media: {}\n", label_text
));
500 send_job_status_mail(to
, &subject
, &text
)
503 fn get_server_url() -> (String
, usize) {
505 // user will surely request that they can change this
507 let nodename
= proxmox
::tools
::nodename();
508 let mut fqdn
= nodename
.to_owned();
510 if let Ok(resolv_conf
) = crate::api2
::node
::dns
::read_etc_resolv_conf() {
511 if let Some(search
) = resolv_conf
["search"].as_str() {
513 fqdn
.push_str(search
);
522 pub fn send_updates_available(
523 updates
: &[&APTUpdateInfo
],
524 ) -> Result
<(), Error
> {
525 // update mails always go to the root@pam configured email..
526 if let Some(email
) = lookup_user_email(Userid
::root_userid()) {
527 let nodename
= proxmox
::tools
::nodename();
528 let subject
= format
!("New software packages available ({})", nodename
);
530 let (fqdn
, port
) = get_server_url();
532 let text
= HANDLEBARS
.render("package_update_template", &json
!({
538 send_job_status_mail(&email
, &subject
, &text
)?
;
543 /// Lookup users email address
544 pub fn lookup_user_email(userid
: &Userid
) -> Option
<String
> {
546 if let Ok(user_config
) = pbs_config
::user
::cached_config() {
547 if let Ok(user
) = user_config
.lookup
::<User
>("user", userid
.as_str()) {
555 /// Lookup Datastore notify settings
556 pub fn lookup_datastore_notify_settings(
558 ) -> (Option
<String
>, DatastoreNotify
) {
560 let mut email
= None
;
562 let notify
= DatastoreNotify { gc: None, verify: None, sync: None }
;
564 let (config
, _digest
) = match pbs_config
::datastore
::config() {
565 Ok(result
) => result
,
566 Err(_
) => return (email
, notify
),
569 let config
: DataStoreConfig
= match config
.lookup("datastore", store
) {
570 Ok(result
) => result
,
571 Err(_
) => return (email
, notify
),
574 email
= match config
.notify_user
{
575 Some(ref userid
) => lookup_user_email(userid
),
576 None
=> lookup_user_email(Userid
::root_userid()),
579 let notify_str
= config
.notify
.unwrap_or_default();
581 if let Ok(value
) = parse_property_string(¬ify_str
, &DatastoreNotify
::API_SCHEMA
) {
582 if let Ok(notify
) = serde_json
::from_value(value
) {
583 return (email
, notify
);
590 // Handlerbar helper functions
592 fn handlebars_humam_bytes_helper(
596 _rc
: &mut RenderContext
,
599 let param
= h
.param(0).map(|v
| v
.value().as_u64())
601 .ok_or_else(|| RenderError
::new("human-bytes: param not found"))?
;
603 out
.write(&HumanByte
::from(param
).to_string())?
;
608 fn handlebars_relative_percentage_helper(
612 _rc
: &mut RenderContext
,
615 let param0
= h
.param(0).map(|v
| v
.value().as_f64())
617 .ok_or_else(|| RenderError
::new("relative-percentage: param0 not found"))?
;
618 let param1
= h
.param(1).map(|v
| v
.value().as_f64())
620 .ok_or_else(|| RenderError
::new("relative-percentage: param1 not found"))?
;
625 out
.write(&format
!("{:.2}%", (param0
*100.0)/param1
))?
;
631 fn test_template_register() {
632 HANDLEBARS
.get_helper("human-bytes").unwrap();
633 HANDLEBARS
.get_helper("relative-percentage").unwrap();
635 assert
!(HANDLEBARS
.has_template("gc_ok_template"));
636 assert
!(HANDLEBARS
.has_template("gc_err_template"));
638 assert
!(HANDLEBARS
.has_template("verify_ok_template"));
639 assert
!(HANDLEBARS
.has_template("verify_err_template"));
641 assert
!(HANDLEBARS
.has_template("sync_ok_template"));
642 assert
!(HANDLEBARS
.has_template("sync_err_template"));
644 assert
!(HANDLEBARS
.has_template("tape_backup_ok_template"));
645 assert
!(HANDLEBARS
.has_template("tape_backup_err_template"));
647 assert
!(HANDLEBARS
.has_template("package_update_template"));