Add support for error response plugins

The Bottle plugin framework only applies to normal responses.

Build on that by monkey-patching the Bottle error handler with
a new one that makes a series of callbacks.

Update the existing plugins such that where appropriate they
can apply their logic to error responses in addition to normal
responses as they already do.

Change-Id: Ifc2bac0e5120a3b0475f3b78f8bd822711f9c736
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/module/obmc/wsgi/apps/rest_dbus.py b/module/obmc/wsgi/apps/rest_dbus.py
index 93f0895..95b9cb0 100644
--- a/module/obmc/wsgi/apps/rest_dbus.py
+++ b/module/obmc/wsgi/apps/rest_dbus.py
@@ -603,11 +603,30 @@
         return wrap
 
 
+class JsonErrorsPlugin(JSONPlugin):
+    ''' Extend the Bottle JSONPlugin such that it also encodes error
+        responses. '''
+
+    def __init__(self, app, **kw):
+        super(JsonErrorsPlugin, self).__init__(**kw)
+        self.json_opts = {
+            x: y for x, y in kw.iteritems()
+            if x in ['indent', 'sort_keys']}
+        app.install_error_callback(self.error_callback)
+
+    def error_callback(self, response_object, response_body, **kw):
+        response_body['body'] = json.dumps(response_object, **self.json_opts)
+        response.content_type = 'application/json'
+
+
 class JsonApiResponsePlugin(object):
-    ''' Emits normal responses in the OpenBMC json api format. '''
+    ''' Emits responses in the OpenBMC json api format. '''
     name = 'json_api_response'
     api = 2
 
+    def __init__(self, app):
+        app.install_error_callback(self.error_callback)
+
     def apply(self, callback, route):
         def wrap(*a, **kw):
             resp = {'data': callback(*a, **kw)}
@@ -616,54 +635,21 @@
             return resp
         return wrap
 
-
-class JsonApiErrorsPlugin(object):
-    ''' Emits error responses in the OpenBMC json api format. '''
-    name = 'json_api_errors'
-    api = 2
-
-    def __init__(self, **kw):
-        self.app = None
-        self.function_type = None
-        self.original = None
-        self.json_opts = {
-            x: y for x, y in kw.iteritems()
-            if x in ['indent', 'sort_keys']}
-
-    def setup(self, app):
-        self.app = app
-        self.function_type = type(app.default_error_handler)
-        self.original = app.default_error_handler
-        self.app.default_error_handler = self.function_type(
-            self.json_errors, app, Bottle)
-
-    def apply(self, callback, route):
-        return callback
-
-    def close(self):
-        self.app.default_error_handler = self.function_type(
-            self.original, self.app, Bottle)
-
-    def json_errors(self, res, error):
-        response_object = {'status': 'error', 'data': {}}
+    def error_callback(self, error, response_object, **kw):
         response_object['message'] = error.status_line
-        response_object['data']['description'] = str(error.body)
+        response_object.setdefault('data', {})['description'] = str(error.body)
         if error.status_code == 500:
             response_object['data']['exception'] = repr(error.exception)
             response_object['data']['traceback'] = error.traceback.splitlines()
 
-        json_response = json.dumps(response_object, **self.json_opts)
-        response.content_type = 'application/json'
-        return json_response
 
-
-class JsonpPlugin(JsonApiErrorsPlugin):
+class JsonpPlugin(object):
     ''' Json javascript wrapper. '''
     name = 'jsonp'
     api = 2
 
-    def __init__(self, **kw):
-        super(JsonpPlugin, self).__init__(**kw)
+    def __init__(self, app, **kw):
+        app.install_error_callback(self.error_callback)
 
     @staticmethod
     def to_jsonp(json):
@@ -678,9 +664,8 @@
             return self.to_jsonp(callback(*a, **kw))
         return wrap
 
-    def json_errors(self, res, error):
-        json = super(JsonpPlugin, self).json_errors(res, error)
-        return self.to_jsonp(json)
+    def error_callback(self, response_body, **kw):
+        response_body['body'] = self.to_jsonp(response_body['body'])
 
 
 class App(Bottle):
@@ -688,6 +673,7 @@
         super(App, self).__init__(autojson=False)
         self.bus = dbus.SystemBus()
         self.mapper = obmc.mapper.Mapper(self.bus)
+        self.error_callbacks = []
 
         self.install_hooks()
         self.install_plugins()
@@ -698,13 +684,18 @@
         # install json api plugins
         json_kw = {'indent': 2, 'sort_keys': True}
         self.install(AuthorizationPlugin())
-        self.install(JsonpPlugin(**json_kw))
-        self.install(JSONPlugin(**json_kw))
-        self.install(JsonApiResponsePlugin())
+        self.install(JsonpPlugin(self, **json_kw))
+        self.install(JsonErrorsPlugin(self, **json_kw))
+        self.install(JsonApiResponsePlugin(self))
         self.install(JsonApiRequestPlugin())
         self.install(JsonApiRequestTypePlugin())
 
     def install_hooks(self):
+        self.error_handler_type = type(self.default_error_handler)
+        self.original_error_handler = self.default_error_handler
+        self.default_error_handler = self.error_handler_type(
+            self.custom_error_handler, self, Bottle)
+
         self.real_router_match = self.router.match
         self.router.match = self.custom_router_match
         self.add_hook('before_request', self.strip_extra_slashes)
@@ -731,6 +722,9 @@
         # this has to come last, since it matches everything
         self.instance_handler.install()
 
+    def install_error_callback(self, callback):
+        self.error_callbacks.insert(0, callback)
+
     def custom_router_match(self, environ):
         ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
             needed doesn't work for us since the instance rules match
@@ -745,6 +739,19 @@
 
         return route, args
 
+    def custom_error_handler(self, res, error):
+        ''' Allow plugins to modify error reponses too via this custom
+            error handler. '''
+
+        response_object = {}
+        response_body = {}
+        for x in self.error_callbacks:
+            x(error=error,
+                response_object=response_object,
+                response_body=response_body)
+
+        return response_body.get('body', "")
+
     @staticmethod
     def strip_extra_slashes():
         path = request.environ['PATH_INFO']