From a114dd3c83df72afcf64aa89fb2f10361102bdb7 Mon Sep 17 00:00:00 2001 From: Tom Swanson Date: Wed, 3 Dec 2014 13:24:39 -0600 Subject: [PATCH] Add Support for Dell Storage Center This driver implements cinder FC and iSCSI volume drivers. The file dell_storagecenter_api.py is called to interface with the Dell Storage Center backend via a REST interface. Dell_storagecenter_common.py is the base class implementation with dell_storagecenter_fc.py and dell_storagecenter_iscsi.py being providing fc and iscsi specific support respectivly. https://bugs.launchpad.net/cinder/+bug/1398951 Implements: blueprint dell-storage-center-block-storage-driver Change-Id: Ic483e54b40349ede20e078c6406f5bab8d7d7cc2 --- cinder/tests/test_dellfc.py | 543 +++++++++ cinder/tests/test_dellsc.py | 917 +++++++++++++++ cinder/volume/drivers/dell/__init__.py | 0 .../drivers/dell/dell_storagecenter_api.py | 1046 +++++++++++++++++ .../drivers/dell/dell_storagecenter_common.py | 332 ++++++ .../drivers/dell/dell_storagecenter_fc.py | 168 +++ .../drivers/dell/dell_storagecenter_iscsi.py | 140 +++ 7 files changed, 3146 insertions(+) create mode 100644 cinder/tests/test_dellfc.py create mode 100644 cinder/tests/test_dellsc.py create mode 100644 cinder/volume/drivers/dell/__init__.py create mode 100644 cinder/volume/drivers/dell/dell_storagecenter_api.py create mode 100644 cinder/volume/drivers/dell/dell_storagecenter_common.py create mode 100644 cinder/volume/drivers/dell/dell_storagecenter_fc.py create mode 100644 cinder/volume/drivers/dell/dell_storagecenter_iscsi.py diff --git a/cinder/tests/test_dellfc.py b/cinder/tests/test_dellfc.py new file mode 100644 index 000000000..7a653eeb1 --- /dev/null +++ b/cinder/tests/test_dellfc.py @@ -0,0 +1,543 @@ +# Copyright (c) 2014 Dell Inc. +# +# 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. + +import mock + +from cinder import context +from cinder import exception +from cinder.openstack.common import log as logging +from cinder import test +from cinder.volume.drivers.dell import dell_storagecenter_api +from cinder.volume.drivers.dell import dell_storagecenter_fc + + +LOG = logging.getLogger(__name__) + +# We patch these here as they are used by every test to keep +# from trying to contact a Dell Storage Center. + + +@mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '__init__', + return_value=None) +@mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'open_connection') +@mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'close_connection') +class DellSCSanFCDriverTestCase(test.TestCase): + + VOLUME = {u'instanceId': u'64702.4829', + u'scSerialNumber': 64702, + u'replicationSource': False, + u'liveVolume': False, + u'vpdId': 4831, + u'objectType': u'ScVolume', + u'index': 4829, + u'volumeFolderPath': u'dopnstktst/', + u'hostCacheEnabled': False, + u'usedByLegacyFluidFsNasVolume': False, + u'inRecycleBin': False, + u'volumeFolderIndex': 17, + u'instanceName': u'5729f1db-4c45-416c-bc15-c8ea13a4465d', + u'statusMessage': u'', + u'status': u'Down', + u'storageType': {u'instanceId': u'64702.1', + u'instanceName': u'Assigned - Redundant - 2 MB', + u'objectType': u'ScStorageType'}, + u'cmmDestination': False, + u'replicationDestination': False, + u'volumeFolder': {u'instanceId': u'64702.17', + u'instanceName': u'opnstktst', + u'objectType': u'ScVolumeFolder'}, + u'deviceId': u'6000d31000fcbe0000000000000012df', + u'active': False, + u'portableVolumeDestination': False, + u'deleteAllowed': True, + u'name': u'5729f1db-4c45-416c-bc15-c8ea13a4465d', + u'scName': u'Storage Center 64702', + u'secureDataUsed': False, + u'serialNumber': u'0000fcbe-000012df', + u'replayAllowed': False, + u'flashOptimized': False, + u'configuredSize': u'1.073741824E9 Bytes', + u'mapped': False, + u'cmmSource': False} + + SCSERVER = {u'scName': u'Storage Center 64702', + u'volumeCount': 0, + u'removeHbasAllowed': True, + u'legacyFluidFs': False, + u'serverFolderIndex': 4, + u'alertOnConnectivity': True, + u'objectType': u'ScPhysicalServer', + u'instanceName': u'Server_21000024ff30441d', + u'instanceId': u'64702.47', + u'serverFolderPath': u'opnstktst/', + u'portType': [u'FibreChannel'], + u'type': u'Physical', + u'statusMessage': u'Only 5 of 6 expected paths are up', + u'status': u'Degraded', + u'scSerialNumber': 64702, + u'serverFolder': {u'instanceId': u'64702.4', + u'instanceName': u'opnstktst', + u'objectType': u'ScServerFolder'}, + u'parentIndex': 0, + u'connectivity': u'Partial', + u'hostCacheIndex': 0, + u'deleteAllowed': True, + u'pathCount': 5, + u'name': u'Server_21000024ff30441d', + u'hbaPresent': True, + u'hbaCount': 2, + u'notes': u'Created by Dell Cinder Driver', + u'mapped': False, + u'operatingSystem': {u'instanceId': u'64702.38', + u'instanceName': u'Red Hat Linux 6.x', + u'objectType': u'ScServerOperatingSystem'} + } + + MAPPING = {u'instanceId': u'64702.2183', + u'scName': u'Storage Center 64702', + u'scSerialNumber': 64702, + u'controller': {u'instanceId': u'64702.64702', + u'instanceName': u'SN 64702', + u'objectType': u'ScController'}, + u'lunUsed': [1], + u'server': {u'instanceId': u'64702.47', + u'instanceName': u'Server_21000024ff30441d', + u'objectType': u'ScPhysicalServer'}, + u'volume': {u'instanceId': u'64702.4829', + u'instanceName': + u'5729f1db-4c45-416c-bc15-c8ea13a4465d', + u'objectType': u'ScVolume'}, + u'connectivity': u'Up', + u'readOnly': False, + u'objectType': u'ScMappingProfile', + u'hostCache': False, + u'mappedVia': u'Server', + u'mapCount': 2, + u'instanceName': u'4829-47', + u'lunRequested': u'N/A' + } + + def setUp(self): + super(DellSCSanFCDriverTestCase, self).setUp() + + # configuration is a mock. A mock is pretty much a blank + # slate. I believe mock's done in setup are not happy time + # mocks. So we just do a few things like driver config here. + self.configuration = mock.Mock() + + self.configuration.san_is_local = False + self.configuration.san_ip = "192.168.0.1" + self.configuration.san_login = "admin" + self.configuration.san_password = "pwd" + self.configuration.dell_sc_ssn = 64702 + self.configuration.dell_sc_server_folder = 'opnstktst' + self.configuration.dell_sc_volume_folder = 'opnstktst' + self.configuration.dell_sc_api_port = 3033 + self._context = context.get_admin_context() + + self.driver = dell_storagecenter_fc.DellStorageCenterFCDriver( + configuration=self.configuration) + + self.driver.do_setup(None) + + self.driver._stats = {'QoS_support': False, + 'volume_backend_name': 'dell-1', + 'free_capacity_gb': 12123, + 'driver_version': '1.0.1', + 'total_capacity_gb': 12388, + 'reserved_percentage': 0, + 'vendor_name': 'Dell', + 'storage_protocol': 'FC'} + + self.volid = '5729f1db-4c45-416c-bc15-c8ea13a4465d' + self.volume_name = "volume" + self.volid + self.connector = {'ip': '192.168.0.77', + 'host': 'cinderfc-vm', + 'wwnns': ['20000024ff30441c', '20000024ff30441d'], + 'initiator': 'iqn.1993-08.org.debian:01:e1b1312f9e1', + 'wwpns': ['21000024ff30441c', '21000024ff30441d']} + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_server_multiple_hbas', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPING) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(1, + [u'5000D31000FCBE3D', + u'5000D31000FCBE35'], + {u'21000024FF30441C': + [u'5000D31000FCBE35'], + u'21000024FF30441D': + [u'5000D31000FCBE3D']})) + def test_initialize_connection(self, + mock_find_wwns, + mock_map_volume, + mock_find_volume, + mock_create_server, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = self.connector + data = self.driver.initialize_connection(volume, connector) + self.assertEqual(data['driver_volume_type'], 'fibre_channel') + # verify find_volume has been called and that is has been called twice + mock_find_volume.assert_any_call(64702, self.volume_name) + assert mock_find_volume.call_count == 2 + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPING) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(None, [], {})) + def test_initialize_connection_no_wwns(self, + mock_find_wwns, + mock_map_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = self.connector + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_server_multiple_hbas', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPING) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(None, [], {})) + def test_initialize_connection_no_server(self, + mock_find_wwns, + mock_map_volume, + mock_find_volume, + mock_create_server, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = self.connector + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPING) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(None, [], {})) + def test_initialize_connection_vol_not_found(self, + mock_find_wwns, + mock_map_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = self.connector + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=True) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(1, + [u'5000D31000FCBE3D', + u'5000D31000FCBE35'], + {u'21000024FF30441C': + [u'5000D31000FCBE35'], + u'21000024FF30441D': + [u'5000D31000FCBE3D']})) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_volume_count', + return_value=1) + def test_terminate_connection(self, + mock_get_volume_count, + mock_find_wwns, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = self.connector + self.driver.terminate_connection(volume, connector) + mock_unmap_volume.assert_called_once_with(self.VOLUME, self.SCSERVER) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=True) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(1, + [u'5000D31000FCBE3D', + u'5000D31000FCBE35'], + {u'21000024FF30441C': + [u'5000D31000FCBE35'], + u'21000024FF30441D': + [u'5000D31000FCBE3D']})) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_volume_count', + return_value=1) + def test_terminate_connection_no_server(self, + mock_get_volume_count, + mock_find_wwns, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = self.connector + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=True) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(1, + [u'5000D31000FCBE3D', + u'5000D31000FCBE35'], + {u'21000024FF30441C': + [u'5000D31000FCBE35'], + u'21000024FF30441D': + [u'5000D31000FCBE3D']})) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_volume_count', + return_value=1) + def test_terminate_connection_no_volume(self, + mock_get_volume_count, + mock_find_wwns, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = self.connector + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=True) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(None, + [], + {})) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_volume_count', + return_value=1) + def test_terminate_connection_no_wwns(self, + mock_get_volume_count, + mock_find_wwns, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = self.connector + # self.assertRaises(exception.VolumeBackendAPIException, + # self.driver.terminate_connection, + # volume, + # connector) + self.driver.terminate_connection(volume, connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=False) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_wwns', + return_value=(1, + [u'5000D31000FCBE3D', + u'5000D31000FCBE35'], + {u'21000024FF30441C': + [u'5000D31000FCBE35'], + u'21000024FF30441D': + [u'5000D31000FCBE3D']})) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_volume_count', + return_value=1) + def test_terminate_connection_failure(self, + mock_get_volume_count, + mock_find_wwns, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = self.connector + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_storage_usage', + return_value={'availableSpace': 100, 'freeSpace': 50}) + def test_update_volume_stats_with_refresh(self, + mock_get_storage_usage, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + stats = self.driver.get_volume_stats(True) + self.assertEqual(stats['storage_protocol'], 'FC') + mock_get_storage_usage.called_once_with(64702) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_storage_usage', + return_value={'availableSpace': 100, 'freeSpace': 50}) + def test_get_volume_stats_no_refresh(self, + mock_get_storage_usage, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + stats = self.driver.get_volume_stats(False) + self.assertEqual(stats['storage_protocol'], 'FC') + assert mock_get_storage_usage.called is False diff --git a/cinder/tests/test_dellsc.py b/cinder/tests/test_dellsc.py new file mode 100644 index 000000000..82e0462ec --- /dev/null +++ b/cinder/tests/test_dellsc.py @@ -0,0 +1,917 @@ +# Copyright (c) 2014 Dell Inc. +# +# 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. + +from cinder import context +from cinder import exception +from cinder.openstack.common import log as logging +from cinder import test +from cinder.volume.drivers.dell import dell_storagecenter_api +from cinder.volume.drivers.dell import dell_storagecenter_iscsi + +import mock + +import uuid + + +LOG = logging.getLogger(__name__) + +# We patch these here as they are used by every test to keep +# from trying to contact a Dell Storage Center. + + +@mock.patch.object(dell_storagecenter_api.StorageCenterApi, + '__init__', + return_value=None) +@mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'open_connection') +@mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'close_connection') +class DellSCSanISCSIDriverTestCase(test.TestCase): + + VOLUME = {u'instanceId': u'64702.3494', + u'scSerialNumber': 64702, + u'replicationSource': False, + u'liveVolume': False, + u'vpdId': 3496, + u'objectType': u'ScVolume', + u'index': 3494, + u'volumeFolderPath': u'devstackvol/fcvm/', + u'hostCacheEnabled': False, + u'usedByLegacyFluidFsNasVolume': False, + u'inRecycleBin': False, + u'volumeFolderIndex': 17, + u'instanceName': u'volume-37883deb-85cd-426a-9a98-62eaad8671ea', + u'statusMessage': u'', + u'status': u'Up', + u'storageType': {u'instanceId': u'64702.1', + u'instanceName': u'Assigned - Redundant - 2 MB', + u'objectType': u'ScStorageType'}, + u'cmmDestination': False, + u'replicationDestination': False, + u'volumeFolder': {u'instanceId': u'64702.17', + u'instanceName': u'fcvm', + u'objectType': u'ScVolumeFolder'}, + u'deviceId': u'6000d31000fcbe000000000000000da8', + u'active': True, + u'portableVolumeDestination': False, + u'deleteAllowed': True, + u'name': u'volume-37883deb-85cd-426a-9a98-62eaad8671ea', + u'scName': u'Storage Center 64702', + u'secureDataUsed': False, + u'serialNumber': u'0000fcbe-00000da8', + u'replayAllowed': True, + u'flashOptimized': False, + u'configuredSize': u'1.073741824E9 Bytes', + u'mapped': False, + u'cmmSource': False} + + SCSERVER = {u'scName': u'Storage Center 64702', + u'volumeCount': 0, + u'removeHbasAllowed': True, + u'legacyFluidFs': False, + u'serverFolderIndex': 4, + u'alertOnConnectivity': True, + u'objectType': u'ScPhysicalServer', + u'instanceName': u'Server_21000024ff30441d', + u'instanceId': u'64702.47', + u'serverFolderPath': u'devstacksrv/', + u'portType': [u'FibreChannel'], + u'type': u'Physical', + u'statusMessage': u'Only 5 of 6 expected paths are up', + u'status': u'Degraded', + u'scSerialNumber': 64702, + u'serverFolder': {u'instanceId': u'64702.4', + u'instanceName': u'devstacksrv', + u'objectType': u'ScServerFolder'}, + u'parentIndex': 0, + u'connectivity': u'Partial', + u'hostCacheIndex': 0, + u'deleteAllowed': True, + u'pathCount': 5, + u'name': u'Server_21000024ff30441d', + u'hbaPresent': True, + u'hbaCount': 2, + u'notes': u'Created by Dell Cinder Driver', + u'mapped': False, + u'operatingSystem': {u'instanceId': u'64702.38', + u'instanceName': u'Red Hat Linux 6.x', + u'objectType': u'ScServerOperatingSystem'} + } + + MAPPINGS = [{u'profile': {u'instanceId': u'64702.104', + u'instanceName': u'92-30', + u'objectType': u'ScMappingProfile'}, + u'status': u'Down', + u'statusMessage': u'', + u'instanceId': u'64702.969.64702', + u'scName': u'Storage Center 64702', + u'scSerialNumber': 64702, + u'controller': {u'instanceId': u'64702.64702', + u'instanceName': u'SN 64702', + u'objectType': u'ScController'}, + u'server': {u'instanceId': u'64702.30', + u'instanceName': + u'Server_iqn.1993-08.org.debian:01:3776df826e4f', + u'objectType': u'ScPhysicalServer'}, + u'volume': {u'instanceId': u'64702.92', + u'instanceName': + u'volume-74a21934-60ad-4cf2-b89b-1f0dda309ddf', + u'objectType': u'ScVolume'}, + u'readOnly': False, + u'lun': 1, + u'lunUsed': [1], + u'serverHba': {u'instanceId': u'64702.3454975614', + u'instanceName': + u'iqn.1993-08.org.debian:01:3776df826e4f', + u'objectType': u'ScServerHba'}, + u'path': {u'instanceId': u'64702.64702.64702.31.8', + u'instanceName': + u'iqn.1993-08.org.debian:' + '01:3776df826e4f-5000D31000FCBE43', + u'objectType': u'ScServerHbaPath'}, + u'controllerPort': {u'instanceId': + u'64702.5764839588723736131.91', + u'instanceName': u'5000D31000FCBE43', + u'objectType': u'ScControllerPort'}, + u'instanceName': u'64702-969', + u'transport': u'Iscsi', + u'objectType': u'ScMapping'}] + + RPLAY = {u'scSerialNumber': 64702, + u'globalIndex': u'64702-46-250', + u'description': u'Cinder Clone Replay', + u'parent': {u'instanceId': u'64702.46.249', + u'instanceName': u'64702-46-249', + u'objectType': u'ScReplay'}, + u'instanceId': u'64702.46.250', + u'scName': u'Storage Center 64702', + u'consistent': False, + u'expires': True, + u'freezeTime': u'12/09/2014 03:52:08 PM', + u'createVolume': {u'instanceId': u'64702.46', + u'instanceName': + u'volume-ff9589d3-2d41-48d5-9ef5-2713a875e85b', + u'objectType': u'ScVolume'}, + u'expireTime': u'12/09/2014 04:52:08 PM', + u'source': u'Manual', + u'spaceRecovery': False, + u'writesHeldDuration': 7910, + u'active': False, + u'markedForExpiration': False, + u'objectType': u'ScReplay', + u'instanceName': u'12/09/2014 03:52:08 PM', + u'size': u'0.0 Bytes' + } + + IQN = 'iqn.2002-03.com.compellent:5000D31000000001' + + def setUp(self): + super(DellSCSanISCSIDriverTestCase, self).setUp() + + # configuration is a mock. A mock is pretty much a blank + # slate. I believe mock's done in setup are not happy time + # mocks. So we just do a few things like driver config here. + self.configuration = mock.Mock() + + self.configuration.san_is_local = False + self.configuration.san_ip = "192.168.0.1" + self.configuration.san_login = "admin" + self.configuration.san_password = "mmm" + self.configuration.dell_sc_ssn = 12345 + self.configuration.dell_sc_server_folder = 'opnstktst' + self.configuration.dell_sc_volume_folder = 'opnstktst' + self.configuration.dell_sc_api_port = 3033 + self.configuration.iscsi_ip_address = '192.168.1.1' + self.configuration.iscsi_port = 3260 + self._context = context.get_admin_context() + + self.driver = dell_storagecenter_iscsi.DellStorageCenterISCSIDriver( + configuration=self.configuration) + + self.driver.do_setup(None) + + self.driver._stats = {'QoS_support': False, + 'volume_backend_name': 'dell-1', + 'free_capacity_gb': 12123, + 'driver_version': '1.0.1', + 'total_capacity_gb': 12388, + 'reserved_percentage': 0, + 'vendor_name': 'Dell', + 'storage_protocol': 'iSCSI'} + + self.volid = str(uuid.uuid4()) + self.volume_name = "volume" + self.volid + self.connector = { + 'ip': '10.0.0.2', + 'initiator': 'iqn.1993-08.org.debian:01:2227dab76162', + 'host': 'fakehost'} + self.access_record_output = [ + "ID Initiator Ipaddress AuthMethod UserName Apply-To", + "--- --------------- ------------- ---------- ---------- --------", + "1 iqn.1993-08.org.debian:01:222 *.*.*.* none both", + " 7dab76162"] + + self.fake_iqn = 'iqn.2002-03.com.compellent:5000D31000000001' + self.properties = { + 'target_discoverd': True, + 'target_portal': '%s:3260' + % self.driver.configuration.dell_sc_iscsi_ip, + 'target_iqn': self.fake_iqn, + 'volume_id': 1} + self._model_update = { + 'provider_location': "%s:3260,1 %s 0" + % (self.driver.configuration.dell_sc_iscsi_ip, + self.fake_iqn) + # , + # 'provider_auth': 'CHAP %s %s' % ( + # self.configuration.eqlx_chap_login, + # self.configuration.eqlx_chap_password) + } + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + def test_create_volume(self, + mock_find_sc, + mock_create_volume, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name, 'size': 1} + self.driver.create_volume(volume) + mock_create_volume.assert_called_once_with(self.volume_name, + 1, + 12345, + u'opnstktst') + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + def test_create_volume_failure(self, + mock_find_sc, + mock_create_volume, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name, 'size': 1} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume, volume) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'delete_volume', + return_value=True) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + def test_delete_volume(self, + mock_find_sc, + mock_delete_volume, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name, 'size': 1} + self.driver.delete_volume(volume) + mock_delete_volume.assert_called_once_with(12345, self.volume_name) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'delete_volume', + return_value=False) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + def test_delete_volume_failure(self, + mock_find_sc, + mock_delete_volume, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name, 'size': 1} + self.assertRaises(exception.VolumeIsBusy, + self.driver.delete_volume, + volume) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPINGS[0]) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_iqn', + return_value=IQN) + def test_initialize_connection(self, + mock_find_iqn, + mock_map_volume, + mock_find_volume, + mock_create_server, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = self.connector + data = self.driver.initialize_connection(volume, connector) + self.assertEqual(data['driver_volume_type'], 'iscsi') + # verify find_volume has been called and that is has been called twice + mock_find_volume.assert_any_call(12345, self.volume_name) + assert mock_find_volume.call_count == 2 + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPINGS) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_iqn', + return_value=None) + def test_initialize_connection_no_iqn(self, + mock_find_iqn, + mock_map_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = {} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_server', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPINGS) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_iqn', + return_value=None) + def test_initialize_connection_no_server(self, + mock_find_iqn, + mock_map_volume, + mock_find_volume, + mock_create_server, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = {} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'map_volume', + return_value=MAPPINGS) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_iqn', + return_value=None) + def test_initialize_connection_vol_not_found(self, + mock_find_iqn, + mock_map_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = {} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.initialize_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=True) + def test_terminate_connection(self, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name} + connector = self.connector + self.driver.terminate_connection(volume, connector) + mock_unmap_volume.assert_called_once_with(self.VOLUME, self.SCSERVER) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=True) + def test_terminate_connection_no_server(self, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = {'initiator': ''} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=True) + def test_terminate_connection_no_volume(self, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = {'initiator': ''} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_server', + return_value=SCSERVER) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'unmap_volume', + return_value=False) + def test_terminate_connection_failure(self, + mock_unmap_volume, + mock_find_volume, + mock_find_server, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name} + connector = {'initiator': ''} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.terminate_connection, + volume, + connector) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_replay', + return_value='fake') + def test_create_snapshot(self, + mock_create_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + snapshot = {'volume_id': self.volume_name, + 'id': self.volume_name} + self.driver.create_snapshot(snapshot) + self.assertEqual('available', snapshot['status']) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_replay', + return_value=None) + def test_create_snapshot_no_volume(self, + mock_create_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + snapshot = {'volume_id': self.volume_name, + 'id': self.volume_name} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, + snapshot) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_replay', + return_value=None) + def test_create_snapshot_failure(self, + mock_create_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + snapshot = {'volume_id': self.volume_name, + 'id': self.volume_name} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_snapshot, + snapshot) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_replay', + return_value='fake') + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_view_volume', + return_value=VOLUME) + def test_create_volume_from_snapshot(self, + mock_create_view_volume, + mock_find_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': 'fake'} + snapshot = {'id': 'fake', 'volume_id': 'fake'} + self.driver.create_volume_from_snapshot(volume, snapshot) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_replay', + return_value='fake') + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_view_volume', + return_value=None) + def test_create_volume_from_snapshot_failed(self, + mock_create_view_volume, + mock_find_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': 'fake'} + snapshot = {'id': 'fake', 'volume_id': 'fake'} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + volume, snapshot) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_replay', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_view_volume', + return_value=VOLUME) + def test_create_volume_from_snapshot_no_replay(self, + mock_create_view_volume, + mock_find_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': 'fake'} + snapshot = {'id': 'fake', 'volume_id': 'fake'} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_volume_from_snapshot, + volume, snapshot) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_replay', + return_value='fake') + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_cloned_volume', + return_value=VOLUME) + def test_create_cloned_volume(self, + mock_create_cloned_volume, + mock_find_volume, + mock_find_replay, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name + '_clone'} + src_vref = {'id': self.volume_name} + self.driver.create_cloned_volume(volume, src_vref) + mock_create_cloned_volume. \ + assert_called_once_with(self.volume_name + '_clone', + self.configuration.dell_sc_volume_folder, + self.VOLUME) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_replay', + return_value='fake') + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'create_cloned_volume', + return_value=VOLUME) + def test_create_cloned_volume_no_volume(self, + mock_create_cloned_volume, + mock_find_volume, + mock_find_replay, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'id': self.volume_name + '_clone'} + src_vref = {'id': self.volume_name} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.create_cloned_volume, + volume, src_vref) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'delete_replay', + return_value=True) + def test_delete_snapshot(self, + mock_delete_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + snapshot = {'volume_id': self.volume_name, + 'id': self.volume_name} + self.driver.delete_snapshot(snapshot) + mock_delete_replay.assert_called_once_with( + self.VOLUME, self.volume_name) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'delete_replay', + return_value=True) + def test_delete_snapshot_no_volume(self, + mock_delete_replay, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + snapshot = {'volume_id': self.volume_name, + 'id': self.volume_name} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.delete_snapshot, + snapshot) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + def test_ensure_export(self, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + context = {} + volume = {'id': self.VOLUME.get(u'name')} + self.driver.ensure_export(context, volume) + mock_find_volume.assert_called_once_with( + 12345, self.VOLUME.get(u'name')) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + def test_ensure_export_failed(self, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + context = {} + volume = {'id': self.VOLUME.get(u'name')} + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.ensure_export, + context, volume) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + def test_ensure_export_no_volume(self, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + context = {} + volume = {'id': self.VOLUME.get(u'name')} + #self.driver.ensure_export(context, volume) + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.ensure_export, + context, + volume) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=VOLUME) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'expand_volume', + return_value=VOLUME) + def test_extend_volume(self, + mock_expand_volume, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name, 'size': 1} + new_size = 2 + self.driver.extend_volume(volume, new_size) + mock_expand_volume.assert_called_once_with(self.VOLUME, new_size) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=12345) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_volume', + return_value=None) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'expand_volume', + return_value=None) + def test_extend_volume_no_volume(self, + mock_expand_volume, + mock_find_volume, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + volume = {'name': self.volume_name, 'size': 1} + new_size = 2 + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.extend_volume, + volume, new_size) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_storage_usage', + return_value={'availableSpace': 100, 'freeSpace': 50}) + def test_update_volume_stats_with_refresh(self, + mock_get_storage_usage, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + stats = self.driver.get_volume_stats(True) + self.assertEqual(stats['storage_protocol'], 'iSCSI') + mock_get_storage_usage.called_once_with(64702) + + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'find_sc', + return_value=64702) + @mock.patch.object(dell_storagecenter_api.StorageCenterApi, + 'get_storage_usage', + return_value={'availableSpace': 100, 'freeSpace': 50}) + def test_get_volume_stats_no_refresh(self, + mock_get_storage_usage, + mock_find_sc, + mock_close_connection, + mock_open_connection, + mock_init): + stats = self.driver.get_volume_stats(False) + self.assertEqual(stats['storage_protocol'], 'iSCSI') + assert mock_get_storage_usage.called is False diff --git a/cinder/volume/drivers/dell/__init__.py b/cinder/volume/drivers/dell/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cinder/volume/drivers/dell/dell_storagecenter_api.py b/cinder/volume/drivers/dell/dell_storagecenter_api.py new file mode 100644 index 000000000..7e0060f20 --- /dev/null +++ b/cinder/volume/drivers/dell/dell_storagecenter_api.py @@ -0,0 +1,1046 @@ +# Copyright 2014 Dell Inc. +# +# 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. +'''Interface for interacting with the Dell Storage Center array.''' + +import json +import os.path + +import requests + +from cinder import exception +from cinder.i18n import _, _LE, _LI, _LW +from cinder.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + + +class PayloadFilter(object): + + '''PayloadFilter + + Simple class for creating filters for interacting with the Dell + Storage API. + + Note that this defaults to "AND" filter types. + ''' + + def __init__(self): + self.payload = {} + self.payload['filterType'] = 'AND' + self.payload['filters'] = [] + + def append(self, name, val, filtertype='Equals'): + if val is not None: + apifilter = {} + apifilter['attributeName'] = name + apifilter['attributeValue'] = val + apifilter['filterType'] = filtertype + self.payload['filters'].append(apifilter) + + +class HttpClient(object): + + '''HttpClient + + Helper for making the REST calls. + ''' + + def __init__(self, host, port, user, password): + self.baseUrl = 'https://%s:%s/api/rest/' % (host, port) + self.session = requests.Session() + self.session.auth = (user, password) + self.header = {} + self.header['Content-Type'] = 'application/json; charset=utf-8' + self.header['x-dell-api-version'] = '1.5' + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.session.close() + + def __formatUrl(self, url): + return '%s%s' % (self.baseUrl, url if url[0] != '/' else url[1:]) + + def get(self, url): + return self.session.get( + self.__formatUrl(url), + headers=self.header, + verify=False) + + def post(self, url, payload): + return self.session.post( + self.__formatUrl(url), + data=json.dumps(payload, + ensure_ascii=False).encode('utf-8'), + headers=self.header, + verify=False) + + def put(self, url, payload): + return self.session.put( + self.__formatUrl(url), + data=json.dumps(payload, + ensure_ascii=False).encode('utf-8'), + headers=self.header, + verify=False) + + def delete(self, url): + return self.session.delete( + self.__formatUrl(url), + headers=self.header, + verify=False) + + +class StorageCenterApiHelper(object): + + '''StorageCenterApiHelper + + Helper class for API access. Handles opening and closing the + connection to the Storage Center. + ''' + + def __init__(self, config): + self.config = config + + def open_connection(self): + '''Open connection to Enterprise Manager.''' + connection = StorageCenterApi(self.config.san_ip, + self.config.dell_sc_api_port, + self.config.san_login, + self.config.san_password) + connection.open_connection() + return connection + + +class StorageCenterApi(object): + + '''StorageCenterApi + + Handles calls to EnterpriseManager via the REST API interface. + ''' + + def __init__(self, host, port, user, password): + self.notes = 'Created by Dell Cinder Driver' + self.client = HttpClient(host, + port, + user, + password) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close_connection() + + def _path_to_array(self, path): + array = [] + while True: + (path, tail) = os.path.split(path) + if tail == '': + array.reverse() + return array + array.append(tail) + + def _first_result(self, blob): + return self._get_result(blob, None, None) + + def _get_result(self, blob, attribute, value): + rsp = None + try: + content = blob.json() + # we can get a list or a dict or nothing + if isinstance(content, list): + for r in content: + if attribute is None or r.get(attribute) == value: + rsp = r + break + else: + if attribute is None or content.get(attribute) == value: + rsp = content + except AttributeError: + LOG.error(_LE('Invalid return blob: %s'), + blob) + if rsp is None: + LOG.debug('Unable to find result where %(attr)s is %(val)s', + {'attr': attribute, + 'val': value}) + return rsp + + def _get_json(self, blob): + try: + return blob.json() + except AttributeError: + LOG.error(_LE('Error invalid json: %s'), + blob) + return None + + def _get_id(self, blob): + try: + if isinstance(blob, dict): + return blob.get('instanceId') + except AttributeError: + LOG.error(_LE('Invalid API object: %s'), + blob) + return None + + def open_connection(self): + # Authenticate against EM + r = self.client.post('ApiConnection/Login', + {}) + if r.status_code != 200: + LOG.error(_LE('Login error: %(c)d %(r)s'), + {'c': r.status_code, + 'r': r.reason}) + raise exception.VolumeBackendAPIException( + _('Failed to connect to Enterprise Manager')) + + def close_connection(self): + r = self.client.post('ApiConnection/Logout', + {}) + if r.status_code != 204: + LOG.warning(_LW('Logout error: %(c)d %(r)s'), + {'c': r.status_code, + 'r': r.reason}) + self.client = None + + def find_sc(self, ssn): + '''This is really just a check that the sc is there and being managed by + EM. + ''' + r = self.client.get('StorageCenter/StorageCenter') + result = self._get_result(r, + 'scSerialNumber', + ssn) + if result is None: + LOG.error(_LE('Failed to find %(s)s. Result %(r)s'), + {'s': ssn, + 'r': r}) + raise exception.VolumeBackendAPIException( + _('Failed to find Storage Center')) + + return self._get_id(result) + + # volume functions + + def _create_folder(self, url, ssn, parent, folder): + '''This is generic to server and volume folders. + ''' + f = None + payload = {} + payload['Name'] = folder + payload['StorageCenter'] = ssn + if parent != '': + payload['Parent'] = parent + payload['Notes'] = self.notes + + r = self.client.post(url, + payload) + if r.status_code != 201: + LOG.debug('%(u)s error: %(c)d %(r)s', + {'u': url, + 'c': r.status_code, + 'r': r.reason}) + else: + f = self._first_result(r) + return f + + def _create_folder_path(self, url, ssn, foldername): + '''This is generic to server and volume folders. + ''' + path = self._path_to_array(foldername) + folderpath = '' + instanceId = '' + # technically the first folder is the root so that is already created. + found = True + f = None + for folder in path: + folderpath = folderpath + folder + # if the last was found see if this part of the path exists too + if found: + listurl = url + '/GetList' + f = self._find_folder(listurl, + ssn, + folderpath) + if f is None: + found = False + # we didn't find it so create it + if found is False: + f = self._create_folder(url, + ssn, + instanceId, + folder) + # if we haven't found a folder or created it then leave + if f is None: + LOG.error(_LE('Unable to create folder path %s'), + folderpath) + break + # next part of the path will need this + instanceId = self._get_id(f) + folderpath = folderpath + '/' + return f + + def _find_folder(self, url, ssn, foldername): + '''Most of the time the folder will already have been created so + we look for the end folder and check that the rest of the path is + right. + + This is generic to server and volume folders. + ''' + pf = PayloadFilter() + pf.append('scSerialNumber', ssn) + basename = os.path.basename(foldername) + pf.append('Name', basename) + # if we have any kind of path we add '/' to match the storage + # center's convention and throw it into the filters. + folderpath = os.path.dirname(foldername) + if folderpath != '': + folderpath += '/' + pf.append('folderPath', folderpath) + folder = None + r = self.client.post(url, + pf.payload) + if r.status_code == 200: + folder = self._get_result(r, + 'folderPath', + folderpath) + else: + LOG.debug('%(u)s error: %(c)d %(r)s', + {'u': url, + 'c': r.status_code, + 'r': r.reason}) + return folder + + def _create_volume_folder_path(self, ssn, foldername): + return self._create_folder_path('StorageCenter/ScVolumeFolder', + ssn, + foldername) + + def _find_volume_folder(self, ssn, foldername): + return self._find_folder('StorageCenter/ScVolumeFolder/GetList', + ssn, + foldername) + + def _init_volume(self, scvolume): + '''Maps the volume to a random server and immediately unmaps + it. This initializes the volume. + + Don't wig out if this fails. + ''' + pf = PayloadFilter() + pf.append('scSerialNumber', scvolume.get('scSerialNumber'), 'Equals') + r = self.client.post('StorageCenter/ScServer/GetList', pf.payload) + if r.status_code == 200: + scservers = self._get_json(r) + # Sort through the servers looking for one with connectivity. + for scserver in scservers: + if scserver.get('status', '').lower() != 'down': + # map to actually create the volume + self.map_volume(scvolume, + scserver) + self.unmap_volume(scvolume, + scserver) + + def create_volume(self, name, size, ssn, volfolder): + '''This creates a new volume on the storage center. It + will create it in volfolder. If volfolder does not + exist it will create it. If it cannot create volfolder + the volume will be created in the root. + ''' + scvolume = None + # find our folder + LOG.debug('Create Volume %(name)s %(ssn)s %(folder)s', + {'name': name, + 'ssn': ssn, + 'folder': volfolder}) + folder = self._find_volume_folder(ssn, + volfolder) + + # doesn't exist? make it + if folder is None: + folder = self._create_volume_folder_path(ssn, + volfolder) + + # if we actually have a place to put our volume create it + if folder is None: + LOG.error(_LE('Unable to create folder %s'), + volfolder) + + # Create the volume + payload = {} + payload['Name'] = name + payload['Notes'] = self.notes + payload['Size'] = '%d GB' % size + payload['StorageCenter'] = ssn + if folder is not None: + payload['VolumeFolder'] = self._get_id(folder) + r = self.client.post('StorageCenter/ScVolume', + payload) + if r.status_code == 201: + scvolume = self._get_json(r) + else: + LOG.error(_LE('ScVolume create error %(name)s: %(c)d %(r)s'), + {'name': name, + 'c': r.status_code, + 'r': r.reason}) + if scvolume is not None: + LOG.info(_LI('Created volume %(index)d: %(name)s'), + {'index': scvolume['index'], + 'name': scvolume['name']}) + return scvolume + + def find_volume(self, ssn, name=None, instanceid=None): + '''search ssn for volume of name and/or instance id + ''' + LOG.debug('finding volume %(sn)s : %(name)s : %(id)s', + {'sn': ssn, + 'name': name, + 'id': instanceid}) + if name is None and instanceid is None: + return None + pf = PayloadFilter() + # we need at least a name and or an instance id. If we have + # that we can find a volume. + pf.append('scSerialNumber', ssn) + pf.append('Name', name) + pf.append('instanceId', instanceid) + r = self.client.post('StorageCenter/ScVolume/GetList', + pf.payload) + if r.status_code != 200: + LOG.debug('ScVolume GetList error %(i)s: %(c)d %(r)s', + {'i': instanceid, + 'c': r.status_code, + 'r': r.reason}) + return self._first_result(r) + + def delete_volume(self, ssn, name): + # find our volume + vol = self.find_volume(ssn, name, None) + if vol is not None: + r = self.client.delete('StorageCenter/ScVolume/%s' + % self._get_id(vol)) + if r.status_code != 200: + raise exception.VolumeBackendAPIException( + _('Error deleting volume %(ssn)s: %(sn)s: %(c)d %(r)s') % + {'ssn': ssn, + 'sn': name, + 'c': r.status_code, + 'r': r.reason}) + # json return should be true or false + return r.json() + LOG.warning(_LW('delete_volume: unable to find volume %s'), + name) + # If we can't find the volume then it is effectively gone. + return True + + def _create_server_folder_path(self, ssn, foldername): + return self._create_folder_path('StorageCenter/ScServerFolder', + ssn, + foldername) + + def _find_server_folder(self, ssn, foldername): + return self._find_folder('StorageCenter/ScServerFolder/GetList', + ssn, + foldername) + + def _add_hba(self, scserver, wwnoriscsiname, isfc=False): + '''Adds an HBA to the scserver. The HBA will be added + even if it has not been seen by the storage center. + ''' + payload = {} + if isfc is True: + payload['HbaPortType'] = 'FibreChannel' + else: + payload['HbaPortType'] = 'Iscsi' + payload['WwnOrIscsiName'] = wwnoriscsiname + payload['AllowManual'] = True + r = self.client.post('StorageCenter/ScPhysicalServer/%s/AddHba' + % self._get_id(scserver), + payload) + if r.status_code != 200: + LOG.error(_LE('AddHba error: %(i)s to %(s)s : %(c)d %(r)s'), + {'i': wwnoriscsiname, + 's': scserver['name'], + 'c': r.status_code, + 'r': r.reason}) + return False + return True + + # we do not know that we are red hat linux 6.x but that works + # best for red hat and ubuntu. So, there. + def _find_serveros(self, ssn, osname='Red Hat Linux 6.x'): + '''Returns the serveros instance id of the specified osname. + Required to create a server. + ''' + pf = PayloadFilter() + pf.append('scSerialNumber', ssn) + r = self.client.post('StorageCenter/ScServerOperatingSystem/GetList', + pf.payload) + if r.status_code == 200: + oslist = r.json() + for srvos in oslist: + name = srvos.get('name', 'nope') + if name.lower() == osname.lower(): + # found it return the id + return self._get_id(srvos) + + LOG.warning(_LW('ScServerOperatingSystem GetList return: %(c)d %(r)s'), + {'c': r.status_code, + 'r': r.reason}) + return None + + def create_server_multiple_hbas(self, ssn, foldername, wwns): + '''Same as create_server except it can take a list of hbas. hbas + can be wwns or iqns. + ''' + # add hbas + scserver = None + # our instance names + for wwn in wwns: + if scserver is None: + # Use the fist wwn to create the server. + scserver = self.create_server(ssn, + foldername, + wwn, + True) + else: + # add the wwn to our server + self._add_hba(scserver, + wwn, + True) + return scserver + + def create_server(self, ssn, foldername, wwnoriscsiname, isfc=False): + '''creates a server on the the storage center ssn. Adds the first + HBA to it. + ''' + scserver = None + payload = {} + payload['Name'] = 'Server_' + wwnoriscsiname + payload['StorageCenter'] = ssn + payload['Notes'] = self.notes + # we pick Red Hat Linux 6.x because it supports multipath and + # will attach luns to paths as they are found. + scserveros = self._find_serveros(ssn, 'Red Hat Linux 6.x') + if scserveros is not None: + payload['OperatingSystem'] = scserveros + + # find our folder or make it + folder = self._find_server_folder(ssn, + foldername) + if folder is None: + folder = self._create_server_folder_path(ssn, + foldername) + + # at this point it doesn't matter if the folder was created or not + # we just attempt to create the server. let it be in the root if + # the folder creation fails. + if folder is not None: + payload['ServerFolder'] = self._get_id(folder) + + # create our server + r = self.client.post('StorageCenter/ScPhysicalServer', + payload) + if r.status_code != 201: + LOG.error(_LE('ScPhysicalServer create error: %(i)s: %(c)d %(r)s'), + {'i': wwnoriscsiname, + 'c': r.status_code, + 'r': r.reason}) + else: + # server was created + scserver = self._first_result(r) + + # add hba to our server + if scserver is not None: + if not self._add_hba(scserver, + wwnoriscsiname, + isfc): + LOG.error(_LE('Error adding HBA to server')) + # Can't have a server without an HBA + self._delete_server(scserver) + scserver = None + # success or failure is determined by the caller + return scserver + + def find_server(self, ssn, instance_name): + '''Hunts for a server by looking for an HBA with the server's IQN + or wwn. + + If found, the server the HBA is attached to, if any, is returned. + ''' + scserver = None + # we search for our server by first finding our HBA + pf = PayloadFilter() + pf.append('scSerialNumber', ssn) + pf.append('instanceName', instance_name) + r = self.client.post('StorageCenter/ScServerHba/GetList', + pf.payload) + if r.status_code != 200: + LOG.debug('ScServerHba error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + else: + hba = self._first_result(r) + # once created hbas stay in the system. So it isn't enough + # that we found one it actually has to be attached to a + # server. + if hba is not None and hba.get('server') is not None: + pf = PayloadFilter() + pf.append('scSerialNumber', ssn) + pf.append('instanceId', self._get_id(hba['server'])) + r = self.client.post('StorageCenter/ScServer/GetList', + pf.payload) + if r.status_code != 200: + LOG.error(_LE('ScServer error: %(c)d %(r)s'), + {'c': r.status_code, + 'r': r.reason}) + else: + scserver = self._first_result(r) + if scserver is None: + LOG.debug('Server (%s) not found.', + instance_name) + return scserver + + def _find_domain(self, cportid, domainip): + '''Returns the fault domain which a given controller port can + be seen by the server + ''' + r = self.client.get('StorageCenter/ScControllerPort/%s/FaultDomainList' + % cportid) + if r.status_code == 200: + domains = r.json() + # wiffle through the domains looking for our + # configured ip + for domain in domains: + # if this is us we return the port + if domain.get('wellKnownIpAddress') == domainip: + return domain + else: + LOG.debug('FaultDomainList error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + LOG.error(_LE('Error getting FaultDomainList')) + return None + + def _find_fc_initiators(self, scserver): + '''_find_fc_initiators + + returns the server's fc HBA's wwns + ''' + initiators = [] + r = self.client.get('StorageCenter/ScServer/%s/HbaList' + % self._get_id(scserver)) + if r.status_code == 200: + hbas = r.json() + for hba in hbas: + wwn = hba.get('instanceName') + if hba.get('portType') == 'FibreChannel' and\ + wwn is not None: + initiators.append(wwn) + else: + LOG.debug('HbaList error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + LOG.error(_LE('Unable to find FC intitiators')) + return initiators + + def get_volume_count(self, scserver): + r = self.client.get('StorageCenter/ScServer/%s/MappingList' + % self._get_id(scserver)) + if r.status_code == 200: + mappings = r.json() + return len(mappings) + # Panic mildly but do not return 0. + return -1 + + def _find_mappings(self, scvolume): + '''find mappings + + returns the volume's mappings + ''' + mappings = [] + if scvolume.get('active', False): + r = self.client.get('StorageCenter/ScVolume/%s/MappingList' + % self._get_id(scvolume)) + if r.status_code == 200: + mappings = r.json() + else: + LOG.debug('MappingList error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + LOG.error(_LE('Unable to find volume mappings: %s'), + scvolume.get('name')) + else: + LOG.error(_LE('_find_mappings: volume is not active')) + return mappings + + def _find_controller_port(self, cportid): + '''_find_controller_port + + returns the controller port dict + ''' + controllerport = None + r = self.client.get('StorageCenter/ScControllerPort/%s' + % cportid) + if r.status_code == 200: + controllerport = self._first_result(r) + else: + LOG.debug('ScControllerPort error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + LOG.error(_LE('Unable to find controller port: %s'), + cportid) + return controllerport + + def find_wwns(self, scvolume, scserver): + '''returns the lun and wwns of the mapped volume''' + # our returnables + lun = None # our lun. We return the first lun. + wwns = [] # list of targets + itmap = {} # dict of initiators and the associated targets + + # make sure we know our server's initiators. Only return + # mappings that contain HBA for this server. + initiators = self._find_fc_initiators(scserver) + # get our volume mappings + mappings = self._find_mappings(scvolume) + if len(mappings) > 0: + # we check each of our mappings. We want to return + # the mapping we have been configured to use. + for mapping in mappings: + # find the controller port for this mapping + cport = mapping.get('controllerPort') + controllerport = self._find_controller_port( + self._get_id(cport)) + if controllerport is not None: + wwn = controllerport.get('WWN') + serverhba = mapping.get('serverHba') + if serverhba is not None: + hbaname = serverhba.get('instanceName') + if hbaname in initiators: + if itmap.get(hbaname) is None: + itmap[hbaname] = [] + itmap[hbaname].append(wwn) + wwns.append(wwn) + + mappinglun = mapping.get('lun') + if lun is None: + lun = mappinglun + elif lun != mappinglun: + LOG.warning(_LW('Inconsistent Luns.')) + else: + LOG.error(_LE('Find_wwns: Volume appears unmapped')) + LOG.debug(lun) + LOG.debug(wwns) + LOG.debug(itmap) + # TODO(tom_swanson): if we have nothing to return raise an exception + # here. We can't do anything with an unmapped volume. We shouldn't + # pretend we succeeded. + return lun, wwns, itmap + + def find_iqn(self, scvolume, ip): + '''find_iqn + + returns the iqn of the port pointed to by the openstack compute + node. This is to make sure that the compute node looks for the + volume on the active controller. + ''' + iqn = None + # get our volume mappings + mappings = self._find_mappings(scvolume) + if len(mappings) > 0: + # we check each of our mappings. We want to return + # the mapping we have been configured to use. + for mapping in mappings: + # find the controller port for this mapping + cport = mapping.get('controllerPort') + cportid = self._get_id(cport) + domain = self._find_domain(cportid, + ip) + if domain is not None: + controllerport = self._find_controller_port(cportid) + if controllerport is not None: + iqn = controllerport.get('iscsiName') + break + else: + LOG.error(_LE('Find_iqn: Volume appears unmapped')) + # TODO(tom_swanson): if we have nothing to return raise an exception + # here. We can't do anything with an unmapped volume. We shouldn't + # pretend we succeeded. + return iqn + + def map_volume(self, scvolume, scserver): + '''map_volume + + The check for server existence is elsewhere; does not create the + server. + ''' + # make sure we have what we think we have + serverid = self._get_id(scserver) + volumeid = self._get_id(scvolume) + if serverid is not None and volumeid is not None: + payload = {} + payload['server'] = serverid + advanced = {} + advanced['MapToDownServerHbas'] = True + payload['Advanced'] = advanced + r = self.client.post('StorageCenter/ScVolume/%s/MapToServer' + % volumeid, + payload) + if r.status_code == 200: + # we just return our mapping + return self._first_result(r) + # Should not be here. + LOG.debug('MapToServer error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + # error out + LOG.error(_LE('Unable to map %(vol)s to %(srv)s'), + {'vol': scvolume['name'], + 'srv': scserver['name']}) + return None + + def unmap_volume(self, scvolume, scserver): + '''unmap_volume + + deletes all mappings to a server, not just the ones on the path + defined in cinder.conf. + ''' + rtn = True + serverid = self._get_id(scserver) + volumeid = self._get_id(scvolume) + if serverid is not None and volumeid is not None: + r = self.client.get('StorageCenter/ScVolume/%s/MappingProfileList' + % volumeid) + if r.status_code == 200: + profiles = self._get_json(r) + for profile in profiles: + prosrv = profile.get('server') + if prosrv is not None and self._get_id(prosrv) == serverid: + r = self.client.delete( + 'StorageCenter/ScMappingProfile/%s' + % self._get_id(profile)) + if (r.status_code != 200 or r.ok is False): + LOG.debug('ScMappingProfile error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + LOG.error(_LE('Unable to unmap Volume %s'), + volumeid) + # 1 failed unmap is as good as 100. + # Fail it and leave + rtn = False + break + LOG.debug('Volume %(v)s unmapped from %(s)s', + {'v': volumeid, + 's': serverid}) + else: + LOG.debug('MappingProfileList error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + rtn = False + return rtn + + def get_storage_usage(self, ssn): + '''get_storage_usage''' + storageusage = None + if ssn is not None: + r = self.client.get('StorageCenter/StorageCenter/%s/StorageUsage' + % ssn) + if r.status_code == 200: + storageusage = self._get_json(r) + else: + LOG.debug('StorageUsage error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + + return storageusage + + def create_replay(self, scvolume, replayid, expire): + '''create_replay + + expire is in minutes. + one could snap a volume before it has been activated, so activate + by mapping and unmapping to a random server and let them. This + should be a fail but the Tempest tests require it. + ''' + replay = None + if scvolume is not None: + if (scvolume.get('active') is not True or + scvolume.get('replayAllowed') is not True): + self._init_volume(scvolume) + payload = {} + payload['description'] = replayid + payload['expireTime'] = expire + if expire == 0: + payload['doNotExpire'] = True + r = self.client.post('StorageCenter/ScVolume/%s/CreateReplay' + % self._get_id(scvolume), + payload) + if r.status_code != 200: + LOG.debug('CreateReplay error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + LOG.error(_LE('Error creating replay.')) + else: + replay = self._first_result(r) + return replay + + def find_replay(self, scvolume, replayid): + '''find_replay + + searches for the replay by replayid which we store in the + replay's description attribute + ''' + replay = None + r = self.client.get('StorageCenter/ScVolume/%s/ReplayList' + % self._get_id(scvolume)) + try: + content = r.json() + # this will be a list. If it isn't bail + if isinstance(content, list): + for r in content: + # The only place to save our information with the public + # api is the description field which isn't quite long + # enough. So we check that our description is pretty much + # the max length and we compare that to the start of + # the snapshot id. + description = r.get('description') + if len(description) >= 30 and \ + replayid.startswith(description) is True and \ + r.get('markedForExpiration') is not True: + replay = r + break + except Exception: + LOG.error(_LE('Invalid ReplayList return: %s'), + r) + + if replay is None: + LOG.debug('Unable to find snapshot %s', + replayid) + + return replay + + def delete_replay(self, scvolume, replayid): + '''delete_replay + + hunts down a replay by replayid string and expires it. + + once marked for expiration we do not return the replay as + a snapshot. + ''' + replay = self.find_replay(scvolume, + replayid) + if replay is not None: + r = self.client.post('StorageCenter/ScReplay/%s/Expire' + % self._get_id(replay), + {}) + if r.status_code != 204: + LOG.debug('ScReplay Expire error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + return False + # We either couldn't find it or expired it. + return True + + def create_view_volume(self, volname, volfolder, screplay): + '''create_view_volume + + creates a new volume named volname in the folder + volfolder from the screplay. + ''' + # find our ssn and get our folder + ssn = screplay.get('scSerialNumber') + folder = self._find_volume_folder(ssn, + volfolder) + + # doesn't exist? make it + if folder is None: + folder = self._create_volume_folder_path(ssn, + volfolder) + + # payload is just the volume name and folder if we have one. + payload = {} + payload['Name'] = volname + payload['Notes'] = self.notes + if folder is not None: + payload['VolumeFolder'] = self._get_id(folder) + r = self.client.post('StorageCenter/ScReplay/%s/CreateView' + % self._get_id(screplay), + payload) + volume = None + if r.status_code == 200: + volume = self._first_result(r) + else: + LOG.debug('ScReplay CreateView error: %(c)d %(r)s', + {'c': r.status_code, + 'r': r.reason}) + + if volume is None: + LOG.error(_LE('Unable to create volume %s from replay'), + volname) + + return volume + + def create_cloned_volume(self, volumename, volumefolder, scvolume): + '''create_cloned_volume + + creates a temporary replay and then creates a + view volume from that. + ''' + clone = None + replay = self.create_replay(scvolume, + 'Cinder Clone Replay', + 60) + if replay is not None: + clone = self.create_view_volume(volumename, + volumefolder, + replay) + else: + LOG.error(_LE('Error: unable to snap replay')) + return clone + + def expand_volume(self, scvolume, newsize): + '''expand_volume''' + payload = {} + payload['NewSize'] = '%d GB' % newsize + r = self.client.post('StorageCenter/ScVolume/%s/ExpandToSize' + % self._get_id(scvolume), + payload) + if r.status_code == 200: + vol = self._get_json(r) + else: + LOG.error(_LE('Error expanding volume %(n)s: %(c)d %(r)s'), + {'n': scvolume['name'], + 'c': r.status_code, + 'r': r.reason}) + if vol is not None: + LOG.debug('Volume expanded: %(i)s %(s)s', + {'i': vol['index'], + 's': vol['configuredSize']}) + return vol + + def _delete_server(self, scserver): + '''_delete_server + + Just give it a shot. If it fails it doesn't matter to cinder. + ''' + if scserver.get('deleteAllowed') is True: + r = self.client.delete('StorageCenter/ScServer/%s' + % self._get_id(scserver)) + LOG.debug('ScServer %(i)s delete return: %(c)d %(r)s', + {'i': self._get_id(scserver), + 'c': r.status_code, + 'r': r.reason}) + else: + LOG.debug('_delete_server: deleteAllowed is False.') diff --git a/cinder/volume/drivers/dell/dell_storagecenter_common.py b/cinder/volume/drivers/dell/dell_storagecenter_common.py new file mode 100644 index 000000000..70e3811c8 --- /dev/null +++ b/cinder/volume/drivers/dell/dell_storagecenter_common.py @@ -0,0 +1,332 @@ +# Copyright 2014 Dell Inc. +# +# 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. + +from oslo.config import cfg +from oslo.utils import excutils + +from cinder import exception +from cinder.i18n import _, _LE, _LW +from cinder.openstack.common import log as logging +from cinder.volume.drivers.dell import dell_storagecenter_api +from cinder.volume.drivers.san import san + + +common_opts = [ + cfg.IntOpt('dell_sc_ssn', + default=64702, + help='Storage Center System Serial Number'), + cfg.IntOpt('dell_sc_api_port', + default=3033, + help='Dell API port'), + cfg.StrOpt('dell_sc_server_folder', + default='openstack', + help='Name of the server folder to use on the Storage Center'), + cfg.StrOpt('dell_sc_volume_folder', + default='openstack', + help='Name of the volume folder to use on the Storage Center') +] + +LOG = logging.getLogger(__name__) + +CONF = cfg.CONF +CONF.register_opts(common_opts) + + +class DellCommonDriver(san.SanDriver): + + def __init__(self, *args, **kwargs): + super(DellCommonDriver, self).__init__(*args, **kwargs) + self.configuration.append_config_values(common_opts) + self.backend_name =\ + self.configuration.safe_get('volume_backend_name') or 'Dell' + + def _bytes_to_gb(self, spacestring): + '''Space is returned in a string like ... + 7.38197504E8 Bytes + Need to split that apart and convert to GB. + + returns gbs in int form + ''' + try: + n = spacestring.split(' ', 1) + fgbs = float(n[0]) / 1073741824.0 + igbs = int(fgbs) + return igbs + except Exception: + # If any of that blew up it isn't in the format we + # thought so eat our error and return None + return None + + def do_setup(self, context): + '''One time driver setup. + + Called once by the manager after the driver is loaded. + Sets up clients, check licenses, sets up protocol + specific helpers. + ''' + self._client = dell_storagecenter_api.StorageCenterApiHelper( + self.configuration) + + def check_for_setup_error(self): + '''Validates the configuration information.''' + with self._client.open_connection() as api: + ssn = self.configuration.safe_get('dell_sc_ssn') + api.find_sc(ssn) + + def create_volume(self, volume): + '''Create a volume.''' + LOG.debug('create_volume') + scvolume = None + with self._client.open_connection() as api: + try: + # we use id as our name as it s unique + volume_name = volume['id'] + volume_size = volume['size'] + volume_folder = self.configuration.dell_sc_volume_folder + ssn = api.find_sc(self.configuration.dell_sc_ssn) + LOG.debug('cv: %(name)s : %(size)s : %(ssn)s', + {'name': volume_name, + 'size': volume_size, + 'ssn': ssn}) + if ssn is not None: + scvolume = api.create_volume(volume_name, + volume_size, + ssn, + volume_folder) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to create volume %s'), + volume['name']) + if scvolume is None: + raise exception.VolumeBackendAPIException( + _('unable to create volume')) + + def delete_volume(self, volume): + deleted = False + # we use id as our name as it s unique + volume_name = volume.get('id') + with self._client.open_connection() as api: + try: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + if ssn is not None: + deleted = api.delete_volume(ssn, + volume_name) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to delete volume %s'), + volume_name) + + # if there was an error we will have raised an + # exception. If it failed to delete it is because + # the conditions to delete a volume were not met. + if deleted is False: + raise exception.VolumeIsBusy(volume_name=volume_name) + + def create_snapshot(self, snapshot): + '''Create snapshot''' + # our volume name is the volume id + volume_name = snapshot.get('volume_id') + snapshot_id = snapshot.get('id') + with self._client.open_connection() as api: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + if ssn is not None: + scvolume = api.find_volume(ssn, + volume_name) + if scvolume is not None: + if api.create_replay(scvolume, + snapshot_id, + 0) is not None: + snapshot['status'] = 'available' + return + else: + LOG.warning(_LW('Unable to locate volume:%s'), + volume_name) + + snapshot['status'] = 'error_creating' + raise exception.VolumeBackendAPIException( + _('Failed to create snapshot %s') % + snapshot_id) + + def create_volume_from_snapshot(self, volume, snapshot): + '''Create new volume from other volume's snapshot on appliance.''' + scvolume = None + src_volume_name = snapshot.get('volume_id') + snapshot_id = snapshot.get('id') + volume_name = volume.get('id') + with self._client.open_connection() as api: + try: + volume_folder = self.configuration.dell_sc_volume_folder + ssn = api.find_sc(self.configuration.dell_sc_ssn) + srcvol = api.find_volume(ssn, + src_volume_name) + if srcvol is not None: + replay = api.find_replay(srcvol, + snapshot_id) + if replay is not None: + volume_name = volume.get('id') + scvolume = api.create_view_volume(volume_name, + volume_folder, + replay) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to create volume %s'), + volume_name) + if scvolume is not None: + LOG.debug('Volume %(n)s created from %(s)s', + {'n': volume_name, + 's': snapshot_id}) + else: + raise exception.VolumeBackendAPIException( + _('Failed to create volume %s') % volume_name) + + def create_cloned_volume(self, volume, src_vref): + '''Creates a clone of the specified volume.''' + LOG.debug('create_cloned_volume') + scvolume = None + src_volume_name = src_vref.get('id') + volume_name = volume.get('id') + with self._client.open_connection() as api: + try: + volume_folder = self.configuration.dell_sc_volume_folder + ssn = api.find_sc(self.configuration.dell_sc_ssn) + srcvol = api.find_volume(ssn, + src_volume_name) + if srcvol is not None: + scvolume = api.create_cloned_volume(volume_name, + volume_folder, + srcvol) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to create volume %s'), + volume_name) + if scvolume is not None: + LOG.debug('Volume %(n)s cloned from %(s)s', + {'n': volume_name, + 's': src_volume_name}) + else: + raise exception.VolumeBackendAPIException( + _('Failed to create volume %s') % volume_name) + + def delete_snapshot(self, snapshot): + '''delete_snapshot''' + volume_name = snapshot.get('volume_id') + snapshot_id = snapshot.get('id') + with self._client.open_connection() as api: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + if ssn is not None: + scvolume = api.find_volume(ssn, + volume_name) + if scvolume is not None: + if api.delete_replay(scvolume, + snapshot_id): + return + # if we are here things went poorly. + snapshot['status'] = 'error_deleting' + raise exception.VolumeBackendAPIException( + _('Failed to delete snapshot %s') % snapshot_id) + + def create_export(self, context, volume): + '''Create an export of a volume. + + The volume exists on creation and will be visible on + initialize connection. So nothing to do here. + ''' + pass + + def ensure_export(self, context, volume): + '''Ensure an export of a volume. + + Per the eqlx driver we just make sure that the volume actually + exists where we think it does. + ''' + scvolume = None + volume_name = volume.get('id') + with self._client.open_connection() as api: + try: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + if ssn is not None: + scvolume = api.find_volume(ssn, + volume_name) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to ensure export of volume %s'), + volume_name) + if scvolume is None: + raise exception.VolumeBackendAPIException( + _('unable to find volume %s') % volume_name) + + def remove_export(self, context, volume): + '''Remove an export of a volume. + + We do nothing here to match the nothing we do in create export. Again + we do everything in initialize and terminate connection. + ''' + pass + + def extend_volume(self, volume, new_size): + '''Extend the size of the volume.''' + volume_name = volume.get('id') + if volume is not None: + with self._client.open_connection() as api: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + if ssn is not None: + scvolume = api.find_volume(ssn, + volume_name) + if api.expand_volume(scvolume, new_size) is not None: + return + # If we are here nothing good happened. + raise exception.VolumeBackendAPIException( + _('Unable to extend volume %s') % volume_name) + + def get_volume_stats(self, refresh=False): + '''Get volume status. + + If 'refresh' is True, run update the stats first. + ''' + if refresh: + self._update_volume_stats() + + return self._stats + + def _update_volume_stats(self): + '''Retrieve stats info from volume group.''' + with self._client.open_connection() as api: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + storageusage = api.get_storage_usage(ssn) + + # all of this is basically static for now + data = {} + data['volume_backend_name'] = self.backend_name + data['vendor_name'] = 'Dell' + data['driver_version'] = self.VERSION + data['storage_protocol'] = 'iSCSI' + data['reserved_percentage'] = 0 + # in theory if storageusage is None then we should have + # blown up getting it. If not just report inifinite. + if storageusage is not None: + totalcapacity = storageusage.get('availableSpace') + totalcapacitygb = self._bytes_to_gb(totalcapacity) + data['total_capacity_gb'] = totalcapacitygb + freespace = storageusage.get('freeSpace') + freespacegb = self._bytes_to_gb(freespace) + data['free_capacity_gb'] = freespacegb + if data.get('total_capacity_gb') is None: + data['total_capacity_gb'] = 'unavailable' + if data.get('free_capacity_gb') is None: + data['free_capacity_gb'] = 'unavailable' + data['QoS_support'] = False + self._stats = data + LOG.debug('Total cap %(t)s Free cap %(f)s', + {'t': totalcapacitygb, + 'f': freespacegb}) diff --git a/cinder/volume/drivers/dell/dell_storagecenter_fc.py b/cinder/volume/drivers/dell/dell_storagecenter_fc.py new file mode 100644 index 000000000..c1ce3995a --- /dev/null +++ b/cinder/volume/drivers/dell/dell_storagecenter_fc.py @@ -0,0 +1,168 @@ +# Copyright 2014 Dell Inc. +# +# 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. + +'''Volume driver for Dell Storage Center.''' + +from oslo.utils import excutils + +from cinder import exception +from cinder.i18n import _, _LE +from cinder.openstack.common import log as logging +from cinder.volume import driver +from cinder.volume.drivers.dell import dell_storagecenter_common +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) + + +class DellStorageCenterFCDriver(driver.FibreChannelDriver, + dell_storagecenter_common.DellCommonDriver): + + '''Implements commands for Dell EqualLogic SAN ISCSI management. + + To enable the driver add the following line to the cinder configuration: + volume_driver=cinder.volume.drivers.dell.DellStorageCenterFCDriver + ''' + + VERSION = '1.0.1' + + def __init__(self, *args, **kwargs): + super(DellStorageCenterFCDriver, self).__init__(*args, **kwargs) + self.backend_name =\ + self.configuration.safe_get('volume_backend_name') or 'Dell-FC' + + @fczm_utils.AddFCZone + def initialize_connection(self, volume, connector): + '''Initializes the connection and returns connection info. + + Assign any created volume to a compute node/host so that it can be + used from that host. + + The driver returns a driver_volume_type of 'fibre_channel'. + The target_wwn can be a single entry or a list of wwns that + correspond to the list of remote wwn(s) that will export the volume. + ''' + + # We use id to name the volume name as it is a + # known unique name. + volume_name = volume.get('id') + LOG.debug('Initialize connection: %s', volume_name) + with self._client.open_connection() as api: + try: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + # Find our server. + wwpns = connector.get('wwpns') + for wwn in wwpns: + scserver = api.find_server(ssn, + wwn) + if scserver is not None: + break + + # No? Create it. + if scserver is None: + server_folder = self.configuration.dell_sc_server_folder + scserver = api.create_server_multiple_hbas(ssn, + server_folder, + wwpns) + # Find the volume on the storage center. + scvolume = api.find_volume(ssn, + volume_name) + if scserver is not None and scvolume is not None: + mapping = api.map_volume(scvolume, + scserver) + if mapping is not None: + # Since we just mapped our volume we had best update + # our sc volume object. + scvolume = api.find_volume(ssn, + volume_name) + lun, targets, init_targ_map = api.find_wwns(scvolume, + scserver) + if lun is not None and len(targets) > 0: + data = {'driver_volume_type': 'fibre_channel', + 'data': {'target_lun': lun, + 'target_discovered': True, + 'target_wwn': targets, + 'initiator_target_map': + init_targ_map}} + LOG.debug('Return FC data:') + LOG.debug(data) + return data + LOG.error(_LE('Lun mapping returned null!')) + + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to initialize connection ')) + + # We get here because our mapping is none so blow up. + raise exception.VolumeBackendAPIException( + _('unable to map volume')) + + @fczm_utils.RemoveFCZone + def terminate_connection(self, volume, connector, force=False, **kwargs): + # Get our volume name + volume_name = volume.get('id') + LOG.debug('Terminate connection: %s', volume_name) + with self._client.open_connection() as api: + try: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + wwpns = connector.get('wwpns') + for wwn in wwpns: + scserver = api.find_server(ssn, + wwn) + if scserver is not None: + break + + # Find the volume on the storage center. + scvolume = api.find_volume(ssn, + volume_name) + # Get our target map so we can return it to free up a zone. + lun, targets, init_targ_map = api.find_wwns(scvolume, + scserver) + # If we have a server and a volume lets unmap them. + if (scserver is not None and + scvolume is not None and + api.unmap_volume(scvolume, scserver) is True): + LOG.debug('Connection terminated') + else: + raise exception.VolumeBackendAPIException( + _('Terminate connection failed')) + + # basic return info... + info = {'driver_volume_type': 'fibre_channel', + 'data': {}} + + # if not then we return the target map so that + # the zone can be freed up. + if api.get_volume_count(scserver) == 0: + info['data'] = {'target_wwn': targets, + 'initiator_target_map': init_targ_map} + return info + + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to terminate connection')) + raise exception.VolumeBackendAPIException( + _('Terminate connection unable to connect to backend.')) + + def get_volume_stats(self, refresh=False): + '''Get volume status. + + If 'refresh' is True, run update the stats first. + ''' + if refresh: + self._update_volume_stats() + # Update our protocol to the correct one. + self._stats['storage_protocol'] = 'FC' + + return self._stats diff --git a/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py b/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py new file mode 100644 index 000000000..658bc5b43 --- /dev/null +++ b/cinder/volume/drivers/dell/dell_storagecenter_iscsi.py @@ -0,0 +1,140 @@ +# Copyright 2014 Dell Inc. +# +# 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. + +'''Volume driver for Dell Storage Center.''' + +from oslo.utils import excutils + +from cinder import exception +from cinder.i18n import _, _LE +from cinder.openstack.common import log as logging +from cinder.volume.drivers.dell import dell_storagecenter_common +from cinder.volume.drivers import san + +LOG = logging.getLogger(__name__) + + +class DellStorageCenterISCSIDriver(san.SanISCSIDriver, + dell_storagecenter_common.DellCommonDriver): + + '''Implements commands for Dell StorageCenter ISCSI management. + + To enable the driver add the following line to the cinder configuration: + volume_driver=cinder.volume.drivers.dell.DellStorageCenterISCSIDriver + ''' + + VERSION = '1.0.1' + + def __init__(self, *args, **kwargs): + super(DellStorageCenterISCSIDriver, self).__init__(*args, **kwargs) + self.backend_name =\ + self.configuration.safe_get('volume_backend_name') or 'Dell-iSCSI' + + def initialize_connection(self, volume, connector): + # We use id to name the volume name as it is a + # known unique name. + volume_name = volume.get('id') + initiator_name = connector.get('initiator') + LOG.debug('initialize_ connection: %(n)s:%(i)s', + {'n': volume_name, + 'i': initiator_name}) + + with self._client.open_connection() as api: + try: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + # Find our server. + server = api.find_server(ssn, + initiator_name) + # No? Create it. + if server is None: + server_folder = self.configuration.dell_sc_server_folder + server = api.create_server(ssn, + server_folder, + initiator_name) + # Find the volume on the storage center. + scvolume = api.find_volume(ssn, + volume_name) + + # if we have a server and a volume lets bring them together. + if server is not None and scvolume is not None: + mapping = api.map_volume(scvolume, + server) + if mapping is not None: + # Since we just mapped our volume we had best update + # our sc volume object. + scvolume = api.find_volume(ssn, + volume_name) + ip = self.configuration.iscsi_ip_address + port = self.configuration.iscsi_port + iqn = api.find_iqn(scvolume, + ip) + if iqn is None: + LOG.error(_LE('Volume mapped to invalid path.')) + else: + properties = {} + properties['target_discovered'] = False + properties['target_lun'] = mapping['lunUsed'][0] + if mapping['readOnly'] is True: + properties['access_mode'] = 'ro' + else: + properties['access_mode'] = 'rw' + properties['target_portal'] = ip + ':' + str(port) + properties['target_iqn'] = iqn + + LOG.debug(properties) + return {'driver_volume_type': 'iscsi', + 'data': properties + } + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to initialize connection ' + ' %(i)s %(n)s'), + {'i': initiator_name, + 'n': volume_name}) + + # We get here because our mapping is none or we have no valid iqn to + # return so blow up. + raise exception.VolumeBackendAPIException( + _('Unable to map volume')) + + def terminate_connection(self, volume, connector, force=False, **kwargs): + # Grab some initial info. + initiator_name = connector.get('initiator') + volume_name = volume.get('id') + LOG.debug('Terminate connection: %(n)s:%(i)s', + {'n': volume_name, + 'i': initiator_name}) + with self._client.open_connection() as api: + try: + ssn = api.find_sc(self.configuration.dell_sc_ssn) + scserver = api.find_server(ssn, + initiator_name) + # Find the volume on the storage center. + scvolume = api.find_volume(ssn, + volume_name) + + # If we have a server and a volume lets pull them apart. + if (scserver is not None and + scvolume is not None and + api.unmap_volume(scvolume, scserver) is True): + LOG.debug('Connection terminated') + return + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to terminate connection ' + '%(i)s %(n)s'), + {'i': initiator_name, + 'n': volume_name}) + raise exception.VolumeBackendAPIException( + _('Terminate connection failed')) -- 2.45.2