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 hmac
 import random
 import sha
-import simplejson
 import sys
 import time
 import urllib
 import socket
 import cherrypy
 import sys
 import time
 import urllib
 import socket
 import cherrypy
+from cherrypy import _cperror
 from StringIO import StringIO
 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"""
 
 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 atexit
     atexit.register(printError)
 
-import templates
-from Cheetah.Template import Template
 import validation
 import cache_acls
 from webcommon import State
 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 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
 
 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
         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['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"):
 
     def __getattr__(self, name):
         if name in ("admin", "overlord"):
@@ -71,6 +95,22 @@ class InvirtWeb(View):
         else:
             return super(InvirtWeb, self).__getattr__(name)
 
         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):
     @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."""
     @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):
         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
                 if not back:
                     raise
                 print >> sys.stderr, err
-                result = err
+                result = str(err)
             else:
                 result = 'Success!'
                 if not back:
             else:
                 result = 'Success!'
                 if not back:
@@ -317,14 +358,6 @@ console will suffer artifacts.
 
     machine = MachineView()
 
 
     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()
 class Checkpoint:
     def __init__(self):
         self.start_time = time.time()
@@ -340,35 +373,6 @@ class Checkpoint:
 
 checkpoint = 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
 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])
 
         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:
 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)
 
         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)
 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
 
              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
 
 def send_error_mail(subject, body):
     import subprocess
 
@@ -684,98 +664,4 @@ Subject: %s
     p.stdin.close()
     p.wait()
 
     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!
 ERROR!
-#end def
+</%def>
 
 
-#def body
 <p>Uh-oh!  We experienced an error.  Sorry about that.  We've gotten
 mail about it.</p>
 
 <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>
 <p>In case you're curious, the gory details are below.</p>
 
 <pre>
-$details
+${details}
 </pre>
 </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
 
 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
 
 from invirt.config import structs as config
 from webcommon import State
 
@@ -27,10 +28,9 @@ class MakoLoader(object):
     
     def __init__(self):
         self.lookups = {}
     
     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:
         # Find the appropriate template lookup.
         key = (tuple(directories), module_directory)
         try:
@@ -45,7 +45,13 @@ class MakoLoader(object):
                                     imports=imports,
                                     )
             self.lookups[key] = lookup
                                     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)
         
         # 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)
 
 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):
 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.
 
   * 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
 
 
 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
  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
  python-dns, python-dnspython, python-cherrypy3,
  python-mako,
 # misc