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:
Alyson Rosa 2021-06-07 18:01:50 +00:00 committed by Fabio Oliveira
parent e1728bb66a
commit 1f96b386b3
15 changed files with 788 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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