NetApp: E-Series remove snapshot limitations

The E-Series backend does not allow snapshots defined under a snapshot
group to be deleted out of order. Only the oldest snapshot image may
be deleted. In order to work around this behavior, we would only define
a single snapshot on each snapshot group. Unfortunately, this would
limit the total number of snapshots to a total of 4 per volume.

In order to remove this limitation without causing deletion errors
when a newer snapshot is deleted, a soft-delete was implemented that
will remove the snapshot from Cinder management and purge the
snapshot from the backend once other dependent snapshots have been
deleted.

In order to ensure that our clone operations have a snapshot group
available, one of the 4 snapshot groups that can be defined on a
volume will be reserved for clone operations and will not be
allowed to be utilized for snapshot creation. This will limit the
total number of snapshots supported per volume to a maximum of 96.

The new 1.4 version of the WebServices proxy is required in order
to support the full snapshot behavior. Since snapshots are such
an integral part of the driver, 1.4 is now required for running
the driver with this release.

Co-Authored-By: Erik Johannes <erik.johannes@netapp.com>
Change-Id: Ie097743b167c1086ecf0536ad696786bdcd241b2
This commit is contained in:
Michael Price 2015-10-23 13:52:12 -07:00
parent 3631fd2576
commit 2ecfb1ba22
11 changed files with 1653 additions and 88 deletions

View File

@ -24,7 +24,29 @@ import mock
from cinder.volume import configuration as conf
from cinder.volume.drivers.netapp.eseries import utils
import cinder.volume.drivers.netapp.options as na_opts
import cinder.volume.drivers.netapp.utils as na_utils
FAKE_CINDER_VOLUME = {
'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef',
'size': 1,
'volume_name': 'lun1',
'host': 'hostname@backend#DDP',
'os_type': 'linux',
'provider_location': 'lun1',
'name_id': '114774fb-e15a-4fae-8ee2-c9723e3645ef',
'provider_auth': 'provider a b',
'project_id': 'project',
'display_name': None,
'display_description': 'lun1',
'volume_type_id': None,
'migration_status': None,
'attach_status': "detached"
}
FAKE_CINDER_SNAPSHOT = {
'id': '78f95b9d-3f02-4781-a512-1a1c921d48a1',
'volume': FAKE_CINDER_VOLUME
}
MULTIATTACH_HOST_GROUP = {
'clusterRef': '8500000060080E500023C7340036035F515B78FC',
@ -636,15 +658,15 @@ STORAGE_SYSTEM = {
}
SNAPSHOT_GROUP = {
'id': '3300000060080E500023C7340000098D5294AC9A',
'status': 'optimal',
'autoDeleteLimit': 0,
'maxRepositoryCapacity': '-65536',
'rollbackStatus': 'none',
'unusableRepositoryCapacity': '0',
'pitGroupRef':
'3300000060080E500023C7340000098D5294AC9A',
'pitGroupRef': '3300000060080E500023C7340000098D5294AC9A',
'clusterSize': 65536,
'label': 'C6JICISVHNG2TFZX4XB5ZWL7O',
'label': 'C6JICISVHNG2TFZX4XB5ZWL7F',
'maxBaseCapacity': '476187142128128',
'repositoryVolume': '3600000060080E500023BB3400001FA952CEF12C',
'fullWarnThreshold': 99,
@ -655,10 +677,13 @@ SNAPSHOT_GROUP = {
'consistencyGroupRef': '0000000000000000000000000000000000000000',
'volumeHandle': 49153,
'consistencyGroup': False,
'baseVolume': '0200000060080E500023C734000009825294A534'
'baseVolume': '0200000060080E500023C734000009825294A534',
'snapshotCount': 32
}
SNAPSHOT_IMAGE = {
'id': '3400000060080E500023BB3400631F335294A5A8',
'baseVol': '0200000060080E500023C734000009825294A534',
'status': 'optimal',
'pitCapacity': '2147483648',
'pitTimestamp': '1389315375',
@ -671,6 +696,84 @@ SNAPSHOT_IMAGE = {
'pitSequenceNumber': '19'
}
SNAPSHOT_VOLUME = {
'id': '35000000600A0980006077F80000F8BF566581AA',
'viewRef': '35000000600A0980006077F80000F8BF566581AA',
'worldWideName': '600A0980006077F80000F8BF566581AA',
'baseVol': '02000000600A0980006077F80000F89B56657E26',
'basePIT': '0000000000000000000000000000000000000000',
'boundToPIT': False,
'accessMode': 'readOnly',
'label': 'UZJ45SLUKNGWRF3QZHBTOG4C4E_DEL',
'status': 'stopped',
'currentManager': '070000000000000000000001',
'preferredManager': '070000000000000000000001',
'repositoryVolume': '0000000000000000000000000000000000000000',
'fullWarnThreshold': 0,
'viewTime': '1449453419',
'viewSequenceNumber': '2104',
'volumeHandle': 16510,
'clusterSize': 0,
'maxRepositoryCapacity': '0',
'unusableRepositoryCapacity': '0',
'membership': {
'viewType': 'individual',
'cgViewRef': None
},
'mgmtClientAttribute': 0,
'offline': False,
'volumeFull': False,
'repositoryCapacity': '0',
'baseVolumeCapacity': '1073741824',
'totalSizeInBytes': '0',
'consistencyGroupId': None,
'volumeCopyTarget': False,
'cloneCopy': False,
'volumeCopySource': False,
'pitBaseVolume': False,
'asyncMirrorTarget': False,
'asyncMirrorSource': False,
'protectionType': 'type0Protection',
'remoteMirrorSource': False,
'remoteMirrorTarget': False,
'wwn': '600A0980006077F80000F8BF566581AA',
'listOfMappings': [],
'mapped': False,
'currentControllerId': '070000000000000000000001',
'preferredControllerId': '070000000000000000000001',
'onlineVolumeCopy': False,
'objectType': 'pitView',
'name': 'UZJ45SLUKNGWRF3QZHBTOG4C4E',
}
FAKE_BACKEND_STORE = {
'key': 'cinder-snapshots',
'value': '{"3300000060080E50003416400000E90D56B047E5":"2"}'
}
FAKE_CINDER_VOLUME = {
'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef',
'size': 1,
'volume_name': 'lun1',
'host': 'hostname@backend#DDP',
'os_type': 'linux',
'provider_location': 'lun1',
'name_id': '114774fb-e15a-4fae-8ee2-c9723e3645ef',
'provider_auth': 'provider a b',
'project_id': 'project',
'display_name': None,
'display_description': 'lun1',
'volume_type_id': None,
'migration_status': None,
'attach_status': "detached"
}
FAKE_CINDER_SNAPSHOT = {
'id': '78f95b9d-3f02-4781-a512-1a1c921d48a1',
'volume': FAKE_CINDER_VOLUME,
'provider_id': '3400000060080E500023BB3400631F335294A5A8'
}
HARDWARE_INVENTORY_SINGLE_CONTROLLER = {
'controllers': [
{
@ -909,6 +1012,15 @@ FAKE_CLIENT_PARAMS = {
}
def list_snapshot_groups(numGroups):
snapshots = []
for n in range(0, numGroups):
s = copy.deepcopy(SNAPSHOT_GROUP)
s['label'] = s['label'][:-1] + str(n)
snapshots.append(s)
return snapshots
def create_configuration_eseries():
config = conf.Configuration(None)
config.append_config_values(na_opts.netapp_connection_opts)
@ -958,10 +1070,13 @@ def deepcopy_return_value_class_decorator(cls):
@deepcopy_return_value_class_decorator
class FakeEseriesClient(object):
features = mock.Mock()
features = na_utils.Features()
def __init__(self, *args, **kwargs):
pass
self.features.add_feature('AUTOSUPPORT')
self.features.add_feature('SSC_API_V2')
self.features.add_feature('REST_1_3_RELEASE')
self.features.add_feature('REST_1_4_RELEASE')
def list_storage_pools(self):
return STORAGE_POOLS
@ -1028,6 +1143,9 @@ class FakeEseriesClient(object):
def get_host(self, *args, **kwargs):
return HOST
def create_volume(self, *args, **kwargs):
return VOLUME
def create_volume_mapping(self, *args, **kwargs):
return VOLUME_MAPPING
@ -1061,6 +1179,9 @@ class FakeEseriesClient(object):
def list_snapshot_images(self):
return [SNAPSHOT_IMAGE]
def list_snapshot_image(self):
return SNAPSHOT_IMAGE
def list_host_types(self):
return [
{
@ -1117,6 +1238,24 @@ class FakeEseriesClient(object):
def delete_vol_copy_job(self, *args, **kwargs):
pass
def create_snapshot_image(self, *args, **kwargs):
return SNAPSHOT_IMAGE
def create_snapshot_volume(self, *args, **kwargs):
return SNAPSHOT_VOLUME
def list_snapshot_volumes(self, *args, **kwargs):
return [SNAPSHOT_VOLUME]
def list_snapshot_volume(self, *args, **kwargs):
return SNAPSHOT_IMAGE
def create_snapshot_group(self, *args, **kwargs):
return SNAPSHOT_GROUP
def list_snapshot_group(self, *args, **kwargs):
return SNAPSHOT_GROUP
def delete_snapshot_volume(self, *args, **kwargs):
pass
@ -1125,3 +1264,21 @@ class FakeEseriesClient(object):
def update_stored_system_password(self, *args, **kwargs):
pass
def update_snapshot_volume(self, *args, **kwargs):
return SNAPSHOT_VOLUME
def delete_snapshot_image(self, *args, **kwargs):
pass
def delete_snapshot_group(self, *args, **kwargs):
pass
def restart_snapshot_volume(self, *args, **kwargs):
pass
def list_backend_store(self, key):
return {}
def save_backend_store(self, key, val):
pass

View File

@ -18,6 +18,7 @@
import copy
import ddt
import json
import mock
from simplejson import scanner
@ -752,6 +753,159 @@ class NetAppEseriesClientDriverTestCase(test.TestCase):
**{'object-id':
fake_volume['id']})
def test_list_snapshot_group(self):
grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=grp))
fake_ref = 'fake'
result = self.my_client.list_snapshot_group(fake_ref)
self.assertEqual(grp, result)
invoke.assert_called_once_with(
'GET', self.my_client.RESOURCE_PATHS['snapshot_group'],
**{'object-id': fake_ref})
def test_list_snapshot_groups(self):
grps = [copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)]
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=grps))
result = self.my_client.list_snapshot_groups()
self.assertEqual(grps, result)
invoke.assert_called_once_with(
'GET', self.my_client.RESOURCE_PATHS['snapshot_groups'])
def test_delete_snapshot_group(self):
invoke = self.mock_object(self.my_client, '_invoke')
fake_ref = 'fake'
self.my_client.delete_snapshot_group(fake_ref)
invoke.assert_called_once_with(
'DELETE', self.my_client.RESOURCE_PATHS['snapshot_group'],
**{'object-id': fake_ref})
@ddt.data((None, None, None, None, None), ('1', 50, 75, 32, 'purgepit'))
@ddt.unpack
def test_create_snapshot_group(self, pool_id, repo, warn, limit, policy):
vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=vol))
snap_grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
result = self.my_client.create_snapshot_group(
snap_grp['label'], snap_grp['id'], pool_id, repo, warn, limit,
policy)
self.assertEqual(vol, result)
invoke.assert_called_once_with(
'POST', self.my_client.RESOURCE_PATHS['snapshot_groups'],
{'baseMappableObjectId': snap_grp['id'], 'name': snap_grp['label'],
'storagePoolId': pool_id, 'repositoryPercentage': repo,
'warningThreshold': warn, 'autoDeleteLimit': limit,
'fullPolicy': policy})
def test_list_snapshot_volumes(self):
vols = [copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)]
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=vols))
result = self.my_client.list_snapshot_volumes()
self.assertEqual(vols, result)
invoke.assert_called_once_with(
'GET', self.my_client.RESOURCE_PATHS['snapshot_volumes'])
def test_delete_snapshot_volume(self):
invoke = self.mock_object(self.my_client, '_invoke')
fake_ref = 'fake'
self.my_client.delete_snapshot_volume(fake_ref)
invoke.assert_called_once_with(
'DELETE', self.my_client.RESOURCE_PATHS['snapshot_volume'],
**{'object-id': fake_ref})
@ddt.data((None, None, None, None), ('1', 50, 75, 'readWrite'))
@ddt.unpack
def test_create_snapshot_volume(self, pool_id, repo, warn, mode):
vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=vol))
result = self.my_client.create_snapshot_volume(
vol['basePIT'], vol['label'], vol['id'], pool_id,
repo, warn, mode)
self.assertEqual(vol, result)
invoke.assert_called_once_with(
'POST', self.my_client.RESOURCE_PATHS['snapshot_volumes'],
mock.ANY)
def test_update_snapshot_volume(self):
snap_id = '1'
label = 'name'
pct = 99
vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=vol))
result = self.my_client.update_snapshot_volume(snap_id, label, pct)
self.assertEqual(vol, result)
invoke.assert_called_once_with(
'POST', self.my_client.RESOURCE_PATHS['snapshot_volume'],
{'name': label, 'fullThreshold': pct}, **{'object-id': snap_id})
def test_create_snapshot_image(self):
img = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=img))
grp_id = '1'
result = self.my_client.create_snapshot_image(grp_id)
self.assertEqual(img, result)
invoke.assert_called_once_with(
'POST', self.my_client.RESOURCE_PATHS['snapshot_images'],
{'groupId': grp_id})
def test_list_snapshot_image(self):
img = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=img))
fake_ref = 'fake'
result = self.my_client.list_snapshot_image(fake_ref)
self.assertEqual(img, result)
invoke.assert_called_once_with(
'GET', self.my_client.RESOURCE_PATHS['snapshot_image'],
**{'object-id': fake_ref})
def test_list_snapshot_images(self):
imgs = [copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)]
invoke = self.mock_object(self.my_client, '_invoke', mock.Mock(
return_value=imgs))
result = self.my_client.list_snapshot_images()
self.assertEqual(imgs, result)
invoke.assert_called_once_with(
'GET', self.my_client.RESOURCE_PATHS['snapshot_images'])
def test_delete_snapshot_image(self):
invoke = self.mock_object(self.my_client, '_invoke')
fake_ref = 'fake'
self.my_client.delete_snapshot_image(fake_ref)
invoke.assert_called_once_with(
'DELETE', self.my_client.RESOURCE_PATHS['snapshot_image'],
**{'object-id': fake_ref})
@ddt.data('00.00.00.00', '01.52.9000.2', '01.52.9001.2', '01.51.9000.3',
'01.51.9001.3', '01.51.9010.5', '0.53.9000.3', '0.53.9001.4')
def test_api_version_not_support_asup(self, api_version):
@ -802,6 +956,31 @@ class NetAppEseriesClientDriverTestCase(test.TestCase):
self.assertTrue(self.my_client.features.SSC_API_V2.supported)
@ddt.data('00.00.00.00', '01.52.9000.5', '01.52.9001.2', '00.53.9001.3',
'01.52.9090.1', '1.52.9010.7', '0.53.9011.7')
def test_api_version_not_support_1_3(self, api_version):
self.mock_object(client.RestClient,
'get_eseries_api_info',
mock.Mock(return_value=('proxy', api_version)))
client.RestClient._init_features(self.my_client)
self.assertFalse(self.my_client.features.REST_1_3_RELEASE.supported)
@ddt.data('01.53.9000.1', '01.53.9000.5', '01.53.8999.1',
'01.54.9010.20', '01.54.9000.1', '02.51.9000.3',
'02.52.8999.3', '02.51.8999.2')
def test_api_version_1_3(self, api_version):
self.mock_object(client.RestClient,
'get_eseries_api_info',
mock.Mock(return_value=('proxy', api_version)))
client.RestClient._init_features(self.my_client)
self.assertTrue(self.my_client.features.REST_1_3_RELEASE.supported)
def test_invoke_bad_content_type(self):
"""Tests the invoke behavior with a non-JSON response"""
fake_response = mock.Mock()
@ -816,6 +995,31 @@ class NetAppEseriesClientDriverTestCase(test.TestCase):
self.my_client._invoke, 'GET',
eseries_fake.FAKE_ENDPOINT_HTTP)
def test_list_backend_store(self):
path = self.my_client.RESOURCE_PATHS.get('persistent-store')
fake_store = copy.deepcopy(eseries_fake.FAKE_BACKEND_STORE)
invoke = self.mock_object(
self.my_client, '_invoke', mock.Mock(
return_value=fake_store))
expected = json.loads(fake_store.get('value'))
result = self.my_client.list_backend_store('key')
self.assertEqual(expected, result)
invoke.assert_called_once_with('GET', path, key='key')
def test_save_backend_store(self):
path = self.my_client.RESOURCE_PATHS.get('persistent-stores')
fake_store = copy.deepcopy(eseries_fake.FAKE_BACKEND_STORE)
key = 'key'
invoke = self.mock_object(
self.my_client, '_invoke',
mock.Mock())
self.my_client.save_backend_store(key, fake_store)
invoke.assert_called_once_with('POST', path, mock.ANY)
@ddt.ddt
class TestWebserviceClientTestCase(test.TestCase):

View File

@ -93,9 +93,12 @@ class NetAppESeriesDriverTestCase(object):
self.library = self.driver.library
self.mock_object(self.library,
'_check_mode_get_or_register_storage_system')
self.mock_object(self.library, '_version_check')
self.mock_object(self.driver.library, '_check_storage_system')
self.driver.do_setup(context='context')
self.driver.library._client._endpoint = fakes.FAKE_ENDPOINT_HTTP
self.driver.library._client.features = mock.Mock()
self.driver.library._client.features.REST_1_4_RELEASE = True
def _set_config(self, configuration):
configuration.netapp_storage_family = 'eseries'
@ -126,13 +129,11 @@ class NetAppESeriesDriverTestCase(object):
pass
def test_embedded_mode(self):
self.mock_object(self.driver.library,
'_check_mode_get_or_register_storage_system')
self.mock_object(client.RestClient, '_init_features')
configuration = self._set_config(self.create_configuration())
configuration.netapp_controller_ips = '127.0.0.1,127.0.0.3'
driver = common.NetAppDriver(configuration=configuration)
self.mock_object(driver.library, '_version_check')
self.mock_object(client.RestClient, 'list_storage_systems', mock.Mock(
return_value=[fakes.STORAGE_SYSTEM]))
driver.do_setup(context='context')

View File

@ -20,6 +20,7 @@
import copy
import ddt
import time
import uuid
import mock
from oslo_utils import units
@ -27,6 +28,7 @@ import six
from six.moves import range
from six.moves import reduce
from cinder import context
from cinder import exception
from cinder import test
@ -44,6 +46,7 @@ from cinder.zonemanager import utils as fczm_utils
def get_fake_volume():
"""Return a fake Cinder Volume that can be used a parameter"""
return {
'id': '114774fb-e15a-4fae-8ee2-c9723e3645ef', 'size': 1,
'volume_name': 'lun1', 'host': 'hostname@backend#DDP',
@ -72,10 +75,19 @@ class NetAppEseriesLibraryTestCase(test.TestCase):
# Deprecated Option
self.library.configuration.netapp_storage_pools = None
self.library._client = eseries_fake.FakeEseriesClient()
self.mock_object(self.library, '_start_periodic_tasks',
new_attr=mock.Mock())
self.mock_object(library.cinder_utils, 'synchronized',
mock.Mock(return_value=lambda f: f))
with mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
new = cinder_utils.ZeroIntervalLoopingCall):
self.library.check_for_setup_error()
self.ctxt = context.get_admin_context()
def test_do_setup(self):
self.mock_object(self.library,
'_check_mode_get_or_register_storage_system')
@ -1070,10 +1082,13 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
self.library = library.NetAppESeriesLibrary("FAKE", **kwargs)
self.library._client = eseries_fake.FakeEseriesClient()
# We don't want the looping calls to run
self.mock_object(library.cinder_utils, 'synchronized',
mock.Mock(return_value=lambda f: f))
self.mock_object(self.library, '_start_periodic_tasks',
new_attr=mock.Mock())
self.ctxt = context.get_admin_context()
with mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
new = cinder_utils.ZeroIntervalLoopingCall):
self.library.check_for_setup_error()
@ -1170,32 +1185,82 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
get_fake_volume())
self.assertFalse(self.library._client.create_volume.call_count)
@ddt.data(0, 1, 2)
def test_create_snapshot(self, group_count):
"""Successful Snapshot creation test"""
fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
self.library._get_volume = mock.Mock(return_value=fake_eseries_volume)
fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL)
self.library._get_storage_pools = mock.Mock(return_value=[fake_pool])
fake_cinder_snapshot = copy.deepcopy(
eseries_fake.FAKE_CINDER_SNAPSHOT)
fake_snapshot_group_list = eseries_fake.list_snapshot_groups(
group_count)
fake_snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
fake_snapshot_image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
self.library._client.create_snapshot_group = mock.Mock(
return_value=fake_snapshot_group)
self.library._client.list_snapshot_groups = mock.Mock(
return_value=fake_snapshot_group_list)
self.library._client.create_snapshot_image = mock.Mock(
return_value=fake_snapshot_image)
self.library.create_snapshot(fake_cinder_snapshot)
@ddt.data(0, 1, 3)
def test_create_cloned_volume(self, snapshot_group_count):
"""Test creating cloned volume with different exist group counts. """
fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
self.library._get_volume = mock.Mock(return_value=fake_eseries_volume)
fake_pool = copy.deepcopy(eseries_fake.STORAGE_POOL)
self.library._get_storage_pools = mock.Mock(return_value=[fake_pool])
fake_snapshot_group_list = eseries_fake.list_snapshot_groups(
snapshot_group_count)
self.library._client.list_snapshot_groups = mock.Mock(
return_value=fake_snapshot_group_list)
fake_snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
self.library._client.create_snapshot_group = mock.Mock(
return_value=fake_snapshot_group)
fake_snapshot_image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
self.library._client.create_snapshot_image = mock.Mock(
return_value=fake_snapshot_image)
self.library._get_snapshot_group_for_snapshot = mock.Mock(
return_value=copy.deepcopy(eseries_fake.SNAPSHOT_GROUP))
fake_created_volume = copy.deepcopy(eseries_fake.VOLUMES[1])
self.library.create_volume_from_snapshot = mock.Mock(
return_value = fake_created_volume)
fake_cinder_volume = copy.deepcopy(eseries_fake.FAKE_CINDER_VOLUME)
extend_vol = {'id': uuid.uuid4(), 'size': 10}
self.library.create_cloned_volume(extend_vol, fake_cinder_volume)
def test_create_volume_from_snapshot(self):
fake_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
self.mock_object(self.library, "_schedule_and_create_volume",
mock.Mock(return_value=fake_eseries_volume))
self.mock_object(self.library, "_create_snapshot_volume",
mock.Mock(return_value=fake_eseries_volume))
self.mock_object(self.library._client, "delete_snapshot_volume")
self.mock_object(self.library, "_get_snapshot",
mock.Mock(return_value=copy.deepcopy(
eseries_fake.SNAPSHOT_IMAGE)))
self.library.create_volume_from_snapshot(
get_fake_volume(), fake_snapshot.fake_snapshot_obj(None))
get_fake_volume(), fake_snap)
self.assertEqual(
1, self.library._schedule_and_create_volume.call_count)
self.assertEqual(1, self.library._create_snapshot_volume.call_count)
self.assertEqual(
1, self.library._client.delete_snapshot_volume.call_count)
def test_create_volume_from_snapshot_create_fails(self):
fake_dest_eseries_volume = copy.deepcopy(eseries_fake.VOLUME)
self.mock_object(self.library, "_schedule_and_create_volume",
mock.Mock(return_value=fake_dest_eseries_volume))
self.mock_object(self.library, "_create_snapshot_volume",
mock.Mock(side_effect=exception.NetAppDriverException)
)
self.mock_object(self.library._client, "delete_snapshot_volume")
self.mock_object(self.library._client, "delete_volume")
self.mock_object(self.library._client, "delete_snapshot_volume")
self.mock_object(self.library, "_get_snapshot",
mock.Mock(return_value=copy.deepcopy(
eseries_fake.SNAPSHOT_IMAGE)))
self.mock_object(self.library._client, "create_snapshot_volume",
mock.Mock(
side_effect=exception.NetAppDriverException))
self.assertRaises(exception.NetAppDriverException,
self.library.create_volume_from_snapshot,
@ -1204,9 +1269,6 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
self.assertEqual(
1, self.library._schedule_and_create_volume.call_count)
self.assertEqual(1, self.library._create_snapshot_volume.call_count)
self.assertEqual(
0, self.library._client.delete_snapshot_volume.call_count)
# Ensure the volume we were going to copy to is cleaned up
self.library._client.delete_volume.assert_called_once_with(
fake_dest_eseries_volume['volumeRef'])
@ -1217,8 +1279,10 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
mock.Mock(return_value=fake_dest_eseries_volume))
self.mock_object(self.library, "_create_snapshot_volume",
mock.Mock(return_value=fake_dest_eseries_volume))
self.mock_object(self.library._client, "delete_snapshot_volume")
self.mock_object(self.library._client, "delete_volume")
self.mock_object(self.library, "_get_snapshot",
mock.Mock(return_value=copy.deepcopy(
eseries_fake.SNAPSHOT_IMAGE)))
fake_failed_volume_copy_job = copy.deepcopy(
eseries_fake.VOLUME_COPY_JOB)
@ -1237,9 +1301,6 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
self.assertEqual(
1, self.library._schedule_and_create_volume.call_count)
self.assertEqual(1, self.library._create_snapshot_volume.call_count)
self.assertEqual(
1, self.library._client.delete_snapshot_volume.call_count)
# Ensure the volume we were going to copy to is cleaned up
self.library._client.delete_volume.assert_called_once_with(
fake_dest_eseries_volume['volumeRef'])
@ -1249,6 +1310,12 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
fake_dest_eseries_volume['volumeRef'] = 'fake_volume_ref'
self.mock_object(self.library, "_schedule_and_create_volume",
mock.Mock(return_value=fake_dest_eseries_volume))
self.mock_object(self.library, "_get_snapshot",
mock.Mock(return_value=copy.deepcopy(
eseries_fake.SNAPSHOT_IMAGE)))
self.mock_object(self.library, '_create_snapshot_volume',
mock.Mock(return_value=copy.deepcopy(
eseries_fake.SNAPSHOT_VOLUME)))
self.mock_object(self.library, "_create_snapshot_volume",
mock.Mock(return_value=copy.deepcopy(
eseries_fake.VOLUME)))
@ -1262,12 +1329,494 @@ class NetAppEseriesLibraryMultiAttachTestCase(test.TestCase):
self.assertEqual(
1, self.library._schedule_and_create_volume.call_count)
self.assertEqual(1, self.library._create_snapshot_volume.call_count)
self.assertEqual(
1, self.library._client.delete_snapshot_volume.call_count)
# Ensure the volume we created is not cleaned up
self.assertEqual(0, self.library._client.delete_volume.call_count)
def test_create_snapshot_group(self):
label = 'label'
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snapshot_group['baseVolume'] = vol['id']
get_call = self.mock_object(
self.library, '_get_storage_pools', mock.Mock(return_value=None))
create_call = self.mock_object(
self.library._client, 'create_snapshot_group',
mock.Mock(return_value=snapshot_group))
actual = self.library._create_snapshot_group(label, vol)
get_call.assert_not_called()
create_call.assert_called_once_with(label, vol['id'], repo_percent=20)
self.assertEqual(snapshot_group, actual)
def test_create_snapshot_group_legacy_ddp(self):
self.library._client.features.REST_1_3_RELEASE = False
vol = copy.deepcopy(eseries_fake.VOLUME)
pools = copy.deepcopy(eseries_fake.STORAGE_POOLS)
pool = pools[-1]
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snapshot_group['baseVolume'] = vol['id']
vol['volumeGroupRef'] = pool['id']
pool['raidLevel'] = 'raidDiskPool'
get_call = self.mock_object(
self.library, '_get_storage_pools', mock.Mock(return_value=pools))
create_call = self.mock_object(
self.library._client, 'create_snapshot_group',
mock.Mock(return_value=snapshot_group))
actual = self.library._create_snapshot_group('label', vol)
create_call.assert_called_with('label', vol['id'],
vol['volumeGroupRef'],
repo_percent=mock.ANY)
get_call.assert_called_once_with()
self.assertEqual(snapshot_group, actual)
def test_create_snapshot_group_legacy_vg(self):
self.library._client.features.REST_1_3_RELEASE = False
vol = copy.deepcopy(eseries_fake.VOLUME)
vol_size_gb = int(vol['totalSizeInBytes']) / units.Gi
pools = copy.deepcopy(eseries_fake.STORAGE_POOLS)
pool = pools[0]
pool['raidLevel'] = 'raid6'
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snapshot_group['baseVolume'] = vol['id']
vol['volumeGroupRef'] = pool['id']
get_call = self.mock_object(
self.library, '_get_sorted_available_storage_pools',
mock.Mock(return_value=pools))
self.mock_object(self.library._client, 'create_snapshot_group',
mock.Mock(return_value=snapshot_group))
actual = self.library._create_snapshot_group('label', vol)
get_call.assert_called_once_with(vol_size_gb)
self.assertEqual(snapshot_group, actual)
def test_get_snapshot(self):
fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
get_snap = self.mock_object(
self.library._client, 'list_snapshot_image', mock.Mock(
return_value=snap))
result = self.library._get_snapshot(fake_snap)
self.assertEqual(snap, result)
get_snap.assert_called_once_with(fake_snap['provider_id'])
def test_get_snapshot_fail(self):
fake_snap = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
get_snap = self.mock_object(
self.library._client, 'list_snapshot_image', mock.Mock(
side_effect=exception.NotFound))
self.assertRaises(exception.NotFound, self.library._get_snapshot,
fake_snap)
get_snap.assert_called_once_with(fake_snap['provider_id'])
def test_get_snapshot_group_for_snapshot(self):
fake_id = 'id'
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
grp = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
get_snap = self.mock_object(
self.library, '_get_snapshot',
mock.Mock(return_value=snap))
get_grp = self.mock_object(self.library._client, 'list_snapshot_group',
mock.Mock(return_value=grp))
result = self.library._get_snapshot_group_for_snapshot(fake_id)
self.assertEqual(grp, result)
get_grp.assert_called_once_with(snap['pitGroupRef'])
get_snap.assert_called_once_with(fake_id)
def test_get_snapshot_group_for_snapshot_fail(self):
fake_id = 'id'
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
get_snap = self.mock_object(
self.library, '_get_snapshot',
mock.Mock(return_value=snap))
get_grp = self.mock_object(self.library._client, 'list_snapshot_group',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(exception.NotFound,
self.library._get_snapshot_group_for_snapshot,
fake_id)
get_grp.assert_called_once_with(snap['pitGroupRef'])
get_snap.assert_called_once_with(fake_id)
def test_get_snapshot_groups_for_volume(self):
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snapshot_group['baseVolume'] = vol['id']
# Generate some snapshot groups that will not match
snapshot_groups = [copy.deepcopy(snapshot_group) for i in range(
self.library.MAX_SNAPSHOT_GROUP_COUNT)]
for i, group in enumerate(snapshot_groups):
group['baseVolume'] = str(i)
snapshot_groups.append(snapshot_group)
get_call = self.mock_object(
self.library._client, 'list_snapshot_groups', mock.Mock(
return_value=snapshot_groups))
groups = self.library._get_snapshot_groups_for_volume(vol)
get_call.assert_called_once_with()
self.assertEqual([snapshot_group], groups)
def test_get_available_snapshot_group(self):
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snapshot_group['baseVolume'] = vol['id']
snapshot_group['snapshotCount'] = 0
# Generate some snapshot groups that will not match
reserved_group = copy.deepcopy(snapshot_group)
reserved_group['label'] += self.library.SNAPSHOT_VOL_COPY_SUFFIX
full_group = copy.deepcopy(snapshot_group)
full_group['snapshotCount'] = self.library.MAX_SNAPSHOT_COUNT
snapshot_groups = [snapshot_group, reserved_group, full_group]
get_call = self.mock_object(
self.library, '_get_snapshot_groups_for_volume', mock.Mock(
return_value=snapshot_groups))
group = self.library._get_available_snapshot_group(vol)
get_call.assert_called_once_with(vol)
self.assertEqual(snapshot_group, group)
def test_get_snapshot_groups_for_volume_not_found(self):
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snapshot_group['baseVolume'] = vol['id']
snapshot_group['snapshotCount'] = self.library.MAX_SNAPSHOT_COUNT
# Generate some snapshot groups that will not match
get_call = self.mock_object(
self.library, '_get_snapshot_groups_for_volume', mock.Mock(
return_value=[snapshot_group]))
group = self.library._get_available_snapshot_group(vol)
get_call.assert_called_once_with(vol)
self.assertIsNone(group)
def test_create_snapshot_available_snap_group(self):
expected_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
expected = {'provider_id': expected_snap['id']}
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
fake_label = 'fakeName'
self.mock_object(self.library, '_get_volume', mock.Mock(
return_value=vol))
create_call = self.mock_object(
self.library._client, 'create_snapshot_image', mock.Mock(
return_value=expected_snap))
self.mock_object(self.library, '_get_available_snapshot_group',
mock.Mock(return_value=snapshot_group))
self.mock_object(utils, 'convert_uuid_to_es_fmt',
mock.Mock(return_value=fake_label))
fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
model_update = self.library.create_snapshot(fake_snapshot)
self.assertEqual(expected, model_update)
create_call.assert_called_once_with(snapshot_group['id'])
@ddt.data(False, True)
def test_create_snapshot_failure(self, cleanup_failure):
"""Validate the behavior for a failure during snapshot creation"""
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snap_vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
fake_label = 'fakeName'
create_fail_exc = exception.NetAppDriverException('fail_create')
cleanup_fail_exc = exception.NetAppDriverException('volume_deletion')
if cleanup_failure:
exc_msg = cleanup_fail_exc.msg
delete_snap_grp = self.mock_object(
self.library, '_delete_snapshot_group',
mock.Mock(side_effect=cleanup_fail_exc))
else:
exc_msg = create_fail_exc.msg
delete_snap_grp = self.mock_object(
self.library, '_delete_snapshot_group')
self.mock_object(self.library, '_get_volume', mock.Mock(
return_value=vol))
self.mock_object(self.library._client, 'create_snapshot_image',
mock.Mock(
side_effect=create_fail_exc))
self.mock_object(self.library._client, 'create_snapshot_volume',
mock.Mock(return_value=snap_vol))
self.mock_object(self.library, '_get_available_snapshot_group',
mock.Mock(return_value=snapshot_group))
self.mock_object(utils, 'convert_uuid_to_es_fmt',
mock.Mock(return_value=fake_label))
fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
self.assertRaisesRegexp(exception.NetAppDriverException,
exc_msg,
self.library.create_snapshot,
fake_snapshot)
self.assertTrue(delete_snap_grp.called)
def test_create_snapshot_no_snap_group(self):
self.library._client.features = mock.Mock()
expected_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
fake_label = 'fakeName'
self.mock_object(self.library, '_get_volume', mock.Mock(
return_value=vol))
create_call = self.mock_object(
self.library._client, 'create_snapshot_image', mock.Mock(
return_value=expected_snap))
self.mock_object(self.library, '_get_snapshot_groups_for_volume',
mock.Mock(return_value=[snapshot_group]))
self.mock_object(self.library, '_get_available_snapshot_group',
mock.Mock(return_value=None))
self.mock_object(utils, 'convert_uuid_to_es_fmt',
mock.Mock(return_value=fake_label))
fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
snapshot = self.library.create_snapshot(fake_snapshot)
expected = {'provider_id': expected_snap['id']}
self.assertEqual(expected, snapshot)
create_call.assert_called_once_with(snapshot_group['id'])
def test_create_snapshot_no_snapshot_groups_remaining(self):
"""Test the failure condition where all snap groups are allocated"""
self.library._client.features = mock.Mock()
expected_snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
vol = copy.deepcopy(eseries_fake.VOLUME)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snap_vol = copy.deepcopy(eseries_fake.SNAPSHOT_VOLUME)
grp_count = (self.library.MAX_SNAPSHOT_GROUP_COUNT -
self.library.RESERVED_SNAPSHOT_GROUP_COUNT)
fake_label = 'fakeName'
self.mock_object(self.library, '_get_volume', mock.Mock(
return_value=vol))
self.mock_object(self.library._client, 'create_snapshot_image',
mock.Mock(return_value=expected_snap))
self.mock_object(self.library._client, 'create_snapshot_volume',
mock.Mock(return_value=snap_vol))
self.mock_object(self.library, '_get_available_snapshot_group',
mock.Mock(return_value=None))
self.mock_object(self.library, '_get_snapshot_groups_for_volume',
mock.Mock(return_value=[snapshot_group] * grp_count))
self.mock_object(utils, 'convert_uuid_to_es_fmt',
mock.Mock(return_value=fake_label))
fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
# Error message should contain the maximum number of supported
# snapshots
self.assertRaisesRegexp(exception.SnapshotLimitExceeded,
str(self.library.MAX_SNAPSHOT_COUNT *
grp_count),
self.library.create_snapshot, fake_snapshot)
def test_delete_snapshot(self):
fake_vol = cinder_utils.create_volume(self.ctxt)
fake_snap = cinder_utils.create_snapshot(self.ctxt, fake_vol['id'])
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
vol = copy.deepcopy(eseries_fake.VOLUME)
self.mock_object(self.library, '_get_volume', mock.Mock(
return_value=vol))
self.mock_object(self.library, '_get_snapshot', mock.Mock(
return_value=snap))
del_snap = self.mock_object(self.library, '_delete_es_snapshot',
mock.Mock())
self.library.delete_snapshot(fake_snap)
del_snap.assert_called_once_with(snap)
def test_delete_es_snapshot(self):
vol = copy.deepcopy(eseries_fake.VOLUME)
snap_count = 30
# Ensure that it's the oldest PIT
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
snapshot_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
fake_volume_refs = ['1', '2', snap['baseVol']]
fake_snapshot_group_refs = ['3', '4', snapshot_group['id']]
snapshots = [copy.deepcopy(snap) for i in range(snap_count)]
bitset = na_utils.BitSet(0)
for i, snapshot in enumerate(snapshots):
volume_ref = fake_volume_refs[i % len(fake_volume_refs)]
group_ref = fake_snapshot_group_refs[i %
len(fake_snapshot_group_refs)]
snapshot['pitGroupRef'] = group_ref
snapshot['baseVol'] = volume_ref
snapshot['pitSequenceNumber'] = str(i)
snapshot['id'] = i
bitset.set(i)
snapshots.append(snap)
filtered_snaps = list(filter(lambda x: x['pitGroupRef'] == snap[
'pitGroupRef'], snapshots))
self.mock_object(self.library, '_get_volume', mock.Mock(
return_value=vol))
self.mock_object(self.library, '_get_snapshot', mock.Mock(
return_value=snap))
self.mock_object(self.library, '_get_soft_delete_map', mock.Mock(
return_value={snap['pitGroupRef']: repr(bitset)}))
self.mock_object(self.library._client, 'list_snapshot_images',
mock.Mock(return_value=snapshots))
delete_image = self.mock_object(
self.library, '_cleanup_snapshot_images',
mock.Mock(return_value=({snap['pitGroupRef']: repr(bitset)},
None)))
self.library._delete_es_snapshot(snap)
delete_image.assert_called_once_with(filtered_snaps, bitset)
def test_delete_snapshot_oldest(self):
vol = copy.deepcopy(eseries_fake.VOLUME)
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
snapshots = [snap]
self.mock_object(self.library, '_get_volume', mock.Mock(
return_value=vol))
self.mock_object(self.library, '_get_snapshot', mock.Mock(
return_value=snap))
self.mock_object(self.library, '_get_soft_delete_map', mock.Mock(
return_value={}))
self.mock_object(self.library._client, 'list_snapshot_images',
mock.Mock(return_value=snapshots))
delete_image = self.mock_object(
self.library, '_cleanup_snapshot_images',
mock.Mock(return_value=(None, [snap['pitGroupRef']])))
self.library._delete_es_snapshot(snap)
delete_image.assert_called_once_with(snapshots,
na_utils.BitSet(1))
def test_get_soft_delete_map(self):
fake_val = 'fake'
self.mock_object(self.library._client, 'list_backend_store', mock.Mock(
return_value=fake_val))
actual = self.library._get_soft_delete_map()
self.assertEqual(fake_val, actual)
def test_cleanup_snapshot_images_delete_all(self):
image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
images = [image] * 32
bitset = na_utils.BitSet()
for i, image in enumerate(images):
image['pitSequenceNumber'] = i
bitset.set(i)
delete_grp = self.mock_object(self.library._client,
'delete_snapshot_group')
updt, keys = self.library._cleanup_snapshot_images(
images, bitset)
delete_grp.assert_called_once_with(image['pitGroupRef'])
self.assertIsNone(updt)
self.assertEqual([image['pitGroupRef']], keys)
def test_cleanup_snapshot_images_delete_all_fail(self):
image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
bitset = na_utils.BitSet(2 ** 32 - 1)
delete_grp = self.mock_object(
self.library._client, 'delete_snapshot_group',
mock.Mock(side_effect=exception.NetAppDriverException))
updt, keys = self.library._cleanup_snapshot_images(
[image], bitset)
delete_grp.assert_called_once_with(image['pitGroupRef'])
self.assertIsNone(updt)
self.assertEqual([image['pitGroupRef']], keys)
def test_cleanup_snapshot_images(self):
image = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
images = [image] * 32
del_count = 16
bitset = na_utils.BitSet()
for i, image in enumerate(images):
image['pitSequenceNumber'] = i
if i < del_count:
bitset.set(i)
exp_bitset = copy.deepcopy(bitset)
exp_bitset >>= 16
delete_img = self.mock_object(
self.library, '_delete_snapshot_image')
updt, keys = self.library._cleanup_snapshot_images(
images, bitset)
self.assertEqual(del_count, delete_img.call_count)
self.assertIsNone(keys)
self.assertEqual({image['pitGroupRef']: exp_bitset}, updt)
def test_delete_snapshot_image(self):
snap_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
self.mock_object(self.library._client, 'list_snapshot_group',
mock.Mock(return_value=snap_group))
self.library._delete_snapshot_image(snap)
def test_delete_snapshot_image_fail_cleanup(self):
snap_group = copy.deepcopy(eseries_fake.SNAPSHOT_GROUP)
snap_group['snapshotCount'] = 0
snap = copy.deepcopy(eseries_fake.SNAPSHOT_IMAGE)
self.mock_object(self.library._client, 'list_snapshot_group',
mock.Mock(return_value=snap_group))
self.library._delete_snapshot_image(snap)
def test_delete_snapshot_not_found(self):
fake_snapshot = copy.deepcopy(eseries_fake.FAKE_CINDER_SNAPSHOT)
get_snap = self.mock_object(self.library, '_get_snapshot',
mock.Mock(side_effect=exception.NotFound))
with mock.patch.object(library, 'LOG', mock.Mock()):
self.library.delete_snapshot(fake_snapshot)
get_snap.assert_called_once_with(fake_snapshot)
self.assertTrue(library.LOG.warning.called)
@ddt.data(['key1', 'key2'], [], None)
def test_merge_soft_delete_changes_keys(self, keys_to_del):
count = len(keys_to_del) if keys_to_del is not None else 0
save_store = self.mock_object(
self.library._client, 'save_backend_store')
index = {'key1': 'val'}
get_store = self.mock_object(self.library, '_get_soft_delete_map',
mock.Mock(return_value=index))
self.library._merge_soft_delete_changes(None, keys_to_del)
if count:
expected = copy.deepcopy(index)
for key in keys_to_del:
expected.pop(key, None)
get_store.assert_called_once_with()
save_store.assert_called_once_with(
self.library.SNAPSHOT_PERSISTENT_STORE_KEY, expected)
else:
get_store.assert_not_called()
save_store.assert_not_called()
@ddt.data(False, True)
def test_get_pool_operation_progress(self, expect_complete):
"""Validate the operation progress is interpreted correctly"""

View File

@ -1,5 +1,6 @@
# Copyright (c) 2014 Clinton Knight. All rights reserved.
# Copyright (c) 2015 Tom Barron. All rights reserved.
# Copyright (c) 2016 Michael Price. All rights reserved.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -810,3 +811,92 @@ class FeaturesTestCase(test.TestCase):
def test_get_attr_missing(self):
self.assertRaises(AttributeError, getattr, self.features, 'FEATURE_4')
@ddt.ddt
class BitSetTestCase(test.TestCase):
def setUp(self):
super(BitSetTestCase, self).setUp()
def test_default(self):
self.assertEqual(na_utils.BitSet(0), na_utils.BitSet())
def test_set(self):
bitset = na_utils.BitSet(0)
bitset.set(16)
self.assertEqual(na_utils.BitSet(1 << 16), bitset)
def test_unset(self):
bitset = na_utils.BitSet(1 << 16)
bitset.unset(16)
self.assertEqual(na_utils.BitSet(0), bitset)
def test_is_set(self):
bitset = na_utils.BitSet(1 << 16)
self.assertTrue(bitset.is_set(16))
def test_not_equal(self):
set1 = na_utils.BitSet(1 << 15)
set2 = na_utils.BitSet(1 << 16)
self.assertNotEqual(set1, set2)
def test_repr(self):
raw_val = 1 << 16
actual = repr(na_utils.BitSet(raw_val))
expected = str(raw_val)
self.assertEqual(actual, expected)
def test_str(self):
raw_val = 1 << 16
actual = str(na_utils.BitSet(raw_val))
expected = bin(raw_val)
self.assertEqual(actual, expected)
def test_int(self):
val = 1 << 16
actual = int(int(na_utils.BitSet(val)))
self.assertEqual(val, actual)
def test_and(self):
actual = na_utils.BitSet(1 << 16 | 1 << 15)
actual &= 1 << 16
self.assertEqual(na_utils.BitSet(1 << 16), actual)
def test_or(self):
actual = na_utils.BitSet()
actual |= 1 << 16
self.assertEqual(na_utils.BitSet(1 << 16), actual)
def test_invert(self):
actual = na_utils.BitSet(1 << 16)
actual = ~actual
self.assertEqual(~(1 << 16), actual)
def test_xor(self):
actual = na_utils.BitSet(1 << 16)
actual ^= 1 << 16
self.assertEqual(na_utils.BitSet(), actual)
def test_lshift(self):
actual = na_utils.BitSet(1)
actual <<= 16
self.assertEqual(na_utils.BitSet(1 << 16), actual)
def test_rshift(self):
actual = na_utils.BitSet(1 << 16)
actual >>= 16
self.assertEqual(na_utils.BitSet(1), actual)

View File

@ -102,6 +102,8 @@ class RestClient(WebserviceClient):
ASUP_VALID_VERSION = (1, 52, 9000, 3)
# We need to check for both the release and the pre-release versions
SSC_VALID_VERSIONS = ((1, 53, 9000, 1), (1, 53, 9010, 17))
REST_1_3_VERSION = (1, 53, 9000, 1)
REST_1_4_VERSIONS = ((1, 54, 9000, 1), (1, 54, 9090, 0))
RESOURCE_PATHS = {
'volumes': '/storage-systems/{system-id}/volumes',
@ -114,7 +116,20 @@ class RestClient(WebserviceClient):
'thin_volume_expand':
'/storage-systems/{system-id}/thin-volumes/{object-id}/expand',
'ssc_volumes': '/storage-systems/{system-id}/ssc/volumes',
'ssc_volume': '/storage-systems/{system-id}/ssc/volumes/{object-id}'
'ssc_volume': '/storage-systems/{system-id}/ssc/volumes/{object-id}',
'snapshot_groups': '/storage-systems/{system-id}/snapshot-groups',
'snapshot_group':
'/storage-systems/{system-id}/snapshot-groups/{object-id}',
'snapshot_volumes': '/storage-systems/{system-id}/snapshot-volumes',
'snapshot_volume':
'/storage-systems/{system-id}/snapshot-volumes/{object-id}',
'snapshot_images': '/storage-systems/{system-id}/snapshot-images',
'snapshot_image':
'/storage-systems/{system-id}/snapshot-images/{object-id}',
'persistent-stores': '/storage-systems/{'
'system-id}/persistent-records/',
'persistent-store': '/storage-systems/{'
'system-id}/persistent-records/{key}'
}
def __init__(self, scheme, host, port, service_path, username,
@ -140,6 +155,13 @@ class RestClient(WebserviceClient):
asup_api_valid_version = self._validate_version(
self.ASUP_VALID_VERSION, api_version_tuple)
rest_1_3_api_valid_version = self._validate_version(
self.REST_1_3_VERSION, api_version_tuple)
rest_1_4_api_valid_version = any(
self._validate_version(valid_version, api_version_tuple)
for valid_version in self.REST_1_4_VERSIONS)
ssc_api_valid_version = any(self._validate_version(valid_version,
api_version_tuple)
for valid_version
@ -153,6 +175,12 @@ class RestClient(WebserviceClient):
supported=ssc_api_valid_version,
min_version=self._version_tuple_to_str(
self.SSC_VALID_VERSIONS[0]))
self.features.add_feature(
'REST_1_3_RELEASE', supported=rest_1_3_api_valid_version,
min_version=self._version_tuple_to_str(self.REST_1_3_VERSION))
self.features.add_feature(
'REST_1_4_RELEASE', supported=rest_1_4_api_valid_version,
min_version=self._version_tuple_to_str(self.REST_1_4_VERSIONS[0]))
def _version_tuple_to_str(self, version):
return ".".join([str(part) for part in version])
@ -405,7 +433,7 @@ class RestClient(WebserviceClient):
try:
return self._invoke('GET', path, **{'object-id': object_id})
except es_exception.WebServiceException as e:
if(404 == e.status_code):
if 404 == e.status_code:
raise exception.VolumeNotFound(volume_id=object_id)
else:
raise
@ -595,14 +623,19 @@ class RestClient(WebserviceClient):
def list_snapshot_groups(self):
"""Lists snapshot groups."""
path = "/storage-systems/{system-id}/snapshot-groups"
path = self.RESOURCE_PATHS['snapshot_groups']
return self._invoke('GET', path)
def create_snapshot_group(self, label, object_id, storage_pool_id,
def list_snapshot_group(self, object_id):
"""Retrieve given snapshot group from the array."""
path = self.RESOURCE_PATHS['snapshot_group']
return self._invoke('GET', path, **{'object-id': object_id})
def create_snapshot_group(self, label, object_id, storage_pool_id=None,
repo_percent=99, warn_thres=99, auto_del_limit=0,
full_policy='failbasewrites'):
"""Creates snapshot group on array."""
path = "/storage-systems/{system-id}/snapshot-groups"
path = self.RESOURCE_PATHS['snapshot_groups']
data = {'baseMappableObjectId': object_id, 'name': label,
'storagePoolId': storage_pool_id,
'repositoryPercentage': repo_percent,
@ -610,33 +643,44 @@ class RestClient(WebserviceClient):
'autoDeleteLimit': auto_del_limit, 'fullPolicy': full_policy}
return self._invoke('POST', path, data)
def update_snapshot_group(self, group_id, label):
"""Modify a snapshot group on the array."""
path = self.RESOURCE_PATHS['snapshot_group']
data = {'name': label}
return self._invoke('POST', path, data, **{'object-id': group_id})
def delete_snapshot_group(self, object_id):
"""Deletes given snapshot group from array."""
path = "/storage-systems/{system-id}/snapshot-groups/{object-id}"
path = self.RESOURCE_PATHS['snapshot_group']
return self._invoke('DELETE', path, **{'object-id': object_id})
def create_snapshot_image(self, group_id):
"""Creates snapshot image in snapshot group."""
path = "/storage-systems/{system-id}/snapshot-images"
path = self.RESOURCE_PATHS['snapshot_images']
data = {'groupId': group_id}
return self._invoke('POST', path, data)
def delete_snapshot_image(self, object_id):
"""Deletes given snapshot image in snapshot group."""
path = "/storage-systems/{system-id}/snapshot-images/{object-id}"
path = self.RESOURCE_PATHS['snapshot_image']
return self._invoke('DELETE', path, **{'object-id': object_id})
def list_snapshot_image(self, object_id):
"""Retrieve given snapshot image from the array."""
path = self.RESOURCE_PATHS['snapshot_image']
return self._invoke('GET', path, **{'object-id': object_id})
def list_snapshot_images(self):
"""Lists snapshot images."""
path = "/storage-systems/{system-id}/snapshot-images"
path = self.RESOURCE_PATHS['snapshot_images']
return self._invoke('GET', path)
def create_snapshot_volume(self, image_id, label, base_object_id,
storage_pool_id,
storage_pool_id=None,
repo_percent=99, full_thres=99,
view_mode='readOnly'):
"""Creates snapshot volume."""
path = "/storage-systems/{system-id}/snapshot-volumes"
path = self.RESOURCE_PATHS['snapshot_volumes']
data = {'snapshotImageId': image_id, 'fullThreshold': full_thres,
'storagePoolId': storage_pool_id,
'name': label, 'viewMode': view_mode,
@ -645,11 +689,22 @@ class RestClient(WebserviceClient):
'repositoryPoolId': storage_pool_id}
return self._invoke('POST', path, data)
def update_snapshot_volume(self, snap_vol_id, label=None, full_thres=None):
"""Modify an existing snapshot volume."""
path = self.RESOURCE_PATHS['snapshot_volume']
data = {'name': label, 'fullThreshold': full_thres}
return self._invoke('POST', path, data, **{'object-id': snap_vol_id})
def delete_snapshot_volume(self, object_id):
"""Deletes given snapshot volume."""
path = "/storage-systems/{system-id}/snapshot-volumes/{object-id}"
path = self.RESOURCE_PATHS['snapshot_volume']
return self._invoke('DELETE', path, **{'object-id': object_id})
def list_snapshot_volumes(self):
"""Lists snapshot volumes/views defined on the array."""
path = self.RESOURCE_PATHS['snapshot_volumes']
return self._invoke('GET', path)
def list_ssc_storage_pools(self):
"""Lists pools and their service quality defined on the array."""
path = "/storage-systems/{system-id}/ssc/pools"
@ -786,3 +841,43 @@ class RestClient(WebserviceClient):
if mode_is_proxy:
api_operating_mode = 'proxy'
return api_operating_mode, about_response_dict['version']
def list_backend_store(self, key):
"""Retrieve data by key from the the persistent store on the backend.
Example response: {"key": "cinder-snapshots", "value": "[]"}
:param key: the persistent store to retrieve
:return a json body representing the value of the store,
or an empty json object
"""
path = self.RESOURCE_PATHS.get('persistent-store')
try:
resp = self._invoke('GET', path, **{'key': key})
except exception.NetAppDriverException:
return dict()
else:
data = resp['value']
if data:
return json.loads(data)
return dict()
def save_backend_store(self, key, store_data):
"""Save a json value to the persistent storage on the backend.
The storage backend provides a small amount of persistent storage
that we can utilize for storing driver information.
:param key: The key utilized for storing/retrieving the data
:param store_data: a python data structure that will be stored as a
json value
"""
path = self.RESOURCE_PATHS.get('persistent-stores')
store_data = json.dumps(store_data, separators=(',', ':'))
data = {
'key': key,
'value': store_data
}
self._invoke('POST', path, data)

View File

@ -60,7 +60,7 @@ class NetAppEseriesFibreChannelDriver(driver.BaseVD,
self.library.delete_volume(volume)
def create_snapshot(self, snapshot):
self.library.create_snapshot(snapshot)
return self.library.create_snapshot(snapshot)
def delete_snapshot(self, snapshot):
self.library.delete_snapshot(snapshot)

View File

@ -62,7 +62,7 @@ class NetAppEseriesISCSIDriver(driver.BaseVD,
self.library.delete_volume(volume)
def create_snapshot(self, snapshot):
self.library.create_snapshot(snapshot)
return self.library.create_snapshot(snapshot)
def delete_snapshot(self, snapshot):
self.library.delete_snapshot(snapshot)

View File

@ -114,6 +114,20 @@ class NetAppESeriesLibrary(object):
DEFAULT_HOST_TYPE = 'linux_dm_mp'
# Define name marker string to use in snapshot groups that are for copying
# volumes. This is to differentiate them from ordinary snapshot groups.
SNAPSHOT_VOL_COPY_SUFFIX = 'SGCV'
# Define a name marker string used to identify snapshot volumes that have
# an underlying snapshot that is awaiting deletion.
SNAPSHOT_VOL_DEL_SUFFIX = '_DEL'
# Maximum number of snapshots per snapshot group
MAX_SNAPSHOT_COUNT = 32
# Maximum number of snapshot groups
MAX_SNAPSHOT_GROUP_COUNT = 4
RESERVED_SNAPSHOT_GROUP_COUNT = 1
SNAPSHOT_PERSISTENT_STORE_KEY = 'cinder-snapshots'
SNAPSHOT_PERSISTENT_STORE_LOCK = str(uuid.uuid4())
def __init__(self, driver_name, driver_protocol="iSCSI",
configuration=None, **kwargs):
self.configuration = configuration
@ -139,6 +153,7 @@ class NetAppESeriesLibrary(object):
self._client = self._create_rest_client(self.configuration)
self._check_mode_get_or_register_storage_system()
self._version_check()
if self.configuration.netapp_enable_multiattach:
self._ensure_multi_attach_host_group_exists()
@ -159,6 +174,17 @@ class NetAppESeriesLibrary(object):
username=configuration.netapp_login,
password=configuration.netapp_password)
def _version_check(self):
"""Ensure that the minimum version of the REST API is available"""
if not self._client.features.REST_1_4_RELEASE:
min_version = (
self._client.features.REST_1_4_RELEASE.minimum_version)
raise exception.NetAppDriverException(
'This version (%(cur)s of the NetApp SANtricity Webservices '
'Proxy is not supported. Install version %(supp)s or '
'later.' % {'cur': self._client.api_version,
'supp': min_version})
def _start_periodic_tasks(self):
ssc_periodic_task = loopingcall.FixedIntervalLoopingCall(
self._update_ssc_info)
@ -356,24 +382,99 @@ class NetAppESeriesLibrary(object):
return self._client.list_volume(uid)
def _get_snapshot_group_for_snapshot(self, snapshot_id):
label = utils.convert_uuid_to_es_fmt(snapshot_id)
def _get_snapshot_group_for_snapshot(self, snapshot):
snapshot = self._get_snapshot(snapshot)
try:
return self._client.list_snapshot_group(snapshot['pitGroupRef'])
except (exception.NetAppDriverException,
eseries_exc.WebServiceException):
msg = _("Specified snapshot group with id %s could not be found.")
raise exception.NotFound(msg % snapshot['pitGroupRef'])
def _get_snapshot_legacy(self, snapshot):
"""Find a E-Series snapshot by the name of the snapshot group.
Snapshots were previously identified by the unique name of the
snapshot group. A snapshot volume is now utilized to uniquely
identify the snapshot, so any snapshots previously defined in this
way must be updated.
:param snapshot_id: Cinder snapshot identifer
:return: An E-Series snapshot image
"""
label = utils.convert_uuid_to_es_fmt(snapshot['id'])
for group in self._client.list_snapshot_groups():
if group['label'] == label:
return group
msg = _("Specified snapshot group with label %s could not be found.")
raise exception.NotFound(msg % label)
image = self._get_oldest_image_in_snapshot_group(group['id'])
group_label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
# Modify the group label so we don't have a name collision
self._client.update_snapshot_group(group['id'],
group_label)
def _get_latest_image_in_snapshot_group(self, snapshot_id):
group = self._get_snapshot_group_for_snapshot(snapshot_id)
snapshot.update({'provider_id': image['id']})
snapshot.save()
return image
raise exception.NotFound(_('Snapshot with id of %s could not be '
'found.') % snapshot['id'])
def _get_snapshot(self, snapshot):
"""Find a E-Series snapshot by its Cinder identifier
An E-Series snapshot image does not have a configuration name/label,
so we define a snapshot volume underneath of it that will help us to
identify it. We retrieve the snapshot volume with the matching name,
and then we find its underlying snapshot.
:param snapshot_id: Cinder snapshot identifer
:return: An E-Series snapshot image
"""
try:
return self._client.list_snapshot_image(
snapshot.get('provider_id'))
except (eseries_exc.WebServiceException or
exception.NetAppDriverException):
try:
LOG.debug('Unable to locate snapshot by its id, falling '
'back to legacy behavior.')
return self._get_snapshot_legacy(snapshot)
except exception.NetAppDriverException:
raise exception.NotFound(_('Snapshot with id of %s could not'
' be found.') % snapshot['id'])
def _get_snapshot_group(self, snapshot_group_id):
try:
return self._client.list_snapshot_group(snapshot_group_id)
except exception.NetAppDriverException:
raise exception.NotFound(_('Unable to retrieve snapshot group '
'with id of %s.') % snapshot_group_id)
def _get_ordered_images_in_snapshot_group(self, snapshot_group_id):
images = self._client.list_snapshot_images()
if images:
filtered_images = filter(lambda img: (img['pitGroupRef'] ==
group['pitGroupRef']),
snapshot_group_id),
images)
sorted_imgs = sorted(filtered_images, key=lambda x: x[
'pitTimestamp'])
return sorted_imgs[0]
return sorted_imgs
return list()
def _get_oldest_image_in_snapshot_group(self, snapshot_group_id):
group = self._get_snapshot_group(snapshot_group_id)
images = self._get_ordered_images_in_snapshot_group(snapshot_group_id)
if images:
return images[0]
msg = _("No snapshot image found in snapshot group %s.")
raise exception.NotFound(msg % group['label'])
def _get_latest_image_in_snapshot_group(self, snapshot_group_id):
group = self._get_snapshot_group(snapshot_group_id)
images = self._get_ordered_images_in_snapshot_group(snapshot_group_id)
if images:
return images[-1]
msg = _("No snapshot image found in snapshot group %s.")
raise exception.NotFound(msg % group['label'])
@ -524,14 +625,24 @@ class NetAppESeriesLibrary(object):
msg = _("Failure creating volume %s.")
raise exception.NetAppDriverException(msg % label)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
def _create_volume_from_snapshot(self, volume, image):
"""Define a new volume based on an E-Series snapshot image.
This method should be synchronized on the snapshot id.
:param volume: a Cinder volume
:param image: an E-Series snapshot image
:return: the clone volume
"""
label = utils.convert_uuid_to_es_fmt(volume['id'])
size = volume['size']
dst_vol = self._schedule_and_create_volume(label, size)
try:
src_vol = None
src_vol = self._create_snapshot_volume(snapshot['id'])
src_vol = self._client.create_snapshot_volume(
image['id'], utils.convert_uuid_to_es_fmt(
uuid.uuid4()), image['baseVol'])
self._copy_volume_high_prior_readonly(src_vol, dst_vol)
LOG.info(_LI("Created volume with label %s."), label)
except exception.NetAppDriverException:
@ -542,21 +653,18 @@ class NetAppESeriesLibrary(object):
try:
self._client.delete_snapshot_volume(src_vol['id'])
except exception.NetAppDriverException as e:
LOG.error(_LE("Failure deleting snap vol. Error: %s."), e)
LOG.error(_LE("Failure restarting snap vol. Error: %s."),
e)
else:
LOG.warning(_LW("Snapshot volume not found."))
def _create_snapshot_volume(self, snapshot_id):
"""Creates snapshot volume for given group with snapshot_id."""
group = self._get_snapshot_group_for_snapshot(snapshot_id)
LOG.debug("Creating snap vol for group %s", group['label'])
image = self._get_latest_image_in_snapshot_group(snapshot_id)
label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
capacity = int(image['pitCapacity']) / units.Gi
storage_pools = self._get_sorted_available_storage_pools(capacity)
s_id = storage_pools[0]['volumeGroupRef']
return self._client.create_snapshot_volume(image['pitRef'], label,
group['baseVolume'], s_id)
return dst_vol
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
es_snapshot = self._get_snapshot(snapshot)
cinder_utils.synchronized(snapshot['id'])(
self._create_volume_from_snapshot)(volume, es_snapshot)
def _copy_volume_high_prior_readonly(self, src_vol, dst_vol):
"""Copies src volume to dest volume."""
@ -596,12 +704,17 @@ class NetAppESeriesLibrary(object):
"""Creates a clone of the specified volume."""
snapshot = {'id': uuid.uuid4(), 'volume_id': src_vref['id'],
'volume': src_vref}
self.create_snapshot(snapshot)
group_name = (utils.convert_uuid_to_es_fmt(snapshot['id']) +
self.SNAPSHOT_VOL_COPY_SUFFIX)
es_vol = self._get_volume(src_vref['id'])
es_snapshot = self._create_es_snapshot(es_vol, group_name)
try:
self.create_volume_from_snapshot(volume, snapshot)
self._create_volume_from_snapshot(volume, es_snapshot)
finally:
try:
self.delete_snapshot(snapshot)
self._client.delete_snapshot_group(es_snapshot['pitGroupRef'])
except exception.NetAppDriverException:
LOG.warning(_LW("Failure deleting temp snapshot %s."),
snapshot['id'])
@ -615,33 +728,332 @@ class NetAppESeriesLibrary(object):
LOG.warning(_LW("Volume %s already deleted."), volume['id'])
return
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
def _create_snapshot_volume(self, snapshot_id, label=None):
"""Creates snapshot volume for given group with snapshot_id."""
image = self._get_snapshot(snapshot_id)
group = self._get_snapshot_group(image['pitGroupRef'])
LOG.debug("Creating snap vol for group %s", group['label'])
if label is None:
label = utils.convert_uuid_to_es_fmt(uuid.uuid4())
return self._client.create_snapshot_volume(image['pitRef'], label,
image['baseVol'])
def _create_snapshot_group(self, label, volume, percentage_capacity=20.0):
"""Define a new snapshot group for a volume
:param label: the label for the snapshot group
:param volume: an E-Series volume
:param percentage_capacity: an optional repository percentage
:return a new snapshot group
"""
# Newer versions of the REST API are capable of automatically finding
# the best pool candidate
if not self._client.features.REST_1_3_RELEASE:
vol_size_gb = int(volume['totalSizeInBytes']) / units.Gi
pools = self._get_sorted_available_storage_pools(vol_size_gb)
volume_pool = next(pool for pool in pools if volume[
'volumeGroupRef'] == pool['id'])
# A disk pool can only utilize a candidate from its own pool
if volume_pool.get('raidLevel') == 'raidDiskPool':
pool_id_to_use = volume_pool['volumeGroupRef']
# Otherwise, choose the best available pool
else:
pool_id_to_use = pools[0]['volumeGroupRef']
group = self._client.create_snapshot_group(
label, volume['volumeRef'], pool_id_to_use,
repo_percent=percentage_capacity)
else:
group = self._client.create_snapshot_group(
label, volume['volumeRef'], repo_percent=percentage_capacity)
return group
def _get_snapshot_groups_for_volume(self, vol):
"""Find all snapshot groups associated with an E-Series volume
:param vol: An E-Series volume object
:return A list of snapshot groups
:raise NetAppDriverException: if the list of snapshot groups cannot be
retrieved
"""
return [grp for grp in self._client.list_snapshot_groups()
if grp['baseVolume'] == vol['id']]
def _get_available_snapshot_group(self, vol):
"""Find a snapshot group that has remaining capacity for snapshots.
In order to minimize repository usage, we prioritize the snapshot
group with remaining snapshot capacity that has most recently had a
snapshot defined on it.
:param vol: An E-Series volume object
:return A valid snapshot group that has available snapshot capacity,
or None
:raise NetAppDriverException: if the list of snapshot groups cannot be
retrieved
"""
groups_for_v = self._get_snapshot_groups_for_volume(vol)
# Filter out reserved snapshot groups
groups = filter(lambda g: self.SNAPSHOT_VOL_COPY_SUFFIX not in g[
'label'], groups_for_v)
# Find all groups with free snapshot capacity
groups = [group for group in groups if group.get('snapshotCount') <
self.MAX_SNAPSHOT_COUNT]
# Order by the last defined snapshot on the group
if len(groups) > 1:
group_by_id = {g['id']: g for g in groups}
snap_imgs = list()
for group in groups:
try:
snap_imgs.append(
self._get_latest_image_in_snapshot_group(group['id']))
except exception.NotFound:
pass
snap_imgs = sorted(snap_imgs, key=lambda x: x['pitSequenceNumber'])
if snap_imgs:
# The newest image
img = snap_imgs[-1]
return group_by_id[img['pitGroupRef']]
else:
return groups[0] if groups else None
# Skip the snapshot image checks if there is only one snapshot group
elif groups:
return groups[0]
else:
return None
def _create_es_snapshot(self, vol, group_name=None):
snap_grp, snap_image = None, None
snapshot_name = utils.convert_uuid_to_es_fmt(snapshot['id'])
os_vol = snapshot['volume']
vol = self._get_volume(os_vol['name_id'])
vol_size_gb = int(vol['totalSizeInBytes']) / units.Gi
pools = self._get_sorted_available_storage_pools(vol_size_gb)
try:
snap_grp = self._client.create_snapshot_group(
snapshot_name, vol['volumeRef'], pools[0]['volumeGroupRef'])
snap_image = self._client.create_snapshot_image(
snap_grp['pitGroupRef'])
LOG.info(_LI("Created snap grp with label %s."), snapshot_name)
snap_grp = self._get_available_snapshot_group(vol)
# If a snapshot group is not available, create one if possible
if snap_grp is None:
snap_groups_for_vol = self._get_snapshot_groups_for_volume(
vol)
# We need a reserved snapshot group
if (group_name is not None and
(self.SNAPSHOT_VOL_COPY_SUFFIX in group_name)):
# First we search for an existing reserved group
for grp in snap_groups_for_vol:
if grp['label'].endswith(
self.SNAPSHOT_VOL_COPY_SUFFIX):
snap_grp = grp
break
# No reserved group exists, so we create it
if (snap_grp is None and
(len(snap_groups_for_vol) <
self.MAX_SNAPSHOT_GROUP_COUNT)):
snap_grp = self._create_snapshot_group(group_name,
vol)
# Ensure we don't exceed the snapshot group limit
elif (len(snap_groups_for_vol) <
(self.MAX_SNAPSHOT_GROUP_COUNT -
self.RESERVED_SNAPSHOT_GROUP_COUNT)):
label = group_name if group_name is not None else (
utils.convert_uuid_to_es_fmt(uuid.uuid4()))
snap_grp = self._create_snapshot_group(label, vol)
LOG.info(_LI("Created snap grp with label %s."), label)
# We couldn't retrieve or create a snapshot group
if snap_grp is None:
raise exception.SnapshotLimitExceeded(
allowed=(self.MAX_SNAPSHOT_COUNT *
(self.MAX_SNAPSHOT_GROUP_COUNT -
self.RESERVED_SNAPSHOT_GROUP_COUNT)))
return self._client.create_snapshot_image(
snap_grp['id'])
except exception.NetAppDriverException:
with excutils.save_and_reraise_exception():
if snap_image is None and snap_grp:
self.delete_snapshot(snapshot)
self._delete_snapshot_group(snap_grp['id'])
def create_snapshot(self, snapshot):
"""Creates a snapshot.
:param snapshot: The Cinder snapshot
:param group_name: An optional label for the snapshot group
:return An E-Series snapshot image
"""
os_vol = snapshot['volume']
vol = self._get_volume(os_vol['name_id'])
snap_image = cinder_utils.synchronized(vol['id'])(
self._create_es_snapshot)(vol)
model_update = {
'provider_id': snap_image['id']
}
return model_update
def _delete_es_snapshot(self, es_snapshot):
"""Perform a soft-delete on an E-Series snapshot.
Mark the snapshot image as no longer needed, so that it can be
purged from the backend when no other snapshots are dependent upon it.
:param es_snapshot: an E-Series snapshot image
:return None
"""
index = self._get_soft_delete_map()
snapgroup_ref = es_snapshot['pitGroupRef']
if snapgroup_ref in index:
bitset = na_utils.BitSet(int((index[snapgroup_ref])))
else:
bitset = na_utils.BitSet(0)
images = [img for img in self._client.list_snapshot_images() if
img['pitGroupRef'] == snapgroup_ref]
for i, image in enumerate(sorted(images, key=lambda x: x[
'pitSequenceNumber'])):
if(image['pitSequenceNumber'] == es_snapshot[
'pitSequenceNumber']):
bitset.set(i)
break
index_update, keys_to_del = (
self._cleanup_snapshot_images(images, bitset))
self._merge_soft_delete_changes(index_update, keys_to_del)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
"""Delete a snapshot."""
try:
snap_grp = self._get_snapshot_group_for_snapshot(snapshot['id'])
es_snapshot = self._get_snapshot(snapshot)
except exception.NotFound:
LOG.warning(_LW("Snapshot %s already deleted."), snapshot['id'])
return
self._client.delete_snapshot_group(snap_grp['pitGroupRef'])
else:
os_vol = snapshot['volume']
vol = self._get_volume(os_vol['name_id'])
cinder_utils.synchronized(vol['id'])(self._delete_es_snapshot)(
es_snapshot)
def _get_soft_delete_map(self):
"""Retrieve the snapshot index from the storage backend"""
return self._client.list_backend_store(
self.SNAPSHOT_PERSISTENT_STORE_KEY)
@cinder_utils.synchronized(SNAPSHOT_PERSISTENT_STORE_LOCK)
def _merge_soft_delete_changes(self, index_update, keys_to_del):
"""Merge changes to the snapshot index and save it on the backend
This method merges provided changes into the index, locking, to ensure
that concurrent changes that don't overlap are not overwritten. No
update will occur if neither an update or keys to delete are provided.
:param index_update: a dict of keys/value pairs to update in the index
:param keys_to_del: a list of keys to purge from the index
"""
if index_update or keys_to_del:
index = self._get_soft_delete_map()
if index_update:
index.update(index_update)
if keys_to_del:
for key in keys_to_del:
if key in index:
del index[key]
self._client.save_backend_store(
self.SNAPSHOT_PERSISTENT_STORE_KEY, index)
def _cleanup_snapshot_images(self, images, bitset):
"""Delete snapshot images that are marked for removal from the backend.
This method will iterate over all snapshots (beginning with the
oldest), that are defined on the same snapshot group as the provided
snapshot image. If the snapshot is marked for deletion, it will be
purged from the backend. Otherwise, the method will return because
no further snapshots can be purged.
The bitset will be updated based on the return from this method.
Any updates to the index will be provided as a dict, and any keys
to be removed from the index should be returned as (dict, list).
:param images: a list of E-Series snapshot images
:param bitset: a bitset representing the snapshot images that are
no longer needed on the backend (and may be deleted when possible)
:return (dict, list) a tuple containing a dict of updates for the
index and a list of keys to remove from the index
"""
snap_grp_ref = images[0]['pitGroupRef']
# All images are marked as deleted, we can delete the snapshot group
if bitset == 2 ** len(images) - 1:
try:
self._delete_snapshot_group(snap_grp_ref)
except exception.NetAppDriverException as e:
LOG.warning(_LW("Unable to remove snapshot group - "
"%s."), e.msg)
return None, [snap_grp_ref]
else:
# Order by their sequence number, from oldest to newest
snapshots = sorted(images,
key=lambda x: x['pitSequenceNumber'])
deleted = 0
for i, snapshot in enumerate(snapshots):
if bitset.is_set(i):
self._delete_snapshot_image(snapshot)
deleted += 1
else:
# Snapshots must be deleted in order, so if the current
# snapshot is not pending deletion, we don't want to
# process any more
break
if deleted:
# Update the bitset based on the deleted snapshots
bitset >>= deleted
LOG.debug('Deleted %(count)s snapshot images from snapshot '
'group: %(grp)s.', {'count': deleted,
'grp': snap_grp_ref})
if deleted >= len(images):
try:
self._delete_snapshot_group(snap_grp_ref)
except exception.NetAppDriverException as e:
LOG.warning(_LW("Unable to remove snapshot group - "
"%s."), e.msg)
return None, [snap_grp_ref]
return {snap_grp_ref: repr(bitset)}, None
def _delete_snapshot_group(self, group_id):
try:
self._client.delete_snapshot_group(group_id)
except eseries_exc.WebServiceException as e:
raise exception.NetAppDriverException(e.msg)
def _delete_snapshot_image(self, es_snapshot):
"""Remove a snapshot image from the storage backend
If a snapshot group has no remaining snapshot images associated with
it, it will be deleted as well. When the snapshot is deleted,
any snapshot volumes that are associated with it will be orphaned,
so they are also deleted.
:param es_snapshot: An E-Series snapshot image
:param snapshot_volumes: Snapshot volumes associated with the snapshot
"""
self._client.delete_snapshot_image(es_snapshot['id'])
def ensure_export(self, context, volume):
"""Synchronously recreates an export for a volume."""

View File

@ -47,7 +47,7 @@ def decode_base32_to_hex(base32_string):
def convert_uuid_to_es_fmt(uuid_str):
"""Converts uuid to e-series compatible name format."""
uuid_base32 = encode_hex_to_base32(uuid.UUID(six.text_type(uuid_str)).hex)
return uuid_base32.strip(b'=')
return str(uuid_base32.strip(b'='))
def convert_es_fmt_to_uuid(es_label):

View File

@ -2,6 +2,7 @@
# Copyright (c) 2014 Navneet Singh. All rights reserved.
# Copyright (c) 2014 Clinton Knight. All rights reserved.
# Copyright (c) 2015 Tom Barron. All rights reserved.
# Copyright (c) 2016 Michael Price. 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
@ -505,3 +506,59 @@ class FeatureState(object):
:returns: True if the feature is supported, otherwise False
"""
return self.supported
class BitSet(object):
def __init__(self, value=0):
self._value = value
def set(self, bit):
self._value |= 1 << bit
return self
def unset(self, bit):
self._value &= ~(1 << bit)
return self
def is_set(self, bit):
return self._value & 1 << bit
def __and__(self, other):
self._value &= other
return self
def __or__(self, other):
self._value |= other
return self
def __invert__(self):
self._value = ~self._value
return self
def __xor__(self, other):
self._value ^= other
return self
def __lshift__(self, other):
self._value <<= other
return self
def __rshift__(self, other):
self._value >>= other
return self
def __int__(self):
return self._value
def __str__(self):
return bin(self._value)
def __repr__(self):
return str(self._value)
def __eq__(self, other):
return (isinstance(other, self.__class__) and self._value ==
other._value) or self._value == int(other)
def __ne__(self, other):
return not self.__eq__(other)