Merge "NetApp ONTAP:Add async single CG replication support"

This commit is contained in:
Zuul
2026-04-08 20:54:14 +00:00
committed by Gerrit Code Review
8 changed files with 2474 additions and 91 deletions
@@ -42,6 +42,7 @@ from cinder.volume.drivers.netapp.dataontap.utils import loopingcalls
from cinder.volume.drivers.netapp.dataontap.utils import utils as dot_utils
from cinder.volume.drivers.netapp import utils as na_utils
from cinder.volume import volume_utils
from oslo_utils import units
@ddt.ddt
@@ -175,6 +176,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
return_value={'fake_map': None})
mock_add_looping_tasks = self.mock_object(
self.library, '_add_looping_tasks')
self.library.replication_enabled = False
self.library.check_for_setup_error()
@@ -183,6 +185,33 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
mock_get_pool_map.assert_called_once_with()
mock_add_looping_tasks.assert_called_once_with()
def test_check_for_setup_error_with_replication_enabled(self):
"""Test brownfield validation is called when replication is enabled."""
super_check_for_setup_error = self.mock_object(
block_base.NetAppBlockStorageLibrary, 'check_for_setup_error')
mock_get_pool_map = self.mock_object(
self.library, '_get_flexvol_to_pool_map',
return_value={'fake_map': None})
mock_add_looping_tasks = self.mock_object(
self.library, '_add_looping_tasks')
mock_validate_brownfield = self.mock_object(
self.library, 'validate_no_conflicting_snapmirrors')
mock_get_flexvol_names = self.mock_object(
self.library.ssc_library, 'get_ssc_flexvol_names',
return_value=['vol1', 'vol2'])
self.library.replication_enabled = True
self.library.backend_name = 'test_backend'
self.library.check_for_setup_error()
self.assertEqual(1, super_check_for_setup_error.call_count)
self.assertEqual(1, mock_add_looping_tasks.call_count)
mock_get_pool_map.assert_called_once_with()
mock_get_flexvol_names.assert_called_once_with()
mock_validate_brownfield.assert_called_once_with(
self.library.configuration, 'test_backend', ['vol1', 'vol2'])
def test_check_for_setup_error_no_filtered_pools(self):
self.mock_object(block_base.NetAppBlockStorageLibrary,
'check_for_setup_error')
@@ -530,7 +559,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
'consistencygroup_support': True,
'consistent_group_snapshot_enabled': True,
'reserved_percentage': 5,
'max_over_subscription_ratio': 10.0,
'max_over_subscription_ratio': 10,
'multiattach': True,
'total_capacity_gb': 10.0,
'free_capacity_gb': 2.0,
@@ -560,7 +589,7 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
if not cluster_credentials:
expected[0].update({
'netapp_aggregate_used_percent': 0,
'netapp_dedupe_used_percent': 0
'netapp_dedupe_used_percent': 0.0
})
if replication_backends:
@@ -1704,6 +1733,18 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
mock_destroy_lun.assert_called_once_with(new_lun_path)
def test_revert_to_snapshot_success_asar2(self):
self.library.configuration.netapp_disaggregated_platform = True
volume = {'name': 'vol1'}
snapshot = {'volume_name': 'vol1', 'name': 'snap1'}
# Mock _revert_to_snapshot since revert_to_snapshot calls it
mock_revert = self.mock_object(self.library, '_revert_to_snapshot')
self.library.revert_to_snapshot(volume, snapshot)
mock_revert.assert_called_once_with(volume, snapshot)
def test__clone_snapshot(self):
lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'],
fake.LUN_WITH_METADATA['name'],
@@ -1949,3 +1990,131 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
mock_log.debug.assert_any_call("Updating perf cache for cluster.")
mock_log.debug.assert_any_call(
"Successfully updated perf cache for cluster.")
@test.testtools.skip("Method _get_disaggregated_capacity not implemented")
def test_get_disaggregated_capacity_basic(self):
"""Aggregates present, all capacities present."""
aggregates = ['aggr1', 'aggr2']
aggr_capacities = {
'aggr1': {'size-total': 10 * units.Gi,
'size-available': 4 * units.Gi},
'aggr2': {'size-total': 6 * units.Gi,
'size-available': 2 * units.Gi},
}
self.library.zapi_client.get_vserver_aggregates.return_value = (
aggregates
)
self.library.zapi_client.get_aggregate_capacities.return_value = (
aggr_capacities
)
result = self.library._get_disaggregated_capacity()
(self.library.zapi_client.
get_vserver_aggregates.assert_called_once_with())
(self.library.zapi_client.
get_aggregate_capacities.assert_called_once_with(
aggregates
))
self.assertEqual(16 * units.Gi, result['size-total'])
self.assertEqual(6 * units.Gi, result['size-available'])
@test.testtools.skip("Method _get_disaggregated_capacity not implemented")
def test_get_disaggregated_capacity_no_aggregates(self):
"""No SVM mapped aggregates returns all zeros."""
aggregates = []
aggr_capacities = {}
self.library.zapi_client.get_vserver_aggregates.return_value = (
aggregates
)
self.library.zapi_client.get_aggregate_capacities.return_value = (
aggr_capacities
)
result = self.library._get_disaggregated_capacity()
(self.library.zapi_client.
get_vserver_aggregates.assert_called_once_with())
(self.library.zapi_client.
get_aggregate_capacities.assert_called_once_with(
aggregates
))
self.assertEqual(0, result['size-total'])
self.assertEqual(0, result['size-available'])
@test.testtools.skip("Method _get_disaggregated_capacity not implemented")
def test_get_disaggregated_capacity_missing_keys(self):
"""Gracefully handle capacity dicts missing size fields."""
aggregates = ['aggr1', 'aggr2']
aggr_capacities = {
'aggr1': {'size-total': 5 * units.Gi}, # missing size-available
'aggr2': {'size-available': 3 * units.Gi}, # missing size-total
}
self.library.zapi_client.get_vserver_aggregates.return_value = (
aggregates
)
self.library.zapi_client.get_aggregate_capacities.return_value = (
aggr_capacities
)
result = self.library._get_disaggregated_capacity()
self.assertEqual(5 * units.Gi, result['size-total'])
self.assertEqual(3 * units.Gi, result['size-available'])
@test.testtools.skip(
"Method _get_disaggregated_provisioned_capacity not implemented")
def test_get_disaggregated_provisioned_capacity_sums_sizes(self):
# Ensure vserver is the expected string
self.library.vserver = 'fake_svm'
storage_units = [
{'name': 'su1', 'uuid': 'uuid1',
'provisioned-size': 10 * units.Gi},
{'name': 'su2', 'uuid': 'uuid2',
'provisioned-size': 20 * units.Gi},
]
self.library.zapi_client.get_storage_units_by_svm.return_value = (
storage_units
)
result = self.library._get_disaggregated_provisioned_capacity()
self.assertEqual(30 * units.Gi, result)
(self.library.zapi_client.get_storage_units_by_svm.
assert_called_once_with(vserver='fake_svm'))
@test.testtools.skip(
"Method _get_disaggregated_provisioned_capacity not implemented")
def test_get_disaggregated_provisioned_capacity_handles_bad_entries(self):
# Ensure vserver is the expected string
self.library.vserver = 'fake_svm'
self.library.zapi_client.get_storage_units_by_svm.return_value = [
{'provisioned-size': '10'},
{'provisioned-size': 'not-a-number'},
{},
]
result = self.library._get_disaggregated_provisioned_capacity()
self.assertEqual(10, result)
self.library.zapi_client.get_storage_units_by_svm. \
assert_called_once_with(vserver='fake_svm')
@test.testtools.skip(
"Method _get_disaggregated_provisioned_capacity not implemented")
def test_get_disaggregated_provisioned_capacity_empty_list(self):
# Ensure vserver is the expected string
self.library.vserver = 'fake_svm'
self.library.zapi_client.get_storage_units_by_svm.return_value = []
result = self.library._get_disaggregated_provisioned_capacity()
self.assertEqual(0, result)
(self.library.zapi_client.get_storage_units_by_svm.
assert_called_once_with(vserver='fake_svm'))
@@ -71,6 +71,22 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase):
self.driver.zapi_client = mock.Mock()
self.driver.using_cluster_credentials = True
# Mock StorageObjectType since it's not defined in na_utils
storage_object_type_mock = mock.Mock()
storage_object_type_mock.VOLUME = 'volume'
self.storage_type_patcher = mock.patch.object(
na_utils, 'StorageObjectType', storage_object_type_mock,
create=True)
self.storage_type_patcher.start()
self.addCleanup(self.storage_type_patcher.stop)
# Mock create_cg_path function
self.cg_path_patcher = mock.patch.object(
na_utils, 'create_cg_path',
lambda cg_name: f'/cg/{cg_name}', create=True)
self.cg_path_patcher.start()
self.addCleanup(self.cg_path_patcher.stop)
def get_config_cmode(self):
config = na_fakes.create_configuration_cmode()
config.netapp_storage_protocol = 'nfs'
@@ -463,6 +479,7 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase):
FLEXGROUP=True))
super_check_for_setup_error = self.mock_object(
nfs_base.NetAppNfsDriver, 'check_for_setup_error')
self.driver.replication_enabled = False
self.driver.check_for_setup_error()
@@ -471,6 +488,34 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase):
mock_add_looping_tasks.assert_called_once_with()
mock_contains_fg.assert_called_once_with()
def test_check_for_setup_error_with_replication_enabled(self):
"""Test brownfield validation is called when replication is enabled."""
mock_add_looping_tasks = self.mock_object(
self.driver, '_add_looping_tasks')
self.mock_object(
self.driver.ssc_library, 'contains_flexgroup_pool',
return_value=False)
self.driver.zapi_client = mock.Mock(features=mock.Mock(
FLEXGROUP=True))
super_check_for_setup_error = self.mock_object(
nfs_base.NetAppNfsDriver, 'check_for_setup_error')
mock_validate_brownfield = self.mock_object(
self.driver, 'validate_no_conflicting_snapmirrors')
mock_get_flexvol_names = self.mock_object(
self.driver.ssc_library, 'get_ssc_flexvol_names',
return_value=['vol1', 'vol2'])
self.driver.replication_enabled = True
self.driver.backend_name = 'test_backend'
self.driver.check_for_setup_error()
self.assertEqual(1, super_check_for_setup_error.call_count)
self.assertEqual(1, mock_add_looping_tasks.call_count)
mock_get_flexvol_names.assert_called_once_with()
mock_validate_brownfield.assert_called_once_with(
self.driver.configuration, 'test_backend', ['vol1', 'vol2'])
def test_check_for_setup_error_fail(self):
mock_add_looping_tasks = self.mock_object(
self.driver, '_add_looping_tasks')
@@ -503,6 +548,9 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase):
return_value=fake_ssc.SSC.keys())
mock_remove_unused_qos_policy_groups = self.mock_object(
self.driver.zapi_client, 'remove_unused_qos_policy_groups')
mock_is_consistent_replication = self.mock_object(
self.driver, '_is_consistent_replication_enabled',
return_value=False)
self.driver.replication_enabled = replication_enabled
self.driver.failed_over = failed_over
@@ -514,12 +562,55 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase):
mock_remove_unused_qos_policy_groups.assert_not_called()
if replication_enabled and not failed_over:
mock_is_consistent_replication.assert_called_once_with(
self.driver.configuration)
ensure_mirrors.assert_called_once_with(
self.driver.configuration, self.driver.backend_name,
fake_ssc.SSC.keys())
else:
self.assertFalse(ensure_mirrors.called)
def test_handle_housekeeping_tasks_with_consistent_replication(self):
"""Test housekeeping calls consistent replication when enabled."""
self.driver.using_cluster_credentials = True
self.driver.replication_enabled = True
self.driver.failed_over = False
ensure_mirrors = self.mock_object(data_motion.DataMotionMixin,
'ensure_snapmirrors')
ensure_consistent_mirrors = self.mock_object(
data_motion.DataMotionMixin,
'ensure_consistent_replication_snapmirrors')
self.mock_object(self.driver.ssc_library, 'get_ssc_flexvol_names',
return_value=['vol1', 'vol2'])
mock_remove_unused_qos_policy_groups = self.mock_object(
self.driver.zapi_client, 'remove_unused_qos_policy_groups')
mock_is_consistent_replication = self.mock_object(
self.driver, '_is_consistent_replication_enabled',
return_value=True)
self.driver._handle_housekeeping_tasks()
mock_remove_unused_qos_policy_groups.assert_called_once_with()
mock_is_consistent_replication.assert_called_once_with(
self.driver.configuration)
ensure_consistent_mirrors.assert_called_once_with(
self.driver.configuration, self.driver.backend_name,
na_utils.StorageObjectType.VOLUME, ['vol1', 'vol2'])
ensure_mirrors.assert_not_called()
@ddt.data(True, False)
def test_is_consistent_replication_enabled(self, config_value):
"""Test _is_consistent_replication_enabled returns config value."""
mock_config = mock.Mock()
mock_config.safe_get.return_value = config_value
result = self.driver._is_consistent_replication_enabled(mock_config)
self.assertEqual(config_value, result)
mock_config.safe_get.assert_called_once_with(
'netapp_consistent_replication')
def test_handle_ems_logging(self):
volume_list = ['vol0', 'vol1', 'vol2']
File diff suppressed because it is too large Load Diff
@@ -149,6 +149,15 @@ class NetAppBlockStorageCmodeLibrary(
'Ensure ASA r2 configuration option is set correctly.')
raise na_utils.NetAppDriverException(msg)
# Validate existing snapmirror scenario for replication
if self.replication_enabled:
LOG.debug("Replication is enabled. Performing existing "
"snapmirror validation to check for conflicting "
"relationships.")
flexvol_names = self.ssc_library.get_ssc_flexvol_names()
self.validate_no_conflicting_snapmirrors(
self.configuration, self.backend_name, flexvol_names)
self._add_looping_tasks()
super(NetAppBlockStorageCmodeLibrary, self).check_for_setup_error()
@@ -128,6 +128,15 @@ class NetAppCmodeNfsDriver(
msg = _('FlexGroup pool requires Data ONTAP 9.8 or later.')
raise na_utils.NetAppDriverException(msg)
# Validate brownfield scenario for replication
if self.replication_enabled:
LOG.debug("Replication is enabled. Performing brownfield "
"validation to check for conflicting SnapMirror "
"relationships.")
flexvol_names = self.ssc_library.get_ssc_flexvol_names()
self.validate_no_conflicting_snapmirrors(
self.configuration, self.backend_name, flexvol_names)
super(NetAppCmodeNfsDriver, self).check_for_setup_error()
def _add_looping_tasks(self):
@@ -182,9 +191,24 @@ class NetAppCmodeNfsDriver(
# Create pool mirrors if whole-backend replication configured
if self.replication_enabled and not self.failed_over:
self.ensure_snapmirrors(
self.configuration, self.backend_name,
self.ssc_library.get_ssc_flexvol_names())
if self._is_consistent_replication_enabled(self.configuration):
LOG.debug("Ensuring consistent replication snapmirrors.")
storage_object_names = self.ssc_library.get_ssc_flexvol_names()
self.ensure_consistent_replication_snapmirrors(
self.configuration, self.backend_name,
na_utils.StorageObjectType.VOLUME,
storage_object_names)
else:
LOG.debug(
"Ensuring replication snapmirrors across each "
"FlexVol")
self.ensure_snapmirrors(
self.configuration, self.backend_name,
self.ssc_library.get_ssc_flexvol_names())
def _is_consistent_replication_enabled(self, config):
return config.safe_get('netapp_consistent_replication')
def _do_qos_for_volume(self, volume, extra_specs, cleanup=True):
try:
File diff suppressed because it is too large Load Diff
+18 -5
View File
@@ -334,12 +334,25 @@ netapp_replication_opts = [
'create to complete and go online.'),
cfg.StrOpt('netapp_replication_policy',
default='MirrorAllSnapshots',
choices=['AutomatedFailOver', 'AutomatedFailOverDuplex',
'Asynchronous', 'MirrorAllSnapshots'],
help='This option defines the replication policy to be used '
'while creating snapmirror relationship. Default is '
'MirrorAllSnapshots which is based on async-mirror.'
'User can pass values like Sync, StrictSync for '
'synchronous snapmirror relationship (SM-S) to achieve '
'zero RPO')]
'while creating snapmirror relationship. Allowed values: '
'AutomatedFailOver (SnapMirror active sync for automatic '
'failover), AutomatedFailOverDuplex (active sync with '
'bidirectional synchronous replication), '
'Asynchronous (async with hourly schedule), '
'MirrorAllSnapshots '
'(default, async mirroring all snapshots and latest '
'active file system).'),
cfg.BoolOpt('netapp_consistent_replication',
default='False',
help='This option defines the way the replication of '
'volume pools in the backend stanza will be done.'
'If set to True, a single consistency group will be '
'created to map to all the FlexVol volumes in the pool '
'and protected with the replication policy defined.'
'Valid values are True/False. Default is False.')]
netapp_support_opts = [
cfg.StrOpt('netapp_api_trace_pattern',
@@ -0,0 +1,39 @@
---
features:
- |
NetApp ONTAP driver: Added support for async single consistency group (CG)
replication. When ``netapp_consistent_replication`` is enabled, the driver
creates a single ONTAP consistency group that includes all FlexVols in the
backend, providing crash-consistent replication across all
volumes. This ensures all volumes in the backend are replicated together
and enables crash consistent disaster recovery. The feature supports both
creating new consistency groups and adopting existing single CGs. Falls
back to per-FlexVol replication when disabled (default behavior).
- |
NetApp ONTAP driver: Added validation during driver initialization to check
for pre-existing SnapMirror relationships when replication is enabled. The
driver validates that existing SnapMirror relationships follow the NetApp
Cinder driver's naming convention (matching source and destination volume
names). If conflicting SnapMirrors are found with different destination
names, the driver raises an error with detailed remediation steps,
preventing replication conflicts.
- |
NetApp ONTAP driver: Improved replication policy configuration with explicit
choices validation. The ``netapp_replication_policy`` option now supports
values: AutomatedFailOver (SnapMirror active sync for automatic failover),
AutomatedFailOverDuplex (active sync with bidirectional replication),
Asynchronous (async with hourly schedule), and MirrorAllSnapshots (default,
async mirroring all snapshots).
fixes:
- |
NetApp ONTAP driver: Fixed SnapMirror creation to always use
'extended_data_protection' (XDP) relationship type instead of the deprecated
'data_protection' (DP) type. XDP is required for consistency group support
and prevents ONTAP error 13001 on newer versions where DP relationships are
no longer supported. This change applies to all new SnapMirrors regardless
of FlexVol or FlexGroup pool type.
- |
NetApp ONTAP driver: Fixed replication policy detection to use explicit
string matching instead of substring matching. This prevents false positives
where policy names containing certain substrings would be incorrectly
identified.