Refactor request and action extensions.

The goal of this refactoring is to eventually eliminate
ExtensionMiddleware and LazySerializationMiddleware completely, by
executing extensions directly within the processing done by
Resource.__call__().  This patch implements the infrastructure
required to perform this extension processing.

Implements blueprint extension-refactor.

Change-Id: I23398fc906a9a105de354a8133337ecfc69a3ad3
This commit is contained in:
Kevin L. Mitchell
2012-01-13 14:13:59 -06:00
parent 6c898e6abf
commit 1d4e35be69
8 changed files with 998 additions and 145 deletions

View File

@@ -15,11 +15,13 @@
# License for the specific language governing permissions and limitations
# under the License.
import inspect
from xml.dom import minidom
from xml.parsers import expat
from lxml import etree
import webob
from webob import exc
from nova import exception
from nova import log as logging
@@ -700,6 +702,65 @@ class ResponseObject(object):
return self._headers.copy()
def action_peek_json(body):
"""Determine action to invoke."""
try:
decoded = utils.loads(body)
except ValueError:
msg = _("cannot understand JSON")
raise exception.MalformedRequestBody(reason=msg)
# Make sure there's exactly one key...
if len(decoded) != 1:
msg = _("too many body keys")
raise exception.MalformedRequestBody(reason=msg)
# Return the action and the decoded body...
return decoded.keys()[0]
def action_peek_xml(body):
"""Determine action to invoke."""
dom = minidom.parseString(body)
action_node = dom.childNodes[0]
return action_node.tagName
class ResourceExceptionHandler(object):
"""Context manager to handle Resource exceptions.
Used when processing exceptions generated by API implementation
methods (or their extensions). Converts most exceptions to Fault
exceptions, with the appropriate logging.
"""
def __enter__(self):
return None
def __exit__(self, ex_type, ex_value, ex_traceback):
if not ex_value:
return True
if isinstance(ex_value, exception.NotAuthorized):
msg = unicode(ex_value)
raise Fault(webob.exc.HTTPUnauthorized(explanation=msg))
elif isinstance(ex_value, TypeError):
LOG.exception(ex_value)
raise Fault(webob.exc.HTTPBadRequest())
elif isinstance(ex_value, Fault):
LOG.info(_("Fault thrown: %s"), unicode(ex_value))
raise ex_value
elif isinstance(ex_value, webob.exc.HTTPException):
LOG.info(_("HTTP exception thrown: %s"), unicode(ex_value))
raise Fault(ex_value)
# We didn't handle the exception
return False
class Resource(wsgi.Application):
"""WSGI app that handles (de)serialization and controller dispatch.
@@ -717,15 +778,17 @@ class Resource(wsgi.Application):
"""
def __init__(self, controller, deserializer=None, serializer=None,
**deserializers):
action_peek=None, **deserializers):
"""
:param controller: object that implement methods created by routes lib
:param deserializer: object that can serialize the output of a
controller into a webob response
:param serializer: object that can deserialize a webob request
into necessary pieces
:param action_peek: dictionary of routines for peeking into an action
request body to determine the desired action
"""
self.controller = controller
self.deserializer = deserializer
self.serializer = serializer
@@ -738,6 +801,45 @@ class Resource(wsgi.Application):
self.default_serializers = dict(xml=XMLDictSerializer,
json=JSONDictSerializer)
self.action_peek = dict(xml=action_peek_xml,
json=action_peek_json)
self.action_peek.update(action_peek or {})
# Copy over the actions dictionary
self.wsgi_actions = {}
if controller:
self.register_actions(controller)
# Save a mapping of extensions
self.wsgi_extensions = {}
self.wsgi_action_extensions = {}
def register_actions(self, controller):
"""Registers controller actions with this resource."""
actions = getattr(controller, 'wsgi_actions', {})
for key, method_name in actions.items():
self.wsgi_actions[key] = getattr(controller, method_name)
def register_extensions(self, controller):
"""Registers controller extensions with this resource."""
extensions = getattr(controller, 'wsgi_extensions', [])
for method_name, action_name in extensions:
# Look up the extending method
extension = getattr(controller, method_name)
if action_name:
# Extending an action...
if action_name not in self.wsgi_action_extensions:
self.wsgi_action_extensions[action_name] = []
self.wsgi_action_extensions[action_name].append(extension)
else:
# Extending a regular method
if method_name not in self.wsgi_extensions:
self.wsgi_extensions[method_name] = []
self.wsgi_extensions[method_name].append(extension)
def get_action_args(self, request_environment):
"""Parse dictionary created by routes library."""
@@ -793,6 +895,66 @@ class Resource(wsgi.Application):
return deserializer().deserialize(body)
def pre_process_extensions(self, extensions, request, action_args):
# List of callables for post-processing extensions
post = []
for ext in extensions:
if inspect.isgeneratorfunction(ext):
response = None
# If it's a generator function, the part before the
# yield is the preprocessing stage
try:
with ResourceExceptionHandler():
gen = ext(req=request, **action_args)
response = gen.next()
except Fault as ex:
response = ex
# We had a response...
if response:
return response, []
# No response, queue up generator for post-processing
post.append(gen)
else:
# Regular functions only perform post-processing
post.append(ext)
# Run post-processing in the reverse order
return None, reversed(post)
def post_process_extensions(self, extensions, resp_obj, request,
action_args):
for ext in extensions:
response = None
if inspect.isgenerator(ext):
# If it's a generator, run the second half of
# processing
try:
with ResourceExceptionHandler():
response = ext.send(resp_obj)
except StopIteration:
# Normal exit of generator
continue
except Fault as ex:
response = ex
else:
# Regular functions get post-processing...
try:
with ResourceExceptionHandler():
response = ext(req=request, resp_obj=resp_obj,
**action_args)
except Fault as ex:
response = ex
# We had a response...
if response:
return response
return None
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
"""WSGI method that controls (de)serialization and method dispatch."""
@@ -809,9 +971,16 @@ class Resource(wsgi.Application):
# Get the implementing method
try:
meth = self.get_method(request, action)
meth, extensions = self.get_method(request, action,
content_type, body)
except (AttributeError, TypeError):
return Fault(webob.exc.HTTPNotFound())
except KeyError as ex:
msg = _("There is no such action: %s") % ex.args[0]
return Fault(webob.exc.HTTPBadRequest(explanation=msg))
except exception.MalformedRequestBody:
msg = _("Malformed request body")
return Fault(webob.exc.HTTPBadRequest(explanation=msg))
# Now, deserialize the request body...
try:
@@ -837,21 +1006,16 @@ class Resource(wsgi.Application):
msg = _("Malformed request url")
return Fault(webob.exc.HTTPBadRequest(explanation=msg))
response = None
try:
action_result = self.dispatch(meth, request, action_args)
except exception.NotAuthorized as ex:
msg = unicode(ex)
response = Fault(webob.exc.HTTPUnauthorized(explanation=msg))
except TypeError as ex:
LOG.exception(ex)
response = Fault(webob.exc.HTTPBadRequest())
except Fault as ex:
LOG.info(_("Fault thrown: %s"), unicode(ex))
response = ex
except webob.exc.HTTPException as ex:
LOG.info(_("HTTP exception thrown: %s"), unicode(ex))
response = Fault(ex)
# Run pre-processing extensions
response, post = self.pre_process_extensions(extensions,
request, action_args)
if not response:
try:
with ResourceExceptionHandler():
action_result = self.dispatch(meth, request, action_args)
except Fault as ex:
response = ex
if not response:
# No exceptions; convert action_result into a
@@ -864,7 +1028,12 @@ class Resource(wsgi.Application):
else:
response = action_result
# Run post-processing extensions
if resp_obj:
response = self.post_process_extensions(post, resp_obj,
request, action_args)
if resp_obj and not response:
if self.serializer:
response = self.serializer.serialize(request,
resp_obj.obj,
@@ -890,12 +1059,29 @@ class Resource(wsgi.Application):
return response
def get_method(self, request, action):
"""Look up the action-specific method."""
def get_method(self, request, action, content_type, body):
"""Look up the action-specific method and its extensions."""
if self.controller is None:
return getattr(self, action)
return getattr(self.controller, action)
# Look up the method
try:
if not self.controller:
meth = getattr(self, action)
else:
meth = getattr(self.controller, action)
except AttributeError as ex:
if action != 'action' or not self.wsgi_actions:
# Propagate the error
raise
else:
return meth, self.wsgi_extensions.get(action, [])
# OK, it's an action; figure out which action...
mtype = _MEDIA_TYPE_MAP.get(content_type)
action_name = self.action_peek[mtype](body)
# Look up the action method
return (self.wsgi_actions[action_name],
self.wsgi_action_extensions.get(action_name, []))
def dispatch(self, method, request, action_args):
"""Dispatch a call to the action-specific method."""
@@ -903,14 +1089,91 @@ class Resource(wsgi.Application):
return method(req=request, **action_args)
def action(name):
"""Mark a function as an action.
The given name will be taken as the action key in the body.
"""
def decorator(func):
func.wsgi_action = name
return func
return decorator
def extends(*args, **kwargs):
"""Indicate a function extends an operation.
Can be used as either::
@extends
def index(...):
pass
or as::
@extends(action='resize')
def _action_resize(...):
pass
"""
def decorator(func):
# Store enough information to find what we're extending
func.wsgi_extends = (func.__name__, kwargs.get('action'))
return func
# If we have positional arguments, call the decorator
if args:
return decorator(*args)
# OK, return the decorator instead
return decorator
class ControllerMetaclass(type):
"""Controller metaclass.
This metaclass automates the task of assembling a dictionary
mapping action keys to method names.
"""
def __new__(mcs, name, bases, cls_dict):
"""Adds the wsgi_actions dictionary to the class."""
# Find all actions
actions = {}
extensions = []
for key, value in cls_dict.items():
if not callable(value):
continue
if getattr(value, 'wsgi_action', None):
actions[value.wsgi_action] = key
elif getattr(value, 'wsgi_extends', None):
extensions.append(value.wsgi_extends)
# Add the actions and extensions to the class dict
cls_dict['wsgi_actions'] = actions
cls_dict['wsgi_extensions'] = extensions
return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
cls_dict)
class Controller(object):
"""Default controller."""
__metaclass__ = ControllerMetaclass
_view_builder_class = None
def __init__(self, view_builder=None):
"""Initialize controller with a view builder instance."""
self._view_builder = view_builder or self._view_builder_class()
if view_builder:
self._view_builder = view_builder
elif self._view_builder_class:
self._view_builder = self._view_builder_class()
else:
self._view_builder = None
class Fault(webob.exc.HTTPException):