From d190aa300116d8176f7bcd5e6718434bb44fc1c2 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 2 Apr 2014 11:44:40 -0400 Subject: [PATCH] Wrap up rrule validation for scheduler --- awx/api/serializers.py | 21 ++++++++++++++------- awx/main/tests/schedules.py | 30 ++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f4d0669cba..c748989677 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -8,6 +8,7 @@ import socket import urlparse import logging import os.path +import datetime from dateutil import rrule # PyYAML @@ -1376,19 +1377,20 @@ class ScheduleSerializer(BaseSerializer): # - INTERVAL is not included # - SECONDLY is used # - TZID is used - # - multiple BYDAY (except WEEKLY and YEARLY (see below)), BYMONTHDAY, BYMONTH - # - multiple BYDAY yearly unless it lists all weekdays OR weekend days # - BYDAY prefixed with a number (MO is good but not 20MO) # - BYYEARDAY # - BYWEEKNO + # - Multiple DTSTART or RRULE elements + # - COUNT > 999 def validate_rrule(self, attrs, source): rrule_value = attrs[source] - multi_by_day = ".*?BYDAY[\:\=][a-zA-Z]{2},[a-zA-Z]{2}" multi_by_month_day = ".*?BYMONTHDAY[\:\=][0-9]+,-*[0-9]+" multi_by_month = ".*?BYMONTH[\:\=][0-9]+,[0-9]+" by_day_with_numeric_prefix = ".*?BYDAY[\:\=][0-9]+[a-zA-Z]{2}" - if not re.match("DTSTART[\:\=][0-9]+T[0-9]+Z", rrule_value): - raise serializers.ValidationError('DTSTART required in rrule, value should match: DTSTART:YYYYMMDDTHHMMSSZ') + match_dtstart = re.match("DTSTART\:[0-9]+T[0-9]+Z", rrule_value) + match_count = re.match(".*?(COUNT\=[0-9]+)", rrule_value) + if not match_dtstart: + raise serializers.ValidationError('DTSTART required in rrule. Value should match: DTSTART:YYYYMMDDTHHMMSSZ') if not 'interval' in rrule_value.lower(): raise serializers.ValidationError('INTERVAL required in rrule') if 'tzid' in rrule_value.lower(): @@ -1405,9 +1407,14 @@ class ScheduleSerializer(BaseSerializer): raise serializers.ValidationError("BYYEARDAY not supported") if 'byweekno' in rrule_value.lower(): raise serializers.ValidationError("BYWEEKNO not supported") - if re.match(multi_by_day, rrule_value) and not re.match(".*?FREQ[\:\=](WEEKLY|YEARLY)", rrule_value): - raise serializers.ValidationError("Multiple BYDAY elements only supported with WEEKLY and YEARLY frequency") + if match_count: + count_val = match_count.groups()[0].strip().split("=") + if int(count_val[1]) > 999: + raise serializers.ValidationError("COUNT > 999 is unsupported") try: + # dtstart_group = match_dtstart.group() + # rrule_value = (rrule_value[0:match_dtstart.start()] + rrule_value[match_dtstart.end():]).strip() + # dtstart_actual = datetime.datetime.strptime(dtstart_group.split(":")[1], "%Y%m%dT%H%M%SZ") sched_rule = rrule.rrulestr(rrule_value) except Exception, e: raise serializers.ValidationError("rrule parsing failed validation") diff --git a/awx/main/tests/schedules.py b/awx/main/tests/schedules.py index 773239d61b..683a0cff21 100644 --- a/awx/main/tests/schedules.py +++ b/awx/main/tests/schedules.py @@ -20,21 +20,30 @@ from awx.main.tests.base import BaseTest, BaseTransactionTest __all__ = ['ScheduleTest'] +UNTIL_SCHEDULE = "DTSTART:20140331T075000Z RRULE:FREQ=MINUTELY;INTERVAL=1;UNTIL=30230401T075000Z" EXPIRED_SCHEDULES = ["DTSTART:19340331T055000Z RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5"] INFINITE_SCHEDULES = ["DTSTART:30340331T055000Z RRULE:FREQ=MINUTELY;INTERVAL=10"] -GOOD_SCHEDULES = ["DTSTART:30340331T055000Z RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5", - # TODO: DTSTART DOESN'T WORK WITH DAILY?!?! - #"DTSTART=20240331T075000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", - # TODO: UNTIL IS BROKEN!! - # "DTSTART=20140331T075000Z RRULE:FREQ=MINUTELY;INTERVAL=1 UNTIL=20230401T075000Z", +GOOD_SCHEDULES = ["DTSTART:20500331T055000Z RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5", + "DTSTART:20240331T075000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1", + "DTSTART:20140331T075000Z RRULE:FREQ=MINUTELY;INTERVAL=1;UNTIL=20230401T075000Z", + "DTSTART:20140331T075000Z RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,WE,FR", + "DTSTART:20140331T075000Z RRULE:FREQ=WEEKLY;INTERVAL=5;BYDAY=MO", + "DTSTART:20140331T075000Z RRULE:FREQ=MONTHLY;INTERVAL=1;BYMONTHDAY=6", + "DTSTART:20140331T075000Z RRULE:FREQ=MONTHLY;INTERVAL=1;BYSETPOS=4;BYDAY=SU", + "DTSTART:20140331T075000Z RRULE:FREQ=MONTHLY;INTERVAL=1;BYSETPOS=-1;BYDAY=MO,TU,WE,TH,FR", + "DTSTART:20140331T075000Z RRULE:FREQ=MONTHLY;INTERVAL=1;BYSETPOS=-1;BYDAY=MO,TU,WE,TH,FR,SA,SU", + "DTSTART:20140331T075000Z RRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH=4;BYMONTHDAY=1", + "DTSTART:20140331T075000Z RRULE:FREQ=YEARLY;INTERVAL=1;BYSETPOS=-1;BYMONTH=8;BYDAY=SU", + "DTSTART:20140331T075000Z RRULE:FREQ=WEEKLY;INTERVAL=1;UNTIL=20230401T075000Z;BYDAY=MO,WE,FR", + "DTSTART:20140331T075000Z RRULE:FREQ=HOURLY;INTERVAL=1;UNTIL=20230610T075000Z" ] BAD_SCHEDULES = ["", "DTSTART:20140331T055000 RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5", "RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5", "FREQ=MINUTELY;INTERVAL=10;COUNT=5", + "DTSTART:20240331T075000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=10000000", "DTSTART;TZID=US-Eastern:19961105T090000 RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5", "DTSTART:20140331T055000Z RRULE:FREQ=SECONDLY;INTERVAL=1", "DTSTART:20140331T055000Z RRULE:FREQ=SECONDLY", - "DTSTART:20140331T055000Z RRULE:FREQ=MONTHLY;BYDAY=SU,MO;INTERVAL=1", "DTSTART:20140331T055000Z RRULE:FREQ=YEARLY;BYDAY=20MO;INTERVAL=1", "DTSTART:20140331T055000Z RRULE:FREQ=MONTHLY;BYMONTHDAY=10,15;INTERVAL=1", "DTSTART:20140331T055000Z RRULE:FREQ=YEARLY;BYMONTH=1,2;INTERVAL=1", @@ -151,20 +160,25 @@ class ScheduleTest(BaseTest): with self.current_user(self.normal_django_user): data = self.post(first_url, new_schedule, expect=201) self.assertEquals(data['dtend'], None) + + long_schedule = dict(name='long_schedule', description='going for a long time', enabled=True, rrule=UNTIL_SCHEDULE) + with self.current_user(self.normal_django_user): + data = self.post(first_url, long_schedule, expect=201) + self.assertNotEquals(data['dtend'], None) def test_schedule_filtering(self): first_url = reverse('api:inventory_source_schedules_list', args=(self.first_inventory_source.pk,)) start_time = now() + datetime.timedelta(minutes=5) dtstart_str = start_time.strftime("%Y%m%dT%H%M%SZ") - new_schedule = dict(name="filter_schedule_1", enabled=True, rrule="DTSTART:%s RRULE:RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5" % dtstart_str) + new_schedule = dict(name="filter_schedule_1", enabled=True, rrule="DTSTART:%s RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5" % dtstart_str) with self.current_user(self.normal_django_user): data = self.post(first_url, new_schedule, expect=201) self.assertTrue(Schedule.objects.enabled().between(now(), now() + datetime.timedelta(minutes=10)).count(), 1) start_time = now() dtstart_str = start_time.strftime("%Y%m%dT%H%M%SZ") - new_schedule_middle = dict(name="runnable_schedule", enabled=True, rrule="DTSTART:%s RRULE:RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5" % dtstart_str) + new_schedule_middle = dict(name="runnable_schedule", enabled=True, rrule="DTSTART:%s RRULE:FREQ=MINUTELY;INTERVAL=10;COUNT=5" % dtstart_str) with self.current_user(self.normal_django_user): data = self.post(first_url, new_schedule_middle, expect=201) self.assertTrue(Schedule.objects.enabled().between(now() - datetime.timedelta(minutes=10), now() + datetime.timedelta(minutes=10)).count(), 1)