Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 1 | #! /usr/bin/env python |
| 2 | # Copyright (C) 2011 OpenStack, LLC. |
| 3 | # Copyright (c) 2012 Hewlett-Packard Development Company, L.P. |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 14 | # License for the specific language governing permissions and limitations |
| 15 | # under the License. |
| 16 | |
| 17 | # manage_projects.py reads a project config file called projects.yaml |
| 18 | # It should look like: |
| 19 | |
| 20 | # - homepage: http://openstack.org |
| 21 | # gerrit-host: review.openstack.org |
| 22 | # local-git-dir: /var/lib/git |
| 23 | # gerrit-key: /home/gerrit2/review_site/etc/ssh_host_rsa_key |
| 24 | # has-wiki: False |
| 25 | # has-issues: False |
| 26 | # has-downloads: False |
| 27 | # --- |
| 28 | # - project: PROJECT_NAME |
| 29 | # options: |
| 30 | # - has-wiki |
| 31 | # - has-issues |
| 32 | # - has-downloads |
| 33 | # - has-pull-requests |
| 34 | # homepage: Some homepage that isn't http://openstack.org |
| 35 | # description: This is a great project |
| 36 | # remote: https://gerrit.googlesource.com/gerrit |
| 37 | # upstream: git://github.com/bushy/beards.git |
| 38 | # acl_config: /path/to/gerrit/project.config |
| 39 | |
| 40 | |
| 41 | import ConfigParser |
| 42 | import logging |
| 43 | import os |
| 44 | import re |
| 45 | import shlex |
| 46 | import subprocess |
| 47 | import tempfile |
| 48 | import yaml |
| 49 | |
| 50 | import github |
| 51 | import gerritlib.gerrit |
| 52 | |
Clark Boylan | da402e5 | 2012-12-04 15:23:43 -0800 | [diff] [blame^] | 53 | import jeepyb.gerritdb |
| 54 | |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 55 | logging.basicConfig(level=logging.ERROR) |
| 56 | log = logging.getLogger("manage_projects") |
| 57 | |
| 58 | |
| 59 | def run_command(cmd, status=False, env={}): |
| 60 | cmd_list = shlex.split(str(cmd)) |
| 61 | newenv = os.environ |
| 62 | newenv.update(env) |
| 63 | log.debug("Executing command: %s" % " ".join(cmd_list)) |
| 64 | p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, |
| 65 | stderr=subprocess.STDOUT, env=newenv) |
| 66 | (out, nothing) = p.communicate() |
| 67 | log.debug("Return code: %s" % p.returncode) |
| 68 | log.debug("Command said: %s" % out.strip()) |
| 69 | if status: |
| 70 | return (p.returncode, out.strip()) |
| 71 | return out.strip() |
| 72 | |
| 73 | |
| 74 | def run_command_status(cmd, env={}): |
| 75 | return run_command(cmd, True, env) |
| 76 | |
| 77 | |
| 78 | def git_command(repo_dir, sub_cmd, env={}): |
| 79 | git_dir = os.path.join(repo_dir, '.git') |
| 80 | cmd = "git --git-dir=%s --work-tree=%s %s" % (git_dir, repo_dir, sub_cmd) |
| 81 | status, _ = run_command(cmd, True, env) |
| 82 | return status |
| 83 | |
| 84 | |
Clark Boylan | 808b513 | 2012-11-14 15:52:02 -0800 | [diff] [blame] | 85 | def git_command_output(repo_dir, sub_cmd, env={}): |
| 86 | git_dir = os.path.join(repo_dir, '.git') |
| 87 | cmd = "git --git-dir=%s --work-tree=%s %s" % (git_dir, repo_dir, sub_cmd) |
| 88 | status, out = run_command(cmd, True, env) |
| 89 | return (status, out) |
| 90 | |
| 91 | |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 92 | def fetch_config(project, remote_url, repo_path, env={}): |
| 93 | status = git_command(repo_path, "fetch %s +refs/meta/config:" |
| 94 | "refs/remotes/gerrit-meta/config" % remote_url, env) |
| 95 | if status != 0: |
| 96 | print "Failed to fetch refs/meta/config for project: %s" % project |
| 97 | return False |
| 98 | # Because the following fails if executed more than once you should only |
| 99 | # run fetch_config once in each repo. |
| 100 | status = git_command(repo_path, "checkout -b config " |
| 101 | "remotes/gerrit-meta/config") |
| 102 | if status != 0: |
| 103 | print "Failed to checkout config for project: %s" % project |
| 104 | return False |
| 105 | |
| 106 | return True |
| 107 | |
| 108 | |
| 109 | def copy_acl_config(project, repo_path, acl_config): |
| 110 | if not os.path.exists(acl_config): |
| 111 | return False |
| 112 | |
| 113 | acl_dest = os.path.join(repo_path, "project.config") |
| 114 | status, _ = run_command("cp %s %s" % |
| 115 | (acl_config, acl_dest), status=True) |
| 116 | if status == 0: |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 117 | status = git_command(repo_path, "diff-index --quiet HEAD --") |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 118 | if status != 0: |
| 119 | return True |
| 120 | return False |
| 121 | |
| 122 | |
| 123 | def push_acl_config(project, remote_url, repo_path, env={}): |
| 124 | cmd = "commit -a -m'Update project config.' --author='Openstack Project " \ |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 125 | "Creator <openstack-infra@lists.openstack.org>'" |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 126 | status = git_command(repo_path, cmd) |
| 127 | if status != 0: |
| 128 | print "Failed to commit config for project: %s" % project |
| 129 | return False |
Clark Boylan | 808b513 | 2012-11-14 15:52:02 -0800 | [diff] [blame] | 130 | status, out = git_command_output(repo_path, |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 131 | "push %s HEAD:refs/meta/config" % |
| 132 | remote_url, env) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 133 | if status != 0: |
| 134 | print "Failed to push config for project: %s" % project |
Clark Boylan | 808b513 | 2012-11-14 15:52:02 -0800 | [diff] [blame] | 135 | print out |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 136 | return False |
| 137 | return True |
| 138 | |
| 139 | |
| 140 | def _get_group_uuid(gerrit, group): |
Clark Boylan | da402e5 | 2012-12-04 15:23:43 -0800 | [diff] [blame^] | 141 | cursor = jeepyb.gerritdb.connect().cursor() |
| 142 | query = "SELECT group_uuid FROM account_groups WHERE name = %s" |
| 143 | cursor.execute(query, group) |
| 144 | data = cursor.fetchone() |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 145 | if data: |
Clark Boylan | da402e5 | 2012-12-04 15:23:43 -0800 | [diff] [blame^] | 146 | return data[0] |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 147 | return None |
| 148 | |
| 149 | |
| 150 | def get_group_uuid(gerrit, group): |
| 151 | uuid = _get_group_uuid(gerrit, group) |
| 152 | if uuid: |
| 153 | return uuid |
Clark Boylan | 0f2a440 | 2012-12-02 10:11:57 -0800 | [diff] [blame] | 154 | gerrit.createGroup(group, owner="Administrators") |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 155 | uuid = _get_group_uuid(gerrit, group) |
| 156 | if uuid: |
| 157 | return uuid |
| 158 | return None |
| 159 | |
| 160 | |
| 161 | def create_groups_file(project, gerrit, repo_path): |
| 162 | acl_config = os.path.join(repo_path, "project.config") |
| 163 | group_file = os.path.join(repo_path, "groups") |
| 164 | uuids = {} |
| 165 | for line in open(acl_config, 'r'): |
Clark Boylan | 5c9670f | 2012-11-14 17:48:55 -0800 | [diff] [blame] | 166 | r = re.match(r'^\s+.*group\s+(.*)$', line) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 167 | if r: |
| 168 | group = r.group(1) |
| 169 | if group in uuids.keys(): |
| 170 | continue |
| 171 | uuid = get_group_uuid(gerrit, group) |
| 172 | if uuid: |
| 173 | uuids[group] = uuid |
| 174 | else: |
| 175 | return False |
| 176 | if uuids: |
| 177 | with open(group_file, 'w') as fp: |
| 178 | for group, uuid in uuids.items(): |
| 179 | fp.write("%s\t%s\n" % (uuid, group)) |
| 180 | status = git_command(repo_path, "add groups") |
| 181 | if status != 0: |
| 182 | print "Failed to add groups file for project: %s" % project |
| 183 | return False |
| 184 | return True |
| 185 | |
| 186 | |
| 187 | def make_ssh_wrapper(gerrit_user, gerrit_key): |
| 188 | (fd, name) = tempfile.mkstemp(text=True) |
| 189 | os.write(fd, '#!/bin/bash\n') |
| 190 | os.write(fd, |
| 191 | 'ssh -i %s -l %s -o "StrictHostKeyChecking no" $@\n' % |
| 192 | (gerrit_key, gerrit_user)) |
| 193 | os.close(fd) |
James E. Blair | d7b5487 | 2012-11-30 16:22:57 -0800 | [diff] [blame] | 194 | os.chmod(name, 0755) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 195 | return dict(GIT_SSH=name) |
| 196 | |
| 197 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 198 | def main(): |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 199 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 200 | PROJECTS_YAML = os.environ.get('PROJECTS_YAML', |
| 201 | '/home/gerrit2/projects.yaml') |
| 202 | configs = [config for config in yaml.load_all(open(PROJECTS_YAML))] |
| 203 | defaults = configs[0][0] |
| 204 | default_has_issues = defaults.get('has-issues', False) |
| 205 | default_has_downloads = defaults.get('has-downloads', False) |
| 206 | default_has_wiki = defaults.get('has-wiki', False) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 207 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 208 | LOCAL_GIT_DIR = defaults.get('local-git-dir', '/var/lib/git') |
| 209 | GERRIT_HOST = defaults.get('gerrit-host') |
| 210 | GERRIT_USER = defaults.get('gerrit-user') |
| 211 | GERRIT_KEY = defaults.get('gerrit-key') |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 212 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 213 | GITHUB_SECURE_CONFIG = defaults.get( |
| 214 | 'github-config', |
| 215 | '/etc/github/github-projects.secure.config') |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 216 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 217 | secure_config = ConfigParser.ConfigParser() |
| 218 | secure_config.read(GITHUB_SECURE_CONFIG) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 219 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 220 | # Project creation doesn't work via oauth |
| 221 | ghub = github.Github(secure_config.get("github", "username"), |
| 222 | secure_config.get("github", "password")) |
| 223 | orgs = ghub.get_user().get_orgs() |
| 224 | orgs_dict = dict(zip([o.login.lower() for o in orgs], orgs)) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 225 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 226 | gerrit = gerritlib.gerrit.Gerrit('localhost', |
| 227 | GERRIT_USER, |
| 228 | 29418, |
| 229 | GERRIT_KEY) |
| 230 | project_list = gerrit.listProjects() |
| 231 | ssh_env = make_ssh_wrapper(GERRIT_USER, GERRIT_KEY) |
| 232 | try: |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 233 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 234 | for section in configs[1]: |
| 235 | project = section['project'] |
| 236 | options = section.get('options', dict()) |
| 237 | description = section.get('description', None) |
| 238 | homepage = section.get('homepage', defaults.get('homepage', None)) |
| 239 | upstream = section.get('upstream', None) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 240 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 241 | project_git = "%s.git" % project |
| 242 | project_dir = os.path.join(LOCAL_GIT_DIR, project_git) |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 243 | |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 244 | # Find the project's repo |
| 245 | project_split = project.split('/', 1) |
| 246 | if len(project_split) > 1: |
| 247 | repo_name = project_split[1] |
| 248 | else: |
| 249 | repo_name = project |
| 250 | has_issues = 'has-issues' in options or default_has_issues |
| 251 | has_downloads = 'has-downloads' in options or default_has_downloads |
| 252 | has_wiki = 'has-wiki' in options or default_has_wiki |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 253 | try: |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 254 | org = orgs_dict[project_split[0].lower()] |
| 255 | except KeyError: |
| 256 | # We do not have control of this github org ignore the project. |
| 257 | continue |
Monty Taylor | 16390e2 | 2012-11-04 22:20:36 +0100 | [diff] [blame] | 258 | try: |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 259 | repo = org.get_repo(repo_name) |
| 260 | except github.GithubException: |
| 261 | repo = org.create_repo(repo_name, |
| 262 | homepage=homepage, |
| 263 | has_issues=has_issues, |
| 264 | has_downloads=has_downloads, |
| 265 | has_wiki=has_wiki) |
| 266 | if description: |
| 267 | repo.edit(repo_name, description=description) |
| 268 | if homepage: |
| 269 | repo.edit(repo_name, homepage=homepage) |
| 270 | |
| 271 | repo.edit(repo_name, has_issues=has_issues, |
| 272 | has_downloads=has_downloads, |
| 273 | has_wiki=has_wiki) |
| 274 | |
| 275 | if 'gerrit' not in [team.name for team in repo.get_teams()]: |
| 276 | teams = org.get_teams() |
| 277 | teams_dict = dict(zip([t.name.lower() for t in teams], teams)) |
| 278 | teams_dict['gerrit'].add_to_repos(repo) |
| 279 | |
| 280 | remote_url = "ssh://localhost:29418/%s" % project |
| 281 | if project not in project_list: |
| 282 | tmpdir = tempfile.mkdtemp() |
| 283 | try: |
| 284 | repo_path = os.path.join(tmpdir, 'repo') |
| 285 | if upstream: |
| 286 | run_command("git clone %(upstream)s %(repo_path)s" % |
| 287 | dict(upstream=upstream, |
| 288 | repo_path=repo_path)) |
Clark Boylan | e0c725b | 2012-11-30 15:15:55 -0800 | [diff] [blame] | 289 | git_command(repo_path, |
| 290 | "fetch origin " |
| 291 | "+refs/heads/*:refs/copy/heads/*", |
| 292 | env=ssh_env) |
| 293 | push_string = "push %s +refs/copy/heads/*:refs/heads/*" |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 294 | else: |
| 295 | run_command("git init %s" % repo_path) |
| 296 | with open(os.path.join(repo_path, |
| 297 | ".gitreview"), |
| 298 | 'w') as gitreview: |
| 299 | gitreview.write(""" |
| 300 | [gerrit] |
| 301 | host=%s |
| 302 | port=29418 |
| 303 | project=%s |
| 304 | """ % (GERRIT_HOST, project_git)) |
| 305 | git_command(repo_path, "add .gitreview") |
| 306 | cmd = "commit -a -m'Added .gitreview' --author=" \ |
| 307 | "'Openstack Project Creator " \ |
| 308 | "<openstack-infra@lists.openstack.org>'" |
| 309 | git_command(repo_path, cmd) |
Clark Boylan | e0c725b | 2012-11-30 15:15:55 -0800 | [diff] [blame] | 310 | push_string = "push --all %s" |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 311 | gerrit.createProject(project) |
| 312 | |
| 313 | if not os.path.exists(project_dir): |
| 314 | run_command("git --bare init %s" % project_dir) |
| 315 | run_command("chown -R gerrit2:gerrit2 %s" |
| 316 | % project_dir) |
| 317 | |
| 318 | git_command(repo_path, |
Clark Boylan | e0c725b | 2012-11-30 15:15:55 -0800 | [diff] [blame] | 319 | push_string % remote_url, |
Monty Taylor | da3bada | 2012-11-22 09:38:22 -0800 | [diff] [blame] | 320 | env=ssh_env) |
| 321 | git_command(repo_path, |
| 322 | "push --tags %s" % remote_url, env=ssh_env) |
| 323 | finally: |
| 324 | run_command("rm -fr %s" % tmpdir) |
| 325 | |
| 326 | if 'acl_config' in section: |
| 327 | tmpdir = tempfile.mkdtemp() |
| 328 | try: |
| 329 | repo_path = os.path.join(tmpdir, 'repo') |
| 330 | ret, _ = run_command_status("git init %s" % repo_path) |
| 331 | if ret != 0: |
| 332 | continue |
| 333 | if (fetch_config(project, |
| 334 | remote_url, |
| 335 | repo_path, |
| 336 | ssh_env) and |
| 337 | copy_acl_config(project, repo_path, |
| 338 | section['acl_config']) and |
| 339 | create_groups_file(project, gerrit, repo_path)): |
| 340 | push_acl_config(project, |
| 341 | remote_url, |
| 342 | repo_path, |
| 343 | ssh_env) |
| 344 | finally: |
| 345 | run_command("rm -fr %s" % tmpdir) |
| 346 | finally: |
| 347 | os.unlink(ssh_env['GIT_SSH']) |