diff options
-rw-r--r-- | nova/api/cloud.py | 42 | ||||
-rw-r--r-- | nova/api/ec2/cloud.py | 8 | ||||
-rw-r--r-- | nova/api/rackspace/context.py | 33 | ||||
-rw-r--r-- | nova/api/rackspace/servers.py | 171 | ||||
-rw-r--r-- | nova/db/sqlalchemy/models.py | 2 | ||||
-rw-r--r-- | nova/tests/api/rackspace/auth.py | 8 | ||||
-rw-r--r-- | nova/tests/api/rackspace/flavors.py | 1 | ||||
-rw-r--r-- | nova/tests/api/rackspace/images.py | 1 | ||||
-rw-r--r-- | nova/tests/api/rackspace/servers.py | 101 | ||||
-rw-r--r-- | nova/tests/api/rackspace/sharedipgroups.py | 1 | ||||
-rw-r--r-- | nova/tests/api/rackspace/test_helper.py | 15 | ||||
-rw-r--r-- | nova/wsgi.py | 20 |
12 files changed, 326 insertions, 77 deletions
diff --git a/nova/api/cloud.py b/nova/api/cloud.py new file mode 100644 index 000000000..345677d4f --- /dev/null +++ b/nova/api/cloud.py @@ -0,0 +1,42 @@ +# 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. + +""" +Methods for API calls to control instances via AMQP. +""" + + +from nova import db +from nova import flags +from nova import rpc + +FLAGS = flags.FLAGS + + +def reboot(instance_id, context=None): + """Reboot the given instance. + + #TODO(gundlach) not actually sure what context is used for by ec2 here + -- I think we can just remove it and use None all the time. + """ + instance_ref = db.instance_get_by_ec2_id(None, instance_id) + host = instance_ref['host'] + rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "reboot_instance", + "args": {"context": None, + "instance_id": instance_ref['id']}}) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index d3f54367b..79c95788b 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -36,6 +36,7 @@ from nova import quota from nova import rpc from nova import utils from nova.compute.instance_types import INSTANCE_TYPES +from nova.api import cloud from nova.api.ec2 import images @@ -684,12 +685,7 @@ class CloudController(object): def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" for id_str in instance_id: - instance_ref = db.instance_get_by_ec2_id(context, id_str) - host = instance_ref['host'] - rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), - {"method": "reboot_instance", - "args": {"context": None, - "instance_id": instance_ref['id']}}) + cloud.reboot(id_str, context=context) return True def update_instance(self, context, instance_id, **kwargs): diff --git a/nova/api/rackspace/context.py b/nova/api/rackspace/context.py new file mode 100644 index 000000000..77394615b --- /dev/null +++ b/nova/api/rackspace/context.py @@ -0,0 +1,33 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" +APIRequestContext +""" + +import random + +class Project(object): + def __init__(self, user_id): + self.id = user_id + +class APIRequestContext(object): + """ This is an adapter class to get around all of the assumptions made in + the FlatNetworking """ + def __init__(self, user_id): + self.user_id = user_id + self.project = Project(user_id) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 08d533563..11efd8aef 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -17,35 +17,45 @@ import time +import webob from webob import exc from nova import flags from nova import rpc from nova import utils from nova import wsgi +from nova.api import cloud from nova.api.rackspace import _id_translator +from nova.api.rackspace import context from nova.api.rackspace import faults +from nova.compute import instance_types from nova.compute import power_state import nova.api.rackspace import nova.image.service FLAGS = flags.FLAGS +flags.DEFINE_string('rs_network_manager', 'nova.network.manager.FlatManager', + 'Networking for rackspace') +def _instance_id_translator(): + """ Helper method for initializing an id translator for Rackspace instance + ids """ + return _id_translator.RackspaceAPIIdTranslator( "instance", 'nova') -def translator_instance(): +def _image_service(): """ Helper method for initializing the image id translator """ service = nova.image.service.ImageService.load() - return _id_translator.RackspaceAPIIdTranslator( - "image", service.__class__.__name__) + return (service, _id_translator.RackspaceAPIIdTranslator( + "image", service.__class__.__name__)) def _filter_params(inst_dict): """ Extracts all updatable parameters for a server update request """ - keys = ['name', 'adminPass'] + keys = dict(name='name', admin_pass='adminPass') new_attrs = {} - for k in keys: - if inst_dict.has_key(k): - new_attrs[k] = inst_dict[k] + for k, v in keys.items(): + if inst_dict.has_key(v): + new_attrs[k] = inst_dict[v] return new_attrs def _entity_list(entities): @@ -84,7 +94,6 @@ def _entity_inst(inst): class Controller(wsgi.Controller): """ The Server API controller for the Openstack API """ - _serialization_metadata = { 'application/xml': { @@ -122,8 +131,11 @@ class Controller(wsgi.Controller): def show(self, req, id): """ Returns server details by server id """ + inst_id_trans = _instance_id_translator() + inst_id = inst_id_trans.from_rs_id(id) + user_id = req.environ['nova.context']['user']['id'] - inst = self.db_driver.instance_get(None, id) + inst = self.db_driver.instance_get_by_ec2_id(None, inst_id) if inst: if inst.user_id == user_id: return _entity_detail(inst) @@ -131,8 +143,11 @@ class Controller(wsgi.Controller): def delete(self, req, id): """ Destroys a server """ + inst_id_trans = _instance_id_translator() + inst_id = inst_id_trans.from_rs_id(id) + user_id = req.environ['nova.context']['user']['id'] - instance = self.db_driver.instance_get(None, id) + instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) if instance and instance['user_id'] == user_id: self.db_driver.instance_destroy(None, id) return faults.Fault(exc.HTTPAccepted()) @@ -140,10 +155,15 @@ class Controller(wsgi.Controller): def create(self, req): """ Creates a new server for a given user """ - if not req.environ.has_key('inst_dict'): + + env = self._deserialize(req.body, req) + if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) - inst = self._build_server_instance(req) + try: + inst = self._build_server_instance(req, env) + except Exception, e: + return faults.Fault(exc.HTTPUnprocessableEntity()) rpc.cast( FLAGS.compute_topic, { @@ -153,62 +173,127 @@ class Controller(wsgi.Controller): def update(self, req, id): """ Updates the server name or password """ - if not req.environ.has_key('inst_dict'): + inst_id_trans = _instance_id_translator() + inst_id = inst_id_trans.from_rs_id(id) + user_id = req.environ['nova.context']['user']['id'] + + inst_dict = self._deserialize(req.body, req) + + if not inst_dict: return faults.Fault(exc.HTTPUnprocessableEntity()) - instance = self.db_driver.instance_get(None, id) - if not instance: + instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) + if not instance or instance.user_id != user_id: return faults.Fault(exc.HTTPNotFound()) - attrs = req.environ['nova.context'].get('model_attributes', None) - if attrs: - self.db_driver.instance_update(None, id, _filter_params(attrs)) + self.db_driver.instance_update(None, id, + _filter_params(inst_dict['server'])) return faults.Fault(exc.HTTPNoContent()) def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ - if not req.environ.has_key('inst_dict'): - return faults.Fault(exc.HTTPUnprocessableEntity()) - - def _build_server_instance(self, req): + input_dict = self._deserialize(req.body, req) + try: + reboot_type = input_dict['reboot']['type'] + except Exception: + raise faults.Fault(webob.exc.HTTPNotImplemented()) + opaque_id = _instance_id_translator().from_rs_id(id) + cloud.reboot(opaque_id) + + def _build_server_instance(self, req, env): """Build instance data structure and save it to the data store.""" ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} - env = req.environ['inst_dict'] + inst_id_trans = _instance_id_translator() + + user_id = req.environ['nova.context']['user']['id'] + + flavor_id = env['server']['flavorId'] + + instance_type, flavor = [(k, v) for k, v in + instance_types.INSTANCE_TYPES.iteritems() + if v['flavorid'] == flavor_id][0] image_id = env['server']['imageId'] - opaque_id = translator_instance().from_rs_id(image_id) + + img_service, image_id_trans = _image_service() - inst['name'] = env['server']['server_name'] - inst['image_id'] = opaque_id - inst['instance_type'] = env['server']['flavorId'] + opaque_image_id = image_id_trans.to_rs_id(image_id) + image = img_service.show(opaque_image_id) - user_id = req.environ['nova.context']['user']['id'] - inst['user_id'] = user_id + if not image: + raise Exception, "Image not found" + inst['server_name'] = env['server']['name'] + inst['image_id'] = opaque_image_id + inst['user_id'] = user_id inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() + inst['project_id'] = user_id + + inst['state_description'] = 'scheduling' + inst['kernel_id'] = image.get('kernelId', FLAGS.default_kernel) + inst['ramdisk_id'] = image.get('ramdiskId', FLAGS.default_ramdisk) + inst['reservation_id'] = utils.generate_uid('r') - inst['project_id'] = env['project']['id'] - inst['reservation_id'] = reservation - reservation = utils.generate_uid('r') + inst['display_name'] = env['server']['name'] + inst['display_description'] = env['server']['name'] - address = self.network.allocate_ip( - inst['user_id'], - inst['project_id'], - mac=inst['mac_address']) + #TODO(dietz) this may be ill advised + key_pair_ref = self.db_driver.key_pair_get_all_by_user( + None, user_id)[0] - inst['private_dns_name'] = str(address) - inst['bridge_name'] = network.BridgedNetwork.get_network_for_project( - inst['user_id'], - inst['project_id'], - 'default')['bridge_name'] + inst['key_data'] = key_pair_ref['public_key'] + inst['key_name'] = key_pair_ref['name'] + + #TODO(dietz) stolen from ec2 api, see TODO there + inst['security_group'] = 'default' + + # Flavor related attributes + inst['instance_type'] = instance_type + inst['memory_mb'] = flavor['memory_mb'] + inst['vcpus'] = flavor['vcpus'] + inst['local_gb'] = flavor['local_gb'] ref = self.db_driver.instance_create(None, inst) - inst['id'] = ref.id + inst['id'] = inst_id_trans.to_rs_id(ref.ec2_id) + + # TODO(dietz): this isn't explicitly necessary, but the networking + # calls depend on an object with a project_id property, and therefore + # should be cleaned up later + api_context = context.APIRequestContext(user_id) + + inst['mac_address'] = utils.generate_mac() + #TODO(dietz) is this necessary? + inst['launch_index'] = 0 + + inst['hostname'] = ref.ec2_id + self.db_driver.instance_update(None, inst['id'], inst) + + network_manager = utils.import_object(FLAGS.rs_network_manager) + address = network_manager.allocate_fixed_ip(api_context, + inst['id']) + + # TODO(vish): This probably should be done in the scheduler + # network is setup when host is assigned + network_topic = self._get_network_topic(user_id) + rpc.call(network_topic, + {"method": "setup_fixed_ip", + "args": {"context": None, + "address": address}}) return inst - + def _get_network_topic(self, user_id): + """Retrieves the network host for a project""" + network_ref = self.db_driver.project_get_network(None, + user_id) + host = network_ref['host'] + if not host: + host = rpc.call(FLAGS.network_topic, + {"method": "set_network_host", + "args": {"context": None, + "project_id": user_id}}) + return self.db_driver.queue_get_for(None, FLAGS.network_topic, host) diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 01e58b05e..6cb377476 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -199,6 +199,8 @@ class Instance(BASE, NovaBase): id = Column(Integer, primary_key=True) ec2_id = Column(String(10), unique=True) + admin_pass = Column(String(255)) + user_id = Column(String(255)) project_id = Column(String(255)) diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index a6e10970f..56677c2f4 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -1,12 +1,14 @@ -import webob -import webob.dec +import datetime import unittest + import stubout +import webob +import webob.dec + import nova.api import nova.api.rackspace.auth from nova import auth from nova.tests.api.rackspace import test_helper -import datetime class Test(unittest.TestCase): def setUp(self): diff --git a/nova/tests/api/rackspace/flavors.py b/nova/tests/api/rackspace/flavors.py index 7bd1ea1c4..d25a2e2be 100644 --- a/nova/tests/api/rackspace/flavors.py +++ b/nova/tests/api/rackspace/flavors.py @@ -38,7 +38,6 @@ class FlavorsTest(unittest.TestCase): def test_get_flavor_list(self): req = webob.Request.blank('/v1.0/flavors') res = req.get_response(nova.api.API()) - print res def test_get_flavor_by_id(self): pass diff --git a/nova/tests/api/rackspace/images.py b/nova/tests/api/rackspace/images.py index 560d8c898..4c9987e8b 100644 --- a/nova/tests/api/rackspace/images.py +++ b/nova/tests/api/rackspace/images.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import stubout import unittest from nova.api.rackspace import images diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 9fd8e5e88..69ad2c1d3 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -26,6 +26,7 @@ import nova.api.rackspace from nova.api.rackspace import servers import nova.db.api from nova.db.sqlalchemy.models import Instance +import nova.rpc from nova.tests.api.test_helper import * from nova.tests.api.rackspace import test_helper @@ -52,8 +53,11 @@ class ServersTest(unittest.TestCase): test_helper.stub_for_testing(self.stubs) test_helper.stub_out_rate_limiting(self.stubs) test_helper.stub_out_auth(self.stubs) + test_helper.stub_out_id_translator(self.stubs) + test_helper.stub_out_key_pair_funcs(self.stubs) + test_helper.stub_out_image_service(self.stubs) self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) - self.stubs.Set(nova.db.api, 'instance_get', return_server) + self.stubs.Set(nova.db.api, 'instance_get_by_ec2_id', return_server) self.stubs.Set(nova.db.api, 'instance_get_all_by_user', return_servers) @@ -67,9 +71,6 @@ class ServersTest(unittest.TestCase): self.assertEqual(res_dict['server']['id'], '1') self.assertEqual(res_dict['server']['name'], 'server1') - def test_get_backup_schedule(self): - pass - def test_get_server_list(self): req = webob.Request.blank('/v1.0/servers') res = req.get_response(nova.api.API()) @@ -82,24 +83,86 @@ class ServersTest(unittest.TestCase): self.assertEqual(s.get('imageId', None), None) i += 1 - #def test_create_instance(self): - # test_helper.stub_out_image_translator(self.stubs) - # body = dict(server=dict( - # name='server_test', imageId=2, flavorId=2, metadata={}, - # personality = {} - # )) - # req = webob.Request.blank('/v1.0/servers') - # req.method = 'POST' - # req.body = json.dumps(body) + def test_create_instance(self): + def server_update(context, id, params): + pass + + def instance_create(context, inst): + class Foo(object): + ec2_id = 1 + return Foo() + + def fake_method(*args, **kwargs): + pass + + def project_get_network(context, user_id): + return dict(id='1', host='localhost') + + def queue_get_for(context, *args): + return 'network_topic' + + self.stubs.Set(nova.db.api, 'project_get_network', project_get_network) + self.stubs.Set(nova.db.api, 'instance_create', instance_create) + self.stubs.Set(nova.rpc, 'cast', fake_method) + self.stubs.Set(nova.rpc, 'call', fake_method) + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for) + self.stubs.Set(nova.network.manager.FlatManager, 'allocate_fixed_ip', + fake_method) + + test_helper.stub_out_id_translator(self.stubs) + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(body) + + res = req.get_response(nova.api.API()) + + self.assertEqual(res.status_int, 200) + + def test_update_no_body(self): + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status_int, 422) + + def test_update_bad_params(self): + """ Confirm that update is filtering params """ + inst_dict = dict(cat='leopard', name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) - # res = req.get_response(nova.api.API()) + def server_update(context, id, params): + self.update_called = True + filtered_dict = dict(name='server_test', admin_pass='bacon') + self.assertEqual(params, filtered_dict) - # print res - def test_update_server_password(self): - pass + self.stubs.Set(nova.db.api, 'instance_update', + server_update) - def test_update_server_name(self): - pass + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) + + def test_update_server(self): + inst_dict = dict(name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + def server_update(context, id, params): + filtered_dict = dict(name='server_test', admin_pass='bacon') + self.assertEqual(params, filtered_dict) + + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) def test_create_backup_schedules(self): req = webob.Request.blank('/v1.0/servers/1/backup_schedules') diff --git a/nova/tests/api/rackspace/sharedipgroups.py b/nova/tests/api/rackspace/sharedipgroups.py index b4b281db7..1906b54f5 100644 --- a/nova/tests/api/rackspace/sharedipgroups.py +++ b/nova/tests/api/rackspace/sharedipgroups.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import stubout import unittest from nova.api.rackspace import sharedipgroups diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index aa7fb382c..2cf154f63 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -9,6 +9,7 @@ from nova import utils from nova import flags import nova.api.rackspace.auth import nova.api.rackspace._id_translator +from nova.image import service from nova.wsgi import Router FLAGS = flags.FLAGS @@ -40,7 +41,19 @@ def fake_wsgi(self, req): req.environ['inst_dict'] = json.loads(req.body) return self.application -def stub_out_image_translator(stubs): +def stub_out_key_pair_funcs(stubs): + def key_pair(context, user_id): + return [dict(name='key', public_key='public_key')] + stubs.Set(nova.db.api, 'key_pair_get_all_by_user', + key_pair) + +def stub_out_image_service(stubs): + def fake_image_show(meh, id): + return dict(kernelId=1, ramdiskId=1) + + stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show) + +def stub_out_id_translator(stubs): class FakeTranslator(object): def __init__(self, id_type, service_name): pass diff --git a/nova/wsgi.py b/nova/wsgi.py index da9374542..b91d91121 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -230,6 +230,15 @@ class Controller(object): serializer = Serializer(request.environ, _metadata) return serializer.to_content_type(data) + def _deserialize(self, data, request): + """ + Deserialize the request body to the response type requested in request. + Uses self._serialization_metadata if it exists, which is a dict mapping + MIME types to information needed to serialize to that type. + """ + _metadata = getattr(type(self), "_serialization_metadata", {}) + serializer = Serializer(request.environ, _metadata) + return serializer.deserialize(data) class Serializer(object): """ @@ -272,10 +281,13 @@ class Serializer(object): The string must be in the format of a supported MIME type. """ datastring = datastring.strip() - is_xml = (datastring[0] == '<') - if not is_xml: - return json.loads(datastring) - return self._from_xml(datastring) + try: + is_xml = (datastring[0] == '<') + if not is_xml: + return json.loads(datastring) + return self._from_xml(datastring) + except: + return None def _from_xml(self, datastring): xmldata = self.metadata.get('application/xml', {}) |