]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/notifications.rs
server: notifications: send sync notifications via notification system
[proxmox-backup.git] / src / server / notifications.rs
CommitLineData
b9e7bcc2 1use anyhow::Error;
57570bda 2use const_format::concatcp;
04f35d0e 3use serde_json::json;
57570bda
LW
4use std::collections::HashMap;
5use std::path::Path;
6use std::time::{Duration, Instant};
b9e7bcc2 7
04f35d0e 8use handlebars::{Handlebars, TemplateError};
57570bda 9use nix::unistd::Uid;
b9e7bcc2 10
6ef1b649 11use proxmox_lang::try_block;
57570bda 12use proxmox_notify::context::pbs::PBS_CONTEXT;
9fa3026a 13use proxmox_schema::ApiType;
ee0ea735 14use proxmox_sys::email::sendmail;
57570bda 15use proxmox_sys::fs::{create_path, CreateOptions};
b9e7bcc2 16
e3619d41 17use pbs_api_types::{
04f35d0e
LW
18 APTUpdateInfo, DataStoreConfig, DatastoreNotify, GarbageCollectionStatus, NotificationMode,
19 Notify, SyncJobConfig, TapeBackupJobSetup, User, Userid, VerificationJobConfig,
b9e7bcc2 20};
04f35d0e
LW
21use proxmox_notify::endpoints::sendmail::{SendmailConfig, SendmailEndpoint};
22use proxmox_notify::{Endpoint, Notification, Severity};
b9e7bcc2 23
57570bda 24const SPOOL_DIR: &str = concatcp!(pbs_buildcfg::PROXMOX_BACKUP_STATE_DIR, "/notifications");
b9e7bcc2 25
86d60245
TL
26const PACKAGE_UPDATES_TEMPLATE: &str = r###"
27Proxmox Backup Server has the following updates available:
28{{#each updates }}
29 {{Package}}: {{OldVersion}} -> {{Version~}}
30{{/each }}
31
3066f564
DM
32To upgrade visit the web interface:
33
34<https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates>
35
86d60245
TL
36"###;
37
8703a68a
DC
38const TAPE_BACKUP_OK_TEMPLATE: &str = r###"
39
40{{#if id ~}}
41Job ID: {{id}}
42{{/if~}}
43Datastore: {{job.store}}
44Tape Pool: {{job.pool}}
45Tape Drive: {{job.drive}}
46
4abd4dbe
DC
47{{#if snapshot-list ~}}
48Snapshots included:
49
50{{#each snapshot-list~}}
51{{this}}
52{{/each~}}
53{{/if}}
54Duration: {{duration}}
36156038
DC
55{{#if used-tapes }}
56Used Tapes:
57{{#each used-tapes~}}
58{{this}}
59{{/each~}}
60{{/if}}
8703a68a
DC
61Tape Backup successful.
62
63
d1d74c43 64Please visit the web interface for further details:
8703a68a
DC
65
66<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
67
68"###;
69
70const TAPE_BACKUP_ERR_TEMPLATE: &str = r###"
71
72{{#if id ~}}
73Job ID: {{id}}
74{{/if~}}
75Datastore: {{job.store}}
76Tape Pool: {{job.pool}}
77Tape Drive: {{job.drive}}
78
4ca3f0c6
DC
79{{#if snapshot-list ~}}
80Snapshots included:
8703a68a 81
4ca3f0c6
DC
82{{#each snapshot-list~}}
83{{this}}
84{{/each~}}
85{{/if}}
36156038
DC
86{{#if used-tapes }}
87Used Tapes:
88{{#each used-tapes~}}
89{{this}}
90{{/each~}}
91{{/if}}
8703a68a
DC
92Tape Backup failed: {{error}}
93
94
d1d74c43 95Please visit the web interface for further details:
8703a68a
DC
96
97<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
98
99"###;
86d60245 100
9e8daa1d
SS
101const ACME_CERTIFICATE_ERR_RENEWAL: &str = r###"
102
103Proxmox Backup Server was not able to renew a TLS certificate.
104
105Error: {{error}}
106
107Please visit the web interface for further details:
108
109<https://{{fqdn}}:{{port}}/#pbsCertificateConfiguration>
110
111"###;
112
ee0ea735 113lazy_static::lazy_static! {
b9e7bcc2
DM
114
115 static ref HANDLEBARS: Handlebars<'static> = {
116 let mut hb = Handlebars::new();
25b4d52d 117 let result: Result<(), TemplateError> = try_block!({
b9e7bcc2 118
25b4d52d 119 hb.set_strict_mode(true);
f24cbee7 120 hb.register_escape_fn(handlebars::no_escape);
b9e7bcc2 121
25b4d52d
DC
122 hb.register_template_string("tape_backup_ok_template", TAPE_BACKUP_OK_TEMPLATE)?;
123 hb.register_template_string("tape_backup_err_template", TAPE_BACKUP_ERR_TEMPLATE)?;
8703a68a 124
25b4d52d
DC
125 hb.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE)?;
126
9e8daa1d
SS
127 hb.register_template_string("certificate_renewal_err_template", ACME_CERTIFICATE_ERR_RENEWAL)?;
128
25b4d52d
DC
129 Ok(())
130 });
131
132 if let Err(err) = result {
dd06b7f1 133 eprintln!("error during template registration: {err}");
25b4d52d 134 }
86d60245 135
b9e7bcc2
DM
136 hb
137 };
138}
139
57570bda
LW
140/// Initialize the notification system by setting context in proxmox_notify
141pub fn init() -> Result<(), Error> {
142 proxmox_notify::context::set_context(&PBS_CONTEXT);
143 Ok(())
144}
145
146/// Create the directory which will be used to temporarily store notifications
147/// which were sent from an unprivileged process.
148pub fn create_spool_dir() -> Result<(), Error> {
149 let backup_user = pbs_config::backup_user()?;
150 let opts = CreateOptions::new()
151 .owner(backup_user.uid)
152 .group(backup_user.gid);
153
154 create_path(SPOOL_DIR, None, Some(opts))?;
155 Ok(())
156}
157
158async fn send_queued_notifications() -> Result<(), Error> {
159 let mut read_dir = tokio::fs::read_dir(SPOOL_DIR).await?;
160
161 let mut notifications = Vec::new();
162
163 while let Some(entry) = read_dir.next_entry().await? {
164 let path = entry.path();
165
166 if let Some(ext) = path.extension() {
167 if ext == "json" {
168 let p = path.clone();
169
170 let bytes = tokio::fs::read(p).await?;
171 let notification: Notification = serde_json::from_slice(&bytes)?;
172 notifications.push(notification);
173
174 // Currently, there is no retry-mechanism in case of failure...
175 // For retries, we'd have to keep track of which targets succeeded/failed
176 // to send, so we do not retry notifying a target which succeeded before.
177 tokio::fs::remove_file(path).await?;
178 }
179 }
180 }
181
182 // Make sure that we send the oldest notification first
183 notifications.sort_unstable_by_key(|n| n.timestamp());
184
185 let res = tokio::task::spawn_blocking(move || {
186 let config = pbs_config::notifications::config()?;
187 for notification in notifications {
188 if let Err(err) = proxmox_notify::api::common::send(&config, &notification) {
189 log::error!("failed to send notification: {err}");
190 }
191 }
192
193 Ok::<(), Error>(())
194 })
195 .await?;
196
197 if let Err(e) = res {
198 log::error!("could not read notification config: {e}");
199 }
200
201 Ok::<(), Error>(())
202}
203
204/// Worker task to periodically send any queued notifications.
205pub async fn notification_worker() {
206 loop {
207 let delay_target = Instant::now() + Duration::from_secs(5);
208
209 if let Err(err) = send_queued_notifications().await {
210 log::error!("notification worker task error: {err}");
211 }
212
213 tokio::time::sleep_until(tokio::time::Instant::from_std(delay_target)).await;
214 }
215}
216
217fn send_notification(notification: Notification) -> Result<(), Error> {
218 if nix::unistd::ROOT == Uid::current() {
219 let config = pbs_config::notifications::config()?;
220 proxmox_notify::api::common::send(&config, &notification)?;
221 } else {
222 let ser = serde_json::to_vec(&notification)?;
223 let path = Path::new(SPOOL_DIR).join(format!("{id}.json", id = notification.id()));
224
225 let backup_user = pbs_config::backup_user()?;
226 let opts = CreateOptions::new()
227 .owner(backup_user.uid)
228 .group(backup_user.gid);
229 proxmox_sys::fs::replace_file(path, &ser, opts, true)?;
230 log::info!("queued notification (id={id})", id = notification.id())
231 }
232
233 Ok(())
234}
235
04f35d0e
LW
236fn send_sendmail_legacy_notification(notification: Notification, email: &str) -> Result<(), Error> {
237 let endpoint = SendmailEndpoint {
238 config: SendmailConfig {
239 mailto: vec![email.into()],
240 ..Default::default()
241 },
242 };
243
244 endpoint.send(&notification)?;
245
246 Ok(())
247}
248
4abd4dbe
DC
249/// Summary of a successful Tape Job
250#[derive(Default)]
251pub struct TapeBackupJobSummary {
252 /// The list of snaphots backed up
253 pub snapshot_list: Vec<String>,
254 /// The total time of the backup job
255 pub duration: std::time::Duration,
36156038
DC
256 /// The labels of the used tapes of the backup job
257 pub used_tapes: Option<Vec<String>>,
4abd4dbe
DC
258}
259
ee0ea735 260fn send_job_status_mail(email: &str, subject: &str, text: &str) -> Result<(), Error> {
e4665261 261 let (config, _) = crate::config::node::config()?;
ee0ea735 262 let from = config.email_from;
e4665261 263
dd06b7f1
TL
264 // NOTE: some (web)mailers have big problems displaying text mails, so include html as well
265 let escaped_text = handlebars::html_escape(text);
266 let html = format!("<html><body><pre>\n{escaped_text}\n<pre>");
b9e7bcc2 267
25877d05 268 let nodename = proxmox_sys::nodename();
b9e7bcc2 269
dd06b7f1 270 let author = format!("Proxmox Backup Server - {nodename}");
b9e7bcc2
DM
271
272 sendmail(
273 &[email],
9a37bd6c
FG
274 subject,
275 Some(text),
b9e7bcc2 276 Some(&html),
e4665261 277 from.as_deref(),
b9e7bcc2
DM
278 Some(&author),
279 )?;
280
281 Ok(())
282}
283
284pub fn send_gc_status(
b9e7bcc2
DM
285 datastore: &str,
286 status: &GarbageCollectionStatus,
287 result: &Result<(), Error>,
288) -> Result<(), Error> {
3066f564
DM
289 let (fqdn, port) = get_server_url();
290 let mut data = json!({
291 "datastore": datastore,
292 "fqdn": fqdn,
293 "port": port,
294 });
295
04f35d0e 296 let (severity, template) = match result {
b9e7bcc2 297 Ok(()) => {
d6373f35 298 let deduplication_factor = if status.disk_bytes > 0 {
ee0ea735 299 (status.index_data_bytes as f64) / (status.disk_bytes as f64)
d6373f35
DM
300 } else {
301 1.0
302 };
303
3066f564
DM
304 data["status"] = json!(status);
305 data["deduplication-factor"] = format!("{:.2}", deduplication_factor).into();
d6373f35 306
04f35d0e 307 (Severity::Info, "gc-ok")
b9e7bcc2
DM
308 }
309 Err(err) => {
3066f564 310 data["error"] = err.to_string().into();
04f35d0e 311 (Severity::Error, "gc-err")
b9e7bcc2
DM
312 }
313 };
04f35d0e
LW
314 let metadata = HashMap::from([
315 ("datastore".into(), datastore.into()),
316 ("hostname".into(), proxmox_sys::nodename().into()),
317 ("type".into(), "gc".into()),
318 ]);
b9e7bcc2 319
04f35d0e 320 let notification = Notification::from_template(severity, template, data, metadata);
b9e7bcc2 321
04f35d0e
LW
322 let (email, notify, mode) = lookup_datastore_notify_settings(datastore);
323 match mode {
324 NotificationMode::LegacySendmail => {
325 let notify = notify.gc.unwrap_or(Notify::Always);
326
327 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
328 return Ok(());
329 }
330
331 if let Some(email) = email {
332 send_sendmail_legacy_notification(notification, &email)?;
333 }
334 }
335 NotificationMode::NotificationSystem => {
336 send_notification(notification)?;
337 }
338 }
b9e7bcc2
DM
339
340 Ok(())
341}
342
343pub fn send_verify_status(
b9e7bcc2 344 job: VerificationJobConfig,
a4915dfc 345 result: &Result<Vec<String>, Error>,
b9e7bcc2 346) -> Result<(), Error> {
3066f564
DM
347 let (fqdn, port) = get_server_url();
348 let mut data = json!({
349 "job": job,
350 "fqdn": fqdn,
351 "port": port,
352 });
b9e7bcc2 353
2432775c
LW
354 let (template, severity) = match result {
355 Ok(errors) if errors.is_empty() => ("verify-ok", Severity::Info),
a4915dfc 356 Ok(errors) => {
3066f564 357 data["errors"] = json!(errors);
2432775c 358 ("verify-err", Severity::Error)
b9e7bcc2 359 }
a4915dfc 360 Err(_) => {
2432775c 361 // aborted job - do not send any notification
a4915dfc
DM
362 return Ok(());
363 }
b9e7bcc2
DM
364 };
365
2432775c
LW
366 let metadata = HashMap::from([
367 ("job-id".into(), job.id.clone()),
368 ("datastore".into(), job.store.clone()),
369 ("hostname".into(), proxmox_sys::nodename().into()),
370 ("type".into(), "verify".into()),
371 ]);
372
373 let notification = Notification::from_template(severity, template, data, metadata);
374
375 let (email, notify, mode) = lookup_datastore_notify_settings(&job.store);
376 match mode {
377 NotificationMode::LegacySendmail => {
378 let notify = notify.verify.unwrap_or(Notify::Always);
379
380 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
c26c9390
DM
381 return Ok(());
382 }
2432775c
LW
383
384 if let Some(email) = email {
385 send_sendmail_legacy_notification(notification, &email)?;
386 }
387 }
388 NotificationMode::NotificationSystem => {
389 send_notification(notification)?;
c26c9390
DM
390 }
391 }
392
b9e7bcc2
DM
393 Ok(())
394}
395
cf91a072
DC
396pub fn send_prune_status(
397 store: &str,
398 jobname: &str,
399 result: &Result<(), Error>,
400) -> Result<(), Error> {
cf91a072
DC
401 let (fqdn, port) = get_server_url();
402 let mut data = json!({
403 "jobname": jobname,
404 "store": store,
405 "fqdn": fqdn,
406 "port": port,
407 });
408
3ca03c05
LW
409 let (template, severity) = match result {
410 Ok(()) => ("prune-ok", Severity::Info),
cf91a072
DC
411 Err(err) => {
412 data["error"] = err.to_string().into();
3ca03c05 413 ("prune-err", Severity::Error)
cf91a072
DC
414 }
415 };
416
3ca03c05
LW
417 let metadata = HashMap::from([
418 ("job-id".into(), jobname.to_string()),
419 ("datastore".into(), store.into()),
420 ("hostname".into(), proxmox_sys::nodename().into()),
421 ("type".into(), "prune".into()),
422 ]);
cf91a072 423
3ca03c05
LW
424 let notification = Notification::from_template(severity, template, data, metadata);
425
426 let (email, notify, mode) = lookup_datastore_notify_settings(store);
427 match mode {
428 NotificationMode::LegacySendmail => {
429 let notify = notify.prune.unwrap_or(Notify::Error);
430
431 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
432 return Ok(());
433 }
434
435 if let Some(email) = email {
436 send_sendmail_legacy_notification(notification, &email)?;
437 }
438 }
439 NotificationMode::NotificationSystem => {
440 send_notification(notification)?;
441 }
442 }
cf91a072
DC
443
444 Ok(())
445}
446
5b23a707 447pub fn send_sync_status(job: &SyncJobConfig, result: &Result<(), Error>) -> Result<(), Error> {
3066f564
DM
448 let (fqdn, port) = get_server_url();
449 let mut data = json!({
450 "job": job,
451 "fqdn": fqdn,
452 "port": port,
453 });
454
5b23a707
LW
455 let (template, severity) = match result {
456 Ok(()) => ("sync-ok", Severity::Info),
9e733dae 457 Err(err) => {
3066f564 458 data["error"] = err.to_string().into();
5b23a707 459 ("sync-err", Severity::Error)
9e733dae
DM
460 }
461 };
462
5b23a707
LW
463 let metadata = HashMap::from([
464 ("job-id".into(), job.id.clone()),
465 ("datastore".into(), job.store.clone()),
466 ("hostname".into(), proxmox_sys::nodename().into()),
467 ("type".into(), "sync".into()),
468 ]);
4ec73327 469
5b23a707 470 let notification = Notification::from_template(severity, template, data, metadata);
9e733dae 471
5b23a707
LW
472 let (email, notify, mode) = lookup_datastore_notify_settings(&job.store);
473 match mode {
474 NotificationMode::LegacySendmail => {
475 let notify = notify.prune.unwrap_or(Notify::Error);
476
477 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
478 return Ok(());
479 }
480
481 if let Some(email) = email {
482 send_sendmail_legacy_notification(notification, &email)?;
483 }
484 }
485 NotificationMode::NotificationSystem => {
486 send_notification(notification)?;
487 }
488 }
9e733dae
DM
489
490 Ok(())
491}
492
8703a68a
DC
493pub fn send_tape_backup_status(
494 email: &str,
495 id: Option<&str>,
496 job: &TapeBackupJobSetup,
497 result: &Result<(), Error>,
4abd4dbe 498 summary: TapeBackupJobSummary,
8703a68a 499) -> Result<(), Error> {
8703a68a 500 let (fqdn, port) = get_server_url();
15cc41b6 501 let duration: proxmox_time::TimeSpan = summary.duration.into();
8703a68a
DC
502 let mut data = json!({
503 "job": job,
504 "fqdn": fqdn,
505 "port": port,
506 "id": id,
4abd4dbe 507 "snapshot-list": summary.snapshot_list,
36156038 508 "used-tapes": summary.used_tapes,
4abd4dbe 509 "duration": duration.to_string(),
8703a68a
DC
510 });
511
512 let text = match result {
ee0ea735 513 Ok(()) => HANDLEBARS.render("tape_backup_ok_template", &data)?,
8703a68a
DC
514 Err(err) => {
515 data["error"] = err.to_string().into();
516 HANDLEBARS.render("tape_backup_err_template", &data)?
517 }
518 };
519
520 let subject = match (result, id) {
dd06b7f1 521 (Ok(()), Some(id)) => format!("Tape Backup '{id}' datastore '{}' successful", job.store,),
ee0ea735 522 (Ok(()), None) => format!("Tape Backup datastore '{}' successful", job.store,),
dd06b7f1 523 (Err(_), Some(id)) => format!("Tape Backup '{id}' datastore '{}' failed", job.store,),
ee0ea735 524 (Err(_), None) => format!("Tape Backup datastore '{}' failed", job.store,),
8703a68a
DC
525 };
526
527 send_job_status_mail(email, &subject, &text)?;
528
529 Ok(())
530}
531
28926247
DC
532/// Send email to a person to request a manual media change
533pub fn send_load_media_email(
bdce7fa1
DC
534 changer: bool,
535 device: &str,
28926247
DC
536 label_text: &str,
537 to: &str,
538 reason: Option<String>,
539) -> Result<(), Error> {
5574114a
WB
540 use std::fmt::Write as _;
541
bdce7fa1
DC
542 let device_type = if changer { "changer" } else { "drive" };
543
544 let subject = format!("Load Media '{label_text}' request for {device_type} '{device}'");
28926247
DC
545
546 let mut text = String::new();
547
548 if let Some(reason) = reason {
5574114a
WB
549 let _ = write!(
550 text,
bdce7fa1 551 "The {device_type} has the wrong or no tape(s) inserted. Error:\n{reason}\n\n"
5574114a 552 );
28926247
DC
553 }
554
bdce7fa1
DC
555 if changer {
556 text.push_str("Please insert the requested media into the changer.\n\n");
557 let _ = writeln!(text, "Changer: {device}");
558 } else {
559 text.push_str("Please insert the requested media into the backup drive.\n\n");
560 let _ = writeln!(text, "Drive: {device}");
561 }
dd06b7f1 562 let _ = writeln!(text, "Media: {label_text}");
28926247
DC
563
564 send_job_status_mail(to, &subject, &text)
565}
566
3066f564 567fn get_server_url() -> (String, usize) {
3066f564
DM
568 // user will surely request that they can change this
569
25877d05 570 let nodename = proxmox_sys::nodename();
3066f564
DM
571 let mut fqdn = nodename.to_owned();
572
573 if let Ok(resolv_conf) = crate::api2::node::dns::read_etc_resolv_conf() {
574 if let Some(search) = resolv_conf["search"].as_str() {
575 fqdn.push('.');
576 fqdn.push_str(search);
577 }
578 }
579
580 let port = 8007;
581
582 (fqdn, port)
583}
584
ee0ea735 585pub fn send_updates_available(updates: &[&APTUpdateInfo]) -> Result<(), Error> {
86d60245
TL
586 // update mails always go to the root@pam configured email..
587 if let Some(email) = lookup_user_email(Userid::root_userid()) {
25877d05 588 let nodename = proxmox_sys::nodename();
dd06b7f1 589 let subject = format!("New software packages available ({nodename})");
86d60245 590
3066f564
DM
591 let (fqdn, port) = get_server_url();
592
ee0ea735
TL
593 let text = HANDLEBARS.render(
594 "package_update_template",
595 &json!({
596 "fqdn": fqdn,
597 "port": port,
598 "updates": updates,
599 }),
600 )?;
86d60245
TL
601
602 send_job_status_mail(&email, &subject, &text)?;
603 }
604 Ok(())
605}
606
9e8daa1d 607/// send email on certificate renewal failure.
9e8daa1d
SS
608pub fn send_certificate_renewal_mail(result: &Result<(), Error>) -> Result<(), Error> {
609 let error: String = match result {
e1db0670 610 Err(e) => e.to_string(),
9e8daa1d
SS
611 _ => return Ok(()),
612 };
613
614 if let Some(email) = lookup_user_email(Userid::root_userid()) {
615 let (fqdn, port) = get_server_url();
616
617 let text = HANDLEBARS.render(
618 "certificate_renewal_err_template",
619 &json!({
620 "fqdn": fqdn,
621 "port": port,
622 "error": error,
623 }),
624 )?;
625
626 let subject = "Could not renew certificate";
627
628 send_job_status_mail(&email, subject, &text)?;
629 }
630
631 Ok(())
632}
633
b9e7bcc2 634/// Lookup users email address
c9793d47 635pub fn lookup_user_email(userid: &Userid) -> Option<String> {
ba3d7e19 636 if let Ok(user_config) = pbs_config::user::cached_config() {
b9e7bcc2 637 if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
44288184 638 return user.email;
b9e7bcc2
DM
639 }
640 }
641
642 None
643}
644
f47c1d3a 645/// Lookup Datastore notify settings
04f35d0e
LW
646pub fn lookup_datastore_notify_settings(
647 store: &str,
648) -> (Option<String>, DatastoreNotify, NotificationMode) {
f47c1d3a
DM
649 let mut email = None;
650
ee0ea735
TL
651 let notify = DatastoreNotify {
652 gc: None,
653 verify: None,
654 sync: None,
cf91a072 655 prune: None,
ee0ea735 656 };
c26c9390 657
e7d4be9d 658 let (config, _digest) = match pbs_config::datastore::config() {
f47c1d3a 659 Ok(result) => result,
04f35d0e 660 Err(_) => return (email, notify, NotificationMode::default()),
f47c1d3a
DM
661 };
662
663 let config: DataStoreConfig = match config.lookup("datastore", store) {
664 Ok(result) => result,
04f35d0e 665 Err(_) => return (email, notify, NotificationMode::default()),
f47c1d3a
DM
666 };
667
668 email = match config.notify_user {
669 Some(ref userid) => lookup_user_email(userid),
ad54df31 670 None => lookup_user_email(Userid::root_userid()),
f47c1d3a
DM
671 };
672
04f35d0e 673 let notification_mode = config.notification_mode.unwrap_or_default();
17c7b46a 674 let notify_str = config.notify.unwrap_or_default();
c26c9390 675
9fa3026a 676 if let Ok(value) = DatastoreNotify::API_SCHEMA.parse_property_string(&notify_str) {
c26c9390 677 if let Ok(notify) = serde_json::from_value(value) {
04f35d0e 678 return (email, notify, notification_mode);
c26c9390 679 }
f47c1d3a
DM
680 }
681
04f35d0e 682 (email, notify, notification_mode)
b9e7bcc2 683}
25b4d52d
DC
684
685#[test]
686fn test_template_register() {
25b4d52d
DC
687 assert!(HANDLEBARS.has_template("tape_backup_ok_template"));
688 assert!(HANDLEBARS.has_template("tape_backup_err_template"));
689
690 assert!(HANDLEBARS.has_template("package_update_template"));
9e8daa1d
SS
691
692 assert!(HANDLEBARS.has_template("certificate_renewal_err_template"));
25b4d52d 693}