Netapp ONTAP: Add support to revert to snapshot
This patch adds support to revert to snapshot for NFS, FC and iSCSI drivers with FlexVol pool. Adds a method on client_cmode to rename a NFS volume file using the file-rename-file zapi call. The revert steps are: 1. Create a clone volume file/LUN from snapshot. 2. Change the original volume file/LUN path to a temporary path. 3. Change clone file/LUN path to the original path. 4. Delete the volume file/LUN on temporary path. If any step fails, the original volume file/LUN is preserved and the clone file/LUN is deleted. For NFS, ONTAP clone file is not supported on FlexGroup pool, in this case the generic implementation will perform the revert to snapshot. Implements: blueprint ontap-revert-to-snapshot Co-Authored-By: Fabio Oliveira <fabioaurelio1269@gmail.com> Change-Id: I347f0ee63d8d13ff181dd41a542a006b7c10b488
This commit is contained in:
parent
e1728bb66a
commit
1f96b386b3
@ -4346,3 +4346,18 @@ class NetAppCmodeClientTestCase(test.TestCase):
|
||||
mock_send_request.assert_called_once_with('lun-copy-cancel',
|
||||
api_args,
|
||||
enable_tunneling=False)
|
||||
|
||||
def test_rename_file(self):
|
||||
self.mock_object(self.client.connection, 'send_request')
|
||||
|
||||
orig_file_name = '/vol/fake_vol/volume-%s' % self.fake_volume
|
||||
new_file_name = '/vol/fake_vol/new-volume-%s' % self.fake_volume
|
||||
|
||||
self.client.rename_file(orig_file_name, new_file_name)
|
||||
|
||||
api_args = {
|
||||
'from-path': orig_file_name,
|
||||
'to-path': new_file_name,
|
||||
}
|
||||
self.client.connection.send_request.assert_called_once_with(
|
||||
'file-rename-file', api_args)
|
||||
|
@ -348,6 +348,26 @@ SNAPSHOT = {
|
||||
'id': 'fake_id'
|
||||
}
|
||||
|
||||
SNAPSHOT_VOLUME = {
|
||||
'id': VOLUME_ID,
|
||||
'name': VOLUME_NAME
|
||||
}
|
||||
|
||||
LUN_WITH_METADATA = {
|
||||
'handle': 'vserver_fake:/vol/fake_flexvol/volume-fake-uuid',
|
||||
'name': 'volume-fake-uuid',
|
||||
'size': 20971520,
|
||||
'metadata': {
|
||||
'Vserver': 'vserver_fake',
|
||||
'Volume': 'fake_flexvol',
|
||||
'Qtree': None,
|
||||
'Path': '/vol/fake_flexvol/volume-fake-uuid',
|
||||
'OsType': 'linux',
|
||||
'SpaceReserved': 'false',
|
||||
'UUID': 'fake-uuid'
|
||||
}
|
||||
}
|
||||
|
||||
VOLUME_REF = {'name': 'fake_vref_name', 'size': 42}
|
||||
|
||||
FAKE_CMODE_VOLUMES = ['open123', 'mixed', 'open321']
|
||||
|
@ -19,6 +19,7 @@
|
||||
from unittest import mock
|
||||
|
||||
import ddt
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder.objects import fields
|
||||
@ -1432,3 +1433,247 @@ class NetAppBlockStorageCmodeLibraryTestCase(test.TestCase):
|
||||
fake_vol, fake.DEST_HOST_STRING, fake.BACKEND_NAME,
|
||||
fake.VSERVER_NAME)
|
||||
self.assertEqual({}, result)
|
||||
|
||||
def test_revert_to_snapshot(self):
|
||||
mock__revert_to_snapshot = self.mock_object(self.library,
|
||||
'_revert_to_snapshot')
|
||||
|
||||
self.library.revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
|
||||
mock__revert_to_snapshot.assert_called_once_with(fake.SNAPSHOT_VOLUME,
|
||||
fake.SNAPSHOT)
|
||||
|
||||
self.library.revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
|
||||
def test_revert_to_snapshot_revert_failed(self):
|
||||
self.mock_object(self.library, '_revert_to_snapshot',
|
||||
side_effect=Exception)
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.library.revert_to_snapshot,
|
||||
fake.SNAPSHOT_VOLUME,
|
||||
fake.SNAPSHOT)
|
||||
|
||||
def test__revert_to_snapshot(self):
|
||||
lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'],
|
||||
fake.LUN_WITH_METADATA['name'],
|
||||
fake.LUN_WITH_METADATA['size'],
|
||||
fake.LUN_WITH_METADATA['metadata'])
|
||||
lun_name = lun_obj.name
|
||||
new_lun_name = 'new-%s' % fake.SNAPSHOT['name']
|
||||
flexvol_name = lun_obj.metadata['Volume']
|
||||
|
||||
mock__clone_snapshot = self.mock_object(
|
||||
self.library, '_clone_snapshot', return_value=new_lun_name)
|
||||
mock__get_lun_from_table = self.mock_object(
|
||||
self.library, '_get_lun_from_table', return_value=lun_obj)
|
||||
mock__swap_luns = self.mock_object(self.library, '_swap_luns')
|
||||
mock_destroy_lun = self.mock_object(self.library.zapi_client,
|
||||
'destroy_lun')
|
||||
|
||||
self.library._revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
|
||||
mock__clone_snapshot.assert_called_once_with(fake.SNAPSHOT['name'])
|
||||
mock__get_lun_from_table.assert_called_once_with(
|
||||
fake.SNAPSHOT_VOLUME['name'])
|
||||
mock__swap_luns.assert_called_once_with(lun_name, new_lun_name,
|
||||
flexvol_name)
|
||||
mock_destroy_lun.assert_not_called()
|
||||
|
||||
@ddt.data(False, True)
|
||||
def test__revert_to_snapshot_swap_exception(self, delete_lun_exception):
|
||||
lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'],
|
||||
fake.LUN_WITH_METADATA['name'],
|
||||
fake.LUN_WITH_METADATA['size'],
|
||||
fake.LUN_WITH_METADATA['metadata'])
|
||||
new_lun_name = 'new-%s' % fake.SNAPSHOT['name']
|
||||
flexvol_name = lun_obj.metadata['Volume']
|
||||
new_lun_path = '/vol/%s/%s' % (flexvol_name, new_lun_name)
|
||||
side_effect = Exception if delete_lun_exception else lambda: True
|
||||
|
||||
self.mock_object(
|
||||
self.library, '_clone_snapshot', return_value=new_lun_name)
|
||||
self.mock_object(
|
||||
self.library, '_get_lun_from_table', return_value=lun_obj)
|
||||
swap_exception = exception.VolumeBackendAPIException(data="data")
|
||||
self.mock_object(self.library, '_swap_luns',
|
||||
side_effect=swap_exception)
|
||||
mock_destroy_lun = self.mock_object(self.library.zapi_client,
|
||||
'destroy_lun',
|
||||
side_effect=side_effect)
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.library._revert_to_snapshot,
|
||||
fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
|
||||
mock_destroy_lun.assert_called_once_with(new_lun_path)
|
||||
|
||||
def test__clone_snapshot(self):
|
||||
lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'],
|
||||
fake.LUN_WITH_METADATA['name'],
|
||||
fake.LUN_WITH_METADATA['size'],
|
||||
fake.LUN_WITH_METADATA['metadata'])
|
||||
new_snap_name = 'new-%s' % fake.SNAPSHOT['name']
|
||||
snapshot_path = lun_obj.metadata['Path']
|
||||
flexvol_name = lun_obj.metadata['Volume']
|
||||
block_count = 40960
|
||||
|
||||
mock__get_lun_from_table = self.mock_object(
|
||||
self.library, '_get_lun_from_table', return_value=lun_obj)
|
||||
mock__get_lun_block_count = self.mock_object(
|
||||
self.library, '_get_lun_block_count', return_value=block_count)
|
||||
mock_create_lun = self.mock_object(self.library.zapi_client,
|
||||
'create_lun')
|
||||
mock__clone_lun = self.mock_object(self.library, '_clone_lun')
|
||||
|
||||
self.library._clone_snapshot(fake.SNAPSHOT['name'])
|
||||
|
||||
mock__get_lun_from_table.assert_called_once_with(fake.SNAPSHOT['name'])
|
||||
mock__get_lun_block_count.assert_called_once_with(snapshot_path)
|
||||
mock_create_lun.assert_called_once_with(flexvol_name, new_snap_name,
|
||||
six.text_type(lun_obj.size),
|
||||
lun_obj.metadata)
|
||||
mock__clone_lun.assert_called_once_with(fake.SNAPSHOT['name'],
|
||||
new_snap_name,
|
||||
block_count=block_count)
|
||||
|
||||
def test__clone_snapshot_invalid_block_count(self):
|
||||
lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'],
|
||||
fake.LUN_WITH_METADATA['name'],
|
||||
fake.LUN_WITH_METADATA['size'],
|
||||
fake.LUN_WITH_METADATA['metadata'])
|
||||
|
||||
self.mock_object(self.library, '_get_lun_from_table',
|
||||
return_value=lun_obj)
|
||||
self.mock_object(self.library, '_get_lun_block_count',
|
||||
return_value=0)
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.library._clone_snapshot,
|
||||
fake.SNAPSHOT['name'])
|
||||
|
||||
def test__clone_snapshot_clone_exception(self):
|
||||
lun_obj = block_base.NetAppLun(fake.LUN_WITH_METADATA['handle'],
|
||||
fake.LUN_WITH_METADATA['name'],
|
||||
fake.LUN_WITH_METADATA['size'],
|
||||
fake.LUN_WITH_METADATA['metadata'])
|
||||
new_snap_name = 'new-%s' % fake.SNAPSHOT['name']
|
||||
snapshot_path = lun_obj.metadata['Path']
|
||||
flexvol_name = lun_obj.metadata['Volume']
|
||||
new_lun_path = '/vol/%s/%s' % (flexvol_name, new_snap_name)
|
||||
block_count = 40960
|
||||
|
||||
mock__get_lun_from_table = self.mock_object(
|
||||
self.library, '_get_lun_from_table', return_value=lun_obj)
|
||||
mock__get_lun_block_count = self.mock_object(
|
||||
self.library, '_get_lun_block_count', return_value=block_count)
|
||||
mock_create_lun = self.mock_object(self.library.zapi_client,
|
||||
'create_lun')
|
||||
side_effect = exception.VolumeBackendAPIException(data='data')
|
||||
mock__clone_lun = self.mock_object(self.library, '_clone_lun',
|
||||
side_effect=side_effect)
|
||||
mock_destroy_lun = self.mock_object(self.library.zapi_client,
|
||||
'destroy_lun')
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.library._clone_snapshot,
|
||||
fake.SNAPSHOT['name'])
|
||||
|
||||
mock__get_lun_from_table.assert_called_once_with(fake.SNAPSHOT['name'])
|
||||
mock__get_lun_block_count.assert_called_once_with(snapshot_path)
|
||||
mock_create_lun.assert_called_once_with(flexvol_name, new_snap_name,
|
||||
six.text_type(lun_obj.size),
|
||||
lun_obj.metadata)
|
||||
mock__clone_lun.assert_called_once_with(fake.SNAPSHOT['name'],
|
||||
new_snap_name,
|
||||
block_count=block_count)
|
||||
mock_destroy_lun.assert_called_once_with(new_lun_path)
|
||||
|
||||
def test__swap_luns(self):
|
||||
original_lun = fake.LUN_WITH_METADATA['name']
|
||||
new_lun = 'new-%s' % fake.SNAPSHOT['name']
|
||||
flexvol = fake.LUN_WITH_METADATA['metadata']['Volume']
|
||||
|
||||
tmp_lun = 'tmp-%s' % original_lun
|
||||
|
||||
path = "/vol/%s/%s" % (flexvol, original_lun) # original path
|
||||
tmp_path = "/vol/%s/%s" % (flexvol, tmp_lun)
|
||||
new_path = "/vol/%s/%s" % (flexvol, new_lun)
|
||||
|
||||
mock_move_lun = self.mock_object(
|
||||
self.library.zapi_client, 'move_lun', return_value=True)
|
||||
mock_destroy_lun = self.mock_object(
|
||||
self.library.zapi_client, 'destroy_lun', return_value=True)
|
||||
|
||||
self.library._swap_luns(original_lun, new_lun, flexvol)
|
||||
|
||||
mock_move_lun.assert_has_calls([
|
||||
mock.call(path, tmp_path),
|
||||
mock.call(new_path, path)
|
||||
])
|
||||
mock_destroy_lun.assert_called_once_with(tmp_path)
|
||||
|
||||
@ddt.data((True, False), (False, False), (False, True))
|
||||
@ddt.unpack
|
||||
def test__swap_luns_move_exception(self, first_move_exception,
|
||||
move_back_exception):
|
||||
original_lun = fake.LUN_WITH_METADATA['name']
|
||||
new_lun = 'new-%s' % fake.SNAPSHOT['name']
|
||||
flexvol = fake.LUN_WITH_METADATA['metadata']['Volume']
|
||||
side_effect = Exception
|
||||
|
||||
def _side_effect_skip():
|
||||
return True
|
||||
|
||||
if not first_move_exception and not move_back_exception:
|
||||
side_effect = [_side_effect_skip, Exception, _side_effect_skip]
|
||||
elif not first_move_exception:
|
||||
side_effect = [_side_effect_skip, Exception, Exception]
|
||||
|
||||
tmp_lun = 'tmp-%s' % original_lun
|
||||
|
||||
path = "/vol/%s/%s" % (flexvol, original_lun) # original path
|
||||
tmp_path = "/vol/%s/%s" % (flexvol, tmp_lun)
|
||||
new_path = "/vol/%s/%s" % (flexvol, new_lun)
|
||||
|
||||
mock_move_lun = self.mock_object(self.library.zapi_client,
|
||||
'move_lun',
|
||||
side_effect=side_effect)
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.library._swap_luns,
|
||||
original_lun,
|
||||
new_lun,
|
||||
flexvol)
|
||||
|
||||
if first_move_exception:
|
||||
mock_move_lun.assert_called_once_with(path, tmp_path)
|
||||
else:
|
||||
mock_move_lun.assert_has_calls([
|
||||
mock.call(path, tmp_path),
|
||||
mock.call(new_path, path),
|
||||
mock.call(tmp_path, path)
|
||||
])
|
||||
|
||||
def test__swap_luns_destroy_exception(self):
|
||||
original_lun = fake.LUN_WITH_METADATA['name']
|
||||
new_lun = 'new-%s' % fake.SNAPSHOT['name']
|
||||
flexvol = fake.LUN_WITH_METADATA['metadata']['Volume']
|
||||
|
||||
tmp_lun = 'tmp-%s' % original_lun
|
||||
|
||||
path = "/vol/%s/%s" % (flexvol, original_lun)
|
||||
tmp_path = "/vol/%s/%s" % (flexvol, tmp_lun)
|
||||
new_path = "/vol/%s/%s" % (flexvol, new_lun)
|
||||
|
||||
mock_move_lun = self.mock_object(
|
||||
self.library.zapi_client, 'move_lun', return_value=True)
|
||||
mock_destroy_lun = self.mock_object(
|
||||
self.library.zapi_client, 'destroy_lun', side_effect=Exception)
|
||||
|
||||
self.library._swap_luns(original_lun, new_lun, flexvol)
|
||||
|
||||
mock_move_lun.assert_has_calls([
|
||||
mock.call(path, tmp_path),
|
||||
mock.call(new_path, path)
|
||||
])
|
||||
mock_destroy_lun.assert_called_once_with(tmp_path)
|
||||
|
@ -0,0 +1,51 @@
|
||||
# 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
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Mock unit tests for NetApp Data ONTAP FibreChannel storage systems."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from cinder import context
|
||||
from cinder.tests.unit import test
|
||||
from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake
|
||||
import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes
|
||||
from cinder.volume.drivers.netapp.dataontap import fc_cmode
|
||||
|
||||
|
||||
class NetAppCmodeFibreChannelDriverTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppCmodeFibreChannelDriverTestCase, self).setUp()
|
||||
|
||||
kwargs = {
|
||||
'configuration': self.get_config_base(),
|
||||
'host': 'openstack@netappblock',
|
||||
}
|
||||
self.library = fc_cmode.NetAppCmodeFibreChannelDriver(**kwargs)
|
||||
self.library.zapi_client = mock.Mock()
|
||||
self.zapi_client = self.library.zapi_client
|
||||
self.mock_request = mock.Mock()
|
||||
self.ctxt = context.RequestContext('fake', 'fake', auth_token=True)
|
||||
|
||||
def get_config_base(self):
|
||||
return na_fakes.create_configuration()
|
||||
|
||||
def test_revert_to_snapshot(self):
|
||||
mock_revert_to_snapshot = self.mock_object(self.library.library,
|
||||
'revert_to_snapshot')
|
||||
|
||||
self.library.revert_to_snapshot(self.ctxt, fake.SNAPSHOT_VOLUME,
|
||||
fake.SNAPSHOT)
|
||||
|
||||
mock_revert_to_snapshot.assert_called_once_with(fake.SNAPSHOT_VOLUME,
|
||||
fake.SNAPSHOT)
|
@ -0,0 +1,52 @@
|
||||
# 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
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Mock unit tests for NetApp Data ONTAP (C-mode) iSCSI storage systems."""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
|
||||
from cinder import context
|
||||
from cinder.tests.unit import test
|
||||
from cinder.tests.unit.volume.drivers.netapp.dataontap import fakes as fake
|
||||
import cinder.tests.unit.volume.drivers.netapp.fakes as na_fakes
|
||||
from cinder.volume.drivers.netapp.dataontap import iscsi_cmode
|
||||
|
||||
|
||||
class NetAppCmodeFibreChannelDriverTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(NetAppCmodeFibreChannelDriverTestCase, self).setUp()
|
||||
|
||||
kwargs = {
|
||||
'configuration': self.get_config_base(),
|
||||
'host': 'openstack@netappblock',
|
||||
}
|
||||
self.library = iscsi_cmode.NetAppCmodeISCSIDriver(**kwargs)
|
||||
self.library.zapi_client = mock.Mock()
|
||||
self.zapi_client = self.library.zapi_client
|
||||
self.mock_request = mock.Mock()
|
||||
self.ctxt = context.RequestContext('fake', 'fake', auth_token=True)
|
||||
|
||||
def get_config_base(self):
|
||||
return na_fakes.create_configuration()
|
||||
|
||||
def test_revert_to_snapshot(self):
|
||||
mock_revert_to_snapshot = self.mock_object(self.library.library,
|
||||
'revert_to_snapshot')
|
||||
|
||||
self.library.revert_to_snapshot(self.ctxt, fake.SNAPSHOT_VOLUME,
|
||||
fake.SNAPSHOT)
|
||||
|
||||
mock_revert_to_snapshot.assert_called_once_with(fake.SNAPSHOT_VOLUME,
|
||||
fake.SNAPSHOT)
|
@ -1167,3 +1167,38 @@ class NetAppNfsDriverTestCase(test.TestCase):
|
||||
self.driver.update_migrated_volume, self.ctxt,
|
||||
fake.test_volume, mock.sentinel.new_volume,
|
||||
mock.sentinel.original_status)
|
||||
|
||||
@ddt.data({'is_flexgroup': False, 'is_flexgroup_supported': False},
|
||||
{'is_flexgroup': False, 'is_flexgroup_supported': True},
|
||||
{'is_flexgroup': True, 'is_flexgroup_supported': False},
|
||||
{'is_flexgroup': True, 'is_flexgroup_supported': True})
|
||||
@ddt.unpack
|
||||
def test_revert_to_snapshot(self, is_flexgroup, is_flexgroup_supported):
|
||||
context = {}
|
||||
|
||||
self.mock_object(self.driver,
|
||||
'_is_flexgroup',
|
||||
return_value=is_flexgroup)
|
||||
self.mock_object(self.driver,
|
||||
'_is_flexgroup_clone_file_supported',
|
||||
return_value=is_flexgroup_supported)
|
||||
mock_revert_to_snapshot = self.mock_object(
|
||||
remotefs.RemoteFSSnapDriverDistributed, 'revert_to_snapshot')
|
||||
mock__revert_to_snapshot = self.mock_object(self.driver,
|
||||
'_revert_to_snapshot')
|
||||
|
||||
self.driver.revert_to_snapshot(context, fake.SNAPSHOT_VOLUME,
|
||||
fake.SNAPSHOT)
|
||||
|
||||
if is_flexgroup and not is_flexgroup_supported:
|
||||
mock_revert_to_snapshot.assert_called_once_with(
|
||||
context, fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
mock__revert_to_snapshot.assert_not_called()
|
||||
else:
|
||||
mock__revert_to_snapshot.assert_called_once_with(
|
||||
fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
mock_revert_to_snapshot.assert_not_called()
|
||||
|
||||
def test__revert_to_snapshot(self):
|
||||
self.assertRaises(NotImplementedError, self.driver._revert_to_snapshot,
|
||||
fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
|
@ -2200,3 +2200,131 @@ class NetAppCmodeNfsDriverTestCase(test.TestCase):
|
||||
mock_migrate_volume_ontap_assisted.assert_not_called()
|
||||
self.assertFalse(migrated)
|
||||
self.assertEqual({}, updates)
|
||||
|
||||
def test__revert_to_snapshot(self):
|
||||
mock_clone_backing_file_for_volume = self.mock_object(
|
||||
self.driver, '_clone_backing_file_for_volume')
|
||||
mock_get_export_ip_path = self.mock_object(
|
||||
self.driver, '_get_export_ip_path',
|
||||
return_value=(fake.SHARE_IP, fake.EXPORT_PATH))
|
||||
mock_get_vserver_for_ip = self.mock_object(
|
||||
self.driver, '_get_vserver_for_ip', return_value=fake.VSERVER_NAME)
|
||||
mock_get_vol_by_junc_vserver = self.mock_object(
|
||||
self.driver.zapi_client, 'get_vol_by_junc_vserver',
|
||||
return_value=fake.FLEXVOL)
|
||||
mock_swap_files = self.mock_object(self.driver, '_swap_files')
|
||||
mock_delete_file = self.mock_object(self.driver.zapi_client,
|
||||
'delete_file')
|
||||
|
||||
self.driver._revert_to_snapshot(fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
|
||||
mock_clone_backing_file_for_volume.assert_called_once_with(
|
||||
fake.SNAPSHOT['name'],
|
||||
'new-%s' % fake.SNAPSHOT['name'],
|
||||
fake.SNAPSHOT_VOLUME['id'],
|
||||
is_snapshot=False)
|
||||
mock_get_export_ip_path.assert_called_once_with(
|
||||
volume_id=fake.SNAPSHOT_VOLUME['id'])
|
||||
mock_get_vserver_for_ip.assert_called_once_with(fake.SHARE_IP)
|
||||
mock_get_vol_by_junc_vserver.assert_called_once_with(
|
||||
fake.VSERVER_NAME, fake.EXPORT_PATH)
|
||||
mock_swap_files.assert_called_once_with(
|
||||
fake.FLEXVOL, fake.SNAPSHOT_VOLUME['name'],
|
||||
'new-%s' % fake.SNAPSHOT['name'])
|
||||
mock_delete_file.assert_not_called()
|
||||
|
||||
@ddt.data(False, True)
|
||||
def test__revert_to_snapshot_swap_exception(self, delete_exception):
|
||||
new_snap_name = 'new-%s' % fake.SNAPSHOT['name']
|
||||
new_file_path = '/vol/%s/%s' % (fake.FLEXVOL, new_snap_name)
|
||||
|
||||
self.mock_object(self.driver, '_clone_backing_file_for_volume')
|
||||
self.mock_object(self.driver, '_get_export_ip_path',
|
||||
return_value=(fake.SHARE_IP, fake.EXPORT_PATH))
|
||||
self.mock_object(self.driver, '_get_vserver_for_ip',
|
||||
return_value=fake.VSERVER_NAME)
|
||||
self.mock_object(self.driver.zapi_client, 'get_vol_by_junc_vserver',
|
||||
return_value=fake.FLEXVOL)
|
||||
swap_exception = exception.VolumeBackendAPIException(data="data")
|
||||
self.mock_object(self.driver, '_swap_files',
|
||||
side_effect=swap_exception)
|
||||
side_effect = Exception if delete_exception else lambda: True
|
||||
mock_delete_file = self.mock_object(self.driver.zapi_client,
|
||||
'delete_file',
|
||||
side_effect=side_effect)
|
||||
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver._revert_to_snapshot,
|
||||
fake.SNAPSHOT_VOLUME, fake.SNAPSHOT)
|
||||
|
||||
mock_delete_file.assert_called_once_with(new_file_path)
|
||||
|
||||
def test__swap_files(self):
|
||||
new_file = 'new-%s' % fake.SNAPSHOT['name']
|
||||
new_file_path = '/vol/%s/%s' % (fake.FLEXVOL, new_file)
|
||||
original_file_path = '/vol/%s/%s' % (fake.FLEXVOL, fake.VOLUME_NAME)
|
||||
tmp_file_path = '/vol/%s/tmp-%s' % (fake.FLEXVOL, fake.VOLUME_NAME)
|
||||
|
||||
mock_rename_file = self.mock_object(
|
||||
self.driver.zapi_client, 'rename_file')
|
||||
mock_delete_file = self.mock_object(
|
||||
self.driver.zapi_client, 'delete_file')
|
||||
|
||||
self.driver._swap_files(fake.FLEXVOL, fake.VOLUME_NAME, new_file)
|
||||
|
||||
mock_rename_file.assert_has_calls([
|
||||
mock.call(original_file_path, tmp_file_path),
|
||||
mock.call(new_file_path, original_file_path)])
|
||||
|
||||
mock_delete_file.assert_called_once_with(tmp_file_path)
|
||||
|
||||
@ddt.data((True, False), (False, False), (False, True))
|
||||
@ddt.unpack
|
||||
def test__swap_files_rename_exception(self, first_exception,
|
||||
rollback_exception):
|
||||
new_file = 'new-%s' % fake.SNAPSHOT['name']
|
||||
new_file_path = '/vol/%s/%s' % (fake.FLEXVOL, new_file)
|
||||
original_file_path = '/vol/%s/%s' % (fake.FLEXVOL, fake.VOLUME_NAME)
|
||||
tmp_file_path = '/vol/%s/tmp-%s' % (fake.FLEXVOL, fake.VOLUME_NAME)
|
||||
side_effect = None
|
||||
|
||||
def _skip_side_effect():
|
||||
return True
|
||||
|
||||
if not first_exception and not rollback_exception:
|
||||
side_effect = [_skip_side_effect,
|
||||
exception.VolumeBackendAPIException(data="data"),
|
||||
_skip_side_effect]
|
||||
elif not first_exception and rollback_exception:
|
||||
side_effect = [_skip_side_effect,
|
||||
exception.VolumeBackendAPIException(data="data"),
|
||||
exception.VolumeBackendAPIException(data="data")]
|
||||
else:
|
||||
side_effect = exception.VolumeBackendAPIException(data="data")
|
||||
|
||||
mock_rename_file = self.mock_object(self.driver.zapi_client,
|
||||
'rename_file',
|
||||
side_effect=side_effect)
|
||||
|
||||
self.assertRaises(
|
||||
na_utils.NetAppDriverException,
|
||||
self.driver._swap_files, fake.FLEXVOL, fake.VOLUME_NAME, new_file)
|
||||
|
||||
if not first_exception:
|
||||
mock_rename_file.assert_has_calls([
|
||||
mock.call(original_file_path, tmp_file_path),
|
||||
mock.call(new_file_path, original_file_path),
|
||||
mock.call(tmp_file_path, original_file_path)])
|
||||
else:
|
||||
mock_rename_file.assert_called_once_with(original_file_path,
|
||||
tmp_file_path)
|
||||
|
||||
def test__swap_files_delete_exception(self):
|
||||
new_file = 'new-%s' % fake.SNAPSHOT['name']
|
||||
|
||||
self.mock_object(self.driver.zapi_client, 'rename_file')
|
||||
side_effect = exception.VolumeBackendAPIException(data="data")
|
||||
self.mock_object(self.driver.zapi_client, 'delete_file',
|
||||
side_effect=side_effect)
|
||||
|
||||
self.driver._swap_files(fake.FLEXVOL, fake.VOLUME_NAME, new_file)
|
||||
|
@ -60,6 +60,7 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
|
||||
2.0.0 - Add support for QoS minimums specs
|
||||
Add support for dynamic Adaptive QoS policy group creation
|
||||
3.0.0 - Add support for Intra-cluster Storage assisted volume migration
|
||||
Add support for revert to snapshot
|
||||
|
||||
"""
|
||||
|
||||
@ -809,3 +810,127 @@ class NetAppBlockStorageCmodeLibrary(block_base.NetAppBlockStorageLibrary,
|
||||
"""Migrate Cinder volume to the specified pool or vserver."""
|
||||
return self.migrate_volume_ontap_assisted(
|
||||
volume, host, self.backend_name, self.configuration.netapp_vserver)
|
||||
|
||||
def revert_to_snapshot(self, volume, snapshot):
|
||||
"""Driver entry point for reverting volume to snapshot."""
|
||||
try:
|
||||
self._revert_to_snapshot(volume, snapshot)
|
||||
except Exception:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
"Revert snapshot failed.")
|
||||
|
||||
def _revert_to_snapshot(self, volume, snapshot):
|
||||
"""Sets up all required resources for _swap_luns.
|
||||
|
||||
If _swap_luns fails, the cloned LUN is destroyed.
|
||||
"""
|
||||
new_lun_name = self._clone_snapshot(snapshot["name"])
|
||||
|
||||
LOG.debug("Cloned from snapshot: %s.", new_lun_name)
|
||||
|
||||
lun = self._get_lun_from_table(volume["name"])
|
||||
volume_path = lun.metadata["Path"]
|
||||
seg = volume_path.split("/")
|
||||
lun_name = seg[-1]
|
||||
flexvol_name = seg[2]
|
||||
|
||||
try:
|
||||
self._swap_luns(lun_name, new_lun_name, flexvol_name)
|
||||
except Exception:
|
||||
LOG.error("Swapping LUN from %s to %s failed.", lun_name,
|
||||
new_lun_name)
|
||||
with excutils.save_and_reraise_exception():
|
||||
try:
|
||||
LOG.debug("Deleting temporary reverted LUN %s.",
|
||||
new_lun_name)
|
||||
new_lun_path = "/vol/%s/%s" % (flexvol_name, new_lun_name)
|
||||
self.zapi_client.destroy_lun(new_lun_path)
|
||||
except Exception:
|
||||
LOG.error("Failure deleting temporary reverted LUN %s. "
|
||||
"A manual deletion is required.", new_lun_name)
|
||||
|
||||
def _clone_snapshot(self, snapshot_name):
|
||||
"""Returns the name of the LUN cloned from snapshot.
|
||||
|
||||
Creates a LUN with same metadata as original LUN and then clones
|
||||
from snapshot. If clone operation fails, the new LUN is deleted.
|
||||
"""
|
||||
snapshot_lun = self._get_lun_from_table(snapshot_name)
|
||||
snapshot_path = snapshot_lun.metadata["Path"]
|
||||
lun_name = snapshot_path.split("/")[-1]
|
||||
flexvol_name = snapshot_path.split("/")[2]
|
||||
|
||||
LOG.info("Cloning LUN %s from snapshot %s in volume %s.", lun_name,
|
||||
snapshot_name, flexvol_name)
|
||||
|
||||
metadata = snapshot_lun.metadata
|
||||
|
||||
block_count = self._get_lun_block_count(snapshot_path)
|
||||
if block_count == 0:
|
||||
msg = _("%s cannot be reverted using clone operation"
|
||||
" as it contains no blocks.")
|
||||
raise exception.VolumeBackendAPIException(data=msg % snapshot_name)
|
||||
|
||||
new_snap_name = "new-%s" % snapshot_name
|
||||
|
||||
self.zapi_client.create_lun(
|
||||
flexvol_name, new_snap_name,
|
||||
six.text_type(snapshot_lun.size), metadata)
|
||||
try:
|
||||
self._clone_lun(snapshot_name, new_snap_name,
|
||||
block_count=block_count)
|
||||
return new_snap_name
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
try:
|
||||
new_lun_path = "/vol/%s/%s" % (flexvol_name, new_snap_name)
|
||||
self.zapi_client.destroy_lun(new_lun_path)
|
||||
except Exception:
|
||||
LOG.error("Failure deleting temporary reverted LUN %s. "
|
||||
"A manual deletion is required.", new_snap_name)
|
||||
|
||||
def _swap_luns(self, original_lun, new_lun, flexvol_name):
|
||||
"""Swaps cloned and original LUNs using a temporary LUN.
|
||||
|
||||
Moves the original LUN to a temporary path, then moves the cloned LUN
|
||||
to the original path (if this fails, moves the temporary LUN back as
|
||||
original LUN) and finally destroys the LUN with temporary path.
|
||||
"""
|
||||
tmp_lun = "tmp-%s" % original_lun
|
||||
|
||||
original_path = "/vol/%s/%s" % (flexvol_name, original_lun)
|
||||
tmp_path = "/vol/%s/%s" % (flexvol_name, tmp_lun)
|
||||
new_path = "/vol/%s/%s" % (flexvol_name, new_lun)
|
||||
|
||||
LOG.debug("Original Path: %s.", original_path)
|
||||
LOG.debug("Temporary Path: %s.", tmp_path)
|
||||
LOG.debug("New Path %s.", new_path)
|
||||
|
||||
try:
|
||||
self.zapi_client.move_lun(original_path, tmp_path)
|
||||
except Exception:
|
||||
msg = _("Failure moving original LUN from %s to %s." %
|
||||
(original_path, tmp_path))
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
try:
|
||||
self.zapi_client.move_lun(new_path, original_path)
|
||||
except Exception:
|
||||
LOG.debug("Move temporary reverted LUN failed. Moving back "
|
||||
"original LUN to original path.")
|
||||
try:
|
||||
self.zapi_client.move_lun(tmp_path, original_path)
|
||||
except Exception:
|
||||
LOG.error("Could not move original LUN path from %s to %s. "
|
||||
"Cinder may lose the volume management. Please, you "
|
||||
"should move it back manually.",
|
||||
tmp_path, original_path)
|
||||
|
||||
msg = _("Failure moving temporary reverted LUN from %s to %s.")
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=msg % (new_path, original_path))
|
||||
try:
|
||||
self.zapi_client.destroy_lun(tmp_path)
|
||||
except Exception:
|
||||
LOG.error("Failure deleting old LUN %s. A manual deletion "
|
||||
"is required.", tmp_lun)
|
||||
|
@ -1696,6 +1696,17 @@ class Client(client_base.Client):
|
||||
}
|
||||
self.connection.send_request('volume-rename', api_args)
|
||||
|
||||
def rename_file(self, orig_file_name, new_file_name):
|
||||
"""Rename a volume file."""
|
||||
LOG.debug("Renaming the file %(original)s to %(new)s.",
|
||||
{'original': orig_file_name, 'new': new_file_name})
|
||||
|
||||
api_args = {
|
||||
'from-path': orig_file_name,
|
||||
'to-path': new_file_name,
|
||||
}
|
||||
self.connection.send_request('file-rename-file', api_args)
|
||||
|
||||
def mount_flexvol(self, flexvol_name, junction_path=None):
|
||||
"""Mounts a volume on a junction path."""
|
||||
api_args = {
|
||||
|
@ -35,6 +35,7 @@ class NetAppCmodeFibreChannelDriver(driver.BaseVD,
|
||||
1.0.0 - Driver development before Wallaby
|
||||
2.0.0 - Wallaby driver version bump
|
||||
3.0.0 - Add support for Intra-cluster Storage assisted volume migration
|
||||
Add support for revert to snapshot
|
||||
|
||||
"""
|
||||
|
||||
@ -155,3 +156,6 @@ class NetAppCmodeFibreChannelDriver(driver.BaseVD,
|
||||
|
||||
def migrate_volume(self, context, volume, host):
|
||||
return self.library.migrate_volume(context, volume, host)
|
||||
|
||||
def revert_to_snapshot(self, context, volume, snapshot):
|
||||
return self.library.revert_to_snapshot(volume, snapshot)
|
||||
|
@ -138,3 +138,6 @@ class NetAppCmodeISCSIDriver(driver.BaseVD,
|
||||
|
||||
def migrate_volume(self, context, volume, host):
|
||||
return self.library.migrate_volume(context, volume, host)
|
||||
|
||||
def revert_to_snapshot(self, context, volume, snapshot):
|
||||
return self.library.revert_to_snapshot(volume, snapshot)
|
||||
|
@ -392,6 +392,19 @@ class NetAppNfsDriver(driver.ManageableVD,
|
||||
|
||||
return nfs_server_ip + ':' + export_path
|
||||
|
||||
def revert_to_snapshot(self, context, volume, snapshot):
|
||||
"""Revert a volume to a given snapshot.
|
||||
|
||||
For a FlexGroup pool, the operation relies on the NFS generic driver
|
||||
because the ONTAP clone file is not supported by FlexGroup yet.
|
||||
"""
|
||||
if (self._is_flexgroup(vol_id=snapshot['volume_id']) and
|
||||
not self._is_flexgroup_clone_file_supported()):
|
||||
super(NetAppNfsDriver, self).revert_to_snapshot(context, volume,
|
||||
snapshot)
|
||||
else:
|
||||
self._revert_to_snapshot(volume, snapshot)
|
||||
|
||||
def _clone_backing_file_for_volume(self, volume_name, clone_name,
|
||||
volume_id, share=None,
|
||||
is_snapshot=False,
|
||||
@ -399,6 +412,9 @@ class NetAppNfsDriver(driver.ManageableVD,
|
||||
"""Clone backing file for Cinder volume."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _revert_to_snapshot(self, volume, snapshot):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _is_flexgroup(self, vol_id=None, host=None):
|
||||
"""Discover if a given volume is a FlexGroup or not"""
|
||||
raise NotImplementedError()
|
||||
|
@ -63,6 +63,7 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver,
|
||||
Add support for dynamic Adaptive QoS policy group creation
|
||||
Implement FlexGroup pool
|
||||
3.0.0 - Add support for Intra-cluster Storage assisted volume migration
|
||||
Add support for revert to snapshot
|
||||
|
||||
"""
|
||||
|
||||
@ -224,6 +225,80 @@ class NetAppCmodeNfsDriver(nfs_base.NetAppNfsDriver,
|
||||
qos_policy_group_is_adaptive,
|
||||
target_path)
|
||||
|
||||
def _revert_to_snapshot(self, volume, snapshot):
|
||||
"""Clone volume from snapshot to perform the file name swap."""
|
||||
new_snap_name = 'new-%s' % snapshot['name']
|
||||
self._clone_backing_file_for_volume(snapshot['name'],
|
||||
new_snap_name,
|
||||
snapshot['volume_id'],
|
||||
is_snapshot=False)
|
||||
|
||||
(host_ip, junction_path) = self._get_export_ip_path(
|
||||
volume_id=volume['id'])
|
||||
vserver = self._get_vserver_for_ip(host_ip)
|
||||
flexvol_name = self.zapi_client.get_vol_by_junc_vserver(vserver,
|
||||
junction_path)
|
||||
|
||||
try:
|
||||
self._swap_files(flexvol_name, volume['name'], new_snap_name)
|
||||
except Exception:
|
||||
LOG.error("Swapping temporary reverted volume name from %s to %s "
|
||||
"failed.", new_snap_name, volume['name'])
|
||||
with excutils.save_and_reraise_exception():
|
||||
try:
|
||||
LOG.debug("Deleting temporary reverted volume file %s.",
|
||||
new_snap_name)
|
||||
file_path = '/vol/%s/%s' % (flexvol_name, new_snap_name)
|
||||
self.zapi_client.delete_file(file_path)
|
||||
except Exception:
|
||||
LOG.error("Could not delete temporary reverted volume %s. "
|
||||
"A manual deletion is required.", new_snap_name)
|
||||
|
||||
def _swap_files(self, flexvol_name, original_file, new_file):
|
||||
"""Swaps cloned and original files using a temporary file.
|
||||
|
||||
Renames the original file path to a temporary path, then changes the
|
||||
cloned file path to the original path (if this fails, change the
|
||||
temporary file path back as original path) and finally deletes the
|
||||
file with temporary path.
|
||||
"""
|
||||
prefix_path_on_backend = '/vol/' + flexvol_name + '/'
|
||||
|
||||
new_file_path = prefix_path_on_backend + new_file
|
||||
original_file_path = prefix_path_on_backend + original_file
|
||||
tmp_file_path = prefix_path_on_backend + 'tmp-%s' % original_file
|
||||
|
||||
try:
|
||||
self.zapi_client.rename_file(original_file_path, tmp_file_path)
|
||||
except exception.VolumeBackendAPIException:
|
||||
msg = _("Could not rename original volume from %s to %s.")
|
||||
raise na_utils.NetAppDriverException(msg % (original_file_path,
|
||||
tmp_file_path))
|
||||
|
||||
try:
|
||||
self.zapi_client.rename_file(new_file_path, original_file_path)
|
||||
except exception.VolumeBackendAPIException:
|
||||
try:
|
||||
LOG.debug("Revert volume failed. Rolling back to its original"
|
||||
" name.")
|
||||
self.zapi_client.rename_file(tmp_file_path, original_file_path)
|
||||
except exception.VolumeBackendAPIException:
|
||||
LOG.error("Could not rollback original volume name from %s "
|
||||
"to %s. Cinder may lose the volume management. "
|
||||
"Please, you should rename it back manually.",
|
||||
tmp_file_path, original_file_path)
|
||||
|
||||
msg = _("Could not rename temporary reverted volume from %s "
|
||||
"to original volume name %s.")
|
||||
raise na_utils.NetAppDriverException(msg % (new_file_path,
|
||||
original_file_path))
|
||||
|
||||
try:
|
||||
self.zapi_client.delete_file(tmp_file_path)
|
||||
except exception.VolumeBackendAPIException:
|
||||
LOG.error("Could not delete old volume %s. A manual deletion is "
|
||||
"required.", tmp_file_path)
|
||||
|
||||
def _clone_backing_file_for_volume(self, volume_name, clone_name,
|
||||
volume_id, share=None,
|
||||
is_snapshot=False,
|
||||
|
@ -903,7 +903,7 @@ driver.linbit_linstor=missing
|
||||
driver.lvm=complete
|
||||
driver.macrosan=missing
|
||||
driver.nec=complete
|
||||
driver.netapp_ontap=missing
|
||||
driver.netapp_ontap=complete
|
||||
driver.netapp_solidfire=complete
|
||||
driver.nexenta=missing
|
||||
driver.nfs=missing
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
NetApp ONTAP driver: Added support to Revert to Snapshot for the iSCSI, FC
|
||||
and NFS drivers with FlexVol pool. This feature does not support FlexGroups
|
||||
and is limited to revert only to the most recent snapshot of a given
|
||||
Cinder volume.
|
Loading…
Reference in New Issue
Block a user