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