2 use std
::collections
::HashSet
;
7 use serde
::{Deserialize, Serialize}
;
9 use proxmox_schema
::api_types
::COMMENT_SCHEMA
;
11 api
, const_regex
, ApiStringFormat
, Schema
, StringSchema
, Updater
, SAFE_ID_REGEX_STR
,
13 use proxmox_time
::{parse_daily_duration, DailyDuration}
;
15 use crate::schema
::ENTITY_NAME_SCHEMA
;
16 use crate::{Error, Notification, Severity}
;
18 pub const MATCHER_TYPENAME
: &str = "matcher";
21 #[derive(Debug, Serialize, Deserialize, Default, Clone, Copy)]
22 #[serde(rename_all = "kebab-case")]
23 pub enum MatchModeOperator
{
24 /// All match statements have to match (AND)
27 /// At least one filter property has to match (OR)
31 impl MatchModeOperator
{
32 /// Apply the mode operator to two bools, lhs and rhs
33 fn apply(&self, lhs
: bool
, rhs
: bool
) -> bool
{
35 MatchModeOperator
::All
=> lhs
&& rhs
,
36 MatchModeOperator
::Any
=> lhs
|| rhs
,
40 // https://en.wikipedia.org/wiki/Identity_element
41 fn neutral_element(&self) -> bool
{
43 MatchModeOperator
::All
=> true,
44 MatchModeOperator
::Any
=> false,
50 pub MATCH_FIELD_ENTRY_REGEX
= concat
!(r
"^(?:(exact|regex):)?(", SAFE_ID_REGEX_STR
!(), r
")=(.*)$");
53 pub const MATCH_FIELD_ENTRY_FORMAT
: ApiStringFormat
=
54 ApiStringFormat
::VerifyFn(verify_field_matcher
);
56 fn verify_field_matcher(s
: &str) -> Result
<(), anyhow
::Error
> {
57 let _
: FieldMatcher
= s
.parse()?
;
61 pub const MATCH_FIELD_ENTRY_SCHEMA
: Schema
= StringSchema
::new("Match metadata field.")
62 .format(&MATCH_FIELD_ENTRY_FORMAT
)
70 schema
: ENTITY_NAME_SCHEMA
,
74 schema
: COMMENT_SCHEMA
,
79 description
: "Fields to match",
87 description
: "Severity level to match.",
95 description
: "Time stamps to match",
103 schema
: ENTITY_NAME_SCHEMA
,
108 #[derive(Debug, Serialize, Deserialize, Updater, Default)]
109 #[serde(rename_all = "kebab-case")]
110 /// Config for Sendmail notification endpoints
111 pub struct MatcherConfig
{
112 /// Name of the matcher
116 /// List of matched metadata fields
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub match_field
: Option
<Vec
<FieldMatcher
>>,
120 /// List of matched severity levels
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub match_severity
: Option
<Vec
<SeverityMatcher
>>,
124 /// List of matched severity levels
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub match_calendar
: Option
<Vec
<CalendarMatcher
>>,
128 /// Decide if 'all' or 'any' match statements must match
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub mode
: Option
<MatchModeOperator
>,
132 /// Invert match of the whole filter
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub invert_match
: Option
<bool
>,
136 /// Targets to notify
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub target
: Option
<Vec
<String
>>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub comment
: Option
<String
>,
145 #[derive(Clone, Debug)]
146 pub enum FieldMatcher
{
149 matched_value
: String
,
153 matched_regex
: Regex
,
157 proxmox_serde
::forward_deserialize_to_from_str
!(FieldMatcher
);
158 proxmox_serde
::forward_serialize_to_display
!(FieldMatcher
);
161 fn matches(&self, notification
: &Notification
) -> bool
{
163 FieldMatcher
::Exact
{
167 let value
= notification
.metadata
.additional_fields
.get(field
);
169 if let Some(value
) = value
{
170 matched_value
== value
172 // Metadata field does not exist, so we do not match
176 FieldMatcher
::Regex
{
180 let value
= notification
.metadata
.additional_fields
.get(field
);
182 if let Some(value
) = value
{
183 matched_regex
.is_match(value
)
185 // Metadata field does not exist, so we do not match
193 impl fmt
::Display
for FieldMatcher
{
194 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
195 // Attention, Display is used to implement Serialize, do not
196 // change the format.
199 FieldMatcher
::Exact
{
203 write
!(f
, "exact:{field}={matched_value}")
205 FieldMatcher
::Regex
{
209 let re
= matched_regex
.as_str();
210 write
!(f
, "regex:{field}={re}")
216 impl FromStr
for FieldMatcher
{
218 fn from_str(s
: &str) -> Result
<Self, Error
> {
219 if !MATCH_FIELD_ENTRY_REGEX
.is_match(s
) {
220 return Err(Error
::FilterFailed(format
!(
221 "invalid match-field statement: {s}"
225 if let Some(remaining
) = s
.strip_prefix("regex:") {
226 match remaining
.split_once('
='
) {
227 None
=> Err(Error
::FilterFailed(format
!(
228 "invalid match-field statement: {s}"
230 Some((field
, expected_value_regex
)) => {
231 let regex
= Regex
::new(expected_value_regex
)
232 .map_err(|err
| Error
::FilterFailed(format
!("invalid regex: {err}")))?
;
236 matched_regex
: regex
,
240 } else if let Some(remaining
) = s
.strip_prefix("exact:") {
241 match remaining
.split_once('
='
) {
242 None
=> Err(Error
::FilterFailed(format
!(
243 "invalid match-field statement: {s}"
245 Some((field
, expected_value
)) => Ok(Self::Exact
{
247 matched_value
: expected_value
.into(),
251 Err(Error
::FilterFailed(format
!(
252 "invalid match-field statement: {s}"
259 pub fn matches(&self, notification
: &Notification
) -> Result
<Option
<&[String
]>, Error
> {
260 let mode
= self.mode
.unwrap_or_default();
262 let mut is_match
= mode
.neutral_element();
263 is_match
= mode
.apply(is_match
, self.check_severity_match(notification
));
264 is_match
= mode
.apply(is_match
, self.check_field_match(notification
)?
);
265 is_match
= mode
.apply(is_match
, self.check_calendar_match(notification
)?
);
267 let invert_match
= self.invert_match
.unwrap_or_default();
269 Ok(if is_match
!= invert_match
{
270 Some(self.target
.as_deref().unwrap_or_default())
276 fn check_field_match(&self, notification
: &Notification
) -> Result
<bool
, Error
> {
277 let mode
= self.mode
.unwrap_or_default();
278 let mut is_match
= mode
.neutral_element();
280 if let Some(match_field
) = self.match_field
.as_deref() {
281 for field_matcher
in match_field
{
282 // let field_matcher: FieldMatcher = match_stmt.parse()?;
283 is_match
= mode
.apply(is_match
, field_matcher
.matches(notification
));
290 fn check_severity_match(&self, notification
: &Notification
) -> bool
{
291 let mode
= self.mode
.unwrap_or_default();
292 let mut is_match
= mode
.neutral_element();
294 if let Some(matchers
) = self.match_severity
.as_ref() {
295 for severity_matcher
in matchers
{
296 is_match
= mode
.apply(is_match
, severity_matcher
.matches(notification
));
303 fn check_calendar_match(&self, notification
: &Notification
) -> Result
<bool
, Error
> {
304 let mode
= self.mode
.unwrap_or_default();
305 let mut is_match
= mode
.neutral_element();
307 if let Some(matchers
) = self.match_calendar
.as_ref() {
308 for matcher
in matchers
{
309 is_match
= mode
.apply(is_match
, matcher
.matches(notification
)?
);
316 #[derive(Clone, Debug)]
317 pub struct SeverityMatcher
{
318 severities
: Vec
<Severity
>,
321 proxmox_serde
::forward_deserialize_to_from_str
!(SeverityMatcher
);
322 proxmox_serde
::forward_serialize_to_display
!(SeverityMatcher
);
324 impl SeverityMatcher
{
325 fn matches(&self, notification
: &Notification
) -> bool
{
326 self.severities
.contains(¬ification
.metadata
.severity
)
330 impl fmt
::Display
for SeverityMatcher
{
331 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
332 let severities
: Vec
<String
> = self.severities
.iter().map(|s
| format
!("{s}")).collect();
333 f
.write_str(&severities
.join(","))
337 impl FromStr
for SeverityMatcher
{
339 fn from_str(s
: &str) -> Result
<Self, Error
> {
340 let mut severities
= Vec
::new();
342 for element
in s
.split('
,'
) {
343 let element
= element
.trim();
344 let severity
: Severity
= element
.parse()?
;
346 severities
.push(severity
)
349 Ok(Self { severities }
)
353 /// Match timestamp of the notification.
354 #[derive(Clone, Debug)]
355 pub struct CalendarMatcher
{
356 schedule
: DailyDuration
,
360 proxmox_serde
::forward_deserialize_to_from_str
!(CalendarMatcher
);
361 proxmox_serde
::forward_serialize_to_display
!(CalendarMatcher
);
363 impl CalendarMatcher
{
364 fn matches(&self, notification
: &Notification
) -> Result
<bool
, Error
> {
366 .time_match(notification
.metadata
.timestamp
, false)
367 .map_err(|err
| Error
::Generic(format
!("could not match timestamp: {err}")))
371 impl fmt
::Display
for CalendarMatcher
{
372 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
373 f
.write_str(&self.original
)
377 impl FromStr
for CalendarMatcher
{
379 fn from_str(s
: &str) -> Result
<Self, Error
> {
380 let schedule
= parse_daily_duration(s
)
381 .map_err(|e
| Error
::Generic(format
!("could not parse schedule: {e}")))?
;
385 original
: s
.to_string(),
390 #[derive(Serialize, Deserialize)]
391 #[serde(rename_all = "kebab-case")]
392 pub enum DeleteableMatcherProperty
{
402 pub fn check_matches
<'a
>(
403 matchers
: &'a
[MatcherConfig
],
404 notification
: &Notification
,
405 ) -> HashSet
<&'a
str> {
406 let mut targets
= HashSet
::new();
408 for matcher
in matchers
{
409 match matcher
.matches(notification
) {
411 let t
= t
.unwrap_or_default();
412 targets
.extend(t
.iter().map(|s
| s
.as_str()));
414 Err(err
) => log
::error
!("matcher '{matcher}' failed: {err}", matcher
= matcher
.name
),
424 use serde_json
::Value
;
425 use std
::collections
::HashMap
;
429 let mut fields
= HashMap
::new();
430 fields
.insert("foo".into(), "bar".into());
433 Notification
::new_templated(Severity
::Notice
, "test", "test", Value
::Null
, fields
);
435 let matcher
: FieldMatcher
= "exact:foo=bar".parse().unwrap();
436 assert
!(matcher
.matches(¬ification
));
438 let matcher
: FieldMatcher
= "regex:foo=b.*".parse().unwrap();
439 assert
!(matcher
.matches(¬ification
));
441 let matcher
: FieldMatcher
= "regex:notthere=b.*".parse().unwrap();
442 assert
!(!matcher
.matches(¬ification
));
444 assert
!("regex:'3=b.*".parse
::<FieldMatcher
>().is_err());
445 assert
!("invalid:'bar=b.*".parse
::<FieldMatcher
>().is_err());
448 fn test_severities() {
449 let notification
= Notification
::new_templated(
457 let matcher
: SeverityMatcher
= "info,notice,warning,error".parse().unwrap();
458 assert
!(matcher
.matches(¬ification
));