summaryrefslogtreecommitdiffstats
path: root/bugzilla/rhbugzilla.py
diff options
context:
space:
mode:
Diffstat (limited to 'bugzilla/rhbugzilla.py')
-rw-r--r--bugzilla/rhbugzilla.py503
1 files changed, 503 insertions, 0 deletions
diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py
new file mode 100644
index 0000000..47a3e22
--- /dev/null
+++ b/bugzilla/rhbugzilla.py
@@ -0,0 +1,503 @@
+# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib.
+#
+# Copyright (C) 2008 Red Hat Inc.
+# Author: Will Woods <wwoods@redhat.com>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
+# the full text of the license.
+
+import bugzilla.base
+from bugzilla3 import Bugzilla32
+import copy, xmlrpclib
+
+class RHBugzilla(bugzilla.base.BugzillaBase):
+ '''Concrete implementation of the Bugzilla protocol. This one uses the
+ methods provided by Red Hat's Bugzilla 2.18 variant.
+
+ RHBugzilla supports XMLRPC MultiCall. The methods which start with a
+ single underscore are thin wrappers around XMLRPC methods and should thus
+ be safe for multicall use.
+
+ Documentation for most of these methods can be found here:
+ https://bugzilla.redhat.com/docs/en/html/api/extensions/compat_xmlrpc/code/webservice.html
+ '''
+
+ version = '0.2'
+ user_agent = bugzilla.base.user_agent + ' RHBugzilla/%s' % version
+
+ def __init__(self,**kwargs):
+ bugzilla.base.BugzillaBase.__init__(self,**kwargs)
+ self.user_agent = self.__class__.user_agent
+
+ def _login(self,user,password):
+ '''Backend login method for RHBugzilla.'''
+ return self._proxy.bugzilla.login(user,password)
+
+ def _logout(self):
+ '''Backend logout method for RHBugzilla.'''
+ # "Logouts are not implemented due to the non-session nature of
+ # XML-RPC communication."
+ # That's funny, since we get a (session-based) login cookie...
+ return True
+
+ #---- Methods and properties with basic bugzilla info
+
+ def _multicall(self):
+ '''This returns kind of a mash-up of the Bugzilla object and the
+ xmlrpclib.MultiCall object. Methods you call on this object will be
+ added to the MultiCall queue, but they will return None. When you're
+ ready, call the run() method and all the methods in the queue will be
+ run and the results of each will be returned in a list. So, for example:
+
+ mc = bz._multicall()
+ mc._getbug(1)
+ mc._getbug(1337)
+ mc._query({'component':'glibc','product':'Fedora','version':'devel'})
+ (bug1, bug1337, queryresult) = mc.run()
+
+ Note that you should only use the raw xmlrpc calls (mostly the methods
+ starting with an underscore). Normal getbug(), for example, tries to
+ return a Bug object, but with the multicall object it'll end up empty
+ and, therefore, useless.
+
+ Further note that run() returns a list of raw xmlrpc results; you'll
+ need to wrap the output in Bug objects yourself if you're doing that
+ kind of thing. For example, Bugzilla.getbugs() could be implemented:
+
+ mc = self._multicall()
+ for id in idlist:
+ mc._getbug(id)
+ rawlist = mc.run()
+ return [Bug(self,dict=b) for b in rawlist]
+ '''
+ mc = copy.copy(self)
+ mc._proxy = xmlrpclib.MultiCall(self._proxy)
+ def run(): return mc._proxy().results
+ mc.run = run
+ return mc
+
+ # Connect the backend methods to the XMLRPC methods
+ def _getbugfields(self):
+ return self._proxy.bugzilla.getBugFields()
+ def _getqueryinfo(self):
+ return self._proxy.bugzilla.getQueryInfo()
+ def _getproducts(self):
+ '''Backend _getproducts method for RH Bugzilla. This predates the
+ Bugzilla3 Products stuff, so we need to massage this data to make it
+ fit the proper format'''
+ r = self._proxy.bugzilla.getProdInfo()
+ n = 0
+ prod = []
+ for name,desc in r.iteritems():
+ # We're making up a fake id, since RHBugzilla doesn't use them
+ prod.append({'id':n,'name':name,'description':desc})
+ n += 1
+ return prod
+ def _getcomponents(self,product):
+ if type(product) == int:
+ product = self._product_id_to_name(product)
+ return self._proxy.bugzilla.getProdCompInfo(product)
+ def _getcomponentsdetails(self,product):
+ if type(product) == int:
+ product = self._product_id_to_name(product)
+ return self._proxy.bugzilla.getProdCompDetails(product)
+ def _get_info(self,product=None):
+ '''This is a convenience method that does getqueryinfo, getproducts,
+ and (optionally) getcomponents in one big fat multicall. This is a bit
+ faster than calling them all separately.
+
+ If you're doing interactive stuff you should call this, with the
+ appropriate product name, after connecting to Bugzilla. This will
+ cache all the info for you and save you an ugly delay later on.'''
+ mc = self._multicall()
+ mc._getqueryinfo()
+ mc._getproducts()
+ mc._getbugfields()
+ if product:
+ mc._getcomponents(product)
+ mc._getcomponentsdetails(product)
+ r = mc.run()
+ (self._querydata,self._querydefaults) = r.pop(0)
+ self._products = r.pop(0)
+ self._bugfields = r.pop(0)
+ if product:
+ self._components[product] = r.pop(0)
+ self._components_details[product] = r.pop(0)
+
+ #---- Methods for reading bugs and bug info
+
+ def _getbug(self,id):
+ '''Return a dict of full bug info for the given bug id'''
+ return self._proxy.bugzilla.getBug(id)
+ def _getbugsimple(self,id):
+ '''Return a short dict of simple bug info for the given bug id'''
+ r = self._proxy.bugzilla.getBugSimple(id)
+ if r and 'bug_id' not in r:
+ # XXX hurr. getBugSimple doesn't fault if the bug is missing.
+ # Let's synthesize one ourselves.
+ raise xmlrpclib.Fault("Server","Could not load bug %s" % id)
+ else:
+ return r
+ # Multicall methods
+ def _getbugs(self,idlist):
+ '''Like _getbug, but takes a list of ids and returns a corresponding
+ list of bug objects. Uses multicall for awesome speed.'''
+ mc = self._multicall()
+ for id in idlist:
+ mc._getbug(id)
+ raw_results = mc.run()
+ del mc
+ # check results for xmlrpc errors, and replace them with None
+ return bugzilla.base.replace_getbug_errors_with_None(raw_results)
+ def _getbugssimple(self,idlist):
+ '''Like _getbugsimple, but takes a list of ids and returns a
+ corresponding list of bug objects. Uses multicall for awesome speed.'''
+ mc = self._multicall()
+ for id in idlist:
+ mc._getbugsimple(id)
+ raw_results = mc.run()
+ del mc
+ # check results for xmlrpc errors, and replace them with None
+ return bugzilla.base.replace_getbug_errors_with_None(raw_results)
+
+ def _query(self,query):
+ '''Query bugzilla and return a list of matching bugs.
+ query must be a dict with fields like those in in querydata['fields'].
+ Returns a dict like this: {'bugs':buglist,
+ 'displaycolumns':columnlist,
+ 'sql':querystring}
+ buglist is a list of dicts describing bugs. You can specify which
+ columns/keys will be listed in the bugs by setting 'column_list' in
+ the query; otherwise the default columns are used (see the list in
+ querydefaults['default_column_list']). The list of columns will be
+ in 'displaycolumns', and the SQL query used by this query will be in
+ 'sql'.
+ '''
+ return self._proxy.bugzilla.runQuery(query)
+
+ #---- Methods for modifying existing bugs.
+
+ # Most of these will probably also be available as Bug methods, e.g.:
+ # Bugzilla.setstatus(id,status) ->
+ # Bug.setstatus(status): self.bugzilla.setstatus(self.bug_id,status)
+
+ def _addcomment(self,id,comment,private=False,
+ timestamp='',worktime='',bz_gid=''):
+ '''Add a comment to the bug with the given ID. Other optional
+ arguments are as follows:
+ private: if True, mark this comment as private.
+ timestamp: comment timestamp, in the form "YYYY-MM-DD HH:MM:SS"
+ worktime: amount of time spent on this comment (undoc in upstream)
+ bz_gid: if present, and the entire bug is *not* already private
+ to this group ID, this comment will be marked private.
+ '''
+ return self._proxy.bugzilla.addComment(id,comment,self.user,'',
+ private,timestamp,worktime,bz_gid)
+ def _setstatus(self,id,status,comment='',private=False,private_in_it=False,nomail=False):
+ '''Set the status of the bug with the given ID. You may optionally
+ include a comment to be added, and may further choose to mark that
+ comment as private.
+ The status may be anything from querydefaults['bug_status_list'].
+ Common statuses: 'NEW','ASSIGNED','MODIFIED','NEEDINFO'
+ Less common: 'VERIFIED','ON_DEV','ON_QA','REOPENED'
+ 'CLOSED' is not valid with this method; use closebug() instead.
+ '''
+ return self._proxy.bugzilla.changeStatus(id,status,self.user,'',
+ comment,private,private_in_it,nomail)
+ def _closebug(self,id,resolution,dupeid,fixedin,comment,isprivate,private_in_it,nomail):
+ '''Raw xmlrpc call for closing bugs. Documentation from Bug.pm is
+ below. Note that we drop the username and password fields because the
+ Bugzilla object contains them already.
+
+ closeBug($bugid, $new_resolution, $username, $password, $dupeid,
+ $new_fixed_in, $comment, $isprivate, $private_in_it, $nomail)
+
+ Close a current Bugzilla bug report with a specific resolution. This will eventually be done in Bugzilla/Bug.pm
+ instead and is meant to only be a quick fix. Please use bugzilla.changesStatus to changed to an opened state.
+ This method will change the bug report's status to CLOSED.
+
+ $bugid
+ # ID of bug report to add comment to.
+ $new_resolution
+ # Valid Bugzilla resolution to transition the report into.
+ # DUPLICATE requires $dupeid to be passed in.
+ $dupeid
+ # Bugzilla report ID that this bug is being closed as
+ # duplicate of.
+ # Requires $new_resolution to be DUPLICATE.
+ $new_fixed_in
+ # OPTIONAL String representing version of product/component
+ # that bug is fixed in.
+ $comment
+ # OPTIONAL Text string containing comment to add.
+ $isprivate
+ # OPTIONAL Whether the comment will be private to the
+ # 'private_comment' Bugzilla group.
+ # Default: false
+ $private_in_it
+ # OPTIONAL if true will make the comment private in
+ # Issue Tracker
+ # Default: follows $isprivate
+ $nomail
+ # OPTIONAL Flag that is either 1 or 0 if you want email to be sent or not for this change
+ '''
+ return self._proxy.bugzilla.closeBug(id,resolution,self.user,'',
+ dupeid,fixedin,comment,isprivate,private_in_it,nomail)
+ def _setassignee(self,id,**data):
+ '''Raw xmlrpc call to set one of the assignee fields on a bug.
+ changeAssignment($id, $data, $username, $password)
+ data: 'assigned_to','reporter','qa_contact','comment'
+ returns: [$id, $mailresults]'''
+ return self._proxy.bugzilla.changeAssignment(id,data)
+ def _updatedeps(self,id,blocked,dependson,action):
+ '''update the deps (blocked/dependson) for the given bug.
+ blocked/dependson: list of bug ids/aliases
+ action: 'add' or 'delete'
+
+ RHBZ call:
+ updateDepends($bug_id,$data,$username,$password,$nodependencyemail)
+ #data: 'blocked'=>id,'dependson'=>id,'action' => ('add','remove')
+
+ RHBZ only does one bug at a time, so this method will loop through
+ the blocked/dependson lists. This may be slow.
+ '''
+ r = []
+ # Massage input to match what RHBZ expects
+ if action == 'delete':
+ action == 'remove'
+ data = {'id':id, 'action':action, 'blocked':'', 'dependson':''}
+ for b in blocked:
+ data['blocked'] = b
+ self._proxy.bugzilla.updateDepends(id,data)
+ data['blocked'] = ''
+ for d in dependson:
+ data['dependson'] = d
+ self._proxy.bugzilla.updateDepends(id,data)
+ def _updatecc(self,id,cclist,action,comment='',nomail=False):
+ '''Updates the CC list using the action and account list specified.
+ cclist must be a list (not a tuple!) of addresses.
+ action may be 'add', 'delete', or 'overwrite'.
+ comment specifies an optional comment to add to the bug.
+ if mail is True, email will be generated for this change.
+ '''
+ # Massage the 'action' param into what the old updateCC call expects
+ if action == 'delete':
+ action = 'remove'
+ elif action == 'overwrite':
+ action = 'makeexact'
+ data = {'id':id, 'action':action, 'cc':','.join(cclist),
+ 'comment':comment, 'nomail':nomail}
+ return self._proxy.bugzilla.updateCC(data)
+ def _updatewhiteboard(self,id,text,which,action):
+ '''Update the whiteboard given by 'which' for the given bug.
+ performs the given action (which may be 'append',' prepend', or
+ 'overwrite') using the given text.'''
+ data = {'type':which,'text':text,'action':action}
+ return self._proxy.bugzilla.updateWhiteboard(id,data)
+ # TODO: update this when the XMLRPC interface grows requestee support
+ def _updateflags(self,id,flags):
+ '''Updates the flags associated with a bug report.
+ data should be a hash of {'flagname':'value'} pairs, like so:
+ {'needinfo':'?','fedora-cvs':'+'}
+ You may also add a "nomail":1 item, which will suppress email if set.
+
+ NOTE: the Red Hat XMLRPC interface does not yet support setting the
+ requestee (as in: needinfo from smartguy@answers.com). Alas.'''
+ return self._proxy.bugzilla.updateFlags(id,flags)
+
+ #---- Methods for working with attachments
+
+ # If your bugzilla wants attachments in something other than base64, you
+ # should override _attachment_encode here.
+ # If your bugzilla uses non-standard paths for attachment.cgi, you'll
+ # want to override _attachment_uri here.
+
+ def _attachfile(self,id,**attachdata):
+ return self._proxy.bugzilla.addAttachment(id,attachdata)
+
+ #---- createbug - call to create a new bug
+
+ def _createbug(self,**data):
+ '''Raw xmlrpc call for createBug() Doesn't bother guessing defaults
+ or checking argument validity. Use with care.
+ Returns bug_id'''
+ r = self._proxy.bugzilla.createBug(data)
+ return r[0]
+
+class RHBugzilla3(Bugzilla32, RHBugzilla):
+ '''Concrete implementation of the Bugzilla protocol. This one uses the
+ methods provided by Red Hat's Bugzilla 3.2+ instance, which is a superset
+ of the Bugzilla 3.2 methods. The additional methods (Bug.search, Bug.update)
+ should make their way into a later upstream Bugzilla release (probably 4.0).
+
+ Note that RHBZ3 *also* supports most of the old RHBZ methods, under the
+ 'bugzilla' namespace, so we use those when BZ3 methods aren't available.
+
+ This class was written using bugzilla.redhat.com's API docs:
+ https://bugzilla.redhat.com/docs/en/html/api/
+
+ By default, _getbugs will multicall Bug.get(id) multiple times, rather than
+ doing a single Bug.get(idlist) call. You can disable this behavior by
+ setting the 'multicall' property to False. This is faster, but less
+ compatible with RHBugzilla.
+ '''
+
+ version = '0.1'
+ user_agent = bugzilla.base.user_agent + ' RHBugzilla3/%s' % version
+
+ def __init__(self,**kwargs):
+ Bugzilla32.__init__(self,**kwargs)
+ self.user_agent = self.__class__.user_agent
+ self.multicall = kwargs.get('multicall',True)
+
+ # XXX it'd be nice if this wasn't just a copy of RHBugzilla's _getbugs
+ def _getbugs(self,idlist):
+ r = []
+ if self.multicall:
+ mc = self._multicall()
+ for id in idlist:
+ mc._proxy.bugzilla.getBug(id)
+ raw_results = mc.run()
+ del mc
+ # check results for xmlrpc errors, and replace them with None
+ r = bugzilla.base.replace_getbug_errors_with_None(raw_results)
+ else:
+ raw_results = self._proxy.Bug.get({'ids':idlist})
+ r = [i['internals'] for i in raw_results['bugs']]
+ return r
+
+ def _query(self,query):
+ '''Query bugzilla and return a list of matching bugs.
+ query must be a dict with fields like those in in querydata['fields'].
+ You can also pass in keys called 'quicksearch' or 'savedsearch' -
+ 'quicksearch' will do a quick keyword search like the simple search
+ on the Bugzilla home page.
+ 'savedsearch' should be the name of a previously-saved search to
+ execute. You need to be logged in for this to work.
+ Returns a dict like this: {'bugs':buglist,
+ 'sql':querystring}
+ buglist is a list of dicts describing bugs, and 'sql' contains the SQL
+ generated by executing the search.
+ '''
+ return self._proxy.Bug.search(query)
+
+ #---- Methods for updating bugs.
+
+ def _update_bugs(self,ids,updates):
+ '''Update the given fields with the given data in one or more bugs.
+ ids should be a list of integers or strings, representing bug ids or
+ aliases.
+ updates is a dict containing pairs like so: {'fieldname':'newvalue'}
+ '''
+ # TODO document changeable fields & return values
+ # TODO I think we need to catch XMLRPC exceptions to get a useful
+ # return value
+ return self._proxy.Bug.update({'ids':ids,'updates':updates})
+
+ def _update_bug(self,id,updates):
+ '''Update a single bug, specified by integer ID or (string) bug alias.
+ Really just a convenience method for _update_bugs(ids=[id],updates)'''
+ return self._update_bugs(ids=[id],updates=updates)
+
+ # Eventually - when RHBugzilla is well and truly obsolete - we'll delete
+ # all of these methods and refactor the Base Bugzilla object so all the bug
+ # modification calls go through _update_bug.
+ # Until then, all of these methods are basically just wrappers around it.
+
+ # TODO: allow multiple bug IDs
+
+ def _setstatus(self,id,status,comment='',private=False,private_in_it=False,nomail=False):
+ '''Set the status of the bug with the given ID.'''
+ update={'bug_status':status}
+ if comment:
+ update['comment'] = comment
+ return self._update_bug(id,update)
+
+ def _closebug(self,id,resolution,dupeid,fixedin,comment,isprivate,private_in_it,nomail):
+ '''Close the given bug. This is the raw call, and no data checking is
+ done here. That's up to the closebug method.
+ Note that the private_in_it and nomail args are ignored.'''
+ update={'bug_status':'CLOSED','resolution':resolution}
+ if dupeid:
+ update['resolution'] = 'DUPLICATE'
+ update['dupe_id'] = dupeid
+ if fixedin:
+ update['fixed_in'] = fixedin
+ if comment:
+ update['comment'] = comment
+ if isprivate:
+ update['commentprivacy'] = True
+ return self._update_bug(id,update)
+
+ def _setassignee(self,id,**data):
+ '''Raw xmlrpc call to set one of the assignee fields on a bug.
+ changeAssignment($id, $data, $username, $password)
+ data: 'assigned_to','reporter','qa_contact','comment'
+ returns: [$id, $mailresults]'''
+ # drop empty items
+ update = dict([(k,v) for k,v in data.iteritems() if v != ''])
+ return self._update_bug(id,update)
+
+ def _updatedeps(self,id,blocked,dependson,action):
+ '''Update the deps (blocked/dependson) for the given bug.
+ blocked, dependson: list of bug ids/aliases
+ action: 'add' or 'delete'
+ '''
+ if action not in ('add','delete'):
+ raise ValueError, "action must be 'add' or 'delete'"
+ update={'%s_blocked' % action: blocked,
+ '%s_dependson' % action: dependson}
+ self._update_bug(id,update)
+
+ def _updatecc(self,id,cclist,action,comment='',nomail=False):
+ '''Updates the CC list using the action and account list specified.
+ cclist must be a list (not a tuple!) of addresses.
+ action may be 'add', 'delete', or 'overwrite'.
+ comment specifies an optional comment to add to the bug.
+ if mail is True, email will be generated for this change.
+ '''
+ update = {}
+ if comment:
+ update['comment'] = comment
+
+ if action in ('add','delete'):
+ update['%s_cc' % action] = cclist
+ self._update_bug(id,update)
+ elif action == 'overwrite':
+ r = self._getbug(id)
+ if 'cc' not in r:
+ raise AttributeError, "Can't find cc list in bug %s" % str(id)
+ self._updatecc(id,r['cc'],'delete')
+ self._updatecc(id,cclist,'add')
+ # XXX we don't check inputs on other backend methods, maybe this
+ # is more appropriate in the public method(s)
+ else:
+ raise ValueError, "action must be 'add','delete', or 'overwrite'"
+
+ def _updatewhiteboard(self,id,text,which,action):
+ '''Update the whiteboard given by 'which' for the given bug.
+ performs the given action (which may be 'append',' prepend', or
+ 'overwrite') using the given text.
+
+ RHBZ3 Bug.update() only supports overwriting, so append/prepend
+ may cause two server roundtrips - one to fetch, and one to update.
+ '''
+ if not which.endswith('_whiteboard'):
+ which = which + '_whiteboard'
+ update = {}
+ if action == 'overwrite':
+ update[which] = text
+ else:
+ r = self._getbug(id)
+ if which not in r:
+ raise ValueError, "No such whiteboard %s in bug %s" % \
+ (which,str(id))
+ wb = r[which]
+ if action == 'prepend':
+ update[which] = text+' '+wb
+ elif action == 'append':
+ update[which] = wb+' '+text
+ self._update_bug(id,update)