Many people have a body of issues in an existing tracker, BugZilla being a very common issue tracker. I was faced with the task of moving all our issues from a bugzilla instance into my shiny new roundup instance. Bugzilla helpfully provides an option to format query results and bugs in RDF (an XML dialect) so using the wonders of ElementTree it isn't too hard to import issues into a roundup instance, provided you can either figure out how to map the information onto the roundup schema, or modify your roundup schema to match. I took the later course, and then modified my schema over time to work better with roundup.
The following is a simple script to chug through the bugzilla issues and then import them into roundup. I cache the issue downloads so I can experiment without killing the bugzilla instance, so it should be easy enough to tweak the script to your heart's content.
I should add the caveat that I haven't used this script in a while, so it might have bitrotted slightly over time. I'm confident that it should work for most people with only minimal tweaking.
bugzilla_to_roundup.py::
1 #!/usr/bin/env python
2
3 """Bugzilla to roundup
4
5 Converts entries in bugzilla to roundup issues
6 """
7
8 import os
9 from optparse import OptionParser
10 import urllib2
11 import sys
12 import logging
13
14 from elementtree import ElementTree
15 from roundup import instance
16 from roundup.date import Date
17
18 logging.basicConfig()
19 logger = logging.getLogger()
20 logger.setLevel(logging.INFO)
21
22 # Map bugzilla states to ours
23 bug_status_mapping = {
24 "UNCONFIRMED": "open",
25 "NEW": "open",
26 "ASSIGNED": "accepted",
27 "REOPENED": "open", # this our chatting equiv?
28 "RESOLVED": "complete",
29 "VERIFIED": "verified",
30 "CLOSED": "cancelled",
31 }
32
33 # Map severity to ours
34 bug_severity_mapping = {
35 "blocker": "critical",
36 "critical": "critical",
37 "major": "urgent",
38 "normal": "bug",
39 "minor": "bug",
40 "trivial": "bug",
41 "enhancement": "feature",
42 }
43
44 # Map properties, this doesn't cover severity and status
45 bug_property_mapping = {
46 "product": "product",
47 "version": "version",
48 "assigned_to": "assignedto",
49 "reporter": "creator",
50 "creation_ts": "creation",
51 "short_desc": "title"
52 }
53
54 usage="""%prog bugzilla_base_url roundup_instace
55
56 e.g. %prog http://example.com/cgi-bin/bugzilla/ $HOME/Servers/roundup/issues
57 """
58
59 def get_bugzilla_bug_list(url_prefix):
60 """Uses bugzilla to get a list of bug ids.
61
62 Uses the RDF output of bugzilla.
63 """
64 result = []
65
66 url = "%sbuglist.cgi?short_desc_type=allwordssubstr&short_desc=&long_desc_type=allwordssubstr&long_desc=&bug_file_loc_type=allwordssubstr&bug_file_loc=&bug_status=UNCONFIRMED&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&bug_status=RESOLVED&bug_status=VERIFIED&bug_status=CLOSED&emailtype1=substring&email1=&emailtype2=substring&email2=&bugidtype=include&bug_id=&votes=&changedin=&chfieldfrom=&chfieldto=Now&chfieldvalue=&cmdtype=doit&newqueryname=&order=Reuse+same+sort+as+last+time&field0-0-0=noop&type0-0-0=noop&value0-0-0=&format=rdf" % url_prefix
67
68 f = urllib2.urlopen(url)
69 rdf_text = f.read()
70 fp = open("bugzilla.rdf", "w")
71 fp.write(rdf_text)
72 fp.close()
73 tree = ElementTree.parse("bugzilla.rdf")
74 root = tree.getroot()
75
76 for bug in root.findall(".//{http://www.bugzilla.org/rdf#}id"):
77 result.append(bug.text)
78
79 return result
80
81 def download_bugs(url_prefix, bug_ids, output_dir):
82 """Downloads all the bugs given from bugzilla to XML files"""
83 for bug_id in bug_ids:
84 f = urllib2.urlopen("%s/xml.cgi?id=%s" % (url_prefix, bug_id))
85 bug_text = f.read()
86 fp = open(os.path.join(output_dir, "%s.xml" % bug_id), "w")
87 fp.write(bug_text)
88 fp.close()
89
90 class BugzillaBug:
91 def __init__(self, tree):
92 self._tree = tree
93 self._root = self._tree.getroot()
94
95 def __getattr__(self, name):
96 # Special case for all the long_desc's there
97 if name in ["long_desc",]:
98 return self._root.findall("bug/%s" % name)
99 return self._root.find("bug/%s" % name).text
100
101 def __getitem__(self, key):
102 return self.__getattr__(key)
103
104 def keys(self):
105 return [x.tag for x in self._root.find("bug")]
106
107 def parse_bug(bug_filename):
108 """Parses a bug"""
109 logger.debug("Parsing %s" % bug_filename)
110 tree = ElementTree.parse(bug_filename)
111 return BugzillaBug(tree)
112
113 def dump_bugs(bug_ids, cache_dir):
114 """Dumps out info on the given bugs"""
115 for bug_id in bug_ids:
116 bug = parse_bug(os.path.join(cache_dir, "%s.xml" % bug_id))
117 for key in bug.keys():
118 if key == "long_desc":
119 continue
120 print "%s: %s = %s" % (bug_id, key, bug[key])
121 for long_desc in bug.long_desc:
122 print "%s:long_desc %s = %s" % (bug_id, "who", long_desc.find("who").text)
123 print "%s:long_desc %s = %s" % (bug_id, "bug_when", long_desc.find("bug_when").text)
124 thetext = long_desc.find("thetext").text
125 thetext = thetext.replace("\n", "\\n")
126 print "%s:long_desc %s = %s" % (bug_id, "thetext", thetext)
127
128 def convert_bugs(bug_ids, cache_dir, roundup_instance):
129 """Converts the bugzilla dumped XML into roundup bugs"""
130 tracker = instance.open(roundup_instance)
131 # NOTE: Is it worth sorting the bugs so the ids increase in chronological order?
132 for bug_id in bug_ids:
133 filename = os.path.join(cache_dir, "%s.xml" % bug_id)
134 logger.info("Processing bug %s" % bug_id)
135 bz_bug = parse_bug(filename)
136 roundup_bug = {}
137 for bz_prop, roundup_prop in bug_property_mapping.items():
138 roundup_bug[roundup_prop] = bz_bug[bz_prop]
139
140 roundup_bug['status'] = bug_status_mapping[bz_bug.bug_status]
141 roundup_bug['priority'] = bug_severity_mapping[bz_bug.bug_severity]
142
143 roundup_bug['messages'] = []
144 for long_desc in bz_bug.long_desc:
145 message = {}
146 message['creator'] = long_desc.find('who').text
147 message['author'] = long_desc.find('who').text
148 message['creation'] = long_desc.find('bug_when').text
149 message['file'] = long_desc.find('thetext').text
150 roundup_bug['messages'].append(message)
151
152 bug_id = add_bug_to_roundup(roundup_bug, tracker)
153 for message in roundup_bug['messages']:
154 add_message_to_roundup(bug_id, message, tracker)
155
156 # Append the bug's XML as a file
157 xml = open(filename, "r").read()
158 add_file_to_roundup(bug_id, filename, xml, "text/xml", tracker)
159
160 def add_bug_to_roundup(bug, tracker):
161 """Add a roundup bug (dict) to a roundup instance
162
163 :param bug: A dictionary of bug data. Pretty much a plain set of
164 property -> value mappings, with the only exception being messages
165 which maps to a list of message dictionaries.
166
167 :param tracker: The roundup tracker instance, this is obtained via
168 `roundup.instance.open`.
169
170 """
171 try:
172 # Open up the db and get the user, creating if necessary
173 db = tracker.open('admin')
174 username = get_roundup_user(db, bug['creator'])
175 db.commit()
176 db.close()
177
178 db = tracker.open(username)
179
180 # Get the issue's properties
181 product_id = get_roundup_property(db, 'product', bug['product'])
182 version_id = get_roundup_property(db, 'version', bug['version'])
183 status_id = get_roundup_property(db, 'status', bug['status'])
184 priority_id = get_roundup_property(db, 'priority', bug['priority'])
185
186 # Get the users relating to the issue
187 # From bugzilla the username is the email, so can use that as a starting point
188 creator_id = get_roundup_user(db, bug['creator'])
189 assignedto_id = get_roundup_user(db, bug['assignedto'])
190
191 bug_id = db.issue.create(
192 title=bug['title'],
193 product=bug['product'],
194 version=bug['version'],
195 status=bug['status'],
196 priority=bug['priority'],
197 #creator=bug['creator'],
198 assignedto=bug['assignedto'].split("@")[0],
199 #creation=bug['creation'],
200 )
201 db.commit()
202 finally:
203 #Ensure the db is always closed no matter what
204 db.close()
205 logger.info("Roundup db connection closed")
206
207 logger.info("Added issue: %s" % bug_id)
208 return bug_id
209
210 def add_message_to_roundup(bug_id, message, tracker):
211 """Adds the given message to to roundup"""
212 try:
213 # Open up the db and get the user, creating if necessary
214 db = tracker.open('admin')
215 username = get_roundup_user(db, message['creator'])
216 db.commit()
217 db.close()
218
219 # Re-open as the user
220 db = tracker.open(username)
221
222 # Create the author if not there
223 get_roundup_user(db, message['author'])
224
225 # Create the message
226 message_id = db.msg.create(
227 content=message['file'],
228 author=message['author'].split("@")[0],
229 date=Date(message['creation']),
230 #creation=message['creation']
231 )
232
233 # Add the message to the bug
234 bug = db.issue.getnode(bug_id)
235 messages = bug.messages
236 messages.append(message_id)
237 bug.messages = messages # Force a setattr
238 db.commit()
239 finally:
240 #Ensure the db is always closed no matter what
241 db.close()
242 logger.info("Roundup db connection closed")
243
244 logger.info("Added message: %s to issue: %s" % (message_id, bug_id))
245 return message_id
246
247 def add_file_to_roundup(bug_id, filename, content, mimetype, tracker):
248 filename = os.path.basename(filename) # ensure there are no directory parts
249 try:
250 db = tracker.open("admin")
251
252 # Create the file
253 file_id = db.file.create(
254 content=content,
255 name=filename,
256 type=mimetype
257 )
258
259 #Append the file to the bug
260 bug = db.issue.getnode(bug_id)
261 files = bug.files
262 files.append(file_id)
263 bug.files = files
264
265 db.commit()
266 finally:
267 db.close()
268 logger.debug("Closed db connection")
269
270 logger.info("Added file %s to issue %s" % (file_id, bug_id))
271 return file_id
272
273 def get_roundup_property(db, propname, value):
274 """Obtains the id of the given property.
275
276 If it doesn't exist it is created.
277 """
278 klass = db.getclass(propname)
279 try:
280 result = klass.lookup(value)
281 except KeyError:
282 result = klass.create(name=value)
283 return result
284
285 def get_roundup_user(db, email):
286 """Find (or create) a user based on their email address
287
288 This assumes everyone has a username which is also their email
289 address prefix.
290 """
291 username = email.split("@")[0]
292 try:
293 db.user.lookup(username)
294 except KeyError:
295 db.user.create(username=username, address=email)
296 return username
297
298 def main():
299 parser = OptionParser(usage=usage)
300 parser.add_option("", "--download", dest="download", default=False, action="store_true",
301 help="Download bugs only. Downloads the individual bug xml files to dir called 'cache'.")
302 parser.add_option("", "--dump", dest="dump", default=False, action="store_true",
303 help="Dumps out details on the parse entries")
304 options, args = parser.parse_args()
305
306 if len(args) != 2:
307 parser.error("You need to give the bugzilla URL and the roundup instance home dir")
308 bugzilla_url, roundup_instance = args
309
310 bug_ids = get_bugzilla_bug_list(bugzilla_url)
311
312 if options.download:
313 download_bugs(bugzilla_url, bug_ids, "cache")
314 sys.exit(0)
315
316 if options.dump:
317 dump_bugs(bug_ids, "cache")
318 sys.exit(0)
319
320 convert_bugs(bug_ids, "cache", roundup_instance)
321
322
323
324 if __name__ == "__main__":
325 main()
From wiki Sat Feb 9 00:38:56 +1100 2008 From: wiki Date: Sat, 09 Feb 2008 00:38:56 +1100 Subject: get the python code Message-ID: <20080209003856+1100@www.mechanicalcat.net>
the python code is available in a usable fashion via the diff page (http://www.mechanicalcat.net/tech/roundup/wiki/ImportingFromBugzilla/diff)
Tobias