From: Walter A. Boring IV Date: Tue, 3 Nov 2015 23:09:50 +0000 (-0800) Subject: Remove the HP CLIQ proxy driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=a30cb10e68b61bc6728712ebaf1a04f2c0300be4;p=openstack-build%2Fcinder-build.git Remove the HP CLIQ proxy driver This patch removes the deprecated HP Lefthand CLIQ/SSH based driver. It also refactors the proxy class, which was nothing but a wrapper for the 2 drivers. The LeftHand driver is now entirely based off of the REST client. Implements: blueprint deprecate-hp-lefthand-cliq DocImpact Change-Id: I12d8bb7b7dcac8ae40796d9d169825265c634a33 --- diff --git a/cinder/tests/unit/test_hplefthand.py b/cinder/tests/unit/test_hplefthand.py index acd437a7f..31dbcd1c4 100644 --- a/cinder/tests/unit/test_hplefthand.py +++ b/cinder/tests/unit/test_hplefthand.py @@ -17,14 +17,12 @@ import mock from oslo_utils import units -import six from cinder import context from cinder import exception from cinder import test from cinder.tests.unit import fake_hp_lefthand_client as hplefthandclient from cinder.volume.drivers.san.hp import hp_lefthand_iscsi -from cinder.volume.drivers.san.hp import hp_lefthand_rest_proxy from cinder.volume import volume_types hpexceptions = hplefthandclient.hpexceptions @@ -89,578 +87,10 @@ class HPLeftHandBaseDriver(object): driver_startup_call_stack = [ mock.call.login('foo1', 'bar2'), mock.call.getClusterByName('CloudCluster1'), - mock.call.getCluster(1), ] -class TestHPLeftHandCLIQISCSIDriver(HPLeftHandBaseDriver, test.TestCase): - - def _fake_cliq_run(self, verb, cliq_args, check_exit_code=True): - """Return fake results for the various methods.""" - - def create_volume(cliq_args): - """Create volume CLIQ input for test. - - input = "createVolume description="fake description" - clusterName=Cluster01 volumeName=fakevolume - thinProvision=0 output=XML size=1GB" - """ - output = """ - - """ - self.assertEqual(self.volume_name, cliq_args['volumeName']) - self.assertEqual('1', cliq_args['thinProvision']) - self.assertEqual('1GB', cliq_args['size']) - return output, None - - def delete_volume(cliq_args): - """Delete volume CLIQ input for test. - - input = "deleteVolume volumeName=fakevolume prompt=false - output=XML" - """ - output = """ - - """ - self.assertEqual(self.volume_name, cliq_args['volumeName']) - self.assertEqual('false', cliq_args['prompt']) - return output, None - - def extend_volume(cliq_args): - """Extend volume CLIQ input for test. - - input = "modifyVolume description="fake description" - volumeName=fakevolume - output=XML size=2GB" - """ - output = """ - - """ - self.assertEqual(self.volume_name, cliq_args['volumeName']) - self.assertEqual('2GB', cliq_args['size']) - return output, None - - def assign_volume(cliq_args): - """Assign volume CLIQ input for test. - - input = "assignVolumeToServer volumeName=fakevolume - serverName=fakehost - output=XML" - """ - output = """ - - """ - self.assertEqual(self.volume_name, cliq_args['volumeName']) - self.assertEqual(self.connector['host'], - cliq_args['serverName']) - return output, None - - def unassign_volume(cliq_args): - """Unassign volume CLIQ input for test. - - input = "unassignVolumeToServer volumeName=fakevolume - serverName=fakehost output=XML - """ - output = """ - - """ - self.assertEqual(self.volume_name, cliq_args['volumeName']) - self.assertEqual(self.connector['host'], - cliq_args['serverName']) - return output, None - - def create_snapshot(cliq_args): - """Create snapshot CLIQ input for test. - - input = "createSnapshot description="fake description" - snapshotName=fakesnapshot - volumeName=fakevolume - output=XML" - """ - output = """ - - """ - self.assertEqual(self.snapshot_name, cliq_args['snapshotName']) - self.assertEqual(self.volume_name, cliq_args['volumeName']) - return output, None - - def delete_snapshot(cliq_args): - """Delete shapshot CLIQ input for test. - - input = "deleteSnapshot snapshotName=fakesnapshot prompt=false - output=XML" - """ - output = """ - - """ - self.assertEqual(self.snapshot_name, cliq_args['snapshotName']) - self.assertEqual('false', cliq_args['prompt']) - return output, None - - def create_volume_from_snapshot(cliq_args): - """Create volume from snapshot CLIQ input for test. - - input = "cloneSnapshot description="fake description" - snapshotName=fakesnapshot - volumeName=fakevolume - output=XML" - """ - output = """ - - """ - self.assertEqual(self.snapshot_name, cliq_args['snapshotName']) - self.assertEqual(self.volume_name, cliq_args['volumeName']) - return output, None - - def get_cluster_info(cliq_args): - """Get cluster info CLIQ input for test. - - input = "getClusterInfo clusterName=Cluster01 searchDepth=1 - verbose=0 output=XML" - """ - output = """ - - - - - - """ - return output, None - - def get_volume_info(cliq_args): - """Get volume info CLIQ input for test. - - input = "getVolumeInfo volumeName=fakevolume output=XML" - """ - output = """ - - - - - """ - return output, None - - def get_snapshot_info(cliq_args): - """Get snapshot info CLIQ input for test. - - input = "getSnapshotInfo snapshotName=fakesnapshot output=XML" - """ - output = """ - - - - - """ - return output, None - - def get_server_info(cliq_args): - """Get server info CLIQ input for test. - - input = "getServerInfo serverName=fakeName" - """ - output = """ - """ - return output, None - - def create_server(cliq_args): - """Create server CLIQ input for test. - - input = "createServer serverName=fakeName initiator=something" - """ - output = """ - """ - return output, None - - def test_error(cliq_args): - output = """ - - """ - return output, None - - def test_paramiko_1_13_0(cliq_args): - - # paramiko 1.13.0 now returns unicode - output = six.text_type( - '\n' - '\n\n \n \n' - ' \n' - ' \n \n \n\n' - '\n ') - return output, None - - def test_paramiko_1_10_0(cliq_args): - - # paramiko 1.10.0 returns python default encoding. - output = ( - '\n' - '\n\n \n \n' - ' \n' - ' \n \n \n\n' - '\n ') - return output, None - - self.assertEqual('XML', cliq_args['output']) - try: - verbs = {'createVolume': create_volume, - 'deleteVolume': delete_volume, - 'modifyVolume': extend_volume, - 'assignVolumeToServer': assign_volume, - 'unassignVolumeToServer': unassign_volume, - 'createSnapshot': create_snapshot, - 'deleteSnapshot': delete_snapshot, - 'cloneSnapshot': create_volume_from_snapshot, - 'getClusterInfo': get_cluster_info, - 'getVolumeInfo': get_volume_info, - 'getSnapshotInfo': get_snapshot_info, - 'getServerInfo': get_server_info, - 'createServer': create_server, - 'testError': test_error, - 'testParamiko_1.10.1': test_paramiko_1_10_0, - 'testParamiko_1.13.1': test_paramiko_1_13_0} - except KeyError: - raise NotImplementedError() - - return verbs[verb](cliq_args) - - def setUp(self): - super(TestHPLeftHandCLIQISCSIDriver, self).setUp() - - self.properties = { - 'target_discovered': True, - 'target_portal': '10.0.1.6:3260', - 'target_iqn': - 'iqn.2003-10.com.lefthandnetworks:group01:25366:fakev', - 'volume_id': self.volume_id} - - def default_mock_conf(self): - - mock_conf = mock.Mock() - mock_conf.san_ip = '10.10.10.10' - mock_conf.san_login = 'foo' - mock_conf.san_password = 'bar' - mock_conf.san_ssh_port = 16022 - mock_conf.san_clustername = 'CloudCluster1' - mock_conf.hplefthand_api_url = None - return mock_conf - - def setup_driver(self, config=None): - - if config is None: - config = self.default_mock_conf() - - self.driver = hp_lefthand_iscsi.HPLeftHandISCSIDriver( - configuration=config) - self.driver.do_setup(None) - - self.driver.proxy._cliq_run = mock.Mock( - side_effect=self._fake_cliq_run) - return self.driver.proxy._cliq_run - - def test_create_volume(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - volume = {'name': self.volume_name, 'size': 1} - model_update = self.driver.create_volume(volume) - expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev 0" - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(expected_location, model_update['provider_location']) - - expected = [ - mock.call( - 'createVolume', { - 'clusterName': 'CloudCluster1', - 'volumeName': 'fakevolume', - 'thinProvision': '1', - 'output': 'XML', - 'size': '1GB'}, - True), - mock.call( - 'getVolumeInfo', { - 'volumeName': 'fakevolume', - 'output': 'XML'}, - True), - mock.call( - 'getClusterInfo', { - 'clusterName': 'Cluster01', - 'searchDepth': '1', - 'verbose': '0', - 'output': 'XML'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_delete_volume(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - volume = {'name': self.volume_name} - self.driver.delete_volume(volume) - - expected = [ - mock.call( - 'getVolumeInfo', { - 'volumeName': 'fakevolume', - 'output': 'XML'}, - True), - mock.call( - 'deleteVolume', { - 'volumeName': 'fakevolume', - 'prompt': 'false', - 'output': 'XML'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_extend_volume(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - volume = {'name': self.volume_name} - self.driver.extend_volume(volume, 2) - - expected = [ - mock.call( - 'modifyVolume', { - 'volumeName': 'fakevolume', - 'output': 'XML', - 'size': '2GB'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_initialize_connection(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - self.driver.proxy._get_iscsi_properties = mock.Mock( - return_value=self.properties) - volume = {'name': self.volume_name} - result = self.driver.initialize_connection(volume, - self.connector) - self.assertEqual('iscsi', result['driver_volume_type']) - self.assertDictMatch(self.properties, result['data']) - - expected = [ - mock.call( - 'getServerInfo', { - 'output': 'XML', - 'serverName': 'fakehost'}, - False), - mock.call( - 'assignVolumeToServer', { - 'volumeName': 'fakevolume', - 'serverName': 'fakehost', - 'output': 'XML'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_terminate_connection(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - volume = {'name': self.volume_name} - self.driver.terminate_connection(volume, self.connector) - - expected = [ - mock.call( - 'unassignVolumeToServer', { - 'volumeName': 'fakevolume', - 'serverName': 'fakehost', - 'output': 'XML'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_create_snapshot(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - snapshot = {'name': self.snapshot_name, - 'volume_name': self.volume_name} - self.driver.create_snapshot(snapshot) - - expected = [ - mock.call( - 'createSnapshot', { - 'snapshotName': 'fakeshapshot', - 'output': 'XML', - 'inheritAccess': 1, - 'volumeName': 'fakevolume'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_delete_snapshot(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - snapshot = {'name': self.snapshot_name} - self.driver.delete_snapshot(snapshot) - - expected = [ - mock.call( - 'getSnapshotInfo', { - 'snapshotName': 'fakeshapshot', - 'output': 'XML'}, - True), - mock.call( - 'deleteSnapshot', { - 'snapshotName': 'fakeshapshot', - 'prompt': 'false', - 'output': 'XML'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_create_volume_from_snapshot(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - - volume = {'name': self.volume_name} - snapshot = {'name': self.snapshot_name} - model_update = self.driver.create_volume_from_snapshot(volume, - snapshot) - expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev 0" - expected_location = "10.0.1.6:3260,1 %s" % expected_iqn - self.assertEqual(expected_location, model_update['provider_location']) - - expected = [ - mock.call( - 'cloneSnapshot', { - 'snapshotName': 'fakeshapshot', - 'output': 'XML', - 'volumeName': 'fakevolume'}, - True), - mock.call( - 'getVolumeInfo', { - 'volumeName': 'fakevolume', - 'output': 'XML'}, - True), - mock.call( - 'getClusterInfo', { - 'clusterName': 'Cluster01', - 'searchDepth': '1', - 'verbose': '0', - 'output': 'XML'}, - True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_get_volume_stats(self): - - # set up driver with default config - mock_cliq_run = self.setup_driver() - volume_stats = self.driver.get_volume_stats(True) - - self.assertEqual('Hewlett-Packard', volume_stats['vendor_name']) - self.assertEqual('iSCSI', volume_stats['storage_protocol']) - - expected = [ - mock.call('getClusterInfo', { - 'searchDepth': 1, - 'clusterName': 'CloudCluster1', - 'output': 'XML'}, True)] - - # validate call chain - mock_cliq_run.assert_has_calls(expected) - - def test_cliq_run_xml_paramiko_1_13_0(self): - - # set up driver with default config - self.setup_driver() - xml = self.driver.proxy._cliq_run_xml('testParamiko_1.13.1', {}) - self.assertIsNotNone(xml) - - def test_cliq_run_xml_paramiko_1_10_0(self): - - # set up driver with default config - self.setup_driver() - xml = self.driver.proxy._cliq_run_xml('testParamiko_1.10.1', {}) - self.assertIsNotNone(xml) - - -class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): +class TestHPLeftHandISCSIDriver(HPLeftHandBaseDriver, test.TestCase): CONSIS_GROUP_ID = '3470cc4c-63b3-4c7a-8120-8a0693b45838' CGSNAPSHOT_ID = '5351d914-6c90-43e7-9a8e-7e84610927da' @@ -670,15 +100,6 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'id': CGSNAPSHOT_ID, 'readOnly': False} - driver_startup_call_stack = [ - mock.call.login('foo1', 'bar2'), - mock.call.getClusterByName('CloudCluster1'), - mock.call.getCluster(1), - mock.call.getVolumes( - cluster='CloudCluster1', - fields=['members[id]', 'members[clusterName]', 'members[size]']), - ] - def default_mock_conf(self): mock_conf = mock.Mock() @@ -703,7 +124,6 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): @mock.patch('hplefthandclient.client.HPLeftHandClient', spec=True) def setup_driver(self, _mock_client, config=None): - if config is None: config = self.default_mock_conf() @@ -742,7 +162,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.createVolume.return_value = { 'iscsiIqn': self.connector['initiator']} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -788,7 +208,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'iscsiIqn': self.connector['initiator']} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -819,7 +239,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.getVolumeByName.return_value = {'id': self.volume_id} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -855,7 +275,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.getVolumeByName.return_value = {'id': self.volume_id} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -892,7 +312,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): } mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -945,7 +365,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): } mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -992,7 +412,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): } mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1035,7 +455,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.findServerVolumes.return_value = [{'id': self.volume_id}] mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1076,7 +496,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): {'id': 99999}] mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1111,7 +531,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.getVolumeByName.return_value = {'id': self.volume_id} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1147,7 +567,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1199,7 +619,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'iscsiIqn': self.connector['initiator']} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1231,7 +651,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'iscsiIqn': self.connector['initiator']} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1270,15 +690,15 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 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.proxy._get_volume_extra_specs( + volume_extra_specs = self.driver._get_volume_extra_specs( volume_with_vt) - extra_specs = self.driver.proxy._get_lh_extra_specs( + extra_specs = self.driver._get_lh_extra_specs( volume_extra_specs, - hp_lefthand_rest_proxy.extra_specs_key_map.keys()) + hp_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.proxy._map_extra_specs(extra_specs) + optional = self.driver._map_extra_specs(extra_specs) self.assertDictMatch({'isThinProvisioned': False}, optional) @@ -1298,15 +718,15 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'hplh:ao': 'true'}} # get the extra specs of interest from this volume's volume type - volume_extra_specs = self.driver.proxy._get_volume_extra_specs( + volume_extra_specs = self.driver._get_volume_extra_specs( volume_with_vt) - extra_specs = self.driver.proxy._get_lh_extra_specs( + extra_specs = self.driver._get_lh_extra_specs( volume_extra_specs, - hp_lefthand_rest_proxy.extra_specs_key_map.keys()) + hp_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.proxy._map_extra_specs(extra_specs) + optional = self.driver._map_extra_specs(extra_specs) # {'hplh:ao': 'true'} should map to # {'isAdaptiveOptimizationEnabled': True} @@ -1336,7 +756,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): volume['host'] = host new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1373,7 +793,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): volume['host'] = host new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1414,7 +834,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): volume['host'] = host new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1452,7 +872,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): volume['host'] = host new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1475,7 +895,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): host = {'host': self.serverName, 'capabilities': {}} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1501,7 +921,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.getVolumeByName.return_value = {'id': self.volume_id} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - location = (self.driver.proxy.DRIVER_LOCATION % { + location = (self.driver.DRIVER_LOCATION % { 'cluster': 'New_CloudCluster', 'vip': '10.10.10.111'}) @@ -1509,7 +929,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'host': self.serverName, 'capabilities': {'location_info': location}} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1545,7 +965,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'resource': None}} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - location = (self.driver.proxy.DRIVER_LOCATION % { + location = (self.driver.DRIVER_LOCATION % { 'cluster': 'New_CloudCluster', 'vip': '10.10.10.111'}) @@ -1553,7 +973,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'host': self.serverName, 'capabilities': {'location_info': location}} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1596,7 +1016,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'resource': 'snapfoo'}} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - location = (self.driver.proxy.DRIVER_LOCATION % { + location = (self.driver.DRIVER_LOCATION % { 'cluster': 'New_CloudCluster', 'vip': '10.10.10.111'}) @@ -1604,7 +1024,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'host': self.serverName, 'capabilities': {'location_info': location}} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1639,7 +1059,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): '_name_id': clone_id, 'provider_location': provider_location} original_volume_status = 'available' - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client actual_update = self.driver.update_migrated_volume( @@ -1661,7 +1081,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'provider_location': provider_location} original_volume_status = 'in-use' - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client actual_update = self.driver.update_migrated_volume( @@ -1688,7 +1108,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'iscsiIqn': self.connector['initiator']} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1726,7 +1146,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'iscsiIqn': self.connector['initiator']} mock_client.getVolumes.return_value = {'total': 1, 'members': []} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -1749,31 +1169,31 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.assert_has_calls(expected) - def test__get_existing_volume_ref_name(self): + def test_get_existing_volume_ref_name(self): self.setup_driver() existing_ref = {'source-name': self.volume_name} - result = self.driver.proxy._get_existing_volume_ref_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.proxy._get_existing_volume_ref_name, + self.driver._get_existing_volume_ref_name, existing_ref) def test_manage_existing(self): mock_client = self.setup_driver() - self.driver.proxy.api_version = "1.1" + self.driver.api_version = "1.1" volume = {'display_name': 'Foo Volume', 'volume_type': None, 'volume_type_id': None, 'id': '12345'} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client mock_client.getVolumeByName.return_value = {'id': self.volume_id} @@ -1816,7 +1236,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'hplh:data_pl': 'r-0', 'volume_type': self.volume_type}} - self.driver.proxy.api_version = "1.1" + self.driver.api_version = "1.1" volume = {'display_name': 'Foo Volume', 'host': 'stack@lefthand#lefthand', @@ -1824,7 +1244,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', 'id': '12345'} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client mock_client.getVolumeByName.return_value = {'id': self.volume_id} @@ -1867,10 +1287,10 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'hplh:data_pl': 'r-0', 'volume_type': self.volume_type}} - self.driver.proxy.retype = mock.Mock( + self.driver.retype = mock.Mock( side_effect=exception.VolumeNotFound(volume_id="fake")) - self.driver.proxy.api_version = "1.1" + self.driver.api_version = "1.1" volume = {'display_name': 'Foo Volume', 'host': 'stack@lefthand#lefthand', @@ -1878,7 +1298,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', 'id': '12345'} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client mock_client.getVolumeByName.return_value = {'id': self.volume_id} @@ -1915,14 +1335,14 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): def test_manage_existing_volume_type_exception(self): mock_client = self.setup_driver() - self.driver.proxy.api_version = "1.1" + 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(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client mock_client.getVolumeByName.return_value = {'id': self.volume_id} @@ -1952,9 +1372,9 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client = self.setup_driver() mock_client.getVolumeByName.return_value = {'size': 2147483648} - self.driver.proxy.api_version = "1.1" + self.driver.api_version = "1.1" - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client mock_client.getVolumes.return_value = { @@ -1985,9 +1405,9 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client = self.setup_driver() mock_client.getVolumeByName.return_value = {'size': 2147483648} - self.driver.proxy.api_version = "1.1" + self.driver.api_version = "1.1" - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -2015,9 +1435,9 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.getVolumeByName.side_effect = ( hpexceptions.HTTPNotFound('fake')) - self.driver.proxy.api_version = "1.1" + self.driver.api_version = "1.1" - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client mock_client.getVolumes.return_value = { @@ -2059,9 +1479,9 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): }] } - self.driver.proxy.api_version = "1.1" + self.driver.api_version = "1.1" - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client self.driver.unmanage(self.volume) @@ -2080,12 +1500,12 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): def test_api_version(self): self.setup_driver() - self.driver.proxy.api_version = "1.1" - self.driver.proxy._check_api_version() + self.driver.api_version = "1.1" + self.driver._check_api_version() - self.driver.proxy.api_version = "1.0" + self.driver.api_version = "1.0" self.assertRaises(exception.InvalidInput, - self.driver.proxy._check_api_version) + self.driver._check_api_version) def test_get_volume_stats(self): @@ -2103,7 +1523,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): }] } - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -2148,7 +1568,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): # set up driver with default config mock_client = self.setup_driver() - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -2175,7 +1595,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): expected_volumes = [mock_volume] self.driver.db.volume_get_all_by_group.return_value = expected_volumes - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -2213,7 +1633,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.createVolume.return_value = { 'iscsiIqn': self.connector['initiator']} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -2255,7 +1675,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock_client.createVolume.return_value = { 'iscsiIqn': self.connector['initiator']} - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -2300,7 +1720,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): expected_snaps = [mock_snap] mock_snap_list.return_value = expected_snaps - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client @@ -2341,7 +1761,7 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): expected_snaps = [mock_snap] mock_snap_list.return_value = expected_snaps - with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver, '_create_client') as mock_do_setup: mock_do_setup.return_value = mock_client diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py deleted file mode 100644 index 2561ad1f0..000000000 --- a/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py +++ /dev/null @@ -1,491 +0,0 @@ -# (c) Copyright 2014 Hewlett-Packard Development Company, L.P. -# 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. -# -""" -HP LeftHand SAN ISCSI Driver. - -The driver communicates to the backend aka Cliq via SSH to perform all the -operations on the SAN. -""" - -from lxml import etree -from oslo_concurrency import processutils -from oslo_log import log as logging -from oslo_utils import units - -from cinder import exception -from cinder.i18n import _, _LE, _LW -from cinder.volume.drivers.san import san - - -LOG = logging.getLogger(__name__) - - -class HPLeftHandCLIQProxy(san.SanISCSIDriver): - """Executes commands relating to HP/LeftHand SAN ISCSI volumes. - - We use the CLIQ interface, over SSH. - - Rough overview of CLIQ commands used: - - :createVolume: (creates the volume) - - :deleteVolume: (deletes the volume) - - :modifyVolume: (extends the volume) - - :createSnapshot: (creates the snapshot) - - :deleteSnapshot: (deletes the snapshot) - - :cloneSnapshot: (creates the volume from a snapshot) - - :getVolumeInfo: (to discover the IQN etc) - - :getSnapshotInfo: (to discover the IQN etc) - - :getClusterInfo: (to discover the iSCSI target IP address) - - The 'trick' here is that the HP SAN enforces security by default, so - normally a volume mount would need both to configure the SAN in the volume - layer and do the mount on the compute layer. Multi-layer operations are - not catered for at the moment in the cinder architecture, so instead we - share the volume using CHAP at volume creation time. Then the mount need - only use those CHAP credentials, so can take place exclusively in the - compute layer. - - Version history: - 1.0.0 - Initial driver - 1.1.0 - Added create/delete snapshot, extend volume, create volume - from snapshot support. - 1.2.0 - Ported into the new HP LeftHand driver. - 1.2.1 - Fixed bug #1279897, HP LeftHand CLIQ proxy may return incorrect - capacity values. - 1.2.2 - Fixed driver with Paramiko 1.13.0, bug #1298608. - 1.2.3 - Added update_migrated_volume #1493546 - """ - - VERSION = "1.2.3" - - device_stats = {} - - def __init__(self, *args, **kwargs): - super(HPLeftHandCLIQProxy, self).__init__(*args, **kwargs) - self.cluster_vip = None - LOG.warning(_LW('The HPLeftHandISCSIDriver CLIQ driver has been ' - 'DEPRECATED as of the 2015.2 release. This driver ' - 'will be removed in the 2016.1 release. Please use ' - 'the HPLeftHandISCSIDriver REST based driver ' - 'instead.')) - - def do_setup(self, context): - pass - - def check_for_setup_error(self): - pass - - def get_version_string(self): - return (_('CLIQ %(proxy_ver)s') % {'proxy_ver': self.VERSION}) - - def _cliq_run(self, verb, cliq_args, check_exit_code=True): - """Runs a CLIQ command over SSH, without doing any result parsing.""" - cmd_list = [verb] - for k, v in cliq_args.items(): - cmd_list.append("%s=%s" % (k, v)) - - return self._run_ssh(cmd_list, check_exit_code) - - def _cliq_run_xml(self, verb, cliq_args, check_cliq_result=True): - """Runs a CLIQ command over SSH, parsing and checking the output.""" - cliq_args['output'] = 'XML' - (out, _err) = self._cliq_run(verb, cliq_args, check_cliq_result) - - LOG.debug("CLIQ command returned %s", out) - - result_xml = etree.fromstring(out.encode('utf8')) - if check_cliq_result: - response_node = result_xml.find("response") - if response_node is None: - msg = (_("Malformed response to CLIQ command " - "%(verb)s %(cliq_args)s. Result=%(out)s") % - {'verb': verb, 'cliq_args': cliq_args, 'out': out}) - raise exception.VolumeBackendAPIException(data=msg) - - result_code = response_node.attrib.get("result") - - if result_code != "0": - msg = (_("Error running CLIQ command %(verb)s %(cliq_args)s. " - " Result=%(out)s") % - {'verb': verb, 'cliq_args': cliq_args, 'out': out}) - raise exception.VolumeBackendAPIException(data=msg) - - return result_xml - - def _cliq_get_cluster_info(self, cluster_name): - """Queries for info about the cluster (including IP).""" - cliq_args = {} - cliq_args['clusterName'] = cluster_name - cliq_args['searchDepth'] = '1' - cliq_args['verbose'] = '0' - - result_xml = self._cliq_run_xml("getClusterInfo", cliq_args) - - return result_xml - - def _cliq_get_cluster_vip(self, cluster_name): - """Gets the IP on which a cluster shares iSCSI volumes.""" - cluster_xml = self._cliq_get_cluster_info(cluster_name) - - vips = [] - for vip in cluster_xml.findall("response/cluster/vip"): - vips.append(vip.attrib.get('ipAddress')) - - if len(vips) == 1: - return vips[0] - - _xml = etree.tostring(cluster_xml) - msg = (_("Unexpected number of virtual ips for cluster " - " %(cluster_name)s. Result=%(_xml)s") % - {'cluster_name': cluster_name, '_xml': _xml}) - raise exception.VolumeBackendAPIException(data=msg) - - def _cliq_get_volume_info(self, volume_name): - """Gets the volume info, including IQN.""" - cliq_args = {} - cliq_args['volumeName'] = volume_name - result_xml = self._cliq_run_xml("getVolumeInfo", cliq_args) - - # Result looks like this: - # - # - # - # - # - # - # - # - - # Flatten the nodes into a dictionary; use prefixes to avoid collisions - volume_attributes = {} - - volume_node = result_xml.find("response/volume") - for k, v in volume_node.attrib.items(): - volume_attributes["volume." + k] = v - - status_node = volume_node.find("status") - if status_node is not None: - for k, v in status_node.attrib.items(): - volume_attributes["status." + k] = v - - # We only consider the first permission node - permission_node = volume_node.find("permission") - if permission_node is not None: - for k, v in status_node.attrib.items(): - volume_attributes["permission." + k] = v - - LOG.debug("Volume info: %(volume_name)s => %(volume_attributes)s", - {'volume_name': volume_name, - 'volume_attributes': volume_attributes}) - return volume_attributes - - def _cliq_get_snapshot_info(self, snapshot_name): - """Gets the snapshot info, including IQN.""" - cliq_args = {} - cliq_args['snapshotName'] = snapshot_name - result_xml = self._cliq_run_xml("getSnapshotInfo", cliq_args) - - # Result looks like this: - # - # - # - # - # - # - # - # - - # Flatten the nodes into a dictionary; use prefixes to avoid collisions - snapshot_attributes = {} - - snapshot_node = result_xml.find("response/snapshot") - for k, v in snapshot_node.attrib.items(): - snapshot_attributes["snapshot." + k] = v - - status_node = snapshot_node.find("status") - if status_node is not None: - for k, v in status_node.attrib.items(): - snapshot_attributes["status." + k] = v - - # We only consider the first permission node - permission_node = snapshot_node.find("permission") - if permission_node is not None: - for k, v in status_node.attrib.items(): - snapshot_attributes["permission." + k] = v - - LOG.debug("Snapshot info: %(name)s => %(attributes)s", - {'name': snapshot_name, 'attributes': snapshot_attributes}) - return snapshot_attributes - - def create_volume(self, volume): - """Creates a volume.""" - cliq_args = {} - cliq_args['clusterName'] = self.configuration.san_clustername - - if self.configuration.san_thin_provision: - cliq_args['thinProvision'] = '1' - else: - cliq_args['thinProvision'] = '0' - - cliq_args['volumeName'] = volume['name'] - cliq_args['size'] = '%sGB' % volume['size'] - - self._cliq_run_xml("createVolume", cliq_args) - - return self._get_model_update(volume['name']) - - def extend_volume(self, volume, new_size): - """Extend the size of an existing volume.""" - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['size'] = '%sGB' % new_size - - self._cliq_run_xml("modifyVolume", cliq_args) - - def create_volume_from_snapshot(self, volume, snapshot): - """Creates a volume from a snapshot.""" - cliq_args = {} - cliq_args['snapshotName'] = snapshot['name'] - cliq_args['volumeName'] = volume['name'] - - self._cliq_run_xml("cloneSnapshot", cliq_args) - - return self._get_model_update(volume['name']) - - def create_snapshot(self, snapshot): - """Creates a snapshot.""" - cliq_args = {} - cliq_args['snapshotName'] = snapshot['name'] - cliq_args['volumeName'] = snapshot['volume_name'] - cliq_args['inheritAccess'] = 1 - self._cliq_run_xml("createSnapshot", cliq_args) - - def delete_volume(self, volume): - """Deletes a volume.""" - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['prompt'] = 'false' # Don't confirm - try: - self._cliq_get_volume_info(volume['name']) - except processutils.ProcessExecutionError: - LOG.error(_LE("Volume did not exist. It will not be deleted")) - return - self._cliq_run_xml("deleteVolume", cliq_args) - - def delete_snapshot(self, snapshot): - """Deletes a snapshot.""" - cliq_args = {} - cliq_args['snapshotName'] = snapshot['name'] - cliq_args['prompt'] = 'false' # Don't confirm - try: - self._cliq_get_snapshot_info(snapshot['name']) - except processutils.ProcessExecutionError: - LOG.error(_LE("Snapshot did not exist. It will not be deleted")) - return - try: - self._cliq_run_xml("deleteSnapshot", cliq_args) - except Exception as ex: - in_use_msg = 'cannot be deleted because it is a clone point' - if in_use_msg in ex.message: - raise exception.SnapshotIsBusy(ex) - - raise exception.VolumeBackendAPIException(ex) - - def local_path(self, volume): - msg = _("local_path not supported") - raise exception.VolumeBackendAPIException(data=msg) - - 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. HP VSA requires a volume to be assigned - to a server. - - This driver returns a driver_volume_type of 'iscsi'. - The format of the driver data is defined in _get_iscsi_properties. - Example return value: - - { - 'driver_volume_type': 'iscsi' - 'data': { - 'target_discovered': True, - 'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001', - 'target_protal': '127.0.0.1:3260', - 'volume_id': 1, - } - } - - """ - self._create_server(connector) - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['serverName'] = connector['host'] - self._cliq_run_xml("assignVolumeToServer", cliq_args) - - iscsi_data = self._get_iscsi_properties(volume) - return { - 'driver_volume_type': 'iscsi', - 'data': iscsi_data - } - - def _create_server(self, connector): - cliq_args = {} - cliq_args['serverName'] = connector['host'] - out = self._cliq_run_xml("getServerInfo", cliq_args, False) - response = out.find("response") - result = response.attrib.get("result") - if result != '0': - cliq_args = {} - cliq_args['serverName'] = connector['host'] - cliq_args['initiator'] = connector['initiator'] - self._cliq_run_xml("createServer", cliq_args) - - def _get_model_update(self, volume_name): - volume_info = self._cliq_get_volume_info(volume_name) - cluster_name = volume_info['volume.clusterName'] - iscsi_iqn = volume_info['volume.iscsiIqn'] - - # TODO(justinsb): Is this always 1? Does it matter? - cluster_interface = '1' - - if not self.cluster_vip: - self.cluster_vip = self._cliq_get_cluster_vip(cluster_name) - iscsi_portal = self.cluster_vip + ":3260," + cluster_interface - - model_update = {} - - # NOTE(jdg): LH volumes always at lun 0 ? - model_update['provider_location'] = ("%s %s %s" % - (iscsi_portal, - iscsi_iqn, - 0)) - return model_update - - def terminate_connection(self, volume, connector, **kwargs): - """Unassign the volume from the host.""" - cliq_args = {} - cliq_args['volumeName'] = volume['name'] - cliq_args['serverName'] = connector['host'] - self._cliq_run_xml("unassignVolumeToServer", cliq_args) - - def get_volume_stats(self, refresh=False): - if refresh: - self._update_backend_status() - - return self.device_stats - - def _update_backend_status(self): - data = {} - backend_name = self.configuration.safe_get('volume_backend_name') - data['volume_backend_name'] = backend_name or self.__class__.__name__ - data['reserved_percentage'] = 0 - data['storage_protocol'] = 'iSCSI' - data['vendor_name'] = 'Hewlett-Packard' - - result_xml = self._cliq_run_xml( - "getClusterInfo", { - 'searchDepth': 1, - 'clusterName': self.configuration.san_clustername}) - cluster_node = result_xml.find("response/cluster") - total_capacity = cluster_node.attrib.get("spaceTotal") - free_capacity = cluster_node.attrib.get("unprovisionedSpace") - GB = units.Gi - - data['total_capacity_gb'] = int(total_capacity) / GB - data['free_capacity_gb'] = int(free_capacity) / GB - self.device_stats = data - - def create_cloned_volume(self, volume, src_vref): - raise NotImplementedError() - - def create_export(self, context, volume, connector): - pass - - def ensure_export(self, context, volume): - pass - - def remove_export(self, context, volume): - pass - - def retype(self, context, 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 migrate - :param new_type: A dictionary describing the volume type to convert to - :param diff: A dictionary with the difference between the two types - """ - return False - - def migrate_volume(self, ctxt, volume, host): - """Migrate the volume to the specified host. - - 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. - """ - return (False, None) - - def update_migrated_volume(self, context, volume, new_volume, - original_volume_status): - raise NotImplementedError() diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py index 01ed065be..fe6b6ba97 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py @@ -1,4 +1,4 @@ -# (c) Copyright 2014 Hewlett-Packard Development Company, L.P. +# (c) Copyright 2014-2015 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,199 +13,1013 @@ # License for the specific language governing permissions and limitations # under the License. # -""" -Volume driver for HP LeftHand Storage array. -This driver requires 11.5 or greater firmware on the LeftHand array, using -the 1.0 or greater version of the hplefthandclient. - -You will need to install the python hplefthandclient. -sudo pip install hplefthandclient - -Set the following in the cinder.conf file to enable the -LeftHand Channel Driver along with the required flags: - -volume_driver=cinder.volume.drivers.san.hp.hp_lefthand_iscsi. - HPLeftHandISCSIDriver - -It also requires the setting of hplefthand_api_url, hplefthand_username, -hplefthand_password for credentials to talk to the REST service on the -LeftHand array. -""" +"""HP LeftHand SAN ISCSI REST Proxy.""" +from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import excutils +from oslo_utils import importutils +from oslo_utils import units +from cinder import context from cinder import exception -from cinder.i18n import _, _LI +from cinder.i18n import _, _LE, _LI, _LW +from cinder import objects from cinder.volume import driver -from cinder.volume.drivers.san.hp import hp_lefthand_cliq_proxy as cliq_proxy -from cinder.volume.drivers.san.hp import hp_lefthand_rest_proxy as rest_proxy +from cinder.volume import utils +from cinder.volume import volume_types + +import six + +import math +import re LOG = logging.getLogger(__name__) +hplefthandclient = importutils.try_import("hplefthandclient") +if hplefthandclient: + from hplefthandclient import client as hp_lh_client + from hplefthandclient import exceptions as hpexceptions + +hplefthand_opts = [ + cfg.StrOpt('hplefthand_api_url', + help="HP LeftHand WSAPI Server Url like " + "https://:8081/lhos"), + cfg.StrOpt('hplefthand_username', + help="HP LeftHand Super user username"), + cfg.StrOpt('hplefthand_password', + help="HP LeftHand Super user password", + secret=True), + cfg.StrOpt('hplefthand_clustername', + help="HP LeftHand cluster name"), + cfg.BoolOpt('hplefthand_iscsi_chap_enabled', + default=False, + help='Configure CHAP authentication for iSCSI connections ' + '(Default: Disabled)'), + cfg.BoolOpt('hplefthand_debug', + default=False, + help="Enable HTTP debugging to LeftHand"), + +] + +CONF = cfg.CONF +CONF.register_opts(hplefthand_opts) + +MIN_API_VERSION = "1.1" MIN_CLIENT_VERSION = '1.0.4' +MIN_CG_CLIENT_VERSION = "1.0.6" +# map the extra spec key to the REST client option key +extra_specs_key_map = { + 'hplh:provisioning': 'isThinProvisioned', + 'hplh:ao': 'isAdaptiveOptimizationEnabled', + 'hplh:data_pl': 'dataProtectionLevel', +} -class HPLeftHandISCSIDriver(driver.TransferVD, - driver.ManageableVD, - driver.ExtendVD, - driver.SnapshotVD, - driver.MigrateVD, - driver.BaseVD, - driver.ConsistencyGroupVD): - """Executes commands relating to HP/LeftHand SAN ISCSI volumes. +# 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} +} + + +class HPLeftHandISCSIDriver(driver.ISCSIDriver): + """Executes REST commands relating to HP/LeftHand SAN ISCSI volumes. Version history: - 1.0.0 - Initial driver + 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 - Fix for no handler for logger during tests - 1.0.4 - Removing locks bug #1395953 - 1.0.5 - Adding support for manage/unmanage. - 1.0.6 - Updated minimum client version. bug #1432757 - 1.0.7 - Update driver to use ABC metaclasses - 1.0.8 - Adds consistency group support - 1.0.9 - Added update_migrated_volume #1493546 + 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 """ - VERSION = "1.0.9" + VERSION = "1.0.14" + + device_stats = {} def __init__(self, *args, **kwargs): super(HPLeftHandISCSIDriver, self).__init__(*args, **kwargs) - self.proxy = None - self.args = args - self.kwargs = kwargs + self.configuration.append_config_values(hplefthand_opts) + if not self.configuration.hplefthand_api_url: + raise exception.NotFound(_("HPLeftHand url not found")) - def _create_proxy(self, *args, **kwargs): + # 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.db = kwargs.get('db') + + def _login(self): + client = self._create_client() try: - proxy = rest_proxy.HPLeftHandRESTProxy(*args, **kwargs) - except exception.NotFound: - proxy = cliq_proxy.HPLeftHandCLIQProxy(*args, **kwargs) + if self.configuration.hplefthand_debug: + client.debug_rest(True) - return proxy + client.login( + self.configuration.hplefthand_username, + self.configuration.hplefthand_password) - def check_for_setup_error(self): - self.proxy.check_for_setup_error() + cluster_info = client.getClusterByName( + self.configuration.hplefthand_clustername) + self.cluster_id = cluster_info['id'] + virtual_ips = cluster_info['virtualIPAddresses'] + self.cluster_vip = virtual_ips[0]['ipV4Address'] + + return client + except hpexceptions.HTTPNotFound: + raise exception.DriverNotInitialized( + _('LeftHand cluster not found')) + except Exception as ex: + raise exception.DriverNotInitialized(ex) + + def _logout(self, client): + client.logout() + + def _create_client(self): + return hp_lh_client.HPLeftHandClient( + self.configuration.hplefthand_api_url) def do_setup(self, context): - self.proxy = self._create_proxy(*self.args, **self.kwargs) + """Set up LeftHand client.""" + if hplefthandclient.version < MIN_CLIENT_VERSION: + ex_msg = (_("Invalid hplefthandclient version found (" + "%(found)s). Version %(minimum)s or greater " + "required.") + % {'found': hplefthandclient.version, + 'minimum': MIN_CLIENT_VERSION}) + LOG.error(ex_msg) + raise exception.InvalidInput(reason=ex_msg) + + 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(_LI("HPLeftHand driver %(driver_ver)s, " - "proxy %(proxy_ver)s"), { - "driver_ver": self.VERSION, - "proxy_ver": self.proxy.get_version_string()}) + LOG.info(_LI("HPLeftHand API version %s"), self.api_version) - if isinstance(self.proxy, cliq_proxy.HPLeftHandCLIQProxy): - self.proxy.do_setup(context) - else: - # Check minimum client version for REST proxy - client_version = rest_proxy.hplefthandclient.version - - if client_version < MIN_CLIENT_VERSION: - ex_msg = (_("Invalid hplefthandclient version found (" - "%(found)s). Version %(minimum)s or greater " - "required.") - % {'found': client_version, - 'minimum': MIN_CLIENT_VERSION}) - LOG.error(ex_msg) - raise exception.InvalidInput(reason=ex_msg) + if self.api_version < MIN_API_VERSION: + LOG.warning(_LW("HPLeftHand 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 get_version_string(self): + return (_('REST %(proxy_ver)s hplefthandclient %(rest_ver)s') % { + 'proxy_ver': self.VERSION, + 'rest_ver': hplefthandclient.get_version_string()}) def create_volume(self, volume): """Creates a volume.""" - return self.proxy.create_volume(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.configuration.hplefthand_clustername + optional['clusterName'] = clusterName + + volume_info = client.createVolume( + volume['name'], self.cluster_id, + volume['size'] * units.Gi, + optional) + + return self._update_provider(volume_info) + except Exception as ex: + raise exception.VolumeBackendAPIException(data=ex) + finally: + self._logout(client) + + def delete_volume(self, volume): + """Deletes a volume.""" + client = self._login() + try: + volume_info = client.getVolumeByName(volume['name']) + client.deleteVolume(volume_info['id']) + except hpexceptions.HTTPNotFound: + LOG.error(_LE("Volume did not exist. It will not be deleted")) + except Exception as ex: + raise exception.VolumeBackendAPIException(ex) + finally: + self._logout(client) def extend_volume(self, volume, new_size): """Extend the size of an existing volume.""" - self.proxy.extend_volume(volume, new_size) + 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) def create_consistencygroup(self, context, group): - """Creates a consistency group.""" - return self.proxy.create_consistencygroup(context, group) + """Creates a consistencygroup.""" + model_update = {'status': 'available'} + return model_update def create_consistencygroup_from_src(self, context, group, volumes, cgsnapshot=None, snapshots=None, source_cg=None, source_vols=None): """Creates a consistency group from a source""" - return self.proxy.create_consistencygroup_from_src( - context, group, volumes, cgsnapshot, snapshots, source_cg, - source_vols) + msg = _("Creating a consistency group from a source is not " + "currently supported.") + LOG.error(msg) + raise NotImplementedError(msg) def delete_consistencygroup(self, context, group, volumes): """Deletes a consistency group.""" - return self.proxy.delete_consistencygroup(context, group) + # TODO(aorourke): Can't eliminate the DB calls here due to CG API. + # Will fix in M release + volumes = self.db.volume_get_all_by_group(context, group.id) + for volume in volumes: + self.delete_volume(volume) + volume.status = 'deleted' + + model_update = {'status': group.status} + + return model_update, volumes def update_consistencygroup(self, context, group, add_volumes=None, remove_volumes=None): - """Updates a consistency group.""" - return self.proxy.update_consistencygroup(context, group, add_volumes, - remove_volumes) + """Updates a consistency group. + + Because the backend has no concept of volume grouping, cinder will + maintain all volume/consistency 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 consistency group. + """ + return None, None, None def create_cgsnapshot(self, context, cgsnapshot, snapshots): """Creates a consistency group snapshot.""" - return self.proxy.create_cgsnapshot(context, cgsnapshot) + client = self._login() + try: + # TODO(aorourke): Can't eliminate the DB calls here due to CG API. + # Will fix in M release + snapshots = objects.SnapshotList().get_all_for_cgsnapshot( + context, cgsnapshot['id']) + + snap_set = [] + snapshot_base_name = "snapshot-" + cgsnapshot['id'] + 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(_LE("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.status = 'available' + + source_volume_id = snap_set[0]['volumeId'] + optional = {'inheritAccess': True} + description = cgsnapshot.get('description', None) + 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(_LE("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, snapshots def delete_cgsnapshot(self, context, cgsnapshot, snapshots): """Deletes a consistency group snapshot.""" - return self.proxy.delete_cgsnapshot(context, cgsnapshot) - def create_volume_from_snapshot(self, volume, snapshot): - """Creates a volume from a snapshot.""" - return self.proxy.create_volume_from_snapshot(volume, snapshot) + client = self._login() + try: + snap_name_base = "snapshot-" + cgsnapshot['id'] + + # TODO(aorourke): Can't eliminate the DB calls here due to CG API. + # Will fix in M release + snapshots = objects.SnapshotList().get_all_for_cgsnapshot( + context, cgsnapshot['id']) + + for i, snapshot in enumerate(snapshots): + try: + snap_name = snap_name_base + "-" + six.text_type(i) + snap_info = client.getSnapshotByName(snap_name) + client.deleteSnapshot(snap_info['id']) + except hpexceptions.HTTPNotFound: + LOG.error(_LE("Snapshot did not exist. It will not be " + "deleted.")) + except hpexceptions.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=snap_name) + + raise exception.VolumeBackendAPIException( + data=six.text_type(ex)) + + except Exception as ex: + raise exception.VolumeBackendAPIException( + data=six.text_type(ex)) + finally: + self._logout(client) + + model_update = {'status': cgsnapshot['status']} + + return model_update, snapshots def create_snapshot(self, snapshot): """Creates a snapshot.""" - self.proxy.create_snapshot(snapshot) + client = self._login() + try: + volume_info = client.getVolumeByName(snapshot['volume_name']) - def delete_volume(self, volume): - """Deletes a volume.""" - self.proxy.delete_volume(volume) + option = {'inheritAccess': True} + client.createSnapshot(snapshot['name'], + volume_info['id'], + option) + except Exception as ex: + raise exception.VolumeBackendAPIException(ex) + finally: + self._logout(client) def delete_snapshot(self, snapshot): """Deletes a snapshot.""" - self.proxy.delete_snapshot(snapshot) + client = self._login() + try: + snap_info = client.getSnapshotByName(snapshot['name']) + client.deleteSnapshot(snap_info['id']) + except hpexceptions.HTTPNotFound: + LOG.error(_LE("Snapshot did not exist. It will not be deleted")) + except hpexceptions.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) + + 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' + data['location_info'] = (self.DRIVER_LOCATION % { + 'cluster': self.configuration.hplefthand_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.configuration.hplefthand_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() + if hplefthandclient.version >= MIN_CG_CLIENT_VERSION: + data['consistencygroup_support'] = True + + self.device_stats = data def initialize_connection(self, volume, connector): - """Assigns the volume to a server.""" - return self.proxy.initialize_connection(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. HP 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) def terminate_connection(self, volume, connector, **kwargs): """Unassign the volume from the host.""" - self.proxy.terminate_connection(volume, connector) + client = self._login() + try: + volume_info = client.getVolumeByName(volume['name']) + server_info = client.getServerByName(connector['host']) + volume_list = client.findServerVolumes(server_info['name']) - def get_volume_stats(self, refresh=False): - data = self.proxy.get_volume_stats(refresh) - data['driver_version'] = self.VERSION - return data + 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 Exception as ex: + raise exception.VolumeBackendAPIException(ex) + finally: + self._logout(client) + + 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']) + return self._update_provider(volume_info) + except Exception as ex: + raise exception.VolumeBackendAPIException(ex) + finally: + self._logout(client) def create_cloned_volume(self, volume, src_vref): - return self.proxy.create_cloned_volume(volume, src_vref) + client = self._login() + try: + volume_info = client.getVolumeByName(src_vref['name']) + clone_info = client.cloneVolume(volume['name'], volume_info['id']) + return self._update_provider(clone_info) + 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: + 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(_LE("'%(value)s' is an invalid value " + "for extra spec '%(key)s'"), + {'value': value, 'key': key}) + return client_options + + def _update_provider(self, volume_info): + # TODO(justinsb): Is this always 1? Does it matter? + cluster_interface = '1' + iscsi_portal = self.cluster_vip + ":3260," + cluster_interface + + return {'provider_location': ( + "%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))} + + def _create_server(self, connector, client): + server_info = None + chap_enabled = self.configuration.hplefthand_iscsi_chap_enabled + try: + server_info = client.getServerByName(connector['host']) + chap_secret = server_info['chapTargetSecret'] + if not chap_enabled and chap_secret: + LOG.warning(_LW('CHAP secret exists for host %s but CHAP is ' + 'disabled'), connector['host']) + if chap_enabled and chap_secret is None: + LOG.warning(_LW('CHAP is enabled, but server secret not ' + 'configured on server %s'), connector['host']) + return server_info + except hpexceptions.HTTPNotFound: + # server does not exist, so create one + pass + + optional = None + if chap_enabled: + chap_secret = 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): - return self.proxy.create_export(context, volume, connector) + pass def ensure_export(self, context, volume): - return self.proxy.ensure_export(context, volume) + pass def remove_export(self, context, volume): - return self.proxy.remove_export(context, volume) + pass - def retype(self, context, volume, new_type, diff, host): - """Convert the volume to be of the new type.""" - return self.proxy.retype(context, volume, new_type, diff, host) + 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') + 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(): + (old, new) = diff['extra_specs'][key] + if old != new: + 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 hpexceptions.HTTPNotFound: + raise exception.VolumeNotFound(volume_id=volume['id']) + except Exception as ex: + LOG.warning(_LW("%s"), ex) + finally: + self._logout(client) + + return False def migrate_volume(self, ctxt, volume, host): - """Migrate directly if source and dest are managed by same storage.""" - return self.proxy.migrate_volume(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. + """ + LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, ' + 'cluster=%(cluster)s', { + 'id': volume['id'], + 'host': host, + 'cluster': self.configuration.hplefthand_clustername}) + + 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() + 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(_LI("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(_LI("Cannot provide backend assisted migration for " + "volume: %s because cluster exists in different " + "management group."), volume['name']) + return false_ret + + except hpexceptions.HTTPNotFound: + LOG.info(_LI("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(_LI("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(_LI("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 hpexceptions.HTTPNotFound: + LOG.info(_LI("Cannot provide backend assisted migration for " + "volume: %s because volume does not exist in this " + "management group."), volume['name']) + return false_ret + except hpexceptions.HTTPServerError as ex: + LOG.error(_LE("Exception: %s"), ex) + return false_ret + finally: + self._logout(client) + + return (True, None) def update_migrated_volume(self, context, volume, new_volume, original_volume_status): - return self.proxy.update_migrated_volume(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(_LI("Volume name changed from %(tmp)s to %(orig)s."), + {'tmp': current_name, 'orig': original_name}) + except Exception as e: + LOG.error(_LE("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} def manage_existing(self, volume, existing_ref): - return self.proxy.manage_existing(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 hpexceptions.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(_LI("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(_LI("Virtual volume %(disp)s '%(new)s' is " + "being retyped."), + {'disp': display_name, 'new': new_vol_name}) + + try: + self.retype(None, + volume, + volume_type, + volume_type['extra_specs'], + volume['host']) + LOG.info(_LI("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(_LW("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(_LI("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 def manage_existing_get_size(self, volume, existing_ref): - return self.proxy.manage_existing_get_size(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 hpexceptions.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)) def unmanage(self, volume): - return self.proxy.unmanage(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(_LI("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}) + + 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 HPLeftHand 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) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py deleted file mode 100644 index 9063eae37..000000000 --- a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py +++ /dev/null @@ -1,1016 +0,0 @@ -# (c) Copyright 2014-2015 Hewlett-Packard Development Company, L.P. -# 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. -# -"""HP LeftHand SAN ISCSI REST Proxy.""" - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_utils import excutils -from oslo_utils import importutils -from oslo_utils import units - -from cinder import context -from cinder import exception -from cinder.i18n import _, _LE, _LI, _LW -from cinder import objects -from cinder.volume import driver -from cinder.volume import utils -from cinder.volume import volume_types - -import six - -import math -import re - -LOG = logging.getLogger(__name__) - -hplefthandclient = importutils.try_import("hplefthandclient") -if hplefthandclient: - from hplefthandclient import client as hp_lh_client - from hplefthandclient import exceptions as hpexceptions - -hplefthand_opts = [ - cfg.StrOpt('hplefthand_api_url', - help="HP LeftHand WSAPI Server Url like " - "https://:8081/lhos"), - cfg.StrOpt('hplefthand_username', - help="HP LeftHand Super user username"), - cfg.StrOpt('hplefthand_password', - help="HP LeftHand Super user password", - secret=True), - cfg.StrOpt('hplefthand_clustername', - help="HP LeftHand cluster name"), - cfg.BoolOpt('hplefthand_iscsi_chap_enabled', - default=False, - help='Configure CHAP authentication for iSCSI connections ' - '(Default: Disabled)'), - cfg.BoolOpt('hplefthand_debug', - default=False, - help="Enable HTTP debugging to LeftHand"), - -] - -CONF = cfg.CONF -CONF.register_opts(hplefthand_opts) - -MIN_API_VERSION = "1.1" -MIN_CG_CLIENT_VERSION = "1.0.6" - -# map the extra spec key to the REST client option key -extra_specs_key_map = { - '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} -} - - -class HPLeftHandRESTProxy(driver.ISCSIDriver): - """Executes REST commands relating to HP/LeftHand SAN ISCSI volumes. - - Version history: - 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 - """ - - VERSION = "1.0.13" - - device_stats = {} - - def __init__(self, *args, **kwargs): - super(HPLeftHandRESTProxy, self).__init__(*args, **kwargs) - self.configuration.append_config_values(hplefthand_opts) - if not self.configuration.hplefthand_api_url: - raise exception.NotFound(_("HPLeftHand 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.db = kwargs.get('db') - - def _login(self): - client = self.do_setup(None) - return client - - def _logout(self, client): - client.logout() - - def _create_client(self): - return hp_lh_client.HPLeftHandClient( - self.configuration.hplefthand_api_url) - - def do_setup(self, context): - """Set up LeftHand client.""" - try: - client = self._create_client() - client.login( - self.configuration.hplefthand_username, - self.configuration.hplefthand_password) - - if self.configuration.hplefthand_debug: - client.debug_rest(True) - - cluster_info = client.getClusterByName( - self.configuration.hplefthand_clustername) - self.cluster_id = cluster_info['id'] - virtual_ips = cluster_info['virtualIPAddresses'] - self.cluster_vip = virtual_ips[0]['ipV4Address'] - self._update_backend_status(client) - - return client - except hpexceptions.HTTPNotFound: - raise exception.DriverNotInitialized( - _('LeftHand cluster not found')) - except Exception as ex: - raise exception.DriverNotInitialized(ex) - - 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(_LI("HPLeftHand API version %s"), self.api_version) - - if self.api_version < MIN_API_VERSION: - LOG.warning(_LW("HPLeftHand 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 get_version_string(self): - return (_('REST %(proxy_ver)s hplefthandclient %(rest_ver)s') % { - 'proxy_ver': self.VERSION, - 'rest_ver': hplefthandclient.get_version_string()}) - - 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.configuration.hplefthand_clustername - optional['clusterName'] = clusterName - - volume_info = client.createVolume( - volume['name'], self.cluster_id, - volume['size'] * units.Gi, - optional) - - return self._update_provider(volume_info) - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - def delete_volume(self, volume): - """Deletes a volume.""" - client = self._login() - try: - volume_info = client.getVolumeByName(volume['name']) - client.deleteVolume(volume_info['id']) - except hpexceptions.HTTPNotFound: - LOG.error(_LE("Volume did not exist. It will not be deleted")) - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - 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) - - def create_consistencygroup(self, context, group): - """Creates a consistencygroup.""" - model_update = {'status': 'available'} - return model_update - - def create_consistencygroup_from_src(self, context, group, volumes, - cgsnapshot=None, snapshots=None, - source_cg=None, source_vols=None): - """Creates a consistency group from a source""" - LOG.error(_LE("Creating a consistency group from a source is not " - "currently supported.")) - raise NotImplementedError() - - def delete_consistencygroup(self, context, group): - """Deletes a consistency group.""" - # TODO(aorourke): Can't eliminate the DB calls here due to CG API. - # Will fix in M release - volumes = self.db.volume_get_all_by_group(context, group.id) - for volume in volumes: - self.delete_volume(volume) - volume.status = 'deleted' - - model_update = {'status': group.status} - - return model_update, volumes - - def update_consistencygroup(self, context, group, - add_volumes=None, remove_volumes=None): - """Updates a consistency group. - - Because the backend has no concept of volume grouping, cinder will - maintain all volume/consistency 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 consistency group. - """ - return None, None, None - - def create_cgsnapshot(self, context, cgsnapshot): - """Creates a consistency group snapshot.""" - client = self._login() - try: - # TODO(aorourke): Can't eliminate the DB calls here due to CG API. - # Will fix in M release - snapshots = objects.SnapshotList().get_all_for_cgsnapshot( - context, cgsnapshot['id']) - - snap_set = [] - snapshot_base_name = "snapshot-" + cgsnapshot['id'] - 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(_LE("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.status = 'available' - - source_volume_id = snap_set[0]['volumeId'] - optional = {'inheritAccess': True} - description = cgsnapshot.get('description', None) - 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(_LE("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, snapshots - - def delete_cgsnapshot(self, context, cgsnapshot): - """Deletes a consistency group snapshot.""" - - client = self._login() - try: - snap_name_base = "snapshot-" + cgsnapshot['id'] - - # TODO(aorourke): Can't eliminate the DB calls here due to CG API. - # Will fix in M release - snapshots = objects.SnapshotList().get_all_for_cgsnapshot( - context, cgsnapshot['id']) - - for i, snapshot in enumerate(snapshots): - try: - snap_name = snap_name_base + "-" + six.text_type(i) - snap_info = client.getSnapshotByName(snap_name) - client.deleteSnapshot(snap_info['id']) - except hpexceptions.HTTPNotFound: - LOG.error(_LE("Snapshot did not exist. It will not be " - "deleted.")) - except hpexceptions.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=snap_name) - - raise exception.VolumeBackendAPIException( - data=six.text_type(ex)) - - except Exception as ex: - raise exception.VolumeBackendAPIException( - data=six.text_type(ex)) - finally: - self._logout(client) - - model_update = {'status': cgsnapshot['status']} - - return model_update, snapshots - - 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) - - def delete_snapshot(self, snapshot): - """Deletes a snapshot.""" - client = self._login() - try: - snap_info = client.getSnapshotByName(snapshot['name']) - client.deleteSnapshot(snap_info['id']) - except hpexceptions.HTTPNotFound: - LOG.error(_LE("Snapshot did not exist. It will not be deleted")) - except hpexceptions.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(ex) - - raise exception.VolumeBackendAPIException(ex) - - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - 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['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' - data['location_info'] = (self.DRIVER_LOCATION % { - 'cluster': self.configuration.hplefthand_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.configuration.hplefthand_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() - if hplefthandclient.version >= MIN_CG_CLIENT_VERSION: - data['consistencygroup_support'] = True - - self.device_stats = data - - 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. HP 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) - - 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 Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - 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']) - return self._update_provider(volume_info) - except Exception as ex: - raise exception.VolumeBackendAPIException(ex) - finally: - self._logout(client) - - 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']) - return self._update_provider(clone_info) - 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: - 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(_LE("'%(value)s' is an invalid value " - "for extra spec '%(key)s'"), - {'value': value, 'key': key}) - return client_options - - def _update_provider(self, volume_info): - # TODO(justinsb): Is this always 1? Does it matter? - cluster_interface = '1' - iscsi_portal = self.cluster_vip + ":3260," + cluster_interface - - return {'provider_location': ( - "%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))} - - def _create_server(self, connector, client): - server_info = None - chap_enabled = self.configuration.hplefthand_iscsi_chap_enabled - try: - server_info = client.getServerByName(connector['host']) - chap_secret = server_info['chapTargetSecret'] - if not chap_enabled and chap_secret: - LOG.warning(_LW('CHAP secret exists for host %s but CHAP is ' - 'disabled'), connector['host']) - if chap_enabled and chap_secret is None: - LOG.warning(_LW('CHAP is enabled, but server secret not ' - 'configured on server %s'), connector['host']) - return server_info - except hpexceptions.HTTPNotFound: - # server does not exist, so create one - pass - - optional = None - if chap_enabled: - chap_secret = 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 - - 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') - 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(): - (old, new) = diff['extra_specs'][key] - if old != new: - 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 hpexceptions.HTTPNotFound: - raise exception.VolumeNotFound(volume_id=volume['id']) - except Exception as ex: - LOG.warning(_LW("%s"), ex) - finally: - self._logout(client) - - return False - - 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. - """ - LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, ' - 'cluster=%(cluster)s', { - 'id': volume['id'], - 'host': host, - 'cluster': self.configuration.hplefthand_clustername}) - - 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() - 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(_LI("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(_LI("Cannot provide backend assisted migration for " - "volume: %s because cluster exists in different " - "management group."), volume['name']) - return false_ret - - except hpexceptions.HTTPNotFound: - LOG.info(_LI("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(_LI("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(_LI("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 hpexceptions.HTTPNotFound: - LOG.info(_LI("Cannot provide backend assisted migration for " - "volume: %s because volume does not exist in this " - "management group."), volume['name']) - return false_ret - except hpexceptions.HTTPServerError as ex: - LOG.error(_LE("Exception: %s"), ex) - return false_ret - finally: - self._logout(client) - - return (True, None) - - 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(_LI("Volume name changed from %(tmp)s to %(orig)s."), - {'tmp': current_name, 'orig': original_name}) - except Exception as e: - LOG.error(_LE("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} - - 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 hpexceptions.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(_LI("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(_LI("Virtual volume %(disp)s '%(new)s' is " - "being retyped."), - {'disp': display_name, 'new': new_vol_name}) - - try: - self.retype(None, - volume, - volume_type, - volume_type['extra_specs'], - volume['host']) - LOG.info(_LI("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(_LW("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(_LI("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 - - 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 hpexceptions.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)) - - 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(_LI("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}) - - 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 HPLeftHand 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)