Add current version of the pkgdb-sync-bugzilla script
This commit is contained in:
parent
db74cc47c6
commit
400b94bd5d
1 changed files with 373 additions and 0 deletions
373
roles/pkgdb2/files/pkgdb-sync-bugzilla
Executable file
373
roles/pkgdb2/files/pkgdb-sync-bugzilla
Executable file
|
@ -0,0 +1,373 @@
|
||||||
|
#!/usr/bin/python -tt
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright © 2013-2014 Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# This copyrighted material is made available to anyone wishing to use, modify,
|
||||||
|
# copy, or redistribute it subject to the terms and conditions of the GNU
|
||||||
|
# General Public License v.2, or (at your option) any later version. This
|
||||||
|
# program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
# WARRANTY expressed or implied, including the implied warranties of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
# Public License for more details. You should have received a copy of the GNU
|
||||||
|
# General Public License along with this program; if not, write to the Free
|
||||||
|
# Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
# 02110-1301, USA. Any Red Hat trademarks that are incorporated in the source
|
||||||
|
# code or documentation are not subject to the GNU General Public License and
|
||||||
|
# may only be used or replicated with the express permission of Red Hat, Inc.
|
||||||
|
#
|
||||||
|
# Red Hat Author(s): Toshio Kuratomi <tkuratom@redhat.com>
|
||||||
|
# Author(s): Mike Watters <valholla75@fedoraproject.org>
|
||||||
|
# Author(s): Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||||
|
#
|
||||||
|
'''
|
||||||
|
sync information from the packagedb into bugzilla
|
||||||
|
|
||||||
|
This short script takes information about package onwership and imports it
|
||||||
|
into bugzilla.
|
||||||
|
'''
|
||||||
|
|
||||||
|
## These two lines are needed to run on EL6
|
||||||
|
__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4']
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import itertools
|
||||||
|
import xmlrpclib
|
||||||
|
import codecs
|
||||||
|
import smtplib
|
||||||
|
import bugzilla
|
||||||
|
import requests
|
||||||
|
from email.Message import Message
|
||||||
|
from fedora.client.fas2 import AccountSystem
|
||||||
|
|
||||||
|
|
||||||
|
if 'PKGDB2_CONFIG' not in os.environ \
|
||||||
|
and os.path.exists('/etc/pkgdb2/pkgdb2.cfg'):
|
||||||
|
print 'Using configuration file `/etc/pkgdb2/pkgdb2.cfg`'
|
||||||
|
os.environ['PKGDB2_CONFIG'] = '/etc/pkgdb2/pkgdb2.cfg'
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pkgdb2
|
||||||
|
except ImportError:
|
||||||
|
sys.path.insert(
|
||||||
|
0, os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
|
||||||
|
import pkgdb2
|
||||||
|
|
||||||
|
|
||||||
|
BZSERVER = pkgdb2.APP.config.get('PKGDB2_BUGZILLA_URL')
|
||||||
|
BZUSER = pkgdb2.APP.config.get('PKGDB2_BUGZILLA_NOTIFY_USER')
|
||||||
|
BZPASS = pkgdb2.APP.config.get('PKGDB2_BUGZILLA_NOTIFY_PASSWORD')
|
||||||
|
BZCOMPAPI = pkgdb2.APP.config.get('BUGZILLA_COMPONENT_API')
|
||||||
|
FASURL = pkgdb2.APP.config.get('PKGDB2_FAS_URL')
|
||||||
|
FASUSER = pkgdb2.APP.config.get('PKGDB2_FAS_USER')
|
||||||
|
FASPASS = pkgdb2.APP.config.get('PKGDB2_FAS_PASSWORD')
|
||||||
|
FASINSECURE = pkgdb2.APP.config.get('PKGDB2_FAS_INSECURE')
|
||||||
|
NOTIFYEMAIL = pkgdb2.APP.config.get('PKGDB2_BUGZILLA_NOTIFY_EMAIL')
|
||||||
|
PKGDBSERVER = pkgdb2.APP.config.get('SITE_URL')
|
||||||
|
DRY_RUN = pkgdb2.APP.config.get('PKGDB2_BUGZILLA_DRY_RUN', False)
|
||||||
|
|
||||||
|
# When querying for current info, take segments of 1000 packages a time
|
||||||
|
BZ_PKG_SEGMENT = 1000
|
||||||
|
|
||||||
|
class DataChangedError(Exception):
|
||||||
|
'''Raised when data we are manipulating changes while we're modifying it.'''
|
||||||
|
pass
|
||||||
|
|
||||||
|
def segment(iterable, chunk, fill=None):
|
||||||
|
'''Collect data into `chunk` sized block'''
|
||||||
|
args = [iter(iterable)] * chunk
|
||||||
|
return itertools.izip_longest(*args, fillvalue=fill)
|
||||||
|
|
||||||
|
class ProductCache(dict):
|
||||||
|
def __init__(self, bz, acls):
|
||||||
|
self.bz = bz
|
||||||
|
self.acls = acls
|
||||||
|
|
||||||
|
# Ask bugzilla for a section of the pkglist.
|
||||||
|
# Save the information from the section that we want.
|
||||||
|
def __getitem__(self, key):
|
||||||
|
try:
|
||||||
|
return super(ProductCache, self).__getitem__(key)
|
||||||
|
except KeyError:
|
||||||
|
# We can only cache products we have pkgdb information for
|
||||||
|
if key not in self.acls:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if BZCOMPAPI == 'getcomponentsdetails':
|
||||||
|
# Old API -- in python-bugzilla. But with current server, this
|
||||||
|
# gives ProxyError
|
||||||
|
products = self.server.getcomponentsdetails(key)
|
||||||
|
elif BZCOMPAPI == 'component.get':
|
||||||
|
# Way that's undocumented in the partner-bugzilla api but works
|
||||||
|
# currently
|
||||||
|
pkglist = acls[key].keys()
|
||||||
|
products = {}
|
||||||
|
for pkg_segment in segment(pkglist, BZ_PKG_SEGMENT):
|
||||||
|
# Format that bugzilla will understand. Strip None's that segment() pads
|
||||||
|
# out the final data segment() with
|
||||||
|
query = [dict(product=key, component=p) for p in pkg_segment if p is not None]
|
||||||
|
raw_data = self.bz._proxy.Component.get(dict(names=query))
|
||||||
|
for package in raw_data['components']:
|
||||||
|
# Reformat data to be the same as what's returned from
|
||||||
|
# getcomponentsdetails
|
||||||
|
product = dict(initialowner=package['default_assignee'],
|
||||||
|
description=package['description'],
|
||||||
|
initialqacontact=package['default_qa_contact'],
|
||||||
|
initialcclist=package['default_cc'])
|
||||||
|
products[package['name'].lower()] = product
|
||||||
|
self[key] = products
|
||||||
|
|
||||||
|
return super(ProductCache, self).__getitem__(key)
|
||||||
|
|
||||||
|
|
||||||
|
class Bugzilla(object):
|
||||||
|
|
||||||
|
def __init__(self, bzServer, username, password, acls):
|
||||||
|
self.bzXmlRpcServer = bzServer
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
self.server = bugzilla.Bugzilla(
|
||||||
|
url=self.bzXmlRpcServer,
|
||||||
|
user=self.username,
|
||||||
|
password=self.password)
|
||||||
|
self.productCache = ProductCache(self.server, acls)
|
||||||
|
|
||||||
|
# Connect to the fedora account system
|
||||||
|
self.fas = AccountSystem(
|
||||||
|
base_url=FASURL,
|
||||||
|
username=FASUSER,
|
||||||
|
password=FASPASS)
|
||||||
|
self.userCache = self.fas.people_by_key(
|
||||||
|
key='username',
|
||||||
|
fields=['bugzilla_email'])
|
||||||
|
|
||||||
|
def _get_bugzilla_email(self, username):
|
||||||
|
'''Return the bugzilla email address for a user.
|
||||||
|
|
||||||
|
First looks in a cache for a username => bugzilla email. If not found,
|
||||||
|
reloads the cache from fas and tries again.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
return self.userCache[username]['bugzilla_email'].lower()
|
||||||
|
except KeyError:
|
||||||
|
if username.startswith('@'):
|
||||||
|
group = self.fas.group_by_name(username[1:])
|
||||||
|
self.userCache[username] = {
|
||||||
|
'bugzilla_email': group.mailing_list}
|
||||||
|
else:
|
||||||
|
person = self.fas.person_by_username(username)
|
||||||
|
bz_email = person.get('bugzilla_email', None)
|
||||||
|
if bz_email is None:
|
||||||
|
print '%s has no bugzilla email, valid account?' % username
|
||||||
|
else:
|
||||||
|
self.userCache[username] = {'bugzilla_email': bz_email}
|
||||||
|
return self.userCache[username]['bugzilla_email'].lower()
|
||||||
|
|
||||||
|
def add_edit_component(self, package, collection, owner, description,
|
||||||
|
qacontact=None, cclist=None):
|
||||||
|
'''Add or update a component to have the values specified.
|
||||||
|
'''
|
||||||
|
# Turn the cclist into something usable by bugzilla
|
||||||
|
if not cclist or 'people' not in cclist:
|
||||||
|
initialCCList = list()
|
||||||
|
else:
|
||||||
|
initialCCList = [
|
||||||
|
self._get_bugzilla_email(cc) for cc in cclist['people']]
|
||||||
|
if 'groups' in cclist:
|
||||||
|
group_cc = [
|
||||||
|
self._get_bugzilla_email(cc) for cc in cclist['groups']]
|
||||||
|
initialCCList.extend(group_cc)
|
||||||
|
|
||||||
|
# Add owner to the cclist so comaintainers taking over a bug don't
|
||||||
|
# have to do this manually
|
||||||
|
owner = self._get_bugzilla_email(owner)
|
||||||
|
if owner not in initialCCList:
|
||||||
|
initialCCList.append(owner)
|
||||||
|
|
||||||
|
# Lookup product
|
||||||
|
try:
|
||||||
|
product = self.productCache[collection]
|
||||||
|
except xmlrpclib.Fault as e:
|
||||||
|
# Output something useful in args
|
||||||
|
e.args = (e.faultCode, e.faultString)
|
||||||
|
raise
|
||||||
|
except xmlrpclib.ProtocolError as e:
|
||||||
|
e.args = ('ProtocolError', e.errcode, e.errmsg)
|
||||||
|
raise
|
||||||
|
|
||||||
|
pkgKey = package.lower()
|
||||||
|
if pkgKey in product:
|
||||||
|
# edit the package information
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
# Grab bugzilla email for things changable via xmlrpc
|
||||||
|
if qacontact:
|
||||||
|
qacontact = self._get_bugzilla_email(qacontact)
|
||||||
|
else:
|
||||||
|
qacontact = 'extras-qa@fedoraproject.org'
|
||||||
|
|
||||||
|
# Check for changes to the owner, qacontact, or description
|
||||||
|
if product[pkgKey]['initialowner'] != owner:
|
||||||
|
data['initialowner'] = owner
|
||||||
|
|
||||||
|
if product[pkgKey]['description'] != description:
|
||||||
|
data['description'] = description
|
||||||
|
if product[pkgKey]['initialqacontact'] != qacontact and (
|
||||||
|
qacontact or product[pkgKey]['initialqacontact']):
|
||||||
|
data['initialqacontact'] = qacontact
|
||||||
|
|
||||||
|
if len(product[pkgKey]['initialcclist']) != len(initialCCList):
|
||||||
|
data['initialcclist'] = initialCCList
|
||||||
|
else:
|
||||||
|
for ccMember in product[pkgKey]['initialcclist']:
|
||||||
|
if ccMember not in initialCCList:
|
||||||
|
data['initialcclist'] = initialCCList
|
||||||
|
break
|
||||||
|
|
||||||
|
if data:
|
||||||
|
### FIXME: initialowner has been made mandatory for some
|
||||||
|
# reason. Asking dkl why.
|
||||||
|
data['initialowner'] = owner
|
||||||
|
|
||||||
|
# Changes occurred. Submit a request to change via xmlrpc
|
||||||
|
data['product'] = collection
|
||||||
|
data['component'] = package
|
||||||
|
if DRY_RUN:
|
||||||
|
print '[EDITCOMP] Changing via editComponent(' \
|
||||||
|
'%s, %s, "xxxxx")' % (data, self.username)
|
||||||
|
print '[EDITCOMP] Former values: %s|%s|%s|%s' % (
|
||||||
|
product[pkgKey]['initialowner'],
|
||||||
|
product[pkgKey]['description'],
|
||||||
|
product[pkgKey]['initialqacontact'],
|
||||||
|
product[pkgKey]['initialcclist'])
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.server.editcomponent(data)
|
||||||
|
except xmlrpclib.Fault, e:
|
||||||
|
# Output something useful in args
|
||||||
|
e.args = (data, e.faultCode, e.faultString)
|
||||||
|
raise
|
||||||
|
except xmlrpclib.ProtocolError, e:
|
||||||
|
e.args = ('ProtocolError', e.errcode, e.errmsg)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# Add component
|
||||||
|
if qacontact:
|
||||||
|
qacontact = self._get_bugzilla_email(qacontact)
|
||||||
|
else:
|
||||||
|
qacontact = 'extras-qa@fedoraproject.org'
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'product': collection,
|
||||||
|
'component': package,
|
||||||
|
'description': description,
|
||||||
|
'initialowner': owner,
|
||||||
|
'initialqacontact': qacontact
|
||||||
|
}
|
||||||
|
if initialCCList:
|
||||||
|
data['initialcclist'] = initialCCList
|
||||||
|
|
||||||
|
if DRY_RUN:
|
||||||
|
print '[ADDCOMP] Adding new component AddComponent:(' \
|
||||||
|
'%s, %s, "xxxxx")' % (data, self.username)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.server.addcomponent(data)
|
||||||
|
except xmlrpclib.Fault, e:
|
||||||
|
# Output something useful in args
|
||||||
|
e.args = (data, e.faultCode, e.faultString)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def send_email(fromAddress, toAddress, subject, message):
|
||||||
|
'''Send an email if there's an error.
|
||||||
|
|
||||||
|
This will be replaced by sending messages to a log later.
|
||||||
|
'''
|
||||||
|
msg = Message()
|
||||||
|
msg.add_header('To', ','.join(toAddress))
|
||||||
|
msg.add_header('From', fromAddress)
|
||||||
|
msg.add_header('Subject', subject)
|
||||||
|
msg.set_payload(message)
|
||||||
|
smtp = smtplib.SMTP('bastion')
|
||||||
|
smtp.sendmail(fromAddress, toAddress, msg.as_string())
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Script syncing information between pkgdb and bugzilla'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug', dest='debug', action='store_true', default=False,
|
||||||
|
help='Print the changes instead of making them in bugzilla')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
DRY_RUN = True
|
||||||
|
|
||||||
|
# Non-fatal errors to alert people about
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Get bugzilla information from the package database
|
||||||
|
req = requests.get('%s/api/bugzilla/?format=json' % PKGDBSERVER)
|
||||||
|
acls = req.json()['bugzillaAcls']
|
||||||
|
|
||||||
|
# Initialize the connection to bugzilla
|
||||||
|
bugzilla = Bugzilla(BZSERVER, BZUSER, BZPASS, acls)
|
||||||
|
|
||||||
|
for product in acls.keys():
|
||||||
|
if product not in ('Fedora', 'Fedora EPEL'):
|
||||||
|
continue
|
||||||
|
for pkg in acls[product]:
|
||||||
|
if DRY_RUN:
|
||||||
|
print pkg
|
||||||
|
pkgInfo = acls[product][pkg]
|
||||||
|
try:
|
||||||
|
bugzilla.add_edit_component(
|
||||||
|
pkg,
|
||||||
|
product,
|
||||||
|
pkgInfo['owner'],
|
||||||
|
pkgInfo['summary'],
|
||||||
|
pkgInfo['qacontact'],
|
||||||
|
pkgInfo['cclist'])
|
||||||
|
except ValueError, e:
|
||||||
|
# A username didn't have a bugzilla address
|
||||||
|
errors.append(str(e.args))
|
||||||
|
except DataChangedError, e:
|
||||||
|
# A Package or Collection was returned via xmlrpc but wasn't
|
||||||
|
# present when we tried to change it
|
||||||
|
errors.append(str(e.args))
|
||||||
|
except xmlrpclib.ProtocolError, e:
|
||||||
|
# Unrecoverable and likely means that nothing is going to
|
||||||
|
# succeed.
|
||||||
|
errors.append(str(e.args))
|
||||||
|
break
|
||||||
|
except xmlrpclib.Error, e:
|
||||||
|
# An error occurred in the xmlrpc call. Shouldn't happen but
|
||||||
|
# we better see what it is
|
||||||
|
errors.append(str(e.args))
|
||||||
|
|
||||||
|
# Send notification of errors
|
||||||
|
if errors:
|
||||||
|
#print '[DEBUG]', '\n'.join(errors)
|
||||||
|
send_email('accounts@fedoraproject.org',
|
||||||
|
NOTIFYEMAIL,
|
||||||
|
'Errors while syncing bugzilla with the PackageDB',
|
||||||
|
'''
|
||||||
|
The following errors were encountered while updating bugzilla with information
|
||||||
|
from the Package Database. Please have the problems taken care of:
|
||||||
|
|
||||||
|
%s
|
||||||
|
''' % ('\n'.join(errors),))
|
||||||
|
|
||||||
|
sys.exit(0)
|
Loading…
Add table
Add a link
Reference in a new issue