diff options
author | Jenkins <jenkins@review.openstack.org> | 2013-10-11 07:46:57 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2013-10-11 07:46:57 +0000 |
commit | 42cfa8cc9dc3241103a6764ae1e3e58cfef2a763 (patch) | |
tree | 9769fe45211f21581344aa73c519448fc1da15e7 | |
parent | 95e87aafc0af9a91e2ca14a5d7da4d962902fafd (diff) | |
parent | 1fd64662fe66eda5fee404a37586f30cae2bf785 (diff) | |
download | glance-42cfa8cc9dc3241103a6764ae1e3e58cfef2a763.tar.gz |
Merge "Fixes rbd _delete_image snapshot with missing image" into milestone-proposed2013.2.rc22013.2
-rw-r--r-- | glance/store/rbd.py | 44 | ||||
-rw-r--r-- | glance/tests/unit/fake_rados.py | 132 | ||||
-rw-r--r-- | glance/tests/unit/test_rbd_store.py | 164 |
3 files changed, 269 insertions, 71 deletions
diff --git a/glance/store/rbd.py b/glance/store/rbd.py index 9cb63d3b5..c53df5f9f 100644 --- a/glance/store/rbd.py +++ b/glance/store/rbd.py @@ -38,7 +38,8 @@ try: import rados import rbd except ImportError: - pass + rados = None + rbd = None DEFAULT_POOL = 'rbd' DEFAULT_CONFFILE = '' # librados will locate the default conf file @@ -249,9 +250,9 @@ class Store(glance.store.base.Store): librbd.create(ioctx, image_name, size, order, old_format=True) return StoreLocation({'image': image_name}) - def _delete_image(self, image_name, snapshot_name): + def _delete_image(self, image_name, snapshot_name=None): """ - Find the image file to delete. + Delete RBD image and snapshot. :param image_name Image's name :param snapshot_name Image snapshot's name @@ -261,17 +262,23 @@ class Store(glance.store.base.Store): """ with rados.Rados(conffile=self.conf_file, rados_id=self.user) as conn: with conn.open_ioctx(self.pool) as ioctx: - if snapshot_name: - with rbd.Image(ioctx, image_name) as image: - try: - image.unprotect_snap(snapshot_name) - except rbd.ImageBusy: - log_msg = _("snapshot %s@%s could not be " - "unprotected because it is in use") - LOG.debug(log_msg % (image_name, snapshot_name)) - raise exception.InUseByStore() - image.remove_snap(snapshot_name) try: + # First remove snapshot. + if snapshot_name is not None: + with rbd.Image(ioctx, image_name) as image: + try: + image.unprotect_snap(snapshot_name) + except rbd.ImageBusy: + log_msg = _("snapshot %(image)s@%(snap)s " + "could not be unprotected because " + "it is in use") + LOG.debug(log_msg % + {'image': image_name, + 'snap': snapshot_name}) + raise exception.InUseByStore() + image.remove_snap(snapshot_name) + + # Then delete image. rbd.RBD().remove(ioctx, image_name) except rbd.ImageNotFound: raise exception.NotFound( @@ -340,11 +347,14 @@ class Store(glance.store.base.Store): if loc.snapshot: image.create_snap(loc.snapshot) image.protect_snap(loc.snapshot) - except: - # Note(zhiyan): clean up already received data when - # error occurs such as ImageSizeLimitExceeded exception. - with excutils.save_and_reraise_exception(): + except Exception as exc: + # Delete image if one was created + try: self._delete_image(loc.image, loc.snapshot) + except exception.NotFound: + pass + + raise exc return (loc.get_uri(), image_size, checksum.hexdigest(), {}) diff --git a/glance/tests/unit/fake_rados.py b/glance/tests/unit/fake_rados.py new file mode 100644 index 000000000..f94d44853 --- /dev/null +++ b/glance/tests/unit/fake_rados.py @@ -0,0 +1,132 @@ +# Copyright 2013 Canonical Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class mock_rados(object): + + class ioctx(object): + def __init__(self, *args, **kwargs): + pass + + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + return False + + def close(self, *args, **kwargs): + pass + + class Rados(object): + + def __init__(self, *args, **kwargs): + pass + + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + return False + + def connect(self, *args, **kwargs): + pass + + def open_ioctx(self, *args, **kwargs): + return mock_rados.ioctx() + + def shutdown(self, *args, **kwargs): + pass + + +class mock_rbd(object): + + class ImageExists(Exception): + pass + + class ImageBusy(Exception): + pass + + class ImageNotFound(Exception): + pass + + class Image(object): + + def __init__(self, *args, **kwargs): + pass + + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + pass + + def create_snap(self, *args, **kwargs): + pass + + def remove_snap(self, *args, **kwargs): + pass + + def protect_snap(self, *args, **kwargs): + pass + + def unprotect_snap(self, *args, **kwargs): + pass + + def read(self, *args, **kwargs): + raise NotImplementedError() + + def write(self, *args, **kwargs): + raise NotImplementedError() + + def resize(self, *args, **kwargs): + raise NotImplementedError() + + def discard(self, offset, length): + raise NotImplementedError() + + def close(self): + pass + + def list_snaps(self): + raise NotImplementedError() + + def parent_info(self): + raise NotImplementedError() + + def size(self): + raise NotImplementedError() + + class RBD(object): + + def __init__(self, *args, **kwargs): + pass + + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + return False + + def create(self, *args, **kwargs): + pass + + def remove(self, *args, **kwargs): + pass + + def list(self, *args, **kwargs): + raise NotImplementedError() + + def clone(self, *args, **kwargs): + raise NotImplementedError() diff --git a/glance/tests/unit/test_rbd_store.py b/glance/tests/unit/test_rbd_store.py index 9612a05f7..23e055fd9 100644 --- a/glance/tests/unit/test_rbd_store.py +++ b/glance/tests/unit/test_rbd_store.py @@ -13,80 +13,136 @@ # License for the specific language governing permissions and limitations # under the License. -import contextlib import StringIO - -import stubout - from glance.common import exception from glance.common import utils -from glance.store.rbd import Store +import glance.store.rbd as rbd_store +from glance.store.location import Location from glance.store.rbd import StoreLocation from glance.tests.unit import base -try: - import rados - import rbd -except ImportError: - rbd = None - - -RBD_CONF = {'verbose': True, - 'debug': True, - 'default_store': 'rbd'} -FAKE_CHUNKSIZE = 1 +from glance.tests.unit.fake_rados import mock_rados +from glance.tests.unit.fake_rados import mock_rbd class TestStore(base.StoreClearingUnitTest): def setUp(self): """Establish a clean test environment""" - self.config(**RBD_CONF) super(TestStore, self).setUp() - self.stubs = stubout.StubOutForTesting() - self.store = Store() - self.store.chunk_size = FAKE_CHUNKSIZE - self.addCleanup(self.stubs.UnsetAll) - - def test_cleanup_when_add_image_exception(self): - if rbd is None: - msg = 'RBD store can not add images, skip test.' - self.skipTest(msg) + self.stubs.Set(rbd_store, 'rados', mock_rados) + self.stubs.Set(rbd_store, 'rbd', mock_rbd) + self.store = rbd_store.Store() + self.store.chunk_size = 2 + self.called_commands_actual = [] + self.called_commands_expected = [] + self.store_specs = {'image': 'fake_image', + 'snapshot': 'fake_snapshot'} + self.location = StoreLocation(self.store_specs) + + def test_add_w_rbd_image_exception(self): + def _fake_create_image(*args, **kwargs): + self.called_commands_actual.append('create') + return self.location - called_commands = [] + def _fake_delete_image(*args, **kwargs): + self.called_commands_actual.append('delete') - class FakeConnection(object): - @contextlib.contextmanager - def open_ioctx(self, *args, **kwargs): - yield None + def _fake_enter(*args, **kwargs): + raise exception.NotFound("") - class FakeImage(object): - def write(self, *args, **kwargs): - called_commands.append('write') - return FAKE_CHUNKSIZE + self.stubs.Set(self.store, '_create_image', _fake_create_image) + self.stubs.Set(self.store, '_delete_image', _fake_delete_image) + self.stubs.Set(mock_rbd.Image, '__enter__', _fake_enter) - @contextlib.contextmanager - def _fake_rados(*args, **kwargs): - yield FakeConnection() + self.assertRaises(exception.NotFound, self.store.add, + 'fake_image_id', StringIO.StringIO('xx'), 2) - @contextlib.contextmanager - def _fake_image(*args, **kwargs): - yield FakeImage() + self.called_commands_expected = ['create', 'delete'] + def test_add_duplicate_image(self): def _fake_create_image(*args, **kwargs): - called_commands.append('create') - return StoreLocation({'image': 'fake_image', - 'snapshot': 'fake_snapshot'}) + self.called_commands_actual.append('create') + raise mock_rbd.ImageExists() + + self.stubs.Set(self.store, '_create_image', _fake_create_image) + self.assertRaises(exception.Duplicate, self.store.add, + 'fake_image_id', StringIO.StringIO('xx'), 2) + self.called_commands_expected = ['create'] + + def test_delete(self): + def _fake_remove(*args, **kwargs): + self.called_commands_actual.append('remove') + + self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) + self.store.delete(Location('test_rbd_store', StoreLocation, + self.location.get_uri())) + self.called_commands_expected = ['remove'] + + def test__delete_image(self): + def _fake_remove(*args, **kwargs): + self.called_commands_actual.append('remove') + + self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) + self.store._delete_image(self.location) + self.called_commands_expected = ['remove'] + + def test__delete_image_w_snap(self): + def _fake_unprotect_snap(*args, **kwargs): + self.called_commands_actual.append('unprotect_snap') + + def _fake_remove_snap(*args, **kwargs): + self.called_commands_actual.append('remove_snap') + + def _fake_remove(*args, **kwargs): + self.called_commands_actual.append('remove') + + self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) + self.stubs.Set(mock_rbd.Image, 'unprotect_snap', _fake_unprotect_snap) + self.stubs.Set(mock_rbd.Image, 'remove_snap', _fake_remove_snap) + self.store._delete_image(self.location, snapshot_name='snap') + + self.called_commands_expected = ['unprotect_snap', 'remove_snap', + 'remove'] + + def test__delete_image_w_snap_exc_image_not_found(self): + def _fake_unprotect_snap(*args, **kwargs): + self.called_commands_actual.append('unprotect_snap') + raise mock_rbd.ImageNotFound() + + self.stubs.Set(mock_rbd.Image, 'unprotect_snap', _fake_unprotect_snap) + self.assertRaises(exception.NotFound, self.store._delete_image, + self.location, snapshot_name='snap') + + self.called_commands_expected = ['unprotect_snap'] + + def test__delete_image_exc_image_not_found(self): + def _fake_remove(*args, **kwargs): + self.called_commands_actual.append('remove') + raise mock_rbd.ImageNotFound() + + self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) + self.assertRaises(exception.NotFound, self.store._delete_image, + self.location, snapshot_name='snap') + + self.called_commands_expected = ['remove'] + + def test_image_size_exceeded_exception(self): + def _fake_write(*args, **kwargs): + if 'write' not in self.called_commands_actual: + self.called_commands_actual.append('write') + raise exception.ImageSizeLimitExceeded def _fake_delete_image(*args, **kwargs): - called_commands.append('delete') + self.called_commands_actual.append('delete') - self.stubs.Set(rados, 'Rados', _fake_rados) - self.stubs.Set(rbd, 'Image', _fake_image) - self.stubs.Set(self.store, '_create_image', _fake_create_image) + self.stubs.Set(mock_rbd.Image, 'write', _fake_write) self.stubs.Set(self.store, '_delete_image', _fake_delete_image) - + data = utils.LimitingReader(StringIO.StringIO('abcd'), 4) self.assertRaises(exception.ImageSizeLimitExceeded, - self.store.add, - 'fake_image_id', - utils.LimitingReader(StringIO.StringIO('xx'), 1), - 2) - self.assertEqual(called_commands, ['create', 'write', 'delete']) + self.store.add, 'fake_image_id', data, 5) + + self.called_commands_expected = ['write', 'delete'] + + def tearDown(self): + self.assertEqual(self.called_commands_actual, + self.called_commands_expected) + super(TestStore, self).tearDown() |