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