Remove DataCore volume drivers
These drivers were marked as unsupported in Rocky due to lack of CI. Current CI report shows last result was posted well over 100 days ago. Change-Id: I3e814f43c9060a1f9ae10c6bee4020771ccb1eb5 Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
This commit is contained in:
parent
e125e57f1f
commit
7f2c9f103e
@ -71,10 +71,6 @@ from cinder import ssh_utils as cinder_sshutils
|
||||
from cinder.transfer import api as cinder_transfer_api
|
||||
from cinder.volume import api as cinder_volume_api
|
||||
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 \
|
||||
cinder_volume_drivers_datera_dateraiscsi
|
||||
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.backup_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_inspur_as13000_as13000driver.
|
||||
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/nfs-volume-driver
|
||||
drivers/sheepdog-driver
|
||||
drivers/datacore-volume-driver
|
||||
drivers/datera-volume-driver
|
||||
drivers/dell-equallogic-driver
|
||||
drivers/dell-storagecenter-driver
|
||||
|
@ -66,30 +66,6 @@ Kaminario iSCSI driver:
|
||||
- Where: `initialize_connection` and `terminate_connection` methods.
|
||||
- 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:
|
||||
- Lock scope: Global.
|
||||
- Critical section: Create or get a host on the backend.
|
||||
|
@ -15,9 +15,6 @@
|
||||
#####################################################################
|
||||
# Drivers:
|
||||
|
||||
[driver.datacore]
|
||||
title=DataCore Storage Driver (FC, iSCSI)
|
||||
|
||||
[driver.datera]
|
||||
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
|
||||
the driver is marked unsupported and is removed if the problem
|
||||
isn't resolved before the end of the subsequent release.
|
||||
driver.datacore=missing
|
||||
driver.datera=complete
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=complete
|
||||
@ -275,7 +271,6 @@ title=Extend an Attached Volume
|
||||
status=optional
|
||||
notes=Cinder supports the ability to extend a volume that is attached to
|
||||
an instance, but not all drivers are able to do this.
|
||||
driver.datacore=complete
|
||||
driver.datera=complete
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=complete
|
||||
@ -342,7 +337,6 @@ title=Snapshot Attachment
|
||||
status=optional
|
||||
notes=This is the ability to directly attach a snapshot to an
|
||||
instance like a volume.
|
||||
driver.datacore=missing
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=missing
|
||||
driver.dell_emc_ps=missing
|
||||
@ -410,7 +404,6 @@ status=optional
|
||||
notes=Vendor drivers that support Quality of Service (QoS) are able
|
||||
to utilize QoS Specs associated with volume extra specs to control
|
||||
QoS settings on a per volume basis.
|
||||
driver.datacore=missing
|
||||
driver.datera=complete
|
||||
driver.dell_emc_powermax=complete
|
||||
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
|
||||
replicated volumes via extra specs. Such drivers are also then able
|
||||
to take advantage of Cinder's failover and failback commands.
|
||||
driver.datacore=missing
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
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
|
||||
completed on the group of volumes, not individually, enabling the
|
||||
creation of consistent snapshots across a group.
|
||||
driver.datacore=missing
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
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
|
||||
than physically exists on the backend. This may also be called
|
||||
'oversubscription'.
|
||||
driver.datacore=missing
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=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
|
||||
can migrate volumes completely through the storage backend's
|
||||
functionality.
|
||||
driver.datacore=missing
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
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
|
||||
supports multi-attach functionality is required to use multi-
|
||||
attach functionality otherwise data corruption may occur.
|
||||
driver.datacore=missing
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
driver.dell_emc_ps=missing
|
||||
@ -825,7 +813,6 @@ title=Revert to Snapshot
|
||||
status=optional
|
||||
notes=Vendor drivers that implement the driver assisted function to revert a
|
||||
volume to the last snapshot taken.
|
||||
driver.datacore=missing
|
||||
driver.datera=missing
|
||||
driver.dell_emc_powermax=complete
|
||||
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…
Reference in New Issue
Block a user