
Most of the election tooling work on seperate events of PTL and TC election. If election is combined then setup_election_config generate the combined event( for example 'TC & PTL Election') and it does not work with those tools/scripts. This fixes it and even for combined election it generates the separate events. If we look at the election site, the separate events are more clear for users. Generated config looks like below: Change-Id: I7d2e540dea9d89c832f6d9e3e9da743fe8d39295 --- release: 2023.2 election_type: combined tag: mar-2023-elections tc_seats: 4 timeframe: name: Zed-2023.1 start: 2022-03-11T00:00 end: 2023-02-15T00:00 email_deadline: 2023-02-15T00:00 timeline: - name: TC Nominations start: 2023-02-01T23:45 end: 2023-02-15T23:45 - name: PTL Nominations start: 2023-02-01T23:45 end: 2023-02-15T23:45 - name: TC Campaigning start: 2023-02-15T23:45 end: 2023-02-22T23:45 - name: TC Election start: 2023-02-22T23:45 end: 2023-03-08T23:45 - name: PTL Election start: 2023-02-22T23:45 end: 2023-03-08T23:45 Change-Id: I5b1766d0fce9eb7ded000a7b582a8e4086314831
291 lines
12 KiB
Python
Executable File
291 lines
12 KiB
Python
Executable File
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import argparse
|
|
import datetime
|
|
import pytz
|
|
import sys
|
|
import yaml
|
|
|
|
from collections import OrderedDict
|
|
|
|
from openstack_election.config import ISO_FMT
|
|
from openstack_election import utils
|
|
|
|
ONE_WEEK = datetime.timedelta(weeks=1)
|
|
TWO_WEEK = datetime.timedelta(weeks=2)
|
|
election_parameters = {
|
|
'PTL': {
|
|
'milestone': 'Release',
|
|
'weeks_prior': 2,
|
|
'events': ['Election', 'Nominations', ],
|
|
},
|
|
'TC': {
|
|
'milestone': 'Release',
|
|
'weeks_prior': 2,
|
|
'events': ['Election', 'Campaigning', 'Nominations', ],
|
|
},
|
|
'combined': {
|
|
'milestone': 'Release',
|
|
'weeks_prior': 2,
|
|
'events': ['Election', 'Campaigning', 'Nominations'],
|
|
},
|
|
}
|
|
|
|
|
|
def _dict_representer(dumper, data):
|
|
return dumper.represent_dict(data.items())
|
|
|
|
|
|
def valid_version(value):
|
|
try:
|
|
value = float(value)
|
|
except ValueError:
|
|
msg = ("{} is not an valid release version. Pass release version, for"
|
|
" example 2023.1".format(value))
|
|
raise Exception(msg)
|
|
return value
|
|
|
|
|
|
def valid_date(opt):
|
|
try:
|
|
d = datetime.datetime.strptime(opt, "%Y-%m-%d")
|
|
except ValueError:
|
|
msg = "Not a valid date: '{0}'.".format(opt)
|
|
raise argparse.ArgumentTypeError(msg)
|
|
return d.replace(tzinfo=pytz.UTC)
|
|
|
|
|
|
def find_previous_wednesday(date):
|
|
# The are smarter ways to do this
|
|
while date.weekday() != 2:
|
|
date -= datetime.timedelta(days=1)
|
|
return date
|
|
|
|
|
|
def iso_fmt(d):
|
|
return d.strftime(ISO_FMT)
|
|
|
|
|
|
def validate_tc_charter(election_type, release_schedule,
|
|
selected_start, selected_end):
|
|
# NOTE (gmann): This function will validate the selected start and
|
|
# end date by this script against what TC charter says.
|
|
# As per the translation from current charter(as of 2023-03-03):
|
|
# - PTL election needs to be held(start and end) on or before R-2 week
|
|
# - TC election needs to be held(start and end) in between of R-8 to R-2
|
|
# week.
|
|
expected_start_date = 0
|
|
expected_end_date = 0
|
|
for week in release_schedule.get('cycle', []):
|
|
if election_type == 'PTL':
|
|
expected_start_date = datetime.datetime.strptime(
|
|
str(release_schedule['start-week']),
|
|
"%Y-%m-%d").replace(tzinfo=pytz.UTC)
|
|
else:
|
|
if week.get('name') == 'R-8':
|
|
expected_start_date = datetime.datetime.strptime(
|
|
week['start'], "%Y-%m-%d").replace(tzinfo=pytz.UTC)
|
|
if week.get('name') == 'R-2':
|
|
expected_end_date = datetime.datetime.strptime(
|
|
week['end'], "%Y-%m-%d").replace(tzinfo=pytz.UTC)
|
|
if expected_start_date and expected_end_date:
|
|
break
|
|
if (selected_start < expected_start_date or
|
|
selected_end > expected_end_date):
|
|
print("Error: generated start and end date as per given dates in\n"
|
|
" parameter is not matching with the TC charter,\n"
|
|
" please select the release date correctly.")
|
|
exit(1)
|
|
|
|
|
|
def select_release_end_date(release_name, release_schedule):
|
|
date = None
|
|
event = '%s-final' % (release_name[0:1])
|
|
for week in release_schedule.get('cycle', []):
|
|
if event in week.get('x-project', []):
|
|
date = valid_date(week['end'])
|
|
return date
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=('Given a release '
|
|
'date pick some dates for '
|
|
'the election'))
|
|
parser.add_argument('release', type=valid_version,
|
|
help='release version. Example 2023.2')
|
|
parser.add_argument('type', choices=['TC', 'PTL', 'combined'])
|
|
parser.add_argument('--tc-seats', default=4, choices=['4', '5'],
|
|
help='number of TC seats up for election')
|
|
# If date is not passed in command line then, this script will
|
|
# automatically pick the release end date.
|
|
parser.add_argument('--date', default=None, type=valid_date,
|
|
help='Date from release schedule in the form '
|
|
'YYYY-MM-DD')
|
|
|
|
args = parser.parse_args()
|
|
|
|
params = election_parameters[args.type]
|
|
offset = datetime.timedelta(weeks=params['weeks_prior'])
|
|
|
|
# We need to know the releases in order. Fortunately this data exists
|
|
# in the releases repo in a really easy format to understand.
|
|
series_data = utils.get_series_data()
|
|
# TODO(gmann): release versions (release-id) is introduced from 2023.1
|
|
# (Antelope) cycle and before that we have only release 'name'. We fetch
|
|
# last 3 release data here so until 2023.1 we can fecth release-ids and
|
|
# rest with 'name'.
|
|
# We will use release version for election tooling and release repo is not
|
|
# yet fully moved to the release version so continue using the release
|
|
# name to fetch the data from release repo.
|
|
release_data = []
|
|
names = []
|
|
for x in series_data:
|
|
names.append(x['name'])
|
|
if 'release-id' in x:
|
|
release_data.append(str(x['release-id']))
|
|
else:
|
|
release_data.append(x['name'])
|
|
# Find where in the list the requested release sits. This will typically
|
|
# be very early but don't assume that.
|
|
idx = (release_data.index(str(args.release)) if str(args.release)
|
|
in release_data else -1)
|
|
# If date is not passed to this script then autimatically
|
|
# select the release end date.
|
|
if (args.date is None):
|
|
schedule = utils.get_schedule_data(names[idx+1])
|
|
args.date = select_release_end_date(names[idx+1], schedule)
|
|
if args.date is None:
|
|
print("Error: no end date found in series data")
|
|
exit(1)
|
|
|
|
# Given the release history:
|
|
# Stein, Rocky[0], Queens[1], Pike[2], Ocata[3]
|
|
# For the Stein elections candidates come from the previous 2 cycles so:
|
|
# Rocky and Queens.
|
|
timeframe_name = '%s-%s' % (release_data[idx+2].capitalize(),
|
|
release_data[idx+1].capitalize())
|
|
|
|
# The Queens development cycle begins when we branch Pike-RC1, so collect
|
|
# that date from the releases data.
|
|
schedule = utils.get_schedule_data(names[idx+3])
|
|
event = '%s-rc1' % (names[idx+3][0:1])
|
|
for week in schedule.get('cycle', []):
|
|
if event in week.get('x-project', []):
|
|
timeframe_start = valid_date(week['end'])
|
|
|
|
print('Setting %s Election\n%s is at: %s' % (args.type,
|
|
params['milestone'],
|
|
args.date.date()))
|
|
end = args.date - offset
|
|
print('Latest possible completion is at: %s' % (end.date()))
|
|
end = find_previous_wednesday(end)
|
|
print('Moving back to Wednesday: %s' % (end.date()))
|
|
|
|
end = end.replace(hour=23, minute=45)
|
|
events = []
|
|
for event in params['events']:
|
|
e_types = [args.type]
|
|
if args.type == 'combined':
|
|
e_types = ['PTL', 'TC']
|
|
if event == 'Campaigning':
|
|
e_types = ['TC']
|
|
start = end - TWO_WEEK
|
|
# For a TC or combined election we want the email deadline to match the
|
|
# beginning of the Campaigning period, which gives the officials time
|
|
# to validate the rolls
|
|
if args.type in ['TC', 'combined'] and event == 'Campaigning':
|
|
# NOTE(gmann): As per TC charter, TC campaigning duration is
|
|
# one week.
|
|
start = end - ONE_WEEK
|
|
email_deadline = start.replace(hour=0, minute=0)
|
|
# Otherwise for a PTL election we want to set the email deadline to the
|
|
# begining of the Nomination period, again to give officials time to
|
|
# validate the rolls
|
|
elif args.type == 'PTL' and event == 'Nominations':
|
|
email_deadline = start.replace(hour=0, minute=0)
|
|
if event == 'Election':
|
|
schedule = utils.get_schedule_data(names[idx+1])
|
|
validate_tc_charter(args.type, schedule, start, end)
|
|
for e_type in e_types:
|
|
name = '%s %s' % (e_type, event)
|
|
events.insert(0, OrderedDict(name=name,
|
|
start=iso_fmt(start),
|
|
end=iso_fmt(end)))
|
|
print('%s from %s to %s' % (name, iso_fmt(start), iso_fmt(end)))
|
|
end = start
|
|
|
|
print('Set email_deadline to %s' % (iso_fmt(email_deadline)))
|
|
|
|
if args.type == 'PTL':
|
|
# For a PTL election we haven't closed the current cycle so we set the
|
|
# timeframe right up to the beginning of the nomination period.
|
|
print('Setting PTL timeframe end to email_deadline')
|
|
timeframe_end = email_deadline
|
|
else:
|
|
# For a TC election we should have completed the previous cycle so grab
|
|
# the release date for it. It is however possible that the gap between
|
|
# that release and the summit doesn't allow for an election. In that
|
|
# case we need to use the email_deadline for timeframe_end
|
|
|
|
# Grab the rlease data and fromvert it to a datetime
|
|
timeframe_end = series_data[idx+1]['initial-release']
|
|
timeframe_end = datetime.datetime.combine(timeframe_end,
|
|
datetime.time(0, 0))
|
|
timeframe_end = timeframe_end.replace(tzinfo=pytz.UTC)
|
|
|
|
if timeframe_end < email_deadline:
|
|
print('Setting TC timeframe end to %s Release date %s' %
|
|
(series_data[idx+1]['name'], iso_fmt(timeframe_end)))
|
|
else:
|
|
timeframe_end = email_deadline
|
|
print('Setting TC timeframe end to email_deadline')
|
|
|
|
print('Begining of %s Cycle @ %s' % (release_data[idx+2].capitalize(),
|
|
timeframe_start))
|
|
print('End of %s cycle @ %s' % (release_data[idx+1].capitalize(),
|
|
timeframe_end))
|
|
|
|
timeframe_span = timeframe_end - timeframe_start
|
|
timeframe_span_ok = (datetime.timedelta(11*365/12) <=
|
|
timeframe_span <=
|
|
datetime.timedelta(13*365/12))
|
|
print('Election timeframe: %ss' % (timeframe_span))
|
|
|
|
if not timeframe_span_ok:
|
|
print('Looks like election timespan is outside of \'normal\'')
|
|
print('Minimum: %s' % (datetime.timedelta(11*365/12)))
|
|
print('Current: %s' % (timeframe_span))
|
|
print('Maximum: %s' % (datetime.timedelta(13*365/12)))
|
|
|
|
configuration = OrderedDict(
|
|
release=args.release,
|
|
election_type=args.type.lower(),
|
|
tag=args.date.strftime('%b-%Y-elections').lower(),
|
|
tc_seats=int(args.tc_seats),
|
|
timeframe=OrderedDict(name=timeframe_name,
|
|
start=iso_fmt(timeframe_start),
|
|
end=iso_fmt(timeframe_end),
|
|
email_deadline=iso_fmt(email_deadline)
|
|
),
|
|
timeline=events,
|
|
)
|
|
yaml.Dumper.add_representer(OrderedDict, _dict_representer)
|
|
print(yaml.dump(configuration, default_flow_style=False,
|
|
default_style='', explicit_start=True))
|
|
|
|
return 0 if timeframe_span_ok else 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|