From b45b3b46cfb1ed08acf859ac03ad2e4c87764664 Mon Sep 17 00:00:00 2001 From: Anthony Lee Date: Fri, 20 Jun 2014 16:05:48 -0700 Subject: [PATCH] 3PAR volume manage/unmanage support HP 3PAR support for managing and unmanaging volumes. Partially Implements: blueprint add-export-import-volumes Change-Id: Ibb0c34eccad4f8bb028025d055d9a59c2a3a6204 --- cinder/tests/test_hp3par.py | 276 ++++++++++++++++++ .../volume/drivers/san/hp/hp_3par_common.py | 118 +++++++- cinder/volume/drivers/san/hp/hp_3par_fc.py | 29 +- cinder/volume/drivers/san/hp/hp_3par_iscsi.py | 29 +- 4 files changed, 449 insertions(+), 3 deletions(-) diff --git a/cinder/tests/test_hp3par.py b/cinder/tests/test_hp3par.py index 6f33950ac..cb554c6a4 100644 --- a/cinder/tests/test_hp3par.py +++ b/cinder/tests/test_hp3par.py @@ -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): diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 4c06fad38..63f1898ed 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -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': } + """ + # 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) diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py index 251e90f20..413a17db6 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_fc.py +++ b/cinder/volume/drivers/san/hp/hp_3par_fc.py @@ -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): diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 7fa08a297..94900fafa 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -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): -- 2.45.2