]>
Commit | Line | Data |
---|---|---|
b9e7bcc2 DM |
1 | use anyhow::Error; |
2 | use serde_json::json; | |
3 | ||
4 | use handlebars::{Handlebars, Helper, Context, RenderError, RenderContext, Output, HelperResult}; | |
5 | ||
6 | use proxmox::tools::email::sendmail; | |
c26c9390 | 7 | use proxmox::api::schema::parse_property_string; |
b9e7bcc2 DM |
8 | |
9 | use crate::{ | |
f47c1d3a | 10 | config::datastore::DataStoreConfig, |
b9e7bcc2 | 11 | config::verify::VerificationJobConfig, |
9e733dae | 12 | config::sync::SyncJobConfig, |
b9e7bcc2 | 13 | api2::types::{ |
86d60245 | 14 | APTUpdateInfo, |
b9e7bcc2 | 15 | GarbageCollectionStatus, |
86d60245 | 16 | Userid, |
f47c1d3a | 17 | Notify, |
c26c9390 | 18 | DatastoreNotify, |
b9e7bcc2 DM |
19 | }, |
20 | tools::format::HumanByte, | |
21 | }; | |
22 | ||
23 | const GC_OK_TEMPLATE: &str = r###" | |
24 | ||
d6373f35 DM |
25 | Datastore: {{datastore}} |
26 | Task ID: {{status.upid}} | |
27 | Index file count: {{status.index-file-count}} | |
b9e7bcc2 | 28 | |
d6373f35 DM |
29 | Removed garbage: {{human-bytes status.removed-bytes}} |
30 | Removed chunks: {{status.removed-chunks}} | |
1143f6ca | 31 | Removed bad chunks: {{status.removed-bad}} |
b9e7bcc2 | 32 | |
1143f6ca | 33 | Leftover bad chunks: {{status.still-bad}} |
d6373f35 | 34 | Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks) |
b9e7bcc2 | 35 | |
d6373f35 | 36 | Original Data usage: {{human-bytes status.index-data-bytes}} |
1143f6ca DM |
37 | On-Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}}) |
38 | On-Disk chunks: {{status.disk-chunks}} | |
d6373f35 DM |
39 | |
40 | Deduplication Factor: {{deduplication-factor}} | |
b9e7bcc2 DM |
41 | |
42 | Garbage collection successful. | |
43 | ||
3066f564 DM |
44 | |
45 | Please visit the web interface for futher details: | |
46 | ||
47 | <https://{{fqdn}}:{{port}}/#DataStore-{{datastore}}> | |
48 | ||
b9e7bcc2 DM |
49 | "###; |
50 | ||
51 | ||
52 | const GC_ERR_TEMPLATE: &str = r###" | |
53 | ||
54 | Datastore: {{datastore}} | |
55 | ||
56 | Garbage collection failed: {{error}} | |
57 | ||
3066f564 DM |
58 | |
59 | Please visit the web interface for futher details: | |
60 | ||
61 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
62 | ||
b9e7bcc2 DM |
63 | "###; |
64 | ||
65 | const VERIFY_OK_TEMPLATE: &str = r###" | |
66 | ||
67 | Job ID: {{job.id}} | |
68 | Datastore: {{job.store}} | |
69 | ||
70 | Verification successful. | |
71 | ||
3066f564 DM |
72 | |
73 | Please visit the web interface for futher details: | |
74 | ||
75 | <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}> | |
76 | ||
b9e7bcc2 DM |
77 | "###; |
78 | ||
79 | const VERIFY_ERR_TEMPLATE: &str = r###" | |
80 | ||
81 | Job ID: {{job.id}} | |
82 | Datastore: {{job.store}} | |
83 | ||
a4915dfc DM |
84 | Verification failed on these snapshots: |
85 | ||
86 | {{#each errors}} | |
07ca4e36 | 87 | {{this~}} |
a4915dfc | 88 | {{/each}} |
b9e7bcc2 | 89 | |
3066f564 DM |
90 | |
91 | Please visit the web interface for futher details: | |
92 | ||
93 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
94 | ||
b9e7bcc2 DM |
95 | "###; |
96 | ||
9e733dae DM |
97 | const SYNC_OK_TEMPLATE: &str = r###" |
98 | ||
99 | Job ID: {{job.id}} | |
100 | Datastore: {{job.store}} | |
101 | Remote: {{job.remote}} | |
102 | Remote Store: {{job.remote-store}} | |
103 | ||
104 | Synchronization successful. | |
105 | ||
3066f564 DM |
106 | |
107 | Please visit the web interface for futher details: | |
108 | ||
109 | <https://{{fqdn}}:{{port}}/#DataStore-{{job.store}}> | |
110 | ||
9e733dae DM |
111 | "###; |
112 | ||
113 | const SYNC_ERR_TEMPLATE: &str = r###" | |
114 | ||
115 | Job ID: {{job.id}} | |
116 | Datastore: {{job.store}} | |
117 | Remote: {{job.remote}} | |
118 | Remote Store: {{job.remote-store}} | |
119 | ||
120 | Synchronization failed: {{error}} | |
121 | ||
3066f564 DM |
122 | |
123 | Please visit the web interface for futher details: | |
124 | ||
125 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:tasks> | |
126 | ||
9e733dae DM |
127 | "###; |
128 | ||
86d60245 TL |
129 | const PACKAGE_UPDATES_TEMPLATE: &str = r###" |
130 | Proxmox Backup Server has the following updates available: | |
131 | {{#each updates }} | |
132 | {{Package}}: {{OldVersion}} -> {{Version~}} | |
133 | {{/each }} | |
134 | ||
3066f564 DM |
135 | To upgrade visit the web interface: |
136 | ||
137 | <https://{{fqdn}}:{{port}}/#pbsServerAdministration:updates> | |
138 | ||
86d60245 TL |
139 | "###; |
140 | ||
141 | ||
b9e7bcc2 DM |
142 | lazy_static::lazy_static!{ |
143 | ||
144 | static ref HANDLEBARS: Handlebars<'static> = { | |
145 | let mut hb = Handlebars::new(); | |
146 | ||
147 | hb.set_strict_mode(true); | |
148 | ||
149 | hb.register_helper("human-bytes", Box::new(handlebars_humam_bytes_helper)); | |
150 | hb.register_helper("relative-percentage", Box::new(handlebars_relative_percentage_helper)); | |
151 | ||
152 | hb.register_template_string("gc_ok_template", GC_OK_TEMPLATE).unwrap(); | |
153 | hb.register_template_string("gc_err_template", GC_ERR_TEMPLATE).unwrap(); | |
154 | ||
155 | hb.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE).unwrap(); | |
156 | hb.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE).unwrap(); | |
157 | ||
9e733dae DM |
158 | hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE).unwrap(); |
159 | hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE).unwrap(); | |
160 | ||
86d60245 TL |
161 | hb.register_template_string("package_update_template", PACKAGE_UPDATES_TEMPLATE).unwrap(); |
162 | ||
b9e7bcc2 DM |
163 | hb |
164 | }; | |
165 | } | |
166 | ||
167 | fn send_job_status_mail( | |
168 | email: &str, | |
169 | subject: &str, | |
170 | text: &str, | |
171 | ) -> Result<(), Error> { | |
172 | ||
173 | // Note: OX has serious problems displaying text mails, | |
174 | // so we include html as well | |
385cf2bd | 175 | let html = format!("<html><body><pre>\n{}\n<pre>", handlebars::html_escape(text)); |
b9e7bcc2 DM |
176 | |
177 | let nodename = proxmox::tools::nodename(); | |
178 | ||
179 | let author = format!("Proxmox Backup Server - {}", nodename); | |
180 | ||
181 | sendmail( | |
182 | &[email], | |
183 | &subject, | |
184 | Some(&text), | |
185 | Some(&html), | |
186 | None, | |
187 | Some(&author), | |
188 | )?; | |
189 | ||
190 | Ok(()) | |
191 | } | |
192 | ||
193 | pub fn send_gc_status( | |
194 | email: &str, | |
c26c9390 | 195 | notify: DatastoreNotify, |
b9e7bcc2 DM |
196 | datastore: &str, |
197 | status: &GarbageCollectionStatus, | |
198 | result: &Result<(), Error>, | |
199 | ) -> Result<(), Error> { | |
200 | ||
c26c9390 DM |
201 | match notify.gc { |
202 | None => { /* send notifications by default */ }, | |
203 | Some(notify) => { | |
204 | if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) { | |
205 | return Ok(()); | |
206 | } | |
207 | } | |
f47c1d3a DM |
208 | } |
209 | ||
3066f564 DM |
210 | let (fqdn, port) = get_server_url(); |
211 | let mut data = json!({ | |
212 | "datastore": datastore, | |
213 | "fqdn": fqdn, | |
214 | "port": port, | |
215 | }); | |
216 | ||
b9e7bcc2 DM |
217 | let text = match result { |
218 | Ok(()) => { | |
d6373f35 DM |
219 | let deduplication_factor = if status.disk_bytes > 0 { |
220 | (status.index_data_bytes as f64)/(status.disk_bytes as f64) | |
221 | } else { | |
222 | 1.0 | |
223 | }; | |
224 | ||
3066f564 DM |
225 | data["status"] = json!(status); |
226 | data["deduplication-factor"] = format!("{:.2}", deduplication_factor).into(); | |
d6373f35 | 227 | |
b9e7bcc2 DM |
228 | HANDLEBARS.render("gc_ok_template", &data)? |
229 | } | |
230 | Err(err) => { | |
3066f564 | 231 | data["error"] = err.to_string().into(); |
b9e7bcc2 DM |
232 | HANDLEBARS.render("gc_err_template", &data)? |
233 | } | |
234 | }; | |
235 | ||
236 | let subject = match result { | |
237 | Ok(()) => format!( | |
238 | "Garbage Collect Datastore '{}' successful", | |
239 | datastore, | |
240 | ), | |
241 | Err(_) => format!( | |
242 | "Garbage Collect Datastore '{}' failed", | |
243 | datastore, | |
244 | ), | |
245 | }; | |
246 | ||
247 | send_job_status_mail(email, &subject, &text)?; | |
248 | ||
249 | Ok(()) | |
250 | } | |
251 | ||
252 | pub fn send_verify_status( | |
253 | email: &str, | |
c26c9390 | 254 | notify: DatastoreNotify, |
b9e7bcc2 | 255 | job: VerificationJobConfig, |
a4915dfc | 256 | result: &Result<Vec<String>, Error>, |
b9e7bcc2 DM |
257 | ) -> Result<(), Error> { |
258 | ||
3066f564 DM |
259 | let (fqdn, port) = get_server_url(); |
260 | let mut data = json!({ | |
261 | "job": job, | |
262 | "fqdn": fqdn, | |
263 | "port": port, | |
264 | }); | |
b9e7bcc2 | 265 | |
c26c9390 DM |
266 | let mut result_is_ok = false; |
267 | ||
b9e7bcc2 | 268 | let text = match result { |
a4915dfc | 269 | Ok(errors) if errors.is_empty() => { |
c26c9390 | 270 | result_is_ok = true; |
b9e7bcc2 DM |
271 | HANDLEBARS.render("verify_ok_template", &data)? |
272 | } | |
a4915dfc | 273 | Ok(errors) => { |
3066f564 | 274 | data["errors"] = json!(errors); |
b9e7bcc2 DM |
275 | HANDLEBARS.render("verify_err_template", &data)? |
276 | } | |
a4915dfc | 277 | Err(_) => { |
d0abba33 | 278 | // aborted job - do not send any email |
a4915dfc DM |
279 | return Ok(()); |
280 | } | |
b9e7bcc2 DM |
281 | }; |
282 | ||
c26c9390 DM |
283 | match notify.verify { |
284 | None => { /* send notifications by default */ }, | |
285 | Some(notify) => { | |
286 | if notify == Notify::Never || (result_is_ok && notify == Notify::Error) { | |
287 | return Ok(()); | |
288 | } | |
289 | } | |
290 | } | |
291 | ||
b9e7bcc2 | 292 | let subject = match result { |
a4915dfc | 293 | Ok(errors) if errors.is_empty() => format!( |
b9e7bcc2 DM |
294 | "Verify Datastore '{}' successful", |
295 | job.store, | |
296 | ), | |
a4915dfc | 297 | _ => format!( |
b9e7bcc2 DM |
298 | "Verify Datastore '{}' failed", |
299 | job.store, | |
300 | ), | |
301 | }; | |
302 | ||
303 | send_job_status_mail(email, &subject, &text)?; | |
304 | ||
305 | Ok(()) | |
306 | } | |
307 | ||
9e733dae DM |
308 | pub fn send_sync_status( |
309 | email: &str, | |
c26c9390 | 310 | notify: DatastoreNotify, |
9e733dae DM |
311 | job: &SyncJobConfig, |
312 | result: &Result<(), Error>, | |
313 | ) -> Result<(), Error> { | |
314 | ||
c26c9390 DM |
315 | match notify.sync { |
316 | None => { /* send notifications by default */ }, | |
317 | Some(notify) => { | |
318 | if notify == Notify::Never || (result.is_ok() && notify == Notify::Error) { | |
319 | return Ok(()); | |
320 | } | |
321 | } | |
f47c1d3a DM |
322 | } |
323 | ||
3066f564 DM |
324 | let (fqdn, port) = get_server_url(); |
325 | let mut data = json!({ | |
326 | "job": job, | |
327 | "fqdn": fqdn, | |
328 | "port": port, | |
329 | }); | |
330 | ||
9e733dae DM |
331 | let text = match result { |
332 | Ok(()) => { | |
9e733dae DM |
333 | HANDLEBARS.render("sync_ok_template", &data)? |
334 | } | |
335 | Err(err) => { | |
3066f564 | 336 | data["error"] = err.to_string().into(); |
9e733dae DM |
337 | HANDLEBARS.render("sync_err_template", &data)? |
338 | } | |
339 | }; | |
340 | ||
341 | let subject = match result { | |
342 | Ok(()) => format!( | |
343 | "Sync remote '{}' datastore '{}' successful", | |
344 | job.remote, | |
345 | job.remote_store, | |
346 | ), | |
347 | Err(_) => format!( | |
348 | "Sync remote '{}' datastore '{}' failed", | |
349 | job.remote, | |
350 | job.remote_store, | |
351 | ), | |
352 | }; | |
353 | ||
354 | send_job_status_mail(email, &subject, &text)?; | |
355 | ||
356 | Ok(()) | |
357 | } | |
358 | ||
3066f564 DM |
359 | fn get_server_url() -> (String, usize) { |
360 | ||
361 | // user will surely request that they can change this | |
362 | ||
363 | let nodename = proxmox::tools::nodename(); | |
364 | let mut fqdn = nodename.to_owned(); | |
365 | ||
366 | if let Ok(resolv_conf) = crate::api2::node::dns::read_etc_resolv_conf() { | |
367 | if let Some(search) = resolv_conf["search"].as_str() { | |
368 | fqdn.push('.'); | |
369 | fqdn.push_str(search); | |
370 | } | |
371 | } | |
372 | ||
373 | let port = 8007; | |
374 | ||
375 | (fqdn, port) | |
376 | } | |
377 | ||
86d60245 TL |
378 | pub fn send_updates_available( |
379 | updates: &Vec<&APTUpdateInfo>, | |
380 | ) -> Result<(), Error> { | |
381 | // update mails always go to the root@pam configured email.. | |
382 | if let Some(email) = lookup_user_email(Userid::root_userid()) { | |
383 | let nodename = proxmox::tools::nodename(); | |
384 | let subject = format!("New software packages available ({})", nodename); | |
385 | ||
3066f564 DM |
386 | let (fqdn, port) = get_server_url(); |
387 | ||
86d60245 | 388 | let text = HANDLEBARS.render("package_update_template", &json!({ |
3066f564 DM |
389 | "fqdn": fqdn, |
390 | "port": port, | |
86d60245 TL |
391 | "updates": updates, |
392 | }))?; | |
393 | ||
394 | send_job_status_mail(&email, &subject, &text)?; | |
395 | } | |
396 | Ok(()) | |
397 | } | |
398 | ||
b9e7bcc2 | 399 | /// Lookup users email address |
f47c1d3a | 400 | fn lookup_user_email(userid: &Userid) -> Option<String> { |
b9e7bcc2 DM |
401 | |
402 | use crate::config::user::{self, User}; | |
403 | ||
b9e7bcc2 DM |
404 | if let Ok(user_config) = user::cached_config() { |
405 | if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) { | |
406 | return user.email.clone(); | |
407 | } | |
408 | } | |
409 | ||
410 | None | |
411 | } | |
412 | ||
f47c1d3a DM |
413 | /// Lookup Datastore notify settings |
414 | pub fn lookup_datastore_notify_settings( | |
415 | store: &str, | |
c26c9390 | 416 | ) -> (Option<String>, DatastoreNotify) { |
f47c1d3a | 417 | |
f47c1d3a DM |
418 | let mut email = None; |
419 | ||
c26c9390 DM |
420 | let notify = DatastoreNotify { gc: None, verify: None, sync: None }; |
421 | ||
f47c1d3a DM |
422 | let (config, _digest) = match crate::config::datastore::config() { |
423 | Ok(result) => result, | |
424 | Err(_) => return (email, notify), | |
425 | }; | |
426 | ||
427 | let config: DataStoreConfig = match config.lookup("datastore", store) { | |
428 | Ok(result) => result, | |
429 | Err(_) => return (email, notify), | |
430 | }; | |
431 | ||
432 | email = match config.notify_user { | |
433 | Some(ref userid) => lookup_user_email(userid), | |
ad54df31 | 434 | None => lookup_user_email(Userid::root_userid()), |
f47c1d3a DM |
435 | }; |
436 | ||
c26c9390 DM |
437 | let notify_str = config.notify.unwrap_or(String::new()); |
438 | ||
439 | if let Ok(value) = parse_property_string(¬ify_str, &DatastoreNotify::API_SCHEMA) { | |
440 | if let Ok(notify) = serde_json::from_value(value) { | |
441 | return (email, notify); | |
442 | } | |
f47c1d3a DM |
443 | } |
444 | ||
445 | (email, notify) | |
446 | } | |
447 | ||
b9e7bcc2 DM |
448 | // Handlerbar helper functions |
449 | ||
450 | fn handlebars_humam_bytes_helper( | |
451 | h: &Helper, | |
452 | _: &Handlebars, | |
453 | _: &Context, | |
454 | _rc: &mut RenderContext, | |
455 | out: &mut dyn Output | |
456 | ) -> HelperResult { | |
457 | let param = h.param(0).map(|v| v.value().as_u64()) | |
458 | .flatten() | |
459 | .ok_or(RenderError::new("human-bytes: param not found"))?; | |
460 | ||
461 | out.write(&HumanByte::from(param).to_string())?; | |
462 | ||
463 | Ok(()) | |
464 | } | |
465 | ||
466 | fn handlebars_relative_percentage_helper( | |
467 | h: &Helper, | |
468 | _: &Handlebars, | |
469 | _: &Context, | |
470 | _rc: &mut RenderContext, | |
471 | out: &mut dyn Output | |
472 | ) -> HelperResult { | |
473 | let param0 = h.param(0).map(|v| v.value().as_f64()) | |
474 | .flatten() | |
475 | .ok_or(RenderError::new("relative-percentage: param0 not found"))?; | |
476 | let param1 = h.param(1).map(|v| v.value().as_f64()) | |
477 | .flatten() | |
478 | .ok_or(RenderError::new("relative-percentage: param1 not found"))?; | |
479 | ||
480 | if param1 == 0.0 { | |
481 | out.write("-")?; | |
482 | } else { | |
483 | out.write(&format!("{:.2}%", (param0*100.0)/param1))?; | |
484 | } | |
485 | Ok(()) | |
486 | } |