Merge "Fixes rbd _delete_image snapshot with missing image"

This commit is contained in:
Jenkins 2013-10-10 16:19:48 +00:00 committed by Gerrit Code Review
commit 1f03b99ec2
3 changed files with 272 additions and 74 deletions

View File

@ -38,7 +38,8 @@ try:
import rados import rados
import rbd import rbd
except ImportError: except ImportError:
pass rados = None
rbd = None
DEFAULT_POOL = 'rbd' DEFAULT_POOL = 'rbd'
DEFAULT_CONFFILE = '' # librados will locate the default conf file 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) librbd.create(ioctx, image_name, size, order, old_format=True)
return StoreLocation({'image': image_name}) 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 image_name Image's name
:param snapshot_name Image snapshot'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 rados.Rados(conffile=self.conf_file, rados_id=self.user) as conn:
with conn.open_ioctx(self.pool) as ioctx: with conn.open_ioctx(self.pool) as ioctx:
if snapshot_name: try:
# First remove snapshot.
if snapshot_name is not None:
with rbd.Image(ioctx, image_name) as image: with rbd.Image(ioctx, image_name) as image:
try: try:
image.unprotect_snap(snapshot_name) image.unprotect_snap(snapshot_name)
except rbd.ImageBusy: except rbd.ImageBusy:
log_msg = _("snapshot %s@%s could not be " log_msg = _("snapshot %(image)s@%(snap)s "
"unprotected because it is in use") "could not be unprotected because "
LOG.debug(log_msg % (image_name, snapshot_name)) "it is in use")
LOG.debug(log_msg %
{'image': image_name,
'snap': snapshot_name})
raise exception.InUseByStore() raise exception.InUseByStore()
image.remove_snap(snapshot_name) image.remove_snap(snapshot_name)
try:
# Then delete image.
rbd.RBD().remove(ioctx, image_name) rbd.RBD().remove(ioctx, image_name)
except rbd.ImageNotFound: except rbd.ImageNotFound:
raise exception.NotFound( raise exception.NotFound(
@ -340,11 +347,14 @@ class Store(glance.store.base.Store):
if loc.snapshot: if loc.snapshot:
image.create_snap(loc.snapshot) image.create_snap(loc.snapshot)
image.protect_snap(loc.snapshot) image.protect_snap(loc.snapshot)
except: except Exception as exc:
# Note(zhiyan): clean up already received data when # Delete image if one was created
# error occurs such as ImageSizeLimitExceeded exception. try:
with excutils.save_and_reraise_exception():
self._delete_image(loc.image, loc.snapshot) self._delete_image(loc.image, loc.snapshot)
except exception.NotFound:
pass
raise exc
return (loc.get_uri(), image_size, checksum.hexdigest(), {}) return (loc.get_uri(), image_size, checksum.hexdigest(), {})

View File

@ -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()

View File

@ -13,80 +13,136 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import contextlib
import StringIO import StringIO
import stubout
from glance.common import exception from glance.common import exception
from glance.common import utils 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.store.rbd import StoreLocation
from glance.tests.unit import base from glance.tests.unit import base
try: from glance.tests.unit.fake_rados import mock_rados
import rados from glance.tests.unit.fake_rados import mock_rbd
import rbd
except ImportError:
rbd = None
RBD_CONF = {'verbose': True,
'debug': True,
'default_store': 'rbd'}
FAKE_CHUNKSIZE = 1
class TestStore(base.StoreClearingUnitTest): class TestStore(base.StoreClearingUnitTest):
def setUp(self): def setUp(self):
"""Establish a clean test environment""" """Establish a clean test environment"""
self.config(**RBD_CONF)
super(TestStore, self).setUp() super(TestStore, self).setUp()
self.stubs = stubout.StubOutForTesting() self.stubs.Set(rbd_store, 'rados', mock_rados)
self.store = Store() self.stubs.Set(rbd_store, 'rbd', mock_rbd)
self.store.chunk_size = FAKE_CHUNKSIZE self.store = rbd_store.Store()
self.addCleanup(self.stubs.UnsetAll) self.store.chunk_size = 2
self.called_commands_actual = []
def test_cleanup_when_add_image_exception(self): self.called_commands_expected = []
if rbd is None: self.store_specs = {'image': 'fake_image',
msg = 'RBD store can not add images, skip test.' 'snapshot': 'fake_snapshot'}
self.skipTest(msg) self.location = StoreLocation(self.store_specs)
called_commands = []
class FakeConnection(object):
@contextlib.contextmanager
def open_ioctx(self, *args, **kwargs):
yield None
class FakeImage(object):
def write(self, *args, **kwargs):
called_commands.append('write')
return FAKE_CHUNKSIZE
@contextlib.contextmanager
def _fake_rados(*args, **kwargs):
yield FakeConnection()
@contextlib.contextmanager
def _fake_image(*args, **kwargs):
yield FakeImage()
def test_add_w_rbd_image_exception(self):
def _fake_create_image(*args, **kwargs): def _fake_create_image(*args, **kwargs):
called_commands.append('create') self.called_commands_actual.append('create')
return StoreLocation({'image': 'fake_image', return self.location
'snapshot': 'fake_snapshot'})
def _fake_delete_image(*args, **kwargs): def _fake_delete_image(*args, **kwargs):
called_commands.append('delete') self.called_commands_actual.append('delete')
def _fake_enter(*args, **kwargs):
raise exception.NotFound("")
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(self.store, '_create_image', _fake_create_image)
self.stubs.Set(self.store, '_delete_image', _fake_delete_image) self.stubs.Set(self.store, '_delete_image', _fake_delete_image)
self.stubs.Set(mock_rbd.Image, '__enter__', _fake_enter)
self.assertRaises(exception.NotFound, self.store.add,
'fake_image_id', StringIO.StringIO('xx'), 2)
self.called_commands_expected = ['create', 'delete']
def test_add_duplicate_image(self):
def _fake_create_image(*args, **kwargs):
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):
self.called_commands_actual.append('delete')
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.assertRaises(exception.ImageSizeLimitExceeded,
self.store.add, self.store.add, 'fake_image_id', data, 5)
'fake_image_id',
utils.LimitingReader(StringIO.StringIO('xx'), 1), self.called_commands_expected = ['write', 'delete']
2)
self.assertEqual(called_commands, ['create', 'write', 'delete']) def tearDown(self):
self.assertEqual(self.called_commands_actual,
self.called_commands_expected)
super(TestStore, self).tearDown()