d20c4523bf
Additionally, the file installation of the scripts wasn't working, so the sync script wasn't actually getting installed. This moves the underlying scripts to be installed by the gerrit module, because it owns /usr/local/gerrit/scripts, and then manages the gerrit hooks which call those scripts in the openstack_project class, since that's where the config choice to enable those functions really should live. Change-Id: I54fb9edd9fb0c634d8d9de4e57f9ddad6af63a99
432 lines
17 KiB
Python
432 lines
17 KiB
Python
#! /usr/bin/env python
|
|
# Copyright (C) 2012 OpenStack, LLC.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
# Synchronize Gerrit users from Launchpad.
|
|
# TODO items:
|
|
# 1. add a temporary (instance level) object store for the launchpad class
|
|
# 2. split out the two classes into separate files to be used as a library
|
|
|
|
import os
|
|
import sys
|
|
import ConfigParser
|
|
import StringIO
|
|
import paramiko
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from launchpadlib.launchpad import Launchpad
|
|
from launchpadlib.uris import LPNET_SERVICE_ROOT
|
|
|
|
from datetime import datetime
|
|
|
|
from openid.consumer import consumer
|
|
from openid.cryptutil import randomString
|
|
|
|
GERRIT_USER = os.environ.get('GERRIT_USER', 'launchpadsync')
|
|
GERRIT_CONFIG = os.environ.get('GERRIT_CONFIG',
|
|
'/home/gerrit2/review_site/etc/gerrit.config')
|
|
GERRIT_SECURE_CONFIG = os.environ.get('GERRIT_SECURE_CONFIG',
|
|
'/home/gerrit2/review_site/etc/secure.config')
|
|
GERRIT_SSH_KEY = os.environ.get('GERRIT_SSH_KEY',
|
|
'/home/gerrit2/.ssh/launchpadsync_rsa')
|
|
GERRIT_CACHE_DIR = os.path.expanduser(os.environ.get('GERRIT_CACHE_DIR',
|
|
'~/.launchpadlib/cache'))
|
|
GERRIT_CREDENTIALS = os.path.expanduser(os.environ.get('GERRIT_CREDENTIALS',
|
|
'~/.launchpadlib/creds'))
|
|
GERRIT_BACKUP_PATH = os.environ.get('GERRIT_BACKUP_PATH',
|
|
'/home/gerrit2/dbupdates')
|
|
|
|
logging.basicConfig(format='%(asctime)-6s: %(name)s - %(levelname)s - %(message)s', filename='/var/log/gerrit/update_users.log')
|
|
logger= logging.getLogger('update_users')
|
|
logger.setLevel(logging.INFO)
|
|
|
|
for check_path in (os.path.dirname(GERRIT_CACHE_DIR),
|
|
os.path.dirname(GERRIT_CREDENTIALS),
|
|
GERRIT_BACKUP_PATH):
|
|
if not os.path.exists(check_path):
|
|
os.makedirs(check_path)
|
|
|
|
def get_broken_config(filename):
|
|
""" gerrit config ini files are broken and have leading tabs """
|
|
text = ""
|
|
with open(filename,"r") as conf:
|
|
for line in conf.readlines():
|
|
text = "%s%s" % (text, line.lstrip())
|
|
|
|
fp = StringIO.StringIO(text)
|
|
c=ConfigParser.ConfigParser()
|
|
c.readfp(fp)
|
|
return c
|
|
|
|
gerrit_config = get_broken_config(GERRIT_CONFIG)
|
|
secure_config = get_broken_config(GERRIT_SECURE_CONFIG)
|
|
|
|
DB_USER = gerrit_config.get("database", "username")
|
|
DB_PASS = secure_config.get("database","password")
|
|
DB_DB = gerrit_config.get("database","database")
|
|
|
|
def make_db_backup():
|
|
db_backup_file = "%s.%s.sql" % (DB_DB, datetime.isoformat(datetime.now()))
|
|
db_backup_path = os.path.join(GERRIT_BACKUP_PATH, db_backup_file)
|
|
retval = os.system("mysqldump --opt -u%s -p%s %s | gzip -9 > %s.gz" %
|
|
(DB_USER, DB_PASS, DB_DB, db_backup_path))
|
|
if retval != 0:
|
|
logger.error("Problem taking a db dump, aborting db update")
|
|
sys.exit(retval)
|
|
|
|
class LaunchpadAction(object):
|
|
def __init__(self):
|
|
logger.info('Connecting to Launchpad')
|
|
self.launchpad= Launchpad.login_with('Gerrit User Sync', LPNET_SERVICE_ROOT,
|
|
GERRIT_CACHE_DIR,
|
|
credentials_file = GERRIT_CREDENTIALS)
|
|
|
|
logger.info('Getting Launchpad teams')
|
|
self.lp_teams= self.get_all_sub_teams('openstack', [])
|
|
|
|
def get_all_sub_teams(self, team, have_teams):
|
|
for sub_team in self.launchpad.people[team].sub_teams:
|
|
if sub_team.name not in have_teams:
|
|
have_teams = self.get_all_sub_teams(sub_team.name, have_teams)
|
|
have_teams.append(team)
|
|
return have_teams
|
|
|
|
def get_sub_teams(self, team):
|
|
sub_teams= []
|
|
for sub_team in self.launchpad.people[team].sub_teams:
|
|
sub_teams.append(sub_team.name)
|
|
return sub_teams
|
|
|
|
def get_teams(self):
|
|
return self.lp_teams
|
|
|
|
def get_all_users(self):
|
|
logger.info('Getting Launchpad users')
|
|
users= []
|
|
for team in self.lp_teams:
|
|
for detail in self.launchpad.people[team].members_details:
|
|
if (detail.status == 'Approved' or detail.status == 'Administrator'):
|
|
name= detail.self_link.split('/')[-1]
|
|
if ((users.count(name) == 0) and (name not in self.lp_teams)):
|
|
users.append(name)
|
|
return users
|
|
|
|
def get_user_data(self, user):
|
|
return self.launchpad.people[user]
|
|
|
|
def get_team_members(self, team, gerrit):
|
|
users= []
|
|
for detail in self.launchpad.people[team].members_details:
|
|
if (detail.status == 'Approved' or detail.status == 'Administrator'):
|
|
name= detail.self_link.split('/')[-1]
|
|
# if we found a subteam
|
|
if name in self.lp_teams:
|
|
# check subteam for implied subteams
|
|
for implied_group in gerrit.get_implied_groups(name):
|
|
if implied_group in self.lp_teams:
|
|
users.extend(self.get_team_members(implied_group, gerrit))
|
|
users.extend(self.get_team_members(name, gerrit))
|
|
continue
|
|
users.append(name)
|
|
# check team for implied teams
|
|
for implied_group in gerrit.get_implied_groups(team):
|
|
if implied_group in self.lp_teams:
|
|
users.extend(self.get_team_members(implied_group, gerrit))
|
|
# filter out dupes
|
|
users= list(set(users))
|
|
return users
|
|
|
|
def get_team_watches(self, team):
|
|
users= []
|
|
for detail in self.launchpad.people[team].members_details:
|
|
if (detail.status == 'Approved' or detail.status == 'Administrator'):
|
|
name= detail.self_link.split('/')[-1]
|
|
if name in self.lp_teams:
|
|
continue
|
|
if users.count(name) == 0:
|
|
users.append(name)
|
|
return users
|
|
|
|
def get_team_display_name(self, team):
|
|
team_data = self.launchpad.people[team]
|
|
return team_data.display_name
|
|
|
|
class GerritAction(object):
|
|
def __init__(self):
|
|
logger.info('Connecting to Gerrit')
|
|
self.ssh= paramiko.SSHClient()
|
|
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
self.ssh.connect('localhost', username=GERRIT_USER, port=29418, key_filename=GERRIT_SSH_KEY)
|
|
|
|
def cleanup(self):
|
|
logger.info('Closing connection to Gerrit')
|
|
self.ssh.close()
|
|
|
|
def run_query(self, query):
|
|
command= 'gerrit gsql --format JSON -c "{0}"'.format(query)
|
|
stdin, stdout, stderr= self.ssh.exec_command(command)
|
|
# trying to get stdout return code or stderr can hang with large result sets
|
|
# for line in stderr:
|
|
# logger.error(line)
|
|
return stdout
|
|
|
|
def get_groups(self):
|
|
logger.info('Getting Gerrit groups')
|
|
groups= []
|
|
query= "select name from account_groups"
|
|
stdout= self.run_query(query)
|
|
for line in stdout:
|
|
row= json.loads(line)
|
|
if row['type'] == 'row':
|
|
group= row['columns']['name']
|
|
groups.append(group)
|
|
return groups
|
|
|
|
def get_users(self):
|
|
logger.info('Getting Gerrit users')
|
|
users= []
|
|
query= "select external_id from account_external_ids"
|
|
stdout= self.run_query(query)
|
|
for line in stdout:
|
|
row= json.loads(line)
|
|
if row['type'] == 'row':
|
|
user= row['columns']['external_id'].replace('username:','')
|
|
users.append(user)
|
|
return users
|
|
|
|
def get_group_id(self, group_name):
|
|
query= "select group_id from account_groups where name='{0}'".format(group_name)
|
|
stdout= self.run_query(query)
|
|
line= stdout.readline()
|
|
row= json.loads(line)
|
|
if row['type'] == 'row':
|
|
return row['columns']['group_id']
|
|
else:
|
|
return 0
|
|
|
|
def get_user_id(self, user_name):
|
|
query= "select account_id from account_external_ids where external_id='username:{0}'".format(user_name)
|
|
stdout= self.run_query(query)
|
|
line= stdout.readline()
|
|
row= json.loads(line)
|
|
return row['columns']['account_id']
|
|
|
|
def get_users_from_group(self, group_name):
|
|
logger.info('Getting Gerrit users from group %s', group_name)
|
|
users= []
|
|
gid= self.get_group_id(group_name)
|
|
|
|
query= "select external_id from account_external_ids join account_group_members on account_group_members.account_id=account_external_ids.account_id where account_group_members.group_id={0} and external_id like 'username%%'".format(gid)
|
|
stdout= self.run_query(query)
|
|
for line in stdout:
|
|
row= json.loads(line)
|
|
if row['type'] == 'row':
|
|
user= row['columns']['external_id'].replace('username:','')
|
|
users.append(user)
|
|
return users
|
|
|
|
def get_users_from_watches(self, group_name):
|
|
logger.info('Getting Gerrit users from watch list %s', group_name)
|
|
users= []
|
|
if group_name.endswith("-core"):
|
|
group_name = group_name[:-5]
|
|
group_name = "openstack/{0}".format(group_name)
|
|
|
|
query= "select external_id from account_external_ids join account_project_watches on account_project_watches.account_id=account_external_ids.account_id where account_project_watches.project_name like '{0}' and external_id like 'username%%'".format(group_name)
|
|
stdout= self.run_query(query)
|
|
for line in stdout:
|
|
row= json.loads(line)
|
|
if row['type'] == 'row':
|
|
user= row['columns']['external_id'].replace('username:','')
|
|
users.append(user)
|
|
return users
|
|
|
|
|
|
def get_implied_groups(self, group_name):
|
|
gid= self.get_group_id(group_name)
|
|
groups= []
|
|
query= "select name from account_groups join account_group_includes on account_group_includes.include_id=account_groups.group_id where account_group_includes.group_id={0}".format(gid)
|
|
stdout= self.run_query(query)
|
|
for line in stdout:
|
|
row= json.loads(line)
|
|
if row['type'] == 'row':
|
|
group= row['columns']['name']
|
|
groups.append(group)
|
|
return groups
|
|
|
|
def add_group(self, group_name, group_display_name):
|
|
logger.info('New group %s (%s)', group_display_name, group)
|
|
query= "insert into account_group_id (s) values (NULL)"
|
|
stdout= self.run_query(query)
|
|
row= json.loads(stdout.readline())
|
|
if row['rowCount'] is not 1:
|
|
print "Could not get a new account group ID"
|
|
raise
|
|
query= "select max(s) from account_group_id"
|
|
stdout= self.run_query(query)
|
|
row= json.loads(stdout.readline())
|
|
gid= row['columns']['max(s)']
|
|
full_uuid= "{0}{1}".format(uuid.uuid4().hex, uuid.uuid4().hex[:8])
|
|
query= "insert into account_groups (group_id, group_type, owner_group_id, name, description, group_uuid) values ({0}, 'INTERNAL', 1, '{1}', '{2}', '{3}')". format(gid, group_name, group_display_name, full_uuid)
|
|
self.run_query(query)
|
|
query= "insert into account_group_names (group_id, name) values ({0}, '{1}')".format(gid, group_name)
|
|
self.run_query(query)
|
|
|
|
def add_user(self, user_name, user_data):
|
|
logger.info("Adding Gerrit user %s", user_name)
|
|
openid_consumer = consumer.Consumer(dict(id=randomString(16, '0123456789abcdef')), None)
|
|
openid_request = openid_consumer.begin("https://launchpad.net/~%s" % user_data.name)
|
|
user_openid_external_id = openid_request.endpoint.getLocalID()
|
|
query= "select account_id from account_external_ids where external_id in ('{0}')".format(user_openid_external_id)
|
|
stdout= self.run_query(query)
|
|
row= json.loads(stdout.readline())
|
|
if row['type'] == 'row':
|
|
# we have a result so this is an updated user name
|
|
account_id= row['columns']['account_id']
|
|
query= "update account_external_ids set external_id='{0}' where external_id like 'username%%' and account_id = {1}".format('username:%s' % user_name, account_id)
|
|
self.run_query(query)
|
|
else:
|
|
# we really do have a new user
|
|
user_ssh_keys= ["%s %s %s" % ('ssh-%s' % key.keytype.lower(), key.keytext, key.comment) for key in user_data.sshkeys]
|
|
user_email= None
|
|
try:
|
|
user_email = user_data.preferred_email_address.email
|
|
except ValueError:
|
|
pass
|
|
query= "insert into account_id (s) values (NULL)"
|
|
self.run_query(query)
|
|
query= "select max(s) from account_id"
|
|
stdout= self.run_query(query)
|
|
row= json.loads(stdout.readline())
|
|
uid= row['columns']['max(s)']
|
|
query= "insert into accounts (account_id, full_name, preferred_email) values ({0}, '{1}', '{2}')".format(uid, user_name, user_email)
|
|
self.run_query(query)
|
|
keyno= 1
|
|
for key in user_ssh_keys:
|
|
query= "insert into account_ssh_keys (ssh_public_key, valid, account_id, seq) values ('{0}', 'Y', {1}, {2})".format(key.strip(), uid, keyno)
|
|
self.run_query(query)
|
|
keyno = keyno + 1
|
|
query= "insert into account_external_ids (account_id, email_address, external_id) values ({0}, '{1}', '{2}')".format(uid, user_email, user_openid_external_id)
|
|
self.run_query(query)
|
|
query= "insert into account_external_ids (account_id, external_id) values ({0}, '{1}')".format(uid, "username:%s" % user_name)
|
|
self.run_query(query)
|
|
if user_email is not None:
|
|
query= "insert into account_external_ids (account_id, email_address, external_id) values ({0}. '{1}', '{2}')".format(uid, user_email, "mailto:%s" % user_email)
|
|
return None
|
|
|
|
def add_user_to_group(self, user_name, group_name):
|
|
logger.info("Adding Gerrit user %s to group %s", user_name, group_name)
|
|
uid= self.get_user_id(user_name)
|
|
gid= self.get_group_id(group_name)
|
|
if gid is 0:
|
|
print "Trying to add user {0} to non-existent group {1}".format(user_name, group_name)
|
|
raise
|
|
query= "insert into account_group_members (account_id, group_id) values ({0}, {1})".format(uid, gid)
|
|
self.run_query(query)
|
|
|
|
def add_user_to_watch(self, user_name, group_name):
|
|
logger.info("Adding Gerrit user %s to watch group %s", user_name, group_name)
|
|
uid= self.get_user_id(user_name)
|
|
if group_name.endswith("-core"):
|
|
group_name = group_name[:-5]
|
|
group_name = "openstack/{0}".format(group_name)
|
|
query= "insert into account_project_watches VALUES ('Y', 'N', 'N', {0}, '{1}', '*')". format(uid, group_name)
|
|
self.run_query(query)
|
|
|
|
|
|
def del_user_from_group(self, user_name, group_name):
|
|
logger.info("Deleting Gerrit user %s from group %s", user_name, group_name)
|
|
uid= self.get_user_id(user_name)
|
|
gid= self.get_group_id(group_name)
|
|
query= "delete from account_group_members where account_id = {0} and group_id = {1}".format(uid, gid)
|
|
self.run_query(query)
|
|
if group_name.endswith("-core"):
|
|
group_name = group_name[:-5]
|
|
group_name= "openstack/{0}".format(group_name)
|
|
query= "delete from account_project_watches where account_id = {0} and project_name= '{1}'".format(uid, group_name)
|
|
self.run_query(query)
|
|
|
|
def rebuild_sub_groups(self, group, sub_groups):
|
|
gid= self.get_group_id(group)
|
|
for sub_group in sub_groups:
|
|
sgid= self.get_group_id(sub_group)
|
|
query= "select group_id from account_group_includes where group_id={0} and include_id={1}".format(gid, sgid)
|
|
stdout= self.run_query(query)
|
|
row= json.loads(stdout.readline())
|
|
if row['type'] != 'row':
|
|
logger.info('Adding implied group %s to group %s', group, sub_group)
|
|
query= "insert into account_group_includes (group_id, include_id) values ({0}, {1})".format(gid, sgid)
|
|
self.run_query(query)
|
|
|
|
|
|
# Actual work starts here!
|
|
|
|
lp= LaunchpadAction()
|
|
gerrit= GerritAction()
|
|
|
|
logger.info('Making DB backup')
|
|
make_db_backup()
|
|
|
|
logger.info('Starting group reconcile')
|
|
lp_groups= lp.get_teams()
|
|
gerrit_groups= gerrit.get_groups()
|
|
|
|
group_diff= filter(lambda a: a not in gerrit_groups, lp_groups)
|
|
for group in group_diff:
|
|
group_display_name= lp.get_team_display_name(group)
|
|
gerrit.add_group(group, group_display_name)
|
|
|
|
for group in lp_groups:
|
|
sub_group= lp.get_sub_teams(group)
|
|
if sub_group:
|
|
gerrit.rebuild_sub_groups(group, sub_group)
|
|
|
|
logger.info('End group reconcile')
|
|
|
|
logger.info('Starting user reconcile')
|
|
lp_users= lp.get_all_users()
|
|
gerrit_users= gerrit.get_users()
|
|
|
|
user_diff= filter(lambda a: a not in gerrit_users, lp_users)
|
|
for user in user_diff:
|
|
gerrit.add_user(user, lp.get_user_data(user))
|
|
|
|
logger.info('End user reconcile')
|
|
|
|
logger.info('Starting user to group reconcile')
|
|
lp_groups= lp.get_teams()
|
|
for group in lp_groups:
|
|
# First find users to attach to groups
|
|
gerrit_group_users= gerrit.get_users_from_group(group)
|
|
lp_group_users= lp.get_team_members(group, gerrit)
|
|
|
|
group_diff= filter(lambda a: a not in gerrit_group_users, lp_group_users)
|
|
for user in group_diff:
|
|
gerrit.add_user_to_group(user, group)
|
|
# Second find users to attach to watches
|
|
lp_group_watches= lp.get_team_watches(group)
|
|
gerrit_group_watches= gerrit.get_users_from_watches(group)
|
|
group_diff= filter(lambda a: a not in gerrit_group_watches, lp_group_watches)
|
|
for user in group_diff:
|
|
gerrit.add_user_to_watch(user, group)
|
|
# Third find users to remove from groups/watches
|
|
group_diff= filter(lambda a: a not in lp_group_users, gerrit_group_users)
|
|
for user in group_diff:
|
|
gerrit.del_user_from_group(user, group)
|
|
|
|
logger.info('Ending user to group reconcile')
|
|
|
|
gerrit.cleanup()
|