]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
3PAR volume manage/unmanage support
authorAnthony Lee <anthony.mic.lee@hp.com>
Fri, 20 Jun 2014 23:05:48 +0000 (16:05 -0700)
committerAnthony Lee <anthony.mic.lee@hp.com>
Thu, 26 Jun 2014 00:17:48 +0000 (17:17 -0700)
HP 3PAR support for managing and unmanaging volumes.

Partially Implements: blueprint add-export-import-volumes
Change-Id: Ibb0c34eccad4f8bb028025d055d9a59c2a3a6204

cinder/tests/test_hp3par.py
cinder/volume/drivers/san/hp/hp_3par_common.py
cinder/volume/drivers/san/hp/hp_3par_fc.py
cinder/volume/drivers/san/hp/hp_3par_iscsi.py

index 6f33950acad7f0fa0c66a00f2cd2b85c28860907..cb554c6a44c15e9abfce64fa777f98ed34ab3d79 100644 (file)
@@ -942,6 +942,282 @@ class HP3PARBaseDriver(object):
                                                          host, None)
         self.assertEqual(expected_info, vlun_info)
 
+    @mock.patch.object(volume_types, 'get_volume_type')
+    def test_manage_existing(self, _mock_volume_types):
+        mock_client = self.setup_driver()
+
+        _mock_volume_types.return_value = {
+            'name': 'gold',
+            'extra_specs': {
+                'cpg': HP3PAR_CPG,
+                'snap_cpg': HP3PAR_CPG_SNAP,
+                'vvs_name': self.VVS_NAME,
+                'qos': self.QOS,
+                'tpvv': True,
+                'volume_type': self.volume_type}}
+        comment = (
+            '{"display_name": "Foo Volume"}')
+        new_comment = (
+            '{"volume_type_name": "gold",'
+            ' "display_name": "Foo Volume",'
+            ' "name": "volume-007dbfce-7579-40bc-8f90-a20b3902283e",'
+            ' "volume_type_id": "acfa9fa4-54a0-4340-a3d8-bfcf19aea65e",'
+            ' "volume_id": "007dbfce-7579-40bc-8f90-a20b3902283e",'
+            ' "qos": {},'
+            ' "type": "OpenStack"}')
+        volume = {'display_name': None,
+                  'volume_type': 'gold',
+                  'volume_type_id': 'acfa9fa4-54a0-4340-a3d8-bfcf19aea65e',
+                  'id': '007dbfce-7579-40bc-8f90-a20b3902283e'}
+
+        mock_client.getVolume.return_value = {'comment': comment}
+
+        unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id'])
+        osv_matcher = self.driver.common._get_3par_vol_name(volume['id'])
+        existing_ref = {'name': unm_matcher}
+
+        obj = self.driver.manage_existing(volume, existing_ref)
+
+        expected_obj = {'display_name': 'Foo Volume'}
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.modifyVolume(existing_ref['name'],
+                                   {'newName': osv_matcher,
+                                    'comment': new_comment}),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_obj, obj)
+
+        volume['display_name'] = 'Test Volume'
+
+        obj = self.driver.manage_existing(volume, existing_ref)
+
+        expected_obj = {'display_name': 'Test Volume'}
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.modifyVolume(existing_ref['name'],
+                                   {'newName': osv_matcher,
+                                    'comment': new_comment}),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_obj, obj)
+
+    def test_manage_existing_no_volume_type(self):
+        mock_client = self.setup_driver()
+
+        comment = (
+            '{"display_name": "Foo Volume"}')
+        new_comment = (
+            '{"type": "OpenStack",'
+            ' "display_name": "Foo Volume",'
+            ' "name": "volume-007dbfce-7579-40bc-8f90-a20b3902283e",'
+            ' "volume_id": "007dbfce-7579-40bc-8f90-a20b3902283e"}')
+        volume = {'display_name': None,
+                  'volume_type': None,
+                  'id': '007dbfce-7579-40bc-8f90-a20b3902283e'}
+
+        mock_client.getVolume.return_value = {'comment': comment}
+
+        unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id'])
+        osv_matcher = self.driver.common._get_3par_vol_name(volume['id'])
+        existing_ref = {'name': unm_matcher}
+
+        obj = self.driver.manage_existing(volume, existing_ref)
+
+        expected_obj = {'display_name': 'Foo Volume'}
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.modifyVolume(existing_ref['name'],
+                                   {'newName': osv_matcher,
+                                    'comment': new_comment}),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_obj, obj)
+
+        volume['display_name'] = 'Test Volume'
+
+        obj = self.driver.manage_existing(volume, existing_ref)
+
+        expected_obj = {'display_name': 'Test Volume'}
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.modifyVolume(existing_ref['name'],
+                                   {'newName': osv_matcher,
+                                    'comment': new_comment}),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_obj, obj)
+
+        mock_client.getVolume.return_value = {}
+        volume['display_name'] = None
+
+        obj = self.driver.manage_existing(volume, existing_ref)
+
+        expected_obj = {'display_name': None}
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.modifyVolume(existing_ref['name'],
+                                   {'newName': osv_matcher,
+                                    'comment': new_comment}),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_obj, obj)
+
+    def test_manage_existing_invalid_input(self):
+        mock_client = self.setup_driver()
+
+        volume = {'display_name': None,
+                  'volume_type': None,
+                  'id': '007dbfce-7579-40bc-8f90-a20b3902283e'}
+
+        mock_client.getVolume.side_effect = hpexceptions.HTTPNotFound('fake')
+
+        unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id'])
+        existing_ref = {'name': unm_matcher}
+
+        self.assertRaises(exception.InvalidInput,
+                          self.driver.manage_existing,
+                          volume=volume,
+                          existing_ref=existing_ref)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+
+    def test_manage_existing_volume_type_exception(self):
+        mock_client = self.setup_driver()
+
+        comment = (
+            '{"display_name": "Foo Volume"}')
+        volume = {'display_name': None,
+                  'volume_type': 'gold',
+                  'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e',
+                  'id': '007dbfce-7579-40bc-8f90-a20b3902283e'}
+
+        mock_client.getVolume.return_value = {'comment': comment}
+
+        unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id'])
+        existing_ref = {'name': unm_matcher}
+
+        self.assertRaises(exception.ManageExistingVolumeTypeMismatch,
+                          self.driver.manage_existing,
+                          volume=volume,
+                          existing_ref=existing_ref)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+
+    def test_manage_existing_get_size(self):
+        mock_client = self.setup_driver()
+        mock_client.getVolume.return_value = {'sizeMiB': 2048}
+
+        unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id'])
+        volume = {}
+        existing_ref = {'name': unm_matcher}
+
+        size = self.driver.manage_existing_get_size(volume, existing_ref)
+
+        expected_size = 2
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected, True)
+        self.assertEqual(expected_size, size)
+
+    def test_manage_existing_get_size_invalid_reference(self):
+        mock_client = self.setup_driver()
+        volume = {}
+        existing_ref = {'name': self.VOLUME_3PAR_NAME}
+
+        self.assertRaises(exception.ManageExistingInvalidReference,
+                          self.driver.manage_existing_get_size,
+                          volume=volume,
+                          existing_ref=existing_ref)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+
+        existing_ref = {}
+
+        self.assertRaises(exception.ManageExistingInvalidReference,
+                          self.driver.manage_existing_get_size,
+                          volume=volume,
+                          existing_ref=existing_ref)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+
+    def test_manage_existing_get_size_invalid_input(self):
+        mock_client = self.setup_driver()
+        mock_client.getVolume.side_effect = hpexceptions.HTTPNotFound('fake')
+
+        unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id'])
+        volume = {}
+        existing_ref = {'name': unm_matcher}
+
+        self.assertRaises(exception.InvalidInput,
+                          self.driver.manage_existing_get_size,
+                          volume=volume,
+                          existing_ref=existing_ref)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume(existing_ref['name']),
+            mock.call.logout()
+        ]
+
+        mock_client.assert_has_calls(expected)
+
+    def test_unmanage(self):
+        mock_client = self.setup_driver()
+
+        self.driver.unmanage(self.volume)
+
+        osv_matcher = self.driver.common._get_3par_vol_name(self.volume['id'])
+        unm_matcher = self.driver.common._get_3par_unm_name(self.volume['id'])
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.modifyVolume(osv_matcher, {'newName': unm_matcher}),
+            mock.call.logout()]
+
+        mock_client.assert_has_calls(expected)
+
 
 class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
 
index 4c06fad38bef7f5589aeecd17244e9b2b64e430d..63f1898edbc57deff62770849c9f36e8b64b8a4c 100644 (file)
@@ -37,6 +37,7 @@ array.
 import ast
 import base64
 import json
+import math
 import pprint
 import re
 import uuid
@@ -129,10 +130,11 @@ class HP3PARCommon(object):
         2.0.10 - Fixed an issue with 3PAR vlun location bug #1315542
         2.0.11 - Remove hp3parclient requirement from unit tests #1315195
         2.0.12 - Volume detach hangs when host is in a host set bug #1317134
+        2.0.13 - Added support for managing/unmanaging of volumes
 
     """
 
-    VERSION = "2.0.12"
+    VERSION = "2.0.13"
 
     stats = {}
 
@@ -267,6 +269,116 @@ class HP3PARCommon(object):
         growth_size_mib = growth_size * units.Ki
         self._extend_volume(volume, volume_name, growth_size_mib)
 
+    def manage_existing(self, volume, existing_ref):
+        """Manage an existing 3PAR volume."""
+        # Check for the existence of the virtual volume.
+        try:
+            vol = self.client.getVolume(existing_ref['name'])
+        except hpexceptions.HTTPNotFound:
+            err = (_("Virtual volume '%s' doesn't exist on array.") %
+                   existing_ref['name'])
+            LOG.error(err)
+            raise exception.InvalidInput(reason=err)
+
+        new_comment = {}
+
+        # Use the display name from the existing volume if no new name
+        # was chosen by the user.
+        if volume['display_name']:
+            display_name = volume['display_name']
+            new_comment['display_name'] = volume['display_name']
+        elif 'comment' in vol:
+            display_name = self._get_3par_vol_comment_value(vol['comment'],
+                                                            'display_name')
+            if display_name:
+                new_comment['display_name'] = display_name
+        else:
+            display_name = None
+
+        # Generate the new volume information based off of the new ID.
+        new_vol_name = self._get_3par_vol_name(volume['id'])
+        name = 'volume-' + volume['id']
+
+        new_comment['volume_id'] = volume['id']
+        new_comment['name'] = name
+        new_comment['type'] = 'OpenStack'
+
+        # Create new comments for the existing volume depending on
+        # whether the user's volume type choice.
+        # TODO(Anthony) when retype is available handle retyping of
+        # a volume.
+        if volume['volume_type']:
+            try:
+                settings = self.get_volume_settings_from_type(volume)
+            except Exception:
+                reason = (_("Volume type ID '%s' is invalid.") %
+                          volume['volume_type_id'])
+                raise exception.ManageExistingVolumeTypeMismatch(reason=reason)
+
+            volume_type = self._get_volume_type(volume['volume_type_id'])
+
+            new_comment['volume_type_name'] = volume_type['name']
+            new_comment['volume_type_id'] = volume['volume_type_id']
+            new_comment['qos'] = settings['qos']
+
+        # Update the existing volume with the new name and comments.
+        self.client.modifyVolume(existing_ref['name'],
+                                 {'newName': new_vol_name,
+                                  'comment': json.dumps(new_comment)})
+
+        LOG.info(_("Virtual volume '%(ref)s' renamed to '%(new)s'.") %
+                 {'ref': existing_ref['name'], 'new': new_vol_name})
+        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}
+
+    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:
+        {'name': <name of the virtual volume>}
+        """
+        # Check that a valid reference was provided.
+        if 'name' not in existing_ref:
+            reason = _("Reference must contain name element.")
+            raise exception.ManageExistingInvalidReference(
+                existing_ref=existing_ref,
+                reason=reason)
+
+        # Make sure the reference is not in use.
+        if re.match('osv-*|oss-*|vvs-*', existing_ref['name']):
+            reason = _("Reference must be for an unmanaged virtual volume.")
+            raise exception.ManageExistingInvalidReference(
+                existing_ref=existing_ref,
+                reason=reason)
+
+        # Check for the existence of the virtual volume.
+        try:
+            vol = self.client.getVolume(existing_ref['name'])
+        except hpexceptions.HTTPNotFound:
+            err = (_("Virtual volume '%s' doesn't exist on array.") %
+                   existing_ref['name'])
+            LOG.error(err)
+            raise exception.InvalidInput(reason=err)
+
+        return int(math.ceil(float(vol['sizeMiB']) / units.Ki))
+
+    def unmanage(self, volume):
+        """Removes the specified volume from Cinder management."""
+        # Rename the volume's name to unm-* format so that it can be
+        # easily found later.
+        vol_name = self._get_3par_vol_name(volume['id'])
+        new_vol_name = self._get_3par_unm_name(volume['id'])
+        self.client.modifyVolume(vol_name, {'newName': new_vol_name})
+
+        LOG.info(_("Virtual volume %(disp)s '%(vol)s' is no longer managed. "
+                   "Volume renamed to '%(new)s'.") %
+                 {'disp': volume['display_name'],
+                  'vol': vol_name,
+                  'new': new_vol_name})
+
     def _extend_volume(self, volume, volume_name, growth_size_mib,
                        _convert_to_base=False):
         try:
@@ -320,6 +432,10 @@ class HP3PARCommon(object):
         vvs_name = self._encode_name(volume_id)
         return "vvs-%s" % vvs_name
 
+    def _get_3par_unm_name(self, volume_id):
+        unm_name = self._encode_name(volume_id)
+        return "unm-%s" % unm_name
+
     def _encode_name(self, name):
         uuid_str = name.replace("-", "")
         vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str)
index 251e90f20a44a356eea0c3ab3d5294aeaae0917f..413a17db611b61899803debc7d127c3a4ddd0757 100644 (file)
@@ -61,10 +61,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
         2.0.0 - Update hp3parclient API uses 3.0.x
         2.0.2 - Add back-end assisted volume migrate
         2.0.3 - Added initiator-target map for FC Zone Manager
+        2.0.4 - Added support for managing/unmanaging of volumes
 
     """
 
-    VERSION = "2.0.3"
+    VERSION = "2.0.4"
 
     def __init__(self, *args, **kwargs):
         super(HP3PARFCDriver, self).__init__(*args, **kwargs)
@@ -355,6 +356,32 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
         finally:
             self.common.client_logout()
 
+    @utils.synchronized('3par', external=True)
+    def manage_existing(self, volume, existing_ref):
+        self.common.client_login()
+        try:
+            return self.common.manage_existing(volume, existing_ref)
+        finally:
+            self.common.client_logout()
+
+    @utils.synchronized('3par', external=True)
+    def manage_existing_get_size(self, volume, existing_ref):
+        self.common.client_login()
+        try:
+            size = self.common.manage_existing_get_size(volume, existing_ref)
+        finally:
+            self.common.client_logout()
+
+        return size
+
+    @utils.synchronized('3par', external=True)
+    def unmanage(self, volume):
+        self.common.client_login()
+        try:
+            self.common.unmanage(volume)
+        finally:
+            self.common.client_logout()
+
     @utils.synchronized('3par', external=True)
     def attach_volume(self, context, volume, instance_uuid, host_name,
                       mountpoint):
index 7fa08a297a82241c2e0c1fc43c1e80d564f7b71f..94900fafa9617f7b612d3ce6b3faff6201a60ba2 100644 (file)
@@ -64,10 +64,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         1.3.0 - Removed all SSH code.  We rely on the hp3parclient now.
         2.0.0 - Update hp3parclient API uses 3.0.x
         2.0.2 - Add back-end assisted volume migrate
+        2.0.3 - Added support for managing/unmanaging of volumes
 
     """
 
-    VERSION = "2.0.2"
+    VERSION = "2.0.3"
 
     def __init__(self, *args, **kwargs):
         super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
@@ -442,6 +443,32 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         finally:
             self.common.client_logout()
 
+    @utils.synchronized('3par', external=True)
+    def manage_existing(self, volume, existing_ref):
+        self.common.client_login()
+        try:
+            return self.common.manage_existing(volume, existing_ref)
+        finally:
+            self.common.client_logout()
+
+    @utils.synchronized('3par', external=True)
+    def manage_existing_get_size(self, volume, existing_ref):
+        self.common.client_login()
+        try:
+            size = self.common.manage_existing_get_size(volume, existing_ref)
+        finally:
+            self.common.client_logout()
+
+        return size
+
+    @utils.synchronized('3par', external=True)
+    def unmanage(self, volume):
+        self.common.client_login()
+        try:
+            self.common.unmanage(volume)
+        finally:
+            self.common.client_logout()
+
     @utils.synchronized('3par', external=True)
     def attach_volume(self, context, volume, instance_uuid, host_name,
                       mountpoint):