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>
-#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
+%def>
+
+Your input was bad:
+
+Field | value | reason |
+${err_field} | ${err_value} | ${errorMessage} |
+%if stderr:
+Printed to standard error:
+${stderr}
+%endif
+
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:
-
-operation | Field | value | reason |
-$op | $err_field | $err_value | $errorMessage |
-#if $stderr
-Printed to standard error:
-$stderr
-#end if
-
-#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