add escalator frame
[escalator.git] / api / escalator / common / wsgi.py
diff --git a/api/escalator/common/wsgi.py b/api/escalator/common/wsgi.py
new file mode 100644 (file)
index 0000000..c4e8bfd
--- /dev/null
@@ -0,0 +1,911 @@
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2010 OpenStack Foundation
+# Copyright 2014 IBM Corp.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+Utility methods for working with WSGI servers
+"""
+from __future__ import print_function
+
+import errno
+import functools
+import os
+import signal
+import sys
+import time
+
+import eventlet
+from eventlet.green import socket
+from eventlet.green import ssl
+import eventlet.greenio
+import eventlet.wsgi
+from oslo_serialization import jsonutils
+from oslo_concurrency import processutils
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_log import loggers
+import routes
+import routes.middleware
+import six
+import webob.dec
+import webob.exc
+from webob import multidict
+
+from escalator.common import exception
+from escalator.common import utils
+from escalator import i18n
+
+
+_ = i18n._
+_LE = i18n._LE
+_LI = i18n._LI
+_LW = i18n._LW
+
+bind_opts = [
+    cfg.StrOpt('bind_host', default='0.0.0.0',
+               help=_('Address to bind the server.  Useful when '
+                      'selecting a particular network interface.')),
+    cfg.IntOpt('bind_port',
+               help=_('The port on which the server will listen.')),
+]
+
+socket_opts = [
+    cfg.IntOpt('backlog', default=4096,
+               help=_('The backlog value that will be used when creating the '
+                      'TCP listener socket.')),
+    cfg.IntOpt('tcp_keepidle', default=600,
+               help=_('The value for the socket option TCP_KEEPIDLE.  This is '
+                      'the time in seconds that the connection must be idle '
+                      'before TCP starts sending keepalive probes.')),
+    cfg.StrOpt('ca_file', help=_('CA certificate file to use to verify '
+                                 'connecting clients.')),
+    cfg.StrOpt('cert_file', help=_('Certificate file to use when starting API '
+                                   'server securely.')),
+    cfg.StrOpt('key_file', help=_('Private key file to use when starting API '
+                                  'server securely.')),
+]
+
+eventlet_opts = [
+    cfg.IntOpt('workers', default=processutils.get_worker_count(),
+               help=_('The number of child process workers that will be '
+                      'created to service requests. The default will be '
+                      'equal to the number of CPUs available.')),
+    cfg.IntOpt('max_header_line', default=16384,
+               help=_('Maximum line size of message headers to be accepted. '
+                      'max_header_line may need to be increased when using '
+                      'large tokens (typically those generated by the '
+                      'Keystone v3 API with big service catalogs')),
+    cfg.BoolOpt('http_keepalive', default=True,
+                help=_('If False, server will return the header '
+                       '"Connection: close", '
+                       'If True, server will return "Connection: Keep-Alive" '
+                       'in its responses. In order to close the client socket '
+                       'connection explicitly after the response is sent and '
+                       'read successfully by the client, you simply have to '
+                       'set this option to False when you create a wsgi '
+                       'server.')),
+]
+
+profiler_opts = [
+    cfg.BoolOpt("enabled", default=False,
+                help=_('If False fully disable profiling feature.')),
+    cfg.BoolOpt("trace_sqlalchemy", default=False,
+                help=_("If False doesn't trace SQL requests."))
+]
+
+
+LOG = logging.getLogger(__name__)
+
+CONF = cfg.CONF
+CONF.register_opts(bind_opts)
+CONF.register_opts(socket_opts)
+CONF.register_opts(eventlet_opts)
+CONF.register_opts(profiler_opts, group="profiler")
+
+ASYNC_EVENTLET_THREAD_POOL_LIST = []
+
+
+def get_bind_addr(default_port=None):
+    """Return the host and port to bind to."""
+    return (CONF.bind_host, CONF.bind_port or default_port)
+
+
+def ssl_wrap_socket(sock):
+    """
+    Wrap an existing socket in SSL
+
+    :param sock: non-SSL socket to wrap
+
+    :returns: An SSL wrapped socket
+    """
+    utils.validate_key_cert(CONF.key_file, CONF.cert_file)
+
+    ssl_kwargs = {
+        'server_side': True,
+        'certfile': CONF.cert_file,
+        'keyfile': CONF.key_file,
+        'cert_reqs': ssl.CERT_NONE,
+    }
+
+    if CONF.ca_file:
+        ssl_kwargs['ca_certs'] = CONF.ca_file
+        ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
+
+    return ssl.wrap_socket(sock, **ssl_kwargs)
+
+
+def get_socket(default_port):
+    """
+    Bind socket to bind ip:port in conf
+
+    note: Mostly comes from Swift with a few small changes...
+
+    :param default_port: port to bind to if none is specified in conf
+
+    :returns : a socket object as returned from socket.listen or
+               ssl.wrap_socket if conf specifies cert_file
+    """
+    bind_addr = get_bind_addr(default_port)
+
+    # TODO(jaypipes): eventlet's greened socket module does not actually
+    # support IPv6 in getaddrinfo(). We need to get around this in the
+    # future or monitor upstream for a fix
+    address_family = [
+        addr[0] for addr in socket.getaddrinfo(bind_addr[0],
+                                               bind_addr[1],
+                                               socket.AF_UNSPEC,
+                                               socket.SOCK_STREAM)
+        if addr[0] in (socket.AF_INET, socket.AF_INET6)
+    ][0]
+
+    use_ssl = CONF.key_file or CONF.cert_file
+    if use_ssl and (not CONF.key_file or not CONF.cert_file):
+        raise RuntimeError(_("When running server in SSL mode, you must "
+                             "specify both a cert_file and key_file "
+                             "option value in your configuration file"))
+
+    sock = utils.get_test_suite_socket()
+    retry_until = time.time() + 30
+
+    while not sock and time.time() < retry_until:
+        try:
+            sock = eventlet.listen(bind_addr,
+                                   backlog=CONF.backlog,
+                                   family=address_family)
+        except socket.error as err:
+            if err.args[0] != errno.EADDRINUSE:
+                raise
+            eventlet.sleep(0.1)
+    if not sock:
+        raise RuntimeError(_("Could not bind to %(host)s:%(port)s after"
+                             " trying for 30 seconds") %
+                           {'host': bind_addr[0],
+                            'port': bind_addr[1]})
+
+    return sock
+
+
+def set_eventlet_hub():
+    try:
+        eventlet.hubs.use_hub('poll')
+    except Exception:
+        try:
+            eventlet.hubs.use_hub('selects')
+        except Exception:
+            msg = _("eventlet 'poll' nor 'selects' hubs are available "
+                    "on this platform")
+            raise exception.WorkerCreationFailure(
+                reason=msg)
+
+
+def get_asynchronous_eventlet_pool(size=1000):
+    """Return eventlet pool to caller.
+
+    Also store pools created in global list, to wait on
+    it after getting signal for graceful shutdown.
+
+    :param size: eventlet pool size
+    :returns: eventlet pool
+    """
+    global ASYNC_EVENTLET_THREAD_POOL_LIST
+
+    pool = eventlet.GreenPool(size=size)
+    # Add pool to global ASYNC_EVENTLET_THREAD_POOL_LIST
+    ASYNC_EVENTLET_THREAD_POOL_LIST.append(pool)
+
+    return pool
+
+
+class Server(object):
+    """Server class to manage multiple WSGI sockets and applications.
+
+    This class requires initialize_escalator_store set to True if
+    escalator store needs to be initialized.
+    """
+
+    def __init__(self, threads=1000, initialize_escalator_store=False):
+        os.umask(0o27)  # ensure files are created with the correct privileges
+        self._logger = logging.getLogger("eventlet.wsgi.server")
+        self._wsgi_logger = loggers.WritableLogger(self._logger)
+        self.threads = threads
+        self.children = set()
+        self.stale_children = set()
+        self.running = True
+        # NOTE(abhishek): Allows us to only re-initialize escalator_store when
+        # the API's configuration reloads.
+        self.initialize_escalator_store = initialize_escalator_store
+        self.pgid = os.getpid()
+        try:
+            # NOTE(flaper87): Make sure this process
+            # runs in its own process group.
+            os.setpgid(self.pgid, self.pgid)
+        except OSError:
+            # NOTE(flaper87): When running escalator-control,
+            # (escalator's functional tests, for example)
+            # setpgid fails with EPERM as escalator-control
+            # creates a fresh session, of which the newly
+            # launched service becomes the leader (session
+            # leaders may not change process groups)
+            #
+            # Running escalator-(api) is safe and
+            # shouldn't raise any error here.
+            self.pgid = 0
+
+    def hup(self, *args):
+        """
+        Reloads configuration files with zero down time
+        """
+        signal.signal(signal.SIGHUP, signal.SIG_IGN)
+        raise exception.SIGHUPInterrupt
+
+    def kill_children(self, *args):
+        """Kills the entire process group."""
+        signal.signal(signal.SIGTERM, signal.SIG_IGN)
+        signal.signal(signal.SIGINT, signal.SIG_IGN)
+        self.running = False
+        os.killpg(self.pgid, signal.SIGTERM)
+
+    def start(self, application, default_port):
+        """
+        Run a WSGI server with the given application.
+
+        :param application: The application to be run in the WSGI server
+        :param default_port: Port to bind to if none is specified in conf
+        """
+        self.application = application
+        self.default_port = default_port
+        self.configure()
+        self.start_wsgi()
+
+    def start_wsgi(self):
+
+        if CONF.workers == 0:
+            # Useful for profiling, test, debug etc.
+            self.pool = self.create_pool()
+            self.pool.spawn_n(self._single_run, self.application, self.sock)
+            return
+        else:
+            LOG.info(_LI("Starting %d workers") % CONF.workers)
+            signal.signal(signal.SIGTERM, self.kill_children)
+            signal.signal(signal.SIGINT, self.kill_children)
+            signal.signal(signal.SIGHUP, self.hup)
+            while len(self.children) < CONF.workers:
+                self.run_child()
+
+    def create_pool(self):
+        return eventlet.GreenPool(size=self.threads)
+
+    def _remove_children(self, pid):
+        if pid in self.children:
+            self.children.remove(pid)
+            LOG.info(_LI('Removed dead child %s') % pid)
+        elif pid in self.stale_children:
+            self.stale_children.remove(pid)
+            LOG.info(_LI('Removed stale child %s') % pid)
+        else:
+            LOG.warn(_LW('Unrecognised child %s') % pid)
+
+    def _verify_and_respawn_children(self, pid, status):
+        if len(self.stale_children) == 0:
+            LOG.debug('No stale children')
+        if os.WIFEXITED(status) and os.WEXITSTATUS(status) != 0:
+            LOG.error(_LE('Not respawning child %d, cannot '
+                          'recover from termination') % pid)
+            if not self.children and not self.stale_children:
+                LOG.info(
+                    _LI('All workers have terminated. Exiting'))
+                self.running = False
+        else:
+            if len(self.children) < CONF.workers:
+                self.run_child()
+
+    def wait_on_children(self):
+        while self.running:
+            try:
+                pid, status = os.wait()
+                if os.WIFEXITED(status) or os.WIFSIGNALED(status):
+                    self._remove_children(pid)
+                    self._verify_and_respawn_children(pid, status)
+            except OSError as err:
+                if err.errno not in (errno.EINTR, errno.ECHILD):
+                    raise
+            except KeyboardInterrupt:
+                LOG.info(_LI('Caught keyboard interrupt. Exiting.'))
+                break
+            except exception.SIGHUPInterrupt:
+                self.reload()
+                continue
+        eventlet.greenio.shutdown_safe(self.sock)
+        self.sock.close()
+        LOG.debug('Exited')
+
+    def configure(self, old_conf=None, has_changed=None):
+        """
+        Apply configuration settings
+
+        :param old_conf: Cached old configuration settings (if any)
+        :param has changed: callable to determine if a parameter has changed
+        """
+        eventlet.wsgi.MAX_HEADER_LINE = CONF.max_header_line
+        self.configure_socket(old_conf, has_changed)
+        if self.initialize_escalator_store:
+            initialize_escalator_store()
+
+    def reload(self):
+        """
+        Reload and re-apply configuration settings
+
+        Existing child processes are sent a SIGHUP signal
+        and will exit after completing existing requests.
+        New child processes, which will have the updated
+        configuration, are spawned. This allows preventing
+        interruption to the service.
+        """
+        def _has_changed(old, new, param):
+            old = old.get(param)
+            new = getattr(new, param)
+            return (new != old)
+
+        old_conf = utils.stash_conf_values()
+        has_changed = functools.partial(_has_changed, old_conf, CONF)
+        CONF.reload_config_files()
+        os.killpg(self.pgid, signal.SIGHUP)
+        self.stale_children = self.children
+        self.children = set()
+
+        # Ensure any logging config changes are picked up
+        logging.setup(CONF, 'escalator')
+
+        self.configure(old_conf, has_changed)
+        self.start_wsgi()
+
+    def wait(self):
+        """Wait until all servers have completed running."""
+        try:
+            if self.children:
+                self.wait_on_children()
+            else:
+                self.pool.waitall()
+        except KeyboardInterrupt:
+            pass
+
+    def run_child(self):
+        def child_hup(*args):
+            """Shuts down child processes, existing requests are handled."""
+            signal.signal(signal.SIGHUP, signal.SIG_IGN)
+            eventlet.wsgi.is_accepting = False
+            self.sock.close()
+
+        pid = os.fork()
+        if pid == 0:
+            signal.signal(signal.SIGHUP, child_hup)
+            signal.signal(signal.SIGTERM, signal.SIG_DFL)
+            # ignore the interrupt signal to avoid a race whereby
+            # a child worker receives the signal before the parent
+            # and is respawned unnecessarily as a result
+            signal.signal(signal.SIGINT, signal.SIG_IGN)
+            # The child has no need to stash the unwrapped
+            # socket, and the reference prevents a clean
+            # exit on sighup
+            self._sock = None
+            self.run_server()
+            LOG.info(_LI('Child %d exiting normally') % os.getpid())
+            # self.pool.waitall() is now called in wsgi's server so
+            # it's safe to exit here
+            sys.exit(0)
+        else:
+            LOG.info(_LI('Started child %s') % pid)
+            self.children.add(pid)
+
+    def run_server(self):
+        """Run a WSGI server."""
+        if cfg.CONF.pydev_worker_debug_host:
+            utils.setup_remote_pydev_debug(cfg.CONF.pydev_worker_debug_host,
+                                           cfg.CONF.pydev_worker_debug_port)
+
+        eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
+        self.pool = self.create_pool()
+        try:
+            eventlet.wsgi.server(self.sock,
+                                 self.application,
+                                 log=self._wsgi_logger,
+                                 custom_pool=self.pool,
+                                 debug=False,
+                                 keepalive=CONF.http_keepalive)
+        except socket.error as err:
+            if err[0] != errno.EINVAL:
+                raise
+
+        # waiting on async pools
+        if ASYNC_EVENTLET_THREAD_POOL_LIST:
+            for pool in ASYNC_EVENTLET_THREAD_POOL_LIST:
+                pool.waitall()
+
+    def _single_run(self, application, sock):
+        """Start a WSGI server in a new green thread."""
+        LOG.info(_LI("Starting single process server"))
+        eventlet.wsgi.server(sock, application, custom_pool=self.pool,
+                             log=self._wsgi_logger,
+                             debug=False,
+                             keepalive=CONF.http_keepalive)
+
+    def configure_socket(self, old_conf=None, has_changed=None):
+        """
+        Ensure a socket exists and is appropriately configured.
+
+        This function is called on start up, and can also be
+        called in the event of a configuration reload.
+
+        When called for the first time a new socket is created.
+        If reloading and either bind_host or bind port have been
+        changed the existing socket must be closed and a new
+        socket opened (laws of physics).
+
+        In all other cases (bind_host/bind_port have not changed)
+        the existing socket is reused.
+
+        :param old_conf: Cached old configuration settings (if any)
+        :param has changed: callable to determine if a parameter has changed
+        """
+        # Do we need a fresh socket?
+        new_sock = (old_conf is None or (
+                    has_changed('bind_host') or
+                    has_changed('bind_port')))
+        # Will we be using https?
+        use_ssl = not (not CONF.cert_file or not CONF.key_file)
+        # Were we using https before?
+        old_use_ssl = (old_conf is not None and not (
+                       not old_conf.get('key_file') or
+                       not old_conf.get('cert_file')))
+        # Do we now need to perform an SSL wrap on the socket?
+        wrap_sock = use_ssl is True and (old_use_ssl is False or new_sock)
+        # Do we now need to perform an SSL unwrap on the socket?
+        unwrap_sock = use_ssl is False and old_use_ssl is True
+
+        if new_sock:
+            self._sock = None
+            if old_conf is not None:
+                self.sock.close()
+            _sock = get_socket(self.default_port)
+            _sock.setsockopt(socket.SOL_SOCKET,
+                             socket.SO_REUSEADDR, 1)
+            # sockets can hang around forever without keepalive
+            _sock.setsockopt(socket.SOL_SOCKET,
+                             socket.SO_KEEPALIVE, 1)
+            self._sock = _sock
+
+        if wrap_sock:
+            self.sock = ssl_wrap_socket(self._sock)
+
+        if unwrap_sock:
+            self.sock = self._sock
+
+        if new_sock and not use_ssl:
+            self.sock = self._sock
+
+        # Pick up newly deployed certs
+        if old_conf is not None and use_ssl is True and old_use_ssl is True:
+            if has_changed('cert_file') or has_changed('key_file'):
+                utils.validate_key_cert(CONF.key_file, CONF.cert_file)
+            if has_changed('cert_file'):
+                self.sock.certfile = CONF.cert_file
+            if has_changed('key_file'):
+                self.sock.keyfile = CONF.key_file
+
+        if new_sock or (old_conf is not None and has_changed('tcp_keepidle')):
+            # This option isn't available in the OS X version of eventlet
+            if hasattr(socket, 'TCP_KEEPIDLE'):
+                self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE,
+                                     CONF.tcp_keepidle)
+
+        if old_conf is not None and has_changed('backlog'):
+            self.sock.listen(CONF.backlog)
+
+
+class Middleware(object):
+    """
+    Base WSGI middleware wrapper. These classes require an application to be
+    initialized that will be called next.  By default the middleware will
+    simply call its wrapped app, or you can override __call__ to customize its
+    behavior.
+    """
+
+    def __init__(self, application):
+        self.application = application
+
+    @classmethod
+    def factory(cls, global_conf, **local_conf):
+        def filter(app):
+            return cls(app)
+        return filter
+
+    def process_request(self, req):
+        """
+        Called on each request.
+
+        If this returns None, the next application down the stack will be
+        executed. If it returns a response then that response will be returned
+        and execution will stop here.
+
+        """
+        return None
+
+    def process_response(self, response):
+        """Do whatever you'd like to the response."""
+        return response
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        response = self.process_request(req)
+        if response:
+            return response
+        response = req.get_response(self.application)
+        response.request = req
+        try:
+            return self.process_response(response)
+        except webob.exc.HTTPException as e:
+            return e
+
+
+class Debug(Middleware):
+    """
+    Helper class that can be inserted into any WSGI application chain
+    to get information about the request and response.
+    """
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        print(("*" * 40) + " REQUEST ENVIRON")
+        for key, value in req.environ.items():
+            print(key, "=", value)
+        print('')
+        resp = req.get_response(self.application)
+
+        print(("*" * 40) + " RESPONSE HEADERS")
+        for (key, value) in six.iteritems(resp.headers):
+            print(key, "=", value)
+        print('')
+
+        resp.app_iter = self.print_generator(resp.app_iter)
+
+        return resp
+
+    @staticmethod
+    def print_generator(app_iter):
+        """
+        Iterator that prints the contents of a wrapper string iterator
+        when iterated.
+        """
+        print(("*" * 40) + " BODY")
+        for part in app_iter:
+            sys.stdout.write(part)
+            sys.stdout.flush()
+            yield part
+        print()
+
+
+class APIMapper(routes.Mapper):
+    """
+    Handle route matching when url is '' because routes.Mapper returns
+    an error in this case.
+    """
+
+    def routematch(self, url=None, environ=None):
+        if url is "":
+            result = self._match("", environ)
+            return result[0], result[1]
+        return routes.Mapper.routematch(self, url, environ)
+
+
+class RejectMethodController(object):
+
+    def reject(self, req, allowed_methods, *args, **kwargs):
+        LOG.debug("The method %s is not allowed for this resource" %
+                  req.environ['REQUEST_METHOD'])
+        raise webob.exc.HTTPMethodNotAllowed(
+            headers=[('Allow', allowed_methods)])
+
+
+class Router(object):
+    """
+    WSGI middleware that maps incoming requests to WSGI apps.
+    """
+
+    def __init__(self, mapper):
+        """
+        Create a router for the given routes.Mapper.
+
+        Each route in `mapper` must specify a 'controller', which is a
+        WSGI app to call.  You'll probably want to specify an 'action' as
+        well and have your controller be a wsgi.Controller, who will route
+        the request to the action method.
+
+        Examples:
+          mapper = routes.Mapper()
+          sc = ServerController()
+
+          # Explicit mapping of one route to a controller+action
+          mapper.connect(None, "/svrlist", controller=sc, action="list")
+
+          # Actions are all implicitly defined
+          mapper.resource("server", "servers", controller=sc)
+
+          # Pointing to an arbitrary WSGI app.  You can specify the
+          # {path_info:.*} parameter so the target app can be handed just that
+          # section of the URL.
+          mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
+        """
+        mapper.redirect("", "/")
+        self.map = mapper
+        self._router = routes.middleware.RoutesMiddleware(self._dispatch,
+                                                          self.map)
+
+    @classmethod
+    def factory(cls, global_conf, **local_conf):
+        return cls(APIMapper())
+
+    @webob.dec.wsgify
+    def __call__(self, req):
+        """
+        Route the incoming request to a controller based on self.map.
+        If no match, return either a 404(Not Found) or 501(Not Implemented).
+        """
+        return self._router
+
+    @staticmethod
+    @webob.dec.wsgify
+    def _dispatch(req):
+        """
+        Called by self._router after matching the incoming request to a route
+        and putting the information into req.environ.  Either returns 404,
+        501, or the routed WSGI app's response.
+        """
+        match = req.environ['wsgiorg.routing_args'][1]
+        if not match:
+            implemented_http_methods = ['GET', 'HEAD', 'POST', 'PUT',
+                                        'DELETE', 'PATCH']
+            if req.environ['REQUEST_METHOD'] not in implemented_http_methods:
+                return webob.exc.HTTPNotImplemented()
+            else:
+                return webob.exc.HTTPNotFound()
+        app = match['controller']
+        return app
+
+
+class Request(webob.Request):
+    """Add some OpenStack API-specific logic to the base webob.Request."""
+
+    def best_match_content_type(self):
+        """Determine the requested response content-type."""
+        supported = ('application/json',)
+        bm = self.accept.best_match(supported)
+        return bm or 'application/json'
+
+    def get_content_type(self, allowed_content_types):
+        """Determine content type of the request body."""
+        if "Content-Type" not in self.headers:
+            raise exception.InvalidContentType(content_type=None)
+
+        content_type = self.content_type
+
+        if content_type not in allowed_content_types:
+            raise exception.InvalidContentType(content_type=content_type)
+        else:
+            return content_type
+
+    def best_match_language(self):
+        """Determines best available locale from the Accept-Language header.
+
+        :returns: the best language match or None if the 'Accept-Language'
+                  header was not available in the request.
+        """
+        if not self.accept_language:
+            return None
+        langs = i18n.get_available_languages('escalator')
+        return self.accept_language.best_match(langs)
+
+    def get_content_range(self):
+        """Return the `Range` in a request."""
+        range_str = self.headers.get('Content-Range')
+        if range_str is not None:
+            range_ = webob.byterange.ContentRange.parse(range_str)
+            if range_ is None:
+                msg = _('Malformed Content-Range header: %s') % range_str
+                raise webob.exc.HTTPBadRequest(explanation=msg)
+            return range_
+
+
+class JSONRequestDeserializer(object):
+    valid_transfer_encoding = frozenset(['chunked', 'compress', 'deflate',
+                                         'gzip', 'identity'])
+
+    def has_body(self, request):
+        """
+        Returns whether a Webob.Request object will possess an entity body.
+
+        :param request:  Webob.Request object
+        """
+        request_encoding = request.headers.get('transfer-encoding', '').lower()
+        is_valid_encoding = request_encoding in self.valid_transfer_encoding
+        if is_valid_encoding and request.is_body_readable:
+            return True
+        elif request.content_length > 0:
+            return True
+
+        return False
+
+    @staticmethod
+    def _sanitizer(obj):
+        """Sanitizer method that will be passed to jsonutils.loads."""
+        return obj
+
+    def from_json(self, datastring):
+        try:
+            return jsonutils.loads(datastring, object_hook=self._sanitizer)
+        except ValueError:
+            msg = _('Malformed JSON in request body.')
+            raise webob.exc.HTTPBadRequest(explanation=msg)
+
+    def default(self, request):
+        if self.has_body(request):
+            return {'body': self.from_json(request.body)}
+        else:
+            return {}
+
+
+class JSONResponseSerializer(object):
+
+    def _sanitizer(self, obj):
+        """Sanitizer method that will be passed to jsonutils.dumps."""
+        if hasattr(obj, "to_dict"):
+            return obj.to_dict()
+        if isinstance(obj, multidict.MultiDict):
+            return obj.mixed()
+        return jsonutils.to_primitive(obj)
+
+    def to_json(self, data):
+        return jsonutils.dumps(data, default=self._sanitizer)
+
+    def default(self, response, result):
+        response.content_type = 'application/json'
+        response.body = self.to_json(result)
+
+
+def translate_exception(req, e):
+    """Translates all translatable elements of the given exception."""
+
+    # The RequestClass attribute in the webob.dec.wsgify decorator
+    # does not guarantee that the request object will be a particular
+    # type; this check is therefore necessary.
+    if not hasattr(req, "best_match_language"):
+        return e
+
+    locale = req.best_match_language()
+
+    if isinstance(e, webob.exc.HTTPError):
+        e.explanation = i18n.translate(e.explanation, locale)
+        e.detail = i18n.translate(e.detail, locale)
+        if getattr(e, 'body_template', None):
+            e.body_template = i18n.translate(e.body_template, locale)
+    return e
+
+
+class Resource(object):
+    """
+    WSGI app that handles (de)serialization and controller dispatch.
+
+    Reads routing information supplied by RoutesMiddleware and calls
+    the requested action method upon its deserializer, controller,
+    and serializer. Those three objects may implement any of the basic
+    controller action methods (create, update, show, index, delete)
+    along with any that may be specified in the api router. A 'default'
+    method may also be implemented to be used in place of any
+    non-implemented actions. Deserializer methods must accept a request
+    argument and return a dictionary. Controller methods must accept a
+    request argument. Additionally, they must also accept keyword
+    arguments that represent the keys returned by the Deserializer. They
+    may raise a webob.exc exception or return a dict, which will be
+    serialized by requested content type.
+    """
+
+    def __init__(self, controller, deserializer=None, serializer=None):
+        """
+        :param controller: object that implement methods created by routes lib
+        :param deserializer: object that supports webob request deserialization
+                             through controller-like actions
+        :param serializer: object that supports webob response serialization
+                           through controller-like actions
+        """
+        self.controller = controller
+        self.serializer = serializer or JSONResponseSerializer()
+        self.deserializer = deserializer or JSONRequestDeserializer()
+
+    @webob.dec.wsgify(RequestClass=Request)
+    def __call__(self, request):
+        """WSGI method that controls (de)serialization and method dispatch."""
+        action_args = self.get_action_args(request.environ)
+        action = action_args.pop('action', None)
+
+        try:
+            deserialized_request = self.dispatch(self.deserializer,
+                                                 action, request)
+            action_args.update(deserialized_request)
+            action_result = self.dispatch(self.controller, action,
+                                          request, **action_args)
+        except webob.exc.WSGIHTTPException as e:
+            exc_info = sys.exc_info()
+            raise translate_exception(request, e), None, exc_info[2]
+
+        try:
+            response = webob.Response(request=request)
+            self.dispatch(self.serializer, action, response, action_result)
+            return response
+        except webob.exc.WSGIHTTPException as e:
+            return translate_exception(request, e)
+        except webob.exc.HTTPException as e:
+            return e
+        # return unserializable result (typically a webob exc)
+        except Exception:
+            return action_result
+
+    def dispatch(self, obj, action, *args, **kwargs):
+        """Find action-specific method on self and call it."""
+        try:
+            method = getattr(obj, action)
+        except AttributeError:
+            method = getattr(obj, 'default')
+
+        return method(*args, **kwargs)
+
+    def get_action_args(self, request_environment):
+        """Parse dictionary created by routes library."""
+        try:
+            args = request_environment['wsgiorg.routing_args'][1].copy()
+        except Exception:
+            return {}
+
+        try:
+            del args['controller']
+        except KeyError:
+            pass
+
+        try:
+            del args['format']
+        except KeyError:
+            pass
+
+        return args