Adding rrule lookup plugin

This commit is contained in:
John Westcott IV 2020-04-16 18:02:39 -04:00 committed by beeankha
parent 0e1a2b899a
commit 12805954e0
4 changed files with 499 additions and 21 deletions

View File

@ -0,0 +1,220 @@
# (c) 2020 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
lookup: tower_schedule_rrule
author: John Westcott IV (@john-westcott-iv)
version_added: "3.7"
short_description: Generate an rrule string which can be used for Tower Schedules
description:
- Returns a string based on criteria which represent an rule
options:
_terms:
description:
- The frequency of the schedule
- none - Run this schedule once
- minute - Run this schedule ever x minutes
- hour - Run this schedule every x hours
- day - Run this schedule ever x days
- week - Run this schedule weekly
- month - Run this schedule monthly
required: True
choices: ['none', 'minute', 'hour', 'day', 'week', 'month']
start_date:
description:
- The date to start the rule
- Used for all frequencies
- Format should be YYYY-MM-DD [HH:MM:SS]
type: str
timezone:
description:
- The timezone to use for this rule
- Used for all frequencies
- Format should be as US/Eastern
- Defaults to America/New_York
type: str
every:
description:
- The repition in months, weeks, days hours or minutes
- Used for all types except none
type: int
end_on:
description:
- How to end this schedule
- If this is not defined this schedule will never end
- If this is a positive integer this schedule will end after this number of occurances
- If this is a date in the format YYYY-MM-DD [HH:MM:SS] this schedule end after this date
- Used for all types except none
type: str
on_days:
description:
- The days to run this schedule on
- A comma seperated list which can contain values sunday, monday, tuesday, wednesday, thursday, friday
- Used for week type schedules
month_day_number:
description:
- The day of the month this schedule will run on (0-31)
- Used for month type schedules
- Can not be used with on_the parameter
type: int
on_the:
description:
- A description on when this schedule will run
- Two strings seperated by space
- First string is one of first, second, third, fourth, last
- Second string is one of sunday, monday, tuesday, wednesday, thursday, friday
- Used for month type schedules
- Can not be used with month_day_number parameters
extends_documentation_fragment: awx.awx.auth
"""
EXAMPLES = """
- name: Create a string for a schedule
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', start_date='1979-09-13 03:45:07') }}"
"""
RETURN = """
_raw:
description:
- String in the rrule format
type: string
"""
from ansible.plugins.lookup import LookupBase
from ansible.errors import AnsibleError
from dateutil.rrule import rrule, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, MO, TU, WE, TH, FR, SA, SU
from datetime import datetime
import re
import pytz
class LookupModule(LookupBase):
frequencies = {
'none': DAILY,
'minute': MINUTELY,
'hour': HOURLY,
'day': DAILY,
'week': WEEKLY,
'month': MONTHLY,
}
weekdays = {
'monday': MO,
'tuesday': TU,
'wednesday': WE,
'thursday': TH,
'friday': FR,
'saturday': SA,
'sunday': SU,
}
set_positions = {
'first': 1,
'second': 2,
'third': 3,
'fourth': 4,
'last': -1,
}
def parse_date_time(self, date_string):
try:
return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S')
except ValueError:
return datetime.strptime(date_string, '%Y-%m-%d')
def run(self, terms, variables=None, **kwargs):
if len(terms) != 1:
raise AnsibleError('You may only pass one schedule type in at a time')
frequency = terms[0].lower()
if frequency not in self.frequencies:
raise AnsibleError('Frequency of {0} is invalid'.format(terms[0]))
rrule_kwargs = {
'freq': self.frequencies[frequency],
'interval': kwargs.get('every', 1),
}
# All frequencies can use a start date
if 'start_date' in kwargs:
try:
rrule_kwargs['dtstart'] = self.parse_date_time(kwargs['start_date'])
except Exception:
raise AnsibleError('Parameter start_date must be in the format YYYY-MM-DD [HH:MM:SS]')
# If we are a none frequency we don't need anything else
if frequency == 'none':
rrule_kwargs['count'] = 1
else:
# All non-none frequencies can have an end_on option
if 'end_on' in kwargs:
end_on = kwargs['end_on']
if re.match(r'^\d+$', end_on):
rrule_kwargs['count'] = end_on
else:
try:
rrule_kwargs['until'] = self.parse_date_time(end_on)
except Exception:
raise AnsibleError('Parameter end_on must either be an integer or in the format YYYY-MM-DD [HH:MM:SS]')
# A week based frequency can also take the on_days parameter
if frequency == 'week' and 'on_days' in kwargs:
days = []
for day in kwargs['on_days'].split(','):
day = day.strip()
if day not in self.weekdays:
raise AnsibleError('Parameter on_days must only contain values {0}'.format(', '.join(self.weekdays.keys())))
days.append(self.weekdays[day])
rrule_kwargs['byweekday'] = days
# A month based frequency can also deal with month_day_number and on_the options
if frequency == 'month':
if 'month_day_number' in kwargs and 'on_the' in kwargs:
raise AnsibleError('Month based frquencies can have month_day_number or on_the but not both')
if 'month_day_number' in kwargs:
try:
my_month_day = int(kwargs['month_day_number'])
if my_month_day < 1 or my_month_day > 31:
raise Exception()
except Exception:
raise AnsibleError('month_day_number must be between 1 and 31')
rrule_kwargs['bymonthday'] = my_month_day
if 'on_the' in kwargs:
try:
(occurance, weekday) = kwargs['on_the'].split(' ')
except Exception:
raise AnsibleError('on_the parameter must be two space seperated words')
if weekday not in self.weekdays:
raise AnsibleError('Weekday portion of on_the parameter is not valid')
if occurance not in self.set_positions:
raise AnsibleError('The first string of the on_the parameter is not valid')
rrule_kwargs['byweekday'] = self.weekdays[weekday]
rrule_kwargs['bysetpos'] = self.set_positions[occurance]
my_rule = rrule(**rrule_kwargs)
# All frequencies can use a timezone but rrule can't support the format that tower uses.
# So we will do a string manip here if we need to
timezone = 'America/New_York'
if 'timezone' in kwargs:
if kwargs['timezone'] not in pytz.all_timezones:
raise AnsibleError('Timezone parameter is not valid')
timezone = kwargs['timezone']
# rrule puts a \n in the rule instad of a space and can't hand timezones
return_rrule = str(my_rule).replace('\n', ' ').replace('DTSTART:', 'DTSTART;TZID={0}:'.format(timezone))
# Tower requires an interval. rrule will not add interval if its set to 1
if kwargs.get('every', 1) == 1:
return_rrule = "{0};INTERVAL=1".format(return_rrule)
return return_rrule

View File

@ -26,6 +26,7 @@ options:
rrule:
description:
- A value representing the schedules iCal recurrence rule.
- See rrule plugin for help constructing this value
required: False
type: str
name:
@ -125,6 +126,23 @@ extends_documentation_fragment: awx.awx.auth
'''
EXAMPLES = '''
- name: Build a schedule for Demo Job Template
tower_schedule:
name: "{{ sched1 }}"
state: present
unified_job_template: "Demo Job Template"
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
ignore_errors: True
- name: Build the same schedule using the rrule plugin
tower_schedule:
name: "{{ sched1 }}"
state: present
unified_job_template: "Demo Job Template"
rrule: "{{ query('awx.awx.tower_schedule_rrule', 'week', start_date='2019-12-19 13:05:51') }}"
register: result
ignore_errors: True
'''
from ..module_utils.tower_api import TowerModule

View File

@ -7,26 +7,14 @@
set_fact:
sched1: "AWX-Collection-tests-tower_schedule-sched1-{{ test_id }}"
# rrule:
# description:
# extra_data:
# inventory:
# scm_branch:
# job_type:
# job_tags:
# skip_tags:
# limit:
# diff_mode:
# verbosity:
- name: Try to create without an rrule
tower_schedule:
name: "{{ sched1 }}"
state: present
unified_job_template: "Demo Job Template"
enabled: True
enabled: true
register: result
ignore_errors: True
ignore_errors: true
- assert:
that:
@ -48,11 +36,11 @@
job_tags: other_tags
skip_tags: some_tags
limit: node1
diff_mode: True
diff_mode: true
verbosity: 4
enabled: True
enabled: true
register: result
ignore_errors: True
ignore_errors: true
- assert:
that:
@ -66,7 +54,7 @@
unified_job_template: "Demo Job Template"
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
ignore_errors: True
ignore_errors: true
- assert:
that:
@ -79,7 +67,7 @@
unified_job_template: "Demo Job Template"
rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1"
register: result
ignore_errors: True
ignore_errors: true
- assert:
that:
@ -89,9 +77,9 @@
tower_schedule:
name: "{{ sched1 }}"
state: present
enabled: "False"
enabled: "false"
register: result
ignore_errors: True
ignore_errors: true
- assert:
that:

View File

@ -0,0 +1,252 @@
---
- name: Test too many params
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', 'weekly', start_date='2020-4-16 03:45:07') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'You may only pass one schedule type in at a time' in result.msg"
- name: Test invalid frequency
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'john', start_date='2020-4-16 03:45:07') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'Frequency of john is invalid' in result.msg"
- name: Test an invalid start date
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', start_date='invalid') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'Parameter start_date must be in the format YYYY-MM-DD' in result.msg"
- name: Test with a valid start date (no time) (also tests none frequency and count)
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', start_date='2020-04-16') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T000000 RRULE:FREQ=DAILY;COUNT=1;INTERVAL=1'
- name: Test with a valid start date and time
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'none', start_date='2020-04-16 03:45:07') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=DAILY;COUNT=1;INTERVAL=1'
- name: Test end_on as count
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'minute', start_date='2020-4-16 03:45:07', end_on='2') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=1'
- name: Test end_on as date
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'minute', start_date='2020-4-16 03:45:07', end_on='2020-4-17 03:45:07') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MINUTELY;UNTIL=20200417T034507;INTERVAL=1'
- name: Test end_on as junk
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'minute', start_date='2020-4-16 03:45:07', end_on='junk') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"Parameter end_on must either be an integer or in the format YYYY-MM-DD" in result.msg'
- name: Test on_days as junk
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'week', start_date='2020-4-16 03:45:07', on_days='junk') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"Parameter on_days must only contain values monday, tuesday, wednesday, thursday, friday, saturday, sunday" in result.msg'
- name: Test on_days as a single day
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'week', start_date='2020-4-16 03:45:07', on_days='saturday') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=WEEKLY;BYDAY=SA;INTERVAL=1'
- name: Test on_days as multiple days (with some whitespaces)
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'week', start_date='2020-4-16 03:45:07', on_days='saturday,monday , friday') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=WEEKLY;BYDAY=MO,FR,SA;INTERVAL=1'
- name: Test combo of both month_day_number and on_the
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', on_the='something', month_day_number='else') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"Month based frquencies can have month_day_number or on_the but not both" in result.msg'
- name: Test month_day_number as not an integer
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', month_day_number='junk') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"month_day_number must be between 1 and 31" in result.msg'
- name: Test month_day_number < 1
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', month_day_number='0') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"month_day_number must be between 1 and 31" in result.msg'
- name: Test month_day_number > 31
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', month_day_number='32') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"month_day_number must be between 1 and 31" in result.msg'
- name: Test valid month_day_number
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', month_day_number='18') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MONTHLY;BYMONTHDAY=18;INTERVAL=1'
- name: Test on_the as junk
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', on_the='junk') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"on_the parameter must be two space seperated words" in result.msg'
- name: Test on_the with invalid occurance
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', on_the='junk wednesday') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"The first string of the on_the parameter is not valid" in result.msg'
- name: Test on_the with invalid weekday
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', on_the='second junk') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- '"Weekday portion of on_the parameter is not valid" in result.msg'
- name: Test a valid on_the
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', on_the='second sunday') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=America/New_York:20200416T034507 RRULE:FREQ=MONTHLY;BYSETPOS=2;BYDAY=SU;INTERVAL=1'
- name: Test an invalid timezone
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', timezone='junk') }}"
ignore_errors: true
register: result
- assert:
that:
- result is failed
- "'Timezone parameter is not valid' in result.msg"
- name: Test an valid timezone
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month', start_date='2020-4-16 03:45:07', timezone='Zulu') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed
- result.msg == 'DTSTART;TZID=Zulu:20200416T034507 RRULE:FREQ=MONTHLY;INTERVAL=1'
- name: Test an empty schedule
debug:
msg: "{{ query('awx.awx.tower_schedule_rrule', 'month') }}"
ignore_errors: true
register: result
- assert:
that:
- result is not failed