]>
Commit | Line | Data |
---|---|---|
b9e7bcc2 DM |
1 | use anyhow::Error; |
2 | use serde_json::json; | |
3 | ||
ee0ea735 TL |
4 | use handlebars::{ |
5 | Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError, TemplateError, | |
6 | }; | |
b9e7bcc2 | 7 | |
6ef1b649 | 8 | use proxmox_lang::try_block; |
9fa3026a | 9 | use proxmox_schema::ApiType; |
ee0ea735 | 10 | use proxmox_sys::email::sendmail; |
b9e7bcc2 | 11 | |
e3619d41 | 12 | use pbs_api_types::{ |
ee0ea735 TL |
13 | APTUpdateInfo, DataStoreConfig, DatastoreNotify, GarbageCollectionStatus, HumanByte, Notify, |
14 | SyncJobConfig, TapeBackupJobSetup, User, Userid, VerificationJobConfig, | |
b9e7bcc2 DM |
15 | }; |
16 | ||
17 | const GC_OK_TEMPLATE: &str = r###" | |
18 | ||
d6373f35 DM |
19 | Datastore: {{datastore}} |
20 | Task ID: {{status.upid}} | |
21 | Index file count: {{status.index-file-count}} | |
b9e7bcc2 | 22 | |
d6373f35 DM |
23 | Removed garbage: {{human-bytes status.removed-bytes}} |
24 | Removed chunks: {{status.removed-chunks}} | |
1143f6ca | 25 | Removed bad chunks: {{status.removed-bad}} |
b9e7bcc2 | 26 | |
1143f6ca | 27 | Leftover bad chunks: {{status.still-bad}} |
d6373f35 | 28 | Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks) |
b9e7bcc2 | 29 | |
d6373f35 | 30 | Original Data usage: {{human-bytes status.index-data-bytes}} |
1143f6ca DM |
31 | On-Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}}) |
32 | On-Disk chunks: {{status.disk-chunks}} | |
d6373f35 DM |
33 | |
34 | Deduplication Factor: {{deduplication-factor}} | |
b9e7bcc2 DM |
35 | |
36 | Garbage collection successful. | |
37 | ||
3066f564 | 38 | |
d1d74c43 | 39 | Please visit the web interface for further details: |
3066f564 DM |
40 | |
41 | <https://{{fqdn}}:{{port}}/#DataStore-{{datastore}}> | |
42 | ||
b9e7bcc2 DM |
43 | "###; |
44 | ||
b9e7bcc2 DM |
45 | const GC_ERR_TEMPLATE: &str = r###" |
46 | ||
47 | Datastore: {{datastore}} | |
48 | ||
49 | Garbage collection failed: {{error}} | |
50 | ||
3066f564 | 51 | |
d1d74c43 | 52 | Please visit the web interface for further details: |
3066f564 DM |
53 | |
54 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
55 | ||
b9e7bcc2 DM |
56 | "###; |
57 | ||
58 | const VERIFY_OK_TEMPLATE: &str = r###" | |
59 | ||
60 | Job ID: {{job.id}} | |
61 | Datastore: {{job.store}} | |
62 | ||
63 | Verification successful. | |
64 | ||
3066f564 | 65 | |
d1d74c43 | 66 | Please visit the web interface for further details: |
3066f564 DM |
67 | |
68 | <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}> | |
69 | ||
b9e7bcc2 DM |
70 | "###; |
71 | ||
72 | const VERIFY_ERR_TEMPLATE: &str = r###" | |
73 | ||
74 | Job ID: {{job.id}} | |
75 | Datastore: {{job.store}} | |
76 | ||
23e4e905 | 77 | Verification failed on these snapshots/groups: |
a4915dfc DM |
78 | |
79 | {{#each errors}} | |
07ca4e36 | 80 | {{this~}} |
a4915dfc | 81 | {{/each}} |
b9e7bcc2 | 82 | |
3066f564 | 83 | |
d1d74c43 | 84 | Please visit the web interface for further details: |
3066f564 DM |
85 | |
86 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
87 | ||
b9e7bcc2 DM |
88 | "###; |
89 | ||
9e733dae DM |
90 | const SYNC_OK_TEMPLATE: &str = r###" |
91 | ||
92 | Job ID: {{job.id}} | |
93 | Datastore: {{job.store}} | |
94 | Remote: {{job.remote}} | |
95 | Remote Store: {{job.remote-store}} | |
96 | ||
97 | Synchronization successful. | |
98 | ||
3066f564 | 99 | |
d1d74c43 | 100 | Please visit the web interface for further details: |
3066f564 DM |
101 | |
102 | <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}> | |
103 | ||
9e733dae DM |
104 | "###; |
105 | ||
106 | const SYNC_ERR_TEMPLATE: &str = r###" | |
107 | ||
108 | Job ID: {{job.id}} | |
109 | Datastore: {{job.store}} | |
110 | Remote: {{job.remote}} | |
111 | Remote Store: {{job.remote-store}} | |
112 | ||
113 | Synchronization failed: {{error}} | |
114 | ||
3066f564 | 115 | |
d1d74c43 | 116 | Please visit the web interface for further details: |
3066f564 DM |
117 | |
118 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
119 | ||
9e733dae DM |
120 | "###; |
121 | ||
86d60245 TL |
122 | const PACKAGE_UPDATES_TEMPLATE: &str = r###" |
123 | Proxmox Backup Server has the following updates available: | |
124 | {{#each updates }} | |
125 | {{Package}}: {{OldVersion}} -> {{Version~}} | |
126 | {{/each }} | |
127 | ||
3066f564 DM |
128 | To upgrade visit the web interface: |
129 | ||
130 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates> | |
131 | ||
86d60245 TL |
132 | "###; |
133 | ||
8703a68a DC |
134 | const TAPE_BACKUP_OK_TEMPLATE: &str = r###" |
135 | ||
136 | {{#if id ~}} | |
137 | Job ID: {{id}} | |
138 | {{/if~}} | |
139 | Datastore: {{job.store}} | |
140 | Tape Pool: {{job.pool}} | |
141 | Tape Drive: {{job.drive}} | |
142 | ||
4abd4dbe DC |
143 | {{#if snapshot-list ~}} |
144 | Snapshots included: | |
145 | ||
146 | {{#each snapshot-list~}} | |
147 | {{this}} | |
148 | {{/each~}} | |
149 | {{/if}} | |
150 | Duration: {{duration}} | |
8703a68a DC |
151 | |
152 | Tape Backup successful. | |
153 | ||
154 | ||
d1d74c43 | 155 | Please visit the web interface for further details: |
8703a68a DC |
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 | ||
4ca3f0c6 DC |
170 | {{#if snapshot-list ~}} |
171 | Snapshots included: | |
8703a68a | 172 | |
4ca3f0c6 DC |
173 | {{#each snapshot-list~}} |
174 | {{this}} | |
175 | {{/each~}} | |
176 | {{/if}} | |
8703a68a DC |
177 | Tape Backup failed: {{error}} |
178 | ||
179 | ||
d1d74c43 | 180 | Please visit the web interface for further details: |
8703a68a DC |
181 | |
182 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
183 | ||
184 | "###; | |
86d60245 | 185 | |
ee0ea735 | 186 | lazy_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)] | |
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, | |
230 | } | |
231 | ||
ee0ea735 | 232 | fn 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 | ||
259 | pub 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 | ||
311 | pub 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 |
360 | pub 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 |
406 | pub 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 |
445 | pub 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 | 470 | fn 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 | 488 | pub 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 | 511 | pub 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 | 522 | pub 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(¬ify_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 | ||
559 | fn 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 | ||
577 | fn 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] | |
604 | fn 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 | } |