diff --git a/bin/nova-rsapi b/bin/nova-rsapi index a17efccc..026880d5 100755 --- a/bin/nova-rsapi +++ b/bin/nova-rsapi @@ -1,4 +1,5 @@ #!/usr/bin/env python +# pylint: disable-msg=C0103 # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright 2010 United States Government as represented by the @@ -17,31 +18,17 @@ # See the License for the specific language governing permissions and # limitations under the License. """ - WSGI daemon for the main API endpoint. + Daemon for the Rackspace API endpoint. """ -import logging -from tornado import ioloop -from wsgiref import simple_server - from nova import flags -from nova import rpc -from nova import server from nova import utils -from nova.auth import manager +from nova import wsgi from nova.endpoint import rackspace FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') -def main(_argv): - api_instance = rackspace.Api() - http_server = simple_server.WSGIServer(('0.0.0.0', FLAGS.cc_port), simple_server.WSGIRequestHandler) - http_server.set_app(api_instance.handler) - logging.debug('Started HTTP server on port %i' % FLAGS.cc_port) - while True: - http_server.handle_request() - if __name__ == '__main__': utils.default_flagfile() - server.serve('nova-rsapi', main) + wsgi.run_server(rackspace.API(), FLAGS.cc_port) diff --git a/nova/endpoint/eventletserver.py b/nova/endpoint/eventletserver.py deleted file mode 100644 index b8c15ff5..00000000 --- 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 0f096ddb..00000000 --- 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 7a3fbe14..f6735a26 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() - } + super(API, self).__init__(Router(webob.exc.HTTPNotFound())) - 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 __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) - 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 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 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. +class AuthenticationAPI(wsgi.Application): + """Handle all authorization requests through WSGI applications.""" - 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 + @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 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): +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 b7bb588c..00000000 --- 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 -