summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--nova/compute/api.py2
-rw-r--r--nova/image/glance.py58
-rw-r--r--nova/test.py31
-rw-r--r--nova/tests/api/openstack/fakes.py19
-rw-r--r--nova/tests/api/openstack/test_images.py41
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):