]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Adding manage/unmanage support for LeftHand driver
authorAnthony Lee <anthony.mic.lee@hp.com>
Wed, 11 Feb 2015 17:19:32 +0000 (09:19 -0800)
committerAnthony Lee <anthony.mic.lee@hp.com>
Wed, 4 Mar 2015 15:47:29 +0000 (07:47 -0800)
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
cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py
cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py

index 409a5dddafbc8cba7d36c116ba91e297988ca61c..0bbb5f7f86e980b6809f3c702d338535f09ef3f4 100644 (file)
@@ -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
index 55295f8fa62318badec7aaa7279833a68c89bcb5..9d84bca55403d835914c2192eeec940b865026fc 100644 (file)
@@ -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)
index 890596976bebc88ccf28a38adc238cdd794cf5ae..507e1dc5618b72bbd77768c2603a7b21d4936de6 100644 (file)
@@ -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': <name of the virtual volume>}
+        """
+        # 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': <name of the virtual volume>}
+        """
+        # 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)