1 use std
::collections
::HashMap
;
2 use std
::error
::Error
as StdError
;
5 use serde
::{Deserialize, Serialize}
;
9 use proxmox_schema
::api
;
10 use proxmox_section_config
::SectionConfigData
;
13 use filter
::{FilterConfig, FilterMatcher, FILTER_TYPENAME}
;
16 use group
::{GroupConfig, GROUP_TYPENAME}
;
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
40 /// The notification's template string could not be rendered
41 RenderError(Box
<dyn StdError
+ Send
+ Sync
>),
42 /// Generic error for anything else
46 impl Display
for Error
{
47 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
49 Error
::ConfigSerialization(err
) => {
50 write
!(f
, "could not serialize configuration: {err}")
52 Error
::ConfigDeserialization(err
) => {
53 write
!(f
, "could not deserialize configuration: {err}")
55 Error
::NotifyFailed(endpoint
, err
) => {
56 write
!(f
, "could not notify via endpoint(s): {endpoint}: {err}")
58 Error
::TargetDoesNotExist(target
) => {
59 write
!(f
, "notification target '{target}' does not exist")
61 Error
::TargetTestFailed(errs
) => {
63 writeln
!(f
, "{err}")?
;
68 Error
::FilterFailed(message
) => {
69 write
!(f
, "could not apply filter: {message}")
71 Error
::RenderError(err
) => write
!(f
, "could not render notification template: {err}"),
72 Error
::Generic(message
) => f
.write_str(message
),
77 impl StdError
for Error
{
78 fn source(&self) -> Option
<&(dyn StdError
+ '
static)> {
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
,
93 #[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd)]
94 #[serde(rename_all = "kebab-case")]
95 /// Severity of a notification
97 /// General information
99 /// A noteworthy event
107 /// Notification endpoint trait, implemented by all endpoint plugins
109 /// Send a documentation
110 fn send(&self, notification
: &Notification
) -> Result
<(), Error
>;
112 /// The name/identifier for this endpoint
113 fn name(&self) -> &str;
115 /// The name of the filter to use
116 fn filter(&self) -> Option
<&str>;
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
126 /// Notification text
128 /// Additional metadata for the notification
129 pub properties
: Option
<Value
>,
132 /// Notification configuration
133 #[derive(Debug, Clone)]
135 config
: SectionConfigData
,
136 private_config
: SectionConfigData
,
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
)?
;
154 pub fn write(&self) -> Result
<(String
, String
), Error
> {
156 config
::write(&self.config
)?
,
157 config
::write_private(&self.private_config
)?
,
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] {
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.
173 endpoints
: HashMap
<String
, Box
<dyn Endpoint
>>,
174 groups
: HashMap
<String
, GroupConfig
>,
175 filters
: Vec
<FilterConfig
>,
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();
184 let configs
: Vec
<$public_config
> = $config
186 .convert_to_typed_array($type_name
)
187 .map_err(|err
| Error
::ConfigDeserialization(err
.into()))?
;
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
{
194 "Could not instantiate endpoint '{name}': \
195 private config has wrong type",
199 let private_config
= <$private_config
>::deserialize(private_config
)
200 .map_err(|err
| Error
::ConfigDeserialization(err
.into()))?
;
202 endpoints
.push(Box
::new($endpoint_type
{
204 private_config
: private_config
.clone(),
208 "Could not instantiate endpoint '{name}': \
209 private config does not exist",
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();
226 let configs
: Vec
<$public_config
> = $config
228 .convert_to_typed_array($type_name
)
229 .map_err(|err
| Error
::ConfigDeserialization(err
.into()))?
;
231 for config
in configs
{
232 endpoints
.push(Box
::new($endpoint_type { config }
));
241 /// Instantiate notification bus from a given configuration.
242 pub fn from_config(config
: &Config
) -> Result
<Self, Error
> {
244 let mut endpoints
= HashMap
::new();
246 // Instantiate endpoints
247 #[cfg(feature = "sendmail")]
249 use endpoints
::sendmail
::SENDMAIL_TYPENAME
;
250 use endpoints
::sendmail
::{SendmailConfig, SendmailEndpoint}
;
252 parse_endpoints_without_private_config
!(
259 .map(|e
| (e
.name().into(), e
)),
263 #[cfg(feature = "gotify")]
265 use endpoints
::gotify
::GOTIFY_TYPENAME
;
266 use endpoints
::gotify
::{GotifyConfig, GotifyEndpoint, GotifyPrivateConfig}
;
268 parse_endpoints_with_private_config
!(
276 .map(|e
| (e
.name().into(), e
)),
280 let groups
: HashMap
<String
, GroupConfig
> = config
282 .convert_to_typed_array(GROUP_TYPENAME
)
283 .map_err(|err
| Error
::ConfigDeserialization(err
.into()))?
285 .map(|group
: GroupConfig
| (group
.name
.clone(), group
))
290 .convert_to_typed_array(FILTER_TYPENAME
)
291 .map_err(|err
| Error
::ConfigDeserialization(err
.into()))?
;
301 pub fn add_endpoint(&mut self, endpoint
: Box
<dyn Endpoint
>) {
302 self.endpoints
.insert(endpoint
.name().to_string(), endpoint
);
306 pub fn add_group(&mut self, group
: GroupConfig
) {
307 self.groups
.insert(group
.name
.clone(), group
);
311 pub fn add_filter(&mut self, filter
: FilterConfig
) {
312 self.filters
.push(filter
)
315 /// Send a notification to a given target (endpoint or group).
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
);
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");
327 log
::info
!("target '{endpoint_or_group}' is a group, notifying all members...");
329 for endpoint
in &group
.endpoint
{
330 self.send_via_single_endpoint(endpoint
, notification
, &mut filter_matcher
);
333 self.send_via_single_endpoint(endpoint_or_group
, notification
, &mut filter_matcher
);
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
343 // If there is an error, only log it and still send
344 log
::error
!("could not apply filter '{filter}': {err}");
353 fn send_via_single_endpoint(
356 notification
: &Notification
,
357 filter_matcher
: &mut FilterMatcher
,
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");
366 match endpoint
.send(notification
) {
368 log
::info
!("notified via target `{name}`");
371 // Only log on errors, do not propagate fail to the caller.
372 log
::error
!("could not notify via target `{name}`: {e}");
376 log
::error
!("could not notify via target '{endpoint}', it does not exist");
380 /// Send a test notification to a target (endpoint or group).
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 }
)),
392 let mut errors
: Vec
<Box
<dyn StdError
+ Send
+ Sync
>> = Vec
::new();
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(¬ification
) {
397 errors
.push(Box
::new(e
));
400 return Err(Error
::TargetDoesNotExist(target
.to_string()));
405 if let Some(group
) = self.groups
.get(target
) {
406 for endpoint_name
in &group
.endpoint
{
407 my_send(endpoint_name
)?
;
413 if !errors
.is_empty() {
414 return Err(Error
::TargetTestFailed(errors
));
423 use std
::{cell::RefCell, rc::Rc}
;
427 #[derive(Default, Clone)]
428 struct MockEndpoint
{
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
>,
436 impl Endpoint
for MockEndpoint
{
437 fn send(&self, message
: &Notification
) -> Result
<(), Error
> {
438 self.messages
.borrow_mut().push(message
.clone());
443 fn name(&self) -> &str {
447 fn filter(&self) -> Option
<&str> {
448 self.filter
.as_deref()
453 fn new(name
: &'
static str, filter
: Option
<String
>) -> Self {
461 fn messages(&self) -> Vec
<Notification
> {
462 self.messages
.borrow().clone()
467 fn test_add_mock_endpoint() -> Result
<(), Error
> {
468 let mock
= MockEndpoint
::new("endpoint", None
);
470 let mut bus
= Bus
::default();
471 bus
.add_endpoint(Box
::new(mock
.clone()));
473 // Send directly to endpoint
477 title
: "Title".into(),
479 severity
: Severity
::Info
,
480 properties
: Default
::default(),
483 let messages
= mock
.messages();
484 assert_eq
!(messages
.len(), 1);
490 fn test_groups() -> Result
<(), Error
> {
491 let endpoint1
= MockEndpoint
::new("mock1", None
);
492 let endpoint2
= MockEndpoint
::new("mock2", None
);
494 let mut bus
= Bus
::default();
496 bus
.add_group(GroupConfig
{
497 name
: "group1".to_string(),
498 endpoint
: vec
!["mock1".into()],
503 bus
.add_group(GroupConfig
{
504 name
: "group2".to_string(),
505 endpoint
: vec
!["mock2".into()],
510 bus
.add_endpoint(Box
::new(endpoint1
.clone()));
511 bus
.add_endpoint(Box
::new(endpoint2
.clone()));
513 let send_to_group
= |channel
| {
517 title
: "Title".into(),
519 severity
: Severity
::Info
,
520 properties
: Default
::default(),
525 send_to_group("group1");
526 assert_eq
!(endpoint1
.messages().len(), 1);
527 assert_eq
!(endpoint2
.messages().len(), 0);
529 send_to_group("group2");
530 assert_eq
!(endpoint1
.messages().len(), 1);
531 assert_eq
!(endpoint2
.messages().len(), 1);
537 fn test_severity_ordering() {
538 // Not intended to be exhaustive, just a quick
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
);
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()));
553 let mut bus
= Bus
::default();
555 bus
.add_endpoint(Box
::new(endpoint1
.clone()));
556 bus
.add_endpoint(Box
::new(endpoint2
.clone()));
558 bus
.add_group(GroupConfig
{
559 name
: "channel1".to_string(),
560 endpoint
: vec
!["mock1".into(), "mock2".into()],
565 bus
.add_filter(FilterConfig
{
566 name
: "filter1".into(),
567 min_severity
: Some(Severity
::Warning
),
573 bus
.add_filter(FilterConfig
{
574 name
: "filter2".into(),
575 min_severity
: Some(Severity
::Error
),
581 let send_with_severity
= |severity
| {
585 title
: "Title".into(),
588 properties
: Default
::default(),
593 send_with_severity(Severity
::Info
);
594 assert_eq
!(endpoint1
.messages().len(), 0);
595 assert_eq
!(endpoint2
.messages().len(), 0);
597 send_with_severity(Severity
::Warning
);
598 assert_eq
!(endpoint1
.messages().len(), 1);
599 assert_eq
!(endpoint2
.messages().len(), 0);
601 send_with_severity(Severity
::Error
);
602 assert_eq
!(endpoint1
.messages().len(), 2);
603 assert_eq
!(endpoint2
.messages().len(), 1);