From 0e13442d2cd83b62361b9c611d987bdcb099cf25 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 26 Jul 2010 23:19:51 -0400 Subject: [PATCH 1/3] Share my updates to the Rackspace API. --- bin/nova-rsapi | 13 +--- exercise_rsapi.py | 51 ------------- nova/endpoint/rackspace.py | 147 +++++++++++++++++++++++++++---------- 3 files changed, 109 insertions(+), 102 deletions(-) delete mode 100644 exercise_rsapi.py diff --git a/bin/nova-rsapi b/bin/nova-rsapi index c2f2c9d7..a529fc66 100755 --- a/bin/nova-rsapi +++ b/bin/nova-rsapi @@ -37,23 +37,12 @@ FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') def main(_argv): - user_manager = users.UserManager() - api_instance = rackspace.Api(user_manager) - conn = rpc.Connection.instance() - rpc_consumer = rpc.AdapterConsumer(connection=conn, - topic=FLAGS.cloud_topic, - proxy=api_instance) - -# TODO: fire rpc response listener (without attach to tornado) -# io_inst = ioloop.IOLoop.instance() -# _injected = consumer.attach_to_tornado(io_inst) - + 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() -# io_inst.start() if __name__ == '__main__': utils.default_flagfile() diff --git a/exercise_rsapi.py b/exercise_rsapi.py deleted file mode 100644 index 20589b9c..00000000 --- a/exercise_rsapi.py +++ /dev/null @@ -1,51 +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. - -import cloudservers - -class IdFake: - def __init__(self, id): - self.id = id - -# to get your access key: -# from nova.auth import users -# users.UserManger.instance().get_users()[0].access -rscloud = cloudservers.CloudServers( - 'admin', - '6cca875e-5ab3-4c60-9852-abf5c5c60cc6' - ) -rscloud.client.AUTH_URL = 'http://localhost:8773/v1.0' - - -rv = rscloud.servers.list() -print "SERVERS: %s" % rv - -if len(rv) == 0: - server = rscloud.servers.create( - "test-server", - IdFake("ami-tiny"), - IdFake("m1.tiny") - ) - print "LAUNCH: %s" % server -else: - server = rv[0] - print "Server to kill: %s" % server - -raw_input("press enter key to kill the server") - -server.delete() diff --git a/nova/endpoint/rackspace.py b/nova/endpoint/rackspace.py index 29a077b2..b561212f 100644 --- a/nova/endpoint/rackspace.py +++ b/nova/endpoint/rackspace.py @@ -48,82 +48,128 @@ FLAGS = flags.FLAGS flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') -# TODO(todd): subclass Exception so we can bubble meaningful errors +class Unauthorized(Exception): + pass + +class NotFound(Exception): + pass class Api(object): - def __init__(self, rpc_mechanism): + def __init__(self): + """build endpoints here""" self.controllers = { "v1.0": RackspaceAuthenticationApi(), "servers": RackspaceCloudServerApi() } - self.rpc_mechanism = rpc_mechanism 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: - # TODO(todd): Exception (404) - raise Exception("Missing Controller") - rv = controller.process(path, environ) - if type(rv) is tuple: - responder(rv[0], rv[1]) - rv = rv[2] - else: - responder("200 OK", []) - return rv + 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"): + # TODO(todd): once we make an actual unique token, this will change rv['user'] = users.UserManager.instance().get_user_from_access_key( - env['HTTP_X_AUTH_TOKEN'] - ) + env['HTTP_X_AUTH_TOKEN']) if rv['user']: rv['project'] = users.UserManager.instance().get_project( - rv['user'].name - ) + 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): - # TODO(todd): Exception (Unauthorized) - raise Exception("Unable to authenticate") + raise Unauthorized("Unable to authenticate") - if len(path) == 0: + 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) - 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: - # TODO(todd): Exception (404) - raise Exception("Missing method %s" % path[0]) + raise NotFound("Missing method %s" % path[0]) def check_authentication(self, env): - if hasattr(self, "process_without_authentication") \ - and getattr(self, "process_without_authentication"): - return True if not env['nova.context']['user']: return False return True -class RackspaceAuthenticationApi(RackspaceApiEndpoint): - - def __init__(self): - self.process_without_authentication = True +class RackspaceAuthenticationApi(object): # TODO(todd): make a actual session with a unique token # just pass the auth key back through for now - def index(self, env): + def index(self, _path, env): response = '204 No Content' headers = [ ('X-Server-Management-Url', 'http://%s' % env['HTTP_HOST']), @@ -141,20 +187,25 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint): self.instdir = model.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): - if env['REQUEST_METHOD'] == 'GET': - return self.detail(env) - elif env['REQUEST_METHOD'] == 'POST': - return self.launch_server(env) + return self.detail(env) def detail(self, args, env): - value = { - "servers": - [] - } + value = {"servers": []} for inst in self.instdir.all: value["servers"].append(self.instance_details(inst)) - return json.dumps(value) ## @@ -227,3 +278,21 @@ class RackspaceCloudServerApi(RackspaceApiEndpoint): "args": {"instance_id": inst.instance_id} } ) + + def delete_server(self, instance_id): + owner_hostname = self.host_for_instance(instance_id) + # it isn't launched? + if not owner_hostname: + return None + 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"] + From 1d5e89286da838c3164b994319268b7744448f9f Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 5 Aug 2010 22:54:08 -0400 Subject: [PATCH 2/3] Use webob to simplify wsgi middleware --- nova/endpoint/new_wsgi.py | 122 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 nova/endpoint/new_wsgi.py diff --git a/nova/endpoint/new_wsgi.py b/nova/endpoint/new_wsgi.py new file mode 100644 index 00000000..c7eee20f --- /dev/null +++ b/nova/endpoint/new_wsgi.py @@ -0,0 +1,122 @@ +import eventlet +import eventlet.wsgi +eventlet.patcher.monkey_patch(all=False, socket=True) +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 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) + print "+" * 80 + Debug()(req) + print "*" * 80 + 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 + +sock = eventlet.listen(('localhost', 12345)) +eventlet.wsgi.server(sock, Debug(Auth(FileRouter(Message())))) From f193f7b95f51031cc3a1100d13a0750d411f5bdf Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 5 Aug 2010 23:35:16 -0400 Subject: [PATCH 3/3] WsgiStack class, eventletserver.serve. Trying to work toward a simple API that anyone can use to start an eventlet-based server composed of several WSGI apps. --- nova/endpoint/eventletserver.py | 7 +++++++ nova/endpoint/new_wsgi.py | 30 ++++++++++++++++++++++-------- nova/endpoint/rackspace.py | 2 -- 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 nova/endpoint/eventletserver.py diff --git a/nova/endpoint/eventletserver.py b/nova/endpoint/eventletserver.py new file mode 100644 index 00000000..b8c15ff5 --- /dev/null +++ b/nova/endpoint/eventletserver.py @@ -0,0 +1,7 @@ +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 index c7eee20f..0f096ddb 100644 --- a/nova/endpoint/new_wsgi.py +++ b/nova/endpoint/new_wsgi.py @@ -1,11 +1,10 @@ -import eventlet -import eventlet.wsgi -eventlet.patcher.monkey_patch(all=False, socket=True) +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 @@ -49,6 +48,19 @@ class WSGILayer(object): 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): @@ -101,9 +113,6 @@ class Reverse(WSGILayer): @wsgify def __call__(self, req): inner_resp = req.get_response(self.application) - print "+" * 80 - Debug()(req) - print "*" * 80 resp = Response() resp.app_iter = itertools.imap(lambda x: x[::-1], inner_resp.app_iter) return resp @@ -118,5 +127,10 @@ class File(WSGILayer): req.response = Response() req.response.app_iter = myfile -sock = eventlet.listen(('localhost', 12345)) -eventlet.wsgi.server(sock, Debug(Auth(FileRouter(Message())))) +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 870aa062..323032eb 100644 --- a/nova/endpoint/rackspace.py +++ b/nova/endpoint/rackspace.py @@ -26,8 +26,6 @@ import logging import multiprocessing import os import time -import tornado.web -from twisted.internet import defer from nova import datastore from nova import exception