Add 'create' method to GlanceImageServiceV2
This commit adds support of 'create' method and related unit tests in GlanceImageServiceV2. partially implements bp use-glance-v2-api Change-Id: Ibebc1ec674dff452096a0c8f5c6d9daca5ee8aaa
This commit is contained in:
@@ -630,6 +630,65 @@ class GlanceImageServiceV2(object):
|
||||
if close_file:
|
||||
data.close()
|
||||
|
||||
def create(self, context, image_meta, data=None):
|
||||
"""Store the image data and return the new image object."""
|
||||
# Here we workaround the situation when user wants to activate an
|
||||
# empty image right after the creation. In Glance v1 api (and
|
||||
# therefore in Nova) it is enough to set 'size = 0'. v2 api
|
||||
# doesn't allow this hack - we have to send an upload request with
|
||||
# empty data.
|
||||
force_activate = data is None and image_meta.get('size') == 0
|
||||
|
||||
sent_service_image_meta = _translate_to_glance(image_meta)
|
||||
|
||||
try:
|
||||
image = self._create_v2(context, sent_service_image_meta,
|
||||
data, force_activate)
|
||||
except glanceclient.exc.HTTPException:
|
||||
_reraise_translated_exception()
|
||||
|
||||
return _translate_from_glance(image)
|
||||
|
||||
def _add_location(self, context, image_id, location):
|
||||
# 'show_multiple_locations' must be enabled in glance api conf file.
|
||||
try:
|
||||
return self._client.call(context, 2, 'add_location', image_id,
|
||||
location, {})
|
||||
except glanceclient.exc.HTTPBadRequest:
|
||||
_reraise_translated_exception()
|
||||
|
||||
def _upload_data(self, context, image_id, data):
|
||||
self._client.call(context, 2, 'upload', image_id, data)
|
||||
return self._client.call(context, 2, 'get', image_id)
|
||||
|
||||
def _create_v2(self, context, sent_service_image_meta, data=None,
|
||||
force_activate=False):
|
||||
# Glance v1 allows image activation without setting disk and
|
||||
# container formats, v2 doesn't. It leads to the dirtiest workaround
|
||||
# where we have to hardcode this parameters.
|
||||
if force_activate:
|
||||
data = ''
|
||||
if 'disk_format' not in sent_service_image_meta:
|
||||
sent_service_image_meta['disk_format'] = 'qcow2'
|
||||
if 'container_format' not in sent_service_image_meta:
|
||||
sent_service_image_meta['container_format'] = 'bare'
|
||||
|
||||
location = sent_service_image_meta.pop('location', None)
|
||||
image = self._client.call(
|
||||
context, 2, 'create', **sent_service_image_meta)
|
||||
image_id = image['id']
|
||||
|
||||
# Sending image location in a separate request.
|
||||
if location:
|
||||
image = self._add_location(context, image_id, location)
|
||||
|
||||
# If we have some data we have to send it in separate request and
|
||||
# update the image then.
|
||||
if data is not None:
|
||||
image = self._upload_data(context, image_id, data)
|
||||
|
||||
return image
|
||||
|
||||
def delete(self, context, image_id):
|
||||
"""Delete the given image.
|
||||
|
||||
@@ -748,9 +807,34 @@ def _is_image_available(context, image):
|
||||
def _translate_to_glance(image_meta):
|
||||
image_meta = _convert_to_string(image_meta)
|
||||
image_meta = _remove_read_only(image_meta)
|
||||
# TODO(mfedosin): Remove this check once we move to glance V2
|
||||
# completely and enable convert to v2 every time.
|
||||
if not CONF.glance.use_glance_v1:
|
||||
# v2 requires several additional changes
|
||||
image_meta = _convert_to_v2(image_meta)
|
||||
return image_meta
|
||||
|
||||
|
||||
def _convert_to_v2(image_meta):
|
||||
output = {}
|
||||
for name, value in six.iteritems(image_meta):
|
||||
if name == 'properties':
|
||||
for prop_name, prop_value in six.iteritems(value):
|
||||
output[prop_name] = str(prop_value)
|
||||
elif name in ('min_ram', 'min_disk'):
|
||||
output[name] = int(value)
|
||||
elif name == 'is_public':
|
||||
output['visibility'] = 'public' if value else 'private'
|
||||
elif name == 'size' or name == 'deleted':
|
||||
continue
|
||||
elif name in ('kernel_id', 'ramdisk_id') and value == 'None':
|
||||
output[name] = None
|
||||
else:
|
||||
output[name] = value
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _translate_from_glance(image, include_locations=False):
|
||||
# TODO(mfedosin): Remove this check once we move to glance V2
|
||||
# completely.
|
||||
|
||||
@@ -1979,7 +1979,8 @@ class TestCreate(test.NoDBTestCase):
|
||||
|
||||
@mock.patch('nova.image.glance._translate_from_glance')
|
||||
@mock.patch('nova.image.glance._translate_to_glance')
|
||||
def test_create_success(self, trans_to_mock, trans_from_mock):
|
||||
def test_create_success_v1(self, trans_to_mock, trans_from_mock):
|
||||
self.flags(use_glance_v1=True, group='glance')
|
||||
translated = {
|
||||
'image_id': mock.sentinel.image_id
|
||||
}
|
||||
@@ -1992,7 +1993,7 @@ class TestCreate(test.NoDBTestCase):
|
||||
service = glance.GlanceImageService(client)
|
||||
image_meta = service.create(ctx, image_mock)
|
||||
|
||||
trans_to_mock.assert_called_once_with(image_mock)
|
||||
trans_to_mock.assert_called_once_with(image_mock,)
|
||||
client.call.assert_called_once_with(ctx, 1, 'create',
|
||||
image_id=mock.sentinel.image_id)
|
||||
trans_from_mock.assert_called_once_with(mock.sentinel.image_meta)
|
||||
@@ -2008,11 +2009,69 @@ class TestCreate(test.NoDBTestCase):
|
||||
image_id=mock.sentinel.image_id,
|
||||
data=mock.sentinel.data)
|
||||
|
||||
@mock.patch('nova.image.glance._translate_from_glance')
|
||||
@mock.patch('nova.image.glance._translate_to_glance')
|
||||
def test_create_success_v2(
|
||||
self, trans_to_mock, trans_from_mock):
|
||||
self.flags(use_glance_v1=False, group='glance')
|
||||
translated = {
|
||||
'name': mock.sentinel.name,
|
||||
}
|
||||
trans_to_mock.return_value = translated
|
||||
trans_from_mock.return_value = mock.sentinel.trans_from
|
||||
image_mock = mock.MagicMock(spec=dict)
|
||||
client = mock.MagicMock()
|
||||
client.call.return_value = {'id': '123'}
|
||||
ctx = mock.sentinel.ctx
|
||||
service = glance.GlanceImageServiceV2(client)
|
||||
image_meta = service.create(ctx, image_mock)
|
||||
trans_to_mock.assert_called_once_with(image_mock)
|
||||
# Verify that the 'id' element has been removed as a kwarg to
|
||||
# the call to glanceclient's update (since the image ID is
|
||||
# supplied as a positional arg), and that the
|
||||
# purge_props default is True.
|
||||
client.call.assert_called_once_with(ctx, 2, 'create',
|
||||
name=mock.sentinel.name)
|
||||
trans_from_mock.assert_called_once_with({'id': '123'})
|
||||
self.assertEqual(mock.sentinel.trans_from, image_meta)
|
||||
|
||||
# Now verify that if we supply image data to the call,
|
||||
# that the client is also called with the data kwarg
|
||||
client.reset_mock()
|
||||
client.call.return_value = {'id': mock.sentinel.image_id}
|
||||
service.create(ctx, {}, data=mock.sentinel.data)
|
||||
|
||||
self.assertEqual(3, client.call.call_count)
|
||||
|
||||
@mock.patch('nova.image.glance._translate_from_glance')
|
||||
@mock.patch('nova.image.glance._translate_to_glance')
|
||||
def test_create_success_v2_with_location(
|
||||
self, trans_to_mock, trans_from_mock):
|
||||
self.flags(use_glance_v1=False, group='glance')
|
||||
translated = {
|
||||
'id': mock.sentinel.id,
|
||||
'name': mock.sentinel.name,
|
||||
'location': mock.sentinel.location
|
||||
}
|
||||
trans_to_mock.return_value = translated
|
||||
trans_from_mock.return_value = mock.sentinel.trans_from
|
||||
image_mock = mock.MagicMock(spec=dict)
|
||||
client = mock.MagicMock()
|
||||
client.call.return_value = translated
|
||||
ctx = mock.sentinel.ctx
|
||||
service = glance.GlanceImageServiceV2(client)
|
||||
image_meta = service.create(ctx, image_mock)
|
||||
trans_to_mock.assert_called_once_with(image_mock)
|
||||
self.assertEqual(2, client.call.call_count)
|
||||
trans_from_mock.assert_called_once_with(translated)
|
||||
self.assertEqual(mock.sentinel.trans_from, image_meta)
|
||||
|
||||
@mock.patch('nova.image.glance._reraise_translated_exception')
|
||||
@mock.patch('nova.image.glance._translate_from_glance')
|
||||
@mock.patch('nova.image.glance._translate_to_glance')
|
||||
def test_create_client_failure(self, trans_to_mock, trans_from_mock,
|
||||
def test_create_client_failure_v1(self, trans_to_mock, trans_from_mock,
|
||||
reraise_mock):
|
||||
self.flags(use_glance_v1=True, group='glance')
|
||||
translated = {}
|
||||
trans_to_mock.return_value = translated
|
||||
image_mock = mock.MagicMock(spec=dict)
|
||||
@@ -2027,6 +2086,26 @@ class TestCreate(test.NoDBTestCase):
|
||||
trans_to_mock.assert_called_once_with(image_mock)
|
||||
self.assertFalse(trans_from_mock.called)
|
||||
|
||||
@mock.patch('nova.image.glance._reraise_translated_exception')
|
||||
@mock.patch('nova.image.glance._translate_from_glance')
|
||||
@mock.patch('nova.image.glance._translate_to_glance')
|
||||
def test_create_client_failure_v2(self, trans_to_mock, trans_from_mock,
|
||||
reraise_mock):
|
||||
self.flags(use_glance_v1=False, group='glance')
|
||||
translated = {}
|
||||
trans_to_mock.return_value = translated
|
||||
image_mock = mock.MagicMock(spec=dict)
|
||||
raised = exception.Invalid()
|
||||
client = mock.MagicMock()
|
||||
client.call.side_effect = glanceclient.exc.BadRequest
|
||||
ctx = mock.sentinel.ctx
|
||||
reraise_mock.side_effect = raised
|
||||
service = glance.GlanceImageServiceV2(client)
|
||||
|
||||
self.assertRaises(exception.Invalid, service.create, ctx, image_mock)
|
||||
trans_to_mock.assert_called_once_with(image_mock)
|
||||
self.assertFalse(trans_from_mock.called)
|
||||
|
||||
|
||||
class TestUpdate(test.NoDBTestCase):
|
||||
|
||||
|
||||
Reference in New Issue
Block a user