Fix Infinidat driver to backup attached volume

For some historical reason, the Infinidat Cinder driver does not
allow to backup an attached volume. This makes it impossible to
create a backup for most use cases. As a possible solution we can
create and use snapshots as a backup source.

This patch implements attach/detach for snapshots and the ability
to use snapshots to perform non-disruptive backups.

Added functions:
* initialize_connection_snapshot()
* terminate_connection_snapshot()
* create_export_snapshot()
* remove_export_snapshot()
* backup_use_temp_snapshot()

And changed functions:
* create_volume_from_snapshot()
* create_cloned_volume()

remove the duplicated code and use the temporary snapshot
as the source for creating the cloned volume.

Closes-bug: #1983287
Change-Id: I0e1534c6015eaa8baef48b7878324625a4cc9f1a
Signed-off-by: Alexander Deiter <adeiter@infinidat.com>
This commit is contained in:
Alexander Deiter 2022-05-23 21:36:35 +04:00
parent 7d442e5b5b
commit 907550015e
3 changed files with 242 additions and 118 deletions

View File

@ -14,11 +14,14 @@
# under the License. # under the License.
"""Unit tests for INFINIDAT InfiniBox volume driver.""" """Unit tests for INFINIDAT InfiniBox volume driver."""
import collections
import copy import copy
import functools import functools
import itertools
import platform import platform
import socket import socket
from unittest import mock from unittest import mock
import uuid
import ddt import ddt
from oslo_utils import units from oslo_utils import units
@ -215,6 +218,14 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self._mock_host.set_metadata_from_dict.assert_called_once_with( self._mock_host.set_metadata_from_dict.assert_called_once_with(
self._generate_mock_host_metadata()) 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 @skip_driver_setup
def test__setup_and_get_system_object(self): def test__setup_and_get_system_object(self):
# This test should skip the driver setup, as it generates more calls to # This test should skip the driver setup, as it generates more calls to
@ -230,6 +241,12 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
infinidat._INFINIDAT_CINDER_IDENTIFIER) infinidat._INFINIDAT_CINDER_IDENTIFIER)
self._system.login.assert_called_once() 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') @mock.patch('cinder.volume.drivers.infinidat.infinisdk.InfiniBox')
@ddt.data(True, False) @ddt.data(True, False)
def test_ssl_options(self, use_ssl, infinibox): def test_ssl_options(self, use_ssl, infinibox):
@ -240,9 +257,31 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
infinibox.assert_called_once_with(self.configuration.san_ip, infinibox.assert_called_once_with(self.configuration.san_ip,
auth=auth, use_ssl=use_ssl) 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 self._system.hosts.safe_get.return_value = None
result = self.driver.initialize_connection(test_volume, test_connector) result = self.driver.initialize_connection(test_volume, test_connector)
get_volume.assert_called_once_with(test_volume)
self.assertEqual(1, result["data"]["target_lun"]) self.assertEqual(1, result["data"]["target_lun"])
def test_initialize_connection_host_exists(self): def test_initialize_connection_host_exists(self):
@ -257,6 +296,13 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
result = self.driver.initialize_connection(test_volume, test_connector) result = self.driver.initialize_connection(test_volume, test_connector)
self.assertEqual(888, result["data"]["target_lun"]) 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): def test_initialize_connection_volume_doesnt_exist(self):
self._system.volumes.safe_get.return_value = None self._system.volumes.safe_get.return_value = None
self.assertRaises(exception.VolumeNotFound, self.assertRaises(exception.VolumeNotFound,
@ -300,11 +346,37 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.assertFalse(self.driver._is_volume_multiattached(volume, self.assertFalse(self.driver._is_volume_multiattached(volume,
connector)) 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 = copy.deepcopy(test_volume)
volume.volume_attachment = [test_attachment1] volume.volume_attachment = [test_attachment1]
volume_multiattached.return_value = False
get_volume.return_value = self._mock_volume
self.assertFalse(self.driver.terminate_connection(volume, self.assertFalse(self.driver.terminate_connection(volume,
test_connector)) 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): def test_terminate_connection_delete_host(self):
self._mock_host.get_luns.return_value = [object()] self._mock_host.get_luns.return_value = [object()]
@ -470,7 +542,7 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
test_clone, test_snapshot) test_clone, test_snapshot)
def test_create_volume_from_snapshot_create_fails(self): 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.assertRaises(exception.VolumeBackendAPIException,
self.driver.create_volume_from_snapshot, self.driver.create_volume_from_snapshot,
test_clone, test_snapshot) test_clone, test_snapshot)
@ -484,13 +556,18 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.driver.create_volume_from_snapshot, self.driver.create_volume_from_snapshot,
test_clone, test_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") @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.volume_utils.brick_get_connector_properties')
return_value=test_connector) @mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
def test_create_volume_from_snapshot_delete_clone_fails(self, *mocks): '_connect_device')
self._mock_volume.delete.side_effect = self._raise_infinisdk def test_create_volume_from_snapshot_connect_fails(self, connect_device,
self.assertRaises(exception.VolumeBackendAPIException, 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, self.driver.create_volume_from_snapshot,
test_clone, test_snapshot) test_clone, test_snapshot)
@ -507,21 +584,28 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.assertRaises(exception.VolumeBackendAPIException, self.assertRaises(exception.VolumeBackendAPIException,
self.driver.delete_snapshot, test_snapshot) self.driver.delete_snapshot, test_snapshot)
@mock.patch("cinder.volume.volume_utils.copy_volume") @mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
@mock.patch("cinder.volume.volume_utils.brick_get_connector") 'delete_snapshot')
@mock.patch("cinder.volume.volume_utils.brick_get_connector_properties", @mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
return_value=test_connector) 'create_volume_from_snapshot')
@mock.patch("cinder.volume.volume_types.get_volume_type_qos_specs") @mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
def test_create_cloned_volume(self, *mocks): '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) self.driver.create_cloned_volume(test_clone, test_volume)
create_snapshot.assert_called_once_with(snapshot)
def test_create_cloned_volume_volume_already_mapped(self): create_volume_from_snapshot.assert_called_once_with(test_clone,
mock_mapping = mock.Mock() snapshot)
mock_mapping.get_volume.return_value = self._mock_volume delete_snapshot.assert_called_once_with(snapshot)
self._mock_volume.get_logical_units.return_value = [mock_mapping]
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.create_cloned_volume,
test_clone, test_volume)
def test_create_cloned_volume_create_fails(self): def test_create_cloned_volume_create_fails(self):
self._system.volumes.create.side_effect = self._raise_infinisdk self._system.volumes.create.side_effect = self._raise_infinisdk
@ -724,21 +808,6 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
self.driver.delete_group_snapshot, self.driver.delete_group_snapshot,
None, test_snapgroup, [test_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): def test_snapshot_revert_use_temp_snapshot(self):
result = self.driver.snapshot_revert_use_temp_snapshot() result = self.driver.snapshot_revert_use_temp_snapshot()
self.assertFalse(result) self.assertFalse(result)
@ -1123,6 +1192,18 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
@ddt.ddt @ddt.ddt
class InfiniboxDriverTestCaseFC(InfiniboxDriverTestCaseBase): 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): def test_initialize_connection_multiple_wwpns(self):
connector = {'wwpns': [TEST_WWN_1, TEST_WWN_2]} connector = {'wwpns': [TEST_WWN_1, TEST_WWN_2]}
result = self.driver.initialize_connection(test_volume, connector) result = self.driver.initialize_connection(test_volume, connector)
@ -1154,6 +1235,19 @@ class InfiniboxDriverTestCaseFC(InfiniboxDriverTestCaseBase):
self.assertTrue(self.driver.terminate_connection(volume, self.assertTrue(self.driver.terminate_connection(volume,
test_connector)) 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 @ddt.ddt
class InfiniboxDriverTestCaseISCSI(InfiniboxDriverTestCaseBase): class InfiniboxDriverTestCaseISCSI(InfiniboxDriverTestCaseBase):
@ -1356,6 +1450,19 @@ class InfiniboxDriverTestCaseISCSI(InfiniboxDriverTestCaseBase):
self.assertFalse(self.driver.terminate_connection(volume, self.assertFalse(self.driver.terminate_connection(volume,
test_connector)) 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): def test_validate_connector(self):
fc_connector = {'wwpns': [TEST_WWN_1, TEST_WWN_2]} fc_connector = {'wwpns': [TEST_WWN_1, TEST_WWN_2]}
iscsi_connector = {'initiator': TEST_INITIATOR_IQN} iscsi_connector = {'initiator': TEST_INITIATOR_IQN}
@ -1365,6 +1472,13 @@ class InfiniboxDriverTestCaseISCSI(InfiniboxDriverTestCaseBase):
class InfiniboxDriverTestCaseQoS(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") @mock.patch("cinder.volume.volume_types.get_volume_type_qos_specs")
def test_qos_max_ipos(self, qos_specs): def test_qos_max_ipos(self, qos_specs):
qos_specs.return_value = {'qos_specs': {'id': 'qos_name', qos_specs.return_value = {'qos_specs': {'id': 'qos_name',

View File

@ -14,12 +14,12 @@
# under the License. # under the License.
"""INFINIDAT InfiniBox Volume Driver.""" """INFINIDAT InfiniBox Volume Driver."""
import collections
from contextlib import contextmanager from contextlib import contextmanager
import functools import functools
import math import math
import platform import platform
import socket import socket
from unittest import mock
import uuid import uuid
from oslo_config import cfg from oslo_config import cfg
@ -129,10 +129,11 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
1.12 - fixed volume multi-attach 1.12 - fixed volume multi-attach
1.13 - fixed consistency groups feature 1.13 - fixed consistency groups feature
1.14 - added storage assisted volume migration 1.14 - added storage assisted volume migration
1.15 - fixed backup for attached volume
""" """
VERSION = '1.14' VERSION = '1.15'
# ThirdPartySystems wiki page # ThirdPartySystems wiki page
CI_WIKI_NAME = "INFINIDAT_CI" CI_WIKI_NAME = "INFINIDAT_CI"
@ -390,8 +391,7 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
port.get_state() == 'OK'): port.get_state() == 'OK'):
yield str(port.get_wwpn()) yield str(port.get_wwpn())
def _initialize_connection_fc(self, volume, connector): def _initialize_connection_fc(self, infinidat_volume, connector):
infinidat_volume = self._get_infinidat_volume(volume)
ports = [wwn.WWN(wwpn) for wwpn in connector['wwpns']] ports = [wwn.WWN(wwpn) for wwpn in connector['wwpns']]
for port in ports: for port in ports:
infinidat_host = self._get_or_create_host(port) infinidat_host = self._get_or_create_host(port)
@ -431,8 +431,7 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
netspace.get_name()) netspace.get_name())
raise exception.VolumeDriverException(message=msg) raise exception.VolumeDriverException(message=msg)
def _initialize_connection_iscsi(self, volume, connector): def _initialize_connection_iscsi(self, infinidat_volume, connector):
infinidat_volume = self._get_infinidat_volume(volume)
port = iqn.IQN(connector['initiator']) port = iqn.IQN(connector['initiator'])
infinidat_host = self._get_or_create_host(port) infinidat_host = self._get_or_create_host(port)
if self.configuration.use_chap_auth: if self.configuration.use_chap_auth:
@ -519,22 +518,40 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
return True return True
return False 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') @coordination.synchronized('infinidat-{self.management_address}-lock')
def initialize_connection(self, volume, connector): def _initialize_connection(self, infinidat_volume, connector):
"""Map an InfiniBox volume to the host"""
if self._protocol == constants.FC: if self._protocol == constants.FC:
return self._initialize_connection_fc(volume, connector) initialize_connection = self._initialize_connection_fc
else: else:
return self._initialize_connection_iscsi(volume, connector) initialize_connection = self._initialize_connection_iscsi
return initialize_connection(infinidat_volume, connector)
@infinisdk_to_cinder_exceptions @infinisdk_to_cinder_exceptions
@coordination.synchronized('infinidat-{self.management_address}-lock') def initialize_connection(self, volume, connector, **kwargs):
def terminate_connection(self, volume, connector, **kwargs): """Map an InfiniBox volume to the host"""
"""Unmap an InfiniBox volume from the host"""
if self._is_volume_multiattached(volume, connector):
return True
infinidat_volume = self._get_infinidat_volume(volume) 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: if self._protocol == constants.FC:
volume_type = 'fibre_channel' volume_type = 'fibre_channel'
else: else:
@ -568,8 +585,22 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
conn_info = dict(driver_volume_type=volume_type, conn_info = dict(driver_volume_type=volume_type,
data=result_data) data=result_data)
fczm_utils.remove_fc_zone(conn_info) 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 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 @infinisdk_to_cinder_exceptions
def get_volume_stats(self, refresh=False): def get_volume_stats(self, refresh=False):
if self._volume_stats is None or refresh: if self._volume_stats is None or refresh:
@ -660,17 +691,17 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
self._set_cinder_object_metadata(infinidat_snapshot, snapshot) self._set_cinder_object_metadata(infinidat_snapshot, snapshot)
@contextmanager @contextmanager
def _connection_context(self, volume): def _connection_context(self, infinidat_volume):
use_multipath = self.configuration.use_multipath_for_image_xfer use_multipath = self.configuration.use_multipath_for_image_xfer
enforce_multipath = self.configuration.enforce_multipath_for_image_xfer enforce_multipath = self.configuration.enforce_multipath_for_image_xfer
connector = volume_utils.brick_get_connector_properties( connector = volume_utils.brick_get_connector_properties(
use_multipath, use_multipath,
enforce_multipath) enforce_multipath)
connection = self.initialize_connection(volume, connector) connection = self._initialize_connection(infinidat_volume, connector)
try: try:
yield connection yield connection
finally: finally:
self.terminate_connection(volume, connector) self._terminate_connection(infinidat_volume, connector)
@contextmanager @contextmanager
def _attach_context(self, connection): def _attach_context(self, connection):
@ -695,8 +726,8 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
attach_info['device']) attach_info['device'])
@contextmanager @contextmanager
def _device_connect_context(self, volume): def _device_connect_context(self, infinidat_volume):
with self._connection_context(volume) as connection: with self._connection_context(infinidat_volume) as connection:
with self._attach_context(connection) as attach_info: with self._attach_context(connection) as attach_info:
yield 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. InfiniBox does not yet support detached clone so use dd to copy data.
This could be a lengthy operation. This could be a lengthy operation.
- create a clone from snapshot and map it - create destination volume
- create a volume and map it - map source snapshot and destination volume
- copy data from clone to volume - copy data from snapshot to volume
- unmap volume and clone and delete the clone - unmap volume and snapshot
""" """
infinidat_snapshot = self._get_infinidat_snapshot(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=[])
try:
infinidat_volume = self._create_volume(volume) infinidat_volume = self._create_volume(volume)
try: try:
src_ctx = self._device_connect_context(clone) src_ctx = self._device_connect_context(infinidat_snapshot)
dst_ctx = self._device_connect_context(volume) dst_ctx = self._device_connect_context(infinidat_volume)
with src_ctx as src_dev, dst_ctx as dst_dev: with src_ctx as src_dev, dst_ctx as dst_dev:
dd_block_size = self.configuration.volume_dd_blocksize dd_block_size = self.configuration.volume_dd_blocksize
volume_utils.copy_volume(src_dev['device']['path'], volume_utils.copy_volume(src_dev['device']['path'],
dst_dev['device']['path'], dst_dev['device']['path'],
snapshot.volume.size * units.Ki, snapshot.volume.size * units.Ki,
dd_block_size, dd_block_size, sparse=True)
sparse=True)
except Exception: except Exception:
infinidat_volume.delete() infinidat_volume.delete()
raise raise
finally:
infinidat_clone.delete()
@infinisdk_to_cinder_exceptions @infinisdk_to_cinder_exceptions
def delete_snapshot(self, snapshot): def delete_snapshot(self, snapshot):
@ -747,20 +767,6 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
return return
snapshot.safe_delete() 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 @infinisdk_to_cinder_exceptions
def create_cloned_volume(self, volume, src_vref): def create_cloned_volume(self, volume, src_vref):
"""Create a clone from source volume. """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. InfiniBox does not yet support detached clone so use dd to copy data.
This could be a lengthy operation. This could be a lengthy operation.
* map source volume * create temporary snapshot from source volume
* map temporary snapshot
* create and map new volume * create and map new volume
* copy data from source to new volume * copy data from temporary snapshot to new volume
* unmap both volumes * unmap volume and temporary snapshot
* delete temporary snapshot
""" """
self._assert_volume_not_mapped(src_vref) attributes = ('id', 'name', 'volume')
infinidat_volume = self._create_volume(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: try:
src_ctx = self._device_connect_context(src_vref) self.create_snapshot(snapshot)
dst_ctx = self._device_connect_context(volume) self.create_volume_from_snapshot(volume, snapshot)
with src_ctx as src_dev, dst_ctx as dst_dev: finally:
dd_block_size = self.configuration.volume_dd_blocksize self.delete_snapshot(snapshot)
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
def _build_initiator_target_map(self, connector, all_target_wwns): def _build_initiator_target_map(self, connector, all_target_wwns):
"""Build the target_wwns and the initiator target map.""" """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.