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