See description of change... what's the difference between that message and this message again?
This commit is contained in:
+2
-2
@@ -24,11 +24,11 @@
|
||||
from nova import flags
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
from nova.endpoint import rackspace
|
||||
from nova.endpoint import newapi
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_integer('cc_port', 8773, 'cloud controller port')
|
||||
|
||||
if __name__ == '__main__':
|
||||
utils.default_flagfile()
|
||||
wsgi.run_server(rackspace.API(), FLAGS.cc_port)
|
||||
wsgi.run_server(newapi.APIVersionRouter(), FLAGS.cc_port)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
:mod:`nova.endpoint` -- Main NOVA Api endpoints
|
||||
=====================================================
|
||||
|
||||
.. automodule:: nova.endpoint
|
||||
:platform: Unix
|
||||
:synopsis: REST APIs for all nova functions
|
||||
.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
|
||||
.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com>
|
||||
.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
|
||||
.. moduleauthor:: Joshua McKenty <joshua@cognition.ca>
|
||||
.. moduleauthor:: Manish Singh <yosh@gimp.org>
|
||||
.. moduleauthor:: Andy Smith <andy@anarkystic.com>
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import routes
|
||||
import webob.dec
|
||||
|
||||
from nova import wsgi
|
||||
|
||||
# TODO(gundlach): temp
|
||||
class API(wsgi.Router):
|
||||
"""WSGI entry point for all AWS API requests."""
|
||||
|
||||
def __init__(self):
|
||||
mapper = routes.Mapper()
|
||||
|
||||
mapper.connect(None, "{all:.*}", controller=self.dummy)
|
||||
|
||||
super(API, self).__init__(mapper)
|
||||
|
||||
@webob.dec.wsgify
|
||||
def dummy(self, req):
|
||||
#TODO(gundlach)
|
||||
msg = "dummy response -- please hook up __init__() to cloud.py instead"
|
||||
return repr({ 'dummy': msg,
|
||||
'kwargs': repr(req.environ['wsgiorg.routing_args'][1]) })
|
||||
@@ -0,0 +1,51 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
:mod:`nova.endpoint` -- Main NOVA Api endpoints
|
||||
=====================================================
|
||||
|
||||
.. automodule:: nova.endpoint
|
||||
:platform: Unix
|
||||
:synopsis: REST APIs for all nova functions
|
||||
.. moduleauthor:: Jesse Andrews <jesse@ansolabs.com>
|
||||
.. moduleauthor:: Devin Carlen <devin.carlen@gmail.com>
|
||||
.. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com>
|
||||
.. moduleauthor:: Joshua McKenty <joshua@cognition.ca>
|
||||
.. moduleauthor:: Manish Singh <yosh@gimp.org>
|
||||
.. moduleauthor:: Andy Smith <andy@anarkystic.com>
|
||||
"""
|
||||
|
||||
from nova import wsgi
|
||||
import routes
|
||||
from nova.endpoint import rackspace
|
||||
from nova.endpoint import aws
|
||||
|
||||
class APIVersionRouter(wsgi.Router):
|
||||
"""Routes top-level requests to the appropriate API."""
|
||||
|
||||
def __init__(self):
|
||||
mapper = routes.Mapper()
|
||||
|
||||
rsapi = rackspace.API()
|
||||
mapper.connect(None, "/v1.0/{path_info:.*}", controller=rsapi)
|
||||
|
||||
mapper.connect(None, "/ec2/{path_info:.*}", controller=aws.API())
|
||||
|
||||
super(APIVersionRouter, self).__init__(mapper)
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Rackspace API Endpoint
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from nova import flags
|
||||
from nova import rpc
|
||||
from nova import utils
|
||||
from nova import wsgi
|
||||
from nova.auth import manager
|
||||
from nova.compute import model as compute
|
||||
from nova.network import model as network
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on')
|
||||
|
||||
|
||||
class API(wsgi.Middleware):
|
||||
"""Entry point for all requests."""
|
||||
|
||||
def __init__(self):
|
||||
super(API, self).__init__(Router(webob.exc.HTTPNotFound()))
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
context = {}
|
||||
if "HTTP_X_AUTH_TOKEN" in environ:
|
||||
context['user'] = manager.AuthManager().get_user_from_access_key(
|
||||
environ['HTTP_X_AUTH_TOKEN'])
|
||||
if context['user']:
|
||||
context['project'] = manager.AuthManager().get_project(
|
||||
context['user'].name)
|
||||
if "user" not in context:
|
||||
return webob.exc.HTTPForbidden()(environ, start_response)
|
||||
environ['nova.context'] = context
|
||||
return self.application(environ, start_response)
|
||||
|
||||
|
||||
class Router(wsgi.Router):
|
||||
"""Route requests to the next WSGI application."""
|
||||
|
||||
def _build_map(self):
|
||||
"""Build routing map for authentication and cloud."""
|
||||
self._connect("/v1.0", controller=AuthenticationAPI())
|
||||
cloud = CloudServerAPI()
|
||||
self._connect("/servers", controller=cloud.launch_server,
|
||||
conditions={"method": ["POST"]})
|
||||
self._connect("/servers/{server_id}", controller=cloud.delete_server,
|
||||
conditions={'method': ["DELETE"]})
|
||||
self._connect("/servers", controller=cloud)
|
||||
|
||||
|
||||
class AuthenticationAPI(wsgi.Application):
|
||||
"""Handle all authorization requests through WSGI applications."""
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req): # pylint: disable-msg=W0221
|
||||
# TODO(todd): make a actual session with a unique token
|
||||
# just pass the auth key back through for now
|
||||
res = webob.Response()
|
||||
res.status = '204 No Content'
|
||||
res.headers.add('X-Server-Management-Url', req.host_url)
|
||||
res.headers.add('X-Storage-Url', req.host_url)
|
||||
res.headers.add('X-CDN-Managment-Url', req.host_url)
|
||||
res.headers.add('X-Auth-Token', req.headers['X-Auth-Key'])
|
||||
return res
|
||||
|
||||
|
||||
class CloudServerAPI(wsgi.Application):
|
||||
"""Handle all server requests through WSGI applications."""
|
||||
|
||||
def __init__(self):
|
||||
super(CloudServerAPI, self).__init__()
|
||||
self.instdir = compute.InstanceDirectory()
|
||||
self.network = network.PublicNetworkController()
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req): # pylint: disable-msg=W0221
|
||||
value = {"servers": []}
|
||||
for inst in self.instdir.all:
|
||||
value["servers"].append(self.instance_details(inst))
|
||||
return json.dumps(value)
|
||||
|
||||
def instance_details(self, inst): # pylint: disable-msg=R0201
|
||||
"""Build the data structure to represent details for an instance."""
|
||||
return {
|
||||
"id": inst.get("instance_id", None),
|
||||
"imageId": inst.get("image_id", None),
|
||||
"flavorId": inst.get("instacne_type", None),
|
||||
"hostId": inst.get("node_name", None),
|
||||
"status": inst.get("state", "pending"),
|
||||
"addresses": {
|
||||
"public": [network.get_public_ip_for_instance(
|
||||
inst.get("instance_id", None))],
|
||||
"private": [inst.get("private_dns_name", None)]},
|
||||
|
||||
# implemented only by Rackspace, not AWS
|
||||
"name": inst.get("name", "Not-Specified"),
|
||||
|
||||
# not supported
|
||||
"progress": "Not-Supported",
|
||||
"metadata": {
|
||||
"Server Label": "Not-Supported",
|
||||
"Image Version": "Not-Supported"}}
|
||||
|
||||
@webob.dec.wsgify
|
||||
def launch_server(self, req):
|
||||
"""Launch a new instance."""
|
||||
data = json.loads(req.body)
|
||||
inst = self.build_server_instance(data, req.environ['nova.context'])
|
||||
rpc.cast(
|
||||
FLAGS.compute_topic, {
|
||||
"method": "run_instance",
|
||||
"args": {"instance_id": inst.instance_id}})
|
||||
|
||||
return json.dumps({"server": self.instance_details(inst)})
|
||||
|
||||
def build_server_instance(self, env, context):
|
||||
"""Build instance data structure and save it to the data store."""
|
||||
reservation = utils.generate_uid('r')
|
||||
ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
inst = self.instdir.new()
|
||||
inst['name'] = env['server']['name']
|
||||
inst['image_id'] = env['server']['imageId']
|
||||
inst['instance_type'] = env['server']['flavorId']
|
||||
inst['user_id'] = context['user'].id
|
||||
inst['project_id'] = context['project'].id
|
||||
inst['reservation_id'] = reservation
|
||||
inst['launch_time'] = ltime
|
||||
inst['mac_address'] = utils.generate_mac()
|
||||
address = self.network.allocate_ip(
|
||||
inst['user_id'],
|
||||
inst['project_id'],
|
||||
mac=inst['mac_address'])
|
||||
inst['private_dns_name'] = str(address)
|
||||
inst['bridge_name'] = network.BridgedNetwork.get_network_for_project(
|
||||
inst['user_id'],
|
||||
inst['project_id'],
|
||||
'default')['bridge_name']
|
||||
# key_data, key_name, ami_launch_index
|
||||
# TODO(todd): key data or root password
|
||||
inst.save()
|
||||
return inst
|
||||
|
||||
@webob.dec.wsgify
|
||||
@wsgi.route_args
|
||||
def delete_server(self, req, route_args): # pylint: disable-msg=R0201
|
||||
"""Delete an instance."""
|
||||
owner_hostname = None
|
||||
instance = compute.Instance.lookup(route_args['server_id'])
|
||||
if instance:
|
||||
owner_hostname = instance["node_name"]
|
||||
if not owner_hostname:
|
||||
return webob.exc.HTTPNotFound("Did not find image, or it was "
|
||||
"not in a running state.")
|
||||
rpc_transport = "%s:%s" % (FLAGS.compute_topic, owner_hostname)
|
||||
rpc.cast(rpc_transport,
|
||||
{"method": "reboot_instance",
|
||||
"args": {"instance_id": route_args['server_id']}})
|
||||
req.status = "202 Accepted"
|
||||
@@ -0,0 +1,83 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Rackspace API Endpoint
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
import routes
|
||||
|
||||
from nova import flags
|
||||
from nova import wsgi
|
||||
from nova.auth import manager
|
||||
from nova.endpoint.rackspace import controllers
|
||||
|
||||
|
||||
class API(wsgi.Middleware):
|
||||
"""WSGI entry point for all Rackspace API requests."""
|
||||
|
||||
def __init__(self):
|
||||
app = AuthMiddleware(APIRouter())
|
||||
super(API, self).__init__(app)
|
||||
|
||||
|
||||
class AuthMiddleware(wsgi.Middleware):
|
||||
"""Authorize the rackspace API request or return an HTTP Forbidden."""
|
||||
|
||||
#TODO(gundlach): isn't this the old Nova API's auth? Should it be replaced
|
||||
#with correct RS API auth?
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
context = {}
|
||||
if "HTTP_X_AUTH_TOKEN" in req.environ:
|
||||
context['user'] = manager.AuthManager().get_user_from_access_key(
|
||||
req.environ['HTTP_X_AUTH_TOKEN'])
|
||||
if context['user']:
|
||||
context['project'] = manager.AuthManager().get_project(
|
||||
context['user'].name)
|
||||
if "user" not in context:
|
||||
return webob.exc.HTTPForbidden()
|
||||
req.environ['nova.context'] = context
|
||||
return self.application
|
||||
|
||||
|
||||
class APIRouter(wsgi.Router):
|
||||
"""
|
||||
Routes requests on the Rackspace API to the appropriate controller
|
||||
and method.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
mapper = routes.Mapper()
|
||||
|
||||
mapper.resource("server", "servers",
|
||||
controller=controllers.ServersController())
|
||||
mapper.resource("image", "images",
|
||||
controller=controllers.ImagesController())
|
||||
mapper.resource("flavor", "flavors",
|
||||
controller=controllers.FlavorsController())
|
||||
mapper.resource("sharedipgroup", "sharedipgroups",
|
||||
controller=controllers.SharedIpGroupsController())
|
||||
|
||||
super(APIRouter, self).__init__(mapper)
|
||||
@@ -0,0 +1,5 @@
|
||||
from nova.endpoint.rackspace.controllers.images import ImagesController
|
||||
from nova.endpoint.rackspace.controllers.flavors import FlavorsController
|
||||
from nova.endpoint.rackspace.controllers.servers import ServersController
|
||||
from nova.endpoint.rackspace.controllers.sharedipgroups import \
|
||||
SharedIpGroupsController
|
||||
@@ -0,0 +1,9 @@
|
||||
from nova import wsgi
|
||||
|
||||
class BaseController(wsgi.Controller):
|
||||
@classmethod
|
||||
def render(cls, instance):
|
||||
if isinstance(instance, list):
|
||||
return { cls.entity_name : cls.render(instance) }
|
||||
else:
|
||||
return { "TODO": "TODO" }
|
||||
@@ -0,0 +1 @@
|
||||
class FlavorsController(object): pass
|
||||
@@ -0,0 +1 @@
|
||||
class ImagesController(object): pass
|
||||
@@ -0,0 +1,63 @@
|
||||
from nova import rpc
|
||||
from nova.compute import model as compute
|
||||
from nova.endpoint.rackspace.controllers.base import BaseController
|
||||
|
||||
class ServersController(BaseController):
|
||||
entity_name = 'servers'
|
||||
|
||||
def index(self, **kwargs):
|
||||
return [instance_details(inst) for inst in compute.InstanceDirectory().all]
|
||||
|
||||
def show(self, **kwargs):
|
||||
instance_id = kwargs['id']
|
||||
return compute.InstanceDirectory().get(instance_id)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
instance_id = kwargs['id']
|
||||
instance = compute.InstanceDirectory().get(instance_id)
|
||||
if not instance:
|
||||
raise ServerNotFound("The requested server was not found")
|
||||
instance.destroy()
|
||||
return True
|
||||
|
||||
def create(self, **kwargs):
|
||||
inst = self.build_server_instance(kwargs['server'])
|
||||
rpc.cast(
|
||||
FLAGS.compute_topic, {
|
||||
"method": "run_instance",
|
||||
"args": {"instance_id": inst.instance_id}})
|
||||
|
||||
def update(self, **kwargs):
|
||||
instance_id = kwargs['id']
|
||||
instance = compute.InstanceDirectory().get(instance_id)
|
||||
if not instance:
|
||||
raise ServerNotFound("The requested server was not found")
|
||||
instance.update(kwargs['server'])
|
||||
instance.save()
|
||||
|
||||
def build_server_instance(self, env):
|
||||
"""Build instance data structure and save it to the data store."""
|
||||
reservation = utils.generate_uid('r')
|
||||
ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
|
||||
inst = self.instdir.new()
|
||||
inst['name'] = env['server']['name']
|
||||
inst['image_id'] = env['server']['imageId']
|
||||
inst['instance_type'] = env['server']['flavorId']
|
||||
inst['user_id'] = env['user']['id']
|
||||
inst['project_id'] = env['project']['id']
|
||||
inst['reservation_id'] = reservation
|
||||
inst['launch_time'] = ltime
|
||||
inst['mac_address'] = utils.generate_mac()
|
||||
address = self.network.allocate_ip(
|
||||
inst['user_id'],
|
||||
inst['project_id'],
|
||||
mac=inst['mac_address'])
|
||||
inst['private_dns_name'] = str(address)
|
||||
inst['bridge_name'] = network.BridgedNetwork.get_network_for_project(
|
||||
inst['user_id'],
|
||||
inst['project_id'],
|
||||
'default')['bridge_name']
|
||||
# key_data, key_name, ami_launch_index
|
||||
# TODO(todd): key data or root password
|
||||
inst.save()
|
||||
return inst
|
||||
@@ -0,0 +1 @@
|
||||
class SharedIpGroupsController(object): pass
|
||||
+167
-66
@@ -29,6 +29,8 @@ import eventlet.wsgi
|
||||
eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||
import routes
|
||||
import routes.middleware
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
|
||||
logging.getLogger("routes.middleware").addHandler(logging.StreamHandler())
|
||||
@@ -41,6 +43,8 @@ def run_server(application, port):
|
||||
|
||||
|
||||
class Application(object):
|
||||
# TODO(gundlach): I think we should toss this class, now that it has no
|
||||
# purpose.
|
||||
"""Base WSGI application wrapper. Subclasses need to implement __call__."""
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
@@ -79,95 +83,192 @@ class Application(object):
|
||||
raise NotImplementedError("You must implement __call__")
|
||||
|
||||
|
||||
class Middleware(Application): # pylint: disable-msg=W0223
|
||||
"""Base WSGI middleware wrapper. These classes require an
|
||||
application to be initialized that will be called next."""
|
||||
class Middleware(Application): # pylint: disable=W0223
|
||||
"""
|
||||
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): # pylint: disable-msg=W0231
|
||||
def __init__(self, application): # pylint: disable=W0231
|
||||
self.application = application
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
"""Override to implement middleware behavior."""
|
||||
return self.application
|
||||
|
||||
|
||||
class Debug(Middleware):
|
||||
"""Helper class that can be insertd into any WSGI application chain
|
||||
"""Helper class that can be inserted into any WSGI application chain
|
||||
to get information about the request and response."""
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
for key, value in environ.items():
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
print ("*" * 40) + " REQUEST ENVIRON"
|
||||
for key, value in req.environ.items():
|
||||
print key, "=", value
|
||||
print
|
||||
wrapper = debug_start_response(start_response)
|
||||
return debug_print_body(self.application(environ, wrapper))
|
||||
resp = req.get_response(self.application)
|
||||
|
||||
|
||||
def debug_start_response(start_response):
|
||||
"""Wrap the start_response to capture when called."""
|
||||
|
||||
def wrapper(status, headers, exc_info=None):
|
||||
"""Print out all headers when start_response is called."""
|
||||
print status
|
||||
for (key, value) in headers:
|
||||
print ("*" * 40) + " RESPONSE HEADERS"
|
||||
for (key, value) in resp.headers:
|
||||
print key, "=", value
|
||||
print
|
||||
start_response(status, headers, exc_info)
|
||||
|
||||
return wrapper
|
||||
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
|
||||
|
||||
|
||||
def debug_print_body(body):
|
||||
"""Print the body of the response as it is sent back."""
|
||||
class Router(object):
|
||||
"""
|
||||
WSGI middleware that maps incoming requests to WSGI apps.
|
||||
"""
|
||||
|
||||
class Wrapper(object):
|
||||
"""Iterate through all the body parts and print before returning."""
|
||||
def __init__(self, mapper):
|
||||
"""
|
||||
Create a router for the given routes.Mapper.
|
||||
|
||||
def __iter__(self):
|
||||
for part in body:
|
||||
sys.stdout.write(part)
|
||||
sys.stdout.flush()
|
||||
yield part
|
||||
print
|
||||
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.
|
||||
|
||||
return Wrapper()
|
||||
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
|
||||
|
||||
@webob.dec.wsgify
|
||||
def _dispatch(self, 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 ParsedRoutes(Middleware):
|
||||
"""Processed parsed routes from routes.middleware.RoutesMiddleware
|
||||
and call either the controller if found or the default application
|
||||
otherwise."""
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
if environ['routes.route'] is None:
|
||||
return self.application(environ, start_response)
|
||||
app = environ['wsgiorg.routing_args'][1]['controller']
|
||||
return app(environ, start_response)
|
||||
class Controller(object):
|
||||
"""
|
||||
WSGI app that reads routing information supplied by RoutesMiddleware
|
||||
and calls the requested action method upon itself. All action methods
|
||||
must, in addition to their normal parameters, accept a 'req' argument
|
||||
which is the incoming webob.Request.
|
||||
"""
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
"""
|
||||
Call the method specified in req.environ by RoutesMiddleware.
|
||||
"""
|
||||
arg_dict = req.environ['wsgiorg.routing_args'][1]
|
||||
action = arg_dict['action']
|
||||
method = getattr(self, action)
|
||||
del arg_dict['controller']
|
||||
del arg_dict['action']
|
||||
arg_dict['req'] = req
|
||||
return method(**arg_dict)
|
||||
|
||||
|
||||
class Router(Middleware): # pylint: disable-msg=R0921
|
||||
"""Wrapper to help setup routes.middleware.RoutesMiddleware."""
|
||||
class Serializer(object):
|
||||
"""
|
||||
Serializes a dictionary to a Content Type specified by a WSGI environment.
|
||||
"""
|
||||
|
||||
def __init__(self, application):
|
||||
self.map = routes.Mapper()
|
||||
self._build_map()
|
||||
application = ParsedRoutes(application)
|
||||
application = routes.middleware.RoutesMiddleware(application, self.map)
|
||||
super(Router, self).__init__(application)
|
||||
def __init__(self, environ, metadata=None):
|
||||
"""
|
||||
Create a serializer based on the given WSGI environment.
|
||||
'metadata' is an optional dict mapping MIME types to information
|
||||
needed to serialize a dictionary to that type.
|
||||
"""
|
||||
self.environ = environ
|
||||
self.metadata = metadata or {}
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.application(environ, start_response)
|
||||
def to_content_type(self, data):
|
||||
"""
|
||||
Serialize a dictionary into a string. The format of the string
|
||||
will be decided based on the Content Type requested in self.environ:
|
||||
by Accept: header, or by URL suffix.
|
||||
"""
|
||||
mimetype = 'application/xml'
|
||||
# TODO(gundlach): determine mimetype from request
|
||||
|
||||
def _build_map(self):
|
||||
"""Method to create new connections for the routing map."""
|
||||
raise NotImplementedError("You must implement _build_map")
|
||||
if mimetype == 'application/json':
|
||||
import json
|
||||
return json.dumps(data)
|
||||
elif mimetype == 'application/xml':
|
||||
metadata = self.metadata.get('application/xml', {})
|
||||
# We expect data to contain a single key which is the XML root.
|
||||
root_key = data.keys()[0]
|
||||
from xml.dom import minidom
|
||||
doc = minidom.Document()
|
||||
node = self._to_xml_node(doc, metadata, root_key, data[root_key])
|
||||
return node.toprettyxml(indent=' ')
|
||||
else:
|
||||
return repr(data)
|
||||
|
||||
def _connect(self, *args, **kwargs):
|
||||
"""Wrapper for the map.connect method."""
|
||||
self.map.connect(*args, **kwargs)
|
||||
|
||||
|
||||
def route_args(application):
|
||||
"""Decorator to make grabbing routing args more convenient."""
|
||||
|
||||
def wrapper(self, req):
|
||||
"""Call application with req and parsed routing args from."""
|
||||
return application(self, req, req.environ['wsgiorg.routing_args'][1])
|
||||
|
||||
return wrapper
|
||||
def _to_xml_node(self, doc, metadata, nodename, data):
|
||||
result = doc.createElement(nodename)
|
||||
if type(data) is list:
|
||||
singular = metadata.get('plurals', {}).get(nodename, None)
|
||||
if singular is None:
|
||||
if nodename.endswith('s'):
|
||||
singular = nodename[:-1]
|
||||
else:
|
||||
singular = 'item'
|
||||
for item in data:
|
||||
node = self._to_xml_node(doc, metadata, singular, item)
|
||||
result.appendChild(node)
|
||||
elif type(data) is dict:
|
||||
attrs = metadata.get('attributes', {}).get(nodename, {})
|
||||
for k,v in data.items():
|
||||
if k in attrs:
|
||||
result.setAttribute(k, str(v))
|
||||
else:
|
||||
node = self._to_xml_node(doc, metadata, k, v)
|
||||
result.appendChild(node)
|
||||
else: # atom
|
||||
node = doc.createTextNode(str(data))
|
||||
result.appendChild(node)
|
||||
return result
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
[Messages Control]
|
||||
disable-msg=C0103
|
||||
disable=C0103
|
||||
# TODOs in code comments are fine...
|
||||
disable=W0511
|
||||
# *args and **kwargs are fine
|
||||
disable=W0142
|
||||
|
||||
[Basic]
|
||||
# Variables can be 1 to 31 characters long, with
|
||||
@@ -10,10 +14,6 @@ variable-rgx=[a-z_][a-z0-9_]{0,30}$
|
||||
# and be lowecased with underscores
|
||||
method-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# TODOs in code comments are fine...
|
||||
disable-msg=W0511
|
||||
|
||||
[Design]
|
||||
max-public-methods=100
|
||||
min-public-methods=0
|
||||
|
||||
@@ -11,7 +11,9 @@ lockfile==0.8
|
||||
python-daemon==1.5.5
|
||||
python-gflags==1.3
|
||||
redis==2.0.0
|
||||
routes==1.12.3
|
||||
tornado==1.0
|
||||
webob==0.9.8
|
||||
wsgiref==0.1.2
|
||||
zope.interface==3.6.1
|
||||
mox==0.5.0
|
||||
|
||||
Reference in New Issue
Block a user