There are some flake8 errors. Clear them before other work.
[escalator.git] / api / escalator / common / wsgi.py
1 # Copyright 2010 United States Government as represented by the
2 # Administrator of the National Aeronautics and Space Administration.
3 # Copyright 2010 OpenStack Foundation
4 # Copyright 2014 IBM Corp.
5 # All Rights Reserved.
6 #
7 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
8 #    not use this file except in compliance with the License. You may obtain
9 #    a copy of the License at
10 #
11 #         http://www.apache.org/licenses/LICENSE-2.0
12 #
13 #    Unless required by applicable law or agreed to in writing, software
14 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 #    License for the specific language governing permissions and limitations
17 #    under the License.
18
19 """
20 Utility methods for working with WSGI servers
21 """
22 from __future__ import print_function
23
24 import errno
25 import functools
26 import os
27 import signal
28 import sys
29 import time
30
31 import eventlet
32 from eventlet.green import socket
33 from eventlet.green import ssl
34 import eventlet.greenio
35 import eventlet.wsgi
36 from oslo_serialization import jsonutils
37 from oslo_concurrency import processutils
38 from oslo_config import cfg
39 from oslo_log import log as logging
40 from oslo_log import loggers
41 import routes
42 import routes.middleware
43 import six
44 import webob.dec
45 import webob.exc
46 from webob import multidict
47
48 from escalator.common import exception
49 from escalator.common import utils
50 from escalator import i18n
51
52
53 _ = i18n._
54 _LE = i18n._LE
55 _LI = i18n._LI
56 _LW = i18n._LW
57
58 bind_opts = [
59     cfg.StrOpt('bind_host', default='0.0.0.0',
60                help=_('Address to bind the server.  Useful when '
61                       'selecting a particular network interface.')),
62     cfg.IntOpt('bind_port',
63                help=_('The port on which the server will listen.')),
64 ]
65
66 socket_opts = [
67     cfg.IntOpt('backlog', default=4096,
68                help=_('The backlog value that will be used when creating the '
69                       'TCP listener socket.')),
70     cfg.IntOpt('tcp_keepidle', default=600,
71                help=_('The value for the socket option TCP_KEEPIDLE.  This is '
72                       'the time in seconds that the connection must be idle '
73                       'before TCP starts sending keepalive probes.')),
74     cfg.StrOpt('ca_file', help=_('CA certificate file to use to verify '
75                                  'connecting clients.')),
76     cfg.StrOpt('cert_file', help=_('Certificate file to use when starting API '
77                                    'server securely.')),
78     cfg.StrOpt('key_file', help=_('Private key file to use when starting API '
79                                   'server securely.')),
80 ]
81
82 eventlet_opts = [
83     cfg.IntOpt('workers', default=processutils.get_worker_count(),
84                help=_('The number of child process workers that will be '
85                       'created to service requests. The default will be '
86                       'equal to the number of CPUs available.')),
87     cfg.IntOpt('max_header_line', default=16384,
88                help=_('Maximum line size of message headers to be accepted. '
89                       'max_header_line may need to be increased when using '
90                       'large tokens (typically those generated by the '
91                       'Keystone v3 API with big service catalogs')),
92     cfg.BoolOpt('http_keepalive', default=True,
93                 help=_('If False, server will return the header '
94                        '"Connection: close", '
95                        'If True, server will return "Connection: Keep-Alive" '
96                        'in its responses. In order to close the client socket '
97                        'connection explicitly after the response is sent and '
98                        'read successfully by the client, you simply have to '
99                        'set this option to False when you create a wsgi '
100                        'server.')),
101 ]
102
103 profiler_opts = [
104     cfg.BoolOpt("enabled", default=False,
105                 help=_('If False fully disable profiling feature.')),
106     cfg.BoolOpt("trace_sqlalchemy", default=False,
107                 help=_("If False doesn't trace SQL requests."))
108 ]
109
110
111 LOG = logging.getLogger(__name__)
112
113 CONF = cfg.CONF
114 CONF.register_opts(bind_opts)
115 CONF.register_opts(socket_opts)
116 CONF.register_opts(eventlet_opts)
117 CONF.register_opts(profiler_opts, group="profiler")
118
119 ASYNC_EVENTLET_THREAD_POOL_LIST = []
120
121
122 def get_bind_addr(default_port=None):
123     """Return the host and port to bind to."""
124     return (CONF.bind_host, CONF.bind_port or default_port)
125
126
127 def ssl_wrap_socket(sock):
128     """
129     Wrap an existing socket in SSL
130
131     :param sock: non-SSL socket to wrap
132
133     :returns: An SSL wrapped socket
134     """
135     utils.validate_key_cert(CONF.key_file, CONF.cert_file)
136
137     ssl_kwargs = {
138         'server_side': True,
139         'certfile': CONF.cert_file,
140         'keyfile': CONF.key_file,
141         'cert_reqs': ssl.CERT_NONE,
142     }
143
144     if CONF.ca_file:
145         ssl_kwargs['ca_certs'] = CONF.ca_file
146         ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
147
148     return ssl.wrap_socket(sock, **ssl_kwargs)
149
150
151 def get_socket(default_port):
152     """
153     Bind socket to bind ip:port in conf
154
155     note: Mostly comes from Swift with a few small changes...
156
157     :param default_port: port to bind to if none is specified in conf
158
159     :returns : a socket object as returned from socket.listen or
160                ssl.wrap_socket if conf specifies cert_file
161     """
162     bind_addr = get_bind_addr(default_port)
163
164     # TODO(jaypipes): eventlet's greened socket module does not actually
165     # support IPv6 in getaddrinfo(). We need to get around this in the
166     # future or monitor upstream for a fix
167     address_family = [
168         addr[0] for addr in socket.getaddrinfo(bind_addr[0],
169                                                bind_addr[1],
170                                                socket.AF_UNSPEC,
171                                                socket.SOCK_STREAM)
172         if addr[0] in (socket.AF_INET, socket.AF_INET6)
173     ][0]
174
175     use_ssl = CONF.key_file or CONF.cert_file
176     if use_ssl and (not CONF.key_file or not CONF.cert_file):
177         raise RuntimeError(_("When running server in SSL mode, you must "
178                              "specify both a cert_file and key_file "
179                              "option value in your configuration file"))
180
181     sock = utils.get_test_suite_socket()
182     retry_until = time.time() + 30
183
184     while not sock and time.time() < retry_until:
185         try:
186             sock = eventlet.listen(bind_addr,
187                                    backlog=CONF.backlog,
188                                    family=address_family)
189         except socket.error as err:
190             if err.args[0] != errno.EADDRINUSE:
191                 raise
192             eventlet.sleep(0.1)
193     if not sock:
194         raise RuntimeError(_("Could not bind to %(host)s:%(port)s after"
195                              " trying for 30 seconds") %
196                            {'host': bind_addr[0],
197                             'port': bind_addr[1]})
198
199     return sock
200
201
202 def set_eventlet_hub():
203     try:
204         eventlet.hubs.use_hub('poll')
205     except Exception:
206         try:
207             eventlet.hubs.use_hub('selects')
208         except Exception:
209             msg = _("eventlet 'poll' nor 'selects' hubs are available "
210                     "on this platform")
211             raise exception.WorkerCreationFailure(
212                 reason=msg)
213
214
215 def get_asynchronous_eventlet_pool(size=1000):
216     """Return eventlet pool to caller.
217
218     Also store pools created in global list, to wait on
219     it after getting signal for graceful shutdown.
220
221     :param size: eventlet pool size
222     :returns: eventlet pool
223     """
224     global ASYNC_EVENTLET_THREAD_POOL_LIST
225
226     pool = eventlet.GreenPool(size=size)
227     # Add pool to global ASYNC_EVENTLET_THREAD_POOL_LIST
228     ASYNC_EVENTLET_THREAD_POOL_LIST.append(pool)
229
230     return pool
231
232
233 class Server(object):
234     """Server class to manage multiple WSGI sockets and applications.
235     """
236
237     def __init__(self, threads=1000):
238         os.umask(0o27)  # ensure files are created with the correct privileges
239         self._logger = logging.getLogger("eventlet.wsgi.server")
240         self._wsgi_logger = loggers.WritableLogger(self._logger)
241         self.threads = threads
242         self.children = set()
243         self.stale_children = set()
244         self.running = True
245         self.pgid = os.getpid()
246         try:
247             # NOTE(flaper87): Make sure this process
248             # runs in its own process group.
249             os.setpgid(self.pgid, self.pgid)
250         except OSError:
251             # NOTE(flaper87): When running escalator-control,
252             # (escalator's functional tests, for example)
253             # setpgid fails with EPERM as escalator-control
254             # creates a fresh session, of which the newly
255             # launched service becomes the leader (session
256             # leaders may not change process groups)
257             #
258             # Running escalator-(api) is safe and
259             # shouldn't raise any error here.
260             self.pgid = 0
261
262     def hup(self, *args):
263         """
264         Reloads configuration files with zero down time
265         """
266         signal.signal(signal.SIGHUP, signal.SIG_IGN)
267         raise exception.SIGHUPInterrupt
268
269     def kill_children(self, *args):
270         """Kills the entire process group."""
271         signal.signal(signal.SIGTERM, signal.SIG_IGN)
272         signal.signal(signal.SIGINT, signal.SIG_IGN)
273         self.running = False
274         os.killpg(self.pgid, signal.SIGTERM)
275
276     def start(self, application, default_port):
277         """
278         Run a WSGI server with the given application.
279
280         :param application: The application to be run in the WSGI server
281         :param default_port: Port to bind to if none is specified in conf
282         """
283         self.application = application
284         self.default_port = default_port
285         self.configure()
286         self.start_wsgi()
287
288     def start_wsgi(self):
289
290         if CONF.workers == 0:
291             # Useful for profiling, test, debug etc.
292             self.pool = self.create_pool()
293             self.pool.spawn_n(self._single_run, self.application, self.sock)
294             return
295         else:
296             LOG.info(_LI("Starting %d workers") % CONF.workers)
297             signal.signal(signal.SIGTERM, self.kill_children)
298             signal.signal(signal.SIGINT, self.kill_children)
299             signal.signal(signal.SIGHUP, self.hup)
300             while len(self.children) < CONF.workers:
301                 self.run_child()
302
303     def create_pool(self):
304         return eventlet.GreenPool(size=self.threads)
305
306     def _remove_children(self, pid):
307         if pid in self.children:
308             self.children.remove(pid)
309             LOG.info(_LI('Removed dead child %s') % pid)
310         elif pid in self.stale_children:
311             self.stale_children.remove(pid)
312             LOG.info(_LI('Removed stale child %s') % pid)
313         else:
314             LOG.warn(_LW('Unrecognised child %s') % pid)
315
316     def _verify_and_respawn_children(self, pid, status):
317         if len(self.stale_children) == 0:
318             LOG.debug('No stale children')
319         if os.WIFEXITED(status) and os.WEXITSTATUS(status) != 0:
320             LOG.error(_LE('Not respawning child %d, cannot '
321                           'recover from termination') % pid)
322             if not self.children and not self.stale_children:
323                 LOG.info(
324                     _LI('All workers have terminated. Exiting'))
325                 self.running = False
326         else:
327             if len(self.children) < CONF.workers:
328                 self.run_child()
329
330     def wait_on_children(self):
331         while self.running:
332             try:
333                 pid, status = os.wait()
334                 if os.WIFEXITED(status) or os.WIFSIGNALED(status):
335                     self._remove_children(pid)
336                     self._verify_and_respawn_children(pid, status)
337             except OSError as err:
338                 if err.errno not in (errno.EINTR, errno.ECHILD):
339                     raise
340             except KeyboardInterrupt:
341                 LOG.info(_LI('Caught keyboard interrupt. Exiting.'))
342                 break
343             except exception.SIGHUPInterrupt:
344                 self.reload()
345                 continue
346         eventlet.greenio.shutdown_safe(self.sock)
347         self.sock.close()
348         LOG.debug('Exited')
349
350     def configure(self, old_conf=None, has_changed=None):
351         """
352         Apply configuration settings
353
354         :param old_conf: Cached old configuration settings (if any)
355         :param has changed: callable to determine if a parameter has changed
356         """
357         eventlet.wsgi.MAX_HEADER_LINE = CONF.max_header_line
358         self.configure_socket(old_conf, has_changed)
359
360     def reload(self):
361         """
362         Reload and re-apply configuration settings
363
364         Existing child processes are sent a SIGHUP signal
365         and will exit after completing existing requests.
366         New child processes, which will have the updated
367         configuration, are spawned. This allows preventing
368         interruption to the service.
369         """
370         def _has_changed(old, new, param):
371             old = old.get(param)
372             new = getattr(new, param)
373             return (new != old)
374
375         old_conf = utils.stash_conf_values()
376         has_changed = functools.partial(_has_changed, old_conf, CONF)
377         CONF.reload_config_files()
378         os.killpg(self.pgid, signal.SIGHUP)
379         self.stale_children = self.children
380         self.children = set()
381
382         # Ensure any logging config changes are picked up
383         logging.setup(CONF, 'escalator')
384
385         self.configure(old_conf, has_changed)
386         self.start_wsgi()
387
388     def wait(self):
389         """Wait until all servers have completed running."""
390         try:
391             if self.children:
392                 self.wait_on_children()
393             else:
394                 self.pool.waitall()
395         except KeyboardInterrupt:
396             pass
397
398     def run_child(self):
399         def child_hup(*args):
400             """Shuts down child processes, existing requests are handled."""
401             signal.signal(signal.SIGHUP, signal.SIG_IGN)
402             eventlet.wsgi.is_accepting = False
403             self.sock.close()
404
405         pid = os.fork()
406         if pid == 0:
407             signal.signal(signal.SIGHUP, child_hup)
408             signal.signal(signal.SIGTERM, signal.SIG_DFL)
409             # ignore the interrupt signal to avoid a race whereby
410             # a child worker receives the signal before the parent
411             # and is respawned unnecessarily as a result
412             signal.signal(signal.SIGINT, signal.SIG_IGN)
413             # The child has no need to stash the unwrapped
414             # socket, and the reference prevents a clean
415             # exit on sighup
416             self._sock = None
417             self.run_server()
418             LOG.info(_LI('Child %d exiting normally') % os.getpid())
419             # self.pool.waitall() is now called in wsgi's server so
420             # it's safe to exit here
421             sys.exit(0)
422         else:
423             LOG.info(_LI('Started child %s') % pid)
424             self.children.add(pid)
425
426     def run_server(self):
427         """Run a WSGI server."""
428         if cfg.CONF.pydev_worker_debug_host:
429             utils.setup_remote_pydev_debug(cfg.CONF.pydev_worker_debug_host,
430                                            cfg.CONF.pydev_worker_debug_port)
431
432         eventlet.wsgi.HttpProtocol.default_request_version = "HTTP/1.0"
433         self.pool = self.create_pool()
434         try:
435             eventlet.wsgi.server(self.sock,
436                                  self.application,
437                                  log=self._wsgi_logger,
438                                  custom_pool=self.pool,
439                                  debug=False,
440                                  keepalive=CONF.http_keepalive)
441         except socket.error as err:
442             if err[0] != errno.EINVAL:
443                 raise
444
445         # waiting on async pools
446         if ASYNC_EVENTLET_THREAD_POOL_LIST:
447             for pool in ASYNC_EVENTLET_THREAD_POOL_LIST:
448                 pool.waitall()
449
450     def _single_run(self, application, sock):
451         """Start a WSGI server in a new green thread."""
452         LOG.info(_LI("Starting single process server"))
453         eventlet.wsgi.server(sock, application, custom_pool=self.pool,
454                              log=self._wsgi_logger,
455                              debug=False,
456                              keepalive=CONF.http_keepalive)
457
458     def configure_socket(self, old_conf=None, has_changed=None):
459         """
460         Ensure a socket exists and is appropriately configured.
461
462         This function is called on start up, and can also be
463         called in the event of a configuration reload.
464
465         When called for the first time a new socket is created.
466         If reloading and either bind_host or bind port have been
467         changed the existing socket must be closed and a new
468         socket opened (laws of physics).
469
470         In all other cases (bind_host/bind_port have not changed)
471         the existing socket is reused.
472
473         :param old_conf: Cached old configuration settings (if any)
474         :param has changed: callable to determine if a parameter has changed
475         """
476         # Do we need a fresh socket?
477         new_sock = (old_conf is None or (
478                     has_changed('bind_host') or
479                     has_changed('bind_port')))
480         # Will we be using https?
481         use_ssl = not (not CONF.cert_file or not CONF.key_file)
482         # Were we using https before?
483         old_use_ssl = (old_conf is not None and not (
484                        not old_conf.get('key_file') or
485                        not old_conf.get('cert_file')))
486         # Do we now need to perform an SSL wrap on the socket?
487         wrap_sock = use_ssl is True and (old_use_ssl is False or new_sock)
488         # Do we now need to perform an SSL unwrap on the socket?
489         unwrap_sock = use_ssl is False and old_use_ssl is True
490
491         if new_sock:
492             self._sock = None
493             if old_conf is not None:
494                 self.sock.close()
495             _sock = get_socket(self.default_port)
496             _sock.setsockopt(socket.SOL_SOCKET,
497                              socket.SO_REUSEADDR, 1)
498             # sockets can hang around forever without keepalive
499             _sock.setsockopt(socket.SOL_SOCKET,
500                              socket.SO_KEEPALIVE, 1)
501             self._sock = _sock
502
503         if wrap_sock:
504             self.sock = ssl_wrap_socket(self._sock)
505
506         if unwrap_sock:
507             self.sock = self._sock
508
509         if new_sock and not use_ssl:
510             self.sock = self._sock
511
512         # Pick up newly deployed certs
513         if old_conf is not None and use_ssl is True and old_use_ssl is True:
514             if has_changed('cert_file') or has_changed('key_file'):
515                 utils.validate_key_cert(CONF.key_file, CONF.cert_file)
516             if has_changed('cert_file'):
517                 self.sock.certfile = CONF.cert_file
518             if has_changed('key_file'):
519                 self.sock.keyfile = CONF.key_file
520
521         if new_sock or (old_conf is not None and has_changed('tcp_keepidle')):
522             # This option isn't available in the OS X version of eventlet
523             if hasattr(socket, 'TCP_KEEPIDLE'):
524                 self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE,
525                                      CONF.tcp_keepidle)
526
527         if old_conf is not None and has_changed('backlog'):
528             self.sock.listen(CONF.backlog)
529
530
531 class Middleware(object):
532     """
533     Base WSGI middleware wrapper. These classes require an application to be
534     initialized that will be called next.  By default the middleware will
535     simply call its wrapped app, or you can override __call__ to customize its
536     behavior.
537     """
538
539     def __init__(self, application):
540         self.application = application
541
542     @classmethod
543     def factory(cls, global_conf, **local_conf):
544         def filter(app):
545             return cls(app)
546         return filter
547
548     def process_request(self, req):
549         """
550         Called on each request.
551
552         If this returns None, the next application down the stack will be
553         executed. If it returns a response then that response will be returned
554         and execution will stop here.
555
556         """
557         return None
558
559     def process_response(self, response):
560         """Do whatever you'd like to the response."""
561         return response
562
563     @webob.dec.wsgify
564     def __call__(self, req):
565         response = self.process_request(req)
566         if response:
567             return response
568         response = req.get_response(self.application)
569         response.request = req
570         try:
571             return self.process_response(response)
572         except webob.exc.HTTPException as e:
573             return e
574
575
576 class Debug(Middleware):
577     """
578     Helper class that can be inserted into any WSGI application chain
579     to get information about the request and response.
580     """
581
582     @webob.dec.wsgify
583     def __call__(self, req):
584         print(("*" * 40) + " REQUEST ENVIRON")
585         for key, value in req.environ.items():
586             print(key, "=", value)
587         print('')
588         resp = req.get_response(self.application)
589
590         print(("*" * 40) + " RESPONSE HEADERS")
591         for (key, value) in six.iteritems(resp.headers):
592             print(key, "=", value)
593         print('')
594
595         resp.app_iter = self.print_generator(resp.app_iter)
596
597         return resp
598
599     @staticmethod
600     def print_generator(app_iter):
601         """
602         Iterator that prints the contents of a wrapper string iterator
603         when iterated.
604         """
605         print(("*" * 40) + " BODY")
606         for part in app_iter:
607             sys.stdout.write(part)
608             sys.stdout.flush()
609             yield part
610         print()
611
612
613 class APIMapper(routes.Mapper):
614     """
615     Handle route matching when url is '' because routes.Mapper returns
616     an error in this case.
617     """
618
619     def routematch(self, url=None, environ=None):
620         if url is "":
621             result = self._match("", environ)
622             return result[0], result[1]
623         return routes.Mapper.routematch(self, url, environ)
624
625
626 class RejectMethodController(object):
627
628     def reject(self, req, allowed_methods, *args, **kwargs):
629         LOG.debug("The method %s is not allowed for this resource" %
630                   req.environ['REQUEST_METHOD'])
631         raise webob.exc.HTTPMethodNotAllowed(
632             headers=[('Allow', allowed_methods)])
633
634
635 class Router(object):
636     """
637     WSGI middleware that maps incoming requests to WSGI apps.
638     """
639
640     def __init__(self, mapper):
641         """
642         Create a router for the given routes.Mapper.
643
644         Each route in `mapper` must specify a 'controller', which is a
645         WSGI app to call.  You'll probably want to specify an 'action' as
646         well and have your controller be a wsgi.Controller, who will route
647         the request to the action method.
648
649         Examples:
650           mapper = routes.Mapper()
651           sc = ServerController()
652
653           # Explicit mapping of one route to a controller+action
654           mapper.connect(None, "/svrlist", controller=sc, action="list")
655
656           # Actions are all implicitly defined
657           mapper.resource("server", "servers", controller=sc)
658
659           # Pointing to an arbitrary WSGI app.  You can specify the
660           # {path_info:.*} parameter so the target app can be handed just that
661           # section of the URL.
662           mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp())
663         """
664         mapper.redirect("", "/")
665         self.map = mapper
666         self._router = routes.middleware.RoutesMiddleware(self._dispatch,
667                                                           self.map)
668
669     @classmethod
670     def factory(cls, global_conf, **local_conf):
671         return cls(APIMapper())
672
673     @webob.dec.wsgify
674     def __call__(self, req):
675         """
676         Route the incoming request to a controller based on self.map.
677         If no match, return either a 404(Not Found) or 501(Not Implemented).
678         """
679         return self._router
680
681     @staticmethod
682     @webob.dec.wsgify
683     def _dispatch(req):
684         """
685         Called by self._router after matching the incoming request to a route
686         and putting the information into req.environ.  Either returns 404,
687         501, or the routed WSGI app's response.
688         """
689         match = req.environ['wsgiorg.routing_args'][1]
690         if not match:
691             implemented_http_methods = ['GET', 'HEAD', 'POST', 'PUT',
692                                         'DELETE', 'PATCH']
693             if req.environ['REQUEST_METHOD'] not in implemented_http_methods:
694                 return webob.exc.HTTPNotImplemented()
695             else:
696                 return webob.exc.HTTPNotFound()
697         app = match['controller']
698         return app
699
700
701 class Request(webob.Request):
702     """Add some OpenStack API-specific logic to the base webob.Request."""
703
704     def best_match_content_type(self):
705         """Determine the requested response content-type."""
706         supported = ('application/json',)
707         bm = self.accept.best_match(supported)
708         return bm or 'application/json'
709
710     def get_content_type(self, allowed_content_types):
711         """Determine content type of the request body."""
712         if "Content-Type" not in self.headers:
713             raise exception.InvalidContentType(content_type=None)
714
715         content_type = self.content_type
716
717         if content_type not in allowed_content_types:
718             raise exception.InvalidContentType(content_type=content_type)
719         else:
720             return content_type
721
722     def best_match_language(self):
723         """Determines best available locale from the Accept-Language header.
724
725         :returns: the best language match or None if the 'Accept-Language'
726                   header was not available in the request.
727         """
728         if not self.accept_language:
729             return None
730         langs = i18n.get_available_languages('escalator')
731         return self.accept_language.best_match(langs)
732
733     def get_content_range(self):
734         """Return the `Range` in a request."""
735         range_str = self.headers.get('Content-Range')
736         if range_str is not None:
737             range_ = webob.byterange.ContentRange.parse(range_str)
738             if range_ is None:
739                 msg = _('Malformed Content-Range header: %s') % range_str
740                 raise webob.exc.HTTPBadRequest(explanation=msg)
741             return range_
742
743
744 class JSONRequestDeserializer(object):
745     valid_transfer_encoding = frozenset(['chunked', 'compress', 'deflate',
746                                          'gzip', 'identity'])
747
748     def has_body(self, request):
749         """
750         Returns whether a Webob.Request object will possess an entity body.
751
752         :param request:  Webob.Request object
753         """
754         request_encoding = request.headers.get('transfer-encoding', '').lower()
755         is_valid_encoding = request_encoding in self.valid_transfer_encoding
756         if is_valid_encoding and request.is_body_readable:
757             return True
758         elif request.content_length > 0:
759             return True
760
761         return False
762
763     @staticmethod
764     def _sanitizer(obj):
765         """Sanitizer method that will be passed to jsonutils.loads."""
766         return obj
767
768     def from_json(self, datastring):
769         try:
770             return jsonutils.loads(datastring, object_hook=self._sanitizer)
771         except ValueError:
772             msg = _('Malformed JSON in request body.')
773             raise webob.exc.HTTPBadRequest(explanation=msg)
774
775     def default(self, request):
776         if self.has_body(request):
777             return {'body': self.from_json(request.body)}
778         else:
779             return {}
780
781
782 class JSONResponseSerializer(object):
783
784     def _sanitizer(self, obj):
785         """Sanitizer method that will be passed to jsonutils.dumps."""
786         if hasattr(obj, "to_dict"):
787             return obj.to_dict()
788         if isinstance(obj, multidict.MultiDict):
789             return obj.mixed()
790         return jsonutils.to_primitive(obj)
791
792     def to_json(self, data):
793         return jsonutils.dumps(data, default=self._sanitizer)
794
795     def default(self, response, result):
796         response.content_type = 'application/json'
797         response.body = self.to_json(result)
798
799
800 def translate_exception(req, e):
801     """Translates all translatable elements of the given exception."""
802
803     # The RequestClass attribute in the webob.dec.wsgify decorator
804     # does not guarantee that the request object will be a particular
805     # type; this check is therefore necessary.
806     if not hasattr(req, "best_match_language"):
807         return e
808
809     locale = req.best_match_language()
810
811     if isinstance(e, webob.exc.HTTPError):
812         e.explanation = i18n.translate(e.explanation, locale)
813         e.detail = i18n.translate(e.detail, locale)
814         if getattr(e, 'body_template', None):
815             e.body_template = i18n.translate(e.body_template, locale)
816     return e
817
818
819 class Resource(object):
820     """
821     WSGI app that handles (de)serialization and controller dispatch.
822
823     Reads routing information supplied by RoutesMiddleware and calls
824     the requested action method upon its deserializer, controller,
825     and serializer. Those three objects may implement any of the basic
826     controller action methods (create, update, show, index, delete)
827     along with any that may be specified in the api router. A 'default'
828     method may also be implemented to be used in place of any
829     non-implemented actions. Deserializer methods must accept a request
830     argument and return a dictionary. Controller methods must accept a
831     request argument. Additionally, they must also accept keyword
832     arguments that represent the keys returned by the Deserializer. They
833     may raise a webob.exc exception or return a dict, which will be
834     serialized by requested content type.
835     """
836
837     def __init__(self, controller, deserializer=None, serializer=None):
838         """
839         :param controller: object that implement methods created by routes lib
840         :param deserializer: object that supports webob request deserialization
841                              through controller-like actions
842         :param serializer: object that supports webob response serialization
843                            through controller-like actions
844         """
845         self.controller = controller
846         self.serializer = serializer or JSONResponseSerializer()
847         self.deserializer = deserializer or JSONRequestDeserializer()
848
849     @webob.dec.wsgify(RequestClass=Request)
850     def __call__(self, request):
851         """WSGI method that controls (de)serialization and method dispatch."""
852         action_args = self.get_action_args(request.environ)
853         action = action_args.pop('action', None)
854
855         try:
856             deserialized_request = self.dispatch(self.deserializer,
857                                                  action, request)
858             action_args.update(deserialized_request)
859             action_result = self.dispatch(self.controller, action,
860                                           request, **action_args)
861         except webob.exc.WSGIHTTPException as e:
862             exc_info = sys.exc_info()
863             raise translate_exception(request, e), None, exc_info[2]
864
865         try:
866             response = webob.Response(request=request)
867             self.dispatch(self.serializer, action, response, action_result)
868             return response
869         except webob.exc.WSGIHTTPException as e:
870             return translate_exception(request, e)
871         except webob.exc.HTTPException as e:
872             return e
873         # return unserializable result (typically a webob exc)
874         except Exception:
875             return action_result
876
877     def dispatch(self, obj, action, *args, **kwargs):
878         """Find action-specific method on self and call it."""
879         try:
880             method = getattr(obj, action)
881         except AttributeError:
882             method = getattr(obj, 'default')
883
884         return method(*args, **kwargs)
885
886     def get_action_args(self, request_environment):
887         """Parse dictionary created by routes library."""
888         try:
889             args = request_environment['wsgiorg.routing_args'][1].copy()
890         except Exception:
891             return {}
892
893         try:
894             del args['controller']
895         except KeyError:
896             pass
897
898         try:
899             del args['format']
900         except KeyError:
901             pass
902
903         return args