Merge "NetApp - Extended Consistency group support for NVMe/TCP driver"
This commit is contained in:
@@ -594,8 +594,8 @@ class NetAppNVMeStorageLibraryTestCase(test.TestCase):
|
||||
expected = [{
|
||||
'pool_name': 'vola',
|
||||
'QoS_support': False,
|
||||
'consistencygroup_support': False,
|
||||
'consistent_group_snapshot_enabled': False,
|
||||
'consistencygroup_support': True,
|
||||
'consistent_group_snapshot_enabled': True,
|
||||
'reserved_percentage': 5,
|
||||
'max_over_subscription_ratio': 10,
|
||||
'multiattach': False,
|
||||
@@ -964,3 +964,165 @@ class NetAppNVMeStorageLibraryTestCase(test.TestCase):
|
||||
connector_list = [None, {'nqn': fake.HOST_NQN}]
|
||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||
executor.map(execute_terminate_connection, connector_list)
|
||||
|
||||
def test_create_group(self):
|
||||
model_update = self.library.create_group(
|
||||
fake.VOLUME_GROUP)
|
||||
self.assertEqual('available', model_update['status'])
|
||||
|
||||
def test_delete_group_volume_delete_failure(self):
|
||||
self.mock_object(nvme_library, 'LOG')
|
||||
self.mock_object(self.library, '_delete_namespace',
|
||||
side_effect=Exception)
|
||||
|
||||
model_update, volumes = self.library.delete_group(
|
||||
fake.VOLUME_GROUP, [fake.VG_VOLUME])
|
||||
|
||||
self.assertEqual('deleted', model_update['status'])
|
||||
self.assertEqual('error_deleting', volumes[0]['status'])
|
||||
self.assertEqual(1, nvme_library.LOG.exception.call_count)
|
||||
|
||||
def test_update_group(self):
|
||||
model_update, add_volumes_update, remove_volumes_update = (
|
||||
self.library.update_group(fake.VOLUME_GROUP))
|
||||
|
||||
self.assertIsNone(model_update)
|
||||
self.assertIsNone(add_volumes_update)
|
||||
self.assertIsNone(remove_volumes_update)
|
||||
|
||||
def test_delete_group_not_found(self):
|
||||
self.mock_object(nvme_library, 'LOG')
|
||||
self.mock_object(self.library, '_get_namespace_attr',
|
||||
return_value=None)
|
||||
|
||||
model_update, volumes = self.library.delete_group(
|
||||
fake.VOLUME_GROUP, [fake.VG_VOLUME])
|
||||
|
||||
self.assertEqual(0, nvme_library.LOG.error.call_count)
|
||||
self.assertEqual(0, nvme_library.LOG.info.call_count)
|
||||
|
||||
self.assertEqual('deleted', model_update['status'])
|
||||
self.assertEqual('deleted', volumes[0]['status'])
|
||||
|
||||
def test_create_group_snapshot_raise_exception(self):
|
||||
self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||
return_value=True)
|
||||
|
||||
mock_extract_host = self.mock_object(
|
||||
volume_utils, 'extract_host', return_value=fake.POOL_NAME)
|
||||
|
||||
self.mock_object(self.client, 'create_cg_snapshot',
|
||||
side_effect=netapp_api.NaApiError)
|
||||
|
||||
self.assertRaises(na_utils.NetAppDriverException,
|
||||
self.library.create_group_snapshot,
|
||||
fake.VOLUME_GROUP,
|
||||
[fake.VG_SNAPSHOT])
|
||||
|
||||
mock_extract_host.assert_called_once_with(
|
||||
fake.VG_SNAPSHOT['volume']['host'], level='pool')
|
||||
|
||||
def test_create_group_snapshot(self):
|
||||
self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||
return_value=False)
|
||||
self.mock_object(self.library,
|
||||
'_get_namespace_from_table',
|
||||
return_value=self.fake_namespace)
|
||||
mock_clone_namespace = self.mock_object(self.library,
|
||||
'_clone_namespace')
|
||||
|
||||
model_update, snapshots_model_update = (
|
||||
self.library.create_group_snapshot(fake.VOLUME_GROUP,
|
||||
[fake.SNAPSHOT]))
|
||||
|
||||
self.assertIsNone(model_update)
|
||||
self.assertIsNone(snapshots_model_update)
|
||||
mock_clone_namespace.assert_called_once_with(self.fake_namespace.name,
|
||||
fake.SNAPSHOT['name'])
|
||||
|
||||
def test_create_consistent_group_snapshot(self):
|
||||
self.mock_object(volume_utils, 'is_group_a_cg_snapshot_type',
|
||||
return_value=True)
|
||||
|
||||
self.mock_object(volume_utils, 'extract_host',
|
||||
return_value=fake.POOL_NAME)
|
||||
mock_create_cg_snapshot = self.mock_object(
|
||||
self.client, 'create_cg_snapshot')
|
||||
mock_clone_namespace = self.mock_object(self.library,
|
||||
'_clone_namespace')
|
||||
mock_wait_for_busy_snapshot = self.mock_object(
|
||||
self.client, 'wait_for_busy_snapshot')
|
||||
mock_delete_snapshot = self.mock_object(
|
||||
self.client, 'delete_snapshot')
|
||||
|
||||
model_update, snapshots_model_update = (
|
||||
self.library.create_group_snapshot(fake.VOLUME_GROUP,
|
||||
[fake.VG_SNAPSHOT]))
|
||||
|
||||
self.assertIsNone(model_update)
|
||||
self.assertIsNone(snapshots_model_update)
|
||||
|
||||
mock_create_cg_snapshot.assert_called_once_with(
|
||||
set([fake.POOL_NAME]), fake.VOLUME_GROUP['id'])
|
||||
mock_clone_namespace.assert_called_once_with(
|
||||
fake.VG_SNAPSHOT['volume']['name'],
|
||||
fake.VG_SNAPSHOT['name'],
|
||||
)
|
||||
mock_wait_for_busy_snapshot.assert_called_once_with(
|
||||
fake.POOL_NAME, fake.VOLUME_GROUP['id'])
|
||||
mock_delete_snapshot.assert_called_once_with(
|
||||
fake.POOL_NAME, fake.VOLUME_GROUP['id'])
|
||||
|
||||
def test_create_group_from_src_snapshot(self):
|
||||
mock_clone_source_to_destination = self.mock_object(
|
||||
self.library, '_clone_source_to_destination')
|
||||
|
||||
actual_return_value = self.library.create_group_from_src(
|
||||
fake.VOLUME_GROUP, [fake.VOLUME], group_snapshot=fake.VG_SNAPSHOT,
|
||||
snapshots=[fake.VG_VOLUME_SNAPSHOT])
|
||||
|
||||
clone_source_to_destination_args = {
|
||||
'name': fake.VG_SNAPSHOT['name'],
|
||||
'size': fake.VG_SNAPSHOT['volume_size'],
|
||||
}
|
||||
mock_clone_source_to_destination.assert_called_once_with(
|
||||
clone_source_to_destination_args, fake.VOLUME)
|
||||
expected_return_value = (None, [])
|
||||
self.assertEqual(expected_return_value, actual_return_value)
|
||||
|
||||
def test_create_group_from_src_group(self):
|
||||
namespace_name = fake.SOURCE_VG_VOLUME['name']
|
||||
mock_namespace = nvme_library.NetAppNamespace(
|
||||
namespace_name, namespace_name, '3', {'UUID': 'fake_uuid'})
|
||||
self.mock_object(self.library, '_get_namespace_from_table',
|
||||
return_value=mock_namespace)
|
||||
mock_clone_source_to_destination = self.mock_object(
|
||||
self.library, '_clone_source_to_destination')
|
||||
|
||||
actual_return_value = self.library.create_group_from_src(
|
||||
fake.VOLUME_GROUP, [fake.VOLUME],
|
||||
source_group=fake.SOURCE_VOLUME_GROUP,
|
||||
source_vols=[fake.SOURCE_VG_VOLUME])
|
||||
|
||||
clone_source_to_destination_args = {
|
||||
'name': fake.SOURCE_VG_VOLUME['name'],
|
||||
'size': fake.SOURCE_VG_VOLUME['size'],
|
||||
}
|
||||
expected_return_value = (None, [])
|
||||
|
||||
mock_clone_source_to_destination.assert_called_once_with(
|
||||
clone_source_to_destination_args, fake.VOLUME)
|
||||
self.assertEqual(expected_return_value, actual_return_value)
|
||||
|
||||
def test_delete_group_snapshot(self):
|
||||
mock_delete_namespace = self.mock_object(self.library,
|
||||
'_delete_namespace')
|
||||
|
||||
model_update, snapshots_model_update = (
|
||||
self.library.delete_group_snapshot(fake.VOLUME_GROUP,
|
||||
[fake.VG_SNAPSHOT]))
|
||||
|
||||
self.assertIsNone(model_update)
|
||||
self.assertIsNone(snapshots_model_update)
|
||||
|
||||
mock_delete_namespace.assert_called_once_with(fake.VG_SNAPSHOT['name'])
|
||||
|
||||
@@ -107,3 +107,27 @@ class NetAppCmodeNVMeDriver(driver.BaseVD):
|
||||
|
||||
def get_pool(self, volume):
|
||||
return self.library.get_pool(volume)
|
||||
|
||||
def create_group(self, context, group):
|
||||
return self.library.create_group(group)
|
||||
|
||||
def delete_group(self, context, group, volumes):
|
||||
return self.library.delete_group(group, volumes)
|
||||
|
||||
def update_group(self, context, group, add_volumes=None,
|
||||
remove_volumes=None):
|
||||
return self.library.update_group(group, add_volumes=None,
|
||||
remove_volumes=None)
|
||||
|
||||
def create_group_snapshot(self, context, group_snapshot, snapshots):
|
||||
return self.library.create_group_snapshot(group_snapshot, snapshots)
|
||||
|
||||
def delete_group_snapshot(self, context, group_snapshot, snapshots):
|
||||
return self.library.delete_group_snapshot(group_snapshot, snapshots)
|
||||
|
||||
def create_group_from_src(self, context, group, volumes,
|
||||
group_snapshot=None, snapshots=None,
|
||||
source_group=None, source_vols=None):
|
||||
return self.library.create_group_from_src(
|
||||
group, volumes, group_snapshot=group_snapshot, snapshots=snapshots,
|
||||
source_group=source_group, source_vols=source_vols)
|
||||
|
||||
@@ -24,6 +24,7 @@ from oslo_utils import units
|
||||
from cinder import coordination
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.objects import fields
|
||||
from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api
|
||||
from cinder.volume.drivers.netapp.dataontap.performance import perf_cmode
|
||||
from cinder.volume.drivers.netapp.dataontap.utils import capabilities
|
||||
@@ -526,8 +527,8 @@ class NetAppNVMeStorageLibrary(
|
||||
pool['QoS_support'] = False
|
||||
pool['multiattach'] = False
|
||||
pool['online_extend_support'] = False
|
||||
pool['consistencygroup_support'] = False
|
||||
pool['consistent_group_snapshot_enabled'] = False
|
||||
pool['consistencygroup_support'] = True
|
||||
pool['consistent_group_snapshot_enabled'] = True
|
||||
pool['reserved_percentage'] = self.reserved_percentage
|
||||
pool['max_over_subscription_ratio'] = (
|
||||
self.max_over_subscription_ratio)
|
||||
@@ -776,3 +777,151 @@ class NetAppNVMeStorageLibrary(
|
||||
metadata = self._get_namespace_attr(name, 'metadata')
|
||||
path = metadata['Path']
|
||||
self._unmap_namespace(path, host_nqn)
|
||||
|
||||
def create_group(self, group):
|
||||
"""Driver entry point for creating a generic volume group.
|
||||
|
||||
ONTAP does not maintain an actual Group construct. As a result, no
|
||||
communication to the backend is necessary for generic volume group
|
||||
creation.
|
||||
|
||||
:returns: Hard-coded model update for generic volume group model.
|
||||
"""
|
||||
model_update = {'status': fields.GroupStatus.AVAILABLE}
|
||||
return model_update
|
||||
|
||||
def delete_group(self, group, volumes):
|
||||
"""Driver entry point for deleting a group.
|
||||
|
||||
:returns: Updated group model and list of volume models
|
||||
for the volumes that were deleted.
|
||||
"""
|
||||
model_update = {'status': fields.GroupStatus.DELETED}
|
||||
volumes_model_update = []
|
||||
for volume in volumes:
|
||||
try:
|
||||
self.delete_volume(volume)
|
||||
volumes_model_update.append(
|
||||
{'id': volume['id'], 'status': 'deleted'})
|
||||
except Exception:
|
||||
volumes_model_update.append(
|
||||
{'id': volume['id'],
|
||||
'status': 'error_deleting'})
|
||||
LOG.exception("Volume %(vol)s in the group could not be "
|
||||
"deleted.", {'vol': volume})
|
||||
return model_update, volumes_model_update
|
||||
|
||||
def update_group(self, group, add_volumes=None, remove_volumes=None):
|
||||
"""Driver entry point for updating a generic volume group.
|
||||
|
||||
Since no actual group construct is ever created in ONTAP, it is not
|
||||
necessary to update any metadata on the backend. Since this is a NO-OP,
|
||||
there is guaranteed to be no change in any of the volumes' statuses.
|
||||
"""
|
||||
return None, None, None
|
||||
|
||||
def create_group_snapshot(self, group_snapshot, snapshots):
|
||||
"""Creates a Cinder group snapshot object.
|
||||
|
||||
The Cinder group snapshot object is created by making use of an
|
||||
ephemeral ONTAP consistency group snapshot in order to provide
|
||||
write-order consistency for a set of flexvol snapshots. First, a list
|
||||
of the flexvols backing the given Cinder group must be gathered. An
|
||||
ONTAP group-snapshot of these flexvols will create a snapshot copy of
|
||||
all the Cinder volumes in the generic volume group. For each Cinder
|
||||
volume in the group, it is then necessary to clone its backing
|
||||
namespace from the ONTAP cg-snapshot. The naming convention used for
|
||||
the clones is what indicates the clone's role as a Cinder snapshot
|
||||
and its inclusion in a Cinder group. The ONTAP cg-snapshot of the
|
||||
flexvols is no longer required after having cloned the namespaces
|
||||
backing the Cinder volumes in the Cinder group.
|
||||
|
||||
:returns: An implicit update for group snapshot and snapshots models
|
||||
that is interpreted by the manager to set their models to
|
||||
available.
|
||||
"""
|
||||
try:
|
||||
if volume_utils.is_group_a_cg_snapshot_type(group_snapshot):
|
||||
self._create_consistent_group_snapshot(group_snapshot,
|
||||
snapshots)
|
||||
else:
|
||||
for snapshot in snapshots:
|
||||
self._create_snapshot(snapshot)
|
||||
except Exception as ex:
|
||||
err_msg = (_("Create group snapshot failed (%s).") % ex)
|
||||
LOG.exception(err_msg, resource=group_snapshot)
|
||||
raise na_utils.NetAppDriverException(err_msg)
|
||||
|
||||
return None, None
|
||||
|
||||
def _create_consistent_group_snapshot(self, group_snapshot, snapshots):
|
||||
flexvols = set()
|
||||
for snapshot in snapshots:
|
||||
flexvols.add(volume_utils.extract_host(
|
||||
snapshot['volume']['host'], level='pool'))
|
||||
|
||||
self.client.create_cg_snapshot(flexvols, group_snapshot['id'])
|
||||
|
||||
for snapshot in snapshots:
|
||||
self._clone_namespace(snapshot['volume']['name'], snapshot['name'])
|
||||
|
||||
for flexvol in flexvols:
|
||||
try:
|
||||
self.client.wait_for_busy_snapshot(
|
||||
flexvol, group_snapshot['id'])
|
||||
self.client.delete_snapshot(
|
||||
flexvol, group_snapshot['id'])
|
||||
except exception.SnapshotIsBusy:
|
||||
self.client.mark_snapshot_for_deletion(
|
||||
flexvol, group_snapshot['id'])
|
||||
|
||||
def delete_group_snapshot(self, group_snapshot, snapshots):
|
||||
"""Delete namespaces backing each snapshot in the group snapshot.
|
||||
|
||||
:returns: An implicit update for snapshots models that is interpreted
|
||||
by the manager to set their models to delete.
|
||||
"""
|
||||
for snapshot in snapshots:
|
||||
self._delete_namespace(snapshot['name'])
|
||||
LOG.debug("Snapshot %s deletion successful", snapshot['name'])
|
||||
|
||||
return None, None
|
||||
|
||||
def create_group_from_src(self, group, volumes, group_snapshot=None,
|
||||
snapshots=None, source_group=None,
|
||||
source_vols=None):
|
||||
"""Creates a group from a group snapshot or a group of cinder vols.
|
||||
|
||||
:returns: An implicit update for the volumes model that is
|
||||
interpreted by the manager as a successful operation.
|
||||
"""
|
||||
LOG.debug("VOLUMES %s ", ', '.join([vol['id'] for vol in volumes]))
|
||||
volume_model_updates = []
|
||||
|
||||
if group_snapshot:
|
||||
vols = zip(volumes, snapshots)
|
||||
|
||||
for volume, snapshot in vols:
|
||||
source = {
|
||||
'name': snapshot['name'],
|
||||
'size': snapshot['volume_size'],
|
||||
}
|
||||
self._clone_source_to_destination(source, volume)
|
||||
'''if volume_model_update is not None:
|
||||
volume_model_update['id'] = volume['id']
|
||||
volume_model_updates.append(volume_model_update)'''
|
||||
|
||||
else:
|
||||
vols = zip(volumes, source_vols)
|
||||
|
||||
for volume, old_src_vref in vols:
|
||||
src_namespace = self._get_namespace_from_table(
|
||||
old_src_vref['name'])
|
||||
source = {'name': src_namespace.name,
|
||||
'size': old_src_vref['size']}
|
||||
self._clone_source_to_destination(source, volume)
|
||||
'''if volume_model_update is not None:
|
||||
volume_model_update['id'] = volume['id']
|
||||
volume_model_updates.append(volume_model_update)'''
|
||||
|
||||
return None, volume_model_updates
|
||||
|
||||
@@ -622,7 +622,7 @@ driver.lvm=missing
|
||||
driver.macrosan=missing
|
||||
driver.nec=missing
|
||||
driver.nec_v=complete
|
||||
driver.netapp_ontap_nvme_tcp=missing
|
||||
driver.netapp_ontap_nvme_tcp=complete
|
||||
driver.netapp_ontap_iscsi_fc=complete
|
||||
driver.netapp_ontap_nfs=complete
|
||||
driver.netapp_solidfire=complete
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
NetApp Driver `bug #2116261
|
||||
<https://bugs.launchpad.net/cinder/+bug/2116261>`_: NetApp already
|
||||
support the consistency group for NFS/iSCSI/FCP protocol. Extend
|
||||
the same support for NVMe/TCP protocol.
|
||||
Reference in New Issue
Block a user