summaryrefslogtreecommitdiffstats
path: root/bin/bugzilla
diff options
context:
space:
mode:
authorWill Woods <wwoods@redhat.com>2008-09-04 14:04:44 -0400
committerWill Woods <wwoods@redhat.com>2008-09-04 14:04:44 -0400
commitbcb002af27ee4a224be7578ded886471231e598e (patch)
treeda21e70d6243862417b99a7efff1786340ae64e9 /bin/bugzilla
parent041a27dbc66b007f2e4c7132c229f14af5163982 (diff)
parentce753c44a0445b796cd19851cba8dff2e5d455d0 (diff)
downloadpython-bugzilla-bcb002af27ee4a224be7578ded886471231e598e.tar.gz
python-bugzilla-bcb002af27ee4a224be7578ded886471231e598e.tar.xz
python-bugzilla-bcb002af27ee4a224be7578ded886471231e598e.zip
Merge from abstractify branch
Diffstat (limited to 'bin/bugzilla')
-rwxr-xr-xbin/bugzilla433
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)