Merge "Fix Infinidat driver to backup attached volume"

This commit is contained in:
Zuul 2023-01-31 20:30:53 +00:00 committed by Gerrit Code Review
commit 625a82a242
3 changed files with 242 additions and 118 deletions

View File

@ -14,11 +14,14 @@
# under the License.
"""Unit tests for INFINIDAT InfiniBox volume driver."""
import collections
import copy
import functools
import itertools
import platform
import socket
from unittest import mock
import uuid
import ddt
from oslo_utils import units
@ -215,6 +218,14 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self._mock_host.set_metadata_from_dict.assert_called_once_with(
self._generate_mock_host_metadata())
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_get_oslo_driver_opts')
def test_get_driver_options(self, _get_oslo_driver_opts):
_get_oslo_driver_opts.return_value = []
result = self.driver.get_driver_options()
actual = (infinidat.infinidat_opts)
self.assertEqual(actual, result)
@skip_driver_setup
def test__setup_and_get_system_object(self):
# This test should skip the driver setup, as it generates more calls to
@ -230,6 +241,12 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
infinidat._INFINIDAT_CINDER_IDENTIFIER)
self._system.login.assert_called_once()
@skip_driver_setup
@mock.patch('cinder.volume.drivers.infinidat.infinisdk', None)
def test_do_setup_no_infinisdk(self):
self.assertRaises(exception.VolumeDriverException,
self.driver.do_setup, None)
@mock.patch('cinder.volume.drivers.infinidat.infinisdk.InfiniBox')
@ddt.data(True, False)
def test_ssl_options(self, use_ssl, infinibox):
@ -240,9 +257,31 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
infinibox.assert_called_once_with(self.configuration.san_ip,
auth=auth, use_ssl=use_ssl)
def test_initialize_connection(self):
def test_create_export_snapshot(self):
self.assertIsNone(self.driver.create_export_snapshot(
None, test_snapshot, test_connector))
def test_remove_export_snapshot(self):
self.assertIsNone(self.driver.remove_export_snapshot(
None, test_snapshot))
def test_backup_use_temp_snapshot(self):
self.assertTrue(self.driver.backup_use_temp_snapshot())
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_get_infinidat_snapshot')
def test_initialize_connection_snapshot(self, get_snapshot):
result = self.driver.initialize_connection_snapshot(
test_snapshot, test_connector)
get_snapshot.assert_called_once_with(test_snapshot)
self.assertEqual(1, result["data"]["target_lun"])
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_get_infinidat_volume')
def test_initialize_connection(self, get_volume):
self._system.hosts.safe_get.return_value = None
result = self.driver.initialize_connection(test_volume, test_connector)
get_volume.assert_called_once_with(test_volume)
self.assertEqual(1, result["data"]["target_lun"])
def test_initialize_connection_host_exists(self):
@ -257,6 +296,13 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
result = self.driver.initialize_connection(test_volume, test_connector)
self.assertEqual(888, result["data"]["target_lun"])
def test_initialize_connection_mapping_not_found(self):
mock_mapping = mock.Mock()
mock_mapping.get_volume.return_value = None
self._mock_host.get_luns.return_value = [mock_mapping]
result = self.driver.initialize_connection(test_volume, test_connector)
self.assertEqual(TEST_LUN, result["data"]["target_lun"])
def test_initialize_connection_volume_doesnt_exist(self):
self._system.volumes.safe_get.return_value = None
self.assertRaises(exception.VolumeNotFound,
@ -300,11 +346,37 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.assertFalse(self.driver._is_volume_multiattached(volume,
connector))
def test_terminate_connection(self):
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_terminate_connection')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_get_infinidat_volume')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_is_volume_multiattached')
def test_terminate_connection(self, volume_multiattached, get_volume,
terminate_connection):
volume = copy.deepcopy(test_volume)
volume.volume_attachment = [test_attachment1]
volume_multiattached.return_value = False
get_volume.return_value = self._mock_volume
self.assertFalse(self.driver.terminate_connection(volume,
test_connector))
volume_multiattached.assert_called_once_with(volume, test_connector)
get_volume.assert_called_once_with(volume)
terminate_connection.assert_called_once_with(self._mock_volume,
test_connector)
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_terminate_connection')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_get_infinidat_snapshot')
def test_terminate_connection_snapshot(self, get_snapshot,
terminate_connection):
get_snapshot.return_value = self._mock_snapshot
self.assertIsNone(self.driver.terminate_connection_snapshot(
test_snapshot, test_connector))
get_snapshot.assert_called_once_with(test_snapshot)
terminate_connection.assert_called_once_with(self._mock_snapshot,
test_connector)
def test_terminate_connection_delete_host(self):
self._mock_host.get_luns.return_value = [object()]
@ -470,7 +542,7 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
test_clone, test_snapshot)
def test_create_volume_from_snapshot_create_fails(self):
self._mock_volume.create_snapshot.side_effect = self._raise_infinisdk
self._system.volumes.create.side_effect = self._raise_infinisdk
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.create_volume_from_snapshot,
test_clone, test_snapshot)
@ -484,13 +556,18 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.driver.create_volume_from_snapshot,
test_clone, test_snapshot)
@mock.patch("cinder.volume.volume_utils.copy_volume")
@mock.patch("cinder.volume.volume_utils.brick_get_connector")
@mock.patch("cinder.volume.volume_utils.brick_get_connector_properties",
return_value=test_connector)
def test_create_volume_from_snapshot_delete_clone_fails(self, *mocks):
self._mock_volume.delete.side_effect = self._raise_infinisdk
self.assertRaises(exception.VolumeBackendAPIException,
@mock.patch('cinder.volume.volume_utils.brick_get_connector')
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
@mock.patch('cinder.volume.volume_utils.brick_get_connector_properties')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_connect_device')
def test_create_volume_from_snapshot_connect_fails(self, connect_device,
connector_properties,
*mocks):
connector_properties.return_value = test_connector
connect_device.side_effect = exception.DeviceUnavailable(
path='/dev/sdb', reason='Block device required')
self.assertRaises(exception.DeviceUnavailable,
self.driver.create_volume_from_snapshot,
test_clone, test_snapshot)
@ -507,21 +584,28 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.delete_snapshot, test_snapshot)
@mock.patch("cinder.volume.volume_utils.copy_volume")
@mock.patch("cinder.volume.volume_utils.brick_get_connector")
@mock.patch("cinder.volume.volume_utils.brick_get_connector_properties",
return_value=test_connector)
@mock.patch("cinder.volume.volume_types.get_volume_type_qos_specs")
def test_create_cloned_volume(self, *mocks):
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'delete_snapshot')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'create_volume_from_snapshot')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'create_snapshot')
@mock.patch('uuid.uuid4')
def test_create_cloned_volume(self, mock_uuid, create_snapshot,
create_volume_from_snapshot,
delete_snapshot):
mock_uuid.return_value = uuid.UUID(test_snapshot.id)
snapshot_attributes = ('id', 'name', 'volume')
Snapshot = collections.namedtuple('Snapshot', snapshot_attributes)
snapshot_id = test_snapshot.id
snapshot_name = self.configuration.snapshot_name_template % snapshot_id
snapshot = Snapshot(id=snapshot_id, name=snapshot_name,
volume=test_volume)
self.driver.create_cloned_volume(test_clone, test_volume)
def test_create_cloned_volume_volume_already_mapped(self):
mock_mapping = mock.Mock()
mock_mapping.get_volume.return_value = self._mock_volume
self._mock_volume.get_logical_units.return_value = [mock_mapping]
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.create_cloned_volume,
test_clone, test_volume)
create_snapshot.assert_called_once_with(snapshot)
create_volume_from_snapshot.assert_called_once_with(test_clone,
snapshot)
delete_snapshot.assert_called_once_with(snapshot)
def test_create_cloned_volume_create_fails(self):
self._system.volumes.create.side_effect = self._raise_infinisdk
@ -724,21 +808,6 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.driver.delete_group_snapshot,
None, test_snapgroup, [test_snapshot])
def test_terminate_connection_force_detach(self):
mock_infinidat_host = mock.Mock()
mock_infinidat_host.get_ports.return_value = [
self._wwn.WWN(TEST_WWN_1)]
mock_mapping = mock.Mock()
mock_mapping.get_host.return_value = mock_infinidat_host
self._mock_volume.get_logical_units.return_value = [mock_mapping]
volume = copy.deepcopy(test_volume)
volume.volume_attachment = [test_attachment1, test_attachment2]
# connector is None - force detach - detach all mappings
self.assertTrue(self.driver.terminate_connection(volume, None))
# make sure we actually detached the host mapping
self._mock_host.unmap_volume.assert_called_once()
self._mock_host.safe_delete.assert_called_once()
def test_snapshot_revert_use_temp_snapshot(self):
result = self.driver.snapshot_revert_use_temp_snapshot()
self.assertFalse(result)
@ -1123,6 +1192,18 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
@ddt.ddt
class InfiniboxDriverTestCaseFC(InfiniboxDriverTestCaseBase):
@ddt.data(*itertools.product(('UP', 'DOWN'), ('OK', 'ERROR')))
@ddt.unpack
def test_initialize_connection_nodes_ports(self, link_state, port_state):
node = mock.Mock()
port = mock.Mock()
port.get_link_state.return_value = link_state
port.get_state.return_value = port_state
node.get_fc_ports.return_value = [port]
self._system.components.nodes.get_all.return_value = [node]
result = self.driver.initialize_connection(test_volume, test_connector)
self.assertEqual(1, result["data"]["target_lun"])
def test_initialize_connection_multiple_wwpns(self):
connector = {'wwpns': [TEST_WWN_1, TEST_WWN_2]}
result = self.driver.initialize_connection(test_volume, connector)
@ -1154,6 +1235,19 @@ class InfiniboxDriverTestCaseFC(InfiniboxDriverTestCaseBase):
self.assertTrue(self.driver.terminate_connection(volume,
test_connector))
def test_terminate_connection_force_detach(self):
mock_infinidat_host = mock.Mock()
mock_infinidat_host.get_ports.return_value = [
self._wwn.WWN(TEST_WWN_1)]
mock_mapping = mock.Mock()
mock_mapping.get_host.return_value = mock_infinidat_host
self._mock_volume.get_logical_units.return_value = [mock_mapping]
volume = copy.deepcopy(test_volume)
volume.volume_attachment = [test_attachment1, test_attachment2]
self.assertTrue(self.driver.terminate_connection(volume, None))
self._mock_host.unmap_volume.assert_called_once()
self._mock_host.safe_delete.assert_called_once()
@ddt.ddt
class InfiniboxDriverTestCaseISCSI(InfiniboxDriverTestCaseBase):
@ -1356,6 +1450,19 @@ class InfiniboxDriverTestCaseISCSI(InfiniboxDriverTestCaseBase):
self.assertFalse(self.driver.terminate_connection(volume,
test_connector))
def test_terminate_connection_force_detach(self):
mock_infinidat_host = mock.Mock()
mock_infinidat_host.get_ports.return_value = [
self._iqn.IQN(TEST_TARGET_IQN)]
mock_mapping = mock.Mock()
mock_mapping.get_host.return_value = mock_infinidat_host
self._mock_volume.get_logical_units.return_value = [mock_mapping]
volume = copy.deepcopy(test_volume)
volume.volume_attachment = [test_attachment1, test_attachment2]
self.assertTrue(self.driver.terminate_connection(volume, None))
self._mock_host.unmap_volume.assert_called_once()
self._mock_host.safe_delete.assert_called_once()
def test_validate_connector(self):
fc_connector = {'wwpns': [TEST_WWN_1, TEST_WWN_2]}
iscsi_connector = {'initiator': TEST_INITIATOR_IQN}
@ -1365,6 +1472,13 @@ class InfiniboxDriverTestCaseISCSI(InfiniboxDriverTestCaseBase):
class InfiniboxDriverTestCaseQoS(InfiniboxDriverTestCaseBase):
@mock.patch("cinder.volume.volume_types.get_volume_type_qos_specs")
def test_no_qos(self, qos_specs):
qos_specs.return_value = None
self.driver.create_volume(test_volume)
self._system.qos_policies.create.assert_not_called()
self._mock_qos_policy.assign_entity.assert_not_called()
@mock.patch("cinder.volume.volume_types.get_volume_type_qos_specs")
def test_qos_max_ipos(self, qos_specs):
qos_specs.return_value = {'qos_specs': {'id': 'qos_name',

View File

@ -14,12 +14,12 @@
# under the License.
"""INFINIDAT InfiniBox Volume Driver."""
import collections
from contextlib import contextmanager
import functools
import math
import platform
import socket
from unittest import mock
import uuid
from oslo_config import cfg
@ -129,10 +129,11 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
1.12 - fixed volume multi-attach
1.13 - fixed consistency groups feature
1.14 - added storage assisted volume migration
1.15 - fixed backup for attached volume
"""
VERSION = '1.14'
VERSION = '1.15'
# ThirdPartySystems wiki page
CI_WIKI_NAME = "INFINIDAT_CI"
@ -390,8 +391,7 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
port.get_state() == 'OK'):
yield str(port.get_wwpn())
def _initialize_connection_fc(self, volume, connector):
infinidat_volume = self._get_infinidat_volume(volume)
def _initialize_connection_fc(self, infinidat_volume, connector):
ports = [wwn.WWN(wwpn) for wwpn in connector['wwpns']]
for port in ports:
infinidat_host = self._get_or_create_host(port)
@ -431,8 +431,7 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
netspace.get_name())
raise exception.VolumeDriverException(message=msg)
def _initialize_connection_iscsi(self, volume, connector):
infinidat_volume = self._get_infinidat_volume(volume)
def _initialize_connection_iscsi(self, infinidat_volume, connector):
port = iqn.IQN(connector['initiator'])
infinidat_host = self._get_or_create_host(port)
if self.configuration.use_chap_auth:
@ -519,22 +518,40 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
return True
return False
@infinisdk_to_cinder_exceptions
def create_export_snapshot(self, context, snapshot, connector):
"""Exports the snapshot."""
pass
def remove_export_snapshot(self, context, snapshot):
"""Removes an export for a snapshot."""
pass
def backup_use_temp_snapshot(self):
"""Use a temporary snapshot for performing non-disruptive backups."""
return True
@coordination.synchronized('infinidat-{self.management_address}-lock')
def initialize_connection(self, volume, connector):
"""Map an InfiniBox volume to the host"""
def _initialize_connection(self, infinidat_volume, connector):
if self._protocol == constants.FC:
return self._initialize_connection_fc(volume, connector)
initialize_connection = self._initialize_connection_fc
else:
return self._initialize_connection_iscsi(volume, connector)
initialize_connection = self._initialize_connection_iscsi
return initialize_connection(infinidat_volume, connector)
@infinisdk_to_cinder_exceptions
@coordination.synchronized('infinidat-{self.management_address}-lock')
def terminate_connection(self, volume, connector, **kwargs):
"""Unmap an InfiniBox volume from the host"""
if self._is_volume_multiattached(volume, connector):
return True
def initialize_connection(self, volume, connector, **kwargs):
"""Map an InfiniBox volume to the host"""
infinidat_volume = self._get_infinidat_volume(volume)
return self._initialize_connection(infinidat_volume, connector)
@infinisdk_to_cinder_exceptions
def initialize_connection_snapshot(self, snapshot, connector, **kwargs):
"""Map an InfiniBox snapshot to the host"""
infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
return self._initialize_connection(infinidat_snapshot, connector)
@coordination.synchronized('infinidat-{self.management_address}-lock')
def _terminate_connection(self, infinidat_volume, connector):
if self._protocol == constants.FC:
volume_type = 'fibre_channel'
else:
@ -568,8 +585,22 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
conn_info = dict(driver_volume_type=volume_type,
data=result_data)
fczm_utils.remove_fc_zone(conn_info)
@infinisdk_to_cinder_exceptions
def terminate_connection(self, volume, connector, **kwargs):
"""Unmap an InfiniBox volume from the host"""
if self._is_volume_multiattached(volume, connector):
return True
infinidat_volume = self._get_infinidat_volume(volume)
self._terminate_connection(infinidat_volume, connector)
return volume.volume_attachment and len(volume.volume_attachment) > 1
@infinisdk_to_cinder_exceptions
def terminate_connection_snapshot(self, snapshot, connector, **kwargs):
"""Unmap an InfiniBox snapshot from the host"""
infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
self._terminate_connection(infinidat_snapshot, connector)
@infinisdk_to_cinder_exceptions
def get_volume_stats(self, refresh=False):
if self._volume_stats is None or refresh:
@ -660,17 +691,17 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
self._set_cinder_object_metadata(infinidat_snapshot, snapshot)
@contextmanager
def _connection_context(self, volume):
def _connection_context(self, infinidat_volume):
use_multipath = self.configuration.use_multipath_for_image_xfer
enforce_multipath = self.configuration.enforce_multipath_for_image_xfer
connector = volume_utils.brick_get_connector_properties(
use_multipath,
enforce_multipath)
connection = self.initialize_connection(volume, connector)
connection = self._initialize_connection(infinidat_volume, connector)
try:
yield connection
finally:
self.terminate_connection(volume, connector)
self._terminate_connection(infinidat_volume, connector)
@contextmanager
def _attach_context(self, connection):
@ -695,8 +726,8 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
attach_info['device'])
@contextmanager
def _device_connect_context(self, volume):
with self._connection_context(volume) as connection:
def _device_connect_context(self, infinidat_volume):
with self._connection_context(infinidat_volume) as connection:
with self._attach_context(connection) as attach_info:
yield attach_info
@ -707,36 +738,25 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
InfiniBox does not yet support detached clone so use dd to copy data.
This could be a lengthy operation.
- create a clone from snapshot and map it
- create a volume and map it
- copy data from clone to volume
- unmap volume and clone and delete the clone
- create destination volume
- map source snapshot and destination volume
- copy data from snapshot to volume
- unmap volume and snapshot
"""
infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
clone_name = self._make_volume_name(volume) + '-internal'
infinidat_clone = infinidat_snapshot.create_snapshot(name=clone_name)
# we need a cinder-volume-like object to map the clone by name
# (which is derived from the cinder id) but the clone is internal
# so there is no such object. mock one
clone = mock.Mock(name_id=str(volume.name_id) + '-internal',
multiattach=False, volume_attachment=[])
infinidat_volume = self._create_volume(volume)
try:
infinidat_volume = self._create_volume(volume)
try:
src_ctx = self._device_connect_context(clone)
dst_ctx = self._device_connect_context(volume)
with src_ctx as src_dev, dst_ctx as dst_dev:
dd_block_size = self.configuration.volume_dd_blocksize
volume_utils.copy_volume(src_dev['device']['path'],
dst_dev['device']['path'],
snapshot.volume.size * units.Ki,
dd_block_size,
sparse=True)
except Exception:
infinidat_volume.delete()
raise
finally:
infinidat_clone.delete()
src_ctx = self._device_connect_context(infinidat_snapshot)
dst_ctx = self._device_connect_context(infinidat_volume)
with src_ctx as src_dev, dst_ctx as dst_dev:
dd_block_size = self.configuration.volume_dd_blocksize
volume_utils.copy_volume(src_dev['device']['path'],
dst_dev['device']['path'],
snapshot.volume.size * units.Ki,
dd_block_size, sparse=True)
except Exception:
infinidat_volume.delete()
raise
@infinisdk_to_cinder_exceptions
def delete_snapshot(self, snapshot):
@ -747,20 +767,6 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
return
snapshot.safe_delete()
def _assert_volume_not_mapped(self, volume):
# copy is not atomic so we can't clone while the volume is mapped
infinidat_volume = self._get_infinidat_volume(volume)
if len(infinidat_volume.get_logical_units()) == 0:
return
# volume has mappings
msg = _("INFINIDAT Cinder driver does not support clone of an "
"attached volume. "
"To get this done, create a snapshot from the attached "
"volume and then create a volume from the snapshot.")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
@infinisdk_to_cinder_exceptions
def create_cloned_volume(self, volume, src_vref):
"""Create a clone from source volume.
@ -768,26 +774,24 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
InfiniBox does not yet support detached clone so use dd to copy data.
This could be a lengthy operation.
* map source volume
* create temporary snapshot from source volume
* map temporary snapshot
* create and map new volume
* copy data from source to new volume
* unmap both volumes
* copy data from temporary snapshot to new volume
* unmap volume and temporary snapshot
* delete temporary snapshot
"""
self._assert_volume_not_mapped(src_vref)
infinidat_volume = self._create_volume(volume)
attributes = ('id', 'name', 'volume')
Snapshot = collections.namedtuple('Snapshot', attributes)
snapshot_id = str(uuid.uuid4())
snapshot_name = CONF.snapshot_name_template % snapshot_id
snapshot = Snapshot(id=snapshot_id, name=snapshot_name,
volume=src_vref)
try:
src_ctx = self._device_connect_context(src_vref)
dst_ctx = self._device_connect_context(volume)
with src_ctx as src_dev, dst_ctx as dst_dev:
dd_block_size = self.configuration.volume_dd_blocksize
volume_utils.copy_volume(src_dev['device']['path'],
dst_dev['device']['path'],
src_vref.size * units.Ki,
dd_block_size,
sparse=True)
except Exception:
infinidat_volume.delete()
raise
self.create_snapshot(snapshot)
self.create_volume_from_snapshot(volume, snapshot)
finally:
self.delete_snapshot(snapshot)
def _build_initiator_target_map(self, connector, all_target_wwns):
"""Build the target_wwns and the initiator target map."""

View File

@ -0,0 +1,6 @@
---
fixes:
- |
Infinidat Driver `bug #1983287
<https://bugs.launchpad.net/cinder/+bug/1983287>`_:
Fixed Infinidat driver to allow backup of an attached volume.