From 01cd9908731ffbf23f58fde2b64153a35e950a80 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Mon, 28 Sep 2009 03:04:33 -0400 Subject: [PATCH] Full error handling svn path=/package_branches/invirt-web/cherrypy-rebased/; revision=2693 --- code/main.py | 224 +++++++---------------------- code/templates/{error.tmpl => error.mako} | 12 +- code/templates/error_raw.mako | 12 ++ code/templates/error_raw.tmpl | 12 -- code/templates/invalid.mako | 16 +++ code/templates/invalid.tmpl | 18 --- code/view.py | 42 +++++- debian/changelog | 3 +- debian/control | 2 +- 9 files changed, 127 insertions(+), 214 deletions(-) rename code/templates/{error.tmpl => error.mako} (73%) create mode 100644 code/templates/error_raw.mako delete mode 100644 code/templates/error_raw.tmpl create mode 100644 code/templates/invalid.mako delete mode 100644 code/templates/invalid.tmpl diff --git a/code/main.py b/code/main.py index 361fbbd..732b7a2 100755 --- a/code/main.py +++ b/code/main.py @@ -8,21 +8,13 @@ import datetime import hmac import random import sha -import simplejson import sys import time import urllib import socket import cherrypy +from cherrypy import _cperror from StringIO import StringIO -def revertStandardError(): - """Move stderr to stdout, and return the contents of the old stderr.""" - errio = sys.stderr - if not isinstance(errio, StringIO): - return '' - sys.stderr = sys.stdout - errio.seek(0) - return errio.read() def printError(): """Revert stderr to stdout, and print the contents of stderr""" @@ -33,8 +25,6 @@ if __name__ == '__main__': import atexit atexit.register(printError) -import templates -from Cheetah.Template import Template import validation import cache_acls from webcommon import State @@ -45,7 +35,7 @@ from invirt.database import Machine, CDROM, session, connect, MachineAccess, Typ from invirt.config import structs as config from invirt.common import InvalidInput, CodeError -from view import View +from view import View, revertStandardError class InvirtUnauthWeb(View): @cherrypy.expose @@ -58,8 +48,42 @@ class InvirtWeb(View): super(self.__class__,self).__init__() connect() self._cp_config['tools.require_login.on'] = True + self._cp_config['tools.catch_stderr.on'] = True self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config', 'from invirt import database'] + self._cp_config['request.error_response'] = self.handle_error + + @cherrypy.expose + @cherrypy.tools.mako(filename="/invalid.mako") + def invalidInput(self): + """Print an error page when an InvalidInput exception occurs""" + err = cherrypy.request.prev.params["err"] + emsg = cherrypy.request.prev.params["emsg"] + d = dict(err_field=err.err_field, + err_value=str(err.err_value), stderr=emsg, + errorMessage=str(err)) + return d + + @cherrypy.expose + @cherrypy.tools.mako(filename="/error.mako") + def error(self): + #op, username, fields, err, emsg, traceback): + """Print an error page when an exception occurs""" + op = cherrypy.request.prev.path_info + username = cherrypy.request.login + err = cherrypy.request.prev.params["err"] + emsg = cherrypy.request.prev.params["emsg"] + traceback = cherrypy.request.prev.params["traceback"] + d = dict(op = op, user=username, fields=cherrypy.request.prev.params, + errorMessage=str(err), stderr=emsg, traceback=traceback) + error_raw = cherrypy.tools.mako.callable.get_lookup(**cherrypy.tools.mako._merged_args()).get_template("/error_raw.mako") + details = error_raw.render(**d) + exclude = config.web.errormail_exclude + if username not in exclude and '*' not in exclude: + send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err), + details) + d['details'] = details + return d def __getattr__(self, name): if name in ("admin", "overlord"): @@ -71,6 +95,22 @@ class InvirtWeb(View): else: return super(InvirtWeb, self).__getattr__(name) + def handle_error(self): + err = sys.exc_info()[1] + if isinstance(err, InvalidInput): + e = revertStandardError() + cherrypy.request.params['err'] = err + cherrypy.request.params['emsg'] = e + raise cherrypy.InternalRedirect('/invalidInput') + if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params: + e = revertStandardError() + cherrypy.request.params['err'] = err + cherrypy.request.params['emsg'] = e + cherrypy.request.params['traceback'] = _cperror.format_exc() + raise cherrypy.InternalRedirect('/error') + # fall back to cherrypy default error page + cherrypy.HTTPError(500).set_response() + @cherrypy.expose @cherrypy.tools.mako(filename="/list.mako") def list(self, result=None): @@ -200,6 +240,7 @@ console will suffer artifacts. @cherrypy.expose def errortest(self): """Throw an error, to test the error-tracing mechanisms.""" + print >>sys.stderr, "look ma, it's a stderr" raise RuntimeError("test of the emergency broadcast system") class MachineView(View): @@ -302,7 +343,7 @@ console will suffer artifacts. if not back: raise print >> sys.stderr, err - result = err + result = str(err) else: result = 'Success!' if not back: @@ -317,14 +358,6 @@ console will suffer artifacts. machine = MachineView() -def pathSplit(path): - if path.startswith('/'): - path = path[1:] - i = path.find('/') - if i == -1: - i = len(path) - return path[:i], path[i:] - class Checkpoint: def __init__(self): self.start_time = time.time() @@ -340,35 +373,6 @@ class Checkpoint: checkpoint = Checkpoint() -def makeErrorPre(old, addition): - if addition is None: - return - if old: - return old[:-6] + '\n----\n' + str(addition) + '' - else: - return '

STDERR:

' + str(addition) + '
' - -Template.database = database -Template.config = config -Template.err = None - -class JsonDict: - """Class to store a dictionary that will be converted to JSON""" - def __init__(self, **kws): - self.data = kws - if 'err' in kws: - err = kws['err'] - del kws['err'] - self.addError(err) - - def __str__(self): - return simplejson.dumps(self.data) - - def addError(self, text): - """Add stderr text to be displayed on the website.""" - self.data['err'] = \ - makeErrorPre(self.data.get('err'), text) - class Defaults: """Class to store default values for fields.""" memory = 256 @@ -388,17 +392,6 @@ class Defaults: for key in kws: setattr(self, key, kws[key]) - - -DEFAULT_HEADERS = {'Content-Type': 'text/html'} - -def invalidInput(op, username, fields, err, emsg): - """Print an error page when an InvalidInput exception occurs""" - d = dict(op=op, user=username, err_field=err.err_field, - err_value=str(err.err_value), stderr=emsg, - errorMessage=str(err)) - return templates.invalid(searchList=[d]) - def hasVnc(status): """Does the machine with a given status list support VNC?""" if status is None: @@ -567,11 +560,6 @@ def modifyDict(username, state, machine_id, fields): controls.renameMachine(machine, oldname, validate.name) return dict(machine=machine) - -def badOperation(u, s, p, e): - """Function called when accessing an unknown URI.""" - return ({'Status': '404 Not Found'}, 'Invalid operation.') - def infoDict(username, state, machine): """Get the variables used by info.tmpl.""" status = controls.statusInfo(machine) @@ -660,14 +648,6 @@ def infoDict(username, state, machine): fields = fields) return d -mapping = dict() - -def printHeaders(headers): - """Print a dictionary as HTTP headers.""" - for key, value in headers.iteritems(): - print '%s: %s' % (key, value) - print - def send_error_mail(subject, body): import subprocess @@ -684,98 +664,4 @@ Subject: %s p.stdin.close() p.wait() -def show_error(op, username, fields, err, emsg, traceback): - """Print an error page when an exception occurs""" - d = dict(op=op, user=username, fields=fields, - errorMessage=str(err), stderr=emsg, traceback=traceback) - details = templates.error_raw(searchList=[d]) - exclude = config.web.errormail_exclude - if username not in exclude and '*' not in exclude: - send_error_mail('xvm error on %s for %s: %s' % (op, username, err), - details) - d['details'] = details - return templates.error(searchList=[d]) - -def handler(username, state, path, fields): - operation, path = pathSplit(path) - if not operation: - operation = 'list' - print 'Starting', operation - fun = mapping.get(operation, badOperation) - return fun(username, state, path, fields) - -class App: - def __init__(self, environ, start_response): - self.environ = environ - self.start = start_response - - self.username = getUser(environ) - self.state = State(self.username) - self.state.environ = environ - - random.seed() #sigh - - def __iter__(self): - start_time = time.time() - database.clear_cache() - sys.stderr = StringIO() - fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ) - operation = self.environ.get('PATH_INFO', '') - if not operation: - self.start("301 Moved Permanently", [('Location', './')]) - return - if self.username is None: - operation = 'unauth' - - try: - checkpoint.checkpoint('Before') - output = handler(self.username, self.state, operation, fields) - checkpoint.checkpoint('After') - - headers = dict(DEFAULT_HEADERS) - if isinstance(output, tuple): - new_headers, output = output - headers.update(new_headers) - e = revertStandardError() - if e: - if hasattr(output, 'addError'): - output.addError(e) - else: - # This only happens on redirects, so it'd be a pain to get - # the message to the user. Maybe in the response is useful. - output = output + '\n\nstderr:\n' + e - output_string = str(output) - checkpoint.checkpoint('output as a string') - except Exception, err: - if not fields.has_key('js'): - if isinstance(err, InvalidInput): - self.start('200 OK', [('Content-Type', 'text/html')]) - e = revertStandardError() - yield str(invalidInput(operation, self.username, fields, - err, e)) - return - import traceback - self.start('500 Internal Server Error', - [('Content-Type', 'text/html')]) - e = revertStandardError() - s = show_error(operation, self.username, fields, - err, e, traceback.format_exc()) - yield str(s) - return - status = headers.setdefault('Status', '200 OK') - del headers['Status'] - self.start(status, headers.items()) - yield output_string - if fields.has_key('timedebug'): - yield '
%s
' % cgi.escape(str(checkpoint)) - -def constructor(): - connect() - return App - -def main(): - from flup.server.fcgi_fork import WSGIServer - WSGIServer(constructor()).run() - -if __name__ == '__main__': - main() +random.seed() diff --git a/code/templates/error.tmpl b/code/templates/error.mako similarity index 73% rename from code/templates/error.tmpl rename to code/templates/error.mako index ff9b4e0..51c6a65 100644 --- a/code/templates/error.tmpl +++ b/code/templates/error.mako @@ -1,11 +1,10 @@ -#from skeleton import skeleton -#extends skeleton +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> -#def title +<%def name="title()"> ERROR! -#end def + -#def body

Uh-oh! We experienced an error. Sorry about that. We've gotten mail about it.

@@ -15,6 +14,5 @@ consistently biting you and we don't seem to be fixing it.

In case you're curious, the gory details are below.

-$details
+${details}
 
-#end def diff --git a/code/templates/error_raw.mako b/code/templates/error_raw.mako new file mode 100644 index 0000000..a8cf902 --- /dev/null +++ b/code/templates/error_raw.mako @@ -0,0 +1,12 @@ +Error on operation ${op} for user ${user}: ${errorMessage} + +Fields: +%for f in fields: +${f}=${fields[f]} +%endfor + +Error output: +${stderr}\ +---- end error output + +${traceback} diff --git a/code/templates/error_raw.tmpl b/code/templates/error_raw.tmpl deleted file mode 100644 index 5ae1490..0000000 --- a/code/templates/error_raw.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -Error on operation $op for user $user: $errorMessage - -Fields: -#for $f in $fields: -$f=$fields[$f].value -#end for - -Error output: -$stderr#slurp ----- end error output - -$traceback diff --git a/code/templates/invalid.mako b/code/templates/invalid.mako new file mode 100644 index 0000000..c597d18 --- /dev/null +++ b/code/templates/invalid.mako @@ -0,0 +1,16 @@ +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> + +<%def name="title()"> +Invalid Input + + +

Your input was bad:

+ + + +%if stderr: +

Printed to standard error:

+
${stderr}
+%endif +
Fieldvaluereason
${err_field}${err_value}${errorMessage}
diff --git a/code/templates/invalid.tmpl b/code/templates/invalid.tmpl deleted file mode 100644 index 952e1c3..0000000 --- a/code/templates/invalid.tmpl +++ /dev/null @@ -1,18 +0,0 @@ -#from skeleton import skeleton -#extends skeleton - -#def title -Invalid Input -#end def - -#def body -

Your input was bad:

- - - -#if $stderr -

Printed to standard error:

-
$stderr
-#end if -
operationFieldvaluereason
$op$err_field$err_value$errorMessage
-#end def diff --git a/code/view.py b/code/view.py index d15d53f..6c9caf8 100644 --- a/code/view.py +++ b/code/view.py @@ -1,10 +1,11 @@ -import os +import os, sys import cherrypy from mako.template import Template from mako.lookup import TemplateLookup import simplejson import datetime, decimal +from StringIO import StringIO from invirt.config import structs as config from webcommon import State @@ -27,10 +28,9 @@ class MakoLoader(object): def __init__(self): self.lookups = {} - - def __call__(self, filename, directories, module_directory=None, - collection_size=-1, content_type='text/html; charset=utf-8', - imports=[]): + + def get_lookup(self, directories, module_directory=None, + collection_size=-1, imports=[], **kwargs): # Find the appropriate template lookup. key = (tuple(directories), module_directory) try: @@ -45,7 +45,13 @@ class MakoLoader(object): imports=imports, ) self.lookups[key] = lookup - cherrypy.request.lookup = lookup + return lookup + + def __call__(self, filename, directories, module_directory=None, + collection_size=-1, content_type='text/html; charset=utf-8', + imports=[]): + cherrypy.request.lookup = lookup = self.get_lookup(directories, module_directory, + collection_size, imports) # Replace the current handler. cherrypy.request.template = t = lookup.get_template(filename) @@ -54,6 +60,30 @@ class MakoLoader(object): main = MakoLoader() cherrypy.tools.mako = cherrypy.Tool('on_start_resource', main) +def revertStandardError(): + """Move stderr to stdout, and return the contents of the old stderr.""" + errio = sys.stderr + if not isinstance(errio, StringIO): + return '' + sys.stderr = sys.stdout + errio.seek(0) + return errio.read() + +def catchStderr(): + old_handler = cherrypy.request.handler + def wrapper(*args, **kwargs): + sys.stderr = StringIO() + ret = old_handler(*args, **kwargs) + e = revertStandardError() + if e: + if isinstance(ret, dict): + ret["error_text"] = e + return ret + if old_handler: + cherrypy.request.handler = wrapper + +cherrypy.tools.catch_stderr = cherrypy.Tool('before_handler', catchStderr) + class JSONEncoder(simplejson.JSONEncoder): def default(self, obj): if isinstance(obj, datetime.datetime): diff --git a/debian/changelog b/debian/changelog index 9ac19a3..ae345b0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,8 +2,9 @@ invirt-web (0.0.25) unstable; urgency=low * Depend on python-cherrypy3 and python-mako in preparation of migrating the web site. + * Remove dependency on python-cheetah - -- Quentin Smith Sat, 02 May 2009 22:32:53 -0400 + -- Quentin Smith Mon, 28 Sep 2009 01:26:06 -0400 invirt-web (0.0.24) unstable; urgency=low diff --git a/debian/control b/debian/control index 3ae872c..e22a9ff 100644 --- a/debian/control +++ b/debian/control @@ -16,7 +16,7 @@ Depends: ${misc:Depends}, libapache2-mod-auth-sslcert, libapache2-mod-auth-kerb, debathena-ssl-certificates, # python libraries - python-flup, python-cheetah, python-simplejson, + python-flup, python-simplejson, python-dns, python-dnspython, python-cherrypy3, python-mako, # misc -- 1.7.9.5