diff --git a/nova/image/glance.py b/nova/image/glance.py index 45ee91850a94..cbce51f7e4e9 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -689,6 +689,49 @@ class GlanceImageServiceV2(object): return image + def update(self, context, image_id, image_meta, data=None, + purge_props=True): + """Modify the given image with the new data.""" + sent_service_image_meta = _translate_to_glance(image_meta) + # NOTE(bcwaldon): id is not an editable field, but it is likely to be + # passed in by calling code. Let's be nice and ignore it. + sent_service_image_meta.pop('id', None) + sent_service_image_meta['image_id'] = image_id + + try: + if purge_props: + # In Glance v2 we have to explicitly set prop names + # we want to remove. + all_props = set(self.show( + context, image_id)['properties'].keys()) + props_to_update = set( + image_meta.get('properties', {}).keys()) + remove_props = list(all_props - props_to_update) + sent_service_image_meta['remove_props'] = remove_props + + image = self._update_v2(context, sent_service_image_meta, data) + except Exception: + _reraise_translated_image_exception(image_id) + + return _translate_from_glance(image) + + def _update_v2(self, context, sent_service_image_meta, data=None): + location = sent_service_image_meta.pop('location', None) + image_id = sent_service_image_meta['image_id'] + image = self._client.call( + context, 2, 'update', **sent_service_image_meta) + + # 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. diff --git a/nova/tests/unit/image/test_glance.py b/nova/tests/unit/image/test_glance.py index 4cc41034f50a..05e599280479 100644 --- a/nova/tests/unit/image/test_glance.py +++ b/nova/tests/unit/image/test_glance.py @@ -2113,7 +2113,9 @@ class TestUpdate(test.NoDBTestCase): @mock.patch('nova.image.glance._translate_from_glance') @mock.patch('nova.image.glance._translate_to_glance') - def test_update_success(self, trans_to_mock, trans_from_mock): + def test_update_success_v1( + self, trans_to_mock, trans_from_mock): + self.flags(use_glance_v1=True, group='glance') translated = { 'id': mock.sentinel.image_id, 'name': mock.sentinel.name @@ -2125,7 +2127,8 @@ class TestUpdate(test.NoDBTestCase): client.call.return_value = mock.sentinel.image_meta ctx = mock.sentinel.ctx service = glance.GlanceImageService(client) - image_meta = service.update(ctx, mock.sentinel.image_id, image_mock) + image_meta = service.update( + ctx, mock.sentinel.image_id, image_mock, purge_props=True) trans_to_mock.assert_called_once_with(image_mock) # Verify that the 'id' element has been removed as a kwarg to @@ -2151,11 +2154,93 @@ class TestUpdate(test.NoDBTestCase): purge_props=True, data=mock.sentinel.data) + @mock.patch('nova.image.glance.GlanceImageServiceV2.show') + @mock.patch('nova.image.glance._translate_from_glance') + @mock.patch('nova.image.glance._translate_to_glance') + def test_update_success_v2( + self, trans_to_mock, trans_from_mock, show_mock): + self.flags(use_glance_v1=False, group='glance') + image = { + 'id': mock.sentinel.image_id, + 'name': mock.sentinel.name, + 'properties': {'prop_to_keep': '4'} + } + + translated = { + 'id': mock.sentinel.image_id, + 'name': mock.sentinel.name, + 'prop_to_keep': '4' + } + + trans_to_mock.return_value = translated + trans_from_mock.return_value = mock.sentinel.trans_from + client = mock.MagicMock() + client.call.return_value = mock.sentinel.image_meta + ctx = mock.sentinel.ctx + show_mock.return_value = { + 'image_id': mock.sentinel.image_id, + 'properties': {'prop_to_remove': '1', + 'prop_to_keep': '3'} + } + service = glance.GlanceImageServiceV2(client) + image_meta = service.update( + ctx, mock.sentinel.image_id, image, purge_props=True) + show_mock.assert_called_once_with( + mock.sentinel.ctx, mock.sentinel.image_id) + trans_to_mock.assert_called_once_with(image) + # 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, 'update', + image_id=mock.sentinel.image_id, + name=mock.sentinel.name, + prop_to_keep='4', + remove_props=['prop_to_remove']) + trans_from_mock.assert_called_once_with(mock.sentinel.image_meta) + 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.update(ctx, mock.sentinel.image_id, {}, + data=mock.sentinel.data) + + self.assertEqual(3, client.call.call_count) + + @mock.patch('nova.image.glance.GlanceImageServiceV2.show') + @mock.patch('nova.image.glance._translate_from_glance') + @mock.patch('nova.image.glance._translate_to_glance') + def test_update_success_v2_with_location( + self, trans_to_mock, trans_from_mock, show_mock): + self.flags(use_glance_v1=False, group='glance') + translated = { + 'id': mock.sentinel.id, + 'name': mock.sentinel.name, + 'location': mock.sentinel.location + } + show_mock.return_value = {'image_id': mock.sentinel.image_id} + 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.update(ctx, mock.sentinel.image_id, + image_mock, purge_props=False) + 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_image_exception') @mock.patch('nova.image.glance._translate_from_glance') @mock.patch('nova.image.glance._translate_to_glance') - def test_update_client_failure(self, trans_to_mock, trans_from_mock, + def test_update_client_failure_v1(self, trans_to_mock, trans_from_mock, reraise_mock): + self.flags(use_glance_v1=True, group='glance') translated = { 'name': mock.sentinel.name } @@ -2179,6 +2264,48 @@ class TestUpdate(test.NoDBTestCase): self.assertFalse(trans_from_mock.called) reraise_mock.assert_called_once_with(mock.sentinel.image_id) + @mock.patch('nova.image.glance.GlanceImageServiceV2.show') + @mock.patch('nova.image.glance._reraise_translated_image_exception') + @mock.patch('nova.image.glance._translate_from_glance') + @mock.patch('nova.image.glance._translate_to_glance') + def test_update_client_failure_v2(self, trans_to_mock, trans_from_mock, + reraise_mock, show_mock): + self.flags(use_glance_v1=False, group='glance') + image = { + 'id': mock.sentinel.image_id, + 'name': mock.sentinel.name, + 'properties': {'prop_to_keep': '4'} + } + + translated = { + 'id': mock.sentinel.image_id, + 'name': mock.sentinel.name, + 'prop_to_keep': '4' + } + trans_to_mock.return_value = translated + trans_from_mock.return_value = mock.sentinel.trans_from + raised = exception.ImageNotAuthorized(image_id=123) + client = mock.MagicMock() + client.call.side_effect = glanceclient.exc.Forbidden + ctx = mock.sentinel.ctx + reraise_mock.side_effect = raised + show_mock.return_value = { + 'image_id': mock.sentinel.image_id, + 'properties': {'prop_to_remove': '1', + 'prop_to_keep': '3'} + } + service = glance.GlanceImageServiceV2(client) + + self.assertRaises(exception.ImageNotAuthorized, + service.update, ctx, mock.sentinel.image_id, + image) + client.call.assert_called_once_with(ctx, 2, 'update', + image_id=mock.sentinel.image_id, + name=mock.sentinel.name, + prop_to_keep='4', + remove_props=['prop_to_remove']) + reraise_mock.assert_called_once_with(mock.sentinel.image_id) + class TestDelete(test.NoDBTestCase):