Full error handling
authorQuentin Smith <quentin@mit.edu>
Mon, 28 Sep 2009 07:04:33 +0000 (03:04 -0400)
committerQuentin Smith <quentin@mit.edu>
Mon, 28 Sep 2009 07:04:33 +0000 (03:04 -0400)
svn path=/package_branches/invirt-web/cherrypy-rebased/; revision=2693

code/main.py
code/templates/error.mako [moved from code/templates/error.tmpl with 73% similarity]
code/templates/error_raw.mako [new file with mode: 0644]
code/templates/error_raw.tmpl [deleted file]
code/templates/invalid.mako [new file with mode: 0644]
code/templates/invalid.tmpl [deleted file]
code/view.py
debian/changelog
debian/control

index 361fbbd..732b7a2 100755 (executable)
@@ -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) + '</pre>'
-    else:
-        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
-
-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 '<pre>%s</pre>' % 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()
similarity index 73%
rename from code/templates/error.tmpl
rename to code/templates/error.mako
index ff9b4e0..51c6a65 100644 (file)
@@ -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>
 
-#def body
 <p>Uh-oh!  We experienced an error.  Sorry about that.  We've gotten
 mail about it.</p>
 
@@ -15,6 +14,5 @@ consistently biting you and we don't seem to be fixing it.</p>
 <p>In case you're curious, the gory details are below.</p>
 
 <pre>
-$details
+${details}
 </pre>
-#end def
diff --git a/code/templates/error_raw.mako b/code/templates/error_raw.mako
new file mode 100644 (file)
index 0000000..a8cf902
--- /dev/null
@@ -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 (file)
index 5ae1490..0000000
+++ /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 (file)
index 0000000..c597d18
--- /dev/null
@@ -0,0 +1,16 @@
+<%page expression_filter="h"/>
+<%inherit file="skeleton.mako" />
+
+<%def name="title()">
+Invalid Input
+</%def>
+
+<p>Your input was bad:</p>
+<table>
+<tr><td>Field</td><td>value</td><td>reason</td></tr>
+<tr><td>${err_field}</td><td>${err_value}</td><td>${errorMessage}</td></tr>
+%if stderr:
+<p>Printed to standard error:</p>
+<pre>${stderr}</pre>
+%endif
+</table>
diff --git a/code/templates/invalid.tmpl b/code/templates/invalid.tmpl
deleted file mode 100644 (file)
index 952e1c3..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-#from skeleton import skeleton
-#extends skeleton
-
-#def title
-Invalid Input
-#end def
-
-#def body
-<p>Your input was bad:</p>
-<table>
-<tr><td>operation</td><td>Field</td><td>value</td><td>reason</td></tr>
-<tr><td>$op</td><td>$err_field</td><td>$err_value</td><td>$errorMessage</td></tr>
-#if $stderr
-<p>Printed to standard error:</p>
-<pre>$stderr</pre>
-#end if
-</table>
-#end def
index d15d53f..6c9caf8 100644 (file)
@@ -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):
index 9ac19a3..ae345b0 100644 (file)
@@ -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 <quentin@mit.edu>  Sat, 02 May 2009 22:32:53 -0400
+ -- Quentin Smith <quentin@mit.edu>  Mon, 28 Sep 2009 01:26:06 -0400
 
 invirt-web (0.0.24) unstable; urgency=low
 
index 3ae872c..e22a9ff 100644 (file)
@@ -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