#!/usr/bin/python -tt # -*- coding: utf-8 -*- # # Copyright © 2008 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 # Author(s): Mike Watters # ''' sync information from the packagedb into bugzilla This short script takes information about package onwership and imports it into bugzilla. ''' import sys import os import getopt import xmlrpclib import codecs import smtplib import bugzilla from configobj import ConfigObj, flatten_errors from email.Message import Message from fedora.client.fas2 import AccountSystem from fedora.client.pkgdb import PackageDB from validate import Validator import q vldtr = Validator() # configspec to set default values and validate types configspec = ''' [global] bugzilla.url = string(default = 'https://bugdev.devel.redhat.com/bugzilla-cvs/xmlrpc.cgi') bugzilla.username = string(default = '') bugzilla.password = string(default = '') bugzilla.component_api = string(default = 'component.get') fas.url = string(default = '') fas.username = string(default = '') fas.password = string(default = '') notify.email = force_list(default = list('')) debug = boolean(default = 'False') '''.splitlines() cfg = ConfigObj('/etc/pkgdb-sync-bugzilla.cfg', configspec = configspec) res = cfg.validate(vldtr, preserve_errors=True) for entry in flatten_errors(cfg, res): section_list, key, error = entry if error == False: restore_default(key) BZSERVER = cfg['global']['bugzilla.url'] BZUSER = cfg['global']['bugzilla.username'] BZPASS = cfg['global']['bugzilla.password'] BZCOMPAPI = cfg['global']['bugzilla.component_api'] FASURL = cfg['global']['fas.url'] FASUSER = cfg['global']['fas.username'] FASPASS = cfg['global']['fas.password'] NOTIFYEMAIL = cfg['global']['notify.email'] PKGDBSERVER = cfg['global']['pkgdbserver.url'] DRY_RUN = cfg['global']['debug'] class DataChangedError(Exception): '''Raised when data we are manipulating changes while we're modifying it.''' pass class ProductCache(dict): @q.t 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. @q.t def __getitem__(self, key): try: return q.q/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': q.q('getcomponentsdetail') # Old API -- in python-bugzilla. But with current server, this # gives ProxyError products = self.server.getcomponentsdetails(key) elif BZCOMPAPI == 'component.get': q.q('component.get') # Way that's undocumented in the partner-bugzilla api but works # currently pkglist = acls[key].keys() query = [dict(product=key, component=p) for p in pkglist] q.q(query) raw_data = self.bz._proxy.Component.get(dict(names=query)) q.q(raw_data) products = {} 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 q.q('past loop') self[key] = products return super(ProductCache, self).__getitem__(key) class Bugzilla(object): @q.t 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: person = self.fas.person_by_username(username) self.userCache[username] = {'bugzilla_email': person.bugzilla_email} return self.userCache[username]['bugzilla_email'].lower() @q.t 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']] # 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: q.q('here') product = self.productCache[collection] q.q('there') 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) opts, args = getopt.getopt(sys.argv[1:], '', ('usage', 'help')) if len(args) > 0: print ''' Usage: bz-make-components.py Sync package information from the package database to bugzilla. ''' sys.exit(1) # Non-fatal errors to alert people about errors = [] # Get bugzilla information from the package database pkgdb = PackageDB(base_url=PKGDBSERVER) acls = pkgdb.get_bugzilla_acls() # 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]: 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)