3 # bugzilla-submit: a command-line script to post bugs to a Bugzilla instance
5 # Authors: Christian Reis <kiko@async.com.br>
6 # Eric S. Raymond <esr@thyrsus.com>
10 # For a usage hint run bugzilla-submit --help
12 # TODO: use RDF output to pick up valid options, as in
13 # http://www.async.com.br/~kiko/mybugzilla/config.cgi?ctype=rdf
18 sys
.stderr
.write("bugzilla-submit: %s\n" % m
)
22 version
= string
.split(string
.split(sys
.version
)[0], ".")[:2]
23 if map(int, version
) < [2, 3]:
24 error("you must upgrade to Python 2.3 or higher to use this script.")
26 import urllib
, re
, os
, netrc
, email
.Parser
, optparse
28 class ErrorURLopener(urllib
.URLopener
):
29 """URLopener that handles HTTP 404s"""
30 def http_error_404(self
, url
, fp
, errcode
, errmsg
, headers
, *extra
):
31 raise ValueError, errmsg
# 'File Not Found'
33 # Set up some aliases -- partly to hide the less friendly fieldnames
34 # behind the names actually used for them in the stock web page presentation,
35 # and partly to provide a place for mappings if the Bugzilla fieldnames
37 field_aliases
= (('hardware', 'rep_platform'),
39 ('summary', 'short_desc'),
40 ('description', 'comment'),
41 ('depends_on', 'dependson'),
42 ('status', 'bug_status'),
43 ('severity', 'bug_severity'),
44 ('url', 'bug_file_loc'),)
46 def header_to_field(hdr
):
47 hdr
= hdr
.lower().replace("-", "_")
48 for (alias
, name
) in field_aliases
:
54 def field_to_header(hdr
):
55 hdr
= "-".join(map(lambda x
: x
.capitalize(), hdr
.split("_")))
56 for (alias
, name
) in field_aliases
:
63 # Take override values from the command line
64 parser
= optparse
.OptionParser(usage
="usage: %prog [options] bugzilla-url")
65 parser
.add_option('-b', '--status', dest
='bug_status',
66 help='Set the Status field.')
67 parser
.add_option('-u', '--url', dest
='bug_file_loc',
68 help='Set the URL field.')
69 parser
.add_option('-p', '--product', dest
='product',
70 help='Set the Product field.')
71 parser
.add_option('-v', '--version', dest
='version',
72 help='Set the Version field.')
73 parser
.add_option('-c', '--component', dest
='component',
74 help='Set the Component field.')
75 parser
.add_option('-s', '--summary', dest
='short_desc',
76 help='Set the Summary field.')
77 parser
.add_option('-H', '--hardware', dest
='rep_platform',
78 help='Set the Hardware field.')
79 parser
.add_option('-o', '--os', dest
='op_sys',
80 help='Set the Operating System field.')
81 parser
.add_option('-r', '--priority', dest
='priority',
82 help='Set the Priority field.')
83 parser
.add_option('-x', '--severity', dest
='bug_severity',
84 help='Set the Severity field.')
85 parser
.add_option('-d', '--description', dest
='comment',
86 help='Set the Description field.')
87 parser
.add_option('-a', '--assigned-to', dest
='assigned_to',
88 help='Set the Assigned-To field.')
89 parser
.add_option('-C', '--cc', dest
='cc',
90 help='Set the Cc field.')
91 parser
.add_option('-k', '--keywords', dest
='keywords',
92 help='Set the Keywords field.')
93 parser
.add_option('-D', '--depends-on', dest
='dependson',
94 help='Set the Depends-On field.')
95 parser
.add_option('-B', '--blocked', dest
='blocked',
96 help='Set the Blocked field.')
97 parser
.add_option('-n', '--no-stdin', dest
='read',
98 default
=True, action
='store_false',
99 help='Suppress reading fields from stdin.')
102 # Fetch user's credential for access to this Bugzilla instance.
103 def get_credentials(bugzilla
):
104 # Work around a quirk in the Python implementation.
105 # The URL has to be quoted, otherwise the parser barfs on the colon.
106 # But the parser doesn't strip the quotes.
107 authenticate_on
= '"' + bugzilla
+ '"'
109 credentials
= netrc
.netrc()
110 except netrc
.NetrcParseError
, e
:
111 error("ill-formed .netrc: %s:%s %s" % (e
.filename
, e
.lineno
, e
.msg
))
113 error("missing .netrc file %s" % str(e
).split()[-1])
114 ret
= credentials
.authenticators(authenticate_on
)
116 # Okay, the literal string passed in failed. Just to make sure,
117 # try adding/removing a slash after the address and looking
118 # again. We don't know what format was used in .netrc, which is
119 # why this rather hackish approach is necessary.
120 if bugzilla
[-1] == "/":
121 authenticate_on
= '"' + bugzilla
[:-1] + '"'
123 authenticate_on
= '"' + bugzilla
+ '/"'
124 ret
= credentials
.authenticators(authenticate_on
)
126 # Apparently, an invalid machine URL will cause credentials == None
127 error("no credentials for Bugzilla instance at %s" % bugzilla
)
130 def process_options(options
):
132 # Initialize bug report fields from message on standard input
134 message_parser
= email
.Parser
.Parser()
135 message
= message_parser
.parse(sys
.stdin
)
136 for (key
, value
) in message
.items():
137 data
.update({header_to_field(key
) : value
})
138 if not 'comment' in data
:
139 data
['comment'] = message
.get_payload()
141 # Merge in options from the command line; they override what's on stdin.
142 for (key
, value
) in options
.__dict
__.items():
143 if key
!= 'read' and value
!= None:
147 def ensure_defaults(data
):
148 # Provide some defaults if the user did not supply them.
149 if 'op_sys' not in data
:
150 if sys
.platform
.startswith('linux'):
151 data
['op_sys'] = 'Linux'
152 if 'rep_platform' not in data
:
153 data
['rep_platform'] = 'PC'
154 if 'bug_status' not in data
:
155 data
['bug_status'] = 'NEW'
156 if 'bug_severity' not in data
:
157 data
['bug_severity'] = 'normal'
158 if 'bug_file_loc' not in data
:
159 data
['bug_file_loc'] = 'http://' # Yes, Bugzilla needs this
160 if 'priority' not in data
:
161 data
['priority'] = 'P2'
163 def validate_fields(data
):
164 # Fields for validation
166 "bug_status", "bug_file_loc", "product", "version", "component",
167 "short_desc", "rep_platform", "op_sys", "priority", "bug_severity",
170 legal_fields
= required_fields
+ (
171 "assigned_to", "cc", "keywords", "dependson", "blocked",
173 my_fields
= data
.keys()
174 for field
in my_fields
:
175 if field
not in legal_fields
:
176 error("invalid field: %s" % field_to_header(field
))
177 for field
in required_fields
:
178 if field
not in my_fields
:
179 error("required field missing: %s" % field_to_header(field
))
181 if not data
['short_desc']:
182 error("summary for bug submission must not be empty")
184 if not data
['comment']:
185 error("comment for bug submission must not be empty")
188 # POST-specific functions
191 def submit_bug_POST(bugzilla
, data
):
192 # Move the request over the wire
193 postdata
= urllib
.urlencode(data
)
195 url
= ErrorURLopener().open("%s/post_bug.cgi" % bugzilla
, postdata
)
197 error("Bugzilla site at %s not found (HTTP returned 404)" % bugzilla
)
199 check_result_POST(ret
, data
)
201 def check_result_POST(ret
, data
):
202 # XXX We can move pre-validation out of here as soon as we pick up
203 # the valid options from config.cgi -- it will become a simple
204 # assertion and ID-grabbing step.
206 # XXX: We use get() here which may return None, but since the user
207 # might not have provided these options, we don't want to die on
209 version
= data
.get('version')
210 product
= data
.get('product')
211 component
= data
.get('component')
212 priority
= data
.get('priority')
213 severity
= data
.get('bug_severity')
214 status
= data
.get('bug_status')
215 assignee
= data
.get('assigned_to')
216 platform
= data
.get('rep_platform')
217 opsys
= data
.get('op_sys')
218 keywords
= data
.get('keywords')
219 deps
= data
.get('dependson', '') + " " + data
.get('blocked', '')
220 deps
= deps
.replace(" ", ", ")
221 # XXX: we should really not be using plain find() here, as it can
222 # match the bug content inadvertedly
223 if ret
.find("A legal Version was not") != -1:
224 error("version %r does not exist for component %s:%s" %
225 (version
, product
, component
))
226 if ret
.find("A legal Priority was not") != -1:
227 error("priority %r does not exist in "
228 "this Bugzilla instance" % priority
)
229 if ret
.find("A legal Severity was not") != -1:
230 error("severity %r does not exist in "
231 "this Bugzilla instance" % severity
)
232 if ret
.find("A legal Status was not") != -1:
233 error("status %r is not a valid creation status in "
234 "this Bugzilla instance" % status
)
235 if ret
.find("A legal Platform was not") != -1:
236 error("platform %r is not a valid platform in "
237 "this Bugzilla instance" % platform
)
238 if ret
.find("A legal OS/Version was not") != -1:
239 error("%r is not a valid OS in "
240 "this Bugzilla instance" % opsys
)
241 if ret
.find("Invalid Username") != -1:
242 error("invalid credentials submitted")
243 if ret
.find("Component Needed") != -1:
244 error("the component %r does not exist in "
245 "this Bugzilla instance" % component
)
246 if ret
.find("Unknown Keyword") != -1:
247 error("keyword(s) %r not registered in "
248 "this Bugzilla instance" % keywords
)
249 if ret
.find("The product name") != -1:
250 error("product %r does not exist in this "
251 "Bugzilla instance" % product
)
252 # XXX: this should be smarter
253 if ret
.find("does not exist") != -1:
254 error("could not mark dependencies for bugs %s: one or "
255 "more bugs didn't exist in this Bugzilla instance" % deps
)
256 if ret
.find("Match Failed") != -1:
257 # XXX: invalid CC hits on this error too
258 error("the bug assignee %r isn't registered in "
259 "this Bugzilla instance" % assignee
)
260 # If all is well, return bug number posted
261 if ret
.find("process_bug.cgi") == -1:
262 error("could not post bug to %s: are you sure this "
263 "is Bugzilla instance's top-level directory?" % bugzilla
)
264 m
= re
.search("Bug ([0-9]+) Submitted", ret
)
267 error("Internal error: bug id not found; please report a bug")
269 print "Bug %s posted." % id
275 if __name__
== "__main__":
276 parser
= setup_parser()
278 # Parser will print help and exit here if we specified --help
279 (options
, args
) = parser
.parse_args()
282 parser
.error("missing Bugzilla host URL")
285 data
= process_options(options
)
287 login
, account
, password
= get_credentials(bugzilla
)
288 if "@" not in login
: # no use even trying to submit
289 error("login %r is invalid (it should be an email address)" % login
)
291 ensure_defaults(data
)
292 validate_fields(data
)
294 # Attach authentication information
295 data
.update({'Bugzilla_login' : login
,
296 'Bugzilla_password' : password
,
297 'GoAheadAndLogIn' : 1,
298 'form_name' : 'enter_bug'})
300 submit_bug_POST(bugzilla
, data
)