409 lines
13 KiB
Python
409 lines
13 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
# Copyright 2010 OpenStack LLC.
|
|
# 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
|
|
"""
|
|
|
|
import datetime
|
|
import errno
|
|
import json
|
|
import logging
|
|
import sys
|
|
import time
|
|
|
|
import eventlet
|
|
from eventlet.green import socket, ssl
|
|
import eventlet.wsgi
|
|
import routes
|
|
import routes.middleware
|
|
import webob.dec
|
|
import webob.exc
|
|
|
|
from glance.common import exception
|
|
|
|
|
|
class WritableLogger(object):
|
|
"""A thin wrapper that responds to `write` and logs."""
|
|
|
|
def __init__(self, logger, level=logging.DEBUG):
|
|
self.logger = logger
|
|
self.level = level
|
|
|
|
def write(self, msg):
|
|
self.logger.log(self.level, msg.strip("\n"))
|
|
|
|
|
|
def get_socket(host, port, conf):
|
|
"""
|
|
Bind socket to bind ip:port in conf
|
|
|
|
note: Mostly comes from Swift with a few small changes...
|
|
|
|
:param host: Host to bind to
|
|
:param port: Port to bind to
|
|
:param conf: Configuration dict to read settings from
|
|
|
|
:returns : a socket object as returned from socket.listen or
|
|
ssl.wrap_socket if conf specifies cert_file
|
|
"""
|
|
bind_addr = (host, 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]
|
|
backlog = int(conf.get('backlog', 4096))
|
|
|
|
cert_file = conf.get('cert_file')
|
|
key_file = conf.get('key_file')
|
|
use_ssl = cert_file or key_file
|
|
if use_ssl and (not cert_file or not key_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 = None
|
|
retry_until = time.time() + 30
|
|
while not sock and time.time() < retry_until:
|
|
try:
|
|
sock = eventlet.listen(bind_addr, backlog=backlog,
|
|
family=address_family)
|
|
if use_ssl:
|
|
sock = ssl.wrap_socket(sock, certfile=cert_file,
|
|
keyfile=key_file)
|
|
except socket.error, err:
|
|
if err.args[0] != errno.EADDRINUSE:
|
|
raise
|
|
eventlet.sleep(0.1)
|
|
if not sock:
|
|
raise RuntimeError(_("Could not bind to %s:%s after trying for 30 "
|
|
"seconds") % bind_addr)
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
# in my experience, sockets can hang around forever without keepalive
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
|
|
|
# This option isn't available in the OS X version of eventlet
|
|
if hasattr(socket, 'TCP_KEEPIDLE'):
|
|
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 600)
|
|
|
|
return sock
|
|
|
|
|
|
class Server(object):
|
|
"""Server class to manage multiple WSGI sockets and applications."""
|
|
|
|
def __init__(self, threads=1000):
|
|
self.pool = eventlet.GreenPool(threads)
|
|
|
|
def start(self, application, port, host='0.0.0.0', conf=None):
|
|
"""
|
|
Run a WSGI server with the given application.
|
|
|
|
:param application: The application to run in the WSGI server
|
|
:param port: Port to bind to
|
|
:param host: Host to bind to
|
|
:param conf: Mapping of configuration options
|
|
"""
|
|
conf = conf or {}
|
|
socket = get_socket(host, port, conf)
|
|
self.pool.spawn_n(self._run, application, socket)
|
|
|
|
def wait(self):
|
|
"""Wait until all servers have completed running."""
|
|
try:
|
|
self.pool.waitall()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
def _run(self, application, socket):
|
|
"""Start a WSGI server in a new green thread."""
|
|
logger = logging.getLogger('eventlet.wsgi.server')
|
|
eventlet.wsgi.server(socket, application, custom_pool=self.pool,
|
|
log=WritableLogger(logger))
|
|
|
|
|
|
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
|
|
|
|
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)
|
|
return self.process_response(response)
|
|
|
|
|
|
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 resp.headers.iteritems():
|
|
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 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())
|
|
"""
|
|
self.map = mapper
|
|
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
|
self.map)
|
|
|
|
@webob.dec.wsgify
|
|
def __call__(self, req):
|
|
"""
|
|
Route the incoming request to a controller based on self.map.
|
|
If no match, return a 404.
|
|
"""
|
|
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
|
|
or the routed WSGI app's response.
|
|
"""
|
|
match = req.environ['wsgiorg.routing_args'][1]
|
|
if not match:
|
|
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 not "Content-Type" 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
|
|
|
|
|
|
class JSONRequestDeserializer(object):
|
|
def has_body(self, request):
|
|
"""
|
|
Returns whether a Webob.Request object will possess an entity body.
|
|
|
|
:param request: Webob.Request object
|
|
"""
|
|
if 'transfer-encoding' in request.headers:
|
|
return True
|
|
elif request.content_length > 0:
|
|
return True
|
|
|
|
return False
|
|
|
|
def from_json(self, datastring):
|
|
return json.loads(datastring)
|
|
|
|
def default(self, request):
|
|
if self.has_body(request):
|
|
return {'body': self.from_json(request.body)}
|
|
else:
|
|
return {}
|
|
|
|
|
|
class JSONResponseSerializer(object):
|
|
|
|
def to_json(self, data):
|
|
def sanitizer(obj):
|
|
if isinstance(obj, datetime.datetime):
|
|
return obj.isoformat()
|
|
return obj
|
|
|
|
return json.dumps(data, default=sanitizer)
|
|
|
|
def default(self, response, result):
|
|
response.headers.add('Content-Type', 'application/json')
|
|
response.body = self.to_json(result)
|
|
|
|
|
|
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, serializer):
|
|
"""
|
|
: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
|
|
self.deserializer = deserializer
|
|
|
|
@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)
|
|
|
|
deserialized_request = self.dispatch(self.deserializer,
|
|
action, request)
|
|
action_args.update(deserialized_request)
|
|
|
|
action_result = self.dispatch(self.controller, action,
|
|
request, **action_args)
|
|
try:
|
|
response = webob.Response()
|
|
self.dispatch(self.serializer, action, response, action_result)
|
|
return response
|
|
|
|
# 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
|