This patch adds support for save to bugzilla, using the python-bugzilla module. We get the bugzilla URL from product.bugUrl and require the user to already have a valid account with that bugzilla instance. That should cut down on potential abuse. To cut down on the number of possible duplicates, we hash the file name, function name, and line of each frame in the traceback and store that hash in the bug itself. Before filing a new bug, we query for any bugs containing that hash value. If found, we simply add the traceback as a new attachment and put the user on the CC list. If not found, we create a new bug. Either way, the user is encouraged to visit their bug and make a more meaningful comment. --- exception.py | 121 ++++++++++++++++++++++++++++++++++++++++++++++++----- gui.py | 6 ++- text.py | 21 ++++++--- ui/exnSave.glade | 52 +++++++++++++++++++++-- 4 files changed, 176 insertions(+), 24 deletions(-) diff --git a/exception.py b/exception.py index fa87389..b6913a4 100644 --- a/exception.py +++ b/exception.py @@ -49,6 +49,8 @@ class AnacondaExceptionDump: self.value = value self.tb = tb + self.tbFile = None + self._dumpHash = {} # Reverse the order that tracebacks are printed so people will hopefully quit @@ -217,6 +219,12 @@ class AnacondaExceptionDump: return hashlib.sha256(s).hexdigest() + def write(self, anaconda): + self.tbFile = "/tmp/anacdump.txt" + fd = open(self.tbFile, "w") + self.dump(fd, anaconda) + fd.close() + # Save the traceback to a removable storage device, such as a floppy disk # or a usb/firewire drive. If there's no filesystem on the disk/partition, # write a vfat one. @@ -271,8 +279,99 @@ def copyExceptionToDisk(anaconda, device): isys.umount("/tmp/crash") return True -def runSaveDialog(anaconda, longTracebackFile): - saveWin = anaconda.intf.saveExceptionWindow(anaconda, longTracebackFile) +def saveToBugzilla(anaconda, exn, dest): + import bugzilla, xmlrpclib + import product, rpmUtils + + if dest[0].strip() == "" or dest[1].strip() == "" or dest[2].strip() == "": + anaconda.intf.messageWindow(_("Invalid Bug Information"), + _("Please provide a valid username, " + "password, and short bug description.")) + return False + + hash = exn.hash() + + if product.bugUrl.startswith("http://"): + bugUrl = "https://" + product.bugUrl[7:] + elif product.bugUrl.startswith("https://"): + bugUrl = product.bugUrl + else: + anaconda.intf.messageWindow(_("No bugzilla URL"), + _("Your distribution does not provide a " + "bug reporting URL, so you cannot save " + "your exception to a remote bug tracking " + "system.")) + return False + + if not exn.tbFile: + exn.write(anaconda) + + bz = bugzilla.Bugzilla(url = "%s/xmlrpc.cgi" % bugUrl) + + if not bz.login(dest[0], dest[1]): + anaconda.intf.messageWindow(_("Unable To Login"), + _("There was an error logging into %s " + "using the provided username and " + "password.") % product.bugUrl) + return False + + # Are there any existing bugs with this hash value? If so we will just + # add this traceback to the bug report and put the reporter on the CC + # list. Otherwise, we need to create a new bug. + try: + buglist = bz.query({'status_whiteboard': hash}) + except xmlrpclib.ProtocolError, e: + anaconda.intf.messageWindow(_("Unable To File Bug"), + _("Your bug could not be filed due to the " + "following error when communicating with " + "bugzilla:\n\n%s" % str(e))) + return False + + # FIXME: need to handle all kinds of errors here + if len(buglist) == 0: + bug = bz.createbug(product=product.productName, + component="anaconda", + version=product.productVersion, + rep_platform=rpmUtils.arch.getBaseArch(), + bug_severity="medium", + priority="medium", + op_sys="Linux", + bug_file_loc="http://", + short_desc=dest[2], + comment="This bug was filed automatically by anaconda.") + bug.setwhiteboard("anaconda_trace_hash:%s" % hash, which="status") + bz.attachfile(bug.bug_id, exn.tbFile, "Attached traceback automatically from anaconda.", + contenttype="text/plain") + + # Tell the user we created a new bug for them and that they should + # go add a descriptive comment. + anaconda.intf.messageWindow(_("Bug Created"), + _("A new bug has been created with your traceback attached. " + "Please add additional information such as what you were doing " + "when you encountered the bug, screenshots, and whatever else " + "is appropriate to the following bug:\n\n%s/%s") % (bugUrl, bug.bug_id), + type="custom", custom_icon="info", + custom_buttons=[_("_Exit installer")]) + sys.exit(0) + else: + id = buglist[0].bug_id + bz.attachfile(id, exn.tbFile, "Attached traceback automatically from anaconda.", + contenttype="text/plain") + bz._updatecc(id, [dest[0]], "add") + + # Tell the user which bug they've been CC'd on and that they should + # go add a descriptive comment. + anaconda.intf.messageWindow(_("Bug Updated"), + _("A bug with your information already exists. Your account has " + "been added to the CC list and your traceback added as a " + "comment. Please add additional descriptive information to the " + "following bug:\n\n%s/%s") % (bugUrl, id), + type="custom", custom_icon="info", + custom_buttons=[_("_Exit installer")]) + sys.exit(0) + +def runSaveDialog(anaconda, exn): + saveWin = anaconda.intf.saveExceptionWindow(anaconda, exn.tbFile) if not saveWin: anaconda.intf.__del__() os.kill(os.getpid(), signal.SIGKILL) @@ -301,7 +400,7 @@ def runSaveDialog(anaconda, longTracebackFile): elif saveWin.saveToLocal(): dest = saveWin.getDest() try: - shutil.copyfile("/tmp/anacdump.txt", "%s/InstallError.txt" %(dest,)) + shutil.copyfile(exn.tbFile, "%s/InstallError.txt" %(dest,)) anaconda.intf.messageWindow(_("Dump Written"), _("Your system's state has been successfully written to " "the disk. The installer will now exit."), @@ -309,14 +408,15 @@ def runSaveDialog(anaconda, longTracebackFile): custom_buttons=[_("_Exit installer")]) sys.exit(0) except Exception, e: - log.error("Failed to copy anacdump.txt to %s/anacdump.txt: %s" %(dest, e)) + log.error("Failed to copy %s to %s/anacdump.txt: %s" %(exn.tbFile, dest, e)) else: anaconda.intf.messageWindow(_("Dump Not Written"), _("There was a problem writing the system state to the " "disk.")) continue else: - continue + if not saveToBugzilla(anaconda, exn, saveWin.getDest()): + continue elif rc == EXN_CANCEL: break @@ -329,14 +429,11 @@ def handleException(anaconda, (type, value, tb)): # restore original exception handler sys.excepthook = sys.__excepthook__ + # Save the exception file to local storage first. exn = AnacondaExceptionDump(type, value, tb) + exn.write(anaconda) text = str(exn) - # save to local storage first - out = open("/tmp/anacdump.txt", "w") - exn.dump(out, anaconda) - out.close() - # see if /mnt/sysimage is present and put exception there as well if os.access("/mnt/sysimage/root", os.X_OK): try: @@ -352,7 +449,7 @@ def handleException(anaconda, (type, value, tb)): except: pass - mainWin = anaconda.intf.mainExceptionWindow(text, "/tmp/anacdump.txt") + mainWin = anaconda.intf.mainExceptionWindow(text, exn.tbFile) if not mainWin: anaconda.intf.__del__() os.kill(os.getpid(), signal.SIGKILL) @@ -396,4 +493,4 @@ def handleException(anaconda, (type, value, tb)): pdb.post_mortem (tb) os.kill(os.getpid(), signal.SIGKILL) elif rc == EXN_SAVE: - runSaveDialog(anaconda, "/tmp/anacdump.txt") + runSaveDialog(anaconda, exn) diff --git a/gui.py b/gui.py index cfa0960..aebe05e 100755 --- a/gui.py +++ b/gui.py @@ -734,6 +734,7 @@ class SaveExceptionWindow: self.usernameEntry = exnxml.get_widget("usernameEntry") self.passwordEntry = exnxml.get_widget("passwordEntry") + self.bugDesc = exnxml.get_widget("bugDesc") self.diskButton = exnxml.get_widget("diskButton") self.diskCombo = exnxml.get_widget("diskCombo") @@ -747,6 +748,8 @@ class SaveExceptionWindow: self.remoteButton.connect("toggled", self.radio_changed) self.localButton.connect("toggled", self.radio_changed) + self.remoteButton.set_label(self.remoteButton.get_label() % product.bugUrl) + cell = gtk.CellRendererText() self.diskCombo.pack_start(cell, True) self.diskCombo.set_attributes(cell, text=1) @@ -797,7 +800,8 @@ class SaveExceptionWindow: elif self.saveToLocal(): return self.localChooser.get_filename() else: - return map(lambda e: e.get_text(), [self.usernameEntry, self.passwordEntry]) + return map(lambda e: e.get_text(), [self.usernameEntry, self.passwordEntry, + self.bugDesc]) def pop(self): self.window.destroy() diff --git a/text.py b/text.py index 14ef53b..304235b 100644 --- a/text.py +++ b/text.py @@ -29,6 +29,7 @@ import iutil import time import signal import parted +import product import string from language import expandLangs from flags import flags @@ -157,9 +158,11 @@ class SaveExceptionWindow: if self.rg.getSelection() == "disk": self.usernameEntry.setFlags(FLAG_DISABLED, FLAGS_SET) self.passwordEntry.setFlags(FLAG_DISABLED, FLAGS_SET) + self.bugDesc.setFlags(FLAG_DISABLED, FLAGS_SET) else: self.usernameEntry.setFlags(FLAG_DISABLED, FLAGS_RESET) self.passwordEntry.setFlags(FLAG_DISABLED, FLAGS_RESET) + self.bugDesc.setFlags(FLAG_DISABLED, FLAGS_RESET) def getrc(self): if self.rc == TEXT_OK_CHECK: @@ -171,7 +174,8 @@ class SaveExceptionWindow: if self.saveToDisk(): return self.diskList.current() else: - return map(lambda e: e.value(), [self.usernameEntry, self.passwordEntry]) + return map(lambda e: e.value(), [self.usernameEntry, self.passwordEntry, + self.bugDesc]) def pop(self): self.screen.popWindow() @@ -182,7 +186,7 @@ class SaveExceptionWindow: self.rg = RadioGroup() self.diskButton = self.rg.add(_("Save to Disk"), "disk", True) - self.remoteButton = self.rg.add(_("Save to Remote"), "remote", False) + self.remoteButton = self.rg.add(_("Save to Remote (%s)") % product.bugUrl, "remote", False) self.diskButton.setCallback(self._destCb, None) self.remoteButton.setCallback(self._destCb, None) @@ -190,14 +194,17 @@ class SaveExceptionWindow: buttons = ButtonBar(self.screen, [TEXT_OK_BUTTON, TEXT_CANCEL_BUTTON]) self.usernameEntry = Entry(24) self.passwordEntry = Entry(24, password=1) + self.bugDesc = Entry(24) self.diskList = Listbox(height=3, scroll=1) - remoteGrid = Grid(2, 2) - remoteGrid.setField(Label(_("User name")), 0, 2, anchorLeft=1) - remoteGrid.setField(self.usernameEntry, 1, 2) - remoteGrid.setField(Label(_("Password")), 0, 3, anchorLeft=1) - remoteGrid.setField(self.passwordEntry, 1, 3) + remoteGrid = Grid(2, 3) + remoteGrid.setField(Label(_("User name")), 0, 0, anchorLeft=1) + remoteGrid.setField(self.usernameEntry, 1, 0) + remoteGrid.setField(Label(_("Password")), 0, 1, anchorLeft=1) + remoteGrid.setField(self.passwordEntry, 1, 1) + remoteGrid.setField(Label(_("Bug Description")), 0, 2, anchorLeft=1) + remoteGrid.setField(self.bugDesc, 1, 2) toplevel.add(self.diskButton, 0, 0, (0, 0, 0, 1)) toplevel.add(self.diskList, 0, 1, (0, 0, 0, 1)) diff --git a/ui/exnSave.glade b/ui/exnSave.glade index 0bab909..6c22e2f 100644 --- a/ui/exnSave.glade +++ b/ui/exnSave.glade @@ -273,8 +273,8 @@ <child> <widget class="GtkLabel" id="label4"> <property name="visible">True</property> - <property name="label">User name</property> - <property name="use_underline">False</property> + <property name="label">_User name</property> + <property name="use_underline">True</property> <property name="use_markup">False</property> <property name="justify">GTK_JUSTIFY_LEFT</property> <property name="wrap">False</property> @@ -298,8 +298,8 @@ <child> <widget class="GtkLabel" id="label5"> <property name="visible">True</property> - <property name="label">Password</property> - <property name="use_underline">False</property> + <property name="label">_Password</property> + <property name="use_underline">True</property> <property name="use_markup">False</property> <property name="justify">GTK_JUSTIFY_LEFT</property> <property name="wrap">False</property> @@ -319,6 +319,31 @@ <property name="fill">True</property> </packing> </child> + + <child> + <widget class="GtkLabel" id="label7"> + <property name="visible">True</property> + <property name="label" translatable="yes">_Bug description</property> + <property name="use_underline">True</property> + <property name="use_markup">False</property> + <property name="justify">GTK_JUSTIFY_LEFT</property> + <property name="wrap">False</property> + <property name="selectable">False</property> + <property name="xalign">0.5</property> + <property name="yalign">0.5</property> + <property name="xpad">0</property> + <property name="ypad">0</property> + <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property> + <property name="width_chars">-1</property> + <property name="single_line_mode">False</property> + <property name="angle">0</property> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> </widget> <packing> <property name="padding">0</property> @@ -371,6 +396,25 @@ <property name="fill">True</property> </packing> </child> + + <child> + <widget class="GtkEntry" id="bugDesc"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="editable">True</property> + <property name="visibility">True</property> + <property name="max_length">0</property> + <property name="text" translatable="yes"></property> + <property name="has_frame">True</property> + <property name="invisible_char">â?¢</property> + <property name="activates_default">False</property> + </widget> + <packing> + <property name="padding">0</property> + <property name="expand">False</property> + <property name="fill">False</property> + </packing> + </child> </widget> <packing> <property name="padding">0</property> -- 1.5.4.5 _______________________________________________ Anaconda-devel-list mailing list Anaconda-devel-list@xxxxxxxxxx https://www.redhat.com/mailman/listinfo/anaconda-devel-list