summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorEric Day <eday@oddments.org>2010-08-07 12:12:10 -0700
committerEric Day <eday@oddments.org>2010-08-07 12:12:10 -0700
commitfd625a55c3725b5cff4449a687b0d54d0d49bd2e (patch)
treea81134c75ff3f5163b15611ff534296489535bf3 /nova
parent91e085b2c272ebd30955a83d3871c402f6749316 (diff)
Reworked WSGI helper module and converted rackspace API endpoint to use it.
Diffstat (limited to 'nova')
-rw-r--r--nova/endpoint/eventletserver.py7
-rw-r--r--nova/endpoint/new_wsgi.py136
-rw-r--r--nova/endpoint/rackspace.py302
-rw-r--r--nova/endpoint/wsgi.py40
-rw-r--r--nova/wsgi.py173
5 files changed, 267 insertions, 391 deletions
diff --git a/nova/endpoint/eventletserver.py b/nova/endpoint/eventletserver.py
deleted file mode 100644
index b8c15ff5d..000000000
--- a/nova/endpoint/eventletserver.py
+++ /dev/null
@@ -1,7 +0,0 @@
-import eventlet
-import eventlet.wsgi
-eventlet.patcher.monkey_patch(all=False, socket=True)
-
-def serve(app, port):
- sock = eventlet.listen(('0.0.0.0', port))
- eventlet.wsgi.server(sock, app)
diff --git a/nova/endpoint/new_wsgi.py b/nova/endpoint/new_wsgi.py
deleted file mode 100644
index 0f096ddb7..000000000
--- a/nova/endpoint/new_wsgi.py
+++ /dev/null
@@ -1,136 +0,0 @@
-import eventletserver
-import carrot.connection
-import carrot.messaging
-import itertools
-import routes
-
-
-# See http://pythonpaste.org/webob/ for usage
-from webob.dec import wsgify
-from webob import exc, Request, Response
-
-class WSGILayer(object):
- def __init__(self, application=None):
- self.application = application
-
- def __call__(self, environ, start_response):
- # Subclasses will probably want to implement __call__ like this:
- #
- # @wsgify
- # def __call__(self, req):
- # # Any of the following objects work as responses:
- #
- # # Option 1: simple string
- # resp = 'message\n'
- #
- # # Option 2: a nicely formatted HTTP exception page
- # resp = exc.HTTPForbidden(detail='Nice try')
- #
- # # Option 3: a webob Response object (in case you need to play with
- # # headers, or you want to be treated like an iterable, or or or)
- # resp = Response(); resp.app_iter = open('somefile')
- #
- # # Option 4: any wsgi app to be run next
- # resp = self.application
- #
- # # Option 5: you can get a Response object for a wsgi app, too, to
- # # play with headers etc
- # resp = req.get_response(self.application)
- #
- #
- # # You can then just return your response...
- # return resp # option 1
- # # ... or set req.response and return None.
- # req.response = resp # option 2
- #
- # See the end of http://pythonpaste.org/webob/modules/dec.html
- # for more info.
- raise NotImplementedError("You must implement __call__")
-
-
-class WsgiStack(WSGILayer):
- def __init__(self, wsgi_layers):
- bottom_up = list(reversed(wsgi_layers))
- app, remaining = bottom_up[0], bottom_up[1:]
- for layer in remaining:
- layer.application = app
- app = layer
- super(WsgiStack, self).__init__(app)
-
- @wsgify
- def __call__(self, req):
- return self.application
-
-class Debug(WSGILayer):
- @wsgify
- def __call__(self, req):
- for k, v in req.environ.items():
- print k, "=", v
- return self.application
-
-class Auth(WSGILayer):
- @wsgify
- def __call__(self, req):
- if not 'openstack.auth.token' in req.environ:
- # Check auth params here
- if True:
- req.environ['openstack.auth.token'] = '12345'
- else:
- return exc.HTTPForbidden(detail="Go away")
-
- response = req.get_response(self.application)
- response.headers['X-Openstack-Auth'] = 'Success'
- return response
-
-class Router(WSGILayer):
- def __init__(self, application=None):
- super(Router, self).__init__(application)
- self.map = routes.Mapper()
- self._connect()
-
- @wsgify
- def __call__(self, req):
- match = self.map.match(req.path_info)
- if match is None:
- return self.application
- req.environ['openstack.match'] = match
- return match['controller']
-
- def _connect(self):
- raise NotImplementedError("You must implement _connect")
-
-class FileRouter(Router):
- def _connect(self):
- self.map.connect(None, '/files/{file}', controller=File())
- self.map.connect(None, '/rfiles/{file}', controller=Reverse(File()))
-
-class Message(WSGILayer):
- @wsgify
- def __call__(self, req):
- return 'message\n'
-
-class Reverse(WSGILayer):
- @wsgify
- def __call__(self, req):
- inner_resp = req.get_response(self.application)
- resp = Response()
- resp.app_iter = itertools.imap(lambda x: x[::-1], inner_resp.app_iter)
- return resp
-
-class File(WSGILayer):
- @wsgify
- def __call__(self, req):
- try:
- myfile = open(req.environ['openstack.match']['file'])
- except IOError, e:
- raise exc.HTTPNotFound()
- req.response = Response()
- req.response.app_iter = myfile
-
-wsgi_layers = [
- Auth(),
- Debug(),
- FileRouter(),
- Message(),
- ]
-eventletserver.serve(app=WsgiStack(wsgi_layers), port=12345)
diff --git a/nova/endpoint/rackspace.py b/nova/endpoint/rackspace.py
index 7a3fbe141..f6735a260 100644
--- a/nova/endpoint/rackspace.py
+++ b/nova/endpoint/rackspace.py
@@ -17,206 +17,95 @@
# under the License.
"""
-Rackspace API
+Rackspace API Endpoint
"""
-import base64
import json
-import logging
-import multiprocessing
-import os
import time
-from nova import datastore
-from nova import exception
+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
+from nova.compute import model as compute
from nova.network import model as network
-from nova.endpoint import images
-from nova.endpoint import wsgi
FLAGS = flags.FLAGS
flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on')
-class Unauthorized(Exception):
- pass
-
-class NotFound(Exception):
- pass
-
-
-class Api(object):
+class API(wsgi.Middleware):
+ """Entry point for all requests."""
def __init__(self):
- """build endpoints here"""
- self.controllers = {
- "v1.0": RackspaceAuthenticationApi(),
- "servers": RackspaceCloudServerApi()
- }
-
- def handler(self, environ, responder):
- """
- This is the entrypoint from wsgi. Read PEP 333 and wsgi.org for
- more intormation. The key points are responder is a callback that
- needs to run before you return, and takes two arguments, response
- code string ("200 OK") and headers (["X-How-Cool-Am-I: Ultra-Suede"])
- and the return value is the body of the response.
- """
- environ['nova.context'] = self.build_context(environ)
- controller, path = wsgi.Util.route(
- environ['PATH_INFO'],
- self.controllers
- )
- logging.debug("Route %s to %s", str(path), str(controller))
- if not controller:
- responder("404 Not Found", [])
- return ""
- try:
- rv = controller.process(path, environ)
- if type(rv) is tuple:
- responder(rv[0], rv[1])
- rv = rv[2]
- else:
- responder("200 OK", [])
- return rv
- except Unauthorized:
- responder("401 Unauthorized", [])
- return ""
- except NotFound:
- responder("404 Not Found", [])
- return ""
-
-
- def build_context(self, env):
- rv = {}
- if env.has_key("HTTP_X_AUTH_TOKEN"):
- rv['user'] = manager.AuthManager().get_user_from_access_key(
- env['HTTP_X_AUTH_TOKEN']
- )
- if rv['user']:
- rv['project'] = manager.AuthManager().get_project(
- rv['user'].name
- )
- return rv
-
-
-class RackspaceApiEndpoint(object):
- def process(self, path, env):
- """
- Main entrypoint for all controllers (what gets run by the wsgi handler).
- Check authentication based on key, raise Unauthorized if invalid.
-
- Select the most appropriate action based on request type GET, POST, etc,
- then pass it through to the implementing controller. Defalut to GET if
- the implementing child doesn't respond to a particular type.
- """
- if not self.check_authentication(env):
- raise Unauthorized("Unable to authenticate")
-
- method = env['REQUEST_METHOD'].lower()
- callback = getattr(self, method, None)
- if not callback:
- callback = getattr(self, "get")
- logging.debug("%s processing %s with %s", self, method, callback)
- return callback(path, env)
-
- def get(self, path, env):
- """
- The default GET will look at the path and call an appropriate
- action within this controller based on the the structure of the path.
-
- Given the following path lengths (with the first part stripped of by
- router, as it is the controller name):
- = 0 -> index
- = 1 -> first component (/servers/details -> details)
- >= 2 -> second path component (/servers/ID/ips/* -> ips)
-
- This should return
- A String if 200 OK and no additional headers
- (CODE, HEADERS, BODY) for custom response code and headers
- """
- if len(path) == 0 and hasattr(self, "index"):
- logging.debug("%s running index", self)
- return self.index(env)
- if len(path) >= 2:
- action = path[1]
- else:
- action = path.pop(0)
-
- logging.debug("%s running action %s", self, action)
- if hasattr(self, action):
- method = getattr(self, action)
- return method(path, env)
- else:
- raise NotFound("Missing method %s" % path[0])
-
- def check_authentication(self, env):
- if not env['nova.context']['user']:
- return False
- return True
-
-
-class RackspaceAuthenticationApi(object):
-
- def process(self, path, env):
- return self.index(path, env)
-
- # TODO(todd): make a actual session with a unique token
- # just pass the auth key back through for now
- def index(self, _path, env):
- response = '204 No Content'
- headers = [
- ('X-Server-Management-Url', 'http://%s' % env['HTTP_HOST']),
- ('X-Storage-Url', 'http://%s' % env['HTTP_HOST']),
- ('X-CDN-Managment-Url', 'http://%s' % env['HTTP_HOST']),
- ('X-Auth-Token', env['HTTP_X_AUTH_KEY'])
- ]
- body = ""
- return (response, headers, body)
-
-
-class RackspaceCloudServerApi(RackspaceApiEndpoint):
+ 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):
- self.instdir = model.InstanceDirectory()
+ super(CloudServerAPI, self).__init__()
+ self.instdir = compute.InstanceDirectory()
self.network = network.PublicNetworkController()
- def post(self, path, env):
- if len(path) == 0:
- return self.launch_server(env)
-
- def delete(self, path_parts, env):
- if self.delete_server(path_parts[0]):
- return ("202 Accepted", [], "")
- else:
- return ("404 Not Found", [],
- "Did not find image, or it was not in a running state")
-
-
- def index(self, env):
- return self.detail(env)
-
- def detail(self, args, env):
+ @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 launch_server(self, env):
- data = json.loads(env['wsgi.input'].read(int(env['CONTENT_LENGTH'])))
- inst = self.build_server_instance(data, env['nova.context'])
- self.schedule_launch_of_instance(inst)
- return json.dumps({"server": self.instance_details(inst)})
-
- def instance_details(self, inst):
+ 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),
@@ -224,11 +113,9 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint):
"hostId": inst.get("node_name", None),
"status": inst.get("state", "pending"),
"addresses": {
- "public": [self.network.get_public_ip_for_instance(
- inst.get("instance_id", None)
- )],
- "private": [inst.get("private_dns_name", None)]
- },
+ "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"),
@@ -237,11 +124,22 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint):
"progress": "Not-Supported",
"metadata": {
"Server Label": "Not-Supported",
- "Image Version": "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()
@@ -253,45 +151,33 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint):
inst['reservation_id'] = reservation
inst['launch_time'] = ltime
inst['mac_address'] = utils.generate_mac()
- address = network.allocate_ip(
+ address = self.network.allocate_ip(
inst['user_id'],
inst['project_id'],
- mac=inst['mac_address']
- )
+ 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' # security group
- )['bridge_name']
+ 'default')['bridge_name']
# key_data, key_name, ami_launch_index
# TODO(todd): key data or root password
inst.save()
return inst
- def schedule_launch_of_instance(self, inst):
- rpc.cast(
- FLAGS.compute_topic,
- {
- "method": "run_instance",
- "args": {"instance_id": inst.instance_id}
- }
- )
-
- def delete_server(self, instance_id):
- owner_hostname = self.host_for_instance(instance_id)
- # it isn't launched?
+ @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 None
+ 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": instance_id}})
- return True
-
- def host_for_instance(self, instance_id):
- instance = model.Instance.lookup(instance_id)
- if not instance:
- return None
- return instance["node_name"]
-
+ "args": {"instance_id": route_args['server_id']}})
+ req.status = "202 Accepted"
diff --git a/nova/endpoint/wsgi.py b/nova/endpoint/wsgi.py
deleted file mode 100644
index b7bb588c3..000000000
--- a/nova/endpoint/wsgi.py
+++ /dev/null
@@ -1,40 +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.
-
-'''
-Utility methods for working with WSGI servers
-'''
-
-class Util(object):
-
- @staticmethod
- def route(reqstr, controllers):
- if len(reqstr) == 0:
- return Util.select_root_controller(controllers), []
- parts = [x for x in reqstr.split("/") if len(x) > 0]
- if len(parts) == 0:
- return Util.select_root_controller(controllers), []
- return controllers[parts[0]], parts[1:]
-
- @staticmethod
- def select_root_controller(controllers):
- if '' in controllers:
- return controllers['']
- else:
- return None
-
diff --git a/nova/wsgi.py b/nova/wsgi.py
new file mode 100644
index 000000000..4fd6e59e3
--- /dev/null
+++ b/nova/wsgi.py
@@ -0,0 +1,173 @@
+# 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 logging
+import sys
+
+import eventlet
+import eventlet.wsgi
+eventlet.patcher.monkey_patch(all=False, socket=True)
+import routes
+import routes.middleware
+
+
+logging.getLogger("routes.middleware").addHandler(logging.StreamHandler())
+
+
+def run_server(application, port):
+ """Run a WSGI server with the given application."""
+ sock = eventlet.listen(('0.0.0.0', port))
+ eventlet.wsgi.server(sock, application)
+
+
+class Application(object):
+ """Base WSGI application wrapper. Subclasses need to implement __call__."""
+
+ def __call__(self, environ, start_response):
+ r"""Subclasses will probably want to implement __call__ like this:
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ # Any of the following objects work as responses:
+
+ # Option 1: simple string
+ res = 'message\n'
+
+ # Option 2: a nicely formatted HTTP exception page
+ res = exc.HTTPForbidden(detail='Nice try')
+
+ # Option 3: a webob Response object (in case you need to play with
+ # headers, or you want to be treated like an iterable, or or or)
+ res = Response();
+ res.app_iter = open('somefile')
+
+ # Option 4: any wsgi app to be run next
+ res = self.application
+
+ # Option 5: you can get a Response object for a wsgi app, too, to
+ # play with headers etc
+ res = req.get_response(self.application)
+
+ # You can then just return your response...
+ return res
+ # ... or set req.response and return None.
+ req.response = res
+
+ See the end of http://pythonpaste.org/webob/modules/dec.html
+ for more info.
+ """
+ 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."""
+
+ def __init__(self, application): # pylint: disable-msg=W0231
+ self.application = application
+
+
+class Debug(Middleware):
+ """Helper class that can be insertd 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():
+ print key, "=", value
+ print
+ wrapper = debug_start_response(start_response)
+ return debug_print_body(self.application(environ, wrapper))
+
+
+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 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."""
+
+ class Wrapper(object):
+ """Iterate through all the body parts and print before returning."""
+
+ def __iter__(self):
+ for part in body:
+ sys.stdout.write(part)
+ sys.stdout.flush()
+ yield part
+ print
+
+ return Wrapper()
+
+
+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 Router(Middleware): # pylint: disable-msg=R0921
+ """Wrapper to help setup routes.middleware.RoutesMiddleware."""
+
+ 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 __call__(self, environ, start_response):
+ return self.application(environ, start_response)
+
+ def _build_map(self):
+ """Method to create new connections for the routing map."""
+ raise NotImplementedError("You must implement _build_map")
+
+ 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