Merge "Plumb image import functionality through our glance module"
This commit is contained in:
commit
bbb8e3a17c
|
@ -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.")
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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]})
|
||||
|
|
Loading…
Reference in New Issue