Merge "Remove DataCore volume drivers"
This commit is contained in:
commit
e15a21e72b
@ -71,10 +71,6 @@ from cinder import ssh_utils as cinder_sshutils
|
|||||||
from cinder.transfer import api as cinder_transfer_api
|
from cinder.transfer import api as cinder_transfer_api
|
||||||
from cinder.volume import api as cinder_volume_api
|
from cinder.volume import api as cinder_volume_api
|
||||||
from cinder.volume import driver as cinder_volume_driver
|
from cinder.volume import driver as cinder_volume_driver
|
||||||
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 \
|
from cinder.volume.drivers.datera import datera_iscsi as \
|
||||||
cinder_volume_drivers_datera_dateraiscsi
|
cinder_volume_drivers_datera_dateraiscsi
|
||||||
from cinder.volume.drivers.dell_emc.powermax import common as \
|
from cinder.volume.drivers.dell_emc.powermax import common as \
|
||||||
@ -258,8 +254,6 @@ def list_opts():
|
|||||||
cinder_volume_driver.scst_opts,
|
cinder_volume_driver.scst_opts,
|
||||||
cinder_volume_driver.backup_opts,
|
cinder_volume_driver.backup_opts,
|
||||||
cinder_volume_driver.image_opts,
|
cinder_volume_driver.image_opts,
|
||||||
cinder_volume_drivers_datacore_driver.datacore_opts,
|
|
||||||
cinder_volume_drivers_datacore_iscsi.datacore_iscsi_opts,
|
|
||||||
cinder_volume_drivers_fusionstorage_dsware.volume_opts,
|
cinder_volume_drivers_fusionstorage_dsware.volume_opts,
|
||||||
cinder_volume_drivers_inspur_as13000_as13000driver.
|
cinder_volume_drivers_inspur_as13000_as13000driver.
|
||||||
inspur_as13000_opts,
|
inspur_as13000_opts,
|
||||||
|
@ -1,732 +0,0 @@
|
|||||||
# 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)
|
|
||||||
self.client.API_RETRY_INTERVAL = 0
|
|
||||||
|
|
||||||
# Make sure failure logging does not get emitted during testing
|
|
||||||
self.mock_object(api, 'LOG')
|
|
||||||
|
|
||||||
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 = """<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<wsdl:definitions name="ExecutiveServices"
|
|
||||||
targetNamespace="http://tempuri.org/"
|
|
||||||
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
|
|
||||||
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
|
||||||
xmlns:tns="http://tempuri.org/"
|
|
||||||
xmlns:wsa10="http://www.w3.org/2005/08/addressing"
|
|
||||||
xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl">
|
|
||||||
<wsdl:types>
|
|
||||||
<xs:schema elementFormDefault="qualified"
|
|
||||||
targetNamespace="http://tempuri.org/"
|
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
||||||
<xs:import
|
|
||||||
namespace="http://schemas.microsoft.com/2003/10/Serialization/Arrays"/>
|
|
||||||
<xs:import
|
|
||||||
namespace="http://schemas.datacontract.org/2004/07/DataCore.Executive"/>
|
|
||||||
<xs:element name="StartExecutive">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence/>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="StartExecutiveResponse">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence/>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="StopExecutive">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence/>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="StopExecutiveResponse">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence/>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="ExecutiveStarted">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence/>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
<xs:element name="ExecutiveStopped">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:sequence/>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:schema>
|
|
||||||
</wsdl:types>
|
|
||||||
<wsdl:message name="IExecutiveServiceEx_StartExecutive_InputMessage">
|
|
||||||
<wsdl:part name="parameters" element="tns:StartExecutive"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:message name="IExecutiveServiceEx_StartExecutive_OutputMessage">
|
|
||||||
<wsdl:part name="parameters" element="tns:StartExecutiveResponse"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:message
|
|
||||||
name="IExecutiveServiceEx_StartExecutive_ExecutiveError_FaultMessage">
|
|
||||||
<wsdl:part name="detail" element="ExecutiveError"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:message name="IExecutiveServiceEx_StopExecutive_InputMessage">
|
|
||||||
<wsdl:part name="parameters" element="tns:StopExecutive"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:message name="IExecutiveServiceEx_StopExecutive_OutputMessage">
|
|
||||||
<wsdl:part name="parameters" element="tns:StopExecutiveResponse"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:message
|
|
||||||
name="IExecutiveServiceEx_StopExecutive_ExecutiveError_FaultMessage">
|
|
||||||
<wsdl:part name="detail" element="ExecutiveError"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:message
|
|
||||||
name="IExecutiveServiceEx_ExecutiveStarted_OutputCallbackMessage">
|
|
||||||
<wsdl:part name="parameters" element="tns:ExecutiveStarted"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:message
|
|
||||||
name="IExecutiveServiceEx_ExecutiveStopped_OutputCallbackMessage">
|
|
||||||
<wsdl:part name="parameters" element="tns:ExecutiveStopped"/>
|
|
||||||
</wsdl:message>
|
|
||||||
<wsdl:portType name="IExecutiveServiceEx">
|
|
||||||
<wsdl:operation name="StartExecutive">
|
|
||||||
<wsdl:input
|
|
||||||
wsaw:Action="http://tempuri.org/IExecutiveService/StartExecutive"
|
|
||||||
message="tns:IExecutiveServiceEx_StartExecutive_InputMessage"/>
|
|
||||||
<wsdl:output
|
|
||||||
wsaw:Action="http://tempuri.org/IExecutiveService/StartExecutiveResponse"
|
|
||||||
message="tns:IExecutiveServiceEx_StartExecutive_OutputMessage"/>
|
|
||||||
<wsdl:fault wsaw:Action="ExecutiveError" name="ExecutiveError"
|
|
||||||
message="tns:IExecutiveServiceEx_StartExecutive_ExecutiveError_FaultMessage"/>
|
|
||||||
</wsdl:operation>
|
|
||||||
<wsdl:operation name="StopExecutive">
|
|
||||||
<wsdl:input
|
|
||||||
wsaw:Action="http://tempuri.org/IExecutiveService/StopExecutive"
|
|
||||||
message="tns:IExecutiveServiceEx_StopExecutive_InputMessage"/>
|
|
||||||
<wsdl:output
|
|
||||||
wsaw:Action="http://tempuri.org/IExecutiveService/StopExecutiveResponse"
|
|
||||||
message="tns:IExecutiveServiceEx_StopExecutive_OutputMessage"/>
|
|
||||||
<wsdl:fault wsaw:Action="ExecutiveError" name="ExecutiveError"
|
|
||||||
message="tns:IExecutiveServiceEx_StopExecutive_ExecutiveError_FaultMessage"/>
|
|
||||||
</wsdl:operation>
|
|
||||||
<wsdl:operation name="ExecutiveStarted">
|
|
||||||
<wsdl:output
|
|
||||||
wsaw:Action="http://tempuri.org/IExecutiveService/ExecutiveStarted"
|
|
||||||
message="tns:IExecutiveServiceEx_ExecutiveStarted_OutputCallbackMessage"/>
|
|
||||||
<wsdl:fault wsaw:Action="ExecutiveError" name="ExecutiveError"
|
|
||||||
message="tns:"/>
|
|
||||||
</wsdl:operation>
|
|
||||||
<wsdl:operation name="ExecutiveStopped">
|
|
||||||
<wsdl:output
|
|
||||||
wsaw:Action="http://tempuri.org/IExecutiveService/ExecutiveStopped"
|
|
||||||
message="tns:IExecutiveServiceEx_ExecutiveStopped_OutputCallbackMessage"/>
|
|
||||||
<wsdl:fault wsaw:Action="ExecutiveError" name="ExecutiveError"
|
|
||||||
message="tns:"/>
|
|
||||||
</wsdl:operation>
|
|
||||||
</wsdl:portType>
|
|
||||||
<wsdl:binding name="CustomBinding_IExecutiveServiceEx"
|
|
||||||
type="tns:IExecutiveServiceEx">
|
|
||||||
<soap:binding transport="http://schemas.microsoft.com/soap/websocket"/>
|
|
||||||
<wsdl:operation name="StartExecutive">
|
|
||||||
<soap:operation
|
|
||||||
soapAction="http://tempuri.org/IExecutiveService/StartExecutive"
|
|
||||||
style="document"/>
|
|
||||||
<wsdl:input>
|
|
||||||
<soap:body use="literal"/>
|
|
||||||
</wsdl:input>
|
|
||||||
<wsdl:output>
|
|
||||||
<soap:body use="literal"/>
|
|
||||||
</wsdl:output>
|
|
||||||
<wsdl:fault name="ExecutiveError">
|
|
||||||
<soap:fault use="literal" name="ExecutiveError" namespace=""/>
|
|
||||||
</wsdl:fault>
|
|
||||||
</wsdl:operation>
|
|
||||||
<wsdl:operation name="StopExecutive">
|
|
||||||
<soap:operation
|
|
||||||
soapAction="http://tempuri.org/IExecutiveService/StopExecutive"
|
|
||||||
style="document"/>
|
|
||||||
<wsdl:input>
|
|
||||||
<soap:body use="literal"/>
|
|
||||||
</wsdl:input>
|
|
||||||
<wsdl:output>
|
|
||||||
<soap:body use="literal"/>
|
|
||||||
</wsdl:output>
|
|
||||||
<wsdl:fault name="ExecutiveError">
|
|
||||||
<soap:fault use="literal" name="ExecutiveError" namespace=""/>
|
|
||||||
</wsdl:fault>
|
|
||||||
</wsdl:operation>
|
|
||||||
<wsdl:operation name="ExecutiveStarted">
|
|
||||||
<soap:operation
|
|
||||||
soapAction="http://tempuri.org/IExecutiveService/ExecutiveStarted"
|
|
||||||
style="document"/>
|
|
||||||
<wsdl:output>
|
|
||||||
<soap:body use="literal"/>
|
|
||||||
</wsdl:output>
|
|
||||||
<wsdl:fault name="ExecutiveError">
|
|
||||||
<soap:fault use="literal" name="ExecutiveError" namespace=""/>
|
|
||||||
</wsdl:fault>
|
|
||||||
</wsdl:operation>
|
|
||||||
<wsdl:operation name="ExecutiveStopped">
|
|
||||||
<soap:operation
|
|
||||||
soapAction="http://tempuri.org/IExecutiveService/ExecutiveStopped"
|
|
||||||
style="document"/>
|
|
||||||
<wsdl:output>
|
|
||||||
<soap:body use="literal"/>
|
|
||||||
</wsdl:output>
|
|
||||||
<wsdl:fault name="ExecutiveError">
|
|
||||||
<soap:fault use="literal" name="ExecutiveError" namespace=""/>
|
|
||||||
</wsdl:fault>
|
|
||||||
</wsdl:operation>
|
|
||||||
</wsdl:binding>
|
|
||||||
<wsdl:service name="ExecutiveServices">
|
|
||||||
<wsdl:port name="CustomBinding_IExecutiveServiceEx"
|
|
||||||
binding="tns:CustomBinding_IExecutiveServiceEx">
|
|
||||||
<soap:address
|
|
||||||
location="ws://mns-vsp-001:3794/IExecutiveServiceEx"/>
|
|
||||||
<wsa10:EndpointReference>
|
|
||||||
<wsa10:Address>ws://mns-vsp-001:3794/IExecutiveServiceEx
|
|
||||||
</wsa10:Address>
|
|
||||||
</wsa10:EndpointReference>
|
|
||||||
</wsdl:port>
|
|
||||||
</wsdl:service>
|
|
||||||
</wsdl:definitions>"""
|
|
||||||
|
|
||||||
|
|
||||||
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'))
|
|
@ -1,702 +0,0 @@
|
|||||||
# 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.tests.unit import utils as testutils
|
|
||||||
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.override_config('datacore_disk_failed_delay', 0)
|
|
||||||
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 = 0
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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()
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
||||||
|
|
||||||
@mock.patch('oslo_service.loopingcall.FixedIntervalLoopingCall',
|
|
||||||
new=testutils.ZeroIntervalLoopingCall)
|
|
||||||
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)
|
|
@ -1,256 +0,0 @@
|
|||||||
# 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)
|
|
@ -1,515 +0,0 @@
|
|||||||
# 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)
|
|
@ -1,289 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
@ -1,78 +0,0 @@
|
|||||||
# 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)
|
|
File diff suppressed because it is too large
Load Diff
@ -1,746 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_driver_options():
|
|
||||||
return datacore_opts
|
|
||||||
|
|
||||||
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)
|
|
@ -1,36 +0,0 @@
|
|||||||
# 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.')
|
|
@ -1,186 +0,0 @@
|
|||||||
# 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'
|
|
||||||
|
|
||||||
# TODO(jsbryant) Remove driver in Stein if CI is not fixed
|
|
||||||
SUPPORTED = False
|
|
||||||
|
|
||||||
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
|
|
@ -1,443 +0,0 @@
|
|||||||
# 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 '
|
|
||||||
'<iqn:target name>, <iqn:target name>, '
|
|
||||||
'<iqn:target name>.'),
|
|
||||||
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'
|
|
||||||
|
|
||||||
# TODO(jsbryant) Remove driver in Stein if CI is not fixed
|
|
||||||
SUPPORTED = False
|
|
||||||
|
|
||||||
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]
|
|
@ -1,171 +0,0 @@
|
|||||||
# 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 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
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
|
|
||||||
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 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 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 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()
|
|
@ -1,73 +0,0 @@
|
|||||||
# 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))]
|
|
@ -1,346 +0,0 @@
|
|||||||
==================================
|
|
||||||
DataCore SANsymphony volume driver
|
|
||||||
==================================
|
|
||||||
|
|
||||||
DataCore SANsymphony volume driver provides OpenStack Compute instances with
|
|
||||||
access to the SANsymphony(TM) Software-defined Storage Platform.
|
|
||||||
|
|
||||||
When volumes are created in OpenStack, the driver creates corresponding
|
|
||||||
virtual disks in the SANsymphony server group. When a volume is attached to an
|
|
||||||
instance in OpenStack, a Linux host is registered and the corresponding virtual
|
|
||||||
disk is served to the host in the SANsymphony server group.
|
|
||||||
|
|
||||||
Requirements
|
|
||||||
-------------
|
|
||||||
|
|
||||||
* DataCore server group running SANsymphony software version 10 PSP6
|
|
||||||
or later.
|
|
||||||
|
|
||||||
* OpenStack Integration has been tested with the OpenStack environment
|
|
||||||
installed on Ubuntu 16.04. For the list of qualified Linux host operating
|
|
||||||
system types, refer to the `Linux Host Configuration
|
|
||||||
Guide <https://datacore.custhelp.com/app/answers/detail/a_id/1546>`_
|
|
||||||
on the `DataCore Technical Support Web
|
|
||||||
page <https://datacore.custhelp.com/>`_.
|
|
||||||
|
|
||||||
* If using multipath I/O, ensure that iSCSI ports are logged in on all
|
|
||||||
OpenStack Compute nodes. (All Fibre Channel ports will be logged in
|
|
||||||
automatically.)
|
|
||||||
|
|
||||||
Python dependencies
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
* ``websocket-client>=0.32.0``
|
|
||||||
|
|
||||||
Install this package using pip:
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ sudo pip install "websocket-client>=0.32.0"
|
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
The volume driver can be configured by editing the `cinder.conf` file.
|
|
||||||
The options below can be configured either per server group or as extra
|
|
||||||
specifications in a volume type configuration.
|
|
||||||
|
|
||||||
Configuration options and default values:
|
|
||||||
|
|
||||||
* ``datacore_disk_pools = None``
|
|
||||||
|
|
||||||
Sets the pools to use for the DataCore OpenStack Cinder Volume Driver. This
|
|
||||||
option acts like a filter and any number of pools may be specified. The list
|
|
||||||
of specified pools will be used to select the storage sources needed for
|
|
||||||
virtual disks; one for single or two for mirrored. Selection is based on
|
|
||||||
the pools with the most free space.
|
|
||||||
|
|
||||||
This option may also be specified as an extra specification of a volume
|
|
||||||
type.
|
|
||||||
|
|
||||||
* ``datacore_disk_type = single``
|
|
||||||
|
|
||||||
Sets the SANsymphony virtual disk type (single or mirrored). **Single**
|
|
||||||
virtual disks are created by default. Specify **mirrored** to override this
|
|
||||||
behavior. Mirrored virtual disks require two DataCore Servers in the server
|
|
||||||
group.
|
|
||||||
|
|
||||||
This option may also be specified as an extra specification of a volume
|
|
||||||
type.
|
|
||||||
|
|
||||||
* ``datacore_storage_profile = Normal``
|
|
||||||
|
|
||||||
Sets the storage profile of the virtual disk. The default setting is Normal.
|
|
||||||
Other valid values include the standard storage profiles (Critical, High,
|
|
||||||
Low, and Archive) and the names of custom profiles that have been created.
|
|
||||||
|
|
||||||
This option may also be specified as an extra specification of a volume
|
|
||||||
type.
|
|
||||||
|
|
||||||
* ``datacore_api_timeout = 300``
|
|
||||||
|
|
||||||
Sets the number of seconds to wait for a response from a DataCore API call.
|
|
||||||
|
|
||||||
This option is used in the server group back-end configuration only.
|
|
||||||
|
|
||||||
* ``datacore_disk_failed_delay = 15``
|
|
||||||
|
|
||||||
Sets the number of seconds to wait for the SANsymphony virtual disk to come
|
|
||||||
out of the "Failed" state.
|
|
||||||
|
|
||||||
This option is used in the server group back-end configuration only.
|
|
||||||
|
|
||||||
* ``datacore_iscsi_unallowed_targets = []``
|
|
||||||
|
|
||||||
Sets a list of iSCSI targets that cannot be used to attach to the volume.
|
|
||||||
By default, the DataCore iSCSI volume driver attaches a volume through all
|
|
||||||
target ports with the Front-end role enabled, unlike the DataCore Fibre
|
|
||||||
Channel volume driver that attaches a volume only through target ports
|
|
||||||
connected to initiator.
|
|
||||||
|
|
||||||
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 ``<iqn:target name>,
|
|
||||||
<iqn:target name>, <iqn:target name>``. For example,
|
|
||||||
``<iqn.2000-08.com.company:Server1-1, iqn.2000-08.com.company:Server2-1,
|
|
||||||
iqn.2000-08.com.company:Server3-1>``.
|
|
||||||
|
|
||||||
This option is used in the server group back-end configuration only.
|
|
||||||
|
|
||||||
* ``datacore_iscsi_chap_enabled = False``
|
|
||||||
|
|
||||||
Sets the CHAP authentication for the iSCSI targets that are used to serve
|
|
||||||
the volume. This option is disabled by default and will allow hosts
|
|
||||||
(OpenStack Compute nodes) to connect to iSCSI storage back-ends without
|
|
||||||
authentication. To enable CHAP authentication, which will prevent hosts
|
|
||||||
(OpenStack Compute nodes) from connecting to back-ends without
|
|
||||||
authentication, set this option to **True**.
|
|
||||||
|
|
||||||
In addition, specify the location where the DataCore volume driver will
|
|
||||||
store CHAP secrets by setting the **datacore_iscsi_chap_storage option**.
|
|
||||||
|
|
||||||
This option is used in the server group back-end configuration only.
|
|
||||||
The driver will enable CHAP only for involved target ports, therefore, not
|
|
||||||
all DataCore Servers may have CHAP configured. *Before enabling CHAP, ensure
|
|
||||||
that there are no SANsymphony volumes attached to any instances.*
|
|
||||||
|
|
||||||
* ``datacore_iscsi_chap_storage = None``
|
|
||||||
|
|
||||||
Sets the path to the iSCSI CHAP authentication password storage file.
|
|
||||||
|
|
||||||
*CHAP secrets are passed from OpenStack Block Storage to compute in clear
|
|
||||||
text. This communication should be secured to ensure that CHAP secrets are
|
|
||||||
not compromised. This can be done by setting up file permissions. Before
|
|
||||||
changing the CHAP configuration, ensure that there are no SANsymphony
|
|
||||||
volumes attached to any instances.*
|
|
||||||
|
|
||||||
This option is used in the server group back-end configuration only.
|
|
||||||
|
|
||||||
Configuration Examples
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Examples of option configuration in the ``cinder.conf`` file.
|
|
||||||
|
|
||||||
* An example using **datacore_disk_pools**, **datacore_disk_type**, and
|
|
||||||
**datacore_storage_profile** to create a mirrored virtual disk with a High
|
|
||||||
priority storage profile using specific pools:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
volume_driver = cinder.volume.drivers.datacore.iscsi.ISCSIVolumeDriver
|
|
||||||
|
|
||||||
san_ip = <DataCore Server IP or DNS name>
|
|
||||||
|
|
||||||
san_login = <User Name>
|
|
||||||
|
|
||||||
san_password = <Password>
|
|
||||||
|
|
||||||
datacore_disk_type = mirrored
|
|
||||||
|
|
||||||
datacore_disk_pools = Disk pool 1, Disk pool 2
|
|
||||||
|
|
||||||
datacore_storage_profile = High
|
|
||||||
|
|
||||||
* An example using **datacore_iscsi_unallowed_targets** to prevent the volume
|
|
||||||
from using the specified targets:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
volume_driver = cinder.volume.drivers.datacore.iscsi.ISCSIVolumeDriver
|
|
||||||
|
|
||||||
san_ip = <DataCore Server IP or DNS name>
|
|
||||||
|
|
||||||
san_login = <User Name>
|
|
||||||
|
|
||||||
san_password = <Password>
|
|
||||||
|
|
||||||
datacore_iscsi_unallowed_targets = iqn.2000-08.com.datacore:mns-ssv-10-1,iqn.2000-08.com.datacore:mns-ssvdev-01-1
|
|
||||||
|
|
||||||
* An example using **datacore_iscsi_chap_enabled** and
|
|
||||||
**datacore_iscsi_chap_storage** to enable CHAP authentication and provide
|
|
||||||
the path to the CHAP password storage file:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
volume_driver = cinder.volume.drivers.datacore.iscsi.ISCSIVolumeDriver
|
|
||||||
|
|
||||||
datacore_iscsi_chap_enabled = True
|
|
||||||
|
|
||||||
datacore_iscsi_chap_storage = /var/lib/cinder/datacore/.chap
|
|
||||||
|
|
||||||
DataCore volume driver stores CHAP secrets in clear text, and the password
|
|
||||||
file must be secured by setting up file permissions. The following example
|
|
||||||
shows how to create a password file and set up permissions. It assumes that
|
|
||||||
the cinder-volume service is running under the user `cinder`.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ sudo mkdir /var/lib/cinder/datacore -p
|
|
||||||
|
|
||||||
$ sudo /bin/sh -c "> /var/lib/cinder/datacore/.chap"
|
|
||||||
|
|
||||||
$ sudo chown cinder:cinder /var/lib/cinder/datacore
|
|
||||||
|
|
||||||
$ sudo chmod -v 750 /var/lib/cinder/datacore
|
|
||||||
|
|
||||||
$ sudo chown cinder:cinder /var/lib/cinder/datacore/.chap
|
|
||||||
|
|
||||||
$ sudo chmod -v 600 /var/lib/cinder/datacore/.chap
|
|
||||||
|
|
||||||
After setting **datacore_iscsi_chap_enabled** and
|
|
||||||
**datacore_iscsi_chap_storage**, CHAP authentication will be enabled in
|
|
||||||
SANsymphony.
|
|
||||||
|
|
||||||
Creating Volume Types
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
Volume types can be created with the DataCore disk type specified in
|
|
||||||
the datacore:disk_type extra specification. In the following example, a volume
|
|
||||||
type named mirrored_disk is created and the disk type is set to mirrored.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ cinder type-create mirrored_disk
|
|
||||||
|
|
||||||
$ cinder type-key mirrored_disk set datacore:disk_type=mirrored
|
|
||||||
|
|
||||||
In addition, volume specifications can also be declared as extra specifications
|
|
||||||
for volume types. The example below sets additional configuration options for
|
|
||||||
the volume type mirrored_disk; storage profile will be set to High and virtual
|
|
||||||
disks will be created from Disk pool 1, Disk pool 2, or Disk pool 3.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
$ cinder type-key mirrored_disk set datacore:storage_profile=High
|
|
||||||
|
|
||||||
$ cinder type-key mirrored_disk set "datacore:disk_pools=Disk pool 1, Disk pool 2, Disk pool 3"
|
|
||||||
|
|
||||||
Configuring Multiple Storage Back Ends
|
|
||||||
--------------------------------------
|
|
||||||
|
|
||||||
OpenStack Block Storage can be configured to use several back-end storage
|
|
||||||
solutions. Multiple back-end configuration allows you to configure different
|
|
||||||
storage configurations for SANsymphony server groups. The configuration options
|
|
||||||
for a group must be defined in the group.
|
|
||||||
|
|
||||||
To enable multiple back ends:
|
|
||||||
|
|
||||||
1. In the ``cinder.conf`` file, set the **enabled_backends** option to identify
|
|
||||||
the groups. One name is associated with each server group back-end
|
|
||||||
configuration. In the example below there are two groups, ``datacore-1``
|
|
||||||
and ``datacore-2``:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[DEFAULT]
|
|
||||||
|
|
||||||
enabled_backends = datacore-1, datacore-2
|
|
||||||
|
|
||||||
2. Define the back-end storage used by each server group in a separate section
|
|
||||||
(for example ``[datacore-1]``):
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
[datacore-1]
|
|
||||||
|
|
||||||
volume_driver = cinder.volume.drivers.datacore.iscsi.ISCSIVolumeDriver
|
|
||||||
|
|
||||||
volume_backend_name = DataCore_iSCSI
|
|
||||||
|
|
||||||
san_ip = <ip_or_dns_name>
|
|
||||||
|
|
||||||
san_login = <user_name>
|
|
||||||
|
|
||||||
san_password = <password>
|
|
||||||
|
|
||||||
datacore_iscsi_chap_enabled = True
|
|
||||||
|
|
||||||
datacore_iscsi_chap_storage = /var/lib/cinder/datacore/.chap
|
|
||||||
|
|
||||||
datacore_iscsi_unallowed_targets = iqn.2000-08.com.datacore:mns-ssv-10-1
|
|
||||||
|
|
||||||
datacore_disk_type = mirrored
|
|
||||||
|
|
||||||
[datacore-2]
|
|
||||||
|
|
||||||
volume_driver = cinder.volume.drivers.datacore.fc.FibreChannelVolumeDriver
|
|
||||||
|
|
||||||
volume_backend_name = DataCore_FibreChannel
|
|
||||||
|
|
||||||
san_ip = <ip_or_dns_name>
|
|
||||||
|
|
||||||
san_login = <user_name>
|
|
||||||
|
|
||||||
san_password = <password>
|
|
||||||
|
|
||||||
datacore_disk_type = mirrored
|
|
||||||
|
|
||||||
datacore_disk_pools = Disk pool 1, Disk pool 2
|
|
||||||
|
|
||||||
datacore_storage_profile = High
|
|
||||||
|
|
||||||
3. Create the volume types
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
$ cinder type-create datacore_iscsi
|
|
||||||
|
|
||||||
$ cinder type-create datacore_fc
|
|
||||||
|
|
||||||
4. Add an extra specification to link the volume type to a back-end name:
|
|
||||||
|
|
||||||
.. code-block:: ini
|
|
||||||
|
|
||||||
$ cinder type-key datacore_iscsi set volume_backend_name=DataCore_iSCSI
|
|
||||||
|
|
||||||
$ cinder type-key datacore_fc set volume_backend_name=DataCore_FibreChannel
|
|
||||||
|
|
||||||
See `Configure multiple-storage back ends
|
|
||||||
<https://docs.openstack.org/cinder/latest/admin/blockstorage-multi-backend.html>`__
|
|
||||||
for additional information.
|
|
||||||
|
|
||||||
Detaching Volumes and Terminating Instances
|
|
||||||
-------------------------------------------
|
|
||||||
|
|
||||||
Notes about the expected behavior of SANsymphony software when detaching
|
|
||||||
volumes and terminating instances in OpenStack:
|
|
||||||
|
|
||||||
1. When a volume is detached from a host in OpenStack, the virtual disk will be
|
|
||||||
unserved from the host in SANsymphony, but the virtual disk will not be
|
|
||||||
deleted.
|
|
||||||
|
|
||||||
2. If all volumes are detached from a host in OpenStack, the host will remain
|
|
||||||
registered and all virtual disks will be unserved from that host in
|
|
||||||
SANsymphony. The virtual disks will not be deleted.
|
|
||||||
|
|
||||||
3. If an instance is terminated in OpenStack, the virtual disk for the instance
|
|
||||||
will be unserved from the host and either be deleted or remain as unserved
|
|
||||||
virtual disk depending on the option selected when terminating.
|
|
||||||
|
|
||||||
Support
|
|
||||||
-------
|
|
||||||
|
|
||||||
In the event that a support bundle is needed, the administrator should save
|
|
||||||
the files from the ``/var/log`` folder on the Linux host and attach to DataCore
|
|
||||||
Technical Support incident manually.
|
|
@ -31,7 +31,6 @@ Driver Configuration Reference
|
|||||||
drivers/lvm-volume-driver
|
drivers/lvm-volume-driver
|
||||||
drivers/nfs-volume-driver
|
drivers/nfs-volume-driver
|
||||||
drivers/sheepdog-driver
|
drivers/sheepdog-driver
|
||||||
drivers/datacore-volume-driver
|
|
||||||
drivers/datera-volume-driver
|
drivers/datera-volume-driver
|
||||||
drivers/dell-equallogic-driver
|
drivers/dell-equallogic-driver
|
||||||
drivers/dell-storagecenter-driver
|
drivers/dell-storagecenter-driver
|
||||||
|
@ -66,30 +66,6 @@ Kaminario iSCSI driver:
|
|||||||
- Where: `initialize_connection` and `terminate_connection` methods.
|
- Where: `initialize_connection` and `terminate_connection` methods.
|
||||||
- File: `cinder/volume/drivers/kaminario/kaminario_iscsi.py`
|
- File: `cinder/volume/drivers/kaminario/kaminario_iscsi.py`
|
||||||
|
|
||||||
DataCore SANsymphony:
|
|
||||||
- Lock scope: Process.
|
|
||||||
- Critical section: Get SOAP context.
|
|
||||||
- Lock name: `datacore-api-request_context`
|
|
||||||
- Where: `_get_soap_context`
|
|
||||||
- File: `cinder/volume/drivers/datacore/api.py`
|
|
||||||
|
|
||||||
DataCore SANsymphony:
|
|
||||||
- Lock scope: Node.
|
|
||||||
- Critical section: Call to backend to serve/unserve Virtual Disk from host.
|
|
||||||
- Lock name: `datacore-backend-{server_group.Id}`
|
|
||||||
- Where: `terminate_connection.unserve_virtual_disk`, and
|
|
||||||
`initialize_connection.serve_virtual_disk` methods.
|
|
||||||
- File: `cinder/volume/drivers/datacore/driver.py`,
|
|
||||||
`cinder/volume/drivers/datacore/iscsi.py`, and
|
|
||||||
`cinder/volume/drivers/datacore/fc.py`
|
|
||||||
|
|
||||||
DataCore SANsymphony:
|
|
||||||
- Lock scope: Node.
|
|
||||||
- Critical section: Modify CHAP passwords on local storage file.
|
|
||||||
- Lock name: `datacore-password_storage-{self._file_path}`
|
|
||||||
- Where: `set_password`, `get_password`, and `delete_password`.
|
|
||||||
- File: `cinder/volume/drivers/datacore/passwd.py`
|
|
||||||
|
|
||||||
Dell EMC Unity:
|
Dell EMC Unity:
|
||||||
- Lock scope: Global.
|
- Lock scope: Global.
|
||||||
- Critical section: Create or get a host on the backend.
|
- Critical section: Create or get a host on the backend.
|
||||||
|
@ -15,9 +15,6 @@
|
|||||||
#####################################################################
|
#####################################################################
|
||||||
# Drivers:
|
# Drivers:
|
||||||
|
|
||||||
[driver.datacore]
|
|
||||||
title=DataCore Storage Driver (FC, iSCSI)
|
|
||||||
|
|
||||||
[driver.datera]
|
[driver.datera]
|
||||||
title=Datera Storage Driver (iSCSI)
|
title=Datera Storage Driver (iSCSI)
|
||||||
|
|
||||||
@ -208,7 +205,6 @@ notes=A vendor driver is considered supported if the vendor is
|
|||||||
accurate results. If a vendor doesn't meet this requirement
|
accurate results. If a vendor doesn't meet this requirement
|
||||||
the driver is marked unsupported and is removed if the problem
|
the driver is marked unsupported and is removed if the problem
|
||||||
isn't resolved before the end of the subsequent release.
|
isn't resolved before the end of the subsequent release.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=complete
|
driver.datera=complete
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=complete
|
driver.dell_emc_ps=complete
|
||||||
@ -275,7 +271,6 @@ title=Extend an Attached Volume
|
|||||||
status=optional
|
status=optional
|
||||||
notes=Cinder supports the ability to extend a volume that is attached to
|
notes=Cinder supports the ability to extend a volume that is attached to
|
||||||
an instance, but not all drivers are able to do this.
|
an instance, but not all drivers are able to do this.
|
||||||
driver.datacore=complete
|
|
||||||
driver.datera=complete
|
driver.datera=complete
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=complete
|
driver.dell_emc_ps=complete
|
||||||
@ -342,7 +337,6 @@ title=Snapshot Attachment
|
|||||||
status=optional
|
status=optional
|
||||||
notes=This is the ability to directly attach a snapshot to an
|
notes=This is the ability to directly attach a snapshot to an
|
||||||
instance like a volume.
|
instance like a volume.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=missing
|
driver.dell_emc_powermax=missing
|
||||||
driver.dell_emc_ps=missing
|
driver.dell_emc_ps=missing
|
||||||
@ -410,7 +404,6 @@ status=optional
|
|||||||
notes=Vendor drivers that support Quality of Service (QoS) are able
|
notes=Vendor drivers that support Quality of Service (QoS) are able
|
||||||
to utilize QoS Specs associated with volume extra specs to control
|
to utilize QoS Specs associated with volume extra specs to control
|
||||||
QoS settings on a per volume basis.
|
QoS settings on a per volume basis.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=complete
|
driver.datera=complete
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=missing
|
driver.dell_emc_ps=missing
|
||||||
@ -479,7 +472,6 @@ notes=Vendor drivers that support volume replication can report this
|
|||||||
capability to be utilized by the scheduler allowing users to request
|
capability to be utilized by the scheduler allowing users to request
|
||||||
replicated volumes via extra specs. Such drivers are also then able
|
replicated volumes via extra specs. Such drivers are also then able
|
||||||
to take advantage of Cinder's failover and failback commands.
|
to take advantage of Cinder's failover and failback commands.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=missing
|
driver.dell_emc_ps=missing
|
||||||
@ -549,7 +541,6 @@ notes=Vendor drivers that support consistency groups are able to
|
|||||||
deletion. Grouping the volumes ensures that operations are only
|
deletion. Grouping the volumes ensures that operations are only
|
||||||
completed on the group of volumes, not individually, enabling the
|
completed on the group of volumes, not individually, enabling the
|
||||||
creation of consistent snapshots across a group.
|
creation of consistent snapshots across a group.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=missing
|
driver.dell_emc_ps=missing
|
||||||
@ -618,7 +609,6 @@ notes=If a volume driver supports thin provisioning it means that it
|
|||||||
will allow the scheduler to provision more storage space
|
will allow the scheduler to provision more storage space
|
||||||
than physically exists on the backend. This may also be called
|
than physically exists on the backend. This may also be called
|
||||||
'oversubscription'.
|
'oversubscription'.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=complete
|
driver.dell_emc_ps=complete
|
||||||
@ -688,7 +678,6 @@ notes=Storage assisted volume migration is like host assisted volume
|
|||||||
assistance of the Cinder host. Vendor drivers that implement this
|
assistance of the Cinder host. Vendor drivers that implement this
|
||||||
can migrate volumes completely through the storage backend's
|
can migrate volumes completely through the storage backend's
|
||||||
functionality.
|
functionality.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=missing
|
driver.dell_emc_ps=missing
|
||||||
@ -758,7 +747,6 @@ notes=Vendor drivers that report multi-attach support are able
|
|||||||
It is important to note that a clustered file system that
|
It is important to note that a clustered file system that
|
||||||
supports multi-attach functionality is required to use multi-
|
supports multi-attach functionality is required to use multi-
|
||||||
attach functionality otherwise data corruption may occur.
|
attach functionality otherwise data corruption may occur.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=missing
|
driver.dell_emc_ps=missing
|
||||||
@ -825,7 +813,6 @@ title=Revert to Snapshot
|
|||||||
status=optional
|
status=optional
|
||||||
notes=Vendor drivers that implement the driver assisted function to revert a
|
notes=Vendor drivers that implement the driver assisted function to revert a
|
||||||
volume to the last snapshot taken.
|
volume to the last snapshot taken.
|
||||||
driver.datacore=missing
|
|
||||||
driver.datera=missing
|
driver.datera=missing
|
||||||
driver.dell_emc_powermax=complete
|
driver.dell_emc_powermax=complete
|
||||||
driver.dell_emc_ps=missing
|
driver.dell_emc_ps=missing
|
||||||
|
5
releasenotes/notes/remove-datacore-300c667e9f504590.yaml
Normal file
5
releasenotes/notes/remove-datacore-300c667e9f504590.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
The DataCore drivers were marked as unsupported in the Rocky release and
|
||||||
|
have now been removed.
|
Loading…
x
Reference in New Issue
Block a user