]> git.proxmox.com Git - proxmox-perl-rs.git/blob - pve-rs/src/notify.rs
notify: implement context for getting default author/mailfrom
[proxmox-perl-rs.git] / pve-rs / src / notify.rs
1 use std::path::Path;
2
3 use log;
4
5 use proxmox_notify::context::Context;
6
7 // Some helpers borrowed and slightly adapted from `proxmox-mail-forward`
8
9 fn normalize_for_return(s: Option<&str>) -> Option<String> {
10 match s?.trim() {
11 "" => None,
12 s => Some(s.to_string()),
13 }
14 }
15
16 fn attempt_file_read<P: AsRef<Path>>(path: P) -> Option<String> {
17 match proxmox_sys::fs::file_read_optional_string(path) {
18 Ok(contents) => contents,
19 Err(err) => {
20 log::error!("{err}");
21 None
22 }
23 }
24 }
25
26 fn lookup_mail_address(content: &str, user: &str) -> Option<String> {
27 normalize_for_return(content.lines().find_map(|line| {
28 let fields: Vec<&str> = line.split(':').collect();
29 #[allow(clippy::get_first)] // to keep expression style consistent
30 match fields.get(0)?.trim() == "user" && fields.get(1)?.trim() == user {
31 true => fields.get(6).copied(),
32 false => None,
33 }
34 }))
35 }
36
37 fn lookup_datacenter_config_key(content: &str, key: &str) -> Option<String> {
38 let key_prefix = format!("{key}:");
39 normalize_for_return(
40 content
41 .lines()
42 .find_map(|line| line.strip_prefix(&key_prefix)),
43 )
44 }
45
46 #[derive(Debug)]
47 struct PVEContext;
48
49 impl Context for PVEContext {
50 fn lookup_email_for_user(&self, user: &str) -> Option<String> {
51 let content = attempt_file_read("/etc/pve/user.cfg");
52 content.and_then(|content| lookup_mail_address(&content, user))
53 }
54
55 fn default_sendmail_author(&self) -> String {
56 "Proxmox VE".into()
57 }
58
59 fn default_sendmail_from(&self) -> String {
60 let content = attempt_file_read("/etc/pve/datacenter.cfg");
61 content
62 .and_then(|content| lookup_datacenter_config_key(&content, "mail_from"))
63 .unwrap_or_else(|| String::from("root"))
64 }
65 }
66
67 #[cfg(test)]
68 mod tests {
69 use crate::notify::{lookup_datacenter_config_key, lookup_mail_address};
70
71 const USER_CONFIG: &str = "
72 user:root@pam:1:0:::root@example.com:::
73 user:test@pve:1:0:::test@example.com:::
74 user:no-mail@pve:1:0::::::
75 ";
76
77 #[test]
78 fn test_parse_mail() {
79 assert_eq!(
80 lookup_mail_address(USER_CONFIG, "root@pam"),
81 Some("root@example.com".to_string())
82 );
83 assert_eq!(
84 lookup_mail_address(USER_CONFIG, "test@pve"),
85 Some("test@example.com".to_string())
86 );
87 assert_eq!(lookup_mail_address(USER_CONFIG, "no-mail@pve"), None);
88 }
89
90 const DC_CONFIG: &str = "
91 email_from: user@example.com
92 keyboard: en-us
93 ";
94 #[test]
95 fn test_parse_dc_config() {
96 assert_eq!(
97 lookup_datacenter_config_key(DC_CONFIG, "email_from"),
98 Some("user@example.com".to_string())
99 );
100 }
101 }
102
103 static CONTEXT: PVEContext = PVEContext;
104
105 pub fn init() {
106 proxmox_notify::context::set_context(&CONTEXT)
107 }
108
109 #[perlmod::package(name = "PVE::RS::Notify")]
110 mod export {
111 use anyhow::{bail, Error};
112 use perlmod::Value;
113 use serde_json::Value as JSONValue;
114 use std::sync::Mutex;
115
116 use proxmox_notify::endpoints::gotify::{
117 DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
118 GotifyPrivateConfigUpdater,
119 };
120 use proxmox_notify::endpoints::sendmail::{
121 DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater,
122 };
123 use proxmox_notify::filter::{
124 DeleteableFilterProperty, FilterConfig, FilterConfigUpdater, FilterModeOperator,
125 };
126 use proxmox_notify::group::{DeleteableGroupProperty, GroupConfig, GroupConfigUpdater};
127 use proxmox_notify::{api, api::ApiError, Config, Notification, Severity};
128
129 pub struct NotificationConfig {
130 config: Mutex<Config>,
131 }
132
133 perlmod::declare_magic!(Box<NotificationConfig> : &NotificationConfig as "PVE::RS::Notify");
134
135 /// Support `dclone` so this can be put into the `ccache` of `PVE::Cluster`.
136 #[export(name = "STORABLE_freeze", raw_return)]
137 fn storable_freeze(
138 #[try_from_ref] this: &NotificationConfig,
139 cloning: bool,
140 ) -> Result<Value, Error> {
141 if !cloning {
142 bail!("freezing Notification config not supported!");
143 }
144
145 let mut cloned = Box::new(NotificationConfig {
146 config: Mutex::new(this.config.lock().unwrap().clone()),
147 });
148 let value = Value::new_pointer::<NotificationConfig>(&mut *cloned);
149 let _perl = Box::leak(cloned);
150 Ok(value)
151 }
152
153 /// Instead of `thaw` we implement `attach` for `dclone`.
154 #[export(name = "STORABLE_attach", raw_return)]
155 fn storable_attach(
156 #[raw] class: Value,
157 cloning: bool,
158 #[raw] serialized: Value,
159 ) -> Result<Value, Error> {
160 if !cloning {
161 bail!("STORABLE_attach called with cloning=false");
162 }
163 let data = unsafe { Box::from_raw(serialized.pv_raw::<NotificationConfig>()?) };
164 Ok(perlmod::instantiate_magic!(&class, MAGIC => data))
165 }
166
167 #[export(raw_return)]
168 fn parse_config(
169 #[raw] class: Value,
170 raw_config: &[u8],
171 raw_private_config: &[u8],
172 ) -> Result<Value, Error> {
173 let raw_config = std::str::from_utf8(raw_config)?;
174 let raw_private_config = std::str::from_utf8(raw_private_config)?;
175
176 Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
177 NotificationConfig {
178 config: Mutex::new(Config::new(raw_config, raw_private_config)?)
179 }
180 )))
181 }
182
183 #[export]
184 fn write_config(#[try_from_ref] this: &NotificationConfig) -> Result<(String, String), Error> {
185 Ok(this.config.lock().unwrap().write()?)
186 }
187
188 #[export]
189 fn digest(#[try_from_ref] this: &NotificationConfig) -> String {
190 let config = this.config.lock().unwrap();
191 hex::encode(config.digest())
192 }
193
194 #[export(serialize_error)]
195 fn send(
196 #[try_from_ref] this: &NotificationConfig,
197 channel: &str,
198 severity: Severity,
199 title: String,
200 body: String,
201 properties: Option<JSONValue>,
202 ) -> Result<(), ApiError> {
203 let config = this.config.lock().unwrap();
204
205 let notification = Notification {
206 severity,
207 title,
208 body,
209 properties,
210 };
211
212 api::common::send(&config, channel, &notification)
213 }
214
215 #[export(serialize_error)]
216 fn test_target(
217 #[try_from_ref] this: &NotificationConfig,
218 target: &str,
219 ) -> Result<(), ApiError> {
220 let config = this.config.lock().unwrap();
221 api::common::test_target(&config, target)
222 }
223
224 #[export(serialize_error)]
225 fn get_groups(#[try_from_ref] this: &NotificationConfig) -> Result<Vec<GroupConfig>, ApiError> {
226 let config = this.config.lock().unwrap();
227 api::group::get_groups(&config)
228 }
229
230 #[export(serialize_error)]
231 fn get_group(
232 #[try_from_ref] this: &NotificationConfig,
233 id: &str,
234 ) -> Result<GroupConfig, ApiError> {
235 let config = this.config.lock().unwrap();
236 api::group::get_group(&config, id)
237 }
238
239 #[export(serialize_error)]
240 fn add_group(
241 #[try_from_ref] this: &NotificationConfig,
242 name: String,
243 endpoints: Vec<String>,
244 comment: Option<String>,
245 filter: Option<String>,
246 ) -> Result<(), ApiError> {
247 let mut config = this.config.lock().unwrap();
248 api::group::add_group(
249 &mut config,
250 &GroupConfig {
251 name,
252 endpoint: endpoints,
253 comment,
254 filter,
255 },
256 )
257 }
258
259 #[export(serialize_error)]
260 fn update_group(
261 #[try_from_ref] this: &NotificationConfig,
262 name: &str,
263 endpoints: Option<Vec<String>>,
264 comment: Option<String>,
265 filter: Option<String>,
266 delete: Option<Vec<DeleteableGroupProperty>>,
267 digest: Option<&str>,
268 ) -> Result<(), ApiError> {
269 let mut config = this.config.lock().unwrap();
270 let digest = digest.map(hex::decode).transpose().map_err(|e| {
271 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
272 })?;
273
274 api::group::update_group(
275 &mut config,
276 name,
277 &GroupConfigUpdater {
278 endpoint: endpoints,
279 comment,
280 filter,
281 },
282 delete.as_deref(),
283 digest.as_deref(),
284 )
285 }
286
287 #[export(serialize_error)]
288 fn delete_group(#[try_from_ref] this: &NotificationConfig, name: &str) -> Result<(), ApiError> {
289 let mut config = this.config.lock().unwrap();
290 api::group::delete_group(&mut config, name)
291 }
292
293 #[export(serialize_error)]
294 fn get_sendmail_endpoints(
295 #[try_from_ref] this: &NotificationConfig,
296 ) -> Result<Vec<SendmailConfig>, ApiError> {
297 let config = this.config.lock().unwrap();
298 api::sendmail::get_endpoints(&config)
299 }
300
301 #[export(serialize_error)]
302 fn get_sendmail_endpoint(
303 #[try_from_ref] this: &NotificationConfig,
304 id: &str,
305 ) -> Result<SendmailConfig, ApiError> {
306 let config = this.config.lock().unwrap();
307 api::sendmail::get_endpoint(&config, id)
308 }
309
310 #[export(serialize_error)]
311 #[allow(clippy::too_many_arguments)]
312 fn add_sendmail_endpoint(
313 #[try_from_ref] this: &NotificationConfig,
314 name: String,
315 mailto: Option<Vec<String>>,
316 mailto_user: Option<Vec<String>>,
317 from_address: Option<String>,
318 author: Option<String>,
319 comment: Option<String>,
320 filter: Option<String>,
321 ) -> Result<(), ApiError> {
322 let mut config = this.config.lock().unwrap();
323
324 api::sendmail::add_endpoint(
325 &mut config,
326 &SendmailConfig {
327 name,
328 mailto,
329 mailto_user,
330 from_address,
331 author,
332 comment,
333 filter,
334 },
335 )
336 }
337
338 #[export(serialize_error)]
339 #[allow(clippy::too_many_arguments)]
340 fn update_sendmail_endpoint(
341 #[try_from_ref] this: &NotificationConfig,
342 name: &str,
343 mailto: Option<Vec<String>>,
344 mailto_user: Option<Vec<String>>,
345 from_address: Option<String>,
346 author: Option<String>,
347 comment: Option<String>,
348 filter: Option<String>,
349 delete: Option<Vec<DeleteableSendmailProperty>>,
350 digest: Option<&str>,
351 ) -> Result<(), ApiError> {
352 let mut config = this.config.lock().unwrap();
353 let digest = digest.map(hex::decode).transpose().map_err(|e| {
354 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
355 })?;
356
357 api::sendmail::update_endpoint(
358 &mut config,
359 name,
360 &SendmailConfigUpdater {
361 mailto,
362 mailto_user,
363 from_address,
364 author,
365 comment,
366 filter,
367 },
368 delete.as_deref(),
369 digest.as_deref(),
370 )
371 }
372
373 #[export(serialize_error)]
374 fn delete_sendmail_endpoint(
375 #[try_from_ref] this: &NotificationConfig,
376 name: &str,
377 ) -> Result<(), ApiError> {
378 let mut config = this.config.lock().unwrap();
379 api::sendmail::delete_endpoint(&mut config, name)
380 }
381
382 #[export(serialize_error)]
383 fn get_gotify_endpoints(
384 #[try_from_ref] this: &NotificationConfig,
385 ) -> Result<Vec<GotifyConfig>, ApiError> {
386 let config = this.config.lock().unwrap();
387 api::gotify::get_endpoints(&config)
388 }
389
390 #[export(serialize_error)]
391 fn get_gotify_endpoint(
392 #[try_from_ref] this: &NotificationConfig,
393 id: &str,
394 ) -> Result<GotifyConfig, ApiError> {
395 let config = this.config.lock().unwrap();
396 api::gotify::get_endpoint(&config, id)
397 }
398
399 #[export(serialize_error)]
400 fn add_gotify_endpoint(
401 #[try_from_ref] this: &NotificationConfig,
402 name: String,
403 server: String,
404 token: String,
405 comment: Option<String>,
406 filter: Option<String>,
407 ) -> Result<(), ApiError> {
408 let mut config = this.config.lock().unwrap();
409 api::gotify::add_endpoint(
410 &mut config,
411 &GotifyConfig {
412 name: name.clone(),
413 server,
414 comment,
415 filter,
416 },
417 &GotifyPrivateConfig { name, token },
418 )
419 }
420
421 #[export(serialize_error)]
422 #[allow(clippy::too_many_arguments)]
423 fn update_gotify_endpoint(
424 #[try_from_ref] this: &NotificationConfig,
425 name: &str,
426 server: Option<String>,
427 token: Option<String>,
428 comment: Option<String>,
429 filter: Option<String>,
430 delete: Option<Vec<DeleteableGotifyProperty>>,
431 digest: Option<&str>,
432 ) -> Result<(), ApiError> {
433 let mut config = this.config.lock().unwrap();
434 let digest = digest.map(hex::decode).transpose().map_err(|e| {
435 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
436 })?;
437
438 api::gotify::update_endpoint(
439 &mut config,
440 name,
441 &GotifyConfigUpdater {
442 server,
443 comment,
444 filter,
445 },
446 &GotifyPrivateConfigUpdater { token },
447 delete.as_deref(),
448 digest.as_deref(),
449 )
450 }
451
452 #[export(serialize_error)]
453 fn delete_gotify_endpoint(
454 #[try_from_ref] this: &NotificationConfig,
455 name: &str,
456 ) -> Result<(), ApiError> {
457 let mut config = this.config.lock().unwrap();
458 api::gotify::delete_gotify_endpoint(&mut config, name)
459 }
460
461 #[export(serialize_error)]
462 fn get_filters(
463 #[try_from_ref] this: &NotificationConfig,
464 ) -> Result<Vec<FilterConfig>, ApiError> {
465 let config = this.config.lock().unwrap();
466 api::filter::get_filters(&config)
467 }
468
469 #[export(serialize_error)]
470 fn get_filter(
471 #[try_from_ref] this: &NotificationConfig,
472 id: &str,
473 ) -> Result<FilterConfig, ApiError> {
474 let config = this.config.lock().unwrap();
475 api::filter::get_filter(&config, id)
476 }
477
478 #[export(serialize_error)]
479 #[allow(clippy::too_many_arguments)]
480 fn add_filter(
481 #[try_from_ref] this: &NotificationConfig,
482 name: String,
483 min_severity: Option<Severity>,
484 mode: Option<FilterModeOperator>,
485 invert_match: Option<bool>,
486 comment: Option<String>,
487 ) -> Result<(), ApiError> {
488 let mut config = this.config.lock().unwrap();
489 api::filter::add_filter(
490 &mut config,
491 &FilterConfig {
492 name,
493 min_severity,
494 mode,
495 invert_match,
496 comment,
497 },
498 )
499 }
500
501 #[export(serialize_error)]
502 #[allow(clippy::too_many_arguments)]
503 fn update_filter(
504 #[try_from_ref] this: &NotificationConfig,
505 name: &str,
506 min_severity: Option<Severity>,
507 mode: Option<FilterModeOperator>,
508 invert_match: Option<bool>,
509 comment: Option<String>,
510 delete: Option<Vec<DeleteableFilterProperty>>,
511 digest: Option<&str>,
512 ) -> Result<(), ApiError> {
513 let mut config = this.config.lock().unwrap();
514 let digest = digest.map(hex::decode).transpose().map_err(|e| {
515 ApiError::internal_server_error(format!("invalid digest: {e}"), Some(Box::new(e)))
516 })?;
517
518 api::filter::update_filter(
519 &mut config,
520 name,
521 &FilterConfigUpdater {
522 min_severity,
523 mode,
524 invert_match,
525 comment,
526 },
527 delete.as_deref(),
528 digest.as_deref(),
529 )
530 }
531
532 #[export(serialize_error)]
533 fn delete_filter(
534 #[try_from_ref] this: &NotificationConfig,
535 name: &str,
536 ) -> Result<(), ApiError> {
537 let mut config = this.config.lock().unwrap();
538 api::filter::delete_filter(&mut config, name)
539 }
540 }