From 08847f26cf84b96d89e4b0ab668b7b3f47c09465 Mon Sep 17 00:00:00 2001 From: Anthony Lee Date: Wed, 11 Feb 2015 09:19:32 -0800 Subject: [PATCH] Adding manage/unmanage support for LeftHand driver Added the needed methods needed to support manage/unmanage in the HP LeftHand driver. A minimum version check needed to be added as previous versions of the LeftHand API won't support the manage/unmanage feature. Implements: blueprint lefthand-manage-unmanage Change-Id: I1d118f579e19c04ee535de0f615de8a919fc3f80 --- cinder/tests/test_hplefthand.py | 283 ++++++++++++++++++ .../drivers/san/hp/hp_lefthand_iscsi.py | 12 +- .../drivers/san/hp/hp_lefthand_rest_proxy.py | 200 ++++++++++++- 3 files changed, 492 insertions(+), 3 deletions(-) diff --git a/cinder/tests/test_hplefthand.py b/cinder/tests/test_hplefthand.py index 409a5ddda..0bbb5f7f8 100644 --- a/cinder/tests/test_hplefthand.py +++ b/cinder/tests/test_hplefthand.py @@ -39,6 +39,7 @@ class HPLeftHandBaseDriver(): volume_id = 1 volume = { 'name': volume_name, + 'display_name': 'Foo Volume', 'provider_location': ('10.0.1.6 iqn.2003-10.com.lefthandnetworks:' 'group01:25366:fakev 0'), 'id': volume_id, @@ -67,6 +68,15 @@ class HPLeftHandBaseDriver(): volume_type_id = 4 init_iqn = 'iqn.1993-08.org.debian:01:222' + volume_type = {'name': 'gold', + 'deleted': False, + 'updated_at': None, + 'extra_specs': {'hplh:provisioning': 'thin', + 'hplh:ao': 'true', + 'hplh:data_pl': 'r-0'}, + 'deleted_at': None, + 'id': 'gold'} + connector = { 'ip': '10.0.0.2', 'initiator': 'iqn.1993-08.org.debian:01:222', @@ -1641,3 +1651,276 @@ class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase): mock.call.logout()] mock_client.assert_has_calls(expected) + + def test__get_existing_volume_ref_name(self): + self.setup_driver() + + existing_ref = {'source-name': self.volume_name} + result = self.driver.proxy._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, + existing_ref) + + def test_manage_existing(self): + mock_client = self.setup_driver() + + self.driver.proxy.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, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + existing_ref = {'source-name': self.volume_name} + + expected_obj = {'display_name': 'Foo Volume'} + + obj = self.driver.manage_existing(volume, existing_ref) + + mock_client.assert_has_calls( + self.driver_startup_call_stack + [ + mock.call.getVolumeByName(self.volume_name), + mock.call.logout()] + + self.driver_startup_call_stack + [ + mock.call.modifyVolume(self.volume_id, + {'name': 'volume-12345'}), + mock.call.logout()]) + self.assertEqual(expected_obj, obj) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_manage_existing_retype(self, _mock_volume_types): + mock_client = self.setup_driver() + + _mock_volume_types.return_value = { + 'name': 'gold', + 'id': 'gold-id', + 'extra_specs': { + 'hplh:provisioning': 'thin', + 'hplh:ao': 'true', + 'hplh:data_pl': 'r-0', + 'volume_type': self.volume_type}} + + self.driver.proxy.api_version = "1.1" + + volume = {'display_name': 'Foo Volume', + 'host': 'stack@lefthand#lefthand', + 'volume_type': 'gold', + 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', + 'id': '12345'} + + with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + existing_ref = {'source-name': self.volume_name} + + expected_obj = {'display_name': 'Foo Volume'} + + obj = self.driver.manage_existing(volume, existing_ref) + + mock_client.assert_has_calls( + self.driver_startup_call_stack + [ + mock.call.getVolumeByName(self.volume_name), + mock.call.logout()] + + self.driver_startup_call_stack + [ + mock.call.modifyVolume(self.volume_id, + {'name': 'volume-12345'}), + mock.call.logout()]) + self.assertEqual(expected_obj, obj) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_manage_existing_retype_exception(self, _mock_volume_types): + mock_client = self.setup_driver() + + _mock_volume_types.return_value = { + 'name': 'gold', + 'id': 'gold-id', + 'extra_specs': { + 'hplh:provisioning': 'thin', + 'hplh:ao': 'true', + 'hplh:data_pl': 'r-0', + 'volume_type': self.volume_type}} + + self.driver.proxy.retype = mock.Mock( + side_effect=exception.VolumeNotFound(volume_id="fake")) + + self.driver.proxy.api_version = "1.1" + + volume = {'display_name': 'Foo Volume', + 'host': 'stack@lefthand#lefthand', + 'volume_type': 'gold', + 'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e', + 'id': '12345'} + + with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + existing_ref = {'source-name': self.volume_name} + + self.assertRaises(exception.VolumeNotFound, + self.driver.manage_existing, + volume, + existing_ref) + + mock_client.assert_has_calls( + self.driver_startup_call_stack + [ + mock.call.getVolumeByName(self.volume_name), + mock.call.logout()] + + self.driver_startup_call_stack + [ + mock.call.modifyVolume(self.volume_id, + {'name': 'volume-12345'}), + mock.call.logout()] + + self.driver_startup_call_stack + [ + mock.call.modifyVolume(self.volume_id, + {'name': 'fakevolume'}), + mock.call.logout()]) + + def test_manage_existing_volume_type_exception(self): + mock_client = self.setup_driver() + + self.driver.proxy.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, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + existing_ref = {'source-name': self.volume_name} + + self.assertRaises(exception.ManageExistingVolumeTypeMismatch, + self.driver.manage_existing, + volume=volume, + existing_ref=existing_ref) + + mock_client.assert_has_calls( + self.driver_startup_call_stack + [ + mock.call.getVolumeByName(self.volume_name), + mock.call.logout()]) + + def test_manage_existing_get_size(self): + mock_client = self.setup_driver() + mock_client.getVolumeByName.return_value = {'size': 2147483648} + + self.driver.proxy.api_version = "1.1" + + with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + + volume = {} + existing_ref = {'source-name': self.volume_name} + + size = self.driver.manage_existing_get_size(volume, existing_ref) + + expected_size = 2 + expected = [mock.call.getVolumeByName(existing_ref['source-name']), + mock.call.logout()] + + mock_client.assert_has_calls( + self.driver_startup_call_stack + + expected) + self.assertEqual(expected_size, size) + + def test_manage_existing_get_size_invalid_reference(self): + mock_client = self.setup_driver() + mock_client.getVolumeByName.return_value = {'size': 2147483648} + + self.driver.proxy.api_version = "1.1" + + with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + + volume = {} + existing_ref = {'source-name': "volume-12345"} + + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, + volume=volume, + existing_ref=existing_ref) + + mock_client.assert_has_calls([]) + + existing_ref = {} + + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver.manage_existing_get_size, + volume=volume, + existing_ref=existing_ref) + + mock_client.assert_has_calls([]) + + def test_manage_existing_get_size_invalid_input(self): + mock_client = self.setup_driver() + mock_client.getVolumeByName.side_effect = ( + hpexceptions.HTTPNotFound('fake')) + + self.driver.proxy.api_version = "1.1" + + with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + + volume = {} + existing_ref = {'source-name': self.volume_name} + + self.assertRaises(exception.InvalidInput, + self.driver.manage_existing_get_size, + volume=volume, + existing_ref=existing_ref) + + expected = [mock.call.getVolumeByName(existing_ref['source-name'])] + + mock_client.assert_has_calls( + self.driver_startup_call_stack + + expected) + + def test_unmanage(self): + mock_client = self.setup_driver() + mock_client.getVolumeByName.return_value = {'id': self.volume_id} + + self.driver.proxy.api_version = "1.1" + + with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy, + '_create_client') as mock_do_setup: + mock_do_setup.return_value = mock_client + self.driver.unmanage(self.volume) + + new_name = 'unm-' + str(self.volume['id']) + + expected = [ + mock.call.getVolumeByName(self.volume['name']), + mock.call.modifyVolume(self.volume['id'], {'name': new_name}), + mock.call.logout() + ] + + mock_client.assert_has_calls( + self.driver_startup_call_stack + + expected) + + def test_api_version(self): + self.setup_driver() + self.driver.proxy.api_version = "1.1" + self.driver.proxy._check_api_version() + + self.driver.proxy.api_version = "1.0" + self.assertRaises(exception.InvalidInput, + self.driver.proxy._check_api_version) \ No newline at end of file diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py index 55295f8fa..9d84bca55 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py @@ -52,9 +52,10 @@ class HPLeftHandISCSIDriver(driver.VolumeDriver): 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. """ - VERSION = "1.0.4" + VERSION = "1.0.5" def __init__(self, *args, **kwargs): super(HPLeftHandISCSIDriver, self).__init__(*args, **kwargs) @@ -152,3 +153,12 @@ class HPLeftHandISCSIDriver(driver.VolumeDriver): 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) + + def manage_existing(self, volume, existing_ref): + return self.proxy.manage_existing(volume, existing_ref) + + def manage_existing_get_size(self, volume, existing_ref): + return self.proxy.manage_existing_get_size(volume, existing_ref) + + def unmanage(self, volume): + return self.proxy.unmanage(volume) 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 890596976..507e1dc56 100644 --- a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py +++ b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py @@ -16,6 +16,7 @@ """HP LeftHand SAN ISCSI REST Proxy.""" from oslo_config import cfg +from oslo_utils import excutils from oslo_utils import importutils from oslo_utils import units @@ -27,6 +28,11 @@ 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") @@ -62,6 +68,7 @@ hplefthand_opts = [ CONF = cfg.CONF CONF.register_opts(hplefthand_opts) +MIN_API_VERSION = "1.1" # map the extra spec key to the REST client option key extra_specs_key_map = { @@ -96,9 +103,10 @@ class HPLeftHandRESTProxy(driver.ISCSIDriver): 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. """ - VERSION = "1.0.8" + VERSION = "1.0.9" device_stats = {} @@ -149,7 +157,21 @@ class HPLeftHandRESTProxy(driver.ISCSIDriver): raise exception.DriverNotInitialized(ex) def check_for_setup_error(self): - pass + """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') % { @@ -614,3 +636,177 @@ class HPLeftHandRESTProxy(driver.ISCSIDriver): self._logout(client) return (True, None) + + 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) -- 2.45.2