summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/nova-rsapi4
-rw-r--r--nova/endpoint/__init__.py32
-rw-r--r--nova/endpoint/aws/__init__.py22
-rw-r--r--nova/endpoint/newapi.py51
-rw-r--r--nova/endpoint/rackspace.py183
-rw-r--r--nova/endpoint/rackspace/__init__.py83
-rw-r--r--nova/endpoint/rackspace/controllers/__init__.py5
-rw-r--r--nova/endpoint/rackspace/controllers/base.py9
-rw-r--r--nova/endpoint/rackspace/controllers/flavors.py1
-rw-r--r--nova/endpoint/rackspace/controllers/images.py1
-rw-r--r--nova/endpoint/rackspace/controllers/servers.py63
-rw-r--r--nova/endpoint/rackspace/controllers/sharedipgroups.py1
-rw-r--r--nova/wsgi.py227
-rw-r--r--pylintrc10
-rw-r--r--tools/pip-requires2
15 files changed, 409 insertions, 285 deletions
diff --git a/bin/nova-rsapi b/bin/nova-rsapi
index 026880d5a..e2722422e 100755
--- a/bin/nova-rsapi
+++ b/bin/nova-rsapi
@@ -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)
diff --git a/nova/endpoint/__init__.py b/nova/endpoint/__init__.py
index 753685149..e69de29bb 100644
--- a/nova/endpoint/__init__.py
+++ b/nova/endpoint/__init__.py
@@ -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>
-"""
diff --git a/nova/endpoint/aws/__init__.py b/nova/endpoint/aws/__init__.py
new file mode 100644
index 000000000..55cbb8fd3
--- /dev/null
+++ b/nova/endpoint/aws/__init__.py
@@ -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]) })
diff --git a/nova/endpoint/newapi.py b/nova/endpoint/newapi.py
new file mode 100644
index 000000000..9aae933af
--- /dev/null
+++ b/nova/endpoint/newapi.py
@@ -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)
+
diff --git a/nova/endpoint/rackspace.py b/nova/endpoint/rackspace.py
deleted file mode 100644
index 75b828e91..000000000
--- a/nova/endpoint/rackspace.py
+++ /dev/null
@@ -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"
diff --git a/nova/endpoint/rackspace/__init__.py b/nova/endpoint/rackspace/__init__.py
new file mode 100644
index 000000000..ac53ee10b
--- /dev/null
+++ b/nova/endpoint/rackspace/__init__.py
@@ -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)
diff --git a/nova/endpoint/rackspace/controllers/__init__.py b/nova/endpoint/rackspace/controllers/__init__.py
new file mode 100644
index 000000000..052b6f365
--- /dev/null
+++ b/nova/endpoint/rackspace/controllers/__init__.py
@@ -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
diff --git a/nova/endpoint/rackspace/controllers/base.py b/nova/endpoint/rackspace/controllers/base.py
new file mode 100644
index 000000000..8cd44f62e
--- /dev/null
+++ b/nova/endpoint/rackspace/controllers/base.py
@@ -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" }
diff --git a/nova/endpoint/rackspace/controllers/flavors.py b/nova/endpoint/rackspace/controllers/flavors.py
new file mode 100644
index 000000000..f256cc852
--- /dev/null
+++ b/nova/endpoint/rackspace/controllers/flavors.py
@@ -0,0 +1 @@
+class FlavorsController(object): pass
diff --git a/nova/endpoint/rackspace/controllers/images.py b/nova/endpoint/rackspace/controllers/images.py
new file mode 100644
index 000000000..ae2a08849
--- /dev/null
+++ b/nova/endpoint/rackspace/controllers/images.py
@@ -0,0 +1 @@
+class ImagesController(object): pass
diff --git a/nova/endpoint/rackspace/controllers/servers.py b/nova/endpoint/rackspace/controllers/servers.py
new file mode 100644
index 000000000..2f8e662d6
--- /dev/null
+++ b/nova/endpoint/rackspace/controllers/servers.py
@@ -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
diff --git a/nova/endpoint/rackspace/controllers/sharedipgroups.py b/nova/endpoint/rackspace/controllers/sharedipgroups.py
new file mode 100644
index 000000000..9d346d623
--- /dev/null
+++ b/nova/endpoint/rackspace/controllers/sharedipgroups.py
@@ -0,0 +1 @@
+class SharedIpGroupsController(object): pass
diff --git a/nova/wsgi.py b/nova/wsgi.py
index 4fd6e59e3..a0a175dc7 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -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
-
-def debug_print_body(body):
- """Print the body of the response as it is sent back."""
+ resp.app_iter = self.print_generator(resp.app_iter)
- class Wrapper(object):
- """Iterate through all the body parts and print before returning."""
+ return resp
- def __iter__(self):
- for part in body:
- sys.stdout.write(part)
- sys.stdout.flush()
- yield part
- print
+ @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
- return Wrapper()
+class Router(object):
+ """
+ WSGI middleware that maps incoming requests to WSGI apps.
+ """
-class ParsedRoutes(Middleware):
- """Processed parsed routes from routes.middleware.RoutesMiddleware
- and call either the controller if found or the default application
- otherwise."""
+ def __init__(self, mapper):
+ """
+ Create a router for the given routes.Mapper.
- 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)
+ 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()
-class Router(Middleware): # pylint: disable-msg=R0921
- """Wrapper to help setup routes.middleware.RoutesMiddleware."""
+ # Explicit mapping of one route to a controller+action
+ mapper.connect(None, "/svrlist", controller=sc, action="list")
- 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)
+ # Actions are all implicitly defined
+ mapper.resource("server", "servers", controller=sc)
- def __call__(self, environ, start_response):
- return self.application(environ, start_response)
+ # 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)
- def _build_map(self):
- """Method to create new connections for the routing map."""
- raise NotImplementedError("You must implement _build_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
- def _connect(self, *args, **kwargs):
- """Wrapper for the map.connect method."""
- self.map.connect(*args, **kwargs)
+ @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 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)
-def route_args(application):
- """Decorator to make grabbing routing args more convenient."""
+class Serializer(object):
+ """
+ Serializes a dictionary to a Content Type specified by a WSGI environment.
+ """
- def wrapper(self, req):
- """Call application with req and parsed routing args from."""
- return application(self, req, req.environ['wsgiorg.routing_args'][1])
+ 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 {}
- return wrapper
+ 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
+
+ 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 _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
diff --git a/pylintrc b/pylintrc
index 53d02d6b2..6c799c7ea 100644
--- a/pylintrc
+++ b/pylintrc
@@ -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
diff --git a/tools/pip-requires b/tools/pip-requires
index c173d6221..28af7bcb9 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -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