Plumb image import functionality through our glance module

This just provides minimal support for calling the import API in Glance.
That API can do more things, but it is unlikely Nova would ever need to
call them, so this is rather opinionated and could be extended later if
needed.

Related to blueprint rbd-glance-multistore

Change-Id: Icf78fcabad8b966b6b5c289e1b660c01c928272d
This commit is contained in:
Dan Smith 2020-05-27 09:21:45 -07:00
parent 1cae0cd722
commit 4a6a366b05
3 changed files with 123 additions and 0 deletions

View File

@ -591,6 +591,10 @@ class ImageBadRequest(Invalid):
"%(response)s")
class ImageImportImpossible(Invalid):
msg_fmt = _("Import of image %(image_id)s refused: %(reason)s")
class ImageQuotaExceeded(NovaException):
msg_fmt = _("Quota exceeded or out of space for image %(image_id)s "
"in the image service.")

View File

@ -654,6 +654,41 @@ class GlanceImageServiceV2(object):
raise exception.ImageDeleteConflict(reason=six.text_type(exc))
return True
def image_import_copy(self, context, image_id, stores):
"""Copy an image to another store using image_import.
This triggers the Glance image_import API with an opinionated
method of 'copy-image' to a list of stores. This will initiate
a copy of the image from one of the existing stores to the
stores provided.
:param context: The RequestContext
:param image_id: The image to copy
:param stores: A list of stores to copy the image to
:raises: ImageNotFound if the image does not exist.
:raises: ImageNotAuthorized if the user is not permitted to
import/copy this image
:raises: ImageImportImpossible if the image cannot be imported
for workflow reasons (not active, etc)
:raises: ImageBadRequest if the image is already in the requested
store (which may be a race)
"""
try:
self._client.call(context, 2, 'image_import', args=(image_id,),
kwargs={'method': 'copy-image',
'stores': stores})
except glanceclient.exc.NotFound:
raise exception.ImageNotFound(image_id=image_id)
except glanceclient.exc.HTTPForbidden:
raise exception.ImageNotAuthorized(image_id=image_id)
except glanceclient.exc.HTTPConflict as exc:
raise exception.ImageImportImpossible(image_id=image_id,
reason=str(exc))
except glanceclient.exc.HTTPBadRequest as exc:
raise exception.ImageBadRequest(image_id=image_id,
response=str(exc))
def _extract_query_params_v2(params):
_params = {}
@ -1189,3 +1224,13 @@ class API(object):
return session.download(context, image_id, data=data,
dst_path=dest_path,
trusted_certs=trusted_certs)
def copy_image_to_store(self, context, image_id, store):
"""Initiate a store-to-store copy in glance.
:param context: The RequestContext.
:param image_id: The image to copy.
:param store: The glance store to target the copy.
"""
session, image_id = self._get_session_and_image_id(context, image_id)
return session.image_import_copy(context, image_id, [store])

View File

@ -2087,3 +2087,77 @@ class TestSafeFSync(test.NoDBTestCase):
"""Validate fsync not called for socket."""
self.common(mock_isfifo, False, mock_issock, True, mock_fstat)
mock_fsync.assert_not_called()
class TestImportCopy(test.NoDBTestCase):
"""Tests the image import/copy methods."""
def _test_import(self, exception=None):
client = mock.MagicMock()
if exception:
client.call.side_effect = exception
else:
client.call.return_value = True
ctx = mock.sentinel.ctx
service = glance.GlanceImageServiceV2(client)
service.image_import_copy(ctx, mock.sentinel.image_id,
[mock.sentinel.store])
return client
def test_image_import_copy_success(self):
client = self._test_import()
client.call.assert_called_once_with(
mock.sentinel.ctx, 2, 'image_import',
args=(mock.sentinel.image_id,),
kwargs={'method': 'copy-image',
'stores': [mock.sentinel.store]})
def test_image_import_copy_not_found(self):
self.assertRaises(exception.ImageNotFound,
self._test_import,
glanceclient.exc.NotFound)
def test_image_import_copy_not_authorized(self):
self.assertRaises(exception.ImageNotAuthorized,
self._test_import,
glanceclient.exc.HTTPForbidden)
def test_image_import_copy_failed_workflow(self):
self.assertRaises(exception.ImageImportImpossible,
self._test_import,
glanceclient.exc.HTTPConflict)
def test_image_import_copy_failed_already_imported(self):
self.assertRaises(exception.ImageBadRequest,
self._test_import,
glanceclient.exc.HTTPBadRequest)
def test_api(self):
api = glance.API()
with mock.patch.object(api, '_get_session_and_image_id') as g:
session = mock.MagicMock()
g.return_value = session, mock.sentinel.image_id
api.copy_image_to_store(mock.sentinel.ctx,
mock.sentinel.image_id,
mock.sentinel.store)
session.image_import_copy.assert_called_once_with(
mock.sentinel.ctx, mock.sentinel.image_id,
[mock.sentinel.store])
def test_api_to_client(self):
# Test all the way down to the client to test the interface between
# API and GlanceImageServiceV2
wrapper = mock.MagicMock()
client = glance.GlanceImageServiceV2(client=wrapper)
api = glance.API()
with mock.patch.object(api, '_get_session_and_image_id') as m:
m.return_value = (client, mock.sentinel.image_id)
api.copy_image_to_store(mock.sentinel.ctx,
mock.sentinel.image_id,
mock.sentinel.store)
wrapper.call.assert_called_once_with(
mock.sentinel.ctx, 2, 'image_import',
args=(mock.sentinel.image_id,),
kwargs={'method': 'copy-image',
'stores': [mock.sentinel.store]})