#!/usr/bin/python # bugzilla - a commandline frontend for the python bugzilla module # # Copyright (C) 2007 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 version = '0.1' 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) def findcookie(): globs = ['~/.mozilla/firefox/*default*/cookies.txt'] for g in globs: log.debug("Looking for cookies.txt in %s", g) cookiefiles = glob.glob(os.path.expanduser(g)) if cookiefiles: # return the first one we find. # TODO: find all cookiefiles, sort by age, use newest return cookiefiles[0] cookiefile = None cmdlist = ('info','query','new','modify') 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. Will attempt to use browser cookie if not specified.") p.add_option('--password', help="password. Will attempt to use browser cookie if not specified.") 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") # TODO: alias, assigned_to, reporter, qa_contact, dependson, blocked elif action == 'query': p.add_option('-p','--product', help="product name (list with 'bugzilla info -p')") p.add_option('-v','--version', help="product version") p.add_option('-c','--component', help="component name (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('-o','--cc', help="search cc lists for given address") p.add_option('-a','--assigned_to', help="search for bugs assigned to this address") p.add_option('--blocked', help="search for bugs that block this bug ID") p.add_option('--dependson', help="search for bugs that depend on this bug ID") p.add_option('-b','--bug_id', help="specify individual bugs by IDs, separated with commas") p.add_option('-t','--bug_status',default="NEW,VERIFIED,ASSIGNED,NEEDINFO,ON_DEV,FAILS_QA,REOPENED", help="comma-separated list of bug statuses to accept") 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('--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 if __name__ == '__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) if global_opt.user and global_opt.password: log.info('Using username/password for authentication') bz.login(global_opt.user,global_opt.password) elif global_opt.cookiefile: log.info('Using cookies in %s for authentication', global_opt.cookiefile) bz.readcookiefile(global_opt.cookiefile) else: cookiefile = findcookie() if cookiefile: log.info('Using cookies in %s for authentication', cookiefile) bz.readcookiefile(cookiefile) else: parser.error("Could not find a Firefox cookie file. Try --user/--password.") # 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','version','long_desc','bug_id', 'short_desc','cc','assigned_to','bug_status', 'blocked','dependson'): if hasattr(opt,a): i = getattr(opt,a) if i: if a in ('bug_status'): # list args q[a] = i.split(',') elif a in ('cc','assigned_to'): # the email query fields are kind of weird - thanks # to Florian La Roche for figuring this bit out. # ex.: {'email1':'foo@bar.com','emailcc1':True} q['email%i' % email_count] = i q['email%s%i' % (a,email_count)] = True email_count += 1 elif a in ('blocked','dependson'): # Chart args are weird. q['field%i-0-0' % chart_id] = a q['type%i-0-0' % chart_id] = 'equals' q['value%i-0-0' % chart_id] = i chart_id += 1 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 buglist = bz.getbugs(bugid_list) for bug in buglist: log.debug("modifying bug %s" % bug.bug_id) for cmd in ['comment','close']: val = getattr(opt,cmd) if not val: continue if cmd == 'comment': log.debug(" add comment: %s" % val) bug.addcomment(val) elif cmd == 'close': log.debug(" close %s" % val) bug.close(val) else: # FIXME error out properly log.error("Unknown command %s slipped through" % cmd) 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 else: parser.error("opt.output was set to something weird.")