Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 1 | #! /usr/bin/env python |
| 2 | # Copyright (C) 2011 OpenStack, LLC. |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | # you may not use this file except in compliance with the License. |
| 6 | # You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | # License for the specific language governing permissions and limitations |
| 14 | # under the License. |
| 15 | |
| 16 | # Synchronize Gerrit users from Launchpad. |
| 17 | |
| 18 | import os |
| 19 | import sys |
Andrew Hutchings | fc29e16 | 2012-05-18 14:33:37 +0100 | [diff] [blame] | 20 | import fcntl |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 21 | import uuid |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 22 | import subprocess |
| 23 | |
| 24 | from datetime import datetime |
| 25 | |
Monty Taylor | c438159 | 2012-05-19 11:14:27 -0400 | [diff] [blame] | 26 | # There is a bug (810019) somewhere deep which causes pkg_resources |
| 27 | # to bitch if it's imported after argparse. launchpadlib imports it, |
| 28 | # so if we head it off at the pass, we can skip cronspam |
| 29 | import pkg_resources |
| 30 | |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 31 | import StringIO |
| 32 | import ConfigParser |
Andrew Hutchings | 6a17893 | 2012-05-17 14:53:01 +0100 | [diff] [blame] | 33 | import argparse |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 34 | import MySQLdb |
| 35 | |
| 36 | from launchpadlib.launchpad import Launchpad |
| 37 | from launchpadlib.uris import LPNET_SERVICE_ROOT |
| 38 | |
| 39 | from openid.consumer import consumer |
| 40 | from openid.cryptutil import randomString |
| 41 | |
| 42 | DEBUG = False |
| 43 | |
Andrew Hutchings | 16a6c46 | 2012-05-25 14:26:41 +0100 | [diff] [blame] | 44 | # suppress pyflakes |
| 45 | pkg_resources.get_supported_platform() |
| 46 | |
Andrew Hutchings | fc29e16 | 2012-05-18 14:33:37 +0100 | [diff] [blame] | 47 | pid_file = '/tmp/update_gerrit_users.pid' |
| 48 | fp = open(pid_file, 'w') |
| 49 | try: |
| 50 | fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB) |
| 51 | except IOError: |
| 52 | # another instance is running |
| 53 | sys.exit(0) |
| 54 | |
Andrew Hutchings | 6a17893 | 2012-05-17 14:53:01 +0100 | [diff] [blame] | 55 | parser = argparse.ArgumentParser() |
| 56 | parser.add_argument('user', help='The gerrit admin user') |
| 57 | parser.add_argument('ssh_key', help='The gerrit admin SSH key file') |
| 58 | parser.add_argument('site', help='The site in use (typically openstack or stackforge)') |
| 59 | options = parser.parse_args() |
| 60 | |
| 61 | GERRIT_USER = options.user |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 62 | GERRIT_CONFIG = os.environ.get('GERRIT_CONFIG', |
| 63 | '/home/gerrit2/review_site/etc/gerrit.config') |
| 64 | GERRIT_SECURE_CONFIG = os.environ.get('GERRIT_SECURE_CONFIG', |
| 65 | '/home/gerrit2/review_site/etc/secure.config') |
Andrew Hutchings | 6a17893 | 2012-05-17 14:53:01 +0100 | [diff] [blame] | 66 | GERRIT_SSH_KEY = options.ssh_key |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 67 | GERRIT_CACHE_DIR = os.path.expanduser(os.environ.get('GERRIT_CACHE_DIR', |
| 68 | '~/.launchpadlib/cache')) |
| 69 | GERRIT_CREDENTIALS = os.path.expanduser(os.environ.get('GERRIT_CREDENTIALS', |
| 70 | '~/.launchpadlib/creds')) |
| 71 | GERRIT_BACKUP_PATH = os.environ.get('GERRIT_BACKUP_PATH', |
| 72 | '/home/gerrit2/dbupdates') |
| 73 | |
| 74 | for check_path in (os.path.dirname(GERRIT_CACHE_DIR), |
| 75 | os.path.dirname(GERRIT_CREDENTIALS), |
| 76 | GERRIT_BACKUP_PATH): |
| 77 | if not os.path.exists(check_path): |
| 78 | os.makedirs(check_path) |
| 79 | |
| 80 | def get_broken_config(filename): |
| 81 | """ gerrit config ini files are broken and have leading tabs """ |
| 82 | text = "" |
| 83 | with open(filename,"r") as conf: |
| 84 | for line in conf.readlines(): |
| 85 | text = "%s%s" % (text, line.lstrip()) |
| 86 | |
| 87 | fp = StringIO.StringIO(text) |
| 88 | c=ConfigParser.ConfigParser() |
| 89 | c.readfp(fp) |
| 90 | return c |
| 91 | |
| 92 | def get_type(in_type): |
| 93 | if in_type == "RSA": |
| 94 | return "ssh-rsa" |
| 95 | else: |
| 96 | return "ssh-dsa" |
| 97 | |
| 98 | gerrit_config = get_broken_config(GERRIT_CONFIG) |
| 99 | secure_config = get_broken_config(GERRIT_SECURE_CONFIG) |
| 100 | |
| 101 | DB_USER = gerrit_config.get("database", "username") |
| 102 | DB_PASS = secure_config.get("database","password") |
| 103 | DB_DB = gerrit_config.get("database","database") |
| 104 | |
| 105 | db_backup_file = "%s.%s.sql" % (DB_DB, datetime.isoformat(datetime.now())) |
| 106 | db_backup_path = os.path.join(GERRIT_BACKUP_PATH, db_backup_file) |
David Shrewsbury | 54a6390 | 2012-05-03 09:27:14 -0400 | [diff] [blame] | 107 | retval = os.system("mysqldump --opt -u%s -p%s %s | gzip -9 > %s.gz" % |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 108 | (DB_USER, DB_PASS, DB_DB, db_backup_path)) |
| 109 | if retval != 0: |
| 110 | print "Problem taking a db dump, aborting db update" |
| 111 | sys.exit(retval) |
| 112 | |
| 113 | conn = MySQLdb.connect(user = DB_USER, passwd = DB_PASS, db = DB_DB) |
| 114 | cur = conn.cursor() |
| 115 | |
| 116 | |
| 117 | launchpad = Launchpad.login_with('Gerrit User Sync', LPNET_SERVICE_ROOT, |
| 118 | GERRIT_CACHE_DIR, |
| 119 | credentials_file = GERRIT_CREDENTIALS) |
| 120 | |
| 121 | def get_sub_teams(team, have_teams): |
| 122 | for sub_team in launchpad.people[team].sub_teams: |
| 123 | if sub_team.name not in have_teams: |
| 124 | have_teams = get_sub_teams(sub_team.name, have_teams) |
| 125 | have_teams.append(team) |
| 126 | return have_teams |
| 127 | |
| 128 | |
| 129 | teams_todo = get_sub_teams('openstack', []) |
| 130 | |
| 131 | users={} |
| 132 | groups={} |
| 133 | groups_in_groups={} |
| 134 | group_implies_groups={} |
| 135 | group_ids={} |
| 136 | projects = subprocess.check_output(['/usr/bin/ssh', '-p', '29418', |
| 137 | '-i', GERRIT_SSH_KEY, |
| 138 | '-l', GERRIT_USER, 'localhost', |
| 139 | 'gerrit', 'ls-projects']).split('\n') |
| 140 | |
| 141 | for team_todo in teams_todo: |
| 142 | |
| 143 | team = launchpad.people[team_todo] |
| 144 | groups[team.name] = team.display_name |
| 145 | |
| 146 | # Attempt to get nested group memberships. ~nova-core, for instance, is a |
| 147 | # member of ~nova, so membership in ~nova-core should imply membership in |
| 148 | # ~nova |
| 149 | group_in_group = groups_in_groups.get(team.name, {}) |
| 150 | for subgroup in team.sub_teams: |
| 151 | group_in_group[subgroup.name] = 1 |
| 152 | # We should now have a dictionary of the form {'nova': {'nova-core': 1}} |
| 153 | groups_in_groups[team.name] = group_in_group |
| 154 | |
| 155 | for detail in team.members_details: |
| 156 | |
| 157 | user = None |
| 158 | |
| 159 | # detail.self_link == |
| 160 | # 'https://api.launchpad.net/1.0/~team/+member/${username}' |
| 161 | login = detail.self_link.split('/')[-1] |
| 162 | |
| 163 | if users.has_key(login): |
| 164 | user = users[login] |
| 165 | else: |
| 166 | |
| 167 | user = dict(add_groups=[]) |
| 168 | |
| 169 | status = detail.status |
| 170 | if (status == "Approved" or status == "Administrator"): |
| 171 | user['add_groups'].append(team.name) |
| 172 | users[login] = user |
| 173 | |
| 174 | # If we picked up subgroups that were not in our original list of groups |
| 175 | # make sure they get added |
| 176 | for (supergroup, subgroups) in groups_in_groups.items(): |
| 177 | for group in subgroups.keys(): |
| 178 | if group not in groups.keys(): |
| 179 | groups[group] = None |
| 180 | |
| 181 | # account_groups |
| 182 | # groups is a dict of team name to team display name |
| 183 | # here, for every group we have in that dict, we're building another dict of |
| 184 | # group_name to group_id - and if the database doesn't already have the |
| 185 | # group, we're adding it |
| 186 | for (group_name, group_display_name) in groups.items(): |
| 187 | if cur.execute("select group_id from account_groups where name = %s", |
| 188 | group_name): |
| 189 | group_ids[group_name] = cur.fetchall()[0][0] |
| 190 | else: |
| 191 | cur.execute("""insert into account_group_id (s) values (NULL)"""); |
| 192 | cur.execute("select max(s) from account_group_id") |
| 193 | group_id = cur.fetchall()[0][0] |
| 194 | |
| 195 | # Match the 40-char 'uuid' that java is producing |
| 196 | group_uuid = uuid.uuid4() |
| 197 | second_uuid = uuid.uuid4() |
| 198 | full_uuid = "%s%s" % (group_uuid.hex, second_uuid.hex[:8]) |
| 199 | |
| 200 | cur.execute("""insert into account_groups |
| 201 | (group_id, group_type, owner_group_id, |
| 202 | name, description, group_uuid) |
| 203 | values |
| 204 | (%s, 'INTERNAL', 1, %s, %s, %s)""", |
| 205 | (group_id, group_name, group_display_name, full_uuid)) |
| 206 | cur.execute("""insert into account_group_names (group_id, name) values |
| 207 | (%s, %s)""", |
| 208 | (group_id, group_name)) |
| 209 | |
| 210 | group_ids[group_name] = group_id |
| 211 | |
| 212 | # account_group_includes |
| 213 | # groups_in_groups should be a dict of dicts, where the key is the larger |
| 214 | # group and the inner dict is a list of groups that are members of the |
| 215 | # larger group. So {'nova': {'nova-core': 1}} |
| 216 | for (group_name, subgroups) in groups_in_groups.items(): |
| 217 | for subgroup_name in subgroups.keys(): |
| 218 | try: |
| 219 | cur.execute("""insert into account_group_includes |
| 220 | (group_id, include_id) |
| 221 | values (%s, %s)""", |
| 222 | (group_ids[group_name], group_ids[subgroup_name])) |
| 223 | except MySQLdb.IntegrityError: |
| 224 | pass |
| 225 | |
| 226 | # Make a list of implied group membership |
| 227 | # building a list which is the opposite of groups_in_group. Here |
| 228 | # group_implies_groups is a dict keyed by group_id containing a list of |
| 229 | # group_ids of implied membership. SO: if nova is 1 and nova-core is 2: |
| 230 | # {'2': [1]} |
| 231 | for group_id in group_ids.values(): |
| 232 | total_groups = [] |
| 233 | groups_todo = [group_id] |
| 234 | while len(groups_todo) > 0: |
| 235 | current_group = groups_todo.pop() |
| 236 | total_groups.append(current_group) |
| 237 | cur.execute("""select group_id from account_group_includes |
| 238 | where include_id = %s""", (current_group)) |
| 239 | for row in cur.fetchall(): |
| 240 | if row[0] != 1 and row[0] not in total_groups: |
| 241 | groups_todo.append(row[0]) |
| 242 | group_implies_groups[group_id] = total_groups |
| 243 | |
| 244 | if DEBUG: |
| 245 | def get_group_name(in_group_id): |
| 246 | for (group_name, group_id) in group_ids.items(): |
| 247 | if group_id == in_group_id: |
| 248 | return group_name |
| 249 | |
| 250 | print "groups in groups" |
| 251 | for (k,v) in groups_in_groups.items(): |
| 252 | print k, v |
| 253 | |
| 254 | print "group_imples_groups" |
| 255 | for (k, v) in group_implies_groups.items(): |
| 256 | print get_group_name(k) |
| 257 | new_groups=[] |
| 258 | for val in v: |
| 259 | new_groups.append(get_group_name(val)) |
| 260 | print "\t", new_groups |
| 261 | |
| 262 | for (username, user_details) in users.items(): |
Andrew Hutchings | fc29e16 | 2012-05-18 14:33:37 +0100 | [diff] [blame] | 263 | member = launchpad.people[username] |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 264 | # accounts |
| 265 | account_id = None |
| 266 | if cur.execute("""select account_id from account_external_ids where |
| 267 | external_id in (%s)""", ("username:%s" % username)): |
| 268 | account_id = cur.fetchall()[0][0] |
| 269 | # We have this bad boy - all we need to do is update his group membership |
| 270 | |
| 271 | else: |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 272 | # We need details |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 273 | if not member.is_team: |
| 274 | |
| 275 | openid_consumer = consumer.Consumer(dict(id=randomString(16, '0123456789abcdef')), None) |
| 276 | openid_request = openid_consumer.begin("https://launchpad.net/~%s" % member.name) |
| 277 | user_details['openid_external_id'] = openid_request.endpoint.getLocalID() |
| 278 | |
| 279 | # Handle username change |
| 280 | if cur.execute("""select account_id from account_external_ids where |
| 281 | external_id in (%s)""", user_details['openid_external_id']): |
| 282 | account_id = cur.fetchall()[0][0] |
| 283 | cur.execute("""update account_external_ids |
| 284 | set external_id=%s |
| 285 | where external_id like 'username%%' |
| 286 | and account_id = %s""", |
| 287 | ('username:%s' % username, account_id)) |
| 288 | else: |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 289 | email = None |
| 290 | try: |
| 291 | email = member.preferred_email_address.email |
| 292 | except ValueError: |
| 293 | pass |
| 294 | user_details['email'] = email |
| 295 | |
| 296 | |
| 297 | cur.execute("""insert into account_id (s) values (NULL)"""); |
| 298 | cur.execute("select max(s) from account_id") |
| 299 | account_id = cur.fetchall()[0][0] |
| 300 | |
| 301 | cur.execute("""insert into accounts (account_id, full_name, preferred_email) values |
| 302 | (%s, %s, %s)""", (account_id, username, user_details['email'])) |
| 303 | |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 304 | # account_external_ids |
| 305 | ## external_id |
| 306 | if not cur.execute("""select account_id from account_external_ids |
| 307 | where account_id = %s and external_id = %s""", |
| 308 | (account_id, user_details['openid_external_id'])): |
| 309 | cur.execute("""insert into account_external_ids |
| 310 | (account_id, email_address, external_id) |
| 311 | values (%s, %s, %s)""", |
| 312 | (account_id, user_details['email'], user_details['openid_external_id'])) |
| 313 | if not cur.execute("""select account_id from account_external_ids |
| 314 | where account_id = %s and external_id = %s""", |
| 315 | (account_id, "username:%s" % username)): |
| 316 | cur.execute("""insert into account_external_ids |
| 317 | (account_id, external_id) values (%s, %s)""", |
| 318 | (account_id, "username:%s" % username)) |
| 319 | |
| 320 | if user_details.get('email', None) is not None: |
| 321 | if not cur.execute("""select account_id from account_external_ids |
| 322 | where account_id = %s and external_id = %s""", |
| 323 | (account_id, "mailto:%s" % user_details['email'])): |
| 324 | cur.execute("""insert into account_external_ids |
| 325 | (account_id, email_address, external_id) |
| 326 | values (%s, %s, %s)""", |
| 327 | (account_id, user_details['email'], "mailto:%s" % |
| 328 | user_details['email'])) |
| 329 | |
| 330 | if account_id is not None: |
Andrew Hutchings | fc29e16 | 2012-05-18 14:33:37 +0100 | [diff] [blame] | 331 | # account_ssh_keys |
| 332 | user_details['ssh_keys'] = ["%s %s %s" % (get_type(key.keytype), key.keytext, key.comment) for key in member.sshkeys] |
| 333 | |
| 334 | for key in user_details['ssh_keys']: |
| 335 | |
| 336 | cur.execute("""select ssh_public_key from account_ssh_keys where |
| 337 | account_id = %s""", account_id) |
Monty Taylor | 73acb4b | 2012-05-27 16:16:55 +0000 | [diff] [blame] | 338 | db_keys = [r[0].strip() for r in cur.fetchall()] |
Andrew Hutchings | fc29e16 | 2012-05-18 14:33:37 +0100 | [diff] [blame] | 339 | if key.strip() not in db_keys: |
| 340 | |
| 341 | cur.execute("""select max(seq)+1 from account_ssh_keys |
| 342 | where account_id = %s""", account_id) |
| 343 | seq = cur.fetchall()[0][0] |
| 344 | if seq is None: |
| 345 | seq = 1 |
| 346 | cur.execute("""insert into account_ssh_keys |
| 347 | (ssh_public_key, valid, account_id, seq) |
| 348 | values |
| 349 | (%s, 'Y', %s, %s)""", |
| 350 | (key.strip(), account_id, seq)) |
| 351 | |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 352 | # account_group_members |
| 353 | # user_details['add_groups'] is a list of group names for which the |
| 354 | # user is either "Approved" or "Administrator" |
| 355 | |
| 356 | groups_to_add = [] |
| 357 | groups_to_watch = {} |
| 358 | groups_to_rm = {} |
| 359 | |
| 360 | for group in user_details['add_groups']: |
| 361 | # if you are in the group nova-core, that should also put you in nova |
| 362 | add_groups = group_implies_groups[group_ids[group]] |
| 363 | add_groups.append(group_ids[group]) |
| 364 | for add_group in add_groups: |
| 365 | if add_group not in groups_to_add: |
| 366 | groups_to_add.append(add_group) |
| 367 | # We only want to add watches for direct project membership groups |
| 368 | groups_to_watch[group_ids[group]] = group |
| 369 | |
| 370 | # groups_to_add is now the full list of all groups we think the user |
| 371 | # should belong to. we want to limit the users groups to this list |
| 372 | for group in groups: |
| 373 | if group_ids[group] not in groups_to_add: |
| 374 | if group not in groups_to_rm.values(): |
| 375 | groups_to_rm[group_ids[group]] = group |
| 376 | |
| 377 | for group_id in groups_to_add: |
| 378 | if not cur.execute("""select account_id from account_group_members |
| 379 | where account_id = %s and group_id = %s""", |
| 380 | (account_id, group_id)): |
| 381 | # The current user does not exist in the group. Add it. |
| 382 | cur.execute("""insert into account_group_members |
| 383 | (account_id, group_id) |
| 384 | values (%s, %s)""", (account_id, group_id)) |
| 385 | os_project_name = groups_to_watch.get(group_id, None) |
| 386 | if os_project_name is not None: |
| 387 | if os_project_name.endswith("-core"): |
| 388 | os_project_name = os_project_name[:-5] |
Andrew Hutchings | 6a17893 | 2012-05-17 14:53:01 +0100 | [diff] [blame] | 389 | os_project_name = "{site}/{project}".format(site=options.site, project=os_project_name) |
Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 390 | if os_project_name in projects: |
| 391 | if not cur.execute("""select account_id |
| 392 | from account_project_watches |
| 393 | where account_id = %s |
| 394 | and project_name = %s""", |
| 395 | (account_id, os_project_name)): |
| 396 | cur.execute("""insert into account_project_watches |
| 397 | VALUES |
| 398 | ("Y", "N", "N", %s, %s, "*")""", |
| 399 | (account_id, os_project_name)) |
| 400 | |
| 401 | for (group_id, group_name) in groups_to_rm.items(): |
| 402 | cur.execute("""delete from account_group_members |
| 403 | where account_id = %s and group_id = %s""", |
| 404 | (account_id, group_id)) |
| 405 | |
| 406 | os.system("ssh -i %s -p29418 %s@localhost gerrit flush-caches" % |
| 407 | (GERRIT_SSH_KEY, GERRIT_USER)) |
| 408 | |
| 409 | conn.commit() |