Browse Source

Merge "Plumb image import functionality through our glance module"

changes/26/738126/1
Zuul 2 weeks ago
committed by Gerrit Code Review
parent
commit
bbb8e3a17c
3 changed files with 123 additions and 0 deletions
  1. +4
    -0
      nova/exception.py
  2. +45
    -0
      nova/image/glance.py
  3. +74
    -0
      nova/tests/unit/image/test_glance.py

+ 4
- 0
nova/exception.py 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.")


+ 45
- 0
nova/image/glance.py 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])

+ 74
- 0
nova/tests/unit/image/test_glance.py 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]})

Loading…
Cancel
Save