diff options
-rw-r--r-- | nova/compute/api.py | 2 | ||||
-rw-r--r-- | nova/image/glance.py | 58 | ||||
-rw-r--r-- | nova/test.py | 31 | ||||
-rw-r--r-- | nova/tests/api/openstack/fakes.py | 19 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_images.py | 41 |
5 files changed, 144 insertions, 7 deletions
diff --git a/nova/compute/api.py b/nova/compute/api.py index 61f8b2a6a..b65590ac8 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -420,7 +420,7 @@ class API(base.Base): :retval: A dict containing image metadata """ - data = {'name': name, 'is_public': False} + data = {'name': name, 'is_public': False, 'instance_id': instance_id} image_meta = self.image_service.create(context, data) params = {'image_id': image_meta['id']} self._cast_compute_message('snapshot_instance', context, instance_id, diff --git a/nova/image/glance.py b/nova/image/glance.py index 15fca69b8..8e6ecbc43 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -36,6 +36,7 @@ GlanceClient = utils.import_class('glance.client.Client') class GlanceImageService(service.BaseImageService): """Provides storage and retrieval of disk image objects within Glance.""" + IMAGE_PROPERTIES = ['instance_id', 'os_type'] def __init__(self): self.client = GlanceClient(FLAGS.glance_host, FLAGS.glance_port) @@ -57,10 +58,12 @@ class GlanceImageService(service.BaseImageService): Returns a dict containing image data for the given opaque image id. """ try: - image = self.client.get_image_meta(image_id) + metadata = self.client.get_image_meta(image_id) except glance_exception.NotFound: raise exception.NotFound - return image + + meta = self._depropertify_metadata_from_glance(metadata) + return meta def show_by_name(self, context, name): """ @@ -88,7 +91,9 @@ class GlanceImageService(service.BaseImageService): raise exception.NotFound for chunk in image_chunks: data.write(chunk) - return metadata + + meta = self._depropertify_metadata_from_glance(metadata) + return meta def create(self, context, metadata, data=None): """ @@ -97,7 +102,12 @@ class GlanceImageService(service.BaseImageService): :raises AlreadyExists if the image already exist. """ - return self.client.add_image(metadata, data) + LOG.debug(_("Creating image in Glance. Metdata passed in %s"), + metadata) + + meta = self._propertify_metadata_for_glance(metadata) + LOG.debug(_("Metadata after formatting for Glance %s"), meta) + return self.client.add_image(meta, data) def update(self, context, image_id, metadata, data=None): """Replace the contents of the given image with the new data. @@ -129,3 +139,43 @@ class GlanceImageService(service.BaseImageService): Clears out all images """ pass + + @classmethod + def _propertify_metadata_for_glance(cls, metadata): + """Return a metadata dict suitable for passing to Glance. + + The ImageService exposes metadata as a flat-dict; however, Glance + distinguishes between two different types of metadata: + + 1. First-class attributes: These are columns on the image table + and represent metadata that is common to all images on all IAAS + providers. + + 2. Properties: These are entries in the image_properties table and + represent image/IAAS-provider specific metadata. + + To reconcile this difference, this function accepts a flat-dict of + metadata, figures out which attributes are stored as image properties + in Glance, and then adds those to a `properties` dict nested within + the metadata. + """ + new_metadata = metadata.copy() + properties = {} + for property_ in cls.IMAGE_PROPERTIES: + if property_ in new_metadata: + value = new_metadata.pop(property_) + properties[property_] = value + new_metadata['properties'] = properties + return new_metadata + + @classmethod + def _depropertify_metadata_from_glance(cls, metadata): + """Return a metadata dict suitable for returning from ImageService + """ + new_metadata = metadata.copy() + properties = new_metadata.pop('properties') + for property_ in cls.IMAGE_PROPERTIES: + if property_ in properties and property_ not in new_metadata: + value = properties[property_] + new_metadata[property_] = value + return new_metadata diff --git a/nova/test.py b/nova/test.py index d8a47464f..c41551bf3 100644 --- a/nova/test.py +++ b/nova/test.py @@ -150,3 +150,34 @@ class TestCase(unittest.TestCase): _wrapped.func_name = self.originalAttach.func_name rpc.Consumer.attach_to_eventlet = _wrapped + + # Useful assertions + def assertDictMatch(self, d1, d2): + """Assert two dicts are equivalent. + + This is a 'deep' match in the sense that it handles nested + dictionaries appropriately. + """ + def raise_assertion(msg): + d1str = str(d1) + d2str = str(d2) + base_msg = ("Dictionaries do not match. %(msg)s d1: %(d1str)s " + "d2: %(d2str)s" % locals()) + raise AssertionError(base_msg) + + d1keys = set(d1.keys()) + d2keys = set(d2.keys()) + if d1keys != d2keys: + d1only = d1keys - d2keys + d2only = d2keys - d1keys + raise_assertion("Keys in d1 and not d2: %(d1only)s. " + "Keys in d2 and not d1: %(d2only)s" % locals()) + + for key in d1keys: + d1value = d1[key] + d2value = d2[key] + if hasattr(d1value, 'keys') and hasattr(d2value, 'keys'): + self.assertDictMatch(d1value, d2value) + elif d1value != d2value: + raise_assertion("d1['%(key)s']=%(d1value)s != " + "d2['%(key)s']=%(d2value)s" % locals()) diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index e50d11a3d..1c7d926ba 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -132,6 +132,19 @@ def stub_out_compute_api_snapshot(stubs): stubs.Set(nova.compute.API, 'snapshot', snapshot) +def stub_out_glance_add_image(stubs, sent_to_glance): + """ + We return the metadata sent to glance by modifying the sent_to_glance dict + in place. + """ + orig_add_image = glance_client.Client.add_image + def fake_add_image(context, metadata, data=None): + sent_to_glance['metadata'] = metadata + sent_to_glance['data'] = data + return orig_add_image(metadata, data) + stubs.Set(glance_client.Client, 'add_image', fake_add_image) + + def stub_out_glance(stubs, initial_fixtures=None): class FakeGlanceClient: @@ -153,8 +166,10 @@ def stub_out_glance(stubs, initial_fixtures=None): raise glance_exc.NotFound def fake_add_image(self, image_meta, data=None): - id = ''.join(random.choice(string.letters) for _ in range(20)) - image_meta['id'] = id + if 'id' not in image_meta: + image_id = ''.join(random.choice(string.letters) + for _ in range(20)) + image_meta['id'] = image_id self.fixtures.append(image_meta) return image_meta diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 76f758929..0e6d538f9 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -28,6 +28,7 @@ import tempfile import stubout import webob +from glance import client as glance_client from nova import context from nova import exception from nova import flags @@ -166,11 +167,51 @@ class GlanceImageServiceTest(test.TestCase, self.service = utils.import_object(service_class) self.context = context.RequestContext(None, None) self.service.delete_all() + self.sent_to_glance = {} + fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) def tearDown(self): self.stubs.UnsetAll() super(GlanceImageServiceTest, self).tearDown() + def test_create_propertified_images_with_instance_id(self): + """ + Some attributes are passed to Glance as image-properties (ex. + instance_id). + + This tests asserts that the ImageService exposes them as if they were + first-class attribrutes, but that they are passed to Glance as image + properties. + """ + fixture = {'id': 123, 'instance_id': 42, 'name': 'test image'} + image_id = self.service.create(self.context, fixture)['id'] + + expected = {'id': 123, + 'name': 'test image', + 'properties': {'instance_id': 42}} + self.assertDictMatch(self.sent_to_glance['metadata'], expected) + + # The ImageService shouldn't leak the fact that the instance_id + # happens to be stored as a property in Glance + expected = {'id': 123, 'instance_id': 42, 'name': 'test image'} + image_meta = self.service.show(self.context, image_id) + self.assertDictMatch(image_meta, expected) + + def test_create_propertified_images_without_instance_id(self): + """ + Some attributes are passed to Glance as image-properties (ex. + instance_id). + + This tests asserts that the ImageService exposes them as if they were + first-class attribrutes, but that they are passed to Glance as image + properties. + """ + fixture = {'id': 123, 'name': 'test image'} + image_id = self.service.create(self.context, fixture)['id'] + + expected = {'id': 123, 'name': 'test image', 'properties': {}} + self.assertDictMatch(self.sent_to_glance['metadata'], expected) + class ImageControllerWithGlanceServiceTest(test.TestCase): |