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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user