#!/usr/local/bin/python # -*- mode: python -*- """ jb2bz.py - a nonce script to import bugs from JitterBug to Bugzilla Written by Tom Emerson, tree@basistech.com This script is provided in the hopes that it will be useful. No rights reserved. No guarantees expressed or implied. Use at your own risk. May be dangerous if swallowed. If it doesn't work for you, don't blame me. It did what I needed it to do. This code requires a recent version of Andy Dustman's MySQLdb interface, http://sourceforge.net/projects/mysql-python Share and enjoy. """ import email, mimetypes, email.utils import sys, re, glob, os, stat, time import MySQLdb, getopt # mimetypes doesn't include everything we might encounter, yet. if not mimetypes.types_map.has_key('.doc'): mimetypes.types_map['.doc'] = 'application/msword' if not mimetypes.encodings_map.has_key('.bz2'): mimetypes.encodings_map['.bz2'] = "bzip2" bug_status='CONFIRMED' component="default" version="unspecified" product="" # this is required, the rest of these are defaulted as above """ Each bug in JitterBug is stored as a text file named by the bug number. Additions to the bug are indicated by suffixes to this: .followup.* .reply.* .notes The dates on the files represent the respective dates they were created/added. All s and .reply.*s include RFC 822 mail headers. These could include MIME file attachments as well that would need to be extracted. There are other additions to the file names, such as .notify which are ignored. Bugs in JitterBug are organized into directories. At Basis we used the following naming conventions: -bugs Open bugs -requests Open Feature Requests -resolved Bugs/Features marked fixed by engineering, but not verified -verified Resolved defects that have been verified by QA where is either: or - """ def process_notes_file(current, fname): try: new_note = {} notes = open(fname, "r") s = os.fstat(notes.fileno()) new_note['text'] = notes.read() new_note['timestamp'] = time.gmtime(s[stat.ST_MTIME]) notes.close() current['notes'].append(new_note) except IOError: pass def process_reply_file(current, fname): new_note = {} reply = open(fname, "r") msg = email.message_from_file(reply) # Add any attachments that may have been in a followup or reply msgtype = msg.get_content_maintype() if msgtype == "multipart": for part in msg.walk(): new_note = {} if part.get_filename() is None: if part.get_content_type() == "text/plain": new_note['timestamp'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date']))) new_note['text'] = "%s\n%s" % (msg['From'], part.get_payload()) current["notes"].append(new_note) else: maybe_add_attachment(part, current) else: new_note['text'] = "%s\n%s" % (msg['From'], msg.get_payload()) new_note['timestamp'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date']))) current["notes"].append(new_note) def add_notes(current): """Add any notes that have been recorded for the current bug.""" process_notes_file(current, "%d.notes" % current['number']) for f in glob.glob("%d.reply.*" % current['number']): process_reply_file(current, f) for f in glob.glob("%d.followup.*" % current['number']): process_reply_file(current, f) def maybe_add_attachment(submsg, current): """Adds the attachment to the current record""" attachment_filename = submsg.get_filename() if attachment_filename is None: return if (submsg.get_content_type() == 'application/octet-stream'): # try get a more specific content-type for this attachment mtype, encoding = mimetypes.guess_type(attachment_filename) if mtype == None: mtype = submsg.get_content_type() else: mtype = submsg.get_content_type() if mtype == 'application/x-pkcs7-signature': return if mtype == 'application/pkcs7-signature': return if mtype == 'application/pgp-signature': return if mtype == 'message/rfc822': return try: data = submsg.get_payload(decode=True) except: return current['attachments'].append( ( attachment_filename, mtype, data ) ) def process_text_plain(msg, current): current['description'] = msg.get_payload() def process_multi_part(msg, current): for part in msg.walk(): if part.get_filename() is None: process_text_plain(part, current) else: maybe_add_attachment(part, current) def process_jitterbug(filename): current = {} current['number'] = int(filename) current['notes'] = [] current['attachments'] = [] current['description'] = '' current['date-reported'] = () current['short-description'] = '' print "Processing: %d" % current['number'] mfile = open(filename, "r") create_date = os.fstat(mfile.fileno()) msg = email.message_from_file(mfile) current['date-reported'] = time.gmtime(email.utils.mktime_tz(email.utils.parsedate_tz(msg['Date']))) if current['date-reported'] is None: current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME]) if current['date-reported'][0] < 1900: current['date-reported'] = time.gmtime(create_date[stat.ST_MTIME]) if msg.has_key('Subject') is not False: current['short-description'] = msg['Subject'] else: current['short-description'] = "Unknown" msgtype = msg.get_content_maintype() if msgtype == 'text': process_text_plain(msg, current) elif msgtype == "multipart": process_multi_part(msg, current) else: # Huh? This should never happen. print "Unknown content-type: %s" % msgtype sys.exit(1) add_notes(current) # At this point we have processed the message: we have all of the notes and # attachments stored, so it's time to add things to the database. # The schema for JitterBug 2.14 can be found at: # # http://www.trilobyte.net/barnsons/html/dbschema.html # # The following fields need to be provided by the user: # # bug_status # product # version # reporter # component # resolution # change this to the user_id of the Bugzilla user who is blessed with the # imported defects reporter=6 # the resolution will need to be set manually resolution="" db = MySQLdb.connect(db='bugs',user='root',host='localhost',passwd='password') cursor = db.cursor() try: cursor.execute( "INSERT INTO bugs SET " \ "bug_id=%s," \ "priority='---'," \ "bug_severity='normal'," \ "op_sys='All'," \ "bug_status=%s," \ "creation_ts=%s," \ "delta_ts=%s," \ "short_desc=%s," \ "product_id=%s," \ "rep_platform='All'," \ "assigned_to=%s," \ "reporter=%s," \ "version=%s," \ "component_id=%s," \ "resolution=%s", [ current['number'], bug_status, time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]), time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]), current['short-description'], product, reporter, reporter, version, component, resolution] ) # This is the initial long description associated with the bug report cursor.execute( "INSERT INTO longdescs SET " \ "bug_id=%s," \ "who=%s," \ "bug_when=%s," \ "thetext=%s", [ current['number'], reporter, time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]), current['description'] ] ) # Add whatever notes are associated with this defect for n in current['notes']: cursor.execute( "INSERT INTO longdescs SET " \ "bug_id=%s," \ "who=%s," \ "bug_when=%s," \ "thetext=%s", [current['number'], reporter, time.strftime("%Y-%m-%d %H:%M:%S", n['timestamp'][:9]), n['text']]) # add attachments associated with this defect for a in current['attachments']: cursor.execute( "INSERT INTO attachments SET " \ "bug_id=%s, creation_ts=%s, description=%s, mimetype=%s," \ "filename=%s, submitter_id=%s", [ current['number'], time.strftime("%Y-%m-%d %H:%M:%S", current['date-reported'][:9]), a[0], a[1], a[0], reporter ]) cursor.execute( "INSERT INTO attach_data SET " \ "id=LAST_INSERT_ID(), thedata=%s", [ a[2] ]) except MySQLdb.IntegrityError, message: errorcode = message[0] if errorcode == 1062: # duplicate return else: raise cursor.execute("COMMIT") cursor.close() db.close() def usage(): print """Usage: jb2bz.py [OPTIONS] Product Where OPTIONS are one or more of the following: -h This help information. -s STATUS One of UNCONFIRMED, CONFIRMED, IN_PROGRESS, RESOLVED, VERIFIED (default is CONFIRMED) -c COMPONENT The component to attach to each bug as it is important. This should be valid component for the Product. -v VERSION Version to assign to these defects. Product is the Product to assign these defects to. All of the JitterBugs in the current directory are imported, including replies, notes, attachments, and similar noise. """ sys.exit(1) def main(): global bug_status, component, version, product opts, args = getopt.getopt(sys.argv[1:], "hs:c:v:") for o,a in opts: if o == "-s": if a in ('UNCONFIRMED','CONFIRMED','IN_PROGRESS','RESOLVED','VERIFIED'): bug_status = a elif o == '-c': component = a elif o == '-v': version = a elif o == '-h': usage() if len(args) != 1: sys.stderr.write("Must specify the Product.\n") sys.exit(1) product = args[0] for bug in filter(lambda x: re.match(r"\d+$", x), glob.glob("*")): process_jitterbug(bug) if __name__ == "__main__": main()