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")
|
"%(response)s")
|
||||||
|
|
||||||
|
|
||||||
|
class ImageImportImpossible(Invalid):
|
||||||
|
msg_fmt = _("Import of image %(image_id)s refused: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
class ImageQuotaExceeded(NovaException):
|
class ImageQuotaExceeded(NovaException):
|
||||||
msg_fmt = _("Quota exceeded or out of space for image %(image_id)s "
|
msg_fmt = _("Quota exceeded or out of space for image %(image_id)s "
|
||||||
"in the image service.")
|
"in the image service.")
|
||||||
|
|
|
@ -654,6 +654,41 @@ class GlanceImageServiceV2(object):
|
||||||
raise exception.ImageDeleteConflict(reason=six.text_type(exc))
|
raise exception.ImageDeleteConflict(reason=six.text_type(exc))
|
||||||
return True
|
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):
|
def _extract_query_params_v2(params):
|
||||||
_params = {}
|
_params = {}
|
||||||
|
@ -1189,3 +1224,13 @@ class API(object):
|
||||||
return session.download(context, image_id, data=data,
|
return session.download(context, image_id, data=data,
|
||||||
dst_path=dest_path,
|
dst_path=dest_path,
|
||||||
trusted_certs=trusted_certs)
|
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."""
|
"""Validate fsync not called for socket."""
|
||||||
self.common(mock_isfifo, False, mock_issock, True, mock_fstat)
|
self.common(mock_isfifo, False, mock_issock, True, mock_fstat)
|
||||||
mock_fsync.assert_not_called()
|
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