From c22f58ae3b6c917e9018e4b27bbad49fc0a59972 Mon Sep 17 00:00:00 2001 From: Doug Hellmann Date: Mon, 9 Jan 2017 09:40:59 -0500 Subject: [PATCH] move aclmanager.py from release-tools repo The aclmanager script needs to be able to get a list of repositories with the cycle-with-milestone release model, and the data for that lives in this repository now instead of the governance repository. Rather than having a tool that relies on this data live in release-tools, move it here. Change-Id: I7c2c54c4949829aea92e569e3ac9ec9007d932ed Signed-off-by: Doug Hellmann --- PROCESS.rst | 13 ++- README.rst | 31 +++++++ tools/aclmanager.py | 212 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 4 deletions(-) create mode 100755 tools/aclmanager.py diff --git a/PROCESS.rst b/PROCESS.rst index 7af08c6f7d..b9ab1aa46c 100644 --- a/PROCESS.rst +++ b/PROCESS.rst @@ -78,14 +78,19 @@ Between Milestone-2 and Milestone-3 1. Update ACLs for refs/heads/stable/$series so that members of $project-release-branch can approve changes. The patch can be - generated (for all release:cycle-with-milestones deliverables) with: - ``aclmanager.py acls /path/to/openstack-infra/project-config $series`` + generated (for all release:cycle-with-milestones deliverables) + with:: + + tox -e venv -- python ./tools/aclmanager.py acls /path/to/openstack-infra/project-config $series 2. Set the population of all $project-release-branch groups to the "Release Managers" group and $project-release. This can be done (for all release:cycle-with-milestones deliverables) by running - ``aclmanager.py groups pre_release $user`` ($user being your Gerrit - username) + ``aclmanager.py``:: + + tox -e venv -- python ./tools/aclmanager.py groups pre_release $user + + ($user being your Gerrit username) 3. Ask the release liaisons for the affected teams to update the contents of their $project-release groups. For new projects in diff --git a/README.rst b/README.rst index 0594c05ed1..e98196ba67 100644 --- a/README.rst +++ b/README.rst @@ -459,3 +459,34 @@ easy as ``pip install .`` in this repository directory. milestone-based projects at the end of the cycle * ``init-series`` initializes a new deliverable directory with stub files based on the previous release. + +tools/aclmanager.py +------------------- + +A script to handle pre-release/post-release ACLs on stable/$SERIES +branches. + +The 'acls' action helps to produce a patch over +openstack-infra/project-config that inserts a specific ACL for +stable/$SERIES. + +The 'groups' action helps to adjust the membership of +$PROJ-release-branch Gerrit group, based on which stage the release +branch is at. At pre-release we remove $PROJ-stable-maint, and add the +$PROJ-release and Release Managers group (pre_release subaction). At +post-release, we remove $PROJ-release and Release Managers, and add +$PROJ-stable-maint (post_release subaction). + +Examples: + +To create the ACL patch for stable/newton: + +:: + + tox -e venv -- python ./tools/aclmanager.py acls ~/branches/openstack-infra/project-config newton + +To set the pre-release group membership: + +:: + + tox -e venv -- python ./tools/aclmanager.py groups pre_release ttx diff --git a/tools/aclmanager.py b/tools/aclmanager.py new file mode 100755 index 0000000000..46391db953 --- /dev/null +++ b/tools/aclmanager.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +# +# Handle pre-release / post-release ACLs for milestone-driven projects +# +# Copyright 2016 Thierry Carrez +# 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 sys + +import requests +import yaml + +import openstack_releases +from openstack_releases import deliverable + +GERRIT_URL = 'https://review.openstack.org/' + +EXCEPTIONS = ['openstack/training-labs', + 'openstack/murano-apps', + 'openstack/trove-image-builder'] + + +def repositories_list(deliverables_dir, series): + """Yields (team, repo) tuples for cycle-with-milestones deliverables""" + deliverables = deliverable.Deliverables(deliverables_dir) + for team, series, dname, dinfo in deliverables.get_deliverables(None, series): + d = deliverable.Deliverable(team, series, dname, dinfo) + if d.model != 'cycle-with-milestones': + continue + for repo in sorted(d.repos): + 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: + aclfiles[acl[repo]] = team + except KeyError: + print('No ACL file defined for %s' % repo) + sys.exit(1) + + 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 + for line in aclfile: + if line.startswith("[receive]"): + newcontent += blob.format( + branch=args.series, + group=group) + hit = True + 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, _ in repositories_list(): + 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') + subparsers = parser.add_subparsers(title='commands') + + do_acls = subparsers.add_parser( + 'acls', + help='patch ACL files') + do_acls.add_argument( + '--deliverables-dir', + default=openstack_releases.deliverable_dir, + help='location of deliverable files', + ) + do_acls.add_argument( + 'repository', + help='location of the local project-config repository') + do_acls.add_argument( + 'series', + help='series to generate ACL for') + 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()