blob: 68162910f8472a29092033eaf3957248d7064173 [file] [log] [blame]
Monty Taylorf45f6ca2012-05-01 17:11:48 -04001#! /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
18import os
19import sys
20import uuid
21import os
22import subprocess
23
24from datetime import datetime
25
26import StringIO
27import ConfigParser
Andrew Hutchings6a178932012-05-17 14:53:01 +010028import argparse
Monty Taylorf45f6ca2012-05-01 17:11:48 -040029import MySQLdb
30
31from launchpadlib.launchpad import Launchpad
32from launchpadlib.uris import LPNET_SERVICE_ROOT
33
34from openid.consumer import consumer
35from openid.cryptutil import randomString
36
37DEBUG = False
38
Andrew Hutchings6a178932012-05-17 14:53:01 +010039parser = argparse.ArgumentParser()
40parser.add_argument('user', help='The gerrit admin user')
41parser.add_argument('ssh_key', help='The gerrit admin SSH key file')
42parser.add_argument('site', help='The site in use (typically openstack or stackforge)')
43options = parser.parse_args()
44
45GERRIT_USER = options.user
Monty Taylorf45f6ca2012-05-01 17:11:48 -040046GERRIT_CONFIG = os.environ.get('GERRIT_CONFIG',
47 '/home/gerrit2/review_site/etc/gerrit.config')
48GERRIT_SECURE_CONFIG = os.environ.get('GERRIT_SECURE_CONFIG',
49 '/home/gerrit2/review_site/etc/secure.config')
Andrew Hutchings6a178932012-05-17 14:53:01 +010050GERRIT_SSH_KEY = options.ssh_key
Monty Taylorf45f6ca2012-05-01 17:11:48 -040051GERRIT_CACHE_DIR = os.path.expanduser(os.environ.get('GERRIT_CACHE_DIR',
52 '~/.launchpadlib/cache'))
53GERRIT_CREDENTIALS = os.path.expanduser(os.environ.get('GERRIT_CREDENTIALS',
54 '~/.launchpadlib/creds'))
55GERRIT_BACKUP_PATH = os.environ.get('GERRIT_BACKUP_PATH',
56 '/home/gerrit2/dbupdates')
57
58for 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
64def 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
76def get_type(in_type):
77 if in_type == "RSA":
78 return "ssh-rsa"
79 else:
80 return "ssh-dsa"
81
82gerrit_config = get_broken_config(GERRIT_CONFIG)
83secure_config = get_broken_config(GERRIT_SECURE_CONFIG)
84
85DB_USER = gerrit_config.get("database", "username")
86DB_PASS = secure_config.get("database","password")
87DB_DB = gerrit_config.get("database","database")
88
89db_backup_file = "%s.%s.sql" % (DB_DB, datetime.isoformat(datetime.now()))
90db_backup_path = os.path.join(GERRIT_BACKUP_PATH, db_backup_file)
David Shrewsbury54a63902012-05-03 09:27:14 -040091retval = os.system("mysqldump --opt -u%s -p%s %s | gzip -9 > %s.gz" %
Monty Taylorf45f6ca2012-05-01 17:11:48 -040092 (DB_USER, DB_PASS, DB_DB, db_backup_path))
93if retval != 0:
94 print "Problem taking a db dump, aborting db update"
95 sys.exit(retval)
96
97conn = MySQLdb.connect(user = DB_USER, passwd = DB_PASS, db = DB_DB)
98cur = conn.cursor()
99
100
101launchpad = Launchpad.login_with('Gerrit User Sync', LPNET_SERVICE_ROOT,
102 GERRIT_CACHE_DIR,
103 credentials_file = GERRIT_CREDENTIALS)
104
105def 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
113teams_todo = get_sub_teams('openstack', [])
114
115users={}
116groups={}
117groups_in_groups={}
118group_implies_groups={}
119group_ids={}
120projects = 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
125for 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
160for (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
170for (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}}
200for (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]}
215for 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
228if 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
246for (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 Hutchings6a178932012-05-17 14:53:01 +0100376 os_project_name = "{site}/{project}".format(site=options.site, project=os_project_name)
Monty Taylorf45f6ca2012-05-01 17:11:48 -0400377 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
393os.system("ssh -i %s -p29418 %s@localhost gerrit flush-caches" %
394 (GERRIT_SSH_KEY, GERRIT_USER))
395
396conn.commit()