#! /usr/bin/env python # Copyright (C) 2011 OpenStack Foundation # Copyright (c) 2012 Hewlett-Packard Development Company, L.P. # # 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. # manage_projects.py reads a config file called projects.ini # It should look like: # # [projects] # homepage=https://opendev.org # acl-dir=/home/gerrit2/acls # local-git-dir=/opt/lib/git # jeepyb-cache-dir=/opt/lib/jeepyb # gerrit-host=review.opendev.org # gerrit-user=project-creator # gerrit-committer=Project Creator # gerrit-key=/home/gerrit2/review_site/etc/ssh_project_rsa_key # has-github=false # # manage_projects.py reads a project listing file called projects.yaml # It should look like: # - project: PROJECT_NAME # options: # - has-wiki # - has-issues # - has-downloads # - has-pull-requests # - track-upstream # homepage: Some homepage that isn't http://opendev.org # description: This is a great project # upstream: https://gerrit.googlesource.com/gerrit # upstream-prefix: upstream # acl-config: /path/to/gerrit/project.config # acl-append: # - /path/to/gerrit/project.config # acl-parameters: # project: OTHER_PROJECT_NAME import argparse from six.moves import configparser import glob import hashlib import json import logging import os import re import shutil import sys import time import gerritlib.gerrit import github import jeepyb.log import jeepyb.utils as u registry = u.ProjectsRegistry() log = logging.getLogger("manage_projects") orgs = None # Gerrit system groups as defined: # https://review.opendev.org/Documentation/access-control.html#system_groups # Need to set Gerrit system group's uuid to the format it expects. GERRIT_SYSTEM_GROUPS = { 'Anonymous Users': 'global:Anonymous-Users', 'Project Owners': 'global:Project-Owners', 'Registered Users': 'global:Registered-Users', 'Change Owner': 'global:Change-Owner', } class FetchConfigException(Exception): pass class CopyACLException(Exception): pass class ProcessACLException(Exception): pass class PushToGerritException(Exception): pass class CreateGroupException(Exception): pass def fetch_config(project, remote_url, repo_path, env=None): env = env or {} # Poll for refs/meta/config as gerrit may not have written it out for # us yet. for x in range(10): status = u.git_command( repo_path, "fetch %s +refs/meta/config:refs/remotes/gerrit-meta/config" % remote_url, env) if status == 0: break else: log.debug("Failed to fetch refs/meta/config for project: %s" % project) time.sleep(2) if status != 0: log.error("Failed to fetch refs/meta/config for project: %s" % project) raise FetchConfigException() # Poll for project.config as gerrit may not have committed an empty # one yet. output = "" for x in range(10): status = u.git_command(repo_path, "remote update --prune", env) if status != 0: log.error("Failed to update remote: %s" % remote_url) time.sleep(2) continue else: status, output = u.git_command_output( repo_path, "ls-files --with-tree=remotes/gerrit-meta/config " "project.config", env) if output.strip() != "project.config" or status != 0: log.debug("Failed to find project.config for project: %s" % project) time.sleep(2) else: break if output.strip() != "project.config" or status != 0: log.error("Failed to find project.config for project: %s" % project) raise FetchConfigException() # Because the following fails if executed more than once you should only # run fetch_config once in each repo. status = u.git_command( repo_path, "checkout -B config remotes/gerrit-meta/config") if status != 0: log.error("Failed to checkout config for project: %s" % project) raise FetchConfigException() def copy_acl_config(project, repo_path, acl_config): if not os.path.exists(acl_config): raise CopyACLException() acl_dest = os.path.join(repo_path, "project.config") status, _ = u.run_command( "cp %s %s" % (acl_config, acl_dest), status=True) if status != 0: raise CopyACLException() status = u.git_command(repo_path, "diff --quiet") return status != 0 def push_acl_config(project, remote_url, repo_path, gitid, env=None): env = env or {} cmd = "commit -a -m'Update project config.' --author='%s'" % gitid status, out = u.git_command_output(repo_path, cmd) if status != 0: log.error("Failed to commit config for project: %s" % project) log.error(out) return False status, out = u.git_command_output( repo_path, "push %s HEAD:refs/meta/config" % remote_url, env) if status != 0: log.error("Failed to push config for project: %s" % project) log.error(out) if 'project state does not permit write' in out: # We tried to push an acl update to a read only project. # This is an error to Gerrit, but we treat it as success # to allow other acls to update. return True else: return False return True def _get_group_uuid(gerrit, group, retries=10): """ Gerrit keeps internal user groups in the DB while it keeps systems groups in All-Projects groups file (in refs/meta/config). This will only get the UUIDs for internal user groups. Note: 'Administrators', 'Non-Interactive Users' and all other custom groups in Gerrit are defined as internal user groups. Wait for up to 10 seconds for the group to be created in the DB. """ for x in range(retries): # Work around gerritlib raising a generic "Exception" exception # when listGroup() finds no group try: group_list = list(gerrit.listGroup(group, verbose=True)) except Exception: group_list = None if group_list: return group_list[0].split('\t')[1] if retries > 1: time.sleep(1) return None def get_group_uuid(gerrit, group): uuid = _get_group_uuid(gerrit, group, retries=1) if uuid: return uuid if group in GERRIT_SYSTEM_GROUPS: return GERRIT_SYSTEM_GROUPS[group] gerrit.createGroup(group) for user in list(gerrit.listMembers(group)): if gerrit.connection.username == user['username']: # Gerrit now adds creating user to groups. We don't want that. gerrit.removeMember(group, gerrit.connection.username) break uuid = _get_group_uuid(gerrit, group) if uuid: return uuid return None def create_groups_file(project, gerrit, repo_path): acl_config = os.path.join(repo_path, "project.config") group_file = os.path.join(repo_path, "groups") uuids = {} for line in open(acl_config, 'r'): r = re.match(r'^.*\sgroup\s+(.*)$', line) if r: group = r.group(1) if group in uuids.keys(): continue uuid = get_group_uuid(gerrit, group) if uuid: uuids[group] = uuid else: log.error("Unable to get UUID for group %s." % group) raise CreateGroupException() if uuids: with open(group_file, 'w') as fp: for group, uuid in uuids.items(): fp.write("%s\t%s\n" % (uuid, group)) status = u.git_command(repo_path, "add groups") if status != 0: log.error("Failed to add groups file for project: %s" % project) raise CreateGroupException() def create_update_github_project( default_has_issues, default_has_downloads, default_has_wiki, github_secure_config, options, project, description, homepage, cache): created = False has_issues = 'has-issues' in options or default_has_issues has_downloads = 'has-downloads' in options or default_has_downloads has_wiki = 'has-wiki' in options or default_has_wiki needs_update = False if not cache.get('created-in-github', False): needs_update = True if not cache.get('gerrit-in-team', False): needs_update = True if cache.get('has_issues', default_has_issues) != has_issues: needs_update = True if cache.get('has_downloads', default_has_downloads) != has_downloads: needs_update = True if cache.get('has_wiki', default_has_wiki) != has_wiki: needs_update = True if not needs_update: return False secure_config = configparser.ConfigParser() secure_config.read(github_secure_config) global orgs if orgs is None: if secure_config.has_option("github", "oauth_token"): ghub = github.Github(secure_config.get("github", "oauth_token")) else: ghub = github.Github(secure_config.get("github", "username"), secure_config.get("github", "password")) log.info('Fetching github org list') orgs = ghub.get_user().get_orgs() orgs_dict = dict(zip([o.login.lower() for o in orgs], orgs)) # Find the project's repo project_split = project.split('/', 1) org_name = project_split[0] if len(project_split) > 1: repo_name = project_split[1] else: repo_name = project try: org = orgs_dict[org_name.lower()] except KeyError: # We do not have control of this github org ignore the project. return False try: log.info("Fetching github info about %s", repo_name) repo = org.get_repo(repo_name) except github.GithubException: log.info("Creating %s in github", repo_name) repo = org.create_repo(repo_name, homepage=homepage, has_issues=has_issues, has_downloads=has_downloads, has_wiki=has_wiki) created = True cache['has_wiki'] = has_wiki cache['has_downloads'] = has_downloads cache['has_issues'] = has_issues kwargs = {} # If necessary, update project on Github if description and description != repo.description: kwargs['description'] = description if homepage and homepage != repo.homepage: kwargs['homepage'] = homepage if has_issues != repo.has_issues: kwargs['has_issues'] = has_issues if has_downloads != repo.has_downloads: kwargs['has_downloads'] = has_downloads if has_wiki != repo.has_wiki: kwargs['has_wiki'] = has_wiki if kwargs: log.info("Updating github repo info about %s", repo_name) repo.edit(repo_name, **kwargs) cache.update(kwargs) if not cache.get('gerrit-in-team', False): if 'gerrit' not in [team.name for team in repo.get_teams()]: log.info("Adding gerrit to github team for %s", repo_name) teams = org.get_teams() teams_dict = dict(zip([t.name.lower() for t in teams], teams)) teams_dict['gerrit'].add_to_repos(repo) cache['gerrit-in-team'] = True created = True return created # TODO(mordred): Inspect repo_dir:default-branch for a description # override def find_description_override(repo_path): return None def push_to_gerrit(repo_path, project, push_string, remote_url, ssh_env): try: u.git_command(repo_path, push_string % remote_url, env=ssh_env) u.git_command(repo_path, "push --tags %s" % remote_url, env=ssh_env) except Exception: log.exception( "Error pushing %s to Gerrit." % project) raise PushToGerritException() def process_acls(acl_config, project, ACL_DIR, section, remote_url, repo_path, ssh_env, gerrit, GERRIT_GITID): if not os.path.isfile(acl_config): return try: fetch_config(project, remote_url, repo_path, ssh_env) if not copy_acl_config(project, repo_path, acl_config): # nothing was copied, so we're done return create_groups_file(project, gerrit, repo_path) rc = push_acl_config(project, remote_url, repo_path, GERRIT_GITID, ssh_env) if not rc: raise Exception("Non zero acl push return value.") except Exception: log.exception( "Exception processing ACLS for %s." % project) raise ProcessACLException() finally: u.git_command(repo_path, 'reset --hard') default_branch = section.get('default-branch', 'master') u.git_command(repo_path, 'checkout %s' % default_branch) u.git_command(repo_path, 'branch -D config') def create_gerrit_project(project, default_branch, project_list, gerrit): if project not in project_list: try: gerrit.createProject(project, branches=[default_branch]) return True except Exception: log.exception( "Exception creating %s in Gerrit." % project) raise return False def create_local_mirror(local_git_dir, project_git, gerrit_system_user, gerrit_system_group): git_mirror_path = os.path.join(local_git_dir, project_git) if not os.path.exists(git_mirror_path): (ret, output) = u.run_command_status( "git --bare init %s" % git_mirror_path) if ret: u.run_command("rm -rf git_mirror_path") raise Exception(output) u.run_command( "chown -R %s:%s %s" % ( gerrit_system_user, gerrit_system_group, git_mirror_path)) def main(): parser = argparse.ArgumentParser(description='Manage projects') jeepyb.log.setup_logging_arguments(parser) parser.add_argument('--nocleanup', action='store_true', help='do not remove temp directories') parser.add_argument('projects', metavar='project', nargs='*', help='name of project(s) to process') args = parser.parse_args() jeepyb.log.configure_logging(args) default_has_github = registry.get_defaults('has-github', True) LOCAL_GIT_DIR = registry.get_defaults('local-git-dir', None) JEEPYB_CACHE_DIR = registry.get_defaults('jeepyb-cache-dir', '/var/lib/jeepyb') ACL_DIR = registry.get_defaults('acl-dir') GERRIT_HOST = registry.get_defaults('gerrit-host') GITREVIEW_GERRIT_HOST = registry.get_defaults( 'gitreview-gerrit-host', GERRIT_HOST) GERRIT_PORT = int(registry.get_defaults('gerrit-port', '29418')) GITREVIEW_GERRIT_PORT = int(registry.get_defaults( 'gitreview-gerrit-port', GERRIT_PORT)) GERRIT_USER = registry.get_defaults('gerrit-user') GERRIT_KEY = registry.get_defaults('gerrit-key') GERRIT_GITID = registry.get_defaults('gerrit-committer') GERRIT_REPLICATE = registry.get_defaults('gerrit-replicate', True) GERRIT_OS_SYSTEM_USER = registry.get_defaults('gerrit-system-user', 'gerrit2') GERRIT_OS_SYSTEM_GROUP = registry.get_defaults('gerrit-system-group', 'gerrit2') DEFAULT_HOMEPAGE = registry.get_defaults('homepage') DEFAULT_HAS_ISSUES = registry.get_defaults('has-issues', False) DEFAULT_HAS_DOWNLOADS = registry.get_defaults('has-downloads', False) DEFAULT_HAS_WIKI = registry.get_defaults('has-wiki', False) GITHUB_SECURE_CONFIG = registry.get_defaults( 'github-config', '/etc/github/github-projects.secure.config') PROJECT_CACHE_FILE = os.path.join(JEEPYB_CACHE_DIR, 'project.cache') project_cache = {} if os.path.exists(PROJECT_CACHE_FILE): project_cache = json.loads(open(PROJECT_CACHE_FILE, 'r').read()) acl_cache = {} for acl_file in glob.glob(os.path.join(ACL_DIR, '*/*.config')): sha256 = hashlib.sha256() sha256.update(open(acl_file, 'r').read().encode('utf-8')) acl_cache[acl_file] = sha256.hexdigest() gerrit = gerritlib.gerrit.Gerrit(GERRIT_HOST, GERRIT_USER, GERRIT_PORT, GERRIT_KEY) project_list = list(gerrit.listProjects()) ssh_env = u.make_ssh_wrapper(GERRIT_USER, GERRIT_KEY) try: # Collect processed errors,if any process_errors = [] for section in registry.all_configs_list: project = section['project'] if args.projects and project not in args.projects: continue try: log.info("Processing project: %s" % project) # Figure out all of the options options = section.get('options', dict()) description = section.get('description', None) homepage = section.get('homepage', DEFAULT_HOMEPAGE) upstream = section.get('upstream', None) default_branch = section.get('default-branch', 'master') repo_path = os.path.join(JEEPYB_CACHE_DIR, project) # If this project doesn't want to use gerrit, exit cleanly. if 'no-gerrit' in options: continue project_git = "%s.git" % project remote_url = "ssh://%s:%s/%s" % ( GERRIT_HOST, GERRIT_PORT, project) git_opts = dict(upstream=upstream, repo_path=repo_path, remote_url=remote_url) acl_config = section.get( 'acl-config', '%s.config' % os.path.join(ACL_DIR, project)) project_cache.setdefault(project, {}) # Create the project in Gerrit first, since it will fail # spectacularly if its project directory or local replica # already exist on disk project_created = project_cache[project].get( 'project-created', False) if not project_created: try: project_created = create_gerrit_project( project, default_branch, project_list, gerrit) project_cache[project]['project-created'] = True except Exception: project_cache[project]['project-created'] = False continue pushed_to_gerrit = project_cache[project].get( 'pushed-to-gerrit', False) if not pushed_to_gerrit: # We haven't pushed to gerrit, so grab the repo again if os.path.exists(repo_path): shutil.rmtree(repo_path) # Make Local repo push_string = u.make_local_copy( repo_path, project, default_branch, project_list, git_opts, ssh_env, upstream, GITREVIEW_GERRIT_HOST, GITREVIEW_GERRIT_PORT, project_git, GERRIT_GITID) description = ( find_description_override(repo_path) or description) u.fsck_repo(repo_path) if push_string: push_to_gerrit( repo_path, project, push_string, remote_url, ssh_env) project_cache[project]['pushed-to-gerrit'] = True if GERRIT_REPLICATE: gerrit.replicate(project) # Create the repo for the local git mirror if LOCAL_GIT_DIR: # This is conditional because new gerrit url pathing # has made local git mirrors less straightfoward. create_local_mirror( LOCAL_GIT_DIR, project_git, GERRIT_OS_SYSTEM_USER, GERRIT_OS_SYSTEM_GROUP) if acl_config: acl_sha = acl_cache.get(acl_config) if project_cache[project].get('acl-sha') != acl_sha: if not os.path.exists(repo_path): u.make_local_copy( repo_path, project, default_branch, project_list, git_opts, ssh_env, upstream, GERRIT_HOST, GERRIT_PORT, project_git, GERRIT_GITID) process_acls( acl_config, project, ACL_DIR, section, remote_url, repo_path, ssh_env, gerrit, GERRIT_GITID) project_cache[project]['acl-sha'] = acl_sha else: log.info("%s has matching sha, skipping ACLs", project) if 'has-github' in options or default_has_github: created = create_update_github_project( DEFAULT_HAS_ISSUES, DEFAULT_HAS_DOWNLOADS, DEFAULT_HAS_WIKI, GITHUB_SECURE_CONFIG, options, project, description, homepage, project_cache[project]) if created and GERRIT_REPLICATE: gerrit.replicate(project) project_cache[project]['created-in-github'] = created except Exception: msg = "Problems creating %s, moving on." % project log.exception(msg) process_errors.append(msg) continue finally: # Clean up after ourselves - this repo has no use if os.path.exists(repo_path): shutil.rmtree(repo_path) finally: with open(PROJECT_CACHE_FILE, 'w') as cache_out: log.info("Writing cache file %s", PROJECT_CACHE_FILE) cache_out.write(json.dumps( project_cache, sort_keys=True, indent=2)) os.unlink(ssh_env['GIT_SSH']) if len(process_errors) > 0: log.error("%d problems has been caught during run:\n %s" % ( len(process_errors), process_errors)) sys.exit(1) if __name__ == "__main__": main()