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