]> git.proxmox.com Git - proxmox-backup.git/blob - src/server/email_notifications.rs
proxy: split out code to run garbage collection job
[proxmox-backup.git] / src / server / email_notifications.rs
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;
7
8 use crate::{
9 config::verify::VerificationJobConfig,
10 config::sync::SyncJobConfig,
11 api2::types::{
12 Userid,
13 GarbageCollectionStatus,
14 },
15 tools::format::HumanByte,
16 };
17
18 const GC_OK_TEMPLATE: &str = r###"
19
20 Datastore: {{datastore}}
21 Task ID: {{status.upid}}
22 Index file count: {{status.index-file-count}}
23
24 Removed garbage: {{human-bytes status.removed-bytes}}
25 Removed chunks: {{status.removed-chunks}}
26 Remove bad files: {{status.removed-bad}}
27
28 Bad files: {{status.still-bad}}
29 Pending removals: {{human-bytes status.pending-bytes}} (in {{status.pending-chunks}} chunks)
30
31 Original Data usage: {{human-bytes status.index-data-bytes}}
32 On Disk usage: {{human-bytes status.disk-bytes}} ({{relative-percentage status.disk-bytes status.index-data-bytes}})
33 On Disk chunks: {{status.disk-chunks}}
34
35 Deduplication Factor: {{deduplication-factor}}
36
37 Garbage collection successful.
38
39 "###;
40
41
42 const GC_ERR_TEMPLATE: &str = r###"
43
44 Datastore: {{datastore}}
45
46 Garbage collection failed: {{error}}
47
48 "###;
49
50 const VERIFY_OK_TEMPLATE: &str = r###"
51
52 Job ID: {{job.id}}
53 Datastore: {{job.store}}
54
55 Verification successful.
56
57 "###;
58
59 const VERIFY_ERR_TEMPLATE: &str = r###"
60
61 Job ID: {{job.id}}
62 Datastore: {{job.store}}
63
64 Verification failed on these snapshots:
65
66 {{#each errors}}
67 {{this}}
68 {{/each}}
69
70 "###;
71
72 const SYNC_OK_TEMPLATE: &str = r###"
73
74 Job ID: {{job.id}}
75 Datastore: {{job.store}}
76 Remote: {{job.remote}}
77 Remote Store: {{job.remote-store}}
78
79 Synchronization successful.
80
81 "###;
82
83 const SYNC_ERR_TEMPLATE: &str = r###"
84
85 Job ID: {{job.id}}
86 Datastore: {{job.store}}
87 Remote: {{job.remote}}
88 Remote Store: {{job.remote-store}}
89
90 Synchronization failed: {{error}}
91
92 "###;
93
94 lazy_static::lazy_static!{
95
96 static ref HANDLEBARS: Handlebars<'static> = {
97 let mut hb = Handlebars::new();
98
99 hb.set_strict_mode(true);
100
101 hb.register_helper("human-bytes", Box::new(handlebars_humam_bytes_helper));
102 hb.register_helper("relative-percentage", Box::new(handlebars_relative_percentage_helper));
103
104 hb.register_template_string("gc_ok_template", GC_OK_TEMPLATE).unwrap();
105 hb.register_template_string("gc_err_template", GC_ERR_TEMPLATE).unwrap();
106
107 hb.register_template_string("verify_ok_template", VERIFY_OK_TEMPLATE).unwrap();
108 hb.register_template_string("verify_err_template", VERIFY_ERR_TEMPLATE).unwrap();
109
110 hb.register_template_string("sync_ok_template", SYNC_OK_TEMPLATE).unwrap();
111 hb.register_template_string("sync_err_template", SYNC_ERR_TEMPLATE).unwrap();
112
113 hb
114 };
115 }
116
117 fn send_job_status_mail(
118 email: &str,
119 subject: &str,
120 text: &str,
121 ) -> Result<(), Error> {
122
123 // Note: OX has serious problems displaying text mails,
124 // so we include html as well
125 let html = format!("<html><body><pre>\n{}\n<pre>", handlebars::html_escape(text));
126
127 let nodename = proxmox::tools::nodename();
128
129 let author = format!("Proxmox Backup Server - {}", nodename);
130
131 sendmail(
132 &[email],
133 &subject,
134 Some(&text),
135 Some(&html),
136 None,
137 Some(&author),
138 )?;
139
140 Ok(())
141 }
142
143 pub fn send_gc_status(
144 email: &str,
145 datastore: &str,
146 status: &GarbageCollectionStatus,
147 result: &Result<(), Error>,
148 ) -> Result<(), Error> {
149
150 let text = match result {
151 Ok(()) => {
152 let deduplication_factor = if status.disk_bytes > 0 {
153 (status.index_data_bytes as f64)/(status.disk_bytes as f64)
154 } else {
155 1.0
156 };
157
158 let data = json!({
159 "status": status,
160 "datastore": datastore,
161 "deduplication-factor": format!("{:.2}", deduplication_factor),
162 });
163
164 HANDLEBARS.render("gc_ok_template", &data)?
165 }
166 Err(err) => {
167 let data = json!({
168 "error": err.to_string(),
169 "datastore": datastore,
170 });
171 HANDLEBARS.render("gc_err_template", &data)?
172 }
173 };
174
175 let subject = match result {
176 Ok(()) => format!(
177 "Garbage Collect Datastore '{}' successful",
178 datastore,
179 ),
180 Err(_) => format!(
181 "Garbage Collect Datastore '{}' failed",
182 datastore,
183 ),
184 };
185
186 send_job_status_mail(email, &subject, &text)?;
187
188 Ok(())
189 }
190
191 pub fn send_verify_status(
192 email: &str,
193 job: VerificationJobConfig,
194 result: &Result<Vec<String>, Error>,
195 ) -> Result<(), Error> {
196
197
198 let text = match result {
199 Ok(errors) if errors.is_empty() => {
200 let data = json!({ "job": job });
201 HANDLEBARS.render("verify_ok_template", &data)?
202 }
203 Ok(errors) => {
204 let data = json!({ "job": job, "errors": errors });
205 HANDLEBARS.render("verify_err_template", &data)?
206 }
207 Err(_) => {
208 // aboreted job - do not send any email
209 return Ok(());
210 }
211 };
212
213 let subject = match result {
214 Ok(errors) if errors.is_empty() => format!(
215 "Verify Datastore '{}' successful",
216 job.store,
217 ),
218 _ => format!(
219 "Verify Datastore '{}' failed",
220 job.store,
221 ),
222 };
223
224 send_job_status_mail(email, &subject, &text)?;
225
226 Ok(())
227 }
228
229 pub fn send_sync_status(
230 email: &str,
231 job: &SyncJobConfig,
232 result: &Result<(), Error>,
233 ) -> Result<(), Error> {
234
235 let text = match result {
236 Ok(()) => {
237 let data = json!({ "job": job });
238 HANDLEBARS.render("sync_ok_template", &data)?
239 }
240 Err(err) => {
241 let data = json!({ "job": job, "error": err.to_string() });
242 HANDLEBARS.render("sync_err_template", &data)?
243 }
244 };
245
246 let subject = match result {
247 Ok(()) => format!(
248 "Sync remote '{}' datastore '{}' successful",
249 job.remote,
250 job.remote_store,
251 ),
252 Err(_) => format!(
253 "Sync remote '{}' datastore '{}' failed",
254 job.remote,
255 job.remote_store,
256 ),
257 };
258
259 send_job_status_mail(email, &subject, &text)?;
260
261 Ok(())
262 }
263
264 /// Lookup users email address
265 ///
266 /// For "backup@pam", this returns the address from "root@pam".
267 pub fn lookup_user_email(userid: &Userid) -> Option<String> {
268
269 use crate::config::user::{self, User};
270
271 if userid == Userid::backup_userid() {
272 return lookup_user_email(Userid::root_userid());
273 }
274
275 if let Ok(user_config) = user::cached_config() {
276 if let Ok(user) = user_config.lookup::<User>("user", userid.as_str()) {
277 return user.email.clone();
278 }
279 }
280
281 None
282 }
283
284 // Handlerbar helper functions
285
286 fn handlebars_humam_bytes_helper(
287 h: &Helper,
288 _: &Handlebars,
289 _: &Context,
290 _rc: &mut RenderContext,
291 out: &mut dyn Output
292 ) -> HelperResult {
293 let param = h.param(0).map(|v| v.value().as_u64())
294 .flatten()
295 .ok_or(RenderError::new("human-bytes: param not found"))?;
296
297 out.write(&HumanByte::from(param).to_string())?;
298
299 Ok(())
300 }
301
302 fn handlebars_relative_percentage_helper(
303 h: &Helper,
304 _: &Handlebars,
305 _: &Context,
306 _rc: &mut RenderContext,
307 out: &mut dyn Output
308 ) -> HelperResult {
309 let param0 = h.param(0).map(|v| v.value().as_f64())
310 .flatten()
311 .ok_or(RenderError::new("relative-percentage: param0 not found"))?;
312 let param1 = h.param(1).map(|v| v.value().as_f64())
313 .flatten()
314 .ok_or(RenderError::new("relative-percentage: param1 not found"))?;
315
316 if param1 == 0.0 {
317 out.write("-")?;
318 } else {
319 out.write(&format!("{:.2}%", (param0*100.0)/param1))?;
320 }
321 Ok(())
322 }