Simplify and update-governance

The existing version does not handle projects under the "distributed" leadership model, if also badly damages projects.yaml and does unneeded, potentially error prone, work to determine the election results

Simplify the tool to address these issues

Change-Id: Icc67336742f43030d4e1743c6db691fafc250cfa
This commit is contained in:
Tony Breeds 2023-07-12 15:09:57 +10:00
parent 64e7417bcc
commit 73ed3f4653
3 changed files with 149 additions and 92 deletions

View File

@ -11,119 +11,170 @@
# under the License.
import argparse
import datetime
import enum
import os
import re
from pprint import pprint # noqa
from openstack_election import config
from openstack_election import utils
from openstack_election import yamlutils
import ruamel.yaml
from ruamel.yaml.comments import CommentedMap
conf = config.load_conf()
def set_TC_apointee(candidates_list, project, candidate):
print('TC Appointee for %s (%s/%s/%s)' %
(project, candidate['fullname'], candidate['ircname'],
candidate['email']))
candidates_list['candidates'][project] = [candidate]
class TC_Status(enum.Enum):
CANDIDATE = 0
EXISTING = 1
EXPIRED = 2
ELECTED = 3
def set_election_winner(candidates_list, project, idx):
candidate = candidates_list['candidates'][project][idx]
print('Setting %s PTL from election to (%s/%s/%s)' %
(project, candidate['fullname'], candidate['ircname'],
candidate['email']))
candidates_list['candidates'][project] = [candidate]
def load_election_results(election):
ptl_results_fname = os.path.join(".", "doc", "source", "results",
election, "ptl.yaml")
ptl_data = yamlutils.load(ptl_results_fname)
tc_results_fname = os.path.join(".", "doc", "source", "results",
election, "tc.yaml")
tc_data = yamlutils.load(tc_results_fname)
def do_fixup(candidates_list, project, idx, key, value):
old = candidates_list['candidates'][project][0][key]
print('Fixing %s/%s(%d) changing %s to %s' %
(project, key, idx, old, value))
candidates_list['candidates'][project][0][key] = value
def load_candidates():
print('Loading Candidates')
candidates_list = utils.build_candidates_list()
print('Done')
return candidates_list
return {"ptl": ptl_data, "tc": tc_data["candidates"]["TC"]}
def load_projects(projects_fname):
with open(projects_fname) as fh:
data = fh.readlines()
return data
return yamlutils.load(projects_fname)
def update_projects(projects_fname, candidates_list, projects):
results = utils.get_ptl_results()
project_count = 0
with open(projects_fname, 'w') as fh:
skip = 0
for line in projects:
if skip > 0:
skip -= 1
continue
# Projects are detectable as they have no whitespace in column 0
match = re.match('^([^ \t][^:]+?):$', line)
if match:
project_count += 1
p = utils.name2dir(match.group(1))
try:
candidates = candidates_list['candidates'][p]
except KeyError:
# Add placeholder for required TC appointment in cases
# where there is no candidate
candidates = [{
'fullname': 'APPOINTMENT NEEDED',
'ircname': '',
'email': 'example@example.org',
}]
print('TC to appoint PTL for %s' % (p))
nr_candidates = len(candidates)
# Remove non-elected candidates if the election is closed
# TODO(fungi): rework this entire function to just use the
# election results file if we have one and not iterate over
# the candidates tree
if nr_candidates > 1:
for c1 in results['candidates'].get(p, []):
if not c1['elected']:
for c2 in list(candidates):
if c1['email'] == c2['email']:
candidates.remove(c2)
nr_candidates = len(candidates)
# Only update the PTL if there is a single candidate
if nr_candidates == 1:
# Replace empty IRC nick strings with something useful
if not candidates[0]['ircname']:
candidates[0]['ircname'] = 'No nick supplied'
line += ((' ptl:\n' +
' name: %(fullname)s\n' +
' irc: %(ircname)s\n' +
' email: %(email)s\n') % (candidates[0]))
# This is a little fragile but the std. form is that the 4
# lines after the project name are the PTL details. We've
# just written out new record so skip the next 4 lines in
# the projects file.
skip = 4
else:
print('Skipping %s election in progress %d candidates' %
(p, nr_candidates))
fh.write(line)
def write_projects(projects_fname, projects):
# FIXME: Extract the rumel code from _write_tc and use here.
with open(projects_fname, "w") as f:
f.write(yamlutils.dumps(projects))
print('Processed %d projects' % (project_count))
def update_projects(projects, results):
for project_name in projects:
dir_name = utils.name2dir(project_name)
ptl = projects[project_name].get("ptl", {})
leadership_type = projects[project_name].get("leadership_type")
if dir_name in results["ptl"]["candidates"]:
elected_ptl = results["ptl"]["candidates"][dir_name][0]
ptl["name"] = elected_ptl["fullname"] or ptl["name"]
ptl["irc"] = elected_ptl["ircname"] or ptl["irc"]
ptl["email"] = elected_ptl["email"] or ptl["email"]
elif dir_name in results["ptl"]["leaderless"]:
ptl["name"] = "APPOINTMENT NEEDED"
ptl["irc"] = ""
ptl["email"] = "example@example.org"
print(f"{project_name} is leaderless!")
elif leadership_type in ["distributed"]:
print(f"{project_name} is \"{leadership_type}\" leadership type")
else:
print(f"{project_name} is Unknown to the election tooling")
def load_tc_members(tc_members_fname):
return yamlutils.load(tc_members_fname)
def write_tc_members(tc_members_fname, tc_members):
def none_representer(dumper, data):
return dumper.represent_scalar('tag:yaml.org,2002:null', 'null')
# This is copied from the governance/yamltools.py
yaml = ruamel.yaml.YAML(typ='rt')
yaml.width = 256
yaml.allow_duplicate_keys = True
yaml.representer.add_representer(type(None), none_representer)
yaml.indent(mapping=2, sequence=4, offset=2)
yaml.Constructor.add_constructor(
'!encrypted/pkcs1-oaep',
ruamel.yaml.SafeConstructor.construct_yaml_seq,
)
with open(tc_members_fname, "w") as f:
yaml.dump(tc_members, f)
def update_tc_members(tc_members, results):
now = datetime.datetime.now().date()
memberid_map = {}
for tc_member in tc_members:
memberid = tc_member["memberid"]
memberid_map[memberid] = tc_member
date = datetime.datetime.strptime(tc_member["date"], "%B %Y").date()
tc_status = TC_Status.EXISTING
# Find candidates that were elected more than 10 months ago
# Using 40 weeks incase the election schedules are off by a little.
if (now - date) > datetime.timedelta(weeks=40):
# We don't remove them at this stage as it's possible an existing
# TC member will be re-elected so flag them and remoev them later if needed
tc_status = TC_Status.EXPIRED
tc_member["tc_status"] = tc_status
tc_member["role"] = None
for candidate in results["tc"]:
if not candidate["elected"]:
continue
tc_status = TC_Status.ELECTED
memberid = utils.lookup_member(candidate["email"])["data"][0]["id"]
if memberid in memberid_map:
memberid_map[memberid]["date"] = now.strftime("%B %Y")
memberid_map[memberid]["tc_status"] = tc_status
else:
member = CommentedMap()
member["name"] = candidate["fullname"]
member["irc"] = candidate["ircname"]
member["email"] = candidate["email"]
member["memberid"] = memberid
member["date"] = now.strftime("%B %Y")
member["role"] = None
member["tc_status"] = tc_status
tc_members.append(member)
# Mutate the existing list to remove the expired TC members
tc_members[:] = [m for m in tc_members if m["tc_status"] != TC_Status.EXPIRED] # noqa E501
# The TC members file has elements separated by blank lines.
# This preserves that to reduce the diff.
blank_line = ruamel.yaml.tokens.CommentToken('\n\n',
ruamel.yaml.error.CommentMark(10), # noqa E501
None)
for member in tc_members:
member.ca.items['role'] = [None, None, blank_line, None]
# Remove the synthetic tc_status element as we don't need that written to disk
del member["tc_status"]
# Also, remove the last blank line.
del member.ca.items['role']
def main():
description = ('Update openstack/gorernance:reference/projects.yaml '
' with the new PTL details')
description = ("Update openstack/gorernance:reference/projects.yaml "
" with the new PTL details")
parser = argparse.ArgumentParser(description)
parser.add_argument('--governance-repo', dest='governance_repo',
parser.add_argument("--governance-repo", dest="governance_repo",
required=True,
help=('Path to a clone of the governance repo'))
help=("Path to a clone of the governance repo"))
parser.add_argument("--election-name", dest="election",
default=conf["release"],
help=(""))
args = parser.parse_args()
results = load_election_results(args.election)
projects_fname = os.path.join(os.path.expanduser(args.governance_repo),
'reference', 'projects.yaml')
"reference", "projects.yaml")
projects = load_projects(projects_fname)
candidates_list = load_candidates()
update_projects(projects_fname, candidates_list, projects)
update_projects(projects, results)
write_projects(projects_fname, projects)
tc_members_fname = os.path.join(os.path.expanduser(args.governance_repo),
"reference", "members.yaml")
tc_members = load_tc_members(tc_members_fname)
update_tc_members(tc_members, results)
write_tc_members(tc_members_fname, tc_members)
return 0

View File

@ -32,3 +32,9 @@ def loads(blob):
"""Load a yaml blob and retain key ordering."""
yaml = ruamel.yaml.YAML()
return yaml.load(blob)
def load(path):
with open(path) as f:
data = f.read()
return loads(data)

View File

@ -35,4 +35,4 @@ commands = ci-check-all-candidate-files -v -v {posargs:--HEAD}
commands = ci-check-all-candidate-files
[flake8]
exclude=.tox,doc/source/conf.py,build
exclude=.tox,doc/source/conf.py,build,.venv