]> git.proxmox.com Git - proxmox-backup.git/blame - src/server/email_notifications.rs
Remove BackupFileDownloader.js file and Makefile entry
[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}}
36156038
DC
151{{#if used-tapes }}
152Used Tapes:
153{{#each used-tapes~}}
154{{this}}
155{{/each~}}
156{{/if}}
8703a68a
DC
157Tape Backup successful.
158
159
d1d74c43 160Please visit the web interface for further details:
8703a68a
DC
161
162<https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}>
163
164"###;
165
166const TAPE_BACKUP_ERR_TEMPLATE: &str = r###"
167
168{{#if id ~}}
169Job ID: {{id}}
170{{/if~}}
171Datastore: {{job.store}}
172Tape Pool: {{job.pool}}
173Tape Drive: {{job.drive}}
174
4ca3f0c6
DC
175{{#if snapshot-list ~}}
176Snapshots included:
8703a68a 177
4ca3f0c6
DC
178{{#each snapshot-list~}}
179{{this}}
180{{/each~}}
181{{/if}}
36156038
DC
182{{#if used-tapes }}
183Used Tapes:
184{{#each used-tapes~}}
185{{this}}
186{{/each~}}
187{{/if}}
8703a68a
DC
188Tape Backup failed: {{error}}
189
190
d1d74c43 191Please visit the web interface for further details:
8703a68a
DC
192
193<https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks>
194
195"###;
86d60245 196
9e8daa1d
SS
197const ACME_CERTIFICATE_ERR_RENEWAL: &str = r###"
198
199Proxmox Backup Server was not able to renew a TLS certificate.
200
201Error: {{error}}
202
203Please visit the web interface for further details:
204
205<https://{{fqdn}}:{{port}}/#pbsCertificateConfiguration>
206
207"###;
208
ee0ea735 209lazy_static::lazy_static! {
b9e7bcc2
DM
210
211 static ref HANDLEBARS: Handlebars<'static> = {
212 let mut hb = Handlebars::new();
25b4d52d 213 let result: Result<(), TemplateError> = try_block!({
b9e7bcc2 214
25b4d52d 215 hb.set_strict_mode(true);
f24cbee7 216 hb.register_escape_fn(handlebars::no_escape);
b9e7bcc2 217
25b4d52d
DC
218 hb.register_helper("human-bytes", Box::new(handlebars_humam_bytes_helper));
219 hb.register_helper("relative-percentage", Box::new(handlebars_relative_percentage_helper));
b9e7bcc2 220
25b4d52d
DC
221 hb.register_template_string("gc_ok_template", GC_OK_TEMPLATE)?;
222 hb.register_template_string("gc_err_template", GC_ERR_TEMPLATE)?;
b9e7bcc2 223
25b4d52d
DC
224 hb.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE)?;
225 hb.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE)?;
b9e7bcc2 226
25b4d52d
DC
227 hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE)?;
228 hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE)?;
9e733dae 229
25b4d52d
DC
230 hb.register_template_string("tape_backup_ok_template", TAPE_BACKUP_OK_TEMPLATE)?;
231 hb.register_template_string("tape_backup_err_template", TAPE_BACKUP_ERR_TEMPLATE)?;
8703a68a 232
25b4d52d
DC
233 hb.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE)?;
234
9e8daa1d
SS
235 hb.register_template_string("certificate_renewal_err_template", ACME_CERTIFICATE_ERR_RENEWAL)?;
236
25b4d52d
DC
237 Ok(())
238 });
239
240 if let Err(err) = result {
241 eprintln!("error during template registration: {}", err);
242 }
86d60245 243
b9e7bcc2
DM
244 hb
245 };
246}
247
4abd4dbe
DC
248/// Summary of a successful Tape Job
249#[derive(Default)]
250pub struct TapeBackupJobSummary {
251 /// The list of snaphots backed up
252 pub snapshot_list: Vec<String>,
253 /// The total time of the backup job
254 pub duration: std::time::Duration,
36156038
DC
255 /// The labels of the used tapes of the backup job
256 pub used_tapes: Option<Vec<String>>,
4abd4dbe
DC
257}
258
ee0ea735 259fn send_job_status_mail(email: &str, subject: &str, text: &str) -> Result<(), Error> {
e4665261 260 let (config, _) = crate::config::node::config()?;
ee0ea735 261 let from = config.email_from;
e4665261 262
b9e7bcc2
DM
263 // Note: OX has serious problems displaying text mails,
264 // so we include html as well
ee0ea735
TL
265 let html = format!(
266 "<html><body><pre>\n{}\n<pre>",
267 handlebars::html_escape(text)
268 );
b9e7bcc2 269
25877d05 270 let nodename = proxmox_sys::nodename();
b9e7bcc2
DM
271
272 let author = format!("Proxmox Backup Server - {}", nodename);
273
274 sendmail(
275 &[email],
9a37bd6c
FG
276 subject,
277 Some(text),
b9e7bcc2 278 Some(&html),
e4665261 279 from.as_deref(),
b9e7bcc2
DM
280 Some(&author),
281 )?;
282
283 Ok(())
284}
285
286pub fn send_gc_status(
287 email: &str,
c26c9390 288 notify: DatastoreNotify,
b9e7bcc2
DM
289 datastore: &str,
290 status: &GarbageCollectionStatus,
291 result: &Result<(), Error>,
292) -> Result<(), Error> {
c26c9390 293 match notify.gc {
ee0ea735 294 None => { /* send notifications by default */ }
c26c9390
DM
295 Some(notify) => {
296 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
297 return Ok(());
298 }
299 }
f47c1d3a
DM
300 }
301
3066f564
DM
302 let (fqdn, port) = get_server_url();
303 let mut data = json!({
304 "datastore": datastore,
305 "fqdn": fqdn,
306 "port": port,
307 });
308
b9e7bcc2
DM
309 let text = match result {
310 Ok(()) => {
d6373f35 311 let deduplication_factor = if status.disk_bytes > 0 {
ee0ea735 312 (status.index_data_bytes as f64) / (status.disk_bytes as f64)
d6373f35
DM
313 } else {
314 1.0
315 };
316
3066f564
DM
317 data["status"] = json!(status);
318 data["deduplication-factor"] = format!("{:.2}", deduplication_factor).into();
d6373f35 319
b9e7bcc2
DM
320 HANDLEBARS.render("gc_ok_template", &data)?
321 }
322 Err(err) => {
3066f564 323 data["error"] = err.to_string().into();
b9e7bcc2
DM
324 HANDLEBARS.render("gc_err_template", &data)?
325 }
326 };
327
328 let subject = match result {
ee0ea735
TL
329 Ok(()) => format!("Garbage Collect Datastore '{}' successful", datastore,),
330 Err(_) => format!("Garbage Collect Datastore '{}' failed", datastore,),
b9e7bcc2
DM
331 };
332
333 send_job_status_mail(email, &subject, &text)?;
334
335 Ok(())
336}
337
338pub fn send_verify_status(
339 email: &str,
c26c9390 340 notify: DatastoreNotify,
b9e7bcc2 341 job: VerificationJobConfig,
a4915dfc 342 result: &Result<Vec<String>, Error>,
b9e7bcc2 343) -> Result<(), Error> {
3066f564
DM
344 let (fqdn, port) = get_server_url();
345 let mut data = json!({
346 "job": job,
347 "fqdn": fqdn,
348 "port": port,
349 });
b9e7bcc2 350
c26c9390
DM
351 let mut result_is_ok = false;
352
b9e7bcc2 353 let text = match result {
a4915dfc 354 Ok(errors) if errors.is_empty() => {
c26c9390 355 result_is_ok = true;
b9e7bcc2
DM
356 HANDLEBARS.render("verify_ok_template", &data)?
357 }
a4915dfc 358 Ok(errors) => {
3066f564 359 data["errors"] = json!(errors);
b9e7bcc2
DM
360 HANDLEBARS.render("verify_err_template", &data)?
361 }
a4915dfc 362 Err(_) => {
d0abba33 363 // aborted job - do not send any email
a4915dfc
DM
364 return Ok(());
365 }
b9e7bcc2
DM
366 };
367
c26c9390 368 match notify.verify {
ee0ea735 369 None => { /* send notifications by default */ }
c26c9390
DM
370 Some(notify) => {
371 if notify == Notify::Never || (result_is_ok && notify == Notify::Error) {
372 return Ok(());
373 }
374 }
375 }
376
b9e7bcc2 377 let subject = match result {
ee0ea735
TL
378 Ok(errors) if errors.is_empty() => format!("Verify Datastore '{}' successful", job.store,),
379 _ => format!("Verify Datastore '{}' failed", job.store,),
b9e7bcc2
DM
380 };
381
382 send_job_status_mail(email, &subject, &text)?;
383
384 Ok(())
385}
386
9e733dae
DM
387pub fn send_sync_status(
388 email: &str,
c26c9390 389 notify: DatastoreNotify,
9e733dae
DM
390 job: &SyncJobConfig,
391 result: &Result<(), Error>,
392) -> Result<(), Error> {
c26c9390 393 match notify.sync {
ee0ea735 394 None => { /* send notifications by default */ }
c26c9390
DM
395 Some(notify) => {
396 if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) {
397 return Ok(());
398 }
399 }
f47c1d3a
DM
400 }
401
3066f564
DM
402 let (fqdn, port) = get_server_url();
403 let mut data = json!({
404 "job": job,
405 "fqdn": fqdn,
406 "port": port,
407 });
408
9e733dae 409 let text = match result {
ee0ea735 410 Ok(()) => HANDLEBARS.render("sync_ok_template", &data)?,
9e733dae 411 Err(err) => {
3066f564 412 data["error"] = err.to_string().into();
9e733dae
DM
413 HANDLEBARS.render("sync_err_template", &data)?
414 }
415 };
416
417 let subject = match result {
418 Ok(()) => format!(
419 "Sync remote '{}' datastore '{}' successful",
ee0ea735 420 job.remote, job.remote_store,
9e733dae
DM
421 ),
422 Err(_) => format!(
423 "Sync remote '{}' datastore '{}' failed",
ee0ea735 424 job.remote, job.remote_store,
9e733dae
DM
425 ),
426 };
427
428 send_job_status_mail(email, &subject, &text)?;
429
430 Ok(())
431}
432
8703a68a
DC
433pub fn send_tape_backup_status(
434 email: &str,
435 id: Option<&str>,
436 job: &TapeBackupJobSetup,
437 result: &Result<(), Error>,
4abd4dbe 438 summary: TapeBackupJobSummary,
8703a68a 439) -> Result<(), Error> {
8703a68a 440 let (fqdn, port) = get_server_url();
15cc41b6 441 let duration: proxmox_time::TimeSpan = summary.duration.into();
8703a68a
DC
442 let mut data = json!({
443 "job": job,
444 "fqdn": fqdn,
445 "port": port,
446 "id": id,
4abd4dbe 447 "snapshot-list": summary.snapshot_list,
36156038 448 "used-tapes": summary.used_tapes,
4abd4dbe 449 "duration": duration.to_string(),
8703a68a
DC
450 });
451
452 let text = match result {
ee0ea735 453 Ok(()) => HANDLEBARS.render("tape_backup_ok_template", &data)?,
8703a68a
DC
454 Err(err) => {
455 data["error"] = err.to_string().into();
456 HANDLEBARS.render("tape_backup_err_template", &data)?
457 }
458 };
459
460 let subject = match (result, id) {
ee0ea735
TL
461 (Ok(()), Some(id)) => format!("Tape Backup '{}' datastore '{}' successful", id, job.store,),
462 (Ok(()), None) => format!("Tape Backup datastore '{}' successful", job.store,),
463 (Err(_), Some(id)) => format!("Tape Backup '{}' datastore '{}' failed", id, job.store,),
464 (Err(_), None) => format!("Tape Backup datastore '{}' failed", job.store,),
8703a68a
DC
465 };
466
467 send_job_status_mail(email, &subject, &text)?;
468
469 Ok(())
470}
471
28926247
DC
472/// Send email to a person to request a manual media change
473pub fn send_load_media_email(
474 drive: &str,
475 label_text: &str,
476 to: &str,
477 reason: Option<String>,
478) -> Result<(), Error> {
5574114a
WB
479 use std::fmt::Write as _;
480
28926247
DC
481 let subject = format!("Load Media '{}' request for drive '{}'", label_text, drive);
482
483 let mut text = String::new();
484
485 if let Some(reason) = reason {
5574114a
WB
486 let _ = write!(
487 text,
ee0ea735
TL
488 "The drive has the wrong or no tape inserted. Error:\n{}\n\n",
489 reason
5574114a 490 );
28926247
DC
491 }
492
493 text.push_str("Please insert the requested media into the backup drive.\n\n");
494
5574114a
WB
495 let _ = writeln!(text, "Drive: {}", drive);
496 let _ = writeln!(text, "Media: {}", label_text);
28926247
DC
497
498 send_job_status_mail(to, &subject, &text)
499}
500
3066f564 501fn get_server_url() -> (String, usize) {
3066f564
DM
502 // user will surely request that they can change this
503
25877d05 504 let nodename = proxmox_sys::nodename();
3066f564
DM
505 let mut fqdn = nodename.to_owned();
506
507 if let Ok(resolv_conf) = crate::api2::node::dns::read_etc_resolv_conf() {
508 if let Some(search) = resolv_conf["search"].as_str() {
509 fqdn.push('.');
510 fqdn.push_str(search);
511 }
512 }
513
514 let port = 8007;
515
516 (fqdn, port)
517}
518
ee0ea735 519pub fn send_updates_available(updates: &[&APTUpdateInfo]) -> Result<(), Error> {
86d60245
TL
520 // update mails always go to the root@pam configured email..
521 if let Some(email) = lookup_user_email(Userid::root_userid()) {
25877d05 522 let nodename = proxmox_sys::nodename();
86d60245
TL
523 let subject = format!("New software packages available ({})", nodename);
524
3066f564
DM
525 let (fqdn, port) = get_server_url();
526
ee0ea735
TL
527 let text = HANDLEBARS.render(
528 "package_update_template",
529 &json!({
530 "fqdn": fqdn,
531 "port": port,
532 "updates": updates,
533 }),
534 )?;
86d60245
TL
535
536 send_job_status_mail(&email, &subject, &text)?;
537 }
538 Ok(())
539}
540
9e8daa1d 541/// send email on certificate renewal failure.
9e8daa1d
SS
542pub fn send_certificate_renewal_mail(result: &Result<(), Error>) -> Result<(), Error> {
543 let error: String = match result {
e1db0670 544 Err(e) => e.to_string(),
9e8daa1d
SS
545 _ => return Ok(()),
546 };
547
548 if let Some(email) = lookup_user_email(Userid::root_userid()) {
549 let (fqdn, port) = get_server_url();
550
551 let text = HANDLEBARS.render(
552 "certificate_renewal_err_template",
553 &json!({
554 "fqdn": fqdn,
555 "port": port,
556 "error": error,
557 }),
558 )?;
559
560 let subject = "Could not renew certificate";
561
562 send_job_status_mail(&email, subject, &text)?;
563 }
564
565 Ok(())
566}
567
b9e7bcc2 568/// Lookup users email address
c9793d47 569pub fn lookup_user_email(userid: &Userid) -> Option<String> {
ba3d7e19 570 if let Ok(user_config) = pbs_config::user::cached_config() {
b9e7bcc2 571 if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
44288184 572 return user.email;
b9e7bcc2
DM
573 }
574 }
575
576 None
577}
578
f47c1d3a 579/// Lookup Datastore notify settings
ee0ea735 580pub fn lookup_datastore_notify_settings(store: &str) -> (Option<String>, DatastoreNotify) {
f47c1d3a
DM
581 let mut email = None;
582
ee0ea735
TL
583 let notify = DatastoreNotify {
584 gc: None,
585 verify: None,
586 sync: None,
587 };
c26c9390 588
e7d4be9d 589 let (config, _digest) = match pbs_config::datastore::config() {
f47c1d3a
DM
590 Ok(result) => result,
591 Err(_) => return (email, notify),
592 };
593
594 let config: DataStoreConfig = match config.lookup("datastore", store) {
595 Ok(result) => result,
596 Err(_) => return (email, notify),
597 };
598
599 email = match config.notify_user {
600 Some(ref userid) => lookup_user_email(userid),
ad54df31 601 None => lookup_user_email(Userid::root_userid()),
f47c1d3a
DM
602 };
603
17c7b46a 604 let notify_str = config.notify.unwrap_or_default();
c26c9390 605
9fa3026a 606 if let Ok(value) = DatastoreNotify::API_SCHEMA.parse_property_string(&notify_str) {
c26c9390
DM
607 if let Ok(notify) = serde_json::from_value(value) {
608 return (email, notify);
609 }
f47c1d3a
DM
610 }
611
612 (email, notify)
613}
614
b9e7bcc2
DM
615// Handlerbar helper functions
616
617fn handlebars_humam_bytes_helper(
618 h: &Helper,
619 _: &Handlebars,
620 _: &Context,
621 _rc: &mut RenderContext,
ee0ea735 622 out: &mut dyn Output,
b9e7bcc2 623) -> HelperResult {
ee0ea735
TL
624 let param = h
625 .param(0)
e1db0670 626 .and_then(|v| v.value().as_u64())
e062ebbc 627 .ok_or_else(|| RenderError::new("human-bytes: param not found"))?;
b9e7bcc2
DM
628
629 out.write(&HumanByte::from(param).to_string())?;
630
631 Ok(())
632}
633
634fn handlebars_relative_percentage_helper(
635 h: &Helper,
636 _: &Handlebars,
637 _: &Context,
638 _rc: &mut RenderContext,
ee0ea735 639 out: &mut dyn Output,
b9e7bcc2 640) -> HelperResult {
ee0ea735
TL
641 let param0 = h
642 .param(0)
e1db0670 643 .and_then(|v| v.value().as_f64())
e062ebbc 644 .ok_or_else(|| RenderError::new("relative-percentage: param0 not found"))?;
ee0ea735
TL
645 let param1 = h
646 .param(1)
e1db0670 647 .and_then(|v| v.value().as_f64())
e062ebbc 648 .ok_or_else(|| RenderError::new("relative-percentage: param1 not found"))?;
b9e7bcc2
DM
649
650 if param1 == 0.0 {
651 out.write("-")?;
652 } else {
ee0ea735 653 out.write(&format!("{:.2}%", (param0 * 100.0) / param1))?;
b9e7bcc2
DM
654 }
655 Ok(())
656}
25b4d52d
DC
657
658#[test]
659fn test_template_register() {
660 HANDLEBARS.get_helper("human-bytes").unwrap();
661 HANDLEBARS.get_helper("relative-percentage").unwrap();
662
663 assert!(HANDLEBARS.has_template("gc_ok_template"));
664 assert!(HANDLEBARS.has_template("gc_err_template"));
665
666 assert!(HANDLEBARS.has_template("verify_ok_template"));
667 assert!(HANDLEBARS.has_template("verify_err_template"));
668
669 assert!(HANDLEBARS.has_template("sync_ok_template"));
670 assert!(HANDLEBARS.has_template("sync_err_template"));
671
672 assert!(HANDLEBARS.has_template("tape_backup_ok_template"));
673 assert!(HANDLEBARS.has_template("tape_backup_err_template"));
674
675 assert!(HANDLEBARS.has_template("package_update_template"));
9e8daa1d
SS
676
677 assert!(HANDLEBARS.has_template("certificate_renewal_err_template"));
25b4d52d 678}