blob: 15baaa9b799ce8c5d88f3c4ef2205881e0426e2c [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
Monty Taylorf45f6ca2012-05-01 17:11:48 -040022import subprocess
23
24from datetime import datetime
25
Monty Taylorc4381592012-05-19 11:14:27 -040026# 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
29import pkg_resources
30
Monty Taylorf45f6ca2012-05-01 17:11:48 -040031import StringIO
32import ConfigParser
Andrew Hutchings6a178932012-05-17 14:53:01 +010033import argparse
Monty Taylorf45f6ca2012-05-01 17:11:48 -040034import MySQLdb
35
36from launchpadlib.launchpad import Launchpad
37from launchpadlib.uris import LPNET_SERVICE_ROOT
38
39from openid.consumer import consumer
40from openid.cryptutil import randomString
41
42DEBUG = False
43
Andrew Hutchings16a6c462012-05-25 14:26:41 +010044# suppress pyflakes
45pkg_resources.get_supported_platform()
46
Andrew Hutchingsfc29e162012-05-18 14:33:37 +010047pid_file = '/tmp/update_gerrit_users.pid'
48fp = open(pid_file, 'w')
49try:
50 fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
51except IOError:
52 # another instance is running
53 sys.exit(0)
54
Andrew Hutchings6a178932012-05-17 14:53:01 +010055parser = argparse.ArgumentParser()
56parser.add_argument('user', help='The gerrit admin user')
57parser.add_argument('ssh_key', help='The gerrit admin SSH key file')
58parser.add_argument('site', help='The site in use (typically openstack or stackforge)')
59options = parser.parse_args()
60
61GERRIT_USER = options.user
Monty Taylorf45f6ca2012-05-01 17:11:48 -040062GERRIT_CONFIG = os.environ.get('GERRIT_CONFIG',
63 '/home/gerrit2/review_site/etc/gerrit.config')
64GERRIT_SECURE_CONFIG = os.environ.get('GERRIT_SECURE_CONFIG',
65 '/home/gerrit2/review_site/etc/secure.config')
Andrew Hutchings6a178932012-05-17 14:53:01 +010066GERRIT_SSH_KEY = options.ssh_key
Monty Taylorf45f6ca2012-05-01 17:11:48 -040067GERRIT_CACHE_DIR = os.path.expanduser(os.environ.get('GERRIT_CACHE_DIR',
68 '~/.launchpadlib/cache'))
69GERRIT_CREDENTIALS = os.path.expanduser(os.environ.get('GERRIT_CREDENTIALS',
70 '~/.launchpadlib/creds'))
71GERRIT_BACKUP_PATH = os.environ.get('GERRIT_BACKUP_PATH',
72 '/home/gerrit2/dbupdates')
73
74for 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
80def 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
92def get_type(in_type):
93 if in_type == "RSA":
94 return "ssh-rsa"
95 else:
96 return "ssh-dsa"
97
98gerrit_config = get_broken_config(GERRIT_CONFIG)
99secure_config = get_broken_config(GERRIT_SECURE_CONFIG)
100
101DB_USER = gerrit_config.get("database", "username")
102DB_PASS = secure_config.get("database","password")
103DB_DB = gerrit_config.get("database","database")
104
105db_backup_file = "%s.%s.sql" % (DB_DB, datetime.isoformat(datetime.now()))
106db_backup_path = os.path.join(GERRIT_BACKUP_PATH, db_backup_file)
David Shrewsbury54a63902012-05-03 09:27:14 -0400107retval = os.system("mysqldump --opt -u%s -p%s %s | gzip -9 > %s.gz" %
Monty Taylorf45f6ca2012-05-01 17:11:48 -0400108 (DB_USER, DB_PASS, DB_DB, db_backup_path))
109if retval != 0:
110 print "Problem taking a db dump, aborting db update"
111 sys.exit(retval)
112
113conn = MySQLdb.connect(user = DB_USER, passwd = DB_PASS, db = DB_DB)
114cur = conn.cursor()
115
116
117launchpad = Launchpad.login_with('Gerrit User Sync', LPNET_SERVICE_ROOT,
118 GERRIT_CACHE_DIR,
119 credentials_file = GERRIT_CREDENTIALS)
120
121def 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
129teams_todo = get_sub_teams('openstack', [])
130
131users={}
132groups={}
133groups_in_groups={}
134group_implies_groups={}
135group_ids={}
136projects = 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
141for 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
176for (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
186for (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}}
216for (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]}
231for 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
244if 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
262for (username, user_details) in users.items():
Andrew Hutchingsfc29e162012-05-18 14:33:37 +0100263 member = launchpad.people[username]
Monty Taylorf45f6ca2012-05-01 17:11:48 -0400264 # 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 Taylorf45f6ca2012-05-01 17:11:48 -0400272 # We need details
Monty Taylorf45f6ca2012-05-01 17:11:48 -0400273 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 Taylorf45f6ca2012-05-01 17:11:48 -0400289 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 Taylorf45f6ca2012-05-01 17:11:48 -0400304 # 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 Hutchingsfc29e162012-05-18 14:33:37 +0100331 # 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)
338 db_keys = [r[0].strip() for r in cur.fetchall()]
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 Taylorf45f6ca2012-05-01 17:11:48 -0400352 # 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 Hutchings6a178932012-05-17 14:53:01 +0100389 os_project_name = "{site}/{project}".format(site=options.site, project=os_project_name)
Monty Taylorf45f6ca2012-05-01 17:11:48 -0400390 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
406os.system("ssh -i %s -p29418 %s@localhost gerrit flush-caches" %
407 (GERRIT_SSH_KEY, GERRIT_USER))
408
409conn.commit()