Merge "NetApp - Extended Consistency group support for NVMe/TCP driver"

This commit is contained in:
Zuul
2025-08-23 13:50:45 +00:00
committed by Gerrit Code Review
5 changed files with 347 additions and 5 deletions

View File

@@ -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'])

View File

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

View File

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

View File

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

View File

@@ -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.