Update timedelta and old schedules as per netapp_snapmirror_schedule

Asynchronous SnapMirror schedules are set using netapp config option
'netapp_snapmirror_schedule'. The delta for determining replica is
in-sync or out-of-sync updated to twice the schedule time seconds.
Also, not only new snapmirrors, but also old ones should
have a schedule according to the current
'netapp_snapmirror_schedule' config.

Closes-bug: #1996859
Depends-On: I0390f82dfdc130d49e3af6928996dd730e3cf69f
Change-Id: Ifbe0575f6c359929344763666e4d93d8c6084e83
This commit is contained in:
Kiran Pawar 2022-12-07 17:13:50 +00:00
parent e31be16130
commit da23b652fb
8 changed files with 172 additions and 11 deletions

View File

@ -2192,7 +2192,8 @@ class NetAppRestClient(object):
fields = ['state', 'source.svm.name', 'source.path', fields = ['state', 'source.svm.name', 'source.path',
'destination.svm.name', 'destination.path', 'destination.svm.name', 'destination.path',
'transfer.end_time', 'uuid', 'policy.type'] 'transfer.end_time', 'uuid', 'policy.type',
'transfer_schedule.name']
query = {} query = {}
query['fields'] = ','.join(fields) query['fields'] = ','.join(fields)
@ -2223,6 +2224,7 @@ class NetAppRestClient(object):
snapmirrors.append({ snapmirrors.append({
'relationship-status': record.get('state'), 'relationship-status': record.get('state'),
'mirror-state': record.get('state'), 'mirror-state': record.get('state'),
'schedule': record['transfer_schedule']['name'],
'source-vserver': record['source']['svm']['name'], 'source-vserver': record['source']['svm']['name'],
'source-volume': (record['source']['path'].split(':')[1] if 'source-volume': (record['source']['path'].split(':')[1] if
record.get('source') else None), record.get('source') else None),
@ -2959,7 +2961,7 @@ class NetAppRestClient(object):
def _set_snapmirror_state(self, state, source_path, destination_path, def _set_snapmirror_state(self, state, source_path, destination_path,
source_vserver, source_volume, source_vserver, source_volume,
destination_vserver, destination_volume, destination_vserver, destination_volume,
wait_result=True): wait_result=True, schedule=None):
"""Change the snapmirror state between two volumes.""" """Change the snapmirror state between two volumes."""
snapmirror = self.get_snapmirrors(source_path, destination_path, snapmirror = self.get_snapmirrors(source_path, destination_path,
@ -2978,7 +2980,12 @@ class NetAppRestClient(object):
raise na_utils.NetAppDriverException(msg) raise na_utils.NetAppDriverException(msg)
uuid = snapmirror[0]['uuid'] uuid = snapmirror[0]['uuid']
body = {'state': state} body = {}
if state:
body.update({'state': state})
if schedule:
body.update({"transfer_schedule": {'name': schedule}})
result = self.send_request(f'/snapmirror/relationships/{uuid}', result = self.send_request(f'/snapmirror/relationships/{uuid}',
'patch', body=body, 'patch', body=body,
wait_on_accepted=wait_result) wait_on_accepted=wait_result)
@ -3023,6 +3030,29 @@ class NetAppRestClient(object):
source_vserver, source_volume, source_vserver, source_volume,
dest_vserver, dest_volume, wait_result=False) dest_vserver, dest_volume, wait_result=False)
@na_utils.trace
def modify_snapmirror_vol(self, source_vserver, source_volume,
dest_vserver, dest_volume,
schedule=None, policy=None, tries=None,
max_transfer_rate=None):
"""Modifies a SnapMirror relationship between volumes."""
return self._modify_snapmirror(
source_vserver=source_vserver, dest_vserver=dest_vserver,
source_volume=source_volume, dest_volume=dest_volume,
schedule=schedule)
@na_utils.trace
def _modify_snapmirror(self, source_path=None, dest_path=None,
source_vserver=None, dest_vserver=None,
source_volume=None, dest_volume=None,
schedule=None):
"""Modifies a SnapMirror relationship."""
return self._set_snapmirror_state(
None, source_path, dest_path,
source_vserver, source_volume,
dest_vserver, dest_volume, wait_result=False,
schedule=schedule)
@na_utils.trace @na_utils.trace
def create_volume_clone(self, volume_name, parent_volume_name, def create_volume_clone(self, volume_name, parent_volume_name,
parent_snapshot_name=None, split=False, parent_snapshot_name=None, split=False,

View File

@ -175,6 +175,7 @@ class DataMotionSession(object):
source_volume=src_volume_name, dest_volume=dest_volume_name, source_volume=src_volume_name, dest_volume=dest_volume_name,
desired_attributes=['relationship-status', desired_attributes=['relationship-status',
'mirror-state', 'mirror-state',
'schedule',
'source-vserver', 'source-vserver',
'source-volume', 'source-volume',
'last-transfer-end-timestamp', 'last-transfer-end-timestamp',
@ -422,6 +423,27 @@ class DataMotionSession(object):
dest_vserver, dest_vserver,
dest_volume_name) dest_volume_name)
def modify_snapmirror(self, source_share_obj, dest_share_obj,
schedule=None):
"""Modify SnapMirror relationship: set schedule"""
dest_volume_name, dest_vserver, dest_backend = (
self.get_backend_info_for_share(dest_share_obj))
dest_client = get_client_for_backend(dest_backend,
vserver_name=dest_vserver)
src_volume_name, src_vserver, __ = self.get_backend_info_for_share(
source_share_obj)
if schedule is None:
config = get_backend_configuration(dest_backend)
schedule = config.netapp_snapmirror_schedule
dest_client.modify_snapmirror_vol(src_vserver,
src_volume_name,
dest_vserver,
dest_volume_name,
schedule=schedule)
def resume_snapmirror(self, source_share_obj, dest_share_obj): def resume_snapmirror(self, source_share_obj, dest_share_obj):
"""Resume SnapMirror relationship from a quiesced state.""" """Resume SnapMirror relationship from a quiesced state."""
dest_volume_name, dest_vserver, dest_backend = ( dest_volume_name, dest_vserver, dest_backend = (

View File

@ -23,6 +23,7 @@ import copy
import datetime import datetime
import json import json
import math import math
import re
import socket import socket
from oslo_config import cfg from oslo_config import cfg
@ -172,6 +173,8 @@ class NetAppCmodeFileStorageLibrary(object):
self._backend_name = self.configuration.safe_get( self._backend_name = self.configuration.safe_get(
'share_backend_name') or driver_name 'share_backend_name') or driver_name
self.message_api = message_api.API() self.message_api = message_api.API()
self._snapmirror_schedule = self._convert_schedule_to_seconds(
schedule=self.configuration.netapp_snapmirror_schedule)
@na_utils.trace @na_utils.trace
def do_setup(self, context): def do_setup(self, context):
@ -2552,6 +2555,35 @@ class NetAppCmodeFileStorageLibrary(object):
self._delete_share(replica, vserver, vserver_client, self._delete_share(replica, vserver, vserver_client,
remove_export=is_readable, remove_qos=is_readable) remove_export=is_readable, remove_qos=is_readable)
@na_utils.trace
def _convert_schedule_to_seconds(self, schedule='hourly'):
"""Convert snapmirror schedule to seconds."""
results = re.findall(r'[\d]+|[^d]+', schedule)
if not results or len(results) > 2:
return 3600 # default (1 hour)
if len(results) == 2:
try:
num = int(results[0])
except ValueError:
return 3600
schedule = results[1]
else:
num = 1
schedule = results[0]
schedule = schedule.lower()
if schedule in ['min', 'minute']:
return (num * 60)
if schedule in ['hour', 'hourly']:
return (num * 3600)
if schedule in ['day', 'daily']:
return (num * 24 * 3600)
if schedule in ['week', 'weekly']:
return (num * 7 * 24 * 3600)
if schedule in ['month', 'monthly']:
return (num * 30 * 24 * 2600)
return 3600
def update_replica_state(self, context, replica_list, replica, def update_replica_state(self, context, replica_list, replica,
access_rules, share_snapshots, share_server=None, access_rules, share_snapshots, share_server=None,
replication=True): replication=True):
@ -2624,14 +2656,26 @@ class NetAppCmodeFileStorageLibrary(object):
LOG.exception("Could not resync snapmirror.") LOG.exception("Could not resync snapmirror.")
return constants.STATUS_ERROR return constants.STATUS_ERROR
current_schedule = snapmirror.get('schedule')
new_schedule = self.configuration.netapp_snapmirror_schedule
if current_schedule != new_schedule:
dm_session.modify_snapmirror(active_replica, replica,
schedule=new_schedule)
LOG.debug('Modify snapmirror schedule for replica:'
'%(replica)s from %(from)s to %(to)s',
{'replica': replica['id'],
'from': current_schedule,
'to': new_schedule})
last_update_timestamp = float( last_update_timestamp = float(
snapmirror.get('last-transfer-end-timestamp', 0)) snapmirror.get('last-transfer-end-timestamp', 0))
# TODO(ameade): Have a configurable RPO for replicas, for now it is # Recovery Point Objective (RPO) indicates the point in time to
# one hour. # which data can be recovered. The RPO target is typically less
# than twice the replication schedule.
if (last_update_timestamp and if (last_update_timestamp and
(timeutils.is_older_than( (timeutils.is_older_than(
datetime.datetime.utcfromtimestamp(last_update_timestamp) datetime.datetime.utcfromtimestamp(last_update_timestamp)
.isoformat(), 3600))): .isoformat(), (2 * self._snapmirror_schedule)))):
return constants.REPLICA_STATE_OUT_OF_SYNC return constants.REPLICA_STATE_OUT_OF_SYNC
replica_backend = share_utils.extract_host(replica['host'], replica_backend = share_utils.extract_host(replica['host'],

View File

@ -3936,7 +3936,10 @@ SNAPMIRROR_GET_ITER_RESPONSE_REST = {
"type": "async" "type": "async"
}, },
"state": "snapmirrored", "state": "snapmirrored",
"healthy": True "healthy": True,
"transfer_schedule": {
"name": "hourly",
},
} }
], ],
"num_records": 1, "num_records": 1,
@ -3951,7 +3954,8 @@ REST_GET_SNAPMIRRORS_RESPONSE = [{
'source-volume': SM_SOURCE_VOLUME, 'source-volume': SM_SOURCE_VOLUME,
'source-vserver': SM_SOURCE_VSERVER, 'source-vserver': SM_SOURCE_VSERVER,
'uuid': FAKE_UUID, 'uuid': FAKE_UUID,
'policy-type': 'async' 'policy-type': 'async',
'schedule': 'hourly'
}] }]
FAKE_CIFS_RECORDS = { FAKE_CIFS_RECORDS = {

View File

@ -2185,7 +2185,8 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
'destination.path': (fake.SM_DEST_VSERVER + 'destination.path': (fake.SM_DEST_VSERVER +
':' + fake.SM_DEST_VOLUME), ':' + fake.SM_DEST_VOLUME),
'fields': 'state,source.svm.name,source.path,destination.svm.name,' 'fields': 'state,source.svm.name,source.path,destination.svm.name,'
'destination.path,transfer.end_time,uuid,policy.type' 'destination.path,transfer.end_time,uuid,policy.type,'
'transfer_schedule.name'
} }
mock_send_request.assert_called_once_with('/snapmirror/relationships', mock_send_request.assert_called_once_with('/snapmirror/relationships',
@ -2216,7 +2217,8 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
'destination.path': (fake.SM_DEST_VSERVER + 'destination.path': (fake.SM_DEST_VSERVER +
':' + fake.SM_DEST_VOLUME), ':' + fake.SM_DEST_VOLUME),
'fields': 'state,source.svm.name,source.path,destination.svm.name,' 'fields': 'state,source.svm.name,source.path,destination.svm.name,'
'destination.path,transfer.end_time,uuid,policy.type' 'destination.path,transfer.end_time,uuid,policy.type,'
'transfer_schedule.name'
} }
mock_send_request.assert_called_once_with('/snapmirror/relationships', mock_send_request.assert_called_once_with('/snapmirror/relationships',
@ -2243,7 +2245,8 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
'destination.path': (fake.SM_DEST_VSERVER + 'destination.path': (fake.SM_DEST_VSERVER +
':' + fake.SM_DEST_VOLUME), ':' + fake.SM_DEST_VOLUME),
'fields': 'state,source.svm.name,source.path,destination.svm.name,' 'fields': 'state,source.svm.name,source.path,destination.svm.name,'
'destination.path,transfer.end_time,uuid,policy.type' 'destination.path,transfer.end_time,uuid,policy.type,'
'transfer_schedule.name'
} }
mock_send_request.assert_called_once_with('/snapmirror/relationships', mock_send_request.assert_called_once_with('/snapmirror/relationships',
@ -2944,6 +2947,34 @@ class NetAppRestCmodeClientTestCase(test.TestCase):
self.assertEqual(expected_job, result) self.assertEqual(expected_job, result)
def test_modify_snapmirror_vol(self):
expected_job = {
'operation-id': None,
'status': None,
'jobid': fake.FAKE_UUID,
'error-code': None,
'error-message': None,
}
mock_set_snapmirror_state = self.mock_object(
self.client,
'_set_snapmirror_state',
mock.Mock(return_value=expected_job))
result = self.client.modify_snapmirror_vol(
fake.SM_SOURCE_VSERVER, fake.SM_SOURCE_VOLUME,
fake.SM_DEST_VSERVER, fake.SM_DEST_VOLUME,
None)
mock_set_snapmirror_state.assert_called_once_with(
None, None, None,
fake.SM_SOURCE_VSERVER, fake.SM_SOURCE_VOLUME,
fake.SM_DEST_VSERVER, fake.SM_DEST_VOLUME,
wait_result=False, schedule=None)
self.assertEqual(expected_job, result)
def test__abort_snapmirror(self): def test__abort_snapmirror(self):
return_snp = fake.REST_GET_SNAPMIRRORS_RESPONSE return_snp = fake.REST_GET_SNAPMIRRORS_RESPONSE
mock_get_snap = self.mock_object(self.client, '_get_snapmirrors', mock_get_snap = self.mock_object(self.client, '_get_snapmirrors',

View File

@ -799,6 +799,7 @@ class NetAppCDOTDataMotionSessionTestCase(test.TestCase):
dest_volume=self.fake_dest_vol_name, dest_volume=self.fake_dest_vol_name,
desired_attributes=['relationship-status', desired_attributes=['relationship-status',
'mirror-state', 'mirror-state',
'schedule',
'source-vserver', 'source-vserver',
'source-volume', 'source-volume',
'last-transfer-end-timestamp', 'last-transfer-end-timestamp',

View File

@ -3991,6 +3991,19 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
data_motion.get_client_for_backend.assert_called_once_with( data_motion.get_client_for_backend.assert_called_once_with(
fake.BACKEND_NAME, vserver_name=fake.VSERVER1) fake.BACKEND_NAME, vserver_name=fake.VSERVER1)
@ddt.data({'seconds': 3600, 'schedule': 'hourly'},
{'seconds': (5 * 3600), 'schedule': '5hourly'},
{'seconds': (30 * 60), 'schedule': '30minute'},
{'seconds': (2 * 24 * 3600), 'schedule': '2DAY'},
{'seconds': 3600, 'schedule': 'fake_shedule'},
{'seconds': 3600, 'schedule': 'fake2'},
{'seconds': 3600, 'schedule': '10fake'})
@ddt.unpack
def test__convert_schedule_to_seconds(self, seconds, schedule):
expected_return = seconds
actual_return = self.library._convert_schedule_to_seconds(schedule)
self.assertEqual(expected_return, actual_return)
def test_update_replica_state_no_snapmirror_share_creating(self): def test_update_replica_state_no_snapmirror_share_creating(self):
vserver_client = mock.Mock() vserver_client = mock.Mock()
self.mock_object(vserver_client, 'volume_exists', self.mock_object(vserver_client, 'volume_exists',
@ -4209,6 +4222,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
def test_update_replica_state_stale_snapmirror(self): def test_update_replica_state_stale_snapmirror(self):
fake_snapmirror = { fake_snapmirror = {
'mirror-state': 'snapmirrored', 'mirror-state': 'snapmirrored',
'schedule': self.library.configuration.netapp_snapmirror_schedule,
'last-transfer-end-timestamp': '%s' % float( 'last-transfer-end-timestamp': '%s' % float(
timeutils.utcnow_ts() - 10000) timeutils.utcnow_ts() - 10000)
} }
@ -4224,6 +4238,9 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
self.mock_object(self.library, self.mock_object(self.library,
'_is_readable_replica', '_is_readable_replica',
mock.Mock(return_value=False)) mock.Mock(return_value=False))
mock_backend_config = fake.get_config_cmode()
self.mock_object(data_motion, 'get_backend_configuration',
mock.Mock(return_value=mock_backend_config))
result = self.library.update_replica_state(None, [fake.SHARE], result = self.library.update_replica_state(None, [fake.SHARE],
fake.SHARE, None, [], fake.SHARE, None, [],
@ -4234,6 +4251,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
def test_update_replica_state_in_sync(self): def test_update_replica_state_in_sync(self):
fake_snapmirror = { fake_snapmirror = {
'mirror-state': 'snapmirrored', 'mirror-state': 'snapmirrored',
'schedule': self.library.configuration.netapp_snapmirror_schedule,
'relationship-status': 'idle', 'relationship-status': 'idle',
'last-transfer-end-timestamp': '%s' % float(time.time()) 'last-transfer-end-timestamp': '%s' % float(time.time())
} }
@ -4276,6 +4294,7 @@ class NetAppFileStorageLibraryTestCase(test.TestCase):
def test_update_replica_state_in_sync_with_snapshots(self): def test_update_replica_state_in_sync_with_snapshots(self):
fake_snapmirror = { fake_snapmirror = {
'mirror-state': 'snapmirrored', 'mirror-state': 'snapmirrored',
'schedule': self.library.configuration.netapp_snapmirror_schedule,
'relationship-status': 'idle', 'relationship-status': 'idle',
'last-transfer-end-timestamp': '%s' % float(time.time()) 'last-transfer-end-timestamp': '%s' % float(time.time())
} }

View File

@ -0,0 +1,10 @@
---
fixes:
- |
NetApp ONTAP driver fixed to consider timestamp delta calculated from
`netapp_snapmirror_schedule` config option instead of fixed one hour
value. Delta is calculated as twice the time of the option. Also, ensure
periodically that existent snapmirrors have the schedule property
according to the `netapp_snapmirror_schedule` configuration value. For
more details, please refer
`Launchpad bug #1996859 <https://bugs.launchpad.net/manila/+bug/1996859>`_