]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Fix for Bug 220724: Provide standalone bug submission program. Includes
authorkiko%async.com.br <>
Fri, 31 Oct 2003 22:00:19 +0000 (22:00 +0000)
committerkiko%async.com.br <>
Fri, 31 Oct 2003 22:00:19 +0000 (22:00 +0000)
a python script that submits bugs to a specified Bugzilla instance.
README, docs and an example bug are included. Work done by Eric Raymond
<esr@thyrsus.com> and myself. a=justdave

contrib/README
contrib/bug-bugzilla/README [new file with mode: 0644]
contrib/bug-bugzilla/bug-bugzilla [new file with mode: 0755]
contrib/bug-bugzilla/bug-bugzilla.xml [new file with mode: 0644]
contrib/bug-bugzilla/bugdata.txt [new file with mode: 0644]
contrib/bugzilla-submit/README [new file with mode: 0644]
contrib/bugzilla-submit/bugdata.txt [new file with mode: 0644]
contrib/bugzilla-submit/bugzilla-submit [new file with mode: 0755]
contrib/bugzilla-submit/bugzilla-submit.xml [new file with mode: 0644]

index 85089e50ca2125e10017f1a6397c8bebe448d41f..e26d00d400da5b2e41d26fa96150101470ed0462 100644 (file)
@@ -5,8 +5,11 @@ be useful to you.
 
 This directory includes:
 
+       bug-bugzilla/ --  A standalone bug submission script.
+
    mysqld-watcher.pl --  This script can be installed as a frequent cron 
                          job to clean up stalled/dead queries.
+
          gnats2bz.pl --  A perl script to help import bugs from a GNATS 
                          database into a Bugzilla database.  Contributed by
                          Tom Schutter <tom@platte.com>
@@ -20,6 +23,7 @@ This directory includes:
  
         yp_nomail.sh --  Script you can run via cron that regularly updates
                          the nomail file for terminated employees 
+
 bugzilla_ldapsync.rb --  Script you can run via cron that queries an LDAP
                          server for e-mail addresses to add Bugzilla users
                          for. Will optionally disable Bugzilla users with
diff --git a/contrib/bug-bugzilla/README b/contrib/bug-bugzilla/README
new file mode 100644 (file)
index 0000000..08bc3d8
--- /dev/null
@@ -0,0 +1,46 @@
+Bug-Bugzilla
+============
+
+Authors: Christian Reis <kiko@async.com.br>
+         Eric Raymond <esr@thyrsus.com>
+
+Bug-Bugzilla is a simple Python program that creates bugs in a Bugzilla
+instance. It takes as input text resembling message headers (RFC-822
+formatted) via standard input, or optionally a number of commandline
+parameters. It communicates using HTTP, which allows it to work over a
+network.
+
+Requirements
+------------
+
+Its only requirement is Python 2.3 or higher; you should have the
+"python" executable in your path.
+
+Usage Notes
+-----------
+
+* Please constrain testing to your own installation of Bugzilla, or use
+* http://landfill.bugzilla.org/ for testing purposes -- opening test
+* bugs on production instances of Bugzilla is definitely not a good idea
+
+Run "bug-bugzilla --help" for a description of the possible options.
+
+An example input file, named bugdata.txt, is provided. You can pipe it
+in as standard input to bug-bugzilla, providing a Bugzilla URI through
+the command-line.
+
+Note that you must create a ~/.netrc entry to authenticate against the
+Bugzilla instance. The entry's machine field is a *quoted* Bugzilla URI,
+the login field is your ID on that host, and the password field is the
+your password password. An example entry follows:
+
+    machine "http://bugzilla.mozilla.org/"
+    login foo@bar.loo
+    password snarf
+
+Documentation
+-------------
+
+Documentation for bug-bugzilla is provided in Docbook format; see
+bug-bugzilla.xml.
+
diff --git a/contrib/bug-bugzilla/bug-bugzilla b/contrib/bug-bugzilla/bug-bugzilla
new file mode 100755 (executable)
index 0000000..e16a968
--- /dev/null
@@ -0,0 +1,279 @@
+#!/usr/bin/env python 
+#
+# bug-bugzilla: a command-line script to post bugs to a Bugzilla instance
+#
+# Authors: Christian Reis <kiko@async.com.br>
+#          Eric S. Raymond <esr@thyrsus.com>
+#
+# This is version 0.5.
+# 
+# For a usage hint run bug-bugzilla --help
+#
+# TODO: use RDF output to pick up valid options, as in
+#   http://www.async.com.br/~kiko/mybugzilla/config.cgi?ctype=rdf
+
+import sys
+
+def error(m):
+    sys.stderr.write("bug-bugzilla: %s\n" % m)
+    sys.stderr.flush()
+    sys.exit(1)
+
+if sys.version[:6] < '2.3.0':
+    error("you must upgrade to Python 2.3 or higher to use this script.")
+
+import urllib, re, os, netrc, email.Parser, optparse
+
+# Set up some aliases -- partly to hide the less friendly fieldnames
+# behind the names actually used for them in the stock web page presentation,
+# and partly to provide a place for mappings if the Bugzilla fieldnames
+# ever change.
+field_aliases = (('hardware', 'rep_platform'),
+                 ('os', 'op_sys'),
+                 ('summary', 'short_desc'),
+                 ('description', 'comment'),
+                 ('depends-on', 'dependson'),
+                 ('status', 'bug_status'),
+                 ('severity', 'bug_severity'),
+                 ('URL', 'bug_file_loc'),)
+
+def header_to_field(hdr):
+    hdr = hdr.lower().replace("-", "_")
+    for (alias, name) in field_aliases:
+        if hdr == alias:
+            hdr = name
+            break
+    return hdr
+
+def field_to_header(hdr):
+    hdr = "-".join(map(lambda x: x.capitalize(), hdr.split("_")))
+    for (alias, name) in field_aliases:
+        if hdr == name:
+            hdr = alias
+            break
+    return hdr
+
+def setup_parser():
+    # Take override values from the command line
+    parser = optparse.OptionParser(usage="usage: %prog [options] bugzilla-url")
+    parser.add_option('-b', '--status', dest='bug_status',
+                      help='Set the Status field.')
+    parser.add_option('-u', '--url', dest='bug_file_loc',
+                      help='Set the URL field.')
+    parser.add_option('-p', '--product', dest='product',
+                      help='Set the Product field.')
+    parser.add_option('-v', '--version', dest='version',
+                      help='Set the Version field.')
+    parser.add_option('-c', '--component', dest='component',
+                      help='Set the Component field.')
+    parser.add_option('-s', '--summary', dest='short_desc',
+                      help='Set the Summary field.')
+    parser.add_option('-H', '--hardware', dest='rep_platform',
+                      help='Set the Hardware field.')
+    parser.add_option('-o', '--os', dest='op_sys',
+                      help='Set the Operating-system field.')
+    parser.add_option('-r', '--priority', dest='priority',
+                      help='Set the Priority field.')
+    parser.add_option('-x', '--severity', dest='bug_severity',
+                      help='Set the Severity field.')
+    parser.add_option('-d', '--description', dest='comment',
+                      help='Set the Description field.')
+    parser.add_option('-a', '--assigned-to', dest='assigned_to',
+                      help='Set the Assigned-To field.')
+    parser.add_option('-C', '--cc', dest='cc',
+                      help='Set the Cc field.')
+    parser.add_option('-k', '--keywords', dest='keywords',
+                      help='Set the Keywords field.')
+    parser.add_option('-D', '--depends-on', dest='dependson',
+                      help='Set the Depends-On field.')
+    parser.add_option('-B', '--blocked', dest='blocked',
+                      help='Set the Blocked field.')
+    parser.add_option('-n', '--no-stdin', dest='read',
+                      default=True, action='store_false',
+                      help='Suppress reading fields from stdin.')
+    return parser
+
+# Fetch user's credential for access to this Bugzilla instance.
+def get_credentials(bugzilla):
+    # Work around a quirk in the Python implementation.
+    # The URL has to be quoted, otherwise the parser barfs on the colon.
+    # But the parser doesn't strip the quotes.
+    authenticate_on = '"' + bugzilla + '"'
+    try:
+        credentials = netrc.netrc()
+    except netrc.NetrcParseError, e:
+        error("ill-formed .netrc: %s:%s %s" % (e.filename, e.lineno, e.msg))
+    except IOError, e:
+        error("missing .netrc file %s" % str(e).split()[-1])
+    ret = credentials.authenticators(authenticate_on)
+    if not ret:
+        # Apparently, an invalid machine URL will cause credentials == None
+        error("no credentials for Bugzilla instance at %s" % bugzilla)
+    return ret
+
+def process_options(options):
+    data = {}
+    # Initialize bug report fields from message on standard input
+    if options.read:
+        message_parser = email.Parser.Parser()
+        message = message_parser.parse(sys.stdin)
+        for (key, value) in message.items():
+            data.update({header_to_field(key) : value})
+        if not 'comment' in data:
+            data['comment'] = message.get_payload() 
+
+    # Merge in options from the command line; they override what's on stdin.
+    for (key, value) in options.__dict__.items():
+        if key != 'read' and value != None:
+            data[key] = value
+    return data
+
+def ensure_defaults(data):
+    # Provide some defaults if the user did not supply them.
+    if 'op_sys' not in data:
+        if sys.platform.startswith('linux'):
+            data['op_sys'] = 'Linux'
+    if 'rep_platform' not in data:
+        data['rep_platform'] = 'PC'
+    if 'bug_status' not in data:
+        data['bug_status'] = 'NEW'
+    if 'bug_severity' not in data:
+        data['bug_severity'] = 'normal'
+    if 'bug_file_loc' not in data:
+        data['bug_file_loc'] = 'http://'        # Yes, Bugzilla needs this
+    if 'priority' not in data:
+        data['priority'] = 'P2'
+
+def validate_fields(data):
+    # Fields for validation
+    required_fields = (
+        "bug_status", "bug_file_loc", "product", "version", "component",
+        "short_desc", "rep_platform", "op_sys", "priority", "bug_severity",
+        "comment",
+    )
+    legal_fields = required_fields + (
+        "assigned_to", "cc", "keywords", "dependson", "blocked",
+    )
+    my_fields = data.keys()
+    for field in my_fields:
+        if field not in legal_fields:
+            error("invalid field: %s" % field_to_header(field))
+    for field in required_fields:
+        if field not in my_fields:
+            error("required field missing: %s" % field_to_header(field))
+    
+    if not data['short_desc']:
+        error("summary for bug submission must not be empty")
+    
+    if not data['comment']:
+        error("comment for bug submission must not be empty")
+
+#
+# POST-specific functions
+#
+
+def submit_bug_POST(bugzilla, data):
+    # Move the request over the wire
+    postdata = urllib.urlencode(data)
+    url = urllib.urlopen("%s/post_bug.cgi" % bugzilla, postdata)
+    ret = url.read()
+    check_result_POST(ret, data)
+
+def check_result_POST(ret, data):
+    # XXX We can move pre-validation out of here as soon as we pick up
+    # the valid options from config.cgi -- it will become a simple
+    # assertion and ID-grabbing step.
+    #
+    # XXX: We use get() here which may return None, but since the user
+    # might not have provided these options, we don't want to die on
+    # them.
+    version = data.get('version')
+    product = data.get('product')
+    component = data.get('component')
+    priority = data.get('priority')
+    severity = data.get('bug_severity')
+    status = data.get('bug_status')
+    assignee = data.get('assigned_to')
+    platform = data.get('rep_platform')
+    opsys = data.get('op_sys')
+    keywords = data.get('keywords')
+    deps = data.get('dependson', '') + " " + data.get('blocked', '') 
+    deps = deps.replace(" ", ", ")
+    # XXX: we should really not be using plain find() here, as it can
+    # match the bug content inadvertedly
+    if ret.find("A legal Version was not") != -1:
+        error("version %r does not exist for component %s:%s" % 
+                (version, product, component))
+    if ret.find("A legal Priority was not") != -1:
+        error("priority %r does not exist in "
+              "this Bugzilla instance" % priority)
+    if ret.find("A legal Severity was not") != -1:
+        error("severity %r does not exist in "
+              "this Bugzilla instance" % severity)
+    if ret.find("A legal Status was not") != -1:
+        error("status %r is not a valid creation status in "
+              "this Bugzilla instance" % status)
+    if ret.find("A legal Platform was not") != -1:
+        error("platform %r is not a valid platform in "
+              "this Bugzilla instance" % platform)
+    if ret.find("A legal OS/Version was not") != -1:
+        error("%r is not a valid OS in "
+              "this Bugzilla instance" % opsys)
+    if ret.find("Invalid Username") != -1:
+        error("invalid credentials submitted")
+    if ret.find("Component Needed") != -1:
+        error("the component %r does not exist in "
+              "this Bugzilla instance" % component)
+    if ret.find("Unknown Keyword") != -1:
+        error("keyword(s) %r not registered in "
+              "this Bugzilla instance" % keywords)
+    if ret.find("The product name") != -1:
+        error("Product %r does not exist in this "
+              "Bugzilla instance" % product)
+    # XXX: this should be smarter
+    if ret.find("does not exist") != -1:
+        error("Could not mark dependencies for bugs %s: one or "
+              "more bugs didn't exist in this Bugzilla instance" % deps)
+    if ret.find("Match Failed") != -1:
+        # XXX: invalid CC hits on this error too
+        error("the bug assignee %r isn't registered in " 
+              "this Bugzilla instance" % assignee)
+    # If all is well, return bug number posted
+    m = re.search("Bug ([0-9]+) Submitted", ret)
+    if not m:
+        print ret
+        error("Internal error: bug id not found; please report a bug")
+    id = m.group(1)
+    print "Bug %s posted." % id
+
+#
+#
+#
+
+if __name__ == "__main__":
+    parser = setup_parser()
+
+    # Parser will print help and exit here if we specified --help
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1:
+        parser.error("missing Bugzilla host URL")
+
+    bugzilla = args[0]
+    data = process_options(options)
+
+    login, account, password = get_credentials(bugzilla)
+    if "@" not in login:   # no use even trying to submit
+        error("login %r is invalid (it should be an email address)" % login)
+
+    ensure_defaults(data)
+    validate_fields(data)
+
+    # Attach authentication information
+    data.update({'Bugzilla_login'    : login,
+                 'Bugzilla_password' : password,
+                 'GoAheadAndLogIn'   : 1,
+                 'form_name'         : 'enter_bug'})
+
+    submit_bug_POST(bugzilla, data)
+
diff --git a/contrib/bug-bugzilla/bug-bugzilla.xml b/contrib/bug-bugzilla/bug-bugzilla.xml
new file mode 100644 (file)
index 0000000..da8a34c
--- /dev/null
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE refentry PUBLIC 
+   "-//OASIS//DTD DocBook XML V4.1.2//EN"
+   "docbook/docbookx.dtd">
+<refentry id='bug-bugzilla.1'>
+<refmeta>
+<refentrytitle>bug-bugzilla</refentrytitle>
+<manvolnum>1</manvolnum>
+<refmiscinfo class='date'>Oct 30, 2003</refmiscinfo>
+</refmeta>
+<refnamediv id='name'>
+<refname>bug-bugzilla</refname>
+<refpurpose>post bugs to a Bugzilla instance</refpurpose>
+</refnamediv>
+<refsynopsisdiv id='synopsis'>
+
+<cmdsynopsis>
+  <command>bug-bugzilla</command>
+  <arg choice='opt'>--status <replaceable>bug_status</replaceable></arg>
+  <arg choice='opt'>--url <replaceable>bug_file_loc</replaceable></arg>
+  <arg choice='opt'>--product <replaceable>product</replaceable></arg>
+  <arg choice='opt'>--version <replaceable>version</replaceable></arg>
+  <arg choice='opt'>--component <replaceable>component</replaceable></arg>
+  <arg choice='opt'>--summary <replaceable>short_desc</replaceable></arg>
+  <arg choice='opt'>--hardware <replaceable>rep_platform</replaceable></arg>
+  <arg choice='opt'>--os <replaceable>op_sys</replaceable></arg>
+  <arg choice='opt'>--priority <replaceable>priority</replaceable></arg>
+  <arg choice='opt'>--severity <replaceable>bug_severity</replaceable></arg>
+  <arg choice='opt'>--assigned-to <replaceable>assigned-to</replaceable></arg>
+  <arg choice='opt'>--cc <replaceable>cc</replaceable></arg>
+  <arg choice='opt'>--keywords <replaceable>keywords</replaceable></arg>
+  <arg choice='opt'>--depends-on <replaceable>dependson</replaceable></arg>
+  <arg choice='opt'>--blocked <replaceable>blocked</replaceable></arg>
+  <arg choice='opt'>--description <replaceable>comment</replaceable></arg>
+  <arg choice='opt'>--no-read </arg>
+  <arg choice='plain'><replaceable>bugzilla-url</replaceable></arg>
+</cmdsynopsis>
+
+</refsynopsisdiv>
+
+<refsect1 id='description'><title>DESCRIPTION</title>
+
+<para><application>bug-bugzilla</application> is a command-line tool
+for posting bug reports to any instance of Bugzilla.  It accepts on
+standard input text resembling an RFC-822 message.  The headers of
+that message, and its body, are used to set error-report field values.
+More field values are merged in from command-line options. If required
+fields have not been set, <application>bug-bugzilla</application>
+tries to compute them.  Finally, the resulting error report is
+validated.  If all required fields are present, and there are no
+illegal fields or values, the report is shipped off to the Mozilla
+instance specified by the single positional argument.  Login/password
+credentials are read from the calling user's <filename>~/.netrc</filename> 
+file.</para>
+
+<para>The program accepts the following options to set or override fields:</para>
+<variablelist>
+<varlistentry>
+<term>-b. --bug-status</term>
+<listitem>
+<para>Set the bug_status field, overriding the Status header from
+standard input if present.  (The stock Bugzilla web presentation
+identifies this field as <quote>Status</quote>.)</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-u, --url</term>
+<listitem>
+<para>Set the bug_file_loc field, overriding the URL header from
+standard input if present. (The stock Bugzilla web presentation
+identifies this field as <quote>URL</quote>.)</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-p, --product</term>
+<listitem>
+<para>Set the product field, overriding the Product header from
+standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-v, --version</term>
+<listitem><para>Set the version field, overriding the Version header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-c, --component</term>
+<listitem><para>Set the component field, overriding the Component header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-s, --summary</term>
+<listitem><para>Set the short_desc field, overriding the Summary header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>Summary</quote>.)</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-H, --hardware</term>
+<listitem><para>Set the rep_platform field, overriding the Hardware header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>Hardware</quote>.)</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-o, --os</term>
+<listitem><para>Set the op_sys field, overriding the Operating-System header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>OS</quote>.)</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-r, --priority</term>
+<listitem><para>Set the priority field, overriding the Priority header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-x, --severity</term>
+<listitem><para>Set the severity field, overriding the Severity header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-d, --description</term>
+<listitem><para>Set the comment field, overriding the Description header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>Description</quote>.)  If there is a
+message body and no Description field and this option is not
+specified, the message body is used as a description.
+</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-a, --assigned-to</term>
+<listitem>
+<para>Set the optional assigned_to field, overriding the Assigned-To
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-C, --cc</term>
+<listitem>
+<para>Set the optional cc field, overriding the Cc
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-k, --keywords</term>
+<listitem>
+<para>Set the optional keywords field, overriding the Keywords
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-D, --depends-on</term>
+<listitem>
+<para>Set the optional dependson field, overriding the Depends-On
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-B, --assigned-to</term>
+<listitem>
+<para>Set the optional blocked field, overriding the Blocked
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-n, --no-stdin</term>
+<listitem><para>Suppress reading fields from standard input.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-h, --help</term>
+<listitem><para>Print usage help and exit.</para></listitem>
+</varlistentry>
+</variablelist>
+
+<para>This program will try to deduce OS and Hardware if those are not
+specified.  If it fails, validation will fail before shipping the
+report.</para>
+
+<para>There is expected to be a single positional argument following
+any options. It should be the URL of the Bugzilla instance to which
+the bug is to be submitted.</para>
+
+</refsect1>
+<refsect1 id='files'><title>FILES</title> 
+<variablelist>
+<varlistentry>
+<term><filename>~/.netrc</filename></term>
+<listitem><para>Must contain an entry in which the machine field is
+the Bugzilla instance URL, the login field is your ID on that host, and the 
+password field is the right password.  The URL in the machine field
+must be enclosed in double quotes.</para></listitem>
+</varlistentry>
+</variablelist>
+
+</refsect1>
+<refsect1 id='author'><title>AUTHORS</title> 
+<para>Christian Reis &lt;kiko@async.com.br&gt;, Eric S. Raymond
+&lt;esr@thyrsus.com&gt;.</para> 
+</refsect1> 
+</refentry>
+
diff --git a/contrib/bug-bugzilla/bugdata.txt b/contrib/bug-bugzilla/bugdata.txt
new file mode 100644 (file)
index 0000000..56f70b9
--- /dev/null
@@ -0,0 +1,11 @@
+Product: FoodReplicator
+Component: Salt
+Version: 1.0
+Priority: P2
+Hardware: PC
+OS: Linux
+Severity: critical
+Summary: Impending electron shortage
+
+We need an emergency supply of electrons.
+
diff --git a/contrib/bugzilla-submit/README b/contrib/bugzilla-submit/README
new file mode 100644 (file)
index 0000000..08bc3d8
--- /dev/null
@@ -0,0 +1,46 @@
+Bug-Bugzilla
+============
+
+Authors: Christian Reis <kiko@async.com.br>
+         Eric Raymond <esr@thyrsus.com>
+
+Bug-Bugzilla is a simple Python program that creates bugs in a Bugzilla
+instance. It takes as input text resembling message headers (RFC-822
+formatted) via standard input, or optionally a number of commandline
+parameters. It communicates using HTTP, which allows it to work over a
+network.
+
+Requirements
+------------
+
+Its only requirement is Python 2.3 or higher; you should have the
+"python" executable in your path.
+
+Usage Notes
+-----------
+
+* Please constrain testing to your own installation of Bugzilla, or use
+* http://landfill.bugzilla.org/ for testing purposes -- opening test
+* bugs on production instances of Bugzilla is definitely not a good idea
+
+Run "bug-bugzilla --help" for a description of the possible options.
+
+An example input file, named bugdata.txt, is provided. You can pipe it
+in as standard input to bug-bugzilla, providing a Bugzilla URI through
+the command-line.
+
+Note that you must create a ~/.netrc entry to authenticate against the
+Bugzilla instance. The entry's machine field is a *quoted* Bugzilla URI,
+the login field is your ID on that host, and the password field is the
+your password password. An example entry follows:
+
+    machine "http://bugzilla.mozilla.org/"
+    login foo@bar.loo
+    password snarf
+
+Documentation
+-------------
+
+Documentation for bug-bugzilla is provided in Docbook format; see
+bug-bugzilla.xml.
+
diff --git a/contrib/bugzilla-submit/bugdata.txt b/contrib/bugzilla-submit/bugdata.txt
new file mode 100644 (file)
index 0000000..56f70b9
--- /dev/null
@@ -0,0 +1,11 @@
+Product: FoodReplicator
+Component: Salt
+Version: 1.0
+Priority: P2
+Hardware: PC
+OS: Linux
+Severity: critical
+Summary: Impending electron shortage
+
+We need an emergency supply of electrons.
+
diff --git a/contrib/bugzilla-submit/bugzilla-submit b/contrib/bugzilla-submit/bugzilla-submit
new file mode 100755 (executable)
index 0000000..e16a968
--- /dev/null
@@ -0,0 +1,279 @@
+#!/usr/bin/env python 
+#
+# bug-bugzilla: a command-line script to post bugs to a Bugzilla instance
+#
+# Authors: Christian Reis <kiko@async.com.br>
+#          Eric S. Raymond <esr@thyrsus.com>
+#
+# This is version 0.5.
+# 
+# For a usage hint run bug-bugzilla --help
+#
+# TODO: use RDF output to pick up valid options, as in
+#   http://www.async.com.br/~kiko/mybugzilla/config.cgi?ctype=rdf
+
+import sys
+
+def error(m):
+    sys.stderr.write("bug-bugzilla: %s\n" % m)
+    sys.stderr.flush()
+    sys.exit(1)
+
+if sys.version[:6] < '2.3.0':
+    error("you must upgrade to Python 2.3 or higher to use this script.")
+
+import urllib, re, os, netrc, email.Parser, optparse
+
+# Set up some aliases -- partly to hide the less friendly fieldnames
+# behind the names actually used for them in the stock web page presentation,
+# and partly to provide a place for mappings if the Bugzilla fieldnames
+# ever change.
+field_aliases = (('hardware', 'rep_platform'),
+                 ('os', 'op_sys'),
+                 ('summary', 'short_desc'),
+                 ('description', 'comment'),
+                 ('depends-on', 'dependson'),
+                 ('status', 'bug_status'),
+                 ('severity', 'bug_severity'),
+                 ('URL', 'bug_file_loc'),)
+
+def header_to_field(hdr):
+    hdr = hdr.lower().replace("-", "_")
+    for (alias, name) in field_aliases:
+        if hdr == alias:
+            hdr = name
+            break
+    return hdr
+
+def field_to_header(hdr):
+    hdr = "-".join(map(lambda x: x.capitalize(), hdr.split("_")))
+    for (alias, name) in field_aliases:
+        if hdr == name:
+            hdr = alias
+            break
+    return hdr
+
+def setup_parser():
+    # Take override values from the command line
+    parser = optparse.OptionParser(usage="usage: %prog [options] bugzilla-url")
+    parser.add_option('-b', '--status', dest='bug_status',
+                      help='Set the Status field.')
+    parser.add_option('-u', '--url', dest='bug_file_loc',
+                      help='Set the URL field.')
+    parser.add_option('-p', '--product', dest='product',
+                      help='Set the Product field.')
+    parser.add_option('-v', '--version', dest='version',
+                      help='Set the Version field.')
+    parser.add_option('-c', '--component', dest='component',
+                      help='Set the Component field.')
+    parser.add_option('-s', '--summary', dest='short_desc',
+                      help='Set the Summary field.')
+    parser.add_option('-H', '--hardware', dest='rep_platform',
+                      help='Set the Hardware field.')
+    parser.add_option('-o', '--os', dest='op_sys',
+                      help='Set the Operating-system field.')
+    parser.add_option('-r', '--priority', dest='priority',
+                      help='Set the Priority field.')
+    parser.add_option('-x', '--severity', dest='bug_severity',
+                      help='Set the Severity field.')
+    parser.add_option('-d', '--description', dest='comment',
+                      help='Set the Description field.')
+    parser.add_option('-a', '--assigned-to', dest='assigned_to',
+                      help='Set the Assigned-To field.')
+    parser.add_option('-C', '--cc', dest='cc',
+                      help='Set the Cc field.')
+    parser.add_option('-k', '--keywords', dest='keywords',
+                      help='Set the Keywords field.')
+    parser.add_option('-D', '--depends-on', dest='dependson',
+                      help='Set the Depends-On field.')
+    parser.add_option('-B', '--blocked', dest='blocked',
+                      help='Set the Blocked field.')
+    parser.add_option('-n', '--no-stdin', dest='read',
+                      default=True, action='store_false',
+                      help='Suppress reading fields from stdin.')
+    return parser
+
+# Fetch user's credential for access to this Bugzilla instance.
+def get_credentials(bugzilla):
+    # Work around a quirk in the Python implementation.
+    # The URL has to be quoted, otherwise the parser barfs on the colon.
+    # But the parser doesn't strip the quotes.
+    authenticate_on = '"' + bugzilla + '"'
+    try:
+        credentials = netrc.netrc()
+    except netrc.NetrcParseError, e:
+        error("ill-formed .netrc: %s:%s %s" % (e.filename, e.lineno, e.msg))
+    except IOError, e:
+        error("missing .netrc file %s" % str(e).split()[-1])
+    ret = credentials.authenticators(authenticate_on)
+    if not ret:
+        # Apparently, an invalid machine URL will cause credentials == None
+        error("no credentials for Bugzilla instance at %s" % bugzilla)
+    return ret
+
+def process_options(options):
+    data = {}
+    # Initialize bug report fields from message on standard input
+    if options.read:
+        message_parser = email.Parser.Parser()
+        message = message_parser.parse(sys.stdin)
+        for (key, value) in message.items():
+            data.update({header_to_field(key) : value})
+        if not 'comment' in data:
+            data['comment'] = message.get_payload() 
+
+    # Merge in options from the command line; they override what's on stdin.
+    for (key, value) in options.__dict__.items():
+        if key != 'read' and value != None:
+            data[key] = value
+    return data
+
+def ensure_defaults(data):
+    # Provide some defaults if the user did not supply them.
+    if 'op_sys' not in data:
+        if sys.platform.startswith('linux'):
+            data['op_sys'] = 'Linux'
+    if 'rep_platform' not in data:
+        data['rep_platform'] = 'PC'
+    if 'bug_status' not in data:
+        data['bug_status'] = 'NEW'
+    if 'bug_severity' not in data:
+        data['bug_severity'] = 'normal'
+    if 'bug_file_loc' not in data:
+        data['bug_file_loc'] = 'http://'        # Yes, Bugzilla needs this
+    if 'priority' not in data:
+        data['priority'] = 'P2'
+
+def validate_fields(data):
+    # Fields for validation
+    required_fields = (
+        "bug_status", "bug_file_loc", "product", "version", "component",
+        "short_desc", "rep_platform", "op_sys", "priority", "bug_severity",
+        "comment",
+    )
+    legal_fields = required_fields + (
+        "assigned_to", "cc", "keywords", "dependson", "blocked",
+    )
+    my_fields = data.keys()
+    for field in my_fields:
+        if field not in legal_fields:
+            error("invalid field: %s" % field_to_header(field))
+    for field in required_fields:
+        if field not in my_fields:
+            error("required field missing: %s" % field_to_header(field))
+    
+    if not data['short_desc']:
+        error("summary for bug submission must not be empty")
+    
+    if not data['comment']:
+        error("comment for bug submission must not be empty")
+
+#
+# POST-specific functions
+#
+
+def submit_bug_POST(bugzilla, data):
+    # Move the request over the wire
+    postdata = urllib.urlencode(data)
+    url = urllib.urlopen("%s/post_bug.cgi" % bugzilla, postdata)
+    ret = url.read()
+    check_result_POST(ret, data)
+
+def check_result_POST(ret, data):
+    # XXX We can move pre-validation out of here as soon as we pick up
+    # the valid options from config.cgi -- it will become a simple
+    # assertion and ID-grabbing step.
+    #
+    # XXX: We use get() here which may return None, but since the user
+    # might not have provided these options, we don't want to die on
+    # them.
+    version = data.get('version')
+    product = data.get('product')
+    component = data.get('component')
+    priority = data.get('priority')
+    severity = data.get('bug_severity')
+    status = data.get('bug_status')
+    assignee = data.get('assigned_to')
+    platform = data.get('rep_platform')
+    opsys = data.get('op_sys')
+    keywords = data.get('keywords')
+    deps = data.get('dependson', '') + " " + data.get('blocked', '') 
+    deps = deps.replace(" ", ", ")
+    # XXX: we should really not be using plain find() here, as it can
+    # match the bug content inadvertedly
+    if ret.find("A legal Version was not") != -1:
+        error("version %r does not exist for component %s:%s" % 
+                (version, product, component))
+    if ret.find("A legal Priority was not") != -1:
+        error("priority %r does not exist in "
+              "this Bugzilla instance" % priority)
+    if ret.find("A legal Severity was not") != -1:
+        error("severity %r does not exist in "
+              "this Bugzilla instance" % severity)
+    if ret.find("A legal Status was not") != -1:
+        error("status %r is not a valid creation status in "
+              "this Bugzilla instance" % status)
+    if ret.find("A legal Platform was not") != -1:
+        error("platform %r is not a valid platform in "
+              "this Bugzilla instance" % platform)
+    if ret.find("A legal OS/Version was not") != -1:
+        error("%r is not a valid OS in "
+              "this Bugzilla instance" % opsys)
+    if ret.find("Invalid Username") != -1:
+        error("invalid credentials submitted")
+    if ret.find("Component Needed") != -1:
+        error("the component %r does not exist in "
+              "this Bugzilla instance" % component)
+    if ret.find("Unknown Keyword") != -1:
+        error("keyword(s) %r not registered in "
+              "this Bugzilla instance" % keywords)
+    if ret.find("The product name") != -1:
+        error("Product %r does not exist in this "
+              "Bugzilla instance" % product)
+    # XXX: this should be smarter
+    if ret.find("does not exist") != -1:
+        error("Could not mark dependencies for bugs %s: one or "
+              "more bugs didn't exist in this Bugzilla instance" % deps)
+    if ret.find("Match Failed") != -1:
+        # XXX: invalid CC hits on this error too
+        error("the bug assignee %r isn't registered in " 
+              "this Bugzilla instance" % assignee)
+    # If all is well, return bug number posted
+    m = re.search("Bug ([0-9]+) Submitted", ret)
+    if not m:
+        print ret
+        error("Internal error: bug id not found; please report a bug")
+    id = m.group(1)
+    print "Bug %s posted." % id
+
+#
+#
+#
+
+if __name__ == "__main__":
+    parser = setup_parser()
+
+    # Parser will print help and exit here if we specified --help
+    (options, args) = parser.parse_args()
+
+    if len(args) != 1:
+        parser.error("missing Bugzilla host URL")
+
+    bugzilla = args[0]
+    data = process_options(options)
+
+    login, account, password = get_credentials(bugzilla)
+    if "@" not in login:   # no use even trying to submit
+        error("login %r is invalid (it should be an email address)" % login)
+
+    ensure_defaults(data)
+    validate_fields(data)
+
+    # Attach authentication information
+    data.update({'Bugzilla_login'    : login,
+                 'Bugzilla_password' : password,
+                 'GoAheadAndLogIn'   : 1,
+                 'form_name'         : 'enter_bug'})
+
+    submit_bug_POST(bugzilla, data)
+
diff --git a/contrib/bugzilla-submit/bugzilla-submit.xml b/contrib/bugzilla-submit/bugzilla-submit.xml
new file mode 100644 (file)
index 0000000..da8a34c
--- /dev/null
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE refentry PUBLIC 
+   "-//OASIS//DTD DocBook XML V4.1.2//EN"
+   "docbook/docbookx.dtd">
+<refentry id='bug-bugzilla.1'>
+<refmeta>
+<refentrytitle>bug-bugzilla</refentrytitle>
+<manvolnum>1</manvolnum>
+<refmiscinfo class='date'>Oct 30, 2003</refmiscinfo>
+</refmeta>
+<refnamediv id='name'>
+<refname>bug-bugzilla</refname>
+<refpurpose>post bugs to a Bugzilla instance</refpurpose>
+</refnamediv>
+<refsynopsisdiv id='synopsis'>
+
+<cmdsynopsis>
+  <command>bug-bugzilla</command>
+  <arg choice='opt'>--status <replaceable>bug_status</replaceable></arg>
+  <arg choice='opt'>--url <replaceable>bug_file_loc</replaceable></arg>
+  <arg choice='opt'>--product <replaceable>product</replaceable></arg>
+  <arg choice='opt'>--version <replaceable>version</replaceable></arg>
+  <arg choice='opt'>--component <replaceable>component</replaceable></arg>
+  <arg choice='opt'>--summary <replaceable>short_desc</replaceable></arg>
+  <arg choice='opt'>--hardware <replaceable>rep_platform</replaceable></arg>
+  <arg choice='opt'>--os <replaceable>op_sys</replaceable></arg>
+  <arg choice='opt'>--priority <replaceable>priority</replaceable></arg>
+  <arg choice='opt'>--severity <replaceable>bug_severity</replaceable></arg>
+  <arg choice='opt'>--assigned-to <replaceable>assigned-to</replaceable></arg>
+  <arg choice='opt'>--cc <replaceable>cc</replaceable></arg>
+  <arg choice='opt'>--keywords <replaceable>keywords</replaceable></arg>
+  <arg choice='opt'>--depends-on <replaceable>dependson</replaceable></arg>
+  <arg choice='opt'>--blocked <replaceable>blocked</replaceable></arg>
+  <arg choice='opt'>--description <replaceable>comment</replaceable></arg>
+  <arg choice='opt'>--no-read </arg>
+  <arg choice='plain'><replaceable>bugzilla-url</replaceable></arg>
+</cmdsynopsis>
+
+</refsynopsisdiv>
+
+<refsect1 id='description'><title>DESCRIPTION</title>
+
+<para><application>bug-bugzilla</application> is a command-line tool
+for posting bug reports to any instance of Bugzilla.  It accepts on
+standard input text resembling an RFC-822 message.  The headers of
+that message, and its body, are used to set error-report field values.
+More field values are merged in from command-line options. If required
+fields have not been set, <application>bug-bugzilla</application>
+tries to compute them.  Finally, the resulting error report is
+validated.  If all required fields are present, and there are no
+illegal fields or values, the report is shipped off to the Mozilla
+instance specified by the single positional argument.  Login/password
+credentials are read from the calling user's <filename>~/.netrc</filename> 
+file.</para>
+
+<para>The program accepts the following options to set or override fields:</para>
+<variablelist>
+<varlistentry>
+<term>-b. --bug-status</term>
+<listitem>
+<para>Set the bug_status field, overriding the Status header from
+standard input if present.  (The stock Bugzilla web presentation
+identifies this field as <quote>Status</quote>.)</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-u, --url</term>
+<listitem>
+<para>Set the bug_file_loc field, overriding the URL header from
+standard input if present. (The stock Bugzilla web presentation
+identifies this field as <quote>URL</quote>.)</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-p, --product</term>
+<listitem>
+<para>Set the product field, overriding the Product header from
+standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-v, --version</term>
+<listitem><para>Set the version field, overriding the Version header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-c, --component</term>
+<listitem><para>Set the component field, overriding the Component header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-s, --summary</term>
+<listitem><para>Set the short_desc field, overriding the Summary header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>Summary</quote>.)</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-H, --hardware</term>
+<listitem><para>Set the rep_platform field, overriding the Hardware header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>Hardware</quote>.)</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-o, --os</term>
+<listitem><para>Set the op_sys field, overriding the Operating-System header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>OS</quote>.)</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-r, --priority</term>
+<listitem><para>Set the priority field, overriding the Priority header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-x, --severity</term>
+<listitem><para>Set the severity field, overriding the Severity header
+from standard input if necessary.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-d, --description</term>
+<listitem><para>Set the comment field, overriding the Description header
+from standard input if necessary. (The stock Bugzilla web presentation
+identifies this field as <quote>Description</quote>.)  If there is a
+message body and no Description field and this option is not
+specified, the message body is used as a description.
+</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-a, --assigned-to</term>
+<listitem>
+<para>Set the optional assigned_to field, overriding the Assigned-To
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-C, --cc</term>
+<listitem>
+<para>Set the optional cc field, overriding the Cc
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-k, --keywords</term>
+<listitem>
+<para>Set the optional keywords field, overriding the Keywords
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-D, --depends-on</term>
+<listitem>
+<para>Set the optional dependson field, overriding the Depends-On
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-B, --assigned-to</term>
+<listitem>
+<para>Set the optional blocked field, overriding the Blocked
+header from standard input if necessary.</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>-n, --no-stdin</term>
+<listitem><para>Suppress reading fields from standard input.</para></listitem>
+</varlistentry>
+<varlistentry>
+<term>-h, --help</term>
+<listitem><para>Print usage help and exit.</para></listitem>
+</varlistentry>
+</variablelist>
+
+<para>This program will try to deduce OS and Hardware if those are not
+specified.  If it fails, validation will fail before shipping the
+report.</para>
+
+<para>There is expected to be a single positional argument following
+any options. It should be the URL of the Bugzilla instance to which
+the bug is to be submitted.</para>
+
+</refsect1>
+<refsect1 id='files'><title>FILES</title> 
+<variablelist>
+<varlistentry>
+<term><filename>~/.netrc</filename></term>
+<listitem><para>Must contain an entry in which the machine field is
+the Bugzilla instance URL, the login field is your ID on that host, and the 
+password field is the right password.  The URL in the machine field
+must be enclosed in double quotes.</para></listitem>
+</varlistentry>
+</variablelist>
+
+</refsect1>
+<refsect1 id='author'><title>AUTHORS</title> 
+<para>Christian Reis &lt;kiko@async.com.br&gt;, Eric S. Raymond
+&lt;esr@thyrsus.com&gt;.</para> 
+</refsect1> 
+</refentry>
+