diff --git a/cinder/opts.py b/cinder/opts.py index a14a77c8dd2..da72732ed43 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -90,8 +90,6 @@ from cinder.volume.drivers.fusionstorage import dsware as \ cinder_volume_drivers_fusionstorage_dsware from cinder.volume.drivers.hpe import hpe_3par_common as \ cinder_volume_drivers_hpe_hpe3parcommon -from cinder.volume.drivers.hpe import hpe_lefthand_iscsi as \ - cinder_volume_drivers_hpe_hpelefthandiscsi from cinder.volume.drivers.huawei import common as \ cinder_volume_drivers_huawei_common from cinder.volume.drivers.ibm import flashsystem_common as \ @@ -291,7 +289,6 @@ def list_opts(): cinder_volume_drivers_fujitsu_eternus_dx_eternusdxcommon. FJ_ETERNUS_DX_OPT_opts, cinder_volume_drivers_hpe_hpe3parcommon.hpe3par_opts, - cinder_volume_drivers_hpe_hpelefthandiscsi.hpelefthand_opts, cinder_volume_drivers_huawei_common.huawei_opts, cinder_volume_drivers_ibm_flashsystemcommon.flashsystem_opts, cinder_volume_drivers_ibm_flashsystemiscsi. diff --git a/cinder/tests/unit/volume/drivers/hpe/fake_hpe_lefthand_client.py b/cinder/tests/unit/volume/drivers/hpe/fake_hpe_lefthand_client.py deleted file mode 100644 index 74331bcf38a..00000000000 --- a/cinder/tests/unit/volume/drivers/hpe/fake_hpe_lefthand_client.py +++ /dev/null @@ -1,28 +0,0 @@ -# (c) Copyright 2014-2016 Hewlett Packard Enterprise Development LP -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Fake HPE client for testing LeftHand without installing the client.""" - -import sys -from unittest import mock - -from cinder.tests.unit.volume.drivers.hpe \ - import fake_hpe_client_exceptions as hpeexceptions - -hpelefthand = mock.Mock() -hpelefthand.version = "2.1.0" -hpelefthand.exceptions = hpeexceptions - -sys.modules['hpelefthandclient'] = hpelefthand diff --git a/cinder/tests/unit/volume/drivers/hpe/test_hpelefthand.py b/cinder/tests/unit/volume/drivers/hpe/test_hpelefthand.py deleted file mode 100644 index b3bc69125e7..00000000000 --- a/cinder/tests/unit/volume/drivers/hpe/test_hpelefthand.py +++ /dev/null @@ -1,3470 +0,0 @@ -# (c) Copyright 2014-2016 Hewlett Packard Enterprise Development LP -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""Unit tests for OpenStack Cinder volume drivers.""" - -import copy -import json -from unittest import mock - -from oslo_utils import units - -from cinder import context -from cinder import exception -from cinder.objects import fields -from cinder import test -from cinder.tests.unit.volume.drivers.hpe \ - import fake_hpe_lefthand_client as hpelefthandclient -from cinder.volume.drivers.hpe import hpe_lefthand_iscsi -from cinder.volume import volume_types - -hpeexceptions = hpelefthandclient.hpeexceptions - -GOODNESS_FUNCTION = \ - "capabilities.capacity_utilization < 0.6? 100 : 25" -FILTER_FUNCTION = \ - "capabilities.total_volumes < 400 && capabilities.capacity_utilization" -HPELEFTHAND_SAN_SSH_CON_TIMEOUT = 44 -HPELEFTHAND_SAN_SSH_PRIVATE = 'foobar' -HPELEFTHAND_API_URL = 'http://fake.foo:8080/lhos' -HPELEFTHAND_API_URL2 = 'http://fake2.foo2:8080/lhos' -HPELEFTHAND_SSH_IP = 'fake.foo' -HPELEFTHAND_SSH_IP2 = 'fake2.foo2' -HPELEFTHAND_USERNAME = 'foo1' -HPELEFTHAND_PASSWORD = 'bar2' -HPELEFTHAND_SSH_PORT = 16022 -HPELEFTHAND_CLUSTER_NAME = 'CloudCluster1' -VOLUME_TYPE_ID_REPLICATED = 'be9181f1-4040-46f2-8298-e7532f2bf9db' -FAKE_FAILOVER_HOST = 'fakefailover@foo#destfakepool' -REPLICATION_BACKEND_ID = 'target' - - -class HPELeftHandBaseDriver(object): - - cluster_id = 1 - - volume_name = "fakevolume" - volume_name_repl = "fakevolume_replicated" - volume_id = 1 - volume = { - 'name': volume_name, - 'display_name': 'Foo Volume', - 'provider_location': ('10.0.1.6 iqn.2003-10.com.lefthandnetworks:' - 'group01:25366:fakev 0'), - 'id': volume_id, - 'provider_auth': None, - 'size': 1} - - volume_extend = { - 'name': volume_name, - 'display_name': 'Foo Volume', - 'provider_location': ('10.0.1.6 iqn.2003-10.com.lefthandnetworks:' - 'group01:25366:fakev 0'), - 'id': volume_id, - 'provider_auth': None, - 'size': 5} - - volume_replicated = { - 'name': volume_name_repl, - 'display_name': 'Foo Volume', - 'provider_location': ('10.0.1.6 iqn.2003-10.com.lefthandnetworks:' - 'group01:25366:fakev 0'), - 'id': volume_id, - 'provider_auth': None, - 'size': 1, - 'volume_type': 'replicated', - 'volume_type_id': VOLUME_TYPE_ID_REPLICATED, - 'replication_driver_data': ('{"location": "' + HPELEFTHAND_API_URL + - '"}')} - - repl_targets = [{'backend_id': 'target', - 'managed_backend_name': FAKE_FAILOVER_HOST, - 'hpelefthand_api_url': HPELEFTHAND_API_URL2, - 'hpelefthand_username': HPELEFTHAND_USERNAME, - 'hpelefthand_password': HPELEFTHAND_PASSWORD, - 'hpelefthand_clustername': HPELEFTHAND_CLUSTER_NAME, - 'hpelefthand_ssh_port': HPELEFTHAND_SSH_PORT, - 'ssh_conn_timeout': HPELEFTHAND_SAN_SSH_CON_TIMEOUT, - 'san_private_key': HPELEFTHAND_SAN_SSH_PRIVATE, - 'cluster_id': 6, - 'cluster_vip': '10.0.1.6'}] - - repl_targets_unmgd = [{'backend_id': 'target', - 'hpelefthand_api_url': HPELEFTHAND_API_URL2, - 'hpelefthand_username': HPELEFTHAND_USERNAME, - 'hpelefthand_password': HPELEFTHAND_PASSWORD, - 'hpelefthand_clustername': HPELEFTHAND_CLUSTER_NAME, - 'hpelefthand_ssh_port': HPELEFTHAND_SSH_PORT, - 'ssh_conn_timeout': HPELEFTHAND_SAN_SSH_CON_TIMEOUT, - 'san_private_key': HPELEFTHAND_SAN_SSH_PRIVATE, - 'cluster_id': 6, - 'cluster_vip': '10.0.1.6'}] - - list_rep_targets = [{'backend_id': REPLICATION_BACKEND_ID}] - - serverName = 'fakehost' - server_id = 0 - server_uri = '/lhos/servers/0' - - snapshot_name = "fakeshapshot" - snapshot_id = 3 - snapshot = { - 'id': snapshot_id, - 'name': snapshot_name, - 'display_name': 'fakesnap', - 'volume_name': volume_name, - 'volume': volume, - 'volume_size': 1} - - cloned_volume_name = "clone_volume" - cloned_volume = {'name': cloned_volume_name, - 'size': 1} - cloned_volume_extend = {'name': cloned_volume_name, - 'size': 5} - - cloned_snapshot_name = "clonedshapshot" - cloned_snapshot_id = 5 - cloned_snapshot = { - 'name': cloned_snapshot_name, - 'volume_name': volume_name} - - volume_type_id = 4 - init_iqn = 'iqn.1993-08.org.debian:01:222' - - volume_type = {'name': 'gold', - 'deleted': False, - 'updated_at': None, - 'extra_specs': {'hpelh:provisioning': 'thin', - 'hpelh:ao': 'true', - 'hpelh:data_pl': 'r-0'}, - 'deleted_at': None, - 'id': 'gold'} - old_volume_type = {'name': 'gold', - 'deleted': False, - 'updated_at': None, - 'extra_specs': {'hplh:provisioning': 'thin', - 'hplh:ao': 'true', - 'hplh:data_pl': 'r-0'}, - 'deleted_at': None, - 'id': 'gold'} - - connector = { - 'ip': '10.0.0.2', - 'initiator': 'iqn.1993-08.org.debian:01:222', - 'host': serverName} - - driver_startup_call_stack = [ - mock.call.login('foo1', 'bar2'), - mock.call.getClusterByName('CloudCluster1'), - mock.call.setSSHOptions( - HPELEFTHAND_SSH_IP, - HPELEFTHAND_USERNAME, - HPELEFTHAND_PASSWORD, - missing_key_policy='AutoAddPolicy', - privatekey=HPELEFTHAND_SAN_SSH_PRIVATE, - known_hosts_file=mock.ANY, - port=HPELEFTHAND_SSH_PORT, - conn_timeout=HPELEFTHAND_SAN_SSH_CON_TIMEOUT), - ] - - -class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase): - - CONSIS_GROUP_ID = '3470cc4c-63b3-4c7a-8120-8a0693b45838' - GROUPSNAPSHOT_ID = '5351d914-6c90-43e7-9a8e-7e84610927da' - - class fake_group_object(object): - volume_type_ids = '371c64d5-b92a-488c-bc14-1e63cef40e08' - name = 'group_name' - groupsnapshot_id = None - id = '3470cc4c-63b3-4c7a-8120-8a0693b45838' - description = 'group' - - class fake_groupsnapshot_object(object): - group_id = '3470cc4c-63b3-4c7a-8120-8a0693b45838' - description = 'groupsnapshot' - id = '5351d914-6c90-43e7-9a8e-7e84610927da' - readOnly = False - - def default_mock_conf(self): - - mock_conf = mock.MagicMock() - mock_conf.hpelefthand_api_url = HPELEFTHAND_API_URL - mock_conf.hpelefthand_username = HPELEFTHAND_USERNAME - mock_conf.hpelefthand_password = HPELEFTHAND_PASSWORD - mock_conf.hpelefthand_ssh_port = HPELEFTHAND_SSH_PORT - mock_conf.ssh_conn_timeout = HPELEFTHAND_SAN_SSH_CON_TIMEOUT - mock_conf.san_private_key = HPELEFTHAND_SAN_SSH_PRIVATE - mock_conf.hpelefthand_iscsi_chap_enabled = False - mock_conf.hpelefthand_debug = False - mock_conf.hpelefthand_clustername = "CloudCluster1" - mock_conf.goodness_function = GOODNESS_FUNCTION - mock_conf.filter_function = FILTER_FUNCTION - mock_conf.reserved_percentage = 25 - - def safe_get(attr): - try: - return mock_conf.__getattribute__(attr) - except AttributeError: - return None - mock_conf.safe_get = safe_get - - return mock_conf - - @mock.patch('hpelefthandclient.client.HPELeftHandClient', spec=True) - def setup_driver(self, _mock_client, config=None): - if config is None: - config = self.default_mock_conf() - - _mock_client.return_value.getClusterByName.return_value = { - 'id': 1, 'virtualIPAddresses': [{'ipV4Address': '10.0.1.6'}]} - _mock_client.return_value.getCluster.return_value = { - 'spaceTotal': units.Gi * 500, - 'spaceAvailable': units.Gi * 250} - _mock_client.return_value.getApiVersion.return_value = '1.2' - _mock_client.return_value.getIPFromCluster.return_value = '1.1.1.1' - self.driver = hpe_lefthand_iscsi.HPELeftHandISCSIDriver( - configuration=config) - self.driver.do_setup(None) - self.cluster_name = config.hpelefthand_clustername - return _mock_client.return_value - - @mock.patch('hpelefthandclient.version', "1.0.0") - def test_unsupported_client_version(self): - - self.assertRaises(exception.InvalidInput, - self.setup_driver) - - @mock.patch('hpelefthandclient.version', "3.0.0") - def test_supported_client_version(self): - - self.setup_driver() - - def test__login(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute driver - self.driver._login() - expected = self.driver_startup_call_stack - mock_client.assert_has_calls(expected) - - # mock HTTPNotFound - mock_client.login.side_effect = ( - hpeexceptions.HTTPNotFound()) - # ensure the raised exception is a cinder exception - self.assertRaises(exception.DriverNotInitialized, - self.driver._login) - - # mock other HTTP exception - mock_client.login.side_effect = ( - hpeexceptions.HTTPServerError()) - # ensure the raised exception is a cinder exception - self.assertRaises(exception.DriverNotInitialized, - self.driver._login) - - def test_get_version_string(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute driver - ver_string = self.driver.get_version_string() - self.assertEqual(self.driver.VERSION, ver_string.split()[1]) - - def test_check_for_setup_error(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - # test for latest version - mock_client.getApiVersion.return_value = ('3.0.0') - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute driver - self.driver.check_for_setup_error() - - expected = self.driver_startup_call_stack + [ - mock.call.getApiVersion(), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - # test for older version - mock_client.reset_mock() - mock_client.getApiVersion.return_value = ('1.0.0') - - # execute driver - self.driver.check_for_setup_error() - mock_client.assert_has_calls(expected) - - def test_create_volume(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - # mock return value of createVolume - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute driver - volume_info = self.driver.create_volume(self.volume) - - self.assertEqual('10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0', - volume_info['provider_location']) - - expected = self.driver_startup_call_stack + [ - mock.call.createVolume( - 'fakevolume', - 1, - units.Gi, - {'isThinProvisioned': True, - 'clusterName': 'CloudCluster1'}), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - # mock HTTPServerError - mock_client.createVolume.side_effect =\ - hpeexceptions.HTTPServerError() - # ensure the raised exception is a cinder exception - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_volume, self.volume) - - @mock.patch.object( - volume_types, - 'get_volume_type', - return_value={'extra_specs': {'hpelh:provisioning': 'full'}}) - def test_create_volume_with_es(self, _mock_volume_type): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - volume_with_vt = self.volume - volume_with_vt['volume_type_id'] = 1 - - # mock return value of createVolume - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute create_volume - volume_info = self.driver.create_volume(volume_with_vt) - - self.assertEqual('10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0', - volume_info['provider_location']) - - expected = self.driver_startup_call_stack + [ - mock.call.createVolume( - 'fakevolume', - 1, - units.Gi, - {'isThinProvisioned': False, - 'clusterName': 'CloudCluster1'}), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - @mock.patch.object( - volume_types, - 'get_volume_type', - return_value={'extra_specs': (HPELeftHandBaseDriver. - old_volume_type['extra_specs'])}) - def test_create_volume_old_volume_type(self, _mock_volume_type): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - # mock return value of createVolume - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute driver - volume_info = self.driver.create_volume(self.volume) - - self.assertEqual('10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0', - volume_info['provider_location']) - - expected = self.driver_startup_call_stack + [ - mock.call.createVolume( - 'fakevolume', - 1, - units.Gi, - {'isThinProvisioned': True, - 'clusterName': 'CloudCluster1'}), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - # mock HTTPServerError - mock_client.createVolume.side_effect =\ - hpeexceptions.HTTPServerError() - # ensure the raised exception is a cinder exception - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.create_volume, self.volume) - - def test_delete_volume(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - # mock return value of getVolumeByName - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute delete_volume - del_volume = self.volume - del_volume['volume_type_id'] = None - self.driver.delete_volume(del_volume) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.deleteVolume(self.volume_id), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - # mock HTTPNotFound (volume not found) - mock_client.getVolumeByName.side_effect =\ - hpeexceptions.HTTPNotFound() - # no exception should escape method - self.driver.delete_volume(del_volume) - - # mock HTTPConflict - mock_client.deleteVolume.side_effect = hpeexceptions.HTTPConflict() - # ensure the raised exception is a cinder exception - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.delete_volume, {}) - - def test_extend_volume(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - # mock return value of getVolumeByName - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute extend_volume - self.driver.extend_volume(self.volume, 2) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.modifyVolume(1, {'size': 2 * units.Gi}), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - # mock HTTPServerError (array failure) - mock_client.modifyVolume.side_effect =\ - hpeexceptions.HTTPServerError() - # ensure the raised exception is a cinder exception - self.assertRaises(exception.VolumeBackendAPIException, - self.driver.extend_volume, self.volume, 2) - - def test_initialize_connection(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - # mock return value of getVolumeByName - mock_client.getServerByName.side_effect = hpeexceptions.HTTPNotFound() - mock_client.createServer.return_value = {'id': self.server_id} - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': None - } - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute initialize_connection - result = self.driver.initialize_connection( - self.volume, - self.connector) - - # validate - self.assertEqual('iscsi', result['driver_volume_type']) - self.assertFalse(result['data']['target_discovered']) - self.assertEqual(self.volume_id, result['data']['volume_id']) - self.assertNotIn('auth_method', result['data']) - - expected = self.driver_startup_call_stack + [ - mock.call.getServerByName('fakehost'), - mock.call.createServer - ( - 'fakehost', - 'iqn.1993-08.org.debian:01:222', - None - ), - mock.call.getVolumeByName('fakevolume'), - mock.call.addServerAccess(1, 0), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - # mock HTTPServerError (array failure) - mock_client.createServer.side_effect =\ - hpeexceptions.HTTPServerError() - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.initialize_connection, self.volume, self.connector) - - def test_create_server(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - # mock return value of getVolumeByName - mock_client.getServerByName.side_effect = hpeexceptions.HTTPNotFound() - mock_client.createServer.return_value = {'id': self.server_id} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # create server - self.driver._create_server(self.connector, mock_client) - - expected = [ - mock.call.getServerByName('fakehost'), - mock.call.createServer( - 'fakehost', - 'iqn.1993-08.org.debian:01:222', - None)] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_initialize_connection_session_exists(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - # mock return value of getVolumeByName - mock_client.getServerByName.side_effect = hpeexceptions.HTTPNotFound() - mock_client.createServer.return_value = {'id': self.server_id} - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': [{'server': {'uri': self.server_uri}}] - } - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute initialize_connection - result = self.driver.initialize_connection( - self.volume, - self.connector) - - # validate - self.assertEqual('iscsi', result['driver_volume_type']) - self.assertFalse(result['data']['target_discovered']) - self.assertEqual(self.volume_id, result['data']['volume_id']) - self.assertNotIn('auth_method', result['data']) - - expected = self.driver_startup_call_stack + [ - mock.call.getServerByName('fakehost'), - mock.call.createServer - ( - 'fakehost', - 'iqn.1993-08.org.debian:01:222', - None - ), - mock.call.getVolumeByName('fakevolume'), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_initialize_connection_with_chaps(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - # mock return value of getVolumeByName - mock_client.getServerByName.side_effect = hpeexceptions.HTTPNotFound() - mock_client.createServer.return_value = { - 'id': self.server_id, - 'chapAuthenticationRequired': True, - 'chapTargetSecret': 'dont_tell'} - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': None - } - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute initialize_connection - result = self.driver.initialize_connection( - self.volume, - self.connector) - - # validate - self.assertEqual('iscsi', result['driver_volume_type']) - self.assertFalse(result['data']['target_discovered']) - self.assertEqual(self.volume_id, result['data']['volume_id']) - self.assertEqual('CHAP', result['data']['auth_method']) - - expected = self.driver_startup_call_stack + [ - mock.call.getServerByName('fakehost'), - mock.call.createServer - ( - 'fakehost', - 'iqn.1993-08.org.debian:01:222', - None - ), - mock.call.getVolumeByName('fakevolume'), - mock.call.addServerAccess(1, 0), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - @mock.patch('cinder.volume.volume_utils.generate_password') - def test_initialize_connection_with_chap_disabled(self, mock_utils): - # setup_mock_client drive with CHAP disabled configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_utils.return_value = 'random-pass' - mock_client.getServerByName.return_value = { - 'id': self.server_id, - 'name': self.serverName, - 'chapTargetSecret': 'random-pass'} - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': None - } - expected = [ - mock.call.getServerByName('fakehost'), - mock.call.getVolumeByName('fakevolume'), - mock.call.addServerAccess(1, 0), - mock.call.logout()] - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # test initialize_connection with chap disabled - # and chapTargetSecret - self.driver.initialize_connection( - self.volume, - self.connector) - mock_client.assert_has_calls(expected) - - @mock.patch('cinder.volume.volume_utils.generate_password') - def test_initialize_connection_with_chap_enabled(self, mock_utils): - # setup_mock_client drive with CHAP enabled configuration - # and return the mock HTTP LeftHand client - conf = self.default_mock_conf() - conf.hpelefthand_iscsi_chap_enabled = True - mock_client = self.setup_driver(config=conf) - mock_client.getServerByName.return_value = { - 'id': self.server_id, - 'name': self.serverName, - 'chapTargetSecret': None} - - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': None} - expected = [ - mock.call.getServerByName('fakehost'), - mock.call.getVolumeByName('fakevolume'), - mock.call.addServerAccess(1, 0), - mock.call.logout()] - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # test initialize_connection with chap enabled - # and chapTargetSecret is None - result = self.driver.initialize_connection( - self.volume, - self.connector) - mock_client.assert_has_calls(expected) - - # test initialize_connection with chapAuthenticationRequired - mock_client.getServerByName.side_effect = ( - hpeexceptions.HTTPNotFound()) - mock_client.createServer.return_value = { - 'id': self.server_id, - 'chapAuthenticationRequired': True, - 'chapTargetSecret': 'random-pass'} - result = self.driver.initialize_connection( - self.volume, - self.connector) - self.assertEqual('random-pass', result['data']['auth_password']) - - def test_terminate_connection(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getServerByName.return_value = { - 'id': self.server_id, - 'name': self.serverName} - mock_client.findServerVolumes.return_value = [{'id': self.volume_id}] - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute terminate_connection - self.driver.terminate_connection(self.volume, self.connector) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.getServerByName('fakehost'), - mock.call.findServerVolumes('fakehost'), - mock.call.removeServerAccess(1, 0), - mock.call.deleteServer(0)] - - # validate call chain - mock_client.assert_has_calls(expected) - - mock_client.getVolumeByName.side_effect = ( - hpeexceptions.HTTPNotFound()) - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.terminate_connection, - self.volume, - self.connector) - - def test_terminate_connection_from_primary_when_failed_over(self): - # setup drive with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getServerByName.side_effect = hpeexceptions.HTTPNotFound( - "The host does not exist.") - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.driver._active_backend_id = 'some_id' - # execute terminate_connection - self.driver.terminate_connection(self.volume, self.connector) - - # When the volume is still attached to the primary array after a - # fail-over, there should be no call to delete the server. - # We can assert this method is not called to make sure - # the proper exceptions are being raised. - self.assertEqual(0, mock_client.removeServerAccess.call_count) - - def test_terminate_connection_multiple_volumes_on_server(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getServerByName.return_value = { - 'id': self.server_id, - 'name': self.serverName} - mock_client.findServerVolumes.return_value = [ - {'id': self.volume_id}, - {'id': 99999}] - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute terminate_connection - self.driver.terminate_connection(self.volume, self.connector) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.getServerByName('fakehost'), - mock.call.findServerVolumes('fakehost'), - mock.call.removeServerAccess(1, 0)] - - # validate call chain - mock_client.assert_has_calls(expected) - self.assertFalse(mock_client.deleteServer.called) - - mock_client.getVolumeByName.side_effect = ( - hpeexceptions.HTTPNotFound()) - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.terminate_connection, - self.volume, - self.connector) - - def test_create_snapshot(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute create_snapshot - self.driver.create_snapshot(self.snapshot) - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.createSnapshot( - 'fakeshapshot', - 1, - {'inheritAccess': True}), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - # mock HTTPServerError (array failure) - mock_client.getVolumeByName.side_effect =\ - hpeexceptions.HTTPNotFound() - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.create_snapshot, self.snapshot) - - def test_delete_snapshot(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute delete_snapshot - self.driver.delete_snapshot(self.snapshot) - - expected = self.driver_startup_call_stack + [ - mock.call.getSnapshotByName('fakeshapshot'), - mock.call.deleteSnapshot(3), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - mock_client.getSnapshotByName.side_effect =\ - hpeexceptions.HTTPNotFound() - # no exception is thrown, just error msg is logged - self.driver.delete_snapshot(self.snapshot) - - # mock HTTPServerError (array failure) - ex = hpeexceptions.HTTPServerError({'message': 'Some message.'}) - mock_client.getSnapshotByName.side_effect = ex - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.delete_snapshot, - self.snapshot) - - # mock HTTPServerError because the snap is in use - ex = hpeexceptions.HTTPServerError({ - 'message': - 'Hey, dude cannot be deleted because it is a clone point' - ' duh.'}) - mock_client.getSnapshotByName.side_effect = ex - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.SnapshotIsBusy, - self.driver.delete_snapshot, - self.snapshot) - - # Exception other than HTTPServerError and HTTPNotFound - ex = hpeexceptions.HTTPBadRequest({ - 'message': 'Bad request'}) - mock_client.getSnapshotByName.side_effect = ex - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.delete_snapshot, - self.snapshot) - - def test_create_volume_from_snapshot(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id} - mock_client.cloneSnapshot.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute create_volume_from_snapshot - model_update = self.driver.create_volume_from_snapshot( - self.volume, self.snapshot) - - expected_iqn = 'iqn.1993-08.org.debian:01:222 0' - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(expected_location, - model_update['provider_location']) - - expected = self.driver_startup_call_stack + [ - mock.call.getSnapshotByName('fakeshapshot'), - mock.call.cloneSnapshot('fakevolume', 3), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_create_volume_from_snapshot_extend(self): - - # setup drive with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id} - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.cloneSnapshot.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute create_volume_from_snapshot - model_update = self.driver.create_volume_from_snapshot( - self.volume_extend, self.snapshot) - - expected_iqn = 'iqn.1993-08.org.debian:01:222 0' - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(expected_location, - model_update['provider_location']) - - expected = self.driver_startup_call_stack + [ - mock.call.getSnapshotByName('fakeshapshot'), - mock.call.cloneSnapshot('fakevolume', 3), - mock.call.login('foo1', 'bar2'), - mock.call.getClusterByName('CloudCluster1'), - mock.call.setSSHOptions( - HPELEFTHAND_SSH_IP, - HPELEFTHAND_USERNAME, - HPELEFTHAND_PASSWORD, - missing_key_policy='AutoAddPolicy', - privatekey=HPELEFTHAND_SAN_SSH_PRIVATE, - known_hosts_file=mock.ANY, - port=HPELEFTHAND_SSH_PORT, - conn_timeout=HPELEFTHAND_SAN_SSH_CON_TIMEOUT), - mock.call.getVolumeByName('fakevolume'), - mock.call.modifyVolume(self.volume_id, { - 'size': self.volume_extend['size'] * units.Gi}), - mock.call.logout(), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_create_volume_from_snapshot_with_replication( - self, - mock_get_volume_type): - - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets_unmgd - mock_client = self.setup_driver(config=conf) - - mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id} - mock_client.cloneSnapshot.return_value = { - 'iscsiIqn': self.connector['initiator']} - - mock_client.doesRemoteSnapshotScheduleExist.return_value = True - mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - - volume = self.volume.copy() - volume['volume_type_id'] = self.volume_type_id - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute create_volume_from_snapshot for volume replication - # enabled, with snapshot scheduled - model_update = self.driver.create_volume_from_snapshot( - volume, self.snapshot) - - expected_iqn = 'iqn.1993-08.org.debian:01:222 0' - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(expected_location, - model_update['provider_location']) - self.assertEqual('enabled', model_update['replication_status']) - # expected calls - expected = self.driver_startup_call_stack + [ - mock.call.getSnapshotByName('fakeshapshot'), - mock.call.cloneSnapshot(self.volume['name'], self.snapshot_id), - mock.call.doesRemoteSnapshotScheduleExist( - 'fakevolume_SCHED_Pri'), - mock.call.startRemoteSnapshotSchedule('fakevolume_SCHED_Pri'), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - def test_create_volume_from_snapshot_exception(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getSnapshotByName.side_effect = ( - hpeexceptions.HTTPNotFound()) - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # testing when getSnapshotByName returns an exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.create_volume_from_snapshot, - self.volume, self.snapshot) - - # expected calls - expected = self.driver_startup_call_stack + [ - mock.call.getSnapshotByName(self.snapshot_name), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - def test_create_cloned_volume(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.cloneVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute create_cloned_volume - model_update = self.driver.create_cloned_volume( - self.cloned_volume, self.volume) - - expected_iqn = 'iqn.1993-08.org.debian:01:222 0' - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(expected_location, - model_update['provider_location']) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.cloneVolume('clone_volume', 1), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_create_cloned_volume_with_replication(self, mock_get_volume_type): - - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets_unmgd - mock_client = self.setup_driver(config=conf) - mock_replicated_client = self.setup_driver(config=conf) - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.cloneVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.doesRemoteSnapshotScheduleExist.return_value = False - - mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - cloned_volume = self.cloned_volume.copy() - cloned_volume['volume_type_id'] = self.volume_type_id - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - # execute create_cloned_volume - model_update = self.driver.create_cloned_volume( - cloned_volume, self.volume) - self.assertEqual('enabled', - model_update['replication_status']) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.cloneVolume('clone_volume', 1), - mock.call.doesRemoteSnapshotScheduleExist( - 'clone_volume_SCHED_Pri'), - mock.call.createRemoteSnapshotSchedule( - 'clone_volume', 'clone_volume_SCHED', 1800, - '1970-01-01T00:00:00Z', 5, 'CloudCluster1', 5, - 'clone_volume', '1.1.1.1', 'foo1', 'bar2'), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - expected_calls_replica_client = [ - mock.call.createVolume('clone_volume', 1, - cloned_volume['size'] * units.Gi, - None), - mock.call.makeVolumeRemote('clone_volume', - 'clone_volume_SS'), - mock.call.getIPFromCluster('CloudCluster1')] - - mock_replicated_client.assert_has_calls( - expected_calls_replica_client) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_create_cloned_volume_with_different_provision(self, - mock_volume_type): - - conf = self.default_mock_conf() - mock_client = self.setup_driver(config=conf) - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.cloneVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - - mock_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': {'hpelh:provisioning': 'full'}} - cloned_volume = self.cloned_volume.copy() - volume = self.volume.copy() - volume['volume_type_id'] = 2 - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - - mock_do_setup.return_value = mock_client - - # execute create_cloned_volume - self.driver.create_cloned_volume( - cloned_volume, volume) - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.cloneVolume('clone_volume', 1), - mock.call.getVolumeByName('clone_volume'), - mock.call.modifyVolume(1, {'isThinProvisioned': False}), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - def test_create_cloned_volume_exception(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.cloneVolume.side_effect = ( - hpeexceptions.HTTPServerError()) - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.create_cloned_volume, - self.cloned_volume, self.volume) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.cloneVolume('clone_volume', 1), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_create_cloned_volume_extend(self): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.cloneVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute create_cloned_volume with extend - model_update = self.driver.create_cloned_volume( - self.cloned_volume_extend, self.volume) - - expected_iqn = 'iqn.1993-08.org.debian:01:222 0' - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(expected_location, - model_update['provider_location']) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.cloneVolume('clone_volume', 1), - mock.call.login('foo1', 'bar2'), - mock.call.getClusterByName('CloudCluster1'), - mock.call.setSSHOptions( - HPELEFTHAND_SSH_IP, - HPELEFTHAND_USERNAME, - HPELEFTHAND_PASSWORD, - missing_key_policy='AutoAddPolicy', - privatekey=HPELEFTHAND_SAN_SSH_PRIVATE, - known_hosts_file=mock.ANY, - port=HPELEFTHAND_SSH_PORT, - conn_timeout=HPELEFTHAND_SAN_SSH_CON_TIMEOUT), - mock.call.getVolumeByName('clone_volume'), - mock.call.modifyVolume(self.volume_id, {'size': 5 * units.Gi}), - mock.call.logout(), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_extra_spec_mapping(self, _mock_get_volume_type): - - # setup driver with default configuration - self.setup_driver() - - # 2 extra specs we don't care about, and - # 1 that will get mapped - _mock_get_volume_type.return_value = { - 'extra_specs': { - 'foo:bar': 'fake', - 'bar:foo': 1234, - 'hpelh:provisioning': 'full'}} - - volume_with_vt = self.volume - volume_with_vt['volume_type_id'] = self.volume_type_id - - # get the extra specs of interest from this volume's volume type - volume_extra_specs = self.driver._get_volume_extra_specs( - volume_with_vt) - extra_specs = self.driver._get_lh_extra_specs( - volume_extra_specs, - hpe_lefthand_iscsi.extra_specs_key_map.keys()) - - # map the extra specs key/value pairs to key/value pairs - # used as optional configuration values by the LeftHand backend - optional = self.driver._map_extra_specs(extra_specs) - - self.assertDictEqual({'isThinProvisioned': False}, optional) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_extra_spec_mapping_invalid_value(self, _mock_get_volume_type): - - # setup driver with default configuration - self.setup_driver() - - volume_with_vt = self.volume - volume_with_vt['volume_type_id'] = self.volume_type_id - - _mock_get_volume_type.return_value = { - 'extra_specs': { - # r-07 is an invalid value for hpelh:ao - 'hpelh:data_pl': 'r-07', - 'hpelh:ao': 'true'}} - - # get the extra specs of interest from this volume's volume type - volume_extra_specs = self.driver._get_volume_extra_specs( - volume_with_vt) - extra_specs = self.driver._get_lh_extra_specs( - volume_extra_specs, - hpe_lefthand_iscsi.extra_specs_key_map.keys()) - - # map the extra specs key/value pairs to key/value pairs - # used as optional configuration values by the LeftHand backend - optional = self.driver._map_extra_specs(extra_specs) - - # {'hpelh:ao': 'true'} should map to - # {'isAdaptiveOptimizationEnabled': True} - # without hpelh:data_pl since r-07 is an invalid value - self.assertDictEqual({'isAdaptiveOptimizationEnabled': True}, optional) - - def test_retype_with_no_LH_extra_specs(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - ctxt = context.get_admin_context() - - host = {'host': self.serverName} - key_specs_old = {'foo': False, 'bar': 2, 'error': True} - key_specs_new = {'foo': True, 'bar': 5, 'error': False} - old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) - new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) - - diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], - new_type_ref['id']) - - volume = dict.copy(self.volume) - old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) - volume['volume_type'] = old_type - volume['host'] = host - new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.driver.retype(ctxt, volume, new_type, diff, host) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_retype_with_only_LH_extra_specs(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - ctxt = context.get_admin_context() - - host = {'host': self.serverName} - key_specs_old = {'hpelh:provisioning': 'thin'} - key_specs_new = {'hpelh:provisioning': 'full', 'hpelh:ao': 'true'} - old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) - new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) - - diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], - new_type_ref['id']) - - volume = dict.copy(self.volume) - old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) - volume['volume_type'] = old_type - volume['host'] = host - new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.driver.retype(ctxt, volume, new_type, diff, host) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.modifyVolume( - 1, { - 'isThinProvisioned': False, - 'isAdaptiveOptimizationEnabled': True, - 'dataProtectionLevel': 0}), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_retype_with_both_extra_specs(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - ctxt = context.get_admin_context() - - host = {'host': self.serverName} - key_specs_old = {'hpelh:provisioning': 'full', 'foo': 'bar'} - key_specs_new = {'hpelh:provisioning': 'thin', 'foo': 'foobar'} - old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) - new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) - - diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], - new_type_ref['id']) - - volume = dict.copy(self.volume) - old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) - volume['volume_type'] = old_type - volume['host'] = host - new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.driver.retype(ctxt, volume, new_type, diff, host) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.modifyVolume(1, {'isThinProvisioned': True, - 'dataProtectionLevel': 0, - 'isAdaptiveOptimizationEnabled': - True}), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_retype_same_extra_specs(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - ctxt = context.get_admin_context() - - host = {'host': self.serverName} - key_specs_old = {'hpelh:provisioning': 'full', 'hpelh:ao': 'true'} - key_specs_new = {'hpelh:provisioning': 'full', 'hpelh:ao': 'false'} - old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) - new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) - - diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], - new_type_ref['id']) - - volume = dict.copy(self.volume) - old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) - volume['volume_type'] = old_type - volume['host'] = host - new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.driver.retype(ctxt, volume, new_type, diff, host) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.modifyVolume( - 1, - {'isAdaptiveOptimizationEnabled': False, - 'dataProtectionLevel': 0}), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_retype_with_default_extra_spec(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - ctxt = context.get_admin_context() - - host = {'host': self.serverName} - key_specs_old = {'hpelh:provisioning': 'full', 'hpelh:ao': 'false'} - key_specs_new = {} - old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) - new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) - - diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], - new_type_ref['id']) - - volume = dict.copy(self.volume) - old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) - volume['volume_type'] = old_type - volume['host'] = host - new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.driver.retype(ctxt, volume, new_type, diff, host) - - expected = self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.modifyVolume( - 1, - {'isAdaptiveOptimizationEnabled': True, - 'isThinProvisioned': True, - 'dataProtectionLevel': 0}), - mock.call.logout()] - - # validate call chain - mock_client.assert_has_calls(expected) - - def test_migrate_no_location(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - host = {'host': self.serverName, 'capabilities': {}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume( - None, - self.volume, - host) - self.assertFalse(migrated) - - mock_client.assert_has_calls([]) - self.assertEqual(0, len(mock_client.method_calls)) - - def test_migrate_incorrect_vip(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getClusterByName.return_value = { - "virtualIPAddresses": [{ - "ipV4Address": "10.10.10.10", - "ipV4NetMask": "255.255.240.0"}], - "id": self.cluster_id} - - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - location = (self.driver.DRIVER_LOCATION % { - 'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - - host = { - 'host': self.serverName, - 'capabilities': {'location_info': location}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume( - None, - self.volume, - host) - self.assertFalse(migrated) - - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - # and nothing else - self.assertEqual( - len(expected), - len(mock_client.method_calls)) - - def test_migrate_with_location(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getClusterByName.return_value = { - "virtualIPAddresses": [{ - "ipV4Address": "10.10.10.111", - "ipV4NetMask": "255.255.240.0"}], - "id": self.cluster_id} - - mock_client.getVolumeByName.return_value = {'id': self.volume_id, - 'iscsiSessions': None} - mock_client.getVolume.return_value = {'snapshots': { - 'resource': None}} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - location = (self.driver.DRIVER_LOCATION % { - 'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - - host = { - 'host': self.serverName, - 'capabilities': {'location_info': location}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume( - None, - self.volume, - host) - self.assertTrue(migrated) - - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] + self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.getVolume( - 1, - 'fields=snapshots,snapshots[resource[members[name]]]'), - mock.call.modifyVolume(1, {'clusterName': 'New_CloudCluster'}), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - # and nothing else - self.assertEqual( - len(expected), - len(mock_client.method_calls)) - - def test_migrate_with_Snapshots(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getClusterByName.return_value = { - "virtualIPAddresses": [{ - "ipV4Address": "10.10.10.111", - "ipV4NetMask": "255.255.240.0"}], - "id": self.cluster_id} - - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': None} - mock_client.getVolume.return_value = {'snapshots': { - 'resource': 'snapfoo'}} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - location = (self.driver.DRIVER_LOCATION % { - 'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - - host = { - 'host': self.serverName, - 'capabilities': {'location_info': location}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume( - None, - self.volume, - host) - self.assertFalse(migrated) - - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] + self.driver_startup_call_stack + [ - mock.call.getVolumeByName('fakevolume'), - mock.call.getVolume( - 1, - 'fields=snapshots,snapshots[resource[members[name]]]'), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - # and nothing else - self.assertEqual( - len(expected), - len(mock_client.method_calls)) - - def test_migrate_volume_error_diff_backends(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - fake_driver_loc = 'FakeDriver %(cluster)s %(vip)s' - location = (fake_driver_loc % {'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - host = {'host': self.serverName, - 'capabilities': {'location_info': location}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume(None, - self.volume, - host) - self.assertFalse(migrated) - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - def test_migrate_volume_error_diff_management_group(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - location = (self.driver.DRIVER_LOCATION % {'cluster': 'FakeCluster', - 'vip': '10.10.10.112'}) - host = {'host': self.serverName, - 'capabilities': {'location_info': location}} - mock_client.getClusterByName.return_value = { - 'id': self.cluster_id, - 'virtualIPAddresses': [{'ipV4Address': '10.0.1.6', - 'ipV4NetMask': '255.255.240.0'}] - } - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume(None, - self.volume, - host) - self.assertFalse(migrated) - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('FakeCluster'), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - def test_migrate_volume_exception_diff_management_group(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - location = (self.driver.DRIVER_LOCATION % { - 'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - host = { - 'host': self.serverName, - 'capabilities': {'location_info': location}} - mock_client.getClusterByName.side_effect = [{ - 'id': self.cluster_id, - 'virtualIPAddresses': [{ - 'ipV4Address': '10.0.1.6', - 'ipV4NetMask': '255.255.240.0'}]}, - hpeexceptions.HTTPNotFound] - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume(None, - self.volume, - host) - self.assertFalse(migrated) - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - def test_migrate_volume_error_exported_volume(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getClusterByName.return_value = { - "virtualIPAddresses": [{ - "ipV4Address": "10.10.10.111", - "ipV4NetMask": "255.255.240.0"}], - "id": self.cluster_id - } - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': [{'server': {'uri': self.server_uri}}] - } - location = (self.driver.DRIVER_LOCATION % { - 'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - host = { - 'host': self.serverName, - 'capabilities': {'location_info': location}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume(None, - self.volume, - host) - self.assertFalse(migrated) - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] + self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume['name']), - mock.call.logout()] - mock_client.assert_has_calls(expected) - - def test_migrate_volume_error_volume_not_exist(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getClusterByName.return_value = { - "virtualIPAddresses": [{ - "ipV4Address": "10.10.10.111", - "ipV4NetMask": "255.255.240.0"}], - "id": self.cluster_id} - - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': None} - mock_client.getVolume.return_value = {'snapshots': { - 'resource': 'snapfoo'}} - - location = (self.driver.DRIVER_LOCATION % { - 'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - host = { - 'host': self.serverName, - 'capabilities': {'location_info': location}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - (migrated, update) = self.driver.migrate_volume( - None, - self.volume, - host) - self.assertFalse(migrated) - - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] + self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume['name']), - mock.call.getVolume( - self.volume['id'], - 'fields=snapshots,snapshots[resource[members[name]]]'), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - def test_migrate_volume_exception(self): - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - mock_client.getClusterByName.return_value = { - "virtualIPAddresses": [{ - "ipV4Address": "10.10.10.111", - "ipV4NetMask": "255.255.240.0"}], - "id": self.cluster_id} - - mock_client.getVolumeByName.return_value = { - 'id': self.volume_id, - 'iscsiSessions': None} - mock_client.getVolume.side_effect = hpeexceptions.HTTPServerError() - - location = (self.driver.DRIVER_LOCATION % { - 'cluster': 'New_CloudCluster', - 'vip': '10.10.10.111'}) - host = { - 'host': self.serverName, - 'capabilities': {'location_info': location}} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - # Testing for any other HTTPServerError - (migrated, update) = self.driver.migrate_volume( - None, - self.volume, - host) - self.assertFalse(migrated) - - expected = self.driver_startup_call_stack + [ - mock.call.getClusterByName('New_CloudCluster'), - mock.call.logout()] + self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume['name']), - mock.call.getVolume( - self.volume['id'], - 'fields=snapshots,snapshots[resource[members[name]]]'), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - def test_update_migrated_volume(self): - mock_client = self.setup_driver() - volume_id = 'fake_vol_id' - clone_id = 'fake_clone_id' - fake_old_volume = {'id': volume_id} - provider_location = 'foo' - fake_new_volume = {'id': clone_id, - '_name_id': clone_id, - 'provider_location': provider_location} - original_volume_status = 'available' - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - actual_update = self.driver.update_migrated_volume( - context.get_admin_context(), fake_old_volume, - fake_new_volume, original_volume_status) - - expected_update = {'_name_id': None, - 'provider_location': None} - self.assertEqual(expected_update, actual_update) - - def test_update_migrated_volume_attached(self): - mock_client = self.setup_driver() - volume_id = 'fake_vol_id' - clone_id = 'fake_clone_id' - fake_old_volume = {'id': volume_id} - provider_location = 'foo' - fake_new_volume = {'id': clone_id, - '_name_id': clone_id, - 'provider_location': provider_location} - original_volume_status = 'in-use' - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - actual_update = self.driver.update_migrated_volume( - context.get_admin_context(), fake_old_volume, - fake_new_volume, original_volume_status) - - expected_update = {'_name_id': fake_new_volume['_name_id'], - 'provider_location': provider_location} - self.assertEqual(expected_update, actual_update) - - def test_update_migrated_volume_failed_to_rename(self): - mock_client = self.setup_driver() - volume_id = 'fake_vol_id' - clone_id = 'fake_clone_id' - fake_old_volume = {'id': volume_id} - provider_location = 'foo' - fake_new_volume = {'id': clone_id, - '_name_id': clone_id, - 'provider_location': provider_location} - original_volume_status = 'available' - # mock HTTPServerError (array failure) - mock_client.modifyVolume.side_effect =\ - hpeexceptions.HTTPServerError() - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - actual_update = self.driver.update_migrated_volume( - context.get_admin_context(), fake_old_volume, - fake_new_volume, original_volume_status) - - expected_update = {'_name_id': clone_id, - 'provider_location': provider_location} - self.assertEqual(expected_update, actual_update) - - @mock.patch.object(volume_types, 'get_volume_type', - return_value={'extra_specs': {'hpelh:ao': 'true'}}) - def test_create_volume_with_ao_true(self, _mock_volume_type): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - volume_with_vt = self.volume - volume_with_vt['volume_type_id'] = 1 - - # mock return value of createVolume - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - volume_info = self.driver.create_volume(volume_with_vt) - - self.assertEqual('10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0', - volume_info['provider_location']) - - # make sure createVolume is called without - # isAdaptiveOptimizationEnabled == true - expected = self.driver_startup_call_stack + [ - mock.call.createVolume( - 'fakevolume', - 1, - units.Gi, - {'isThinProvisioned': True, - 'clusterName': 'CloudCluster1'}), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - @mock.patch.object(volume_types, 'get_volume_type', - return_value={'extra_specs': {'hpelh:ao': 'false'}}) - def test_create_volume_with_ao_false(self, _mock_volume_type): - - # setup driver with default configuration - # and return the mock HTTP LeftHand client - mock_client = self.setup_driver() - - volume_with_vt = self.volume - volume_with_vt['volume_type_id'] = 1 - - # mock return value of createVolume - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - volume_info = self.driver.create_volume(volume_with_vt) - - self.assertEqual('10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0', - volume_info['provider_location']) - - # make sure createVolume is called with - # isAdaptiveOptimizationEnabled == false - expected = self.driver_startup_call_stack + [ - mock.call.createVolume( - 'fakevolume', - 1, - units.Gi, - {'isThinProvisioned': True, - 'clusterName': 'CloudCluster1', - 'isAdaptiveOptimizationEnabled': False}), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - def test_get_existing_volume_ref_name(self): - self.setup_driver() - - existing_ref = {'source-name': self.volume_name} - result = self.driver._get_existing_volume_ref_name( - existing_ref) - self.assertEqual(self.volume_name, result) - - existing_ref = {'bad-key': 'foo'} - self.assertRaises( - exception.ManageExistingInvalidReference, - self.driver._get_existing_volume_ref_name, - existing_ref) - - def test_manage_existing(self): - mock_client = self.setup_driver() - - self.driver.api_version = "1.1" - - volume = {'display_name': 'Foo Volume', - 'volume_type': None, - 'volume_type_id': None, - 'id': '12345'} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": self.volume_id, - "clusterName": self.cluster_name, - "size": 1 - }] - } - - existing_ref = {'source-name': self.volume_name} - - expected_obj = {'display_name': 'Foo Volume'} - - obj = self.driver.manage_existing(volume, existing_ref) - - mock_client.assert_has_calls( - self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume_name), - mock.call.logout()] + - self.driver_startup_call_stack + [ - mock.call.modifyVolume(self.volume_id, - {'name': 'volume-12345'}), - mock.call.logout()]) - self.assertEqual(expected_obj, obj) - - def test_manage_existing_with_non_existing_virtual_volume(self): - mock_client = self.setup_driver() - self.driver.api_version = "1.1" - existing_ref = {'source-name': self.volume_name} - mock_client.getVolumeByName.side_effect = hpeexceptions.HTTPNotFound - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - self.assertRaises(exception.InvalidInput, - self.driver.manage_existing, - self.volume, - existing_ref) - - mock_client.assert_has_calls( - self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume_name), - mock.call.logout()]) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_manage_existing_retype(self, _mock_volume_types): - mock_client = self.setup_driver() - - _mock_volume_types.return_value = { - 'name': 'gold', - 'id': 'gold-id', - 'extra_specs': { - 'hpelh:provisioning': 'thin', - 'hpelh:ao': 'true', - 'hpelh:data_pl': 'r-0', - 'volume_type': self.volume_type}} - - self.driver.api_version = "1.1" - - volume = {'display_name': 'Foo Volume', - 'host': 'stack@lefthand#lefthand', - 'volume_type': 'gold', - 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', - 'id': '12345'} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": self.volume_id, - "clusterName": self.cluster_name, - "size": 1 - }] - } - - existing_ref = {'source-name': self.volume_name} - - expected_obj = {'display_name': 'Foo Volume'} - - obj = self.driver.manage_existing(volume, existing_ref) - - mock_client.assert_has_calls( - self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume_name), - mock.call.logout()] + - self.driver_startup_call_stack + [ - mock.call.modifyVolume(self.volume_id, - {'name': 'volume-12345'}), - mock.call.logout()], - self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume_name), - mock.call.modifyVolume(self.volume_id, - {'isThinProvisioned': True, - 'dataProtectionLevel': 0, - 'isAdaptiveOptimizationEnabled': - True}), - mock.call.logout()]) - self.assertEqual(expected_obj, obj) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_manage_existing_retype_exception(self, _mock_volume_types): - mock_client = self.setup_driver() - - _mock_volume_types.return_value = { - 'name': 'gold', - 'id': 'gold-id', - 'extra_specs': { - 'hpelh:provisioning': 'thin', - 'hpelh:ao': 'true', - 'hpelh:data_pl': 'r-0', - 'volume_type': self.volume_type}} - - self.driver.retype = mock.Mock( - side_effect=exception.VolumeNotFound(volume_id="fake")) - - self.driver.api_version = "1.1" - - volume = {'display_name': 'Foo Volume', - 'host': 'stack@lefthand#lefthand', - 'volume_type': 'gold', - 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', - 'id': '12345'} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": self.volume_id, - "clusterName": self.cluster_name, - "size": 1 - }] - } - - existing_ref = {'source-name': self.volume_name} - - self.assertRaises(exception.VolumeNotFound, - self.driver.manage_existing, - volume, - existing_ref) - - mock_client.assert_has_calls( - self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume_name), - mock.call.logout()] + - self.driver_startup_call_stack + [ - mock.call.modifyVolume(self.volume_id, - {'name': 'volume-12345'}), - mock.call.logout()] + - self.driver_startup_call_stack + [ - mock.call.modifyVolume(self.volume_id, - {'name': 'fakevolume'}), - mock.call.logout()]) - - def test_manage_existing_volume_type_exception(self): - mock_client = self.setup_driver() - - self.driver.api_version = "1.1" - - volume = {'display_name': 'Foo Volume', - 'volume_type': 'gold', - 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', - 'id': '12345'} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": self.volume_id, - "clusterName": self.cluster_name, - "size": 1 - }] - } - - existing_ref = {'source-name': self.volume_name} - - self.assertRaises(exception.ManageExistingVolumeTypeMismatch, - self.driver.manage_existing, - volume=volume, - existing_ref=existing_ref) - - mock_client.assert_has_calls( - self.driver_startup_call_stack + [ - mock.call.getVolumeByName(self.volume_name), - mock.call.logout()]) - - def test_manage_existing_snapshot(self): - mock_client = self.setup_driver() - - self.driver.api_version = "1.1" - - volume = { - 'id': '111', - } - snapshot = { - 'display_name': 'Foo Snap', - 'id': '12345', - 'volume': volume, - 'volume_id': '111', - } - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - mock_client.getSnapshotByName.return_value = { - 'id': self.snapshot_id - } - mock_client.getSnapshotParentVolume.return_value = { - 'name': 'volume-111' - } - - existing_ref = {'source-name': self.snapshot_name} - expected_obj = {'display_name': 'Foo Snap'} - - obj = self.driver.manage_existing_snapshot(snapshot, existing_ref) - - mock_client.assert_has_calls( - self.driver_startup_call_stack + [ - mock.call.getSnapshotByName(self.snapshot_name), - mock.call.getSnapshotParentVolume(self.snapshot_name), - mock.call.modifySnapshot(self.snapshot_id, - {'name': 'snapshot-12345'}), - mock.call.logout()]) - self.assertEqual(expected_obj, obj) - - def test_manage_existing_snapshot_failed_over_volume(self): - mock_client = self.setup_driver() - - self.driver.api_version = "1.1" - - volume = { - 'id': self.volume_id, - 'replication_status': 'failed-over', - } - snapshot = { - 'display_name': 'Foo Snap', - 'id': '12345', - 'volume': volume, - } - existing_ref = {'source-name': self.snapshot_name} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.assertRaises(exception.InvalidInput, - self.driver.manage_existing_snapshot, - snapshot=snapshot, - existing_ref=existing_ref) - - def test_manage_existing_get_size(self): - mock_client = self.setup_driver() - mock_client.getVolumeByName.return_value = {'size': 2147483648} - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": self.volume_id, - "clusterName": self.cluster_name, - "size": 1 - }] - } - - volume = {} - existing_ref = {'source-name': self.volume_name} - - size = self.driver.manage_existing_get_size(volume, existing_ref) - - expected_size = 2 - expected = [mock.call.getVolumeByName(existing_ref['source-name']), - mock.call.logout()] - - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - self.assertEqual(expected_size, size) - - def test_manage_existing_get_size_invalid_reference(self): - mock_client = self.setup_driver() - mock_client.getVolumeByName.return_value = {'size': 2147483648} - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - volume = {} - existing_ref = {'source-name': "volume-12345"} - - self.assertRaises(exception.ManageExistingInvalidReference, - self.driver.manage_existing_get_size, - volume=volume, - existing_ref=existing_ref) - - mock_client.assert_has_calls([]) - - existing_ref = {} - - self.assertRaises(exception.ManageExistingInvalidReference, - self.driver.manage_existing_get_size, - volume=volume, - existing_ref=existing_ref) - - mock_client.assert_has_calls([]) - - def test_manage_existing_get_size_invalid_input(self): - mock_client = self.setup_driver() - mock_client.getVolumeByName.side_effect = ( - hpeexceptions.HTTPNotFound('fake')) - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": self.volume_id, - "clusterName": self.cluster_name, - "size": 1 - }] - } - - volume = {} - existing_ref = {'source-name': self.volume_name} - - self.assertRaises(exception.InvalidInput, - self.driver.manage_existing_get_size, - volume=volume, - existing_ref=existing_ref) - - expected = [mock.call.getVolumeByName(existing_ref['source-name'])] - - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - - def test_manage_existing_snapshot_get_size(self): - mock_client = self.setup_driver() - mock_client.getSnapshotByName.return_value = {'size': 2147483648} - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - snapshot = {} - existing_ref = {'source-name': self.snapshot_name} - - size = self.driver.manage_existing_snapshot_get_size(snapshot, - existing_ref) - - expected_size = 2 - expected = [mock.call.getSnapshotByName( - existing_ref['source-name']), - mock.call.logout()] - - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - self.assertEqual(expected_size, size) - - def test_manage_existing_snapshot_get_size_invalid_reference(self): - mock_client = self.setup_driver() - mock_client.getSnapshotByName.return_value = {'size': 2147483648} - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - snapshot = {} - existing_ref = {'source-name': "snapshot-12345"} - - self.assertRaises(exception.ManageExistingInvalidReference, - self.driver.manage_existing_snapshot_get_size, - snapshot=snapshot, - existing_ref=existing_ref) - - mock_client.assert_has_calls([]) - - existing_ref = {} - - self.assertRaises(exception.ManageExistingInvalidReference, - self.driver.manage_existing_snapshot_get_size, - snapshot=snapshot, - existing_ref=existing_ref) - - mock_client.assert_has_calls([]) - - def test_manage_snapshot(self): - mock_client = self.setup_driver() - self.driver.api_version = "1.1" - existing_ref = {'source-name': self.snapshot_name} - snapshot = {'volume_id': '222'} - snapshot.update(self.snapshot) - snapshot['id'] = '4' - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # Failed to update the existing snapshot with the new name - mock_client.getSnapshotByName.return_value = {'id': '4'} - mock_client.getSnapshotParentVolume.return_value = { - 'name': 'volume-222'} - mock_client.modifySnapshot.side_effect = \ - hpeexceptions.HTTPServerError - expected = [mock.call.getSnapshotByName('fakeshapshot'), - mock.call.getSnapshotParentVolume('fakeshapshot'), - mock.call.modifySnapshot('4', {'name': 'snapshot-4'})] - - self.driver._manage_snapshot( - client=mock_client, - volume=snapshot['volume'], - snapshot=snapshot, - target_snap_name=existing_ref['source-name'], - existing_ref=existing_ref) - - mock_client.assert_has_calls( - expected) - - # The provided snapshot is not a snapshot of the provided volume, - # raises InvalidInput exception - mock_client.getSnapshotParentVolume.return_value = { - 'name': 'volume-111'} - self.assertRaises(exception.InvalidInput, - self.driver._manage_snapshot, - client=mock_client, - volume=snapshot['volume'], - snapshot=snapshot, - target_snap_name=existing_ref['source-name'], - existing_ref=existing_ref) - - # Non existence of parent volume of a snapshot - # raises HTTPNotFound exception - mock_client.getSnapshotParentVolume.side_effect =\ - hpeexceptions.HTTPNotFound - self.assertRaises(exception.InvalidInput, - self.driver._manage_snapshot, - client=mock_client, - volume=snapshot['volume'], - snapshot=snapshot, - target_snap_name=existing_ref['source-name'], - existing_ref=existing_ref) - - # Non existence of a snapshot raises HTTPNotFound exception - mock_client.getSnapshotByName.side_effect =\ - hpeexceptions.HTTPNotFound - self.assertRaises(exception.InvalidInput, - self.driver._manage_snapshot, - client=mock_client, - volume=snapshot['volume'], - snapshot=snapshot, - target_snap_name=existing_ref['source-name'], - existing_ref=existing_ref) - - def test_manage_existing_snapshot_get_size_invalid_input(self): - mock_client = self.setup_driver() - mock_client.getSnapshotByName.side_effect = ( - hpeexceptions.HTTPNotFound('fake')) - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - snapshot = {} - existing_ref = {'source-name': self.snapshot_name} - - self.assertRaises(exception.InvalidInput, - self.driver.manage_existing_snapshot_get_size, - snapshot=snapshot, - existing_ref=existing_ref) - - expected = [mock.call.getSnapshotByName( - existing_ref['source-name'])] - - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - - def test_unmanage(self): - mock_client = self.setup_driver() - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - - # mock return value of getVolumes - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": self.volume_id, - "clusterName": self.cluster_name, - "size": 1 - }] - } - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - self.driver.unmanage(self.volume) - - new_name = 'unm-' + str(self.volume['id']) - - expected = [ - mock.call.getVolumeByName(self.volume['name']), - mock.call.modifyVolume(self.volume['id'], {'name': new_name}), - mock.call.logout() - ] - - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - - def test_unmanage_snapshot(self): - mock_client = self.setup_driver() - volume = { - 'id': self.volume_id, - } - snapshot = { - 'name': self.snapshot_name, - 'display_name': 'Foo Snap', - 'volume': volume, - 'id': self.snapshot_id, - } - mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id, } - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - self.driver.unmanage_snapshot(snapshot) - - new_name = 'ums-' + str(self.snapshot_id) - - expected = [ - mock.call.getSnapshotByName(snapshot['name']), - mock.call.modifySnapshot(self.snapshot_id, {'name': new_name}), - mock.call.logout() - ] - - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - - def test_unmanage_snapshot_failed_over_volume(self): - mock_client = self.setup_driver() - volume = { - 'id': self.volume_id, - 'replication_status': 'failed-over', - } - snapshot = { - 'name': self.snapshot_name, - 'display_name': 'Foo Snap', - 'volume': volume, - 'id': self.snapshot_id, - } - mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id, } - - self.driver.api_version = "1.1" - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - self.assertRaises(exception.SnapshotIsBusy, - self.driver.unmanage_snapshot, - snapshot=snapshot) - - def test_api_version(self): - self.setup_driver() - self.driver.api_version = "1.1" - self.driver._check_api_version() - - self.driver.api_version = "1.0" - self.assertRaises(exception.InvalidInput, - self.driver._check_api_version) - - def test_get_volume_stats(self): - - # set up driver with default config - mock_client = self.setup_driver() - - # mock return value of getVolumes - mock_client.getVolumes.return_value = { - "type": "volume", - "total": 1, - "members": [{ - "id": 12345, - "clusterName": self.cluster_name, - "size": 1 * units.Gi - }] - } - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # execute driver - stats = self.driver.get_volume_stats(True) - - self.assertEqual('iSCSI', stats['storage_protocol']) - self.assertEqual(GOODNESS_FUNCTION, stats['goodness_function']) - self.assertEqual(FILTER_FUNCTION, stats['filter_function']) - self.assertEqual(1, int(stats['total_volumes'])) - self.assertTrue(stats['thin_provisioning_support']) - self.assertTrue(stats['thick_provisioning_support']) - self.assertEqual(1, int(stats['provisioned_capacity_gb'])) - self.assertEqual(25, int(stats['reserved_percentage'])) - - cap_util = ( - float(units.Gi * 500 - units.Gi * 250) / float(units.Gi * 500) - ) * 100 - - self.assertEqual(cap_util, float(stats['capacity_utilization'])) - - expected = self.driver_startup_call_stack + [ - mock.call.getCluster(1), - mock.call.getVolumes(fields=['members[id]', - 'members[clusterName]', - 'members[size]'], - cluster=self.cluster_name), - mock.call.logout()] - - mock_client.assert_has_calls(expected) - - @mock.patch.object(volume_types, 'get_volume_type') - @mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type') - def test_create_group(self, cg_ss_enabled, mock_get_volume_type): - cg_ss_enabled.side_effect = [False, True, True] - ctxt = context.get_admin_context() - mock_get_volume_type.return_value = { - 'name': 'gold', - 'extra_specs': { - 'replication_enabled': ' False'}} - # set up driver with default config - mock_client = self.setup_driver() - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # create a group object - group = self.fake_group_object() - - # create a group with consistent_group_snapshot_enabled flag to - # False - self.assertRaises(NotImplementedError, - self.driver.create_group, ctxt, group) - - # create a group with consistent_group_snapshot_enabled flag to - # True - model_update = self.driver.create_group(ctxt, group) - - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status']) - - # create a group with replication enabled on volume type - mock_get_volume_type.return_value = { - 'name': 'gold', - 'extra_specs': { - 'replication_enabled': ' True'}} - model_update = self.driver.create_group(ctxt, group) - self.assertEqual(fields.GroupStatus.ERROR, - model_update['status']) - - @mock.patch.object(volume_types, 'get_volume_type') - @mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type') - def test_delete_group(self, cg_ss_enabled, mock_get_volume_type): - cg_ss_enabled.return_value = True - ctxt = context.get_admin_context() - mock_get_volume_type.return_value = { - 'name': 'gold', - 'extra_specs': { - 'replication_enabled': ' False'}} - # set up driver with default config - mock_client = self.setup_driver() - - mock_volume = mock.MagicMock() - volumes = [mock_volume] - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # create a group - group = self.fake_group_object() - model_update = self.driver.create_group(ctxt, group) - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status']) - - # delete the group - group.status = fields.GroupStatus.DELETING - model_update, vols = self.driver.delete_group(ctxt, group, - volumes) - self.assertEqual(fields.GroupStatus.DELETING, - model_update['status']) - - @mock.patch.object(volume_types, 'get_volume_type') - @mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type') - def test_update_group_add_vol_delete_group(self, cg_ss_enabled, - mock_get_volume_type): - cg_ss_enabled.return_value = True - ctxt = context.get_admin_context() - mock_get_volume_type.return_value = { - 'name': 'gold', - 'extra_specs': { - 'replication_enabled': ' False'}} - - # set up driver with default config - mock_client = self.setup_driver() - - mock_volume = mock.MagicMock() - volumes = [mock_volume] - - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - # mock return value of createVolume - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # create a group - group = self.fake_group_object() - model_update = self.driver.create_group(ctxt, group) - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status']) - - # add volume to group - model_update = self.driver.update_group( - ctxt, group, add_volumes=[self.volume], remove_volumes=None) - - # delete the group - group.status = fields.GroupStatus.DELETING - model_update, vols = self.driver.delete_group(ctxt, group, - volumes) - self.assertEqual(fields.GroupStatus.DELETING, - model_update['status']) - - @mock.patch.object(volume_types, 'get_volume_type') - @mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type') - def test_update_group_remove_vol_delete_group(self, cg_ss_enabled, - mock_get_volume_type): - cg_ss_enabled.return_value = True - ctxt = context.get_admin_context() - mock_get_volume_type.return_value = { - 'name': 'gold', - 'extra_specs': { - 'replication_enabled': ' False'}} - # set up driver with default config - mock_client = self.setup_driver() - - mock_volume = mock.MagicMock() - volumes = [mock_volume] - - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - - # mock return value of createVolume - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # create a group - group = self.fake_group_object() - model_update = self.driver.create_group(ctxt, group) - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status']) - - # add volume to group - model_update = self.driver.update_group( - ctxt, group, add_volumes=[self.volume], remove_volumes=None) - - # remove volume from group - model_update = self.driver.update_group( - ctxt, group, add_volumes=None, remove_volumes=[self.volume]) - - # delete the group - group.status = fields.GroupStatus.DELETING - model_update, vols = self.driver.delete_group(ctxt, group, - volumes) - self.assertEqual(fields.GroupStatus.DELETING, - model_update['status']) - - @mock.patch.object(volume_types, 'get_volume_type') - @mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type') - def test_create_groupsnapshot(self, cg_ss_enabled, mock_get_volume_type): - cg_ss_enabled.return_value = True - ctxt = context.get_admin_context() - mock_get_volume_type.return_value = { - 'name': 'gold', - 'extra_specs': { - 'replication_enabled': ' False'}} - # set up driver with default config - mock_client = self.setup_driver() - - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - - mock_snap = mock.MagicMock() - mock_snap.volumeName = self.volume_name - expected_snaps = [mock_snap] - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # create a group - group = self.fake_group_object() - model_update = self.driver.create_group(ctxt, group) - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status']) - - # create volume and add it to the group - self.driver.update_group( - ctxt, group, add_volumes=[self.volume], remove_volumes=None) - - # create the group snapshot - groupsnapshot = self.fake_groupsnapshot_object() - madel_update, snaps = self.driver.create_group_snapshot( - ctxt, groupsnapshot, expected_snaps) - self.assertEqual('available', madel_update['status']) - - # mock HTTPServerError (array failure) - mock_client.createSnapshotSet.side_effect = ( - hpeexceptions.HTTPServerError()) - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.create_group_snapshot, - ctxt, groupsnapshot, expected_snaps) - - # mock HTTPServerError (array failure) - mock_client.getVolumeByName.side_effect = ( - hpeexceptions.HTTPNotFound()) - # ensure the raised exception is a cinder exception - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.create_group_snapshot, - ctxt, groupsnapshot, expected_snaps) - - @mock.patch.object(volume_types, 'get_volume_type') - @mock.patch('cinder.volume.volume_utils.is_group_a_cg_snapshot_type') - def test_delete_groupsnapshot(self, cg_ss_enabled, mock_get_volume_type): - cg_ss_enabled.return_value = True - ctxt = context.get_admin_context() - mock_get_volume_type.return_value = { - 'name': 'gold', - 'extra_specs': { - 'replication_enabled': ' False'}} - # set up driver with default config - mock_client = self.setup_driver() - - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - - mock_snap = mock.MagicMock() - mock_snap.volumeName = self.volume_name - expected_snaps = [mock_snap] - - with mock.patch.object(hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup: - mock_do_setup.return_value = mock_client - - # create a group - group = self.fake_group_object() - model_update = self.driver.create_group(ctxt, group) - self.assertEqual(fields.GroupStatus.AVAILABLE, - model_update['status']) - - # create volume and add it to the group - self.driver.update_group( - ctxt, group, add_volumes=[self.volume], remove_volumes=None) - - # delete the group snapshot - groupsnapshot = self.fake_groupsnapshot_object() - groupsnapshot.status = 'deleting' - model_update, snaps = self.driver.delete_group_snapshot( - ctxt, groupsnapshot, expected_snaps) - self.assertEqual('deleting', model_update['status']) - - # mock HTTPServerError - ex = hpeexceptions.HTTPServerError({ - 'message': - 'Hey, dude cannot be deleted because it is a clone point' - ' duh.'}) - mock_client.getSnapshotByName.side_effect = ex - # ensure the raised exception is a cinder exception - cgsnap, snaps = self.driver.delete_group_snapshot( - ctxt, groupsnapshot, expected_snaps) - self.assertEqual('error', snaps[0]['status']) - - # mock HTTP other errors - ex = hpeexceptions.HTTPConflict({'message': 'Some message.'}) - mock_client.getSnapshotByName.side_effect = ex - # ensure the raised exception is a cinder exception - cgsnap, snaps = self.driver.delete_group_snapshot( - ctxt, groupsnapshot, expected_snaps) - self.assertEqual('error', snaps[0]['status']) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_create_volume_replicated(self, _mock_get_volume_type): - # set up driver with default config - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets_unmgd - mock_client = self.setup_driver(config=conf) - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.doesRemoteSnapshotScheduleExist.return_value = False - mock_replicated_client = self.setup_driver(config=conf) - - _mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - return_model = self.driver.create_volume(self.volume_replicated) - - expected = [ - mock.call.createVolume( - 'fakevolume_replicated', - 1, - units.Gi, - {'isThinProvisioned': True, - 'clusterName': 'CloudCluster1'}), - mock.call.doesRemoteSnapshotScheduleExist( - 'fakevolume_replicated_SCHED_Pri'), - mock.call.createRemoteSnapshotSchedule( - 'fakevolume_replicated', - 'fakevolume_replicated_SCHED', - 1800, - '1970-01-01T00:00:00Z', - 5, - 'CloudCluster1', - 5, - 'fakevolume_replicated', - '1.1.1.1', - 'foo1', - 'bar2'), - mock.call.logout()] - - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - prov_location = '10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0' - rep_data = json.dumps({"location": HPELEFTHAND_API_URL}) - self.assertEqual({'replication_status': 'enabled', - 'replication_driver_data': rep_data, - 'provider_location': prov_location}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_delete_volume_replicated(self, _mock_get_volume_type): - # set up driver with default config - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets - mock_client = self.setup_driver(config=conf) - mock_client.getVolumeByName.return_value = {'id': self.volume_id} - mock_client.getVolumes.return_value = {'total': 1, 'members': []} - mock_replicated_client = self.setup_driver(config=conf) - - _mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - self.driver.delete_volume(self.volume_replicated) - - expected = [ - mock.call.deleteRemoteSnapshotSchedule( - 'fakevolume_replicated_SCHED'), - mock.call.getVolumeByName('fakevolume_replicated'), - mock.call.deleteVolume(1)] - mock_client.assert_has_calls( - self.driver_startup_call_stack + - expected) - - # mock HTTPNotFound (volume not found) - mock_client.getVolumeByName.side_effect = ( - hpeexceptions.HTTPNotFound()) - # no exception should escape method - self.driver.delete_volume(self.volume_replicated) - - # mock HTTPNotFound (remote snapshot not found) - mock_client.deleteRemoteSnapshotSchedule.side_effect = ( - hpeexceptions.HTTPNotFound()) - # no exception should escape method - self.driver.delete_volume(self.volume_replicated) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_failover_host(self, _mock_get_volume_type): - ctxt = context.get_admin_context() - # set up driver with default config - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets - mock_client = self.setup_driver(config=conf) - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getVolumeByName.return_value = { - 'iscsiIqn': self.connector['initiator']} - - _mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - invalid_backend_id = 'INVALID' - - # Test invalid secondary target. - self.assertRaises( - exception.InvalidReplicationTarget, - self.driver.failover_host, - ctxt, - [self.volume_replicated], - invalid_backend_id) - - # Test a successful failover. - return_model = self.driver.failover_host( - context.get_admin_context(), - [self.volume_replicated], - REPLICATION_BACKEND_ID) - prov_location = '10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0' - expected_model = (REPLICATION_BACKEND_ID, - [{'updates': {'replication_status': - 'failed-over', - 'provider_location': - prov_location}, - 'volume_id': 1}], - []) - self.assertEqual(expected_model, return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_failover_host_exceptions(self, _mock_get_volume_type): - - # set up driver with default config - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets - mock_client = self.setup_driver(config=conf) - mock_replicated_client = self.setup_driver(config=conf) - - _mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - # mock HTTPNotFound - client - mock_client.stopRemoteSnapshotSchedule.side_effect = ( - hpeexceptions.HTTPNotFound()) - self.driver.failover_host( - context.get_admin_context(), - [self.volume_replicated], - REPLICATION_BACKEND_ID) - - # mock HTTPNotFound - replicated client - mock_replicated_client.stopRemoteSnapshotSchedule.side_effect = ( - hpeexceptions.HTTPNotFound()) - self.driver.failover_host( - context.get_admin_context(), - [self.volume_replicated], - REPLICATION_BACKEND_ID) - - # mock HTTPNotFound - replicated client - mock_replicated_client.getVolumeByName.side_effect = ( - hpeexceptions.HTTPNotFound()) - self.driver.failover_host( - context.get_admin_context(), - [self.volume_replicated], - REPLICATION_BACKEND_ID) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_failback_host_ready(self, _mock_get_volume_type): - # set up driver with default config - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets_unmgd - mock_client = self.setup_driver(config=conf) - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getVolumeByName.return_value = { - 'iscsiIqn': self.connector['initiator'], - 'isPrimary': True} - mock_replicated_client.getRemoteSnapshotSchedule.return_value = ( - ['', - 'HP StoreVirtual LeftHand OS Command Line Interface', - '(C) Copyright 2007-2016', - '', - 'RESPONSE', - ' result 0', - ' period 1800', - ' paused false']) - - _mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - volume = self.volume_replicated.copy() - rep_data = json.dumps({"primary_config_group": "failover_group"}) - volume['replication_driver_data'] = rep_data - return_model = self.driver.failover_host( - context.get_admin_context(), - [volume], - 'default') - prov_location = '10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0' - expected_model = (None, - [{'updates': {'replication_status': - 'available', - 'provider_location': - prov_location}, - 'volume_id': 1}], - []) - self.assertEqual(expected_model, return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_failback_host_not_ready(self, - _mock_get_volume_type): - # set up driver with default config - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets_unmgd - mock_client = self.setup_driver(config=conf) - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getVolumeByName.return_value = { - 'iscsiIqn': self.connector['initiator'], - 'isPrimary': False} - mock_replicated_client.getRemoteSnapshotSchedule.return_value = ( - ['', - 'HP StoreVirtual LeftHand OS Command Line Interface', - '(C) Copyright 2007-2016', - '', - 'RESPONSE', - ' result 0', - ' period 1800', - ' paused true']) - - _mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True'}} - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - volume = self.volume_replicated.copy() - self.assertRaises( - exception.InvalidReplicationTarget, - self.driver.failover_host, - context.get_admin_context(), - [volume], - 'default') - - @mock.patch.object(volume_types, 'get_volume_type') - def test_do_volume_replication_setup(self, mock_get_volume_type): - # set up driver with default config - conf = self.default_mock_conf() - conf.replication_device = self.repl_targets_unmgd - mock_client = self.setup_driver(config=conf) - volume = self.volume - volume['volume_type_id'] = 4 - mock_client.createVolume.return_value = { - 'iscsiIqn': self.connector['initiator']} - mock_client.doesRemoteSnapshotScheduleExist.return_value = False - mock_replicated_client = self.setup_driver(config=conf) - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - client = self.driver._login() - # failed to create remote snapshot schedule on the primary system - mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': {'replication_enabled': ' True'}} - mock_client.createRemoteSnapshotSchedule.side_effect =\ - hpeexceptions.HTTPServerError() - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver._do_volume_replication_setup, - volume, - client) - # remote retention count of a volume greater than max retention - # count raises an exception - mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True', - 'replication:remote_retention_count': '52'}} - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver._do_volume_replication_setup, - volume, - client) - # retention count of a volume greater than max retention - # count raises an exception - mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True', - 'replication:retention_count': '52'}} - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver._do_volume_replication_setup, - volume, - client) - # sync period of a volume less than minimum sync period - # raises an exception - mock_get_volume_type.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True', - 'replication:sync_period': '1500'}} - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver._do_volume_replication_setup, - volume, - client) - - def test_do_setup_with_incorrect_replication_device_information(self): - conf = self.default_mock_conf() - repl_target = copy.deepcopy(self.repl_targets) - # delete left hand password so that replication device - # information become incorrect - del repl_target[0]['hpelefthand_password'] - conf.replication_device = repl_target - mock_client = self.setup_driver(config=conf) - mock_replicated_client = self.setup_driver(config=conf) - - with mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_client') as mock_do_setup, \ - mock.patch.object( - hpe_lefthand_iscsi.HPELeftHandISCSIDriver, - '_create_replication_client') as mock_replication_client: - mock_do_setup.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - self.driver.do_setup(None) - self.assertListEqual([], self.driver._replication_targets) - - def test__create_replication_client(self): - # set up driver with default config - self.setup_driver() - - # Ensure creating a replication client works without specifying - # ssh_conn_timeout or san_private_key. - remote_array = { - 'hpelefthand_api_url': 'https://1.1.1.1:8080/lhos', - 'hpelefthand_username': 'user', - 'hpelefthand_password': 'password', - 'hpelefthand_ssh_port': '16022'} - cl = self.driver._create_replication_client(remote_array) - cl.setSSHOptions.assert_called_with( - '1.1.1.1', - 'user', - 'password', - conn_timeout=30, - known_hosts_file=mock.ANY, - missing_key_policy='AutoAddPolicy', - port='16022', - privatekey='') - - # Verify we can create a replication client with custom values for - # ssh_conn_timeout and san_private_key. - cl.reset_mock() - remote_array['ssh_conn_timeout'] = 45 - remote_array['san_private_key'] = 'foobarkey' - cl = self.driver._create_replication_client(remote_array) - cl.setSSHOptions.assert_called_with( - '1.1.1.1', - 'user', - 'password', - conn_timeout=45, - known_hosts_file=mock.ANY, - missing_key_policy='AutoAddPolicy', - port='16022', - privatekey='foobarkey') - - # mock HTTPNotFound - cl.login.side_effect = hpeexceptions.HTTPNotFound() - # ensure the raised exception is a cinder exception - self.assertRaises(exception.DriverNotInitialized, - self.driver._create_replication_client, remote_array) - - # mock other HTTP error - cl.login.side_effect = hpeexceptions.HTTPServerError() - # ensure the raised exception is a cinder exception - self.assertRaises(exception.DriverNotInitialized, - self.driver._create_replication_client, remote_array) diff --git a/cinder/volume/drivers/hpe/hpe_lefthand_iscsi.py b/cinder/volume/drivers/hpe/hpe_lefthand_iscsi.py deleted file mode 100644 index dae50f58b97..00000000000 --- a/cinder/volume/drivers/hpe/hpe_lefthand_iscsi.py +++ /dev/null @@ -1,2047 +0,0 @@ -# (c) Copyright 2014-2016 Hewlett Packard Enterprise Development LP -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# -"""HPE LeftHand SAN ISCSI REST Proxy. - -Volume driver for HPE LeftHand Storage array. -This driver requires 11.5 or greater firmware on the LeftHand array, using -the 2.0 or greater version of the hpelefthandclient. - -You will need to install the python hpelefthandclient module. -sudo pip install python-lefthandclient - -Set the following in the cinder.conf file to enable the -LeftHand iSCSI REST Driver along with the required flags: - -volume_driver=cinder.volume.drivers.hpe.hpe_lefthand_iscsi. - HPELeftHandISCSIDriver - -It also requires the setting of hpelefthand_api_url, hpelefthand_username, -hpelefthand_password for credentials to talk to the REST service on the -LeftHand array. - -""" - -import math -import re - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_serialization import jsonutils as json -from oslo_utils import excutils -from oslo_utils import importutils -from oslo_utils import units -import six - -from cinder import context -from cinder import coordination -from cinder import exception -from cinder.i18n import _ -from cinder import interface -from cinder.objects import fields -from cinder import utils as cinder_utils -from cinder.volume import configuration -from cinder.volume import driver -from cinder.volume.drivers.san import san -from cinder.volume import volume_types -from cinder.volume import volume_utils - -LOG = logging.getLogger(__name__) - -hpelefthandclient = importutils.try_import("hpelefthandclient") -if hpelefthandclient: - from hpelefthandclient import client as hpe_lh_client - from hpelefthandclient import exceptions as hpeexceptions - -hpelefthand_opts = [ - cfg.URIOpt('hpelefthand_api_url', - default=None, - help="HPE LeftHand WSAPI Server Url like " - "https://:8081/lhos"), - cfg.StrOpt('hpelefthand_username', - default=None, - help="HPE LeftHand Super user username"), - cfg.StrOpt('hpelefthand_password', - default=None, - help="HPE LeftHand Super user password", - secret=True), - cfg.StrOpt('hpelefthand_clustername', - default=None, - help="HPE LeftHand cluster name"), - cfg.BoolOpt('hpelefthand_iscsi_chap_enabled', - default=False, - help='Configure CHAP authentication for iSCSI connections ' - '(Default: Disabled)'), - cfg.BoolOpt('hpelefthand_debug', - default=False, - help="Enable HTTP debugging to LeftHand"), - cfg.PortOpt('hpelefthand_ssh_port', - default=16022, - help="Port number of SSH service."), - -] - -CONF = cfg.CONF -CONF.register_opts(hpelefthand_opts, group=configuration.SHARED_CONF_GROUP) - -MIN_API_VERSION = "1.1" -MIN_CLIENT_VERSION = '2.1.0' - -# map the extra spec key to the REST client option key -extra_specs_key_map = { - 'hpelh:provisioning': 'isThinProvisioned', - 'hpelh:ao': 'isAdaptiveOptimizationEnabled', - 'hpelh:data_pl': 'dataProtectionLevel', - 'hplh:provisioning': 'isThinProvisioned', - 'hplh:ao': 'isAdaptiveOptimizationEnabled', - 'hplh:data_pl': 'dataProtectionLevel', -} - -# map the extra spec value to the REST client option value -extra_specs_value_map = { - 'isThinProvisioned': {'thin': True, 'full': False}, - 'isAdaptiveOptimizationEnabled': {'true': True, 'false': False}, - 'dataProtectionLevel': { - 'r-0': 0, 'r-5': 1, 'r-10-2': 2, 'r-10-3': 3, 'r-10-4': 4, 'r-6': 5} -} - -extra_specs_default_key_value_map = { - 'hpelh:provisioning': 'thin', - 'hpelh:ao': 'true', - 'hpelh:data_pl': 'r-0' -} - - -@interface.volumedriver -class HPELeftHandISCSIDriver(driver.ISCSIDriver): - """Executes REST commands relating to HPE/LeftHand SAN ISCSI volumes. - - Version history: - - .. code-block:: none - - 1.0.0 - Initial REST iSCSI proxy - 1.0.1 - Added support for retype - 1.0.2 - Added support for volume migrate - 1.0.3 - Fixed bug #1285829, HP LeftHand backend assisted migration - should check for snapshots - 1.0.4 - Fixed bug #1285925, LeftHand AO volume create performance - improvement - 1.0.5 - Fixed bug #1311350, Live-migration of an instance when - attached to a volume was causing an error. - 1.0.6 - Removing locks bug #1395953 - 1.0.7 - Fixed bug #1353137, Server was not removed from the HP - Lefthand backend after the last volume was detached. - 1.0.8 - Fixed bug #1418201, A cloned volume fails to attach. - 1.0.9 - Adding support for manage/unmanage. - 1.0.10 - Add stats for goodness_function and filter_function - 1.0.11 - Add over subscription support - 1.0.12 - Adds consistency group support - 1.0.13 - Added update_migrated_volume #1493546 - 1.0.14 - Removed the old CLIQ based driver - 2.0.0 - Rebranded HP to HPE - 2.0.1 - Remove db access for consistency groups - 2.0.2 - Adds v2 managed replication support - 2.0.3 - Adds v2 unmanaged replication support - 2.0.4 - Add manage/unmanage snapshot support - 2.0.5 - Changed minimum client version to be 2.1.0 - 2.0.6 - Update replication to version 2.1 - 2.0.7 - Fixed bug #1554746, Create clone volume with new size. - 2.0.8 - Add defaults for creating a replication client, bug #1556331 - 2.0.9 - Fix terminate connection on failover - 2.0.10 - Add entry point tracing - 2.0.11 - Fix extend volume if larger than snapshot bug #1560654 - 2.0.12 - add CG capability to generic volume groups. - 2.0.13 - Fix cloning operation related to provisioning, bug #1688243 - 2.0.14 - Fixed bug #1710072, Volume doesn't show expected parameters - after Retype - 2.0.15 - Fixed bug #1710098, Managed volume, does not pick up the extra - specs/capabilities of the selected volume type. - 2.0.16 - Handled concurrent attachment requests. bug #1779654 - """ - - VERSION = "2.0.16" - - CI_WIKI_NAME = "HPE_Storage_CI" - - # TODO(walter-boring) Remove this driver in the 'U' release as it is no - # longer maintained. - SUPPORTED = False - - device_stats = {} - - # v2 replication constants - EXTRA_SPEC_REP_SYNC_PERIOD = "replication:sync_period" - EXTRA_SPEC_REP_RETENTION_COUNT = "replication:retention_count" - EXTRA_SPEC_REP_REMOTE_RETENTION_COUNT = ( - "replication:remote_retention_count") - MIN_REP_SYNC_PERIOD = 1800 - DEFAULT_RETENTION_COUNT = 5 - MAX_RETENTION_COUNT = 50 - DEFAULT_REMOTE_RETENTION_COUNT = 5 - MAX_REMOTE_RETENTION_COUNT = 50 - REP_SNAPSHOT_SUFFIX = "_SS" - REP_SCHEDULE_SUFFIX = "_SCHED" - FAILBACK_VALUE = 'default' - - def __init__(self, *args, **kwargs): - super(HPELeftHandISCSIDriver, self).__init__(*args, **kwargs) - self.configuration.append_config_values(hpelefthand_opts) - self.configuration.append_config_values(san.san_opts) - if not self.configuration.hpelefthand_api_url: - raise exception.NotFound(_("HPELeftHand url not found")) - - # blank is the only invalid character for cluster names - # so we need to use it as a separator - self.DRIVER_LOCATION = self.__class__.__name__ + ' %(cluster)s %(vip)s' - self._client_conf = {} - self._replication_targets = [] - self._replication_enabled = False - self._active_backend_id = kwargs.get('active_backend_id', None) - - @staticmethod - def get_driver_options(): - return hpelefthand_opts - - def _login(self, timeout=None): - conf = self._get_lefthand_config() - if conf: - self._client_conf['hpelefthand_username'] = ( - conf['hpelefthand_username']) - self._client_conf['hpelefthand_password'] = ( - conf['hpelefthand_password']) - self._client_conf['hpelefthand_clustername'] = ( - conf['hpelefthand_clustername']) - self._client_conf['hpelefthand_api_url'] = ( - conf['hpelefthand_api_url']) - self._client_conf['hpelefthand_ssh_port'] = ( - conf['hpelefthand_ssh_port']) - self._client_conf['hpelefthand_iscsi_chap_enabled'] = ( - conf['hpelefthand_iscsi_chap_enabled']) - self._client_conf['ssh_conn_timeout'] = conf['ssh_conn_timeout'] - self._client_conf['san_private_key'] = conf['san_private_key'] - else: - self._client_conf['hpelefthand_username'] = ( - self.configuration.hpelefthand_username) - self._client_conf['hpelefthand_password'] = ( - self.configuration.hpelefthand_password) - self._client_conf['hpelefthand_clustername'] = ( - self.configuration.hpelefthand_clustername) - self._client_conf['hpelefthand_api_url'] = ( - self.configuration.hpelefthand_api_url) - self._client_conf['hpelefthand_ssh_port'] = ( - self.configuration.hpelefthand_ssh_port) - self._client_conf['hpelefthand_iscsi_chap_enabled'] = ( - self.configuration.hpelefthand_iscsi_chap_enabled) - self._client_conf['ssh_conn_timeout'] = ( - self.configuration.ssh_conn_timeout) - self._client_conf['san_private_key'] = ( - self.configuration.san_private_key) - - client = self._create_client(timeout=timeout) - try: - if self.configuration.hpelefthand_debug: - client.debug_rest(True) - - client.login( - self._client_conf['hpelefthand_username'], - self._client_conf['hpelefthand_password']) - - cluster_info = client.getClusterByName( - self._client_conf['hpelefthand_clustername']) - self.cluster_id = cluster_info['id'] - virtual_ips = cluster_info['virtualIPAddresses'] - self.cluster_vip = virtual_ips[0]['ipV4Address'] - - # Extract IP address from API URL - ssh_ip = self._extract_ip_from_url( - self._client_conf['hpelefthand_api_url']) - known_hosts_file = CONF.ssh_hosts_key_file - policy = "AutoAddPolicy" - if CONF.strict_ssh_host_key_policy: - policy = "RejectPolicy" - client.setSSHOptions( - ssh_ip, - self._client_conf['hpelefthand_username'], - self._client_conf['hpelefthand_password'], - port=self._client_conf['hpelefthand_ssh_port'], - conn_timeout=self._client_conf['ssh_conn_timeout'], - privatekey=self._client_conf['san_private_key'], - missing_key_policy=policy, - known_hosts_file=known_hosts_file) - - return client - except hpeexceptions.HTTPNotFound: - raise exception.DriverNotInitialized( - _('LeftHand cluster not found')) - except Exception as ex: - raise exception.DriverNotInitialized(ex) - - def _logout(self, client): - if client is not None: - client.logout() - - def _create_client(self, timeout=None): - # Timeout is only supported in version 2.0.1 and greater of the - # python-lefthandclient. - hpelefthand_api_url = self._client_conf['hpelefthand_api_url'] - client = hpe_lh_client.HPELeftHandClient( - hpelefthand_api_url, timeout=timeout) - return client - - def _create_replication_client(self, remote_array): - cl = hpe_lh_client.HPELeftHandClient( - remote_array['hpelefthand_api_url']) - try: - cl.login( - remote_array['hpelefthand_username'], - remote_array['hpelefthand_password']) - - ssh_conn_timeout = remote_array.get('ssh_conn_timeout', 30) - san_private_key = remote_array.get('san_private_key', '') - - # Extract IP address from API URL - ssh_ip = self._extract_ip_from_url( - remote_array['hpelefthand_api_url']) - known_hosts_file = CONF.ssh_hosts_key_file - policy = "AutoAddPolicy" - if CONF.strict_ssh_host_key_policy: - policy = "RejectPolicy" - cl.setSSHOptions( - ssh_ip, - remote_array['hpelefthand_username'], - remote_array['hpelefthand_password'], - port=remote_array['hpelefthand_ssh_port'], - conn_timeout=ssh_conn_timeout, - privatekey=san_private_key, - missing_key_policy=policy, - known_hosts_file=known_hosts_file) - - return cl - except hpeexceptions.HTTPNotFound: - raise exception.DriverNotInitialized( - _('LeftHand cluster not found')) - except Exception as ex: - raise exception.DriverNotInitialized(ex) - - def _destroy_replication_client(self, client): - if client is not None: - client.logout() - - def _extract_ip_from_url(self, url): - result = re.search("://(.*):", url) - ip = result.group(1) - return ip - - def do_setup(self, context): - """Set up LeftHand client.""" - if not hpelefthandclient: - # Checks if client was successfully imported - ex_msg = _("HPELeftHand client is not installed. Please" - " install using 'pip install " - "python-lefthandclient'.") - LOG.error(ex_msg) - raise exception.VolumeDriverException(ex_msg) - - if hpelefthandclient.version < MIN_CLIENT_VERSION: - ex_msg = (_("Invalid hpelefthandclient version found (" - "%(found)s). Version %(minimum)s or greater " - "required. Run 'pip install --upgrade " - "python-lefthandclient' to upgrade the " - "hpelefthandclient.") - % {'found': hpelefthandclient.version, - 'minimum': MIN_CLIENT_VERSION}) - LOG.error(ex_msg) - raise exception.InvalidInput(reason=ex_msg) - - self._do_replication_setup() - - def check_for_setup_error(self): - """Checks for incorrect LeftHand API being used on backend.""" - client = self._login() - try: - self.api_version = client.getApiVersion() - - LOG.info("HPELeftHand API version %s", self.api_version) - - if self.api_version < MIN_API_VERSION: - LOG.warning("HPELeftHand API is version %(current)s. " - "A minimum version of %(min)s is needed for " - "manage/unmanage support.", - {'current': self.api_version, - 'min': MIN_API_VERSION}) - finally: - self._logout(client) - - def check_replication_flags(self, options, required_flags): - for flag in required_flags: - if not options.get(flag, None): - msg = _('%s is not set and is required for the replication ' - 'device to be valid.') % flag - LOG.error(msg) - raise exception.InvalidInput(reason=msg) - - def get_version_string(self): - return (_('REST %(proxy_ver)s hpelefthandclient %(rest_ver)s') % { - 'proxy_ver': self.VERSION, - 'rest_ver': hpelefthandclient.get_version_string()}) - - @cinder_utils.trace - def create_volume(self, volume): - """Creates a volume.""" - client = self._login() - try: - # get the extra specs of interest from this volume's volume type - volume_extra_specs = self._get_volume_extra_specs(volume) - extra_specs = self._get_lh_extra_specs( - volume_extra_specs, - extra_specs_key_map.keys()) - - # map the extra specs key/value pairs to key/value pairs - # used as optional configuration values by the LeftHand backend - optional = self._map_extra_specs(extra_specs) - - # if provisioning is not set, default to thin - if 'isThinProvisioned' not in optional: - optional['isThinProvisioned'] = True - - # AdaptiveOptimization defaults to 'true' if you don't specify the - # value on a create, and that is the most efficient way to create - # a volume. If you pass in 'false' or 'true' for AO, it will result - # in an update operation following the create operation to set this - # value, so it is best to not specify the value and let it default - # to 'true'. - if optional.get('isAdaptiveOptimizationEnabled'): - del optional['isAdaptiveOptimizationEnabled'] - - clusterName = self._client_conf['hpelefthand_clustername'] - optional['clusterName'] = clusterName - - volume_info = client.createVolume( - volume['name'], self.cluster_id, - volume['size'] * units.Gi, - optional) - - model_update = self._update_provider(volume_info) - - # v2 replication check - if self._volume_of_replicated_type(volume) and ( - self._do_volume_replication_setup(volume, client, optional)): - model_update['replication_status'] = 'enabled' - model_update['replication_driver_data'] = (json.dumps( - {'location': self._client_conf['hpelefthand_api_url']})) - - return model_update - except Exception as ex: - raise exception.VolumeBackendAPIException(data=ex) - finally: - self._logout(client) - - @cinder_utils.trace - def delete_volume(self, volume): - """Deletes a volume.""" - client = self._login() - # v2 replication check - # If the volume type is replication enabled, we want to call our own - # method of deconstructing the volume and its dependencies - if self._volume_of_replicated_type(volume): - self._do_volume_replication_destroy(volume, client) - return - - try: - volume_info = client.getVolumeByName(volume['name']) - client.deleteVolume(volume_info['id']) - except hpeexceptions.HTTPNotFound: - LOG.error("Volume did not exist. It will not be deleted") - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - @cinder_utils.trace - def extend_volume(self, volume, new_size): - """Extend the size of an existing volume.""" - client = self._login() - try: - volume_info = client.getVolumeByName(volume['name']) - - # convert GB to bytes - options = {'size': int(new_size) * units.Gi} - client.modifyVolume(volume_info['id'], options) - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - @cinder_utils.trace - def create_group(self, context, group): - """Creates a group.""" - LOG.debug("Creating group.") - if not volume_utils.is_group_a_cg_snapshot_type(group): - raise NotImplementedError() - for vol_type_id in group.volume_type_ids: - replication_type = self._volume_of_replicated_type( - None, vol_type_id) - if replication_type: - # An unsupported configuration - LOG.error('Unable to create group: create group with ' - 'replication volume type is not supported.') - model_update = {'status': fields.GroupStatus.ERROR} - return model_update - - return {'status': fields.GroupStatus.AVAILABLE} - - @cinder_utils.trace - def create_group_from_src(self, context, group, volumes, - group_snapshot=None, snapshots=None, - source_group=None, source_vols=None): - """Creates a group from a source""" - msg = _("Creating a group from a source is not " - "supported when consistent_group_snapshot_enabled to true.") - if not volume_utils.is_group_a_cg_snapshot_type(group): - raise NotImplementedError() - else: - raise exception.VolumeBackendAPIException(data=msg) - - @cinder_utils.trace - def delete_group(self, context, group, volumes): - """Deletes a group.""" - if not volume_utils.is_group_a_cg_snapshot_type(group): - raise NotImplementedError() - - volume_model_updates = [] - for volume in volumes: - volume_update = {'id': volume.id} - try: - self.delete_volume(volume) - volume_update['status'] = 'deleted' - except Exception as ex: - LOG.error("There was an error deleting volume %(id)s: " - "%(error)s.", - {'id': volume.id, - 'error': ex}) - volume_update['status'] = 'error' - volume_model_updates.append(volume_update) - - model_update = {'status': group.status} - - return model_update, volume_model_updates - - @cinder_utils.trace - def update_group(self, context, group, add_volumes=None, - remove_volumes=None): - """Updates a group. - - Because the backend has no concept of volume grouping, cinder will - maintain all volume/group relationships. Because of this - functionality, there is no need to make any client calls; instead - simply returning out of this function allows cinder to properly - add/remove volumes from the group. - """ - LOG.debug("Updating group.") - if not volume_utils.is_group_a_cg_snapshot_type(group): - raise NotImplementedError() - - return None, None, None - - @cinder_utils.trace - def create_group_snapshot(self, context, group_snapshot, snapshots): - """Creates a group snapshot.""" - if not volume_utils.is_group_a_cg_snapshot_type(group_snapshot): - raise NotImplementedError() - client = self._login() - try: - snap_set = [] - snapshot_base_name = "snapshot-" + group_snapshot.id - snapshot_model_updates = [] - for i, snapshot in enumerate(snapshots): - volume = snapshot.volume - volume_name = volume['name'] - try: - volume_info = client.getVolumeByName(volume_name) - except Exception as ex: - error = six.text_type(ex) - LOG.error("Could not find volume with name %(name)s. " - "Error: %(error)s", - {'name': volume_name, - 'error': error}) - raise exception.VolumeBackendAPIException(data=error) - - volume_id = volume_info['id'] - snapshot_name = snapshot_base_name + "-" + six.text_type(i) - snap_set_member = {'volumeName': volume_name, - 'volumeId': volume_id, - 'snapshotName': snapshot_name} - snap_set.append(snap_set_member) - snapshot_update = {'id': snapshot['id'], - 'status': fields.SnapshotStatus.AVAILABLE} - snapshot_model_updates.append(snapshot_update) - - source_volume_id = snap_set[0]['volumeId'] - optional = {'inheritAccess': True} - description = group_snapshot.description - if description: - optional['description'] = description - - try: - client.createSnapshotSet(source_volume_id, snap_set, optional) - except Exception as ex: - error = six.text_type(ex) - LOG.error("Could not create snapshot set. Error: '%s'", - error) - raise exception.VolumeBackendAPIException( - data=error) - - except Exception as ex: - raise exception.VolumeBackendAPIException(data=six.text_type(ex)) - finally: - self._logout(client) - - model_update = {'status': 'available'} - - return model_update, snapshot_model_updates - - @cinder_utils.trace - def delete_group_snapshot(self, context, group_snapshot, snapshots): - """Deletes a group snapshot.""" - if not volume_utils.is_group_a_cg_snapshot_type(group_snapshot): - raise NotImplementedError() - client = self._login() - snap_name_base = "snapshot-" + group_snapshot.id - - snapshot_model_updates = [] - for i, snapshot in enumerate(snapshots): - snapshot_update = {'id': snapshot['id']} - try: - snap_name = snap_name_base + "-" + six.text_type(i) - snap_info = client.getSnapshotByName(snap_name) - client.deleteSnapshot(snap_info['id']) - snapshot_update['status'] = fields.SnapshotStatus.DELETED - except hpeexceptions.HTTPServerError as ex: - in_use_msg = ('cannot be deleted because it is a clone ' - 'point') - if in_use_msg in ex.get_description(): - LOG.error("The snapshot cannot be deleted because " - "it is a clone point.") - snapshot_update['status'] = fields.SnapshotStatus.ERROR - except Exception as ex: - LOG.error("There was an error deleting snapshot %(id)s: " - "%(error)s.", - {'id': snapshot['id'], - 'error': six.text_type(ex)}) - snapshot_update['status'] = fields.SnapshotStatus.ERROR - snapshot_model_updates.append(snapshot_update) - - self._logout(client) - - model_update = {'status': group_snapshot.status} - - return model_update, snapshot_model_updates - - @cinder_utils.trace - def create_snapshot(self, snapshot): - """Creates a snapshot.""" - client = self._login() - try: - volume_info = client.getVolumeByName(snapshot['volume_name']) - - option = {'inheritAccess': True} - client.createSnapshot(snapshot['name'], - volume_info['id'], - option) - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - @cinder_utils.trace - def delete_snapshot(self, snapshot): - """Deletes a snapshot.""" - client = self._login() - try: - snap_info = client.getSnapshotByName(snapshot['name']) - client.deleteSnapshot(snap_info['id']) - except hpeexceptions.HTTPNotFound: - LOG.error("Snapshot did not exist. It will not be deleted") - except hpeexceptions.HTTPServerError as ex: - in_use_msg = 'cannot be deleted because it is a clone point' - if in_use_msg in ex.get_description(): - raise exception.SnapshotIsBusy(snapshot_name=snapshot['name']) - - raise exception.VolumeBackendAPIException(ex) - - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - @cinder_utils.trace - def get_volume_stats(self, refresh=False): - """Gets volume stats.""" - client = self._login() - try: - if refresh: - self._update_backend_status(client) - - return self.device_stats - finally: - self._logout(client) - - def _update_backend_status(self, client): - data = {} - backend_name = self.configuration.safe_get('volume_backend_name') - data['driver_version'] = self.VERSION - data['volume_backend_name'] = backend_name or self.__class__.__name__ - data['reserved_percentage'] = ( - self.configuration.safe_get('reserved_percentage')) - data['storage_protocol'] = 'iSCSI' - data['vendor_name'] = 'Hewlett Packard Enterprise' - data['location_info'] = (self.DRIVER_LOCATION % { - 'cluster': self._client_conf['hpelefthand_clustername'], - 'vip': self.cluster_vip}) - data['thin_provisioning_support'] = True - data['thick_provisioning_support'] = True - data['max_over_subscription_ratio'] = ( - self.configuration.safe_get('max_over_subscription_ratio')) - - cluster_info = client.getCluster(self.cluster_id) - - total_capacity = cluster_info['spaceTotal'] - free_capacity = cluster_info['spaceAvailable'] - - # convert to GB - data['total_capacity_gb'] = int(total_capacity) / units.Gi - data['free_capacity_gb'] = int(free_capacity) / units.Gi - - # Collect some stats - capacity_utilization = ( - (float(total_capacity - free_capacity) / - float(total_capacity)) * 100) - # Don't have a better way to get the total number volumes - # so try to limit the size of data for now. Once new lefthand API is - # available, replace this call. - total_volumes = 0 - provisioned_size = 0 - volumes = client.getVolumes( - cluster=self._client_conf['hpelefthand_clustername'], - fields=['members[id]', 'members[clusterName]', 'members[size]']) - if volumes: - total_volumes = volumes['total'] - provisioned_size = sum( - members['size'] for members in volumes['members']) - data['provisioned_capacity_gb'] = int(provisioned_size) / units.Gi - data['capacity_utilization'] = capacity_utilization - data['total_volumes'] = total_volumes - data['filter_function'] = self.get_filter_function() - data['goodness_function'] = self.get_goodness_function() - data['consistent_group_snapshot_enabled'] = True - data['replication_enabled'] = self._replication_enabled - data['replication_type'] = ['periodic'] - data['replication_count'] = len(self._replication_targets) - data['replication_targets'] = self._get_replication_targets() - - self.device_stats = data - - @cinder_utils.trace - def initialize_connection(self, volume, connector): - """Assigns the volume to a server. - - Assign any created volume to a compute node/host so that it can be - used from that host. HPE VSA requires a volume to be assigned - to a server. - """ - client = self._login() - try: - server_info = self._create_server(connector, client) - volume_info = client.getVolumeByName(volume['name']) - - access_already_enabled = False - if volume_info['iscsiSessions'] is not None: - # Extract the server id for each session to check if the - # new server already has access permissions enabled. - for session in volume_info['iscsiSessions']: - server_id = int(session['server']['uri'].split('/')[3]) - if server_id == server_info['id']: - access_already_enabled = True - break - - if not access_already_enabled: - client.addServerAccess( - volume_info['id'], - server_info['id']) - - iscsi_properties = self._get_iscsi_properties(volume) - - if ('chapAuthenticationRequired' in server_info and - server_info['chapAuthenticationRequired']): - iscsi_properties['auth_method'] = 'CHAP' - iscsi_properties['auth_username'] = connector['initiator'] - iscsi_properties['auth_password'] = ( - server_info['chapTargetSecret']) - - return {'driver_volume_type': 'iscsi', 'data': iscsi_properties} - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - @cinder_utils.trace - def terminate_connection(self, volume, connector, **kwargs): - """Unassign the volume from the host.""" - client = self._login() - try: - volume_info = client.getVolumeByName(volume['name']) - server_info = client.getServerByName(connector['host']) - volume_list = client.findServerVolumes(server_info['name']) - - removeServer = True - for entry in volume_list: - if entry['id'] != volume_info['id']: - removeServer = False - break - - client.removeServerAccess( - volume_info['id'], - server_info['id']) - - if removeServer: - client.deleteServer(server_info['id']) - except hpeexceptions.HTTPNotFound as ex: - # If a host is failed-over, we want to allow the detach to - # to 'succeed' when it cannot find the host. We can simply - # return out of the terminate connection in order for things - # to be updated correctly. - if self._active_backend_id: - LOG.warning("Because the host is currently in a " - "failed-over state, the volume will not " - "be properly detached from the primary " - "array. The detach will be considered a " - "success as far as Cinder is concerned. " - "The volume can now be attached to the " - "secondary target.") - return - else: - raise exception.VolumeBackendAPIException(ex) - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - @cinder_utils.trace - def create_volume_from_snapshot(self, volume, snapshot): - """Creates a volume from a snapshot.""" - client = self._login() - try: - snap_info = client.getSnapshotByName(snapshot['name']) - volume_info = client.cloneSnapshot( - volume['name'], - snap_info['id']) - - # Extend volume - if volume['size'] > snapshot['volume_size']: - LOG.debug("Resize the new volume to %s.", volume['size']) - self.extend_volume(volume, volume['size']) - - model_update = self._update_provider(volume_info) - - # v2 replication check - if self._volume_of_replicated_type(volume) and ( - self._do_volume_replication_setup(volume, client)): - model_update['replication_status'] = 'enabled' - model_update['replication_driver_data'] = (json.dumps( - {'location': self._client_conf['hpelefthand_api_url']})) - - return model_update - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - @cinder_utils.trace - def create_cloned_volume(self, volume, src_vref): - client = self._login() - try: - volume_info = client.getVolumeByName(src_vref['name']) - clone_info = client.cloneVolume(volume['name'], volume_info['id']) - - # Extend volume - if volume['size'] > src_vref['size']: - LOG.debug("Resize the new volume to %s.", volume['size']) - self.extend_volume(volume, volume['size']) - # TODO(kushal) : we will use volume.volume_types when we re-write - # the design for unit tests to use objects instead of dicts. - # Get the extra specs of interest from this volume's volume type - volume_extra_specs = self._get_volume_extra_specs(src_vref) - extra_specs = self._get_lh_extra_specs( - volume_extra_specs, - extra_specs_key_map.keys()) - - # Check provisioning type of source volume. If it's full then need - # to change provisioning of clone volume to full as lefthand - # creates clone volume only with thin provisioning type. - if extra_specs.get('hpelh:provisioning') == 'full': - options = {'isThinProvisioned': False} - clone_volume_info = client.getVolumeByName(volume['name']) - client.modifyVolume(clone_volume_info['id'], options) - - model_update = self._update_provider(clone_info) - - # v2 replication check - if self._volume_of_replicated_type(volume) and ( - self._do_volume_replication_setup(volume, client)): - model_update['replication_status'] = 'enabled' - model_update['replication_driver_data'] = (json.dumps( - {'location': self._client_conf['hpelefthand_api_url']})) - - return model_update - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - def _get_volume_extra_specs(self, volume): - """Get extra specs from a volume.""" - extra_specs = {} - type_id = volume.get('volume_type_id', None) - if type_id is not None: - ctxt = context.get_admin_context() - volume_type = volume_types.get_volume_type(ctxt, type_id) - extra_specs = volume_type.get('extra_specs') - return extra_specs - - def _get_lh_extra_specs(self, extra_specs, valid_keys): - """Get LeftHand extra_specs (valid_keys only).""" - extra_specs_of_interest = {} - for key, value in extra_specs.items(): - if key in valid_keys: - prefix = key.split(":") - if prefix[0] == "hplh": - LOG.warning("The 'hplh' prefix is deprecated. Use " - "'hpelh' instead.") - extra_specs_of_interest[key] = value - return extra_specs_of_interest - - def _map_extra_specs(self, extra_specs): - """Map the extra spec key/values to LeftHand key/values.""" - client_options = {} - for key, value in extra_specs.items(): - # map extra spec key to lh client option key - client_key = extra_specs_key_map[key] - # map extra spect value to lh client option value - try: - value_map = extra_specs_value_map[client_key] - # an invalid value will throw KeyError - client_value = value_map[value] - client_options[client_key] = client_value - except KeyError: - LOG.error("'%(value)s' is an invalid value " - "for extra spec '%(key)s'", - {'value': value, 'key': key}) - return client_options - - def _update_provider(self, volume_info, cluster_vip=None): - if not cluster_vip: - cluster_vip = self.cluster_vip - # TODO(justinsb): Is this always 1? Does it matter? - cluster_interface = '1' - iscsi_portal = cluster_vip + ":3260," + cluster_interface - - return {'provider_location': ( - "%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))} - - @coordination.synchronized('VSA-{connector[host]}') - def _create_server(self, connector, client): - server_info = None - chap_enabled = self._client_conf.get('hpelefthand_iscsi_chap_enabled') - try: - server_info = client.getServerByName(connector['host']) - chap_secret = server_info['chapTargetSecret'] - if not chap_enabled and chap_secret: - LOG.warning('CHAP secret exists for host %s but CHAP is ' - 'disabled', connector['host']) - if chap_enabled and chap_secret is None: - LOG.warning('CHAP is enabled, but server secret not ' - 'configured on server %s', connector['host']) - return server_info - except hpeexceptions.HTTPNotFound: - # server does not exist, so create one - pass - - optional = None - if chap_enabled: - chap_secret = volume_utils.generate_password() - optional = {'chapName': connector['initiator'], - 'chapTargetSecret': chap_secret, - 'chapAuthenticationRequired': True - } - - server_info = client.createServer(connector['host'], - connector['initiator'], - optional) - return server_info - - def create_export(self, context, volume, connector): - pass - - def ensure_export(self, context, volume): - pass - - def remove_export(self, context, volume): - pass - - @cinder_utils.trace - def retype(self, ctxt, volume, new_type, diff, host): - """Convert the volume to be of the new type. - - Returns a boolean indicating whether the retype occurred. - - :param ctxt: Context - :param volume: A dictionary describing the volume to retype - :param new_type: A dictionary describing the volume type to convert to - :param diff: A dictionary with the difference between the two types - :param host: A dictionary describing the host, where - host['host'] is its name, and host['capabilities'] is a - dictionary of its reported capabilities. - """ - LOG.debug('enter: retype: id=%(id)s, new_type=%(new_type)s,' - 'diff=%(diff)s, host=%(host)s', {'id': volume['id'], - 'new_type': new_type, - 'diff': diff, - 'host': host}) - client = self._login() - try: - volume_info = client.getVolumeByName(volume['name']) - - # pick out the LH extra specs - new_extra_specs = dict(new_type).get('extra_specs') - - # in the absence of LH capability in diff, - # True should be return as retype is not needed - if not list(filter((lambda key: extra_specs_key_map.get(key)), - diff['extra_specs'].keys())): - return True - - # add capability of LH, which are absent in new type, - # so default value gets set for those capability - for key, value in extra_specs_default_key_value_map.items(): - if key not in new_extra_specs.keys(): - new_extra_specs[key] = value - - lh_extra_specs = self._get_lh_extra_specs( - new_extra_specs, - extra_specs_key_map.keys()) - - LOG.debug('LH specs=%(specs)s', {'specs': lh_extra_specs}) - - # only set the ones that have changed - changed_extra_specs = {} - for key, value in lh_extra_specs.items(): - try: - (old, new) = diff['extra_specs'][key] - if old != new: - changed_extra_specs[key] = value - except KeyError: - changed_extra_specs[key] = value - - # map extra specs to LeftHand options - options = self._map_extra_specs(changed_extra_specs) - if len(options) > 0: - client.modifyVolume(volume_info['id'], options) - return True - except hpeexceptions.HTTPNotFound: - raise exception.VolumeNotFound(volume_id=volume['id']) - except Exception as ex: - LOG.warning("%s", ex) - finally: - self._logout(client) - - return False - - @cinder_utils.trace - def migrate_volume(self, ctxt, volume, host): - """Migrate the volume to the specified host. - - Backend assisted volume migration will occur if and only if; - - 1. Same LeftHand backend - 2. Volume cannot be attached - 3. Volumes with snapshots cannot be migrated - 4. Source and Destination clusters must be in the same management group - - Volume re-type is not supported. - - Returns a boolean indicating whether the migration occurred, as well as - model_update. - - :param ctxt: Context - :param volume: A dictionary describing the volume to migrate - :param host: A dictionary describing the host to migrate to, where - host['host'] is its name, and host['capabilities'] is a - dictionary of its reported capabilities. - """ - false_ret = (False, None) - if 'location_info' not in host['capabilities']: - return false_ret - - host_location = host['capabilities']['location_info'] - (driver, cluster, vip) = host_location.split(' ') - client = self._login() - LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, ' - 'cluster=%(cluster)s', { - 'id': volume['id'], - 'host': host, - 'cluster': self._client_conf['hpelefthand_clustername']}) - try: - # get the cluster info, if it exists and compare - cluster_info = client.getClusterByName(cluster) - LOG.debug('Cluster info: %s', cluster_info) - virtual_ips = cluster_info['virtualIPAddresses'] - - if driver != self.__class__.__name__: - LOG.info("Cannot provide backend assisted migration for " - "volume: %s because volume is from a different " - "backend.", volume['name']) - return false_ret - if vip != virtual_ips[0]['ipV4Address']: - LOG.info("Cannot provide backend assisted migration for " - "volume: %s because cluster exists in different " - "management group.", volume['name']) - return false_ret - - except hpeexceptions.HTTPNotFound: - LOG.info("Cannot provide backend assisted migration for " - "volume: %s because cluster exists in different " - "management group.", volume['name']) - return false_ret - finally: - self._logout(client) - - client = self._login() - try: - volume_info = client.getVolumeByName(volume['name']) - LOG.debug('Volume info: %s', volume_info) - - # can't migrate if server is attached - if volume_info['iscsiSessions'] is not None: - LOG.info("Cannot provide backend assisted migration " - "for volume: %s because the volume has been " - "exported.", volume['name']) - return false_ret - - # can't migrate if volume has snapshots - snap_info = client.getVolume( - volume_info['id'], - 'fields=snapshots,snapshots[resource[members[name]]]') - LOG.debug('Snapshot info: %s', snap_info) - if snap_info['snapshots']['resource'] is not None: - LOG.info("Cannot provide backend assisted migration " - "for volume: %s because the volume has " - "snapshots.", volume['name']) - return false_ret - - options = {'clusterName': cluster} - client.modifyVolume(volume_info['id'], options) - except hpeexceptions.HTTPNotFound: - LOG.info("Cannot provide backend assisted migration for " - "volume: %s because volume does not exist in this " - "management group.", volume['name']) - return false_ret - except hpeexceptions.HTTPServerError as ex: - LOG.error("Exception: %s", ex) - return false_ret - finally: - self._logout(client) - - return (True, None) - - @cinder_utils.trace - def update_migrated_volume(self, context, volume, new_volume, - original_volume_status): - """Rename the new (temp) volume to it's original name. - - - This method tries to rename the new volume to it's original - name after the migration has completed. - - """ - LOG.debug("Update volume name for %(id)s.", {'id': new_volume['id']}) - name_id = None - provider_location = None - if original_volume_status == 'available': - # volume isn't attached and can be updated - original_name = CONF.volume_name_template % volume['id'] - current_name = CONF.volume_name_template % new_volume['id'] - client = self._login() - try: - volume_info = client.getVolumeByName(current_name) - volumeMods = {'name': original_name} - client.modifyVolume(volume_info['id'], volumeMods) - LOG.info("Volume name changed from %(tmp)s to %(orig)s.", - {'tmp': current_name, 'orig': original_name}) - except Exception as e: - LOG.error("Changing the volume name from %(tmp)s to " - "%(orig)s failed because %(reason)s.", - {'tmp': current_name, 'orig': original_name, - 'reason': e}) - name_id = new_volume['_name_id'] or new_volume['id'] - provider_location = new_volume['provider_location'] - finally: - self._logout(client) - else: - # the backend can't change the name. - name_id = new_volume['_name_id'] or new_volume['id'] - provider_location = new_volume['provider_location'] - - return {'_name_id': name_id, 'provider_location': provider_location} - - @cinder_utils.trace - def manage_existing(self, volume, existing_ref): - """Manage an existing LeftHand volume. - - existing_ref is a dictionary of the form: - {'source-name': } - """ - # Check API Version - self._check_api_version() - - target_vol_name = self._get_existing_volume_ref_name(existing_ref) - - # Check for the existence of the virtual volume. - client = self._login() - try: - volume_info = client.getVolumeByName(target_vol_name) - except hpeexceptions.HTTPNotFound: - err = (_("Virtual volume '%s' doesn't exist on array.") % - target_vol_name) - LOG.error(err) - raise exception.InvalidInput(reason=err) - finally: - self._logout(client) - - # Generate the new volume information based on the new ID. - new_vol_name = 'volume-' + volume['id'] - - volume_type = None - if volume['volume_type_id']: - try: - volume_type = self._get_volume_type(volume['volume_type_id']) - except Exception: - reason = (_("Volume type ID '%s' is invalid.") % - volume['volume_type_id']) - raise exception.ManageExistingVolumeTypeMismatch(reason=reason) - - new_vals = {"name": new_vol_name} - - client = self._login() - try: - # Update the existing volume with the new name. - client.modifyVolume(volume_info['id'], new_vals) - finally: - self._logout(client) - - LOG.info("Virtual volume '%(ref)s' renamed to '%(new)s'.", - {'ref': existing_ref['source-name'], 'new': new_vol_name}) - - display_name = None - if volume['display_name']: - display_name = volume['display_name'] - - if volume_type: - LOG.info("Virtual volume %(disp)s '%(new)s' is being retyped.", - {'disp': display_name, 'new': new_vol_name}) - - # Creates a diff as it needed for retype operation. - diff = {} - diff['extra_specs'] = {key: (None, value) for key, value - in volume_type['extra_specs'].items()} - try: - self.retype(None, - volume, - volume_type, - diff, - volume['host']) - LOG.info("Virtual volume %(disp)s successfully retyped to " - "%(new_type)s.", - {'disp': display_name, - 'new_type': volume_type.get('name')}) - except Exception: - with excutils.save_and_reraise_exception(): - LOG.warning("Failed to manage virtual volume %(disp)s " - "due to error during retype.", - {'disp': display_name}) - # Try to undo the rename and clear the new comment. - client = self._login() - try: - client.modifyVolume( - volume_info['id'], - {'name': target_vol_name}) - finally: - self._logout(client) - - updates = {'display_name': display_name} - - LOG.info("Virtual volume %(disp)s '%(new)s' is now being managed.", - {'disp': display_name, 'new': new_vol_name}) - - # Return display name to update the name displayed in the GUI and - # any model updates from retype. - return updates - - @cinder_utils.trace - def manage_existing_snapshot(self, snapshot, existing_ref): - """Manage an existing LeftHand snapshot. - - existing_ref is a dictionary of the form: - {'source-name': } - """ - # Check API Version - self._check_api_version() - - # Potential parent volume for the snapshot - volume = snapshot['volume'] - - if volume.get('replication_status') == 'failed-over': - err = (_("Managing of snapshots to failed-over volumes is " - "not allowed.")) - raise exception.InvalidInput(reason=err) - - target_snap_name = self._get_existing_volume_ref_name(existing_ref) - - # Check for the existence of the virtual volume. - client = self._login() - try: - updates = self._manage_snapshot(client, - volume, - snapshot, - target_snap_name, - existing_ref) - finally: - self._logout(client) - - # Return display name to update the name displayed in the GUI and - # any model updates from retype. - return updates - - def _manage_snapshot(self, client, volume, snapshot, target_snap_name, - existing_ref): - # Check for the existence of the virtual volume. - try: - snapshot_info = client.getSnapshotByName(target_snap_name) - except hpeexceptions.HTTPNotFound: - err = (_("Snapshot '%s' doesn't exist on array.") % - target_snap_name) - LOG.error(err) - raise exception.InvalidInput(reason=err) - - # Make sure the snapshot is being associated with the correct volume. - try: - parent_vol = client.getSnapshotParentVolume(target_snap_name) - except hpeexceptions.HTTPNotFound: - err = (_("Could not find the parent volume for Snapshot '%s' on " - "array.") % target_snap_name) - LOG.error(err) - raise exception.InvalidInput(reason=err) - - parent_vol_name = 'volume-' + snapshot['volume_id'] - if parent_vol_name != parent_vol['name']: - err = (_("The provided snapshot '%s' is not a snapshot of " - "the provided volume.") % target_snap_name) - LOG.error(err) - raise exception.InvalidInput(reason=err) - - # Generate the new snapshot information based on the new ID. - new_snap_name = 'snapshot-' + snapshot['id'] - - new_vals = {"name": new_snap_name} - - try: - # Update the existing snapshot with the new name. - client.modifySnapshot(snapshot_info['id'], new_vals) - except hpeexceptions.HTTPServerError: - err = (_("An error occurred while attempting to modify " - "Snapshot '%s'.") % snapshot_info['id']) - LOG.error(err) - - LOG.info("Snapshot '%(ref)s' renamed to '%(new)s'.", - {'ref': existing_ref['source-name'], 'new': new_snap_name}) - - display_name = None - if snapshot['display_name']: - display_name = snapshot['display_name'] - - updates = {'display_name': display_name} - - LOG.info("Snapshot %(disp)s '%(new)s' is now being managed.", - {'disp': display_name, 'new': new_snap_name}) - - return updates - - @cinder_utils.trace - def manage_existing_get_size(self, volume, existing_ref): - """Return size of volume to be managed by manage_existing. - - existing_ref is a dictionary of the form: - {'source-name': } - """ - # Check API version. - self._check_api_version() - - target_vol_name = self._get_existing_volume_ref_name(existing_ref) - - # Make sure the reference is not in use. - if re.match('volume-*|snapshot-*', target_vol_name): - reason = _("Reference must be the volume name of an unmanaged " - "virtual volume.") - raise exception.ManageExistingInvalidReference( - existing_ref=target_vol_name, - reason=reason) - - # Check for the existence of the virtual volume. - client = self._login() - try: - volume_info = client.getVolumeByName(target_vol_name) - except hpeexceptions.HTTPNotFound: - err = (_("Virtual volume '%s' doesn't exist on array.") % - target_vol_name) - LOG.error(err) - raise exception.InvalidInput(reason=err) - finally: - self._logout(client) - - return int(math.ceil(float(volume_info['size']) / units.Gi)) - - @cinder_utils.trace - def manage_existing_snapshot_get_size(self, snapshot, existing_ref): - """Return size of volume to be managed by manage_existing. - - existing_ref is a dictionary of the form: - {'source-name': } - """ - # Check API version. - self._check_api_version() - - target_snap_name = self._get_existing_volume_ref_name(existing_ref) - - # Make sure the reference is not in use. - if re.match('volume-*|snapshot-*|unm-*', target_snap_name): - reason = _("Reference must be the name of an unmanaged " - "snapshot.") - raise exception.ManageExistingInvalidReference( - existing_ref=target_snap_name, - reason=reason) - - # Check for the existence of the virtual volume. - client = self._login() - try: - snapshot_info = client.getSnapshotByName(target_snap_name) - except hpeexceptions.HTTPNotFound: - err = (_("Snapshot '%s' doesn't exist on array.") % - target_snap_name) - LOG.error(err) - raise exception.InvalidInput(reason=err) - finally: - self._logout(client) - - return int(math.ceil(float(snapshot_info['size']) / units.Gi)) - - @cinder_utils.trace - def unmanage(self, volume): - """Removes the specified volume from Cinder management.""" - # Check API version. - self._check_api_version() - - # Rename the volume's name to unm-* format so that it can be - # easily found later. - client = self._login() - try: - volume_info = client.getVolumeByName(volume['name']) - new_vol_name = 'unm-' + six.text_type(volume['id']) - options = {'name': new_vol_name} - client.modifyVolume(volume_info['id'], options) - finally: - self._logout(client) - - LOG.info("Virtual volume %(disp)s '%(vol)s' is no longer managed. " - "Volume renamed to '%(new)s'.", - {'disp': volume['display_name'], - 'vol': volume['name'], - 'new': new_vol_name}) - - @cinder_utils.trace - def unmanage_snapshot(self, snapshot): - """Removes the specified snapshot from Cinder management.""" - # Check API version. - self._check_api_version() - - # Potential parent volume for the snapshot - volume = snapshot['volume'] - - if volume.get('replication_status') == 'failed-over': - err = (_("Unmanaging of snapshots from 'failed-over' volumes is " - "not allowed.")) - LOG.error(err) - # TODO(leeantho) Change this exception to Invalid when the volume - # manager supports handling that. - raise exception.SnapshotIsBusy(snapshot_name=snapshot['id']) - - # Rename the snapshots's name to ums-* format so that it can be - # easily found later. - client = self._login() - try: - snapshot_info = client.getSnapshotByName(snapshot['name']) - new_snap_name = 'ums-' + six.text_type(snapshot['id']) - options = {'name': new_snap_name} - client.modifySnapshot(snapshot_info['id'], options) - LOG.info("Snapshot %(disp)s '%(vol)s' is no longer managed. " - "Snapshot renamed to '%(new)s'.", - {'disp': snapshot['display_name'], - 'vol': snapshot['name'], - 'new': new_snap_name}) - finally: - self._logout(client) - - def _get_existing_volume_ref_name(self, existing_ref): - """Returns the volume name of an existing reference. - - Checks if an existing volume reference has a source-name element. - If source-name is not present an error will be thrown. - """ - if 'source-name' not in existing_ref: - reason = _("Reference must contain source-name.") - raise exception.ManageExistingInvalidReference( - existing_ref=existing_ref, - reason=reason) - - return existing_ref['source-name'] - - def _check_api_version(self): - """Checks that the API version is correct.""" - if (self.api_version < MIN_API_VERSION): - ex_msg = (_('Invalid HPELeftHand API version found: %(found)s. ' - 'Version %(minimum)s or greater required for ' - 'manage/unmanage support.') - % {'found': self.api_version, - 'minimum': MIN_API_VERSION}) - LOG.error(ex_msg) - raise exception.InvalidInput(reason=ex_msg) - - def _get_volume_type(self, type_id): - ctxt = context.get_admin_context() - return volume_types.get_volume_type(ctxt, type_id) - - # v2 replication methods - @cinder_utils.trace - def failover_host(self, context, volumes, secondary_id=None, groups=None): - """Force failover to a secondary replication target.""" - if secondary_id and secondary_id == self.FAILBACK_VALUE: - volume_update_list = self._replication_failback(volumes) - target_id = None - else: - failover_target = None - for target in self._replication_targets: - if target['backend_id'] == secondary_id: - failover_target = target - break - if not failover_target: - msg = _("A valid secondary target MUST be specified in order " - "to failover.") - LOG.error(msg) - raise exception.InvalidReplicationTarget(reason=msg) - - target_id = failover_target['backend_id'] - volume_update_list = [] - for volume in volumes: - if self._volume_of_replicated_type(volume): - # Try and stop the remote snapshot schedule. If the primary - # array is down, we will continue with the failover. - client = None - try: - client = self._login(timeout=30) - name = volume['name'] + self.REP_SCHEDULE_SUFFIX + ( - "_Pri") - client.stopRemoteSnapshotSchedule(name) - except Exception: - LOG.warning("The primary array is currently " - "offline, remote copy has been " - "automatically paused.") - finally: - self._logout(client) - - # Update provider location to the new array. - cl = None - try: - cl = self._create_replication_client(failover_target) - # Stop snapshot schedule - try: - name = volume['name'] + ( - self.REP_SCHEDULE_SUFFIX + "_Rmt") - cl.stopRemoteSnapshotSchedule(name) - except Exception: - pass - # Make the volume primary so it can be attached after a - # fail-over. - cl.makeVolumePrimary(volume['name']) - - # Update the provider info for a proper fail-over. - volume_info = cl.getVolumeByName(volume['name']) - prov_location = self._update_provider( - volume_info, - cluster_vip=failover_target['cluster_vip']) - volume_update_list.append( - {'volume_id': volume['id'], - 'updates': {'replication_status': 'failed-over', - 'provider_location': - prov_location['provider_location']}}) - except Exception as ex: - LOG.error("There was a problem with the failover " - "(%(error)s) and it was unsuccessful. " - "Volume '%(volume)s will not be available " - "on the failed over target.", - {'error': six.text_type(ex), - 'volume': volume['id']}) - volume_update_list.append( - {'volume_id': volume['id'], - 'updates': {'replication_status': 'error'}}) - finally: - self._destroy_replication_client(cl) - else: - # If the volume is not of replicated type, we need to - # force the status into error state so a user knows they - # do not have access to the volume. - volume_update_list.append( - {'volume_id': volume['id'], - 'updates': {'status': 'error'}}) - - self._active_backend_id = target_id - - return target_id, volume_update_list, [] - - def _do_replication_setup(self): - default_san_ssh_port = self.configuration.hpelefthand_ssh_port - default_ssh_conn_timeout = self.configuration.ssh_conn_timeout - default_san_private_key = self.configuration.san_private_key - - replication_targets = [] - replication_devices = self.configuration.replication_device - if replication_devices: - # We do not want to fail if we cannot log into the client here - # as a failover can still occur, so we need out replication - # devices to exist. - for dev in replication_devices: - remote_array = dict(dev.items()) - # Override and set defaults for certain entries - remote_array['managed_backend_name'] = ( - dev.get('managed_backend_name')) - remote_array['hpelefthand_ssh_port'] = ( - dev.get('hpelefthand_ssh_port', default_san_ssh_port)) - remote_array['ssh_conn_timeout'] = ( - dev.get('ssh_conn_timeout', default_ssh_conn_timeout)) - remote_array['san_private_key'] = ( - dev.get('san_private_key', default_san_private_key)) - # Format hpe3par_iscsi_chap_enabled as a bool - remote_array['hpelefthand_iscsi_chap_enabled'] = ( - dev.get('hpelefthand_iscsi_chap_enabled') == 'True') - remote_array['cluster_id'] = None - remote_array['cluster_vip'] = None - array_name = remote_array['backend_id'] - - # Make sure we can log into the array, that it has been - # correctly configured, and its API version meets the - # minimum requirement. - cl = None - try: - cl = self._create_replication_client(remote_array) - api_version = cl.getApiVersion() - cluster_info = cl.getClusterByName( - remote_array['hpelefthand_clustername']) - remote_array['cluster_id'] = cluster_info['id'] - virtual_ips = cluster_info['virtualIPAddresses'] - remote_array['cluster_vip'] = virtual_ips[0]['ipV4Address'] - - if api_version < MIN_API_VERSION: - LOG.warning("The secondary array must have an API " - "version of %(min_ver)s or higher. " - "Array '%(target)s' is on %(target_ver)s, " - "therefore it will not be added as a " - "valid replication target.", - {'min_ver': MIN_API_VERSION, - 'target': array_name, - 'target_ver': api_version}) - elif not self._is_valid_replication_array(remote_array): - LOG.warning("'%s' is not a valid replication array. " - "In order to be valid, backend_id, " - "hpelefthand_api_url, " - "hpelefthand_username, " - "hpelefthand_password, and " - "hpelefthand_clustername, " - "must be specified. If the target is " - "managed, managed_backend_name must be " - "set as well.", array_name) - else: - replication_targets.append(remote_array) - except Exception: - LOG.error("Could not log in to LeftHand array (%s) with " - "the provided credentials.", array_name) - finally: - self._destroy_replication_client(cl) - - self._replication_targets = replication_targets - if self._is_replication_configured_correct(): - self._replication_enabled = True - - def _replication_failback(self, volumes): - array_config = {'hpelefthand_api_url': - self.configuration.hpelefthand_api_url, - 'hpelefthand_username': - self.configuration.hpelefthand_username, - 'hpelefthand_password': - self.configuration.hpelefthand_password, - 'hpelefthand_ssh_port': - self.configuration.hpelefthand_ssh_port} - - # Make sure the proper steps on the backend have been completed before - # we allow a failback. - if not self._is_host_ready_for_failback(volumes, array_config): - msg = _("The host is not ready to be failed back. Please " - "resynchronize the volumes and resume replication on the " - "LeftHand backends.") - LOG.error(msg) - raise exception.InvalidReplicationTarget(reason=msg) - - cl = None - volume_update_list = [] - for volume in volumes: - if self._volume_of_replicated_type(volume): - try: - cl = self._create_replication_client(array_config) - # Update the provider info for a proper fail-back. - volume_info = cl.getVolumeByName(volume['name']) - cluster_info = cl.getClusterByName( - self.configuration.hpelefthand_clustername) - virtual_ips = cluster_info['virtualIPAddresses'] - cluster_vip = virtual_ips[0]['ipV4Address'] - provider_location = self._update_provider( - volume_info, cluster_vip=cluster_vip) - volume_update_list.append( - {'volume_id': volume['id'], - 'updates': {'replication_status': 'available', - 'provider_location': - provider_location['provider_location']}}) - except Exception as ex: - # The secondary array was not able to execute the fail-back - # properly. The replication status is now in an unknown - # state, so we will treat it as an error. - LOG.error("There was a problem with the failover " - "(%(error)s) and it was unsuccessful. " - "Volume '%(volume)s will not be available " - "on the failed over target.", - {'error': ex, - 'volume': volume['id']}) - volume_update_list.append( - {'volume_id': volume['id'], - 'updates': {'replication_status': 'error'}}) - finally: - self._destroy_replication_client(cl) - else: - # Upon failing back, we can move the non-replicated volumes - # back into available state. - volume_update_list.append( - {'volume_id': volume['id'], - 'updates': {'status': 'available'}}) - - return volume_update_list - - def _is_host_ready_for_failback(self, volumes, array_config): - """Checks to make sure the volumes have been synchronized - - This entails ensuring the remote snapshot schedule has been resumed - on the backends and the secondary volume's data has been copied back - to the primary. - """ - is_ready = True - cl = None - try: - for volume in volumes: - if self._volume_of_replicated_type(volume): - schedule_name = volume['name'] + ( - self.REP_SCHEDULE_SUFFIX + "_Pri") - cl = self._create_replication_client(array_config) - schedule = cl.getRemoteSnapshotSchedule(schedule_name) - schedule = ''.join(schedule) - # We need to check the status of the schedule to make sure - # it is not paused. - result = re.search(r".*paused\s+(\w+)", schedule) - is_schedule_active = result.group(1) == 'false' - - volume_info = cl.getVolumeByName(volume['name']) - if not volume_info['isPrimary'] or not is_schedule_active: - is_ready = False - break - except Exception as ex: - LOG.error("There was a problem when trying to determine if " - "the volume can be failed-back: %s", ex) - is_ready = False - finally: - self._destroy_replication_client(cl) - - return is_ready - - def _get_replication_targets(self): - replication_targets = [] - for target in self._replication_targets: - replication_targets.append(target['backend_id']) - - return replication_targets - - def _is_valid_replication_array(self, target): - required_flags = ['hpelefthand_api_url', 'hpelefthand_username', - 'hpelefthand_password', 'backend_id', - 'hpelefthand_clustername'] - try: - self.check_replication_flags(target, required_flags) - return True - except Exception: - return False - - def _is_replication_configured_correct(self): - rep_flag = True - # Make sure there is at least one replication target. - if len(self._replication_targets) < 1: - LOG.error("There must be at least one valid replication " - "device configured.") - rep_flag = False - return rep_flag - - def _volume_of_replicated_type(self, volume, vol_type_id=None): - # TODO(kushal) : we will use volume.volume_types when we re-write - # the design for unit tests to use objects instead of dicts. - replicated_type = False - volume_type_id = vol_type_id if vol_type_id else volume.get( - 'volume_type_id') - if volume_type_id: - volume_type = self._get_volume_type(volume_type_id) - - extra_specs = volume_type.get('extra_specs') - if extra_specs and 'replication_enabled' in extra_specs: - rep_val = extra_specs['replication_enabled'] - replicated_type = (rep_val == " True") - - return replicated_type - - def _does_snapshot_schedule_exist(self, schedule_name, client): - try: - exists = client.doesRemoteSnapshotScheduleExist(schedule_name) - except Exception: - exists = False - return exists - - def _get_lefthand_config(self): - conf = None - for target in self._replication_targets: - if target['backend_id'] == self._active_backend_id: - conf = target - break - - return conf - - def _do_volume_replication_setup(self, volume, client, optional=None): - """This function will do or ensure the following: - - -Create volume on main array (already done in create_volume) - -Create volume on secondary array - -Make volume remote on secondary array - -Create the snapshot schedule - - If anything here fails, we will need to clean everything up in - reverse order, including the original volume. - """ - schedule_name = volume['name'] + self.REP_SCHEDULE_SUFFIX - # If there is already a snapshot schedule, the volume is setup - # for replication on the backend. Start the schedule and return - # success. - if self._does_snapshot_schedule_exist(schedule_name + "_Pri", client): - try: - client.startRemoteSnapshotSchedule(schedule_name + "_Pri") - except Exception: - pass - return True - - # Grab the extra_spec entries for replication and make sure they - # are set correctly. - volume_type = self._get_volume_type(volume["volume_type_id"]) - extra_specs = volume_type.get("extra_specs") - - # Get and check replication sync period - replication_sync_period = extra_specs.get( - self.EXTRA_SPEC_REP_SYNC_PERIOD) - if replication_sync_period: - replication_sync_period = int(replication_sync_period) - if replication_sync_period < self.MIN_REP_SYNC_PERIOD: - msg = (_("The replication sync period must be at least %s " - "seconds.") % self.MIN_REP_SYNC_PERIOD) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - else: - # If there is no extra_spec value for replication sync period, we - # will default it to the required minimum and log a warning. - replication_sync_period = self.MIN_REP_SYNC_PERIOD - LOG.warning("There was no extra_spec value for %(spec_name)s, " - "so the default value of %(def_val)s will be " - "used. To overwrite this, set this value in the " - "volume type extra_specs.", - {'spec_name': self.EXTRA_SPEC_REP_SYNC_PERIOD, - 'def_val': self.MIN_REP_SYNC_PERIOD}) - - # Get and check retention count - retention_count = extra_specs.get( - self.EXTRA_SPEC_REP_RETENTION_COUNT) - if retention_count: - retention_count = int(retention_count) - if retention_count > self.MAX_RETENTION_COUNT: - msg = (_("The retention count must be %s or less.") % - self.MAX_RETENTION_COUNT) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - else: - # If there is no extra_spec value for retention count, we - # will default it and log a warning. - retention_count = self.DEFAULT_RETENTION_COUNT - LOG.warning("There was no extra_spec value for %(spec_name)s, " - "so the default value of %(def_val)s will be " - "used. To overwrite this, set this value in the " - "volume type extra_specs.", - {'spec_name': self.EXTRA_SPEC_REP_RETENTION_COUNT, - 'def_val': self.DEFAULT_RETENTION_COUNT}) - - # Get and checkout remote retention count - remote_retention_count = extra_specs.get( - self.EXTRA_SPEC_REP_REMOTE_RETENTION_COUNT) - if remote_retention_count: - remote_retention_count = int(remote_retention_count) - if remote_retention_count > self.MAX_REMOTE_RETENTION_COUNT: - msg = (_("The remote retention count must be %s or less.") % - self.MAX_REMOTE_RETENTION_COUNT) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - else: - # If there is no extra_spec value for remote retention count, we - # will default it and log a warning. - remote_retention_count = self.DEFAULT_REMOTE_RETENTION_COUNT - spec_name = self.EXTRA_SPEC_REP_REMOTE_RETENTION_COUNT - LOG.warning("There was no extra_spec value for %(spec_name)s, " - "so the default value of %(def_val)s will be " - "used. To overwrite this, set this value in the " - "volume type extra_specs.", - {'spec_name': spec_name, - 'def_val': self.DEFAULT_REMOTE_RETENTION_COUNT}) - - cl = None - try: - # Create volume on secondary system - for remote_target in self._replication_targets: - cl = self._create_replication_client(remote_target) - - if optional: - optional['clusterName'] = ( - remote_target['hpelefthand_clustername']) - cl.createVolume(volume['name'], - remote_target['cluster_id'], - volume['size'] * units.Gi, - optional) - - # Make secondary volume a remote volume - # NOTE: The snapshot created when making a volume remote is - # not managed by cinder. This snapshot will be removed when - # _do_volume_replication_destroy is called. - snap_name = volume['name'] + self.REP_SNAPSHOT_SUFFIX - cl.makeVolumeRemote(volume['name'], snap_name) - - # A remote IP address is needed from the cluster in order to - # create the snapshot schedule. - remote_ip = cl.getIPFromCluster( - remote_target['hpelefthand_clustername']) - - # Destroy remote client - self._destroy_replication_client(cl) - - # Create remote snapshot schedule on the primary system. - # We want to start the remote snapshot schedule instantly; a - # date in the past will do that. We will use the Linux epoch - # date formatted to ISO 8601 (YYYY-MM-DDTHH:MM:SSZ). - start_date = "1970-01-01T00:00:00Z" - remote_vol_name = volume['name'] - - client.createRemoteSnapshotSchedule( - volume['name'], - schedule_name, - replication_sync_period, - start_date, - retention_count, - remote_target['hpelefthand_clustername'], - remote_retention_count, - remote_vol_name, - remote_ip, - remote_target['hpelefthand_username'], - remote_target['hpelefthand_password']) - - return True - except Exception as ex: - # Destroy the replication client that was created - self._destroy_replication_client(cl) - # Deconstruct what we tried to create - self._do_volume_replication_destroy(volume, client) - msg = (_("There was an error setting up a remote schedule " - "on the LeftHand arrays: ('%s'). The volume will not be " - "recognized as replication type.") % - six.text_type(ex)) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - def _do_volume_replication_destroy(self, volume, client): - """This will remove all dependencies of a replicated volume - - It should be used when deleting a replication enabled volume - or if setting up a remote copy group fails. It will try and do the - following: - -Delete the snapshot schedule - -Delete volume and snapshots on secondary array - -Delete volume and snapshots on primary array - """ - # Delete snapshot schedule - try: - schedule_name = volume['name'] + self.REP_SCHEDULE_SUFFIX - client.deleteRemoteSnapshotSchedule(schedule_name) - except Exception: - pass - - # Delete volume on secondary array(s) - remote_vol_name = volume['name'] - for remote_target in self._replication_targets: - try: - cl = self._create_replication_client(remote_target) - volume_info = cl.getVolumeByName(remote_vol_name) - cl.deleteVolume(volume_info['id']) - except Exception: - pass - finally: - # Destroy the replication client that was created - self._destroy_replication_client(cl) - - # Delete volume on primary array - try: - volume_info = client.getVolumeByName(volume['name']) - client.deleteVolume(volume_info['id']) - except Exception: - pass diff --git a/doc/source/configuration/block-storage/drivers/hpe-lefthand-driver.rst b/doc/source/configuration/block-storage/drivers/hpe-lefthand-driver.rst deleted file mode 100644 index 39ea991977b..00000000000 --- a/doc/source/configuration/block-storage/drivers/hpe-lefthand-driver.rst +++ /dev/null @@ -1,220 +0,0 @@ -================================ -HPE LeftHand/StoreVirtual driver -================================ - -The ``HPELeftHandISCSIDriver`` is based on the Block Storage service plug-in -architecture. Volume operations are run by communicating with the HPE -LeftHand/StoreVirtual system over HTTPS, or SSH connections. HTTPS -communications use the ``python-lefthandclient``, which is part of the Python -standard library. - -The ``HPELeftHandISCSIDriver`` can be configured to run using a REST client to -communicate with the array. For performance improvements and new functionality -the ``python-lefthandclient`` must be downloaded, and HP LeftHand/StoreVirtual -Operating System software version 11.5 or higher is required on the array. To -configure the driver in standard mode, see -`HPE LeftHand/StoreVirtual REST driver`_. - -For information about how to manage HPE LeftHand/StoreVirtual storage systems, -see the HPE LeftHand/StoreVirtual user documentation. - -HPE LeftHand/StoreVirtual REST driver -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This section describes how to configure the HPE LeftHand/StoreVirtual Block -Storage driver. - -System requirements -------------------- - -To use the HPE LeftHand/StoreVirtual driver, do the following: - -* Install LeftHand/StoreVirtual Operating System software version 11.5 or - higher on the HPE LeftHand/StoreVirtual storage system. - -* Create a cluster group. - -* Install the ``python-lefthandclient`` version 2.1.0 from the Python Package - Index on the system with the enabled Block Storage service - volume drivers. - -Supported operations --------------------- - -* Create, delete, attach, and detach volumes. - -* Create, list, and delete volume snapshots. - -* Create a volume from a snapshot. - -* Copy an image to a volume. - -* Copy a volume to an image. - -* Clone a volume. - -* Extend a volume. - -* Get volume statistics. - -* Migrate a volume with back-end assistance. - -* Retype a volume. - -* Manage and unmanage a volume. - -* Manage and unmanage a snapshot. - -* Replicate host volumes. - -* Fail-over host volumes. - -* Fail-back host volumes. - -* Create, delete, update, snapshot, and clone generic volume groups. - -* Create and delete generic volume group snapshots. - -* Create a generic volume group from a group snapshot or another group. - -When you use back end assisted volume migration, both source and destination -clusters must be in the same HPE LeftHand/StoreVirtual management group. -The HPE LeftHand/StoreVirtual array will use native LeftHand APIs to migrate -the volume. The volume cannot be attached or have snapshots to migrate. - -Volume type support for the driver includes the ability to set the -following capabilities in the Block Storage API -``cinder.api.contrib.types_extra_specs`` volume type extra specs -extension module. - -* ``hpelh:provisioning`` - -* ``hpelh:ao`` - -* ``hpelh:data_pl`` - -To work with the default filter scheduler, the key-value pairs are -case-sensitive and scoped with ``hpelh:``. For information about how to set -the key-value pairs and associate them with a volume type, run the following -command: - -.. code-block:: console - - $ openstack help volume type - -* The following keys require the HPE LeftHand/StoreVirtual storage - array be configured for: - - ``hpelh:ao`` - The HPE LeftHand/StoreVirtual storage array must be configured for - Adaptive Optimization. - - ``hpelh:data_pl`` - The HPE LeftHand/StoreVirtual storage array must be able to support the - Data Protection level specified by the extra spec. - -* If volume types are not used or a particular key is not set for a volume - type, the following defaults are used: - - ``hpelh:provisioning`` - Defaults to ``thin`` provisioning, the valid values are, ``thin`` and - ``full`` - - ``hpelh:ao`` - Defaults to ``true``, the valid values are, ``true`` and ``false``. - - ``hpelh:data_pl`` - Defaults to ``r-0``, Network RAID-0 (None), the valid values are, - - * ``r-0``, Network RAID-0 (None) - - * ``r-5``, Network RAID-5 (Single Parity) - - * ``r-10-2``, Network RAID-10 (2-Way Mirror) - - * ``r-10-3``, Network RAID-10 (3-Way Mirror) - - * ``r-10-4``, Network RAID-10 (4-Way Mirror) - - * ``r-6``, Network RAID-6 (Dual Parity) - -Enable the HPE LeftHand/StoreVirtual iSCSI driver -------------------------------------------------- - -The ``HPELeftHandISCSIDriver`` is installed with the OpenStack software. - -#. Install the ``python-lefthandclient`` Python package on the OpenStack Block - Storage system. - - .. code-block:: console - - $ pip install 'python-lefthandclient>=2.1,<3.0' - -#. If you are not using an existing cluster, create a cluster on the HPE - LeftHand storage system to be used as the cluster for creating volumes. - -#. Make the following changes in the ``/etc/cinder/cinder.conf`` file: - - .. code-block:: ini - - # LeftHand WS API Server URL - hpelefthand_api_url=https://10.10.0.141:8081/lhos - - # LeftHand Super user username - hpelefthand_username=lhuser - - # LeftHand Super user password - hpelefthand_password=lhpass - - # LeftHand cluster to use for volume creation - hpelefthand_clustername=ClusterLefthand - - # LeftHand iSCSI driver - volume_driver=cinder.volume.drivers.hpe.hpe_lefthand_iscsi.HPELeftHandISCSIDriver - - # Should CHAPS authentication be used (default=false) - hpelefthand_iscsi_chap_enabled=false - - # Enable HTTP debugging to LeftHand (default=false) - hpelefthand_debug=false - - # The ratio of oversubscription when thin provisioned volumes are - # involved. Default ratio is 20.0, this means that a provisioned capacity - # can be 20 times of the total physical capacity. - max_over_subscription_ratio=20.0 - - # This flag represents the percentage of reserved back-end capacity. - reserved_percentage=15 - - You can enable only one driver on each cinder instance unless you enable - multiple back end support. See the Cinder multiple back end support - instructions to enable this feature. - - If the ``hpelefthand_iscsi_chap_enabled`` is set to ``true``, the driver - will associate randomly-generated CHAP secrets with all hosts on the HPE - LeftHand/StoreVirtual system. OpenStack Compute nodes use these secrets - when creating iSCSI connections. - - .. important:: - - CHAP secrets are passed from OpenStack Block Storage to Compute in clear - text. This communication should be secured to ensure that CHAP secrets - are not discovered. - - .. note:: - - CHAP secrets are added to existing hosts as well as newly-created ones. - If the CHAP option is enabled, hosts will not be able to access the - storage without the generated secrets. - -#. Save the changes to the ``cinder.conf`` file and restart the - ``cinder-volume`` service. - -The HPE LeftHand/StoreVirtual driver is now enabled on your OpenStack system. -If you experience problems, review the Block Storage service log files for -errors. - -.. note:: - Previous versions implement a HPE LeftHand/StoreVirtual CLIQ driver that - enable the Block Storage service driver configuration in legacy mode. This - is removed from Mitaka onwards. diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index bb6a45d26c3..2afa492f94c 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -48,9 +48,6 @@ title=Fujitsu ETERNUS Driver (FC, iSCSI) [driver.hpe_3par] title=HPE 3PAR Storage Driver (FC, iSCSI) -[driver.hpe_lefthand] -title=HPE Lefthand Driver (iSCSI) - [driver.hpe_msa] title=HPE MSA Driver (iSCSI, FC) @@ -198,7 +195,6 @@ driver.dell_emc_vxflexos=complete driver.dell_emc_xtremio=complete driver.fujitsu_eternus=complete driver.hpe_3par=complete -driver.hpe_lefthand=missing driver.hpe_msa=complete driver.huawei_t_v1=complete driver.huawei_t_v2=complete @@ -258,7 +254,6 @@ driver.dell_emc_vxflexos=complete driver.dell_emc_xtremio=complete driver.fujitsu_eternus=complete driver.hpe_3par=complete -driver.hpe_lefthand=complete driver.hpe_msa=complete driver.huawei_t_v1=complete driver.huawei_t_v2=complete @@ -318,7 +313,6 @@ driver.dell_emc_vxflexos=complete driver.dell_emc_xtremio=missing driver.fujitsu_eternus=missing driver.hpe_3par=missing -driver.hpe_lefthand=missing driver.hpe_msa=missing driver.huawei_t_v1=complete driver.huawei_t_v2=complete @@ -381,7 +375,6 @@ driver.dell_emc_vxflexos=complete driver.dell_emc_xtremio=missing driver.fujitsu_eternus=missing driver.hpe_3par=complete -driver.hpe_lefthand=missing driver.hpe_msa=missing driver.huawei_t_v1=missing driver.huawei_t_v2=complete @@ -443,7 +436,6 @@ driver.dell_emc_vxflexos=missing driver.dell_emc_xtremio=missing driver.fujitsu_eternus=missing driver.hpe_3par=complete -driver.hpe_lefthand=complete driver.hpe_msa=missing driver.huawei_t_v1=missing driver.huawei_t_v2=missing @@ -506,7 +498,6 @@ driver.dell_emc_vxflexos=complete driver.dell_emc_xtremio=complete driver.fujitsu_eternus=missing driver.hpe_3par=complete -driver.hpe_lefthand=complete driver.hpe_msa=missing driver.huawei_t_v1=missing driver.huawei_t_v2=missing @@ -568,7 +559,6 @@ driver.dell_emc_vxflexos=complete driver.dell_emc_xtremio=complete driver.fujitsu_eternus=complete driver.hpe_3par=complete -driver.hpe_lefthand=complete driver.hpe_msa=missing driver.huawei_t_v1=missing driver.huawei_t_v2=missing @@ -631,7 +621,6 @@ driver.dell_emc_vxflexos=missing driver.dell_emc_xtremio=missing driver.fujitsu_eternus=missing driver.hpe_3par=missing -driver.hpe_lefthand=missing driver.hpe_msa=missing driver.huawei_t_v1=missing driver.huawei_t_v2=missing @@ -694,7 +683,6 @@ driver.dell_emc_vxflexos=complete driver.dell_emc_xtremio=complete driver.fujitsu_eternus=missing driver.hpe_3par=complete -driver.hpe_lefthand=complete driver.hpe_msa=complete driver.huawei_t_v1=missing driver.huawei_t_v2=missing @@ -754,7 +742,6 @@ driver.dell_emc_vxflexos=missing driver.dell_emc_xtremio=missing driver.fujitsu_eternus=missing driver.hpe_3par=complete -driver.hpe_lefthand=missing driver.hpe_msa=missing driver.huawei_t_v1=missing driver.huawei_t_v2=missing @@ -818,7 +805,6 @@ driver.dell_emc_vxflexos=missing driver.dell_emc_xtremio=missing driver.fujitsu_eternus=missing driver.hpe_3par=missing -driver.hpe_lefthand=missing driver.hpe_msa=missing driver.huawei_t_v1=missing driver.huawei_t_v2=missing diff --git a/doc/source/reference/support-matrix.rst b/doc/source/reference/support-matrix.rst index 9bae48f1b01..53732c50f8c 100644 --- a/doc/source/reference/support-matrix.rst +++ b/doc/source/reference/support-matrix.rst @@ -87,6 +87,7 @@ release. * Nexenta Edge Storage Driver * Ussuri + * HPE Lefthand Driver (iSCSI) * ProphetStor Flexvisor Driver * Sheepdog Driver * Veritas Access Storage Driver diff --git a/releasenotes/notes/remove-hpe-lefthand-driver-57b03ca9ada2654c.yaml b/releasenotes/notes/remove-hpe-lefthand-driver-57b03ca9ada2654c.yaml new file mode 100644 index 00000000000..26705238e1a --- /dev/null +++ b/releasenotes/notes/remove-hpe-lefthand-driver-57b03ca9ada2654c.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + The HPE Lefthand Driver (iSCSI) was marked unsupported in the + Train release as the StoreVirtual product line has gone EOL and + the LeftHand OS no longer receives upgrades. The driver has been + removed in this release. All data on backends powered by HPE + LeftHand OS should be migrated to a supported storage backend + before upgrading your Cinder installation.