44d6736a5e
Per best practices, explicitly use the python3 executable rather than assuming the platform with have a "python" executable that maps to python3. https://www.python.org/dev/peps/pep-0394/ Change-Id: I39b8a1013a891f4570f374d8faa3cfa2ecaf3347 Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
245 lines
8.7 KiB
Python
Executable File
245 lines
8.7 KiB
Python
Executable File
#!/usr/bin/python3
|
|
#
|
|
# Handle pre-release / post-release ACLs for milestone-driven projects
|
|
#
|
|
# Copyright 2016 Thierry Carrez <thierry@openstack.org>
|
|
# All Rights Reserved.
|
|
#
|
|
# 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 getpass
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
import requests
|
|
from requests.packages import urllib3
|
|
import yaml
|
|
|
|
import openstack_releases
|
|
from openstack_releases import defaults
|
|
from openstack_releases import deliverable
|
|
|
|
# Turn of warnings about bad SSL config.
|
|
# https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings
|
|
urllib3.disable_warnings()
|
|
|
|
GERRIT_URL = 'https://review.opendev.org/'
|
|
|
|
EXCEPTIONS = ['openstack/release-test']
|
|
|
|
|
|
def repositories_list(deliverables_dir, series):
|
|
"""Yields (team, repo) tuples for cycle-with-milestones deliverables"""
|
|
deliverables = deliverable.Deliverables(deliverables_dir)
|
|
for d in deliverables.get_deliverables(None, series):
|
|
if d.model not in ['cycle-with-milestones', 'cycle-with-rc']:
|
|
continue
|
|
if not d.repos:
|
|
print('WARNING: no releases for {} in {}'.format(d.name, d.series))
|
|
for repo in sorted(d.repos):
|
|
if repo not in EXCEPTIONS:
|
|
yield (d.team, repo)
|
|
|
|
|
|
def patch_acls(args):
|
|
"""Handles the acls action"""
|
|
|
|
blob = """[access "refs/heads/stable/{branch}"]
|
|
abandon = group Change Owner
|
|
abandon = group Project Bootstrappers
|
|
abandon = group {group}
|
|
exclusiveGroupPermissions = abandon label-Code-Review label-Workflow
|
|
label-Code-Review = -2..+2 group Project Bootstrappers
|
|
label-Code-Review = -2..+2 group {group}
|
|
label-Code-Review = -1..+1 group Registered Users
|
|
label-Workflow = -1..+0 group Change Owner
|
|
label-Workflow = -1..+1 group Project Bootstrappers
|
|
label-Workflow = -1..+1 group {group}
|
|
|
|
"""
|
|
# Load repo/aclfile mapping from Gerrit config
|
|
projectsyaml = os.path.join(args.repository, 'gerrit', 'projects.yaml')
|
|
acl = {}
|
|
config = yaml.load(open(projectsyaml))
|
|
for project in config:
|
|
aclfilename = project.get('acl-config')
|
|
if aclfilename:
|
|
(head, tail) = os.path.split(aclfilename)
|
|
acl[project['project']] = os.path.join(os.path.basename(head),
|
|
tail)
|
|
else:
|
|
acl[project['project']] = project['project'] + '.config'
|
|
|
|
# Get the list of ACL files to update
|
|
aclfiles = {}
|
|
for team, repo in repositories_list(args.deliverables_dir, args.series):
|
|
try:
|
|
to_update = acl[repo]
|
|
print('ACLs for {} owned by {} to be updated in {}'.format(
|
|
repo, team, to_update))
|
|
except KeyError:
|
|
print('No ACL file defined for %s' % repo)
|
|
raise RuntimeError('No ACL file defined for %s' % repo)
|
|
aclfiles[to_update] = team
|
|
|
|
for aclfn, teamname in aclfiles.items():
|
|
newcontent = ''
|
|
fullfilename = os.path.join(args.repository, 'gerrit', 'acls', aclfn)
|
|
group = '%s-release-branch' % teamname
|
|
print('Patching %s' % fullfilename)
|
|
if args.dryrun:
|
|
print('Adding stable/%s ACL with rights for %s' %
|
|
(args.series, group))
|
|
else:
|
|
with open(fullfilename) as aclfile:
|
|
hit = False
|
|
skip = False
|
|
for line in aclfile:
|
|
# Skip until start of next section if in skip mode
|
|
if skip:
|
|
if line.startswith('['):
|
|
skip = False
|
|
else:
|
|
continue
|
|
|
|
if re.match('^\[access "refs/heads/stable/[a-z]', line):
|
|
# We just hit a specific stable section.
|
|
# Skip the file until the next section starts.
|
|
skip = True
|
|
continue
|
|
|
|
if ((line.startswith("[receive]") or
|
|
line.startswith('[access "refs/tag')) and not hit):
|
|
# We reached the a later section: let's place
|
|
# our specific stable section here.
|
|
newcontent += blob.format(
|
|
branch=args.series,
|
|
group=group)
|
|
hit = True
|
|
|
|
# Copy the current line over
|
|
newcontent += line
|
|
|
|
if not hit:
|
|
print("Could not update %s automatically" % fullfilename)
|
|
|
|
with open(fullfilename, 'w') as aclfile:
|
|
aclfile.write(newcontent)
|
|
|
|
|
|
def gerrit_group_membership_test(gerritauth, action, group, member):
|
|
"""Test for Gerrit group membership based on action taken"""
|
|
|
|
call = 'a/groups/%s/groups/%s' % (group, member)
|
|
r = requests.get(GERRIT_URL + call, auth=gerritauth)
|
|
if action == 'PUT':
|
|
# For PUT operations, return true if member is missing
|
|
return r.status_code == 404
|
|
else:
|
|
# For DELETE operations, return true if member already there
|
|
return r.status_code == 200
|
|
|
|
|
|
def modify_gerrit_groups(args):
|
|
"""Handles the 'groups' action"""
|
|
|
|
gerritauth = requests.auth.HTTPDigestAuth(args.username, getpass.getpass())
|
|
if args.stage == 'pre_release':
|
|
# At pre-release stage we want to have $PROJECT-release and
|
|
# Release Managers (and remove $PROJECT-stable-maint if present)
|
|
actions = [
|
|
('PUT', lambda x: '%s-release' % x),
|
|
('PUT', lambda x: 'Release Managers'),
|
|
('DELETE', lambda x: '%s-stable-maint' % x),
|
|
]
|
|
elif args.stage == 'post_release':
|
|
# At post-release stage we want to have $PROJECT-stable-maint
|
|
# (and remove Release Managers and $PROJECT-release if present)
|
|
actions = [
|
|
('PUT', lambda x: '%s-stable-maint' % x),
|
|
('DELETE', lambda x: 'Release Managers'),
|
|
('DELETE', lambda x: '%s-release' % x),
|
|
]
|
|
|
|
# Build the list of calls to make
|
|
print('Computing the list of modifications')
|
|
calls = set()
|
|
for team, repo in repositories_list(args.deliverables_dir, args.series):
|
|
group = '%s-release-branch' % team
|
|
for (verb, memberformat) in actions:
|
|
member = memberformat(team)
|
|
# Filter based on already-handled names
|
|
if gerrit_group_membership_test(gerritauth, verb, group, member):
|
|
calls.add((verb, group, member))
|
|
else:
|
|
print("Skipping %s %s in %s (already done)" %
|
|
(verb, member, group))
|
|
|
|
for verb, group, member in calls:
|
|
call = 'a/groups/%s/groups/%s' % (group, member)
|
|
print('Updating %s group using %s %s' % (group, verb, call))
|
|
if not args.dryrun:
|
|
r = requests.request(verb, GERRIT_URL + call, auth=gerritauth)
|
|
if r.status_code not in (201, 204):
|
|
print('Error (%d) while updating group' % r.status_code)
|
|
|
|
|
|
def main(args=sys.argv[1:]):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
'--dryrun',
|
|
default=False,
|
|
help='do not actually do anything',
|
|
action='store_true')
|
|
parser.add_argument(
|
|
'--deliverables-dir',
|
|
default=openstack_releases.deliverable_dir,
|
|
help='location of deliverable files',
|
|
)
|
|
parser.add_argument(
|
|
'--series',
|
|
default=defaults.RELEASE,
|
|
help='release series to use for repo/team membership',
|
|
)
|
|
subparsers = parser.add_subparsers(title='commands')
|
|
|
|
do_acls = subparsers.add_parser(
|
|
'acls',
|
|
help='patch ACL files')
|
|
do_acls.add_argument(
|
|
'repository',
|
|
help='location of the local project-config repository')
|
|
do_acls.set_defaults(func=patch_acls)
|
|
|
|
do_groups = subparsers.add_parser(
|
|
'groups',
|
|
help='modify Gerrit groups membership')
|
|
do_groups.add_argument(
|
|
'stage',
|
|
choices=['pre_release', 'post_release'],
|
|
help='type of modification to push')
|
|
do_groups.add_argument(
|
|
'username',
|
|
help='gerrit HTTP username')
|
|
do_groups.set_defaults(func=modify_gerrit_groups)
|
|
args = parser.parse_args(args)
|
|
if args.dryrun:
|
|
print('Running in dry run mode, no action will be actually taken')
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|