From fa51dab15d381371368b7d566f4ad8217541061c Mon Sep 17 00:00:00 2001 From: Jim Branen Date: Fri, 14 Feb 2014 09:24:53 -0800 Subject: [PATCH] Implement retype in HP LeftHand driver This adds the ability to modify the type of an existing HP LeftHand volume to another type supported on the same backend. Main use case for this is changing provisioning, (full vs. thin), adaptive optimization and data protections settings for a volume on-demand. Change-Id: I78e53431708bcb3151c5dc4016e65d1a655e03c6 Implements: blueprint retype-support-for-hplefthand-driver --- cinder/tests/test_hplefthand.py | 144 +++++++++++++++++- .../drivers/san/hp/hp_lefthand_cliq_proxy.py | 15 ++ .../drivers/san/hp/hp_lefthand_iscsi.py | 8 +- .../drivers/san/hp/hp_lefthand_rest_proxy.py | 75 +++++++-- 4 files changed, 228 insertions(+), 14 deletions(-) diff --git a/cinder/tests/test_hplefthand.py b/cinder/tests/test_hplefthand.py index 7d3b395ed..2f757377c 100644 --- a/cinder/tests/test_hplefthand.py +++ b/cinder/tests/test_hplefthand.py @@ -18,6 +18,7 @@ import mock from hplefthandclient import exceptions as hpexceptions +from cinder import context from cinder import exception from cinder.openstack.common import log as logging from cinder import test @@ -974,8 +975,10 @@ 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 - extra_specs = self.driver.proxy._get_extra_specs( - volume_with_vt, + volume_extra_specs = self.driver.proxy._get_volume_extra_specs( + volume_with_vt) + extra_specs = self.driver.proxy._get_lh_extra_specs( + volume_extra_specs, hp_lefthand_rest_proxy.extra_specs_key_map.keys()) # map the extra specs key/value pairs to key/value pairs @@ -1000,8 +1003,10 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): 'hplh:ao': 'true'}} # get the extra specs of interest from this volume's volume type - extra_specs = self.driver.proxy._get_extra_specs( - volume_with_vt, + volume_extra_specs = self.driver.proxy._get_volume_extra_specs( + volume_with_vt) + extra_specs = self.driver.proxy._get_lh_extra_specs( + volume_extra_specs, hp_lefthand_rest_proxy.extra_specs_key_map.keys()) # map the extra specs key/value pairs to key/value pairs @@ -1012,3 +1017,134 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): # {'isAdaptiveOptimizationEnabled': True} # without hplh:data_pl since r-07 is an invalid value self.assertDictMatch({'isAdaptiveOptimizationEnabled': True}, optional) + + def test_retype_with_no_LH_extra_specs(self): + # setup drive with default configuration + # and return the mock HTTP LeftHand client + mock_client = self.setup_driver() + + ctxt = context.get_admin_context() + + host = {'host': self.serverName} + key_specs_old = {'foo': False, 'bar': 2, 'error': True} + key_specs_new = {'foo': True, 'bar': 5, 'error': False} + old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) + new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) + + diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], + new_type_ref['id']) + + volume = dict.copy(self.volume) + old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) + volume['volume_type'] = old_type + volume['host'] = host + new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) + + self.driver.retype(ctxt, volume, new_type, diff, host) + + expected = self.driver_startup_call_stack + [ + mock.call.getVolumeByName('fakevolume')] + + # validate call chain + mock_client.assert_has_calls(expected) + + def test_retype_with_only_LH_extra_specs(self): + # setup drive with default configuration + # and return the mock HTTP LeftHand client + mock_client = self.setup_driver() + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + ctxt = context.get_admin_context() + + host = {'host': self.serverName} + key_specs_old = {'hplh:provisioning': 'thin'} + key_specs_new = {'hplh:provisioning': 'full', 'hplh:ao': 'true'} + old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) + new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) + + diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], + new_type_ref['id']) + + volume = dict.copy(self.volume) + old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) + volume['volume_type'] = old_type + volume['host'] = host + new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) + + self.driver.retype(ctxt, volume, new_type, diff, host) + + expected = self.driver_startup_call_stack + [ + mock.call.getVolumeByName('fakevolume'), + mock.call.modifyVolume( + 1, { + 'isThinProvisioned': False, + 'isAdaptiveOptimizationEnabled': True})] + + # validate call chain + mock_client.assert_has_calls(expected) + + def test_retype_with_both_extra_specs(self): + # setup drive with default configuration + # and return the mock HTTP LeftHand client + mock_client = self.setup_driver() + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + ctxt = context.get_admin_context() + + host = {'host': self.serverName} + key_specs_old = {'hplh:provisioning': 'full', 'foo': 'bar'} + key_specs_new = {'hplh:provisioning': 'thin', 'foo': 'foobar'} + old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) + new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) + + diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], + new_type_ref['id']) + + volume = dict.copy(self.volume) + old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) + volume['volume_type'] = old_type + volume['host'] = host + new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) + + self.driver.retype(ctxt, volume, new_type, diff, host) + + expected = self.driver_startup_call_stack + [ + mock.call.getVolumeByName('fakevolume'), + mock.call.modifyVolume(1, {'isThinProvisioned': True})] + + # validate call chain + mock_client.assert_has_calls(expected) + + def test_retype_same_extra_specs(self): + # setup drive with default configuration + # and return the mock HTTP LeftHand client + mock_client = self.setup_driver() + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + ctxt = context.get_admin_context() + + host = {'host': self.serverName} + key_specs_old = {'hplh:provisioning': 'full', 'hplh:ao': 'true'} + key_specs_new = {'hplh:provisioning': 'full', 'hplh:ao': 'false'} + old_type_ref = volume_types.create(ctxt, 'old', key_specs_old) + new_type_ref = volume_types.create(ctxt, 'new', key_specs_new) + + diff, equal = volume_types.volume_types_diff(ctxt, old_type_ref['id'], + new_type_ref['id']) + + volume = dict.copy(self.volume) + old_type = volume_types.get_volume_type(ctxt, old_type_ref['id']) + volume['volume_type'] = old_type + volume['host'] = host + new_type = volume_types.get_volume_type(ctxt, new_type_ref['id']) + + self.driver.retype(ctxt, volume, new_type, diff, host) + + expected = self.driver_startup_call_stack + [ + mock.call.getVolumeByName('fakevolume'), + mock.call.modifyVolume( + 1, + {'isAdaptiveOptimizationEnabled': False})] + + # validate call chain + mock_client.assert_has_calls(expected) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py index 15f7dc057..5105080a1 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py @@ -448,3 +448,18 @@ class HPLeftHandCLIQProxy(SanISCSIDriver): 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 + :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 diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py index 65ccf11ef..7c2b78ed6 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py @@ -46,9 +46,10 @@ class HPLeftHandISCSIDriver(VolumeDriver): Version history: 1.0.0 - Initial driver + 1.0.1 - Added support for retype """ - VERSION = "1.0.0" + VERSION = "1.0.1" def __init__(self, *args, **kwargs): super(HPLeftHandISCSIDriver, self).__init__(*args, **kwargs) @@ -135,3 +136,8 @@ class HPLeftHandISCSIDriver(VolumeDriver): @utils.synchronized('lefthand', external=True) def remove_export(self, context, volume): return self.proxy.remove_export(context, volume) + + @utils.synchronized('lefthand', external=True) + 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) diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py index 0bdaab1b7..3465324ac 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py @@ -83,9 +83,10 @@ class HPLeftHandRESTProxy(ISCSIDriver): Version history: 1.0.0 - Initial REST iSCSI proxy + 1.0.1 - Added support for retype """ - VERSION = "1.0.0" + VERSION = "1.0.1" device_stats = {} @@ -131,8 +132,9 @@ class HPLeftHandRESTProxy(ISCSIDriver): """Creates a volume.""" try: # get the extra specs of interest from this volume's volume type - extra_specs = self._get_extra_specs( - volume, + 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 @@ -285,19 +287,24 @@ class HPLeftHandRESTProxy(ISCSIDriver): except Exception as ex: raise exception.VolumeBackendAPIException(str(ex)) - def _get_extra_specs(self, volume, valid_keys): - """Get extra specs of interest (valid_keys) from volume type.""" + 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) - specs = volume_type.get('extra_specs') - for key, value in specs.iteritems(): - if key in valid_keys: - extra_specs[key] = value + 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.iteritems(): + 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 = {} @@ -361,3 +368,53 @@ class HPLeftHandRESTProxy(ISCSIDriver): 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}) + try: + volume_info = self.client.getVolumeByName(volume['name']) + except hpexceptions.HTTPNotFound: + raise exception.VolumeNotFound(volume_id=volume['id']) + + try: + # 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.iteritems(): + (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: + self.client.modifyVolume(volume_info['id'], options) + return True + + except Exception as ex: + LOG.warning("%s" % str(ex)) + + return False -- 2.45.2