From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id AB1779FA20 for ; Tue, 7 Nov 2023 11:19:33 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8C887301EB for ; Tue, 7 Nov 2023 11:19:03 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Tue, 7 Nov 2023 11:19:02 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 4F42B4692E for ; Tue, 7 Nov 2023 11:19:02 +0100 (CET) From: Lukas Wagner To: pve-devel@lists.proxmox.com Date: Tue, 7 Nov 2023 11:18:04 +0100 Message-Id: <20231107101827.340100-5-l.wagner@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20231107101827.340100-1-l.wagner@proxmox.com> References: <20231107101827.340100-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.017 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH proxmox 04/27] notify: add calendar matcher X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 07 Nov 2023 10:19:33 -0000 This allows matching by a notification's timestamp: matcher: foo match-calendar mon..fri 8-12 Signed-off-by: Lukas Wagner --- proxmox-notify/src/api/matcher.rs | 6 +++ proxmox-notify/src/lib.rs | 4 ++ proxmox-notify/src/matcher.rs | 65 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/proxmox-notify/src/api/matcher.rs b/proxmox-notify/src/api/matcher.rs index e37b74f..0592b14 100644 --- a/proxmox-notify/src/api/matcher.rs +++ b/proxmox-notify/src/api/matcher.rs @@ -80,6 +80,7 @@ pub fn update_matcher( match deleteable_property { DeleteableMatcherProperty::MatchSeverity => matcher.match_severity = None, DeleteableMatcherProperty::MatchField => matcher.match_field = None, + DeleteableMatcherProperty::MatchCalendar => matcher.match_calendar = None, DeleteableMatcherProperty::Target => matcher.target = None, DeleteableMatcherProperty::Mode => matcher.mode = None, DeleteableMatcherProperty::InvertMatch => matcher.invert_match = None, @@ -96,6 +97,10 @@ pub fn update_matcher( matcher.match_field = Some(match_field.clone()); } + if let Some(match_calendar) = &matcher_updater.match_calendar { + matcher.match_calendar = Some(match_calendar.clone()); + } + if let Some(mode) = matcher_updater.mode { matcher.mode = Some(mode); } @@ -200,6 +205,7 @@ matcher: matcher2 mode: Some(MatchModeOperator::Any), match_field: None, match_severity: None, + match_calendar: None, invert_match: Some(true), target: Some(vec!["foo".into()]), comment: Some("new comment".into()), diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs index 1f95ae0..9997cef 100644 --- a/proxmox-notify/src/lib.rs +++ b/proxmox-notify/src/lib.rs @@ -154,6 +154,8 @@ pub enum Content { pub struct Metadata { /// Notification severity severity: Severity, + /// Timestamp of the notification as a UNIX epoch + timestamp: i64, /// Additional fields for additional key-value metadata additional_fields: HashMap, } @@ -179,6 +181,7 @@ impl Notification { metadata: Metadata { severity, additional_fields: fields, + timestamp: proxmox_time::epoch_i64(), }, content: Content::Template { title_template: title.as_ref().to_string(), @@ -393,6 +396,7 @@ impl Bus { severity: Severity::Info, // TODO: what fields would make sense for test notifications? additional_fields: Default::default(), + timestamp: proxmox_time::epoch_i64(), }, content: Content::Template { title_template: "Test notification".into(), diff --git a/proxmox-notify/src/matcher.rs b/proxmox-notify/src/matcher.rs index c24726d..b03d11d 100644 --- a/proxmox-notify/src/matcher.rs +++ b/proxmox-notify/src/matcher.rs @@ -10,6 +10,7 @@ use proxmox_schema::api_types::COMMENT_SCHEMA; use proxmox_schema::{ api, const_regex, ApiStringFormat, Schema, StringSchema, Updater, SAFE_ID_REGEX_STR, }; +use proxmox_time::{parse_daily_duration, DailyDuration}; use crate::schema::ENTITY_NAME_SCHEMA; use crate::{Error, Notification, Severity}; @@ -88,6 +89,14 @@ pub const MATCH_FIELD_ENTRY_SCHEMA: Schema = StringSchema::new("Match metadata f }, optional: true, }, + "match-calendar": { + type: Array, + items: { + description: "Time stamps to match", + type: String + }, + optional: true, + }, "target": { type: Array, items: { @@ -112,6 +121,10 @@ pub struct MatcherConfig { #[serde(skip_serializing_if = "Option::is_none")] pub match_severity: Option>, + /// List of matched severity levels + #[serde(skip_serializing_if = "Option::is_none")] + pub match_calendar: Option>, + /// Decide if 'all' or 'any' match statements must match #[serde(skip_serializing_if = "Option::is_none")] pub mode: Option, @@ -249,6 +262,7 @@ impl MatcherConfig { let mut is_match = mode.neutral_element(); is_match = mode.apply(is_match, self.check_severity_match(notification)); is_match = mode.apply(is_match, self.check_field_match(notification)?); + is_match = mode.apply(is_match, self.check_calendar_match(notification)?); let invert_match = self.invert_match.unwrap_or_default(); @@ -285,6 +299,19 @@ impl MatcherConfig { is_match } + + fn check_calendar_match(&self, notification: &Notification) -> Result { + let mode = self.mode.unwrap_or_default(); + let mut is_match = mode.neutral_element(); + + if let Some(matchers) = self.match_calendar.as_ref() { + for matcher in matchers { + is_match = mode.apply(is_match, matcher.matches(notification)?); + } + } + + Ok(is_match) + } } #[derive(Clone, Debug)] pub struct SeverityMatcher { @@ -323,11 +350,49 @@ impl FromStr for SeverityMatcher { } } +/// Match timestamp of the notification. +#[derive(Clone, Debug)] +pub struct CalendarMatcher { + schedule: DailyDuration, + original: String, +} + +proxmox_serde::forward_deserialize_to_from_str!(CalendarMatcher); +proxmox_serde::forward_serialize_to_display!(CalendarMatcher); + +impl CalendarMatcher { + fn matches(&self, notification: &Notification) -> Result { + self.schedule + .time_match(notification.metadata.timestamp, false) + .map_err(|err| Error::Generic(format!("could not match timestamp: {err}"))) + } +} + +impl fmt::Display for CalendarMatcher { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.original) + } +} + +impl FromStr for CalendarMatcher { + type Err = Error; + fn from_str(s: &str) -> Result { + let schedule = parse_daily_duration(s) + .map_err(|e| Error::Generic(format!("could not parse schedule: {e}")))?; + + Ok(Self { + schedule, + original: s.to_string(), + }) + } +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum DeleteableMatcherProperty { MatchSeverity, MatchField, + MatchCalendar, Target, Mode, InvertMatch, -- 2.39.2