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 3DBE86110A for ; Fri, 4 Sep 2020 14:33:42 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 3B8F820CE5 for ; Fri, 4 Sep 2020 14:33:42 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (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 id 327A020C80 for ; Fri, 4 Sep 2020 14:33:37 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id F40FA44A17 for ; Fri, 4 Sep 2020 14:33:36 +0200 (CEST) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Fri, 4 Sep 2020 14:33:34 +0200 Message-Id: <20200904123334.3731-12-d.csapak@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200904123334.3731-1-d.csapak@proxmox.com> References: <20200904123334.3731-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.090 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods NO_DNS_FOR_FROM 0.379 Envelope sender has no MX or A DNS records RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [event.day, time.rs] Subject: [pbs-devel] [PATCH proxmox-backup v2 11/11] tools/systemd/time: enable dates for calendarevents X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 04 Sep 2020 12:33:42 -0000 this implements parsing and calculating calendarevents that have a basic date component (year-mon-day) with the usual syntax options (*, ranges, lists) and some special events: monthly yearly/annually (like systemd) quarterly semiannually,semi-annually (like systemd) includes some regression tests the ~ syntax for days (the last x days of the month) is not yet implemented Signed-off-by: Dominik Csapak --- src/tools/systemd/parse_time.rs | 77 +++++++++++++++++++++++++++++++-- src/tools/systemd/time.rs | 70 +++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/src/tools/systemd/parse_time.rs b/src/tools/systemd/parse_time.rs index 051c4968..d30a1acc 100644 --- a/src/tools/systemd/parse_time.rs +++ b/src/tools/systemd/parse_time.rs @@ -186,6 +186,25 @@ fn parse_time_spec(i: &str) -> IResult<&str, (Vec, Vec IResult<&str, (Vec, Vec, Vec)> { + + // TODO: implement ~ for days (man systemd.time) + if let Ok((i, (year, month, day))) = tuple(( + parse_date_time_comp_list(2200), // the upper limit for systemd, stay compatible + preceded(tag("-"), parse_date_time_comp_list(13)), + preceded(tag("-"), parse_date_time_comp_list(32)), + ))(i) { + Ok((i, (year, month, day))) + } else if let Ok((i, (month, day))) = tuple(( + parse_date_time_comp_list(13), + preceded(tag("-"), parse_date_time_comp_list(32)), + ))(i) { + Ok((i, (Vec::new(), month, day))) + } else { + Err(parse_error(i, "invalid date spec")) + } +} + pub fn parse_calendar_event(i: &str) -> Result { parse_complete_line("calendar event", i, parse_calendar_event_incomplete) } @@ -194,7 +213,7 @@ fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> let mut has_dayspec = false; let mut has_timespec = false; - let has_datespec = false; + let mut has_datespec = false; let mut event = CalendarEvent::default(); @@ -231,8 +250,52 @@ fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> ..Default::default() })); } - "monthly" | "yearly" | "quarterly" | "semiannually" => { - return Err(parse_error(i, "unimplemented date or time specification")); + "monthly" => { + return Ok(("", CalendarEvent { + hour: vec![DateTimeValue::Single(0)], + minute: vec![DateTimeValue::Single(0)], + second: vec![DateTimeValue::Single(0)], + day: vec![DateTimeValue::Single(1)], + ..Default::default() + })); + } + "yearly" | "annually" => { + return Ok(("", CalendarEvent { + hour: vec![DateTimeValue::Single(0)], + minute: vec![DateTimeValue::Single(0)], + second: vec![DateTimeValue::Single(0)], + day: vec![DateTimeValue::Single(1)], + month: vec![DateTimeValue::Single(1)], + ..Default::default() + })); + } + "quarterly" => { + return Ok(("", CalendarEvent { + hour: vec![DateTimeValue::Single(0)], + minute: vec![DateTimeValue::Single(0)], + second: vec![DateTimeValue::Single(0)], + day: vec![DateTimeValue::Single(1)], + month: vec![ + DateTimeValue::Single(1), + DateTimeValue::Single(4), + DateTimeValue::Single(7), + DateTimeValue::Single(10), + ], + ..Default::default() + })); + } + "semiannually" | "semi-annually" => { + return Ok(("", CalendarEvent { + hour: vec![DateTimeValue::Single(0)], + minute: vec![DateTimeValue::Single(0)], + second: vec![DateTimeValue::Single(0)], + day: vec![DateTimeValue::Single(1)], + month: vec![ + DateTimeValue::Single(1), + DateTimeValue::Single(7), + ], + ..Default::default() + })); } _ => { /* continue */ } } @@ -249,7 +312,13 @@ fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> for range in range_list { event.days.insert(range); } } - // todo: support date specs + if let (n, Some((year, month, day))) = opt(parse_date_spec)(i)? { + event.year = year; + event.month = month; + event.day = day; + has_datespec = true; + i = space0(n)?.0; + } if let (n, Some((hour, minute, second))) = opt(parse_time_spec)(i)? { event.hour = hour; diff --git a/src/tools/systemd/time.rs b/src/tools/systemd/time.rs index 7717ecee..c30e86f9 100644 --- a/src/tools/systemd/time.rs +++ b/src/tools/systemd/time.rs @@ -103,14 +103,12 @@ pub struct CalendarEvent { pub minute: Vec, /// the hour(s) this event should trigger pub hour: Vec, -/* FIXME: TODO /// the day(s) in a month this event should trigger pub day: Vec, /// the month(s) in a year this event should trigger pub month: Vec, /// the years(s) this event should trigger pub year: Vec, -*/ } #[derive(Default)] @@ -175,6 +173,45 @@ pub fn compute_next_event( count += 1; } + if !event.year.is_empty() { + let year: u32 = t.year().try_into()?; + if !DateTimeValue::list_contains(&event.year, year) { + if let Some(n) = DateTimeValue::find_next(&event.year, year) { + t.add_years((n - year).try_into()?)?; + continue; + } else { + // if we have no valid year, we cannot find a correct timestamp + return Ok(None); + } + } + } + + if !event.month.is_empty() { + let month: u32 = t.month().try_into()?; + if !DateTimeValue::list_contains(&event.month, month) { + if let Some(n) = DateTimeValue::find_next(&event.month, month) { + t.add_months((n - month).try_into()?)?; + } else { + // if we could not find valid month, retry next year + t.add_years(1)?; + } + continue; + } + } + + if !event.day.is_empty() { + let day: u32 = t.day().try_into()?; + if !DateTimeValue::list_contains(&event.day, day) { + if let Some(n) = DateTimeValue::find_next(&event.day, day) { + t.add_days((n - day).try_into()?)?; + } else { + // if we could not find valid mday, retry next month + t.add_months(1)?; + } + continue; + } + } + if !all_days { // match day first let day_num: u32 = t.day_num().try_into()?; let day = WeekDays::from_bits(1< Result<(), Error> { + let event = match parse_calendar_event(v) { + Ok(event) => event, + Err(err) => bail!("parsing '{}' failed - {}", v, err), + }; + + match compute_next_event(&event, last, true)? { + None => Ok(()), + Some(next) => bail!("compute next for '{}' succeeded, but expected fail - result {}", v, next), + } + }; + const MIN: i64 = 60; const HOUR: i64 = 3600; const DAY: i64 = 3600*24; @@ -355,6 +404,23 @@ mod test { n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?; } + // test date functionality + + test_value("2020-07-31", 0, JUL_31_2020)?; + test_value("02-28", 0, (31+27)*DAY)?; + test_value("02-29", 0, 2*365*DAY + (31+28)*DAY)?; // 1972-02-29 + test_value("1965/5-01-01", -1, THURSDAY_00_00)?; + test_value("2020-7..9-2/2", JUL_31_2020, JUL_31_2020 + 2*DAY)?; + test_value("2020,2021-12-31", JUL_31_2020, DEC_31_2020)?; + + test_value("monthly", 0, 31*DAY)?; + test_value("quarterly", 0, (31+28+31)*DAY)?; + test_value("semiannually", 0, (31+28+31+30+31+30)*DAY)?; + test_value("yearly", 0, (365)*DAY)?; + + test_never("2021-02-29", 0)?; + test_never("02-30", 0)?; + Ok(()) } -- 2.20.1