From 7c8c2f57c752cd8681eef073349f9bdcaa95c868 Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Tue, 28 Sep 2010 14:48:03 -0400 Subject: Adds --force option to run_tests.sh to clear virtualenv. Useful when dependencies change --- run_tests.sh | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index 6ea40d95e..ec727d094 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,6 +6,7 @@ function usage { echo "" echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." echo " -h, --help Print this usage message" echo "" echo "Note: with no options specified, the script will try to run the tests in a virtual environment," @@ -14,20 +15,12 @@ function usage { exit } -function process_options { - array=$1 - elements=${#array[@]} - for (( x=0;x<$elements;x++)); do - process_option ${array[${x}]} - done -} - function process_option { - option=$1 - case $option in + case "$1" in -h|--help) usage;; -V|--virtual-env) let always_venv=1; let never_venv=0;; -N|--no-virtual-env) let always_venv=0; let never_venv=1;; + -f|--force) let force=1;; esac } @@ -35,9 +28,11 @@ venv=.nova-venv with_venv=tools/with_venv.sh always_venv=0 never_venv=0 -options=("$@") +force=0 -process_options $options +for arg in "$@"; do + process_option $arg +done if [ $never_venv -eq 1 ]; then # Just run the test suites in current environment @@ -45,6 +40,12 @@ if [ $never_venv -eq 1 ]; then exit fi +# Remove the virtual environment if --force used +if [ $force -eq 1 ]; then + echo "Cleaning virtualenv..." + rm -rf ${venv} +fi + if [ -e ${venv} ]; then ${with_venv} python run_tests.py $@ else -- cgit From 58ae192764b11b19f5676f9496f287a4ea2a71bd Mon Sep 17 00:00:00 2001 From: Cerberus Date: Thu, 30 Sep 2010 20:07:26 -0500 Subject: refactoring --- nova/api/cloud.py | 2 +- nova/api/rackspace/servers.py | 25 ++++++------------------- nova/tests/api/rackspace/servers.py | 2 +- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/nova/api/cloud.py b/nova/api/cloud.py index 345677d4f..57e94a17a 100644 --- a/nova/api/cloud.py +++ b/nova/api/cloud.py @@ -34,7 +34,7 @@ def reboot(instance_id, context=None): #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) + instance_ref = db.instance_get_by_internal_id(None, instance_id) host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "reboot_instance", diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 11efd8aef..39e784be2 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -35,9 +35,6 @@ 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 """ @@ -131,11 +128,8 @@ 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_by_ec2_id(None, inst_id) + inst = self.db_driver.instance_get_by_instance_id(None, id) if inst: if inst.user_id == user_id: return _entity_detail(inst) @@ -143,11 +137,8 @@ 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_by_ec2_id(None, inst_id) + instance = self.db_driver.instance_get_by_internal_id(None, id) if instance and instance['user_id'] == user_id: self.db_driver.instance_destroy(None, id) return faults.Fault(exc.HTTPAccepted()) @@ -173,8 +164,6 @@ class Controller(wsgi.Controller): def update(self, req, id): """ Updates the server name or password """ - 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) @@ -182,7 +171,7 @@ class Controller(wsgi.Controller): if not inst_dict: return faults.Fault(exc.HTTPUnprocessableEntity()) - instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) + instance = self.db_driver.instance_get_by_internal_id(None, id) if not instance or instance.user_id != user_id: return faults.Fault(exc.HTTPNotFound()) @@ -206,8 +195,6 @@ class Controller(wsgi.Controller): ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} - inst_id_trans = _instance_id_translator() - user_id = req.environ['nova.context']['user']['id'] flavor_id = env['server']['flavorId'] @@ -258,7 +245,7 @@ class Controller(wsgi.Controller): inst['local_gb'] = flavor['local_gb'] ref = self.db_driver.instance_create(None, inst) - inst['id'] = inst_id_trans.to_rs_id(ref.ec2_id) + inst['id'] = ref.internal_id # TODO(dietz): this isn't explicitly necessary, but the networking # calls depend on an object with a project_id property, and therefore @@ -270,10 +257,10 @@ class Controller(wsgi.Controller): #TODO(dietz) is this necessary? inst['launch_index'] = 0 - inst['hostname'] = ref.ec2_id + inst['hostname'] = ref.internal_id self.db_driver.instance_update(None, inst['id'], inst) - network_manager = utils.import_object(FLAGS.rs_network_manager) + network_manager = utils.import_object(FLAGS.network_manager) address = network_manager.allocate_fixed_ip(api_context, inst['id']) diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 69ad2c1d3..ee60cfcbc 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -57,7 +57,7 @@ class ServersTest(unittest.TestCase): 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_by_ec2_id', return_server) + self.stubs.Set(nova.db.api, 'instance_get_by_internal_id', return_server) self.stubs.Set(nova.db.api, 'instance_get_all_by_user', return_servers) -- cgit From c58acf2c59420a78f6b7195e3c1ef25e84f12e20 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 30 Sep 2010 21:19:35 -0400 Subject: Replace database instance 'ec2_id' with 'internal_id' throughout the nova.db package. internal_id is now an integer -- we need to figure out how to make this a bigint or something. --- nova/db/api.py | 4 ++-- nova/db/sqlalchemy/api.py | 20 +++++++++++--------- nova/db/sqlalchemy/models.py | 4 ++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/nova/db/api.py b/nova/db/api.py index b68a0fe8f..c3c29c2fc 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -280,9 +280,9 @@ def instance_get_floating_address(context, instance_id): return IMPL.instance_get_floating_address(context, instance_id) -def instance_get_by_ec2_id(context, ec2_id): +def instance_get_by_internal_id(context, internal_id): """Get an instance by ec2 id.""" - return IMPL.instance_get_by_ec2_id(context, ec2_id) + return IMPL.instance_get_by_internal_id(context, internal_id) def instance_is_vpn(context, instance_id): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 9c3caf9af..6dd6b545a 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -384,10 +384,11 @@ def instance_create(_context, values): session = get_session() with session.begin(): - while instance_ref.ec2_id == None: - ec2_id = utils.generate_uid(instance_ref.__prefix__) - if not instance_ec2_id_exists(_context, ec2_id, session=session): - instance_ref.ec2_id = ec2_id + while instance_ref.internal_id == None: + internal_id = utils.generate_uid(instance_ref.__prefix__) + if not instance_internal_id_exists(_context, internal_id, + session=session): + instance_ref.internal_id = internal_id instance_ref.save(session=session) return instance_ref @@ -446,22 +447,23 @@ def instance_get_all_by_reservation(_context, reservation_id): ).all() -def instance_get_by_ec2_id(context, ec2_id): +def instance_get_by_internal_id(context, internal_id): session = get_session() instance_ref = session.query(models.Instance - ).filter_by(ec2_id=ec2_id + ).filter_by(internal_id=internal_id ).filter_by(deleted=_deleted(context) ).first() if not instance_ref: - raise exception.NotFound('Instance %s not found' % (ec2_id)) + raise exception.NotFound('Instance %s not found' % (internal_id)) return instance_ref -def instance_ec2_id_exists(context, ec2_id, session=None): +def instance_internal_id_exists(context, internal_id, session=None): if not session: session = get_session() - return session.query(exists().where(models.Instance.id==ec2_id)).one()[0] + return session.query(exists().where(models.Instance.id==internal_id) + ).one()[0] def instance_get_fixed_address(_context, instance_id): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 6cb377476..5c93c92a0 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -197,7 +197,7 @@ class Instance(BASE, NovaBase): __tablename__ = 'instances' __prefix__ = 'i' id = Column(Integer, primary_key=True) - ec2_id = Column(String(10), unique=True) + internal_id = Column(Integer, unique=True) admin_pass = Column(String(255)) @@ -214,7 +214,7 @@ class Instance(BASE, NovaBase): @property def name(self): - return self.ec2_id + return self.internal_id image_id = Column(String(255)) kernel_id = Column(String(255)) -- cgit From 58773e16ddd6f3aaa4aafefde55a3ae631e806dd Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 30 Sep 2010 21:59:52 -0400 Subject: Convert EC2 cloud.py from assuming that EC2 IDs are stored directly in the database, to assuming that EC2 IDs should be converted to internal IDs. The conversion between the internal ID and the EC2 ID is imperfect -- right now it turns internal IDs like 408 into EC2 IDs like i-408, and vice versa. Instead, EC2 IDs are supposed to be i-[base 36 of the integer]. --- nova/api/ec2/cloud.py | 58 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 79c95788b..2fec49da8 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -113,6 +113,16 @@ class CloudController(object): result[key] = [line] return result + def ec2_id_to_internal_id(ec2_id): + """Convert an ec2 ID (i-[base 36 number]) to an internal id (int)""" + # TODO(gundlach): Maybe this should actually work? + return ec2_id[2:] + + def internal_id_to_ec2_id(internal_id): + """Convert an internal ID (int) to an ec2 ID (i-[base 36 number])""" + # TODO(gundlach): Yo maybe this should actually convert to base 36 + return "i-%d" % internal_id + def get_metadata(self, address): instance_ref = db.fixed_ip_get_instance(None, address) if instance_ref is None: @@ -144,7 +154,7 @@ class CloudController(object): }, 'hostname': hostname, 'instance-action': 'none', - 'instance-id': instance_ref['ec2_id'], + 'instance-id': internal_id_to_ec2_id(instance_ref['internal_id']), 'instance-type': instance_ref['instance_type'], 'local-hostname': hostname, 'local-ipv4': address, @@ -244,9 +254,11 @@ class CloudController(object): def delete_security_group(self, context, group_name, **kwargs): return True - def get_console_output(self, context, instance_id, **kwargs): - # instance_id is passed in as a list of instances - instance_ref = db.instance_get_by_ec2_id(context, instance_id[0]) + def get_console_output(self, context, ec2_id_list, **kwargs): + # ec2_id_list is passed in as a list of instances + ec2_id = ec2_id_list[0] + internal_id = ec2_id_to_internal_id(ec2_id) + instance_ref = db.instance_get_by_ec2_id(context, internal_id) return rpc.call('%s.%s' % (FLAGS.compute_topic, instance_ref['host']), {"method": "get_console_output", @@ -326,7 +338,8 @@ class CloudController(object): raise exception.ApiError("Volume status must be available") if volume_ref['attach_status'] == "attached": raise exception.ApiError("Volume is already attached") - instance_ref = db.instance_get_by_ec2_id(context, instance_id) + internal_id = ec2_id_to_internal_id(instance_id) + instance_ref = db.instance_get_by_internal_id(context, internal_id) host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "attach_volume", @@ -360,9 +373,11 @@ class CloudController(object): # If the instance doesn't exist anymore, # then we need to call detach blind db.volume_detached(context) + internal_id = instance_ref['internal_id'] + ec2_id = internal_id_to_ec2_id(internal_id) return {'attachTime': volume_ref['attach_time'], 'device': volume_ref['mountpoint'], - 'instanceId': instance_ref['ec2_id'], + 'instanceId': internal_id, 'requestId': context.request_id, 'status': volume_ref['attach_status'], 'volumeId': volume_ref['id']} @@ -411,7 +426,9 @@ class CloudController(object): if instance['image_id'] == FLAGS.vpn_image_id: continue i = {} - i['instanceId'] = instance['ec2_id'] + internal_id = instance['internal_id'] + ec2_id = internal_id_to_ec2_id(internal_id) + i['instanceId'] = ec2_id i['imageId'] = instance['image_id'] i['instanceState'] = { 'code': instance['state'], @@ -464,9 +481,10 @@ class CloudController(object): instance_id = None if (floating_ip_ref['fixed_ip'] and floating_ip_ref['fixed_ip']['instance']): - instance_id = floating_ip_ref['fixed_ip']['instance']['ec2_id'] + internal_id = floating_ip_ref['fixed_ip']['instance']['ec2_id'] + ec2_id = internal_id_to_ec2_id(internal_id) address_rv = {'public_ip': address, - 'instance_id': instance_id} + 'instance_id': ec2_id} if context.user.is_admin(): details = "%s (%s)" % (address_rv['instance_id'], floating_ip_ref['project_id']) @@ -498,8 +516,9 @@ class CloudController(object): "floating_address": floating_ip_ref['address']}}) return {'releaseResponse': ["Address released."]} - def associate_address(self, context, instance_id, public_ip, **kwargs): - instance_ref = db.instance_get_by_ec2_id(context, instance_id) + def associate_address(self, context, ec2_id, public_ip, **kwargs): + internal_id = ec2_id_to_internal_id(ec2_id) + instance_ref = db.instance_get_by_internal_id(context, internal_id) fixed_address = db.instance_get_fixed_address(context, instance_ref['id']) floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) @@ -610,7 +629,9 @@ class CloudController(object): inst = {} inst['mac_address'] = utils.generate_mac() inst['launch_index'] = num - inst['hostname'] = instance_ref['ec2_id'] + internal_id = instance_ref['internal_id'] + ec2_id = internal_id_to_ec2_id(internal_id) + inst['hostname'] = ec2_id db.instance_update(context, inst_id, inst) address = self.network_manager.allocate_fixed_ip(context, inst_id, @@ -634,12 +655,14 @@ class CloudController(object): return self._format_run_instances(context, reservation_id) - def terminate_instances(self, context, instance_id, **kwargs): + def terminate_instances(self, context, ec2_id_list, **kwargs): logging.debug("Going to start terminating instances") - for id_str in instance_id: + for id_str in ec2_id_list: + internal_id = ec2_id_to_internal_id(id_str) logging.debug("Going to try and terminate %s" % id_str) try: - instance_ref = db.instance_get_by_ec2_id(context, id_str) + instance_ref = db.instance_get_by_internal_id(context, + internal_id) except exception.NotFound: logging.warning("Instance %s was not found during terminate" % id_str) @@ -688,7 +711,7 @@ class CloudController(object): cloud.reboot(id_str, context=context) return True - def update_instance(self, context, instance_id, **kwargs): + def update_instance(self, context, ec2_id, **kwargs): updatable_fields = ['display_name', 'display_description'] changes = {} for field in updatable_fields: @@ -696,7 +719,8 @@ class CloudController(object): changes[field] = kwargs[field] if changes: db_context = {} - inst = db.instance_get_by_ec2_id(db_context, instance_id) + internal_id = ec2_id_to_internal_id(ec2_id) + inst = db.instance_get_by_internal_id(db_context, internal_id) db.instance_update(db_context, inst['id'], kwargs) return True -- cgit From 39080e5f5000e0f401ff19f3fd9dd8cfbffffe69 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 30 Sep 2010 22:05:16 -0400 Subject: Find other places in the code that used ec2_id or get_instance_by_ec2_id and use internal_id as appropriate --- nova/compute/manager.py | 6 +++--- nova/tests/cloud_unittest.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index f370ede8b..131fac406 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -67,7 +67,7 @@ class ComputeManager(manager.Manager): def run_instance(self, context, instance_id, **_kwargs): """Launch a new instance with specified options.""" instance_ref = self.db.instance_get(context, instance_id) - if instance_ref['ec2_id'] in self.driver.list_instances(): + if instance_ref['internal_id'] in self.driver.list_instances(): raise exception.Error("Instance has already been created") logging.debug("instance %s: starting...", instance_id) project_id = instance_ref['project_id'] @@ -129,7 +129,7 @@ class ComputeManager(manager.Manager): raise exception.Error( 'trying to reboot a non-running' 'instance: %s (state: %s excepted: %s)' % - (instance_ref['ec2_id'], + (instance_ref['internal_id'], instance_ref['state'], power_state.RUNNING)) @@ -151,7 +151,7 @@ class ComputeManager(manager.Manager): if FLAGS.connection_type == 'libvirt': fname = os.path.abspath(os.path.join(FLAGS.instances_path, - instance_ref['ec2_id'], + instance_ref['internal_id'], 'console.log')) with open(fname, 'r') as f: output = f.read() diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index ae7dea1db..d316db153 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -236,7 +236,7 @@ class CloudTestCase(test.TrialTestCase): def test_update_of_instance_display_fields(self): inst = db.instance_create({}, {}) - self.cloud.update_instance(self.context, inst['ec2_id'], + self.cloud.update_instance(self.context, inst['internal_id'], display_name='c00l 1m4g3') inst = db.instance_get({}, inst['id']) self.assertEqual('c00l 1m4g3', inst['display_name']) -- cgit From 06cdef056b508e15869623da28ad18cc817e6848 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Thu, 30 Sep 2010 22:09:46 -0400 Subject: First attempt at a uuid generator -- but we've lost a 'topic' input so i don't know what that did. --- nova/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nova/utils.py b/nova/utils.py index d18dd9843..86ff3d22e 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -126,7 +126,9 @@ def runthis(prompt, cmd, check_exit_code = True): def generate_uid(topic, size=8): - return '%s-%s' % (topic, ''.join([random.choice('01234567890abcdefghijklmnopqrstuvwxyz') for x in xrange(size)])) + #TODO(gundlach): we want internal ids to just be ints now. i just dropped + #off a topic prefix, so what have I broken? + return random.randint(0, 2**64-1) def generate_mac(): -- cgit From 0ef621d47eeea421820a2191de53dee9e83d8c44 Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Fri, 1 Oct 2010 16:06:14 -0400 Subject: Adds BaseImageService and flag to control image service loading. Adds unit test for local image service. --- nova/api/rackspace/images.py | 7 +- nova/flags.py | 4 ++ nova/image/service.py | 114 ++++++++++++++++++++++++++++---- nova/tests/api/rackspace/test_images.py | 102 +++++++++++++++++++++++++--- 4 files changed, 206 insertions(+), 21 deletions(-) diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 4a7dd489c..d4ab8ce3c 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -17,12 +17,17 @@ from webob import exc +from nova import flags +from nova import utils from nova import wsgi from nova.api.rackspace import _id_translator import nova.api.rackspace import nova.image.service from nova.api.rackspace import faults + +FLAGS = flags.FLAGS + class Controller(wsgi.Controller): _serialization_metadata = { @@ -35,7 +40,7 @@ class Controller(wsgi.Controller): } def __init__(self): - self._service = nova.image.service.ImageService.load() + self._service = utils.import_object(FLAGS.image_service) self._id_translator = _id_translator.RackspaceAPIIdTranslator( "image", self._service.__class__.__name__) diff --git a/nova/flags.py b/nova/flags.py index c32cdd7a4..ab80e83fb 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -222,6 +222,10 @@ DEFINE_string('volume_manager', 'nova.volume.manager.AOEManager', DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager', 'Manager for scheduler') +# The service to use for image search and retrieval +DEFINE_string('image_service', 'nova.image.service.LocalImageService', + 'The service to use for retrieving and searching for images.') + DEFINE_string('host', socket.gethostname(), 'name of this node') diff --git a/nova/image/service.py b/nova/image/service.py index 1a7a258b7..4bceab6ee 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -20,34 +20,117 @@ import os.path import random import string -class ImageService(object): - """Provides storage and retrieval of disk image objects.""" +from nova import utils +from nova import flags - @staticmethod - def load(): - """Factory method to return image service.""" - #TODO(gundlach): read from config. - class_ = LocalImageService - return class_() + +FLAGS = flags.FLAGS + + +flags.DEFINE_string('glance_teller_address', '127.0.0.1', + 'IP address or URL where Glance\'s Teller service resides') +flags.DEFINE_string('glance_teller_port', '9191', + 'Port for Glance\'s Teller service') +flags.DEFINE_string('glance_parallax_address', '127.0.0.1', + 'IP address or URL where Glance\'s Parallax service resides') +flags.DEFINE_string('glance_parallax_port', '9191', + 'Port for Glance\'s Parallax service') + + +class BaseImageService(object): + + """Base class for providing image search and retrieval services""" def index(self): """ Return a dict from opaque image id to image data. """ + raise NotImplementedError def show(self, id): """ Returns a dict containing image data for the given opaque image id. """ + raise NotImplementedError + + def create(self, data): + """ + Store the image data and return the new image id. + + :raises AlreadyExists if the image already exist. + """ + raise NotImplementedError + + def update(self, image_id, data): + """Replace the contents of the given image with the new data. -class GlanceImageService(ImageService): + :raises NotFound if the image does not exist. + + """ + raise NotImplementedError + + def delete(self, image_id): + """ + Delete the given image. + + :raises NotFound if the image does not exist. + + """ + raise NotImplementedError + + +class GlanceImageService(BaseImageService): + """Provides storage and retrieval of disk image objects within Glance.""" - # TODO(gundlach): once Glance has an API, build this. - pass + def index(self): + """ + Calls out to Parallax for a list of images available + """ + raise NotImplementedError + + def show(self, id): + """ + Returns a dict containing image data for the given opaque image id. + """ + raise NotImplementedError + + def create(self, data): + """ + Store the image data and return the new image id. + + :raises AlreadyExists if the image already exist. + + """ + raise NotImplementedError + + def update(self, image_id, data): + """Replace the contents of the given image with the new data. + + :raises NotFound if the image does not exist. + + """ + raise NotImplementedError + + def delete(self, image_id): + """ + Delete the given image. + + :raises NotFound if the image does not exist. + + """ + raise NotImplementedError + + def delete_all(self): + """ + Clears out all images + """ + pass + + +class LocalImageService(BaseImageService): -class LocalImageService(ImageService): """Image service storing images to local disk.""" def __init__(self): @@ -88,3 +171,10 @@ class LocalImageService(ImageService): Delete the given image. Raises OSError if the image does not exist. """ os.unlink(self._path_to(image_id)) + + def delete_all(self): + """ + Clears out all images in local directory + """ + for f in os.listdir(self._path): + os.unlink(self._path_to(f)) diff --git a/nova/tests/api/rackspace/test_images.py b/nova/tests/api/rackspace/test_images.py index 489e35052..21dad7648 100644 --- a/nova/tests/api/rackspace/test_images.py +++ b/nova/tests/api/rackspace/test_images.py @@ -15,25 +15,111 @@ # License for the specific language governing permissions and limitations # under the License. +import logging import unittest import stubout +from nova import utils from nova.api.rackspace import images -class ImagesTest(unittest.TestCase): +#{ Fixtures + + +fixture_images = [ + { + 'name': 'image #1', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}, + { + 'name': 'image #2', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}, + { + 'name': 'image #3', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}] + + +#} + + +class BaseImageServiceTests(): + + """Tasks to test for all image services""" + + def test_create_and_index(self): + for i in fixture_images: + self.service.create(i) + + self.assertEquals(len(fixture_images), len(self.service.index())) + + def test_create_and_update(self): + ids = {} + temp = 0 + for i in fixture_images: + ids[self.service.create(i)] = temp + temp += 1 + + self.assertEquals(len(fixture_images), len(self.service.index())) + + for image_id, num in ids.iteritems(): + new_data = fixture_images[num] + new_data['updated'] = 'test' + str(num) + self.service.update(image_id, new_data) + + images = self.service.index() + + for i in images: + self.assertEquals('test' + str(ids[i['id']]), + i['updated']) + + def test_create_and_show(self): + ids = {} + temp = 0 + for i in fixture_images: + ids[self.service.create(i)] = temp + temp += 1 + + for i in fixture_images: + image = self.service.show(i['id']) + index = ids[i['id']] + self.assertEquals(image, fixture_images[index]) + + +class LocalImageServiceTest(unittest.TestCase, + BaseImageServiceTests): + + """Tests the local image service""" + def setUp(self): self.stubs = stubout.StubOutForTesting() + self.service = utils.import_object('nova.image.service.LocalImageService') def tearDown(self): + self.service.delete_all() self.stubs.UnsetAll() - def test_get_image_list(self): - pass - def test_delete_image(self): - pass - - def test_create_image(self): - pass +#class GlanceImageServiceTest(unittest.TestCase, +# BaseImageServiceTests): +# +# """Tests the local image service""" +# +# def setUp(self): +# self.stubs = stubout.StubOutForTesting() +# self.service = utils.import_object('nova.image.service.GlanceImageService') +# +# def tearDown(self): +# self.service.delete_all() +# self.stubs.UnsetAll() -- cgit From 4b11351aba7e71154f82a6a76590c786b4d7a53a Mon Sep 17 00:00:00 2001 From: Todd Willey Date: Fri, 1 Oct 2010 21:46:36 -0400 Subject: Keep handles to loggers open after daemonizing. --- nova/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nova/server.py b/nova/server.py index d4563bfe0..c58a15041 100644 --- a/nova/server.py +++ b/nova/server.py @@ -106,6 +106,7 @@ def serve(name, main): def daemonize(args, name, main): """Does the work of daemonizing the process""" logging.getLogger('amqplib').setLevel(logging.WARN) + files_to_keep = [] if FLAGS.daemonize: logger = logging.getLogger() formatter = logging.Formatter( @@ -114,12 +115,14 @@ def daemonize(args, name, main): syslog = logging.handlers.SysLogHandler(address='/dev/log') syslog.setFormatter(formatter) logger.addHandler(syslog) + files_to_keep.append(syslog.socket) else: if not FLAGS.logfile: FLAGS.logfile = '%s.log' % name logfile = logging.FileHandler(FLAGS.logfile) logfile.setFormatter(formatter) logger.addHandler(logfile) + files_to_keep.append(logfile.stream) stdin, stdout, stderr = None, None, None else: stdin, stdout, stderr = sys.stdin, sys.stdout, sys.stderr @@ -139,6 +142,7 @@ def daemonize(args, name, main): stdout=stdout, stderr=stderr, uid=FLAGS.uid, - gid=FLAGS.gid + gid=FLAGS.gid, + files_preserve=files_to_keep ): main(args) -- cgit From 5c4b1a38b8a82ee0a8f14f813f91d319a9715cc3 Mon Sep 17 00:00:00 2001 From: mdietz Date: Mon, 4 Oct 2010 16:01:44 +0000 Subject: More clean up and conflict resolution --- nova/api/ec2/cloud.py | 2 +- nova/compute/manager.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 2fec49da8..f43da42bd 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -719,7 +719,7 @@ class CloudController(object): changes[field] = kwargs[field] if changes: db_context = {} - internal_id = ec2_id_to_internal_id(ec2_id) + internal_id = self.ec2_id_to_internal_id(ec2_id) inst = db.instance_get_by_internal_id(db_context, internal_id) db.instance_update(db_context, inst['id'], kwargs) return True diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 131fac406..fb0876578 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -174,7 +174,7 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) dev_path = yield self.volume_manager.setup_compute_volume(context, volume_id) - yield self.driver.attach_volume(instance_ref['ec2_id'], + yield self.driver.attach_volume(instance_ref['internal_id'], dev_path, mountpoint) self.db.volume_attached(context, volume_id, instance_id, mountpoint) @@ -189,7 +189,7 @@ class ComputeManager(manager.Manager): volume_id) instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.detach_volume(instance_ref['ec2_id'], + yield self.driver.detach_volume(instance_ref['internal_id'], volume_ref['mountpoint']) self.db.volume_detached(context, volume_id) defer.returnValue(True) -- cgit From 7e66ee636910763630fcf5e6ff23848389713c81 Mon Sep 17 00:00:00 2001 From: mdietz Date: Mon, 4 Oct 2010 17:52:08 +0000 Subject: Accidentally renamed volume related stuff --- nova/compute/manager.py | 4 ++-- nova/db/sqlalchemy/api.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index fb0876578..131fac406 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -174,7 +174,7 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) dev_path = yield self.volume_manager.setup_compute_volume(context, volume_id) - yield self.driver.attach_volume(instance_ref['internal_id'], + yield self.driver.attach_volume(instance_ref['ec2_id'], dev_path, mountpoint) self.db.volume_attached(context, volume_id, instance_id, mountpoint) @@ -189,7 +189,7 @@ class ComputeManager(manager.Manager): volume_id) instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.detach_volume(instance_ref['internal_id'], + yield self.driver.detach_volume(instance_ref['ec2_id'], volume_ref['mountpoint']) self.db.volume_detached(context, volume_id) defer.returnValue(True) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 6bf020aee..f79473c00 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -463,8 +463,8 @@ def instance_get_by_internal_id(context, internal_id): def instance_internal_id_exists(context, internal_id, session=None): if not session: session = get_session() - return session.query(exists().where(models.Instance.id==internal_id) - ).one()[0] + return session.query(exists().where + (models.Instance.internal_id==internal_id)).one()[0] def instance_get_fixed_address(_context, instance_id): -- cgit From 2a8e4a3e818f1d279a886e2e5f5ae49f3de26a4d Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 4 Oct 2010 14:26:55 -0400 Subject: Revert r312 --- nova/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nova/utils.py b/nova/utils.py index 86ff3d22e..5f64b13c4 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -126,9 +126,8 @@ def runthis(prompt, cmd, check_exit_code = True): def generate_uid(topic, size=8): - #TODO(gundlach): we want internal ids to just be ints now. i just dropped - #off a topic prefix, so what have I broken? - return random.randint(0, 2**64-1) + return '%s-%s' % (topic, ''.join([random.choice('01234567890abcdefghijklmnopqrstuvwxyz') for x in xrange(size)])) + def generate_mac(): -- cgit From dd0f365c98ae68afff9a0fbc75e7d5b88499b282 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 4 Oct 2010 16:39:05 -0400 Subject: Fix broken unit tests --- nova/api/ec2/cloud.py | 24 ++++++++++++++---------- nova/db/sqlalchemy/api.py | 3 +++ nova/tests/cloud_unittest.py | 3 ++- nova/utils.py | 11 +++++++++-- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 2fec49da8..7f5f4c4e9 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -72,6 +72,20 @@ def _gen_key(context, user_id, key_name): return {'private_key': private_key, 'fingerprint': fingerprint} +def ec2_id_to_internal_id(ec2_id): + """Convert an ec2 ID (i-[base 36 number]) to an internal id (int)""" + return int(ec2_id[2:], 36) + + +def internal_id_to_ec2_id(internal_id): + """Convert an internal ID (int) to an ec2 ID (i-[base 36 number])""" + digits = [] + while internal_id != 0: + internal_id, remainder = divmod(internal_id, 36) + digits.append('0123456789abcdefghijklmnopqrstuvwxyz'[remainder]) + return "i-%s" % ''.join(reversed(digits)) + + class CloudController(object): """ CloudController provides the critical dispatch between inbound API calls through the endpoint and messages @@ -113,16 +127,6 @@ class CloudController(object): result[key] = [line] return result - def ec2_id_to_internal_id(ec2_id): - """Convert an ec2 ID (i-[base 36 number]) to an internal id (int)""" - # TODO(gundlach): Maybe this should actually work? - return ec2_id[2:] - - def internal_id_to_ec2_id(internal_id): - """Convert an internal ID (int) to an ec2 ID (i-[base 36 number])""" - # TODO(gundlach): Yo maybe this should actually convert to base 36 - return "i-%d" % internal_id - def get_metadata(self, address): instance_ref = db.fixed_ip_get_instance(None, address) if instance_ref is None: diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 6dd6b545a..9d43da3ba 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -377,6 +377,9 @@ def fixed_ip_update(_context, address, values): ################### +#TODO(gundlach): instance_create and volume_create are nearly identical +#and should be refactored. I expect there are other copy-and-paste +#functions between the two of them as well. def instance_create(_context, values): instance_ref = models.Instance() for (key, value) in values.iteritems(): diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index d316db153..615e589cf 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -236,7 +236,8 @@ class CloudTestCase(test.TrialTestCase): def test_update_of_instance_display_fields(self): inst = db.instance_create({}, {}) - self.cloud.update_instance(self.context, inst['internal_id'], + ec2_id = cloud.internal_id_to_ec2_id(inst['internal_id']) + self.cloud.update_instance(self.context, ec2_id, display_name='c00l 1m4g3') inst = db.instance_get({}, inst['id']) self.assertEqual('c00l 1m4g3', inst['display_name']) diff --git a/nova/utils.py b/nova/utils.py index 5f64b13c4..b1699bda8 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -126,8 +126,15 @@ def runthis(prompt, cmd, check_exit_code = True): def generate_uid(topic, size=8): - return '%s-%s' % (topic, ''.join([random.choice('01234567890abcdefghijklmnopqrstuvwxyz') for x in xrange(size)])) - + if topic == "i": + # Instances have integer internal ids. + #TODO(gundlach): We should make this more than 32 bits, but we need to + #figure out how to make the DB happy with 64 bit integers. + return random.randint(0, 2**32-1) + else: + characters = '01234567890abcdefghijklmnopqrstuvwxyz' + choices = [random.choice(characters) for x in xrange(size)] + return '%s-%s' % (topic, ''.join(choices)) def generate_mac(): -- cgit From 32bd6c198a4ed96768649f58628e22fb25a95855 Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Mon, 4 Oct 2010 16:47:08 -0400 Subject: Adds ParallaxClient and TellerClient plumbing for GlanceImageService. Adds stubs FakeParallaxClient and unit tests for LocalImageService and GlanceImageService. --- nova/image/service.py | 99 ++++++++++++++++++++++-- nova/tests/api/rackspace/fakes.py | 74 ++++++++++++++++++ nova/tests/api/rackspace/test_images.py | 132 ++++++++++++++++---------------- 3 files changed, 230 insertions(+), 75 deletions(-) diff --git a/nova/image/service.py b/nova/image/service.py index 4bceab6ee..3b6d3b6e3 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -16,9 +16,14 @@ # under the License. import cPickle as pickle +import httplib +import json import os.path import random import string +import urlparse + +import webob.exc from nova import utils from nova import flags @@ -27,11 +32,11 @@ from nova import flags FLAGS = flags.FLAGS -flags.DEFINE_string('glance_teller_address', '127.0.0.1', +flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1', 'IP address or URL where Glance\'s Teller service resides') flags.DEFINE_string('glance_teller_port', '9191', 'Port for Glance\'s Teller service') -flags.DEFINE_string('glance_parallax_address', '127.0.0.1', +flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1', 'IP address or URL where Glance\'s Parallax service resides') flags.DEFINE_string('glance_parallax_port', '9191', 'Port for Glance\'s Parallax service') @@ -80,21 +85,101 @@ class BaseImageService(object): raise NotImplementedError +class TellerClient(object): + + def __init__(self): + self.address = FLAGS.glance_teller_address + self.port = FLAGS.glance_teller_port + url = urlparse.urlparse(self.address) + self.netloc = url.netloc + self.connection_type = {'http': httplib.HTTPConnection, + 'https': httplib.HTTPSConnection}[url.scheme] + + +class ParallaxClient(object): + + def __init__(self): + self.address = FLAGS.glance_parallax_address + self.port = FLAGS.glance_parallax_port + url = urlparse.urlparse(self.address) + self.netloc = url.netloc + self.connection_type = {'http': httplib.HTTPConnection, + 'https': httplib.HTTPSConnection}[url.scheme] + + def get_images(self): + """ + Returns a list of image data mappings from Parallax + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("GET", "images") + res = c.getresponse() + if res.status == 200: + data = json.loads(res.read()) + return data + else: + # TODO(jaypipes): return or raise HTTP error? + return [] + finally: + c.close() + + def get_image_metadata(self, image_id): + """ + Returns a mapping of image metadata from Parallax + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("GET", "images/%s" % image_id) + res = c.getresponse() + if res.status == 200: + data = json.loads(res.read()) + return data + else: + # TODO(jaypipes): return or raise HTTP error? + return [] + finally: + c.close() + + def add_image_metadata(self, image_metadata): + """ + Tells parallax about an image's metadata + """ + pass + + def update_image_metadata(self, image_id, image_metadata): + """ + Updates Parallax's information about an image + """ + pass + + def delete_image_metadata(self, image_id): + """ + Deletes Parallax's information about an image + """ + pass + + class GlanceImageService(BaseImageService): """Provides storage and retrieval of disk image objects within Glance.""" + def __init__(self): + self.teller = TellerClient() + self.parallax = ParallaxClient() + def index(self): """ Calls out to Parallax for a list of images available """ - raise NotImplementedError + images = self.parallax.get_images() + return images def show(self, id): """ Returns a dict containing image data for the given opaque image id. """ - raise NotImplementedError + image = self.parallax.get_image_metadata(id) + return image def create(self, data): """ @@ -103,7 +188,7 @@ class GlanceImageService(BaseImageService): :raises AlreadyExists if the image already exist. """ - raise NotImplementedError + return self.parallax.add_image_metadata(data) def update(self, image_id, data): """Replace the contents of the given image with the new data. @@ -111,7 +196,7 @@ class GlanceImageService(BaseImageService): :raises NotFound if the image does not exist. """ - raise NotImplementedError + self.parallax.update_image_metadata(image_id, data) def delete(self, image_id): """ @@ -120,7 +205,7 @@ class GlanceImageService(BaseImageService): :raises NotFound if the image does not exist. """ - raise NotImplementedError + self.parallax.delete_image_metadata(image_id) def delete_all(self): """ diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py index 2c4447920..3765b859e 100644 --- a/nova/tests/api/rackspace/fakes.py +++ b/nova/tests/api/rackspace/fakes.py @@ -1,5 +1,24 @@ +# 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. + import datetime import json +import random +import string import webob import webob.dec @@ -7,6 +26,7 @@ import webob.dec from nova import auth from nova import utils from nova import flags +from nova import exception as exc import nova.api.rackspace.auth import nova.api.rackspace._id_translator from nova.image import service @@ -105,6 +125,60 @@ def stub_out_networking(stubs): FLAGS.FAKE_subdomain = 'rs' +def stub_out_glance(stubs): + + class FakeParallaxClient: + + def __init__(self): + self.fixtures = {} + + def fake_get_images(self): + return self.fixtures + + def fake_get_image_metadata(self, image_id): + for k, f in self.fixtures.iteritems(): + if k == image_id: + return f + raise exc.NotFound + + def fake_add_image_metadata(self, image_data): + id = ''.join(random.choice(string.letters) for _ in range(20)) + image_data['id'] = id + self.fixtures[id] = image_data + return id + + def fake_update_image_metadata(self, image_id, image_data): + + if image_id not in self.fixtures.keys(): + raise exc.NotFound + + self.fixtures[image_id].update(image_data) + + def fake_delete_image_metadata(self, image_id): + + if image_id not in self.fixtures.keys(): + raise exc.NotFound + + del self.fixtures[image_id] + + def fake_delete_all(self): + self.fixtures = {} + + fake_parallax_client = FakeParallaxClient() + stubs.Set(nova.image.service.ParallaxClient, 'get_images', + fake_parallax_client.fake_get_images) + stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata', + fake_parallax_client.fake_get_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata', + fake_parallax_client.fake_add_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata', + fake_parallax_client.fake_update_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata', + fake_parallax_client.fake_delete_image_metadata) + stubs.Set(nova.image.service.GlanceImageService, 'delete_all', + fake_parallax_client.fake_delete_all) + + class FakeAuthDatabase(object): data = {} diff --git a/nova/tests/api/rackspace/test_images.py b/nova/tests/api/rackspace/test_images.py index 21dad7648..669346680 100644 --- a/nova/tests/api/rackspace/test_images.py +++ b/nova/tests/api/rackspace/test_images.py @@ -22,79 +22,74 @@ import stubout from nova import utils from nova.api.rackspace import images +from nova.tests.api.rackspace import fakes -#{ Fixtures +class BaseImageServiceTests(): + """Tasks to test for all image services""" -fixture_images = [ - { - 'name': 'image #1', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None}, - { - 'name': 'image #2', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None}, - { - 'name': 'image #3', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None}] + def test_create(self): + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} -#} + num_images = len(self.service.index()) + id = self.service.create(fixture) -class BaseImageServiceTests(): - - """Tasks to test for all image services""" + self.assertNotEquals(None, id) + self.assertEquals(num_images + 1, len(self.service.index())) - def test_create_and_index(self): - for i in fixture_images: - self.service.create(i) + def test_update(self): - self.assertEquals(len(fixture_images), len(self.service.index())) + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} - def test_create_and_update(self): - ids = {} - temp = 0 - for i in fixture_images: - ids[self.service.create(i)] = temp - temp += 1 + id = self.service.create(fixture) - self.assertEquals(len(fixture_images), len(self.service.index())) + fixture['status'] = 'in progress' + + self.service.update(id, fixture) + new_image_data = self.service.show(id) + self.assertEquals('in progress', new_image_data['status']) - for image_id, num in ids.iteritems(): - new_data = fixture_images[num] - new_data['updated'] = 'test' + str(num) - self.service.update(image_id, new_data) + def test_delete(self): - images = self.service.index() + fixtures = [ + {'name': 'test image 1', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}, + {'name': 'test image 2', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}] - for i in images: - self.assertEquals('test' + str(ids[i['id']]), - i['updated']) + ids = [] + for fixture in fixtures: + new_id = self.service.create(fixture) + ids.append(new_id) - def test_create_and_show(self): - ids = {} - temp = 0 - for i in fixture_images: - ids[self.service.create(i)] = temp - temp += 1 + num_images = len(self.service.index()) + self.assertEquals(2, num_images) + + self.service.delete(ids[0]) - for i in fixture_images: - image = self.service.show(i['id']) - index = ids[i['id']] - self.assertEquals(image, fixture_images[index]) + num_images = len(self.service.index()) + self.assertEquals(1, num_images) class LocalImageServiceTest(unittest.TestCase, @@ -111,15 +106,16 @@ class LocalImageServiceTest(unittest.TestCase, self.stubs.UnsetAll() -#class GlanceImageServiceTest(unittest.TestCase, -# BaseImageServiceTests): -# -# """Tests the local image service""" -# -# def setUp(self): -# self.stubs = stubout.StubOutForTesting() -# self.service = utils.import_object('nova.image.service.GlanceImageService') -# -# def tearDown(self): -# self.service.delete_all() -# self.stubs.UnsetAll() +class GlanceImageServiceTest(unittest.TestCase, + BaseImageServiceTests): + + """Tests the local image service""" + + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_glance(self.stubs) + self.service = utils.import_object('nova.image.service.GlanceImageService') + + def tearDown(self): + self.service.delete_all() + self.stubs.UnsetAll() -- cgit From 6bdbb567f1a9e0a8b980ff916183d47375fe11bf Mon Sep 17 00:00:00 2001 From: mdietz Date: Mon, 4 Oct 2010 21:20:33 +0000 Subject: One last bad line --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 3f440c85c..7f5f4c4e9 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -723,7 +723,7 @@ class CloudController(object): changes[field] = kwargs[field] if changes: db_context = {} - internal_id = self.ec2_id_to_internal_id(ec2_id) + internal_id = ec2_id_to_internal_id(ec2_id) inst = db.instance_get_by_internal_id(db_context, internal_id) db.instance_update(db_context, inst['id'], kwargs) return True -- cgit From bf727292794026694c37b84201172b933b41ad2d Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Mon, 4 Oct 2010 17:32:01 -0400 Subject: Update Parallax default port number to match Glance --- nova/image/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/image/service.py b/nova/image/service.py index 3b6d3b6e3..66be669dd 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -38,7 +38,7 @@ flags.DEFINE_string('glance_teller_port', '9191', 'Port for Glance\'s Teller service') flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1', 'IP address or URL where Glance\'s Parallax service resides') -flags.DEFINE_string('glance_parallax_port', '9191', +flags.DEFINE_string('glance_parallax_port', '9292', 'Port for Glance\'s Parallax service') -- cgit From b61f4ceff6ea5dbb4d9c63b9f7345c0b31785984 Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Tue, 5 Oct 2010 13:29:27 -0400 Subject: Adds unit test for calling show() on a non-existing image. Changes return from real Parallax service per sirp's recommendation for actual returned dict() values. --- nova/image/service.py | 36 ++++++++++++++++++++++++-------- nova/tests/api/rackspace/fakes.py | 2 +- nova/tests/api/rackspace/test_images.py | 20 ++++++++++++++++++ nova/tests/api/rackspace/test_servers.py | 1 + 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/nova/image/service.py b/nova/image/service.py index 66be669dd..2e570e8a4 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -18,6 +18,7 @@ import cPickle as pickle import httplib import json +import logging import os.path import random import string @@ -27,6 +28,7 @@ import webob.exc from nova import utils from nova import flags +from nova import exception FLAGS = flags.FLAGS @@ -55,6 +57,8 @@ class BaseImageService(object): def show(self, id): """ Returns a dict containing image data for the given opaque image id. + + :raises NotFound if the image does not exist """ raise NotImplementedError @@ -115,10 +119,12 @@ class ParallaxClient(object): c.request("GET", "images") res = c.getresponse() if res.status == 200: - data = json.loads(res.read()) + # Parallax returns a JSONified dict(images=image_list) + data = json.loads(res.read())['images'] return data else: - # TODO(jaypipes): return or raise HTTP error? + logging.warn("Parallax returned HTTP error %d from " + "request for /images", res.status_int) return [] finally: c.close() @@ -132,11 +138,12 @@ class ParallaxClient(object): c.request("GET", "images/%s" % image_id) res = c.getresponse() if res.status == 200: - data = json.loads(res.read()) + # Parallax returns a JSONified dict(image=image_info) + data = json.loads(res.read())['image'] return data else: - # TODO(jaypipes): return or raise HTTP error? - return [] + # TODO(jaypipes): log the error? + return None finally: c.close() @@ -179,7 +186,9 @@ class GlanceImageService(BaseImageService): Returns a dict containing image data for the given opaque image id. """ image = self.parallax.get_image_metadata(id) - return image + if image: + return image + raise exception.NotFound def create(self, data): """ @@ -236,7 +245,10 @@ class LocalImageService(BaseImageService): return [ self.show(id) for id in self._ids() ] def show(self, id): - return pickle.load(open(self._path_to(id))) + try: + return pickle.load(open(self._path_to(id))) + except IOError: + raise exception.NotFound def create(self, data): """ @@ -249,13 +261,19 @@ class LocalImageService(BaseImageService): def update(self, image_id, data): """Replace the contents of the given image with the new data.""" - pickle.dump(data, open(self._path_to(image_id), 'w')) + try: + pickle.dump(data, open(self._path_to(image_id), 'w')) + except IOError: + raise exception.NotFound def delete(self, image_id): """ Delete the given image. Raises OSError if the image does not exist. """ - os.unlink(self._path_to(image_id)) + try: + os.unlink(self._path_to(image_id)) + except IOError: + raise exception.NotFound def delete_all(self): """ diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py index 3765b859e..c7d9216c8 100644 --- a/nova/tests/api/rackspace/fakes.py +++ b/nova/tests/api/rackspace/fakes.py @@ -139,7 +139,7 @@ def stub_out_glance(stubs): for k, f in self.fixtures.iteritems(): if k == image_id: return f - raise exc.NotFound + return None def fake_add_image_metadata(self, image_data): id = ''.join(random.choice(string.letters) for _ in range(20)) diff --git a/nova/tests/api/rackspace/test_images.py b/nova/tests/api/rackspace/test_images.py index 669346680..a7f320b46 100644 --- a/nova/tests/api/rackspace/test_images.py +++ b/nova/tests/api/rackspace/test_images.py @@ -20,6 +20,7 @@ import unittest import stubout +from nova import exception from nova import utils from nova.api.rackspace import images from nova.tests.api.rackspace import fakes @@ -45,6 +46,25 @@ class BaseImageServiceTests(): self.assertNotEquals(None, id) self.assertEquals(num_images + 1, len(self.service.index())) + def test_create_and_show_non_existing_image(self): + + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} + + num_images = len(self.service.index()) + + id = self.service.create(fixture) + + self.assertNotEquals(None, id) + + self.assertRaises(exception.NotFound, + self.service.show, + 'bad image id') + def test_update(self): fixture = {'name': 'test image', diff --git a/nova/tests/api/rackspace/test_servers.py b/nova/tests/api/rackspace/test_servers.py index 9c1860879..b20a8d432 100644 --- a/nova/tests/api/rackspace/test_servers.py +++ b/nova/tests/api/rackspace/test_servers.py @@ -33,6 +33,7 @@ from nova.tests.api.rackspace import fakes FLAGS = flags.FLAGS +FLAGS.verbose = True def return_server(context, id): return stub_instance(id) -- cgit From 8a2d7efa542e168fda81f703fa8e8c19467bf800 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 5 Oct 2010 13:40:17 -0400 Subject: Fix clause comparing id to internal_id --- nova/db/sqlalchemy/api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 61feed9d0..870e7b1a5 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -663,8 +663,9 @@ def instance_get_by_internal_id(context, internal_id): def instance_internal_id_exists(context, internal_id, session=None): if not session: session = get_session() - return session.query(exists().where(models.Instance.id==internal_id) - ).one()[0] + return session.query( + exists().where(models.Instance.internal_id==internal_id) + ).one()[0] @require_context -- cgit From 091cf4ec5851e87bf722ed0bbbbfdf64dd599389 Mon Sep 17 00:00:00 2001 From: mdietz Date: Tue, 5 Oct 2010 19:52:12 +0000 Subject: A little more clean up --- nova/db/sqlalchemy/api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 7429057ca..6f1ea7c23 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -29,8 +29,7 @@ from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import joinedload_all -from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.orm import joinedload, joinedload_all from sqlalchemy.sql import exists, func FLAGS = flags.FLAGS @@ -451,7 +450,6 @@ def fixed_ip_create(_context, values): fixed_ip_ref.save() return fixed_ip_ref['address'] - @require_context def fixed_ip_disassociate(context, address): session = get_session() -- cgit From c86462d11a6709bf9f2130056bf04712fe3db2d9 Mon Sep 17 00:00:00 2001 From: mdietz Date: Tue, 5 Oct 2010 20:07:11 +0000 Subject: merge prop fixes --- nova/api/rackspace/servers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 868b697e0..5cfb7a431 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -129,7 +129,7 @@ class Controller(wsgi.Controller): def show(self, req, id): """ Returns server details by server id """ user_id = req.environ['nova.context']['user']['id'] - inst = self.db_driver.instance_get_by_internal_id(None, id) + inst = self.db_driver.instance_get_by_internal_id(None, int(id)) if inst: if inst.user_id == user_id: return _entity_detail(inst) @@ -138,7 +138,7 @@ class Controller(wsgi.Controller): def delete(self, req, id): """ Destroys a server """ user_id = req.environ['nova.context']['user']['id'] - instance = self.db_driver.instance_get_by_internal_id(None, id) + instance = self.db_driver.instance_get_by_internal_id(None, int(id)) if instance and instance['user_id'] == user_id: self.db_driver.instance_destroy(None, id) return faults.Fault(exc.HTTPAccepted()) @@ -171,11 +171,11 @@ class Controller(wsgi.Controller): if not inst_dict: return faults.Fault(exc.HTTPUnprocessableEntity()) - instance = self.db_driver.instance_get_by_internal_id(None, id) + instance = self.db_driver.instance_get_by_internal_id(None, int(id)) if not instance or instance.user_id != user_id: return faults.Fault(exc.HTTPNotFound()) - self.db_driver.instance_update(None, id, + self.db_driver.instance_update(None, int(id), _filter_params(inst_dict['server'])) return faults.Fault(exc.HTTPNoContent()) @@ -187,7 +187,7 @@ class Controller(wsgi.Controller): reboot_type = input_dict['reboot']['type'] except Exception: raise faults.Fault(webob.exc.HTTPNotImplemented()) - opaque_id = _instance_id_translator().from_rs_id(id) + opaque_id = _instance_id_translator().from_rs_id(int(id)) cloud.reboot(opaque_id) def _build_server_instance(self, req, env): @@ -257,7 +257,7 @@ class Controller(wsgi.Controller): #TODO(dietz) is this necessary? inst['launch_index'] = 0 - inst['hostname'] = ref.internal_id + inst['hostname'] = str(ref.internal_id) self.db_driver.instance_update(None, inst['id'], inst) network_manager = utils.import_object(FLAGS.network_manager) -- cgit From db620f323c2fc5e65a722a33ae8a42b54817dae1 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Tue, 5 Oct 2010 16:16:42 -0400 Subject: Missed an ec2_id conversion to internal_id --- nova/api/ec2/cloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 7f5f4c4e9..175bb493c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -262,7 +262,7 @@ class CloudController(object): # ec2_id_list is passed in as a list of instances ec2_id = ec2_id_list[0] internal_id = ec2_id_to_internal_id(ec2_id) - instance_ref = db.instance_get_by_ec2_id(context, internal_id) + instance_ref = db.instance_get_by_internal_id(context, internal_id) return rpc.call('%s.%s' % (FLAGS.compute_topic, instance_ref['host']), {"method": "get_console_output", -- cgit From fbd1bc015bd5615963b9073eefb895ea04c55a3e Mon Sep 17 00:00:00 2001 From: "jaypipes@gmail.com" <> Date: Tue, 5 Oct 2010 16:19:55 -0400 Subject: Merge overwrote import_object() load of image service. --- nova/api/rackspace/servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 5cfb7a431..b23867bbf 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -42,7 +42,7 @@ def _instance_id_translator(): def _image_service(): """ Helper method for initializing the image id translator """ - service = nova.image.service.ImageService.load() + service = utils.import_object(FLAGS.image_service) return (service, _id_translator.RackspaceAPIIdTranslator( "image", service.__class__.__name__)) -- cgit From 684c1ed50aebaed07cf89e6f1f7ee189a1b79b9b Mon Sep 17 00:00:00 2001 From: mdietz Date: Tue, 5 Oct 2010 20:43:23 +0000 Subject: Huge sweeping changes --- nova/tests/api/rackspace/test_servers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nova/tests/api/rackspace/test_servers.py b/nova/tests/api/rackspace/test_servers.py index 5a21356eb..92377538b 100644 --- a/nova/tests/api/rackspace/test_servers.py +++ b/nova/tests/api/rackspace/test_servers.py @@ -72,7 +72,7 @@ class ServersTest(unittest.TestCase): req = webob.Request.blank('/v1.0/servers/1') res = req.get_response(nova.api.API()) res_dict = json.loads(res.body) - self.assertEqual(res_dict['server']['id'], '1') + self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server1') def test_get_server_list(self): @@ -93,7 +93,7 @@ class ServersTest(unittest.TestCase): def instance_create(context, inst): class Foo(object): - internal_id = 1 + internal_id = '1' return Foo() def fake_method(*args, **kwargs): -- cgit From 5f40379b407301c0907a72cde988197f3d18ea56 Mon Sep 17 00:00:00 2001 From: mdietz Date: Tue, 5 Oct 2010 21:00:05 +0000 Subject: Merge prop suggestions --- nova/tests/api/rackspace/fakes.py | 16 ---------------- nova/tests/api/rackspace/test_servers.py | 4 +--- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py index 2c4447920..f623524ed 100644 --- a/nova/tests/api/rackspace/fakes.py +++ b/nova/tests/api/rackspace/fakes.py @@ -60,22 +60,6 @@ def stub_out_image_service(stubs): 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 - - def to_rs_id(self, id): - return id - - def from_rs_id(self, id): - return id - - stubs.Set(nova.api.rackspace._id_translator, - 'RackspaceAPIIdTranslator', FakeTranslator) - - def stub_out_auth(stubs): def fake_auth_init(self, app): self.application = app diff --git a/nova/tests/api/rackspace/test_servers.py b/nova/tests/api/rackspace/test_servers.py index 92377538b..7fed0cc26 100644 --- a/nova/tests/api/rackspace/test_servers.py +++ b/nova/tests/api/rackspace/test_servers.py @@ -57,7 +57,6 @@ class ServersTest(unittest.TestCase): fakes.stub_out_networking(self.stubs) fakes.stub_out_rate_limiting(self.stubs) fakes.stub_out_auth(self.stubs) - fakes.stub_out_id_translator(self.stubs) fakes.stub_out_key_pair_funcs(self.stubs) fakes.stub_out_image_service(self.stubs) self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) @@ -93,7 +92,7 @@ class ServersTest(unittest.TestCase): def instance_create(context, inst): class Foo(object): - internal_id = '1' + internal_id = 1 return Foo() def fake_method(*args, **kwargs): @@ -115,7 +114,6 @@ class ServersTest(unittest.TestCase): self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip', fake_method) - fakes.stub_out_id_translator(self.stubs) body = dict(server=dict( name='server_test', imageId=2, flavorId=2, metadata={}, personality = {} -- cgit From 8f524607856dbf4cecf7c7503e53e14c42888307 Mon Sep 17 00:00:00 2001 From: Hisaki Ohara Date: Wed, 6 Oct 2010 18:04:18 +0900 Subject: Defined images_path for nova-compute. Without its setting, it fails to launch instances by exception at _fetch_local_image. --- nova/virt/images.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nova/virt/images.py b/nova/virt/images.py index a60bcc4c1..9ba5b7890 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -26,6 +26,7 @@ import time import urlparse from nova import flags +from nova import utils from nova import process from nova.auth import manager from nova.auth import signer @@ -34,6 +35,8 @@ from nova.auth import signer FLAGS = flags.FLAGS flags.DEFINE_bool('use_s3', True, 'whether to get images from s3 or use local copy') +flags.DEFINE_string('images_path', utils.abspath('../images'), + 'path to decrypted images') def fetch(image, path, user, project): -- cgit From b7028c0d0262d3d4395077a8bd2d95664c6bf16e Mon Sep 17 00:00:00 2001 From: Hisaki Ohara Date: Thu, 7 Oct 2010 23:03:43 +0900 Subject: Imported images_path from nova.objectstore for nova-compute. Without its setting, it fails to launch instances by exception at _fetch_local_image. --- nova/virt/images.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nova/virt/images.py b/nova/virt/images.py index 9ba5b7890..dc50764d9 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -26,17 +26,15 @@ import time import urlparse from nova import flags -from nova import utils from nova import process from nova.auth import manager from nova.auth import signer +from nova.objectstore import image FLAGS = flags.FLAGS flags.DEFINE_bool('use_s3', True, 'whether to get images from s3 or use local copy') -flags.DEFINE_string('images_path', utils.abspath('../images'), - 'path to decrypted images') def fetch(image, path, user, project): -- cgit From db87fd5a8145d045c4767a8d02cde5a0750113f8 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Fri, 8 Oct 2010 12:21:26 -0400 Subject: Remove redis dependency from Images controller. LocalImageService now works with integer ids, so there's no need for the translator. Once Glance exists we'll have to revisit this. --- nova/api/rackspace/backup_schedules.py | 1 - nova/api/rackspace/images.py | 10 +--------- nova/image/service.py | 16 +++++++++------- nova/tests/api/rackspace/fakes.py | 1 - 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py index cb83023bc..9c0d41fa0 100644 --- a/nova/api/rackspace/backup_schedules.py +++ b/nova/api/rackspace/backup_schedules.py @@ -19,7 +19,6 @@ import time from webob import exc from nova import wsgi -from nova.api.rackspace import _id_translator from nova.api.rackspace import faults import nova.image.service diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index d4ab8ce3c..82dcd2049 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -20,7 +20,6 @@ from webob import exc from nova import flags from nova import utils from nova import wsgi -from nova.api.rackspace import _id_translator import nova.api.rackspace import nova.image.service from nova.api.rackspace import faults @@ -41,8 +40,6 @@ class Controller(wsgi.Controller): def __init__(self): self._service = utils.import_object(FLAGS.image_service) - self._id_translator = _id_translator.RackspaceAPIIdTranslator( - "image", self._service.__class__.__name__) def index(self, req): """Return all public images in brief.""" @@ -53,16 +50,11 @@ class Controller(wsgi.Controller): """Return all public images in detail.""" data = self._service.index() data = nova.api.rackspace.limited(data, req) - for img in data: - img['id'] = self._id_translator.to_rs_id(img['id']) return dict(images=data) def show(self, req, id): """Return data about the given image id.""" - opaque_id = self._id_translator.from_rs_id(id) - img = self._service.show(opaque_id) - img['id'] = id - return dict(image=img) + return dict(image=self._service.show(id)) def delete(self, req, id): # Only public images are supported for now. diff --git a/nova/image/service.py b/nova/image/service.py index 2e570e8a4..5276e1312 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -225,7 +225,9 @@ class GlanceImageService(BaseImageService): class LocalImageService(BaseImageService): - """Image service storing images to local disk.""" + """Image service storing images to local disk. + + It assumes that image_ids are integers.""" def __init__(self): self._path = "/tmp/nova/images" @@ -234,12 +236,12 @@ class LocalImageService(BaseImageService): except OSError: # exists pass - def _path_to(self, image_id=''): - return os.path.join(self._path, image_id) + def _path_to(self, image_id): + return os.path.join(self._path, str(image_id)) def _ids(self): """The list of all image ids.""" - return os.listdir(self._path) + return [int(i) for i in os.listdir(self._path)] def index(self): return [ self.show(id) for id in self._ids() ] @@ -254,7 +256,7 @@ class LocalImageService(BaseImageService): """ Store the image data and return the new image id. """ - id = ''.join(random.choice(string.letters) for _ in range(20)) + id = random.randint(0, 2**32-1) data['id'] = id self.update(id, data) return id @@ -279,5 +281,5 @@ class LocalImageService(BaseImageService): """ Clears out all images in local directory """ - for f in os.listdir(self._path): - os.unlink(self._path_to(f)) + for id in self._ids(): + os.unlink(self._path_to(id)) diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py index b5fba2dfa..6a25720a9 100644 --- a/nova/tests/api/rackspace/fakes.py +++ b/nova/tests/api/rackspace/fakes.py @@ -28,7 +28,6 @@ from nova import utils from nova import flags from nova import exception as exc import nova.api.rackspace.auth -import nova.api.rackspace._id_translator from nova.image import service from nova.wsgi import Router -- cgit From f1a48207dfc1948ba847f262d5a4ff825b02202c Mon Sep 17 00:00:00 2001 From: mdietz Date: Fri, 8 Oct 2010 18:56:32 +0000 Subject: Start stripping out the translators --- nova/api/rackspace/servers.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index b23867bbf..8c489ed83 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -25,7 +25,6 @@ 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 @@ -35,11 +34,6 @@ import nova.image.service FLAGS = flags.FLAGS -def _instance_id_translator(): - """ Helper method for initializing an id translator for Rackspace instance - ids """ - return _id_translator.RackspaceAPIIdTranslator( "instance", 'nova') - def _image_service(): """ Helper method for initializing the image id translator """ service = utils.import_object(FLAGS.image_service) @@ -182,13 +176,16 @@ class Controller(wsgi.Controller): def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ + user_id = req.environ['nova.context']['user']['id'] 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(int(id)) - cloud.reboot(opaque_id) + inst_ref = self.db.instance_get_by_internal_id(None, int(id)) + if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): + return faults.Fault(exc.HTTPUnprocessableEntity()) + cloud.reboot(id) def _build_server_instance(self, req, env): """Build instance data structure and save it to the data store.""" -- cgit From 90f38090ecd586a39257b3efd2c86c2c60b7fdb9 Mon Sep 17 00:00:00 2001 From: mdietz Date: Fri, 8 Oct 2010 20:39:00 +0000 Subject: Mass renaming --- nova/api/__init__.py | 4 +- nova/api/openstack/__init__.py | 190 ++++++++++++++++ nova/api/openstack/_id_translator.py | 42 ++++ nova/api/openstack/auth.py | 101 +++++++++ nova/api/openstack/backup_schedules.py | 38 ++++ nova/api/openstack/context.py | 33 +++ nova/api/openstack/faults.py | 62 ++++++ nova/api/openstack/flavors.py | 58 +++++ nova/api/openstack/images.py | 71 ++++++ nova/api/openstack/notes.txt | 23 ++ nova/api/openstack/ratelimiting/__init__.py | 122 ++++++++++ nova/api/openstack/servers.py | 276 +++++++++++++++++++++++ nova/api/openstack/sharedipgroups.py | 20 ++ nova/api/rackspace/__init__.py | 190 ---------------- nova/api/rackspace/_id_translator.py | 42 ---- nova/api/rackspace/auth.py | 101 --------- nova/api/rackspace/backup_schedules.py | 38 ---- nova/api/rackspace/context.py | 33 --- nova/api/rackspace/faults.py | 62 ------ nova/api/rackspace/flavors.py | 58 ----- nova/api/rackspace/images.py | 71 ------ nova/api/rackspace/notes.txt | 23 -- nova/api/rackspace/ratelimiting/__init__.py | 122 ---------- nova/api/rackspace/ratelimiting/tests.py | 237 -------------------- nova/api/rackspace/servers.py | 283 ------------------------ nova/api/rackspace/sharedipgroups.py | 20 -- nova/tests/api/__init__.py | 6 +- nova/tests/api/openstack/__init__.py | 108 +++++++++ nova/tests/api/openstack/fakes.py | 205 +++++++++++++++++ nova/tests/api/openstack/test_auth.py | 108 +++++++++ nova/tests/api/openstack/test_faults.py | 40 ++++ nova/tests/api/openstack/test_flavors.py | 48 ++++ nova/tests/api/openstack/test_images.py | 141 ++++++++++++ nova/tests/api/openstack/test_ratelimiting.py | 237 ++++++++++++++++++++ nova/tests/api/openstack/test_servers.py | 249 +++++++++++++++++++++ nova/tests/api/openstack/test_sharedipgroups.py | 39 ++++ nova/tests/api/rackspace/__init__.py | 108 --------- nova/tests/api/rackspace/fakes.py | 205 ----------------- nova/tests/api/rackspace/test_auth.py | 108 --------- nova/tests/api/rackspace/test_faults.py | 40 ---- nova/tests/api/rackspace/test_flavors.py | 48 ---- nova/tests/api/rackspace/test_images.py | 141 ------------ nova/tests/api/rackspace/test_servers.py | 249 --------------------- nova/tests/api/rackspace/test_sharedipgroups.py | 39 ---- 44 files changed, 2216 insertions(+), 2223 deletions(-) create mode 100644 nova/api/openstack/__init__.py create mode 100644 nova/api/openstack/_id_translator.py create mode 100644 nova/api/openstack/auth.py create mode 100644 nova/api/openstack/backup_schedules.py create mode 100644 nova/api/openstack/context.py create mode 100644 nova/api/openstack/faults.py create mode 100644 nova/api/openstack/flavors.py create mode 100644 nova/api/openstack/images.py create mode 100644 nova/api/openstack/notes.txt create mode 100644 nova/api/openstack/ratelimiting/__init__.py create mode 100644 nova/api/openstack/servers.py create mode 100644 nova/api/openstack/sharedipgroups.py delete mode 100644 nova/api/rackspace/__init__.py delete mode 100644 nova/api/rackspace/_id_translator.py delete mode 100644 nova/api/rackspace/auth.py delete mode 100644 nova/api/rackspace/backup_schedules.py delete mode 100644 nova/api/rackspace/context.py delete mode 100644 nova/api/rackspace/faults.py delete mode 100644 nova/api/rackspace/flavors.py delete mode 100644 nova/api/rackspace/images.py delete mode 100644 nova/api/rackspace/notes.txt delete mode 100644 nova/api/rackspace/ratelimiting/__init__.py delete mode 100644 nova/api/rackspace/ratelimiting/tests.py delete mode 100644 nova/api/rackspace/servers.py delete mode 100644 nova/api/rackspace/sharedipgroups.py create mode 100644 nova/tests/api/openstack/__init__.py create mode 100644 nova/tests/api/openstack/fakes.py create mode 100644 nova/tests/api/openstack/test_auth.py create mode 100644 nova/tests/api/openstack/test_faults.py create mode 100644 nova/tests/api/openstack/test_flavors.py create mode 100644 nova/tests/api/openstack/test_images.py create mode 100644 nova/tests/api/openstack/test_ratelimiting.py create mode 100644 nova/tests/api/openstack/test_servers.py create mode 100644 nova/tests/api/openstack/test_sharedipgroups.py delete mode 100644 nova/tests/api/rackspace/__init__.py delete mode 100644 nova/tests/api/rackspace/fakes.py delete mode 100644 nova/tests/api/rackspace/test_auth.py delete mode 100644 nova/tests/api/rackspace/test_faults.py delete mode 100644 nova/tests/api/rackspace/test_flavors.py delete mode 100644 nova/tests/api/rackspace/test_images.py delete mode 100644 nova/tests/api/rackspace/test_servers.py delete mode 100644 nova/tests/api/rackspace/test_sharedipgroups.py diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 744abd621..627883018 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -27,7 +27,7 @@ from nova import flags from nova import wsgi from nova.api import cloudpipe from nova.api import ec2 -from nova.api import rackspace +from nova.api import openstack from nova.api.ec2 import metadatarequesthandler @@ -57,7 +57,7 @@ class API(wsgi.Router): mapper.sub_domains = True mapper.connect("/", controller=self.rsapi_versions, conditions=rsdomain) - mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API(), + mapper.connect("/v1.0/{path_info:.*}", controller=openstack.API(), conditions=rsdomain) mapper.connect("/", controller=self.ec2api_versions, diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py new file mode 100644 index 000000000..5e81ba2bd --- /dev/null +++ b/nova/api/openstack/__init__.py @@ -0,0 +1,190 @@ +# 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. + +""" +WSGI middleware for OpenStack API controllers. +""" + +import json +import time + +import routes +import webob.dec +import webob.exc +import webob + +from nova import flags +from nova import utils +from nova import wsgi +from nova.api.openstack import faults +from nova.api.openstack import backup_schedules +from nova.api.openstack import flavors +from nova.api.openstack import images +from nova.api.openstack import ratelimiting +from nova.api.openstack import servers +from nova.api.openstack import sharedipgroups +from nova.auth import manager + + +FLAGS = flags.FLAGS +flags.DEFINE_string('nova_api_auth', + 'nova.api.openstack.auth.BasicApiAuthManager', + 'The auth mechanism to use for the OpenStack API implemenation') + +class API(wsgi.Middleware): + """WSGI entry point for all OpenStack API requests.""" + + def __init__(self): + app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) + super(API, self).__init__(app) + +class AuthMiddleware(wsgi.Middleware): + """Authorize the openstack API request or return an HTTP Forbidden.""" + + def __init__(self, application): + self.auth_driver = utils.import_class(FLAGS.nova_api_auth)() + super(AuthMiddleware, self).__init__(application) + + @webob.dec.wsgify + def __call__(self, req): + if not req.headers.has_key("X-Auth-Token"): + return self.auth_driver.authenticate(req) + + user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) + + if not user: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + if not req.environ.has_key('nova.context'): + req.environ['nova.context'] = {} + req.environ['nova.context']['user'] = user + return self.application + +class RateLimitingMiddleware(wsgi.Middleware): + """Rate limit incoming requests according to the OpenStack rate limits.""" + + def __init__(self, application, service_host=None): + """Create a rate limiting middleware that wraps the given application. + + By default, rate counters are stored in memory. If service_host is + specified, the middleware instead relies on the ratelimiting.WSGIApp + at the given host+port to keep rate counters. + """ + super(RateLimitingMiddleware, self).__init__(application) + if not service_host: + #TODO(gundlach): These limits were based on limitations of Cloud + #Servers. We should revisit them in Nova. + self.limiter = ratelimiting.Limiter(limits={ + 'DELETE': (100, ratelimiting.PER_MINUTE), + 'PUT': (10, ratelimiting.PER_MINUTE), + 'POST': (10, ratelimiting.PER_MINUTE), + 'POST servers': (50, ratelimiting.PER_DAY), + 'GET changes-since': (3, ratelimiting.PER_MINUTE), + }) + else: + self.limiter = ratelimiting.WSGIAppProxy(service_host) + + @webob.dec.wsgify + def __call__(self, req): + """Rate limit the request. + + If the request should be rate limited, return a 413 status with a + Retry-After header giving the time when the request would succeed. + """ + username = req.headers['X-Auth-User'] + action_name = self.get_action_name(req) + if not action_name: # not rate limited + return self.application + delay = self.get_delay(action_name, username) + if delay: + # TODO(gundlach): Get the retry-after format correct. + exc = webob.exc.HTTPRequestEntityTooLarge( + explanation='Too many requests.', + headers={'Retry-After': time.time() + delay}) + raise faults.Fault(exc) + return self.application + + def get_delay(self, action_name, username): + """Return the delay for the given action and username, or None if + the action would not be rate limited. + """ + if action_name == 'POST servers': + # "POST servers" is a POST, so it counts against "POST" too. + # Attempt the "POST" first, lest we are rate limited by "POST" but + # use up a precious "POST servers" call. + delay = self.limiter.perform("POST", username=username) + if delay: + return delay + return self.limiter.perform(action_name, username=username) + + def get_action_name(self, req): + """Return the action name for this request.""" + if req.method == 'GET' and 'changes-since' in req.GET: + return 'GET changes-since' + if req.method == 'POST' and req.path_info.startswith('/servers'): + return 'POST servers' + if req.method in ['PUT', 'POST', 'DELETE']: + return req.method + return None + + +class APIRouter(wsgi.Router): + """ + Routes requests on the OpenStack API to the appropriate controller + and method. + """ + + def __init__(self): + mapper = routes.Mapper() + mapper.resource("server", "servers", controller=servers.Controller(), + collection={ 'detail': 'GET'}, + member={'action':'POST'}) + + mapper.resource("backup_schedule", "backup_schedules", + controller=backup_schedules.Controller(), + parent_resource=dict(member_name='server', + collection_name = 'servers')) + + mapper.resource("image", "images", controller=images.Controller(), + collection={'detail': 'GET'}) + mapper.resource("flavor", "flavors", controller=flavors.Controller(), + collection={'detail': 'GET'}) + mapper.resource("sharedipgroup", "sharedipgroups", + controller=sharedipgroups.Controller()) + + super(APIRouter, self).__init__(mapper) + + +def limited(items, req): + """Return a slice of items according to requested offset and limit. + + items - a sliceable + req - wobob.Request possibly containing offset and limit GET variables. + offset is where to start in the list, and limit is the maximum number + of items to return. + + If limit is not specified, 0, or > 1000, defaults to 1000. + """ + offset = int(req.GET.get('offset', 0)) + limit = int(req.GET.get('limit', 0)) + if not limit: + limit = 1000 + limit = min(1000, limit) + range_end = offset + limit + return items[offset:range_end] + diff --git a/nova/api/openstack/_id_translator.py b/nova/api/openstack/_id_translator.py new file mode 100644 index 000000000..333aa8434 --- /dev/null +++ b/nova/api/openstack/_id_translator.py @@ -0,0 +1,42 @@ +from nova import datastore + +class RackspaceAPIIdTranslator(object): + """ + Converts Rackspace API ids to and from the id format for a given + strategy. + """ + + def __init__(self, id_type, service_name): + """ + Creates a translator for ids of the given type (e.g. 'flavor'), for the + given storage service backend class name (e.g. 'LocalFlavorService'). + """ + + self._store = datastore.Redis.instance() + key_prefix = "rsapi.idtranslator.%s.%s" % (id_type, service_name) + # Forward (strategy format -> RS format) and reverse translation keys + self._fwd_key = "%s.fwd" % key_prefix + self._rev_key = "%s.rev" % key_prefix + + def to_rs_id(self, opaque_id): + """Convert an id from a strategy-specific one to a Rackspace one.""" + result = self._store.hget(self._fwd_key, str(opaque_id)) + if result: # we have a mapping from opaque to RS for this strategy + return int(result) + else: + # Store the mapping. + nextid = self._store.incr("%s.lastid" % self._fwd_key) + if self._store.hsetnx(self._fwd_key, str(opaque_id), nextid): + # If someone else didn't beat us to it, store the reverse + # mapping as well. + self._store.hset(self._rev_key, nextid, str(opaque_id)) + return nextid + else: + # Someone beat us to it; use their number instead, and + # discard nextid (which is OK -- we don't require that + # every int id be used.) + return int(self._store.hget(self._fwd_key, str(opaque_id))) + + def from_rs_id(self, rs_id): + """Convert a Rackspace id to a strategy-specific one.""" + return self._store.hget(self._rev_key, rs_id) diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py new file mode 100644 index 000000000..4c909293e --- /dev/null +++ b/nova/api/openstack/auth.py @@ -0,0 +1,101 @@ +import datetime +import hashlib +import json +import time + +import webob.exc +import webob.dec + +from nova import auth +from nova import db +from nova import flags +from nova import manager +from nova import utils +from nova.api.openstack import faults + +FLAGS = flags.FLAGS + +class Context(object): + pass + +class BasicApiAuthManager(object): + """ Implements a somewhat rudimentary version of OpenStack Auth""" + + def __init__(self, host=None, db_driver=None): + if not host: + host = FLAGS.host + self.host = host + if not db_driver: + db_driver = FLAGS.db_driver + self.db = utils.import_object(db_driver) + self.auth = auth.manager.AuthManager() + self.context = Context() + super(BasicApiAuthManager, self).__init__() + + def authenticate(self, req): + # Unless the request is explicitly made against // don't + # honor it + path_info = req.path_info + if len(path_info) > 1: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + try: + username, key = req.headers['X-Auth-User'], \ + req.headers['X-Auth-Key'] + except KeyError: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] + token, user = self._authorize_user(username, key) + if user and token: + res = webob.Response() + res.headers['X-Auth-Token'] = token['token_hash'] + res.headers['X-Server-Management-Url'] = \ + token['server_management_url'] + res.headers['X-Storage-Url'] = token['storage_url'] + res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] + res.content_type = 'text/plain' + res.status = '204' + return res + else: + return faults.Fault(webob.exc.HTTPUnauthorized()) + + def authorize_token(self, token_hash): + """ retrieves user information from the datastore given a token + + If the token has expired, returns None + If the token is not found, returns None + Otherwise returns the token + + This method will also remove the token if the timestamp is older than + 2 days ago. + """ + token = self.db.auth_get_token(self.context, token_hash) + if token: + delta = datetime.datetime.now() - token['created_at'] + if delta.days >= 2: + self.db.auth_destroy_token(self.context, token) + else: + user = self.auth.get_user(token['user_id']) + return { 'id':user['uid'] } + return None + + def _authorize_user(self, username, key): + """ Generates a new token and assigns it to a user """ + user = self.auth.get_user_from_access_key(key) + if user and user['name'] == username: + token_hash = hashlib.sha1('%s%s%f' % (username, key, + time.time())).hexdigest() + token = {} + token['token_hash'] = token_hash + token['cdn_management_url'] = '' + token['server_management_url'] = self._get_server_mgmt_url() + token['storage_url'] = '' + token['user_id'] = user['uid'] + self.db.auth_create_token(self.context, token) + return token, user + return None, None + + def _get_server_mgmt_url(self): + return 'https://%s/v1.0/' % self.host + diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py new file mode 100644 index 000000000..76ad6ef87 --- /dev/null +++ b/nova/api/openstack/backup_schedules.py @@ -0,0 +1,38 @@ +# 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. + +import time +from webob import exc + +from nova import wsgi +from nova.api.openstack import faults +import nova.image.service + +class Controller(wsgi.Controller): + def __init__(self): + pass + + def index(self, req, server_id): + return faults.Fault(exc.HTTPNotFound()) + + def create(self, req, server_id): + """ No actual update method required, since the existing API allows + both create and update through a POST """ + return faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, server_id): + return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/context.py b/nova/api/openstack/context.py new file mode 100644 index 000000000..77394615b --- /dev/null +++ b/nova/api/openstack/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/openstack/faults.py b/nova/api/openstack/faults.py new file mode 100644 index 000000000..32e5c866f --- /dev/null +++ b/nova/api/openstack/faults.py @@ -0,0 +1,62 @@ +# 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. + + +import webob.dec +import webob.exc + +from nova import wsgi + + +class Fault(webob.exc.HTTPException): + + """An RS API fault response.""" + + _fault_names = { + 400: "badRequest", + 401: "unauthorized", + 403: "resizeNotAllowed", + 404: "itemNotFound", + 405: "badMethod", + 409: "inProgress", + 413: "overLimit", + 415: "badMediaType", + 501: "notImplemented", + 503: "serviceUnavailable"} + + def __init__(self, exception): + """Create a Fault for the given webob.exc.exception.""" + self.wrapped_exc = exception + + @webob.dec.wsgify + def __call__(self, req): + """Generate a WSGI response based on the exception passed to ctor.""" + # Replace the body with fault details. + code = self.wrapped_exc.status_int + fault_name = self._fault_names.get(code, "cloudServersFault") + fault_data = { + fault_name: { + 'code': code, + 'message': self.wrapped_exc.explanation}} + if code == 413: + retry = self.wrapped_exc.headers['Retry-After'] + fault_data[fault_name]['retryAfter'] = retry + # 'code' is an attribute on the fault tag itself + metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} + serializer = wsgi.Serializer(req.environ, metadata) + self.wrapped_exc.body = serializer.to_content_type(fault_data) + return self.wrapped_exc diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py new file mode 100644 index 000000000..793984a5d --- /dev/null +++ b/nova/api/openstack/flavors.py @@ -0,0 +1,58 @@ +# 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. + +from webob import exc + +from nova.api.openstack import faults +from nova.compute import instance_types +from nova import wsgi +import nova.api.openstack + +class Controller(wsgi.Controller): + """Flavor controller for the OpenStack API.""" + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "flavor": [ "id", "name", "ram", "disk" ] + } + } + } + + def index(self, req): + """Return all flavors in brief.""" + return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) + for flavor in self.detail(req)['flavors']]) + + def detail(self, req): + """Return all flavors in detail.""" + items = [self.show(req, id)['flavor'] for id in self._all_ids()] + items = nova.api.openstack.limited(items, req) + return dict(flavors=items) + + def show(self, req, id): + """Return data about the given flavor id.""" + for name, val in instance_types.INSTANCE_TYPES.iteritems(): + if val['flavorid'] == int(id): + item = dict(ram=val['memory_mb'], disk=val['local_gb'], + id=val['flavorid'], name=name) + return dict(flavor=item) + raise faults.Fault(exc.HTTPNotFound()) + + def _all_ids(self): + """Return the list of all flavorids.""" + return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()] diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py new file mode 100644 index 000000000..aa438739c --- /dev/null +++ b/nova/api/openstack/images.py @@ -0,0 +1,71 @@ +# 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. + +from webob import exc + +from nova import flags +from nova import utils +from nova import wsgi +import nova.api.openstack +import nova.image.service +from nova.api.openstack import faults + + +FLAGS = flags.FLAGS + +class Controller(wsgi.Controller): + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "image": [ "id", "name", "updated", "created", "status", + "serverId", "progress" ] + } + } + } + + def __init__(self): + self._service = utils.import_object(FLAGS.image_service) + + def index(self, req): + """Return all public images in brief.""" + return dict(images=[dict(id=img['id'], name=img['name']) + for img in self.detail(req)['images']]) + + def detail(self, req): + """Return all public images in detail.""" + data = self._service.index() + data = nova.api.openstack.limited(data, req) + return dict(images=data) + + def show(self, req, id): + """Return data about the given image id.""" + return dict(image=self._service.show(id)) + + def delete(self, req, id): + # Only public images are supported for now. + raise faults.Fault(exc.HTTPNotFound()) + + def create(self, req): + # Only public images are supported for now, so a request to + # make a backup of a server cannot be supproted. + raise faults.Fault(exc.HTTPNotFound()) + + def update(self, req, id): + # Users may not modify public images, and that's all that + # we support for now. + raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/notes.txt b/nova/api/openstack/notes.txt new file mode 100644 index 000000000..2330f1002 --- /dev/null +++ b/nova/api/openstack/notes.txt @@ -0,0 +1,23 @@ +We will need: + +ImageService +a service that can do crud on image information. not user-specific. opaque +image ids. + +GlanceImageService(ImageService): +image ids are URIs. + +LocalImageService(ImageService): +image ids are random strings. + +OpenstackAPITranslationStore: +translates RS server/images/flavor/etc ids into formats required +by a given ImageService strategy. + +api.openstack.images.Controller: +uses an ImageService strategy behind the scenes to do its fetching; it just +converts int image id into a strategy-specific image id. + +who maintains the mapping from user to [images he owns]? nobody, because +we have no way of enforcing access to his images, without kryptex which +won't be in Austin. diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py new file mode 100644 index 000000000..f843bac0f --- /dev/null +++ b/nova/api/openstack/ratelimiting/__init__.py @@ -0,0 +1,122 @@ +"""Rate limiting of arbitrary actions.""" + +import httplib +import time +import urllib +import webob.dec +import webob.exc + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + +class Limiter(object): + + """Class providing rate limiting of arbitrary actions.""" + + def __init__(self, limits): + """Create a rate limiter. + + limits: a dict mapping from action name to a tuple. The tuple contains + the number of times the action may be performed, and the time period + (in seconds) during which the number must not be exceeded for this + action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would + allow 10 'reboot' actions per minute. + """ + self.limits = limits + self._levels = {} + + def perform(self, action_name, username='nobody'): + """Attempt to perform an action by the given username. + + action_name: the string name of the action to perform. This must + be a key in the limits dict passed to the ctor. + + username: an optional string name of the user performing the action. + Each user has her own set of rate limiting counters. Defaults to + 'nobody' (so that if you never specify a username when calling + perform(), a single set of counters will be used.) + + Return None if the action may proceed. If the action may not proceed + because it has been rate limited, return the float number of seconds + until the action would succeed. + """ + # Think of rate limiting as a bucket leaking water at 1cc/second. The + # bucket can hold as many ccs as there are seconds in the rate + # limiting period (e.g. 3600 for per-hour ratelimits), and if you can + # perform N actions in that time, each action fills the bucket by + # 1/Nth of its volume. You may only perform an action if the bucket + # would not overflow. + now = time.time() + key = '%s:%s' % (username, action_name) + last_time_performed, water_level = self._levels.get(key, (now, 0)) + # The bucket leaks 1cc/second. + water_level -= (now - last_time_performed) + if water_level < 0: + water_level = 0 + num_allowed_per_period, period_in_secs = self.limits[action_name] + # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. + capacity = period_in_secs + new_level = water_level + (capacity * 1.0 / num_allowed_per_period) + if new_level > capacity: + # Delay this many seconds. + return new_level - capacity + self._levels[key] = (now, new_level) + return None + + +# If one instance of this WSGIApps is unable to handle your load, put a +# sharding app in front that shards by username to one of many backends. + +class WSGIApp(object): + + """Application that tracks rate limits in memory. Send requests to it of + this form: + + POST /limiter// + + and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header + containing the number of seconds to wait before the action would succeed. + """ + + def __init__(self, limiter): + """Create the WSGI application using the given Limiter instance.""" + self.limiter = limiter + + @webob.dec.wsgify + def __call__(self, req): + parts = req.path_info.split('/') + # format: /limiter// + if req.method != 'POST': + raise webob.exc.HTTPMethodNotAllowed() + if len(parts) != 4 or parts[1] != 'limiter': + raise webob.exc.HTTPNotFound() + username = parts[2] + action_name = urllib.unquote(parts[3]) + delay = self.limiter.perform(action_name, username) + if delay: + return webob.exc.HTTPForbidden( + headers={'X-Wait-Seconds': "%.2f" % delay}) + else: + return '' # 200 OK + + +class WSGIAppProxy(object): + + """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" + + def __init__(self, service_host): + """Creates a proxy pointing to a ratelimiting.WSGIApp at the given + host.""" + self.service_host = service_host + + def perform(self, action, username='nobody'): + conn = httplib.HTTPConnection(self.service_host) + conn.request('POST', '/limiter/%s/%s' % (username, action)) + resp = conn.getresponse() + if resp.status == 200: + return None # no delay + return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py new file mode 100644 index 000000000..f234af7de --- /dev/null +++ b/nova/api/openstack/servers.py @@ -0,0 +1,276 @@ +# 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. + +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.openstack import context +from nova.api.openstack import faults +from nova.compute import instance_types +from nova.compute import power_state +import nova.api.openstack +import nova.image.service + +FLAGS = flags.FLAGS + +def _filter_params(inst_dict): + """ Extracts all updatable parameters for a server update request """ + keys = dict(name='name', admin_pass='adminPass') + new_attrs = {} + 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): + """ Coerces a list of servers into proper dictionary format """ + return dict(servers=entities) + +def _entity_detail(inst): + """ Maps everything to valid attributes for return""" + power_mapping = { + power_state.NOSTATE: 'build', + power_state.RUNNING: 'active', + power_state.BLOCKED: 'active', + power_state.PAUSED: 'suspended', + power_state.SHUTDOWN: 'active', + power_state.SHUTOFF: 'active', + power_state.CRASHED: 'error' + } + inst_dict = {} + + mapped_keys = dict(status='state', imageId='image_id', + flavorId='instance_type', name='server_name', id='id') + + for k, v in mapped_keys.iteritems(): + inst_dict[k] = inst[v] + + inst_dict['status'] = power_mapping[inst_dict['status']] + inst_dict['addresses'] = dict(public=[], private=[]) + inst_dict['metadata'] = {} + inst_dict['hostId'] = '' + + return dict(server=inst_dict) + +def _entity_inst(inst): + """ Filters all model attributes save for id and name """ + return dict(server=dict(id=inst['id'], name=inst['server_name'])) + +class Controller(wsgi.Controller): + """ The Server API controller for the OpenStack API """ + + _serialization_metadata = { + 'application/xml': { + "attributes": { + "server": [ "id", "imageId", "name", "flavorId", "hostId", + "status", "progress", "progress" ] + } + } + } + + def __init__(self, db_driver=None): + if not db_driver: + db_driver = FLAGS.db_driver + self.db_driver = utils.import_object(db_driver) + super(Controller, self).__init__() + + def index(self, req): + """ Returns a list of server names and ids for a given user """ + return self._items(req, entity_maker=_entity_inst) + + def detail(self, req): + """ Returns a list of server details for a given user """ + return self._items(req, entity_maker=_entity_detail) + + def _items(self, req, entity_maker): + """Returns a list of servers for a given user. + + entity_maker - either _entity_detail or _entity_inst + """ + user_id = req.environ['nova.context']['user']['id'] + instance_list = self.db_driver.instance_get_all_by_user(None, user_id) + limited_list = nova.api.openstack.limited(instance_list, req) + res = [entity_maker(inst)['server'] for inst in limited_list] + return _entity_list(res) + + def show(self, req, id): + """ Returns server details by server id """ + user_id = req.environ['nova.context']['user']['id'] + inst = self.db_driver.instance_get_by_internal_id(None, int(id)) + if inst: + if inst.user_id == user_id: + return _entity_detail(inst) + raise faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, id): + """ Destroys a server """ + user_id = req.environ['nova.context']['user']['id'] + instance = self.db_driver.instance_get_by_internal_id(None, int(id)) + if instance and instance['user_id'] == user_id: + self.db_driver.instance_destroy(None, id) + return faults.Fault(exc.HTTPAccepted()) + return faults.Fault(exc.HTTPNotFound()) + + def create(self, req): + """ Creates a new server for a given user """ + + env = self._deserialize(req.body, req) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + #try: + inst = self._build_server_instance(req, env) + #except Exception, e: + # return faults.Fault(exc.HTTPUnprocessableEntity()) + + rpc.cast( + FLAGS.compute_topic, { + "method": "run_instance", + "args": {"instance_id": inst['id']}}) + return _entity_inst(inst) + + def update(self, req, id): + """ Updates the server name or password """ + 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_by_internal_id(None, int(id)) + if not instance or instance.user_id != user_id: + return faults.Fault(exc.HTTPNotFound()) + + self.db_driver.instance_update(None, int(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 """ + user_id = req.environ['nova.context']['user']['id'] + input_dict = self._deserialize(req.body, req) + try: + reboot_type = input_dict['reboot']['type'] + except Exception: + raise faults.Fault(webob.exc.HTTPNotImplemented()) + inst_ref = self.db.instance_get_by_internal_id(None, int(id)) + if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): + return faults.Fault(exc.HTTPUnprocessableEntity()) + cloud.reboot(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 = {} + + 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'] + + img_service = utils.import_object(FLAGS.image_service) + + image = img_service.show(image_id) + + if not image: + raise Exception, "Image not found" + + inst['server_name'] = env['server']['name'] + inst['image_id'] = 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['display_name'] = env['server']['name'] + inst['display_description'] = env['server']['name'] + + #TODO(dietz) this may be ill advised + key_pair_ref = self.db_driver.key_pair_get_all_by_user( + None, user_id)[0] + + 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.internal_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'] = str(ref.internal_id) + self.db_driver.instance_update(None, inst['id'], inst) + + network_manager = utils.import_object(FLAGS.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/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py new file mode 100644 index 000000000..4d2d0ede1 --- /dev/null +++ b/nova/api/openstack/sharedipgroups.py @@ -0,0 +1,20 @@ +# 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. + +from nova import wsgi + +class Controller(wsgi.Controller): pass diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py deleted file mode 100644 index 89a4693ad..000000000 --- a/nova/api/rackspace/__init__.py +++ /dev/null @@ -1,190 +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. - -""" -WSGI middleware for Rackspace API controllers. -""" - -import json -import time - -import routes -import webob.dec -import webob.exc -import webob - -from nova import flags -from nova import utils -from nova import wsgi -from nova.api.rackspace import faults -from nova.api.rackspace import backup_schedules -from nova.api.rackspace import flavors -from nova.api.rackspace import images -from nova.api.rackspace import ratelimiting -from nova.api.rackspace import servers -from nova.api.rackspace import sharedipgroups -from nova.auth import manager - - -FLAGS = flags.FLAGS -flags.DEFINE_string('nova_api_auth', - 'nova.api.rackspace.auth.BasicApiAuthManager', - 'The auth mechanism to use for the Rackspace API implemenation') - -class API(wsgi.Middleware): - """WSGI entry point for all Rackspace API requests.""" - - def __init__(self): - app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) - super(API, self).__init__(app) - -class AuthMiddleware(wsgi.Middleware): - """Authorize the rackspace API request or return an HTTP Forbidden.""" - - def __init__(self, application): - self.auth_driver = utils.import_class(FLAGS.nova_api_auth)() - super(AuthMiddleware, self).__init__(application) - - @webob.dec.wsgify - def __call__(self, req): - if not req.headers.has_key("X-Auth-Token"): - return self.auth_driver.authenticate(req) - - user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) - - if not user: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - if not req.environ.has_key('nova.context'): - req.environ['nova.context'] = {} - req.environ['nova.context']['user'] = user - return self.application - -class RateLimitingMiddleware(wsgi.Middleware): - """Rate limit incoming requests according to the OpenStack rate limits.""" - - def __init__(self, application, service_host=None): - """Create a rate limiting middleware that wraps the given application. - - By default, rate counters are stored in memory. If service_host is - specified, the middleware instead relies on the ratelimiting.WSGIApp - at the given host+port to keep rate counters. - """ - super(RateLimitingMiddleware, self).__init__(application) - if not service_host: - #TODO(gundlach): These limits were based on limitations of Cloud - #Servers. We should revisit them in Nova. - self.limiter = ratelimiting.Limiter(limits={ - 'DELETE': (100, ratelimiting.PER_MINUTE), - 'PUT': (10, ratelimiting.PER_MINUTE), - 'POST': (10, ratelimiting.PER_MINUTE), - 'POST servers': (50, ratelimiting.PER_DAY), - 'GET changes-since': (3, ratelimiting.PER_MINUTE), - }) - else: - self.limiter = ratelimiting.WSGIAppProxy(service_host) - - @webob.dec.wsgify - def __call__(self, req): - """Rate limit the request. - - If the request should be rate limited, return a 413 status with a - Retry-After header giving the time when the request would succeed. - """ - username = req.headers['X-Auth-User'] - action_name = self.get_action_name(req) - if not action_name: # not rate limited - return self.application - delay = self.get_delay(action_name, username) - if delay: - # TODO(gundlach): Get the retry-after format correct. - exc = webob.exc.HTTPRequestEntityTooLarge( - explanation='Too many requests.', - headers={'Retry-After': time.time() + delay}) - raise faults.Fault(exc) - return self.application - - def get_delay(self, action_name, username): - """Return the delay for the given action and username, or None if - the action would not be rate limited. - """ - if action_name == 'POST servers': - # "POST servers" is a POST, so it counts against "POST" too. - # Attempt the "POST" first, lest we are rate limited by "POST" but - # use up a precious "POST servers" call. - delay = self.limiter.perform("POST", username=username) - if delay: - return delay - return self.limiter.perform(action_name, username=username) - - def get_action_name(self, req): - """Return the action name for this request.""" - if req.method == 'GET' and 'changes-since' in req.GET: - return 'GET changes-since' - if req.method == 'POST' and req.path_info.startswith('/servers'): - return 'POST servers' - if req.method in ['PUT', 'POST', 'DELETE']: - return req.method - return None - - -class APIRouter(wsgi.Router): - """ - Routes requests on the Rackspace API to the appropriate controller - and method. - """ - - def __init__(self): - mapper = routes.Mapper() - mapper.resource("server", "servers", controller=servers.Controller(), - collection={ 'detail': 'GET'}, - member={'action':'POST'}) - - mapper.resource("backup_schedule", "backup_schedules", - controller=backup_schedules.Controller(), - parent_resource=dict(member_name='server', - collection_name = 'servers')) - - mapper.resource("image", "images", controller=images.Controller(), - collection={'detail': 'GET'}) - mapper.resource("flavor", "flavors", controller=flavors.Controller(), - collection={'detail': 'GET'}) - mapper.resource("sharedipgroup", "sharedipgroups", - controller=sharedipgroups.Controller()) - - super(APIRouter, self).__init__(mapper) - - -def limited(items, req): - """Return a slice of items according to requested offset and limit. - - items - a sliceable - req - wobob.Request possibly containing offset and limit GET variables. - offset is where to start in the list, and limit is the maximum number - of items to return. - - If limit is not specified, 0, or > 1000, defaults to 1000. - """ - offset = int(req.GET.get('offset', 0)) - limit = int(req.GET.get('limit', 0)) - if not limit: - limit = 1000 - limit = min(1000, limit) - range_end = offset + limit - return items[offset:range_end] - diff --git a/nova/api/rackspace/_id_translator.py b/nova/api/rackspace/_id_translator.py deleted file mode 100644 index 333aa8434..000000000 --- a/nova/api/rackspace/_id_translator.py +++ /dev/null @@ -1,42 +0,0 @@ -from nova import datastore - -class RackspaceAPIIdTranslator(object): - """ - Converts Rackspace API ids to and from the id format for a given - strategy. - """ - - def __init__(self, id_type, service_name): - """ - Creates a translator for ids of the given type (e.g. 'flavor'), for the - given storage service backend class name (e.g. 'LocalFlavorService'). - """ - - self._store = datastore.Redis.instance() - key_prefix = "rsapi.idtranslator.%s.%s" % (id_type, service_name) - # Forward (strategy format -> RS format) and reverse translation keys - self._fwd_key = "%s.fwd" % key_prefix - self._rev_key = "%s.rev" % key_prefix - - def to_rs_id(self, opaque_id): - """Convert an id from a strategy-specific one to a Rackspace one.""" - result = self._store.hget(self._fwd_key, str(opaque_id)) - if result: # we have a mapping from opaque to RS for this strategy - return int(result) - else: - # Store the mapping. - nextid = self._store.incr("%s.lastid" % self._fwd_key) - if self._store.hsetnx(self._fwd_key, str(opaque_id), nextid): - # If someone else didn't beat us to it, store the reverse - # mapping as well. - self._store.hset(self._rev_key, nextid, str(opaque_id)) - return nextid - else: - # Someone beat us to it; use their number instead, and - # discard nextid (which is OK -- we don't require that - # every int id be used.) - return int(self._store.hget(self._fwd_key, str(opaque_id))) - - def from_rs_id(self, rs_id): - """Convert a Rackspace id to a strategy-specific one.""" - return self._store.hget(self._rev_key, rs_id) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py deleted file mode 100644 index c45156ebd..000000000 --- a/nova/api/rackspace/auth.py +++ /dev/null @@ -1,101 +0,0 @@ -import datetime -import hashlib -import json -import time - -import webob.exc -import webob.dec - -from nova import auth -from nova import db -from nova import flags -from nova import manager -from nova import utils -from nova.api.rackspace import faults - -FLAGS = flags.FLAGS - -class Context(object): - pass - -class BasicApiAuthManager(object): - """ Implements a somewhat rudimentary version of Rackspace Auth""" - - def __init__(self, host=None, db_driver=None): - if not host: - host = FLAGS.host - self.host = host - if not db_driver: - db_driver = FLAGS.db_driver - self.db = utils.import_object(db_driver) - self.auth = auth.manager.AuthManager() - self.context = Context() - super(BasicApiAuthManager, self).__init__() - - def authenticate(self, req): - # Unless the request is explicitly made against // don't - # honor it - path_info = req.path_info - if len(path_info) > 1: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - try: - username, key = req.headers['X-Auth-User'], \ - req.headers['X-Auth-Key'] - except KeyError: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] - token, user = self._authorize_user(username, key) - if user and token: - res = webob.Response() - res.headers['X-Auth-Token'] = token['token_hash'] - res.headers['X-Server-Management-Url'] = \ - token['server_management_url'] - res.headers['X-Storage-Url'] = token['storage_url'] - res.headers['X-CDN-Management-Url'] = token['cdn_management_url'] - res.content_type = 'text/plain' - res.status = '204' - return res - else: - return faults.Fault(webob.exc.HTTPUnauthorized()) - - def authorize_token(self, token_hash): - """ retrieves user information from the datastore given a token - - If the token has expired, returns None - If the token is not found, returns None - Otherwise returns the token - - This method will also remove the token if the timestamp is older than - 2 days ago. - """ - token = self.db.auth_get_token(self.context, token_hash) - if token: - delta = datetime.datetime.now() - token['created_at'] - if delta.days >= 2: - self.db.auth_destroy_token(self.context, token) - else: - user = self.auth.get_user(token['user_id']) - return { 'id':user['uid'] } - return None - - def _authorize_user(self, username, key): - """ Generates a new token and assigns it to a user """ - user = self.auth.get_user_from_access_key(key) - if user and user['name'] == username: - token_hash = hashlib.sha1('%s%s%f' % (username, key, - time.time())).hexdigest() - token = {} - token['token_hash'] = token_hash - token['cdn_management_url'] = '' - token['server_management_url'] = self._get_server_mgmt_url() - token['storage_url'] = '' - token['user_id'] = user['uid'] - self.db.auth_create_token(self.context, token) - return token, user - return None, None - - def _get_server_mgmt_url(self): - return 'https://%s/v1.0/' % self.host - diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py deleted file mode 100644 index 9c0d41fa0..000000000 --- a/nova/api/rackspace/backup_schedules.py +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -import time -from webob import exc - -from nova import wsgi -from nova.api.rackspace import faults -import nova.image.service - -class Controller(wsgi.Controller): - def __init__(self): - pass - - def index(self, req, server_id): - return faults.Fault(exc.HTTPNotFound()) - - def create(self, req, server_id): - """ No actual update method required, since the existing API allows - both create and update through a POST """ - return faults.Fault(exc.HTTPNotFound()) - - def delete(self, req, server_id): - return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/rackspace/context.py b/nova/api/rackspace/context.py deleted file mode 100644 index 77394615b..000000000 --- a/nova/api/rackspace/context.py +++ /dev/null @@ -1,33 +0,0 @@ -# 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/faults.py b/nova/api/rackspace/faults.py deleted file mode 100644 index 32e5c866f..000000000 --- a/nova/api/rackspace/faults.py +++ /dev/null @@ -1,62 +0,0 @@ -# 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. - - -import webob.dec -import webob.exc - -from nova import wsgi - - -class Fault(webob.exc.HTTPException): - - """An RS API fault response.""" - - _fault_names = { - 400: "badRequest", - 401: "unauthorized", - 403: "resizeNotAllowed", - 404: "itemNotFound", - 405: "badMethod", - 409: "inProgress", - 413: "overLimit", - 415: "badMediaType", - 501: "notImplemented", - 503: "serviceUnavailable"} - - def __init__(self, exception): - """Create a Fault for the given webob.exc.exception.""" - self.wrapped_exc = exception - - @webob.dec.wsgify - def __call__(self, req): - """Generate a WSGI response based on the exception passed to ctor.""" - # Replace the body with fault details. - code = self.wrapped_exc.status_int - fault_name = self._fault_names.get(code, "cloudServersFault") - fault_data = { - fault_name: { - 'code': code, - 'message': self.wrapped_exc.explanation}} - if code == 413: - retry = self.wrapped_exc.headers['Retry-After'] - fault_data[fault_name]['retryAfter'] = retry - # 'code' is an attribute on the fault tag itself - metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} - serializer = wsgi.Serializer(req.environ, metadata) - self.wrapped_exc.body = serializer.to_content_type(fault_data) - return self.wrapped_exc diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py deleted file mode 100644 index 916449854..000000000 --- a/nova/api/rackspace/flavors.py +++ /dev/null @@ -1,58 +0,0 @@ -# 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. - -from webob import exc - -from nova.api.rackspace import faults -from nova.compute import instance_types -from nova import wsgi -import nova.api.rackspace - -class Controller(wsgi.Controller): - """Flavor controller for the Rackspace API.""" - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "flavor": [ "id", "name", "ram", "disk" ] - } - } - } - - def index(self, req): - """Return all flavors in brief.""" - return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) - for flavor in self.detail(req)['flavors']]) - - def detail(self, req): - """Return all flavors in detail.""" - items = [self.show(req, id)['flavor'] for id in self._all_ids()] - items = nova.api.rackspace.limited(items, req) - return dict(flavors=items) - - def show(self, req, id): - """Return data about the given flavor id.""" - for name, val in instance_types.INSTANCE_TYPES.iteritems(): - if val['flavorid'] == int(id): - item = dict(ram=val['memory_mb'], disk=val['local_gb'], - id=val['flavorid'], name=name) - return dict(flavor=item) - raise faults.Fault(exc.HTTPNotFound()) - - def _all_ids(self): - """Return the list of all flavorids.""" - return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()] diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py deleted file mode 100644 index 82dcd2049..000000000 --- a/nova/api/rackspace/images.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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. - -from webob import exc - -from nova import flags -from nova import utils -from nova import wsgi -import nova.api.rackspace -import nova.image.service -from nova.api.rackspace import faults - - -FLAGS = flags.FLAGS - -class Controller(wsgi.Controller): - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "image": [ "id", "name", "updated", "created", "status", - "serverId", "progress" ] - } - } - } - - def __init__(self): - self._service = utils.import_object(FLAGS.image_service) - - def index(self, req): - """Return all public images in brief.""" - return dict(images=[dict(id=img['id'], name=img['name']) - for img in self.detail(req)['images']]) - - def detail(self, req): - """Return all public images in detail.""" - data = self._service.index() - data = nova.api.rackspace.limited(data, req) - return dict(images=data) - - def show(self, req, id): - """Return data about the given image id.""" - return dict(image=self._service.show(id)) - - def delete(self, req, id): - # Only public images are supported for now. - raise faults.Fault(exc.HTTPNotFound()) - - def create(self, req): - # Only public images are supported for now, so a request to - # make a backup of a server cannot be supproted. - raise faults.Fault(exc.HTTPNotFound()) - - def update(self, req, id): - # Users may not modify public images, and that's all that - # we support for now. - raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/rackspace/notes.txt b/nova/api/rackspace/notes.txt deleted file mode 100644 index e133bf5ea..000000000 --- a/nova/api/rackspace/notes.txt +++ /dev/null @@ -1,23 +0,0 @@ -We will need: - -ImageService -a service that can do crud on image information. not user-specific. opaque -image ids. - -GlanceImageService(ImageService): -image ids are URIs. - -LocalImageService(ImageService): -image ids are random strings. - -RackspaceAPITranslationStore: -translates RS server/images/flavor/etc ids into formats required -by a given ImageService strategy. - -api.rackspace.images.Controller: -uses an ImageService strategy behind the scenes to do its fetching; it just -converts int image id into a strategy-specific image id. - -who maintains the mapping from user to [images he owns]? nobody, because -we have no way of enforcing access to his images, without kryptex which -won't be in Austin. diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/rackspace/ratelimiting/__init__.py deleted file mode 100644 index f843bac0f..000000000 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Rate limiting of arbitrary actions.""" - -import httplib -import time -import urllib -import webob.dec -import webob.exc - - -# Convenience constants for the limits dictionary passed to Limiter(). -PER_SECOND = 1 -PER_MINUTE = 60 -PER_HOUR = 60 * 60 -PER_DAY = 60 * 60 * 24 - -class Limiter(object): - - """Class providing rate limiting of arbitrary actions.""" - - def __init__(self, limits): - """Create a rate limiter. - - limits: a dict mapping from action name to a tuple. The tuple contains - the number of times the action may be performed, and the time period - (in seconds) during which the number must not be exceeded for this - action. Example: dict(reboot=(10, ratelimiting.PER_MINUTE)) would - allow 10 'reboot' actions per minute. - """ - self.limits = limits - self._levels = {} - - def perform(self, action_name, username='nobody'): - """Attempt to perform an action by the given username. - - action_name: the string name of the action to perform. This must - be a key in the limits dict passed to the ctor. - - username: an optional string name of the user performing the action. - Each user has her own set of rate limiting counters. Defaults to - 'nobody' (so that if you never specify a username when calling - perform(), a single set of counters will be used.) - - Return None if the action may proceed. If the action may not proceed - because it has been rate limited, return the float number of seconds - until the action would succeed. - """ - # Think of rate limiting as a bucket leaking water at 1cc/second. The - # bucket can hold as many ccs as there are seconds in the rate - # limiting period (e.g. 3600 for per-hour ratelimits), and if you can - # perform N actions in that time, each action fills the bucket by - # 1/Nth of its volume. You may only perform an action if the bucket - # would not overflow. - now = time.time() - key = '%s:%s' % (username, action_name) - last_time_performed, water_level = self._levels.get(key, (now, 0)) - # The bucket leaks 1cc/second. - water_level -= (now - last_time_performed) - if water_level < 0: - water_level = 0 - num_allowed_per_period, period_in_secs = self.limits[action_name] - # Fill the bucket by 1/Nth its capacity, and hope it doesn't overflow. - capacity = period_in_secs - new_level = water_level + (capacity * 1.0 / num_allowed_per_period) - if new_level > capacity: - # Delay this many seconds. - return new_level - capacity - self._levels[key] = (now, new_level) - return None - - -# If one instance of this WSGIApps is unable to handle your load, put a -# sharding app in front that shards by username to one of many backends. - -class WSGIApp(object): - - """Application that tracks rate limits in memory. Send requests to it of - this form: - - POST /limiter// - - and receive a 200 OK, or a 403 Forbidden with an X-Wait-Seconds header - containing the number of seconds to wait before the action would succeed. - """ - - def __init__(self, limiter): - """Create the WSGI application using the given Limiter instance.""" - self.limiter = limiter - - @webob.dec.wsgify - def __call__(self, req): - parts = req.path_info.split('/') - # format: /limiter// - if req.method != 'POST': - raise webob.exc.HTTPMethodNotAllowed() - if len(parts) != 4 or parts[1] != 'limiter': - raise webob.exc.HTTPNotFound() - username = parts[2] - action_name = urllib.unquote(parts[3]) - delay = self.limiter.perform(action_name, username) - if delay: - return webob.exc.HTTPForbidden( - headers={'X-Wait-Seconds': "%.2f" % delay}) - else: - return '' # 200 OK - - -class WSGIAppProxy(object): - - """Limiter lookalike that proxies to a ratelimiting.WSGIApp.""" - - def __init__(self, service_host): - """Creates a proxy pointing to a ratelimiting.WSGIApp at the given - host.""" - self.service_host = service_host - - def perform(self, action, username='nobody'): - conn = httplib.HTTPConnection(self.service_host) - conn.request('POST', '/limiter/%s/%s' % (username, action)) - resp = conn.getresponse() - if resp.status == 200: - return None # no delay - return float(resp.getheader('X-Wait-Seconds')) diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py deleted file mode 100644 index 4c9510917..000000000 --- a/nova/api/rackspace/ratelimiting/tests.py +++ /dev/null @@ -1,237 +0,0 @@ -import httplib -import StringIO -import time -import unittest -import webob - -import nova.api.rackspace.ratelimiting as ratelimiting - -class LimiterTest(unittest.TestCase): - - def setUp(self): - self.limits = { - 'a': (5, ratelimiting.PER_SECOND), - 'b': (5, ratelimiting.PER_MINUTE), - 'c': (5, ratelimiting.PER_HOUR), - 'd': (1, ratelimiting.PER_SECOND), - 'e': (100, ratelimiting.PER_SECOND)} - self.rl = ratelimiting.Limiter(self.limits) - - def exhaust(self, action, times_until_exhausted, **kwargs): - for i in range(times_until_exhausted): - when = self.rl.perform(action, **kwargs) - self.assertEqual(when, None) - num, period = self.limits[action] - delay = period * 1.0 / num - # Verify that we are now thoroughly delayed - for i in range(10): - when = self.rl.perform(action, **kwargs) - self.assertAlmostEqual(when, delay, 2) - - def test_second(self): - self.exhaust('a', 5) - time.sleep(0.2) - self.exhaust('a', 1) - time.sleep(1) - self.exhaust('a', 5) - - def test_minute(self): - self.exhaust('b', 5) - - def test_one_per_period(self): - def allow_once_and_deny_once(): - when = self.rl.perform('d') - self.assertEqual(when, None) - when = self.rl.perform('d') - self.assertAlmostEqual(when, 1, 2) - return when - time.sleep(allow_once_and_deny_once()) - time.sleep(allow_once_and_deny_once()) - allow_once_and_deny_once() - - def test_we_can_go_indefinitely_if_we_spread_out_requests(self): - for i in range(200): - when = self.rl.perform('e') - self.assertEqual(when, None) - time.sleep(0.01) - - def test_users_get_separate_buckets(self): - self.exhaust('c', 5, username='alice') - self.exhaust('c', 5, username='bob') - self.exhaust('c', 5, username='chuck') - self.exhaust('c', 0, username='chuck') - self.exhaust('c', 0, username='bob') - self.exhaust('c', 0, username='alice') - - -class FakeLimiter(object): - """Fake Limiter class that you can tell how to behave.""" - def __init__(self, test): - self._action = self._username = self._delay = None - self.test = test - def mock(self, action, username, delay): - self._action = action - self._username = username - self._delay = delay - def perform(self, action, username): - self.test.assertEqual(action, self._action) - self.test.assertEqual(username, self._username) - return self._delay - - -class WSGIAppTest(unittest.TestCase): - - def setUp(self): - self.limiter = FakeLimiter(self) - self.app = ratelimiting.WSGIApp(self.limiter) - - def test_invalid_methods(self): - requests = [] - for method in ['GET', 'PUT', 'DELETE']: - req = webob.Request.blank('/limits/michael/breakdance', - dict(REQUEST_METHOD=method)) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 405) - - def test_invalid_urls(self): - requests = [] - for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: - req = webob.Request.blank('/%s/michael/breakdance' % prefix, - dict(REQUEST_METHOD='POST')) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 404) - - def verify(self, url, username, action, delay=None): - """Make sure that POSTing to the given url causes the given username - to perform the given action. Make the internal rate limiter return - delay and make sure that the WSGI app returns the correct response. - """ - req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) - self.limiter.mock(action, username, delay) - resp = req.get_response(self.app) - if not delay: - self.assertEqual(resp.status_int, 200) - else: - self.assertEqual(resp.status_int, 403) - self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) - - def test_good_urls(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot') - - def test_escaping(self): - self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') - - def test_response_to_delays(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) - - -class FakeHttplibSocket(object): - """a fake socket implementation for httplib.HTTPResponse, trivial""" - - def __init__(self, response_string): - self._buffer = StringIO.StringIO(response_string) - - def makefile(self, _mode, _other): - """Returns the socket's internal buffer""" - return self._buffer - - -class FakeHttplibConnection(object): - """A fake httplib.HTTPConnection - - Requests made via this connection actually get translated and routed into - our WSGI app, we then wait for the response and turn it back into - an httplib.HTTPResponse. - """ - def __init__(self, app, host, is_secure=False): - self.app = app - self.host = host - - def request(self, method, path, data='', headers={}): - req = webob.Request.blank(path) - req.method = method - req.body = data - req.headers = headers - req.host = self.host - # Call the WSGI app, get the HTTP response - resp = str(req.get_response(self.app)) - # For some reason, the response doesn't have "HTTP/1.0 " prepended; I - # guess that's a function the web server usually provides. - resp = "HTTP/1.0 %s" % resp - sock = FakeHttplibSocket(resp) - self.http_response = httplib.HTTPResponse(sock) - self.http_response.begin() - - def getresponse(self): - return self.http_response - - -def wire_HTTPConnection_to_WSGI(host, app): - """Monkeypatches HTTPConnection so that if you try to connect to host, you - are instead routed straight to the given WSGI app. - - After calling this method, when any code calls - - httplib.HTTPConnection(host) - - the connection object will be a fake. Its requests will be sent directly - to the given WSGI app rather than through a socket. - - Code connecting to hosts other than host will not be affected. - - This method may be called multiple times to map different hosts to - different apps. - """ - class HTTPConnectionDecorator(object): - """Wraps the real HTTPConnection class so that when you instantiate - the class you might instead get a fake instance.""" - def __init__(self, wrapped): - self.wrapped = wrapped - def __call__(self, connection_host, *args, **kwargs): - if connection_host == host: - return FakeHttplibConnection(app, host) - else: - return self.wrapped(connection_host, *args, **kwargs) - httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) - - -class WSGIAppProxyTest(unittest.TestCase): - - def setUp(self): - """Our WSGIAppProxy is going to call across an HTTPConnection to a - WSGIApp running a limiter. The proxy will send input, and the proxy - should receive that same input, pass it to the limiter who gives a - result, and send the expected result back. - - The HTTPConnection isn't real -- it's monkeypatched to point straight - at the WSGIApp. And the limiter isn't real -- it's a fake that - behaves the way we tell it to. - """ - self.limiter = FakeLimiter(self) - app = ratelimiting.WSGIApp(self.limiter) - wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) - self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') - - def test_200(self): - self.limiter.mock('conquer', 'caesar', None) - when = self.proxy.perform('conquer', 'caesar') - self.assertEqual(when, None) - - def test_403(self): - self.limiter.mock('grumble', 'proletariat', 1.5) - when = self.proxy.perform('grumble', 'proletariat') - self.assertEqual(when, 1.5) - - def test_failure(self): - def shouldRaise(): - self.limiter.mock('murder', 'brutus', None) - self.proxy.perform('stab', 'brutus') - self.assertRaises(AssertionError, shouldRaise) - - -if __name__ == '__main__': - unittest.main() diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py deleted file mode 100644 index 8c489ed83..000000000 --- a/nova/api/rackspace/servers.py +++ /dev/null @@ -1,283 +0,0 @@ -# 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. - -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 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 - -def _image_service(): - """ Helper method for initializing the image id translator """ - service = utils.import_object(FLAGS.image_service) - return (service, _id_translator.RackspaceAPIIdTranslator( - "image", service.__class__.__name__)) - -def _filter_params(inst_dict): - """ Extracts all updatable parameters for a server update request """ - keys = dict(name='name', admin_pass='adminPass') - new_attrs = {} - 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): - """ Coerces a list of servers into proper dictionary format """ - return dict(servers=entities) - -def _entity_detail(inst): - """ Maps everything to Rackspace-like attributes for return""" - power_mapping = { - power_state.NOSTATE: 'build', - power_state.RUNNING: 'active', - power_state.BLOCKED: 'active', - power_state.PAUSED: 'suspended', - power_state.SHUTDOWN: 'active', - power_state.SHUTOFF: 'active', - power_state.CRASHED: 'error' - } - inst_dict = {} - - mapped_keys = dict(status='state', imageId='image_id', - flavorId='instance_type', name='server_name', id='id') - - for k, v in mapped_keys.iteritems(): - inst_dict[k] = inst[v] - - inst_dict['status'] = power_mapping[inst_dict['status']] - inst_dict['addresses'] = dict(public=[], private=[]) - inst_dict['metadata'] = {} - inst_dict['hostId'] = '' - - return dict(server=inst_dict) - -def _entity_inst(inst): - """ Filters all model attributes save for id and name """ - return dict(server=dict(id=inst['id'], name=inst['server_name'])) - -class Controller(wsgi.Controller): - """ The Server API controller for the Openstack API """ - - _serialization_metadata = { - 'application/xml': { - "attributes": { - "server": [ "id", "imageId", "name", "flavorId", "hostId", - "status", "progress", "progress" ] - } - } - } - - def __init__(self, db_driver=None): - if not db_driver: - db_driver = FLAGS.db_driver - self.db_driver = utils.import_object(db_driver) - super(Controller, self).__init__() - - def index(self, req): - """ Returns a list of server names and ids for a given user """ - return self._items(req, entity_maker=_entity_inst) - - def detail(self, req): - """ Returns a list of server details for a given user """ - return self._items(req, entity_maker=_entity_detail) - - def _items(self, req, entity_maker): - """Returns a list of servers for a given user. - - entity_maker - either _entity_detail or _entity_inst - """ - user_id = req.environ['nova.context']['user']['id'] - instance_list = self.db_driver.instance_get_all_by_user(None, user_id) - limited_list = nova.api.rackspace.limited(instance_list, req) - res = [entity_maker(inst)['server'] for inst in limited_list] - return _entity_list(res) - - def show(self, req, id): - """ Returns server details by server id """ - user_id = req.environ['nova.context']['user']['id'] - inst = self.db_driver.instance_get_by_internal_id(None, int(id)) - if inst: - if inst.user_id == user_id: - return _entity_detail(inst) - raise faults.Fault(exc.HTTPNotFound()) - - def delete(self, req, id): - """ Destroys a server """ - user_id = req.environ['nova.context']['user']['id'] - instance = self.db_driver.instance_get_by_internal_id(None, int(id)) - if instance and instance['user_id'] == user_id: - self.db_driver.instance_destroy(None, id) - return faults.Fault(exc.HTTPAccepted()) - return faults.Fault(exc.HTTPNotFound()) - - def create(self, req): - """ Creates a new server for a given user """ - - env = self._deserialize(req.body, req) - if not env: - return faults.Fault(exc.HTTPUnprocessableEntity()) - - #try: - inst = self._build_server_instance(req, env) - #except Exception, e: - # return faults.Fault(exc.HTTPUnprocessableEntity()) - - rpc.cast( - FLAGS.compute_topic, { - "method": "run_instance", - "args": {"instance_id": inst['id']}}) - return _entity_inst(inst) - - def update(self, req, id): - """ Updates the server name or password """ - 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_by_internal_id(None, int(id)) - if not instance or instance.user_id != user_id: - return faults.Fault(exc.HTTPNotFound()) - - self.db_driver.instance_update(None, int(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 """ - user_id = req.environ['nova.context']['user']['id'] - input_dict = self._deserialize(req.body, req) - try: - reboot_type = input_dict['reboot']['type'] - except Exception: - raise faults.Fault(webob.exc.HTTPNotImplemented()) - inst_ref = self.db.instance_get_by_internal_id(None, int(id)) - if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): - return faults.Fault(exc.HTTPUnprocessableEntity()) - cloud.reboot(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 = {} - - 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'] - - img_service, image_id_trans = _image_service() - - opaque_image_id = image_id_trans.to_rs_id(image_id) - image = img_service.show(opaque_image_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['display_name'] = env['server']['name'] - inst['display_description'] = env['server']['name'] - - #TODO(dietz) this may be ill advised - key_pair_ref = self.db_driver.key_pair_get_all_by_user( - None, user_id)[0] - - 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.internal_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'] = str(ref.internal_id) - self.db_driver.instance_update(None, inst['id'], inst) - - network_manager = utils.import_object(FLAGS.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/api/rackspace/sharedipgroups.py b/nova/api/rackspace/sharedipgroups.py deleted file mode 100644 index 4d2d0ede1..000000000 --- a/nova/api/rackspace/sharedipgroups.py +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -from nova import wsgi - -class Controller(wsgi.Controller): pass diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index ec76aa827..2c7f7fd3e 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -44,8 +44,8 @@ class Test(unittest.TestCase): req = webob.Request.blank(url, environ_keys) return req.get_response(api.API()) - def test_rackspace(self): - self.stubs.Set(api.rackspace, 'API', APIStub) + def test_openstack(self): + self.stubs.Set(api.openstack, 'API', APIStub) result = self._request('/v1.0/cloud', 'rs') self.assertEqual(result.body, "/cloud") @@ -56,7 +56,7 @@ class Test(unittest.TestCase): def test_not_found(self): self.stubs.Set(api.ec2, 'API', APIStub) - self.stubs.Set(api.rackspace, 'API', APIStub) + self.stubs.Set(api.openstack, 'API', APIStub) result = self._request('/test/cloud', 'ec2') self.assertNotEqual(result.body, "/cloud") diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py new file mode 100644 index 000000000..b534897f5 --- /dev/null +++ b/nova/tests/api/openstack/__init__.py @@ -0,0 +1,108 @@ +# 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. + +import unittest + +from nova.api.openstack import limited +from nova.api.openstack import RateLimitingMiddleware +from nova.tests.api.fakes import APIStub +from webob import Request + + +class RateLimitingMiddlewareTest(unittest.TestCase): + + def test_get_action_name(self): + middleware = RateLimitingMiddleware(APIStub()) + def verify(method, url, action_name): + req = Request.blank(url) + req.method = method + action = middleware.get_action_name(req) + self.assertEqual(action, action_name) + verify('PUT', '/servers/4', 'PUT') + verify('DELETE', '/servers/4', 'DELETE') + verify('POST', '/images/4', 'POST') + verify('POST', '/servers/4', 'POST servers') + verify('GET', '/foo?a=4&changes-since=never&b=5', 'GET changes-since') + verify('GET', '/foo?a=4&monkeys-since=never&b=5', None) + verify('GET', '/servers/4', None) + verify('HEAD', '/servers/4', None) + + def exhaust(self, middleware, method, url, username, times): + req = Request.blank(url, dict(REQUEST_METHOD=method), + headers={'X-Auth-User': username}) + for i in range(times): + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 200) + resp = req.get_response(middleware) + self.assertEqual(resp.status_int, 413) + self.assertTrue('Retry-After' in resp.headers) + + def test_single_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100) + self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100) + + def test_POST_servers_action_implies_POST_action(self): + middleware = RateLimitingMiddleware(APIStub()) + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10) + self.assertTrue(set(middleware.limiter._levels) == + set(['usr1:POST', 'usr1:POST servers', 'usr2:POST'])) + + def test_POST_servers_action_correctly_ratelimited(self): + middleware = RateLimitingMiddleware(APIStub()) + # Use up all of our "POST" allowance for the minute, 5 times + for i in range(5): + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) + # Reset the 'POST' action counter. + del middleware.limiter._levels['usr1:POST'] + # All 50 daily "POST servers" actions should be all used up + self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0) + + def test_proxy_ctor_works(self): + middleware = RateLimitingMiddleware(APIStub()) + self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") + middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') + self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") + + +class LimiterTest(unittest.TestCase): + + def test_limiter(self): + items = range(2000) + req = Request.blank('/') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=3') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=2005') + self.assertEqual(limited(items, req), []) + req = Request.blank('/?limit=10') + self.assertEqual(limited(items, req), items[ :10]) + req = Request.blank('/?limit=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?limit=3000') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=1&limit=3') + self.assertEqual(limited(items, req), items[1:4]) + req = Request.blank('/?offset=3&limit=0') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3&limit=1500') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3000&limit=10') + self.assertEqual(limited(items, req), []) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py new file mode 100644 index 000000000..1119fa714 --- /dev/null +++ b/nova/tests/api/openstack/fakes.py @@ -0,0 +1,205 @@ +# 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. + +import datetime +import json +import random +import string + +import webob +import webob.dec + +from nova import auth +from nova import utils +from nova import flags +from nova import exception as exc +import nova.api.openstack.auth +from nova.image import service +from nova.wsgi import Router + + +FLAGS = flags.FLAGS + + +class Context(object): + pass + + +class FakeRouter(Router): + def __init__(self): + pass + + @webob.dec.wsgify + def __call__(self, req): + res = webob.Response() + res.status = '200' + res.headers['X-Test-Success'] = 'True' + return res + + +def fake_auth_init(self): + self.db = FakeAuthDatabase() + self.context = Context() + self.auth = FakeAuthManager() + self.host = 'foo' + + +@webob.dec.wsgify +def fake_wsgi(self, req): + req.environ['nova.context'] = dict(user=dict(id=1)) + if req.body: + req.environ['inst_dict'] = json.loads(req.body) + return self.application + + +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_auth(stubs): + def fake_auth_init(self, app): + self.application = app + + stubs.Set(nova.api.openstack.AuthMiddleware, + '__init__', fake_auth_init) + stubs.Set(nova.api.openstack.AuthMiddleware, + '__call__', fake_wsgi) + + +def stub_out_rate_limiting(stubs): + def fake_rate_init(self, app): + super(nova.api.openstack.RateLimitingMiddleware, self).__init__(app) + self.application = app + + stubs.Set(nova.api.openstack.RateLimitingMiddleware, + '__init__', fake_rate_init) + + stubs.Set(nova.api.openstack.RateLimitingMiddleware, + '__call__', fake_wsgi) + + +def stub_out_networking(stubs): + def get_my_ip(): + return '127.0.0.1' + stubs.Set(nova.utils, 'get_my_ip', get_my_ip) + FLAGS.FAKE_subdomain = 'rs' + + +def stub_out_glance(stubs): + + class FakeParallaxClient: + + def __init__(self): + self.fixtures = {} + + def fake_get_images(self): + return self.fixtures + + def fake_get_image_metadata(self, image_id): + for k, f in self.fixtures.iteritems(): + if k == image_id: + return f + return None + + def fake_add_image_metadata(self, image_data): + id = ''.join(random.choice(string.letters) for _ in range(20)) + image_data['id'] = id + self.fixtures[id] = image_data + return id + + def fake_update_image_metadata(self, image_id, image_data): + + if image_id not in self.fixtures.keys(): + raise exc.NotFound + + self.fixtures[image_id].update(image_data) + + def fake_delete_image_metadata(self, image_id): + + if image_id not in self.fixtures.keys(): + raise exc.NotFound + + del self.fixtures[image_id] + + def fake_delete_all(self): + self.fixtures = {} + + fake_parallax_client = FakeParallaxClient() + stubs.Set(nova.image.service.ParallaxClient, 'get_images', + fake_parallax_client.fake_get_images) + stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata', + fake_parallax_client.fake_get_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata', + fake_parallax_client.fake_add_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata', + fake_parallax_client.fake_update_image_metadata) + stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata', + fake_parallax_client.fake_delete_image_metadata) + stubs.Set(nova.image.service.GlanceImageService, 'delete_all', + fake_parallax_client.fake_delete_all) + + +class FakeAuthDatabase(object): + data = {} + + @staticmethod + def auth_get_token(context, token_hash): + return FakeAuthDatabase.data.get(token_hash, None) + + @staticmethod + def auth_create_token(context, token): + token['created_at'] = datetime.datetime.now() + FakeAuthDatabase.data[token['token_hash']] = token + + @staticmethod + def auth_destroy_token(context, token): + if FakeAuthDatabase.data.has_key(token['token_hash']): + del FakeAuthDatabase.data['token_hash'] + + +class FakeAuthManager(object): + auth_data = {} + + def add_user(self, key, user): + FakeAuthManager.auth_data[key] = user + + def get_user(self, uid): + for k, v in FakeAuthManager.auth_data.iteritems(): + if v['uid'] == uid: + return v + return None + + def get_user_from_access_key(self, key): + return FakeAuthManager.auth_data.get(key, None) + + +class FakeRateLimiter(object): + def __init__(self, application): + self.application = application + + @webob.dec.wsgify + def __call__(self, req): + return self.application diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py new file mode 100644 index 000000000..d2ba80243 --- /dev/null +++ b/nova/tests/api/openstack/test_auth.py @@ -0,0 +1,108 @@ +import datetime +import unittest + +import stubout +import webob +import webob.dec + +import nova.api +import nova.api.openstack.auth +from nova import auth +from nova.tests.api.openstack import fakes + +class Test(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + self.stubs.Set(nova.api.openstack.auth.BasicApiAuthManager, + '__init__', fakes.fake_auth_init) + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_networking(self.stubs) + + def tearDown(self): + self.stubs.UnsetAll() + fakes.fake_data_store = {} + + def test_authorize_user(self): + f = fakes.FakeAuthManager() + f.add_user('derp', { 'uid': 1, 'name':'herp' } ) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(len(result.headers['X-Auth-Token']), 40) + self.assertEqual(result.headers['X-CDN-Management-Url'], + "") + self.assertEqual(result.headers['X-Storage-Url'], "") + + def test_authorize_token(self): + f = fakes.FakeAuthManager() + f.add_user('derp', { 'uid': 1, 'name':'herp' } ) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '204 No Content') + self.assertEqual(len(result.headers['X-Auth-Token']), 40) + self.assertEqual(result.headers['X-Server-Management-Url'], + "https://foo/v1.0/") + self.assertEqual(result.headers['X-CDN-Management-Url'], + "") + self.assertEqual(result.headers['X-Storage-Url'], "") + + token = result.headers['X-Auth-Token'] + self.stubs.Set(nova.api.openstack, 'APIRouter', + fakes.FakeRouter) + req = webob.Request.blank('/v1.0/fake') + req.headers['X-Auth-Token'] = token + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '200 OK') + self.assertEqual(result.headers['X-Test-Success'], 'True') + + def test_token_expiry(self): + self.destroy_called = False + token_hash = 'bacon' + + def destroy_token_mock(meh, context, token): + self.destroy_called = True + + def bad_token(meh, context, token_hash): + return { 'token_hash':token_hash, + 'created_at':datetime.datetime(1990, 1, 1) } + + self.stubs.Set(fakes.FakeAuthDatabase, 'auth_destroy_token', + destroy_token_mock) + + self.stubs.Set(fakes.FakeAuthDatabase, 'auth_get_token', + bad_token) + + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-Token'] = 'bacon' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + self.assertEqual(self.destroy_called, True) + + def test_bad_user(self): + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-User'] = 'herp' + req.headers['X-Auth-Key'] = 'derp' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + + def test_no_user(self): + req = webob.Request.blank('/v1.0/') + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + + def test_bad_token(self): + req = webob.Request.blank('/v1.0/') + req.headers['X-Auth-Token'] = 'baconbaconbacon' + result = req.get_response(nova.api.API()) + self.assertEqual(result.status, '401 Unauthorized') + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/openstack/test_faults.py b/nova/tests/api/openstack/test_faults.py new file mode 100644 index 000000000..70a811469 --- /dev/null +++ b/nova/tests/api/openstack/test_faults.py @@ -0,0 +1,40 @@ +import unittest +import webob +import webob.dec +import webob.exc + +from nova.api.openstack import faults + +class TestFaults(unittest.TestCase): + + def test_fault_parts(self): + req = webob.Request.blank('/.xml') + f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + resp = req.get_response(f) + + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['']) + body_without_spaces = ''.join(resp.body.split()) + self.assertTrue('scram' in body_without_spaces) + + def test_retry_header(self): + req = webob.Request.blank('/.xml') + exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry', + headers={'Retry-After': 4}) + f = faults.Fault(exc) + resp = req.get_response(f) + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['']) + body_sans_spaces = ''.join(resp.body.split()) + self.assertTrue('sorry' in body_sans_spaces) + self.assertTrue('4' in body_sans_spaces) + self.assertEqual(resp.headers['Retry-After'], 4) + + def test_raise(self): + @webob.dec.wsgify + def raiser(req): + raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) + req = webob.Request.blank('/.xml') + resp = req.get_response(raiser) + self.assertEqual(resp.status_int, 404) + self.assertTrue('whut?' in resp.body) diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py new file mode 100644 index 000000000..8dd4d1f29 --- /dev/null +++ b/nova/tests/api/openstack/test_flavors.py @@ -0,0 +1,48 @@ +# 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. + +import unittest + +import stubout +import webob + +import nova.api +from nova.api.openstack import flavors +from nova.tests.api.openstack import fakes + + +class FlavorsTest(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_flavor_list(self): + req = webob.Request.blank('/v1.0/flavors') + res = req.get_response(nova.api.API()) + + def test_get_flavor_by_id(self): + pass + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py new file mode 100644 index 000000000..505fea3e2 --- /dev/null +++ b/nova/tests/api/openstack/test_images.py @@ -0,0 +1,141 @@ +# 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. + +import logging +import unittest + +import stubout + +from nova import exception +from nova import utils +from nova.api.openstack import images +from nova.tests.api.openstack import fakes + + +class BaseImageServiceTests(): + + """Tasks to test for all image services""" + + def test_create(self): + + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} + + num_images = len(self.service.index()) + + id = self.service.create(fixture) + + self.assertNotEquals(None, id) + self.assertEquals(num_images + 1, len(self.service.index())) + + def test_create_and_show_non_existing_image(self): + + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} + + num_images = len(self.service.index()) + + id = self.service.create(fixture) + + self.assertNotEquals(None, id) + + self.assertRaises(exception.NotFound, + self.service.show, + 'bad image id') + + def test_update(self): + + fixture = {'name': 'test image', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None} + + id = self.service.create(fixture) + + fixture['status'] = 'in progress' + + self.service.update(id, fixture) + new_image_data = self.service.show(id) + self.assertEquals('in progress', new_image_data['status']) + + def test_delete(self): + + fixtures = [ + {'name': 'test image 1', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}, + {'name': 'test image 2', + 'updated': None, + 'created': None, + 'status': None, + 'serverId': None, + 'progress': None}] + + ids = [] + for fixture in fixtures: + new_id = self.service.create(fixture) + ids.append(new_id) + + num_images = len(self.service.index()) + self.assertEquals(2, num_images) + + self.service.delete(ids[0]) + + num_images = len(self.service.index()) + self.assertEquals(1, num_images) + + +class LocalImageServiceTest(unittest.TestCase, + BaseImageServiceTests): + + """Tests the local image service""" + + def setUp(self): + self.stubs = stubout.StubOutForTesting() + self.service = utils.import_object('nova.image.service.LocalImageService') + + def tearDown(self): + self.service.delete_all() + self.stubs.UnsetAll() + + +class GlanceImageServiceTest(unittest.TestCase, + BaseImageServiceTests): + + """Tests the local image service""" + + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_glance(self.stubs) + self.service = utils.import_object('nova.image.service.GlanceImageService') + + def tearDown(self): + self.service.delete_all() + self.stubs.UnsetAll() diff --git a/nova/tests/api/openstack/test_ratelimiting.py b/nova/tests/api/openstack/test_ratelimiting.py new file mode 100644 index 000000000..ad9e67454 --- /dev/null +++ b/nova/tests/api/openstack/test_ratelimiting.py @@ -0,0 +1,237 @@ +import httplib +import StringIO +import time +import unittest +import webob + +import nova.api.openstack.ratelimiting as ratelimiting + +class LimiterTest(unittest.TestCase): + + def setUp(self): + self.limits = { + 'a': (5, ratelimiting.PER_SECOND), + 'b': (5, ratelimiting.PER_MINUTE), + 'c': (5, ratelimiting.PER_HOUR), + 'd': (1, ratelimiting.PER_SECOND), + 'e': (100, ratelimiting.PER_SECOND)} + self.rl = ratelimiting.Limiter(self.limits) + + def exhaust(self, action, times_until_exhausted, **kwargs): + for i in range(times_until_exhausted): + when = self.rl.perform(action, **kwargs) + self.assertEqual(when, None) + num, period = self.limits[action] + delay = period * 1.0 / num + # Verify that we are now thoroughly delayed + for i in range(10): + when = self.rl.perform(action, **kwargs) + self.assertAlmostEqual(when, delay, 2) + + def test_second(self): + self.exhaust('a', 5) + time.sleep(0.2) + self.exhaust('a', 1) + time.sleep(1) + self.exhaust('a', 5) + + def test_minute(self): + self.exhaust('b', 5) + + def test_one_per_period(self): + def allow_once_and_deny_once(): + when = self.rl.perform('d') + self.assertEqual(when, None) + when = self.rl.perform('d') + self.assertAlmostEqual(when, 1, 2) + return when + time.sleep(allow_once_and_deny_once()) + time.sleep(allow_once_and_deny_once()) + allow_once_and_deny_once() + + def test_we_can_go_indefinitely_if_we_spread_out_requests(self): + for i in range(200): + when = self.rl.perform('e') + self.assertEqual(when, None) + time.sleep(0.01) + + def test_users_get_separate_buckets(self): + self.exhaust('c', 5, username='alice') + self.exhaust('c', 5, username='bob') + self.exhaust('c', 5, username='chuck') + self.exhaust('c', 0, username='chuck') + self.exhaust('c', 0, username='bob') + self.exhaust('c', 0, username='alice') + + +class FakeLimiter(object): + """Fake Limiter class that you can tell how to behave.""" + def __init__(self, test): + self._action = self._username = self._delay = None + self.test = test + def mock(self, action, username, delay): + self._action = action + self._username = username + self._delay = delay + def perform(self, action, username): + self.test.assertEqual(action, self._action) + self.test.assertEqual(username, self._username) + return self._delay + + +class WSGIAppTest(unittest.TestCase): + + def setUp(self): + self.limiter = FakeLimiter(self) + self.app = ratelimiting.WSGIApp(self.limiter) + + def test_invalid_methods(self): + requests = [] + for method in ['GET', 'PUT', 'DELETE']: + req = webob.Request.blank('/limits/michael/breakdance', + dict(REQUEST_METHOD=method)) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 405) + + def test_invalid_urls(self): + requests = [] + for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: + req = webob.Request.blank('/%s/michael/breakdance' % prefix, + dict(REQUEST_METHOD='POST')) + requests.append(req) + for req in requests: + self.assertEqual(req.get_response(self.app).status_int, 404) + + def verify(self, url, username, action, delay=None): + """Make sure that POSTing to the given url causes the given username + to perform the given action. Make the internal rate limiter return + delay and make sure that the WSGI app returns the correct response. + """ + req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) + self.limiter.mock(action, username, delay) + resp = req.get_response(self.app) + if not delay: + self.assertEqual(resp.status_int, 200) + else: + self.assertEqual(resp.status_int, 403) + self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) + + def test_good_urls(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot') + + def test_escaping(self): + self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') + + def test_response_to_delays(self): + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) + self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) + + +class FakeHttplibSocket(object): + """a fake socket implementation for httplib.HTTPResponse, trivial""" + + def __init__(self, response_string): + self._buffer = StringIO.StringIO(response_string) + + def makefile(self, _mode, _other): + """Returns the socket's internal buffer""" + return self._buffer + + +class FakeHttplibConnection(object): + """A fake httplib.HTTPConnection + + Requests made via this connection actually get translated and routed into + our WSGI app, we then wait for the response and turn it back into + an httplib.HTTPResponse. + """ + def __init__(self, app, host, is_secure=False): + self.app = app + self.host = host + + def request(self, method, path, data='', headers={}): + req = webob.Request.blank(path) + req.method = method + req.body = data + req.headers = headers + req.host = self.host + # Call the WSGI app, get the HTTP response + resp = str(req.get_response(self.app)) + # For some reason, the response doesn't have "HTTP/1.0 " prepended; I + # guess that's a function the web server usually provides. + resp = "HTTP/1.0 %s" % resp + sock = FakeHttplibSocket(resp) + self.http_response = httplib.HTTPResponse(sock) + self.http_response.begin() + + def getresponse(self): + return self.http_response + + +def wire_HTTPConnection_to_WSGI(host, app): + """Monkeypatches HTTPConnection so that if you try to connect to host, you + are instead routed straight to the given WSGI app. + + After calling this method, when any code calls + + httplib.HTTPConnection(host) + + the connection object will be a fake. Its requests will be sent directly + to the given WSGI app rather than through a socket. + + Code connecting to hosts other than host will not be affected. + + This method may be called multiple times to map different hosts to + different apps. + """ + class HTTPConnectionDecorator(object): + """Wraps the real HTTPConnection class so that when you instantiate + the class you might instead get a fake instance.""" + def __init__(self, wrapped): + self.wrapped = wrapped + def __call__(self, connection_host, *args, **kwargs): + if connection_host == host: + return FakeHttplibConnection(app, host) + else: + return self.wrapped(connection_host, *args, **kwargs) + httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) + + +class WSGIAppProxyTest(unittest.TestCase): + + def setUp(self): + """Our WSGIAppProxy is going to call across an HTTPConnection to a + WSGIApp running a limiter. The proxy will send input, and the proxy + should receive that same input, pass it to the limiter who gives a + result, and send the expected result back. + + The HTTPConnection isn't real -- it's monkeypatched to point straight + at the WSGIApp. And the limiter isn't real -- it's a fake that + behaves the way we tell it to. + """ + self.limiter = FakeLimiter(self) + app = ratelimiting.WSGIApp(self.limiter) + wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) + self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') + + def test_200(self): + self.limiter.mock('conquer', 'caesar', None) + when = self.proxy.perform('conquer', 'caesar') + self.assertEqual(when, None) + + def test_403(self): + self.limiter.mock('grumble', 'proletariat', 1.5) + when = self.proxy.perform('grumble', 'proletariat') + self.assertEqual(when, 1.5) + + def test_failure(self): + def shouldRaise(): + self.limiter.mock('murder', 'brutus', None) + self.proxy.perform('stab', 'brutus') + self.assertRaises(AssertionError, shouldRaise) + + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py new file mode 100644 index 000000000..d1ee533b6 --- /dev/null +++ b/nova/tests/api/openstack/test_servers.py @@ -0,0 +1,249 @@ +# 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. + +import json +import unittest + +import stubout +import webob + +from nova import db +from nova import flags +import nova.api.openstack +from nova.api.openstack import servers +import nova.db.api +from nova.db.sqlalchemy.models import Instance +import nova.rpc +from nova.tests.api.openstack import fakes + + +FLAGS = flags.FLAGS + +FLAGS.verbose = True + +def return_server(context, id): + return stub_instance(id) + + +def return_servers(context, user_id=1): + return [stub_instance(i, user_id) for i in xrange(5)] + + +def stub_instance(id, user_id=1): + return Instance( + id=id, state=0, image_id=10, server_name='server%s'%id, + user_id=user_id + ) + + +class ServersTest(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + fakes.FakeAuthManager.auth_data = {} + fakes.FakeAuthDatabase.data = {} + fakes.stub_out_networking(self.stubs) + fakes.stub_out_rate_limiting(self.stubs) + fakes.stub_out_auth(self.stubs) + fakes.stub_out_key_pair_funcs(self.stubs) + fakes.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_by_internal_id', return_server) + self.stubs.Set(nova.db.api, 'instance_get_all_by_user', + return_servers) + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_server_by_id(self): + req = webob.Request.blank('/v1.0/servers/1') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server1') + + def test_get_server_list(self): + req = webob.Request.blank('/v1.0/servers') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s.get('imageId', None), None) + i += 1 + + def test_create_instance(self): + def server_update(context, id, params): + pass + + def instance_create(context, inst): + class Foo(object): + internal_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.VlanManager, 'allocate_fixed_ip', + fake_method) + + 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)) + + def server_update(context, id, params): + self.update_called = True + 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_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') + req.method = 'POST' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_delete_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + req.method = 'DELETE' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_server_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_all_server_details(self): + req = webob.Request.blank('/v1.0/servers/detail') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s['imageId'], 10) + i += 1 + + def test_server_reboot(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) + + def test_server_rebuild(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) + + def test_server_resize(self): + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) + + def test_delete_server_instance(self): + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'DELETE' + + self.server_delete_called = False + def instance_destroy_mock(context, id): + self.server_delete_called = True + + self.stubs.Set(nova.db.api, 'instance_destroy', + instance_destroy_mock) + + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '202 Accepted') + self.assertEqual(self.server_delete_called, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/nova/tests/api/openstack/test_sharedipgroups.py b/nova/tests/api/openstack/test_sharedipgroups.py new file mode 100644 index 000000000..d199951d8 --- /dev/null +++ b/nova/tests/api/openstack/test_sharedipgroups.py @@ -0,0 +1,39 @@ +# 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. + +import unittest + +import stubout + +from nova.api.openstack import sharedipgroups + + +class SharedIpGroupsTest(unittest.TestCase): + def setUp(self): + self.stubs = stubout.StubOutForTesting() + + def tearDown(self): + self.stubs.UnsetAll() + + def test_get_shared_ip_groups(self): + pass + + def test_create_shared_ip_group(self): + pass + + def test_delete_shared_ip_group(self): + pass diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py deleted file mode 100644 index 1834f91b1..000000000 --- a/nova/tests/api/rackspace/__init__.py +++ /dev/null @@ -1,108 +0,0 @@ -# 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. - -import unittest - -from nova.api.rackspace import limited -from nova.api.rackspace import RateLimitingMiddleware -from nova.tests.api.fakes import APIStub -from webob import Request - - -class RateLimitingMiddlewareTest(unittest.TestCase): - - def test_get_action_name(self): - middleware = RateLimitingMiddleware(APIStub()) - def verify(method, url, action_name): - req = Request.blank(url) - req.method = method - action = middleware.get_action_name(req) - self.assertEqual(action, action_name) - verify('PUT', '/servers/4', 'PUT') - verify('DELETE', '/servers/4', 'DELETE') - verify('POST', '/images/4', 'POST') - verify('POST', '/servers/4', 'POST servers') - verify('GET', '/foo?a=4&changes-since=never&b=5', 'GET changes-since') - verify('GET', '/foo?a=4&monkeys-since=never&b=5', None) - verify('GET', '/servers/4', None) - verify('HEAD', '/servers/4', None) - - def exhaust(self, middleware, method, url, username, times): - req = Request.blank(url, dict(REQUEST_METHOD=method), - headers={'X-Auth-User': username}) - for i in range(times): - resp = req.get_response(middleware) - self.assertEqual(resp.status_int, 200) - resp = req.get_response(middleware) - self.assertEqual(resp.status_int, 413) - self.assertTrue('Retry-After' in resp.headers) - - def test_single_action(self): - middleware = RateLimitingMiddleware(APIStub()) - self.exhaust(middleware, 'DELETE', '/servers/4', 'usr1', 100) - self.exhaust(middleware, 'DELETE', '/servers/4', 'usr2', 100) - - def test_POST_servers_action_implies_POST_action(self): - middleware = RateLimitingMiddleware(APIStub()) - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) - self.exhaust(middleware, 'POST', '/images/4', 'usr2', 10) - self.assertTrue(set(middleware.limiter._levels) == - set(['usr1:POST', 'usr1:POST servers', 'usr2:POST'])) - - def test_POST_servers_action_correctly_ratelimited(self): - middleware = RateLimitingMiddleware(APIStub()) - # Use up all of our "POST" allowance for the minute, 5 times - for i in range(5): - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 10) - # Reset the 'POST' action counter. - del middleware.limiter._levels['usr1:POST'] - # All 50 daily "POST servers" actions should be all used up - self.exhaust(middleware, 'POST', '/servers/4', 'usr1', 0) - - def test_proxy_ctor_works(self): - middleware = RateLimitingMiddleware(APIStub()) - self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") - middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') - self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") - - -class LimiterTest(unittest.TestCase): - - def test_limiter(self): - items = range(2000) - req = Request.blank('/') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?offset=0') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?offset=3') - self.assertEqual(limited(items, req), items[3:1003]) - req = Request.blank('/?offset=2005') - self.assertEqual(limited(items, req), []) - req = Request.blank('/?limit=10') - self.assertEqual(limited(items, req), items[ :10]) - req = Request.blank('/?limit=0') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?limit=3000') - self.assertEqual(limited(items, req), items[ :1000]) - req = Request.blank('/?offset=1&limit=3') - self.assertEqual(limited(items, req), items[1:4]) - req = Request.blank('/?offset=3&limit=0') - self.assertEqual(limited(items, req), items[3:1003]) - req = Request.blank('/?offset=3&limit=1500') - self.assertEqual(limited(items, req), items[3:1003]) - req = Request.blank('/?offset=3000&limit=10') - self.assertEqual(limited(items, req), []) diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py deleted file mode 100644 index 6a25720a9..000000000 --- a/nova/tests/api/rackspace/fakes.py +++ /dev/null @@ -1,205 +0,0 @@ -# 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. - -import datetime -import json -import random -import string - -import webob -import webob.dec - -from nova import auth -from nova import utils -from nova import flags -from nova import exception as exc -import nova.api.rackspace.auth -from nova.image import service -from nova.wsgi import Router - - -FLAGS = flags.FLAGS - - -class Context(object): - pass - - -class FakeRouter(Router): - def __init__(self): - pass - - @webob.dec.wsgify - def __call__(self, req): - res = webob.Response() - res.status = '200' - res.headers['X-Test-Success'] = 'True' - return res - - -def fake_auth_init(self): - self.db = FakeAuthDatabase() - self.context = Context() - self.auth = FakeAuthManager() - self.host = 'foo' - - -@webob.dec.wsgify -def fake_wsgi(self, req): - req.environ['nova.context'] = dict(user=dict(id=1)) - if req.body: - req.environ['inst_dict'] = json.loads(req.body) - return self.application - - -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_auth(stubs): - def fake_auth_init(self, app): - self.application = app - - stubs.Set(nova.api.rackspace.AuthMiddleware, - '__init__', fake_auth_init) - stubs.Set(nova.api.rackspace.AuthMiddleware, - '__call__', fake_wsgi) - - -def stub_out_rate_limiting(stubs): - def fake_rate_init(self, app): - super(nova.api.rackspace.RateLimitingMiddleware, self).__init__(app) - self.application = app - - stubs.Set(nova.api.rackspace.RateLimitingMiddleware, - '__init__', fake_rate_init) - - stubs.Set(nova.api.rackspace.RateLimitingMiddleware, - '__call__', fake_wsgi) - - -def stub_out_networking(stubs): - def get_my_ip(): - return '127.0.0.1' - stubs.Set(nova.utils, 'get_my_ip', get_my_ip) - FLAGS.FAKE_subdomain = 'rs' - - -def stub_out_glance(stubs): - - class FakeParallaxClient: - - def __init__(self): - self.fixtures = {} - - def fake_get_images(self): - return self.fixtures - - def fake_get_image_metadata(self, image_id): - for k, f in self.fixtures.iteritems(): - if k == image_id: - return f - return None - - def fake_add_image_metadata(self, image_data): - id = ''.join(random.choice(string.letters) for _ in range(20)) - image_data['id'] = id - self.fixtures[id] = image_data - return id - - def fake_update_image_metadata(self, image_id, image_data): - - if image_id not in self.fixtures.keys(): - raise exc.NotFound - - self.fixtures[image_id].update(image_data) - - def fake_delete_image_metadata(self, image_id): - - if image_id not in self.fixtures.keys(): - raise exc.NotFound - - del self.fixtures[image_id] - - def fake_delete_all(self): - self.fixtures = {} - - fake_parallax_client = FakeParallaxClient() - stubs.Set(nova.image.service.ParallaxClient, 'get_images', - fake_parallax_client.fake_get_images) - stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata', - fake_parallax_client.fake_get_image_metadata) - stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata', - fake_parallax_client.fake_add_image_metadata) - stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata', - fake_parallax_client.fake_update_image_metadata) - stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata', - fake_parallax_client.fake_delete_image_metadata) - stubs.Set(nova.image.service.GlanceImageService, 'delete_all', - fake_parallax_client.fake_delete_all) - - -class FakeAuthDatabase(object): - data = {} - - @staticmethod - def auth_get_token(context, token_hash): - return FakeAuthDatabase.data.get(token_hash, None) - - @staticmethod - def auth_create_token(context, token): - token['created_at'] = datetime.datetime.now() - FakeAuthDatabase.data[token['token_hash']] = token - - @staticmethod - def auth_destroy_token(context, token): - if FakeAuthDatabase.data.has_key(token['token_hash']): - del FakeAuthDatabase.data['token_hash'] - - -class FakeAuthManager(object): - auth_data = {} - - def add_user(self, key, user): - FakeAuthManager.auth_data[key] = user - - def get_user(self, uid): - for k, v in FakeAuthManager.auth_data.iteritems(): - if v['uid'] == uid: - return v - return None - - def get_user_from_access_key(self, key): - return FakeAuthManager.auth_data.get(key, None) - - -class FakeRateLimiter(object): - def __init__(self, application): - self.application = application - - @webob.dec.wsgify - def __call__(self, req): - return self.application diff --git a/nova/tests/api/rackspace/test_auth.py b/nova/tests/api/rackspace/test_auth.py deleted file mode 100644 index 374cfe42b..000000000 --- a/nova/tests/api/rackspace/test_auth.py +++ /dev/null @@ -1,108 +0,0 @@ -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 fakes - -class Test(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - self.stubs.Set(nova.api.rackspace.auth.BasicApiAuthManager, - '__init__', fakes.fake_auth_init) - fakes.FakeAuthManager.auth_data = {} - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_networking(self.stubs) - - def tearDown(self): - self.stubs.UnsetAll() - fakes.fake_data_store = {} - - def test_authorize_user(self): - f = fakes.FakeAuthManager() - f.add_user('derp', { 'uid': 1, 'name':'herp' } ) - - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '204 No Content') - self.assertEqual(len(result.headers['X-Auth-Token']), 40) - self.assertEqual(result.headers['X-CDN-Management-Url'], - "") - self.assertEqual(result.headers['X-Storage-Url'], "") - - def test_authorize_token(self): - f = fakes.FakeAuthManager() - f.add_user('derp', { 'uid': 1, 'name':'herp' } ) - - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '204 No Content') - self.assertEqual(len(result.headers['X-Auth-Token']), 40) - self.assertEqual(result.headers['X-Server-Management-Url'], - "https://foo/v1.0/") - self.assertEqual(result.headers['X-CDN-Management-Url'], - "") - self.assertEqual(result.headers['X-Storage-Url'], "") - - token = result.headers['X-Auth-Token'] - self.stubs.Set(nova.api.rackspace, 'APIRouter', - fakes.FakeRouter) - req = webob.Request.blank('/v1.0/fake') - req.headers['X-Auth-Token'] = token - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '200 OK') - self.assertEqual(result.headers['X-Test-Success'], 'True') - - def test_token_expiry(self): - self.destroy_called = False - token_hash = 'bacon' - - def destroy_token_mock(meh, context, token): - self.destroy_called = True - - def bad_token(meh, context, token_hash): - return { 'token_hash':token_hash, - 'created_at':datetime.datetime(1990, 1, 1) } - - self.stubs.Set(fakes.FakeAuthDatabase, 'auth_destroy_token', - destroy_token_mock) - - self.stubs.Set(fakes.FakeAuthDatabase, 'auth_get_token', - bad_token) - - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-Token'] = 'bacon' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - self.assertEqual(self.destroy_called, True) - - def test_bad_user(self): - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-User'] = 'herp' - req.headers['X-Auth-Key'] = 'derp' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - - def test_no_user(self): - req = webob.Request.blank('/v1.0/') - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - - def test_bad_token(self): - req = webob.Request.blank('/v1.0/') - req.headers['X-Auth-Token'] = 'baconbaconbacon' - result = req.get_response(nova.api.API()) - self.assertEqual(result.status, '401 Unauthorized') - -if __name__ == '__main__': - unittest.main() diff --git a/nova/tests/api/rackspace/test_faults.py b/nova/tests/api/rackspace/test_faults.py deleted file mode 100644 index b2931bc98..000000000 --- a/nova/tests/api/rackspace/test_faults.py +++ /dev/null @@ -1,40 +0,0 @@ -import unittest -import webob -import webob.dec -import webob.exc - -from nova.api.rackspace import faults - -class TestFaults(unittest.TestCase): - - def test_fault_parts(self): - req = webob.Request.blank('/.xml') - f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) - resp = req.get_response(f) - - first_two_words = resp.body.strip().split()[:2] - self.assertEqual(first_two_words, ['']) - body_without_spaces = ''.join(resp.body.split()) - self.assertTrue('scram' in body_without_spaces) - - def test_retry_header(self): - req = webob.Request.blank('/.xml') - exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry', - headers={'Retry-After': 4}) - f = faults.Fault(exc) - resp = req.get_response(f) - first_two_words = resp.body.strip().split()[:2] - self.assertEqual(first_two_words, ['']) - body_sans_spaces = ''.join(resp.body.split()) - self.assertTrue('sorry' in body_sans_spaces) - self.assertTrue('4' in body_sans_spaces) - self.assertEqual(resp.headers['Retry-After'], 4) - - def test_raise(self): - @webob.dec.wsgify - def raiser(req): - raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) - req = webob.Request.blank('/.xml') - resp = req.get_response(raiser) - self.assertEqual(resp.status_int, 404) - self.assertTrue('whut?' in resp.body) diff --git a/nova/tests/api/rackspace/test_flavors.py b/nova/tests/api/rackspace/test_flavors.py deleted file mode 100644 index affdd2406..000000000 --- a/nova/tests/api/rackspace/test_flavors.py +++ /dev/null @@ -1,48 +0,0 @@ -# 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. - -import unittest - -import stubout -import webob - -import nova.api -from nova.api.rackspace import flavors -from nova.tests.api.rackspace import fakes - - -class FlavorsTest(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.auth_data = {} - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - - def tearDown(self): - self.stubs.UnsetAll() - - def test_get_flavor_list(self): - req = webob.Request.blank('/v1.0/flavors') - res = req.get_response(nova.api.API()) - - def test_get_flavor_by_id(self): - pass - -if __name__ == '__main__': - unittest.main() diff --git a/nova/tests/api/rackspace/test_images.py b/nova/tests/api/rackspace/test_images.py deleted file mode 100644 index a7f320b46..000000000 --- a/nova/tests/api/rackspace/test_images.py +++ /dev/null @@ -1,141 +0,0 @@ -# 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. - -import logging -import unittest - -import stubout - -from nova import exception -from nova import utils -from nova.api.rackspace import images -from nova.tests.api.rackspace import fakes - - -class BaseImageServiceTests(): - - """Tasks to test for all image services""" - - def test_create(self): - - fixture = {'name': 'test image', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None} - - num_images = len(self.service.index()) - - id = self.service.create(fixture) - - self.assertNotEquals(None, id) - self.assertEquals(num_images + 1, len(self.service.index())) - - def test_create_and_show_non_existing_image(self): - - fixture = {'name': 'test image', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None} - - num_images = len(self.service.index()) - - id = self.service.create(fixture) - - self.assertNotEquals(None, id) - - self.assertRaises(exception.NotFound, - self.service.show, - 'bad image id') - - def test_update(self): - - fixture = {'name': 'test image', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None} - - id = self.service.create(fixture) - - fixture['status'] = 'in progress' - - self.service.update(id, fixture) - new_image_data = self.service.show(id) - self.assertEquals('in progress', new_image_data['status']) - - def test_delete(self): - - fixtures = [ - {'name': 'test image 1', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None}, - {'name': 'test image 2', - 'updated': None, - 'created': None, - 'status': None, - 'serverId': None, - 'progress': None}] - - ids = [] - for fixture in fixtures: - new_id = self.service.create(fixture) - ids.append(new_id) - - num_images = len(self.service.index()) - self.assertEquals(2, num_images) - - self.service.delete(ids[0]) - - num_images = len(self.service.index()) - self.assertEquals(1, num_images) - - -class LocalImageServiceTest(unittest.TestCase, - BaseImageServiceTests): - - """Tests the local image service""" - - def setUp(self): - self.stubs = stubout.StubOutForTesting() - self.service = utils.import_object('nova.image.service.LocalImageService') - - def tearDown(self): - self.service.delete_all() - self.stubs.UnsetAll() - - -class GlanceImageServiceTest(unittest.TestCase, - BaseImageServiceTests): - - """Tests the local image service""" - - def setUp(self): - self.stubs = stubout.StubOutForTesting() - fakes.stub_out_glance(self.stubs) - self.service = utils.import_object('nova.image.service.GlanceImageService') - - def tearDown(self): - self.service.delete_all() - self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/test_servers.py b/nova/tests/api/rackspace/test_servers.py deleted file mode 100644 index 57040621b..000000000 --- a/nova/tests/api/rackspace/test_servers.py +++ /dev/null @@ -1,249 +0,0 @@ -# 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. - -import json -import unittest - -import stubout -import webob - -from nova import db -from nova import flags -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.rackspace import fakes - - -FLAGS = flags.FLAGS - -FLAGS.verbose = True - -def return_server(context, id): - return stub_instance(id) - - -def return_servers(context, user_id=1): - return [stub_instance(i, user_id) for i in xrange(5)] - - -def stub_instance(id, user_id=1): - return Instance( - id=id, state=0, image_id=10, server_name='server%s'%id, - user_id=user_id - ) - - -class ServersTest(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - fakes.FakeAuthManager.auth_data = {} - fakes.FakeAuthDatabase.data = {} - fakes.stub_out_networking(self.stubs) - fakes.stub_out_rate_limiting(self.stubs) - fakes.stub_out_auth(self.stubs) - fakes.stub_out_key_pair_funcs(self.stubs) - fakes.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_by_internal_id', return_server) - self.stubs.Set(nova.db.api, 'instance_get_all_by_user', - return_servers) - - def tearDown(self): - self.stubs.UnsetAll() - - def test_get_server_by_id(self): - req = webob.Request.blank('/v1.0/servers/1') - res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) - self.assertEqual(res_dict['server']['id'], 1) - self.assertEqual(res_dict['server']['name'], 'server1') - - def test_get_server_list(self): - req = webob.Request.blank('/v1.0/servers') - res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) - - i = 0 - for s in res_dict['servers']: - self.assertEqual(s['id'], i) - self.assertEqual(s['name'], 'server%d'%i) - self.assertEqual(s.get('imageId', None), None) - i += 1 - - def test_create_instance(self): - def server_update(context, id, params): - pass - - def instance_create(context, inst): - class Foo(object): - internal_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.VlanManager, 'allocate_fixed_ip', - fake_method) - - 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)) - - def server_update(context, id, params): - self.update_called = True - 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_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') - req.method = 'POST' - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '404 Not Found') - - def test_delete_backup_schedules(self): - req = webob.Request.blank('/v1.0/servers/1/backup_schedules') - req.method = 'DELETE' - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '404 Not Found') - - def test_get_server_backup_schedules(self): - req = webob.Request.blank('/v1.0/servers/1/backup_schedules') - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '404 Not Found') - - def test_get_all_server_details(self): - req = webob.Request.blank('/v1.0/servers/detail') - res = req.get_response(nova.api.API()) - res_dict = json.loads(res.body) - - i = 0 - for s in res_dict['servers']: - self.assertEqual(s['id'], i) - self.assertEqual(s['name'], 'server%d'%i) - self.assertEqual(s['imageId'], 10) - i += 1 - - def test_server_reboot(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality = {} - )) - req = webob.Request.blank('/v1.0/servers/1/action') - req.method = 'POST' - req.content_type= 'application/json' - req.body = json.dumps(body) - res = req.get_response(nova.api.API()) - - def test_server_rebuild(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality = {} - )) - req = webob.Request.blank('/v1.0/servers/1/action') - req.method = 'POST' - req.content_type= 'application/json' - req.body = json.dumps(body) - res = req.get_response(nova.api.API()) - - def test_server_resize(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality = {} - )) - req = webob.Request.blank('/v1.0/servers/1/action') - req.method = 'POST' - req.content_type= 'application/json' - req.body = json.dumps(body) - res = req.get_response(nova.api.API()) - - def test_delete_server_instance(self): - req = webob.Request.blank('/v1.0/servers/1') - req.method = 'DELETE' - - self.server_delete_called = False - def instance_destroy_mock(context, id): - self.server_delete_called = True - - self.stubs.Set(nova.db.api, 'instance_destroy', - instance_destroy_mock) - - res = req.get_response(nova.api.API()) - self.assertEqual(res.status, '202 Accepted') - self.assertEqual(self.server_delete_called, True) - - -if __name__ == "__main__": - unittest.main() diff --git a/nova/tests/api/rackspace/test_sharedipgroups.py b/nova/tests/api/rackspace/test_sharedipgroups.py deleted file mode 100644 index 31ce967d0..000000000 --- a/nova/tests/api/rackspace/test_sharedipgroups.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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. - -import unittest - -import stubout - -from nova.api.rackspace import sharedipgroups - - -class SharedIpGroupsTest(unittest.TestCase): - def setUp(self): - self.stubs = stubout.StubOutForTesting() - - def tearDown(self): - self.stubs.UnsetAll() - - def test_get_shared_ip_groups(self): - pass - - def test_create_shared_ip_group(self): - pass - - def test_delete_shared_ip_group(self): - pass -- cgit From c1190d55e130a80ac831ce15e6e30c28c5621aff Mon Sep 17 00:00:00 2001 From: mdietz Date: Fri, 8 Oct 2010 21:08:48 +0000 Subject: That's what I get for not using a good vimrc --- nova/api/openstack/servers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f234af7de..5d1ed9822 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -170,14 +170,14 @@ class Controller(wsgi.Controller): def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ - user_id = req.environ['nova.context']['user']['id'] + user_id = req.environ['nova.context']['user']['id'] input_dict = self._deserialize(req.body, req) try: reboot_type = input_dict['reboot']['type'] except Exception: raise faults.Fault(webob.exc.HTTPNotImplemented()) - inst_ref = self.db.instance_get_by_internal_id(None, int(id)) - if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): + inst_ref = self.db.instance_get_by_internal_id(None, int(id)) + if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): return faults.Fault(exc.HTTPUnprocessableEntity()) cloud.reboot(id) -- cgit From f447e1a3a2234e0ab3a5e281442659626f8d99bd Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 11 Oct 2010 13:39:33 +0200 Subject: Rename ec2 get_console_output's instance ID argument to 'instance_id'. It's passed as a kwarg, based on key in the http query, so it must be named this way. --- nova/api/ec2/cloud.py | 6 +++--- nova/api/ec2/images.py | 3 +++ nova/fakerabbit.py | 14 ++++++++++++++ nova/rpc.py | 9 +++++++++ nova/tests/cloud_unittest.py | 31 ++++++++++++++++++++----------- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 175bb493c..11e54d2b5 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -258,9 +258,9 @@ class CloudController(object): def delete_security_group(self, context, group_name, **kwargs): return True - def get_console_output(self, context, ec2_id_list, **kwargs): - # ec2_id_list is passed in as a list of instances - ec2_id = ec2_id_list[0] + def get_console_output(self, context, instance_id, **kwargs): + # instance_id is passed in as a list of instances + ec2_id = instance_id[0] internal_id = ec2_id_to_internal_id(ec2_id) instance_ref = db.instance_get_by_internal_id(context, internal_id) return rpc.call('%s.%s' % (FLAGS.compute_topic, diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index cb54cdda2..f0a43dad6 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -69,6 +69,9 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ + if FLAGS.connection_type == 'fake': + return [{ 'imageId' : 'bar'}] + # FIXME: send along the list of only_images to check for response = conn(context).make_request( method='GET', diff --git a/nova/fakerabbit.py b/nova/fakerabbit.py index 068025249..835973810 100644 --- a/nova/fakerabbit.py +++ b/nova/fakerabbit.py @@ -22,6 +22,7 @@ import logging import Queue as queue from carrot.backends import base +from eventlet import greenthread class Message(base.BaseMessage): @@ -38,6 +39,7 @@ class Exchange(object): def publish(self, message, routing_key=None): logging.debug('(%s) publish (key: %s) %s', self.name, routing_key, message) + routing_key = routing_key.split('.')[0] if routing_key in self._routes: for f in self._routes[routing_key]: logging.debug('Publishing to route %s', f) @@ -94,6 +96,18 @@ class Backend(object): self._exchanges[exchange].bind(self._queues[queue].push, routing_key) + def declare_consumer(self, queue, callback, *args, **kwargs): + self.current_queue = queue + self.current_callback = callback + + def consume(self, *args, **kwargs): + while True: + item = self.get(self.current_queue) + if item: + self.current_callback(item) + raise StopIteration() + greenthread.sleep(0) + def get(self, queue, no_ack=False): if not queue in self._queues or not self._queues[queue].size(): return None diff --git a/nova/rpc.py b/nova/rpc.py index fe52ad35f..447ad3b93 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -28,6 +28,7 @@ import uuid from carrot import connection as carrot_connection from carrot import messaging +from eventlet import greenthread from twisted.internet import defer from twisted.internet import task @@ -107,6 +108,14 @@ class Consumer(messaging.Consumer): logging.exception("Failed to fetch message from queue") self.failed_connection = True + def attach_to_eventlet(self): + """Only needed for unit tests!""" + def fetch_repeatedly(): + while True: + self.fetch(enable_callbacks=True) + greenthread.sleep(0.1) + greenthread.spawn(fetch_repeatedly) + def attach_to_twisted(self): """Attach a callback to twisted that fires 10 times a second""" loop = task.LoopingCall(self.fetch, enable_callbacks=True) diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 615e589cf..8e5881edb 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -16,6 +16,7 @@ # License for the specific language governing permissions and limitations # under the License. +from base64 import b64decode import json import logging from M2Crypto import BIO @@ -63,11 +64,17 @@ class CloudTestCase(test.TrialTestCase): self.cloud = cloud.CloudController() # set up a service - self.compute = utils.import_class(FLAGS.compute_manager) + self.compute = utils.import_class(FLAGS.compute_manager)() self.compute_consumer = rpc.AdapterConsumer(connection=self.conn, topic=FLAGS.compute_topic, proxy=self.compute) - self.compute_consumer.attach_to_twisted() + self.compute_consumer.attach_to_eventlet() + self.network = utils.import_class(FLAGS.network_manager)() + self.network_consumer = rpc.AdapterConsumer(connection=self.conn, + topic=FLAGS.network_topic, + proxy=self.network) + self.network_consumer.attach_to_eventlet() + self.manager = manager.AuthManager() self.user = self.manager.create_user('admin', 'admin', 'admin', True) @@ -85,15 +92,17 @@ class CloudTestCase(test.TrialTestCase): return cloud._gen_key(self.context, self.context.user.id, name) def test_console_output(self): - if FLAGS.connection_type == 'fake': - logging.debug("Can't test instances without a real virtual env.") - return - instance_id = 'foo' - inst = yield self.compute.run_instance(instance_id) - output = yield self.cloud.get_console_output(self.context, [instance_id]) - logging.debug(output) - self.assert_(output) - rv = yield self.compute.terminate_instance(instance_id) + image_id = FLAGS.default_image + instance_type = FLAGS.default_instance_type + max_count = 1 + kwargs = {'image_id': image_id, + 'instance_type': instance_type, + 'max_count': max_count } + rv = yield self.cloud.run_instances(self.context, **kwargs) + instance_id = rv['instancesSet'][0]['instanceId'] + output = yield self.cloud.get_console_output(context=self.context, instance_id=[instance_id]) + self.assertEquals(b64decode(output['output']), 'FAKE CONSOLE OUTPUT') + rv = yield self.cloud.terminate_instances(self.context, [instance_id]) def test_key_generation(self): -- cgit From 3ca549942e96e4ff769e914f227919f3a4d98686 Mon Sep 17 00:00:00 2001 From: Soren Hansen Date: Mon, 11 Oct 2010 14:09:24 +0200 Subject: If machine manifest includes a kernel and/or ramdisk id, include it in the image's metadata. --- nova/objectstore/image.py | 10 ++++++-- nova/tests/bundle/1mb.manifest.xml | 2 +- .../bundle/1mb.no_kernel_or_ramdisk.manifest.xml | 1 + nova/tests/objectstore_unittest.py | 29 ++++++++++++++++++---- 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index def1b8167..c01b041bb 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -191,14 +191,14 @@ class Image(object): if kernel_id == 'true': image_type = 'kernel' except: - pass + kernel_id = None try: ramdisk_id = manifest.find("machine_configuration/ramdisk_id").text if ramdisk_id == 'true': image_type = 'ramdisk' except: - pass + ramdisk_id = None info = { 'imageId': image_id, @@ -209,6 +209,12 @@ class Image(object): 'imageType' : image_type } + if kernel_id: + info['kernelId'] = kernel_id + + if ramdisk_id: + info['ramdiskId'] = ramdisk_id + def write_state(state): info['imageState'] = state with open(os.path.join(image_path, 'info.json'), "w") as f: diff --git a/nova/tests/bundle/1mb.manifest.xml b/nova/tests/bundle/1mb.manifest.xml index dc3315957..01648a544 100644 --- a/nova/tests/bundle/1mb.manifest.xml +++ b/nova/tests/bundle/1mb.manifest.xml @@ -1 +1 @@ -2007-10-10euca-tools1.231337x86_641mb42machineda39a3ee5e6b4b0d3255bfef95601890afd807091048576113633a2ea00dc64083dd9a10eb5e233635b42a7beb1670ab75452087d9de74c60aba1cd27c136fda56f62beb581de128fb1f10d072b9e556fd25e903107a57827c21f6ee8a93a4ff55b11311fcef217e3eefb07e81f71e88216f43b4b54029c1f2549f2925a839a73947d2d5aeecec4a62ece4af9156d557ae907978298296d99154c11147fd8caf92447e90ce339928933d7579244c2f8ffb07cc0ea35f8738da8b90eff6c7a49671a84500e993e9462e4c36d5c19c0b3a2b397d035b4c0cce742b58e12552175d81d129b0425e9f71ebacb9aeb539fa9dd2ac36749fb82876f6902e5fb24b6ec19f35ec4c20acd50437fd30966e99c4d9a0647577970a8fa302314bd082c9715f071160c69bbfb070f51d2ba1076775f1d988ccde150e515088156b248e4b5a64e46c4fe064feeeedfe14511f7fde478a51acb89f9b2f6c84b60593e5c3f792ba6b01fed9bf2158fdac03086374883b39d13a3ca74497eeaaf579fc3f26effc73bfd9446a2a8c4061f0874bfaca058905180e22d3d8881551cb38f7606f19f00e4e19535dd234b66b31b77e9c7bad3885d9c9efa75c863631fd4f82a009e17d789066d9cc6032a436f05384832f6d9a3283d3e63eab04fa0da5c8c87db9b17e854e842c3fb416507d067a266b44538125ce732e486098e8ebd1ca91fa3079f007fce7d14957a9b7e57282407ead3c6eb68fe975df3d83190021b1mb.part.0c4413423cf7a57e71187e19bfd5cd4b514a642831mb.part.19d4262e6589393d09a11a0332af169887bc2e57d4e00b5ba28114dda4a9df7eeae94be847ec46117a09a1cbe41e578660642f0660dda1776b39fb3bf826b6cfec019e2a5e9c566728d186b7400ebc989a30670eb1db26ce01e68bd9d3f31290370077a85b81c66b63c1e0d5499bac115c06c17a21a81b6d3a67ebbce6c17019095af7ab07f3796c708cc843e58efc12ddc788c5e \ No newline at end of file +2007-10-10euca-tools1.231337x86_64aki-testari-test1mb42machineda39a3ee5e6b4b0d3255bfef95601890afd807091048576113633a2ea00dc64083dd9a10eb5e233635b42a7beb1670ab75452087d9de74c60aba1cd27c136fda56f62beb581de128fb1f10d072b9e556fd25e903107a57827c21f6ee8a93a4ff55b11311fcef217e3eefb07e81f71e88216f43b4b54029c1f2549f2925a839a73947d2d5aeecec4a62ece4af9156d557ae907978298296d99154c11147fd8caf92447e90ce339928933d7579244c2f8ffb07cc0ea35f8738da8b90eff6c7a49671a84500e993e9462e4c36d5c19c0b3a2b397d035b4c0cce742b58e12552175d81d129b0425e9f71ebacb9aeb539fa9dd2ac36749fb82876f6902e5fb24b6ec19f35ec4c20acd50437fd30966e99c4d9a0647577970a8fa302314bd082c9715f071160c69bbfb070f51d2ba1076775f1d988ccde150e515088156b248e4b5a64e46c4fe064feeeedfe14511f7fde478a51acb89f9b2f6c84b60593e5c3f792ba6b01fed9bf2158fdac03086374883b39d13a3ca74497eeaaf579fc3f26effc73bfd9446a2a8c4061f0874bfaca058905180e22d3d8881551cb38f7606f19f00e4e19535dd234b66b31b77e9c7bad3885d9c9efa75c863631fd4f82a009e17d789066d9cc6032a436f05384832f6d9a3283d3e63eab04fa0da5c8c87db9b17e854e842c3fb416507d067a266b44538125ce732e486098e8ebd1ca91fa3079f007fce7d14957a9b7e57282407ead3c6eb68fe975df3d83190021b1mb.part.0c4413423cf7a57e71187e19bfd5cd4b514a642831mb.part.19d4262e6589393d09a11a0332af169887bc2e57d4e00b5ba28114dda4a9df7eeae94be847ec46117a09a1cbe41e578660642f0660dda1776b39fb3bf826b6cfec019e2a5e9c566728d186b7400ebc989a30670eb1db26ce01e68bd9d3f31290370077a85b81c66b63c1e0d5499bac115c06c17a21a81b6d3a67ebbce6c17019095af7ab07f3796c708cc843e58efc12ddc788c5e diff --git a/nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml b/nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml new file mode 100644 index 000000000..73d7ace00 --- /dev/null +++ b/nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml @@ -0,0 +1 @@ +2007-10-10euca-tools1.231337x86_641mb42machineda39a3ee5e6b4b0d3255bfef95601890afd807091048576113633a2ea00dc64083dd9a10eb5e233635b42a7beb1670ab75452087d9de74c60aba1cd27c136fda56f62beb581de128fb1f10d072b9e556fd25e903107a57827c21f6ee8a93a4ff55b11311fcef217e3eefb07e81f71e88216f43b4b54029c1f2549f2925a839a73947d2d5aeecec4a62ece4af9156d557ae907978298296d99154c11147fd8caf92447e90ce339928933d7579244c2f8ffb07cc0ea35f8738da8b90eff6c7a49671a84500e993e9462e4c36d5c19c0b3a2b397d035b4c0cce742b58e12552175d81d129b0425e9f71ebacb9aeb539fa9dd2ac36749fb82876f6902e5fb24b6ec19f35ec4c20acd50437fd30966e99c4d9a0647577970a8fa302314bd082c9715f071160c69bbfb070f51d2ba1076775f1d988ccde150e515088156b248e4b5a64e46c4fe064feeeedfe14511f7fde478a51acb89f9b2f6c84b60593e5c3f792ba6b01fed9bf2158fdac03086374883b39d13a3ca74497eeaaf579fc3f26effc73bfd9446a2a8c4061f0874bfaca058905180e22d3d8881551cb38f7606f19f00e4e19535dd234b66b31b77e9c7bad3885d9c9efa75c863631fd4f82a009e17d789066d9cc6032a436f05384832f6d9a3283d3e63eab04fa0da5c8c87db9b17e854e842c3fb416507d067a266b44538125ce732e486098e8ebd1ca91fa3079f007fce7d14957a9b7e57282407ead3c6eb68fe975df3d83190021b1mb.part.0c4413423cf7a57e71187e19bfd5cd4b514a642831mb.part.19d4262e6589393d09a11a0332af169887bc2e57d4e00b5ba28114dda4a9df7eeae94be847ec46117a09a1cbe41e578660642f0660dda1776b39fb3bf826b6cfec019e2a5e9c566728d186b7400ebc989a30670eb1db26ce01e68bd9d3f31290370077a85b81c66b63c1e0d5499bac115c06c17a21a81b6d3a67ebbce6c17019095af7ab07f3796c708cc843e58efc12ddc788c5e diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index 5a599ff3a..eb2ee0406 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -133,13 +133,22 @@ class ObjectStoreTestCase(test.TrialTestCase): self.assertRaises(NotFound, objectstore.bucket.Bucket, 'new_bucket') def test_images(self): + self.do_test_images('1mb.manifest.xml', True, + 'image_bucket1', 'i-testing1') + + def test_images_no_kernel_or_ramdisk(self): + self.do_test_images('1mb.no_kernel_or_ramdisk.manifest.xml', + False, 'image_bucket2', 'i-testing2') + + def do_test_images(self, manifest_file, expect_kernel_and_ramdisk, + image_bucket, image_name): "Test the image API." self.context.user = self.auth_manager.get_user('user1') self.context.project = self.auth_manager.get_project('proj1') # create a bucket for our bundle - objectstore.bucket.Bucket.create('image_bucket', self.context) - bucket = objectstore.bucket.Bucket('image_bucket') + objectstore.bucket.Bucket.create(image_bucket, self.context) + bucket = objectstore.bucket.Bucket(image_bucket) # upload an image manifest/parts bundle_path = os.path.join(os.path.dirname(__file__), 'bundle') @@ -147,18 +156,28 @@ class ObjectStoreTestCase(test.TrialTestCase): bucket[os.path.basename(path)] = open(path, 'rb').read() # register an image - image.Image.register_aws_image('i-testing', - 'image_bucket/1mb.manifest.xml', + image.Image.register_aws_image(image_name, + '%s/%s' % (image_bucket, manifest_file), self.context) # verify image - my_img = image.Image('i-testing') + my_img = image.Image(image_name) result_image_file = os.path.join(my_img.path, 'image') self.assertEqual(os.stat(result_image_file).st_size, 1048576) sha = hashlib.sha1(open(result_image_file).read()).hexdigest() self.assertEqual(sha, '3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3') + if expect_kernel_and_ramdisk: + # Verify the default kernel and ramdisk are set + self.assertEqual(my_img.metadata['kernelId'], 'aki-test') + self.assertEqual(my_img.metadata['ramdiskId'], 'ari-test') + else: + # Verify that the default kernel and ramdisk (the one from FLAGS) + # doesn't get embedded in the metadata + self.assertFalse('kernelId' in my_img.metadata) + self.assertFalse('ramdiskId' in my_img.metadata) + # verify image permissions self.context.user = self.auth_manager.get_user('user2') self.context.project = self.auth_manager.get_project('proj2') -- cgit From 76a76244ccee2502903a67f3f17dda97664e6687 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 11 Oct 2010 11:21:26 -0400 Subject: Fix bug 658444 --- nova/api/ec2/cloud.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 175bb493c..ca0f64e99 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -659,7 +659,12 @@ class CloudController(object): return self._format_run_instances(context, reservation_id) - def terminate_instances(self, context, ec2_id_list, **kwargs): + def terminate_instances(self, context, instance_id, **kwargs): + """Terminate each instance in instance_id, which is a list of ec2 ids. + + instance_id is a kwarg so its name cannot be modified. + """ + ec2_id_list = instance_id logging.debug("Going to start terminating instances") for id_str in ec2_id_list: internal_id = ec2_id_to_internal_id(id_str) -- cgit From f9b2f70f22bdc8a9cf08ada5f7ec45eea6060866 Mon Sep 17 00:00:00 2001 From: Michael Gundlach Date: Mon, 11 Oct 2010 11:43:58 -0400 Subject: Rename rsapi to osapi, and make the default subdomain for OpenStack API calls be 'api' instead of 'rs'. --- nova/api/__init__.py | 26 +++++++++++++------------- nova/tests/api/__init__.py | 4 ++-- nova/tests/api/openstack/fakes.py | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 627883018..8ec7094d7 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -31,12 +31,12 @@ from nova.api import openstack from nova.api.ec2 import metadatarequesthandler -flags.DEFINE_string('rsapi_subdomain', 'rs', - 'subdomain running the RS API') +flags.DEFINE_string('osapi_subdomain', 'api', + 'subdomain running the OpenStack API') flags.DEFINE_string('ec2api_subdomain', 'ec2', 'subdomain running the EC2 API') flags.DEFINE_string('FAKE_subdomain', None, - 'set to rs or ec2 to fake the subdomain of the host for testing') + 'set to api or ec2 to fake the subdomain of the host for testing') FLAGS = flags.FLAGS @@ -44,21 +44,21 @@ class API(wsgi.Router): """Routes top-level requests to the appropriate controller.""" def __init__(self): - rsdomain = {'sub_domain': [FLAGS.rsapi_subdomain]} + osapidomain = {'sub_domain': [FLAGS.osapi_subdomain]} ec2domain = {'sub_domain': [FLAGS.ec2api_subdomain]} - # If someone wants to pretend they're hitting the RS subdomain - # on their local box, they can set FAKE_subdomain to 'rs', which - # removes subdomain restrictions from the RS routes below. - if FLAGS.FAKE_subdomain == 'rs': - rsdomain = {} + # If someone wants to pretend they're hitting the OSAPI subdomain + # on their local box, they can set FAKE_subdomain to 'api', which + # removes subdomain restrictions from the OpenStack API routes below. + if FLAGS.FAKE_subdomain == 'api': + osapidomain = {} elif FLAGS.FAKE_subdomain == 'ec2': ec2domain = {} mapper = routes.Mapper() mapper.sub_domains = True - mapper.connect("/", controller=self.rsapi_versions, - conditions=rsdomain) + mapper.connect("/", controller=self.osapi_versions, + conditions=osapidomain) mapper.connect("/v1.0/{path_info:.*}", controller=openstack.API(), - conditions=rsdomain) + conditions=osapidomain) mapper.connect("/", controller=self.ec2api_versions, conditions=ec2domain) @@ -81,7 +81,7 @@ class API(wsgi.Router): super(API, self).__init__(mapper) @webob.dec.wsgify - def rsapi_versions(self, req): + def osapi_versions(self, req): """Respond to a request for all OpenStack API versions.""" response = { "versions": [ diff --git a/nova/tests/api/__init__.py b/nova/tests/api/__init__.py index 2c7f7fd3e..f051e2390 100644 --- a/nova/tests/api/__init__.py +++ b/nova/tests/api/__init__.py @@ -46,7 +46,7 @@ class Test(unittest.TestCase): def test_openstack(self): self.stubs.Set(api.openstack, 'API', APIStub) - result = self._request('/v1.0/cloud', 'rs') + result = self._request('/v1.0/cloud', 'api') self.assertEqual(result.body, "/cloud") def test_ec2(self): @@ -61,7 +61,7 @@ class Test(unittest.TestCase): self.assertNotEqual(result.body, "/cloud") def test_query_api_versions(self): - result = self._request('/', 'rs') + result = self._request('/', 'api') self.assertTrue('CURRENT' in result.body) def test_metadata(self): diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 1119fa714..34bc1f2a9 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -105,7 +105,7 @@ def stub_out_networking(stubs): def get_my_ip(): return '127.0.0.1' stubs.Set(nova.utils, 'get_my_ip', get_my_ip) - FLAGS.FAKE_subdomain = 'rs' + FLAGS.FAKE_subdomain = 'api' def stub_out_glance(stubs): -- cgit