From: Mark Sturdevant Date: Mon, 9 Jun 2014 02:57:21 +0000 (+0800) Subject: 3PAR with pool-aware-cinder-scheduler X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=b94146b348908462fafad23ac09699e50dd969ff;p=openstack-build%2Fcinder-build.git 3PAR with pool-aware-cinder-scheduler HP 3PAR support for the pool-aware scheduler. Adds support for a list of CPGs (pools). Uses model update when our volume-type CPG is used instead of the scheduler selected CPG. In cinder.conf, hp3par_cpg now accepts a list of CPGs. DocImpact Implements: blueprint pool-aware-cinder-scheduler-hp3par Change-Id: I74ef0af5390311eb1a529e0d5d4875c4f7d04156 --- diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index 8a0a70a95..a547bed3b 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -31,6 +31,7 @@ from cinder.volume.drivers.san.hp import hp_3par_common as hpcommon from cinder.volume.drivers.san.hp import hp_3par_fc as hpfcdriver from cinder.volume.drivers.san.hp import hp_3par_iscsi as hpdriver from cinder.volume import qos_specs +from cinder.volume import utils as volume_utils from cinder.volume import volume_types hpexceptions = hp3parclient.hpexceptions @@ -40,6 +41,8 @@ LOG = logging.getLogger(__name__) CONF = cfg.CONF HP3PAR_CPG = 'OpenStackCPG' +HP3PAR_CPG2 = 'fakepool' +HP3PAR_CPG_QOS = 'qospool' HP3PAR_CPG_SNAP = 'OpenStackCPGSnap' HP3PAR_USER_NAME = 'testUser' HP3PAR_USER_PASS = 'testPassword' @@ -109,6 +112,14 @@ class HP3PARBaseDriver(object): 'volume_type': None, 'volume_type_id': None} + volume_pool = {'name': VOLUME_NAME, + 'id': VOLUME_ID, + 'display_name': 'Foo Volume', + 'size': 2, + 'host': volume_utils.append_host(FAKE_HOST, HP3PAR_CPG2), + 'volume_type': None, + 'volume_type_id': None} + volume_qos = {'name': VOLUME_NAME, 'id': VOLUME_ID, 'display_name': 'Foo Volume', @@ -140,7 +151,8 @@ class HP3PARBaseDriver(object): volume_type = {'name': 'gold', 'deleted': False, 'updated_at': None, - 'extra_specs': {'qos:maxIOPS': '1000', + 'extra_specs': {'cpg': HP3PAR_CPG2, + 'qos:maxIOPS': '1000', 'qos:maxBWS': '50', 'qos:minIOPS': '100', 'qos:minBWS': '25', @@ -259,7 +271,7 @@ class HP3PARBaseDriver(object): 'name': 'blue', 'id': RETYPE_VOLUME_TYPE_ID, 'extra_specs': { - 'cpg': HP3PAR_CPG, + 'cpg': HP3PAR_CPG_QOS, 'snap_cpg': HP3PAR_CPG_SNAP, 'vvs': RETYPE_VVS_NAME, 'qos': RETYPE_QOS_SPECS, @@ -353,7 +365,7 @@ class HP3PARBaseDriver(object): configuration.hp3par_username = HP3PAR_USER_NAME configuration.hp3par_password = HP3PAR_USER_PASS configuration.hp3par_api_url = 'https://1.1.1.1/api/v1' - configuration.hp3par_cpg = HP3PAR_CPG + configuration.hp3par_cpg = [HP3PAR_CPG, HP3PAR_CPG2] configuration.hp3par_cpg_snap = HP3PAR_CPG_SNAP configuration.iscsi_ip_address = '1.1.1.2' configuration.iscsi_port = '1234' @@ -411,6 +423,7 @@ class HP3PARBaseDriver(object): conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT), mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG(HP3PAR_CPG2), mock.call.logout()] mock_client.assert_has_calls(expected) @@ -441,6 +454,7 @@ class HP3PARBaseDriver(object): conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT), mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG(HP3PAR_CPG2), mock.call.logout()] mock_client.assert_has_calls(expected) @@ -471,6 +485,7 @@ class HP3PARBaseDriver(object): conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT), mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG(HP3PAR_CPG2), mock.call.logout()] mock_client.assert_has_calls(expected) @@ -520,6 +535,30 @@ class HP3PARBaseDriver(object): mock_client.assert_has_calls(expected) + def test_create_volume_in_pool(self): + + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + mock_client = self.setup_driver() + return_model = self.driver.create_volume(self.volume_pool) + comment = ( + '{"display_name": "Foo Volume", "type": "OpenStack",' + ' "name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7",' + ' "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7"}') + expected = [ + mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), + mock.call.createVolume( + self.VOLUME_3PAR_NAME, + HP3PAR_CPG2, + 1907, { + 'comment': comment, + 'tpvv': True, + 'snapCPG': HP3PAR_CPG_SNAP}), + mock.call.logout()] + + mock_client.assert_has_calls(expected) + self.assertEqual(return_model, None) + @mock.patch.object(volume_types, 'get_volume_type') def test_create_volume_qos(self, _mock_volume_types): # setup_mock_client drive with default configuration @@ -529,14 +568,14 @@ class HP3PARBaseDriver(object): _mock_volume_types.return_value = { 'name': 'gold', 'extra_specs': { - 'cpg': HP3PAR_CPG, + 'cpg': HP3PAR_CPG_QOS, 'snap_cpg': HP3PAR_CPG_SNAP, 'vvs_name': self.VVS_NAME, 'qos': self.QOS, 'tpvv': True, 'volume_type': self.volume_type}} - self.driver.create_volume(self.volume_qos) + return_model = self.driver.create_volume(self.volume_qos) comment = ( '{"volume_type_name": "gold", "display_name": "Foo Volume"' ', "name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7' @@ -545,9 +584,10 @@ class HP3PARBaseDriver(object): expected = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), + mock.call.getCPG(HP3PAR_CPG_QOS), mock.call.createVolume( self.VOLUME_3PAR_NAME, - HP3PAR_CPG, + HP3PAR_CPG_QOS, 1907, { 'comment': comment, 'tpvv': True, @@ -555,6 +595,9 @@ class HP3PARBaseDriver(object): mock.call.logout()] mock_client.assert_has_calls(expected) + self.assertEqual(return_model, + {'host': volume_utils.append_host(self.FAKE_HOST, + HP3PAR_CPG_QOS)}) @mock.patch.object(volume_types, 'get_volume_type') def test_retype_not_3par(self, _mock_volume_types): @@ -803,7 +846,9 @@ class HP3PARBaseDriver(object): volume = {'id': HP3PARBaseDriver.CLONE_ID} - self.driver.retype(self.ctxt, volume, type_ref, None, self.RETYPE_HOST) + retyped = self.driver.retype( + self.ctxt, volume, type_ref, None, self.RETYPE_HOST) + self.assertTrue(retyped) expected = [ mock.call.modifyVolume('osv-0DM4qZEVSKON-AAAAAAAAA', @@ -876,18 +921,51 @@ class HP3PARBaseDriver(object): 'id': HP3PARBaseDriver.CLONE_ID, 'display_name': 'Foo Volume', 'size': 2, - 'host': HP3PARBaseDriver.FAKE_HOST, + 'host': volume_utils.append_host(self.FAKE_HOST, + HP3PAR_CPG2), 'source_volid': HP3PARBaseDriver.VOLUME_ID} src_vref = {} model_update = self.driver.create_cloned_volume(volume, src_vref) - self.assertIsNotNone(model_update) + self.assertIsNone(model_update) expected = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.copyVolume( self.VOLUME_3PAR_NAME, 'osv-0DM4qZEVSKON-AAAAAAAAA', - HP3PAR_CPG, + HP3PAR_CPG2, + {'snapCPG': 'OpenStackCPGSnap', 'tpvv': True, + 'online': True}), + mock.call.logout()] + + mock_client.assert_has_calls(expected) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_cloned_qos_volume(self, _mock_volume_types): + _mock_volume_types.return_value = self.RETYPE_VOLUME_TYPE_2 + mock_client = self.setup_driver() + mock_client.copyVolume.return_value = {'taskid': 1} + + src_vref = {} + volume = self.volume_qos.copy() + host = "TEST_HOST" + pool = "TEST_POOL" + volume_host = volume_utils.append_host(host, pool) + expected_cpg = self.RETYPE_VOLUME_TYPE_2['extra_specs']['cpg'] + expected_volume_host = volume_utils.append_host(host, expected_cpg) + volume['id'] = HP3PARBaseDriver.CLONE_ID + volume['host'] = volume_host + volume['source_volid'] = HP3PARBaseDriver.VOLUME_ID + model_update = self.driver.create_cloned_volume(volume, src_vref) + self.assertEqual(model_update, {'host': expected_volume_host}) + + expected = [ + mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), + mock.call.getCPG(expected_cpg), + mock.call.copyVolume( + self.VOLUME_3PAR_NAME, + 'osv-0DM4qZEVSKON-AAAAAAAAA', + expected_cpg, {'snapCPG': 'OpenStackCPGSnap', 'tpvv': True, 'online': True}), mock.call.logout()] @@ -1146,7 +1224,9 @@ class HP3PARBaseDriver(object): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client mock_client = self.setup_driver() - self.driver.create_volume_from_snapshot(self.volume, self.snapshot) + model_update = self.driver.create_volume_from_snapshot(self.volume, + self.snapshot) + self.assertIsNone(model_update) comment = ( '{"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",' @@ -1185,7 +1265,11 @@ class HP3PARBaseDriver(object): volume = self.volume.copy() volume['size'] = self.volume['size'] + 10 - self.driver.create_volume_from_snapshot(volume, self.snapshot) + model_update = self.driver.create_volume_from_snapshot(volume, + self.snapshot) + self.assertEqual(model_update, + {'host': volume_utils.append_host(self.FAKE_HOST, + HP3PAR_CPG)}) comment = ( '{"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",' @@ -1204,7 +1288,68 @@ class HP3PARBaseDriver(object): { 'comment': comment, 'readOnly': False}), - mock.call.copyVolume(osv_matcher, omv_matcher, mock.ANY, mock.ANY), + mock.call.copyVolume( + osv_matcher, omv_matcher, HP3PAR_CPG, mock.ANY), + mock.call.getTask(mock.ANY), + mock.call.getVolume(osv_matcher), + mock.call.deleteVolume(osv_matcher), + mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}), + mock.call.growVolume(osv_matcher, 10 * 1024), + mock.call.logout()] + + mock_client.assert_has_calls(expected) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_from_snapshot_and_extend_with_qos( + self, _mock_volume_types): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + conf = { + 'getTask.return_value': { + 'status': 1}, + 'copyVolume.return_value': {'taskid': 1}, + 'getVolume.return_value': {} + } + + mock_client = self.setup_driver(mock_conf=conf) + _mock_volume_types.return_value = { + 'name': 'gold', + 'extra_specs': { + 'cpg': HP3PAR_CPG_QOS, + 'snap_cpg': HP3PAR_CPG_SNAP, + 'vvs_name': self.VVS_NAME, + 'qos': self.QOS, + 'tpvv': True, + 'volume_type': self.volume_type}} + + volume = self.volume_qos.copy() + volume['size'] = self.volume['size'] + 10 + model_update = self.driver.create_volume_from_snapshot(volume, + self.snapshot) + self.assertEqual(model_update, + {'host': volume_utils.append_host(self.FAKE_HOST, + HP3PAR_CPG_QOS)}) + + comment = ( + '{"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",' + ' "display_name": "Foo Volume",' + ' "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7"}') + + volume_name_3par = self.driver.common._encode_name(volume['id']) + osv_matcher = 'osv-' + volume_name_3par + omv_matcher = 'omv-' + volume_name_3par + + expected = [ + mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), + mock.call.createSnapshot( + self.VOLUME_3PAR_NAME, + 'oss-L4I73ONuTci9Fd4ceij-MQ', + { + 'comment': comment, + 'readOnly': False}), + mock.call.getCPG(HP3PAR_CPG_QOS), + mock.call.copyVolume( + osv_matcher, omv_matcher, HP3PAR_CPG_QOS, mock.ANY), mock.call.getTask(mock.ANY), mock.call.getVolume(osv_matcher), mock.call.deleteVolume(osv_matcher), @@ -1537,6 +1682,7 @@ class HP3PARBaseDriver(object): "type": "OpenStack"} volume = {'display_name': None, + 'host': 'my-stack1@3parxxx#CPGNOTUSED', 'volume_type': 'gold', 'volume_type_id': 'acfa9fa4-54a0-4340-a3d8-bfcf19aea65e', 'id': '007dbfce-7579-40bc-8f90-a20b3902283e'} @@ -1552,7 +1698,8 @@ class HP3PARBaseDriver(object): obj = self.driver.manage_existing(volume, existing_ref) - expected_obj = {'display_name': 'Foo Volume'} + expected_obj = {'display_name': 'Foo Volume', + 'host': 'my-stack1@3parxxx#fakepool'} expected_manage = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), @@ -1593,10 +1740,11 @@ class HP3PARBaseDriver(object): 'bwMinGoalKB': 25600, 'priority': 1, 'latencyGoal': 25, 'bwMaxLimitKB': 51200}), mock.call.addVolumeToVolumeSet(vvs_matcher, osv_matcher), - mock.call.modifyVolume(osv_matcher, - {'action': 6, 'userCPG': 'OpenStackCPG', - 'conversionOperation': 1, - 'tuneOperation': 1}), + mock.call.modifyVolume( + osv_matcher, + {'action': 6, + 'userCPG': self.volume_type['extra_specs']['cpg'], + 'conversionOperation': 1, 'tuneOperation': 1}), mock.call.getTask(1), mock.call.logout() ] @@ -1624,6 +1772,7 @@ class HP3PARBaseDriver(object): "type": "OpenStack"} volume = {'display_name': 'Test Volume', + 'host': 'my-stack1@3parxxx#CPGNOTUSED', 'volume_type': 'gold', 'volume_type_id': 'acfa9fa4-54a0-4340-a3d8-bfcf19aea65e', 'id': id} @@ -1636,7 +1785,8 @@ class HP3PARBaseDriver(object): obj = self.driver.manage_existing(volume, existing_ref) - expected_obj = {'display_name': 'Test Volume'} + expected_obj = {'display_name': 'Test Volume', + 'host': 'my-stack1@3parxxx#qospool'} expected_manage = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getVolume(existing_ref['source-name']), @@ -1661,7 +1811,8 @@ class HP3PARBaseDriver(object): mock.call.deleteVolumeSet(vvs_matcher), mock.call.addVolumeToVolumeSet(vvs, osv_matcher), mock.call.modifyVolume(osv_matcher, - {'action': 6, 'userCPG': 'OpenStackCPG', + {'action': 6, 'userCPG': + test_volume_type['extra_specs']['cpg'], 'conversionOperation': 1, 'tuneOperation': 1}), mock.call.getTask(1), @@ -1976,6 +2127,7 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT), mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG(HP3PAR_CPG2), mock.call.logout()] mock_client.assert_has_calls(expected) mock_client.reset_mock() @@ -2264,30 +2416,33 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): # and return the mock HTTP 3PAR client mock_client = self.setup_driver() mock_client.getCPG.return_value = self.cpgs[0] - totalCapacityMiB = 8000 - freeCapacityMiB = 4000 mock_client.getStorageSystemInfo.return_value = { 'serialNumber': '1234', - 'totalCapacityMiB': totalCapacityMiB, - 'freeCapacityMiB': freeCapacityMiB + 'freeCapacityMiB': 1024.0 * 2, + 'totalCapacityMiB': 1024.0 * 123 } stats = self.driver.get_volume_stats(True) const = 0.0009765625 self.assertEqual(stats['storage_protocol'], 'FC') - self.assertEqual(stats['total_capacity_gb'], totalCapacityMiB * const) - self.assertEqual(stats['free_capacity_gb'], freeCapacityMiB * const) + self.assertEqual(stats['total_capacity_gb'], 0) + self.assertEqual(stats['free_capacity_gb'], 0) + self.assertEqual(stats['pools'][0]['total_capacity_gb'], 123.0) + self.assertEqual(stats['pools'][0]['free_capacity_gb'], 2.0) expected = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getStorageSystemInfo(), mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG(HP3PAR_CPG2), mock.call.logout()] mock_client.assert_has_calls(expected) stats = self.driver.get_volume_stats(True) self.assertEqual(stats['storage_protocol'], 'FC') - self.assertEqual(stats['total_capacity_gb'], totalCapacityMiB * const) - self.assertEqual(stats['free_capacity_gb'], freeCapacityMiB * const) + self.assertEqual(stats['total_capacity_gb'], 0) + self.assertEqual(stats['free_capacity_gb'], 0) + self.assertEqual(stats['pools'][0]['total_capacity_gb'], 123.0) + self.assertEqual(stats['pools'][0]['free_capacity_gb'], 2.0) cpg2 = self.cpgs[0].copy() cpg2.update({'SDGrowth': {'limitMiB': 8192}}) @@ -2296,10 +2451,14 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase): stats = self.driver.get_volume_stats(True) self.assertEqual(stats['storage_protocol'], 'FC') total_capacity_gb = 8192 * const - self.assertEqual(stats['total_capacity_gb'], total_capacity_gb) + self.assertEqual(stats['total_capacity_gb'], 0) + self.assertEqual(stats['pools'][0]['total_capacity_gb'], + total_capacity_gb) free_capacity_gb = int( (8192 - self.cpgs[0]['UsrUsage']['usedMiB']) * const) - self.assertEqual(stats['free_capacity_gb'], free_capacity_gb) + self.assertEqual(stats['free_capacity_gb'], 0) + self.assertEqual(stats['pools'][0]['free_capacity_gb'], + free_capacity_gb) self.driver.common.client.deleteCPG(HP3PAR_CPG) self.driver.common.client.createCPG(HP3PAR_CPG, {}) @@ -2501,6 +2660,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT), mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG(HP3PAR_CPG2), mock.call.logout(), mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getPorts(), @@ -2557,23 +2717,24 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): # and return the mock HTTP 3PAR client mock_client = self.setup_driver() mock_client.getCPG.return_value = self.cpgs[0] - totalCapacityMiB = 8000 - freeCapacityMiB = 4000 mock_client.getStorageSystemInfo.return_value = { 'serialNumber': '1234', - 'totalCapacityMiB': totalCapacityMiB, - 'freeCapacityMiB': freeCapacityMiB + 'freeCapacityMiB': 1024.0 * 2, + 'totalCapacityMiB': 1024.0 * 123 } stats = self.driver.get_volume_stats(True) const = 0.0009765625 self.assertEqual(stats['storage_protocol'], 'iSCSI') - self.assertEqual(stats['total_capacity_gb'], totalCapacityMiB * const) - self.assertEqual(stats['free_capacity_gb'], freeCapacityMiB * const) + self.assertEqual(stats['total_capacity_gb'], 0) + self.assertEqual(stats['free_capacity_gb'], 0) + self.assertEqual(stats['pools'][0]['total_capacity_gb'], 123.0) + self.assertEqual(stats['pools'][0]['free_capacity_gb'], 2.0) expected = [ mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS), mock.call.getStorageSystemInfo(), mock.call.getCPG(HP3PAR_CPG), + mock.call.getCPG(HP3PAR_CPG2), mock.call.logout()] mock_client.assert_has_calls(expected) @@ -2585,10 +2746,14 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): stats = self.driver.get_volume_stats(True) self.assertEqual(stats['storage_protocol'], 'iSCSI') total_capacity_gb = 8192 * const - self.assertEqual(stats['total_capacity_gb'], total_capacity_gb) + self.assertEqual(stats['total_capacity_gb'], 0) + self.assertEqual(stats['pools'][0]['total_capacity_gb'], + total_capacity_gb) free_capacity_gb = int( (8192 - self.cpgs[0]['UsrUsage']['usedMiB']) * const) - self.assertEqual(stats['free_capacity_gb'], free_capacity_gb) + self.assertEqual(stats['free_capacity_gb'], 0) + self.assertEqual(stats['pools'][0]['free_capacity_gb'], + free_capacity_gb) def test_create_host(self): # setup_mock_client drive with default configuration @@ -3325,6 +3490,24 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase): mock_client.assert_has_calls(expected) self.assertEqual(model, expected_model) + @mock.patch.object(volume_types, 'get_volume_type') + def test_get_volume_settings_default_pool(self, _mock_volume_types): + _mock_volume_types.return_value = { + 'name': 'gold', + 'id': 'gold-id', + 'extra_specs': {}} + self.setup_driver() + volume = {'host': 'test-host@3pariscsi#pool_foo', + 'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'} + model = self.driver.common.get_volume_settings_from_type_id('gold-id', + volume) + self.assertEqual(model['cpg'], 'pool_foo') + + def test_get_model_update(self): + self.setup_driver() + model_update = self.driver.common._get_model_update('xxx@yyy#zzz', + 'CPG') + self.assertEqual(model_update, {'host': 'xxx@yyy#CPG'}) VLUNS5_RET = ({'members': [{'portPos': {'node': 0, 'slot': 8, 'cardPort': 2}, diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 768163526..1f0157883 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -59,6 +59,7 @@ from cinder.openstack.common import log as logging from cinder.openstack.common import loopingcall from cinder.openstack.common import units from cinder.volume import qos_specs +from cinder.volume import utils as volume_utils from cinder.volume import volume_types import taskflow.engines @@ -81,13 +82,13 @@ hp3par_opts = [ default='', help="3PAR Super user password", secret=True), - cfg.StrOpt('hp3par_cpg', - default="OpenStack", - help="The CPG to use for volume creation"), + cfg.ListOpt('hp3par_cpg', + default=["OpenStack"], + help="List of the CPG(s) to use for volume creation"), cfg.StrOpt('hp3par_cpg_snap', default="", help="The CPG to use for Snapshots for volumes. " - "If empty hp3par_cpg will be used"), + "If empty the userCPG will be used."), cfg.StrOpt('hp3par_snapshot_retention', default="", help="The time in hours to retain a snapshot. " @@ -151,10 +152,11 @@ class HP3PARCommon(object): 2.0.21 - Remove bogus invalid snapCPG=None exception 2.0.22 - HP 3PAR drivers should not claim to have 'infinite' space 2.0.23 - Increase the hostname size from 23 to 31 Bug #1371242 + 2.0.24 - Add pools (hp3par_cpg now accepts a list of CPGs) """ - VERSION = "2.0.23" + VERSION = "2.0.24" stats = {} @@ -270,8 +272,10 @@ class HP3PARCommon(object): self.client_login() try: - # make sure the default CPG exists - self.validate_cpg(self.config.hp3par_cpg) + cpg_names = self.config.hp3par_cpg + for cpg_name in cpg_names: + self.validate_cpg(cpg_name) + finally: self.client_logout() @@ -365,12 +369,15 @@ class HP3PARCommon(object): LOG.info(_("Virtual volume '%(ref)s' renamed to '%(new)s'.") % {'ref': existing_ref['source-name'], 'new': new_vol_name}) + retyped = False + model_update = None if volume_type: LOG.info(_("Virtual volume %(disp)s '%(new)s' is being retyped.") % {'disp': display_name, 'new': new_vol_name}) try: - self._retype_from_no_type(volume, volume_type) + retyped, model_update = self._retype_from_no_type(volume, + volume_type) LOG.info(_("Virtual volume %(disp)s successfully retyped to " "%(new_type)s.") % {'disp': display_name, @@ -386,11 +393,16 @@ class HP3PARCommon(object): {'newName': existing_ref['source-name'], 'comment': old_comment_str}) + updates = {'display_name': display_name} + if retyped and model_update: + updates.update(model_update) + LOG.info(_("Virtual volume %(disp)s '%(new)s' is now being managed.") % {'disp': display_name, 'new': new_vol_name}) - # Return display name to update the name displayed in the GUI. - return {'display_name': display_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. @@ -439,30 +451,33 @@ class HP3PARCommon(object): def _extend_volume(self, volume, volume_name, growth_size_mib, _convert_to_base=False): + model_update = None try: if _convert_to_base: LOG.debug("Converting to base volume prior to growing.") - self._convert_to_base_volume(volume) + model_update = self._convert_to_base_volume(volume) self.client.growVolume(volume_name, growth_size_mib) except Exception as ex: with excutils.save_and_reraise_exception() as ex_ctxt: if (not _convert_to_base and isinstance(ex, hpexceptions.HTTPForbidden) and ex.get_code() == 150): - # Error code 150 means 'invalid operation: Cannot grow - # this type of volume'. - # Suppress raising this exception because we can - # resolve it by converting it into a base volume. - # Afterwards, extending the volume should succeed, or - # fail with a different exception/error code. - ex_ctxt.reraise = False - self._extend_volume(volume, volume_name, - growth_size_mib, - _convert_to_base=True) + # Error code 150 means 'invalid operation: Cannot grow + # this type of volume'. + # Suppress raising this exception because we can + # resolve it by converting it into a base volume. + # Afterwards, extending the volume should succeed, or + # fail with a different exception/error code. + ex_ctxt.reraise = False + model_update = self._extend_volume( + volume, volume_name, + growth_size_mib, + _convert_to_base=True) else: LOG.error(_("Error extending volume: %(vol)s. " "Exception: %(ex)s") % {'vol': volume_name, 'ex': ex}) + return model_update def _get_3par_vol_name(self, volume_id): """Get converted 3PAR volume name. @@ -616,40 +631,47 @@ class HP3PARCommon(object): # storage_protocol and volume_backend_name are # set in the child classes - stats = {'driver_version': '1.0', - 'free_capacity_gb': 'unknown', - 'reserved_percentage': 0, - 'storage_protocol': None, - 'total_capacity_gb': 'unknown', - 'QoS_support': True, - 'vendor_name': 'Hewlett-Packard', - 'volume_backend_name': None} + pools = [] info = self.client.getStorageSystemInfo() - try: - cpg = self.client.getCPG(self.config.hp3par_cpg) - if 'limitMiB' not in cpg['SDGrowth']: - # System capacity is best we can do for now. - total_capacity = info['totalCapacityMiB'] * const - free_capacity = info['freeCapacityMiB'] * const - else: - total_capacity = int(cpg['SDGrowth']['limitMiB'] * const) - free_capacity = int((cpg['SDGrowth']['limitMiB'] - - cpg['UsrUsage']['usedMiB']) * const) - - stats['total_capacity_gb'] = total_capacity - stats['free_capacity_gb'] = free_capacity - except hpexceptions.HTTPNotFound: - err = (_("CPG (%s) doesn't exist on array") - % self.config.hp3par_cpg) - LOG.error(err) - raise exception.InvalidInput(reason=err) + for cpg_name in self.config.hp3par_cpg: + try: + cpg = self.client.getCPG(cpg_name) + if 'limitMiB' not in cpg['SDGrowth']: + # System capacity is best we can do for now. + total_capacity = info['totalCapacityMiB'] * const + free_capacity = info['freeCapacityMiB'] * const + else: + total_capacity = int(cpg['SDGrowth']['limitMiB'] * const) + free_capacity = int((cpg['SDGrowth']['limitMiB'] - + cpg['UsrUsage']['usedMiB']) * const) - stats['location_info'] = ('HP3PARDriver:%(sys_id)s:%(dest_cpg)s' % - {'sys_id': info['serialNumber'], - 'dest_cpg': self.config.safe_get( - 'hp3par_cpg')}) - self.stats = stats + except hpexceptions.HTTPNotFound: + err = (_("CPG (%s) doesn't exist on array") + % cpg_name) + LOG.error(err) + raise exception.InvalidInput(reason=err) + + pool = {'pool_name': cpg_name, + 'total_capacity_gb': total_capacity, + 'free_capacity_gb': free_capacity, + 'QoS_support': True, + 'reserved_percentage': 0, + 'location_info': ('HP3PARDriver:%(sys_id)s:%(dest_cpg)s' % + {'sys_id': info['serialNumber'], + 'dest_cpg': cpg_name}) + } + pools.append(pool) + + self.stats = {'driver_version': '1.0', + 'storage_protocol': None, + 'vendor_name': 'Hewlett-Packard', + 'volume_backend_name': None, + # Use zero capacities here so we always use a pool. + 'total_capacity_gb': 0, + 'free_capacity_gb': 0, + 'reserved_percentage': 0, + 'pools': pools} def _get_vlun(self, volume_name, hostname, lun_id=None): """find a VLUN on a 3PAR host.""" @@ -921,11 +943,13 @@ class HP3PARCommon(object): qos = self._get_qos_by_volume_type(volume_type) return hp3par_keys, qos, volume_type, vvs_name - def get_volume_settings_from_type_id(self, type_id): + def get_volume_settings_from_type_id(self, type_id, volume): """Get 3PAR volume settings given a type_id. Combines type info and config settings to return a dictionary describing the 3PAR volume settings. Does some validation (CPG). + Uses volume['host'] to determine default cpg (when not specified in + volume type specs). :param type_id: :return: dict @@ -933,9 +957,22 @@ class HP3PARCommon(object): hp3par_keys, qos, volume_type, vvs_name = self.get_type_info(type_id) - cpg = self._get_key_value(hp3par_keys, 'cpg', - self.config.hp3par_cpg) - if cpg is not self.config.hp3par_cpg: + # Default to 1st configured CPG unless we can extract pool from host. + default_cpg = self.config.hp3par_cpg[0] + try: + pool = volume_utils.extract_host(volume['host'], 'pool') + if pool: + default_cpg = pool + LOG.debug("Default CPG from volume['host'] is (%s)" % + default_cpg) + else: + LOG.debug("Default CPG from volume['host'] not found") + except Exception as ex: + LOG.debug("Default CPG from volume['host'] not found due to (%s)" % + ex) + + cpg = self._get_key_value(hp3par_keys, 'cpg', default_cpg) + if cpg not in self.config.hp3par_cpg: # The cpg was specified in a volume type extra spec so it # needs to be validated that it's in the correct domain. self.validate_cpg(cpg) @@ -987,7 +1024,8 @@ class HP3PARCommon(object): type_id = volume.get('volume_type_id', None) - volume_settings = self.get_volume_settings_from_type_id(type_id) + volume_settings = self.get_volume_settings_from_type_id(type_id, + volume) # check for valid persona even if we don't use it until # attach time, this will give the end user notice that the @@ -1060,6 +1098,8 @@ class HP3PARCommon(object): LOG.error(ex) raise exception.CinderException(ex) + return self._get_model_update(volume['host'], cpg) + def _copy_volume(self, src_name, dest_name, cpg, snap_cpg=None, tpvv=True): # Virtual volume sets are not supported with the -online option @@ -1088,6 +1128,32 @@ class HP3PARCommon(object): return comment_dict[key] return None + def _get_model_update(self, volume_host, cpg): + """Get model_update dict to use when we select a pool. + + The pools implementation uses a volume['host'] suffix of :poolname. + When the volume comes in with this selected pool, we sometimes use + a different pool (e.g. because the type says to use a different pool). + So in the several places that we do this, we need to return a model + update so that the volume will have the actual pool name in the host + suffix after the operation. + + Given a volume_host, which should (might) have the pool suffix, and + given the CPG we actually chose to use, return a dict to use for a + model update iff an update is needed. + + :param volume_host: The volume's host string. + :param cpg: The actual pool (cpg) used, for example from the type. + :return: dict Model update if we need to update volume host, else None + """ + model_update = None + host = volume_utils.extract_host(volume_host, 'backend') + host_and_pool = volume_utils.append_host(host, cpg) + if volume_host != host_and_pool: + # Since we selected a pool based on type, update the model. + model_update = {'host': host_and_pool} + return model_update + def create_cloned_volume(self, volume, src_vref): try: orig_name = self._get_3par_vol_name(volume['source_volid']) @@ -1097,10 +1163,13 @@ class HP3PARCommon(object): # make the 3PAR copy the contents. # can't delete the original until the copy is done. - self._copy_volume(orig_name, vol_name, cpg=type_info['cpg'], + cpg = type_info['cpg'] + self._copy_volume(orig_name, vol_name, cpg=cpg, snap_cpg=type_info['snap_cpg'], tpvv=type_info['tpvv']) - return None + + return self._get_model_update(volume['host'], cpg) + except hpexceptions.HTTPForbidden: raise exception.NotAuthorized() except hpexceptions.HTTPNotFound: @@ -1188,6 +1257,7 @@ class HP3PARCommon(object): (pprint.pformat(volume['display_name']), pprint.pformat(snapshot['display_name']))) + model_update = None if volume['size'] < snapshot['volume_size']: err = ("You cannot reduce size of the volume. It must " "be greater than or equal to the snapshot.") @@ -1225,7 +1295,7 @@ class HP3PARCommon(object): try: LOG.debug('Converting to base volume type: %s.' % volume['id']) - self._convert_to_base_volume(volume) + model_update = self._convert_to_base_volume(volume) growth_size_mib = growth_size * units.Gi / units.Mi LOG.debug('Growing volume: %(id)s by %(size)s GiB.' % {'id': volume['id'], 'size': growth_size}) @@ -1238,11 +1308,11 @@ class HP3PARCommon(object): raise exception.CinderException(ex) if qos or vvs_name is not None: - cpg = self._get_key_value(hp3par_keys, 'cpg', - self.config.hp3par_cpg) + cpg_names = self._get_key_value(hp3par_keys, 'cpg', + self.config.hp3par_cpg) try: self._add_volume_to_volume_set(volume, volume_name, - cpg, vvs_name, qos) + cpg_names[0], vvs_name, qos) except Exception as ex: # Delete the volume if unable to add it to the volume set self.client.deleteVolume(volume_name) @@ -1257,6 +1327,7 @@ class HP3PARCommon(object): except Exception as ex: LOG.error(ex) raise exception.CinderException(ex) + return model_update def create_snapshot(self, snapshot): LOG.debug("Create Snapshot\n%s" % pprint.pformat(snapshot)) @@ -1489,6 +1560,8 @@ class HP3PARCommon(object): LOG.error(ex) raise exception.CinderException(ex) + return self._get_model_update(volume['host'], cpg) + def delete_snapshot(self, snapshot): LOG.debug("Delete Snapshot id %s %s" % (snapshot['id'], pprint.pformat(snapshot))) @@ -1733,7 +1806,7 @@ class HP3PARCommon(object): new_type_name = new_type['name'] new_type_id = new_type['id'] new_volume_settings = self.get_volume_settings_from_type_id( - new_type_id) + new_type_id, volume) new_cpg = new_volume_settings['cpg'] new_snap_cpg = new_volume_settings['snap_cpg'] new_tpvv = new_volume_settings['tpvv'] @@ -1765,7 +1838,11 @@ class HP3PARCommon(object): host, new_persona, old_cpg, new_cpg, old_snap_cpg, new_snap_cpg, old_tpvv, new_tpvv, old_vvs, new_vvs, old_qos, new_qos, old_comment) - return True + + if host: + return True, self._get_model_update(host['host'], new_cpg) + else: + return True, self._get_model_update(volume['host'], new_cpg) def _retype_from_no_type(self, volume, new_type): """Convert the volume to be of the new type. Starting from no type. @@ -1777,7 +1854,8 @@ class HP3PARCommon(object): volume-type is not used here. This method uses None. :param new_type: A dictionary describing the volume type to convert to """ - none_type_settings = self.get_volume_settings_from_type_id(None) + none_type_settings = self.get_volume_settings_from_type_id( + None, volume) return self._retype_from_old_to_new(volume, new_type, none_type_settings, None) diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py index abfa77675..38101065c 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_fc.py +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -34,6 +34,7 @@ try: except ImportError: hpexceptions = None +from cinder import exception from cinder.i18n import _ from cinder.openstack.common import log as logging from cinder import utils @@ -69,10 +70,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): 2.0.7 - Only one FC port is used when a single FC path is present. bug #1360001 2.0.8 - Fixing missing login/logout around attach/detach bug #1367429 + 2.0.9 - Add support for pools with model update """ - VERSION = "2.0.8" + VERSION = "2.0.9" def __init__(self, *args, **kwargs): super(HP3PARFCDriver, self).__init__(*args, **kwargs) @@ -118,8 +120,7 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): def create_volume(self, volume): self.common.client_login() try: - metadata = self.common.create_volume(volume) - return {'metadata': metadata} + return self.common.create_volume(volume) finally: self.common.client_logout() @@ -127,8 +128,7 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): def create_cloned_volume(self, volume, src_vref): self.common.client_login() try: - new_vol = self.common.create_cloned_volume(volume, src_vref) - return {'metadata': new_vol} + return self.common.create_cloned_volume(volume, src_vref) finally: self.common.client_logout() @@ -148,9 +148,8 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): """ self.common.client_login() try: - metadata = self.common.create_volume_from_snapshot(volume, - snapshot) - return {'metadata': metadata} + return self.common.create_volume_from_snapshot(volume, + snapshot) finally: self.common.client_logout() @@ -466,3 +465,14 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver): return self.common.migrate_volume(volume, host) finally: self.common.client_logout() + + def get_pool(self, volume): + self.common.client_login() + try: + return self.common.get_cpg(volume) + except hpexceptions.HTTPNotFound: + reason = (_("Volume %s doesn't exist on array.") % volume) + LOG.error(reason) + raise exception.InvalidVolume(reason) + finally: + self.common.client_logout() diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 3027df043..3500d9e71 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -74,10 +74,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): 2.0.5 - Added CHAP support, requires 3.1.3 MU1 firmware and hp3parclient 3.1.0. 2.0.6 - Fixing missing login/logout around attach/detach bug #1367429 + 2.0.7 - Add support for pools with model update """ - VERSION = "2.0.6" + VERSION = "2.0.7" def __init__(self, *args, **kwargs): super(HP3PARISCSIDriver, self).__init__(*args, **kwargs) @@ -189,8 +190,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): def create_volume(self, volume): self.common.client_login() try: - metadata = self.common.create_volume(volume) - return {'metadata': metadata} + return self.common.create_volume(volume) finally: self.common.client_logout() @@ -199,8 +199,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): """Clone an existing volume.""" self.common.client_login() try: - new_vol = self.common.create_cloned_volume(volume, src_vref) - return {'metadata': new_vol} + return self.common.create_cloned_volume(volume, src_vref) finally: self.common.client_logout() @@ -220,9 +219,8 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): """ self.common.client_login() try: - metadata = self.common.create_volume_from_snapshot(volume, - snapshot) - return {'metadata': metadata} + return self.common.create_volume_from_snapshot(volume, + snapshot) finally: self.common.client_logout() @@ -674,3 +672,14 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): return self.common.migrate_volume(volume, host) finally: self.common.client_logout() + + def get_pool(self, volume): + self.common.client_login() + try: + return self.common.get_cpg(volume) + except hpexceptions.HTTPNotFound: + reason = (_("Volume %s doesn't exist on array.") % volume) + LOG.error(reason) + raise exception.InvalidVolume(reason) + finally: + self.common.client_logout() diff --git a/etc/cinder/cinder.conf.sample b/etc/cinder/cinder.conf.sample index 4f3fda0c5..02c212c45 100644 --- a/etc/cinder/cinder.conf.sample +++ b/etc/cinder/cinder.conf.sample @@ -1910,11 +1910,11 @@ # 3PAR Super user password (string value) #hp3par_password= -# The CPG to use for volume creation (string value) +# List of the CPG(s) to use for volume creation (list value) #hp3par_cpg=OpenStack -# The CPG to use for Snapshots for volumes. If empty -# hp3par_cpg will be used (string value) +# The CPG to use for Snapshots for volumes. If empty the +# userCPG will be used. (string value) #hp3par_cpg_snap= # The time in hours to retain a snapshot. You can't delete it