summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRafael G. Martins <rafael.martins@collabora.com>2012-07-13 20:56:31 (GMT)
committerRafael G. Martins <rafael.martins@collabora.com>2012-07-13 20:56:31 (GMT)
commit0dc47264f1631186c1c75c5d1b2935332e8c05e6 (patch)
treeea74b4e525f00e8f7087a692e6014cfb7610f284
parentf880883648b15fb6b15cd8c59bf462f7b0cb9401 (diff)
downloadqa-bughandler-0dc47264f1631186c1c75c5d1b2935332e8c05e6.tar.gz
qa-bughandler-0dc47264f1631186c1c75c5d1b2935332e8c05e6.tar.xz
several improvements. see the full commit message.
- added special support for 2 custom fields - image: a comma-separated list of image names - package: package name with version The field names aren't fix, and are provided in the configuration file. 'image' and 'package' are just the sane defaults the script uses. - added several comments - made the code more pep8/pyflakes compliant
-rwxr-xr-xqa-bughandler.py153
1 files changed, 132 insertions, 21 deletions
diff --git a/qa-bughandler.py b/qa-bughandler.py
index 3c2877a..746aef0 100755
--- a/qa-bughandler.py
+++ b/qa-bughandler.py
@@ -5,6 +5,7 @@
~~~~~~~~~~~~~
:copyright: (c) 2012 Collabora Ltd.
+ :author: Rafael G. Martins <rafael.martins@collabora.com>
:license: LGPL-2.1+
This library is free software; you can redistribute it and/or
@@ -27,7 +28,7 @@ from argparse import ArgumentParser
from ConfigParser import ConfigParser
from datetime import datetime
from xmlrpclib import ServerProxy
-from urlparse import ParseResult, urlparse, urlunparse
+from urlparse import urlparse, urlunparse
import logging
import os
import sys
@@ -47,10 +48,20 @@ class Config(object):
self.filename = filename
self.parser = ConfigParser()
if not self.parser.read(self.filename):
- raise RuntimeError('Configuration file not found: %s' % self.filename)
+ raise RuntimeError('Configuration file not found: %s' %
+ self.filename)
if not self.parser.has_section('settings'):
raise RuntimeError('[settings] section not found.')
+ def get(self, option, default=None):
+ """Reads a setting from the [settings] section of the configuration
+ file. If not found, the value of the 'default' parameter will be
+ returned.
+ """
+ if not self.parser.has_option('settings', option):
+ return default
+ return self.parser.get('settings', option)
+
def __getattr__(self, option):
"""Anything from the [settings] section will be loaded automatically
as an attribute of this class. If a setting isn't found, this method
@@ -68,16 +79,25 @@ class Cli(object):
def __init__(self):
self.parser = ArgumentParser()
+
+ # declaration of all the argparse arguments
+ self.parser.add_argument('--status', '-s', dest='status',
+ metavar='STATUS', help='modify bug status')
self.parser.add_argument('--resolution', '-r', dest='resolution',
metavar='RESOLUTION',
help='modify bug resolution')
- self.parser.add_argument('--status', '-s', dest='status',
- metavar='STATUS', help='modify bug status')
+ self.parser.add_argument('--image', '-i', dest='image',
+ metavar='IMAGE', help='the name of the '
+ 'image used')
+ self.parser.add_argument('--package', '-p', dest='package',
+ metavar='PACKAGE', help='the name of the '
+ 'package, with the version, that fixes the '
+ 'bug')
self.parser.add_argument('--field', '-f', dest='fields',
action='append', metavar='fieldname=value',
help='modify the given fields.')
self.parser.add_argument('--config-file', '-c', dest='config_file',
- metavar='FILE', help='configuration file, ' \
+ metavar='FILE', help='configuration file, '
'defaults to `~/.qa-bughandler.conf\'',
default=os.path.expanduser(
'~/.qa-bughandler.conf'))
@@ -86,16 +106,25 @@ class Cli(object):
'INFO', 'DEBUG'],
default='INFO', help='logging level.')
self.parser.add_argument('--log-file', dest='log_file', metavar='FILE',
- help='send logging data to FILE, instead of ' \
- 'stderr.')
+ help='send logging data to FILE, instead '
+ 'of stderr.')
self.parser.add_argument('bug_id', help='bug identifier')
+
+ # run the parser through argv (implicit)
self.args = self.parser.parse_args()
class Settings(object):
+ """This class is just a common endpoint for Cli and Config. It also
+ provides logging capabilities, that depends on the settings that were
+ collected from Cli.
+ """
def __init__(self):
+ # load cli settings
self.cli = Cli()
+
+ # setup logging
self.logger = logging.getLogger('qa-bughandler')
self.logger.setLevel(logging.getLevelName(self.cli.args.log_level))
handler = logging.StreamHandler(sys.stderr)
@@ -104,10 +133,15 @@ class Settings(object):
handler.setFormatter(logging.Formatter(LOG_FORMATTER, LOG_DATEFORMAT))
self.logger.addHandler(handler)
self.logger.info('Starting qa-bughandler.py. Version: %s', __version__)
+
+ # load config settings
self.config = Config(self.cli.args.config_file)
class Bugzilla(object):
+ """This class is a XML-RPC client, that handles our calls to the Bugzilla
+ API methods.
+ """
def __init__(self, url, bug_id, logger, verbose=False):
self.url = url
@@ -117,22 +151,37 @@ class Bugzilla(object):
self.handler = ServerProxy(self.url, verbose=self.verbose)
def login(self, username, password):
+ """This method implements the HTTP login, adding the user name and the
+ password to the url, while keeping the url property of the class still
+ safe to be displayed/logged.
+
+ Warning: This isn't going to work with bugzilla setups that rely on the
+ Bugzilla cookie-based auth.
+ """
self.logger.debug('Login in bugzilla: %s', self.url)
+
+ # split and unsplit the url with the username and password
oldurl = urlparse(self.url)
newurl = (oldurl.scheme,
'%s:%s@%s' % (username, password, oldurl.netloc),
oldurl.path, oldurl.params, oldurl.query, oldurl.fragment)
- self.url = urlunparse(newurl)
- self.handler = ServerProxy(self.url, verbose=self.verbose)
+
+ # rebuild the handler with the new url
+ self.handler = ServerProxy(urlunparse(newurl), verbose=self.verbose)
def get(self):
+ """This method returns data from #bug_id bug."""
params = {'ids': [self.bug_id]}
self.logger.debug('XML-RPC call: Bug.get, params: %r', params)
rv = self.handler.Bug.get(params)
self.logger.debug('XML-RPC response: %r', rv)
+
+ # 'bugs' is the dict element with a list of bugs data. We just provided
+ # one bug_id, then we just have one element in the list.
return rv['bugs'][0]
def update(self, **fields):
+ """This method updates a bug with, changing the required fields."""
params = {'ids': [self.bug_id]}
params.update(fields)
self.logger.debug('XML-RPC call: Bug.update, params: %r', params)
@@ -141,6 +190,7 @@ class Bugzilla(object):
return rv
def add_comment(self, comment):
+ """This method adds a comment to the required bug."""
params = {'id': self.bug_id, 'comment': comment}
self.logger.debug('XML-RPC call: Bug.add_comment, params: %r', params)
rv = self.handler.Bug.add_comment(params)
@@ -151,39 +201,100 @@ class Bugzilla(object):
def main():
settings = Settings()
fields = {}
- if settings.cli.args.resolution is not None:
- fields['resolution'] = settings.cli.args.resolution
+
+ ## handle cli arguments
+
+ # handle image
+ if settings.cli.args.image is not None:
+
+ # image field name comes from the config file, or defaults to 'image'
+ field_name = settings.config.get('field_image', 'image')
+ fields[field_name] = settings.cli.args.image
+
+ # handle package
+ if settings.cli.args.package is not None:
+
+ # package field name comes from the config file, or defaults to
+ # 'package'
+ field_name = settings.config.get('field_package', 'package')
+ fields[field_name] = settings.cli.args.package
+
+ # handle status
if settings.cli.args.status is not None:
fields['status'] = settings.cli.args.status
+
+ # handle resolution
+ if settings.cli.args.resolution is not None:
+ fields['resolution'] = settings.cli.args.resolution
+
+ # handle additional fields
if settings.cli.args.fields is not None:
+
+ # 'fields' is a list of 'key=value' strings
for field in settings.cli.args.fields:
+
+ # invalid format, ignore
if '=' not in field:
continue
+
+ # everything before the first '=' is the key
key, value = field.split('=', 1)
+
+ # key and value shouldn't have empty chars around
fields[key.strip()] = value.strip()
try:
bugz = Bugzilla(settings.config.url, settings.cli.args.bug_id,
settings.logger)
+
+ # we need to login before any request
bugz.login(settings.config.username, settings.config.password)
+
+ # we should get the bug info to validate the fields, and handle special
+ # fields properly
bug = bugz.get()
+
+ # 'image' is a special field. it is a list of image names, and we need
+ # to just append our current image to it, if needed.
+ field_image = settings.config.get('field_image', 'image')
+ if field_image in fields: # we don't need to touch it if no image was
+ # provided :)
+ if field_image not in bug: # field not present in our bz setup.
+ raise RuntimeError('Custom field not found: %s' % field_image)
+ images = [i.strip() for i in bug[field_image].split(',')]
+ images.append(fields[field_image])
+ fields[field_image] = ', '.join(images)
+
+ # We will force all the time/data to be in UTC, to make the things
+ # easier to handle
now = datetime.utcnow()
- try:
- comment_template = settings.config.comment_template
- except RuntimeError:
- comment_template = 'Ticket updated by %(script)s, %(date)s ' \
- '%(time)s %(timezone)s:'
- comment = comment_template % {'script': 'qa-bughandler',
- 'date': now.strftime('%Y-%m-%d'),
- 'time': now.strftime('%H:%M:%S'),
- 'timezone': 'UTC'}
+
+ # We will add a comment to the bug. For this we need a template,
+ # provided in the configuration file. If not found, use a sane default.
+ template = settings.config.get('comment_template',
+ 'Ticket updated by {script}, '
+ '{date} {time} {timezone}:')
+
+ # Replace the template variables.
+ comment = template.format(script='qa-bughandler',
+ date=now.strftime('%Y-%m-%d'),
+ time=now.strftime('%H:%M:%S'),
+ timezone='UTC')
comment = comment.strip() + '\n\n'
+
+ # validate fields and add them to the comment.
for field in fields:
if field not in bug:
raise RuntimeError('Field not found: %s' % field)
comment += '* %s: %s\n' % (field, fields[field])
- bugz.add_comment(comment)
+
+ # update the bug before send the comment, to avoid adding a dumb
+ # comment, if the update fails for some reason.
bugz.update(**fields)
+
+ # send the comment.
+ bugz.add_comment(comment)
+
except Exception, e:
settings.logger.critical('%s: %s', e.__class__.__name__, e)
return -1