#!/usr/bin/python # bugzilla - a commandline frontend for the python bugzilla module # # Copyright (C) 2007,2008 Red Hat Inc. # Author: Will Woods # # 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('--oneline', action='store_const', dest='output', const='oneline',help="one line summary of the bug (useful for scripts)") 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 generate_man_page(): try: from logilab.common.optik_ext import ManHelpFormatter import datetime except ImportError: return today = datetime.date.today() datestr = today.strftime("%B %d, %Y") manpage = \ '''.TH bugzilla 1 "%s" "version %s" "User Commands" .SH NAME bugzilla \- command-line interface to Bugzilla over XML-RPC .SH SYNOPSIS .B bugzilla [\\fIoptions\\fR] [\\fIcommand\\fR] [\\fIcommand-options\\fR] .SH DESCRIPTION .PP .BR bugzilla is a command-line utility that allows access to the XML-RPC interface provided by Bugzilla. .PP \\fIcommand\\fP is one of: .br .I \\fR * login - log into the given bugzilla instance .br .I \\fR * new - create a new bug .br .I \\fR * query - search for bugs matching given criteria .br .I \\fR * modify - modify existing bugs .br .I \\fR * info - get info about the given bugzilla instance ''' % (datestr,version) manformatter = ManHelpFormatter() parser = setup_parser() parser.formatter = manformatter opt_section = parser.format_option_help() manpage += opt_section.replace("OPTIONS","GLOBAL OPTIONS") for action in cmdlist: action_parser = setup_action_parser(action) action_parser.formatter = manformatter opt_section = action_parser.format_option_help() manpage += opt_section.replace("OPTIONS", '\[oq]%s\[cq] OPTIONS' % action.upper()) manpage += \ '''.SH EXAMPLES .TP bugzilla query --bug_id 62037 .SH EXIT STATUS .BR bugzilla returns 1 if login fails or it is interrupted, and 0 otherwise. .SH NOTES Not everything that's exposed in the Web UI is exposed by XML-RPC, and not everything that's exposed by XML-RPC is used by .BR bugzilla . .SH BUGS Bugs? In a sub-1.0 release? Preposterous. .SH AUTHOR Will Woods ''' print manpage 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) b.refresh() 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['author'],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) elif opt.output == 'oneline': fullbuglist = bz.getbugs([b.bug_id for b in buglist]) for b in fullbuglist: flags='' cve='' #grab all the flags that are set for i in b.flag_types: for s in i['flags']: if s['status']: flags += i['name'] + ":" + str(s['status']) + " " #grab CVEs by searching the keywords and grabbing that bugzilla if b.keywords.find("Security") != -1: for bl in b.blocked: cvebug = bz.getbug(bl) if cvebug.alias.find("CVE") != -1: cve += cvebug.alias + " " print "#%s %8s %22s %s\t[%s] %s %s" % (b.bug_id, b.bug_status, b.assigned_to, b.component, b.target_milestone, flags, cve) else: parser.error("opt.output was set to something weird.") if __name__ == '__main__': try: if '--generate-man' in sys.argv: generate_man_page() else: main() except KeyboardInterrupt: print "\ninterrupted." sys.exit(1)