]>
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}} | |
36156038 DC |
151 | {{#if used-tapes }} |
152 | Used Tapes: | |
153 | {{#each used-tapes~}} | |
154 | {{this}} | |
155 | {{/each~}} | |
156 | {{/if}} | |
8703a68a DC |
157 | Tape Backup successful. |
158 | ||
159 | ||
d1d74c43 | 160 | Please visit the web interface for further details: |
8703a68a DC |
161 | |
162 | <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}> | |
163 | ||
164 | "###; | |
165 | ||
166 | const TAPE_BACKUP_ERR_TEMPLATE: &str = r###" | |
167 | ||
168 | {{#if id ~}} | |
169 | Job ID: {{id}} | |
170 | {{/if~}} | |
171 | Datastore: {{job.store}} | |
172 | Tape Pool: {{job.pool}} | |
173 | Tape Drive: {{job.drive}} | |
174 | ||
4ca3f0c6 DC |
175 | {{#if snapshot-list ~}} |
176 | Snapshots included: | |
8703a68a | 177 | |
4ca3f0c6 DC |
178 | {{#each snapshot-list~}} |
179 | {{this}} | |
180 | {{/each~}} | |
181 | {{/if}} | |
36156038 DC |
182 | {{#if used-tapes }} |
183 | Used Tapes: | |
184 | {{#each used-tapes~}} | |
185 | {{this}} | |
186 | {{/each~}} | |
187 | {{/if}} | |
8703a68a DC |
188 | Tape Backup failed: {{error}} |
189 | ||
190 | ||
d1d74c43 | 191 | Please visit the web interface for further details: |
8703a68a DC |
192 | |
193 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
194 | ||
195 | "###; | |
86d60245 | 196 | |
9e8daa1d SS |
197 | const ACME_CERTIFICATE_ERR_RENEWAL: &str = r###" |
198 | ||
199 | Proxmox Backup Server was not able to renew a TLS certificate. | |
200 | ||
201 | Error: {{error}} | |
202 | ||
203 | Please visit the web interface for further details: | |
204 | ||
205 | <https://{{fqdn}}:{{port}}/#pbsCertificateConfiguration> | |
206 | ||
207 | "###; | |
208 | ||
ee0ea735 | 209 | lazy_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)] | |
250 | pub 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 | 259 | fn 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 | ||
286 | pub 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 | ||
338 | pub 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 |
387 | pub 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 |
433 | pub 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 |
473 | pub 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 | 501 | fn 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 | 519 | pub 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 |
542 | pub 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 | 569 | pub 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 | 580 | pub 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(¬ify_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 | ||
617 | fn 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 | ||
634 | fn 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] | |
659 | fn 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 | } |