]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/email_notifications.rs
api-types: add namespace to BackupGroup
[proxmox-backup.git] / src / server / email_notifications.rs
CommitLineData
b9e7bcc2
DM
1use anyhow::Error;
2use serde_json::json;
3
ee0ea735
TL
4use handlebars::{
5 Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, TemplateError,
6};
b9e7bcc2 7
6ef1b649 8use proxmox_lang::try_block;
9fa3026a 9use proxmox_schema::ApiType;
ee0ea735 10use proxmox_sys::email::sendmail;
b9e7bcc2 11
e3619d41 12use pbs_api_types::{
ee0ea735
TL
13 APTUpdateInfo, DataStoreConfig, DatastoreNotify, GarbageCollectionStatus, HumanByte, Notify,
14 SyncJobConfig, TapeBackupJobSetup, User, Userid, VerificationJobConfig,
b9e7bcc2
DM
15};
16
17const GC_OK_TEMPLATE: &str = r###"
18
d6373f35
DM
19Datastore: {{datastore}}
20Task ID: {{status.upid}}
21Index file count: {{status.index-file-count}}
b9e7bcc2 22
d6373f35
DM
23Removed garbage: {{human-bytes status.removed-bytes}}
24Removed chunks: {{status.removed-chunks}}
1143f6ca 25Removed bad chunks: {{status.removed-bad}}
b9e7bcc2 26
1143f6ca 27Leftover bad chunks: {{status.still-bad}}
d6373f35 28Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks)
b9e7bcc2 29
d6373f35 30Original Data usage: {{human-bytes status.index-data-bytes}}
1143f6ca
DM
31On-Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}})
32On-Disk chunks: {{status.disk-chunks}}
d6373f35
DM
33
34Deduplication Factor: {{deduplication-factor}}
b9e7bcc2
DM
35
36Garbage collection successful.
37
3066f564 38
d1d74c43 39Please visit the web interface for further details:
3066f564
DM
40
41<https://{{fqdn}}:{{port}}/#DataStore-{{datastore}}>
42
b9e7bcc2
DM
43"###;
44
b9e7bcc2
DM
45const GC_ERR_TEMPLATE: &str = r###"
46
47Datastore: {{datastore}}
48
49Garbage collection failed: {{error}}
50
3066f564 51
d1d74c43 52Please visit the web interface for further details:
3066f564
DM
53
54<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
55
b9e7bcc2
DM
56"###;
57
58const VERIFY_OK_TEMPLATE: &str = r###"
59
60Job ID: {{job.id}}
61Datastore: {{job.store}}
62
63Verification successful.
64
3066f564 65
d1d74c43 66Please visit the web interface for further details:
3066f564
DM
67
68<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
69
b9e7bcc2
DM
70"###;
71
72const VERIFY_ERR_TEMPLATE: &str = r###"
73
74Job ID: {{job.id}}
75Datastore: {{job.store}}
76
23e4e905 77Verification failed on these snapshots/groups:
a4915dfc
DM
78
79{{#each errors}}
07ca4e36 80 {{this~}}
a4915dfc 81{{/each}}
b9e7bcc2 82
3066f564 83
d1d74c43 84Please visit the web interface for further details:
3066f564
DM
85
86<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
87
b9e7bcc2
DM
88"###;
89
9e733dae
DM
90const SYNC_OK_TEMPLATE: &str = r###"
91
92Job ID: {{job.id}}
93Datastore: {{job.store}}
94Remote: {{job.remote}}
95Remote Store: {{job.remote-store}}
96
97Synchronization successful.
98
3066f564 99
d1d74c43 100Please visit the web interface for further details:
3066f564
DM
101
102<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
103
9e733dae
DM
104"###;
105
106const SYNC_ERR_TEMPLATE: &str = r###"
107
108Job ID: {{job.id}}
109Datastore: {{job.store}}
110Remote: {{job.remote}}
111Remote Store: {{job.remote-store}}
112
113Synchronization failed: {{error}}
114
3066f564 115
d1d74c43 116Please visit the web interface for further details:
3066f564
DM
117
118<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
119
9e733dae
DM
120"###;
121
86d60245
TL
122const PACKAGE_UPDATES_TEMPLATE: &str = r###"
123Proxmox Backup Server has the following updates available:
124{{#each updates }}
125 {{Package}}: {{OldVersion}} -> {{Version~}}
126{{/each }}
127
3066f564
DM
128To upgrade visit the web interface:
129
130<https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates>
131
86d60245
TL
132"###;
133
8703a68a
DC
134const TAPE_BACKUP_OK_TEMPLATE: &str = r###"
135
136{{#if id ~}}
137Job ID: {{id}}
138{{/if~}}
139Datastore: {{job.store}}
140Tape Pool: {{job.pool}}
141Tape Drive: {{job.drive}}
142
4abd4dbe
DC
143{{#if snapshot-list ~}}
144Snapshots included:
145
146{{#each snapshot-list~}}
147{{this}}
148{{/each~}}
149{{/if}}
150Duration: {{duration}}
8703a68a
DC
151
152Tape Backup successful.
153
154
d1d74c43 155Please visit the web interface for further details:
8703a68a
DC
156
157<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
158
159"###;
160
161const TAPE_BACKUP_ERR_TEMPLATE: &str = r###"
162
163{{#if id ~}}
164Job ID: {{id}}
165{{/if~}}
166Datastore: {{job.store}}
167Tape Pool: {{job.pool}}
168Tape Drive: {{job.drive}}
169
4ca3f0c6
DC
170{{#if snapshot-list ~}}
171Snapshots included:
8703a68a 172
4ca3f0c6
DC
173{{#each snapshot-list~}}
174{{this}}
175{{/each~}}
176{{/if}}
8703a68a
DC
177Tape Backup failed: {{error}}
178
179
d1d74c43 180Please visit the web interface for further details:
8703a68a
DC
181
182<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
183
184"###;
86d60245 185
ee0ea735 186lazy_static::lazy_static! {
b9e7bcc2
DM
187
188 static ref HANDLEBARS: Handlebars<'static> = {
189 let mut hb = Handlebars::new();
25b4d52d 190 let result: Result<(), TemplateError> = try_block!({
b9e7bcc2 191
25b4d52d 192 hb.set_strict_mode(true);
f24cbee7 193 hb.register_escape_fn(handlebars::no_escape);
b9e7bcc2 194
25b4d52d
DC
195 hb.register_helper("human-bytes", Box::new(handlebars_humam_bytes_helper));
196 hb.register_helper("relative-percentage", Box::new(handlebars_relative_percentage_helper));
b9e7bcc2 197
25b4d52d
DC
198 hb.register_template_string("gc_ok_template", GC_OK_TEMPLATE)?;
199 hb.register_template_string("gc_err_template", GC_ERR_TEMPLATE)?;
b9e7bcc2 200
25b4d52d
DC
201 hb.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE)?;
202 hb.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE)?;
b9e7bcc2 203
25b4d52d
DC
204 hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE)?;
205 hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE)?;
9e733dae 206
25b4d52d
DC
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)?;
8703a68a 209
25b4d52d
DC
210 hb.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE)?;
211
212 Ok(())
213 });
214
215 if let Err(err) = result {
216 eprintln!("error during template registration: {}", err);
217 }
86d60245 218
b9e7bcc2
DM
219 hb
220 };
221}
222
4abd4dbe
DC
223/// Summary of a successful Tape Job
224#[derive(Default)]
225pub 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,
230}
231
ee0ea735 232fn send_job_status_mail(email: &str, subject: &str, text: &str) -> Result<(), Error> {
e4665261 233 let (config, _) = crate::config::node::config()?;
ee0ea735 234 let from = config.email_from;
e4665261 235
b9e7bcc2
DM
236 // Note: OX has serious problems displaying text mails,
237 // so we include html as well
ee0ea735
TL
238 let html = format!(
239 "<html><body><pre>\n{}\n<pre>",
240 handlebars::html_escape(text)
241 );
b9e7bcc2 242
25877d05 243 let nodename = proxmox_sys::nodename();
b9e7bcc2
DM
244
245 let author = format!("Proxmox Backup Server - {}", nodename);
246
247 sendmail(
248 &[email],
9a37bd6c
FG
249 subject,
250 Some(text),
b9e7bcc2 251 Some(&html),
e4665261 252 from.as_deref(),
b9e7bcc2
DM
253 Some(&author),
254 )?;
255
256 Ok(())
257}
258
259pub fn send_gc_status(
260 email: &str,
c26c9390 261 notify: DatastoreNotify,
b9e7bcc2
DM
262 datastore: &str,
263 status: &GarbageCollectionStatus,
264 result: &Result<(), Error>,
265) -> Result<(), Error> {
c26c9390 266 match notify.gc {
ee0ea735 267 None => { /* send notifications by default */ }
c26c9390
DM
268 Some(notify) => {
269 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
270 return Ok(());
271 }
272 }
f47c1d3a
DM
273 }
274
3066f564
DM
275 let (fqdn, port) = get_server_url();
276 let mut data = json!({
277 "datastore": datastore,
278 "fqdn": fqdn,
279 "port": port,
280 });
281
b9e7bcc2
DM
282 let text = match result {
283 Ok(()) => {
d6373f35 284 let deduplication_factor = if status.disk_bytes > 0 {
ee0ea735 285 (status.index_data_bytes as f64) / (status.disk_bytes as f64)
d6373f35
DM
286 } else {
287 1.0
288 };
289
3066f564
DM
290 data["status"] = json!(status);
291 data["deduplication-factor"] = format!("{:.2}", deduplication_factor).into();
d6373f35 292
b9e7bcc2
DM
293 HANDLEBARS.render("gc_ok_template", &data)?
294 }
295 Err(err) => {
3066f564 296 data["error"] = err.to_string().into();
b9e7bcc2
DM
297 HANDLEBARS.render("gc_err_template", &data)?
298 }
299 };
300
301 let subject = match result {
ee0ea735
TL
302 Ok(()) => format!("Garbage Collect Datastore '{}' successful", datastore,),
303 Err(_) => format!("Garbage Collect Datastore '{}' failed", datastore,),
b9e7bcc2
DM
304 };
305
306 send_job_status_mail(email, &subject, &text)?;
307
308 Ok(())
309}
310
311pub fn send_verify_status(
312 email: &str,
c26c9390 313 notify: DatastoreNotify,
b9e7bcc2 314 job: VerificationJobConfig,
a4915dfc 315 result: &Result<Vec<String>, Error>,
b9e7bcc2 316) -> Result<(), Error> {
3066f564
DM
317 let (fqdn, port) = get_server_url();
318 let mut data = json!({
319 "job": job,
320 "fqdn": fqdn,
321 "port": port,
322 });
b9e7bcc2 323
c26c9390
DM
324 let mut result_is_ok = false;
325
b9e7bcc2 326 let text = match result {
a4915dfc 327 Ok(errors) if errors.is_empty() => {
c26c9390 328 result_is_ok = true;
b9e7bcc2
DM
329 HANDLEBARS.render("verify_ok_template", &data)?
330 }
a4915dfc 331 Ok(errors) => {
3066f564 332 data["errors"] = json!(errors);
b9e7bcc2
DM
333 HANDLEBARS.render("verify_err_template", &data)?
334 }
a4915dfc 335 Err(_) => {
d0abba33 336 // aborted job - do not send any email
a4915dfc
DM
337 return Ok(());
338 }
b9e7bcc2
DM
339 };
340
c26c9390 341 match notify.verify {
ee0ea735 342 None => { /* send notifications by default */ }
c26c9390
DM
343 Some(notify) => {
344 if notify == Notify::Never || (result_is_ok && notify == Notify::Error) {
345 return Ok(());
346 }
347 }
348 }
349
b9e7bcc2 350 let subject = match result {
ee0ea735
TL
351 Ok(errors) if errors.is_empty() => format!("Verify Datastore '{}' successful", job.store,),
352 _ => format!("Verify Datastore '{}' failed", job.store,),
b9e7bcc2
DM
353 };
354
355 send_job_status_mail(email, &subject, &text)?;
356
357 Ok(())
358}
359
9e733dae
DM
360pub fn send_sync_status(
361 email: &str,
c26c9390 362 notify: DatastoreNotify,
9e733dae
DM
363 job: &SyncJobConfig,
364 result: &Result<(), Error>,
365) -> Result<(), Error> {
c26c9390 366 match notify.sync {
ee0ea735 367 None => { /* send notifications by default */ }
c26c9390
DM
368 Some(notify) => {
369 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
370 return Ok(());
371 }
372 }
f47c1d3a
DM
373 }
374
3066f564
DM
375 let (fqdn, port) = get_server_url();
376 let mut data = json!({
377 "job": job,
378 "fqdn": fqdn,
379 "port": port,
380 });
381
9e733dae 382 let text = match result {
ee0ea735 383 Ok(()) => HANDLEBARS.render("sync_ok_template", &data)?,
9e733dae 384 Err(err) => {
3066f564 385 data["error"] = err.to_string().into();
9e733dae
DM
386 HANDLEBARS.render("sync_err_template", &data)?
387 }
388 };
389
390 let subject = match result {
391 Ok(()) => format!(
392 "Sync remote '{}' datastore '{}' successful",
ee0ea735 393 job.remote, job.remote_store,
9e733dae
DM
394 ),
395 Err(_) => format!(
396 "Sync remote '{}' datastore '{}' failed",
ee0ea735 397 job.remote, job.remote_store,
9e733dae
DM
398 ),
399 };
400
401 send_job_status_mail(email, &subject, &text)?;
402
403 Ok(())
404}
405
8703a68a
DC
406pub fn send_tape_backup_status(
407 email: &str,
408 id: Option<&str>,
409 job: &TapeBackupJobSetup,
410 result: &Result<(), Error>,
4abd4dbe 411 summary: TapeBackupJobSummary,
8703a68a 412) -> Result<(), Error> {
8703a68a 413 let (fqdn, port) = get_server_url();
15cc41b6 414 let duration: proxmox_time::TimeSpan = summary.duration.into();
8703a68a
DC
415 let mut data = json!({
416 "job": job,
417 "fqdn": fqdn,
418 "port": port,
419 "id": id,
4abd4dbe
DC
420 "snapshot-list": summary.snapshot_list,
421 "duration": duration.to_string(),
8703a68a
DC
422 });
423
424 let text = match result {
ee0ea735 425 Ok(()) => HANDLEBARS.render("tape_backup_ok_template", &data)?,
8703a68a
DC
426 Err(err) => {
427 data["error"] = err.to_string().into();
428 HANDLEBARS.render("tape_backup_err_template", &data)?
429 }
430 };
431
432 let subject = match (result, id) {
ee0ea735
TL
433 (Ok(()), Some(id)) => format!("Tape Backup '{}' datastore '{}' successful", id, job.store,),
434 (Ok(()), None) => format!("Tape Backup datastore '{}' successful", job.store,),
435 (Err(_), Some(id)) => format!("Tape Backup '{}' datastore '{}' failed", id, job.store,),
436 (Err(_), None) => format!("Tape Backup datastore '{}' failed", job.store,),
8703a68a
DC
437 };
438
439 send_job_status_mail(email, &subject, &text)?;
440
441 Ok(())
442}
443
28926247
DC
444/// Send email to a person to request a manual media change
445pub fn send_load_media_email(
446 drive: &str,
447 label_text: &str,
448 to: &str,
449 reason: Option<String>,
450) -> Result<(), Error> {
28926247
DC
451 let subject = format!("Load Media '{}' request for drive '{}'", label_text, drive);
452
453 let mut text = String::new();
454
455 if let Some(reason) = reason {
ee0ea735
TL
456 text.push_str(&format!(
457 "The drive has the wrong or no tape inserted. Error:\n{}\n\n",
458 reason
459 ));
28926247
DC
460 }
461
462 text.push_str("Please insert the requested media into the backup drive.\n\n");
463
464 text.push_str(&format!("Drive: {}\n", drive));
465 text.push_str(&format!("Media: {}\n", label_text));
466
467 send_job_status_mail(to, &subject, &text)
468}
469
3066f564 470fn get_server_url() -> (String, usize) {
3066f564
DM
471 // user will surely request that they can change this
472
25877d05 473 let nodename = proxmox_sys::nodename();
3066f564
DM
474 let mut fqdn = nodename.to_owned();
475
476 if let Ok(resolv_conf) = crate::api2::node::dns::read_etc_resolv_conf() {
477 if let Some(search) = resolv_conf["search"].as_str() {
478 fqdn.push('.');
479 fqdn.push_str(search);
480 }
481 }
482
483 let port = 8007;
484
485 (fqdn, port)
486}
487
ee0ea735 488pub fn send_updates_available(updates: &[&APTUpdateInfo]) -> Result<(), Error> {
86d60245
TL
489 // update mails always go to the root@pam configured email..
490 if let Some(email) = lookup_user_email(Userid::root_userid()) {
25877d05 491 let nodename = proxmox_sys::nodename();
86d60245
TL
492 let subject = format!("New software packages available ({})", nodename);
493
3066f564
DM
494 let (fqdn, port) = get_server_url();
495
ee0ea735
TL
496 let text = HANDLEBARS.render(
497 "package_update_template",
498 &json!({
499 "fqdn": fqdn,
500 "port": port,
501 "updates": updates,
502 }),
503 )?;
86d60245
TL
504
505 send_job_status_mail(&email, &subject, &text)?;
506 }
507 Ok(())
508}
509
b9e7bcc2 510/// Lookup users email address
c9793d47 511pub fn lookup_user_email(userid: &Userid) -> Option<String> {
ba3d7e19 512 if let Ok(user_config) = pbs_config::user::cached_config() {
b9e7bcc2 513 if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
44288184 514 return user.email;
b9e7bcc2
DM
515 }
516 }
517
518 None
519}
520
f47c1d3a 521/// Lookup Datastore notify settings
ee0ea735 522pub fn lookup_datastore_notify_settings(store: &str) -> (Option<String>, DatastoreNotify) {
f47c1d3a
DM
523 let mut email = None;
524
ee0ea735
TL
525 let notify = DatastoreNotify {
526 gc: None,
527 verify: None,
528 sync: None,
529 };
c26c9390 530
e7d4be9d 531 let (config, _digest) = match pbs_config::datastore::config() {
f47c1d3a
DM
532 Ok(result) => result,
533 Err(_) => return (email, notify),
534 };
535
536 let config: DataStoreConfig = match config.lookup("datastore", store) {
537 Ok(result) => result,
538 Err(_) => return (email, notify),
539 };
540
541 email = match config.notify_user {
542 Some(ref userid) => lookup_user_email(userid),
ad54df31 543 None => lookup_user_email(Userid::root_userid()),
f47c1d3a
DM
544 };
545
17c7b46a 546 let notify_str = config.notify.unwrap_or_default();
c26c9390 547
9fa3026a 548 if let Ok(value) = DatastoreNotify::API_SCHEMA.parse_property_string(&notify_str) {
c26c9390
DM
549 if let Ok(notify) = serde_json::from_value(value) {
550 return (email, notify);
551 }
f47c1d3a
DM
552 }
553
554 (email, notify)
555}
556
b9e7bcc2
DM
557// Handlerbar helper functions
558
559fn handlebars_humam_bytes_helper(
560 h: &Helper,
561 _: &Handlebars,
562 _: &Context,
563 _rc: &mut RenderContext,
ee0ea735 564 out: &mut dyn Output,
b9e7bcc2 565) -> HelperResult {
ee0ea735
TL
566 let param = h
567 .param(0)
568 .map(|v| v.value().as_u64())
b9e7bcc2 569 .flatten()
e062ebbc 570 .ok_or_else(|| RenderError::new("human-bytes: param not found"))?;
b9e7bcc2
DM
571
572 out.write(&HumanByte::from(param).to_string())?;
573
574 Ok(())
575}
576
577fn handlebars_relative_percentage_helper(
578 h: &Helper,
579 _: &Handlebars,
580 _: &Context,
581 _rc: &mut RenderContext,
ee0ea735 582 out: &mut dyn Output,
b9e7bcc2 583) -> HelperResult {
ee0ea735
TL
584 let param0 = h
585 .param(0)
586 .map(|v| v.value().as_f64())
b9e7bcc2 587 .flatten()
e062ebbc 588 .ok_or_else(|| RenderError::new("relative-percentage: param0 not found"))?;
ee0ea735
TL
589 let param1 = h
590 .param(1)
591 .map(|v| v.value().as_f64())
b9e7bcc2 592 .flatten()
e062ebbc 593 .ok_or_else(|| RenderError::new("relative-percentage: param1 not found"))?;
b9e7bcc2
DM
594
595 if param1 == 0.0 {
596 out.write("-")?;
597 } else {
ee0ea735 598 out.write(&format!("{:.2}%", (param0 * 100.0) / param1))?;
b9e7bcc2
DM
599 }
600 Ok(())
601}
25b4d52d
DC
602
603#[test]
604fn test_template_register() {
605 HANDLEBARS.get_helper("human-bytes").unwrap();
606 HANDLEBARS.get_helper("relative-percentage").unwrap();
607
608 assert!(HANDLEBARS.has_template("gc_ok_template"));
609 assert!(HANDLEBARS.has_template("gc_err_template"));
610
611 assert!(HANDLEBARS.has_template("verify_ok_template"));
612 assert!(HANDLEBARS.has_template("verify_err_template"));
613
614 assert!(HANDLEBARS.has_template("sync_ok_template"));
615 assert!(HANDLEBARS.has_template("sync_err_template"));
616
617 assert!(HANDLEBARS.has_template("tape_backup_ok_template"));
618 assert!(HANDLEBARS.has_template("tape_backup_err_template"));
619
620 assert!(HANDLEBARS.has_template("package_update_template"));
621}