]> git.proxmox.com Git - proxmox.git/blob - proxmox-notify/src/lib.rs
notify: introduce Error::Generic
[proxmox.git] / proxmox-notify / src / lib.rs
1 use std::collections::HashMap;
2 use std::error::Error as StdError;
3 use std::fmt::Display;
4
5 use serde::{Deserialize, Serialize};
6 use serde_json::json;
7 use serde_json::Value;
8
9 use proxmox_schema::api;
10 use proxmox_section_config::SectionConfigData;
11
12 pub mod filter;
13 use filter::{FilterConfig, FilterMatcher, FILTER_TYPENAME};
14
15 pub mod group;
16 use group::{GroupConfig, GROUP_TYPENAME};
17
18 pub mod api;
19 pub mod context;
20 pub mod endpoints;
21 pub mod renderer;
22 pub mod schema;
23
24 mod config;
25
26 #[derive(Debug)]
27 pub enum Error {
28 /// There was an error serializing the config
29 ConfigSerialization(Box<dyn StdError + Send + Sync>),
30 /// There was an error deserializing the config
31 ConfigDeserialization(Box<dyn StdError + Send + Sync>),
32 /// An endpoint failed to send a notification
33 NotifyFailed(String, Box<dyn StdError + Send + Sync>),
34 /// A target does not exist
35 TargetDoesNotExist(String),
36 /// Testing one or more notification targets failed
37 TargetTestFailed(Vec<Box<dyn StdError + Send + Sync>>),
38 /// A filter could not be applied
39 FilterFailed(String),
40 /// The notification's template string could not be rendered
41 RenderError(Box<dyn StdError + Send + Sync>),
42 /// Generic error for anything else
43 Generic(String),
44 }
45
46 impl Display for Error {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Error::ConfigSerialization(err) => {
50 write!(f, "could not serialize configuration: {err}")
51 }
52 Error::ConfigDeserialization(err) => {
53 write!(f, "could not deserialize configuration: {err}")
54 }
55 Error::NotifyFailed(endpoint, err) => {
56 write!(f, "could not notify via endpoint(s): {endpoint}: {err}")
57 }
58 Error::TargetDoesNotExist(target) => {
59 write!(f, "notification target '{target}' does not exist")
60 }
61 Error::TargetTestFailed(errs) => {
62 for err in errs {
63 writeln!(f, "{err}")?;
64 }
65
66 Ok(())
67 }
68 Error::FilterFailed(message) => {
69 write!(f, "could not apply filter: {message}")
70 }
71 Error::RenderError(err) => write!(f, "could not render notification template: {err}"),
72 Error::Generic(message) => f.write_str(message),
73 }
74 }
75 }
76
77 impl StdError for Error {
78 fn source(&self) -> Option<&(dyn StdError + 'static)> {
79 match self {
80 Error::ConfigSerialization(err) => Some(&**err),
81 Error::ConfigDeserialization(err) => Some(&**err),
82 Error::NotifyFailed(_, err) => Some(&**err),
83 Error::TargetDoesNotExist(_) => None,
84 Error::TargetTestFailed(errs) => Some(&*errs[0]),
85 Error::FilterFailed(_) => None,
86 Error::RenderError(err) => Some(&**err),
87 Error::Generic(_) => None,
88 }
89 }
90 }
91
92 #[api()]
93 #[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd)]
94 #[serde(rename_all = "kebab-case")]
95 /// Severity of a notification
96 pub enum Severity {
97 /// General information
98 Info,
99 /// A noteworthy event
100 Notice,
101 /// Warning
102 Warning,
103 /// Error
104 Error,
105 }
106
107 /// Notification endpoint trait, implemented by all endpoint plugins
108 pub trait Endpoint {
109 /// Send a documentation
110 fn send(&self, notification: &Notification) -> Result<(), Error>;
111
112 /// The name/identifier for this endpoint
113 fn name(&self) -> &str;
114
115 /// The name of the filter to use
116 fn filter(&self) -> Option<&str>;
117 }
118
119 #[derive(Debug, Clone)]
120 /// Notification which can be sent
121 pub struct Notification {
122 /// Notification severity
123 pub severity: Severity,
124 /// The title of the notification
125 pub title: String,
126 /// Notification text
127 pub body: String,
128 /// Additional metadata for the notification
129 pub properties: Option<Value>,
130 }
131
132 /// Notification configuration
133 #[derive(Debug, Clone)]
134 pub struct Config {
135 config: SectionConfigData,
136 private_config: SectionConfigData,
137 digest: [u8; 32],
138 }
139
140 impl Config {
141 /// Parse raw config
142 pub fn new(raw_config: &str, raw_private_config: &str) -> Result<Self, Error> {
143 let (config, digest) = config::config(raw_config)?;
144 let (private_config, _) = config::private_config(raw_private_config)?;
145
146 Ok(Self {
147 config,
148 digest,
149 private_config,
150 })
151 }
152
153 /// Serialize config
154 pub fn write(&self) -> Result<(String, String), Error> {
155 Ok((
156 config::write(&self.config)?,
157 config::write_private(&self.private_config)?,
158 ))
159 }
160
161 /// Returns the SHA256 digest of the configuration.
162 /// The digest is only computed once when the configuration deserialized.
163 pub fn digest(&self) -> &[u8; 32] {
164 &self.digest
165 }
166 }
167
168 /// Notification bus - distributes notifications to all registered endpoints
169 // The reason for the split between `Config` and this struct is to make testing with mocked
170 // endpoints a bit easier.
171 #[derive(Default)]
172 pub struct Bus {
173 endpoints: HashMap<String, Box<dyn Endpoint>>,
174 groups: HashMap<String, GroupConfig>,
175 filters: Vec<FilterConfig>,
176 }
177
178 #[allow(unused_macros)]
179 macro_rules! parse_endpoints_with_private_config {
180 ($config:ident, $public_config:ty, $private_config:ty, $endpoint_type:ident, $type_name:expr) => {
181 (|| -> Result<Vec<Box<dyn Endpoint>>, Error> {
182 let mut endpoints = Vec::<Box<dyn Endpoint>>::new();
183
184 let configs: Vec<$public_config> = $config
185 .config
186 .convert_to_typed_array($type_name)
187 .map_err(|err| Error::ConfigDeserialization(err.into()))?;
188
189 for config in configs {
190 match $config.private_config.sections.get(&config.name) {
191 Some((section_type_name, private_config)) => {
192 if $type_name != section_type_name {
193 log::error!(
194 "Could not instantiate endpoint '{name}': \
195 private config has wrong type",
196 name = config.name
197 );
198 }
199 let private_config = <$private_config>::deserialize(private_config)
200 .map_err(|err| Error::ConfigDeserialization(err.into()))?;
201
202 endpoints.push(Box::new($endpoint_type {
203 config,
204 private_config: private_config.clone(),
205 }));
206 }
207 None => log::error!(
208 "Could not instantiate endpoint '{name}': \
209 private config does not exist",
210 name = config.name
211 ),
212 }
213 }
214
215 Ok(endpoints)
216 })()
217 };
218 }
219
220 #[allow(unused_macros)]
221 macro_rules! parse_endpoints_without_private_config {
222 ($config:ident, $public_config:ty, $endpoint_type:ident, $type_name:expr) => {
223 (|| -> Result<Vec<Box<dyn Endpoint>>, Error> {
224 let mut endpoints = Vec::<Box<dyn Endpoint>>::new();
225
226 let configs: Vec<$public_config> = $config
227 .config
228 .convert_to_typed_array($type_name)
229 .map_err(|err| Error::ConfigDeserialization(err.into()))?;
230
231 for config in configs {
232 endpoints.push(Box::new($endpoint_type { config }));
233 }
234
235 Ok(endpoints)
236 })()
237 };
238 }
239
240 impl Bus {
241 /// Instantiate notification bus from a given configuration.
242 pub fn from_config(config: &Config) -> Result<Self, Error> {
243 #[allow(unused_mut)]
244 let mut endpoints = HashMap::new();
245
246 // Instantiate endpoints
247 #[cfg(feature = "sendmail")]
248 {
249 use endpoints::sendmail::SENDMAIL_TYPENAME;
250 use endpoints::sendmail::{SendmailConfig, SendmailEndpoint};
251 endpoints.extend(
252 parse_endpoints_without_private_config!(
253 config,
254 SendmailConfig,
255 SendmailEndpoint,
256 SENDMAIL_TYPENAME
257 )?
258 .into_iter()
259 .map(|e| (e.name().into(), e)),
260 );
261 }
262
263 #[cfg(feature = "gotify")]
264 {
265 use endpoints::gotify::GOTIFY_TYPENAME;
266 use endpoints::gotify::{GotifyConfig, GotifyEndpoint, GotifyPrivateConfig};
267 endpoints.extend(
268 parse_endpoints_with_private_config!(
269 config,
270 GotifyConfig,
271 GotifyPrivateConfig,
272 GotifyEndpoint,
273 GOTIFY_TYPENAME
274 )?
275 .into_iter()
276 .map(|e| (e.name().into(), e)),
277 );
278 }
279
280 let groups: HashMap<String, GroupConfig> = config
281 .config
282 .convert_to_typed_array(GROUP_TYPENAME)
283 .map_err(|err| Error::ConfigDeserialization(err.into()))?
284 .into_iter()
285 .map(|group: GroupConfig| (group.name.clone(), group))
286 .collect();
287
288 let filters = config
289 .config
290 .convert_to_typed_array(FILTER_TYPENAME)
291 .map_err(|err| Error::ConfigDeserialization(err.into()))?;
292
293 Ok(Bus {
294 endpoints,
295 groups,
296 filters,
297 })
298 }
299
300 #[cfg(test)]
301 pub fn add_endpoint(&mut self, endpoint: Box<dyn Endpoint>) {
302 self.endpoints.insert(endpoint.name().to_string(), endpoint);
303 }
304
305 #[cfg(test)]
306 pub fn add_group(&mut self, group: GroupConfig) {
307 self.groups.insert(group.name.clone(), group);
308 }
309
310 #[cfg(test)]
311 pub fn add_filter(&mut self, filter: FilterConfig) {
312 self.filters.push(filter)
313 }
314
315 /// Send a notification to a given target (endpoint or group).
316 ///
317 /// Any errors will not be returned but only logged.
318 pub fn send(&self, endpoint_or_group: &str, notification: &Notification) {
319 let mut filter_matcher = FilterMatcher::new(&self.filters, notification);
320
321 if let Some(group) = self.groups.get(endpoint_or_group) {
322 if !Bus::check_filter(&mut filter_matcher, group.filter.as_deref()) {
323 log::info!("skipped target '{endpoint_or_group}', filter did not match");
324 return;
325 }
326
327 log::info!("target '{endpoint_or_group}' is a group, notifying all members...");
328
329 for endpoint in &group.endpoint {
330 self.send_via_single_endpoint(endpoint, notification, &mut filter_matcher);
331 }
332 } else {
333 self.send_via_single_endpoint(endpoint_or_group, notification, &mut filter_matcher);
334 }
335 }
336
337 fn check_filter(filter_matcher: &mut FilterMatcher, filter: Option<&str>) -> bool {
338 if let Some(filter) = filter {
339 match filter_matcher.check_filter_match(filter) {
340 // If the filter does not match, do nothing
341 Ok(r) => r,
342 Err(err) => {
343 // If there is an error, only log it and still send
344 log::error!("could not apply filter '{filter}': {err}");
345 true
346 }
347 }
348 } else {
349 true
350 }
351 }
352
353 fn send_via_single_endpoint(
354 &self,
355 endpoint: &str,
356 notification: &Notification,
357 filter_matcher: &mut FilterMatcher,
358 ) {
359 if let Some(endpoint) = self.endpoints.get(endpoint) {
360 let name = endpoint.name();
361 if !Bus::check_filter(filter_matcher, endpoint.filter()) {
362 log::info!("skipped target '{name}', filter did not match");
363 return;
364 }
365
366 match endpoint.send(notification) {
367 Ok(_) => {
368 log::info!("notified via target `{name}`");
369 }
370 Err(e) => {
371 // Only log on errors, do not propagate fail to the caller.
372 log::error!("could not notify via target `{name}`: {e}");
373 }
374 }
375 } else {
376 log::error!("could not notify via target '{endpoint}', it does not exist");
377 }
378 }
379
380 /// Send a test notification to a target (endpoint or group).
381 ///
382 /// In contrast to the `send` function, this function will return
383 /// any errors to the caller.
384 pub fn test_target(&self, target: &str) -> Result<(), Error> {
385 let notification = Notification {
386 severity: Severity::Info,
387 title: "Test notification".into(),
388 body: "This is a test of the notification target '{{ target }}'".into(),
389 properties: Some(json!({ "target": target })),
390 };
391
392 let mut errors: Vec<Box<dyn StdError + Send + Sync>> = Vec::new();
393
394 let mut my_send = |target: &str| -> Result<(), Error> {
395 if let Some(endpoint) = self.endpoints.get(target) {
396 if let Err(e) = endpoint.send(&notification) {
397 errors.push(Box::new(e));
398 }
399 } else {
400 return Err(Error::TargetDoesNotExist(target.to_string()));
401 }
402 Ok(())
403 };
404
405 if let Some(group) = self.groups.get(target) {
406 for endpoint_name in &group.endpoint {
407 my_send(endpoint_name)?;
408 }
409 } else {
410 my_send(target)?;
411 }
412
413 if !errors.is_empty() {
414 return Err(Error::TargetTestFailed(errors));
415 }
416
417 Ok(())
418 }
419 }
420
421 #[cfg(test)]
422 mod tests {
423 use std::{cell::RefCell, rc::Rc};
424
425 use super::*;
426
427 #[derive(Default, Clone)]
428 struct MockEndpoint {
429 name: &'static str,
430 // Needs to be an Rc so that we can clone MockEndpoint before
431 // passing it to Bus, while still retaining a handle to the Vec
432 messages: Rc<RefCell<Vec<Notification>>>,
433 filter: Option<String>,
434 }
435
436 impl Endpoint for MockEndpoint {
437 fn send(&self, message: &Notification) -> Result<(), Error> {
438 self.messages.borrow_mut().push(message.clone());
439
440 Ok(())
441 }
442
443 fn name(&self) -> &str {
444 self.name
445 }
446
447 fn filter(&self) -> Option<&str> {
448 self.filter.as_deref()
449 }
450 }
451
452 impl MockEndpoint {
453 fn new(name: &'static str, filter: Option<String>) -> Self {
454 Self {
455 name,
456 filter,
457 ..Default::default()
458 }
459 }
460
461 fn messages(&self) -> Vec<Notification> {
462 self.messages.borrow().clone()
463 }
464 }
465
466 #[test]
467 fn test_add_mock_endpoint() -> Result<(), Error> {
468 let mock = MockEndpoint::new("endpoint", None);
469
470 let mut bus = Bus::default();
471 bus.add_endpoint(Box::new(mock.clone()));
472
473 // Send directly to endpoint
474 bus.send(
475 "endpoint",
476 &Notification {
477 title: "Title".into(),
478 body: "Body".into(),
479 severity: Severity::Info,
480 properties: Default::default(),
481 },
482 );
483 let messages = mock.messages();
484 assert_eq!(messages.len(), 1);
485
486 Ok(())
487 }
488
489 #[test]
490 fn test_groups() -> Result<(), Error> {
491 let endpoint1 = MockEndpoint::new("mock1", None);
492 let endpoint2 = MockEndpoint::new("mock2", None);
493
494 let mut bus = Bus::default();
495
496 bus.add_group(GroupConfig {
497 name: "group1".to_string(),
498 endpoint: vec!["mock1".into()],
499 comment: None,
500 filter: None,
501 });
502
503 bus.add_group(GroupConfig {
504 name: "group2".to_string(),
505 endpoint: vec!["mock2".into()],
506 comment: None,
507 filter: None,
508 });
509
510 bus.add_endpoint(Box::new(endpoint1.clone()));
511 bus.add_endpoint(Box::new(endpoint2.clone()));
512
513 let send_to_group = |channel| {
514 bus.send(
515 channel,
516 &Notification {
517 title: "Title".into(),
518 body: "Body".into(),
519 severity: Severity::Info,
520 properties: Default::default(),
521 },
522 )
523 };
524
525 send_to_group("group1");
526 assert_eq!(endpoint1.messages().len(), 1);
527 assert_eq!(endpoint2.messages().len(), 0);
528
529 send_to_group("group2");
530 assert_eq!(endpoint1.messages().len(), 1);
531 assert_eq!(endpoint2.messages().len(), 1);
532
533 Ok(())
534 }
535
536 #[test]
537 fn test_severity_ordering() {
538 // Not intended to be exhaustive, just a quick
539 // sanity check ;)
540
541 assert!(Severity::Info < Severity::Notice);
542 assert!(Severity::Info < Severity::Warning);
543 assert!(Severity::Info < Severity::Error);
544 assert!(Severity::Error > Severity::Warning);
545 assert!(Severity::Warning > Severity::Notice);
546 }
547
548 #[test]
549 fn test_multiple_endpoints_with_different_filters() -> Result<(), Error> {
550 let endpoint1 = MockEndpoint::new("mock1", Some("filter1".into()));
551 let endpoint2 = MockEndpoint::new("mock2", Some("filter2".into()));
552
553 let mut bus = Bus::default();
554
555 bus.add_endpoint(Box::new(endpoint1.clone()));
556 bus.add_endpoint(Box::new(endpoint2.clone()));
557
558 bus.add_group(GroupConfig {
559 name: "channel1".to_string(),
560 endpoint: vec!["mock1".into(), "mock2".into()],
561 comment: None,
562 filter: None,
563 });
564
565 bus.add_filter(FilterConfig {
566 name: "filter1".into(),
567 min_severity: Some(Severity::Warning),
568 mode: None,
569 invert_match: None,
570 comment: None,
571 });
572
573 bus.add_filter(FilterConfig {
574 name: "filter2".into(),
575 min_severity: Some(Severity::Error),
576 mode: None,
577 invert_match: None,
578 comment: None,
579 });
580
581 let send_with_severity = |severity| {
582 bus.send(
583 "channel1",
584 &Notification {
585 title: "Title".into(),
586 body: "Body".into(),
587 severity,
588 properties: Default::default(),
589 },
590 );
591 };
592
593 send_with_severity(Severity::Info);
594 assert_eq!(endpoint1.messages().len(), 0);
595 assert_eq!(endpoint2.messages().len(), 0);
596
597 send_with_severity(Severity::Warning);
598 assert_eq!(endpoint1.messages().len(), 1);
599 assert_eq!(endpoint2.messages().len(), 0);
600
601 send_with_severity(Severity::Error);
602 assert_eq!(endpoint1.messages().len(), 2);
603 assert_eq!(endpoint2.messages().len(), 1);
604
605 Ok(())
606 }
607 }