diff options
Diffstat (limited to 'bin')
-rwxr-xr-x | bin/bugzilla | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/bin/bugzilla b/bin/bugzilla new file mode 100755 index 0000000..c55efc8 --- /dev/null +++ b/bin/bugzilla @@ -0,0 +1,433 @@ +#!/usr/bin/python +# bugzilla - a commandline frontend for the python bugzilla module +# +# Copyright (C) 2007,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, optparse +import os, sys, glob, re +import logging +import getpass + +version = '0.4' +default_bz = 'https://bugzilla.redhat.com/xmlrpc.cgi' + +# Initial simple logging stuff +logging.basicConfig() +log = logging.getLogger("bugzilla") +if '--debug' in sys.argv: + log.setLevel(logging.DEBUG) +elif '--verbose' in sys.argv: + log.setLevel(logging.INFO) + +cmdlist = ('info','query','new','modify','login') +def setup_parser(): + u = "usage: %prog [global options] COMMAND [options]" + u += "\nCommands: %s" % ', '.join(cmdlist) + p = optparse.OptionParser(usage=u) + p.disable_interspersed_args() + # General bugzilla connection options + p.add_option('--bugzilla',default=default_bz, + help="bugzilla XMLRPC URI. default: %s" % default_bz) + p.add_option('--user', + help="username") + p.add_option('--password', + help="password") + p.add_option('--cookiefile', + help="cookie file to use for bugzilla authentication") + p.add_option('--verbose',action='store_true', + help="give more info about what's going on") + p.add_option('--debug',action='store_true', + help="output bunches of debugging info") + return p + +def setup_action_parser(action): + p = optparse.OptionParser(usage="usage: %%prog %s [options]" % action) + # TODO: product and version could default to current system + # info (read from /etc/redhat-release?) + if action == 'new': + p.add_option('-p','--product', + help="REQUIRED: product name (list with 'bugzilla info -p')") + p.add_option('-v','--version', + help="REQUIRED: product version") + p.add_option('-c','--component', + help="REQUIRED: component name (list with 'bugzilla info -c PRODUCT')") + p.add_option('-l','--comment', + help="REQUIRED: initial bug comment") + p.add_option('-s','--summary',dest='short_desc', + help="REQUIRED: bug summary") + p.add_option('-o','--os',default='Linux',dest='op_sys', + help="OPTIONAL: operating system (default: Linux)") + p.add_option('-a','--arch',default='All',dest='rep_platform', + help="OPTIONAL: arch this bug occurs on (default: All)") + p.add_option('--severity',default='medium',dest='bug_severity', + help="OPTIONAL: bug severity (default: medium)") + p.add_option('--priority',default='medium',dest='priority', + help="OPTIONAL: bug priority (default: medium)") + p.add_option('-u','--url',dest='bug_file_loc',default='http://', + help="OPTIONAL: URL for further bug info") + p.add_option('--cc', + help="OPTIONAL: add emails to initial CC list") + + elif action == 'query': + # General bug metadata + p.add_option('-b','--bug_id', + help="specify individual bugs by IDs, separated with commas") + p.add_option('-p','--product', + help="product name, comma-separated (list with 'bugzilla info -p')") + p.add_option('-v','--version', + help="product version") + p.add_option('-c','--component', + help="component name(s), comma-separated (list with 'bugzilla info -c PRODUCT')") + p.add_option('--components_file', + help="list of component names from a file, one component per line (list with 'bugzilla info -c PRODUCT')") + p.add_option('-l','--long_desc', + help="search inside bug comments") + p.add_option('-s','--short_desc', + help="search bug summaries") + p.add_option('-t','--bug_status',default="NEW", + help="comma-separated list of bug statuses to accept [Default:NEW] [Available:NEW,ASSIGNED,NEEDINFO,ON_DEV,MODIFIED,POST,ON_QA,FAILS_QA,PASSES_QA,REOPENED,VERIFIED,RELEASE_PENDING,CLOSED]") + p.add_option('-x','--severity', + help="search severities, comma-separated") + p.add_option('-z','--priority', + help="search priorities, comma-separated") + + # Email + p.add_option('-E','--emailtype', + help="Email: specify searching option for emails, ie. substring,notsubstring,exact,... [Default: substring]",default="substring") + p.add_option('-o','--cc', + help="Email: search cc lists for given address") + p.add_option('-r','--reporter', + help="Email: search reporter email for given address") + p.add_option('-a','--assigned_to', + help="Email: search for bugs assigned to this address") + p.add_option('-q','--qa_contact', + help="Email: search for bugs which have QA Contact assigned to this address") + + # Strings + p.add_option('-u','--url', + help="search keywords field for given url") + p.add_option('-U','--url_type', + help="specify searching option for urls, ie. anywords,allwords,nowords") + p.add_option('-k','--keywords', + help="search keywords field for specified words") + p.add_option('-K','--keywords_type', + help="specify searching option for keywords, ie. anywords,allwords,nowords") + p.add_option('-w','--status_whiteboard', + help="search Status Whiteboard field for specified words") + p.add_option('-W','--status_whiteboard_type', + help="specify searching option for Status Whiteboard, ie. anywords,allwords,nowords") + + # Boolean Charts + p.add_option('-B','--booleantype', + help="specify searching option for booleans, ie. substring,notsubstring,exact,... [Default: substring]",default="substring") + p.add_option('--boolean_query', + help="Boolean:Create your own query. Format: BooleanName-Condition-Parameter &/| ... . ie, keywords-substring-Partner & keywords-notsubstring-OtherQA") + p.add_option('--blocked', + help="Boolean:search for bugs that block this bug ID") + p.add_option('--dependson', + help="Boolean:search for bugs that depend on this bug ID") + p.add_option('--flag', + help="Boolean:search for bugs that have certain flag states present") + p.add_option('--qa_whiteboard', + help="Boolean:search for bugs that have certain QA Whiteboard text present") + p.add_option('--devel_whiteboard', + help="Boolean:search for bugs that have certain Devel Whiteboard text present") + p.add_option('--alias', + help="Boolean:search for bugs that have the provided alias") + p.add_option('--fixed_in', + help="search Status Whiteboard field for specified words") + + elif action == 'info': + p.add_option('-p','--products',action='store_true', + help='Get a list of products') + p.add_option('-c','--components',metavar="PRODUCT", + help='List the components in the given product') + p.add_option('-o','--component_owners',metavar="PRODUCT", + help='List components (and their owners)') + p.add_option('-v','--versions',metavar="PRODUCT", + help='List the versions for the given product') + elif action == 'modify': + p.set_usage("usage: %prog modify [options] BUGID BUGID ...") + p.add_option('-l','--comment', + help='Add a comment') + # FIXME: check value for resolution + p.add_option('-k','--close',metavar="RESOLUTION", + help='Close with the given resolution') + # TODO: --keyword, --flag, --tag, --status, --assignee, --cc, ... + + if action in ('new','query'): + # output modifiers + p.add_option('-f','--full',action='store_const',dest='output', + const='full',default='normal',help="output detailed bug info") + p.add_option('-i','--ids',action='store_const',dest='output', + const='ids',help="output only bug IDs") + p.add_option('-e','--extra',action='store_const',dest='output', + const='extra',help="output additional bug information (keywords, Whiteboards, etc.)") + p.add_option('--outputformat', + help="Print output in the form given. You can use RPM-style "+ + "tags that match bug fields, e.g.: '%{bug_id}: %{short_desc}'") + return p + +def main(): + # Set up parser for global args + parser = setup_parser() + # Parse the commandline, woo + (global_opt,args) = parser.parse_args() + # Get our action from these args + if len(args) and args[0] in cmdlist: + action = args.pop(0) + else: + parser.error("command must be one of: %s" % ','.join(cmdlist)) + # Parse action-specific args + action_parser = setup_action_parser(action) + (opt, args) = action_parser.parse_args(args) + + # Connect to bugzilla + log.info('Connecting to %s',global_opt.bugzilla) + bz=bugzilla.Bugzilla(url=global_opt.bugzilla) + + # Handle 'login' action + if action == 'login': + if not global_opt.user: + sys.stdout.write('Username: ') + user = sys.stdin.readline() + global_opt.user = user.strip() + if not global_opt.password: + global_opt.password = getpass.getpass() + sys.stdout.write('Logging in... ') + # XXX NOTE: This will return success if you have a valid login cookie, + # even if you give a bad username and password. WEIRD. + if bz.login(global_opt.user,global_opt.password): + print 'Authorization cookie received.' + sys.exit(0) + else: + print 'failed.' + sys.exit(1) + + # Set up authentication + if global_opt.user: + if not global_opt.password: + global_opt.password = getpass.getpass() + log.info('Using username/password for authentication') + bz.login(global_opt.user,global_opt.password) + else: + if global_opt.cookiefile: + bz.cookiefile = global_opt.cookiefile + cookiefile = bz.cookiefile + if os.path.exists(cookiefile): + log.info('Using cookies in %s for authentication', cookiefile) + else: + # FIXME check to see if .bugzillarc is in use + log.info('No authentication info provided.') + + # And now we actually execute the given command + buglist = list() # save the results of query/new/modify here + if action == 'info': + if opt.products: + for k in sorted(bz.products): + print k + + if opt.components: + for c in sorted(bz.getcomponents(opt.components)): + print c + + if opt.component_owners: + component_details = bz.getcomponentsdetails(opt.component_owners) + for c in sorted(component_details): + print "%s: %s" % (c, component_details[c]['initialowner']) + + if opt.versions: + for p in bz.querydata['product']: + if p['name'] == opt.versions: + for v in p['versions']: + print v + + elif action == 'query': + # shortcut for specific bug_ids + if opt.bug_id: + log.debug("bz.getbugs(%s)", opt.bug_id.split(',')) + buglist=bz.getbugs(opt.bug_id.split(',')) + else: + # Construct the query from the list of queryable options + q = dict() + email_count = 1 + chart_id = 0 + for a in ('product','component','components_file','version','long_desc','bug_id', + 'short_desc','cc','assigned_to','reporter','qa_contact','bug_status', + 'blocked','dependson','keywords','keywords_type','url','url_type','status_whiteboard', + 'status_whiteboard_type','fixed_in','fixed_in_type','flag','alias','qa_whiteboard', + 'devel_whiteboard','boolean_query','severity','priority'): + if hasattr(opt,a): + i = getattr(opt,a) + if i: + if a in ('bug_status'): # list args + # FIXME: statuses can differ between bugzilla instances.. + if i == 'ALL': + # Alias for all available bug statuses + q[a] = 'NEW,ASSIGNED,NEEDINFO,ON_DEV,MODIFIED,POST,ON_QA,FAILS_QA,PASSES_QA,REOPENED,VERIFIED,RELEASE_PENDING,CLOSED'.split(',') + elif i == 'DEV': + # Alias for all development bug statuses + q[a] = 'NEW,ASSIGNED,NEEDINFO,ON_DEV,MODIFIED,POST,REOPENED'.split(',') + elif i == 'QE': + # Alias for all QE relevant bug statuses + q[a] = 'ASSIGNED,ON_QA,FAILS_QA,PASSES_QA'.split(',') + elif i == 'EOL': + # Alias for EndOfLife bug statuses + q[a] = 'VERIFIED,RELEASE_PENDING,CLOSED'.split(',') + else: + q[a] = i.split(',') + elif a in ('cc','assigned_to','reporter','qa_contact'): + # Emails + # ex.: {'email1':'foo@bar.com','emailcc1':True} + q['email%i' % email_count] = i + q['email%s%i' % (a,email_count)] = True + q['emailtype%i' % email_count] = opt.emailtype + email_count += 1 + elif a in ('components_file'): + # Components slurped in from file (one component per line) + # This can be made more robust + arr = [] + f = open (i, 'r') + for line in f.readlines(): + line = line.rstrip("\n") + arr.append(line) + q['component'] = ",".join(arr) + elif a in ('keywords','keywords_type','url','url_type','status_whiteboard', + 'status_whiteboard_type','severity','priority'): + if a in ('url'): + q['bug_file_loc'] = i + elif a in ('url'): + q['bug_file_loc_type'] = i + else: + q['%s' % a] = i + elif a in ('fixed_in','blocked','dependson','flag','qa_whiteboard','devel_whiteboard','alias'): + # Boolean Charts + if a in ('flag'): + # Flags have strange parameter name + q['field%i-0-0' % chart_id] = 'flagtypes.name' + else: + q['field%i-0-0' % chart_id] = a + q['value%i-0-0' % chart_id] = i + q['type%i-0-0' % chart_id] = opt.booleantype + chart_id += 1 + elif a in ('boolean_query'): + # Custom Boolean Chart query + # Format: BooleanName-Condition-Parameter &/| BooleanName-Condition-Parameter &/| ... + # ie, keywords-substring-Partner | keywords-notsubstring-PartnerVerified & keywords-notsubstring-OtherQA + chart_id = 0 + and_count = 0 + or_count = 0 + # Manually specified boolean query + x = i.split(' ') + for par in x : + if par.find('&') != -1: + and_count += 1 + elif par.find('|') != -1: + or_count += 1 + elif par.find('-') != -1: + args = par.split('-') + q['field%i-%i-%i' % (chart_id,and_count,or_count)] = args[0] + q['type%i-%i-%i' % (chart_id,and_count,or_count)] = args[1] + q['value%i-%i-%i' % (chart_id,and_count,or_count)] = args[2] + else: + parser.error('Malformed boolean query: %s' % i) + else: + q[a] = i + log.debug("bz.query: %s", q) + buglist = bz.query(q) + + elif action == 'new': + data = dict() + required=['product','component','version','short_desc','comment', + 'rep_platform','bug_severity','op_sys','bug_file_loc','priority'] + optional=['cc'] + for a in required + optional: + i = getattr(opt,a) + if i: + data[a] = i + for k in required: + if k not in data: + parser.error('Missing required argument: %s' % k) + log.debug("bz.createbug(%s)", data) + b = bz.createbug(**data) + buglist = [b] + + elif action == 'modify': + bugid_list = [] + for a in args: + if ',' in a: + for b in a.split(','): + bugid_list.append(b) + else: + bugid_list.append(a) + # Surely there's a simpler way to do that.. + # bail out if no bugs were given + if not bugid_list: + parser.error('No bug IDs given (maybe you forgot an argument somewhere?)') + # Iterate over a list of Bug objects + # FIXME: this should totally use some multicall magic + buglist = bz.getbugssimple(bugid_list) + for id,bug in zip(bugid_list,buglist): + if not bug: + log.error(" failed to load bug %s" % id) + continue + log.debug("modifying bug %s" % bug.bug_id) + if opt.comment: + log.debug(" add comment: %s" % opt.comment) + bug.addcomment(opt.comment) + if opt.close: + log.debug(" close %s" % opt.close) + bug.close(opt.close) + else: + print "Sorry - '%s' not implemented yet." % action + + # If we're doing new/query/modify, output our results + if action in ('new','query'): + if opt.outputformat: + format_field_re = re.compile("%{[a-z0-9_]+}") + def bug_field(matchobj): + fieldname = matchobj.group()[2:-1] + return str(getattr(b,fieldname)) + for b in buglist: + print format_field_re.sub(bug_field,opt.outputformat) + elif opt.output == 'ids': + for b in buglist: + print b.bug_id + elif opt.output == 'full': + fullbuglist = bz.getbugs([b.bug_id for b in buglist]) + for b in fullbuglist: + print b + if b.cc: print "CC: %s" % " ".join(b.cc) + if b.blocked: print "Blocked: %s" % " ".join([str(i) for i in b.blocked]) + if b.dependson: print "Depends: %s" % " ".join([str(i) for i in b.dependson]) + for c in b.longdescs: + print "* %s - %s (%s):\n%s\n" % (c['time'],c['name'],c['email'] or c['safe_email'],c['body']) + elif opt.output == 'normal': + for b in buglist: + print b + elif opt.output == 'extra': + print "Grabbing 'extra' bug information. This could take a moment." + fullbuglist = bz.getbugs([b.bug_id for b in buglist]) + for b in fullbuglist: + print b + if b.keywords: print " +Keywords: ",b.keywords + if b.qa_whiteboard: print " +QA Whiteboard: ",b.qa_whiteboard + if b.status_whiteboard: print " +Status Whiteboard: ",b.status_whiteboard + if b.devel_whiteboard: print " +Devel Whiteboard: ",b.devel_whiteboard + print "\nBugs listed: ",len(buglist) + else: + parser.error("opt.output was set to something weird.") + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print "\ninterrupted." + sys.exit(0) |