]>
Commit | Line | Data |
---|---|---|
b9e7bcc2 | 1 | use anyhow::Error; |
57570bda | 2 | use const_format::concatcp; |
04f35d0e | 3 | use serde_json::json; |
57570bda LW |
4 | use std::collections::HashMap; |
5 | use std::path::Path; | |
6 | use std::time::{Duration, Instant}; | |
b9e7bcc2 | 7 | |
04f35d0e | 8 | use handlebars::{Handlebars, TemplateError}; |
57570bda | 9 | use nix::unistd::Uid; |
b9e7bcc2 | 10 | |
6ef1b649 | 11 | use proxmox_lang::try_block; |
57570bda | 12 | use proxmox_notify::context::pbs::PBS_CONTEXT; |
9fa3026a | 13 | use proxmox_schema::ApiType; |
ee0ea735 | 14 | use proxmox_sys::email::sendmail; |
57570bda | 15 | use proxmox_sys::fs::{create_path, CreateOptions}; |
b9e7bcc2 | 16 | |
e3619d41 | 17 | use pbs_api_types::{ |
04f35d0e LW |
18 | APTUpdateInfo, DataStoreConfig, DatastoreNotify, GarbageCollectionStatus, NotificationMode, |
19 | Notify, SyncJobConfig, TapeBackupJobSetup, User, Userid, VerificationJobConfig, | |
b9e7bcc2 | 20 | }; |
04f35d0e LW |
21 | use proxmox_notify::endpoints::sendmail::{SendmailConfig, SendmailEndpoint}; |
22 | use proxmox_notify::{Endpoint, Notification, Severity}; | |
b9e7bcc2 | 23 | |
57570bda | 24 | const SPOOL_DIR: &str = concatcp!(pbs_buildcfg::PROXMOX_BACKUP_STATE_DIR, "/notifications"); |
b9e7bcc2 | 25 | |
86d60245 TL |
26 | const PACKAGE_UPDATES_TEMPLATE: &str = r###" |
27 | Proxmox Backup Server has the following updates available: | |
28 | {{#each updates }} | |
29 | {{Package}}: {{OldVersion}} -> {{Version~}} | |
30 | {{/each }} | |
31 | ||
3066f564 DM |
32 | To upgrade visit the web interface: |
33 | ||
34 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates> | |
35 | ||
86d60245 TL |
36 | "###; |
37 | ||
8703a68a DC |
38 | const TAPE_BACKUP_OK_TEMPLATE: &str = r###" |
39 | ||
40 | {{#if id ~}} | |
41 | Job ID: {{id}} | |
42 | {{/if~}} | |
43 | Datastore: {{job.store}} | |
44 | Tape Pool: {{job.pool}} | |
45 | Tape Drive: {{job.drive}} | |
46 | ||
4abd4dbe DC |
47 | {{#if snapshot-list ~}} |
48 | Snapshots included: | |
49 | ||
50 | {{#each snapshot-list~}} | |
51 | {{this}} | |
52 | {{/each~}} | |
53 | {{/if}} | |
54 | Duration: {{duration}} | |
36156038 DC |
55 | {{#if used-tapes }} |
56 | Used Tapes: | |
57 | {{#each used-tapes~}} | |
58 | {{this}} | |
59 | {{/each~}} | |
60 | {{/if}} | |
8703a68a DC |
61 | Tape Backup successful. |
62 | ||
63 | ||
d1d74c43 | 64 | Please visit the web interface for further details: |
8703a68a DC |
65 | |
66 | <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}> | |
67 | ||
68 | "###; | |
69 | ||
70 | const TAPE_BACKUP_ERR_TEMPLATE: &str = r###" | |
71 | ||
72 | {{#if id ~}} | |
73 | Job ID: {{id}} | |
74 | {{/if~}} | |
75 | Datastore: {{job.store}} | |
76 | Tape Pool: {{job.pool}} | |
77 | Tape Drive: {{job.drive}} | |
78 | ||
4ca3f0c6 DC |
79 | {{#if snapshot-list ~}} |
80 | Snapshots included: | |
8703a68a | 81 | |
4ca3f0c6 DC |
82 | {{#each snapshot-list~}} |
83 | {{this}} | |
84 | {{/each~}} | |
85 | {{/if}} | |
36156038 DC |
86 | {{#if used-tapes }} |
87 | Used Tapes: | |
88 | {{#each used-tapes~}} | |
89 | {{this}} | |
90 | {{/each~}} | |
91 | {{/if}} | |
8703a68a DC |
92 | Tape Backup failed: {{error}} |
93 | ||
94 | ||
d1d74c43 | 95 | Please visit the web interface for further details: |
8703a68a DC |
96 | |
97 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
98 | ||
99 | "###; | |
86d60245 | 100 | |
9e8daa1d SS |
101 | const ACME_CERTIFICATE_ERR_RENEWAL: &str = r###" |
102 | ||
103 | Proxmox Backup Server was not able to renew a TLS certificate. | |
104 | ||
105 | Error: {{error}} | |
106 | ||
107 | Please visit the web interface for further details: | |
108 | ||
109 | <https://{{fqdn}}:{{port}}/#pbsCertificateConfiguration> | |
110 | ||
111 | "###; | |
112 | ||
ee0ea735 | 113 | lazy_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 |
141 | pub 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. | |
148 | pub 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 | ||
158 | async 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, ¬ification) { | |
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. | |
205 | pub 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 | ||
217 | fn 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, ¬ification)?; | |
221 | } else { | |
222 | let ser = serde_json::to_vec(¬ification)?; | |
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 |
236 | fn 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(¬ification)?; | |
245 | ||
246 | Ok(()) | |
247 | } | |
248 | ||
4abd4dbe DC |
249 | /// Summary of a successful Tape Job |
250 | #[derive(Default)] | |
251 | pub 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 | 260 | fn 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 | ||
284 | pub 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 | ||
343 | pub 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 |
396 | pub 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 | 447 | pub 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 |
493 | pub 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 |
533 | pub 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 | 567 | fn 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 | 585 | pub 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 |
608 | pub 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 | 635 | pub 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 |
646 | pub 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(¬ify_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] | |
686 | fn 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 | } |