diff --git a/bin/nova-import-canonical-imagestore b/bin/nova-import-canonical-imagestore new file mode 100755 index 000000000000..804b0e272412 --- /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() + diff --git a/bin/nova-rsapi b/bin/nova-rsapi new file mode 100755 index 000000000000..24b469f750e6 --- /dev/null +++ b/bin/nova-rsapi @@ -0,0 +1,58 @@ +#!/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 wsgiref import simple_server + +from nova import vendor +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 + +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) + + 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() + server.serve('nova-rsapi', main) diff --git a/exercise_rsapi.py b/exercise_rsapi.py new file mode 100644 index 000000000000..4b9e65fc6f4d --- /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/cloud.py b/nova/endpoint/cloud.py index 9dccc24dcb8e..32c7cbce0dda 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/endpoint/rackspace.py b/nova/endpoint/rackspace.py new file mode 100644 index 000000000000..ac32d20a415d --- /dev/null +++ b/nova/endpoint/rackspace.py @@ -0,0 +1,226 @@ +# 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 wsgi +from nova.endpoint import images +from nova.volume import storage + + +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 Api(object): + + def __init__(self, rpc_mechanism): + self.controllers = { + "v1.0": RackspaceAuthenticationApi(), + "servers": RackspaceCloudServerApi() + } + self.rpc_mechanism = rpc_mechanism + + def handler(self, environ, responder): + 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: + responder(rv[0], rv[1]) + rv = rv[2] + else: + 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) + + action = path.pop(0) + if hasattr(self, action): + 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://%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): + + 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} + } + ) diff --git a/nova/endpoint/wsgi.py b/nova/endpoint/wsgi.py new file mode 100644 index 000000000000..e0817fd0e2dc --- /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 + diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index 462ad90b1fcb..2482f6feaed2 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -286,8 +286,8 @@ class ImageHandler(BaseRequestHandler): if not bucket_object.is_authorized(self.context): raise web.HTTPError(403) - p = multiprocessing.Process(target=image.Image.create,args= - (image_id, image_location, self.context)) + p = multiprocessing.Process(target=image.Image.register_aws_image, + args=(image_id, image_location, self.context)) p.start() self.finish() diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index dc4667ec289a..4de41ea96dd6 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -100,7 +100,69 @@ class Image(object): return json.load(f) @staticmethod - def create(image_id, image_location, context): + def add(src, description, kernel=None, ramdisk=None, public=True): + """adds an image to imagestore + + @type src: str + @param src: location of the partition image on disk + + @type description: str + @param description: string describing the image contents + + @type kernel: bool or str + @param kernel: either TRUE meaning this partition is a kernel image or + a string of the image id for the kernel + + @type ramdisk: bool or str + @param ramdisk: either TRUE meaning this partition is a ramdisk image or + a string of the image id for the ramdisk + + + @type public: bool + @param public: determine if this is a public image or private + + @rtype: str + @return: a string with the image id + """ + + image_type = 'machine' + image_id = utils.generate_uid('ami') + + if kernel is True: + image_type = 'kernel' + image_id = utils.generate_uid('aki') + if ramdisk is True: + image_type = 'ramdisk' + image_id = utils.generate_uid('ari') + + image_path = os.path.join(FLAGS.images_path, image_id) + os.makedirs(image_path) + + shutil.copyfile(src, os.path.join(image_path, 'image')) + + info = { + 'imageId': image_id, + 'imageLocation': description, + 'imageOwnerId': 'system', + 'isPublic': public, + 'architecture': 'x86_64', + 'type': image_type, + 'state': 'available' + } + + if type(kernel) is str and len(kernel) > 0: + info['kernelId'] = kernel + + if type(ramdisk) is str and len(ramdisk) > 0: + info['ramdiskId'] = ramdisk + + with open(os.path.join(image_path, 'info.json'), "w") as f: + json.dump(info, f) + + return image_id + + @staticmethod + def register_aws_image(image_id, image_location, context): image_path = os.path.join(FLAGS.images_path, image_id) os.makedirs(image_path) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index cee567c8b3c6..ddd455a73433 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')