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