From 2f70e6af26b206274182a31abec3a7de599e1e98 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Sat, 19 Jun 2010 22:05:28 -0400 Subject: [PATCH 1/5] Commit what I have almost working before diverging. --- bin/nova-rsapi | 65 +++++++++ nova/endpoint/rackspace.py | 268 +++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100755 bin/nova-rsapi create mode 100644 nova/endpoint/rackspace.py diff --git a/bin/nova-rsapi b/bin/nova-rsapi new file mode 100755 index 00000000..cd2f285c --- /dev/null +++ b/bin/nova-rsapi @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright [2010] [Anso Labs, LLC] +# +# 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. +""" + WSGI daemon for the main API endpoint. +""" + +import logging + +from nova import vendor +from tornado import httpserver +import tornado.web +from tornado import ioloop + +from nova import flags +from nova import rpc +from nova import server +from nova import utils +from nova.auth import users +from nova.endpoint import rackspace +from nova.endpoint import wsgi_wrapper +FLAGS = flags.FLAGS +flags.DEFINE_integer('cc_port', 8773, 'api server port') + +def application(_args): + """ Wrap WSGI application as tornado """ + wsgi_wrapper.Wrap(rackspace.Api) + +def main(_argv): + user_manager = users.UserManager() + controllers = { + 'Servers': rackspace.ServersController() + } + _app = rackspace.RackspaceAPIServerApplication(user_manager, controllers) + + conn = rpc.Connection.instance() + consumer = rpc.AdapterConsumer(connection=conn, + topic=FLAGS.cloud_topic, + proxy=controllers['Servers']) + + io_inst = ioloop.IOLoop.instance() + _injected = consumer.attach_to_tornado(io_inst) + + http_server = httpserver.HTTPServer(_app) + http_server.listen(FLAGS.cc_port) + logging.debug('Started HTTP server on %s', FLAGS.cc_port) + io_inst.start() + + +if __name__ == '__main__': + utils.default_flagfile() + server.serve('nova-rsapi', main) diff --git a/nova/endpoint/rackspace.py b/nova/endpoint/rackspace.py new file mode 100644 index 00000000..c4828565 --- /dev/null +++ b/nova/endpoint/rackspace.py @@ -0,0 +1,268 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright [2010] [Anso Labs, LLC] +# +# 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 +""" + +import base64 +import json +import logging +import multiprocessing +import os +import time + +from nova import vendor +import tornado.web +from twisted.internet import defer + +from nova import datastore +from nova import flags +from nova import rpc +from nova import utils +from nova import exception +from nova.auth import users +from nova.compute import model +from nova.compute import network +from nova.endpoint import images +from nova.volume import storage + +FLAGS = flags.FLAGS + +flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') + +def _gen_key(user_id, key_name): + """ Tuck this into UserManager """ + try: + manager = users.UserManager.instance() + private_key, fingerprint = manager.generate_key_pair(user_id, key_name) + except Exception as ex: + return {'exception': ex} + return {'private_key': private_key, 'fingerprint': fingerprint} + + +class ServersController(object): + """ ServersController provides the critical dispatch between + inbound API calls through the endpoint and messages + sent to the other nodes. +""" + def __init__(self): + self.instdir = model.InstanceDirectory() + self.network = network.PublicNetworkController() + self.setup() + + @property + def instances(self): + """ All instances in the system, as dicts """ + for instance in self.instdir.all: + yield {instance['instance_id']: instance} + + @property + def volumes(self): + """ returns a list of all volumes """ + for volume_id in datastore.Redis.instance().smembers("volumes"): + volume = storage.Volume(volume_id=volume_id) + yield volume + + def __str__(self): + return 'ServersController' + + def setup(self): + """ Ensure the keychains and folders exist. """ + # Create keys folder, if it doesn't exist + if not os.path.exists(FLAGS.keys_path): + os.makedirs(os.path.abspath(FLAGS.keys_path)) + # Gen root CA, if we don't have one + root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) + if not os.path.exists(root_ca_path): + start = os.getcwd() + os.chdir(FLAGS.ca_path) + utils.runthis("Generating root CA: %s", "sh genrootca.sh") + os.chdir(start) + # TODO: Do this with M2Crypto instead + + def get_instance_by_ip(self, ip): + return self.instdir.by_ip(ip) + + def get_metadata(self, ip): + i = self.get_instance_by_ip(ip) + if i is None: + return None + if i['key_name']: + keys = { + '0': { + '_name': i['key_name'], + 'openssh-key': i['key_data'] + } + } + else: + keys = '' + data = { + 'user-data': base64.b64decode(i['user_data']), + 'meta-data': { + 'ami-id': i['image_id'], + 'ami-launch-index': i['ami_launch_index'], + 'ami-manifest-path': 'FIXME', # image property + 'block-device-mapping': { # TODO: replace with real data + 'ami': 'sda1', + 'ephemeral0': 'sda2', + 'root': '/dev/sda1', + 'swap': 'sda3' + }, + 'hostname': i['private_dns_name'], # is this public sometimes? + 'instance-action': 'none', + 'instance-id': i['instance_id'], + 'instance-type': i.get('instance_type', ''), + 'local-hostname': i['private_dns_name'], + 'local-ipv4': i['private_dns_name'], # TODO: switch to IP + 'kernel-id': i.get('kernel_id', ''), + 'placement': { + 'availaibility-zone': i.get('availability_zone', 'nova'), + }, + 'public-hostname': i.get('dns_name', ''), + 'public-ipv4': i.get('dns_name', ''), # TODO: switch to IP + 'public-keys' : keys, + 'ramdisk-id': i.get('ramdisk_id', ''), + 'reservation-id': i['reservation_id'], + 'security-groups': i.get('groups', '') + } + } + if False: # TODO: store ancestor ids + data['ancestor-ami-ids'] = [] + if i.get('product_codes', None): + data['product-codes'] = i['product_codes'] + return data + + def update_state(self, topic, value): + """ accepts status reports from the queue and consolidates them """ + # TODO(jmc): if an instance has disappeared from + # the node, call instance_death + if topic == "instances": + return defer.succeed(True) + aggregate_state = getattr(self, topic) + node_name = value.keys()[0] + items = value[node_name] + logging.debug("Updating %s state for %s" % (topic, node_name)) + for item_id in items.keys(): + if (aggregate_state.has_key('pending') and + aggregate_state['pending'].has_key(item_id)): + del aggregate_state['pending'][item_id] + aggregate_state[node_name] = items + return defer.succeed(True) + +class RackspaceAPIServerApplication(tornado.web.Application): + def __init__(self, user_manager, controllers): + tornado.web.Application.__init__(self, [ + (r'/servers/?(.*)', RackspaceServerRequestHandler), + ], pool=multiprocessing.Pool(4)) + self.user_manager = user_manager + self.controllers = controllers + +class RackspaceServerRequestHandler(tornado.web.RequestHandler): + + def get(self, controller_name): + self.execute(controller_name) + + @tornado.web.asynchronous + def execute(self, controller_name): + controller = self.application.controllers['Servers'] + + # Obtain the appropriate controller for this request. +# try: +# controller = self.application.controllers[controller_name] +# except KeyError: +# self._error('unhandled', 'no controller named %s' % controller_name) +# return +# + args = self.request.arguments + logging.error("ARGS: %s" % args) + + # Read request signature. + try: + signature = args.pop('Signature')[0] + except: + raise tornado.web.HTTPError(400) + + # Make a copy of args for authentication and signature verification. + auth_params = {} + for key, value in args.items(): + auth_params[key] = value[0] + + # Get requested action and remove authentication args for final request. + try: + action = args.pop('Action')[0] + access = args.pop('AWSAccessKeyId')[0] + args.pop('SignatureMethod') + args.pop('SignatureVersion') + args.pop('Version') + args.pop('Timestamp') + except: + raise tornado.web.HTTPError(400) + + # Authenticate the request. + try: + (user, project) = users.UserManager.instance().authenticate( + access, + signature, + auth_params, + self.request.method, + self.request.host, + self.request.path + ) + + except exception.Error, ex: + logging.debug("Authentication Failure: %s" % ex) + raise tornado.web.HTTPError(403) + + _log.debug('action: %s' % action) + + for key, value in args.items(): + _log.debug('arg: %s\t\tval: %s' % (key, value)) + + request = APIRequest(controller, action) + context = APIRequestContext(self, user, project) + d = request.send(context, **args) + # d.addCallback(utils.debug) + + # TODO: Wrap response in AWS XML format + d.addCallbacks(self._write_callback, self._error_callback) + + def _write_callback(self, data): + self.set_header('Content-Type', 'text/xml') + self.write(data) + self.finish() + + def _error_callback(self, failure): + try: + failure.raiseException() + except exception.ApiError as ex: + self._error(type(ex).__name__ + "." + ex.code, ex.message) + # TODO(vish): do something more useful with unknown exceptions + except Exception as ex: + self._error(type(ex).__name__, str(ex)) + raise + + def post(self, controller_name): + self.execute(controller_name) + + def _error(self, code, message): + self._status_code = 400 + self.set_header('Content-Type', 'text/xml') + self.write('\n') + self.write('%s' + '%s' + '?' % (code, message)) + self.finish() + From 62e69c356b80d3a5f883428dcd8597591efd50a8 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Sun, 20 Jun 2010 20:18:56 -0400 Subject: [PATCH 2/5] git checkpoint commit post-wsgi --- bin/nova-rsapi | 37 +++--- nova/endpoint/rackspace.py | 247 +++++++------------------------------ nova/endpoint/wsgi.py | 23 ++++ 3 files changed, 83 insertions(+), 224 deletions(-) create mode 100644 nova/endpoint/wsgi.py diff --git a/bin/nova-rsapi b/bin/nova-rsapi index cd2f285c..24b469f7 100755 --- a/bin/nova-rsapi +++ b/bin/nova-rsapi @@ -19,10 +19,9 @@ """ import logging +from wsgiref import simple_server from nova import vendor -from tornado import httpserver -import tornado.web from tornado import ioloop from nova import flags @@ -31,34 +30,28 @@ from nova import server from nova import utils from nova.auth import users from nova.endpoint import rackspace -from nova.endpoint import wsgi_wrapper -FLAGS = flags.FLAGS -flags.DEFINE_integer('cc_port', 8773, 'api server port') -def application(_args): - """ Wrap WSGI application as tornado """ - wsgi_wrapper.Wrap(rackspace.Api) +FLAGS = flags.FLAGS +flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') def main(_argv): user_manager = users.UserManager() - controllers = { - 'Servers': rackspace.ServersController() - } - _app = rackspace.RackspaceAPIServerApplication(user_manager, controllers) - + api_instance = rackspace.Api(user_manager) conn = rpc.Connection.instance() - consumer = rpc.AdapterConsumer(connection=conn, + rpc_consumer = rpc.AdapterConsumer(connection=conn, topic=FLAGS.cloud_topic, - proxy=controllers['Servers']) + proxy=api_instance) - io_inst = ioloop.IOLoop.instance() - _injected = consumer.attach_to_tornado(io_inst) - - http_server = httpserver.HTTPServer(_app) - http_server.listen(FLAGS.cc_port) - logging.debug('Started HTTP server on %s', FLAGS.cc_port) - io_inst.start() +# TODO: fire rpc response listener (without attach to tornado) +# io_inst = ioloop.IOLoop.instance() +# _injected = consumer.attach_to_tornado(io_inst) + 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/nova/endpoint/rackspace.py b/nova/endpoint/rackspace.py index c4828565..7438e3bd 100644 --- a/nova/endpoint/rackspace.py +++ b/nova/endpoint/rackspace.py @@ -36,6 +36,7 @@ from nova import exception from nova.auth import users from nova.compute import model from nova.compute import network +from nova.endpoint import wsgi from nova.endpoint import images from nova.volume import storage @@ -53,216 +54,58 @@ def _gen_key(user_id, key_name): return {'private_key': private_key, 'fingerprint': fingerprint} -class ServersController(object): - """ ServersController provides the critical dispatch between - inbound API calls through the endpoint and messages - sent to the other nodes. -""" - def __init__(self): - self.instdir = model.InstanceDirectory() - self.network = network.PublicNetworkController() - self.setup() +class Api(object): - @property - def instances(self): - """ All instances in the system, as dicts """ - for instance in self.instdir.all: - yield {instance['instance_id']: instance} - - @property - def volumes(self): - """ returns a list of all volumes """ - for volume_id in datastore.Redis.instance().smembers("volumes"): - volume = storage.Volume(volume_id=volume_id) - yield volume - - def __str__(self): - return 'ServersController' - - def setup(self): - """ Ensure the keychains and folders exist. """ - # Create keys folder, if it doesn't exist - if not os.path.exists(FLAGS.keys_path): - os.makedirs(os.path.abspath(FLAGS.keys_path)) - # Gen root CA, if we don't have one - root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file) - if not os.path.exists(root_ca_path): - start = os.getcwd() - os.chdir(FLAGS.ca_path) - utils.runthis("Generating root CA: %s", "sh genrootca.sh") - os.chdir(start) - # TODO: Do this with M2Crypto instead - - def get_instance_by_ip(self, ip): - return self.instdir.by_ip(ip) - - def get_metadata(self, ip): - i = self.get_instance_by_ip(ip) - if i is None: - return None - if i['key_name']: - keys = { - '0': { - '_name': i['key_name'], - 'openssh-key': i['key_data'] - } - } - else: - keys = '' - data = { - 'user-data': base64.b64decode(i['user_data']), - 'meta-data': { - 'ami-id': i['image_id'], - 'ami-launch-index': i['ami_launch_index'], - 'ami-manifest-path': 'FIXME', # image property - 'block-device-mapping': { # TODO: replace with real data - 'ami': 'sda1', - 'ephemeral0': 'sda2', - 'root': '/dev/sda1', - 'swap': 'sda3' - }, - 'hostname': i['private_dns_name'], # is this public sometimes? - 'instance-action': 'none', - 'instance-id': i['instance_id'], - 'instance-type': i.get('instance_type', ''), - 'local-hostname': i['private_dns_name'], - 'local-ipv4': i['private_dns_name'], # TODO: switch to IP - 'kernel-id': i.get('kernel_id', ''), - 'placement': { - 'availaibility-zone': i.get('availability_zone', 'nova'), - }, - 'public-hostname': i.get('dns_name', ''), - 'public-ipv4': i.get('dns_name', ''), # TODO: switch to IP - 'public-keys' : keys, - 'ramdisk-id': i.get('ramdisk_id', ''), - 'reservation-id': i['reservation_id'], - 'security-groups': i.get('groups', '') - } + def __init__(self, rpc_mechanism): + self.controllers = { + "v1.0": RackspaceAuthenticationApi(), + "server": RackspaceCloudServerApi() } - if False: # TODO: store ancestor ids - data['ancestor-ami-ids'] = [] - if i.get('product_codes', None): - data['product-codes'] = i['product_codes'] - return data + self.rpc_mechanism = rpc_mechanism - def update_state(self, topic, value): - """ accepts status reports from the queue and consolidates them """ - # TODO(jmc): if an instance has disappeared from - # the node, call instance_death - if topic == "instances": - return defer.succeed(True) - aggregate_state = getattr(self, topic) - node_name = value.keys()[0] - items = value[node_name] - logging.debug("Updating %s state for %s" % (topic, node_name)) - for item_id in items.keys(): - if (aggregate_state.has_key('pending') and - aggregate_state['pending'].has_key(item_id)): - del aggregate_state['pending'][item_id] - aggregate_state[node_name] = items - return defer.succeed(True) + def handler(self, environ, responder): + logging.error("*** %s" % environ) + controller, path = wsgi.Util.route(environ['PATH_INFO'], self.controllers) + if not controller: + 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 -class RackspaceAPIServerApplication(tornado.web.Application): - def __init__(self, user_manager, controllers): - tornado.web.Application.__init__(self, [ - (r'/servers/?(.*)', RackspaceServerRequestHandler), - ], pool=multiprocessing.Pool(4)) - self.user_manager = user_manager - self.controllers = controllers +class RackspaceApiEndpoint(object): + def process(self, path, env): + if len(path) == 0: + return self.index(env) -class RackspaceServerRequestHandler(tornado.web.RequestHandler): + action = path.pop(0) + if hasattr(self, action): + method = getattr(self, action) + return method(path, env) + else: + raise Exception("Missing method %s" % path[0]) - def get(self, controller_name): - self.execute(controller_name) - @tornado.web.asynchronous - def execute(self, controller_name): - controller = self.application.controllers['Servers'] +class RackspaceAuthenticationApi(RackspaceApiEndpoint): - # Obtain the appropriate controller for this request. -# try: -# controller = self.application.controllers[controller_name] -# except KeyError: -# self._error('unhandled', 'no controller named %s' % controller_name) -# return -# - args = self.request.arguments - logging.error("ARGS: %s" % args) + def index(self, env): + response = '204 No Content' + headers = [ + ('X-Server-Management-Url', 'http://localhost:8773/server'), + ('X-Storage-Url', 'http://localhost:8773/server'), + ('X-CDN-Managment-Url', 'http://localhost:8773/server'), + ] + body = "" + return (response, headers, body) + - # Read request signature. - try: - signature = args.pop('Signature')[0] - except: - raise tornado.web.HTTPError(400) +class RackspaceCloudServerApi(object): - # Make a copy of args for authentication and signature verification. - auth_params = {} - for key, value in args.items(): - auth_params[key] = value[0] - - # Get requested action and remove authentication args for final request. - try: - action = args.pop('Action')[0] - access = args.pop('AWSAccessKeyId')[0] - args.pop('SignatureMethod') - args.pop('SignatureVersion') - args.pop('Version') - args.pop('Timestamp') - except: - raise tornado.web.HTTPError(400) - - # Authenticate the request. - try: - (user, project) = users.UserManager.instance().authenticate( - access, - signature, - auth_params, - self.request.method, - self.request.host, - self.request.path - ) - - except exception.Error, ex: - logging.debug("Authentication Failure: %s" % ex) - raise tornado.web.HTTPError(403) - - _log.debug('action: %s' % action) - - for key, value in args.items(): - _log.debug('arg: %s\t\tval: %s' % (key, value)) - - request = APIRequest(controller, action) - context = APIRequestContext(self, user, project) - d = request.send(context, **args) - # d.addCallback(utils.debug) - - # TODO: Wrap response in AWS XML format - d.addCallbacks(self._write_callback, self._error_callback) - - def _write_callback(self, data): - self.set_header('Content-Type', 'text/xml') - self.write(data) - self.finish() - - def _error_callback(self, failure): - try: - failure.raiseException() - except exception.ApiError as ex: - self._error(type(ex).__name__ + "." + ex.code, ex.message) - # TODO(vish): do something more useful with unknown exceptions - except Exception as ex: - self._error(type(ex).__name__, str(ex)) - raise - - def post(self, controller_name): - self.execute(controller_name) - - def _error(self, code, message): - self._status_code = 400 - self.set_header('Content-Type', 'text/xml') - self.write('\n') - self.write('%s' - '%s' - '?' % (code, message)) - self.finish() + def index(self): + return "IDX" + def list(self, args): + return "%s" % args diff --git a/nova/endpoint/wsgi.py b/nova/endpoint/wsgi.py new file mode 100644 index 00000000..e0817fd0 --- /dev/null +++ b/nova/endpoint/wsgi.py @@ -0,0 +1,23 @@ + +''' +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 + From 897782eea966bd2258686b494eb87e2027ff8b30 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Mon, 21 Jun 2010 12:44:39 -0400 Subject: [PATCH 3/5] More rackspace API. --- bin/nova-compute | 2 +- exercise_rsapi.py | 33 ++++++++ nova/endpoint/rackspace.py | 157 ++++++++++++++++++++++++++++++++----- 3 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 exercise_rsapi.py diff --git a/bin/nova-compute b/bin/nova-compute index bd3648d2..aa90f2c3 100755 --- a/bin/nova-compute +++ b/bin/nova-compute @@ -55,7 +55,7 @@ logging.getLogger().setLevel(logging.DEBUG) def main(): logging.warn('Starting compute node') - n = node.NetworkNode() + n = node.Node() d = n.adopt_instances() d.addCallback(lambda x: logging.info('Adopted %d instances', x)) diff --git a/exercise_rsapi.py b/exercise_rsapi.py new file mode 100644 index 00000000..4b9e65fc --- /dev/null +++ b/exercise_rsapi.py @@ -0,0 +1,33 @@ +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 7438e3bd..ac32d20a 100644 --- a/nova/endpoint/rackspace.py +++ b/nova/endpoint/rackspace.py @@ -40,18 +40,12 @@ from nova.endpoint import wsgi from nova.endpoint import images from nova.volume import storage -FLAGS = flags.FLAGS +FLAGS = flags.FLAGS flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on') -def _gen_key(user_id, key_name): - """ Tuck this into UserManager """ - try: - manager = users.UserManager.instance() - private_key, fingerprint = manager.generate_key_pair(user_id, key_name) - except Exception as ex: - return {'exception': ex} - return {'private_key': private_key, 'fingerprint': fingerprint} + +# TODO(todd): subclass Exception so we can bubble meaningful errors class Api(object): @@ -59,14 +53,18 @@ class Api(object): def __init__(self, rpc_mechanism): self.controllers = { "v1.0": RackspaceAuthenticationApi(), - "server": RackspaceCloudServerApi() + "servers": RackspaceCloudServerApi() } self.rpc_mechanism = rpc_mechanism def handler(self, environ, responder): - logging.error("*** %s" % environ) - controller, path = wsgi.Util.route(environ['PATH_INFO'], self.controllers) + environ['nova.context'] = self.build_context(environ) + controller, path = wsgi.Util.route( + environ['PATH_INFO'], + self.controllers + ) if not controller: + # TODO(todd): Exception (404) raise Exception("Missing Controller") rv = controller.process(path, environ) if type(rv) is tuple: @@ -76,8 +74,25 @@ class Api(object): responder("200 OK", []) return rv + def build_context(self, env): + rv = {} + if env.has_key("HTTP_X_AUTH_TOKEN"): + rv['user'] = users.UserManager.instance().get_user_from_access_key( + env['HTTP_X_AUTH_TOKEN'] + ) + if rv['user']: + rv['project'] = users.UserManager.instance().get_project( + rv['user'].name + ) + return rv + + class RackspaceApiEndpoint(object): def process(self, path, env): + if not self.check_authentication(env): + # TODO(todd): Exception (Unauthorized) + raise Exception("Unable to authenticate") + if len(path) == 0: return self.index(env) @@ -86,26 +101,126 @@ class RackspaceApiEndpoint(object): method = getattr(self, action) return method(path, env) else: + # TODO(todd): Exception (404) raise Exception("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 + + # TODO(todd): make a actual session with a unique token + # just pass the auth key back through for now def index(self, env): response = '204 No Content' headers = [ - ('X-Server-Management-Url', 'http://localhost:8773/server'), - ('X-Storage-Url', 'http://localhost:8773/server'), - ('X-CDN-Managment-Url', 'http://localhost:8773/server'), + ('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(object): - def index(self): - return "IDX" +class RackspaceCloudServerApi(RackspaceApiEndpoint): - def list(self, args): - return "%s" % args + def __init__(self): + self.instdir = model.InstanceDirectory() + self.network = network.PublicNetworkController() + + def index(self, env): + if env['REQUEST_METHOD'] == 'GET': + return self.detail(env) + elif env['REQUEST_METHOD'] == 'POST': + return self.launch_server(env) + + def detail(self, args, env): + 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): + 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": [self.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" + } + } + + def build_server_instance(self, env, context): + 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 = 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' # security group + )['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} + } + ) From f12f933de8ee55ebd2584eda00f8f8fd5809fbac Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Wed, 14 Jul 2010 21:26:10 -0500 Subject: [PATCH 4/5] work on importing images --- nova/endpoint/cloud.py | 21 +++++++++++++++------ nova/tests/objectstore_unittest.py | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 9dccc24d..32c7cbce 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -514,6 +514,18 @@ class CloudController(object): # vpn image is private so it doesn't show up on lists if kwargs['image_id'] != FLAGS.vpn_image_id: image = self._get_image(context, kwargs['image_id']) + + # FIXME(ja): if image is cloudpipe, this breaks + + # get defaults from imagestore + image_id = image['imageId'] + kernel_id = image.get('kernelId', None) + ramdisk_id = image.get('ramdiskId', None) + + # API parameters overrides of defaults + kernel_id = kwargs.get('kernel_id', kernel_id) + ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id) + logging.debug("Going to run instances...") reservation_id = utils.generate_uid('r') launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) @@ -534,12 +546,9 @@ class CloudController(object): bridge_name = net['bridge_name'] for num in range(int(kwargs['max_count'])): inst = self.instdir.new() - # TODO(ja): add ari, aki - inst['image_id'] = kwargs['image_id'] - if 'kernel_id' in kwargs: - inst['kernel_id'] = kwargs['kernel_id'] - if 'ramdisk_id' in kwargs: - inst['ramdisk_id'] = kwargs['ramdisk_id'] + inst['image_id'] = image_id + inst['kernel_id'] = kernel_id + inst['ramdisk_id'] = ramdisk_id inst['user_data'] = kwargs.get('user_data', '') inst['instance_type'] = kwargs.get('instance_type', 'm1.small') inst['reservation_id'] = reservation_id diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index cee567c8..ddd455a7 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -155,7 +155,7 @@ class ObjectStoreTestCase(test.BaseTestCase): bucket[os.path.basename(path)] = open(path, 'rb').read() # register an image - objectstore.image.Image.create('i-testing', 'image_bucket/1mb.manifest.xml', self.context) + objectstore.image.Image.register_aws_image('i-testing', 'image_bucket/1mb.manifest.xml', self.context) # verify image my_img = objectstore.image.Image('i-testing') From 9ab093b39600b767f760c8d1ff7e050eb41db8f7 Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Thu, 15 Jul 2010 00:48:02 -0400 Subject: [PATCH 5/5] bin to import images from canonical image store --- bin/nova-import-canonical-imagestore | 82 ++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100755 bin/nova-import-canonical-imagestore diff --git a/bin/nova-import-canonical-imagestore b/bin/nova-import-canonical-imagestore new file mode 100755 index 00000000..804b0e27 --- /dev/null +++ b/bin/nova-import-canonical-imagestore @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +""" + Download images from Canonical Image Store +""" + +import json +import os +import tempfile +import shutil +import subprocess +import sys +import urllib2 + +from nova.objectstore import image +from nova import flags +from nova import utils + +FLAGS = flags.FLAGS + +api_url = 'https://imagestore.canonical.com/api/dashboard' + +image_cache = None +def images(): + global image_cache + if not image_cache: + try: + images = json.load(urllib2.urlopen(api_url))['images'] + image_cache = [i for i in images if i['title'].find('amd64') > -1] + except Exception: + print 'unable to download canonical image list' + sys.exit(1) + return image_cache + +# FIXME(ja): add checksum/signature checks +def download(img): + tempdir = tempfile.mkdtemp(prefix='cis-') + + kernel_id = None + ramdisk_id = None + + for f in img['files']: + if f['kind'] == 'kernel': + dest = os.path.join(tempdir, 'kernel') + subprocess.call(['curl', f['url'], '-o', dest]) + kernel_id = image.Image.add(dest, + description='kernel/' + img['title'], kernel=True) + + for f in img['files']: + if f['kind'] == 'ramdisk': + dest = os.path.join(tempdir, 'ramdisk') + subprocess.call(['curl', f['url'], '-o', dest]) + ramdisk_id = image.Image.add(dest, + description='ramdisk/' + img['title'], ramdisk=True) + + for f in img['files']: + if f['kind'] == 'image': + dest = os.path.join(tempdir, 'image') + subprocess.call(['curl', f['url'], '-o', dest]) + ramdisk_id = image.Image.add(dest, + description=img['title'], kernel=kernel_id, ramdisk=ramdisk_id) + + shutil.rmtree(tempdir) + +def main(): + utils.default_flagfile() + argv = FLAGS(sys.argv) + + if len(argv) == 2: + for img in images(): + if argv[1] == 'all' or argv[1] == img['title']: + download(img) + else: + print 'usage: %s (title|all)' + print 'available images:' + for image in images(): + print image['title'] + +if __name__ == '__main__': + main() +