diff --git a/makefile.am b/makefile.am index ddde8ea..c73e169 100644 --- a/makefile.am +++ b/makefile.am @@ -196,7 +196,7 @@ python_bugzilla_tar_dir = python-bugzilla-$(python_bugzilla_ver) python_bugzilla_tar = $(python_bugzilla_tar_dir).tar.bz2 python_bugzilla_srcdir = $(python_bugzilla_tar_dir)/bugzilla -python_bugzilla_tgtdir = python/report/plugins/RHEL-bugzilla/bugzillaCopy +python_bugzilla_tgtdir = python/report1/plugins/RHEL-bugzilla/bugzillaCopy README: $(python_bugzilla_tgtdir)/VERSION diff --git a/po/POTFILES.in b/po/POTFILES.in index 3adfa06..d683feb 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,8 +1,8 @@ # List of source files which contain translatable strings. -python/bin/report -python/report/__init__.py -python/report/plugins/scp/__init__.py -python/report/plugins/strata/__init__.py -python/report/plugins/localsave/__init__.py -python/report/plugins/ftp/__init__.py -python/report/plugins/bugzilla/__init__.py +python/bin/report1 +python/report1/__init__.py +python/report1/plugins/scp/__init__.py +python/report1/plugins/strata/__init__.py +python/report1/plugins/localsave/__init__.py +python/report1/plugins/ftp/__init__.py +python/report1/plugins/bugzilla/__init__.py diff --git a/python/Makefile.am b/python/Makefile.am index 06c428e..a5793d4 100644 --- a/python/Makefile.am +++ b/python/Makefile.am @@ -1,29 +1,29 @@ nobase_dist_pyexec_DATA = \ -report/__init__.py report/accountmanager.py \ -report/release_information.py \ -report/plugins/__init__.py \ -report/plugins/strata/__init__.py \ -report/plugins/strata/strata.py \ -report/plugins/bugzilla/__init__.py \ -report/plugins/bugzilla/filer.py \ -report/plugins/RHEL-bugzilla/__init__.py \ -report/plugins/RHEL-bugzilla/filer.py \ -$(wildcard report/plugins/RHEL-bugzilla/bugzillaCopy/*.py) \ -report/io/__init__.py \ -report/io/GTKIO.py \ -report/io/TextIO.py \ -report/io/NewtIO.py \ -$(wildcard report/plugins/ftp/*.py) \ -report/plugins/localsave/__init__.py \ -report/plugins/scp/__init__.py +report1/__init__.py report1/accountmanager.py \ +report1/release_information.py \ +report1/plugins/__init__.py \ +report1/plugins/strata/__init__.py \ +report1/plugins/strata/strata.py \ +report1/plugins/bugzilla/__init__.py \ +report1/plugins/bugzilla/filer.py \ +report1/plugins/RHEL-bugzilla/__init__.py \ +report1/plugins/RHEL-bugzilla/filer.py \ +$(wildcard report1/plugins/RHEL-bugzilla/bugzillaCopy/*.py) \ +report1/io/__init__.py \ +report1/io/GTKIO.py \ +report1/io/TextIO.py \ +report1/io/NewtIO.py \ +$(wildcard report1/plugins/ftp/*.py) \ +report1/plugins/localsave/__init__.py \ +report1/plugins/scp/__init__.py -bin_SCRIPTS = bin/report +bin_SCRIPTS = bin/report1 EXTRA_DIST = $(bin_SCRIPTS) gettext.h -dist_man_MANS = bin/report.1 +dist_man_MANS = bin/report1.1 -nobase_dist_noinst_DATA = report/plugins/RHEL-bugzilla/bugzillaCopy/VERSION +nobase_dist_noinst_DATA = report1/plugins/RHEL-bugzilla/bugzillaCopy/VERSION LDADD= $(LIBINTL) diff --git a/python/bin/report b/python/bin/report deleted file mode 100755 index b73e039..0000000 --- a/python/bin/report +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/python -""" - The main entry point to the Report library. - Copyright (C) 2009 Red Hat, Inc - - Author(s): Gavin Romig-Koch - Adam Stokes - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import sys -import os -from subprocess import Popen, PIPE -import locale -import report -import report.io.TextIO -import report.accountmanager -from optparse import OptionParser - -import gettext -_ = lambda x: gettext.ldgettext("report", x) - -_FoundGTKIO = False -try: - import report.io.GTKIO - _FoundGTKIO = True -except: - pass - -_FoundNewtIO = False -try: - import report.io.NewtIO - _FoundNewtIO = True -except: - pass - -Username_account_name = "bugzilla.redhat.com" -Username_config_filename = None -try: - Username_config_filename = os.environ['HOME'] + "/.report_username_for_" + Username_account_name -except: - pass - -_description="""\ -report will deliver FILE to a TARGET. A TARGET can be specified on -the command line, in the [main] section of the report configuration -files, or it will be queried. Each TARGET is associated with a plugin -which actually delivers the FILE. Additional parameters needed by the -plugin are plugin specific, and may be specified on the command line, -in TARGET specific configuration sections, or they will be queried. -""" - -def parse_options(options): - parser = OptionParser(usage="report [opts] FILE", - description=_description) - parser.add_option('--target', dest='target', - help='Select target', default=None) - parser.add_option('--ticket', dest='ticket', - help='Ticket to associate FILE with', default=None) - parser.add_option('--host', dest='host', - help='Define a host for plugin', default=None) - parser.add_option('--path', dest='path', - help='Define path for plugin', default=None) - - if _FoundGTKIO: - gtk_option_help = 'Use GTK for I/O' - else: - gtk_option_help = '(disabled) Use GTK for I/O' - - parser.add_option('--gtk', dest='gtkio', action='store_true', - help=gtk_option_help) - - if _FoundNewtIO: - parser.add_option('--newt', dest='newtio', action='store_true') - - cmdopts, cmdargs = parser.parse_args(options) - if len(cmdargs) < 1: - raise SystemExit(_('Needs a filename.')) - elif len(cmdargs) > 1: - raise SystemExit(_('Please specify only 1 filename.')) - else: - cmdopts.filename = os.path.abspath(cmdargs[0]) - try: - file(cmdopts.filename) - except IOError as error: - raise SystemExit((_("Error accessing '%s': ") % (cmdargs[0],)) - + str(error)) - - if not os.path.exists(cmdopts.filename): - raise SystemExit(_('File %s does not exist.') % (cmdopts.filename,)) - - if cmdopts.gtkio and not _FoundGTKIO: - raise SystemExit(_('--gtk option specified, but report.io.GTKIO package not found.')) - - return (cmdopts, cmdargs) - -if __name__=="__main__": - try: - accounts = None - # pull in locale info from environment variables - locale.setlocale(locale.LC_ALL,'') - - # parse cmdline options - opts, args = parse_options(sys.argv[1:]) - - if opts.gtkio: - if Username_config_filename \ - and os.path.exists(Username_config_filename): - - # only create 'accounts' if config file exists - accounts = report.accountmanager.AccountManager() - - f = open(Username_config_filename) - username = f.read().strip() - f.close() - - # only add account if username is not empty - if username: - accounts.addAccount(Username_account_name, username) - - io = report.io.GTKIO.GTKIO(accounts) - - else: - io = report.io.GTKIO.GTKIO() - - elif _FoundNewtIO and opts.newtio: - io = report.io.NewtIO.NewtIO() - - else: - io = report.io.TextIO.TextIO() - - if report.isSignatureFile(opts.filename): - signature = report.createSignatureFromFile(opts.filename, io) - - else: - p = Popen(["file","-L","-b", opts.filename], stdout=PIPE,stderr=PIPE) - out, err = p.communicate() - isBinary = True - if 'text' in out: - isBinary = False - - signature = report.createSimpleFileSignature(opts.filename, isBinary) - - if not signature: - exit(128) - - # convert config object into dict - optsDict = {} - for k,v in vars(opts).iteritems(): - if v and k in ('target','ticket','host','path'): - optsDict[k] = v - - app = report.report(signature, io, **optsDict) - - if Username_config_filename and accounts and Username_account_name: - remember_account_name = None - if accounts and accounts.hasAccount(Username_account_name): - accountInfo = accounts.lookupAccount(Username_account_name) - if accountInfo.remember_me: - remember_account_name = accountInfo.username - - - if remember_account_name != None: - f = open(Username_config_filename,"w") - f.write(accountInfo.username) - f.close() - else: - Popen(["rm", "-rf", Username_config_filename]) - - exit(0) - - except KeyboardInterrupt: - exit(130) - - -# vim:ts=4 sw=4 et diff --git a/python/bin/report.1 b/python/bin/report.1 deleted file mode 100644 index 08d48a4..0000000 --- a/python/bin/report.1 +++ /dev/null @@ -1,61 +0,0 @@ -.TH "report" "1" "" "REPORT" "" -.SH "NAME" -report \- Report library frontend -.SH "SYNOPSIS" -\fBreport\fR [options] \fBfile\fR -.SH "DESCRIPTION" -.LP -\fBreport\fR will deliver \fBfile\fR to a \fBtarget\fR. A -\fBtarget\fR can be specified on the command line, in the [main] -section of the \fBreport\fR configuration files, or it will be -queried. Each \fBtarget\fR is associated with a plugin which actually -delivers the \fBfile\fR. Additional parameters needed by the plugin -are plugin specific, and may be specified on the command line, in -\fBtarget\fR specific configuration sections, or they will be queried. - -.LP -The \fBfile\fR can be a specifically formatted problem -report or an arbitrary file. A 'target' can be ticket/case/bug tracking -systems, in which case \fBreport\fR can create new tickets or attach to -existing ones; or a 'target' can be a simple directory on local or -remote file systems. - -.SH "OPTIONS" -.IP "\fB--target\fR" -Specify the target. This must be one of the targets configured in -/etc/report.d/*.conf. -.IP "\fB--ticket\fR" -Specify a ticket or case or bug to the plugin specified by the target. This -option is only valid for plugins that deal in cases, tickets or -bugs; it is ignored by other plugins. If the target config file also -defines a 'ticket' configuration, the command line overrides this. -.IP "\fB--host\fR" -Specify a host or case or bug to the plugin specified by the target. This -option is only valid for plugins that deal in hosts; it is ignored by other -plugins. If the target config file also defines a 'host' configuration, -the command line overrides this. -.IP "\fB--path\fR" -Specify a path or case or bug to the plugin specified by the target. This -option is only valid for plugins that deal in paths; it is ignored by other -plugins. If the target config file also defines a 'path' configuration, -the command line overrides this. -.IP "\fB--gtk\fR" -Use GTK dialog boxes for I/O from the Report library. -.IP "\fB-h\fR" -.IP "\fB--help\fR" -Command line help. - - -.SH "FILES" -.nf -/etc/report.conf -/etc/report.d/ -.fi - -.SH "SEE ALSO" -.nf -.I report.conf (5) -.fi - -.SH "BUGS" -File a ticket within http://bugzilla.redhat.com/ diff --git a/python/bin/report1 b/python/bin/report1 index e69de29..7d52f5f 100755 --- a/python/bin/report1 +++ b/python/bin/report1 @@ -0,0 +1,189 @@ +#!/usr/bin/python +""" + The main entry point to the Report library. + Copyright (C) 2009 Red Hat, Inc + + Author(s): Gavin Romig-Koch + Adam Stokes + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import sys +import os +from subprocess import Popen, PIPE +import locale +import report1 +import report1.io.TextIO +import report1.accountmanager +from optparse import OptionParser + +import gettext +_ = lambda x: gettext.ldgettext("report", x) + +_FoundGTKIO = False +try: + import report1.io.GTKIO + _FoundGTKIO = True +except: + pass + +_FoundNewtIO = False +try: + import report1.io.NewtIO + _FoundNewtIO = True +except: + pass + +Username_account_name = "bugzilla.redhat.com" +Username_config_filename = None +try: + Username_config_filename = os.environ['HOME'] + "/.report_username_for_" + Username_account_name +except: + pass + +_description="""\ +report will deliver FILE to a TARGET. A TARGET can be specified on +the command line, in the [main] section of the report configuration +files, or it will be queried. Each TARGET is associated with a plugin +which actually delivers the FILE. Additional parameters needed by the +plugin are plugin specific, and may be specified on the command line, +in TARGET specific configuration sections, or they will be queried. +""" + +def parse_options(options): + parser = OptionParser(usage="%s [opts] FILE" % sys.argv[0], + description=_description) + parser.add_option('--target', dest='target', + help='Select target', default=None) + parser.add_option('--ticket', dest='ticket', + help='Ticket to associate FILE with', default=None) + parser.add_option('--host', dest='host', + help='Define a host for plugin', default=None) + parser.add_option('--path', dest='path', + help='Define path for plugin', default=None) + + if _FoundGTKIO: + gtk_option_help = 'Use GTK for I/O' + else: + gtk_option_help = '(disabled) Use GTK for I/O' + + parser.add_option('--gtk', dest='gtkio', action='store_true', + help=gtk_option_help) + + if _FoundNewtIO: + parser.add_option('--newt', dest='newtio', action='store_true') + + cmdopts, cmdargs = parser.parse_args(options) + if len(cmdargs) < 1: + raise SystemExit(_('Needs a filename.')) + elif len(cmdargs) > 1: + raise SystemExit(_('Please specify only 1 filename.')) + else: + cmdopts.filename = os.path.abspath(cmdargs[0]) + try: + file(cmdopts.filename) + except IOError as error: + raise SystemExit((_("Error accessing '%s': ") % (cmdargs[0],)) + + str(error)) + + if not os.path.exists(cmdopts.filename): + raise SystemExit(_('File %s does not exist.') % (cmdopts.filename,)) + + if cmdopts.gtkio and not _FoundGTKIO: + raise SystemExit(_('--gtk option specified, but report1.io.GTKIO package not found.')) + + return (cmdopts, cmdargs) + +if __name__=="__main__": + try: + accounts = None + # pull in locale info from environment variables + locale.setlocale(locale.LC_ALL,'') + + # parse cmdline options + opts, args = parse_options(sys.argv[1:]) + + if opts.gtkio: + if Username_config_filename \ + and os.path.exists(Username_config_filename): + + # only create 'accounts' if config file exists + accounts = report1.accountmanager.AccountManager() + + f = open(Username_config_filename) + username = f.read().strip() + f.close() + + # only add account if username is not empty + if username: + accounts.addAccount(Username_account_name, username) + + io = report1.io.GTKIO.GTKIO(accounts) + + else: + io = report1.io.GTKIO.GTKIO() + + elif _FoundNewtIO and opts.newtio: + io = report1.io.NewtIO.NewtIO() + + else: + io = report1.io.TextIO.TextIO() + + if report1.isSignatureFile(opts.filename): + signature = report1.createSignatureFromFile(opts.filename, io) + + else: + p = Popen(["file","-L","-b", opts.filename], stdout=PIPE,stderr=PIPE) + out, err = p.communicate() + isBinary = True + if 'text' in out: + isBinary = False + + signature = report1.createSimpleFileSignature(opts.filename, isBinary) + + if not signature: + exit(128) + + # convert config object into dict + optsDict = {} + for k,v in vars(opts).iteritems(): + if v and k in ('target','ticket','host','path'): + optsDict[k] = v + + app = report1.report(signature, io, **optsDict) + + if Username_config_filename and accounts and Username_account_name: + remember_account_name = None + if accounts and accounts.hasAccount(Username_account_name): + accountInfo = accounts.lookupAccount(Username_account_name) + if accountInfo.remember_me: + remember_account_name = accountInfo.username + + + if remember_account_name != None: + f = open(Username_config_filename,"w") + f.write(accountInfo.username) + f.close() + else: + Popen(["rm", "-rf", Username_config_filename]) + + exit(0) + + except KeyboardInterrupt: + exit(130) + + +# vim:ts=4 sw=4 et diff --git a/python/bin/report1.1 b/python/bin/report1.1 index e69de29..3411c5b 100644 --- a/python/bin/report1.1 +++ b/python/bin/report1.1 @@ -0,0 +1,61 @@ +.TH "report1" "1" "" "REPORT" "" +.SH "NAME" +report1 \- Report library frontend +.SH "SYNOPSIS" +\fBreport1\fR [options] \fBfile\fR +.SH "DESCRIPTION" +.LP +\fBreport1\fR will deliver \fBfile\fR to a \fBtarget\fR. A +\fBtarget\fR can be specified on the command line, in the [main] +section of the \fBreport1\fR configuration files, or it will be +queried. Each \fBtarget\fR is associated with a plugin which actually +delivers the \fBfile\fR. Additional parameters needed by the plugin +are plugin specific, and may be specified on the command line, in +\fBtarget\fR specific configuration sections, or they will be queried. + +.LP +The \fBfile\fR can be a specifically formatted problem +report or an arbitrary file. A 'target' can be ticket/case/bug tracking +systems, in which case \fBreport1\fR can create new tickets or attach to +existing ones; or a 'target' can be a simple directory on local or +remote file systems. + +.SH "OPTIONS" +.IP "\fB--target\fR" +Specify the target. This must be one of the targets configured in +/etc/report.d/*.conf. +.IP "\fB--ticket\fR" +Specify a ticket or case or bug to the plugin specified by the target. This +option is only valid for plugins that deal in cases, tickets or +bugs; it is ignored by other plugins. If the target config file also +defines a 'ticket' configuration, the command line overrides this. +.IP "\fB--host\fR" +Specify a host or case or bug to the plugin specified by the target. This +option is only valid for plugins that deal in hosts; it is ignored by other +plugins. If the target config file also defines a 'host' configuration, +the command line overrides this. +.IP "\fB--path\fR" +Specify a path or case or bug to the plugin specified by the target. This +option is only valid for plugins that deal in paths; it is ignored by other +plugins. If the target config file also defines a 'path' configuration, +the command line overrides this. +.IP "\fB--gtk\fR" +Use GTK dialog boxes for I/O from the Report library. +.IP "\fB-h\fR" +.IP "\fB--help\fR" +Command line help. + + +.SH "FILES" +.nf +/etc/report.conf +/etc/report.d/ +.fi + +.SH "SEE ALSO" +.nf +.I report.conf (5) +.fi + +.SH "BUGS" +File a ticket within http://bugzilla.redhat.com/ diff --git a/python/report/BugzillaReporter.py b/python/report/BugzillaReporter.py deleted file mode 100644 index 1a800e9..0000000 --- a/python/report/BugzillaReporter.py +++ /dev/null @@ -1,54 +0,0 @@ -""" - Copyright (C) 2009 Red Hat, Inc - - Author: Gavin Romig-Koch - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - - -# -# This is experimental code that I'm keeping just for reference -# purposes. -# -# - - - -from filer import sendToBugzilla - -class HashedBugzillaReporter: - def __init__(self, actualURL, displayURL, fromSignature = defaultFromSignature, alternateFiler = None): - self.actualURL = actualURL - self.displayURL = displayURL - self.fromSignature = fromSignature - self.alternateFiler = alternateFiler - - def report(self, signature, parameters, io): - hashedReport = self.fromSignature( signature ) - - sendToBugzilla( hashedReport['component'], - hashedReport['hashmarkername'], - hashedReport['hashvalue'], - hashedReport['summary'], - hashedReport['firstComment'], - hashedReport['fileName'], - hashedReport['fileDescription'], - parameters['username'], - parameters['password'], - io, self.alternateFiler) - - - diff --git a/python/report/__init__.py b/python/report/__init__.py deleted file mode 100644 index 8e989ed..0000000 --- a/python/report/__init__.py +++ /dev/null @@ -1,711 +0,0 @@ -""" - The main entry point to the Report library. - Copyright (C) 2009 Red Hat, Inc - - Author(s): Gavin Romig-Koch - Adam Stokes - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import sys -import os -import os.path -import glob -import imputil -import tempfile -import tarfile -import exceptions -import re -import ConfigParser - -import xml.etree.ElementTree as etree - -from optparse import OptionParser -from report import io as iomodule -from report.io import DisplayFailMessage -from report.io import DisplaySuccessMessage - -from report import release_information - -import gettext -gettext_dir = "/usr/share/locale" -gettext_app = "report" -gettext.bindtextdomain(gettext_app, gettext_dir) - -def _report(msg): - return gettext.dgettext(gettext_app, msg) - -_ = lambda x: _report(x) - -# -# A Signature, for the purposes of this library, is a mapping of names -# to values (in Python terms a Dictionary). For maximum portablility, -# names must be ASCII alpha-numeric characters. Values should be -# types that conform to the SignatureValue api below. -# - -class SignatureValue: - # roughly an arbitrary value that can be collected, stored, and - # shipped over the wire. - # - # a SignatureValue can be any type that conforms to this protocol - # - # asString() - return a string representation of the data - # asFile() - return a file representation of the data - # asFileName() - return the name of a file (on the local file - # system) that contains the data, can be a temporary - # file - # - # isBinary - False if the data is a UTF-8 (or compatible) character stream, - # True if the data is not character data. - # isFile - was this created as a file - # fileName - the data has a system independent name - # this is generally _not_ where the data is actually - # stored. If you need access to the data use one of the - # asXXX() functions to get the data in the form you - # need it. - # - pass - -class StringSignatureValue: - def __init__(self, data, isBinary = False): - self._data = data - self._fileLocation = None - - self.isBinary = isBinary - self.isFile = False - - def asString(self): - return self._data - - def asFile(self): - return file(self.asFileName()) - - def asFileName(self): - if not self._fileLocation: - if self.isBinary: - mymode = 'w+b' - else: - mymode = 'w+' - tmp = tempfile.NamedTemporaryFile(mode=mymode,prefix="report-", - delete=False) - tmp.write(self._data) - self._fileLocation = tmp.name - tmp.close() - return self._fileLocation - - def __del__(self): - if self._fileLocation: - os.remove(self._fileLocation) - self._fileLocation = None - -class NamedFileSignatureValue: - def __init__(self, fileLocation, isBinary, fileName=None): - - # if the file can't be read for some reason (permissions, - # non-existance, etc.) better to notice that now, and - # throw an exception now - this will accomplish this - file(fileLocation).read(1) - - self._fileLocation = fileLocation - - self.isBinary = isBinary - self.isFile = True - if fileName != None: - self.fileName = fileName - else: - self.fileName = fileLocation - - def asString(self): - return file(self._fileLocation).read() - - def asFile(self): - return file(self._fileLocation) - - def asFileName(self): - return self._fileLocation - -class FileSignatureValue: - def __init__(self, afile, isBinary, fileName=None): - self._afile = afile - - self.isBinary = isBinary - self.isFile = True - self._fileLocation = None - if fileName != None: - self.fileName = fileName - else: - self.fileName = afile.name - - def asString(self): - if self._fileLocation: - return file(self._fileLocation).read() - else: - return self._afile.read() - - def asFile(self): - if self._fileLocation: - return file(self._fileLocation) - else: - return self._afile - - def asFileName(self): - if not self._fileLocation: - if self.isBinary: - mymode = 'w+b' - else: - mymode = 'w+' - tmp = tempfile.NamedTemporaryFile(mode=mymode,prefix="report-", - delete=False) - tmp.write(self._afile.read()) - self._fileLocation = tmp.name - tmp.close() - - self._afile.close() - self._afile = None - - return self._fileLocation - - def __del__(self): - if self._fileLocation: - os.remove(self._fileLocation) - self._fileLocation = None - -def addReleaseInformation(signature): - if not signature: - signature = {} - - if 'product' not in signature: - product = release_information.getProduct() - if product: - signature['product'] = StringSignatureValue(product) - - if 'version' not in signature: - version = release_information.getVersion() - if version: - signature['version'] = StringSignatureValue(version) - - return signature - -def createAlertSignature(component, hashmarkername, hashvalue, summary, alertSignature): - return addReleaseInformation( - { "component" : StringSignatureValue(component), - "hashmarkername" : StringSignatureValue(hashmarkername), - "localhash" : StringSignatureValue(hashvalue), - "summary" : StringSignatureValue(summary), - "description" : StringSignatureValue(alertSignature) } - ) - -def createPythonUnhandledExceptionSignature(component, hashmarkername, hashvalue, summary, description, exnFileName): - return addReleaseInformation( - { "component" : StringSignatureValue(component), - "hashmarkername" : StringSignatureValue(hashmarkername), - "localhash" : StringSignatureValue(hashvalue), - "summary" : StringSignatureValue(summary), - "description" : StringSignatureValue(description), - "pythonUnhandledException" : NamedFileSignatureValue(exnFileName,False) } - ) - -def createSimpleFileSignature(exnFileName, isBinary=True): - return addReleaseInformation( - { "simpleFile" : NamedFileSignatureValue(exnFileName, isBinary) } - ) - -def open_signature_file( filename, io, skipErrorMessage = False ): - try: - tar_file = None - if tarfile.is_tarfile( filename ): - tar_file = tarfile.open(filename, mode='r:*') - try: - xml_file = tar_file.extractfile("content.xml") - except KeyError: - if not skipErrorMessage: - DisplayFailMessage(io, (_("Signature File Format Error"), - _("file %s is a tarfile that does not contain a member " \ - "named 'context.xml'" % (filename,)))) - return False - - else: - xml_file = file(filename) - - except Exception,e: - if not skipErrorMessage: - DisplayFailMessage(io, _("Signature File Format Error"), - _("Failed to open file %(filename)s: %(error)s" % - {'filename':filename, 'error':e})) - return False - - - if not xml_file: - if not skipErrorMessage: - DisplayFailMessage(io, _("Signature File Format Error"), - _("Failed to open file %s" % (filename,))) - return xml_file - - try: - signature_tree = etree.parse( xml_file ) - - except Exception,e: - if not skipErrorMessage: - DisplayFailMessage(io, _("Signature File Format Error"), - _("Error while parseing: %(filename)s: %(error)s" % - {'filename':filename, 'error':e})) - return False - - if not signature_tree: - if not skipErrorMessage: - DisplayFailMessage(io, _("Signature File Format Error"), - _("Could not parse XML file %s" % (filename,))) - return signature_tree - - signature_root = signature_tree.getroot() - if signature_root.tag != "report" and not re.match(r"\{.*\}report", signature_root.tag): - if not skipErrorMessage: - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %(filename)s has document tag that is " \ - "not valid: %(signature)s" % - {'filename':filename,'signature':signature_root.tag})) - return False - - return (signature_root, tar_file) - -def isSignatureFile( filename ): - file_pair = open_signature_file( filename, None, skipErrorMessage = True ) - if not file_pair: - return file_pair - else: - return True - -def createSignatureFromFile( filename, io ): - - file_pair = open_signature_file( filename, io ) - if not file_pair: - return file_pair - - (signature_root, tar_file) = file_pair - - signature = {} - - for each in signature_root: - if each.tag != "binding" and not re.match(r"\{.*\}binding", each.tag): - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %(filename)s has document element that " \ - "has children with an invalid tag: %(tag)s" % - {'filename':filename,'tag':each.tag})) - return True - - if "name" in each.attrib: - name = each.attrib["name"] - else: - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %s has binding element that has no 'name' attribute" % (filename,))) - return False - - isBinary = False - if "type" in each.attrib: - if each.attrib["type"] == "binary": - isBinary = True - - fileName = None - if "fileName" in each.attrib: - if each.attrib["fileName"] != "": - fileName = each.attrib["fileName"] - - if "href" in each.attrib: - if not tar_file: - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %s has a binding with an 'href' but no content" % (filename,))) - return False - - try: - member_name = each.attrib['href'] - afile = tar_file.extractfile(member_name) - except KeyError: - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %(filename)s is a tarfile that does not contain a " \ - "member named '%(member)s'" % - {'filename':filename,'member':member_name})) - return False - - if not afile: - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %(filename)s is a tarfile that does not contain a " \ - "member named '%(member)s'" % - {'filename':filename,'member':member_name})) - return False - - - signature[name] = FileSignatureValue(afile, isBinary, fileName) - - else: - if "value" in each.attrib: - if each.text: - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %(filename)s has a binding, %(binding)s, " \ - "that has both a 'value' attribute, and a text child" % - {'filename':filename,'binding':name})) - return False - else: - value = each.attrib["value"] - else: - if each.text: - value = each.text - else: - DisplayFailMessage(io, _("Signature File Format Error"), - _("file %(filename)s has a binding, %(binding)s, " \ - "that has neither a 'value' attribute, or a text child" % - {'filename':filename,'binding':name})) - return False - - signature[name] = StringSignatureValue(value, isBinary) - - return signature - - -def buildChoices(signature, io, config, rptopts): - """ builds an array of choices """ - choices = [] - priorities = [] - choice = None - - (modulefile, modulepath, moduletype) = imputil.imp.find_module("plugins",sys.modules[__name__].__path__) - try: - alternatives = imputil.imp.load_module("report.plugins", modulefile, modulepath, moduletype) - finally: - if modulefile: - modulefile.close() - - optionsDictStart = {} - - config_sections = config.sections() - if "main" in config_sections: - for eachOption in config.options("main"): - optionsDictStart[eachOption] = config.get("main",eachOption) - config_sections.remove("main") - - for eachSection in config_sections: - optionsDict = optionsDictStart.copy() - for eachOption in config.options(eachSection): - optionsDict[eachOption] = config.get(eachSection,eachOption) - - module = None - if "plugin" in optionsDict: - moduleName = optionsDict["plugin"] - else: - moduleName = eachSection - - try: - (modulefile, modulepath, moduletype) = imputil.imp.find_module(moduleName,alternatives.__path__) - module = imputil.imp.load_module("report.plugins." + moduleName, modulefile, modulepath, moduletype) - - except ImportError as error: - if 'target' not in optionsDict or \ - optionsDict['target'] == eachSection: - DisplayFailMessage(io, _("Could Not Load Plugin"), - (_("The target '%(target)s' requires the plugin '%(plugin)s' which can't be loaded: ") % \ - {'target':eachSection, - 'plugin':moduleName}) + \ - str(error) + "\n" + \ - _("This target will be ignored.")) - - finally: - if modulefile: - modulefile.close() - - if module: - for k,v in rptopts.iteritems(): - optionsDict[k] = v - - if 'target' not in optionsDict: - this_choice = iomodule.ChoiceValue( \ - module.labelFunction(eachSection), - module.descriptionFunction(optionsDict), - (lambda module, optionsDict: - lambda signature, io : - module.report(signature, io, optionsDict))(module, optionsDict)) - if 'priority' in optionsDict: - try: - this_priority = int(optionsDict['priority']) - except ValueError: - this_priority = None - else: - this_priority = None - - if this_priority is None: - choices.append(this_choice) - priorities.append(this_priority) - - else: - for index in range(len(priorities)): - if priorities[index] is None \ - or this_priority < priorities[index]: - break; - else: - index = len(priorities) - - choices.insert(index, this_choice) - priorities.insert(index, this_priority) - - elif optionsDict['target'] == module.labelFunction(eachSection) : - return (lambda module, optionsDict: - lambda signature, io : - module.report(signature, io, optionsDict))(module, optionsDict) - - - # if we haven't loaded any choices from the config files, - # assume they are not readable, load all plugins as choices - if len(choices) == 0: - - # from the 'alternatives' directory, get the list of unique (set) - # basenames with the extension stripped off - moduleNames = set(map( - lambda x: os.path.splitext(os.path.basename(x))[0], - glob.glob(os.path.join(alternatives.__path__[0],"*")))) - - for moduleName in moduleNames: - if moduleName == "__init__": - continue - - (modulefile, modulepath, moduletype) = \ - imputil.imp.find_module(moduleName,alternatives.__path__) - try: - module = imputil.imp.load_module( - "report.plugins." + moduleName, modulefile, - modulepath, moduletype) - finally: - if modulefile: - modulefile.close() - - optionsDict = { 'plugin' : moduleName } - for k,v in rptopts.iteritems(): - optionsDict[k] = v - - if 'target' not in optionsDict: - choices.append( \ - iomodule.ChoiceValue( \ - moduleName, - module.descriptionFunction(optionsDict), - (lambda module, optionsDict: lambda signature, io : module.report(signature, io, optionsDict))(module, optionsDict))) - - elif optionsDict['target'] == moduleName: - return (lambda module, optionsDict: lambda signature, io : module.report(signature, io, optionsDict))(module, optionsDict) - - - if 'target' in rptopts: - DisplayFailMessage(io, _("No Such Plugin"), - _("No plugin matching the requested: %s.") % rptopts['target']) - return False - - if len(choices) >= 1: - choice = io.queryChoice(_("Where do you want to send this report:"), choices) - return choice - - else: - DisplayFailMessage(io, _("No Plugins"), - _("No usable plugins.")) - return False - -def report(signature, io, **rptopts): - if not io: - DisplayFailMessage(None, _("No IO specified."), - _("Cannot determine IO.")) - return False - - config = ConfigParser.RawConfigParser() - config.optionxform = str - - # Just continue, if we can't read the config files - try: - config.read("/etc/report.conf") - config.read(glob.glob("/etc/report.d/*.conf")) - except: - pass - - retval = False - while (retval == False): - choice = buildChoices(signature, io, config, rptopts) - if not choice: - return choice - else: - retval = choice(signature, io) - if retval == False and 'target' in rptopts: - del rptopts['target'] - - return retval -# -# This writes out the report/signature to an on-disk/over-the-wire format -# called the 'external format'. -# -# The external format is either an XML file, or a TAR file containing -# an XML file, and files containing the contents of some of the members -# of the report/signature. A reader of the external format must -# be capable of reading either format, a writer of the external format -# may choose either format, but should choose the format that is most -# efficient for the nature of the members of the report/signature it -# is writing. -# -# if asSignature -# only non-binary members are included in the external format -# all non-binary members are included directly into a single XML -# file called .xml -# -# otherwise -# all members are included in the external format -# if there are any files in the external format -# the external format is a tarfile called .tar.gz -# which includes a member named "content".xml which is an XML -# file containing -# the direct contents of all the non-file members -# and references to all of file references -# the contents of all file members are stored directly -# in a sub-directory of the tarfile, called "contents" -# else -# all members are included directly into a single XML -# file called .xml -# - -def serialize( signature, fileNameBase, asSignature ): - - reportFile = None - reportFileName = None - - if fileNameBase is None: - fileNameBase = "report" - else: - fileNameBase = os.path.basename( fileNameBase ) - - if fileNameBase == "": - fileNameBase = "report" - - root = etree.Element("report") - - root.attrib["xmlns"] = "http://www.redhat.com/gss/strata" - - for (key,value) in signature.iteritems(): - - if not asSignature or not value.isBinary: - elem = etree.Element("binding", name=key) - - if value.isFile: - if value.fileName and value.fileName != "": - elem.attrib["fileName"] = value.fileName - - if value.isBinary: - elem.attrib["type"] = "binary" - else: - elem.attrib["type"] = "text" - - if asSignature or not value.isFile: - elem.text = value.asString() - - else: - if reportFile == None: - baseFile = tempfile.NamedTemporaryFile( - prefix=fileNameBase, - suffix=".tar.gz", - delete=False) - reportFileName = baseFile.name - reportFile = tarfile.open(mode="w|gz", - fileobj=baseFile) - - realfilename = value.asFileName() - - # as we copy the file into the tarball, we want to - # rename it slightly: remove any leading "../"'s - # add a leading "content" - # and normalize - if value.isFile and value.fileName: - internalfilename = value.fileName - else: - internalfilename = realfilename - while internalfilename.startswith("../"): - internalfilename = internalfilename[3:] - internalfilename = os.path.normpath("content/" + internalfilename) - reportFile.add(realfilename, internalfilename) - elem.attrib["href"] = internalfilename - - root.append( elem ) - - rootstring = etree.tostring(root) - if reportFile == None: - reportFile = tempfile.NamedTemporaryFile( - prefix=fileNameBase, - suffix=".xml", - delete=False) - reportFileName = reportFile.name - reportFile.write(rootstring) - reportFile.close() - else: - tmpfile = tempfile.NamedTemporaryFile(delete=False) - tmpfile.write(rootstring) - tmpfile.close() - reportFile.add(tmpfile.name,"content.xml") - reportFile.close() - baseFile.close() - - return reportFileName - -def serializeAsSignature( signature, fileNameBase="signature" ): - return serialize( signature, fileNameBase, asSignature=True ) - -def serializeAsReport( signature, fileNameBase="report" ): - return serialize( signature, fileNameBase, asSignature=False ) - -# -# serializeToFile -# is for use by plugins that can/must only write a signature as -# a single file. For 'simpleFile' reports/signatures, it serializes -# to that file. For 'pythonUnhandledException', 'description', and -# 'signature' reports/signatures, this serializes them as a Signature. -# For all other repors/signatures this serializes them as a Report. -# - -def serializeToFile( signature, io, fileNameBase = None ): - if signature.has_key("simpleFile"): - return signature["simpleFile"].asFileName() - - elif signature.has_key("pythonUnhandledException"): - if fileNameBase is None: - if signature["pythonUnhandledException"].isFile and \ - signature["pythonUnhandledException"].fileName: - fileNameBase = signature["pythonUnhandledException"].fileName - else: - fileNameBase = "pythonUnhandledException" - return serializeAsSignature(signature, fileNameBase) - - elif signature.has_key("description"): - if fileNameBase is None: - if signature["description"].isFile and \ - signature["description"].fileName: - fileNameBase = signature["description"].fileName - else: - fileNameBase = "description" - return serializeAsSignature(signature, fileNameBase) - - elif signature.has_key("signature"): - if fileNameBase is None: - if signature["signature"].isFile and \ - signature["signature"].fileName: - fileNameBase = signature["signature"].fileName - else: - fileNameBase = "signature" - return serializeAsSignature(signature, fileNameBase) - - else: - if fileNameBase is None: - fileNameBase = "report" - return serializeAsReport(signature, fileNameBase) - diff --git a/python/report/accountmanager.py b/python/report/accountmanager.py deleted file mode 100644 index 10f0e5b..0000000 --- a/python/report/accountmanager.py +++ /dev/null @@ -1,167 +0,0 @@ -""" - Utility routines for managing saved account/username/password information. - Copyright (C) 2009 Red Hat, Inc - - Author: Gavin Romig-Koch - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -HAVE_gnomekeyring = False - -try: - import gnomekeyring - HAVE_gnomekeyring = True -except: - pass - - -class AccountManager: - class LoginAccount: - def __init__(self): - self.username = "" - self.remember_me = True - self.password = None - - def __init__(self): - self.accounts = {} - - def addAccount(self,accountName,username): - if not self.accounts.has_key(accountName): - self.accounts[accountName] = self.LoginAccount() - - self.accounts[accountName].username = username - - def hasAccount(self,accountName): - return self.accounts.has_key(accountName) - - def lookupAccount(self,accountName): - return self.accounts[accountName] - - def queryLogin(self,accountName): - global HAVE_gnomekeyring - - if self.accounts.has_key(accountName): - username = self.accounts[accountName].username - password = self.accounts[accountName].password - remember = self.accounts[accountName].remember_me - - if not username: - username = "" - if not password: - password = "" - if not remember: - remember = False - - if not HAVE_gnomekeyring: - remember = None - - if remember: - try: - items = gnomekeyring.find_items_sync( - gnomekeyring.ITEM_GENERIC_SECRET, - {"user": username, "server": accountName}) - password = items[0].secret - except: - pass - - else: - username = "" - password = None - remember = False - - if HAVE_gnomekeyring: - try: - items = gnomekeyring.find_items_sync( - gnomekeyring.ITEM_GENERIC_SECRET, - {"server": accountName}) - - # should not just user first, - # should use the one with the latest mtime - for i in range(0,len(items)): - if 'user' in items[i].attributes: - username = items[i].attributes['user'] - password = items[i].secret - remember = True - break; - - except gnomekeyring.NoMatchError: - pass - - except: - # should log these, but for now just go on - pass - - if not username: - username = "" - if not password: - password = "" - if not remember: - remember = False - - if not HAVE_gnomekeyring: - remember = None - - return (accountName,username,password,remember) - - def updateLogin(self,accountName,loginResult): - global HAVE_gnomekeyring - - if not loginResult.has_key('remember') or \ - loginResult['remember'] == None: - pass - - elif loginResult['remember']: - if not self.accounts.has_key(accountName): - self.accounts[accountName] = self.LoginAccount() - - self.accounts[accountName].password = loginResult['password'] - self.accounts[accountName].username = loginResult['username'] - - if HAVE_gnomekeyring: - try: - gnomekeyring.item_create_sync( - gnomekeyring.get_default_keyring_sync(), - gnomekeyring.ITEM_GENERIC_SECRET, - "password for user %s at %s" % ( - loginResult['username'], - accountName), - {"user" : loginResult['username'], - "server": accountName}, - loginResult['password'], - True) - - except: - pass - - else: - if self.accounts.has_key(accountName): - del self.accounts[accountName] - - if HAVE_gnomekeyring: - try: - items = gnomekeyring.find_items_sync( - gnomekeyring.ITEM_GENERIC_SECRET, - {"user": loginResult['username'], - "server": accountName}) - - for i in range(0,len(items)): - gnomekeyring.item_delete_sync( - items[i].keyring, - items[i].item_id) - - except: - pass - diff --git a/python/report/io/GTKIO.py b/python/report/io/GTKIO.py deleted file mode 100644 index 953aa49..0000000 --- a/python/report/io/GTKIO.py +++ /dev/null @@ -1,265 +0,0 @@ -""" - A GTK plugin for the general purpose I/O functions provided to - report plugins. - Copyright (C) 2009 Red Hat, Inc - - Author: Gavin Romig-Koch - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -from report import _report as _ -import report.accountmanager -import os -import gio -if 'DISPLAY' in os.environ and len(os.environ["DISPLAY"]) > 0: - import gtk - -class GTKIO: - def __init__(self,loginManager = None): - import gtk - if loginManager == None: - loginManager = report.accountmanager.AccountManager() - self.loginManager = loginManager - - def infoMessage(self,title,msg): - MessageDialog(title,msg) - - def failMessage(self,title,msg): - FailDialog(title,msg) - - def successMessage(self, title, msg, actualURL, displayURL): - SuccessDialog(title, msg, actualURL, displayURL) - - def queryLogin(self, accountName): - (accountName,username,password,remember) = \ - self.loginManager.queryLogin(accountName) - return LoginDialog(accountName,username,password,remember).run() - - def updateLogin(self,accountName,loginResult): - self.loginManager.updateLogin(accountName,loginResult) - - def queryField(self,fieldName): - return FieldDialog(fieldName).run() - - def queryChoice(self,msg,choices): - buttons = () - returnValues = [] - count = 0 - for each in choices: - count += 1 - buttons += (each.title,count) - returnValues.append(each.returnValue) - - choice = ButtonBoxDialog(msg,buttons).run() - - if not choice or choice < 1 or count < choice: - return None - - return returnValues[choice-1] - -class LoginDialog: - def __init__(self, account, username, password, remember): - self.dialog = gtk.Dialog(_("Login for %s" % account), None, - gtk.DIALOG_MODAL, - (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, - gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - self.dialog.set_resizable(True) - self.dialog.set_border_width(0) - self.dialog.set_position(gtk.WIN_POS_CENTER) - self.dialog.set_default_response(gtk.RESPONSE_ACCEPT) - - usernameHBox = gtk.HBox(False,10) - self.dialog.vbox.pack_start(usernameHBox, True, True, 0) - - usernameLabel = gtk.Label(_("Username")) - usernameHBox.pack_start(usernameLabel, True, True, 0) - - self.usernameEntry = gtk.Entry() - self.usernameEntry.set_text(username) - self.usernameEntry.set_visibility(True) - self.usernameEntry.set_activates_default(True) - usernameHBox.pack_start(self.usernameEntry, True, True, 0) - - passwordHBox = gtk.HBox(False,10) - self.dialog.vbox.pack_start(passwordHBox, True, True, 0) - - passwordLabel = gtk.Label(_("Password")) - passwordHBox.pack_start(passwordLabel, True, True, 0) - - self.passwordEntry = gtk.Entry() - self.passwordEntry.set_text(password) - self.passwordEntry.set_visibility(False) - self.passwordEntry.set_activates_default(True) - passwordHBox.pack_start(self.passwordEntry, True, True, 0) - - if remember == None: - self.keyringCheckBox = None - else: - self.keyringCheckBox = gtk.CheckButton( - _("Save password in keyring")) - self.dialog.vbox.pack_start(self.keyringCheckBox, True, True, 0) - self.keyringCheckBox.set_active(remember) - - def run(self): - self.dialog.show_all() - rc = self.dialog.run() - responseDict = {} - if rc == gtk.RESPONSE_ACCEPT: - responseDict['username'] = self.usernameEntry.get_text() - responseDict['password'] = self.passwordEntry.get_text() - if self.keyringCheckBox == None: - responseDict['remember'] = None - else: - responseDict['remember'] = self.keyringCheckBox.get_active() - self.dialog.destroy() - return responseDict - else: - self.dialog.destroy() - return None - - -class FieldDialog: - def __init__(self, fieldName): - self.dialog = gtk.Dialog(_("Enter %s") % fieldName, None, - gtk.DIALOG_MODAL, - (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, - gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) - self.dialog.set_resizable(True) - self.dialog.set_border_width(0) - self.dialog.set_position(gtk.WIN_POS_CENTER) - self.dialog.set_default_response(gtk.RESPONSE_ACCEPT) - - fieldHBox = gtk.HBox(False,10) - self.dialog.vbox.pack_start(fieldHBox, True, True, 0) - - fieldLabel = gtk.Label(fieldName) - fieldHBox.pack_start(fieldLabel, True, True, 0) - - self.fieldEntry = gtk.Entry() - self.fieldEntry.set_visibility(True) - self.fieldEntry.set_activates_default(True) - fieldHBox.pack_start(self.fieldEntry, True, True, 0) - - def run(self): - self.dialog.show_all() - rc = self.dialog.run() - if rc == gtk.RESPONSE_ACCEPT: - r = self.fieldEntry.get_text() - self.dialog.destroy() - return r - else: - self.dialog.destroy() - return None - -class ButtonBoxDialog: - def __init__(self, msg, buttons): - - self.dialog = gtk.Dialog(msg, None, gtk.DIALOG_MODAL) - - self.dialog.set_resizable(True) - self.dialog.set_border_width(0) - self.dialog.set_position(gtk.WIN_POS_CENTER) - - label = gtk.Label(msg) - self.dialog.vbox.pack_start(label) - - for i in range(0, len(buttons), 2): - label_item = buttons[i] - response_item = buttons[i+1] - button = gtk.Button(label=label_item) - button.connect("clicked", - lambda b, r: self.dialog.response(r), - response_item) - self.dialog.vbox.pack_start(button) - - - button = gtk.Button(stock=gtk.STOCK_CANCEL) - button.connect("clicked", - lambda b, r: self.dialog.response(r), - gtk.RESPONSE_REJECT) - self.dialog.vbox.pack_start(button) - - - def run(self): - self.dialog.show_all() - rc = self.dialog.run() - self.dialog.destroy() - return rc - -class FailDialog(): - def __init__(self, title, message): - dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, - gtk.BUTTONS_OK, - message) - dlg.set_title(title) - dlg.set_position(gtk.WIN_POS_CENTER) - dlg.show_all() - rc = dlg.run() - dlg.destroy() - -class MessageDialog(): - def __init__(self, title, message): - dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO, - gtk.BUTTONS_OK, - message) - dlg.set_title(title) - dlg.set_position(gtk.WIN_POS_CENTER) - dlg.set_default_response(message) - dlg.set_activates_default(True) - dlg.show_all() - rc = dlg.run() - dlg.destroy() - -class SuccessDialog(): - def __init__(self, title, message, actualURL, displayURL): - - # a blank URL is an empty URL - if actualURL and "" == actualURL.strip(): - actualURL = None - - if displayURL and "" == displayURL.strip(): - displayURL = None - - # default display to actual - if actualURL and not displayURL: - displayURL = actualURL - - dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO, - gtk.BUTTONS_OK, - message) - dlg.set_title(title) - dlg.set_position(gtk.WIN_POS_CENTER) - - make_link = False - if actualURL: - scheme = actualURL.partition(':')[0] - if scheme and gio.app_info_get_default_for_uri_scheme(scheme): - make_link = True - - if make_link: - dlg.vbox.pack_start( - gtk.LinkButton(actualURL, _("View %s") % displayURL), - True, True, 0) - - else: - dlg.vbox.pack_start(gtk.Label(displayURL), True, True, 0) - if actualURL and actualURL != displayURL: - dlg.vbox.pack_start(gtk.Label(actualURL), True, True, 0) - - dlg.show_all() - dlg.run() - dlg.destroy() - diff --git a/python/report/io/NewtIO.py b/python/report/io/NewtIO.py deleted file mode 100644 index 280a1cd..0000000 --- a/python/report/io/NewtIO.py +++ /dev/null @@ -1,166 +0,0 @@ -""" - A Newt based plugin for the general purpose I/O functions provided to - report plugins. - Copyright (C) 2009 Red Hat, Inc - - Author: Gavin Romig-Koch - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import snack -import gettext -_ = lambda x: gettext.ldgettext("report", x) - -import string - -class NewtIO: - def __init__(self,screen = None): - self.cleanupScreen = False - if screen == None: - self.screen = snack.SnackScreen() - self.cleanupScreen = True - else: - self.screen = screen - - def __del__(self): - if self.cleanupScreen: - self.screen.finish() - self.screen = None - self.cleanupScreen = False - - def infoMessage(self,title,msg): - snack.ButtonChoiceWindow(self.screen, title, msg, width=60, - buttons=[_("OK")]) - self.screen.popWindow() - self.screen.refresh() - - def failMessage(self,title,msg): - snack.ButtonChoiceWindow(self.screen, title, msg, width=60, - buttons=[_("OK")]) - self.screen.popWindow() - self.screen.refresh() - - def successMessage(self, title, msg, actualURL, displayURL): - - if displayURL: - msg += '\n ' + displayURL - if actualURL and actualURL != displayURL: - msg += '\n ' + actualURL - - snack.ButtonChoiceWindow(self.screen, title, msg, width=60, - buttons=[_("OK")]) - self.screen.popWindow() - self.screen.refresh() - - def queryLogin(self, accountName): - toplevel = snack.GridForm(self.screen, - _("Login for %s") % accountName, - 1, 2) - - buttons = snack.ButtonBar(self.screen, [_("OK"), _("Cancel")]) - usernameEntry = snack.Entry(24) - passwordEntry = snack.Entry(24, password=1) - - grid = snack.Grid(2, 2) - grid.setField(snack.Label(_("Username ")), 0, 0, anchorLeft=1) - grid.setField(usernameEntry, 1, 0) - grid.setField(snack.Label(_("Password ")), 0, 1, anchorLeft=1) - grid.setField(passwordEntry, 1, 1) - - toplevel.add(grid, 0, 0, (0, 0, 0, 1)) - toplevel.add(buttons, 0, 1, growx=1) - - result = toplevel.run() - rc = buttons.buttonPressed(result) - - self.screen.popWindow() - self.screen.refresh() - - if rc == string.lower(_("OK")): - responseDict = {} - responseDict['username'] = usernameEntry.value() - responseDict['password'] = passwordEntry.value() - responseDict['remember'] = False - return responseDict - - else: - return None - - def updateLogin(self,accountName,loginResult): - pass - - def queryField(self,fieldName): - toplevel = snack.GridForm(self.screen, _("Enter %s") % fieldName, 1, 2) - - buttons = snack.ButtonBar(self.screen, [_("OK"), _("Cancel")]) - fieldEntry = snack.Entry(24) - - grid = snack.Grid(2, 1) - grid.setField(snack.Label(fieldName + ' '), 0, 0, anchorLeft=1) - grid.setField(fieldEntry, 1, 0) - - toplevel.add(grid, 0, 0, (0, 0, 0, 1)) - toplevel.add(buttons, 0, 1, growx=1) - - result = toplevel.run() - rc = buttons.buttonPressed(result) - - self.screen.popWindow() - self.screen.refresh() - - if rc == string.lower(_("OK")): - return fieldEntry.value() - - else: - return None - - def queryChoice(self,msg,choices): - cancel_label = _("CANCEL") - - buttons = [] - returnValues = [] - for each in choices: - buttons.append(each.title) - returnValues.append(each.returnValue) - - buttons.append(cancel_label) - - toplevel = snack.GridForm(self.screen, msg, 1, 2) - - buttonBar = snack.ButtonBar(self.screen, buttons) - - toplevel.add(snack.Label(msg), 0, 0, (0, 0, 0, 1)) - toplevel.add(buttonBar, 0, 1, growx=1) - - result = toplevel.run() - rc = buttonBar.buttonPressed(result) - - self.screen.popWindow() - self.screen.refresh() - - if rc == cancel_label.lower(): - return None - - count = 0 - for each in buttons: - if rc == each.lower(): - return returnValues[count] - else: - count += 1 - - return None - - diff --git a/python/report/io/TextIO.py b/python/report/io/TextIO.py deleted file mode 100644 index 17daa2a..0000000 --- a/python/report/io/TextIO.py +++ /dev/null @@ -1,99 +0,0 @@ -""" - A console/text plugin for the general purpose I/O functions provided to - report plugins. - Copyright (C) 2009 Red Hat, Inc - - Author: Gavin Romig-Koch - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import getpass - -class TextIO: - def infoMessage(self,title,msg): - print - print title - print msg - - def failMessage(self,title,msg): - print - print title - print msg - - def successMessage(self, title, msg, actualURL, displayURL): - print - print title - print msg - if displayURL: - print displayURL - if actualURL and actualURL != displayURL: - print actualURL - - def queryLogin(self, accountName): - print - print "Login for %s" % accountName - try: - username = raw_input("Username: ") - password = getpass.getpass("Password: ") - except EOFError: - print "input canceled (EOF)" - return None - - responseDict = {} - responseDict['username'] = username - responseDict['password'] = password - responseDict['remember'] = False - return responseDict - - def updateLogin(self,accountName,loginResult): - pass - - def queryField(self,fieldName): - print - try: - fieldValue = raw_input("%s: " % fieldName) - except EOFError: - print "input canceled (EOF)" - return None - return fieldValue - - def queryChoice(self,msg,choices): - while True: - print("\n") - print(msg) - - count = 1 - for each in choices: - print "%s: %s" % (count,each.title) - count += 1 - print "0: %s" % ("cancel",) - - try: - choice = raw_input("Choice (0-%s): " % (count-1,)) - except EOFError: - print "input canceled (EOF)" - return None - try: - choice = int(choice) - except ValueError: - choice = count - if 0 < choice and choice < count: - return choices[choice-1].returnValue - if choice == 0: - return None - print "Invalid choice" - - diff --git a/python/report/io/__init__.py b/python/report/io/__init__.py deleted file mode 100644 index 22b36c5..0000000 --- a/python/report/io/__init__.py +++ /dev/null @@ -1,131 +0,0 @@ -import syslog -import ConfigParser - -_Loglevel = None - -def _GetLoglevel(): - global _Loglevel - - if _Loglevel == None: - try: - config = ConfigParser.RawConfigParser() - config.optionxform = str - config.read("/etc/report.conf") - - # Acceptable priorities for syslog - prio_mappings = {'LOG_INFO': syslog.LOG_INFO, - 'LOG_CRIT': syslog.LOG_CRIT, - 'LOG_DEBUG' : syslog.LOG_DEBUG, - 'LOG_WARNING': syslog.LOG_WARNING} - - _Loglevel = prio_mappings[config.get("main","loglevel")] - - except: - _Loglevel = syslog.LOG_INFO - return _Loglevel - -def DisplayFailMessage(io, title, msg): - """ display error message, title and msg or strings - return nothing - """ - logmsg = 'report: ' - if msg: - if title: - logmsg += title + ': ' + msg - else: - logmsg += msg - else: - if title: - logmsg += title + ': DisplayFailMessage called without message' - else: - logmsg += 'DisplayFailMessage called without message' - - syslog.syslog(_GetLoglevel(), logmsg) - if io: - io.failMessage(title, msg) - -def DisplaySuccessMessage(io, title, msg, actualURL, displayURL): - """ display a sucess message, all args are strings, - displayURL and actualURL should both refer to the same - internet resource, displayURL is for display to the user, - actualURL is for if you want to link to the resource. - If actualURL is empty but displayURL is not, displayURL - is shown but not as a link. - if displayURL is empty but actualURL is not, displayURL - defaults to actualURL - displayURL and actualURL can be the same string. - return nothing - """ - if displayURL: - URL = displayURL - else: - URL = actualURL - - logmsg = 'report: ' - if msg: - if title: - logmsg += title + ': ' + msg - else: - logmsg += msg - else: - if title: - logmsg += title + ': DisplaySuccessMessage called without message' - else: - logmsg += 'DisplaySuccessMessage called without message' - - if URL: - logmsg += '\n' + URL - - syslog.syslog(_GetLoglevel(), logmsg) - if io: - io.successMessage(title, msg, actualURL, displayURL) - -class ChoiceValue: - def __init__(self,title,explanation,returnValue): - self.title = title - self.explanation = explanation - self.returnValue = returnValue - -class IO: - # IO is a callback mechinism for communicating with the user - # IO can be any type that conforms to the following protocol - # def infoMessage(self,title,msg): - # display message, title and msg are strings - # return nothing - # def failMessage(self,title,msg): - # display an error message, title and msg are strings - # return nothing - # def successMessage(self,title,msg,actualURL,displayURL) - # display a sucess message, all args are strings, - # displayURL and actualURL should both refer to the same - # internet resource, displayURL is for display to the user, - # actualURL is for if you want to link to the resource. - # If actualURL is empty but displayURL is not, displayURL - # is shown but not as a link. - # if displayURL is empty but actualURL is not, displayURL - # defaults to actualURL - # displayURL and actualURL can be the same string. - # return nothing - # def queryLogin(self,account): - # Ask the user for the username and password for logging into - # account (a string). - # return a dictionary which contains at least two members with - # the keys "username" and "password". The values of these members - # should be strings. - # def updateLogin(self,account,loginResult): - # Update the login information for account. - # If you call queryLogin, and the login is then successfull, - # call this function with the result of the queryLogin to - # tell the account manager that the login was successfull - # returns nothing - # def queryField(self,fieldName) - # asks for a string value, returns string value - # def queryChoice(self,msg,choices): - # msg is a message about the choices - # choices is a sequence of ChoiceValues - # Each ChoiceValue (title,explanation,returnValue) - # returns the returnValue of the choice the user made - pass - - - diff --git a/python/report/plugins/RHEL-bugzilla/__init__.py b/python/report/plugins/RHEL-bugzilla/__init__.py deleted file mode 100644 index 6c0a543..0000000 --- a/python/report/plugins/RHEL-bugzilla/__init__.py +++ /dev/null @@ -1,369 +0,0 @@ -""" - A Report plugin to send a report to bugzilla.redhat.com. - Copyright (C) 2009 Red Hat, Inc - - Author(s): Gavin Romig-Koch - - Much of the code in this module is derived from code written by - Chris Lumens . - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import os -import report.io as iomodule -from report.io import DisplaySuccessMessage -from report.io import DisplayFailMessage -from report import _report as _ - -def labelFunction(label): - if label: - return label - retValue = displayURL(optionsDict) - if retValue.startswith("http://"): - retValue = retValue[len("http://"):] - if retValue.startswith("https://"): - retValue = retValue[len("https://"):] - return retValue - -def descriptionFunction(optionsDict): - if optionsDict.has_key("description"): - return optionsDict["description"] - return "Send report to " + displayURL(optionsDict) - -def displayURL(optionsDict): - if optionsDict.has_key("displayURL"): - return optionsDict["displayURL"] - returnURL = bugURL(optionsDict) - if returnURL.endswith("/xmlrpc.cgi"): - returnURL = returnURL[:len(returnURL) - len("/xmlrpc.cgi")] - return returnURL - -def bugURL(optionsDict): - if optionsDict.has_key("bugURL"): - return optionsDict["bugURL"] - host = "bugzilla.redhat.com" - if optionsDict.has_key("bugzilla_host"): - host = optionsDict["bugzilla_host"] - return "https://" + host + "/xmlrpc.cgi" - -def report(signature, io, optionsDict): - if not io: - DisplayFailMessage(None, _("No IO"), - _("No io provided.")) - return False - - if 'pythonUnhandledException' in signature: - fileName = signature["pythonUnhandledException"].asFileName() - fileDescription = "Attached traceback automatically from %s." % signature["component"].asString() - elif 'simpleFile' in signature: - fileName = signature['simpleFile'].asFileName() - fileDescription = "Attached file %s." % (signature['simpleFile'].asFileName(),) - else: - fileName = None - fileDescription = None - - if 'product' in signature: - product = signature['product'].asString() - else: - product = filer.getProduct() - - if 'version' in signature: - version = signature['version'].asString() - else: - version = filer.getVersion() - - bzfiler = filer.BugzillaFiler(bugURL(optionsDict), - displayURL(optionsDict), - version, product) - - if optionsDict.has_key("testing_component"): - component = optionsDict["testing_component"] - elif 'component' in signature: - component = signature["component"].asString() - else: - component = None - - if 'hashmarkername' in signature: - hashmarkername = signature["hashmarkername"].asString() - else: - hashmarkername = None - - if 'localhash' in signature: - localhash = signature["localhash"].asString() - else: - localhash = None - - if 'summary' in signature: - summary = signature["summary"].asString() - else: - summary = None - - if 'description' in signature: - description = signature["description"].asString() - else: - description = None - - return sendToBugzilla(component, - hashmarkername, - localhash, - summary, - description, - fileName, - fileDescription, - io, - optionsDict, - bzfiler) - - - - - - - - -import filer -# -# This function was abstracted from similar code in both python-meh and -# setroubleshoot. Beyond parameterizing this code, and using IO, this -# code differs from those others in that this version includes the -# 'component' in the .query for duplicates. -# -def sendToBugzilla( component, hashmarkername, localhash, summary, description, fileName, fileDescription, io, optionsDict, bzfiler): - - import rpmUtils.arch - - class BugzillaCommunicationException (Exception): - pass - - def withBugzillaDo(bz, fn): - try: - retval = fn(bz) - return retval - except filer.CommunicationError, e: - msg = _("Your bug could not be filed due to the following error " - "when communicating with bugzilla:\n\n%s" % str(e)) - DisplayFailMessage(io, _("Unable To File Bug"), msg) - raise BugzillaCommunicationException() - - except (TypeError, ValueError), e: - msg = _("Your bug could not be filed due to bad information in " - "the bug fields. This is most likely an error in " - "the bug filing program:\n\n%s" % str(e)) - DisplayFailMessage(io, _("Unable To File Bug"), msg) - raise BugzillaCommunicationException() - - try: - if not bzfiler: - bzfiler = filer.BugzillaFiler("https://bugzilla.redhat.com/xmlrpc.cgi", - "http://bugzilla.redhat.com", - filer.getVersion(), filer.getProduct()) - - if not bzfiler or not bzfiler.supportsFiling() or not bzfiler.bugUrl: - DisplayFailMessage(io, _("Bug Filing Not Supported"), - _("Your distribution does not provide a " - "supported bug filing system, so you " - "cannot save your exception this way.")) - return False - - bugzilla_host = os.path.basename(os.path.dirname(bzfiler.bugUrl)) - - loginResult = io.queryLogin(bugzilla_host) - if loginResult: - password = loginResult['password'] - username = loginResult['username'] - - elif loginResult == None: - return None - - else: - DisplayFailMessage(io, _("No Login Information"), - _("Please provide a valid username and password.")) - return False - - try: - withBugzillaDo(bzfiler, lambda b: b.login(username, password)) - except filer.LoginError: - DisplayFailMessage(io, _("Unable To Login"), - _("There was an error logging into %s " - "using the provided username and " - "password.") % bzfiler.displayUrl) - return False - - io.updateLogin(bugzilla_host,loginResult) - - # figure out whether to attach to an existing bug, create a new bug, - # or search for matching bugs - if 'ticket' in optionsDict: - bug_number = optionsDict['ticket'] - bug = (withBugzillaDo(bzfiler, - lambda b: b.getbug(bug_number))) - - if not bug or bug == "": - DisplayFailMessage(io, _("Bug not found"), - _("Unable to find bug %s" % bug_number)) - return False - else: - buglist = [bug] - wb = "" - - elif localhash and hashmarkername: - # Are there any existing bugs with this hash value? If so we - # will just add any attachment to the bug report and put the - # reporter on the CC list. Otherwise, we need to create a new bug. - wb = "%s_trace_hash:%s" % (hashmarkername, localhash) - buglist = withBugzillaDo(bzfiler, lambda b: b.query( - {'status_whiteboard': wb, - 'status_whiteboard_type':'allwordssubstr', - 'bug_status': []})) - - elif component and (fileDescription or description): - # then we should just go ahead and create a new case - wb = "" - buglist = [] - - else: - # ask create or attach? - choice_attach = 4 - choice_new = 5 - - choices = [ - iomodule.ChoiceValue(_("Create new bug"), _("Create a new bug and attach report to it."), choice_new), - iomodule.ChoiceValue(_("Attach to existing bug"), _("Attach report to an existing bug."), choice_attach) - ] - - choice = io.queryChoice(_("Do you want to attach the report to an existing bug or create a new bug?"), choices) - - if choice is None: - return None - - elif choice == choice_new: - wb = "" - buglist = [] - - if component == None: - component = io.queryField('Enter component for new bug'); - if component is None: - return None - component = component.strip() - - if summary == None: - summary = io.queryField('Enter summary for new bug'); - if summary == None: - return None - summary = summary.strip() - - if description == None: - description = io.queryField( - 'Enter description for new bug'); - if description is None: - return None - description = description.strip() - - else: - bug_number = io.queryField("Enter existing bug number") - if bug_number == None: - return None - - bug = (withBugzillaDo(bzfiler, - lambda b: b.getbug(bug_number))) - - if not bug or bug == "": - DisplayFailMessage(io, _("Bug not found"), - _("Unable to find bug %s" % bug_number)) - return False - else: - buglist = [bug] - wb = "" - - if not buglist or len(buglist) == 0: - - # cleanup summary and description - if not summary or not summary.strip(): - summary = "New bug for %s" % (component,) - - if not description or not description.strip(): - if fileDescription: - description = fileDescription - else: - description = '' - - bug = withBugzillaDo(bzfiler, lambda b: b.createbug( - product=bzfiler.getproduct(), - component=component, - version=bzfiler.getversion(), - platform=rpmUtils.arch.getBaseArch(), - bug_severity="medium", - priority="medium", - op_sys="Linux", - bug_file_loc="http://", - summary=summary, - comment=description, - status_whiteboard=wb)) - - if fileName: - if fileDescription == None: - fileDescription = "" - withBugzillaDo(bug, lambda b: b.attachfile(fileName, fileDescription, - contenttype="text/plain", - filename=os.path.basename(fileName))) - - # Tell the user we created a new bug for them and that they should - # go add a descriptive comment. - bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) - - bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) - - DisplaySuccessMessage(io, _("Bug Created"), - _("A new bug has been created with your information added. " - "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:"), - bugURL, - bugDisplayURL) - return True - else: - bug = buglist[0] - if fileName: - if fileDescription == None: - fileDescription = "" - - withBugzillaDo(bug, lambda b: b.attachfile(fileName,fileDescription, - contenttype="text/plain", - filename=os.path.basename(fileName))) - withBugzillaDo(bug, lambda b: b.addCC(username)) - - # Tell the user which bug they've been CC'd on and that they should - # go add a descriptive comment. - bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) - - bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) - - DisplaySuccessMessage(io, _("Bug Updated"), - _("A bug with your information already exists. Your account and information has " - "been added to this bug. Please add additional descriptive information to the " - "following bug:"), - bugURL, - bugDisplayURL) - - return True - - except BugzillaCommunicationException: - # this indicates that doWithBugzilla caught some problem - # communicating with bugzilla and displayed a message about it - # and all we want to do now is get out of sendToBugzilla - return False - - diff --git a/python/report/plugins/RHEL-bugzilla/filer.py b/python/report/plugins/RHEL-bugzilla/filer.py deleted file mode 100644 index 73e86ca..0000000 --- a/python/report/plugins/RHEL-bugzilla/filer.py +++ /dev/null @@ -1,497 +0,0 @@ -# Copyright (C) 2008, 2009, 2010 Red Hat, Inc. -# All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Author(s): Chris Lumens -# Gavin Romig-Koch -# - -import socket -import xmlrpclib -import os -from report import _report as _ -import report.release_information - -_hardcoded_default_product = 'Red Hat Enterprise Linux' -_hardcoded_default_version = '6.0' -_hardcoded_default_product_for_bugzilla = 'Red Hat Enterprise Linux 6' - -def getProduct(): - product = report.release_information.getProduct() - if product: - return product - return _hardcoded_default_product - -def getVersion(): - version = report.release_information.getVersion() - if version: - return version - return _hardcoded_default_version - -class LoginError(Exception): - """An error occurred while logging into the bug reporting system.""" - def __init__(self, bugUrl, username): - self.bugUrl = bugUrl - self.username = username - - def __str__(self): - return "Could not login to %s with username %s" % (self.bugUrl, self.username) - -class CommunicationError(Exception): - """Some miscellaneous error occurred while communicating with the - bug reporting system. This could include XML-RPC errors, passing - bad data, or network problems.""" - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return "Error communicating with bug system: %s" % self.msg - - -# These classes don't do anything except say that automated bug filing are not -# supported. They also define the interface that concrete classes should use, -# as this is what will be expected by exception.py. -class AbstractFiler(object): - """The base class for Filer objects. This is an abstract class. - - Within this class's help, Bug refers to a concrete AbstractBug subclass - and Filer refers to a concrete AbstractFiler subclass. - - A Filer object communicates with a bug filing system - like bugzilla - - that a distribution uses to track defects. Install classes specify - what bug filing system they use by instantiating a subclass of - AbstractFiler. The intention is that each subclass of AbstractFiler - will make use of some system library to handle the actual communication - with the bug filing system. For now, all systems will be assumed to act - like bugzilla. - - Methods in this class should raise the following exceptions: - - CommunicationError -- For all problems communicating with the remote - bug filing system. - LoginError -- For invalid login information. - ValueError -- For all other operations where the client - supplied values are not correct. - """ - def __init__(self, bugUrl, displayUrl, version, product): - """Create a new AbstractFiler instance. This method need not be - overridden by subclasses. - - bugUrl -- The XML-RPC interface to the bug tracking system. - displayUrl -- The URL to use in the UI. - product -- The name of the product we should attempt to file - bugs against. This must be set. - version -- The version of the product we should attempt to - file bugs against. This must be set. - """ - self.bugUrl = bugUrl - self.displayUrl = displayUrl - self.version = str(version) - self.product = product - - def login(self, username, password): - """Using the given username and password, attempt to login to the - bug filing system. This method must be provided by all subclasses, - and should raise LoginError if login is unsuccessful. - """ - raise NotImplementedError - - def createbug(self, *args, **kwargs): - """Create a new bug. The kwargs dictionary is all the arguments that - should be used when creating the new bug and is entirely up to the - subclass to handle. This method must be provided by all subclasses. - On success, it should return a Bug instance. - """ - raise NotImplementedError - - def getbug(self, id): - """Search for a bug given by id and return it. This method must be - provided by all subclasses. On success, it should return a Bug - instance. On error, it should return an instance that is empty. - """ - raise NotImplementedError - - def getbugs(self, idlist): - """Search for all the bugs given by the IDs in idlist and return. - This method must be provided by all subclasses. On success, it - should return a list of Bug instances, or an empty instance for - invalid IDs. - """ - raise NotImplementedError - - def getproduct(self): - """Verify that self.product is a valid product name. If it is, return - that same product name. If not, return self.defaultProduct. This - method queries the bug filing system for a list of valid products. - It must be provided by all subclasses. - """ - raise NotImplementedError - - def getversion(self): - """Verify that self.version is a valid version number for the product - name self.product. If it is, return that same version number as a - string. If not, return "rawhide" if it exists or the latest version - number otherwise. This method queries the bug filing system for a - list of valid versions numbers. It must be provided by all - subclasses. - """ - raise NotImplementedError - - def query(self, query): - """Perform the provided query and return a list of Bug instances that - meet the query. What the query is depends on the exact bug filing - system, though it will be treated as a dictionary of bug attributes - since this is what bugzilla expects. Other filing systems will need - to take extra work to munge this data into the expected format. - This method must be provided by all subclasses. - """ - raise NotImplementedError - - def supportsFiling(self): - """Does this class support filing bugs? All subclasses should override - this method and return True, or automatic filing will not work. - Automatic filing will not be attempted on unknown products. - """ - return False - -class AbstractBug(object): - """The base class for Bug objects. This is an abstract class. - - Within this class's help, Bug refers to a concrete AbstractBug subclass - and Filer refers to a concrete AbstractFiler subclass. - - A Bug object represents one single bug within a Filer. This is where - most of the interesting stuff happens - attaching files, adding comments - and email addresses, and modifying whiteboards. Subclasses of this - class are returned by most operations within a Filer subclass. For now, - all bugs will be assumed to act like bugzilla's bugs. - - Bug objects wrap objects in the underlying module that communicates with - the bug filing system. For example, the bugzilla filer uses the - python-bugzilla module to communicate. This module has its own Bug - object. So, BugzillaBug wraps that object. Therefore, Bugs may be - created out of existing BugzillaBugs or may create their own if - necessary. - - Methods in this class should raise the following exceptions: - - CommunicationError -- For all problems communicating with the remote - bug filing system. - ValueError -- For all other operations where the client - supplied values are not correct (invalid - resolution, status, whiteboard, etc.). - """ - def __init__(self, filer, bug=None, *args, **kwargs): - """Create a new Bug instance. It is recommended that subclasses - override this method to add extra attributes. - - filer -- A reference to a Filer object used when performing - certain operations. This may be None if it is not - required by the Filer or Bug objects. - bug -- If None, the filer-specific code should create a new - bug object. Otherwise, the filer-specific code - should use the provided object as needed. - args, kwargs -- If provided, these arguments should be passed as-is - when creating a new underlying bug object. This - only makes sense if bug is not None. - """ - self.filer = filer - - def __str__(self): - raise NotImplementedError - - def __repr__(self): - raise NotImplementedError - - def addCC(self, address): - """Add the provided email address to this bug. This method must be - provided by all subclasses, and return some non-None value on - success. - """ - raise NotImplementedError - - def addcomment(self, comment): - """Add the provided comment to this bug. This method must be provided - by all subclasses, and return some non-None value on success. - """ - raise NotImplementedError - - def attachfile(self, file, description, **kwargs): - """Attach the filename given by file, with the given description, to - this bug. If provided, the given kwargs will be passed along to - the Filer when attaching the file. These args may be useful for - doing things like setting the MIME type of the file. This method - must be provided by all subclasses and return some non-None value - on success. - """ - raise NotImplementedError - - def close(self, resolution, dupeid=0, comment=''): - """Close this bug with the given resolution, optionally closing it - as a duplicate of the provided dupeid and with the optional comment. - resolution must be a value accepted by the Filer. This method must - be provided by all subclasses and return some non-None value on - success. - """ - raise NotImplementedError - - def id(self): - """Return this bug's ID number. This method must be provided by all - subclasses. - """ - raise NotImplementedError - - def setstatus(self, status, comment=''): - """Set this bug's status and optionally add a comment. status must be - a value accepted by the Filer. This method must be provided by all - subclasses and return some non-None value on success. - """ - raise NotImplementedError - - def setassignee(self, assigned_to='', reporter='', comment=''): - """Assign this bug to the person given by assigned_to, optionally - changing the reporter and attaching a comment. assigned_to must be - a valid account in the Filer. This method must be provided by all - subclasses and return some non-None value on success. - """ - raise NotImplementedError - - def getwhiteboard(self, which=''): - """Get the given whiteboard from this bug and return it. Not all bug - filing systems support the concept of whiteboards, so this method - is optional. - """ - return "" - - def appendwhiteboard(self, text, which=''): - """Append the given text to the given whiteboard. Not all bug filing - systems support the concept of whiteboards, so this method is - optional. If provided, it should return some non-None value on - success. - """ - return True - - def prependwhiteboard(self, text, which=''): - """Put the given text at the front of the given whiteboard. Not all - bug filing systems support the concept of whiteboards, so this - method is optional. If provided, it should return some non-None - value on success. - """ - return True - - def setwhiteboard(self, text, which=''): - """Set the given whiteboard to be the given text. Not all bug filing - systems support the concept of whiteboards, so this method is - optional. If provided, it should return some non-None value on - success. - """ - return True - - -# Concrete classes for automatically filing bugs against Bugzilla instances. -# This requires the python-bugzilla module to do almost all of the real work. -# We basically just make some really thin wrappers around it here since we -# expect all bug filing systems to act similar to bugzilla. -class BugzillaFiler(AbstractFiler): - def __withBugzillaDo(self, fn): - try: - retval = fn(self._bz) - return retval - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def __init__(self, bugUrl, displayUrl, version, product): - AbstractFiler.__init__(self, bugUrl, displayUrl, version, product) - self._bz = None - - def login(self, username, password): - import bugzillaCopy - - try: - self._bz = bugzillaCopy.Bugzilla(url=self.bugUrl) - retval = self._bz.login(username, password) - except socket.error, e: - raise CommunicationError(str(e)) - - if not retval: - raise LoginError(self.bugUrl, username) - - return retval - - def createbug(self, *args, **kwargs): - whiteboards = [] - - for (key, val) in kwargs.items(): - if key.endswith("_whiteboard"): - wb = key.split("_")[0] - whiteboards.append((wb, val)) - kwargs.pop(key) - - if key == "platform": - platformLst = self.__withBugzillaDo(lambda b: b._proxy.Bug.legal_values({'field': 'platform'})) - if not val in platformLst['values']: - kwargs[key] = platformLst['values'][0] - - bug = self.__withBugzillaDo(lambda b: b.createbug(**kwargs)) - for (wb, val) in whiteboards: - bug.setwhiteboard(val, which=wb) - - return BugzillaBug(self, bug=bug) - - def getbug(self, id): - return BugzillaBug(self, bug=self.__withBugzillaDo(lambda b: b.getbug(id))) - - def getbugs(self, idlist): - lst = self.__withBugzillaDo(lambda b: b.getbugs(idlist)) - return map(lambda b: BugzillaBug(self, bug=b), lst) - - def getproduct(self): - details = self.__withBugzillaDo(lambda b: b.getproducts()) - for d in details: - if d['name'] == self.product: - return self.product - - # Extend product with high order number of version - product_with_version = self.product + ' ' + self.version.split('.')[0] - for d in details: - if d['name'] == product_with_version: - return product_with_version - - # If the product given to us by the caller isn't valid, fall back - # to asking the running system and then to something hard coded. - defaultProduct = getProduct() - for d in details: - if d['name'] == defaultProduct: - return defaultProduct - - product_with_version = getProduct() + ' ' + getVersion().split('.')[0] - for d in details: - if d['name'] == product_with_version: - return product_with_version - - return _hardcoded_default_product_for_bugzilla - - def getversion(self): - # Convert all version numbers from bugzilla into strings. Sometimes - # bugzilla gives us strings ("rawhide", "development"), sometimes it - # gives us integers (11, 12), and sometimes it gives us floats - # (5.4, 5.5, 6.0). - details = self.__withBugzillaDo(lambda b: b._proxy.bugzilla.getProductDetails(self.getproduct())) - bugzillaVers = map(str, details[1]) - bugzillaVers.sort() - - # Double check to make sure this is a string. - ver = str(self.version) - - # If the version given to us by the caller isn't valid, fall back to - # asking the running system and then to something hard coded. - if not ver in bugzillaVers: - defaultVersion = getVersion() - if defaultVersion in bugzillaVers: - return defaultVersion - - return _hardcoded_default_version - else: - return str(self.version) - - def query(self, query): - lst = self.__withBugzillaDo(lambda b: b.query(query)) - return map(lambda b: BugzillaBug(self, bug=b), lst) - - def supportsFiling(self): - return True - -class BugzillaBug(AbstractBug): - def __withBugDo(self, fn): - try: - retval = fn(self._bug) - return retval - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def __init__(self, filer, bug=None, *args, **kwargs): - import bugzillaCopy - - self.filer = filer - - if not bug: - self._bug = bugzillaCopy.Bug(self.filer, *args, **kwargs) - else: - self._bug = bug - - def __str__(self): - return self._bug.__str__() - - def __repr__(self): - return self._bug.__repr__() - - def addCC(self, address): - try: - return self.filer._bz._updatecc(self._bug.bug_id, [address], 'add') - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def addcomment(self, comment): - return self.__withBugDo(lambda b: b.addcomment(comment)) - - def attachfile(self, file, description, **kwargs): - try: - return self.filer._bz.attachfile(self._bug.bug_id, file, description, **kwargs) - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def id(self): - return self._bug.bug_id - - def close(self, resolution, dupeid=0, comment=''): - return self.__withBugDo(lambda b: b.close(resolution, dupeid=dupeid, - comment=comment)) - - def setstatus(self, status, comment=''): - return self.__withBugDo(lambda b: b.setstatus(status, comment=comment)) - - def setassignee(self, assigned_to='', reporter='', comment=''): - return self.__withBugDo(lambda b: b.setassignee(assigned_to=assigned_to, - reporter=reporter, - comment=comment)) - - def getwhiteboard(self, which='status'): - return self.__withBugDo(lambda b: b.getwhiteboard(which=which)) - - def appendwhiteboard(self, text, which='status'): - return self.__withBugDo(lambda b: b.appendwhiteboard(text, which=which)) - - def prependwhiteboard(self, text, which='status'): - return self.__withBugDo(lambda b: b.prependwhiteboard(text, which=which)) - - def setwhiteboard(self, text, which='status'): - return self.__withBugDo(lambda b: b.setwhiteboard(text, which=which)) - diff --git a/python/report/plugins/__init__.py b/python/report/plugins/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/python/report/plugins/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python/report/plugins/bugzilla/__init__.py b/python/report/plugins/bugzilla/__init__.py deleted file mode 100644 index 86df739..0000000 --- a/python/report/plugins/bugzilla/__init__.py +++ /dev/null @@ -1,363 +0,0 @@ -""" - A Report plugin to send a report to bugzilla.redhat.com. - Copyright (C) 2009 Red Hat, Inc - - Author(s): Gavin Romig-Koch - - Much of the code in this module is derived from code written by - Chris Lumens . - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import os -import report.io as iomodule -from report.io import DisplayFailMessage -from report.io import DisplaySuccessMessage -from report import _report as _ - -def labelFunction(label): - if label: - return label - retValue = displayURL(optionsDict) - if retValue.startswith("http://"): - retValue = retValue[len("http://"):] - if retValue.startswith("https://"): - retValue = retValue[len("https://"):] - return retValue - -def descriptionFunction(optionsDict): - if optionsDict.has_key("description"): - return optionsDict["description"] - return "Send report to " + displayURL(optionsDict) - -def displayURL(optionsDict): - if optionsDict.has_key("displayURL"): - return optionsDict["displayURL"] - returnURL = bugURL(optionsDict) - if returnURL.endswith("/xmlrpc.cgi"): - returnURL = returnURL[:len(returnURL) - len("/xmlrpc.cgi")] - return returnURL - -def bugURL(optionsDict): - if 'bugURL' in optionsDict: - return optionsDict["bugURL"] - host = "bugzilla.redhat.com" - if 'bugzilla_host' in optionsDict: - host = optionsDict["bugzilla_host"] - return "https://" + host + "/xmlrpc.cgi" - -def report(signature, io, optionsDict): - if not io: - DisplayFailMessage(None, _("No IO"), - _("No io provided.")) - return False - - if 'pythonUnhandledException' in signature: - fileName = signature["pythonUnhandledException"].asFileName() - fileDescription = "Attached traceback automatically from %s." % signature["component"].asString() - elif 'simpleFile' in signature: - fileName = signature['simpleFile'].asFileName() - fileDescription = "Attached file %s." % (signature['simpleFile'].asFileName(),) - else: - fileName = None - fileDescription = None - - if 'product' in signature: - product = signature['product'].asString() - else: - product = filer.getProduct() - - if 'version' in signature: - version = signature['version'].asString() - else: - version = filer.getVersion() - - bzfiler = filer.BugzillaFiler(bugURL(optionsDict), - displayURL(optionsDict), - version, product) - - # must pass a component - if 'component' in signature: - component = signature["component"].asString() - elif 'testing_component' in optionsDict: - component = optionsDict["testing_component"] - else: - component = None - - return sendToBugzilla(component, - signature, - io, - bzfiler, - optionsDict, - fileName, - fileDescription) - -import filer -# -# This function was abstracted from similar code in both python-meh and -# setroubleshoot. Beyond parameterizing this code, and using IO, this -# code differs from those others in that this version includes the -# 'component' in the .query for duplicates. -# -def sendToBugzilla(component, signature, io, bzfiler, - optionsDict, fileName, fileDescription): - - import rpmUtils.arch - - class BugzillaCommunicationException (Exception): - pass - - def withBugzillaDo(bz, fn): - try: - retval = fn(bz) - return retval - except filer.CommunicationError, e: - msg = _("Your bug could not be filed due to the following error " \ - "when communicating with bugzilla:\n\n%s" % str(e)) - DisplayFailMessage(io, _("Unable To File Bug"), msg) - raise BugzillaCommunicationException() - - except (TypeError, ValueError), e: - msg = _("Your bug could not be filed due to bad information in " \ - "the bug fields. This is most likely an error in " \ - "the bug filing program:\n\n%s" % str(e)) - DisplayFailMessage(io, _("Unable To File Bug"), msg) - raise BugzillaCommunicationException() - - try: - if not bzfiler: - if 'product' in signature: - product = signature['product'].asString() - else: - product = filer.getProduct() - - if 'version' in signature: - version = signature['version'].asString() - else: - version = filer.getVersion() - - bzfiler = filer.BugzillaFiler("https://bugzilla.redhat.com/xmlrpc.cgi", - "http://bugzilla.redhat.com", - version, product) - - if not bzfiler or not bzfiler.supportsFiling() or not bzfiler.bugUrl: - DisplayFailMessage(io, _("Bug Filing Not Supported"), - _("Your distribution does not provide a " \ - "supported bug filing system, so you " \ - "cannot save your exception this way.")) - return False - - bugzilla_host = os.path.basename(os.path.dirname(bzfiler.bugUrl)) - - loginResult = io.queryLogin(bugzilla_host) - if not loginResult: - return None - - if 'username' not in loginResult and \ - 'password' not in loginResult: - DisplayFailMessage(io, _("No Login Information"), - _("Please provide a valid username and password.")) - return False - - try: - withBugzillaDo(bzfiler, lambda b: b.login(loginResult['username'], - loginResult['password'])) - except filer.LoginError: - DisplayFailMessage(io, _("Unable To Login"), - _("There was an error logging into %s " \ - "using the provided username and " \ - "password.") % bzfiler.displayUrl) - return False - - io.updateLogin(bugzilla_host, loginResult) - - # grab summary and description if we have it - if 'summary' in signature: - summary = signature['summary'].asString() - else: - summary = None - - if 'description' in signature: - description = signature['description'].asString() - else: - description = None - - # figure out whether to attach to an existing bug, create a new bug, - # or search for matching bugs - if 'ticket' in optionsDict: - bug_number = optionsDict['ticket'] - bug = (withBugzillaDo(bzfiler, - lambda b: b.getbug(bug_number))) - - if not bug or bug == "": - DisplayFailMessage(io, _("Bug not found"), - _("Unable to find bug %s" % bug_number)) - return False - else: - buglist = [bug] - wb = "" - - elif 'localhash' in signature and 'hashmarkername' in signature: - # Are there any existing bugs with this hash value? If so we - # will just add any attachment to the bug report and put the - # reporter on the CC list. Otherwise, we need to create a new bug. - wb = "%s_trace_hash:%s" % (signature['hashmarkername'].asString(), - signature['localhash'].asString()) - buglist = withBugzillaDo(bzfiler, lambda b: b.query( - {'status_whiteboard': wb, - 'status_whiteboard_type':'allwordssubstr', - 'bug_status': []})) - - elif 'component' in signature and (fileDescription or - ('description' in signature)): - # then we should just go ahead and create a new case - wb = "" - buglist = [] - - else: - # ask create or attach? - choice_attach = 4 - choice_new = 5 - - choices = [ - iomodule.ChoiceValue(_("Create new bug"), _("Create a new bug and attach report to it."), choice_new), - iomodule.ChoiceValue(_("Attach to existing bug"), _("Attach report to an existing bug."), choice_attach) - ] - - choice = io.queryChoice(_("Do you want to attach the report to an existing bug or create a new bug?"), choices) - - if choice is None: - return None - - elif choice == choice_new: - wb = "" - buglist = [] - - if 'component' not in signature: - component = io.queryField('Enter component for new bug'); - if component is None: - return None - component = component.strip() - - if summary == None: - summary = io.queryField('Enter summary for new bug'); - if summary == None: - return None - summary = summary.strip() - - if description == None: - description = io.queryField( - 'Enter description for new bug'); - if description is None: - return None - description = description.strip() - - else: - bug_number = io.queryField("Enter existing bug number") - if bug_number == None: - return None - - bug = (withBugzillaDo(bzfiler, - lambda b: b.getbug(bug_number))) - - if not bug or bug == "": - DisplayFailMessage(io, _("Bug not found"), - _("Unable to find bug %s" % bug_number)) - return False - else: - buglist = [bug] - wb = "" - - - if not buglist or len(buglist) == 0: - - # cleanup summary and description - if not summary or not summary.strip(): - summary = "New bug for %s" % (component,) - - if not description or not description.strip(): - if fileDescription: - description = fileDescription - else: - description = '' - - bug = withBugzillaDo(bzfiler, lambda b: b.createbug( - product=bzfiler.getproduct(), - component=component, - version=bzfiler.getversion(), - platform=rpmUtils.arch.getBaseArch(), - bug_severity="medium", - priority="medium", - op_sys="Linux", - bug_file_loc="http://", - summary=summary, - comment=description, - status_whiteboard=wb)) - - if fileName: - if not fileDescription: - fileDescription = "" - withBugzillaDo(bug, lambda b: b.attachfile(fileName, fileDescription, - contenttype="text/plain", - filename=os.path.basename(fileName))) - - # Tell the user we created a new bug for them and that they should - # go add a descriptive comment. - bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) - - bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) - - DisplaySuccessMessage(io, _("Bug Created"), - _("A new bug has been created with your information added. " - "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:"), - bugURL, - bugDisplayURL) - return True - else: - bug = buglist[0] - if fileName: - if not fileDescription: - fileDescription = "" - - withBugzillaDo(bug, lambda b: b.attachfile(fileName, fileDescription, - contenttype="text/plain", - filename=os.path.basename(fileName))) - withBugzillaDo(bug, lambda b: b.addCC(loginResult['username'])) - - # Tell the user which bug they've been CC'd on and that they should - # go add a descriptive comment. - bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) - - bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) - - DisplaySuccessMessage(io, _("Bug Updated"), - _("A bug with your information already exists. Your account and information has " - "been added to this bug. Please add additional descriptive information to the " - "following bug:"), - bugURL, - bugDisplayURL) - - return True - - except BugzillaCommunicationException: - # this indicates that doWithBugzilla caught some problem - # communicating with bugzilla and displayed a message about it - # and all we want to do now is get out of sendToBugzilla - return False - - diff --git a/python/report/plugins/bugzilla/filer.py b/python/report/plugins/bugzilla/filer.py deleted file mode 100644 index 1104717..0000000 --- a/python/report/plugins/bugzilla/filer.py +++ /dev/null @@ -1,504 +0,0 @@ -# Copyright (C) 2008, 2009, 2010 Red Hat, Inc. -# All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Author(s): Chris Lumens -# Gavin Romig-Koch -# - -import socket -import xmlrpclib -import os -from report import _report as _ -import report.release_information - -_hardcoded_default_product = 'Fedora' -_hardcoded_default_version = 'rawhide' -_hardcoded_default_product_for_bugzilla = _hardcoded_default_product - -def getProduct(): - product = report.release_information.getProduct() - if product: - return product - return _hardcoded_default_product - -def getVersion(): - version = report.release_information.getVersion() - if version: - return version - return _hardcoded_default_version - -class LoginError(Exception): - """An error occurred while logging into the bug reporting system.""" - def __init__(self, bugUrl, username): - self.bugUrl = bugUrl - self.username = username - - def __str__(self): - return "Could not login to %s with username %s" % (self.bugUrl, self.username) - -class CommunicationError(Exception): - """Some miscellaneous error occurred while communicating with the - bug reporting system. This could include XML-RPC errors, passing - bad data, or network problems.""" - def __init__(self, msg): - self.msg = msg - - def __str__(self): - return "Error communicating with bug system: %s" % self.msg - - -# These classes don't do anything except say that automated bug filing are not -# supported. They also define the interface that concrete classes should use, -# as this is what will be expected by exception.py. -class AbstractFiler(object): - """The base class for Filer objects. This is an abstract class. - - Within this class's help, Bug refers to a concrete AbstractBug subclass - and Filer refers to a concrete AbstractFiler subclass. - - A Filer object communicates with a bug filing system - like bugzilla - - that a distribution uses to track defects. Install classes specify - what bug filing system they use by instantiating a subclass of - AbstractFiler. The intention is that each subclass of AbstractFiler - will make use of some system library to handle the actual communication - with the bug filing system. For now, all systems will be assumed to act - like bugzilla. - - Methods in this class should raise the following exceptions: - - CommunicationError -- For all problems communicating with the remote - bug filing system. - LoginError -- For invalid login information. - ValueError -- For all other operations where the client - supplied values are not correct. - """ - def __init__(self, bugUrl, displayUrl, version, product): - """Create a new AbstractFiler instance. This method need not be - overridden by subclasses. - - bugUrl -- The XML-RPC interface to the bug tracking system. - displayUrl -- The URL to use in the UI. - product -- The name of the product we should attempt to file - bugs against. This must be set. - version -- The version of the product we should attempt to - file bugs against. This must be set. - """ - self.bugUrl = bugUrl - self.displayUrl = displayUrl - self.version = str(version) - self.product = product - - def login(self, username, password): - """Using the given username and password, attempt to login to the - bug filing system. This method must be provided by all subclasses, - and should raise LoginError if login is unsuccessful. - """ - raise NotImplementedError - - def createbug(self, *args, **kwargs): - """Create a new bug. The kwargs dictionary is all the arguments that - should be used when creating the new bug and is entirely up to the - subclass to handle. This method must be provided by all subclasses. - On success, it should return a Bug instance. - """ - raise NotImplementedError - - def getbug(self, id): - """Search for a bug given by id and return it. This method must be - provided by all subclasses. On success, it should return a Bug - instance. On error, it should return an instance that is empty. - """ - raise NotImplementedError - - def getbugs(self, idlist): - """Search for all the bugs given by the IDs in idlist and return. - This method must be provided by all subclasses. On success, it - should return a list of Bug instances, or an empty instance for - invalid IDs. - """ - raise NotImplementedError - - def getproduct(self): - """Verify that self.product is a valid product name. If it is, return - that same product name. If not, return self.defaultProduct. This - method queries the bug filing system for a list of valid products. - It must be provided by all subclasses. - """ - raise NotImplementedError - - def getversion(self): - """Verify that self.version is a valid version number for the product - name self.product. If it is, return that same version number as a - string. If not, return "rawhide" if it exists or the latest version - number otherwise. This method queries the bug filing system for a - list of valid versions numbers. It must be provided by all - subclasses. - """ - raise NotImplementedError - - def query(self, query): - """Perform the provided query and return a list of Bug instances that - meet the query. What the query is depends on the exact bug filing - system, though it will be treated as a dictionary of bug attributes - since this is what bugzilla expects. Other filing systems will need - to take extra work to munge this data into the expected format. - This method must be provided by all subclasses. - """ - raise NotImplementedError - - def supportsFiling(self): - """Does this class support filing bugs? All subclasses should override - this method and return True, or automatic filing will not work. - Automatic filing will not be attempted on unknown products. - """ - return False - -class AbstractBug(object): - """The base class for Bug objects. This is an abstract class. - - Within this class's help, Bug refers to a concrete AbstractBug subclass - and Filer refers to a concrete AbstractFiler subclass. - - A Bug object represents one single bug within a Filer. This is where - most of the interesting stuff happens - attaching files, adding comments - and email addresses, and modifying whiteboards. Subclasses of this - class are returned by most operations within a Filer subclass. For now, - all bugs will be assumed to act like bugzilla's bugs. - - Bug objects wrap objects in the underlying module that communicates with - the bug filing system. For example, the bugzilla filer uses the - python-bugzilla module to communicate. This module has its own Bug - object. So, BugzillaBug wraps that object. Therefore, Bugs may be - created out of existing BugzillaBugs or may create their own if - necessary. - - Methods in this class should raise the following exceptions: - - CommunicationError -- For all problems communicating with the remote - bug filing system. - ValueError -- For all other operations where the client - supplied values are not correct (invalid - resolution, status, whiteboard, etc.). - """ - def __init__(self, filer, bug=None, *args, **kwargs): - """Create a new Bug instance. It is recommended that subclasses - override this method to add extra attributes. - - filer -- A reference to a Filer object used when performing - certain operations. This may be None if it is not - required by the Filer or Bug objects. - bug -- If None, the filer-specific code should create a new - bug object. Otherwise, the filer-specific code - should use the provided object as needed. - args, kwargs -- If provided, these arguments should be passed as-is - when creating a new underlying bug object. This - only makes sense if bug is not None. - """ - self.filer = filer - - def __str__(self): - raise NotImplementedError - - def __repr__(self): - raise NotImplementedError - - def addCC(self, address): - """Add the provided email address to this bug. This method must be - provided by all subclasses, and return some non-None value on - success. - """ - raise NotImplementedError - - def addcomment(self, comment): - """Add the provided comment to this bug. This method must be provided - by all subclasses, and return some non-None value on success. - """ - raise NotImplementedError - - def attachfile(self, file, description, **kwargs): - """Attach the filename given by file, with the given description, to - this bug. If provided, the given kwargs will be passed along to - the Filer when attaching the file. These args may be useful for - doing things like setting the MIME type of the file. This method - must be provided by all subclasses and return some non-None value - on success. - """ - raise NotImplementedError - - def close(self, resolution, dupeid=0, comment=''): - """Close this bug with the given resolution, optionally closing it - as a duplicate of the provided dupeid and with the optional comment. - resolution must be a value accepted by the Filer. This method must - be provided by all subclasses and return some non-None value on - success. - """ - raise NotImplementedError - - def id(self): - """Return this bug's ID number. This method must be provided by all - subclasses. - """ - raise NotImplementedError - - def setstatus(self, status, comment=''): - """Set this bug's status and optionally add a comment. status must be - a value accepted by the Filer. This method must be provided by all - subclasses and return some non-None value on success. - """ - raise NotImplementedError - - def setassignee(self, assigned_to='', reporter='', comment=''): - """Assign this bug to the person given by assigned_to, optionally - changing the reporter and attaching a comment. assigned_to must be - a valid account in the Filer. This method must be provided by all - subclasses and return some non-None value on success. - """ - raise NotImplementedError - - def getwhiteboard(self, which=''): - """Get the given whiteboard from this bug and return it. Not all bug - filing systems support the concept of whiteboards, so this method - is optional. - """ - return "" - - def appendwhiteboard(self, text, which=''): - """Append the given text to the given whiteboard. Not all bug filing - systems support the concept of whiteboards, so this method is - optional. If provided, it should return some non-None value on - success. - """ - return True - - def prependwhiteboard(self, text, which=''): - """Put the given text at the front of the given whiteboard. Not all - bug filing systems support the concept of whiteboards, so this - method is optional. If provided, it should return some non-None - value on success. - """ - return True - - def setwhiteboard(self, text, which=''): - """Set the given whiteboard to be the given text. Not all bug filing - systems support the concept of whiteboards, so this method is - optional. If provided, it should return some non-None value on - success. - """ - return True - - -# Concrete classes for automatically filing bugs against Bugzilla instances. -# This requires the python-bugzilla module to do almost all of the real work. -# We basically just make some really thin wrappers around it here since we -# expect all bug filing systems to act similar to bugzilla. -class BugzillaFiler(AbstractFiler): - def __withBugzillaDo(self, fn): - try: - retval = fn(self._bz) - return retval - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def __init__(self, bugUrl, displayUrl, version, product): - AbstractFiler.__init__(self, bugUrl, displayUrl, version, product) - self._bz = None - - def login(self, username, password): - import bugzilla - - try: - self._bz = bugzilla.Bugzilla(url=self.bugUrl) - retval = self._bz.login(username, password) - except socket.error, e: - raise CommunicationError(str(e)) - - if not retval: - raise LoginError(self.bugUrl, username) - - return retval - - def createbug(self, *args, **kwargs): - whiteboards = [] - - for (key, val) in kwargs.items(): - if key.endswith("_whiteboard"): - wb = key.split("_")[0] - whiteboards.append((wb, val)) - kwargs.pop(key) - - if key == "platform": - platformLst = self.__withBugzillaDo(lambda b: b._proxy.Bug.legal_values({'field': 'platform'})) - if not val in platformLst['values']: - kwargs[key] = platformLst['values'][0] - - bug = self.__withBugzillaDo(lambda b: b.createbug(**kwargs)) - for (wb, val) in whiteboards: - bug.setwhiteboard(val, which=wb) - - return BugzillaBug(self, bug=bug) - - def getbug(self, id): - return BugzillaBug(self, bug=self.__withBugzillaDo(lambda b: b.getbug(id))) - - def getbugs(self, idlist): - lst = self.__withBugzillaDo(lambda b: b.getbugs(idlist)) - return map(lambda b: BugzillaBug(self, bug=b), lst) - - def getproduct(self): - details = self.__withBugzillaDo(lambda b: b.getproducts()) - for d in details: - if d['name'] == self.product: - return self.product - - # Extend product with high order number of version - product_with_version = self.product + ' ' + self.version.split('.')[0] - for d in details: - if d['name'] == product_with_version: - return product_with_version - - # If the product given to us by the caller isn't valid, fall back - # to asking the running system and then to something hard coded. - defaultProduct = getProduct() - for d in details: - if d['name'] == defaultProduct: - return defaultProduct - - product_with_version = getProduct() + ' ' + getVersion().split('.')[0] - for d in details: - if d['name'] == product_with_version: - return product_with_version - - return _hardcoded_default_product_for_bugzilla - - def getversion(self): - # Convert all version numbers from bugzilla into strings. Sometimes - # bugzilla gives us strings ("rawhide", "development"), sometimes it - # gives us integers (11, 12), and sometimes it gives us floats - # (5.4, 5.5, 6.0). - details = self.__withBugzillaDo(lambda b: b._proxy.bugzilla.getProductDetails(self.getproduct())) - bugzillaVers = map(str, details[1]) - bugzillaVers.sort() - - # Double check to make sure this is a string. - ver = str(self.version) - - # If the version given to us by the caller isn't valid, fall back to - # asking the running system and then to something hard coded. - if ver in bugzillaVers: - return str(self.version) - - PossibleSuffixes = [ "-Alpha", "-Beta" ] - for suffix in PossibleSuffixes: - if ver.endswith(suffix): - shortver = ver[0:-len(suffix)] - if shortver in bugzillaVers: - return shortver - - defaultVersion = getVersion() - if defaultVersion in bugzillaVers: - return defaultVersion - - return _hardcoded_default_version - - def query(self, query): - lst = self.__withBugzillaDo(lambda b: b.query(query)) - return map(lambda b: BugzillaBug(self, bug=b), lst) - - def supportsFiling(self): - return True - -class BugzillaBug(AbstractBug): - def __withBugDo(self, fn): - try: - retval = fn(self._bug) - return retval - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def __init__(self, filer, bug=None, *args, **kwargs): - import bugzilla - - self.filer = filer - - if not bug: - self._bug = bugzilla.Bug(self.filer, *args, **kwargs) - else: - self._bug = bug - - def __str__(self): - return self._bug.__str__() - - def __repr__(self): - return self._bug.__repr__() - - def addCC(self, address): - try: - return self.filer._bz._updatecc(self._bug.bug_id, [address], 'add') - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def addcomment(self, comment): - return self.__withBugDo(lambda b: b.addcomment(comment)) - - def attachfile(self, file, description, **kwargs): - try: - return self.filer._bz.attachfile(self._bug.bug_id, file, description, **kwargs) - except xmlrpclib.ProtocolError, e: - raise CommunicationError(str(e)) - except xmlrpclib.Fault, e: - raise ValueError(str(e)) - except socket.error, e: - raise CommunicationError(str(e)) - - def id(self): - return self._bug.bug_id - - def close(self, resolution, dupeid=0, comment=''): - return self.__withBugDo(lambda b: b.close(resolution, dupeid=dupeid, - comment=comment)) - - def setstatus(self, status, comment=''): - return self.__withBugDo(lambda b: b.setstatus(status, comment=comment)) - - def setassignee(self, assigned_to='', reporter='', comment=''): - return self.__withBugDo(lambda b: b.setassignee(assigned_to=assigned_to, - reporter=reporter, - comment=comment)) - - def getwhiteboard(self, which='status'): - return self.__withBugDo(lambda b: b.getwhiteboard(which=which)) - - def appendwhiteboard(self, text, which='status'): - return self.__withBugDo(lambda b: b.appendwhiteboard(text, which=which)) - - def prependwhiteboard(self, text, which='status'): - return self.__withBugDo(lambda b: b.prependwhiteboard(text, which=which)) - - def setwhiteboard(self, text, which='status'): - return self.__withBugDo(lambda b: b.setwhiteboard(text, which=which)) - diff --git a/python/report/plugins/ftp/__init__.py b/python/report/plugins/ftp/__init__.py deleted file mode 100644 index 1407236..0000000 --- a/python/report/plugins/ftp/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -""" - a report plugin to send to reports to ftp sites - Copyright (C) 2010 Red Hat, Inc - - Author(s): Adam Stokes - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import os -import socket - -import report as reportmodule - -from report.io import DisplayFailMessage -from report.io import DisplaySuccessMessage -from report import _report as _ - - -def labelFunction(label): - if label: - return label - return 'ftp' - -def descriptionFunction(optionsDict): - if optionsDict.has_key('description'): - return optionsDict['description'] - return 'ftp plugin' - -def report(signature, io, optionsDict): - if not io: - DisplayFailMessage(None, _("No IO"), - _("No io provided.")) - return False - - fileName = reportmodule.serializeToFile(signature,io) - - if fileName is None: - return None - - elif fileName is False: - return False - - else: - return ftpFile(fileName, file(fileName), io, optionsDict) - -def ftpFile(fileName, fileBlob, io, optionsDict): - username = None - password = None - - from urlparse import urlparse - if optionsDict.has_key('urldir'): - ftpserver = optionsDict['urldir'] - else: - ftpserver = io.queryField(_("Enter remote FTP directory as URL")) - if ftpserver is None: - return None - - if not ftpserver.startswith("ftp://"): - ftpserver = "ftp://" + ftpserver - - scheme, netloc, path, params, query, fragment = urlparse(ftpserver) - login = None - # check for user/pass - if netloc.find('@') > 0: - login, netloc = netloc.split('@') - # split user/pass - if login and login.find(':') > 0: - username, password = login.split(':') - # split netloc/port - if netloc.find(':') > 0: - netloc, port = netloc.split(':') - else: - port = 21 - - try: - import ftplib - ftp = ftplib.FTP() - ftp.connect(netloc, port) - if username and password: - ftp.login(username, password) - else: - ftp.login() - ftp.cwd(path) - ftp.set_pasv(True) - ftp.storbinary('STOR %s' % os.path.basename(fileName), fileBlob) - ftp.quit() - except ftplib.all_errors, e: - DisplayFailMessage(io, _("Upload failed"), - _("Upload has failed for remote path: %(ftpserver)s, %(error)s" % {'ftpserver':ftpserver,'error':e})) - return False - else: - DisplaySuccessMessage(io, _("Upload Successful"), - _("The signature was successfully uploaded to:"), - None, ftpserver + '/' + os.path.basename(fileName)) - return True diff --git a/python/report/plugins/localsave/__init__.py b/python/report/plugins/localsave/__init__.py deleted file mode 100644 index fd68cfe..0000000 --- a/python/report/plugins/localsave/__init__.py +++ /dev/null @@ -1,110 +0,0 @@ -""" - A Report plugin to save a report to a local file. - Copyright (C) 2009 Red Hat, Inc - - Author: Gavin Romig-Koch - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import os -import stat -import shutil - -from report import io as iomodule -from report.io import DisplaySuccessMessage -from report.io import DisplayFailMessage -import report as reportmodule -from report import _report as _ - -def labelFunction(label): - if label: - return label - return 'localsave' -def descriptionFunction(optionsDict): - if optionsDict.has_key('description'): - return optionsDict['description'] - return 'localsave plugin' - -def report(signature, io, optionsDict): - if not io: - DisplayFailMessage(None, _("No IO"), - _("No io provided.")) - return False - - fileName = reportmodule.serializeToFile(signature,io) - - if fileName is None: - return None - - elif fileName is False: - return False - - else: - return copyFile(fileName, io, optionsDict) - -def copyFile(fileName, io, optionsDict): - - if optionsDict.has_key('path'): - dirpath = optionsDict['path'] - else: - dirpath = io.queryField(_("directory to store report in")) - if dirpath == None: - return None - if not dirpath or dirpath.strip() == "": - DisplayFailMessage(io, _("local save Failed"), - _("directory name required")) - return False - - if os.path.exists(dirpath): - mode = os.stat(dirpath)[stat.ST_MODE] - if not stat.S_ISDIR(mode): - DisplayFailMessage(io, _("local save Failed"), - _("'%s' already exists, but is not a directory") % dirpath) - return False - else: - createp = io.queryChoice(_("'%s' does not exist, create it?") % dirpath, - [ iomodule.ChoiceValue(_("Yes"), - _("Create the directory"), - True), - iomodule.ChoiceValue(_("No"), - _("Do not create the directory"), - False) ]) - if createp is None: - return None - - elif createp: - try: - os.makedirs(dirpath) - - except EnvironmentError, e: - DisplayFailMessage(io, _("local save Failed"), - _("could not create '%(dir)s': %(error)s") % {'dir':dirpath,'error':str(e)}) - return False - - target = "%s/%s" % (dirpath, os.path.basename(fileName)) - try: - if os.path.realpath(fileName) != os.path.realpath(target): - shutil.copyfile(fileName, target) - - except EnvironmentError, e: - DisplayFailMessage(io, _("local save Failed"), - _("could not save report to '%(target)s': %(error)s") % {'target':target,'error':str(e)}) - return False - - DisplaySuccessMessage(io, _("local save Successful"), - _("The signature was successfully copied to:"), - None, target) - return True diff --git a/python/report/plugins/scp/__init__.py b/python/report/plugins/scp/__init__.py deleted file mode 100644 index 480fe2d..0000000 --- a/python/report/plugins/scp/__init__.py +++ /dev/null @@ -1,178 +0,0 @@ -""" - A Report plugin to send a report to another host using SCP. - Copyright (C) 2009 Red Hat, Inc - - Author(s): Gavin Romig-Koch - Adam Stokes - - Much of the code in this module was derived from code written by - Chris Lumens and Will Woods . - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import os -import pty -from report.io import DisplaySuccessMessage -from report.io import DisplayFailMessage -from report import _report as _ - -import report as reportmodule - -def labelFunction(label): - if label: - return label - return 'scp' -def descriptionFunction(optionsDict): - if optionsDict.has_key('description'): - return optionsDict['description'] - return "scp the problem to a given host and filename" - -def report(signature, io, optionsDict): - if not io: - DisplayFailMessage(None, _("No IO"), - _("No io provided.")) - return False - - fileName = reportmodule.serializeToFile(signature,io) - - if fileName is None: - return None - - elif fileName is False: - return False - - else: - return copyFileToRemote(fileName, io, optionsDict) - -def scpAuthenticate(master, childpid, password): - childoutput = "" - while True: - # Read up to password prompt. Propagate OSError exceptions, which - # can occur for anything that causes scp to immediately die (bad - # hostname, host down, etc.) - buf = os.read(master, 4096) - childoutput += buf - if buf.lower().find("password: ") != -1: - os.write(master, password+"\n") - # read the space and newline that get echoed back - buf = os.read(master, 2) - childoutput += buf - break - - while True: - try: - buf = os.read(master, 4096) - childoutput += buf - except (OSError, EOFError): - break - - (pid, childstatus) = os.waitpid (childpid, 0) - return (childstatus,childoutput) - -def copyFileToRemote(exnFileName, io, optionsDict): - - if optionsDict.has_key('host'): - host = optionsDict['host'] - else: - host = io.queryField("host") - if host == None: - return None - if not host or host.strip() == "": - DisplayFailMessage(io, _("No Host"), - _("Please provide a valid hostname")) - return False - - if host.find(":") != -1: - (host, port) = host.split(":") - - # Try to convert the port to an integer just as a check to see - # if it's a valid port number. If not, they'll get a chance to - # correct the information when scp fails. - try: - int(port) - portArgs = ["-P", port] - except ValueError: - portArgs = [] - else: - portArgs = [] - - loginResult = io.queryLogin(host) - if not loginResult: - return None - - if 'username' not in loginResult and \ - 'password' not in loginResult: - DisplayFailMessage(io, _("Login Input Failed"), - _("Please provide a valid username and password")) - return False - - if 'path' in optionsDict: - path = optionsDict['path'] - else: - path = io.queryField("path") - if path == None: - return None - if not path or path.strip() == "": - DisplayFailMessage(io, _("No Path"), - _("Please provide a path")) - return False - - target = "%s@%s:%s" % (loginResult['username'], host, path) - - # Fork ssh into its own pty - (childpid, master) = pty.fork() - if childpid < 0: - raise RuntimeError("Could not fork process to run scp") - elif childpid == 0: - # child process - run scp - args = ["scp", - "-oGSSAPIAuthentication=no", - "-oHostbasedAuthentication=no", - "-oPubkeyAuthentication=no", - "-oChallengeResponseAuthentication=no", - "-oPasswordAuthentication=yes", - "-oNumberOfPasswordPrompts=1", - "-oStrictHostKeyChecking=no", - "-oUserKnownHostsFile=/dev/null", - ] + portArgs + \ - [exnFileName, target] - os.execvp("scp", args) - - # parent process - try: - (childstatus,childoutput) = scpAuthenticate(master, childpid, loginResult['password']) - except OSError, e: - DisplayFailMessage(io, _("scp failed"), - _("OSError during scp file from %(filename)s to %(target)s: %(error)s") % - {'filename':exnFileName,'target':target,'error':e}) - return False - - os.close(master) - - if os.WIFEXITED(childstatus) and os.WEXITSTATUS(childstatus) == 0: - io.updateLogin(host,loginResult) - DisplaySuccessMessage(io, _("scp Successful"), - _("The signature was successfully copied to:"), - None, target) - return True - else: - DisplayFailMessage(io, _("scp failed"), - (_("unexpected child status (%(childstatus)s) during scp\n" \ - "scp %(filename)s %(target)s\n%(childoutput)s") % - {'childstatus': childstatus, 'filename':exnFileName, - 'target':target, 'childoutput':childoutput})) - return False - diff --git a/python/report/plugins/strata/__init__.py b/python/report/plugins/strata/__init__.py deleted file mode 100644 index 5c69a1f..0000000 --- a/python/report/plugins/strata/__init__.py +++ /dev/null @@ -1,309 +0,0 @@ -""" - A Report plugin to send a report to the Strata API. - Copyright (C) 2009 Red Hat, Inc - - Author: Gavin Romig-Koch - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -""" - -import os -import stat -import shutil -import re - -from report import io as iomodule -from report.io import DisplaySuccessMessage -from report.io import DisplayFailMessage -import report as reportmodule -from report import _report as _ -import xml.etree.ElementTree as etree - -from report import release_information - -from .strata import post_signature, send_report_to_new_case, send_report_to_existing_case, strata_client_strerror - -def labelFunction(label): - if label: - return label - return 'strata' - -def descriptionFunction(optionsDict): - if optionsDict.has_key('description'): - return optionsDict['description'] - return 'strata plugin' - -def strataURL(optionsDict): - if optionsDict.has_key("strataURL"): - return optionsDict["strataURL"] - strata_host = "access.redhat.com" - if optionsDict.has_key("strata_host"): - strata_host = optionsDict["strata_host"] - return "http://" + strata_host + "/Strata" - - - -def report(signature, io, optionsDict): - - if not io: - DisplayFailMessage(None, _("No IO"), - _("No io provided.")) - return False - - file_list = [] - if signature.has_key("simpleFile"): - file_list.append((signature["simpleFile"].asFileName(), - signature["simpleFile"].fileName)) - - else: - filelocation = reportmodule.serializeAsSignature(signature) - if filelocation is None: - return None - elif filelocation is False: - return False - - file_list.append((filelocation,"report.xml")) - for (key,value) in signature.iteritems(): - if value.isBinary: - file_list.append((value.asFileName(),value.fileName)) - - if 'component' in signature: - component = signature['component'].asString() - else: - component = None - - if 'summary' in signature: - summary = signature['summary'].asString() - else: - summary = None - - if not summary: - if not component: - summary = "Case Created By Report Library" - else: - summary = "Case Created for %s" % (component,) - - if 'description' in signature: - description = signature['description'].asString() - else: - description = None - - if not description: - description = summary - - choice_attach = 4 - choice_new = 5 - if 'ticket' in optionsDict: - choice = choice_attach - - else: - choices = [ - iomodule.ChoiceValue(_("Create new case"), _("Create a new case and attach report to it."), choice_new), - iomodule.ChoiceValue(_("Attach to existing case"), _("Attach report to an existing case."), choice_attach) - ] - - choice = io.queryChoice(_("Do you want to attach the report to an existing case or create a new case?"), choices) - if choice is None: - return None - - - URL = strataURL(optionsDict) - cert_data = None - - if 'sslcertdata' in optionsDict: - cert_data = optionsDict['sslcertdata'] - - strata_host = os.path.basename(os.path.dirname(URL)) - loginResult = io.queryLogin(strata_host) - if not loginResult: - return None - - if 'username' not in loginResult and \ - 'password' not in loginResult: - DisplayFailMessage(io, _("Missing Login Information"), - _("Please provide a valid username and password.")) - return False - - - if choice == choice_new: - if 'product' in signature: - product = signature['product'].asString() - else: - product = release_information.getProduct() - - if 'version' in signature: - version = signature['version'].asString() - else: - version = release_information.getVersion() - - - # - # FIXME: - # In the case of a simpleFile report, when it is forwarded - # from one machine to another, it looses it's 'product' and - # 'version' information. So for these cases, whatever - # information we got is possibly bad. So make it good. - # - # The first fix needed here is to get good_xxx information - # from the server. The second fix is to allow someone - # to fix bad information with good. The third fix needed - # is to correct the simpleFile problem. - # - if signature.has_key("simpleFile"): - good_products = ['Red Hat Enterprise Linux'] - good_versions = ['6'] - if len(good_products) > 0 and product not in good_products: - product = good_products[0] - if len(good_versions) > 0 and version not in good_versions: - version = good_versions[0] - - (filelocation,filename) = file_list[0] - response = send_report_to_new_case(URL, - cert_data, - loginResult['username'], - loginResult['password'], - summary, description, - component, - product, - version, - None, - filename, - filelocation) - - if not response: - DisplayFailMessage(io, _("Case Creation Failed"), strata_client_strerror()) - return False - - title = _("Case Creation Response") - body = _("Case Creation Succeeded") - displayURL = "" - actualURL = "" - case_number = "" - - elif choice == choice_attach: - if 'ticket' in optionsDict: - case_number = optionsDict['ticket'] - else: - case_number = io.queryField("Enter existing case number") - - if case_number is None: - return None - - (filelocation,filename) = file_list[0] - response = send_report_to_existing_case(URL, - cert_data, - loginResult['username'], - loginResult['password'], - case_number, - None, - filename, - filelocation) - - if not response: - DisplayFailMessage(io, _("Report Attachement Failed"), strata_client_strerror()) - return False - - title = _("Report Attachment Response") - body = _("Report Attachment Succeded") - displayURL = "" - actualURL = "" - - io.updateLogin(strata_host, loginResult) - - try: - xml = etree.XML(response) - except Exception,e: - xml = None - - if xml: - for each in xml: - if each.tag == "title" and each.text: - title = each.text - elif each.tag == "body" and each.text: - body = each.text - elif each.tag == "URL": - if each.text: - displayURL = each.text - if 'href' in each.attrib and each.attrib['href']: - actualURL = each.attrib['href'] - else: - body = response - - if len(file_list) > 1 and not case_number and actualURL: - leading = actualURL - sep = '/' - trailing = '' - while leading and sep and not trailing: - (leading,sep,trailing) = leading.rpartition('/') - if trailing: - case_number = trailing - - if case_number and len(file_list) > 1: - for (filelocation,filename) in file_list[1:]: - response = send_report_to_existing_case(URL, - cert_data, - loginResult['username'], - loginResult['password'], - case_number, - None, - filename, - filelocation); - - if response: - try: - xml = etree.XML(response) - except Exception,e: - xml = None - - if xml: - for each in xml: - if each.tag == "title" and each.text: - if title: - title += "; " + each.text - else: - title = each.title - elif each.tag == "body" and each.text: - if body: - body += '\n' + each.text - else: - body = each.text - else: - if body: - body += '\n' + response - else: - body = response - - if 'buttonURLPattern' in optionsDict: - buttonURLPattern = optionsDict['buttonURLPattern'] - else: - buttonURLPattern = None - - if 'buttonURLRepl' in optionsDict: - buttonURLRepl = optionsDict['buttonURLRepl'] - else: - buttonURLRepl = None - - if buttonURLPattern and buttonURLRepl: - if actualURL: - newURL = re.sub(buttonURLPattern, - buttonURLRepl, - actualURL) - if displayURL == actualURL: - displayURL = newURL - actualURL = newURL - - DisplaySuccessMessage(io, title, body, actualURL, displayURL) - return True - diff --git a/python/report/plugins/strata/strata.py b/python/report/plugins/strata/strata.py deleted file mode 100644 index dc3d933..0000000 --- a/python/report/plugins/strata/strata.py +++ /dev/null @@ -1,21 +0,0 @@ -from ctypes import * - -strata_client_lib = CDLL('libstrata_client.so') - -post_signature = strata_client_lib.post_signature -post_signature.argtypes = [ c_char_p, c_char_p, c_char_p, c_char_p ] -post_signature.restype = c_char_p - -send_report_to_new_case = strata_client_lib.send_report_to_new_case -send_report_to_new_case.argtypes = [ c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p ] -send_report_to_new_case.restype = c_char_p - -send_report_to_existing_case = strata_client_lib.send_report_to_existing_case -send_report_to_existing_case.argtypes = [ c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p ] -send_report_to_existing_case.restype = c_char_p - -strata_client_strerror = strata_client_lib.strata_client_strerror -strata_client_strerror.argtypes = [] -strata_client_strerror.restype = c_char_p - - diff --git a/python/report/release_information.py b/python/report/release_information.py deleted file mode 100644 index 3d4870b..0000000 --- a/python/report/release_information.py +++ /dev/null @@ -1,142 +0,0 @@ -# Copyright (C) 2008, 2009, 2010 Red Hat, Inc. -# All rights reserved. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -# Author(s): Gavin Romig-Koch -# - -import os - -SYSTEM_RELEASE_PATHS = ["/etc/system-release","/etc/redhat-release"] -SYSTEM_RELEASE_DEPS = ["system-release", "redhat-release"] - -_hardcoded_default_product = "" -_hardcoded_default_version = "" - -def getProduct_fromRPM(): - try: - import rpm - ts = rpm.TransactionSet() - for each_dep in SYSTEM_RELEASE_DEPS: - mi = ts.dbMatch('provides', each_dep) - for h in mi: - if h['name']: - return h['name'].split("-")[0].capitalize() - - return "" - except: - return "" - -def getProduct_fromPRODUCT(): - try: - from pyanaconda import product - return product.productName - except: - try: - import product - return product.productName - except: - return "" - -def getProduct_fromFILE(): - for each_path in SYSTEM_RELEASE_PATHS: - if os.path.exists(each_path): - file = open(each_path, "r") - content = file.read() - if content.startswith("Red Hat Enterprise Linux"): - return "Red Hat Enterprise Linux" - - if content.startswith("Fedora"): - return "Fedora" - - i = content.find(" release") - if i > -1: - return content[0:i] - - return "" - -def getVersion_fromRPM(): - try: - import rpm - ts = rpm.TransactionSet() - for each_dep in SYSTEM_RELEASE_DEPS: - mi = ts.dbMatch('provides', each_dep) - for h in mi: - if h['version']: - return str(h['version']) - - return "" - except: - return "" - -def getVersion_fromFILE(): - for each_path in SYSTEM_RELEASE_PATHS: - if os.path.exists(each_path): - file = open(each_path, "r") - content = file.read() - if content.find("Rawhide") > -1: - return "rawhide" - - clist = content.split(" ") - i = clist.index("release") - return clist[i+1] - else: - return "" - -def getVersion_fromPRODUCT(): - try: - from pyanaconda import product - return product.productVersion - except: - try: - import product - return product.productVersion - except: - return "" - - -def getProduct(): - """Attempt to determine the product of the running system by first asking - rpm, and then falling back on a hardcoded default. - """ - product = getProduct_fromPRODUCT() - if product: - return product - product = getProduct_fromFILE() - if product: - return product - product = getProduct_fromRPM() - if product: - return product - - return _hardcoded_default_product - -def getVersion(): - """Attempt to determine the version of the running system by first asking - rpm, and then falling back on a hardcoded default. Always return as - a string. - """ - version = getVersion_fromPRODUCT() - if version: - return version - version = getVersion_fromFILE() - if version: - return version - version = getVersion_fromRPM() - if version: - return version - - return _hardcoded_default_version - diff --git a/python/report1/BugzillaReporter.py b/python/report1/BugzillaReporter.py index e69de29..1a800e9 100644 --- a/python/report1/BugzillaReporter.py +++ b/python/report1/BugzillaReporter.py @@ -0,0 +1,54 @@ +""" + Copyright (C) 2009 Red Hat, Inc + + Author: Gavin Romig-Koch + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + + +# +# This is experimental code that I'm keeping just for reference +# purposes. +# +# + + + +from filer import sendToBugzilla + +class HashedBugzillaReporter: + def __init__(self, actualURL, displayURL, fromSignature = defaultFromSignature, alternateFiler = None): + self.actualURL = actualURL + self.displayURL = displayURL + self.fromSignature = fromSignature + self.alternateFiler = alternateFiler + + def report(self, signature, parameters, io): + hashedReport = self.fromSignature( signature ) + + sendToBugzilla( hashedReport['component'], + hashedReport['hashmarkername'], + hashedReport['hashvalue'], + hashedReport['summary'], + hashedReport['firstComment'], + hashedReport['fileName'], + hashedReport['fileDescription'], + parameters['username'], + parameters['password'], + io, self.alternateFiler) + + + diff --git a/python/report1/__init__.py b/python/report1/__init__.py index e69de29..d04d136 100644 --- a/python/report1/__init__.py +++ b/python/report1/__init__.py @@ -0,0 +1,711 @@ +""" + The main entry point to the Report library. + Copyright (C) 2009 Red Hat, Inc + + Author(s): Gavin Romig-Koch + Adam Stokes + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import sys +import os +import os.path +import glob +import imputil +import tempfile +import tarfile +import exceptions +import re +import ConfigParser + +import xml.etree.ElementTree as etree + +from optparse import OptionParser +from report1 import io as iomodule +from report1.io import DisplayFailMessage +from report1.io import DisplaySuccessMessage + +from report1 import release_information + +import gettext +gettext_dir = "/usr/share/locale" +gettext_app = "report" +gettext.bindtextdomain(gettext_app, gettext_dir) + +def _report(msg): + return gettext.dgettext(gettext_app, msg) + +_ = lambda x: _report(x) + +# +# A Signature, for the purposes of this library, is a mapping of names +# to values (in Python terms a Dictionary). For maximum portablility, +# names must be ASCII alpha-numeric characters. Values should be +# types that conform to the SignatureValue api below. +# + +class SignatureValue: + # roughly an arbitrary value that can be collected, stored, and + # shipped over the wire. + # + # a SignatureValue can be any type that conforms to this protocol + # + # asString() - return a string representation of the data + # asFile() - return a file representation of the data + # asFileName() - return the name of a file (on the local file + # system) that contains the data, can be a temporary + # file + # + # isBinary - False if the data is a UTF-8 (or compatible) character stream, + # True if the data is not character data. + # isFile - was this created as a file + # fileName - the data has a system independent name + # this is generally _not_ where the data is actually + # stored. If you need access to the data use one of the + # asXXX() functions to get the data in the form you + # need it. + # + pass + +class StringSignatureValue: + def __init__(self, data, isBinary = False): + self._data = data + self._fileLocation = None + + self.isBinary = isBinary + self.isFile = False + + def asString(self): + return self._data + + def asFile(self): + return file(self.asFileName()) + + def asFileName(self): + if not self._fileLocation: + if self.isBinary: + mymode = 'w+b' + else: + mymode = 'w+' + tmp = tempfile.NamedTemporaryFile(mode=mymode,prefix="report-", + delete=False) + tmp.write(self._data) + self._fileLocation = tmp.name + tmp.close() + return self._fileLocation + + def __del__(self): + if self._fileLocation: + os.remove(self._fileLocation) + self._fileLocation = None + +class NamedFileSignatureValue: + def __init__(self, fileLocation, isBinary, fileName=None): + + # if the file can't be read for some reason (permissions, + # non-existance, etc.) better to notice that now, and + # throw an exception now - this will accomplish this + file(fileLocation).read(1) + + self._fileLocation = fileLocation + + self.isBinary = isBinary + self.isFile = True + if fileName != None: + self.fileName = fileName + else: + self.fileName = fileLocation + + def asString(self): + return file(self._fileLocation).read() + + def asFile(self): + return file(self._fileLocation) + + def asFileName(self): + return self._fileLocation + +class FileSignatureValue: + def __init__(self, afile, isBinary, fileName=None): + self._afile = afile + + self.isBinary = isBinary + self.isFile = True + self._fileLocation = None + if fileName != None: + self.fileName = fileName + else: + self.fileName = afile.name + + def asString(self): + if self._fileLocation: + return file(self._fileLocation).read() + else: + return self._afile.read() + + def asFile(self): + if self._fileLocation: + return file(self._fileLocation) + else: + return self._afile + + def asFileName(self): + if not self._fileLocation: + if self.isBinary: + mymode = 'w+b' + else: + mymode = 'w+' + tmp = tempfile.NamedTemporaryFile(mode=mymode,prefix="report-", + delete=False) + tmp.write(self._afile.read()) + self._fileLocation = tmp.name + tmp.close() + + self._afile.close() + self._afile = None + + return self._fileLocation + + def __del__(self): + if self._fileLocation: + os.remove(self._fileLocation) + self._fileLocation = None + +def addReleaseInformation(signature): + if not signature: + signature = {} + + if 'product' not in signature: + product = release_information.getProduct() + if product: + signature['product'] = StringSignatureValue(product) + + if 'version' not in signature: + version = release_information.getVersion() + if version: + signature['version'] = StringSignatureValue(version) + + return signature + +def createAlertSignature(component, hashmarkername, hashvalue, summary, alertSignature): + return addReleaseInformation( + { "component" : StringSignatureValue(component), + "hashmarkername" : StringSignatureValue(hashmarkername), + "localhash" : StringSignatureValue(hashvalue), + "summary" : StringSignatureValue(summary), + "description" : StringSignatureValue(alertSignature) } + ) + +def createPythonUnhandledExceptionSignature(component, hashmarkername, hashvalue, summary, description, exnFileName): + return addReleaseInformation( + { "component" : StringSignatureValue(component), + "hashmarkername" : StringSignatureValue(hashmarkername), + "localhash" : StringSignatureValue(hashvalue), + "summary" : StringSignatureValue(summary), + "description" : StringSignatureValue(description), + "pythonUnhandledException" : NamedFileSignatureValue(exnFileName,False) } + ) + +def createSimpleFileSignature(exnFileName, isBinary=True): + return addReleaseInformation( + { "simpleFile" : NamedFileSignatureValue(exnFileName, isBinary) } + ) + +def open_signature_file( filename, io, skipErrorMessage = False ): + try: + tar_file = None + if tarfile.is_tarfile( filename ): + tar_file = tarfile.open(filename, mode='r:*') + try: + xml_file = tar_file.extractfile("content.xml") + except KeyError: + if not skipErrorMessage: + DisplayFailMessage(io, (_("Signature File Format Error"), + _("file %s is a tarfile that does not contain a member " \ + "named 'context.xml'" % (filename,)))) + return False + + else: + xml_file = file(filename) + + except Exception,e: + if not skipErrorMessage: + DisplayFailMessage(io, _("Signature File Format Error"), + _("Failed to open file %(filename)s: %(error)s" % + {'filename':filename, 'error':e})) + return False + + + if not xml_file: + if not skipErrorMessage: + DisplayFailMessage(io, _("Signature File Format Error"), + _("Failed to open file %s" % (filename,))) + return xml_file + + try: + signature_tree = etree.parse( xml_file ) + + except Exception,e: + if not skipErrorMessage: + DisplayFailMessage(io, _("Signature File Format Error"), + _("Error while parseing: %(filename)s: %(error)s" % + {'filename':filename, 'error':e})) + return False + + if not signature_tree: + if not skipErrorMessage: + DisplayFailMessage(io, _("Signature File Format Error"), + _("Could not parse XML file %s" % (filename,))) + return signature_tree + + signature_root = signature_tree.getroot() + if signature_root.tag != "report" and not re.match(r"\{.*\}report", signature_root.tag): + if not skipErrorMessage: + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %(filename)s has document tag that is " \ + "not valid: %(signature)s" % + {'filename':filename,'signature':signature_root.tag})) + return False + + return (signature_root, tar_file) + +def isSignatureFile( filename ): + file_pair = open_signature_file( filename, None, skipErrorMessage = True ) + if not file_pair: + return file_pair + else: + return True + +def createSignatureFromFile( filename, io ): + + file_pair = open_signature_file( filename, io ) + if not file_pair: + return file_pair + + (signature_root, tar_file) = file_pair + + signature = {} + + for each in signature_root: + if each.tag != "binding" and not re.match(r"\{.*\}binding", each.tag): + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %(filename)s has document element that " \ + "has children with an invalid tag: %(tag)s" % + {'filename':filename,'tag':each.tag})) + return True + + if "name" in each.attrib: + name = each.attrib["name"] + else: + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %s has binding element that has no 'name' attribute" % (filename,))) + return False + + isBinary = False + if "type" in each.attrib: + if each.attrib["type"] == "binary": + isBinary = True + + fileName = None + if "fileName" in each.attrib: + if each.attrib["fileName"] != "": + fileName = each.attrib["fileName"] + + if "href" in each.attrib: + if not tar_file: + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %s has a binding with an 'href' but no content" % (filename,))) + return False + + try: + member_name = each.attrib['href'] + afile = tar_file.extractfile(member_name) + except KeyError: + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %(filename)s is a tarfile that does not contain a " \ + "member named '%(member)s'" % + {'filename':filename,'member':member_name})) + return False + + if not afile: + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %(filename)s is a tarfile that does not contain a " \ + "member named '%(member)s'" % + {'filename':filename,'member':member_name})) + return False + + + signature[name] = FileSignatureValue(afile, isBinary, fileName) + + else: + if "value" in each.attrib: + if each.text: + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %(filename)s has a binding, %(binding)s, " \ + "that has both a 'value' attribute, and a text child" % + {'filename':filename,'binding':name})) + return False + else: + value = each.attrib["value"] + else: + if each.text: + value = each.text + else: + DisplayFailMessage(io, _("Signature File Format Error"), + _("file %(filename)s has a binding, %(binding)s, " \ + "that has neither a 'value' attribute, or a text child" % + {'filename':filename,'binding':name})) + return False + + signature[name] = StringSignatureValue(value, isBinary) + + return signature + + +def buildChoices(signature, io, config, rptopts): + """ builds an array of choices """ + choices = [] + priorities = [] + choice = None + + (modulefile, modulepath, moduletype) = imputil.imp.find_module("plugins",sys.modules[__name__].__path__) + try: + alternatives = imputil.imp.load_module("report1.plugins", modulefile, modulepath, moduletype) + finally: + if modulefile: + modulefile.close() + + optionsDictStart = {} + + config_sections = config.sections() + if "main" in config_sections: + for eachOption in config.options("main"): + optionsDictStart[eachOption] = config.get("main",eachOption) + config_sections.remove("main") + + for eachSection in config_sections: + optionsDict = optionsDictStart.copy() + for eachOption in config.options(eachSection): + optionsDict[eachOption] = config.get(eachSection,eachOption) + + module = None + if "plugin" in optionsDict: + moduleName = optionsDict["plugin"] + else: + moduleName = eachSection + + try: + (modulefile, modulepath, moduletype) = imputil.imp.find_module(moduleName,alternatives.__path__) + module = imputil.imp.load_module("report1.plugins." + moduleName, modulefile, modulepath, moduletype) + + except ImportError as error: + if 'target' not in optionsDict or \ + optionsDict['target'] == eachSection: + DisplayFailMessage(io, _("Could Not Load Plugin"), + (_("The target '%(target)s' requires the plugin '%(plugin)s' which can't be loaded: ") % \ + {'target':eachSection, + 'plugin':moduleName}) + \ + str(error) + "\n" + \ + _("This target will be ignored.")) + + finally: + if modulefile: + modulefile.close() + + if module: + for k,v in rptopts.iteritems(): + optionsDict[k] = v + + if 'target' not in optionsDict: + this_choice = iomodule.ChoiceValue( \ + module.labelFunction(eachSection), + module.descriptionFunction(optionsDict), + (lambda module, optionsDict: + lambda signature, io : + module.report(signature, io, optionsDict))(module, optionsDict)) + if 'priority' in optionsDict: + try: + this_priority = int(optionsDict['priority']) + except ValueError: + this_priority = None + else: + this_priority = None + + if this_priority is None: + choices.append(this_choice) + priorities.append(this_priority) + + else: + for index in range(len(priorities)): + if priorities[index] is None \ + or this_priority < priorities[index]: + break; + else: + index = len(priorities) + + choices.insert(index, this_choice) + priorities.insert(index, this_priority) + + elif optionsDict['target'] == module.labelFunction(eachSection) : + return (lambda module, optionsDict: + lambda signature, io : + module.report(signature, io, optionsDict))(module, optionsDict) + + + # if we haven't loaded any choices from the config files, + # assume they are not readable, load all plugins as choices + if len(choices) == 0: + + # from the 'alternatives' directory, get the list of unique (set) + # basenames with the extension stripped off + moduleNames = set(map( + lambda x: os.path.splitext(os.path.basename(x))[0], + glob.glob(os.path.join(alternatives.__path__[0],"*")))) + + for moduleName in moduleNames: + if moduleName == "__init__": + continue + + (modulefile, modulepath, moduletype) = \ + imputil.imp.find_module(moduleName,alternatives.__path__) + try: + module = imputil.imp.load_module( + "report1.plugins." + moduleName, modulefile, + modulepath, moduletype) + finally: + if modulefile: + modulefile.close() + + optionsDict = { 'plugin' : moduleName } + for k,v in rptopts.iteritems(): + optionsDict[k] = v + + if 'target' not in optionsDict: + choices.append( \ + iomodule.ChoiceValue( \ + moduleName, + module.descriptionFunction(optionsDict), + (lambda module, optionsDict: lambda signature, io : module.report(signature, io, optionsDict))(module, optionsDict))) + + elif optionsDict['target'] == moduleName: + return (lambda module, optionsDict: lambda signature, io : module.report(signature, io, optionsDict))(module, optionsDict) + + + if 'target' in rptopts: + DisplayFailMessage(io, _("No Such Plugin"), + _("No plugin matching the requested: %s.") % rptopts['target']) + return False + + if len(choices) >= 1: + choice = io.queryChoice(_("Where do you want to send this report:"), choices) + return choice + + else: + DisplayFailMessage(io, _("No Plugins"), + _("No usable plugins.")) + return False + +def report(signature, io, **rptopts): + if not io: + DisplayFailMessage(None, _("No IO specified."), + _("Cannot determine IO.")) + return False + + config = ConfigParser.RawConfigParser() + config.optionxform = str + + # Just continue, if we can't read the config files + try: + config.read("/etc/report.conf") + config.read(glob.glob("/etc/report.d/*.conf")) + except: + pass + + retval = False + while (retval == False): + choice = buildChoices(signature, io, config, rptopts) + if not choice: + return choice + else: + retval = choice(signature, io) + if retval == False and 'target' in rptopts: + del rptopts['target'] + + return retval +# +# This writes out the report/signature to an on-disk/over-the-wire format +# called the 'external format'. +# +# The external format is either an XML file, or a TAR file containing +# an XML file, and files containing the contents of some of the members +# of the report/signature. A reader of the external format must +# be capable of reading either format, a writer of the external format +# may choose either format, but should choose the format that is most +# efficient for the nature of the members of the report/signature it +# is writing. +# +# if asSignature +# only non-binary members are included in the external format +# all non-binary members are included directly into a single XML +# file called .xml +# +# otherwise +# all members are included in the external format +# if there are any files in the external format +# the external format is a tarfile called .tar.gz +# which includes a member named "content".xml which is an XML +# file containing +# the direct contents of all the non-file members +# and references to all of file references +# the contents of all file members are stored directly +# in a sub-directory of the tarfile, called "contents" +# else +# all members are included directly into a single XML +# file called .xml +# + +def serialize( signature, fileNameBase, asSignature ): + + reportFile = None + reportFileName = None + + if fileNameBase is None: + fileNameBase = "report" + else: + fileNameBase = os.path.basename( fileNameBase ) + + if fileNameBase == "": + fileNameBase = "report" + + root = etree.Element("report") + + root.attrib["xmlns"] = "http://www.redhat.com/gss/strata" + + for (key,value) in signature.iteritems(): + + if not asSignature or not value.isBinary: + elem = etree.Element("binding", name=key) + + if value.isFile: + if value.fileName and value.fileName != "": + elem.attrib["fileName"] = value.fileName + + if value.isBinary: + elem.attrib["type"] = "binary" + else: + elem.attrib["type"] = "text" + + if asSignature or not value.isFile: + elem.text = value.asString() + + else: + if reportFile == None: + baseFile = tempfile.NamedTemporaryFile( + prefix=fileNameBase, + suffix=".tar.gz", + delete=False) + reportFileName = baseFile.name + reportFile = tarfile.open(mode="w|gz", + fileobj=baseFile) + + realfilename = value.asFileName() + + # as we copy the file into the tarball, we want to + # rename it slightly: remove any leading "../"'s + # add a leading "content" + # and normalize + if value.isFile and value.fileName: + internalfilename = value.fileName + else: + internalfilename = realfilename + while internalfilename.startswith("../"): + internalfilename = internalfilename[3:] + internalfilename = os.path.normpath("content/" + internalfilename) + reportFile.add(realfilename, internalfilename) + elem.attrib["href"] = internalfilename + + root.append( elem ) + + rootstring = etree.tostring(root) + if reportFile == None: + reportFile = tempfile.NamedTemporaryFile( + prefix=fileNameBase, + suffix=".xml", + delete=False) + reportFileName = reportFile.name + reportFile.write(rootstring) + reportFile.close() + else: + tmpfile = tempfile.NamedTemporaryFile(delete=False) + tmpfile.write(rootstring) + tmpfile.close() + reportFile.add(tmpfile.name,"content.xml") + reportFile.close() + baseFile.close() + + return reportFileName + +def serializeAsSignature( signature, fileNameBase="signature" ): + return serialize( signature, fileNameBase, asSignature=True ) + +def serializeAsReport( signature, fileNameBase="report" ): + return serialize( signature, fileNameBase, asSignature=False ) + +# +# serializeToFile +# is for use by plugins that can/must only write a signature as +# a single file. For 'simpleFile' reports/signatures, it serializes +# to that file. For 'pythonUnhandledException', 'description', and +# 'signature' reports/signatures, this serializes them as a Signature. +# For all other repors/signatures this serializes them as a Report. +# + +def serializeToFile( signature, io, fileNameBase = None ): + if signature.has_key("simpleFile"): + return signature["simpleFile"].asFileName() + + elif signature.has_key("pythonUnhandledException"): + if fileNameBase is None: + if signature["pythonUnhandledException"].isFile and \ + signature["pythonUnhandledException"].fileName: + fileNameBase = signature["pythonUnhandledException"].fileName + else: + fileNameBase = "pythonUnhandledException" + return serializeAsSignature(signature, fileNameBase) + + elif signature.has_key("description"): + if fileNameBase is None: + if signature["description"].isFile and \ + signature["description"].fileName: + fileNameBase = signature["description"].fileName + else: + fileNameBase = "description" + return serializeAsSignature(signature, fileNameBase) + + elif signature.has_key("signature"): + if fileNameBase is None: + if signature["signature"].isFile and \ + signature["signature"].fileName: + fileNameBase = signature["signature"].fileName + else: + fileNameBase = "signature" + return serializeAsSignature(signature, fileNameBase) + + else: + if fileNameBase is None: + fileNameBase = "report" + return serializeAsReport(signature, fileNameBase) + diff --git a/python/report1/accountmanager.py b/python/report1/accountmanager.py index e69de29..10f0e5b 100644 --- a/python/report1/accountmanager.py +++ b/python/report1/accountmanager.py @@ -0,0 +1,167 @@ +""" + Utility routines for managing saved account/username/password information. + Copyright (C) 2009 Red Hat, Inc + + Author: Gavin Romig-Koch + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +HAVE_gnomekeyring = False + +try: + import gnomekeyring + HAVE_gnomekeyring = True +except: + pass + + +class AccountManager: + class LoginAccount: + def __init__(self): + self.username = "" + self.remember_me = True + self.password = None + + def __init__(self): + self.accounts = {} + + def addAccount(self,accountName,username): + if not self.accounts.has_key(accountName): + self.accounts[accountName] = self.LoginAccount() + + self.accounts[accountName].username = username + + def hasAccount(self,accountName): + return self.accounts.has_key(accountName) + + def lookupAccount(self,accountName): + return self.accounts[accountName] + + def queryLogin(self,accountName): + global HAVE_gnomekeyring + + if self.accounts.has_key(accountName): + username = self.accounts[accountName].username + password = self.accounts[accountName].password + remember = self.accounts[accountName].remember_me + + if not username: + username = "" + if not password: + password = "" + if not remember: + remember = False + + if not HAVE_gnomekeyring: + remember = None + + if remember: + try: + items = gnomekeyring.find_items_sync( + gnomekeyring.ITEM_GENERIC_SECRET, + {"user": username, "server": accountName}) + password = items[0].secret + except: + pass + + else: + username = "" + password = None + remember = False + + if HAVE_gnomekeyring: + try: + items = gnomekeyring.find_items_sync( + gnomekeyring.ITEM_GENERIC_SECRET, + {"server": accountName}) + + # should not just user first, + # should use the one with the latest mtime + for i in range(0,len(items)): + if 'user' in items[i].attributes: + username = items[i].attributes['user'] + password = items[i].secret + remember = True + break; + + except gnomekeyring.NoMatchError: + pass + + except: + # should log these, but for now just go on + pass + + if not username: + username = "" + if not password: + password = "" + if not remember: + remember = False + + if not HAVE_gnomekeyring: + remember = None + + return (accountName,username,password,remember) + + def updateLogin(self,accountName,loginResult): + global HAVE_gnomekeyring + + if not loginResult.has_key('remember') or \ + loginResult['remember'] == None: + pass + + elif loginResult['remember']: + if not self.accounts.has_key(accountName): + self.accounts[accountName] = self.LoginAccount() + + self.accounts[accountName].password = loginResult['password'] + self.accounts[accountName].username = loginResult['username'] + + if HAVE_gnomekeyring: + try: + gnomekeyring.item_create_sync( + gnomekeyring.get_default_keyring_sync(), + gnomekeyring.ITEM_GENERIC_SECRET, + "password for user %s at %s" % ( + loginResult['username'], + accountName), + {"user" : loginResult['username'], + "server": accountName}, + loginResult['password'], + True) + + except: + pass + + else: + if self.accounts.has_key(accountName): + del self.accounts[accountName] + + if HAVE_gnomekeyring: + try: + items = gnomekeyring.find_items_sync( + gnomekeyring.ITEM_GENERIC_SECRET, + {"user": loginResult['username'], + "server": accountName}) + + for i in range(0,len(items)): + gnomekeyring.item_delete_sync( + items[i].keyring, + items[i].item_id) + + except: + pass + diff --git a/python/report1/io/GTKIO.py b/python/report1/io/GTKIO.py index e69de29..60710c7 100644 --- a/python/report1/io/GTKIO.py +++ b/python/report1/io/GTKIO.py @@ -0,0 +1,265 @@ +""" + A GTK plugin for the general purpose I/O functions provided to + report plugins. + Copyright (C) 2009 Red Hat, Inc + + Author: Gavin Romig-Koch + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +from report1 import _report as _ +import report1.accountmanager +import os +import gio +if 'DISPLAY' in os.environ and len(os.environ["DISPLAY"]) > 0: + import gtk + +class GTKIO: + def __init__(self,loginManager = None): + import gtk + if loginManager == None: + loginManager = report1.accountmanager.AccountManager() + self.loginManager = loginManager + + def infoMessage(self,title,msg): + MessageDialog(title,msg) + + def failMessage(self,title,msg): + FailDialog(title,msg) + + def successMessage(self, title, msg, actualURL, displayURL): + SuccessDialog(title, msg, actualURL, displayURL) + + def queryLogin(self, accountName): + (accountName,username,password,remember) = \ + self.loginManager.queryLogin(accountName) + return LoginDialog(accountName,username,password,remember).run() + + def updateLogin(self,accountName,loginResult): + self.loginManager.updateLogin(accountName,loginResult) + + def queryField(self,fieldName): + return FieldDialog(fieldName).run() + + def queryChoice(self,msg,choices): + buttons = () + returnValues = [] + count = 0 + for each in choices: + count += 1 + buttons += (each.title,count) + returnValues.append(each.returnValue) + + choice = ButtonBoxDialog(msg,buttons).run() + + if not choice or choice < 1 or count < choice: + return None + + return returnValues[choice-1] + +class LoginDialog: + def __init__(self, account, username, password, remember): + self.dialog = gtk.Dialog(_("Login for %s" % account), None, + gtk.DIALOG_MODAL, + (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, + gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + self.dialog.set_resizable(True) + self.dialog.set_border_width(0) + self.dialog.set_position(gtk.WIN_POS_CENTER) + self.dialog.set_default_response(gtk.RESPONSE_ACCEPT) + + usernameHBox = gtk.HBox(False,10) + self.dialog.vbox.pack_start(usernameHBox, True, True, 0) + + usernameLabel = gtk.Label(_("Username")) + usernameHBox.pack_start(usernameLabel, True, True, 0) + + self.usernameEntry = gtk.Entry() + self.usernameEntry.set_text(username) + self.usernameEntry.set_visibility(True) + self.usernameEntry.set_activates_default(True) + usernameHBox.pack_start(self.usernameEntry, True, True, 0) + + passwordHBox = gtk.HBox(False,10) + self.dialog.vbox.pack_start(passwordHBox, True, True, 0) + + passwordLabel = gtk.Label(_("Password")) + passwordHBox.pack_start(passwordLabel, True, True, 0) + + self.passwordEntry = gtk.Entry() + self.passwordEntry.set_text(password) + self.passwordEntry.set_visibility(False) + self.passwordEntry.set_activates_default(True) + passwordHBox.pack_start(self.passwordEntry, True, True, 0) + + if remember == None: + self.keyringCheckBox = None + else: + self.keyringCheckBox = gtk.CheckButton( + _("Save password in keyring")) + self.dialog.vbox.pack_start(self.keyringCheckBox, True, True, 0) + self.keyringCheckBox.set_active(remember) + + def run(self): + self.dialog.show_all() + rc = self.dialog.run() + responseDict = {} + if rc == gtk.RESPONSE_ACCEPT: + responseDict['username'] = self.usernameEntry.get_text() + responseDict['password'] = self.passwordEntry.get_text() + if self.keyringCheckBox == None: + responseDict['remember'] = None + else: + responseDict['remember'] = self.keyringCheckBox.get_active() + self.dialog.destroy() + return responseDict + else: + self.dialog.destroy() + return None + + +class FieldDialog: + def __init__(self, fieldName): + self.dialog = gtk.Dialog(_("Enter %s") % fieldName, None, + gtk.DIALOG_MODAL, + (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, + gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) + self.dialog.set_resizable(True) + self.dialog.set_border_width(0) + self.dialog.set_position(gtk.WIN_POS_CENTER) + self.dialog.set_default_response(gtk.RESPONSE_ACCEPT) + + fieldHBox = gtk.HBox(False,10) + self.dialog.vbox.pack_start(fieldHBox, True, True, 0) + + fieldLabel = gtk.Label(fieldName) + fieldHBox.pack_start(fieldLabel, True, True, 0) + + self.fieldEntry = gtk.Entry() + self.fieldEntry.set_visibility(True) + self.fieldEntry.set_activates_default(True) + fieldHBox.pack_start(self.fieldEntry, True, True, 0) + + def run(self): + self.dialog.show_all() + rc = self.dialog.run() + if rc == gtk.RESPONSE_ACCEPT: + r = self.fieldEntry.get_text() + self.dialog.destroy() + return r + else: + self.dialog.destroy() + return None + +class ButtonBoxDialog: + def __init__(self, msg, buttons): + + self.dialog = gtk.Dialog(msg, None, gtk.DIALOG_MODAL) + + self.dialog.set_resizable(True) + self.dialog.set_border_width(0) + self.dialog.set_position(gtk.WIN_POS_CENTER) + + label = gtk.Label(msg) + self.dialog.vbox.pack_start(label) + + for i in range(0, len(buttons), 2): + label_item = buttons[i] + response_item = buttons[i+1] + button = gtk.Button(label=label_item) + button.connect("clicked", + lambda b, r: self.dialog.response(r), + response_item) + self.dialog.vbox.pack_start(button) + + + button = gtk.Button(stock=gtk.STOCK_CANCEL) + button.connect("clicked", + lambda b, r: self.dialog.response(r), + gtk.RESPONSE_REJECT) + self.dialog.vbox.pack_start(button) + + + def run(self): + self.dialog.show_all() + rc = self.dialog.run() + self.dialog.destroy() + return rc + +class FailDialog(): + def __init__(self, title, message): + dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, + gtk.BUTTONS_OK, + message) + dlg.set_title(title) + dlg.set_position(gtk.WIN_POS_CENTER) + dlg.show_all() + rc = dlg.run() + dlg.destroy() + +class MessageDialog(): + def __init__(self, title, message): + dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO, + gtk.BUTTONS_OK, + message) + dlg.set_title(title) + dlg.set_position(gtk.WIN_POS_CENTER) + dlg.set_default_response(message) + dlg.set_activates_default(True) + dlg.show_all() + rc = dlg.run() + dlg.destroy() + +class SuccessDialog(): + def __init__(self, title, message, actualURL, displayURL): + + # a blank URL is an empty URL + if actualURL and "" == actualURL.strip(): + actualURL = None + + if displayURL and "" == displayURL.strip(): + displayURL = None + + # default display to actual + if actualURL and not displayURL: + displayURL = actualURL + + dlg = gtk.MessageDialog(None, 0, gtk.MESSAGE_INFO, + gtk.BUTTONS_OK, + message) + dlg.set_title(title) + dlg.set_position(gtk.WIN_POS_CENTER) + + make_link = False + if actualURL: + scheme = actualURL.partition(':')[0] + if scheme and gio.app_info_get_default_for_uri_scheme(scheme): + make_link = True + + if make_link: + dlg.vbox.pack_start( + gtk.LinkButton(actualURL, _("View %s") % displayURL), + True, True, 0) + + else: + dlg.vbox.pack_start(gtk.Label(displayURL), True, True, 0) + if actualURL and actualURL != displayURL: + dlg.vbox.pack_start(gtk.Label(actualURL), True, True, 0) + + dlg.show_all() + dlg.run() + dlg.destroy() + diff --git a/python/report1/io/NewtIO.py b/python/report1/io/NewtIO.py index e69de29..280a1cd 100644 --- a/python/report1/io/NewtIO.py +++ b/python/report1/io/NewtIO.py @@ -0,0 +1,166 @@ +""" + A Newt based plugin for the general purpose I/O functions provided to + report plugins. + Copyright (C) 2009 Red Hat, Inc + + Author: Gavin Romig-Koch + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import snack +import gettext +_ = lambda x: gettext.ldgettext("report", x) + +import string + +class NewtIO: + def __init__(self,screen = None): + self.cleanupScreen = False + if screen == None: + self.screen = snack.SnackScreen() + self.cleanupScreen = True + else: + self.screen = screen + + def __del__(self): + if self.cleanupScreen: + self.screen.finish() + self.screen = None + self.cleanupScreen = False + + def infoMessage(self,title,msg): + snack.ButtonChoiceWindow(self.screen, title, msg, width=60, + buttons=[_("OK")]) + self.screen.popWindow() + self.screen.refresh() + + def failMessage(self,title,msg): + snack.ButtonChoiceWindow(self.screen, title, msg, width=60, + buttons=[_("OK")]) + self.screen.popWindow() + self.screen.refresh() + + def successMessage(self, title, msg, actualURL, displayURL): + + if displayURL: + msg += '\n ' + displayURL + if actualURL and actualURL != displayURL: + msg += '\n ' + actualURL + + snack.ButtonChoiceWindow(self.screen, title, msg, width=60, + buttons=[_("OK")]) + self.screen.popWindow() + self.screen.refresh() + + def queryLogin(self, accountName): + toplevel = snack.GridForm(self.screen, + _("Login for %s") % accountName, + 1, 2) + + buttons = snack.ButtonBar(self.screen, [_("OK"), _("Cancel")]) + usernameEntry = snack.Entry(24) + passwordEntry = snack.Entry(24, password=1) + + grid = snack.Grid(2, 2) + grid.setField(snack.Label(_("Username ")), 0, 0, anchorLeft=1) + grid.setField(usernameEntry, 1, 0) + grid.setField(snack.Label(_("Password ")), 0, 1, anchorLeft=1) + grid.setField(passwordEntry, 1, 1) + + toplevel.add(grid, 0, 0, (0, 0, 0, 1)) + toplevel.add(buttons, 0, 1, growx=1) + + result = toplevel.run() + rc = buttons.buttonPressed(result) + + self.screen.popWindow() + self.screen.refresh() + + if rc == string.lower(_("OK")): + responseDict = {} + responseDict['username'] = usernameEntry.value() + responseDict['password'] = passwordEntry.value() + responseDict['remember'] = False + return responseDict + + else: + return None + + def updateLogin(self,accountName,loginResult): + pass + + def queryField(self,fieldName): + toplevel = snack.GridForm(self.screen, _("Enter %s") % fieldName, 1, 2) + + buttons = snack.ButtonBar(self.screen, [_("OK"), _("Cancel")]) + fieldEntry = snack.Entry(24) + + grid = snack.Grid(2, 1) + grid.setField(snack.Label(fieldName + ' '), 0, 0, anchorLeft=1) + grid.setField(fieldEntry, 1, 0) + + toplevel.add(grid, 0, 0, (0, 0, 0, 1)) + toplevel.add(buttons, 0, 1, growx=1) + + result = toplevel.run() + rc = buttons.buttonPressed(result) + + self.screen.popWindow() + self.screen.refresh() + + if rc == string.lower(_("OK")): + return fieldEntry.value() + + else: + return None + + def queryChoice(self,msg,choices): + cancel_label = _("CANCEL") + + buttons = [] + returnValues = [] + for each in choices: + buttons.append(each.title) + returnValues.append(each.returnValue) + + buttons.append(cancel_label) + + toplevel = snack.GridForm(self.screen, msg, 1, 2) + + buttonBar = snack.ButtonBar(self.screen, buttons) + + toplevel.add(snack.Label(msg), 0, 0, (0, 0, 0, 1)) + toplevel.add(buttonBar, 0, 1, growx=1) + + result = toplevel.run() + rc = buttonBar.buttonPressed(result) + + self.screen.popWindow() + self.screen.refresh() + + if rc == cancel_label.lower(): + return None + + count = 0 + for each in buttons: + if rc == each.lower(): + return returnValues[count] + else: + count += 1 + + return None + + diff --git a/python/report1/io/TextIO.py b/python/report1/io/TextIO.py index e69de29..17daa2a 100644 --- a/python/report1/io/TextIO.py +++ b/python/report1/io/TextIO.py @@ -0,0 +1,99 @@ +""" + A console/text plugin for the general purpose I/O functions provided to + report plugins. + Copyright (C) 2009 Red Hat, Inc + + Author: Gavin Romig-Koch + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import getpass + +class TextIO: + def infoMessage(self,title,msg): + print + print title + print msg + + def failMessage(self,title,msg): + print + print title + print msg + + def successMessage(self, title, msg, actualURL, displayURL): + print + print title + print msg + if displayURL: + print displayURL + if actualURL and actualURL != displayURL: + print actualURL + + def queryLogin(self, accountName): + print + print "Login for %s" % accountName + try: + username = raw_input("Username: ") + password = getpass.getpass("Password: ") + except EOFError: + print "input canceled (EOF)" + return None + + responseDict = {} + responseDict['username'] = username + responseDict['password'] = password + responseDict['remember'] = False + return responseDict + + def updateLogin(self,accountName,loginResult): + pass + + def queryField(self,fieldName): + print + try: + fieldValue = raw_input("%s: " % fieldName) + except EOFError: + print "input canceled (EOF)" + return None + return fieldValue + + def queryChoice(self,msg,choices): + while True: + print("\n") + print(msg) + + count = 1 + for each in choices: + print "%s: %s" % (count,each.title) + count += 1 + print "0: %s" % ("cancel",) + + try: + choice = raw_input("Choice (0-%s): " % (count-1,)) + except EOFError: + print "input canceled (EOF)" + return None + try: + choice = int(choice) + except ValueError: + choice = count + if 0 < choice and choice < count: + return choices[choice-1].returnValue + if choice == 0: + return None + print "Invalid choice" + + diff --git a/python/report1/io/__init__.py b/python/report1/io/__init__.py index e69de29..22b36c5 100644 --- a/python/report1/io/__init__.py +++ b/python/report1/io/__init__.py @@ -0,0 +1,131 @@ +import syslog +import ConfigParser + +_Loglevel = None + +def _GetLoglevel(): + global _Loglevel + + if _Loglevel == None: + try: + config = ConfigParser.RawConfigParser() + config.optionxform = str + config.read("/etc/report.conf") + + # Acceptable priorities for syslog + prio_mappings = {'LOG_INFO': syslog.LOG_INFO, + 'LOG_CRIT': syslog.LOG_CRIT, + 'LOG_DEBUG' : syslog.LOG_DEBUG, + 'LOG_WARNING': syslog.LOG_WARNING} + + _Loglevel = prio_mappings[config.get("main","loglevel")] + + except: + _Loglevel = syslog.LOG_INFO + return _Loglevel + +def DisplayFailMessage(io, title, msg): + """ display error message, title and msg or strings + return nothing + """ + logmsg = 'report: ' + if msg: + if title: + logmsg += title + ': ' + msg + else: + logmsg += msg + else: + if title: + logmsg += title + ': DisplayFailMessage called without message' + else: + logmsg += 'DisplayFailMessage called without message' + + syslog.syslog(_GetLoglevel(), logmsg) + if io: + io.failMessage(title, msg) + +def DisplaySuccessMessage(io, title, msg, actualURL, displayURL): + """ display a sucess message, all args are strings, + displayURL and actualURL should both refer to the same + internet resource, displayURL is for display to the user, + actualURL is for if you want to link to the resource. + If actualURL is empty but displayURL is not, displayURL + is shown but not as a link. + if displayURL is empty but actualURL is not, displayURL + defaults to actualURL + displayURL and actualURL can be the same string. + return nothing + """ + if displayURL: + URL = displayURL + else: + URL = actualURL + + logmsg = 'report: ' + if msg: + if title: + logmsg += title + ': ' + msg + else: + logmsg += msg + else: + if title: + logmsg += title + ': DisplaySuccessMessage called without message' + else: + logmsg += 'DisplaySuccessMessage called without message' + + if URL: + logmsg += '\n' + URL + + syslog.syslog(_GetLoglevel(), logmsg) + if io: + io.successMessage(title, msg, actualURL, displayURL) + +class ChoiceValue: + def __init__(self,title,explanation,returnValue): + self.title = title + self.explanation = explanation + self.returnValue = returnValue + +class IO: + # IO is a callback mechinism for communicating with the user + # IO can be any type that conforms to the following protocol + # def infoMessage(self,title,msg): + # display message, title and msg are strings + # return nothing + # def failMessage(self,title,msg): + # display an error message, title and msg are strings + # return nothing + # def successMessage(self,title,msg,actualURL,displayURL) + # display a sucess message, all args are strings, + # displayURL and actualURL should both refer to the same + # internet resource, displayURL is for display to the user, + # actualURL is for if you want to link to the resource. + # If actualURL is empty but displayURL is not, displayURL + # is shown but not as a link. + # if displayURL is empty but actualURL is not, displayURL + # defaults to actualURL + # displayURL and actualURL can be the same string. + # return nothing + # def queryLogin(self,account): + # Ask the user for the username and password for logging into + # account (a string). + # return a dictionary which contains at least two members with + # the keys "username" and "password". The values of these members + # should be strings. + # def updateLogin(self,account,loginResult): + # Update the login information for account. + # If you call queryLogin, and the login is then successfull, + # call this function with the result of the queryLogin to + # tell the account manager that the login was successfull + # returns nothing + # def queryField(self,fieldName) + # asks for a string value, returns string value + # def queryChoice(self,msg,choices): + # msg is a message about the choices + # choices is a sequence of ChoiceValues + # Each ChoiceValue (title,explanation,returnValue) + # returns the returnValue of the choice the user made + pass + + + diff --git a/python/report1/plugins/RHEL-bugzilla/__init__.py b/python/report1/plugins/RHEL-bugzilla/__init__.py index e69de29..35f38b6 100644 --- a/python/report1/plugins/RHEL-bugzilla/__init__.py +++ b/python/report1/plugins/RHEL-bugzilla/__init__.py @@ -0,0 +1,369 @@ +""" + A Report plugin to send a report to bugzilla.redhat.com. + Copyright (C) 2009 Red Hat, Inc + + Author(s): Gavin Romig-Koch + + Much of the code in this module is derived from code written by + Chris Lumens . + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import os +import report1.io as iomodule +from report1.io import DisplaySuccessMessage +from report1.io import DisplayFailMessage +from report1 import _report as _ + +def labelFunction(label): + if label: + return label + retValue = displayURL(optionsDict) + if retValue.startswith("http://"): + retValue = retValue[len("http://"):] + if retValue.startswith("https://"): + retValue = retValue[len("https://"):] + return retValue + +def descriptionFunction(optionsDict): + if optionsDict.has_key("description"): + return optionsDict["description"] + return "Send report to " + displayURL(optionsDict) + +def displayURL(optionsDict): + if optionsDict.has_key("displayURL"): + return optionsDict["displayURL"] + returnURL = bugURL(optionsDict) + if returnURL.endswith("/xmlrpc.cgi"): + returnURL = returnURL[:len(returnURL) - len("/xmlrpc.cgi")] + return returnURL + +def bugURL(optionsDict): + if optionsDict.has_key("bugURL"): + return optionsDict["bugURL"] + host = "bugzilla.redhat.com" + if optionsDict.has_key("bugzilla_host"): + host = optionsDict["bugzilla_host"] + return "https://" + host + "/xmlrpc.cgi" + +def report(signature, io, optionsDict): + if not io: + DisplayFailMessage(None, _("No IO"), + _("No io provided.")) + return False + + if 'pythonUnhandledException' in signature: + fileName = signature["pythonUnhandledException"].asFileName() + fileDescription = "Attached traceback automatically from %s." % signature["component"].asString() + elif 'simpleFile' in signature: + fileName = signature['simpleFile'].asFileName() + fileDescription = "Attached file %s." % (signature['simpleFile'].asFileName(),) + else: + fileName = None + fileDescription = None + + if 'product' in signature: + product = signature['product'].asString() + else: + product = filer.getProduct() + + if 'version' in signature: + version = signature['version'].asString() + else: + version = filer.getVersion() + + bzfiler = filer.BugzillaFiler(bugURL(optionsDict), + displayURL(optionsDict), + version, product) + + if optionsDict.has_key("testing_component"): + component = optionsDict["testing_component"] + elif 'component' in signature: + component = signature["component"].asString() + else: + component = None + + if 'hashmarkername' in signature: + hashmarkername = signature["hashmarkername"].asString() + else: + hashmarkername = None + + if 'localhash' in signature: + localhash = signature["localhash"].asString() + else: + localhash = None + + if 'summary' in signature: + summary = signature["summary"].asString() + else: + summary = None + + if 'description' in signature: + description = signature["description"].asString() + else: + description = None + + return sendToBugzilla(component, + hashmarkername, + localhash, + summary, + description, + fileName, + fileDescription, + io, + optionsDict, + bzfiler) + + + + + + + + +import filer +# +# This function was abstracted from similar code in both python-meh and +# setroubleshoot. Beyond parameterizing this code, and using IO, this +# code differs from those others in that this version includes the +# 'component' in the .query for duplicates. +# +def sendToBugzilla( component, hashmarkername, localhash, summary, description, fileName, fileDescription, io, optionsDict, bzfiler): + + import rpmUtils.arch + + class BugzillaCommunicationException (Exception): + pass + + def withBugzillaDo(bz, fn): + try: + retval = fn(bz) + return retval + except filer.CommunicationError, e: + msg = _("Your bug could not be filed due to the following error " + "when communicating with bugzilla:\n\n%s" % str(e)) + DisplayFailMessage(io, _("Unable To File Bug"), msg) + raise BugzillaCommunicationException() + + except (TypeError, ValueError), e: + msg = _("Your bug could not be filed due to bad information in " + "the bug fields. This is most likely an error in " + "the bug filing program:\n\n%s" % str(e)) + DisplayFailMessage(io, _("Unable To File Bug"), msg) + raise BugzillaCommunicationException() + + try: + if not bzfiler: + bzfiler = filer.BugzillaFiler("https://bugzilla.redhat.com/xmlrpc.cgi", + "http://bugzilla.redhat.com", + filer.getVersion(), filer.getProduct()) + + if not bzfiler or not bzfiler.supportsFiling() or not bzfiler.bugUrl: + DisplayFailMessage(io, _("Bug Filing Not Supported"), + _("Your distribution does not provide a " + "supported bug filing system, so you " + "cannot save your exception this way.")) + return False + + bugzilla_host = os.path.basename(os.path.dirname(bzfiler.bugUrl)) + + loginResult = io.queryLogin(bugzilla_host) + if loginResult: + password = loginResult['password'] + username = loginResult['username'] + + elif loginResult == None: + return None + + else: + DisplayFailMessage(io, _("No Login Information"), + _("Please provide a valid username and password.")) + return False + + try: + withBugzillaDo(bzfiler, lambda b: b.login(username, password)) + except filer.LoginError: + DisplayFailMessage(io, _("Unable To Login"), + _("There was an error logging into %s " + "using the provided username and " + "password.") % bzfiler.displayUrl) + return False + + io.updateLogin(bugzilla_host,loginResult) + + # figure out whether to attach to an existing bug, create a new bug, + # or search for matching bugs + if 'ticket' in optionsDict: + bug_number = optionsDict['ticket'] + bug = (withBugzillaDo(bzfiler, + lambda b: b.getbug(bug_number))) + + if not bug or bug == "": + DisplayFailMessage(io, _("Bug not found"), + _("Unable to find bug %s" % bug_number)) + return False + else: + buglist = [bug] + wb = "" + + elif localhash and hashmarkername: + # Are there any existing bugs with this hash value? If so we + # will just add any attachment to the bug report and put the + # reporter on the CC list. Otherwise, we need to create a new bug. + wb = "%s_trace_hash:%s" % (hashmarkername, localhash) + buglist = withBugzillaDo(bzfiler, lambda b: b.query( + {'status_whiteboard': wb, + 'status_whiteboard_type':'allwordssubstr', + 'bug_status': []})) + + elif component and (fileDescription or description): + # then we should just go ahead and create a new case + wb = "" + buglist = [] + + else: + # ask create or attach? + choice_attach = 4 + choice_new = 5 + + choices = [ + iomodule.ChoiceValue(_("Create new bug"), _("Create a new bug and attach report to it."), choice_new), + iomodule.ChoiceValue(_("Attach to existing bug"), _("Attach report to an existing bug."), choice_attach) + ] + + choice = io.queryChoice(_("Do you want to attach the report to an existing bug or create a new bug?"), choices) + + if choice is None: + return None + + elif choice == choice_new: + wb = "" + buglist = [] + + if component == None: + component = io.queryField('Enter component for new bug'); + if component is None: + return None + component = component.strip() + + if summary == None: + summary = io.queryField('Enter summary for new bug'); + if summary == None: + return None + summary = summary.strip() + + if description == None: + description = io.queryField( + 'Enter description for new bug'); + if description is None: + return None + description = description.strip() + + else: + bug_number = io.queryField("Enter existing bug number") + if bug_number == None: + return None + + bug = (withBugzillaDo(bzfiler, + lambda b: b.getbug(bug_number))) + + if not bug or bug == "": + DisplayFailMessage(io, _("Bug not found"), + _("Unable to find bug %s" % bug_number)) + return False + else: + buglist = [bug] + wb = "" + + if not buglist or len(buglist) == 0: + + # cleanup summary and description + if not summary or not summary.strip(): + summary = "New bug for %s" % (component,) + + if not description or not description.strip(): + if fileDescription: + description = fileDescription + else: + description = '' + + bug = withBugzillaDo(bzfiler, lambda b: b.createbug( + product=bzfiler.getproduct(), + component=component, + version=bzfiler.getversion(), + platform=rpmUtils.arch.getBaseArch(), + bug_severity="medium", + priority="medium", + op_sys="Linux", + bug_file_loc="http://", + summary=summary, + comment=description, + status_whiteboard=wb)) + + if fileName: + if fileDescription == None: + fileDescription = "" + withBugzillaDo(bug, lambda b: b.attachfile(fileName, fileDescription, + contenttype="text/plain", + filename=os.path.basename(fileName))) + + # Tell the user we created a new bug for them and that they should + # go add a descriptive comment. + bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) + + bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) + + DisplaySuccessMessage(io, _("Bug Created"), + _("A new bug has been created with your information added. " + "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:"), + bugURL, + bugDisplayURL) + return True + else: + bug = buglist[0] + if fileName: + if fileDescription == None: + fileDescription = "" + + withBugzillaDo(bug, lambda b: b.attachfile(fileName,fileDescription, + contenttype="text/plain", + filename=os.path.basename(fileName))) + withBugzillaDo(bug, lambda b: b.addCC(username)) + + # Tell the user which bug they've been CC'd on and that they should + # go add a descriptive comment. + bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) + + bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) + + DisplaySuccessMessage(io, _("Bug Updated"), + _("A bug with your information already exists. Your account and information has " + "been added to this bug. Please add additional descriptive information to the " + "following bug:"), + bugURL, + bugDisplayURL) + + return True + + except BugzillaCommunicationException: + # this indicates that doWithBugzilla caught some problem + # communicating with bugzilla and displayed a message about it + # and all we want to do now is get out of sendToBugzilla + return False + + diff --git a/python/report1/plugins/RHEL-bugzilla/filer.py b/python/report1/plugins/RHEL-bugzilla/filer.py index e69de29..c6cf5e0 100644 --- a/python/report1/plugins/RHEL-bugzilla/filer.py +++ b/python/report1/plugins/RHEL-bugzilla/filer.py @@ -0,0 +1,497 @@ +# Copyright (C) 2008, 2009, 2010 Red Hat, Inc. +# All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Author(s): Chris Lumens +# Gavin Romig-Koch +# + +import socket +import xmlrpclib +import os +from report1 import _report as _ +import report1.release_information + +_hardcoded_default_product = 'Red Hat Enterprise Linux' +_hardcoded_default_version = '6.0' +_hardcoded_default_product_for_bugzilla = 'Red Hat Enterprise Linux 6' + +def getProduct(): + product = report1.release_information.getProduct() + if product: + return product + return _hardcoded_default_product + +def getVersion(): + version = report1.release_information.getVersion() + if version: + return version + return _hardcoded_default_version + +class LoginError(Exception): + """An error occurred while logging into the bug reporting system.""" + def __init__(self, bugUrl, username): + self.bugUrl = bugUrl + self.username = username + + def __str__(self): + return "Could not login to %s with username %s" % (self.bugUrl, self.username) + +class CommunicationError(Exception): + """Some miscellaneous error occurred while communicating with the + bug reporting system. This could include XML-RPC errors, passing + bad data, or network problems.""" + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return "Error communicating with bug system: %s" % self.msg + + +# These classes don't do anything except say that automated bug filing are not +# supported. They also define the interface that concrete classes should use, +# as this is what will be expected by exception.py. +class AbstractFiler(object): + """The base class for Filer objects. This is an abstract class. + + Within this class's help, Bug refers to a concrete AbstractBug subclass + and Filer refers to a concrete AbstractFiler subclass. + + A Filer object communicates with a bug filing system - like bugzilla - + that a distribution uses to track defects. Install classes specify + what bug filing system they use by instantiating a subclass of + AbstractFiler. The intention is that each subclass of AbstractFiler + will make use of some system library to handle the actual communication + with the bug filing system. For now, all systems will be assumed to act + like bugzilla. + + Methods in this class should raise the following exceptions: + + CommunicationError -- For all problems communicating with the remote + bug filing system. + LoginError -- For invalid login information. + ValueError -- For all other operations where the client + supplied values are not correct. + """ + def __init__(self, bugUrl, displayUrl, version, product): + """Create a new AbstractFiler instance. This method need not be + overridden by subclasses. + + bugUrl -- The XML-RPC interface to the bug tracking system. + displayUrl -- The URL to use in the UI. + product -- The name of the product we should attempt to file + bugs against. This must be set. + version -- The version of the product we should attempt to + file bugs against. This must be set. + """ + self.bugUrl = bugUrl + self.displayUrl = displayUrl + self.version = str(version) + self.product = product + + def login(self, username, password): + """Using the given username and password, attempt to login to the + bug filing system. This method must be provided by all subclasses, + and should raise LoginError if login is unsuccessful. + """ + raise NotImplementedError + + def createbug(self, *args, **kwargs): + """Create a new bug. The kwargs dictionary is all the arguments that + should be used when creating the new bug and is entirely up to the + subclass to handle. This method must be provided by all subclasses. + On success, it should return a Bug instance. + """ + raise NotImplementedError + + def getbug(self, id): + """Search for a bug given by id and return it. This method must be + provided by all subclasses. On success, it should return a Bug + instance. On error, it should return an instance that is empty. + """ + raise NotImplementedError + + def getbugs(self, idlist): + """Search for all the bugs given by the IDs in idlist and return. + This method must be provided by all subclasses. On success, it + should return a list of Bug instances, or an empty instance for + invalid IDs. + """ + raise NotImplementedError + + def getproduct(self): + """Verify that self.product is a valid product name. If it is, return + that same product name. If not, return self.defaultProduct. This + method queries the bug filing system for a list of valid products. + It must be provided by all subclasses. + """ + raise NotImplementedError + + def getversion(self): + """Verify that self.version is a valid version number for the product + name self.product. If it is, return that same version number as a + string. If not, return "rawhide" if it exists or the latest version + number otherwise. This method queries the bug filing system for a + list of valid versions numbers. It must be provided by all + subclasses. + """ + raise NotImplementedError + + def query(self, query): + """Perform the provided query and return a list of Bug instances that + meet the query. What the query is depends on the exact bug filing + system, though it will be treated as a dictionary of bug attributes + since this is what bugzilla expects. Other filing systems will need + to take extra work to munge this data into the expected format. + This method must be provided by all subclasses. + """ + raise NotImplementedError + + def supportsFiling(self): + """Does this class support filing bugs? All subclasses should override + this method and return True, or automatic filing will not work. + Automatic filing will not be attempted on unknown products. + """ + return False + +class AbstractBug(object): + """The base class for Bug objects. This is an abstract class. + + Within this class's help, Bug refers to a concrete AbstractBug subclass + and Filer refers to a concrete AbstractFiler subclass. + + A Bug object represents one single bug within a Filer. This is where + most of the interesting stuff happens - attaching files, adding comments + and email addresses, and modifying whiteboards. Subclasses of this + class are returned by most operations within a Filer subclass. For now, + all bugs will be assumed to act like bugzilla's bugs. + + Bug objects wrap objects in the underlying module that communicates with + the bug filing system. For example, the bugzilla filer uses the + python-bugzilla module to communicate. This module has its own Bug + object. So, BugzillaBug wraps that object. Therefore, Bugs may be + created out of existing BugzillaBugs or may create their own if + necessary. + + Methods in this class should raise the following exceptions: + + CommunicationError -- For all problems communicating with the remote + bug filing system. + ValueError -- For all other operations where the client + supplied values are not correct (invalid + resolution, status, whiteboard, etc.). + """ + def __init__(self, filer, bug=None, *args, **kwargs): + """Create a new Bug instance. It is recommended that subclasses + override this method to add extra attributes. + + filer -- A reference to a Filer object used when performing + certain operations. This may be None if it is not + required by the Filer or Bug objects. + bug -- If None, the filer-specific code should create a new + bug object. Otherwise, the filer-specific code + should use the provided object as needed. + args, kwargs -- If provided, these arguments should be passed as-is + when creating a new underlying bug object. This + only makes sense if bug is not None. + """ + self.filer = filer + + def __str__(self): + raise NotImplementedError + + def __repr__(self): + raise NotImplementedError + + def addCC(self, address): + """Add the provided email address to this bug. This method must be + provided by all subclasses, and return some non-None value on + success. + """ + raise NotImplementedError + + def addcomment(self, comment): + """Add the provided comment to this bug. This method must be provided + by all subclasses, and return some non-None value on success. + """ + raise NotImplementedError + + def attachfile(self, file, description, **kwargs): + """Attach the filename given by file, with the given description, to + this bug. If provided, the given kwargs will be passed along to + the Filer when attaching the file. These args may be useful for + doing things like setting the MIME type of the file. This method + must be provided by all subclasses and return some non-None value + on success. + """ + raise NotImplementedError + + def close(self, resolution, dupeid=0, comment=''): + """Close this bug with the given resolution, optionally closing it + as a duplicate of the provided dupeid and with the optional comment. + resolution must be a value accepted by the Filer. This method must + be provided by all subclasses and return some non-None value on + success. + """ + raise NotImplementedError + + def id(self): + """Return this bug's ID number. This method must be provided by all + subclasses. + """ + raise NotImplementedError + + def setstatus(self, status, comment=''): + """Set this bug's status and optionally add a comment. status must be + a value accepted by the Filer. This method must be provided by all + subclasses and return some non-None value on success. + """ + raise NotImplementedError + + def setassignee(self, assigned_to='', reporter='', comment=''): + """Assign this bug to the person given by assigned_to, optionally + changing the reporter and attaching a comment. assigned_to must be + a valid account in the Filer. This method must be provided by all + subclasses and return some non-None value on success. + """ + raise NotImplementedError + + def getwhiteboard(self, which=''): + """Get the given whiteboard from this bug and return it. Not all bug + filing systems support the concept of whiteboards, so this method + is optional. + """ + return "" + + def appendwhiteboard(self, text, which=''): + """Append the given text to the given whiteboard. Not all bug filing + systems support the concept of whiteboards, so this method is + optional. If provided, it should return some non-None value on + success. + """ + return True + + def prependwhiteboard(self, text, which=''): + """Put the given text at the front of the given whiteboard. Not all + bug filing systems support the concept of whiteboards, so this + method is optional. If provided, it should return some non-None + value on success. + """ + return True + + def setwhiteboard(self, text, which=''): + """Set the given whiteboard to be the given text. Not all bug filing + systems support the concept of whiteboards, so this method is + optional. If provided, it should return some non-None value on + success. + """ + return True + + +# Concrete classes for automatically filing bugs against Bugzilla instances. +# This requires the python-bugzilla module to do almost all of the real work. +# We basically just make some really thin wrappers around it here since we +# expect all bug filing systems to act similar to bugzilla. +class BugzillaFiler(AbstractFiler): + def __withBugzillaDo(self, fn): + try: + retval = fn(self._bz) + return retval + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def __init__(self, bugUrl, displayUrl, version, product): + AbstractFiler.__init__(self, bugUrl, displayUrl, version, product) + self._bz = None + + def login(self, username, password): + import bugzillaCopy + + try: + self._bz = bugzillaCopy.Bugzilla(url=self.bugUrl) + retval = self._bz.login(username, password) + except socket.error, e: + raise CommunicationError(str(e)) + + if not retval: + raise LoginError(self.bugUrl, username) + + return retval + + def createbug(self, *args, **kwargs): + whiteboards = [] + + for (key, val) in kwargs.items(): + if key.endswith("_whiteboard"): + wb = key.split("_")[0] + whiteboards.append((wb, val)) + kwargs.pop(key) + + if key == "platform": + platformLst = self.__withBugzillaDo(lambda b: b._proxy.Bug.legal_values({'field': 'platform'})) + if not val in platformLst['values']: + kwargs[key] = platformLst['values'][0] + + bug = self.__withBugzillaDo(lambda b: b.createbug(**kwargs)) + for (wb, val) in whiteboards: + bug.setwhiteboard(val, which=wb) + + return BugzillaBug(self, bug=bug) + + def getbug(self, id): + return BugzillaBug(self, bug=self.__withBugzillaDo(lambda b: b.getbug(id))) + + def getbugs(self, idlist): + lst = self.__withBugzillaDo(lambda b: b.getbugs(idlist)) + return map(lambda b: BugzillaBug(self, bug=b), lst) + + def getproduct(self): + details = self.__withBugzillaDo(lambda b: b.getproducts()) + for d in details: + if d['name'] == self.product: + return self.product + + # Extend product with high order number of version + product_with_version = self.product + ' ' + self.version.split('.')[0] + for d in details: + if d['name'] == product_with_version: + return product_with_version + + # If the product given to us by the caller isn't valid, fall back + # to asking the running system and then to something hard coded. + defaultProduct = getProduct() + for d in details: + if d['name'] == defaultProduct: + return defaultProduct + + product_with_version = getProduct() + ' ' + getVersion().split('.')[0] + for d in details: + if d['name'] == product_with_version: + return product_with_version + + return _hardcoded_default_product_for_bugzilla + + def getversion(self): + # Convert all version numbers from bugzilla into strings. Sometimes + # bugzilla gives us strings ("rawhide", "development"), sometimes it + # gives us integers (11, 12), and sometimes it gives us floats + # (5.4, 5.5, 6.0). + details = self.__withBugzillaDo(lambda b: b._proxy.bugzilla.getProductDetails(self.getproduct())) + bugzillaVers = map(str, details[1]) + bugzillaVers.sort() + + # Double check to make sure this is a string. + ver = str(self.version) + + # If the version given to us by the caller isn't valid, fall back to + # asking the running system and then to something hard coded. + if not ver in bugzillaVers: + defaultVersion = getVersion() + if defaultVersion in bugzillaVers: + return defaultVersion + + return _hardcoded_default_version + else: + return str(self.version) + + def query(self, query): + lst = self.__withBugzillaDo(lambda b: b.query(query)) + return map(lambda b: BugzillaBug(self, bug=b), lst) + + def supportsFiling(self): + return True + +class BugzillaBug(AbstractBug): + def __withBugDo(self, fn): + try: + retval = fn(self._bug) + return retval + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def __init__(self, filer, bug=None, *args, **kwargs): + import bugzillaCopy + + self.filer = filer + + if not bug: + self._bug = bugzillaCopy.Bug(self.filer, *args, **kwargs) + else: + self._bug = bug + + def __str__(self): + return self._bug.__str__() + + def __repr__(self): + return self._bug.__repr__() + + def addCC(self, address): + try: + return self.filer._bz._updatecc(self._bug.bug_id, [address], 'add') + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def addcomment(self, comment): + return self.__withBugDo(lambda b: b.addcomment(comment)) + + def attachfile(self, file, description, **kwargs): + try: + return self.filer._bz.attachfile(self._bug.bug_id, file, description, **kwargs) + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def id(self): + return self._bug.bug_id + + def close(self, resolution, dupeid=0, comment=''): + return self.__withBugDo(lambda b: b.close(resolution, dupeid=dupeid, + comment=comment)) + + def setstatus(self, status, comment=''): + return self.__withBugDo(lambda b: b.setstatus(status, comment=comment)) + + def setassignee(self, assigned_to='', reporter='', comment=''): + return self.__withBugDo(lambda b: b.setassignee(assigned_to=assigned_to, + reporter=reporter, + comment=comment)) + + def getwhiteboard(self, which='status'): + return self.__withBugDo(lambda b: b.getwhiteboard(which=which)) + + def appendwhiteboard(self, text, which='status'): + return self.__withBugDo(lambda b: b.appendwhiteboard(text, which=which)) + + def prependwhiteboard(self, text, which='status'): + return self.__withBugDo(lambda b: b.prependwhiteboard(text, which=which)) + + def setwhiteboard(self, text, which='status'): + return self.__withBugDo(lambda b: b.setwhiteboard(text, which=which)) + diff --git a/python/report1/plugins/__init__.py b/python/report1/plugins/__init__.py index e69de29..8b13789 100644 --- a/python/report1/plugins/__init__.py +++ b/python/report1/plugins/__init__.py @@ -0,0 +1 @@ + diff --git a/python/report1/plugins/bugzilla/__init__.py b/python/report1/plugins/bugzilla/__init__.py index e69de29..caede97 100644 --- a/python/report1/plugins/bugzilla/__init__.py +++ b/python/report1/plugins/bugzilla/__init__.py @@ -0,0 +1,363 @@ +""" + A Report plugin to send a report to bugzilla.redhat.com. + Copyright (C) 2009 Red Hat, Inc + + Author(s): Gavin Romig-Koch + + Much of the code in this module is derived from code written by + Chris Lumens . + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import os +import report1.io as iomodule +from report1.io import DisplayFailMessage +from report1.io import DisplaySuccessMessage +from report1 import _report as _ + +def labelFunction(label): + if label: + return label + retValue = displayURL(optionsDict) + if retValue.startswith("http://"): + retValue = retValue[len("http://"):] + if retValue.startswith("https://"): + retValue = retValue[len("https://"):] + return retValue + +def descriptionFunction(optionsDict): + if optionsDict.has_key("description"): + return optionsDict["description"] + return "Send report to " + displayURL(optionsDict) + +def displayURL(optionsDict): + if optionsDict.has_key("displayURL"): + return optionsDict["displayURL"] + returnURL = bugURL(optionsDict) + if returnURL.endswith("/xmlrpc.cgi"): + returnURL = returnURL[:len(returnURL) - len("/xmlrpc.cgi")] + return returnURL + +def bugURL(optionsDict): + if 'bugURL' in optionsDict: + return optionsDict["bugURL"] + host = "bugzilla.redhat.com" + if 'bugzilla_host' in optionsDict: + host = optionsDict["bugzilla_host"] + return "https://" + host + "/xmlrpc.cgi" + +def report(signature, io, optionsDict): + if not io: + DisplayFailMessage(None, _("No IO"), + _("No io provided.")) + return False + + if 'pythonUnhandledException' in signature: + fileName = signature["pythonUnhandledException"].asFileName() + fileDescription = "Attached traceback automatically from %s." % signature["component"].asString() + elif 'simpleFile' in signature: + fileName = signature['simpleFile'].asFileName() + fileDescription = "Attached file %s." % (signature['simpleFile'].asFileName(),) + else: + fileName = None + fileDescription = None + + if 'product' in signature: + product = signature['product'].asString() + else: + product = filer.getProduct() + + if 'version' in signature: + version = signature['version'].asString() + else: + version = filer.getVersion() + + bzfiler = filer.BugzillaFiler(bugURL(optionsDict), + displayURL(optionsDict), + version, product) + + # must pass a component + if 'component' in signature: + component = signature["component"].asString() + elif 'testing_component' in optionsDict: + component = optionsDict["testing_component"] + else: + component = None + + return sendToBugzilla(component, + signature, + io, + bzfiler, + optionsDict, + fileName, + fileDescription) + +import filer +# +# This function was abstracted from similar code in both python-meh and +# setroubleshoot. Beyond parameterizing this code, and using IO, this +# code differs from those others in that this version includes the +# 'component' in the .query for duplicates. +# +def sendToBugzilla(component, signature, io, bzfiler, + optionsDict, fileName, fileDescription): + + import rpmUtils.arch + + class BugzillaCommunicationException (Exception): + pass + + def withBugzillaDo(bz, fn): + try: + retval = fn(bz) + return retval + except filer.CommunicationError, e: + msg = _("Your bug could not be filed due to the following error " \ + "when communicating with bugzilla:\n\n%s" % str(e)) + DisplayFailMessage(io, _("Unable To File Bug"), msg) + raise BugzillaCommunicationException() + + except (TypeError, ValueError), e: + msg = _("Your bug could not be filed due to bad information in " \ + "the bug fields. This is most likely an error in " \ + "the bug filing program:\n\n%s" % str(e)) + DisplayFailMessage(io, _("Unable To File Bug"), msg) + raise BugzillaCommunicationException() + + try: + if not bzfiler: + if 'product' in signature: + product = signature['product'].asString() + else: + product = filer.getProduct() + + if 'version' in signature: + version = signature['version'].asString() + else: + version = filer.getVersion() + + bzfiler = filer.BugzillaFiler("https://bugzilla.redhat.com/xmlrpc.cgi", + "http://bugzilla.redhat.com", + version, product) + + if not bzfiler or not bzfiler.supportsFiling() or not bzfiler.bugUrl: + DisplayFailMessage(io, _("Bug Filing Not Supported"), + _("Your distribution does not provide a " \ + "supported bug filing system, so you " \ + "cannot save your exception this way.")) + return False + + bugzilla_host = os.path.basename(os.path.dirname(bzfiler.bugUrl)) + + loginResult = io.queryLogin(bugzilla_host) + if not loginResult: + return None + + if 'username' not in loginResult and \ + 'password' not in loginResult: + DisplayFailMessage(io, _("No Login Information"), + _("Please provide a valid username and password.")) + return False + + try: + withBugzillaDo(bzfiler, lambda b: b.login(loginResult['username'], + loginResult['password'])) + except filer.LoginError: + DisplayFailMessage(io, _("Unable To Login"), + _("There was an error logging into %s " \ + "using the provided username and " \ + "password.") % bzfiler.displayUrl) + return False + + io.updateLogin(bugzilla_host, loginResult) + + # grab summary and description if we have it + if 'summary' in signature: + summary = signature['summary'].asString() + else: + summary = None + + if 'description' in signature: + description = signature['description'].asString() + else: + description = None + + # figure out whether to attach to an existing bug, create a new bug, + # or search for matching bugs + if 'ticket' in optionsDict: + bug_number = optionsDict['ticket'] + bug = (withBugzillaDo(bzfiler, + lambda b: b.getbug(bug_number))) + + if not bug or bug == "": + DisplayFailMessage(io, _("Bug not found"), + _("Unable to find bug %s" % bug_number)) + return False + else: + buglist = [bug] + wb = "" + + elif 'localhash' in signature and 'hashmarkername' in signature: + # Are there any existing bugs with this hash value? If so we + # will just add any attachment to the bug report and put the + # reporter on the CC list. Otherwise, we need to create a new bug. + wb = "%s_trace_hash:%s" % (signature['hashmarkername'].asString(), + signature['localhash'].asString()) + buglist = withBugzillaDo(bzfiler, lambda b: b.query( + {'status_whiteboard': wb, + 'status_whiteboard_type':'allwordssubstr', + 'bug_status': []})) + + elif 'component' in signature and (fileDescription or + ('description' in signature)): + # then we should just go ahead and create a new case + wb = "" + buglist = [] + + else: + # ask create or attach? + choice_attach = 4 + choice_new = 5 + + choices = [ + iomodule.ChoiceValue(_("Create new bug"), _("Create a new bug and attach report to it."), choice_new), + iomodule.ChoiceValue(_("Attach to existing bug"), _("Attach report to an existing bug."), choice_attach) + ] + + choice = io.queryChoice(_("Do you want to attach the report to an existing bug or create a new bug?"), choices) + + if choice is None: + return None + + elif choice == choice_new: + wb = "" + buglist = [] + + if 'component' not in signature: + component = io.queryField('Enter component for new bug'); + if component is None: + return None + component = component.strip() + + if summary == None: + summary = io.queryField('Enter summary for new bug'); + if summary == None: + return None + summary = summary.strip() + + if description == None: + description = io.queryField( + 'Enter description for new bug'); + if description is None: + return None + description = description.strip() + + else: + bug_number = io.queryField("Enter existing bug number") + if bug_number == None: + return None + + bug = (withBugzillaDo(bzfiler, + lambda b: b.getbug(bug_number))) + + if not bug or bug == "": + DisplayFailMessage(io, _("Bug not found"), + _("Unable to find bug %s" % bug_number)) + return False + else: + buglist = [bug] + wb = "" + + + if not buglist or len(buglist) == 0: + + # cleanup summary and description + if not summary or not summary.strip(): + summary = "New bug for %s" % (component,) + + if not description or not description.strip(): + if fileDescription: + description = fileDescription + else: + description = '' + + bug = withBugzillaDo(bzfiler, lambda b: b.createbug( + product=bzfiler.getproduct(), + component=component, + version=bzfiler.getversion(), + platform=rpmUtils.arch.getBaseArch(), + bug_severity="medium", + priority="medium", + op_sys="Linux", + bug_file_loc="http://", + summary=summary, + comment=description, + status_whiteboard=wb)) + + if fileName: + if not fileDescription: + fileDescription = "" + withBugzillaDo(bug, lambda b: b.attachfile(fileName, fileDescription, + contenttype="text/plain", + filename=os.path.basename(fileName))) + + # Tell the user we created a new bug for them and that they should + # go add a descriptive comment. + bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) + + bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) + + DisplaySuccessMessage(io, _("Bug Created"), + _("A new bug has been created with your information added. " + "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:"), + bugURL, + bugDisplayURL) + return True + else: + bug = buglist[0] + if fileName: + if not fileDescription: + fileDescription = "" + + withBugzillaDo(bug, lambda b: b.attachfile(fileName, fileDescription, + contenttype="text/plain", + filename=os.path.basename(fileName))) + withBugzillaDo(bug, lambda b: b.addCC(loginResult['username'])) + + # Tell the user which bug they've been CC'd on and that they should + # go add a descriptive comment. + bugDisplayURL = "Bug %s on %s" % (bug.id(),bugzilla_host) + + bugURL = os.path.dirname(bzfiler.bugUrl) + "/show_bug.cgi?id=" + str(bug.id()) + + DisplaySuccessMessage(io, _("Bug Updated"), + _("A bug with your information already exists. Your account and information has " + "been added to this bug. Please add additional descriptive information to the " + "following bug:"), + bugURL, + bugDisplayURL) + + return True + + except BugzillaCommunicationException: + # this indicates that doWithBugzilla caught some problem + # communicating with bugzilla and displayed a message about it + # and all we want to do now is get out of sendToBugzilla + return False + + diff --git a/python/report1/plugins/bugzilla/filer.py b/python/report1/plugins/bugzilla/filer.py index e69de29..4916f62 100644 --- a/python/report1/plugins/bugzilla/filer.py +++ b/python/report1/plugins/bugzilla/filer.py @@ -0,0 +1,504 @@ +# Copyright (C) 2008, 2009, 2010 Red Hat, Inc. +# All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Author(s): Chris Lumens +# Gavin Romig-Koch +# + +import socket +import xmlrpclib +import os +from report1 import _report as _ +import report1.release_information + +_hardcoded_default_product = 'Fedora' +_hardcoded_default_version = 'rawhide' +_hardcoded_default_product_for_bugzilla = _hardcoded_default_product + +def getProduct(): + product = report1.release_information.getProduct() + if product: + return product + return _hardcoded_default_product + +def getVersion(): + version = report1.release_information.getVersion() + if version: + return version + return _hardcoded_default_version + +class LoginError(Exception): + """An error occurred while logging into the bug reporting system.""" + def __init__(self, bugUrl, username): + self.bugUrl = bugUrl + self.username = username + + def __str__(self): + return "Could not login to %s with username %s" % (self.bugUrl, self.username) + +class CommunicationError(Exception): + """Some miscellaneous error occurred while communicating with the + bug reporting system. This could include XML-RPC errors, passing + bad data, or network problems.""" + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return "Error communicating with bug system: %s" % self.msg + + +# These classes don't do anything except say that automated bug filing are not +# supported. They also define the interface that concrete classes should use, +# as this is what will be expected by exception.py. +class AbstractFiler(object): + """The base class for Filer objects. This is an abstract class. + + Within this class's help, Bug refers to a concrete AbstractBug subclass + and Filer refers to a concrete AbstractFiler subclass. + + A Filer object communicates with a bug filing system - like bugzilla - + that a distribution uses to track defects. Install classes specify + what bug filing system they use by instantiating a subclass of + AbstractFiler. The intention is that each subclass of AbstractFiler + will make use of some system library to handle the actual communication + with the bug filing system. For now, all systems will be assumed to act + like bugzilla. + + Methods in this class should raise the following exceptions: + + CommunicationError -- For all problems communicating with the remote + bug filing system. + LoginError -- For invalid login information. + ValueError -- For all other operations where the client + supplied values are not correct. + """ + def __init__(self, bugUrl, displayUrl, version, product): + """Create a new AbstractFiler instance. This method need not be + overridden by subclasses. + + bugUrl -- The XML-RPC interface to the bug tracking system. + displayUrl -- The URL to use in the UI. + product -- The name of the product we should attempt to file + bugs against. This must be set. + version -- The version of the product we should attempt to + file bugs against. This must be set. + """ + self.bugUrl = bugUrl + self.displayUrl = displayUrl + self.version = str(version) + self.product = product + + def login(self, username, password): + """Using the given username and password, attempt to login to the + bug filing system. This method must be provided by all subclasses, + and should raise LoginError if login is unsuccessful. + """ + raise NotImplementedError + + def createbug(self, *args, **kwargs): + """Create a new bug. The kwargs dictionary is all the arguments that + should be used when creating the new bug and is entirely up to the + subclass to handle. This method must be provided by all subclasses. + On success, it should return a Bug instance. + """ + raise NotImplementedError + + def getbug(self, id): + """Search for a bug given by id and return it. This method must be + provided by all subclasses. On success, it should return a Bug + instance. On error, it should return an instance that is empty. + """ + raise NotImplementedError + + def getbugs(self, idlist): + """Search for all the bugs given by the IDs in idlist and return. + This method must be provided by all subclasses. On success, it + should return a list of Bug instances, or an empty instance for + invalid IDs. + """ + raise NotImplementedError + + def getproduct(self): + """Verify that self.product is a valid product name. If it is, return + that same product name. If not, return self.defaultProduct. This + method queries the bug filing system for a list of valid products. + It must be provided by all subclasses. + """ + raise NotImplementedError + + def getversion(self): + """Verify that self.version is a valid version number for the product + name self.product. If it is, return that same version number as a + string. If not, return "rawhide" if it exists or the latest version + number otherwise. This method queries the bug filing system for a + list of valid versions numbers. It must be provided by all + subclasses. + """ + raise NotImplementedError + + def query(self, query): + """Perform the provided query and return a list of Bug instances that + meet the query. What the query is depends on the exact bug filing + system, though it will be treated as a dictionary of bug attributes + since this is what bugzilla expects. Other filing systems will need + to take extra work to munge this data into the expected format. + This method must be provided by all subclasses. + """ + raise NotImplementedError + + def supportsFiling(self): + """Does this class support filing bugs? All subclasses should override + this method and return True, or automatic filing will not work. + Automatic filing will not be attempted on unknown products. + """ + return False + +class AbstractBug(object): + """The base class for Bug objects. This is an abstract class. + + Within this class's help, Bug refers to a concrete AbstractBug subclass + and Filer refers to a concrete AbstractFiler subclass. + + A Bug object represents one single bug within a Filer. This is where + most of the interesting stuff happens - attaching files, adding comments + and email addresses, and modifying whiteboards. Subclasses of this + class are returned by most operations within a Filer subclass. For now, + all bugs will be assumed to act like bugzilla's bugs. + + Bug objects wrap objects in the underlying module that communicates with + the bug filing system. For example, the bugzilla filer uses the + python-bugzilla module to communicate. This module has its own Bug + object. So, BugzillaBug wraps that object. Therefore, Bugs may be + created out of existing BugzillaBugs or may create their own if + necessary. + + Methods in this class should raise the following exceptions: + + CommunicationError -- For all problems communicating with the remote + bug filing system. + ValueError -- For all other operations where the client + supplied values are not correct (invalid + resolution, status, whiteboard, etc.). + """ + def __init__(self, filer, bug=None, *args, **kwargs): + """Create a new Bug instance. It is recommended that subclasses + override this method to add extra attributes. + + filer -- A reference to a Filer object used when performing + certain operations. This may be None if it is not + required by the Filer or Bug objects. + bug -- If None, the filer-specific code should create a new + bug object. Otherwise, the filer-specific code + should use the provided object as needed. + args, kwargs -- If provided, these arguments should be passed as-is + when creating a new underlying bug object. This + only makes sense if bug is not None. + """ + self.filer = filer + + def __str__(self): + raise NotImplementedError + + def __repr__(self): + raise NotImplementedError + + def addCC(self, address): + """Add the provided email address to this bug. This method must be + provided by all subclasses, and return some non-None value on + success. + """ + raise NotImplementedError + + def addcomment(self, comment): + """Add the provided comment to this bug. This method must be provided + by all subclasses, and return some non-None value on success. + """ + raise NotImplementedError + + def attachfile(self, file, description, **kwargs): + """Attach the filename given by file, with the given description, to + this bug. If provided, the given kwargs will be passed along to + the Filer when attaching the file. These args may be useful for + doing things like setting the MIME type of the file. This method + must be provided by all subclasses and return some non-None value + on success. + """ + raise NotImplementedError + + def close(self, resolution, dupeid=0, comment=''): + """Close this bug with the given resolution, optionally closing it + as a duplicate of the provided dupeid and with the optional comment. + resolution must be a value accepted by the Filer. This method must + be provided by all subclasses and return some non-None value on + success. + """ + raise NotImplementedError + + def id(self): + """Return this bug's ID number. This method must be provided by all + subclasses. + """ + raise NotImplementedError + + def setstatus(self, status, comment=''): + """Set this bug's status and optionally add a comment. status must be + a value accepted by the Filer. This method must be provided by all + subclasses and return some non-None value on success. + """ + raise NotImplementedError + + def setassignee(self, assigned_to='', reporter='', comment=''): + """Assign this bug to the person given by assigned_to, optionally + changing the reporter and attaching a comment. assigned_to must be + a valid account in the Filer. This method must be provided by all + subclasses and return some non-None value on success. + """ + raise NotImplementedError + + def getwhiteboard(self, which=''): + """Get the given whiteboard from this bug and return it. Not all bug + filing systems support the concept of whiteboards, so this method + is optional. + """ + return "" + + def appendwhiteboard(self, text, which=''): + """Append the given text to the given whiteboard. Not all bug filing + systems support the concept of whiteboards, so this method is + optional. If provided, it should return some non-None value on + success. + """ + return True + + def prependwhiteboard(self, text, which=''): + """Put the given text at the front of the given whiteboard. Not all + bug filing systems support the concept of whiteboards, so this + method is optional. If provided, it should return some non-None + value on success. + """ + return True + + def setwhiteboard(self, text, which=''): + """Set the given whiteboard to be the given text. Not all bug filing + systems support the concept of whiteboards, so this method is + optional. If provided, it should return some non-None value on + success. + """ + return True + + +# Concrete classes for automatically filing bugs against Bugzilla instances. +# This requires the python-bugzilla module to do almost all of the real work. +# We basically just make some really thin wrappers around it here since we +# expect all bug filing systems to act similar to bugzilla. +class BugzillaFiler(AbstractFiler): + def __withBugzillaDo(self, fn): + try: + retval = fn(self._bz) + return retval + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def __init__(self, bugUrl, displayUrl, version, product): + AbstractFiler.__init__(self, bugUrl, displayUrl, version, product) + self._bz = None + + def login(self, username, password): + import bugzilla + + try: + self._bz = bugzilla.Bugzilla(url=self.bugUrl) + retval = self._bz.login(username, password) + except socket.error, e: + raise CommunicationError(str(e)) + + if not retval: + raise LoginError(self.bugUrl, username) + + return retval + + def createbug(self, *args, **kwargs): + whiteboards = [] + + for (key, val) in kwargs.items(): + if key.endswith("_whiteboard"): + wb = key.split("_")[0] + whiteboards.append((wb, val)) + kwargs.pop(key) + + if key == "platform": + platformLst = self.__withBugzillaDo(lambda b: b._proxy.Bug.legal_values({'field': 'platform'})) + if not val in platformLst['values']: + kwargs[key] = platformLst['values'][0] + + bug = self.__withBugzillaDo(lambda b: b.createbug(**kwargs)) + for (wb, val) in whiteboards: + bug.setwhiteboard(val, which=wb) + + return BugzillaBug(self, bug=bug) + + def getbug(self, id): + return BugzillaBug(self, bug=self.__withBugzillaDo(lambda b: b.getbug(id))) + + def getbugs(self, idlist): + lst = self.__withBugzillaDo(lambda b: b.getbugs(idlist)) + return map(lambda b: BugzillaBug(self, bug=b), lst) + + def getproduct(self): + details = self.__withBugzillaDo(lambda b: b.getproducts()) + for d in details: + if d['name'] == self.product: + return self.product + + # Extend product with high order number of version + product_with_version = self.product + ' ' + self.version.split('.')[0] + for d in details: + if d['name'] == product_with_version: + return product_with_version + + # If the product given to us by the caller isn't valid, fall back + # to asking the running system and then to something hard coded. + defaultProduct = getProduct() + for d in details: + if d['name'] == defaultProduct: + return defaultProduct + + product_with_version = getProduct() + ' ' + getVersion().split('.')[0] + for d in details: + if d['name'] == product_with_version: + return product_with_version + + return _hardcoded_default_product_for_bugzilla + + def getversion(self): + # Convert all version numbers from bugzilla into strings. Sometimes + # bugzilla gives us strings ("rawhide", "development"), sometimes it + # gives us integers (11, 12), and sometimes it gives us floats + # (5.4, 5.5, 6.0). + details = self.__withBugzillaDo(lambda b: b._proxy.bugzilla.getProductDetails(self.getproduct())) + bugzillaVers = map(str, details[1]) + bugzillaVers.sort() + + # Double check to make sure this is a string. + ver = str(self.version) + + # If the version given to us by the caller isn't valid, fall back to + # asking the running system and then to something hard coded. + if ver in bugzillaVers: + return str(self.version) + + PossibleSuffixes = [ "-Alpha", "-Beta" ] + for suffix in PossibleSuffixes: + if ver.endswith(suffix): + shortver = ver[0:-len(suffix)] + if shortver in bugzillaVers: + return shortver + + defaultVersion = getVersion() + if defaultVersion in bugzillaVers: + return defaultVersion + + return _hardcoded_default_version + + def query(self, query): + lst = self.__withBugzillaDo(lambda b: b.query(query)) + return map(lambda b: BugzillaBug(self, bug=b), lst) + + def supportsFiling(self): + return True + +class BugzillaBug(AbstractBug): + def __withBugDo(self, fn): + try: + retval = fn(self._bug) + return retval + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def __init__(self, filer, bug=None, *args, **kwargs): + import bugzilla + + self.filer = filer + + if not bug: + self._bug = bugzilla.Bug(self.filer, *args, **kwargs) + else: + self._bug = bug + + def __str__(self): + return self._bug.__str__() + + def __repr__(self): + return self._bug.__repr__() + + def addCC(self, address): + try: + return self.filer._bz._updatecc(self._bug.bug_id, [address], 'add') + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def addcomment(self, comment): + return self.__withBugDo(lambda b: b.addcomment(comment)) + + def attachfile(self, file, description, **kwargs): + try: + return self.filer._bz.attachfile(self._bug.bug_id, file, description, **kwargs) + except xmlrpclib.ProtocolError, e: + raise CommunicationError(str(e)) + except xmlrpclib.Fault, e: + raise ValueError(str(e)) + except socket.error, e: + raise CommunicationError(str(e)) + + def id(self): + return self._bug.bug_id + + def close(self, resolution, dupeid=0, comment=''): + return self.__withBugDo(lambda b: b.close(resolution, dupeid=dupeid, + comment=comment)) + + def setstatus(self, status, comment=''): + return self.__withBugDo(lambda b: b.setstatus(status, comment=comment)) + + def setassignee(self, assigned_to='', reporter='', comment=''): + return self.__withBugDo(lambda b: b.setassignee(assigned_to=assigned_to, + reporter=reporter, + comment=comment)) + + def getwhiteboard(self, which='status'): + return self.__withBugDo(lambda b: b.getwhiteboard(which=which)) + + def appendwhiteboard(self, text, which='status'): + return self.__withBugDo(lambda b: b.appendwhiteboard(text, which=which)) + + def prependwhiteboard(self, text, which='status'): + return self.__withBugDo(lambda b: b.prependwhiteboard(text, which=which)) + + def setwhiteboard(self, text, which='status'): + return self.__withBugDo(lambda b: b.setwhiteboard(text, which=which)) + diff --git a/python/report1/plugins/ftp/__init__.py b/python/report1/plugins/ftp/__init__.py index e69de29..c0a663c 100644 --- a/python/report1/plugins/ftp/__init__.py +++ b/python/report1/plugins/ftp/__init__.py @@ -0,0 +1,108 @@ +""" + a report plugin to send to reports to ftp sites + Copyright (C) 2010 Red Hat, Inc + + Author(s): Adam Stokes + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import os +import socket + +import report1 as reportmodule + +from report1.io import DisplayFailMessage +from report1.io import DisplaySuccessMessage +from report1 import _report as _ + + +def labelFunction(label): + if label: + return label + return 'ftp' + +def descriptionFunction(optionsDict): + if optionsDict.has_key('description'): + return optionsDict['description'] + return 'ftp plugin' + +def report(signature, io, optionsDict): + if not io: + DisplayFailMessage(None, _("No IO"), + _("No io provided.")) + return False + + fileName = reportmodule.serializeToFile(signature,io) + + if fileName is None: + return None + + elif fileName is False: + return False + + else: + return ftpFile(fileName, file(fileName), io, optionsDict) + +def ftpFile(fileName, fileBlob, io, optionsDict): + username = None + password = None + + from urlparse import urlparse + if optionsDict.has_key('urldir'): + ftpserver = optionsDict['urldir'] + else: + ftpserver = io.queryField(_("Enter remote FTP directory as URL")) + if ftpserver is None: + return None + + if not ftpserver.startswith("ftp://"): + ftpserver = "ftp://" + ftpserver + + scheme, netloc, path, params, query, fragment = urlparse(ftpserver) + login = None + # check for user/pass + if netloc.find('@') > 0: + login, netloc = netloc.split('@') + # split user/pass + if login and login.find(':') > 0: + username, password = login.split(':') + # split netloc/port + if netloc.find(':') > 0: + netloc, port = netloc.split(':') + else: + port = 21 + + try: + import ftplib + ftp = ftplib.FTP() + ftp.connect(netloc, port) + if username and password: + ftp.login(username, password) + else: + ftp.login() + ftp.cwd(path) + ftp.set_pasv(True) + ftp.storbinary('STOR %s' % os.path.basename(fileName), fileBlob) + ftp.quit() + except ftplib.all_errors, e: + DisplayFailMessage(io, _("Upload failed"), + _("Upload has failed for remote path: %(ftpserver)s, %(error)s" % {'ftpserver':ftpserver,'error':e})) + return False + else: + DisplaySuccessMessage(io, _("Upload Successful"), + _("The signature was successfully uploaded to:"), + None, ftpserver + '/' + os.path.basename(fileName)) + return True diff --git a/python/report1/plugins/localsave/__init__.py b/python/report1/plugins/localsave/__init__.py index e69de29..09cdfc7 100644 --- a/python/report1/plugins/localsave/__init__.py +++ b/python/report1/plugins/localsave/__init__.py @@ -0,0 +1,110 @@ +""" + A Report plugin to save a report to a local file. + Copyright (C) 2009 Red Hat, Inc + + Author: Gavin Romig-Koch + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import os +import stat +import shutil + +from report1 import io as iomodule +from report1.io import DisplaySuccessMessage +from report1.io import DisplayFailMessage +import report1 as reportmodule +from report1 import _report as _ + +def labelFunction(label): + if label: + return label + return 'localsave' +def descriptionFunction(optionsDict): + if optionsDict.has_key('description'): + return optionsDict['description'] + return 'localsave plugin' + +def report(signature, io, optionsDict): + if not io: + DisplayFailMessage(None, _("No IO"), + _("No io provided.")) + return False + + fileName = reportmodule.serializeToFile(signature,io) + + if fileName is None: + return None + + elif fileName is False: + return False + + else: + return copyFile(fileName, io, optionsDict) + +def copyFile(fileName, io, optionsDict): + + if optionsDict.has_key('path'): + dirpath = optionsDict['path'] + else: + dirpath = io.queryField(_("directory to store report in")) + if dirpath == None: + return None + if not dirpath or dirpath.strip() == "": + DisplayFailMessage(io, _("local save Failed"), + _("directory name required")) + return False + + if os.path.exists(dirpath): + mode = os.stat(dirpath)[stat.ST_MODE] + if not stat.S_ISDIR(mode): + DisplayFailMessage(io, _("local save Failed"), + _("'%s' already exists, but is not a directory") % dirpath) + return False + else: + createp = io.queryChoice(_("'%s' does not exist, create it?") % dirpath, + [ iomodule.ChoiceValue(_("Yes"), + _("Create the directory"), + True), + iomodule.ChoiceValue(_("No"), + _("Do not create the directory"), + False) ]) + if createp is None: + return None + + elif createp: + try: + os.makedirs(dirpath) + + except EnvironmentError, e: + DisplayFailMessage(io, _("local save Failed"), + _("could not create '%(dir)s': %(error)s") % {'dir':dirpath,'error':str(e)}) + return False + + target = "%s/%s" % (dirpath, os.path.basename(fileName)) + try: + if os.path.realpath(fileName) != os.path.realpath(target): + shutil.copyfile(fileName, target) + + except EnvironmentError, e: + DisplayFailMessage(io, _("local save Failed"), + _("could not save report to '%(target)s': %(error)s") % {'target':target,'error':str(e)}) + return False + + DisplaySuccessMessage(io, _("local save Successful"), + _("The signature was successfully copied to:"), + None, target) + return True diff --git a/python/report1/plugins/scp/__init__.py b/python/report1/plugins/scp/__init__.py index e69de29..908c072 100644 --- a/python/report1/plugins/scp/__init__.py +++ b/python/report1/plugins/scp/__init__.py @@ -0,0 +1,178 @@ +""" + A Report plugin to send a report to another host using SCP. + Copyright (C) 2009 Red Hat, Inc + + Author(s): Gavin Romig-Koch + Adam Stokes + + Much of the code in this module was derived from code written by + Chris Lumens and Will Woods . + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import os +import pty +from report1.io import DisplaySuccessMessage +from report1.io import DisplayFailMessage +from report1 import _report as _ + +import report1 as reportmodule + +def labelFunction(label): + if label: + return label + return 'scp' +def descriptionFunction(optionsDict): + if optionsDict.has_key('description'): + return optionsDict['description'] + return "scp the problem to a given host and filename" + +def report(signature, io, optionsDict): + if not io: + DisplayFailMessage(None, _("No IO"), + _("No io provided.")) + return False + + fileName = reportmodule.serializeToFile(signature,io) + + if fileName is None: + return None + + elif fileName is False: + return False + + else: + return copyFileToRemote(fileName, io, optionsDict) + +def scpAuthenticate(master, childpid, password): + childoutput = "" + while True: + # Read up to password prompt. Propagate OSError exceptions, which + # can occur for anything that causes scp to immediately die (bad + # hostname, host down, etc.) + buf = os.read(master, 4096) + childoutput += buf + if buf.lower().find("password: ") != -1: + os.write(master, password+"\n") + # read the space and newline that get echoed back + buf = os.read(master, 2) + childoutput += buf + break + + while True: + try: + buf = os.read(master, 4096) + childoutput += buf + except (OSError, EOFError): + break + + (pid, childstatus) = os.waitpid (childpid, 0) + return (childstatus,childoutput) + +def copyFileToRemote(exnFileName, io, optionsDict): + + if optionsDict.has_key('host'): + host = optionsDict['host'] + else: + host = io.queryField("host") + if host == None: + return None + if not host or host.strip() == "": + DisplayFailMessage(io, _("No Host"), + _("Please provide a valid hostname")) + return False + + if host.find(":") != -1: + (host, port) = host.split(":") + + # Try to convert the port to an integer just as a check to see + # if it's a valid port number. If not, they'll get a chance to + # correct the information when scp fails. + try: + int(port) + portArgs = ["-P", port] + except ValueError: + portArgs = [] + else: + portArgs = [] + + loginResult = io.queryLogin(host) + if not loginResult: + return None + + if 'username' not in loginResult and \ + 'password' not in loginResult: + DisplayFailMessage(io, _("Login Input Failed"), + _("Please provide a valid username and password")) + return False + + if 'path' in optionsDict: + path = optionsDict['path'] + else: + path = io.queryField("path") + if path == None: + return None + if not path or path.strip() == "": + DisplayFailMessage(io, _("No Path"), + _("Please provide a path")) + return False + + target = "%s@%s:%s" % (loginResult['username'], host, path) + + # Fork ssh into its own pty + (childpid, master) = pty.fork() + if childpid < 0: + raise RuntimeError("Could not fork process to run scp") + elif childpid == 0: + # child process - run scp + args = ["scp", + "-oGSSAPIAuthentication=no", + "-oHostbasedAuthentication=no", + "-oPubkeyAuthentication=no", + "-oChallengeResponseAuthentication=no", + "-oPasswordAuthentication=yes", + "-oNumberOfPasswordPrompts=1", + "-oStrictHostKeyChecking=no", + "-oUserKnownHostsFile=/dev/null", + ] + portArgs + \ + [exnFileName, target] + os.execvp("scp", args) + + # parent process + try: + (childstatus,childoutput) = scpAuthenticate(master, childpid, loginResult['password']) + except OSError, e: + DisplayFailMessage(io, _("scp failed"), + _("OSError during scp file from %(filename)s to %(target)s: %(error)s") % + {'filename':exnFileName,'target':target,'error':e}) + return False + + os.close(master) + + if os.WIFEXITED(childstatus) and os.WEXITSTATUS(childstatus) == 0: + io.updateLogin(host,loginResult) + DisplaySuccessMessage(io, _("scp Successful"), + _("The signature was successfully copied to:"), + None, target) + return True + else: + DisplayFailMessage(io, _("scp failed"), + (_("unexpected child status (%(childstatus)s) during scp\n" \ + "scp %(filename)s %(target)s\n%(childoutput)s") % + {'childstatus': childstatus, 'filename':exnFileName, + 'target':target, 'childoutput':childoutput})) + return False + diff --git a/python/report1/plugins/strata/__init__.py b/python/report1/plugins/strata/__init__.py index e69de29..862c264 100644 --- a/python/report1/plugins/strata/__init__.py +++ b/python/report1/plugins/strata/__init__.py @@ -0,0 +1,309 @@ +""" + A Report plugin to send a report to the Strata API. + Copyright (C) 2009 Red Hat, Inc + + Author: Gavin Romig-Koch + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +""" + +import os +import stat +import shutil +import re + +from report1 import io as iomodule +from report1.io import DisplaySuccessMessage +from report1.io import DisplayFailMessage +import report1 as reportmodule +from report1 import _report as _ +import xml.etree.ElementTree as etree + +from report1 import release_information + +from .strata import post_signature, send_report_to_new_case, send_report_to_existing_case, strata_client_strerror + +def labelFunction(label): + if label: + return label + return 'strata' + +def descriptionFunction(optionsDict): + if optionsDict.has_key('description'): + return optionsDict['description'] + return 'strata plugin' + +def strataURL(optionsDict): + if optionsDict.has_key("strataURL"): + return optionsDict["strataURL"] + strata_host = "access.redhat.com" + if optionsDict.has_key("strata_host"): + strata_host = optionsDict["strata_host"] + return "http://" + strata_host + "/Strata" + + + +def report(signature, io, optionsDict): + + if not io: + DisplayFailMessage(None, _("No IO"), + _("No io provided.")) + return False + + file_list = [] + if signature.has_key("simpleFile"): + file_list.append((signature["simpleFile"].asFileName(), + signature["simpleFile"].fileName)) + + else: + filelocation = reportmodule.serializeAsSignature(signature) + if filelocation is None: + return None + elif filelocation is False: + return False + + file_list.append((filelocation,"report.xml")) + for (key,value) in signature.iteritems(): + if value.isBinary: + file_list.append((value.asFileName(),value.fileName)) + + if 'component' in signature: + component = signature['component'].asString() + else: + component = None + + if 'summary' in signature: + summary = signature['summary'].asString() + else: + summary = None + + if not summary: + if not component: + summary = "Case Created By Report Library" + else: + summary = "Case Created for %s" % (component,) + + if 'description' in signature: + description = signature['description'].asString() + else: + description = None + + if not description: + description = summary + + choice_attach = 4 + choice_new = 5 + if 'ticket' in optionsDict: + choice = choice_attach + + else: + choices = [ + iomodule.ChoiceValue(_("Create new case"), _("Create a new case and attach report to it."), choice_new), + iomodule.ChoiceValue(_("Attach to existing case"), _("Attach report to an existing case."), choice_attach) + ] + + choice = io.queryChoice(_("Do you want to attach the report to an existing case or create a new case?"), choices) + if choice is None: + return None + + + URL = strataURL(optionsDict) + cert_data = None + + if 'sslcertdata' in optionsDict: + cert_data = optionsDict['sslcertdata'] + + strata_host = os.path.basename(os.path.dirname(URL)) + loginResult = io.queryLogin(strata_host) + if not loginResult: + return None + + if 'username' not in loginResult and \ + 'password' not in loginResult: + DisplayFailMessage(io, _("Missing Login Information"), + _("Please provide a valid username and password.")) + return False + + + if choice == choice_new: + if 'product' in signature: + product = signature['product'].asString() + else: + product = release_information.getProduct() + + if 'version' in signature: + version = signature['version'].asString() + else: + version = release_information.getVersion() + + + # + # FIXME: + # In the case of a simpleFile report, when it is forwarded + # from one machine to another, it looses it's 'product' and + # 'version' information. So for these cases, whatever + # information we got is possibly bad. So make it good. + # + # The first fix needed here is to get good_xxx information + # from the server. The second fix is to allow someone + # to fix bad information with good. The third fix needed + # is to correct the simpleFile problem. + # + if signature.has_key("simpleFile"): + good_products = ['Red Hat Enterprise Linux'] + good_versions = ['6'] + if len(good_products) > 0 and product not in good_products: + product = good_products[0] + if len(good_versions) > 0 and version not in good_versions: + version = good_versions[0] + + (filelocation,filename) = file_list[0] + response = send_report_to_new_case(URL, + cert_data, + loginResult['username'], + loginResult['password'], + summary, description, + component, + product, + version, + None, + filename, + filelocation) + + if not response: + DisplayFailMessage(io, _("Case Creation Failed"), strata_client_strerror()) + return False + + title = _("Case Creation Response") + body = _("Case Creation Succeeded") + displayURL = "" + actualURL = "" + case_number = "" + + elif choice == choice_attach: + if 'ticket' in optionsDict: + case_number = optionsDict['ticket'] + else: + case_number = io.queryField("Enter existing case number") + + if case_number is None: + return None + + (filelocation,filename) = file_list[0] + response = send_report_to_existing_case(URL, + cert_data, + loginResult['username'], + loginResult['password'], + case_number, + None, + filename, + filelocation) + + if not response: + DisplayFailMessage(io, _("Report Attachement Failed"), strata_client_strerror()) + return False + + title = _("Report Attachment Response") + body = _("Report Attachment Succeded") + displayURL = "" + actualURL = "" + + io.updateLogin(strata_host, loginResult) + + try: + xml = etree.XML(response) + except Exception,e: + xml = None + + if xml: + for each in xml: + if each.tag == "title" and each.text: + title = each.text + elif each.tag == "body" and each.text: + body = each.text + elif each.tag == "URL": + if each.text: + displayURL = each.text + if 'href' in each.attrib and each.attrib['href']: + actualURL = each.attrib['href'] + else: + body = response + + if len(file_list) > 1 and not case_number and actualURL: + leading = actualURL + sep = '/' + trailing = '' + while leading and sep and not trailing: + (leading,sep,trailing) = leading.rpartition('/') + if trailing: + case_number = trailing + + if case_number and len(file_list) > 1: + for (filelocation,filename) in file_list[1:]: + response = send_report_to_existing_case(URL, + cert_data, + loginResult['username'], + loginResult['password'], + case_number, + None, + filename, + filelocation); + + if response: + try: + xml = etree.XML(response) + except Exception,e: + xml = None + + if xml: + for each in xml: + if each.tag == "title" and each.text: + if title: + title += "; " + each.text + else: + title = each.title + elif each.tag == "body" and each.text: + if body: + body += '\n' + each.text + else: + body = each.text + else: + if body: + body += '\n' + response + else: + body = response + + if 'buttonURLPattern' in optionsDict: + buttonURLPattern = optionsDict['buttonURLPattern'] + else: + buttonURLPattern = None + + if 'buttonURLRepl' in optionsDict: + buttonURLRepl = optionsDict['buttonURLRepl'] + else: + buttonURLRepl = None + + if buttonURLPattern and buttonURLRepl: + if actualURL: + newURL = re.sub(buttonURLPattern, + buttonURLRepl, + actualURL) + if displayURL == actualURL: + displayURL = newURL + actualURL = newURL + + DisplaySuccessMessage(io, title, body, actualURL, displayURL) + return True + diff --git a/python/report1/plugins/strata/strata.py b/python/report1/plugins/strata/strata.py index e69de29..dc3d933 100644 --- a/python/report1/plugins/strata/strata.py +++ b/python/report1/plugins/strata/strata.py @@ -0,0 +1,21 @@ +from ctypes import * + +strata_client_lib = CDLL('libstrata_client.so') + +post_signature = strata_client_lib.post_signature +post_signature.argtypes = [ c_char_p, c_char_p, c_char_p, c_char_p ] +post_signature.restype = c_char_p + +send_report_to_new_case = strata_client_lib.send_report_to_new_case +send_report_to_new_case.argtypes = [ c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p ] +send_report_to_new_case.restype = c_char_p + +send_report_to_existing_case = strata_client_lib.send_report_to_existing_case +send_report_to_existing_case.argtypes = [ c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p, c_char_p ] +send_report_to_existing_case.restype = c_char_p + +strata_client_strerror = strata_client_lib.strata_client_strerror +strata_client_strerror.argtypes = [] +strata_client_strerror.restype = c_char_p + + diff --git a/python/report1/release_information.py b/python/report1/release_information.py index e69de29..3d4870b 100644 --- a/python/report1/release_information.py +++ b/python/report1/release_information.py @@ -0,0 +1,142 @@ +# Copyright (C) 2008, 2009, 2010 Red Hat, Inc. +# All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# Author(s): Gavin Romig-Koch +# + +import os + +SYSTEM_RELEASE_PATHS = ["/etc/system-release","/etc/redhat-release"] +SYSTEM_RELEASE_DEPS = ["system-release", "redhat-release"] + +_hardcoded_default_product = "" +_hardcoded_default_version = "" + +def getProduct_fromRPM(): + try: + import rpm + ts = rpm.TransactionSet() + for each_dep in SYSTEM_RELEASE_DEPS: + mi = ts.dbMatch('provides', each_dep) + for h in mi: + if h['name']: + return h['name'].split("-")[0].capitalize() + + return "" + except: + return "" + +def getProduct_fromPRODUCT(): + try: + from pyanaconda import product + return product.productName + except: + try: + import product + return product.productName + except: + return "" + +def getProduct_fromFILE(): + for each_path in SYSTEM_RELEASE_PATHS: + if os.path.exists(each_path): + file = open(each_path, "r") + content = file.read() + if content.startswith("Red Hat Enterprise Linux"): + return "Red Hat Enterprise Linux" + + if content.startswith("Fedora"): + return "Fedora" + + i = content.find(" release") + if i > -1: + return content[0:i] + + return "" + +def getVersion_fromRPM(): + try: + import rpm + ts = rpm.TransactionSet() + for each_dep in SYSTEM_RELEASE_DEPS: + mi = ts.dbMatch('provides', each_dep) + for h in mi: + if h['version']: + return str(h['version']) + + return "" + except: + return "" + +def getVersion_fromFILE(): + for each_path in SYSTEM_RELEASE_PATHS: + if os.path.exists(each_path): + file = open(each_path, "r") + content = file.read() + if content.find("Rawhide") > -1: + return "rawhide" + + clist = content.split(" ") + i = clist.index("release") + return clist[i+1] + else: + return "" + +def getVersion_fromPRODUCT(): + try: + from pyanaconda import product + return product.productVersion + except: + try: + import product + return product.productVersion + except: + return "" + + +def getProduct(): + """Attempt to determine the product of the running system by first asking + rpm, and then falling back on a hardcoded default. + """ + product = getProduct_fromPRODUCT() + if product: + return product + product = getProduct_fromFILE() + if product: + return product + product = getProduct_fromRPM() + if product: + return product + + return _hardcoded_default_product + +def getVersion(): + """Attempt to determine the version of the running system by first asking + rpm, and then falling back on a hardcoded default. Always return as + a string. + """ + version = getVersion_fromPRODUCT() + if version: + return version + version = getVersion_fromFILE() + if version: + return version + version = getVersion_fromRPM() + if version: + return version + + return _hardcoded_default_version + diff --git a/report.spec.in b/report.spec.in index 46fda4f..74a1ac5 100644 --- a/report.spec.in +++ b/report.spec.in @@ -278,21 +278,21 @@ make install DESTDIR=$RPM_BUILD_ROOT %if 0%{?bugzilla} %if ! 0%{?bugzilla_rhel} rm $RPM_BUILD_ROOT%{_sysconfdir}/report.d/RHEL-bugzilla.redhat.com.conf -rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report/plugins/RHEL-bugzilla +rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report1/plugins/RHEL-bugzilla %else rm $RPM_BUILD_ROOT%{_sysconfdir}/report.d/bugzilla.redhat.com.conf -rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report/plugins/bugzilla +rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report1/plugins/bugzilla %endif %else rm $RPM_BUILD_ROOT%{_sysconfdir}/report.d/RHEL-bugzilla.redhat.com.conf -rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report/plugins/RHEL-bugzilla +rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report1/plugins/RHEL-bugzilla rm $RPM_BUILD_ROOT%{_sysconfdir}/report.d/bugzilla.redhat.com.conf -rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report/plugins/bugzilla +rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report1/plugins/bugzilla %endif %if ! 0%{?strata} rm -rf $RPM_BUILD_ROOT%{_bindir}/strata -rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report/plugins/strata +rm -rf $RPM_BUILD_ROOT%{python_sitearch}/report1/plugins/strata rm -rf $RPM_BUILD_ROOT%{_sysconfdir}/report.d/RHEL.conf %endif %if ! 0%{?strata_test} @@ -310,18 +310,18 @@ rm -rf $RPM_BUILD_ROOT %files -f %{name}.lang %defattr(-,root,root,-) %doc README LICENCE -%dir %{python_sitearch}/report -%{python_sitearch}/report/__init__.py* -%{python_sitearch}/report/accountmanager.py* -%{python_sitearch}/report/release_information.py* -%dir %{python_sitearch}/report/io -%{python_sitearch}/report/io/__init__.py* -%{python_sitearch}/report/io/TextIO.py* -%dir %{python_sitearch}/report/plugins -%{python_sitearch}/report/plugins/__init__.py* +%dir %{python_sitearch}/report1 +%{python_sitearch}/report1/__init__.py* +%{python_sitearch}/report1/accountmanager.py* +%{python_sitearch}/report1/release_information.py* +%dir %{python_sitearch}/report1/io +%{python_sitearch}/report1/io/__init__.py* +%{python_sitearch}/report1/io/TextIO.py* +%dir %{python_sitearch}/report1/plugins +%{python_sitearch}/report1/plugins/__init__.py* %dir %{_sysconfdir}/report.d -%{_bindir}/report -%{_mandir}/man1/report.1.gz +%{_bindir}/report1 +%{_mandir}/man1/report1.1.gz %{_mandir}/man5/report.conf.5.gz %dir %{_var}/report %attr(0644,root,root) %config(noreplace) %{_sysconfdir}/report.conf @@ -333,7 +333,7 @@ rm -rf $RPM_BUILD_ROOT %{_libdir}/libstrata_client.so.0 %{_libdir}/libstrata_client.so.0.0.0 %if 0%{?strata} -%{python_sitearch}/report/plugins/strata +%{python_sitearch}/report1/plugins/strata %config(noreplace) %{_sysconfdir}/report.d/RHEL.conf %if 0%{?strata_test} %config(noreplace) %{_sysconfdir}/report.d/strata-test.conf @@ -342,34 +342,34 @@ rm -rf $RPM_BUILD_ROOT %files gtk %defattr(-,root,root,-) -%{python_sitearch}/report/io/GTKIO.py* +%{python_sitearch}/report1/io/GTKIO.py* %files newt %defattr(-,root,root,-) -%{python_sitearch}/report/io/NewtIO.py* +%{python_sitearch}/report1/io/NewtIO.py* %files plugin-ftp %defattr(-,root,root,-) -%{python_sitearch}/report/plugins/ftp +%{python_sitearch}/report1/plugins/ftp %files plugin-scp %defattr(-,root,root,-) -%{python_sitearch}/report/plugins/scp +%{python_sitearch}/report1/plugins/scp %files plugin-localsave %defattr(-,root,root,-) -%{python_sitearch}/report/plugins/localsave +%{python_sitearch}/report1/plugins/localsave %if 0%{?bugzilla} %if 0%{?bugzilla_rhel} %files plugin-RHEL-bugzilla %defattr(-,root,root,-) -%{python_sitearch}/report/plugins/RHEL-bugzilla +%{python_sitearch}/report1/plugins/RHEL-bugzilla %else %files plugin-bugzilla %defattr(-,root,root,-) -%{python_sitearch}/report/plugins/bugzilla +%{python_sitearch}/report1/plugins/bugzilla %endif %endif