diff --git a/cinder/opts.py b/cinder/opts.py
index 20990687b09..51f349323e9 100644
--- a/cinder/opts.py
+++ b/cinder/opts.py
@@ -74,6 +74,10 @@ from cinder.volume.drivers.coprhd import common as \
cinder_volume_drivers_coprhd_common
from cinder.volume.drivers.coprhd import scaleio as \
cinder_volume_drivers_coprhd_scaleio
+from cinder.volume.drivers.datacore import driver as \
+ cinder_volume_drivers_datacore_driver
+from cinder.volume.drivers.datacore import iscsi as \
+ cinder_volume_drivers_datacore_iscsi
from cinder.volume.drivers.datera import datera_iscsi as \
cinder_volume_drivers_datera_dateraiscsi
from cinder.volume.drivers.dell_emc import ps as \
@@ -245,6 +249,8 @@ def list_opts():
[cinder_volume_api.az_cache_time_opt],
cinder_volume_driver.volume_opts,
cinder_volume_driver.iser_opts,
+ cinder_volume_drivers_datacore_driver.datacore_opts,
+ cinder_volume_drivers_datacore_iscsi.datacore_iscsi_opts,
cinder_volume_drivers_inspur_instorage_instoragecommon.
instorage_mcs_opts,
cinder_volume_drivers_inspur_instorage_instorageiscsi.
diff --git a/cinder/tests/unit/volume/drivers/datacore/__init__.py b/cinder/tests/unit/volume/drivers/datacore/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/cinder/tests/unit/volume/drivers/datacore/test_datacore_api.py b/cinder/tests/unit/volume/drivers/datacore/test_datacore_api.py
new file mode 100644
index 00000000000..52db51ec973
--- /dev/null
+++ b/cinder/tests/unit/volume/drivers/datacore/test_datacore_api.py
@@ -0,0 +1,728 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Unit tests for classes that are used to invoke DataCore SANsymphony API."""
+
+import mock
+from oslo_utils import units
+import six
+import suds
+from suds.sax import parser
+from suds import wsdl
+
+from cinder import test
+from cinder.volume.drivers.datacore import api
+from cinder.volume.drivers.datacore import exception
+
+
+class FakeWebSocketException(Exception):
+ pass
+
+
+class DataCoreClientTestCase(test.TestCase):
+ """Tests for the DataCore SANsymphony client."""
+
+ def setUp(self):
+ super(DataCoreClientTestCase, self).setUp()
+ self.mock_storage_services = mock.MagicMock()
+ self.mock_executive_service = mock.MagicMock()
+
+ self.mock_suds_client = mock.MagicMock()
+ self.mock_object(
+ api.suds_client, 'Client', return_value=self.mock_suds_client)
+
+ self.mock_channel = mock.MagicMock()
+ mock_websocket = self.mock_object(api, 'websocket')
+ mock_websocket.WebSocketException = FakeWebSocketException
+ mock_websocket.create_connection.return_value = self.mock_channel
+
+ setattr(self.mock_suds_client.service.__getitem__,
+ 'side_effect',
+ self._get_service_side_effect)
+
+ self.client = api.DataCoreClient('hostname', 'username', 'password', 1)
+
+ def _get_service_side_effect(self, service_name):
+ self.assertIn(service_name,
+ [
+ api.DataCoreClient.STORAGE_SERVICES_BINDING,
+ api.DataCoreClient.EXECUTIVE_SERVICE_BINDING
+ ])
+
+ if service_name is api.DataCoreClient.STORAGE_SERVICES_BINDING:
+ return self.mock_storage_services
+ else:
+ return self.mock_executive_service
+
+ def _assert_storage_services_method_called(self, method_name):
+ return self.mock_storage_services.__getitem__.assert_called_with(
+ method_name)
+
+ @property
+ def mock_storage_service_context(self):
+ return self.mock_storage_services.__getitem__()()
+
+ @property
+ def mock_executive_service_context(self):
+ return self.mock_executive_service.__getitem__()()
+
+ def test_process_request_failed(self):
+ def fail_with_socket_error():
+ raise FakeWebSocketException()
+
+ def fail_with_web_fault(message):
+ fault = mock.Mock()
+ fault.faultstring = "General error."
+ document = mock.Mock()
+ raise suds.WebFault(fault, document)
+
+ self.mock_channel.recv.side_effect = fail_with_socket_error
+ self.assertRaises(exception.DataCoreConnectionException,
+ self.client.get_server_groups)
+ self.mock_channel.recv.side_effect = None
+
+ (self.mock_storage_service_context.process_reply
+ .side_effect) = fail_with_web_fault
+ self.assertRaises(exception.DataCoreFaultException,
+ self.client.get_server_groups)
+
+ def test_channel_closing_failed(self):
+ def fail_with_socket_error():
+ raise FakeWebSocketException()
+
+ def fail_with_web_fault(message):
+ fault = mock.Mock()
+ fault.faultstring = "General error."
+ document = mock.Mock()
+ raise suds.WebFault(fault, document)
+
+ self.mock_channel.close.side_effect = fail_with_socket_error
+ (self.mock_storage_service_context.process_reply
+ .side_effect) = fail_with_web_fault
+ self.assertRaises(exception.DataCoreFaultException,
+ self.client.get_server_groups)
+
+ def test_update_api_endpoints(self):
+ def fail_with_socket_error():
+ try:
+ raise FakeWebSocketException()
+ finally:
+ self.mock_channel.recv.side_effect = None
+
+ self.mock_channel.recv.side_effect = fail_with_socket_error
+
+ mock_executive_endpoints = [{
+ 'network_address': '127.0.0.1:3794',
+ 'http_endpoint': 'http://127.0.0.1:3794/',
+ 'ws_endpoint': 'ws://127.0.0.1:3794/',
+ }]
+ self.mock_object(self.client,
+ '_executive_service_endpoints',
+ mock_executive_endpoints)
+
+ mock_storage_endpoint = {
+ 'network_address': '127.0.0.1:3794',
+ 'http_endpoint': 'http://127.0.0.1:3794/',
+ 'ws_endpoint': 'ws://127.0.0.1:3794/',
+ }
+ self.mock_object(self.client,
+ '_storage_services_endpoint',
+ mock_storage_endpoint)
+
+ node = mock.Mock()
+ node.HostAddress = '127.0.0.1:3794'
+ reply = mock.MagicMock()
+ reply.RegionNodeData = [node]
+ self.mock_storage_service_context.process_reply.return_value = reply
+
+ result = self.client.get_server_groups()
+ self.assertIsNotNone(result)
+
+ def test_update_api_endpoints_failed(self):
+ def fail_with_socket_error():
+ try:
+ raise FakeWebSocketException()
+ finally:
+ self.mock_channel.recv.side_effect = None
+
+ self.mock_channel.recv.side_effect = fail_with_socket_error
+
+ mock_executive_endpoints = [{
+ 'network_address': '127.0.0.1:3794',
+ 'http_endpoint': 'http://127.0.0.1:3794/',
+ 'ws_endpoint': 'ws://127.0.0.1:3794/',
+ }]
+ self.mock_object(self.client,
+ '_executive_service_endpoints',
+ mock_executive_endpoints)
+
+ reply = mock.MagicMock()
+ reply.RegionNodeData = []
+ self.mock_storage_service_context.process_reply.return_value = reply
+
+ self.mock_executive_service_context.process_reply.return_value = None
+
+ result = self.client.get_server_groups()
+ self.assertIsNotNone(result)
+
+ def test_get_server_groups(self):
+ self.client.get_server_groups()
+ self._assert_storage_services_method_called('GetServerGroups')
+
+ def test_get_servers(self):
+ self.client.get_servers()
+ self._assert_storage_services_method_called('GetServers')
+
+ def test_get_disk_pools(self):
+ self.client.get_disk_pools()
+ self._assert_storage_services_method_called('GetDiskPools')
+
+ def test_get_logical_disks(self):
+ self.client.get_logical_disks()
+ self._assert_storage_services_method_called('GetLogicalDisks')
+
+ def test_create_pool_logical_disk(self):
+ pool_id = 'pool_id'
+ pool_volume_type = 'Striped'
+ size = 1 * units.Gi
+ min_quota = 1
+ max_quota = 1 * units.Gi
+ self.client.create_pool_logical_disk(
+ pool_id, pool_volume_type, size, min_quota, max_quota)
+ self._assert_storage_services_method_called('CreatePoolLogicalDisk')
+
+ def test_delete_logical_disk(self):
+ logical_disk_id = 'disk_id'
+ self.client.delete_logical_disk(logical_disk_id)
+ self._assert_storage_services_method_called('DeleteLogicalDisk')
+
+ def test_get_logical_disk_chunk_allocation_map(self):
+ logical_disk_id = 'disk_id'
+ self.client.get_logical_disk_chunk_allocation_map(logical_disk_id)
+ self._assert_storage_services_method_called(
+ 'GetLogicalDiskChunkAllocationMap')
+
+ def test_get_next_virtual_disk_alias(self):
+ base_alias = 'volume'
+ self.client.get_next_virtual_disk_alias(base_alias)
+ self._assert_storage_services_method_called('GetNextVirtualDiskAlias')
+
+ def test_get_virtual_disks(self):
+ self.client.get_virtual_disks()
+ self._assert_storage_services_method_called('GetVirtualDisks')
+
+ def test_build_virtual_disk_data(self):
+ disk_alias = 'alias'
+ disk_type = 'Mirrored'
+ size = 1 * units.Gi
+ description = 'description'
+ storage_profile_id = 'storage_profile_id'
+
+ vd_data = self.client.build_virtual_disk_data(
+ disk_alias, disk_type, size, description, storage_profile_id)
+
+ self.assertEqual(disk_alias, vd_data.Alias)
+ self.assertEqual(size, vd_data.Size.Value)
+ self.assertEqual(description, vd_data.Description)
+ self.assertEqual(storage_profile_id, vd_data.StorageProfileId)
+ self.assertTrue(hasattr(vd_data, 'Type'))
+ self.assertTrue(hasattr(vd_data, 'SubType'))
+ self.assertTrue(hasattr(vd_data, 'DiskStatus'))
+ self.assertTrue(hasattr(vd_data, 'RecoveryPriority'))
+
+ def test_create_virtual_disk_ex2(self):
+ disk_alias = 'alias'
+ disk_type = 'Mirrored'
+ size = 1 * units.Gi
+ description = 'description'
+ storage_profile_id = 'storage_profile_id'
+ first_disk_id = 'disk_id'
+ second_disk_id = 'disk_id'
+ add_redundancy = True
+ vd_data = self.client.build_virtual_disk_data(
+ disk_alias, disk_type, size, description, storage_profile_id)
+ self.client.create_virtual_disk_ex2(
+ vd_data, first_disk_id, second_disk_id, add_redundancy)
+ self._assert_storage_services_method_called('CreateVirtualDiskEx2')
+
+ def test_set_virtual_disk_size(self):
+ disk_id = 'disk_id'
+ size = 1 * units.Gi
+ self.client.set_virtual_disk_size(disk_id, size)
+ self._assert_storage_services_method_called('SetVirtualDiskSize')
+
+ def test_delete_virtual_disk(self):
+ virtual_disk_id = 'disk_id'
+ delete_logical_disks = True
+ self.client.delete_virtual_disk(virtual_disk_id, delete_logical_disks)
+ self._assert_storage_services_method_called('DeleteVirtualDisk')
+
+ def test_serve_virtual_disks_to_host(self):
+ host_id = 'host_id'
+ disks = ['disk_id']
+ self.client.serve_virtual_disks_to_host(host_id, disks)
+ self._assert_storage_services_method_called('ServeVirtualDisksToHost')
+
+ def test_unserve_virtual_disks_from_host(self):
+ host_id = 'host_id'
+ disks = ['disk_id']
+ self.client.unserve_virtual_disks_from_host(host_id, disks)
+ self._assert_storage_services_method_called(
+ 'UnserveVirtualDisksFromHost')
+
+ def test_unserve_virtual_disks_from_port(self):
+ port_id = 'port_id'
+ disks = ['disk_id']
+ self.client.unserve_virtual_disks_from_port(port_id, disks)
+ self._assert_storage_services_method_called(
+ 'UnserveVirtualDisksFromPort')
+
+ def test_bind_logical_disk(self):
+ disk_id = 'disk_id'
+ logical_disk_id = 'disk_id'
+ role = 'Second'
+ create_mirror_mappings = True
+ create_client_mappings = False
+ add_redundancy = True
+ self.client.bind_logical_disk(
+ disk_id, logical_disk_id, role, create_mirror_mappings,
+ create_client_mappings, add_redundancy)
+ self._assert_storage_services_method_called(
+ 'BindLogicalDisk')
+
+ def test_get_snapshots(self):
+ self.client.get_snapshots()
+ self._assert_storage_services_method_called('GetSnapshots')
+
+ def test_create_snapshot(self):
+ disk_id = 'disk_id'
+ name = 'name'
+ description = 'description'
+ pool_id = 'pool_id'
+ snapshot_type = 'Full'
+ duplicate_disk_id = False
+ storage_profile_id = 'profile_id'
+ self.client.create_snapshot(
+ disk_id, name, description, pool_id, snapshot_type,
+ duplicate_disk_id, storage_profile_id)
+ self._assert_storage_services_method_called('CreateSnapshot')
+
+ def test_delete_snapshot(self):
+ snapshot_id = "snapshot_id"
+ self.client.delete_snapshot(snapshot_id)
+ self._assert_storage_services_method_called('DeleteSnapshot')
+
+ def test_get_storage_profiles(self):
+ self.client.get_storage_profiles()
+ self._assert_storage_services_method_called('GetStorageProfiles')
+
+ def test_designate_map_store(self):
+ pool_id = 'pool_id'
+ self.client.designate_map_store(pool_id)
+ self._assert_storage_services_method_called('DesignateMapStore')
+
+ def test_get_performance_by_type(self):
+ types = ['DiskPoolPerformance']
+ self.client.get_performance_by_type(types)
+ self._assert_storage_services_method_called('GetPerformanceByType')
+
+ def test_get_ports(self):
+ self.client.get_ports()
+ self._assert_storage_services_method_called('GetPorts')
+
+ def test_build_scsi_port_data(self):
+ host_id = 'host_id'
+ port_name = 'port_name'
+ port_mode = 'Initiator'
+ port_type = 'iSCSI'
+
+ port_data = self.client.build_scsi_port_data(
+ host_id, port_name, port_mode, port_type)
+
+ self.assertEqual(host_id, port_data.HostId)
+ self.assertEqual(port_name, port_data.PortName)
+ self.assertTrue(hasattr(port_data, 'PortMode'))
+ self.assertTrue(hasattr(port_data, 'PortType'))
+
+ def test_register_port(self):
+ port_data = self.client.build_scsi_port_data(
+ 'host_id', 'port_name', 'initiator', 'iSCSI')
+ self.client.register_port(port_data)
+ self._assert_storage_services_method_called('RegisterPort')
+
+ def test_assign_port(self):
+ client_id = 'client_id'
+ port_id = 'port_id'
+ self.client.assign_port(client_id, port_id)
+ self._assert_storage_services_method_called('AssignPort')
+
+ def test_set_server_port_properties(self):
+ port_id = 'port_id'
+ port_properties = mock.MagicMock()
+ self.client.set_server_port_properties(port_id, port_properties)
+ self._assert_storage_services_method_called('SetServerPortProperties')
+
+ def test_build_access_token(self):
+ initiator_node_name = 'initiator'
+ initiator_username = 'initiator_username'
+ initiator_password = 'initiator_password'
+ mutual_authentication = True
+ target_username = 'target_username'
+ target_password = 'target_password'
+
+ access_token = self.client.build_access_token(
+ initiator_node_name, initiator_username, initiator_password,
+ mutual_authentication, target_username, target_password)
+
+ self.assertEqual(initiator_node_name, access_token.InitiatorNodeName)
+ self.assertEqual(initiator_username, access_token.InitiatorUsername)
+ self.assertEqual(initiator_password, access_token.InitiatorPassword)
+ self.assertEqual(mutual_authentication,
+ access_token.MutualAuthentication)
+ self.assertEqual(target_username, access_token.TargetUsername)
+ self.assertEqual(target_password, access_token.TargetPassword)
+
+ def test_set_access_token(self):
+ port_id = 'port_id'
+ access_token = self.client.build_access_token(
+ 'initiator_name', None, None, False, 'initiator_name', 'password')
+ self.client.set_access_token(port_id, access_token)
+ self._assert_storage_services_method_called('SetAccessToken')
+
+ def test_get_clients(self):
+ self.client.get_clients()
+ self._assert_storage_services_method_called('GetClients')
+
+ def test_register_client(self):
+ host_name = 'name'
+ description = 'description'
+ machine_type = 'Other'
+ mode = 'PreferredServer'
+ preferred_server_ids = None
+ self.client.register_client(
+ host_name, description, machine_type, mode, preferred_server_ids)
+ self._assert_storage_services_method_called('RegisterClient')
+
+ def test_set_client_capabilities(self):
+ client_id = 'client_id'
+ mpio = True
+ alua = True
+ self.client.set_client_capabilities(client_id, mpio, alua)
+ self._assert_storage_services_method_called('SetClientCapabilities')
+
+ def test_get_target_domains(self):
+ self.client.get_target_domains()
+ self._assert_storage_services_method_called('GetTargetDomains')
+
+ def test_create_target_domain(self):
+ initiator_host_id = 'host_id'
+ target_host_id = 'host_id'
+ self.client.create_target_domain(initiator_host_id, target_host_id)
+ self._assert_storage_services_method_called('CreateTargetDomain')
+
+ def test_delete_target_domain(self):
+ domain_id = 'domain_id'
+ self.client.delete_target_domain(domain_id)
+ self._assert_storage_services_method_called('DeleteTargetDomain')
+
+ def test_get_target_devices(self):
+ self.client.get_target_devices()
+ self._assert_storage_services_method_called('GetTargetDevices')
+
+ def test_build_scsi_port_nexus_data(self):
+ initiator_id = 'initiator_id'
+ target_id = 'target_id'
+
+ nexus = self.client.build_scsi_port_nexus_data(initiator_id, target_id)
+
+ self.assertEqual(initiator_id, nexus.InitiatorPortId)
+ self.assertEqual(target_id, nexus.TargetPortId)
+
+ def test_create_target_device(self):
+ domain_id = 'domain_id'
+ nexus = self.client.build_scsi_port_nexus_data('initiator_id',
+ 'target_id')
+ self.client.create_target_device(domain_id, nexus)
+ self._assert_storage_services_method_called('CreateTargetDevice')
+
+ def test_delete_target_device(self):
+ device_id = 'device_id'
+ self.client.delete_target_device(device_id)
+ self._assert_storage_services_method_called('DeleteTargetDevice')
+
+ def test_get_next_free_lun(self):
+ device_id = 'device_id'
+ self.client.get_next_free_lun(device_id)
+ self._assert_storage_services_method_called('GetNextFreeLun')
+
+ def test_get_logical_units(self):
+ self.client.get_logical_units()
+ self._assert_storage_services_method_called('GetLogicalUnits')
+
+ def test_map_logical_disk(self):
+ disk_id = 'disk_id'
+ lun = 0
+ host_id = 'host_id'
+ mapping_type = 'Client'
+ initiator_id = 'initiator_id'
+ target_id = 'target_id'
+ nexus = self.client.build_scsi_port_nexus_data(initiator_id, target_id)
+ self.client.map_logical_disk(
+ disk_id, nexus, lun, host_id, mapping_type)
+ self._assert_storage_services_method_called('MapLogicalDisk')
+
+ def test_unmap_logical_disk(self):
+ logical_disk_id = 'disk_id'
+ nexus = self.client.build_scsi_port_nexus_data('initiator_id',
+ 'target_id')
+ self.client.unmap_logical_disk(logical_disk_id, nexus)
+ self._assert_storage_services_method_called('UnmapLogicalDisk')
+
+
+FAKE_WSDL_DOCUMENT = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ws://mns-vsp-001:3794/IExecutiveServiceEx
+
+
+
+
+"""
+
+
+class FaultDefinitionsFilterTestCase(test.TestCase):
+ """Tests for the plugin to process the DataCore API WSDL document."""
+
+ @staticmethod
+ def _binding_operation_has_fault(document, operation_name):
+ for binding in document.getChildren('binding', wsdl.wsdlns):
+ for operation in binding.getChildren('operation', wsdl.wsdlns):
+ if operation.get('name') == operation_name:
+ fault = operation.getChildren('fault', wsdl.wsdlns)
+ if fault:
+ return True
+ return False
+
+ @staticmethod
+ def _port_type_operation_has_fault(document, operation_name):
+ for port_type in document.getChildren('portType', wsdl.wsdlns):
+ for operation in port_type.getChildren('operation', wsdl.wsdlns):
+ if operation.get('name') == operation_name:
+ fault = operation.getChildren('fault', wsdl.wsdlns)
+ if fault:
+ return True
+ return False
+
+ def _operation_has_fault(self, document, operation_name):
+ _binding_has_fault = self._binding_operation_has_fault(
+ document, operation_name)
+ _port_type_has_fault = self._port_type_operation_has_fault(
+ document, operation_name)
+ self.assertEqual(_binding_has_fault, _port_type_has_fault)
+ return _binding_has_fault
+
+ def test_parsed(self):
+ context = mock.Mock()
+ sax = parser.Parser()
+ wsdl_document = FAKE_WSDL_DOCUMENT
+ if isinstance(wsdl_document, six.text_type):
+ wsdl_document = wsdl_document.encode('utf-8')
+ context.document = sax.parse(string=wsdl_document).root()
+ self.assertTrue(self._operation_has_fault(context.document,
+ 'StartExecutive'))
+ self.assertTrue(self._operation_has_fault(context.document,
+ 'StopExecutive'))
+ self.assertTrue(self._operation_has_fault(context.document,
+ 'ExecutiveStarted'))
+ self.assertTrue(self._operation_has_fault(context.document,
+ 'ExecutiveStopped'))
+ plugin = api.FaultDefinitionsFilter()
+ plugin.parsed(context)
+ self.assertTrue(self._operation_has_fault(context.document,
+ 'StartExecutive'))
+ self.assertTrue(self._operation_has_fault(context.document,
+ 'StopExecutive'))
+ self.assertFalse(self._operation_has_fault(context.document,
+ 'ExecutiveStarted'))
+ self.assertFalse(self._operation_has_fault(context.document,
+ 'ExecutiveStopped'))
diff --git a/cinder/tests/unit/volume/drivers/datacore/test_datacore_driver.py b/cinder/tests/unit/volume/drivers/datacore/test_datacore_driver.py
new file mode 100644
index 00000000000..d4c5ce1661f
--- /dev/null
+++ b/cinder/tests/unit/volume/drivers/datacore/test_datacore_driver.py
@@ -0,0 +1,678 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Unit tests for the base Driver for DataCore SANsymphony storage array."""
+
+from __future__ import division
+
+import abc
+import mock
+from oslo_utils import units
+
+from cinder import exception as cinder_exception
+from cinder.tests.unit import fake_constants
+from cinder.volume import configuration as conf
+from cinder.volume.drivers.datacore import driver as datacore_driver
+from cinder.volume.drivers.datacore import exception as datacore_exception
+from cinder.volume.drivers.san import san
+
+
+SERVER_GROUPS = [
+ mock.Mock(Id='server_group_id1',
+ OurGroup=True),
+ mock.Mock(Id='server_group_id2',
+ OurGroup=False),
+]
+
+SERVERS = [
+ mock.Mock(Id='server_id1',
+ State='Online'),
+ mock.Mock(Id='server_id2',
+ State='Online'),
+]
+
+DISK_POOLS = [
+ mock.Mock(Id='disk_pool_id1',
+ Caption='disk_pool1',
+ ServerId='server_id1',
+ PoolStatus='Running'),
+ mock.Mock(Id='disk_pool_id2',
+ Caption='disk_pool2',
+ ServerId='server_id2',
+ PoolStatus='Running'),
+ mock.Mock(Id='disk_pool_id3',
+ Caption='disk_pool3',
+ ServerId='server_id1',
+ PoolStatus='Offline'),
+ mock.Mock(Id='disk_pool_id4',
+ Caption='disk_pool4',
+ ServerId='server_id2',
+ PoolStatus='Unknown'),
+]
+
+DISK_POOL_PERFORMANCE = [
+ mock.Mock(ObjectId='disk_pool_id1',
+ PerformanceData=mock.Mock(BytesTotal=5 * units.Gi,
+ BytesAllocated=2 * units.Gi,
+ BytesAvailable=3 * units.Gi,
+ BytesReserved=0)),
+ mock.Mock(ObjectId='disk_pool_id2',
+ PerformanceData=mock.Mock(BytesTotal=5 * units.Gi,
+ BytesAllocated=3 * units.Gi,
+ BytesAvailable=1 * units.Gi,
+ BytesReserved=1 * units.Gi)),
+ mock.Mock(ObjectId='disk_pool_id3',
+ PerformanceData=None),
+ mock.Mock(ObjectId='disk_pool_id4',
+ PerformanceData=None),
+]
+
+STORAGE_PROFILES = [
+ mock.Mock(Id='storage_profile_id1',
+ Caption='storage_profile1'),
+ mock.Mock(Id='storage_profile_id2',
+ Caption='storage_profile2'),
+ mock.Mock(Id='storage_profile_id3',
+ Caption='storage_profile3'),
+]
+
+VIRTUAL_DISKS = [
+ mock.Mock(Id='virtual_disk_id1',
+ DiskStatus='Online',
+ IsServed=False,
+ FirstHostId='server_id1'),
+ mock.Mock(Id='virtual_disk_id2',
+ DiskStatus='Failed',
+ IsServed=False,
+ FirstHostId='server_id2'),
+ mock.Mock(Id='virtual_disk_id3',
+ DiskStatus='Online',
+ IsServed=True,
+ FirstHostId='server_id1',
+ SecondHostId='server_id2'),
+ mock.Mock(Id='virtual_disk_id4',
+ DiskStatus='Failed',
+ IsServed=False,
+ FirstHostId='server_id1',
+ SecondHostId='server_id2'),
+]
+
+VIRTUAL_DISK_SNAPSHOTS = [
+ mock.Mock(Id='snapshot_id1',
+ State='Migrated',
+ Failure='NoFailure',
+ DestinationLogicalDiskId='logical_disk_id1'),
+ mock.Mock(Id='snapshot_id2',
+ State='Failed',
+ Failure='NotAccessible',
+ DestinationLogicalDiskId='logical_disk_id2'),
+ mock.Mock(Id='snapshot_id3',
+ State='Migrated',
+ Failure='NoFailure',
+ DestinationLogicalDiskId='logical_disk_id2'),
+]
+
+LOGICAL_DISKS = [
+ mock.Mock(Id='logical_disk_id1',
+ VirtualDiskId='virtual_disk_id1',
+ ServerHostId='server_id1',
+ PoolId='disk_pool_id1',
+ Size=mock.Mock(Value=1 * units.Gi)),
+ mock.Mock(Id='logical_disk_id2',
+ VirtualDiskId='virtual_disk_id2',
+ ServerHostId='server_id1',
+ PoolId='disk_pool_id3',
+ Size=mock.Mock(Value=1 * units.Gi)),
+ mock.Mock(Id='logical_disk_id3',
+ VirtualDiskId='virtual_disk_id3',
+ ServerHostId='server_id1',
+ PoolId='disk_pool_id1',
+ Size=mock.Mock(Value=1 * units.Gi)),
+ mock.Mock(Id='logical_disk_id4',
+ VirtualDiskId='virtual_disk_id3',
+ ServerHostId='server_id2',
+ PoolId='disk_pool_id2',
+ Size=mock.Mock(Value=1 * units.Gi)),
+ mock.Mock(Id='logical_disk_id5',
+ VirtualDiskId='virtual_disk_id4',
+ ServerHostId='server_id1',
+ PoolId='disk_pool_id3',
+ Size=mock.Mock(Value=1 * units.Gi)),
+ mock.Mock(Id='logical_disk_id6',
+ VirtualDiskId='virtual_disk_id4',
+ ServerHostId='server_id2',
+ PoolId='disk_pool_id4',
+ Size=mock.Mock(Value=1 * units.Gi)),
+]
+
+LOGICAL_UNITS = [
+ mock.Mock(VirtualTargetDeviceId='target_device_id1',
+ LogicalDiskId='logical_disk_id3'),
+ mock.Mock(VirtualTargetDeviceId='target_device_id2',
+ LogicalDiskId='logical_disk_id4'),
+]
+
+TARGET_DEVICES = [
+ mock.Mock(Id='target_device_id1',
+ InitiatorPortId='initiator_port_id1'),
+ mock.Mock(Id='target_device_id2',
+ InitiatorPortId='initiator_port_id1'),
+]
+
+CLIENTS = [
+ mock.Mock(Id='client_id1',
+ HostName='client_host_name1'),
+ mock.Mock(Id='client_id2',
+ HostName='client_host_name2'),
+]
+
+VOLUME = {
+ 'id': fake_constants.VOLUME_ID,
+ 'display_name': 'volume_1',
+ 'volume_type_id': None,
+ 'size': 1,
+}
+
+SNAPSHOT = {
+ 'id': fake_constants.SNAPSHOT_ID,
+ 'display_name': 'snapshot_1',
+}
+
+
+class DataCoreVolumeDriverTestCase(object):
+ """Tests for the base Driver for DataCore SANsymphony storage array."""
+
+ def setUp(self):
+ super(DataCoreVolumeDriverTestCase, self).setUp()
+ self.mock_client = mock.Mock()
+ self.mock_client.get_servers.return_value = SERVERS
+ self.mock_client.get_disk_pools.return_value = DISK_POOLS
+ (self.mock_client.get_performance_by_type
+ .return_value) = DISK_POOL_PERFORMANCE
+ self.mock_client.get_virtual_disks.return_value = VIRTUAL_DISKS
+ self.mock_client.get_storage_profiles.return_value = STORAGE_PROFILES
+ self.mock_client.get_snapshots.return_value = VIRTUAL_DISK_SNAPSHOTS
+ self.mock_client.get_logical_disks.return_value = LOGICAL_DISKS
+ self.mock_client.get_clients.return_value = CLIENTS
+ self.mock_client.get_server_groups.return_value = SERVER_GROUPS
+ self.mock_object(datacore_driver.api,
+ 'DataCoreClient',
+ return_value=self.mock_client)
+
+ @staticmethod
+ @abc.abstractmethod
+ def init_driver(config):
+ raise NotImplementedError()
+
+ @staticmethod
+ def create_configuration():
+ config = conf.Configuration(None)
+ config.append_config_values(san.san_opts)
+ config.append_config_values(datacore_driver.datacore_opts)
+ return config
+
+ def setup_default_configuration(self):
+ config = self.create_configuration()
+ config.volume_backend_name = 'DataCore'
+ config.san_ip = '127.0.0.1'
+ config.san_login = 'dcsadmin'
+ config.san_password = 'password'
+ config.datacore_api_timeout = 300
+ return config
+
+ def test_do_setup(self):
+ config = self.setup_default_configuration()
+ self.init_driver(config)
+
+ def test_do_setup_failed(self):
+ config = self.setup_default_configuration()
+ config.san_ip = None
+ self.assertRaises(cinder_exception.InvalidInput,
+ self.init_driver,
+ config)
+
+ config = self.setup_default_configuration()
+ config.san_login = None
+ self.assertRaises(cinder_exception.InvalidInput,
+ self.init_driver,
+ config)
+
+ config = self.setup_default_configuration()
+ config.san_password = None
+ self.assertRaises(cinder_exception.InvalidInput,
+ self.init_driver,
+ config)
+
+ def test_get_volume_stats(self):
+ aggregation = [(getattr(perf.PerformanceData, 'BytesTotal', 0),
+ getattr(perf.PerformanceData, 'BytesAvailable', 0),
+ getattr(perf.PerformanceData, 'BytesReserved', 0),)
+ for perf in DISK_POOL_PERFORMANCE]
+
+ total, available, reserved = map(sum, zip(*aggregation))
+ free = (available + reserved) / units.Gi
+ reserved = 100.0 * reserved / total
+ total /= units.Gi
+ provisioned = sum(disk.Size.Value for disk in LOGICAL_DISKS)
+ provisioned /= units.Gi
+ ratio = 2.0
+
+ config = self.setup_default_configuration()
+ config.max_over_subscription_ratio = ratio
+ driver = self.init_driver(config)
+ expected_volume_stats = {
+ 'vendor_name': 'DataCore',
+ 'QoS_support': False,
+ 'total_capacity_gb': total,
+ 'free_capacity_gb': free,
+ 'provisioned_capacity_gb': provisioned,
+ 'reserved_percentage': reserved,
+ 'max_over_subscription_ratio': ratio,
+ 'thin_provisioning_support': True,
+ 'thick_provisioning_support': False,
+ 'volume_backend_name': driver.get_volume_backend_name(),
+ 'driver_version': driver.get_version(),
+ 'storage_protocol': driver.get_storage_protocol(),
+ }
+ volume_stats = driver.get_volume_stats(refresh=True)
+ self.assertDictEqual(expected_volume_stats, volume_stats)
+ volume_stats_cached = driver.get_volume_stats(refresh=False)
+ self.assertEqual(volume_stats, volume_stats_cached)
+
+ def test_create_volume(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.create_virtual_disk_ex2.return_value = virtual_disk
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ result = driver.create_volume(volume)
+ self.assertIn('provider_location', result)
+ self.assertEqual(virtual_disk.Id, result['provider_location'])
+
+ def test_create_volume_mirrored_disk_type_specified(self):
+ virtual_disk = VIRTUAL_DISKS[2]
+ self.mock_client.create_virtual_disk_ex2.return_value = virtual_disk
+
+ config = self.setup_default_configuration()
+ config.datacore_disk_type = 'mirrored'
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ result = driver.create_volume(volume)
+ self.assertIn('provider_location', result)
+ self.assertEqual(virtual_disk.Id, result['provider_location'])
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume_type = {
+ 'extra_specs': {driver.DATACORE_DISK_TYPE_KEY: 'mirrored'}
+ }
+ get_volume_type = self.mock_object(datacore_driver.volume_types,
+ 'get_volume_type')
+ get_volume_type.return_value = volume_type
+ volume = VOLUME.copy()
+ volume['volume_type_id'] = 'volume_type_id'
+ result = driver.create_volume(volume)
+ self.assertIn('provider_location', result)
+ self.assertEqual(virtual_disk.Id, result['provider_location'])
+
+ def test_create_volume_profile_specified(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.create_virtual_disk_ex2.return_value = virtual_disk
+
+ config = self.setup_default_configuration()
+ config.datacore_storage_profile = 'storage_profile1'
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ result = driver.create_volume(volume)
+ self.assertIn('provider_location', result)
+ self.assertEqual(virtual_disk.Id, result['provider_location'])
+
+ volume_type = {
+ 'extra_specs': {
+ driver.DATACORE_STORAGE_PROFILE_KEY: 'storage_profile2'
+ }
+ }
+ get_volume_type = self.mock_object(datacore_driver.volume_types,
+ 'get_volume_type')
+ get_volume_type.return_value = volume_type
+ volume = VOLUME.copy()
+ volume['volume_type_id'] = 'volume_type_id'
+ result = driver.create_volume(volume)
+ self.assertIn('provider_location', result)
+ self.assertEqual(virtual_disk.Id, result['provider_location'])
+
+ def test_create_volume_pool_specified(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.create_virtual_disk_ex2.return_value = virtual_disk
+
+ config = self.setup_default_configuration()
+ config.datacore_disk_pools = ['disk_pool1']
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ result = driver.create_volume(volume)
+ self.assertIn('provider_location', result)
+ self.assertEqual(virtual_disk.Id, result['provider_location'])
+
+ volume_type = {
+ 'extra_specs': {driver.DATACORE_DISK_POOLS_KEY: 'disk_pool2'}
+ }
+ get_volume_type = self.mock_object(datacore_driver.volume_types,
+ 'get_volume_type')
+ get_volume_type.return_value = volume_type
+ volume = VOLUME.copy()
+ volume['volume_type_id'] = 'volume_type_id'
+ result = driver.create_volume(volume)
+ self.assertIn('provider_location', result)
+ self.assertEqual(virtual_disk.Id, result['provider_location'])
+
+ def test_create_volume_failed(self):
+ def fail_with_datacore_fault(*args):
+ raise datacore_exception.DataCoreFaultException(
+ reason="General error.")
+
+ (self.mock_client.create_virtual_disk_ex2
+ .side_effect) = fail_with_datacore_fault
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ self.assertRaises(datacore_exception.DataCoreFaultException,
+ driver.create_volume,
+ volume)
+
+ def test_create_volume_unknown_disk_type_specified(self):
+ config = self.setup_default_configuration()
+ config.datacore_disk_type = 'unknown'
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume,
+ volume)
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume_type = {
+ 'extra_specs': {driver.DATACORE_DISK_TYPE_KEY: 'unknown'}
+ }
+ get_volume_type = self.mock_object(datacore_driver.volume_types,
+ 'get_volume_type')
+ get_volume_type.return_value = volume_type
+ volume = VOLUME.copy()
+ volume['volume_type_id'] = 'volume_type_id'
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume,
+ volume)
+
+ def test_create_volume_unknown_profile_specified(self):
+ config = self.setup_default_configuration()
+ config.datacore_storage_profile = 'unknown'
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume,
+ volume)
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume_type = {
+ 'extra_specs': {driver.DATACORE_STORAGE_PROFILE_KEY: 'unknown'}
+ }
+ get_volume_type = self.mock_object(datacore_driver.volume_types,
+ 'get_volume_type')
+ get_volume_type.return_value = volume_type
+ volume = VOLUME.copy()
+ volume['volume_type_id'] = 'volume_type_id'
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume,
+ volume)
+
+ def test_create_volume_on_failed_pool(self):
+ config = self.setup_default_configuration()
+ config.datacore_disk_pools = ['disk_pool3', 'disk_pool4']
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume,
+ volume)
+
+ def test_create_volume_await_online_timed_out(self):
+ virtual_disk = VIRTUAL_DISKS[1]
+ self.mock_client.create_virtual_disk_ex2.return_value = virtual_disk
+
+ config = self.setup_default_configuration()
+ config.datacore_disk_failed_delay = 1
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume,
+ volume)
+
+ def test_extend_volume(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ driver.extend_volume(volume, 2147483648)
+
+ def test_extend_volume_failed_not_found(self):
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ volume['provider_location'] = 'wrong_virtual_disk_id'
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.extend_volume,
+ volume,
+ 2147483648)
+
+ def test_delete_volume(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ driver.delete_volume(volume)
+
+ def test_delete_volume_assigned(self):
+ self.mock_client.get_logical_disks.return_value = LOGICAL_DISKS
+ self.mock_client.get_logical_units.return_value = LOGICAL_UNITS
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ virtual_disk = VIRTUAL_DISKS[2]
+ volume['provider_location'] = virtual_disk.Id
+ driver.delete_volume(volume)
+
+ def test_create_snapshot(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[0]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ snapshot = SNAPSHOT.copy()
+ snapshot['volume'] = volume
+ result = driver.create_snapshot(snapshot)
+ self.assertIn('provider_location', result)
+
+ def test_create_snapshot_on_failed_pool(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ config = self.setup_default_configuration()
+ config.datacore_disk_pools = ['disk_pool3', 'disk_pool4']
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ snapshot = SNAPSHOT.copy()
+ snapshot['volume'] = volume
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_snapshot,
+ snapshot)
+
+ def test_create_snapshot_await_migrated_timed_out(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[1]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ snapshot = SNAPSHOT.copy()
+ snapshot['volume'] = volume
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_snapshot,
+ snapshot)
+
+ def test_delete_snapshot(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ snapshot = SNAPSHOT.copy()
+ snapshot['provider_location'] = virtual_disk.Id
+ driver.delete_snapshot(snapshot)
+
+ def test_create_volume_from_snapshot(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.set_virtual_disk_size.return_value = virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[0]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ snapshot = SNAPSHOT.copy()
+ snapshot['provider_location'] = virtual_disk.Id
+ result = driver.create_volume_from_snapshot(volume, snapshot)
+ self.assertIn('provider_location', result)
+
+ def test_create_volume_from_snapshot_mirrored_disk_type_specified(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.set_virtual_disk_size.return_value = virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[0]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ config = self.setup_default_configuration()
+ config.datacore_disk_type = 'mirrored'
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ snapshot = SNAPSHOT.copy()
+ snapshot['provider_location'] = virtual_disk.Id
+ result = driver.create_volume_from_snapshot(volume, snapshot)
+ self.assertIn('provider_location', result)
+
+ def test_create_volume_from_snapshot_on_failed_pool(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.set_virtual_disk_size.return_value = virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[0]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ config = self.setup_default_configuration()
+ config.datacore_disk_type = 'mirrored'
+ config.datacore_disk_pools = ['disk_pool1', 'disk_pool4']
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ snapshot = SNAPSHOT.copy()
+ snapshot['provider_location'] = virtual_disk.Id
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume_from_snapshot,
+ volume,
+ snapshot)
+
+ def test_create_volume_from_snapshot_await_online_timed_out(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ snapshot_virtual_disk = VIRTUAL_DISKS[1]
+ (self.mock_client.set_virtual_disk_size
+ .return_value) = snapshot_virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[2]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ snapshot = SNAPSHOT.copy()
+ snapshot['provider_location'] = virtual_disk.Id
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_volume_from_snapshot,
+ volume,
+ snapshot)
+
+ def test_create_cloned_volume(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.set_virtual_disk_size.return_value = virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[0]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ src_vref = VOLUME.copy()
+ src_vref['provider_location'] = virtual_disk.Id
+ result = driver.create_cloned_volume(volume, src_vref)
+ self.assertIn('provider_location', result)
+
+ def test_create_cloned_volume_mirrored_disk_type_specified(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.set_virtual_disk_size.return_value = virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[0]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ config = self.setup_default_configuration()
+ config.datacore_disk_type = 'mirrored'
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ src_vref = VOLUME.copy()
+ src_vref['provider_location'] = virtual_disk.Id
+ result = driver.create_cloned_volume(volume, src_vref)
+ self.assertIn('provider_location', result)
+
+ def test_create_cloned_volume_on_failed_pool(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ self.mock_client.set_virtual_disk_size.return_value = virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[0]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ config = self.setup_default_configuration()
+ config.datacore_disk_type = 'mirrored'
+ config.datacore_disk_pools = ['disk_pool1', 'disk_pool4']
+ driver = self.init_driver(config)
+ volume = VOLUME.copy()
+ src_vref = VOLUME.copy()
+ src_vref['provider_location'] = virtual_disk.Id
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_cloned_volume,
+ volume,
+ src_vref)
+
+ def test_create_cloned_volume_await_online_timed_out(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ snapshot_virtual_disk = VIRTUAL_DISKS[1]
+ (self.mock_client.set_virtual_disk_size
+ .return_value) = snapshot_virtual_disk
+ virtual_disk_snapshot = VIRTUAL_DISK_SNAPSHOTS[2]
+ self.mock_client.create_snapshot.return_value = virtual_disk_snapshot
+
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ src_vref = VOLUME.copy()
+ src_vref['provider_location'] = virtual_disk.Id
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.create_cloned_volume,
+ volume,
+ src_vref)
+
+ def test_terminate_connection(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ client = CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ connector = {'host': client.HostName}
+ driver.terminate_connection(volume, connector)
+
+ def test_terminate_connection_connector_is_none(self):
+ virtual_disk = VIRTUAL_DISKS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ driver.terminate_connection(volume, None)
diff --git a/cinder/tests/unit/volume/drivers/datacore/test_datacore_fc.py b/cinder/tests/unit/volume/drivers/datacore/test_datacore_fc.py
new file mode 100644
index 00000000000..b6861069d6e
--- /dev/null
+++ b/cinder/tests/unit/volume/drivers/datacore/test_datacore_fc.py
@@ -0,0 +1,256 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Unit tests for the Fibre Channel Driver for DataCore SANsymphony
+storage array.
+"""
+
+import mock
+
+from cinder import exception as cinder_exception
+from cinder import test
+from cinder.tests.unit.volume.drivers.datacore import test_datacore_driver
+from cinder.volume.drivers.datacore import fc
+
+
+PORTS = [
+ mock.Mock(Id='initiator_port_id1',
+ PortType='FibreChannel',
+ PortMode='Initiator',
+ PortName='AA-AA-AA-AA-AA-AA-AA-AA',
+ HostId='client_id1'),
+ mock.Mock(Id='initiator_port_id2',
+ PortType='FibreChannel',
+ PortMode='Initiator',
+ PortName='BB-BB-BB-BB-BB-BB-BB-BB'),
+ mock.Mock(Id='target_port_id1',
+ PortMode='Target',
+ PortName='CC-CC-CC-CC-CC-CC-CC-CC',
+ HostId='server_id1'),
+ mock.Mock(Id='target_port_id2',
+ PortMode='Target',
+ PortName='DD-DD-DD-DD-DD-DD-DD-DD',
+ HostId='server_id1'),
+]
+
+LOGICAL_UNITS = [
+ mock.Mock(VirtualTargetDeviceId='target_device_id1',
+ Lun=mock.Mock(Quad=4)),
+ mock.Mock(VirtualTargetDeviceId='target_device_id2',
+ Lun=mock.Mock(Quad=3)),
+ mock.Mock(VirtualTargetDeviceId='target_device_id3',
+ Lun=mock.Mock(Quad=2)),
+ mock.Mock(VirtualTargetDeviceId='target_device_id4',
+ Lun=mock.Mock(Quad=1)),
+]
+
+TARGET_DEVICES = [
+ mock.Mock(Id='target_device_id1',
+ TargetPortId='target_port_id1',
+ InitiatorPortId='initiator_port_id1'),
+ mock.Mock(Id='target_device_id2',
+ TargetPortId='target_port_id2',
+ InitiatorPortId='initiator_port_id1'),
+ mock.Mock(Id='target_device_id3',
+ TargetPortId='target_port_id2',
+ InitiatorPortId='initiator_port_id1'),
+ mock.Mock(Id='target_device_id4',
+ TargetPortId='target_port_id2',
+ InitiatorPortId='initiator_port_id2'),
+]
+
+
+class FibreChannelVolumeDriverTestCase(
+ test_datacore_driver.DataCoreVolumeDriverTestCase, test.TestCase):
+ """Tests for the FC Driver for DataCore SANsymphony storage array."""
+
+ def setUp(self):
+ super(FibreChannelVolumeDriverTestCase, self).setUp()
+ self.mock_client.get_ports.return_value = PORTS
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ @staticmethod
+ def init_driver(config):
+ driver = fc.FibreChannelVolumeDriver(configuration=config)
+ driver.do_setup(None)
+ return driver
+
+ def test_validate_connector(self):
+ driver = self.init_driver(self.setup_default_configuration())
+ connector = {
+ 'host': 'host_name',
+ 'wwpns': ['AA-AA-AA-AA-AA-AA-AA-AA'],
+ }
+ driver.validate_connector(connector)
+
+ def test_validate_connector_failed(self):
+ driver = self.init_driver(self.setup_default_configuration())
+ connector = {}
+ self.assertRaises(cinder_exception.InvalidConnectorException,
+ driver.validate_connector,
+ connector)
+
+ connector = {'host': 'host_name'}
+ self.assertRaises(cinder_exception.InvalidConnectorException,
+ driver.validate_connector,
+ connector)
+
+ connector = {'wwpns': ['AA-AA-AA-AA-AA-AA-AA-AA']}
+ self.assertRaises(cinder_exception.InvalidConnectorException,
+ driver.validate_connector,
+ connector)
+
+ def test_initialize_connection(self):
+ (self.mock_client.serve_virtual_disks_to_host
+ .return_value) = LOGICAL_UNITS
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_wwpns = [port.PortName.replace('-', '').lower() for port
+ in PORTS
+ if port.PortMode == 'Initiator']
+ connector = {
+ 'host': client.HostName,
+ 'wwpns': initiator_wwpns,
+ }
+ result = driver.initialize_connection(volume, connector)
+ self.assertEqual('fibre_channel', result['driver_volume_type'])
+
+ target_wwns = [port.PortName.replace('-', '').lower() for port
+ in PORTS
+ if port.PortMode == 'Target']
+ self.assertIn(result['data']['target_wwn'], target_wwns)
+
+ target_wwn = result['data']['target_wwn']
+ target_port_id = next((
+ port.Id for port
+ in PORTS
+ if port.PortName.replace('-', '').lower() == target_wwn), None)
+ target_device_id = next((
+ device.Id for device
+ in TARGET_DEVICES
+ if device.TargetPortId == target_port_id), None)
+ target_lun = next((
+ unit.Lun.Quad for unit
+ in LOGICAL_UNITS
+ if unit.VirtualTargetDeviceId == target_device_id), None)
+ self.assertEqual(target_lun, result['data']['target_lun'])
+
+ self.assertFalse(result['data']['target_discovered'])
+ self.assertEqual(volume['id'], result['data']['volume_id'])
+ self.assertEqual('rw', result['data']['access_mode'])
+
+ def test_initialize_connection_unknown_client(self):
+ client = test_datacore_driver.CLIENTS[0]
+ self.mock_client.register_client.return_value = client
+ (self.mock_client.get_clients
+ .return_value) = test_datacore_driver.CLIENTS[1:]
+ (self.mock_client.serve_virtual_disks_to_host
+ .return_value) = LOGICAL_UNITS
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_wwpns = [port.PortName.replace('-', '').lower() for port
+ in PORTS
+ if port.PortMode == 'Initiator']
+ connector = {
+ 'host': client.HostName,
+ 'wwpns': initiator_wwpns,
+ }
+ result = driver.initialize_connection(volume, connector)
+ self.assertEqual('fibre_channel', result['driver_volume_type'])
+
+ target_wwns = [port.PortName.replace('-', '').lower() for port
+ in PORTS
+ if port.PortMode == 'Target']
+ self.assertIn(result['data']['target_wwn'], target_wwns)
+
+ target_wwn = result['data']['target_wwn']
+ target_port_id = next((
+ port.Id for port
+ in PORTS
+ if port.PortName.replace('-', '').lower() == target_wwn), None)
+ target_device_id = next((
+ device.Id for device
+ in TARGET_DEVICES
+ if device.TargetPortId == target_port_id), None)
+ target_lun = next((
+ unit.Lun.Quad for unit
+ in LOGICAL_UNITS
+ if unit.VirtualTargetDeviceId == target_device_id), None)
+ self.assertEqual(target_lun, result['data']['target_lun'])
+
+ self.assertFalse(result['data']['target_discovered'])
+ self.assertEqual(volume['id'], result['data']['volume_id'])
+ self.assertEqual('rw', result['data']['access_mode'])
+
+ def test_initialize_connection_failed_not_found(self):
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = 'wrong_virtual_disk_id'
+ initiator_wwpns = [port.PortName.replace('-', '').lower() for port
+ in PORTS
+ if port.PortMode == 'Initiator']
+ connector = {
+ 'host': client.HostName,
+ 'wwpns': initiator_wwpns,
+ }
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.initialize_connection,
+ volume,
+ connector)
+
+ def test_initialize_connection_failed_initiator_not_found(self):
+ (self.mock_client.serve_virtual_disks_to_host
+ .return_value) = LOGICAL_UNITS
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ connector = {
+ 'host': client.HostName,
+ 'wwpns': ['0000000000000000'],
+ }
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.initialize_connection,
+ volume,
+ connector)
+
+ def test_initialize_connection_failed_on_serve(self):
+ self.mock_client.serve_virtual_disks_to_host.return_value = []
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_wwpns = [port.PortName.replace('-', '').lower() for port
+ in PORTS
+ if port.PortMode == 'Initiator']
+ connector = {
+ 'host': client.HostName,
+ 'wwpns': initiator_wwpns,
+ }
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.initialize_connection,
+ volume,
+ connector)
diff --git a/cinder/tests/unit/volume/drivers/datacore/test_datacore_iscsi.py b/cinder/tests/unit/volume/drivers/datacore/test_datacore_iscsi.py
new file mode 100644
index 00000000000..4530be3eb5f
--- /dev/null
+++ b/cinder/tests/unit/volume/drivers/datacore/test_datacore_iscsi.py
@@ -0,0 +1,515 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Unit tests for the iSCSI Driver for DataCore SANsymphony storage array."""
+
+import mock
+
+from cinder import exception as cinder_exception
+from cinder import test
+from cinder.tests.unit.volume.drivers.datacore import test_datacore_driver
+from cinder.tests.unit.volume.drivers.datacore import test_datacore_passwd
+from cinder.volume.drivers.datacore import exception as datacore_exception
+from cinder.volume.drivers.datacore import iscsi
+
+
+ISCSI_PORT_STATE_INFO_READY = mock.Mock(
+ PortalsState=mock.Mock(
+ PortalStateInfo=[mock.Mock(State='Ready')]
+ )
+)
+
+ISCSI_PORT_CONFIG_INFO = mock.Mock(
+ PortalsConfig=mock.Mock(
+ iScsiPortalConfigInfo=[mock.Mock(
+ Address=mock.Mock(Address='127.0.0.1'), TcpPort='3260')]
+ )
+)
+
+PORTS = [
+ mock.Mock(Id='initiator_port_id1',
+ PortType='iSCSI',
+ PortMode='Initiator',
+ PortName='iqn.1993-08.org.debian:1:1',
+ HostId='client_id1'),
+ mock.Mock(Id='initiator_port_id2',
+ PortType='iSCSI',
+ PortMode='Initiator',
+ PortName='iqn.1993-08.org.debian:1:2'),
+ mock.Mock(__class__=mock.Mock(__name__='ServeriScsiPortData'),
+ Id='target_port_id1',
+ PortType='iSCSI',
+ PortMode='Target',
+ PortName='iqn.2000-08.com.datacore:server-1-1',
+ HostId='server_id1',
+ PresenceStatus='Present',
+ ServerPortProperties=mock.Mock(Role="Frontend",
+ Authentication='None'),
+ IScsiPortStateInfo=ISCSI_PORT_STATE_INFO_READY,
+ PortConfigInfo=ISCSI_PORT_CONFIG_INFO),
+ mock.Mock(Id='target_port_id2',
+ PortType='iSCSI',
+ PortMode='Target',
+ PortName='iqn.2000-08.com.datacore:server-1-2',
+ HostId='server_id1',
+ PresenceStatus='Present',
+ ServerPortProperties=mock.Mock(Role="Frontend",
+ Authentication='None'),
+ IScsiPortStateInfo=ISCSI_PORT_STATE_INFO_READY,
+ PortConfigInfo=ISCSI_PORT_CONFIG_INFO),
+]
+
+LOGICAL_UNITS = [
+ mock.Mock(VirtualTargetDeviceId='target_device_id1',
+ Lun=mock.Mock(Quad=4)),
+ mock.Mock(VirtualTargetDeviceId='target_device_id2',
+ Lun=mock.Mock(Quad=3)),
+ mock.Mock(VirtualTargetDeviceId='target_device_id3',
+ Lun=mock.Mock(Quad=2)),
+ mock.Mock(VirtualTargetDeviceId='target_device_id4',
+ Lun=mock.Mock(Quad=1)),
+]
+
+TARGET_DEVICES = [
+ mock.Mock(Id='target_device_id1',
+ TargetPortId='target_port_id1',
+ InitiatorPortId='initiator_port_id1'),
+ mock.Mock(Id='target_device_id2',
+ TargetPortId='target_port_id2',
+ InitiatorPortId='initiator_port_id1'),
+ mock.Mock(Id='target_device_id3',
+ TargetPortId='target_port_id2',
+ InitiatorPortId='initiator_port_id1'),
+ mock.Mock(Id='target_device_id4',
+ TargetPortId='target_port_id2',
+ InitiatorPortId='initiator_port_id2'),
+]
+
+
+class ISCSIVolumeDriverTestCase(
+ test_datacore_driver.DataCoreVolumeDriverTestCase, test.TestCase):
+ """Tests for the iSCSI Driver for DataCore SANsymphony storage array."""
+
+ def setUp(self):
+ super(ISCSIVolumeDriverTestCase, self).setUp()
+ self.mock_client.get_ports.return_value = PORTS
+ (self.mock_client.build_scsi_port_nexus_data
+ .side_effect) = self._build_nexus_data
+ self.mock_client.map_logical_disk.side_effect = self._map_logical_disk
+
+ @staticmethod
+ def _build_nexus_data(initiator_port_id, target_port_id):
+ return mock.Mock(InitiatorPortId=initiator_port_id,
+ TargetPortId=target_port_id)
+
+ @staticmethod
+ def _map_logical_disk(logical_disk_id, nexus, *args):
+ target_device_id = next((
+ device.Id for device in TARGET_DEVICES
+ if device.TargetPortId == nexus.TargetPortId
+ and device.InitiatorPortId == nexus.InitiatorPortId), None)
+ return next(unit for unit in LOGICAL_UNITS
+ if unit.VirtualTargetDeviceId == target_device_id)
+
+ @staticmethod
+ def init_driver(config):
+ driver = iscsi.ISCSIVolumeDriver(configuration=config)
+ driver.do_setup(None)
+ return driver
+
+ @staticmethod
+ def create_configuration():
+ config = super(ISCSIVolumeDriverTestCase,
+ ISCSIVolumeDriverTestCase).create_configuration()
+ config.append_config_values(iscsi.datacore_iscsi_opts)
+ return config
+
+ def test_do_setup_failed(self):
+ super(ISCSIVolumeDriverTestCase, self).test_do_setup_failed()
+
+ config = self.setup_default_configuration()
+ config.datacore_iscsi_chap_enabled = True
+ config.datacore_iscsi_chap_storage = None
+ self.assertRaises(cinder_exception.InvalidInput,
+ self.init_driver,
+ config)
+
+ def test_validate_connector(self):
+ driver = self.init_driver(self.setup_default_configuration())
+ connector = {
+ 'host': 'host_name',
+ 'initiator': 'iqn.1993-08.org.debian:1:1',
+ }
+ driver.validate_connector(connector)
+
+ def test_validate_connector_failed(self):
+ driver = self.init_driver(self.setup_default_configuration())
+ connector = {}
+ self.assertRaises(cinder_exception.InvalidConnectorException,
+ driver.validate_connector,
+ connector)
+
+ connector = {'host': 'host_name'}
+ self.assertRaises(cinder_exception.InvalidConnectorException,
+ driver.validate_connector,
+ connector)
+
+ connector = {'initiator': 'iqn.1993-08.org.debian:1:1'}
+ self.assertRaises(cinder_exception.InvalidConnectorException,
+ driver.validate_connector,
+ connector)
+
+ def test_initialize_connection(self):
+ self.mock_client.get_logical_units.return_value = []
+ self.mock_client.get_target_domains.return_value = []
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ result = driver.initialize_connection(volume, connector)
+ self.assertEqual('iscsi', result['driver_volume_type'])
+
+ target_iqn = [port.PortName for port
+ in PORTS
+ if port.PortMode == 'Target']
+ self.assertIn(result['data']['target_iqn'], target_iqn)
+
+ target_iqn = result['data']['target_iqn']
+ target_port = next((
+ port for port
+ in PORTS
+ if port.PortName == target_iqn), None)
+ target_device_id = next((
+ device.Id for device
+ in TARGET_DEVICES
+ if device.TargetPortId == target_port.Id), None)
+ target_lun = next((
+ unit.Lun.Quad for unit
+ in LOGICAL_UNITS
+ if unit.VirtualTargetDeviceId == target_device_id), None)
+ self.assertEqual(target_lun, result['data']['target_lun'])
+
+ self.assertEqual('127.0.0.1:3260', result['data']['target_portal'])
+ self.assertFalse(result['data']['target_discovered'])
+ self.assertEqual(volume['id'], result['data']['volume_id'])
+ self.assertEqual('rw', result['data']['access_mode'])
+
+ def test_initialize_connection_unknown_client(self):
+ client = test_datacore_driver.CLIENTS[0]
+ self.mock_client.register_client.return_value = client
+ (self.mock_client.get_clients
+ .return_value) = test_datacore_driver.CLIENTS[1:]
+ self.mock_client.get_logical_units.return_value = []
+ self.mock_client.get_target_domains.return_value = []
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ result = driver.initialize_connection(volume, connector)
+ self.assertEqual('iscsi', result['driver_volume_type'])
+
+ target_iqn = [port.PortName for port
+ in PORTS
+ if port.PortMode == 'Target']
+ self.assertIn(result['data']['target_iqn'], target_iqn)
+
+ target_iqn = result['data']['target_iqn']
+ target_port = next((
+ port for port
+ in PORTS
+ if port.PortName == target_iqn), None)
+ target_device_id = next((
+ device.Id for device
+ in TARGET_DEVICES
+ if device.TargetPortId == target_port.Id), None)
+ target_lun = next((
+ unit.Lun.Quad for unit
+ in LOGICAL_UNITS
+ if unit.VirtualTargetDeviceId == target_device_id), None)
+ self.assertEqual(target_lun, result['data']['target_lun'])
+
+ self.assertEqual('127.0.0.1:3260', result['data']['target_portal'])
+ self.assertFalse(result['data']['target_discovered'])
+ self.assertEqual(volume['id'], result['data']['volume_id'])
+ self.assertEqual('rw', result['data']['access_mode'])
+
+ def test_initialize_connection_unknown_initiator(self):
+ self.mock_client.register_port.return_value = PORTS[0]
+ self.mock_client.get_ports.return_value = PORTS[1:]
+ self.mock_client.get_logical_units.return_value = []
+ self.mock_client.get_target_domains.return_value = []
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ result = driver.initialize_connection(volume, connector)
+ self.assertEqual('iscsi', result['driver_volume_type'])
+
+ target_iqn = [port.PortName for port
+ in PORTS
+ if port.PortMode == 'Target']
+ self.assertIn(result['data']['target_iqn'], target_iqn)
+
+ target_iqn = result['data']['target_iqn']
+ target_port = next((
+ port for port
+ in PORTS
+ if port.PortName == target_iqn), None)
+ target_device_id = next((
+ device.Id for device
+ in TARGET_DEVICES
+ if device.TargetPortId == target_port.Id), None)
+ target_lun = next((
+ unit.Lun.Quad for unit
+ in LOGICAL_UNITS
+ if unit.VirtualTargetDeviceId == target_device_id), None)
+ self.assertEqual(target_lun, result['data']['target_lun'])
+
+ self.assertEqual('127.0.0.1:3260', result['data']['target_portal'])
+ self.assertFalse(result['data']['target_discovered'])
+ self.assertEqual(volume['id'], result['data']['volume_id'])
+ self.assertEqual('rw', result['data']['access_mode'])
+
+ def test_initialize_connection_failed_not_found(self):
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = 'wrong_virtual_disk_id'
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.initialize_connection,
+ volume,
+ connector)
+
+ def test_initialize_connection_failed_target_not_found(self):
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ config = self.setup_default_configuration()
+ config.datacore_iscsi_unallowed_targets = [
+ port.PortName for port in PORTS if port.PortMode == 'Target'
+ ]
+ driver = self.init_driver(config)
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.initialize_connection,
+ volume,
+ connector)
+
+ def test_initialize_connection_failed_on_map(self):
+ def fail_with_datacore_fault(*args):
+ raise datacore_exception.DataCoreFaultException(
+ reason="General error.")
+
+ (self.mock_client.map_logical_disk
+ .side_effect) = fail_with_datacore_fault
+ self.mock_client.get_logical_units.return_value = []
+ self.mock_client.get_target_domains.return_value = []
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ self.assertRaises(datacore_exception.DataCoreFaultException,
+ driver.initialize_connection,
+ volume,
+ connector)
+
+ def test_initialize_connection_chap(self):
+ mock_file_storage = self.mock_object(iscsi.passwd, 'FileStorage')
+ mock_file_storage.return_value = test_datacore_passwd.FakeFileStorage()
+ target_port = mock.Mock(
+ Id='target_port_id1',
+ PortType='iSCSI',
+ PortMode='Target',
+ PortName='iqn.2000-08.com.datacore:server-1-1',
+ HostId='server_id1',
+ PresenceStatus='Present',
+ ServerPortProperties=mock.Mock(Role="Frontend",
+ Authentication='None'),
+ IScsiPortStateInfo=ISCSI_PORT_STATE_INFO_READY,
+ PortConfigInfo=ISCSI_PORT_CONFIG_INFO,
+ iSCSINodes=mock.Mock(Node=[]))
+ ports = PORTS[:2]
+ ports.append(target_port)
+ self.mock_client.get_ports.return_value = ports
+ self.mock_client.get_logical_units.return_value = []
+ self.mock_client.get_target_domains.return_value = []
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ config = self.setup_default_configuration()
+ config.datacore_iscsi_chap_enabled = True
+ config.datacore_iscsi_chap_storage = 'fake_file_path'
+ driver = self.init_driver(config)
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ result = driver.initialize_connection(volume, connector)
+ self.assertEqual('iscsi', result['driver_volume_type'])
+
+ target_iqn = [port.PortName for port
+ in PORTS
+ if port.PortMode == 'Target']
+ self.assertIn(result['data']['target_iqn'], target_iqn)
+
+ target_iqn = result['data']['target_iqn']
+ target_port = next((
+ port for port
+ in PORTS
+ if port.PortName == target_iqn), None)
+ target_device_id = next((
+ device.Id for device
+ in TARGET_DEVICES
+ if device.TargetPortId == target_port.Id), None)
+ target_lun = next((
+ unit.Lun.Quad for unit
+ in LOGICAL_UNITS
+ if unit.VirtualTargetDeviceId == target_device_id), None)
+ self.assertEqual(target_lun, result['data']['target_lun'])
+
+ self.assertEqual('127.0.0.1:3260', result['data']['target_portal'])
+ self.assertFalse(result['data']['target_discovered'])
+ self.assertEqual(volume['id'], result['data']['volume_id'])
+ self.assertEqual('rw', result['data']['access_mode'])
+ self.assertEqual('CHAP', result['data']['auth_method'])
+ self.assertEqual(initiator_iqn, result['data']['auth_username'])
+ self.assertIsNotNone(result['data']['auth_password'])
+
+ def test_initialize_connection_chap_failed_check(self):
+ target_port = mock.Mock(
+ __class__=mock.Mock(__name__='ServeriScsiPortData'),
+ Id='target_port_id2',
+ PortType='iSCSI',
+ PortMode='Target',
+ PortName='iqn.2000-08.com.datacore:server-1-2',
+ HostId='server_id1',
+ PresenceStatus='Present',
+ ServerPortProperties=mock.Mock(Role="Frontend",
+ Authentication='CHAP'),
+ IScsiPortStateInfo=ISCSI_PORT_STATE_INFO_READY,
+ PortConfigInfo=ISCSI_PORT_CONFIG_INFO)
+ ports = PORTS[:2]
+ ports.append(target_port)
+ self.mock_client.get_ports.return_value = ports
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+ self.mock_client.get_logical_units.return_value = LOGICAL_UNITS
+ self.mock_client.get_target_domains.return_value = []
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ driver = self.init_driver(self.setup_default_configuration())
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ self.assertRaises(cinder_exception.VolumeDriverException,
+ driver.initialize_connection,
+ volume,
+ connector)
+
+ def test_initialize_connection_chap_failed_on_set_port_properties(self):
+ def fail_with_datacore_fault(*args):
+ raise datacore_exception.DataCoreFaultException(
+ reason="General error.")
+
+ mock_file_storage = self.mock_object(iscsi.passwd, 'FileStorage')
+ mock_file_storage.return_value = test_datacore_passwd.FakeFileStorage()
+ target_port = mock.Mock(
+ __class__=mock.Mock(__name__='ServeriScsiPortData'),
+ Id='target_port_id1',
+ PortType='iSCSI',
+ PortMode='Target',
+ PortName='iqn.2000-08.com.datacore:server-1-1',
+ HostId='server_id1',
+ PresenceStatus='Present',
+ ServerPortProperties=mock.Mock(Role="Frontend",
+ Authentication='None'),
+ IScsiPortStateInfo=ISCSI_PORT_STATE_INFO_READY,
+ PortConfigInfo=ISCSI_PORT_CONFIG_INFO,
+ iSCSINodes=mock.Mock(Node=[]))
+ ports = PORTS[:2]
+ ports.append(target_port)
+ self.mock_client.get_ports.return_value = ports
+ (self.mock_client.set_server_port_properties
+ .side_effect) = fail_with_datacore_fault
+ self.mock_client.get_logical_units.return_value = []
+ self.mock_client.get_target_domains.return_value = []
+ self.mock_client.get_target_devices.return_value = TARGET_DEVICES
+
+ virtual_disk = test_datacore_driver.VIRTUAL_DISKS[0]
+ client = test_datacore_driver.CLIENTS[0]
+ config = self.setup_default_configuration()
+ config.datacore_iscsi_chap_enabled = True
+ config.datacore_iscsi_chap_storage = 'fake_file_path'
+ driver = self.init_driver(config)
+ volume = test_datacore_driver.VOLUME.copy()
+ volume['provider_location'] = virtual_disk.Id
+ initiator_iqn = PORTS[0].PortName
+ connector = {
+ 'host': client.HostName,
+ 'initiator': initiator_iqn
+ }
+ self.assertRaises(datacore_exception.DataCoreFaultException,
+ driver.initialize_connection,
+ volume,
+ connector)
diff --git a/cinder/tests/unit/volume/drivers/datacore/test_datacore_passwd.py b/cinder/tests/unit/volume/drivers/datacore/test_datacore_passwd.py
new file mode 100644
index 00000000000..62978ffcf49
--- /dev/null
+++ b/cinder/tests/unit/volume/drivers/datacore/test_datacore_passwd.py
@@ -0,0 +1,283 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Unit tests for the password storage."""
+
+import collections
+import json
+import os
+import stat
+
+import mock
+import six
+
+from cinder import test
+from cinder.volume.drivers.datacore import passwd
+
+
+class FakeFileStorage(object):
+ """Mock FileStorage class."""
+ def __init__(self):
+ self._storage = {
+ 'resource1': {
+ 'user1': 'resource1-user1',
+ 'user2': 'resource1-user2',
+ },
+ 'resource2': {
+ 'user1': 'resource2-user1',
+ }
+ }
+
+ def open(self):
+ return self
+
+ def load(self):
+ return self._storage
+
+ def save(self, storage):
+ self._storage = storage
+
+ def close(self):
+ pass
+
+
+class PasswordFileStorageTestCase(test.TestCase):
+ """Tests for the password storage."""
+
+ def test_get_password(self):
+ fake_file_storage = FakeFileStorage()
+ passwords = fake_file_storage.load()
+ resource = six.next(six.iterkeys(passwords))
+ user, expected = six.next(six.iteritems(passwords[resource]))
+
+ self._mock_file_storage(fake_file_storage)
+ password_storage = passwd.PasswordFileStorage('fake_file_path')
+
+ result = password_storage.get_password(resource, user)
+ self.assertEqual(expected, result)
+
+ result = password_storage.get_password(resource.upper(), user)
+ self.assertIsNone(result)
+
+ def test_set_password(self):
+ fake_file_storage = FakeFileStorage()
+ user = 'user3'
+ resource1 = 'resource2'
+ password1 = 'resource2-user3'
+ resource2 = 'resource3'
+ password2 = 'resource3-user3'
+
+ self._mock_file_storage(fake_file_storage)
+ password_storage = passwd.PasswordFileStorage('fake_file_path')
+
+ password_storage.set_password(resource1, user, password1)
+ passwords = fake_file_storage.load()
+ self.assertIn(resource1, passwords)
+ self.assertIn(user, passwords[resource1])
+ self.assertEqual(password1, passwords[resource1][user])
+
+ password_storage.set_password(resource2, user, password2)
+ passwords = fake_file_storage.load()
+ self.assertIn(resource2, passwords)
+ self.assertIn(user, passwords[resource2])
+ self.assertEqual(password2, passwords[resource2][user])
+
+ def test_delete_password(self):
+ fake_file_storage = FakeFileStorage()
+ passwords = fake_file_storage.load()
+ resource1, resource2 = 'resource1', 'resource2'
+ user1 = six.next(six.iterkeys(passwords[resource1]))
+ user2 = six.next(six.iterkeys(passwords[resource2]))
+
+ self._mock_file_storage(fake_file_storage)
+ password_storage = passwd.PasswordFileStorage('fake_file_path')
+
+ password_storage.delete_password(resource1, user1)
+ passwords = fake_file_storage.load()
+ self.assertIn(resource1, passwords)
+ self.assertNotIn(user1, passwords[resource1])
+
+ password_storage.delete_password(resource2, user2)
+ passwords = fake_file_storage.load()
+ self.assertNotIn(resource2, passwords)
+
+ def _mock_file_storage(self, fake_file_storage):
+ self.mock_object(passwd, 'FileStorage', return_value=fake_file_storage)
+
+
+class FileStorageTestCase(test.TestCase):
+ """Test for the file storage."""
+
+ def test_open(self):
+ fake_file_path = 'file_storage.data'
+ self.mock_object(passwd.os.path, 'isfile', return_value=True)
+ self.mock_object(passwd.os.path, 'isdir', return_value=True)
+ mock_open = self.mock_object(passwd, 'open', mock.mock_open())
+
+ file_storage = passwd.FileStorage(fake_file_path)
+ file_storage.open()
+ mock_open.assert_called_once_with(fake_file_path, 'r+')
+
+ def test_open_not_existing(self):
+ fake_file_path = '/fake_path/file_storage.data'
+ fake_dir_name = os.path.dirname(fake_file_path)
+ mock_chmod_calls = [
+ mock.call(fake_dir_name,
+ stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP),
+ mock.call(fake_file_path, stat.S_IRUSR | stat.S_IWUSR)
+ ]
+ mock_open_calls = [
+ mock.call(fake_file_path, 'w'),
+ mock.call(fake_file_path, 'r+'),
+ ]
+
+ self.mock_object(passwd.os.path, 'isfile', return_value=False)
+ self.mock_object(passwd.os.path, 'isdir', return_value=False)
+ mock_makedirs = self.mock_object(passwd.os, 'makedirs')
+ mock_chmod = self.mock_object(passwd.os, 'chmod')
+ mock_open = self.mock_object(
+ passwd, 'open', return_value=mock.MagicMock())
+
+ file_storage = passwd.FileStorage(fake_file_path)
+ file_storage.open()
+ mock_makedirs.assert_called_with(fake_dir_name)
+ mock_chmod.assert_has_calls(mock_chmod_calls, any_order=True)
+ mock_open.assert_has_calls(mock_open_calls, any_order=True)
+
+ def test_open_not_closed(self):
+ fake_file_path = 'file_storage.data'
+ fake_file = mock.MagicMock()
+ mock_open_calls = [
+ mock.call(fake_file_path, 'r+'),
+ mock.call(fake_file_path, 'r+'),
+ ]
+ self.mock_object(passwd.os.path, 'isfile', return_value=True)
+ self.mock_object(passwd.os.path, 'isdir', return_value=True)
+ mock_open = self.mock_object(passwd, 'open', return_value=fake_file)
+
+ file_storage = passwd.FileStorage(fake_file_path)
+ file_storage.open()
+ file_storage.open()
+ mock_open.assert_has_calls(mock_open_calls)
+ fake_file.close.assert_called_once_with()
+
+ def test_load(self):
+ passwords = {
+ 'resource1': {
+ 'user1': 'resource1-user1',
+ 'user2': 'resource1-user2',
+ },
+ 'resource2': {
+ 'user1': 'resource2-user1',
+ 'user2': 'resource2-user2'
+ }
+ }
+ fake_file_name = 'file_storage.data'
+ fake_file_content = json.dumps(passwords)
+ fake_file = self._get_fake_file(fake_file_content)
+ fake_os_stat = self._get_fake_os_stat(1)
+
+ self._mock_file_open(fake_file, fake_os_stat)
+
+ file_storage = passwd.FileStorage(fake_file_name)
+ file_storage.open()
+ result = file_storage.load()
+ self.assertEqual(passwords, result)
+
+ def test_load_empty_file(self):
+ fake_file_name = 'file_storage.data'
+ fake_file = self._get_fake_file()
+ fake_os_stat = self._get_fake_os_stat(0)
+
+ self._mock_file_open(fake_file, fake_os_stat)
+
+ file_storage = passwd.FileStorage(fake_file_name)
+ file_storage.open()
+ result = file_storage.load()
+ expected = {}
+ self.assertEqual(expected, result)
+
+ def test_load_malformed_file(self):
+ fake_file_name = 'file_storage.data'
+ fake_file = self._get_fake_file('[1, 2, 3]')
+ fake_os_stat = self._get_fake_os_stat(1)
+
+ self._mock_file_open(fake_file, fake_os_stat)
+
+ file_storage = passwd.FileStorage(fake_file_name)
+ file_storage.open()
+ self.assertRaises(ValueError, file_storage.load)
+
+ def test_save(self):
+ fake_file_name = 'file_storage.data'
+ fake_file = self._get_fake_file('')
+ fake_os_stat = self._get_fake_os_stat(0)
+
+ self._mock_file_open(fake_file, fake_os_stat)
+
+ passwords = {
+ 'resource1': {
+ 'user1': 'resource1-user1',
+ 'user2': 'resource1-user2',
+ },
+ 'resource2': {
+ 'user1': 'resource2-user1',
+ 'user2': 'resource2-user2'
+ }
+ }
+ fake_file_content = json.dumps(passwords)
+ file_storage = passwd.FileStorage(fake_file_name)
+ file_storage.open()
+ file_storage.save(passwords)
+ self.assertEqual(fake_file_content, fake_file.getvalue())
+
+ def test_save_not_dictionary(self):
+ fake_file_name = 'file_storage.data'
+ fake_file = self._get_fake_file('')
+ fake_os_stat = self._get_fake_os_stat(0)
+
+ self._mock_file_open(fake_file, fake_os_stat)
+
+ file_storage = passwd.FileStorage(fake_file_name)
+ file_storage.open()
+ self.assertRaises(TypeError, file_storage.save, [])
+
+ def test_close(self):
+ fake_file_name = 'file_storage.data'
+ fake_file = mock.MagicMock()
+
+ self.mock_object(passwd.os.path, 'isfile', return_value=True)
+ self.mock_object(passwd.os.path, 'isdir', return_value=True)
+ self.mock_object(passwd, 'open', return_value=fake_file)
+
+ file_storage = passwd.FileStorage(fake_file_name)
+ file_storage.open()
+ file_storage.close()
+ fake_file.close.assert_called_once_with()
+
+ def _mock_file_open(self, fake_file, fake_os_stat):
+ self.mock_object(passwd.os.path, 'isfile', return_value=True)
+ self.mock_object(passwd.os.path, 'isdir', return_value=True)
+ self.mock_object(passwd.os, 'stat', return_value=fake_os_stat)
+ self.mock_object(passwd, 'open', return_value=fake_file)
+
+ @staticmethod
+ def _get_fake_file(content=None):
+ return six.StringIO(content)
+
+ @staticmethod
+ def _get_fake_os_stat(st_size):
+ os_stat = collections.namedtuple('fake_os_stat', ['st_size'])
+ os_stat.st_size = st_size
+ return os_stat
diff --git a/cinder/tests/unit/volume/drivers/datacore/test_datacore_utils.py b/cinder/tests/unit/volume/drivers/datacore/test_datacore_utils.py
new file mode 100644
index 00000000000..e8c2c597c8e
--- /dev/null
+++ b/cinder/tests/unit/volume/drivers/datacore/test_datacore_utils.py
@@ -0,0 +1,78 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Unit tests for utilities and helper functions."""
+
+from cinder import test
+from cinder.volume.drivers.datacore import utils
+
+
+class GenericUtilsTestCase(test.TestCase):
+ """Tests for the generic utilities and helper functions."""
+
+ def test_build_network_address(self):
+ ipv4_address = '127.0.0.1'
+ ipv6_address = '::1'
+ host_name = 'localhost'
+ port = 3498
+ self.assertEqual('%s:%s' % (ipv4_address, port),
+ utils.build_network_address(ipv4_address, port))
+ self.assertEqual('[%s]:%s' % (ipv6_address, port),
+ utils.build_network_address(ipv6_address, port))
+ self.assertEqual('%s:%s' % (host_name, port),
+ utils.build_network_address(host_name, port))
+
+ def test_get_first(self):
+ disk_a = {'id': 'disk-a', 'type': 'Single', 'size': 5}
+ disk_b = {'id': 'disk-b', 'type': 'Single', 'size': 1}
+ disk_c = {'id': 'disk-c', 'type': 'Mirrored', 'size': 5}
+ disk_d = {'id': 'disk-d', 'type': 'Single', 'size': 10}
+ test_source = [disk_a, disk_b, disk_c, disk_d]
+
+ first = utils.get_first(lambda item: item['id'] == 'disk-c',
+ test_source)
+ self.assertEqual(disk_c, first)
+
+ self.assertRaises(StopIteration,
+ utils.get_first,
+ lambda item: item['type'] == 'Dual',
+ test_source)
+
+ def test_get_first_or_default(self):
+ disk_a = {'id': 'disk-a', 'type': 'Single', 'size': 5}
+ disk_b = {'id': 'disk-b', 'type': 'Single', 'size': 1}
+ disk_c = {'id': 'disk-c', 'type': 'Mirrored', 'size': 5}
+ disk_d = {'id': 'disk-d', 'type': 'Single', 'size': 10}
+ test_source = [disk_a, disk_b, disk_c, disk_d]
+
+ first = utils.get_first_or_default(lambda item: item['size'] == 1,
+ test_source,
+ None)
+ self.assertEqual(disk_b, first)
+
+ default = utils.get_first_or_default(lambda item: item['size'] == 15,
+ test_source,
+ None)
+ self.assertIsNone(default)
+
+ def test_get_distinct_by(self):
+ disk_a = {'id': 'disk-a', 'type': 'Single', 'size': 5}
+ disk_b = {'id': 'disk-b', 'type': 'Single', 'size': 1}
+ disk_c = {'id': 'disk-c', 'type': 'Mirrored', 'size': 5}
+ disk_d = {'id': 'disk-d', 'type': 'Single', 'size': 10}
+ test_source = [disk_a, disk_b, disk_c, disk_d]
+
+ distinct_values = utils.get_distinct_by(lambda item: item['type'],
+ test_source)
+ self.assertEqual([disk_a, disk_c], distinct_values)
diff --git a/cinder/volume/drivers/datacore/__init__.py b/cinder/volume/drivers/datacore/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/cinder/volume/drivers/datacore/api.py b/cinder/volume/drivers/datacore/api.py
new file mode 100644
index 00000000000..e0bc028bfbf
--- /dev/null
+++ b/cinder/volume/drivers/datacore/api.py
@@ -0,0 +1,1062 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Classes to invoke DataCore SANsymphony API."""
+
+import copy
+import sys
+import uuid
+
+from oslo_log import log as logging
+from oslo_utils import excutils
+from oslo_utils import importutils
+import retrying
+import six
+import socket
+import suds
+from suds import client as suds_client
+from suds import plugin
+from suds.sax import attribute
+from suds.sax import element
+from suds import wsdl
+from suds import wsse
+from suds import xsd
+
+from cinder.i18n import _
+from cinder import utils as cinder_utils
+from cinder.volume.drivers.datacore import exception as datacore_exceptions
+from cinder.volume.drivers.datacore import utils as datacore_utils
+
+websocket = importutils.try_import('websocket')
+
+
+LOG = logging.getLogger(__name__)
+
+
+class FaultDefinitionsFilter(plugin.DocumentPlugin):
+ """Plugin to process the DataCore API WSDL document.
+
+ The document plugin removes fault definitions for callback operations
+ from the DataCore API WSDL.
+ """
+
+ def parsed(self, context):
+ document = context.document
+ tns = self._get_tns(document)
+
+ message_qrefs = set()
+ for message in self._get_wsdl_messages(document):
+ message_qrefs.add((message.get('name'), tns[1]))
+
+ bindings = self._get_wsdl_operation_bindings(document)
+
+ for port_type in self._get_wsdl_port_types(document):
+ for operation in self._get_wsdl_operations(port_type):
+ self._filter_faults(
+ document, operation, bindings, message_qrefs, tns)
+
+ @staticmethod
+ def _get_tns(document):
+ target_namespace = document.get('targetNamespace')
+ prefix = document.findPrefix(target_namespace) or 'tns'
+ return prefix, target_namespace
+
+ @staticmethod
+ def _get_wsdl_port_types(document):
+ return document.getChildren('portType', wsdl.wsdlns)
+
+ @staticmethod
+ def _get_wsdl_operations(port_type):
+ return port_type.getChildren('operation', wsdl.wsdlns)
+
+ @staticmethod
+ def _get_wsdl_messages(document):
+ return document.getChildren('message', wsdl.wsdlns)
+
+ @staticmethod
+ def _get_wsdl_operation_bindings(document):
+ bindings = []
+ for binding in document.getChildren('binding', wsdl.wsdlns):
+ operations = {}
+ for operation in binding.getChildren('operation', wsdl.wsdlns):
+ operations[operation.get('name')] = operation
+ bindings.append(operations)
+ return bindings
+
+ @staticmethod
+ def _filter_faults(document, operation, operation_bindings,
+ message_qrefs, tns):
+ filtered_faults = {}
+ for fault in operation.getChildren('fault', wsdl.wsdlns):
+ fault_message = fault.get('message')
+ qref = xsd.qualify(fault_message, document, tns)
+ if qref not in message_qrefs:
+ filtered_faults[fault.get('name')] = fault
+ for fault in filtered_faults.values():
+ operation.remove(fault)
+ if filtered_faults:
+ for binding in operation_bindings:
+ filtered_binding_faults = []
+ faults = binding[operation.get('name')].getChildren(
+ 'fault', wsdl.wsdlns)
+ for binding_fault in faults:
+ if binding_fault.get('name') in filtered_faults:
+ filtered_binding_faults.append(binding_fault)
+ for binding_fault in filtered_binding_faults:
+ binding[operation.get('name')].remove(binding_fault)
+
+
+class DataCoreClient(object):
+ """DataCore SANsymphony client."""
+
+ API_RETRY_INTERVAL = 10
+
+ DATACORE_EXECUTIVE_PORT = '3794'
+
+ STORAGE_SERVICES = 'IStorageServices'
+ STORAGE_SERVICES_BINDING = 'CustomBinding_IStorageServices'
+
+ EXECUTIVE_SERVICE = 'IExecutiveServiceEx'
+ EXECUTIVE_SERVICE_BINDING = 'CustomBinding_IExecutiveServiceEx'
+
+ NS_WSA = ('wsa', 'http://www.w3.org/2005/08/addressing')
+ WSA_ANONYMOUS = 'http://www.w3.org/2005/08/addressing/anonymous'
+ MUST_UNDERSTAND = attribute.Attribute('SOAP-ENV:mustUnderstand', '1')
+
+ # Namespaces that are defined within DataCore API WSDL
+ NS_DATACORE_EXECUTIVE = ('http://schemas.datacontract.org/2004/07/'
+ 'DataCore.Executive')
+ NS_DATACORE_EXECUTIVE_SCSI = ('http://schemas.datacontract.org/2004/07/'
+ 'DataCore.Executive.Scsi')
+ NS_DATACORE_EXECUTIVE_ISCSI = ('http://schemas.datacontract.org/2004/07/'
+ 'DataCore.Executive.iSCSI')
+ NS_SERIALIZATION_ARRAYS = ('http://schemas.microsoft.com/2003/10/'
+ 'Serialization/Arrays')
+
+ # Fully qualified names of objects that are defined within
+ # DataCore API WSDL
+ O_ACCESS_TOKEN = '{%s}AccessToken' % NS_DATACORE_EXECUTIVE_ISCSI
+ O_ARRAY_OF_PERFORMANCE_TYPE = ('{%s}ArrayOfPerformanceType'
+ % NS_DATACORE_EXECUTIVE)
+ O_ARRAY_OF_STRING = '{%s}ArrayOfstring' % NS_SERIALIZATION_ARRAYS
+ O_CLIENT_MACHINE_TYPE = '{%s}ClientMachineType' % NS_DATACORE_EXECUTIVE
+ O_DATA_SIZE = '{%s}DataSize' % NS_DATACORE_EXECUTIVE
+ O_LOGICAL_DISK_ROLE = '{%s}LogicalDiskRole' % NS_DATACORE_EXECUTIVE
+ O_LOGICAL_UNIT_TYPE = '{%s}LogicalUnitType' % NS_DATACORE_EXECUTIVE
+ O_MIRROR_RECOVERY_PRIORITY = ('{%s}MirrorRecoveryPriority'
+ % NS_DATACORE_EXECUTIVE)
+ O_PATH_POLICY = '{%s}PathPolicy' % NS_DATACORE_EXECUTIVE
+ O_PERFORMANCE_TYPE = '{%s}PerformanceType' % NS_DATACORE_EXECUTIVE
+ O_POOL_VOLUME_TYPE = '{%s}PoolVolumeType' % NS_DATACORE_EXECUTIVE
+ O_SNAPSHOT_TYPE = '{%s}SnapshotType' % NS_DATACORE_EXECUTIVE
+ O_SCSI_MODE = '{%s}ScsiMode' % NS_DATACORE_EXECUTIVE_SCSI
+ O_SCSI_PORT_DATA = '{%s}ScsiPortData' % NS_DATACORE_EXECUTIVE
+ O_SCSI_PORT_NEXUS_DATA = '{%s}ScsiPortNexusData' % NS_DATACORE_EXECUTIVE
+ O_SCSI_PORT_TYPE = '{%s}ScsiPortType' % NS_DATACORE_EXECUTIVE_SCSI
+ O_VIRTUAL_DISK_DATA = '{%s}VirtualDiskData' % NS_DATACORE_EXECUTIVE
+ O_VIRTUAL_DISK_STATUS = '{%s}VirtualDiskStatus' % NS_DATACORE_EXECUTIVE
+ O_VIRTUAL_DISK_SUB_TYPE = '{%s}VirtualDiskSubType' % NS_DATACORE_EXECUTIVE
+ O_VIRTUAL_DISK_TYPE = '{%s}VirtualDiskType' % NS_DATACORE_EXECUTIVE
+
+ def __init__(self, host, username, password, timeout):
+ if websocket is None:
+ msg = _("Failed to import websocket-client python module."
+ " Please, ensure the module is installed.")
+ raise datacore_exceptions.DataCoreException(msg)
+
+ self.timeout = timeout
+
+ executive_service_net_addr = datacore_utils.build_network_address(
+ host, self.DATACORE_EXECUTIVE_PORT)
+ executive_service_endpoint = self._build_service_endpoint(
+ executive_service_net_addr, self.EXECUTIVE_SERVICE)
+
+ security_options = wsse.Security()
+ username_token = wsse.UsernameToken(username, password)
+ security_options.tokens.append(username_token)
+
+ self._executive_service_client = suds_client.Client(
+ executive_service_endpoint['http_endpoint'] + '?singlewsdl',
+ nosend=True,
+ timeout=self.timeout,
+ wsse=security_options,
+ plugins=[FaultDefinitionsFilter()])
+
+ self._update_storage_services_endpoint(executive_service_endpoint)
+
+ storage_services_endpoint = self._get_storage_services_endpoint()
+
+ self._storage_services_client = suds_client.Client(
+ storage_services_endpoint['http_endpoint'] + '?singlewsdl',
+ nosend=True,
+ timeout=self.timeout,
+ wsse=security_options,
+ plugins=[FaultDefinitionsFilter()])
+
+ self._update_executive_service_endpoints(storage_services_endpoint)
+
+ @staticmethod
+ def _get_list_data(obj, attribute_name):
+ return getattr(obj, attribute_name, [])
+
+ @staticmethod
+ def _build_service_endpoint(network_address, path):
+ return {
+ 'network_address': network_address,
+ 'http_endpoint': '%s://%s/%s' % ('http', network_address, path),
+ 'ws_endpoint': '%s://%s/%s' % ('ws', network_address, path),
+ }
+
+ @cinder_utils.synchronized('datacore-api-request_context')
+ def _get_soap_context(self, service_client, service_binding, method,
+ message_id, *args, **kwargs):
+ soap_action = (service_client.wsdl.services[0].port(service_binding)
+ .methods[method].soap.action)
+
+ soap_headers = self._get_soap_headers(soap_action, message_id)
+
+ service_client.set_options(soapheaders=soap_headers)
+ context = service_client.service[service_binding][method](
+ *args, **kwargs)
+
+ return context
+
+ def _get_soap_headers(self, soap_action, message_id):
+ headers = [
+ element.Element('Action', ns=self.NS_WSA)
+ .setText(soap_action.replace('"', ''))
+ .append(self.MUST_UNDERSTAND),
+
+ element.Element('To', ns=self.NS_WSA)
+ .setText(self.WSA_ANONYMOUS)
+ .append(self.MUST_UNDERSTAND),
+
+ element.Element('MessageID', ns=self.NS_WSA)
+ .setText(message_id),
+
+ element.Element('ReplyTo', ns=self.NS_WSA)
+ .insert(element.Element('Address', ns=self.NS_WSA)
+ .setText(self.WSA_ANONYMOUS)),
+ ]
+ return headers
+
+ def _process_request(self, service_client, service_binding,
+ service_endpoint, method, *args, **kwargs):
+ message_id = uuid.uuid4().urn
+
+ context = self._get_soap_context(
+ service_client, service_binding,
+ method, message_id, *args, **kwargs)
+
+ channel = None
+ try:
+ channel = websocket.create_connection(
+ service_endpoint,
+ timeout=self.timeout,
+ subprotocols=['soap'],
+ header=['soap-content-type: text/xml'])
+ channel.send(context.envelope)
+ response = channel.recv()
+ if isinstance(response, six.text_type):
+ response = response.encode('utf-8')
+ return context.process_reply(response)
+ except (socket.error, websocket.WebSocketException) as e:
+ traceback = sys.exc_info()[2]
+ error = datacore_exceptions.DataCoreConnectionException(reason=e)
+ six.reraise(datacore_exceptions.DataCoreConnectionException,
+ error,
+ traceback)
+ except suds.WebFault as e:
+ traceback = sys.exc_info()[2]
+ fault = datacore_exceptions.DataCoreFaultException(reason=e)
+ six.reraise(datacore_exceptions.DataCoreFaultException,
+ fault,
+ traceback)
+ finally:
+ if channel and channel.connected:
+ try:
+ channel.close()
+ except (socket.error, websocket.WebSocketException) as e:
+ LOG.debug("Closing a connection to "
+ "DataCore server failed. %s", e)
+
+ def _invoke_storage_services(self, method, *args, **kwargs):
+
+ @retrying.retry(
+ retry_on_exception=lambda e:
+ isinstance(e, datacore_exceptions.DataCoreConnectionException),
+ wait_fixed=self.API_RETRY_INTERVAL * 1000,
+ stop_max_delay=self.timeout * 1000)
+ def retry_call():
+ storage_services_endpoint = self._get_storage_services_endpoint()
+ try:
+ result = self._process_request(
+ self._storage_services_client,
+ self.STORAGE_SERVICES_BINDING,
+ storage_services_endpoint['ws_endpoint'],
+ method, *args, **kwargs)
+ return result
+ except datacore_exceptions.DataCoreConnectionException:
+ with excutils.save_and_reraise_exception():
+ self._update_api_endpoints()
+
+ return retry_call()
+
+ def _update_api_endpoints(self):
+ executive_service_endpoints = self._get_executive_service_endpoints()
+ for endpoint in executive_service_endpoints:
+ try:
+ self._update_storage_services_endpoint(endpoint)
+ break
+ except datacore_exceptions.DataCoreConnectionException as e:
+ LOG.warning("Failed to update DataCore Server Group "
+ "endpoints. %s.", e)
+
+ storage_services_endpoint = self._get_storage_services_endpoint()
+ try:
+ self._update_executive_service_endpoints(
+ storage_services_endpoint)
+ except datacore_exceptions.DataCoreConnectionException as e:
+ LOG.warning("Failed to update DataCore Server Group "
+ "endpoints. %s.", e)
+
+ @cinder_utils.synchronized('datacore-api-storage_services_endpoint')
+ def _get_storage_services_endpoint(self):
+ if self._storage_services_endpoint:
+ return copy.copy(self._storage_services_endpoint)
+ return None
+
+ @cinder_utils.synchronized('datacore-api-storage_services_endpoint')
+ def _update_storage_services_endpoint(self, executive_service_endpoint):
+ controller_address = self._process_request(
+ self._executive_service_client,
+ self.EXECUTIVE_SERVICE_BINDING,
+ executive_service_endpoint['ws_endpoint'],
+ 'GetControllerAddress')
+
+ if not controller_address:
+ msg = _("Could not determine controller node.")
+ raise datacore_exceptions.DataCoreConnectionException(reason=msg)
+
+ controller_host = controller_address.rsplit(':', 1)[0].strip('[]')
+ controller_net_addr = datacore_utils.build_network_address(
+ controller_host,
+ self.DATACORE_EXECUTIVE_PORT)
+
+ self._storage_services_endpoint = self._build_service_endpoint(
+ controller_net_addr,
+ self.STORAGE_SERVICES)
+
+ @cinder_utils.synchronized('datacore-api-executive_service_endpoints')
+ def _get_executive_service_endpoints(self):
+ if self._executive_service_endpoints:
+ return self._executive_service_endpoints[:]
+ return []
+
+ @cinder_utils.synchronized('datacore-api-executive_service_endpoints')
+ def _update_executive_service_endpoints(self, storage_services_endpoint):
+ endpoints = []
+ nodes = self._get_list_data(
+ self._process_request(self._storage_services_client,
+ self.STORAGE_SERVICES_BINDING,
+ storage_services_endpoint['ws_endpoint'],
+ 'GetNodes'),
+ 'RegionNodeData')
+
+ if not nodes:
+ msg = _("Could not determine executive nodes.")
+ raise datacore_exceptions.DataCoreConnectionException(reason=msg)
+
+ for node in nodes:
+ host = node.HostAddress.rsplit(':', 1)[0].strip('[]')
+ endpoint = self._build_service_endpoint(
+ datacore_utils.build_network_address(
+ host, self.DATACORE_EXECUTIVE_PORT),
+ self.EXECUTIVE_SERVICE)
+ endpoints.append(endpoint)
+
+ self._executive_service_endpoints = endpoints
+
+ def get_server_groups(self):
+ """Get all the server groups in the configuration.
+
+ :return: A list of server group data.
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetServerGroups'),
+ 'ServerHostGroupData')
+
+ def get_servers(self):
+ """Get all the server hosts in the configuration.
+
+ :return: A list of server host data
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetServers'),
+ 'ServerHostData')
+
+ def get_disk_pools(self):
+ """Get all the pools in the server group.
+
+ :return: A list of disk pool data
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetDiskPools'),
+ 'DiskPoolData')
+
+ def get_logical_disks(self):
+ """Get all the logical disks defined in the system.
+
+ :return: A list of logical disks
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetLogicalDisks'),
+ 'LogicalDiskData')
+
+ def create_pool_logical_disk(self, pool_id, pool_volume_type, size,
+ min_quota=None, max_quota=None):
+ """Create the pool logical disk.
+
+ :param pool_id: Pool id
+ :param pool_volume_type: Type, either striped or spanned
+ :param size: Size
+ :param min_quota: Min quota
+ :param max_quota: Max quota
+ :return: New logical disk data
+ """
+
+ volume_type = getattr(self._storage_services_client.factory
+ .create(self.O_POOL_VOLUME_TYPE),
+ pool_volume_type)
+
+ data_size = (self._storage_services_client.factory
+ .create(self.O_DATA_SIZE))
+ data_size.Value = size
+
+ data_size_min_quota = None
+ if min_quota:
+ data_size_min_quota = (self._storage_services_client.factory
+ .create(self.O_DATA_SIZE))
+ data_size_min_quota.Value = min_quota
+
+ data_size_max_quota = None
+ if max_quota:
+ data_size_max_quota = (self._storage_services_client.factory
+ .create(self.O_DATA_SIZE))
+ data_size_max_quota.Value = max_quota
+
+ return self._invoke_storage_services('CreatePoolLogicalDisk',
+ poolId=pool_id,
+ type=volume_type,
+ size=data_size,
+ minQuota=data_size_min_quota,
+ maxQuota=data_size_max_quota)
+
+ def delete_logical_disk(self, logical_disk_id):
+ """Delete the logical disk.
+
+ :param logical_disk_id: Logical disk id
+ """
+
+ self._invoke_storage_services('DeleteLogicalDisk',
+ logicalDiskId=logical_disk_id)
+
+ def get_logical_disk_chunk_allocation_map(self, logical_disk_id):
+ """Get the logical disk chunk allocation map.
+
+ The logical disk allocation map details all the physical disk chunks
+ that are currently allocated to this logical disk.
+
+ :param logical_disk_id: Logical disk id
+ :return: A list of member allocation maps, restricted to chunks
+ allocated on to this logical disk
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetLogicalDiskChunkAllocationMap',
+ logicalDiskId=logical_disk_id),
+ 'MemberAllocationInfoData')
+
+ def get_next_virtual_disk_alias(self, base_alias):
+ """Get the next available (unused) virtual disk alias.
+
+ :param base_alias: Base string of the new alias
+ :return: New alias
+ """
+
+ return self._invoke_storage_services('GetNextVirtualDiskAlias',
+ baseAlias=base_alias)
+
+ def get_virtual_disks(self):
+ """Get all the virtual disks in the configuration.
+
+ :return: A list of virtual disk's data
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetVirtualDisks'),
+ 'VirtualDiskData')
+
+ def build_virtual_disk_data(self, virtual_disk_alias, virtual_disk_type,
+ size, description, storage_profile_id):
+ """Create VirtualDiskData object.
+
+ :param virtual_disk_alias: User-visible alias of the virtual disk,
+ which must be unique
+ :param virtual_disk_type: Virtual disk type
+ :param size: Virtual disk size
+ :param description: A user-readable description of the virtual disk
+ :param storage_profile_id: Virtual disk storage profile
+ :return: VirtualDiskData object
+ """
+
+ vd_data = (self._storage_services_client.factory
+ .create(self.O_VIRTUAL_DISK_DATA))
+ vd_data.Size = (self._storage_services_client.factory
+ .create(self.O_DATA_SIZE))
+ vd_data.Size.Value = size
+ vd_data.Alias = virtual_disk_alias
+ vd_data.Description = description
+ vd_data.Type = getattr(self._storage_services_client.factory
+ .create(self.O_VIRTUAL_DISK_TYPE),
+ virtual_disk_type)
+ vd_data.SubType = getattr(self._storage_services_client.factory
+ .create(self.O_VIRTUAL_DISK_SUB_TYPE),
+ 'Standard')
+ vd_data.DiskStatus = getattr(self._storage_services_client.factory
+ .create(self.O_VIRTUAL_DISK_STATUS),
+ 'Online')
+ vd_data.RecoveryPriority = getattr(
+ self._storage_services_client.factory
+ .create(self.O_MIRROR_RECOVERY_PRIORITY),
+ 'Unset')
+ vd_data.StorageProfileId = storage_profile_id
+
+ return vd_data
+
+ def create_virtual_disk_ex2(self, virtual_disk_data, first_logical_disk_id,
+ second_logical_disk_id, add_redundancy):
+ """Create a virtual disk specifying the both logical disks.
+
+ :param virtual_disk_data: Virtual disk's properties
+ :param first_logical_disk_id: Id of the logical disk to use
+ :param second_logical_disk_id: Id of the second logical disk to use
+ :param add_redundancy: If True, the mirror has redundant mirror paths
+ :return: New virtual disk's data
+ """
+
+ return self._invoke_storage_services(
+ 'CreateVirtualDiskEx2',
+ virtualDisk=virtual_disk_data,
+ firstLogicalDiskId=first_logical_disk_id,
+ secondLogicalDiskId=second_logical_disk_id,
+ addRedundancy=add_redundancy)
+
+ def set_virtual_disk_size(self, virtual_disk_id, size):
+ """Change the size of a virtual disk.
+
+ :param virtual_disk_id: Id of the virtual disk
+ :param size: New size
+ :return: Virtual disk's data
+ """
+
+ data_size = (self._storage_services_client.factory
+ .create(self.O_DATA_SIZE))
+ data_size.Value = size
+
+ return self._invoke_storage_services('SetVirtualDiskSize',
+ virtualDiskId=virtual_disk_id,
+ size=data_size)
+
+ def delete_virtual_disk(self, virtual_disk_id, delete_logical_disks):
+ """Delete a virtual disk.
+
+ :param virtual_disk_id: Id of the virtual disk
+ :param delete_logical_disks: If True, delete the associated
+ logical disks
+ """
+
+ self._invoke_storage_services('DeleteVirtualDisk',
+ virtualDiskId=virtual_disk_id,
+ deleteLogicalDisks=delete_logical_disks)
+
+ def serve_virtual_disks_to_host(self, host_id, virtual_disks):
+ """Serve multiple virtual disks to a specified host.
+
+ :param host_id: Id of the host machine
+ :param virtual_disks: A list of virtual disks to serve
+ :return: A list of the virtual disks actually served to the host
+ """
+
+ virtual_disk_array = (self._storage_services_client.factory
+ .create(self.O_ARRAY_OF_STRING))
+ virtual_disk_array.string = virtual_disks
+
+ return self._get_list_data(
+ self._invoke_storage_services('ServeVirtualDisksToHost',
+ hostId=host_id,
+ virtualDisks=virtual_disk_array),
+ 'VirtualLogicalUnitData')
+
+ def unserve_virtual_disks_from_host(self, host_id, virtual_disks):
+ """Unserve multiple virtual disks from a specified host.
+
+ :param host_id: Id of the host machine
+ :param virtual_disks: A list of virtual disks to unserve
+ """
+
+ virtual_disk_array = (self._storage_services_client.factory
+ .create(self.O_ARRAY_OF_STRING))
+ virtual_disk_array.string = virtual_disks
+
+ self._invoke_storage_services('UnserveVirtualDisksFromHost',
+ hostId=host_id,
+ virtualDisks=virtual_disk_array)
+
+ def unserve_virtual_disks_from_port(self, port_id, virtual_disks):
+ """Unserve multiple virtual disks from a specified initiator port.
+
+ :param port_id: Id of the initiator port
+ :param virtual_disks: A list of virtual disks to unserve
+ """
+
+ virtual_disk_array = (self._storage_services_client.factory
+ .create(self.O_ARRAY_OF_STRING))
+ virtual_disk_array.string = virtual_disks
+
+ self._invoke_storage_services('UnserveVirtualDisksFromPort',
+ portId=port_id,
+ virtualDisks=virtual_disk_array)
+
+ def bind_logical_disk(self, virtual_disk_id, logical_disk_id, role,
+ create_mirror_mappings, create_client_mappings,
+ add_redundancy):
+ """Bind (add) a logical disk to a virtual disk.
+
+ :param virtual_disk_id: Id of the virtual disk to bind to
+ :param logical_disk_id: Id of the logical disk being bound
+ :param role: logical disk's role
+ :param create_mirror_mappings: If True, automatically create the
+ mirror mappings to this disk, assuming
+ there is already another logical disk
+ bound
+ :param create_client_mappings: If True, automatically create mappings
+ from mapped hosts to the new disk
+ :param add_redundancy: If True, the mirror has redundant mirror paths
+ :return: Updated virtual disk data
+ """
+
+ logical_disk_role = getattr(self._storage_services_client.factory
+ .create(self.O_LOGICAL_DISK_ROLE),
+ role)
+
+ return self._invoke_storage_services(
+ 'BindLogicalDisk',
+ virtualDiskId=virtual_disk_id,
+ logicalDiskId=logical_disk_id,
+ role=logical_disk_role,
+ createMirrorMappings=create_mirror_mappings,
+ createClientMappings=create_client_mappings,
+ addRedundancy=add_redundancy)
+
+ def get_snapshots(self):
+ """Get all the snapshots on all the servers in the region.
+
+ :return: A list of snapshot data.
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetSnapshots'),
+ 'SnapshotData')
+
+ def create_snapshot(self, virtual_disk_id, name, description,
+ destination_pool_id, snapshot_type,
+ duplicate_disk_id, storage_profile_id):
+ """Create a snapshot relationship.
+
+ :param virtual_disk_id: Virtual disk id
+ :param name: Name of snapshot
+ :param description: Description
+ :param destination_pool_id: Destination pool id
+ :param snapshot_type: Type of snapshot
+ :param duplicate_disk_id: If set to True then the destination virtual
+ disk's SCSI id will be a duplicate of the
+ source's
+ :param storage_profile_id: Specifies the destination virtual disk's
+ storage profile
+ :return: New snapshot data
+ """
+
+ st_type = getattr(self._storage_services_client.factory
+ .create(self.O_SNAPSHOT_TYPE),
+ snapshot_type)
+
+ return self._invoke_storage_services(
+ 'CreateSnapshot',
+ virtualDiskId=virtual_disk_id,
+ name=name,
+ description=description,
+ destinationPoolId=destination_pool_id,
+ type=st_type,
+ duplicateDiskId=duplicate_disk_id,
+ storageProfileId=storage_profile_id)
+
+ def delete_snapshot(self, snapshot_id):
+ """Delete the snapshot.
+
+ :param snapshot_id: Snapshot id
+ """
+
+ self._invoke_storage_services('DeleteSnapshot', snapshotId=snapshot_id)
+
+ def get_storage_profiles(self):
+ """Get all the all the defined storage profiles.
+
+ :return: A list of storage profiles
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetStorageProfiles'),
+ 'StorageProfileData')
+
+ def designate_map_store(self, pool_id):
+ """Designate which pool the snapshot mapstore will be allocated from.
+
+ :param pool_id: Pool id
+ :return: Updated server host data, which includes the mapstore pool id
+ """
+
+ return self._invoke_storage_services('DesignateMapStore',
+ poolId=pool_id)
+
+ def get_performance_by_type(self, performance_types):
+ """Get performance data for specific types of performance counters.
+
+ :param performance_types: A list of performance counter types
+ :return: A list of performance data points
+ """
+
+ prfm_type_array = (self._storage_services_client.factory
+ .create(self.O_ARRAY_OF_PERFORMANCE_TYPE))
+ prfm_type_array.PerformanceType = list(
+ getattr(self._storage_services_client.factory
+ .create(self.O_PERFORMANCE_TYPE),
+ performance_type)
+ for performance_type in performance_types)
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetPerformanceByType',
+ types=prfm_type_array),
+ 'CollectionPointData')
+
+ def get_ports(self):
+ """Get all ports in the configuration.
+
+ :return: A list of SCSI ports
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetPorts'),
+ 'ScsiPortData')
+
+ def build_scsi_port_data(self, host_id, port_name, port_mode, port_type):
+ """Create ScsiPortData object that represents SCSI port, of any type.
+
+ :param host_id: Id of the port's host computer
+ :param port_name: Unique name of the port.
+ :param port_mode: Mode of port: initiator or target
+ :param port_type: Type of port, Fc, iSCSI or loopback
+ :return: ScsiPortData object
+ """
+
+ scsi_port_data = (self._storage_services_client.factory
+ .create(self.O_SCSI_PORT_DATA))
+ scsi_port_data.HostId = host_id
+ scsi_port_data.PortName = port_name
+ scsi_port_data.PortMode = getattr(self._storage_services_client.factory
+ .create(self.O_SCSI_MODE),
+ port_mode)
+ scsi_port_data.PortType = getattr(self._storage_services_client.factory
+ .create(self.O_SCSI_PORT_TYPE),
+ port_type)
+
+ return scsi_port_data
+
+ def register_port(self, scsi_port_data):
+ """Register a port in the configuration.
+
+ :param scsi_port_data: Port data
+ :return: Updated port data
+ """
+
+ return self._invoke_storage_services('RegisterPort',
+ port=scsi_port_data)
+
+ def assign_port(self, client_id, port_id):
+ """Assign a port to a client.
+
+ :param client_id: Client id
+ :param port_id: Port id
+ :return: Updated port data,
+ which will now have its host id set to the client id
+ """
+
+ return self._invoke_storage_services('AssignPort',
+ clientId=client_id,
+ portId=port_id)
+
+ def set_server_port_properties(self, port_id, properties):
+ """Set a server port's properties.
+
+ :param port_id: Port id
+ :param properties: New properties
+ :return: Updated port data
+ """
+
+ return self._invoke_storage_services('SetServerPortProperties',
+ portId=port_id,
+ properties=properties)
+
+ def build_access_token(self, initiator_node_name, initiator_username,
+ initiator_password, mutual_authentication,
+ target_username, target_password):
+ """Create an AccessToken object.
+
+ :param initiator_node_name: Initiator node name
+ :param initiator_username: Initiator user name
+ :param initiator_password: Initiator password
+ :param mutual_authentication: If True the target and the initiator
+ authenticate each other.
+ A separate secret is set for each target
+ and for each initiator in the storage
+ area network (SAN).
+ :param target_username: Target user name
+ :param target_password: Target password
+ :return: AccessToken object
+ """
+
+ access_token = (self._storage_services_client.factory
+ .create(self.O_ACCESS_TOKEN))
+ access_token.InitiatorNodeName = initiator_node_name
+ access_token.InitiatorUsername = initiator_username
+ access_token.InitiatorPassword = initiator_password
+ access_token.MutualAuthentication = mutual_authentication
+ access_token.TargetUsername = target_username
+ access_token.TargetPassword = target_password
+
+ return access_token
+
+ def set_access_token(self, iscsi_port_id, access_token):
+ """Set the access token.
+
+ The access token allows access to a specific network node
+ from a specific iSCSI port.
+
+ :param iscsi_port_id: Id of the initiator iSCSI port
+ :param access_token: Access token to be validated
+ :return: Port data
+ """
+
+ return self._invoke_storage_services('SetAccessToken',
+ iScsiPortId=iscsi_port_id,
+ inputToken=access_token)
+
+ def get_clients(self):
+ """Get all the clients in the configuration.
+
+ :return: A list of client data
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetClients'),
+ 'ClientHostData')
+
+ def register_client(self, host_name, description, machine_type,
+ mode, preferred_server_ids):
+ """Register the client, creating a client object in the configuration.
+
+ :param host_name: Name of the client
+ :param description: Description
+ :param machine_type: Type of client
+ :param mode: Path policy mode of the client
+ :param preferred_server_ids: Preferred server ids
+ :return: New client data
+ """
+
+ client_machine_type = getattr(self._storage_services_client.factory
+ .create(self.O_CLIENT_MACHINE_TYPE),
+ machine_type)
+ client_mode = getattr(self._storage_services_client.factory
+ .create(self.O_PATH_POLICY),
+ mode)
+
+ return self._invoke_storage_services(
+ 'RegisterClient',
+ hostName=host_name,
+ description=description,
+ type=client_machine_type,
+ mode=client_mode,
+ preferredServerIds=preferred_server_ids)
+
+ def set_client_capabilities(self, client_id, mpio, alua):
+ """Set the client capabilities for MPIO and ALUA.
+
+ :param client_id: Client id
+ :param mpio: If set to True then MPIO-capable
+ :param alua: If set to True then ALUA-capable
+ :return: Updated client data
+ """
+
+ return self._invoke_storage_services('SetClientCapabilities',
+ clientId=client_id,
+ mpio=mpio,
+ alua=alua)
+
+ def get_target_domains(self):
+ """Get all the target domains in the configuration.
+
+ :return: A list of target domains
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetTargetDomains'),
+ 'VirtualTargetDomainData')
+
+ def create_target_domain(self, initiator_host_id, target_host_id):
+ """Create a target domain given a pair of hosts, target and initiator.
+
+ :param initiator_host_id: Id of the initiator host machine
+ :param target_host_id: Id of the target host server
+ :return: New target domain
+ """
+
+ return self._invoke_storage_services('CreateTargetDomain',
+ initiatorHostId=initiator_host_id,
+ targetHostId=target_host_id)
+
+ def delete_target_domain(self, target_domain_id):
+ """Delete a target domain.
+
+ :param target_domain_id: Target domain id
+ """
+
+ self._invoke_storage_services('DeleteTargetDomain',
+ targetDomainId=target_domain_id)
+
+ def get_target_devices(self):
+ """Get all the target devices in the configuration.
+
+ :return: A list of target devices
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetTargetDevices'),
+ 'VirtualTargetDeviceData')
+
+ def build_scsi_port_nexus_data(self, initiator_port_id, target_port_id):
+ """Create a ScsiPortNexusData object.
+
+ Nexus is a pair of ports that can communicate, one being the initiator,
+ the other the target
+
+ :param initiator_port_id: Id of the initiator port
+ :param target_port_id: Id of the target port
+ :return: ScsiPortNexusData object
+ """
+
+ scsi_port_nexus_data = (self._storage_services_client.factory
+ .create(self.O_SCSI_PORT_NEXUS_DATA))
+ scsi_port_nexus_data.InitiatorPortId = initiator_port_id
+ scsi_port_nexus_data.TargetPortId = target_port_id
+
+ return scsi_port_nexus_data
+
+ def create_target_device(self, target_domain_id, nexus):
+ """Create a target device, given a target domain and a nexus.
+
+ :param target_domain_id: Target domain id
+ :param nexus: Nexus, or pair of ports
+ :return: New target device
+ """
+
+ return self._invoke_storage_services('CreateTargetDevice',
+ targetDomainId=target_domain_id,
+ nexus=nexus)
+
+ def delete_target_device(self, target_device_id):
+ """Delete a target device.
+
+ :param target_device_id: Target device id
+ """
+
+ self._invoke_storage_services('DeleteTargetDevice',
+ targetDeviceId=target_device_id)
+
+ def get_next_free_lun(self, target_device_id):
+ """Find the next unused LUN number for a specified target device.
+
+ :param target_device_id: Target device id
+ :return: LUN number
+ """
+
+ return self._invoke_storage_services('GetNextFreeLun',
+ targetDeviceId=target_device_id)
+
+ def get_logical_units(self):
+ """Get all the mappings configured in the system.
+
+ :return: A list of mappings
+ """
+
+ return self._get_list_data(
+ self._invoke_storage_services('GetLogicalUnits'),
+ 'VirtualLogicalUnitData')
+
+ def map_logical_disk(self, logical_disk_id, nexus, lun,
+ initiator_host_id, mapping_type):
+ """Map a logical disk to a host.
+
+ :param logical_disk_id: Id of the logical disk
+ :param nexus: Nexus, or pair of ports
+ :param lun: Logical Unit Number
+ :param initiator_host_id: Id of the initiator host machine
+ :param mapping_type: Type of mapping
+ :return: New mapping
+ """
+
+ logical_unit_type = getattr(self._storage_services_client.factory
+ .create(self.O_LOGICAL_UNIT_TYPE),
+ mapping_type)
+
+ return self._invoke_storage_services('MapLogicalDisk',
+ logicalDiskId=logical_disk_id,
+ nexus=nexus,
+ lun=lun,
+ initiatorHostId=initiator_host_id,
+ mappingType=logical_unit_type)
+
+ def unmap_logical_disk(self, logical_disk_id, nexus):
+ """Unmap a logical disk mapped with a specified nexus.
+
+ :param logical_disk_id: Id of the logical disk
+ :param nexus: Nexus, or pair of ports
+ """
+
+ self._invoke_storage_services('UnmapLogicalDisk',
+ logicalDiskId=logical_disk_id,
+ nexusData=nexus)
diff --git a/cinder/volume/drivers/datacore/driver.py b/cinder/volume/drivers/datacore/driver.py
new file mode 100644
index 00000000000..1bacd0a4caa
--- /dev/null
+++ b/cinder/volume/drivers/datacore/driver.py
@@ -0,0 +1,742 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Base Driver for DataCore SANsymphony storage array."""
+
+import time
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_service import loopingcall
+from oslo_utils import excutils
+from oslo_utils import units
+import six
+
+from cinder import context as cinder_context
+from cinder import exception as cinder_exception
+from cinder.i18n import _
+from cinder import utils as cinder_utils
+from cinder.volume import driver
+from cinder.volume.drivers.datacore import api
+from cinder.volume.drivers.datacore import exception as datacore_exception
+from cinder.volume.drivers.datacore import utils as datacore_utils
+from cinder.volume.drivers.san import san
+from cinder.volume import volume_types
+
+
+LOG = logging.getLogger(__name__)
+
+datacore_opts = [
+ cfg.StrOpt('datacore_disk_type',
+ default='single',
+ choices=['single', 'mirrored'],
+ help='DataCore virtual disk type (single/mirrored). '
+ 'Mirrored virtual disks require two storage servers in '
+ 'the server group.'),
+ cfg.StrOpt('datacore_storage_profile',
+ default=None,
+ help='DataCore virtual disk storage profile.'),
+ cfg.ListOpt('datacore_disk_pools',
+ default=[],
+ help='List of DataCore disk pools that can be used '
+ 'by volume driver.'),
+ cfg.IntOpt('datacore_api_timeout',
+ default=300,
+ min=1,
+ help='Seconds to wait for a response from a '
+ 'DataCore API call.'),
+ cfg.IntOpt('datacore_disk_failed_delay',
+ default=15,
+ min=0,
+ help='Seconds to wait for DataCore virtual '
+ 'disk to come out of the "Failed" state.'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(datacore_opts)
+
+
+class DataCoreVolumeDriver(driver.BaseVD):
+ """DataCore SANsymphony base volume driver."""
+
+ STORAGE_PROTOCOL = 'N/A'
+
+ AWAIT_DISK_ONLINE_INTERVAL = 10
+ AWAIT_SNAPSHOT_ONLINE_INTERVAL = 10
+ AWAIT_SNAPSHOT_ONLINE_INITIAL_DELAY = 5
+
+ DATACORE_SINGLE_DISK = 'single'
+ DATACORE_MIRRORED_DISK = 'mirrored'
+
+ DATACORE_DISK_TYPE_KEY = 'datacore:disk_type'
+ DATACORE_STORAGE_PROFILE_KEY = 'datacore:storage_profile'
+ DATACORE_DISK_POOLS_KEY = 'datacore:disk_pools'
+
+ VALID_VOLUME_TYPE_KEYS = (DATACORE_DISK_TYPE_KEY,
+ DATACORE_STORAGE_PROFILE_KEY,
+ DATACORE_DISK_POOLS_KEY,)
+
+ def __init__(self, *args, **kwargs):
+ super(DataCoreVolumeDriver, self).__init__(*args, **kwargs)
+ self.configuration.append_config_values(san.san_opts)
+ self.configuration.append_config_values(datacore_opts)
+ self._api = None
+ self._default_volume_options = None
+
+ def do_setup(self, context):
+ """Perform validations and establish connection to server.
+
+ :param context: Context information
+ """
+
+ required_params = [
+ 'san_ip',
+ 'san_login',
+ 'san_password',
+ ]
+ for param in required_params:
+ if not getattr(self.configuration, param, None):
+ raise cinder_exception.InvalidInput(_("%s not set.") % param)
+
+ self._api = api.DataCoreClient(
+ self.configuration.san_ip,
+ self.configuration.san_login,
+ self.configuration.san_password,
+ self.configuration.datacore_api_timeout)
+
+ disk_type = self.configuration.datacore_disk_type
+ if disk_type:
+ disk_type = disk_type.lower()
+ storage_profile = self.configuration.datacore_storage_profile
+ if storage_profile:
+ storage_profile = storage_profile.lower()
+ disk_pools = self.configuration.datacore_disk_pools
+ if disk_pools:
+ disk_pools = [pool.lower() for pool in disk_pools]
+
+ self._default_volume_options = {
+ self.DATACORE_DISK_TYPE_KEY: disk_type,
+ self.DATACORE_STORAGE_PROFILE_KEY: storage_profile,
+ self.DATACORE_DISK_POOLS_KEY: disk_pools,
+ }
+
+ def check_for_setup_error(self):
+ pass
+
+ def get_volume_backend_name(self):
+ """Get volume backend name of the volume service.
+
+ :return: Volume backend name
+ """
+
+ backend_name = self.configuration.safe_get('volume_backend_name')
+ return (backend_name or
+ 'datacore_' + self.get_storage_protocol().lower())
+
+ def get_storage_protocol(self):
+ """Get storage protocol of the volume backend.
+
+ :return: Storage protocol
+ """
+
+ return self.STORAGE_PROTOCOL
+
+ def get_volume_stats(self, refresh=False):
+ """Obtain status of the volume service.
+
+ :param refresh: Whether to get refreshed information
+ """
+
+ if refresh:
+ self._update_volume_stats()
+ return self._stats
+
+ def create_volume(self, volume):
+ """Creates a volume.
+
+ :param volume: Volume object
+ :return: Dictionary of changes to the volume object to be persisted
+ """
+
+ volume_options = self._get_volume_options(volume)
+
+ disk_type = volume_options[self.DATACORE_DISK_TYPE_KEY]
+ if disk_type == self.DATACORE_MIRRORED_DISK:
+ logical_disk_count = 2
+ virtual_disk_type = 'MultiPathMirrored'
+ elif disk_type == self.DATACORE_SINGLE_DISK:
+ logical_disk_count = 1
+ virtual_disk_type = 'NonMirrored'
+ else:
+ msg = _("Virtual disk type '%s' is not valid.") % disk_type
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ profile_id = self._get_storage_profile_id(
+ volume_options[self.DATACORE_STORAGE_PROFILE_KEY])
+
+ pools = datacore_utils.get_distinct_by(
+ lambda pool: pool.ServerId,
+ self._get_available_disk_pools(
+ volume_options[self.DATACORE_DISK_POOLS_KEY]))
+
+ if len(pools) < logical_disk_count:
+ msg = _("Suitable disk pools were not found for "
+ "creating virtual disk.")
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ disk_size = self._get_size_in_bytes(volume['size'])
+
+ logical_disks = []
+ virtual_disk = None
+ try:
+ for logical_disk_pool in pools[:logical_disk_count]:
+ logical_disks.append(
+ self._api.create_pool_logical_disk(
+ logical_disk_pool.Id, 'Striped', disk_size))
+
+ virtual_disk_data = self._api.build_virtual_disk_data(
+ volume['id'],
+ virtual_disk_type,
+ disk_size,
+ volume['display_name'],
+ profile_id)
+
+ virtual_disk = self._api.create_virtual_disk_ex2(
+ virtual_disk_data,
+ logical_disks[0].Id,
+ logical_disks[1].Id if logical_disk_count == 2 else None,
+ True)
+
+ virtual_disk = self._await_virtual_disk_online(virtual_disk.Id)
+
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.exception("Creation of volume %(volume)s failed.",
+ {'volume': volume['id']})
+ try:
+ if virtual_disk:
+ self._api.delete_virtual_disk(virtual_disk.Id, True)
+ else:
+ for logical_disk in logical_disks:
+ self._api.delete_logical_disk(logical_disk.Id)
+ except datacore_exception.DataCoreException as e:
+ LOG.warning("An error occurred on a cleanup after failed "
+ "creation of volume %(volume)s: %(error)s.",
+ {'volume': volume['id'], 'error': e})
+
+ return {'provider_location': virtual_disk.Id}
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a volume from a snapshot.
+
+ :param volume: Volume object
+ :param snapshot: Snapshot object
+ :return: Dictionary of changes to the volume object to be persisted
+ """
+
+ return self._create_volume_from(volume, snapshot)
+
+ def create_cloned_volume(self, volume, src_vref):
+ """Creates volume clone.
+
+ :param volume: New Volume object
+ :param src_vref: Volume object that must be cloned
+ :return: Dictionary of changes to the volume object to be persisted
+ """
+
+ return self._create_volume_from(volume, src_vref)
+
+ def extend_volume(self, volume, new_size):
+ """Extend an existing volume's size.
+
+ :param volume: Volume object
+ :param new_size: new size in GB to extend this volume to
+ """
+
+ virtual_disk = self._get_virtual_disk_for(volume, raise_not_found=True)
+ self._set_virtual_disk_size(virtual_disk,
+ self._get_size_in_bytes(new_size))
+
+ def delete_volume(self, volume):
+ """Deletes a volume.
+
+ :param volume: Volume object
+ """
+
+ virtual_disk = self._get_virtual_disk_for(volume)
+ if virtual_disk:
+ if virtual_disk.IsServed:
+ logical_disks = self._api.get_logical_disks()
+ logical_units = self._api.get_logical_units()
+ target_devices = self._api.get_target_devices()
+ logical_disks = [disk.Id for disk in logical_disks
+ if disk.VirtualDiskId == virtual_disk.Id]
+ logical_unit_devices = [unit.VirtualTargetDeviceId
+ for unit in logical_units
+ if unit.LogicalDiskId in logical_disks]
+ initiator_ports = set(device.InitiatorPortId
+ for device in target_devices
+ if device.Id in logical_unit_devices)
+ for port in initiator_ports:
+ self._api.unserve_virtual_disks_from_port(
+ port, [virtual_disk.Id])
+ self._api.delete_virtual_disk(virtual_disk.Id, True)
+
+ def create_snapshot(self, snapshot):
+ """Creates a snapshot.
+
+ :param snapshot: Snapshot object
+ :return: Dictionary of changes to the snapshot object to be persisted
+ """
+
+ src_virtual_disk = self._get_virtual_disk_for(snapshot['volume'],
+ raise_not_found=True)
+
+ volume_options = self._get_volume_options(snapshot['volume'])
+ profile_name = volume_options[self.DATACORE_STORAGE_PROFILE_KEY]
+ profile_id = self._get_storage_profile_id(profile_name)
+ pool_names = volume_options[self.DATACORE_DISK_POOLS_KEY]
+
+ if src_virtual_disk.DiskStatus != 'Online':
+ LOG.warning("Attempting to make a snapshot from virtual disk "
+ "%(disk)s that is in %(state)s state.",
+ {'disk': src_virtual_disk.Id,
+ 'state': src_virtual_disk.DiskStatus})
+
+ snapshot_virtual_disk = self._create_virtual_disk_copy(
+ src_virtual_disk,
+ snapshot['id'],
+ snapshot['display_name'],
+ profile_id=profile_id,
+ pool_names=pool_names)
+
+ return {'provider_location': snapshot_virtual_disk.Id}
+
+ def delete_snapshot(self, snapshot):
+ """Deletes a snapshot.
+
+ :param snapshot: Snapshot object
+ """
+
+ snapshot_virtual_disk = self._get_virtual_disk_for(snapshot)
+ if snapshot_virtual_disk:
+ self._api.delete_virtual_disk(snapshot_virtual_disk.Id, True)
+
+ def ensure_export(self, context, volume):
+ pass
+
+ def create_export(self, context, volume, connector):
+ pass
+
+ def remove_export(self, context, volume):
+ pass
+
+ def terminate_connection(self, volume, connector, **kwargs):
+ """Disallow connection from connector.
+
+ :param volume: Volume object
+ :param connector: Connector information
+ """
+
+ virtual_disk = self._get_virtual_disk_for(volume)
+ if virtual_disk:
+ if connector:
+ clients = [self._get_client(connector['host'],
+ create_new=False)]
+ else:
+ clients = self._api.get_clients()
+
+ server_group = self._get_our_server_group()
+
+ @cinder_utils.synchronized(
+ 'datacore-backend-%s' % server_group.Id, external=True)
+ def unserve_virtual_disk(client_id):
+ self._api.unserve_virtual_disks_from_host(
+ client_id, [virtual_disk.Id])
+
+ for client in clients:
+ unserve_virtual_disk(client.Id)
+
+ def _update_volume_stats(self):
+ performance_data = self._api.get_performance_by_type(
+ ['DiskPoolPerformance'])
+ total = 0
+ available = 0
+ reserved = 0
+ for performance in performance_data:
+ missing_perf_data = []
+
+ if hasattr(performance.PerformanceData, 'BytesTotal'):
+ total += performance.PerformanceData.BytesTotal
+ else:
+ missing_perf_data.append('BytesTotal')
+
+ if hasattr(performance.PerformanceData, 'BytesAvailable'):
+ available += performance.PerformanceData.BytesAvailable
+ else:
+ missing_perf_data.append('BytesAvailable')
+
+ if hasattr(performance.PerformanceData, 'BytesReserved'):
+ reserved += performance.PerformanceData.BytesReserved
+ else:
+ missing_perf_data.append('BytesReserved')
+
+ if missing_perf_data:
+ LOG.warning("Performance data %(data)s is missing for "
+ "disk pool %(pool)s",
+ {'data': missing_perf_data,
+ 'pool': performance.ObjectId})
+ provisioned = 0
+ logical_disks = self._api.get_logical_disks()
+ for disk in logical_disks:
+ if getattr(disk, 'PoolId', None):
+ provisioned += disk.Size.Value
+ total_capacity_gb = self._get_size_in_gigabytes(total)
+ free = available + reserved
+ free_capacity_gb = self._get_size_in_gigabytes(free)
+ provisioned_capacity_gb = self._get_size_in_gigabytes(provisioned)
+ reserved_percentage = 100.0 * reserved / total if total else 0.0
+ ratio = self.configuration.max_over_subscription_ratio
+ stats_data = {
+ 'vendor_name': 'DataCore',
+ 'QoS_support': False,
+ 'volume_backend_name': self.get_volume_backend_name(),
+ 'driver_version': self.get_version(),
+ 'storage_protocol': self.get_storage_protocol(),
+ 'total_capacity_gb': total_capacity_gb,
+ 'free_capacity_gb': free_capacity_gb,
+ 'provisioned_capacity_gb': provisioned_capacity_gb,
+ 'reserved_percentage': reserved_percentage,
+ 'max_over_subscription_ratio': ratio,
+ 'thin_provisioning_support': True,
+ 'thick_provisioning_support': False,
+ }
+ self._stats = stats_data
+
+ def _get_our_server_group(self):
+ server_group = datacore_utils.get_first(lambda group: group.OurGroup,
+ self._api.get_server_groups())
+
+ return server_group
+
+ def _get_volume_options_from_type(self, type_id, default_options):
+ options = dict(default_options.items())
+ if type_id:
+ admin_context = cinder_context.get_admin_context()
+ volume_type = volume_types.get_volume_type(admin_context, type_id)
+ specs = dict(volume_type).get('extra_specs')
+
+ for key, value in six.iteritems(specs):
+ if key in self.VALID_VOLUME_TYPE_KEYS:
+ if key == self.DATACORE_DISK_POOLS_KEY:
+ options[key] = [v.strip().lower()
+ for v in value.split(',')]
+ else:
+ options[key] = value.lower()
+
+ return options
+
+ def _get_volume_options(self, volume):
+ type_id = volume['volume_type_id']
+
+ volume_options = self._get_volume_options_from_type(
+ type_id, self._default_volume_options)
+
+ return volume_options
+
+ def _get_online_servers(self):
+ servers = self._api.get_servers()
+ online_servers = [server for server in servers
+ if server.State == 'Online']
+ return online_servers
+
+ def _get_available_disk_pools(self, disk_pool_names=None):
+ online_servers = [server.Id for server in self._get_online_servers()]
+
+ pool_performance = {
+ performance.ObjectId: performance.PerformanceData for performance
+ in self._api.get_performance_by_type(['DiskPoolPerformance'])}
+
+ disk_pools = self._api.get_disk_pools()
+
+ lower_disk_pool_names = ([name.lower() for name in disk_pool_names]
+ if disk_pool_names else [])
+
+ available_disk_pools = [
+ pool for pool in disk_pools
+ if (self._is_pool_healthy(pool, pool_performance, online_servers)
+ and (not lower_disk_pool_names
+ or pool.Caption.lower() in lower_disk_pool_names))]
+
+ available_disk_pools.sort(
+ key=lambda p: pool_performance[p.Id].BytesAvailable, reverse=True)
+
+ return available_disk_pools
+
+ def _get_virtual_disk_for(self, obj, raise_not_found=False):
+ disk_id = obj.get('provider_location')
+
+ virtual_disk = datacore_utils.get_first_or_default(
+ lambda disk: disk.Id == disk_id,
+ self._api.get_virtual_disks(),
+ None)
+ if not virtual_disk:
+ msg = (_("Virtual disk not found for %(object)s %(object_id)s.")
+ % {'object': obj.__class__.__name__.lower(),
+ 'object_id': obj['id']})
+ if raise_not_found:
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+ else:
+ LOG.warning(msg)
+
+ return virtual_disk
+
+ def _set_virtual_disk_size(self, virtual_disk, new_size):
+ return self._api.set_virtual_disk_size(virtual_disk.Id, new_size)
+
+ def _get_storage_profile(self, profile_name, raise_not_found=False):
+ profiles = self._api.get_storage_profiles()
+ profile = datacore_utils.get_first_or_default(
+ lambda p: p.Caption.lower() == profile_name.lower(),
+ profiles,
+ None)
+ if not profile and raise_not_found:
+ msg = (_("Specified storage profile %s not found.")
+ % profile_name)
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ return profile
+
+ def _get_storage_profile_id(self, profile_name):
+ profile_id = None
+ if profile_name:
+ profile = self._get_storage_profile(profile_name,
+ raise_not_found=True)
+ profile_id = profile.Id
+ return profile_id
+
+ def _await_virtual_disk_online(self, virtual_disk_id):
+ def inner(start_time):
+ disk_failed_delay = self.configuration.datacore_disk_failed_delay
+ virtual_disk = datacore_utils.get_first(
+ lambda disk: disk.Id == virtual_disk_id,
+ self._api.get_virtual_disks())
+ if virtual_disk.DiskStatus == 'Online':
+ raise loopingcall.LoopingCallDone(virtual_disk)
+ elif (virtual_disk.DiskStatus != 'FailedRedundancy'
+ and time.time() - start_time >= disk_failed_delay):
+ msg = (_("Virtual disk %(disk)s did not come out of the "
+ "%(state)s state after %(timeout)s seconds.")
+ % {'disk': virtual_disk.Id,
+ 'state': virtual_disk.DiskStatus,
+ 'timeout': disk_failed_delay})
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ inner_loop = loopingcall.FixedIntervalLoopingCall(inner, time.time())
+ return inner_loop.start(self.AWAIT_DISK_ONLINE_INTERVAL).wait()
+
+ def _create_volume_from(self, volume, src_obj):
+ src_virtual_disk = self._get_virtual_disk_for(src_obj,
+ raise_not_found=True)
+
+ if src_virtual_disk.DiskStatus != 'Online':
+ LOG.warning("Attempting to create a volume from virtual disk "
+ "%(disk)s that is in %(state)s state.",
+ {'disk': src_virtual_disk.Id,
+ 'state': src_virtual_disk.DiskStatus})
+
+ volume_options = self._get_volume_options(volume)
+ profile_id = self._get_storage_profile_id(
+ volume_options[self.DATACORE_STORAGE_PROFILE_KEY])
+ pool_names = volume_options[self.DATACORE_DISK_POOLS_KEY]
+
+ volume_virtual_disk = self._create_virtual_disk_copy(
+ src_virtual_disk,
+ volume['id'],
+ volume['display_name'],
+ profile_id=profile_id,
+ pool_names=pool_names)
+
+ volume_logical_disk = datacore_utils.get_first(
+ lambda disk: disk.VirtualDiskId == volume_virtual_disk.Id,
+ self._api.get_logical_disks())
+
+ try:
+ volume_virtual_disk = self._set_virtual_disk_size(
+ volume_virtual_disk,
+ self._get_size_in_bytes(volume['size']))
+
+ disk_type = volume_options[self.DATACORE_DISK_TYPE_KEY]
+ if disk_type == self.DATACORE_MIRRORED_DISK:
+ pools = self._get_available_disk_pools(pool_names)
+ selected_pool = datacore_utils.get_first_or_default(
+ lambda pool: (
+ pool.ServerId != volume_logical_disk.ServerHostId
+ and pool.Id != volume_logical_disk.PoolId),
+ pools,
+ None)
+ if selected_pool:
+ logical_disk = self._api.create_pool_logical_disk(
+ selected_pool.Id,
+ 'Striped',
+ volume_virtual_disk.Size.Value)
+ self._api.bind_logical_disk(volume_virtual_disk.Id,
+ logical_disk.Id,
+ 'Second',
+ True,
+ False,
+ True)
+ else:
+ msg = _("Can not create mirrored virtual disk. "
+ "Suitable disk pools not found.")
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ volume_virtual_disk = self._await_virtual_disk_online(
+ volume_virtual_disk.Id)
+
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.exception("Creation of volume %(volume)s failed.",
+ {'volume': volume['id']})
+ try:
+ self._api.delete_virtual_disk(volume_virtual_disk.Id, True)
+ except datacore_exception.DataCoreException as e:
+ LOG.warning("An error occurred on a cleanup after failed "
+ "creation of volume %(volume)s: %(error)s.",
+ {'volume': volume['id'], 'error': e})
+
+ return {'provider_location': volume_virtual_disk.Id}
+
+ def _create_full_snapshot(self, description, name, pool_names, profile_id,
+ src_virtual_disk):
+ pools = self._get_available_disk_pools(pool_names)
+ destination_pool = datacore_utils.get_first_or_default(
+ lambda pool: (pool.ServerId == src_virtual_disk.FirstHostId
+ or pool.ServerId == src_virtual_disk.SecondHostId),
+ pools,
+ None)
+
+ if not destination_pool:
+ msg = _("Suitable snapshot destination disk pool not found for "
+ "virtual disk %s.") % src_virtual_disk.Id
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+ server = datacore_utils.get_first(
+ lambda srv: srv.Id == destination_pool.ServerId,
+ self._api.get_servers())
+ if not server.SnapshotMapStorePoolId:
+ self._api.designate_map_store(destination_pool.Id)
+ snapshot = self._api.create_snapshot(src_virtual_disk.Id,
+ name,
+ description,
+ destination_pool.Id,
+ 'Full',
+ False,
+ profile_id)
+ return snapshot
+
+ def _await_snapshot_migrated(self, snapshot_id):
+ def inner():
+ snapshot_data = datacore_utils.get_first(
+ lambda snapshot: snapshot.Id == snapshot_id,
+ self._api.get_snapshots())
+ if snapshot_data.State == 'Migrated':
+ raise loopingcall.LoopingCallDone(snapshot_data)
+ elif (snapshot_data.State != 'Healthy'
+ and snapshot_data.Failure != 'NoFailure'):
+ msg = (_("Full migration of snapshot %(snapshot)s failed. "
+ "Snapshot is in %(state)s state.")
+ % {'snapshot': snapshot_data.Id,
+ 'state': snapshot_data.State})
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ loop = loopingcall.FixedIntervalLoopingCall(inner)
+ return loop.start(self.AWAIT_SNAPSHOT_ONLINE_INTERVAL,
+ self.AWAIT_SNAPSHOT_ONLINE_INITIAL_DELAY).wait()
+
+ def _create_virtual_disk_copy(self, src_virtual_disk, name, description,
+ profile_id=None, pool_names=None):
+ snapshot = self._create_full_snapshot(
+ description, name, pool_names, profile_id, src_virtual_disk)
+
+ try:
+ snapshot = self._await_snapshot_migrated(snapshot.Id)
+ self._api.delete_snapshot(snapshot.Id)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.exception("Split operation failed for snapshot "
+ "%(snapshot)s.", {'snapshot': snapshot.Id})
+ try:
+ logical_disk_copy = datacore_utils.get_first(
+ lambda disk: (
+ disk.Id == snapshot.DestinationLogicalDiskId),
+ self._api.get_logical_disks())
+
+ virtual_disk_copy = datacore_utils.get_first(
+ lambda disk: (
+ disk.Id == logical_disk_copy.VirtualDiskId),
+ self._api.get_virtual_disks())
+
+ self._api.delete_virtual_disk(virtual_disk_copy.Id, True)
+ except datacore_exception.DataCoreException as e:
+ LOG.warning("An error occurred on a cleanup after failed "
+ "split of snapshot %(snapshot)s: %(error)s.",
+ {'snapshot': snapshot.Id, 'error': e})
+
+ logical_disk_copy = datacore_utils.get_first(
+ lambda disk: disk.Id == snapshot.DestinationLogicalDiskId,
+ self._api.get_logical_disks())
+
+ virtual_disk_copy = datacore_utils.get_first(
+ lambda disk: disk.Id == logical_disk_copy.VirtualDiskId,
+ self._api.get_virtual_disks())
+
+ return virtual_disk_copy
+
+ def _get_client(self, name, create_new=False):
+ client_hosts = self._api.get_clients()
+
+ client = datacore_utils.get_first_or_default(
+ lambda host: host.HostName == name, client_hosts, None)
+
+ if create_new:
+ if not client:
+ client = self._api.register_client(
+ name, None, 'Other', 'PreferredServer', None)
+ self._api.set_client_capabilities(client.Id, True, True)
+
+ return client
+
+ @staticmethod
+ def _is_pool_healthy(pool, pool_performance, online_servers):
+ if (pool.PoolStatus == 'Running'
+ and hasattr(pool_performance[pool.Id], 'BytesAvailable')
+ and pool.ServerId in online_servers):
+ return True
+ return False
+
+ @staticmethod
+ def _get_size_in_bytes(size_in_gigabytes):
+ return size_in_gigabytes * units.Gi
+
+ @staticmethod
+ def _get_size_in_gigabytes(size_in_bytes):
+ return size_in_bytes / float(units.Gi)
diff --git a/cinder/volume/drivers/datacore/exception.py b/cinder/volume/drivers/datacore/exception.py
new file mode 100644
index 00000000000..1fab1050e38
--- /dev/null
+++ b/cinder/volume/drivers/datacore/exception.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Exception definitions."""
+
+from cinder import exception
+from cinder.i18n import _
+
+
+class DataCoreException(exception.VolumeBackendAPIException):
+ """Base DataCore Exception."""
+
+ message = _('DataCore exception.')
+
+
+class DataCoreConnectionException(DataCoreException):
+ """Thrown when there are connection problems during a DataCore API call."""
+
+ message = _('Failed to connect to DataCore Server Group: %(reason)s.')
+
+
+class DataCoreFaultException(DataCoreException):
+ """Thrown when there are faults during a DataCore API call."""
+
+ message = _('DataCore Server Group reported an error: %(reason)s.')
diff --git a/cinder/volume/drivers/datacore/fc.py b/cinder/volume/drivers/datacore/fc.py
new file mode 100644
index 00000000000..7524bf0c89b
--- /dev/null
+++ b/cinder/volume/drivers/datacore/fc.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Fibre Channel Driver for DataCore SANsymphony storage array."""
+
+from oslo_log import log as logging
+
+from cinder import exception as cinder_exception
+from cinder.i18n import _
+from cinder import interface
+from cinder import utils as cinder_utils
+from cinder.volume.drivers.datacore import driver
+from cinder.volume.drivers.datacore import exception as datacore_exception
+
+
+LOG = logging.getLogger(__name__)
+
+
+@interface.volumedriver
+class FibreChannelVolumeDriver(driver.DataCoreVolumeDriver):
+ """DataCore SANsymphony Fibre Channel volume driver.
+
+ Version history:
+
+ .. code-block:: none
+
+ 1.0.0 - Initial driver
+
+ """
+
+ VERSION = '1.0.0'
+ STORAGE_PROTOCOL = 'FC'
+ CI_WIKI_NAME = 'DataCore_CI'
+
+ def __init__(self, *args, **kwargs):
+ super(FibreChannelVolumeDriver, self).__init__(*args, **kwargs)
+
+ def validate_connector(self, connector):
+ """Fail if connector doesn't contain all the data needed by the driver.
+
+ :param connector: Connector information
+ """
+
+ required_data = ['host', 'wwpns']
+ for required in required_data:
+ if required not in connector:
+ LOG.error("The volume driver requires %(data)s "
+ "in the connector.", {'data': required})
+ raise cinder_exception.InvalidConnectorException(
+ missing=required)
+
+ def initialize_connection(self, volume, connector):
+ """Allow connection to connector and return connection info.
+
+ :param volume: Volume object
+ :param connector: Connector information
+ :return: Connection information
+ """
+
+ LOG.debug("Initialize connection for volume %(volume)s for "
+ "connector %(connector)s.",
+ {'volume': volume['id'], 'connector': connector})
+
+ virtual_disk = self._get_virtual_disk_for(volume, raise_not_found=True)
+
+ if virtual_disk.DiskStatus != 'Online':
+ LOG.warning("Attempting to attach virtual disk %(disk)s "
+ "that is in %(state)s state.",
+ {'disk': virtual_disk.Id,
+ 'state': virtual_disk.DiskStatus})
+
+ serve_result = self._serve_virtual_disk(connector, virtual_disk.Id)
+
+ online_servers = [server.Id for server in self._get_online_servers()]
+ online_ports = self._get_online_ports(online_servers)
+ online_devices = self._get_online_devices(online_ports)
+ online_units = [unit for unit in serve_result[1]
+ if unit.VirtualTargetDeviceId in online_devices]
+
+ if not online_units:
+ msg = (_("Volume %(volume)s can not be attached "
+ "to connector %(connector)s due to backend state.")
+ % {'volume': volume['id'], 'connector': connector})
+ LOG.error(msg)
+ try:
+ self._api.unserve_virtual_disks_from_host(serve_result[0].Id,
+ [virtual_disk.Id])
+ except datacore_exception.DataCoreException as e:
+ LOG.warning("An error occurred on a cleanup after failed "
+ "attaching of volume %(volume)s to connector "
+ "%(connector)s: %(error)s.",
+ {'volume': volume['id'],
+ 'connector': connector,
+ 'error': e})
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ target_device = online_devices[online_units[0].VirtualTargetDeviceId]
+ target_port = online_ports[target_device.TargetPortId]
+
+ connection_data = {
+ 'target_discovered': False,
+ 'target_lun': online_units[0].Lun.Quad,
+ 'target_wwn': target_port.PortName.replace('-', '').lower(),
+ 'volume_id': volume['id'],
+ 'access_mode': 'rw',
+ }
+
+ LOG.debug("Connection data: %s", connection_data)
+
+ return {
+ 'driver_volume_type': 'fibre_channel',
+ 'data': connection_data,
+ }
+
+ def _serve_virtual_disk(self, connector, virtual_disk_id):
+ server_group = self._get_our_server_group()
+
+ @cinder_utils.synchronized(
+ 'datacore-backend-%s' % server_group.Id, external=True)
+ def serve_virtual_disk():
+ connector_wwpns = list(wwpn.replace('-', '').lower()
+ for wwpn in connector['wwpns'])
+
+ client = self._get_client(connector['host'], create_new=True)
+
+ available_ports = self._api.get_ports()
+
+ initiators = []
+ for port in available_ports:
+ port_name = port.PortName.replace('-', '').lower()
+ if (port.PortType == 'FibreChannel'
+ and port.PortMode == 'Initiator'
+ and port_name in connector_wwpns):
+ initiators.append(port)
+ if not initiators:
+ msg = _("Fibre Channel ports not found for "
+ "connector: %s") % connector
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+ else:
+ for initiator in initiators:
+ if initiator.HostId != client.Id:
+ try:
+ self._api.assign_port(client.Id, initiator.Id)
+ except datacore_exception.DataCoreException as e:
+ LOG.info("Assigning initiator port %(initiator)s "
+ "to client %(client)s failed with "
+ "error: %(error)s",
+ {'initiator': initiator.Id,
+ 'client': client.Id,
+ 'error': e})
+
+ virtual_logical_units = self._api.serve_virtual_disks_to_host(
+ client.Id, [virtual_disk_id])
+
+ return client, virtual_logical_units
+
+ return serve_virtual_disk()
+
+ def _get_online_ports(self, online_servers):
+ ports = self._api.get_ports()
+ online_ports = {port.Id: port for port in ports
+ if port.HostId in online_servers}
+
+ return online_ports
+
+ def _get_online_devices(self, online_ports):
+ devices = self._api.get_target_devices()
+ online_devices = {device.Id: device for device in devices
+ if device.TargetPortId in online_ports}
+
+ return online_devices
diff --git a/cinder/volume/drivers/datacore/iscsi.py b/cinder/volume/drivers/datacore/iscsi.py
new file mode 100644
index 00000000000..8ae5abf3c94
--- /dev/null
+++ b/cinder/volume/drivers/datacore/iscsi.py
@@ -0,0 +1,440 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""iSCSI Driver for DataCore SANsymphony storage array."""
+
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import excutils
+
+from cinder import exception as cinder_exception
+from cinder.i18n import _
+from cinder import interface
+from cinder import utils as cinder_utils
+from cinder.volume.drivers.datacore import driver
+from cinder.volume.drivers.datacore import exception as datacore_exception
+from cinder.volume.drivers.datacore import passwd
+from cinder.volume.drivers.datacore import utils as datacore_utils
+from cinder.volume import utils as volume_utils
+
+
+LOG = logging.getLogger(__name__)
+
+datacore_iscsi_opts = [
+ cfg.ListOpt('datacore_iscsi_unallowed_targets',
+ default=[],
+ help='List of iSCSI targets that cannot be used to attach '
+ 'volume. To prevent the DataCore iSCSI volume driver '
+ 'from using some front-end targets in volume attachment, '
+ 'specify this option and list the iqn and target machine '
+ 'for each target as the value, such as '
+ ', , '
+ '.'),
+ cfg.BoolOpt('datacore_iscsi_chap_enabled',
+ default=False,
+ help='Configure CHAP authentication for iSCSI connections.'),
+ cfg.StrOpt('datacore_iscsi_chap_storage',
+ default=None,
+ help='iSCSI CHAP authentication password storage file.'),
+]
+
+CONF = cfg.CONF
+CONF.register_opts(datacore_iscsi_opts)
+
+
+@interface.volumedriver
+class ISCSIVolumeDriver(driver.DataCoreVolumeDriver):
+ """DataCore SANsymphony iSCSI volume driver.
+
+ Version history:
+
+ .. code-block:: none
+
+ 1.0.0 - Initial driver
+
+ """
+
+ VERSION = '1.0.0'
+ STORAGE_PROTOCOL = 'iSCSI'
+ CI_WIKI_NAME = 'DataCore_CI'
+
+ def __init__(self, *args, **kwargs):
+ super(ISCSIVolumeDriver, self).__init__(*args, **kwargs)
+ self.configuration.append_config_values(datacore_iscsi_opts)
+ self._password_storage = None
+
+ def do_setup(self, context):
+ """Perform validations and establish connection to server.
+
+ :param context: Context information
+ """
+
+ super(ISCSIVolumeDriver, self).do_setup(context)
+
+ password_storage_path = getattr(self.configuration,
+ 'datacore_iscsi_chap_storage', None)
+ if (self.configuration.datacore_iscsi_chap_enabled
+ and not password_storage_path):
+ raise cinder_exception.InvalidInput(
+ _("datacore_iscsi_chap_storage not set."))
+ elif password_storage_path:
+ self._password_storage = passwd.PasswordFileStorage(
+ self.configuration.datacore_iscsi_chap_storage)
+
+ def validate_connector(self, connector):
+ """Fail if connector doesn't contain all the data needed by the driver.
+
+ :param connector: Connector information
+ """
+
+ required_data = ['host', 'initiator']
+ for required in required_data:
+ if required not in connector:
+ LOG.error("The volume driver requires %(data)s "
+ "in the connector.", {'data': required})
+ raise cinder_exception.InvalidConnectorException(
+ missing=required)
+
+ def initialize_connection(self, volume, connector):
+ """Allow connection to connector and return connection info.
+
+ :param volume: Volume object
+ :param connector: Connector information
+ :return: Connection information
+ """
+
+ LOG.debug("Initialize connection for volume %(volume)s for "
+ "connector %(connector)s.",
+ {'volume': volume['id'], 'connector': connector})
+
+ virtual_disk = self._get_virtual_disk_for(volume, raise_not_found=True)
+
+ if virtual_disk.DiskStatus != 'Online':
+ LOG.warning("Attempting to attach virtual disk %(disk)s "
+ "that is in %(state)s state.",
+ {'disk': virtual_disk.Id,
+ 'state': virtual_disk.DiskStatus})
+
+ server_group = self._get_our_server_group()
+
+ @cinder_utils.synchronized(
+ 'datacore-backend-%s' % server_group.Id, external=True)
+ def serve_virtual_disk():
+ available_ports = self._api.get_ports()
+
+ iscsi_initiator = self._get_initiator(connector['host'],
+ connector['initiator'],
+ available_ports)
+
+ iscsi_targets = self._get_targets(virtual_disk, available_ports)
+
+ if not iscsi_targets:
+ msg = (_("Suitable targets not found for "
+ "virtual disk %(disk)s for volume %(volume)s.")
+ % {'disk': virtual_disk.Id, 'volume': volume['id']})
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ auth_params = self._setup_iscsi_chap_authentication(
+ iscsi_targets, iscsi_initiator)
+
+ virtual_logical_units = self._map_virtual_disk(
+ virtual_disk, iscsi_targets, iscsi_initiator)
+
+ return iscsi_targets, virtual_logical_units, auth_params
+
+ targets, logical_units, chap_params = serve_virtual_disk()
+
+ target_portal = datacore_utils.build_network_address(
+ targets[0].PortConfigInfo.PortalsConfig.iScsiPortalConfigInfo[0]
+ .Address.Address,
+ targets[0].PortConfigInfo.PortalsConfig.iScsiPortalConfigInfo[0]
+ .TcpPort)
+
+ connection_data = {}
+
+ if chap_params:
+ connection_data['auth_method'] = 'CHAP'
+ connection_data['auth_username'] = chap_params[0]
+ connection_data['auth_password'] = chap_params[1]
+
+ connection_data['target_discovered'] = False
+ connection_data['target_iqn'] = targets[0].PortName
+ connection_data['target_portal'] = target_portal
+ connection_data['target_lun'] = logical_units[targets[0]].Lun.Quad
+ connection_data['volume_id'] = volume['id']
+ connection_data['access_mode'] = 'rw'
+
+ LOG.debug("Connection data: %s", connection_data)
+
+ return {
+ 'driver_volume_type': 'iscsi',
+ 'data': connection_data,
+ }
+
+ def _map_virtual_disk(self, virtual_disk, targets, initiator):
+ logical_disks = self._api.get_logical_disks()
+
+ logical_units = {}
+ created_mapping = {}
+ created_devices = []
+ created_domains = []
+ try:
+ for target in targets:
+ target_domain = self._get_target_domain(target, initiator)
+ if not target_domain:
+ target_domain = self._api.create_target_domain(
+ initiator.HostId, target.HostId)
+ created_domains.append(target_domain)
+
+ nexus = self._api.build_scsi_port_nexus_data(
+ initiator.Id, target.Id)
+
+ target_device = self._get_target_device(
+ target_domain, target, initiator)
+ if not target_device:
+ target_device = self._api.create_target_device(
+ target_domain.Id, nexus)
+ created_devices.append(target_device)
+
+ logical_disk = self._get_logical_disk_on_host(
+ virtual_disk.Id, target.HostId, logical_disks)
+
+ logical_unit = self._get_logical_unit(
+ logical_disk, target_device)
+ if not logical_unit:
+ logical_unit = self._create_logical_unit(
+ logical_disk, nexus, target_device)
+ created_mapping[logical_unit] = target_device
+ logical_units[target] = logical_unit
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.exception("Mapping operation for virtual disk %(disk)s "
+ "failed with error.",
+ {'disk': virtual_disk.Id})
+ try:
+ for logical_unit in created_mapping:
+ nexus = self._api.build_scsi_port_nexus_data(
+ created_mapping[logical_unit].InitiatorPortId,
+ created_mapping[logical_unit].TargetPortId)
+ self._api.unmap_logical_disk(
+ logical_unit.LogicalDiskId, nexus)
+ for target_device in created_devices:
+ self._api.delete_target_device(target_device.Id)
+ for target_domain in created_domains:
+ self._api.delete_target_domain(target_domain.Id)
+ except datacore_exception.DataCoreException as e:
+ LOG.warning("An error occurred on a cleanup after "
+ "failed mapping operation: %s.", e)
+
+ return logical_units
+
+ def _get_target_domain(self, target, initiator):
+ target_domains = self._api.get_target_domains()
+ target_domain = datacore_utils.get_first_or_default(
+ lambda domain: (domain.InitiatorHostId == initiator.HostId
+ and domain.TargetHostId == target.HostId),
+ target_domains,
+ None)
+ return target_domain
+
+ def _get_target_device(self, target_domain, target, initiator):
+ target_devices = self._api.get_target_devices()
+ target_device = datacore_utils.get_first_or_default(
+ lambda device: (device.TargetDomainId == target_domain.Id
+ and device.InitiatorPortId == initiator.Id
+ and device.TargetPortId == target.Id),
+ target_devices,
+ None)
+ return target_device
+
+ def _get_logical_unit(self, logical_disk, target_device):
+ logical_units = self._api.get_logical_units()
+ logical_unit = datacore_utils.get_first_or_default(
+ lambda unit: (unit.LogicalDiskId == logical_disk.Id
+ and unit.VirtualTargetDeviceId == target_device.Id),
+ logical_units,
+ None)
+ return logical_unit
+
+ def _create_logical_unit(self, logical_disk, nexus, target_device):
+ free_lun = self._api.get_next_free_lun(target_device.Id)
+ logical_unit = self._api.map_logical_disk(logical_disk.Id,
+ nexus,
+ free_lun,
+ logical_disk.ServerHostId,
+ 'Client')
+ return logical_unit
+
+ def _check_iscsi_chap_configuration(self, iscsi_chap_enabled, targets):
+ logical_units = self._api.get_logical_units()
+ target_devices = self._api.get_target_devices()
+
+ for logical_unit in logical_units:
+ target_device_id = logical_unit.VirtualTargetDeviceId
+ target_device = datacore_utils.get_first(
+ lambda device, key=target_device_id: device.Id == key,
+ target_devices)
+ target_port_id = target_device.TargetPortId
+ target = datacore_utils.get_first_or_default(
+ lambda target_port, key=target_port_id: target_port.Id == key,
+ targets,
+ None)
+ if (target and iscsi_chap_enabled ==
+ (target.ServerPortProperties.Authentication == 'None')):
+ msg = _("iSCSI CHAP authentication can't be configured for "
+ "target %s. Device exists that served through "
+ "this target.") % target.PortName
+ LOG.error(msg)
+ raise cinder_exception.VolumeDriverException(message=msg)
+
+ def _setup_iscsi_chap_authentication(self, targets, initiator):
+ iscsi_chap_enabled = self.configuration.datacore_iscsi_chap_enabled
+
+ self._check_iscsi_chap_configuration(iscsi_chap_enabled, targets)
+
+ server_group = self._get_our_server_group()
+ update_access_token = False
+ access_token = None
+ chap_secret = None
+ if iscsi_chap_enabled:
+ authentication = 'CHAP'
+ chap_secret = self._password_storage.get_password(
+ server_group.Id, initiator.PortName)
+ update_access_token = False
+ if not chap_secret:
+ chap_secret = volume_utils.generate_password(length=15)
+ self._password_storage.set_password(
+ server_group.Id, initiator.PortName, chap_secret)
+ update_access_token = True
+ access_token = self._api.build_access_token(
+ initiator.PortName,
+ None,
+ None,
+ False,
+ initiator.PortName,
+ chap_secret)
+ else:
+ authentication = 'None'
+ if self._password_storage:
+ self._password_storage.delete_password(server_group.Id,
+ initiator.PortName)
+ changed_targets = {}
+ try:
+ for target in targets:
+ if iscsi_chap_enabled:
+ target_iscsi_nodes = getattr(target.iSCSINodes, 'Node', [])
+ iscsi_node = datacore_utils.get_first_or_default(
+ lambda node: node.Name == initiator.PortName,
+ target_iscsi_nodes,
+ None)
+ if (not iscsi_node
+ or not iscsi_node.AccessToken.TargetUsername
+ or update_access_token):
+ self._api.set_access_token(target.Id, access_token)
+ properties = target.ServerPortProperties
+ if properties.Authentication != authentication:
+ changed_targets[target] = properties.Authentication
+ properties.Authentication = authentication
+ self._api.set_server_port_properties(
+ target.Id, properties)
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.exception("Configuring of iSCSI CHAP authentication for "
+ "initiator %(initiator)s failed.",
+ {'initiator': initiator.PortName})
+ try:
+ for target in changed_targets:
+ properties = target.ServerPortProperties
+ properties.Authentication = changed_targets[target]
+ self._api.set_server_port_properties(
+ target.Id, properties)
+ except datacore_exception.DataCoreException as e:
+ LOG.warning("An error occurred on a cleanup after failed "
+ "configuration of iSCSI CHAP authentication "
+ "on initiator %(initiator)s: %(error)s.",
+ {'initiator': initiator.PortName, 'error': e})
+ if iscsi_chap_enabled:
+ return initiator.PortName, chap_secret
+
+ def _get_initiator(self, host, iqn, available_ports):
+ client = self._get_client(host, create_new=True)
+
+ iscsi_initiator_ports = self._get_host_iscsi_initiator_ports(
+ client, available_ports)
+
+ iscsi_initiator = datacore_utils.get_first_or_default(
+ lambda port: port.PortName == iqn,
+ iscsi_initiator_ports,
+ None)
+
+ if not iscsi_initiator:
+ scsi_port_data = self._api.build_scsi_port_data(
+ client.Id, iqn, 'Initiator', 'iSCSI')
+ iscsi_initiator = self._api.register_port(scsi_port_data)
+ return iscsi_initiator
+
+ def _get_targets(self, virtual_disk, available_ports):
+ unallowed_targets = self.configuration.datacore_iscsi_unallowed_targets
+ iscsi_target_ports = self._get_frontend_iscsi_target_ports(
+ available_ports)
+ server_port_map = {}
+ for target_port in iscsi_target_ports:
+ if target_port.HostId in server_port_map:
+ server_port_map[target_port.HostId].append(target_port)
+ else:
+ server_port_map[target_port.HostId] = [target_port]
+ iscsi_targets = []
+ if virtual_disk.FirstHostId in server_port_map:
+ iscsi_targets += server_port_map[virtual_disk.FirstHostId]
+ if virtual_disk.SecondHostId in server_port_map:
+ iscsi_targets += server_port_map[virtual_disk.SecondHostId]
+ iscsi_targets = [target for target in iscsi_targets
+ if target.PortName not in unallowed_targets]
+ return iscsi_targets
+
+ @staticmethod
+ def _get_logical_disk_on_host(virtual_disk_id,
+ host_id, logical_disks):
+ logical_disk = datacore_utils.get_first(
+ lambda disk: (disk.ServerHostId == host_id
+ and disk.VirtualDiskId == virtual_disk_id),
+ logical_disks)
+ return logical_disk
+
+ @staticmethod
+ def _is_iscsi_frontend_port(port):
+ if (port.PortType == 'iSCSI'
+ and port.PortMode == 'Target'
+ and port.HostId
+ and port.PresenceStatus == 'Present'
+ and hasattr(port, 'IScsiPortStateInfo')):
+ port_roles = port.ServerPortProperties.Role.split()
+ port_state = (port.IScsiPortStateInfo.PortalsState
+ .PortalStateInfo[0].State)
+ if 'Frontend' in port_roles and port_state == 'Ready':
+ return True
+ return False
+
+ @staticmethod
+ def _get_frontend_iscsi_target_ports(ports):
+ return [target_port for target_port in ports
+ if ISCSIVolumeDriver._is_iscsi_frontend_port(target_port)]
+
+ @staticmethod
+ def _get_host_iscsi_initiator_ports(host, ports):
+ return [port for port in ports
+ if port.PortType == 'iSCSI'
+ and port.PortMode == 'Initiator'
+ and port.HostId == host.Id]
diff --git a/cinder/volume/drivers/datacore/passwd.py b/cinder/volume/drivers/datacore/passwd.py
new file mode 100644
index 00000000000..96b4faca281
--- /dev/null
+++ b/cinder/volume/drivers/datacore/passwd.py
@@ -0,0 +1,166 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Password storage."""
+
+import contextlib
+import json
+import os
+import stat
+
+from oslo_log import log as logging
+
+from cinder.i18n import _
+from cinder import utils as cinder_utils
+
+
+LOG = logging.getLogger(__name__)
+
+
+class FileStorage(object):
+ """Represents a file as a dictionary."""
+
+ def __init__(self, file_path):
+ self._file_path = file_path
+ self._file = None
+ self._is_open = False
+
+ def open(self):
+ """Open a file for simultaneous reading and writing.
+
+ If the specified file does not exist, it will be created
+ with the 0600 access permissions for the current user, if needed
+ the appropriate directories will be created with the 0750 access
+ permissions for the current user.
+ """
+
+ file_dir = os.path.dirname(self._file_path)
+ if file_dir and not os.path.isdir(file_dir):
+ os.makedirs(file_dir)
+ os.chmod(file_dir, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP)
+ if not os.path.isfile(self._file_path):
+ open(self._file_path, 'w').close()
+ os.chmod(self._file_path, stat.S_IRUSR | stat.S_IWUSR)
+
+ if self._file:
+ self.close()
+ self._file = open(self._file_path, 'r+')
+ return self
+
+ def load(self):
+ """Reads the file and returns corresponded dictionary object.
+
+ :return: The dictionary that represents the file content.
+ """
+
+ storage = {}
+ if os.stat(self._file_path).st_size != 0:
+ storage = json.load(self._file)
+ if not isinstance(storage, dict):
+ msg = _('File %s has a malformed format.') % self._file_path
+ raise ValueError(msg)
+ return storage
+
+ def save(self, storage):
+ """Writes the specified dictionary to the file.
+
+ :param storage: Dictionary that should be written to the file.
+ """
+
+ if not isinstance(storage, dict):
+ msg = _('%s is not a dict.') % repr(storage)
+ raise TypeError(msg)
+
+ self._file.seek(0)
+ self._file.truncate()
+ json.dump(storage, self._file)
+
+ def close(self):
+ """Close the file."""
+
+ if self._file:
+ self._file.close()
+ self._file = None
+
+
+class PasswordFileStorage(object):
+ """Password storage implementation.
+
+ It stores passwords in a file in a clear text. The password file must be
+ secured by setting up file permissions.
+ """
+
+ def __init__(self, file_path):
+ self._file_path = file_path
+ self._file_storage = FileStorage(file_path)
+
+ def set_password(self, resource, username, password):
+ """Store the credential for the resource.
+
+ :param resource: Resource name for which credential will be stored
+ :param username: User name
+ :param password: Password
+ """
+
+ @cinder_utils.synchronized(
+ 'datacore-password_storage-' + self._file_path, external=True)
+ def _set_password():
+ with contextlib.closing(self._file_storage.open()) as storage:
+ passwords = storage.load()
+ if resource not in passwords:
+ passwords[resource] = {}
+ passwords[resource][username] = password
+ storage.save(passwords)
+
+ _set_password()
+
+ def get_password(self, resource, username):
+ """Returns the stored password for the resource.
+
+ If the password does not exist, it will return None
+
+ :param resource: Resource name for which credential was stored
+ :param username: User name
+ :return password: Password
+ """
+
+ @cinder_utils.synchronized(
+ 'datacore-password_storage-' + self._file_path, external=True)
+ def _get_password():
+ with contextlib.closing(self._file_storage.open()) as storage:
+ passwords = storage.load()
+ if resource in passwords:
+ return passwords[resource].get(username)
+
+ return _get_password()
+
+ def delete_password(self, resource, username):
+ """Delete the stored credential for the resource.
+
+ :param resource: Resource name for which credential was stored
+ :param username: User name
+ """
+
+ @cinder_utils.synchronized(
+ 'datacore-password_storage-' + self._file_path, external=True)
+ def _delete_password():
+ with contextlib.closing(self._file_storage.open()) as storage:
+ passwords = storage.load()
+ if resource in passwords and username in passwords[resource]:
+ del passwords[resource][username]
+ if not passwords[resource].keys():
+ del passwords[resource]
+ storage.save(passwords)
+
+ _delete_password()
diff --git a/cinder/volume/drivers/datacore/utils.py b/cinder/volume/drivers/datacore/utils.py
new file mode 100644
index 00000000000..5b82112f904
--- /dev/null
+++ b/cinder/volume/drivers/datacore/utils.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2017 DataCore Software Corp. 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.
+
+"""Utilities and helper functions."""
+
+from oslo_utils import netutils
+import six
+
+
+def build_network_address(host, port):
+ """Combines the specified host name or IP address with the specified port.
+
+ :param host: Host name or IP address in presentation (string) format
+ :param port: Port number
+ :return: The host name or IP address and port combination;
+ IPv6 addresses are enclosed in the square brackets
+ """
+ if netutils.is_valid_ipv6(host):
+ return '[%s]:%s' % (host, port)
+ else:
+ return '%s:%s' % (host, port)
+
+
+def get_first(predicate, source):
+ """Searches for an item that matches the conditions.
+
+ :param predicate: Defines the conditions of the item to search for
+ :param source: Iterable collection of items
+ :return: The first item that matches the conditions defined by the
+ specified predicate, if found; otherwise StopIteration is raised
+ """
+
+ return six.next(item for item in source if predicate(item))
+
+
+def get_first_or_default(predicate, source, default):
+ """Searches for an item that matches the conditions.
+
+ :param predicate: Defines the conditions of the item to search for
+ :param source: Iterable collection of items
+ :param default: Value that is returned if the iterator is exhausted
+ :return: The first item that matches the conditions defined by the
+ specified predicate, if found; otherwise the default value
+ """
+
+ try:
+ return get_first(predicate, source)
+ except StopIteration:
+ return default
+
+
+def get_distinct_by(key, source):
+ """Finds distinct items for the key and returns the result in a list.
+
+ :param key: Function computing a key value for each item
+ :param source: Iterable collection of items
+ :return: The list of distinct by the key value items
+ """
+
+ seen_keys = set()
+ return [item for item in source
+ if key(item) not in seen_keys and not seen_keys.add(key(item))]
diff --git a/driver-requirements.txt b/driver-requirements.txt
index 85c13fce6cd..20a4f4c77fd 100644
--- a/driver-requirements.txt
+++ b/driver-requirements.txt
@@ -43,3 +43,5 @@ capacity # BSD
infi.dtypes.wwn # PSF
infi.dtypes.iqn # PSF
+# DataCore SANsymphony
+websocket-client>=0.32.0 # LGPLv2+
diff --git a/releasenotes/notes/add-datacore-volume-driver-3775797b0515f538.yaml b/releasenotes/notes/add-datacore-volume-driver-3775797b0515f538.yaml
new file mode 100644
index 00000000000..810d04fdcff
--- /dev/null
+++ b/releasenotes/notes/add-datacore-volume-driver-3775797b0515f538.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - Added iSCSI and Fibre Channel volume drivers for DataCore’s
+ SANsymphony and Hyper-converged Virtual SAN storage.