From 62b0acb5035beab5651e97eb29515a6dc129e064 Mon Sep 17 00:00:00 2001 From: rajinir Date: Thu, 19 May 2016 10:37:26 -0500 Subject: [PATCH] Volume manage/unmanage support for Eqlx driver This change will allow cinder to manage existing volumes on Dell PS Series(Equallogic) backend and also unmanage them. DocImpact Implements: blueprint eqlx-volume-manage-unmanage Change-Id: I2a2f950e9ea3017d8c0bee8dbc025cf4f4a8eb99 --- cinder/tests/unit/test_eqlx.py | 66 +++++++++++ cinder/volume/drivers/eqlx.py | 104 +++++++++++++++++- ...lume-manage-unmanage-a24ec7f0d9989df3.yaml | 3 + 3 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/eqlx-volume-manage-unmanage-a24ec7f0d9989df3.yaml diff --git a/cinder/tests/unit/test_eqlx.py b/cinder/tests/unit/test_eqlx.py index 265cfebdb..00318eb77 100644 --- a/cinder/tests/unit/test_eqlx.py +++ b/cinder/tests/unit/test_eqlx.py @@ -66,6 +66,8 @@ class DellEQLSanISCSIDriverTestCase(test.TestCase): configuration=self.configuration) self.volume_name = "fakevolume" self.volid = "fakeid" + self.volume = {'name': self.volume_name, + 'display_name': 'fake_display_name'} self.connector = { 'ip': '10.0.0.2', 'initiator': 'iqn.1993-08.org.debian:01:2227dab76162', @@ -78,6 +80,11 @@ class DellEQLSanISCSIDriverTestCase(test.TestCase): self.fake_iqn = 'iqn.2003-10.com.equallogic:group01:25366:fakev' self.fake_iqn_return = ['iSCSI target name is %s.' % self.fake_iqn] + self.fake_volume_output = ["Size: 5GB", + "iSCSI Name: %s" % self.fake_iqn, + "Description: "] + self.fake_volume_info = {'size': 5.0, + 'iSCSI_Name': self.fake_iqn} self.driver._group_ip = '10.0.1.6' self.properties = { 'target_discovered': True, @@ -240,6 +247,63 @@ class DellEQLSanISCSIDriverTestCase(test.TestCase): mock_eql_execute.configure_mock(**mock_attrs) self.driver.extend_volume(volume, new_size) + def test_get_volume_info(self): + attrs = ('volume', 'select', self.volume, 'show') + with mock.patch.object(self.driver, + '_eql_execute') as mock_eql_execute: + mock_eql_execute.return_value = self.fake_volume_output + data = self.driver._get_volume_info(self.volume) + mock_eql_execute.assert_called_with(*attrs) + self.assertEqual(self.fake_volume_info, data) + + def test_get_volume_info_negative(self): + with mock.patch.object(self.driver, + '_eql_execute') as mock_eql_execute: + mock_eql_execute.side_effect = processutils.ProcessExecutionError( + stdout='% Error ..... does not exist.\n') + self.assertRaises(exception.ManageExistingInvalidReference, + self.driver._get_volume_info, self.volume_name) + + def test_manage_existing(self): + ref = {'source-name': self.volume_name} + attrs = ('volume', 'select', self.volume_name, + 'multihost-access', 'enable') + with mock.patch.object(self.driver, + '_eql_execute') as mock_eql_execute: + with mock.patch.object(self.driver, + '_get_volume_info') as mock_volume_info: + mock_volume_info.return_value = self.fake_volume_info + mock_eql_execute.return_value = self.fake_iqn_return + model_update = self.driver.manage_existing(self.volume, ref) + mock_eql_execute.assert_called_with(*attrs) + self.assertEqual(self._model_update, model_update) + + def test_manage_existing_invalid_ref(self): + ref = {} + self.assertRaises(exception.InvalidInput, + self.driver.manage_existing, self.volume, ref) + + def test_manage_existing_get_size(self): + ref = {'source-name': self.volume_name} + with mock.patch.object(self.driver, + '_eql_execute') as mock_eql_execute: + mock_eql_execute.return_value = self.fake_volume_output + size = self.driver.manage_existing_get_size(self.volume, ref) + self.assertEqual(float('5.0'), size) + + def test_manage_existing_get_size_invalid_ref(self): + """Error on manage with invalid reference.""" + ref = {} + self.assertRaises(exception.InvalidInput, + self.driver.manage_existing_get_size, + self.volume, ref) + + def test_unmanage(self): + with mock.patch.object(self.driver, + '_eql_execute') as mock_eql_execute: + mock_eql_execute.return_value = None + self.driver.unmanage(self.volume) + def test_initialize_connection(self): volume = {'name': self.volume_name} mock_attrs = {'args': ['volume', 'select', volume['name'], 'access', @@ -349,8 +413,10 @@ class DellEQLSanISCSIDriverTestCase(test.TestCase): def test_get_space_in_gb(self): self.assertEqual(123.0, self.driver._get_space_in_gb('123.0GB')) + self.assertEqual(124.0, self.driver._get_space_in_gb('123.5GB')) self.assertEqual(123.0 * 1024, self.driver._get_space_in_gb('123.0TB')) self.assertEqual(1.0, self.driver._get_space_in_gb('1024.0MB')) + self.assertEqual(2.0, self.driver._get_space_in_gb('1536.0MB')) def test_get_output(self): diff --git a/cinder/volume/drivers/eqlx.py b/cinder/volume/drivers/eqlx.py index c1d7956f3..b36e5f5c3 100644 --- a/cinder/volume/drivers/eqlx.py +++ b/cinder/volume/drivers/eqlx.py @@ -16,6 +16,7 @@ """Volume driver for Dell EqualLogic Storage.""" import functools +import math import random import eventlet @@ -153,10 +154,11 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver): 1.0 - Initial driver 1.1.0 - Misc fixes 1.2.0 - Deprecated eqlx_cli_timeout infavor of ssh_conn_timeout + 1.3.0 - Added support for manage/unmanage volume """ - VERSION = "1.2.0" + VERSION = "1.3.0" def __init__(self, *args, **kwargs): super(DellEQLSanISCSIDriver, self).__init__(*args, **kwargs) @@ -300,6 +302,9 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver): def _get_volume_data(self, lines): prefix = 'iSCSI target name is ' target_name = self._get_prefixed_value(lines, prefix)[:-1] + return self._get_model_update(target_name) + + def _get_model_update(self, target_name): lun_id = "%s:%s,1 %s 0" % (self._group_ip, '3260', target_name) model_update = {} model_update['provider_location'] = lun_id @@ -318,7 +323,7 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver): elif val.endswith('TB'): scale = 1.0 * 1024 part = 'TB' - return scale * float(val.partition(part)[0]) + return math.ceil(scale * float(val.partition(part)[0])) def _update_volume_stats(self): """Retrieve stats info from eqlx group.""" @@ -369,6 +374,25 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver): self._stats = data + def _get_volume_info(self, volume_name): + """Get the volume details on the array""" + command = ['volume', 'select', volume_name, 'show'] + try: + data = {} + for line in self._eql_execute(*command): + if line.startswith('Size:'): + out_tup = line.rstrip().partition(' ') + data['size'] = self._get_space_in_gb(out_tup[-1]) + elif line.startswith('iSCSI Name:'): + out_tup = line.rstrip().partition(': ') + data['iSCSI_Name'] = out_tup[-1] + return data + except processutils.ProcessExecutionError: + msg = (_("Volume does not exists %s.") % volume_name) + LOG.error(msg) + raise exception.ManageExistingInvalidReference( + existing_ref=volume_name, reason=msg) + def _check_volume(self, volume): """Check if the volume exists on the Array.""" command = ['volume', 'select', volume['name'], 'show'] @@ -452,6 +476,18 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver): 'for volume "%s".'), volume['name']) + def _set_volume_description(self, volume, description): + """Set the description of the volume""" + try: + cmd = ['volume', 'select', + volume['name'], 'description', description] + self._eql_execute(*cmd) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to set description ' + 'for volume "%s".'), + volume['name']) + def delete_volume(self, volume): """Delete a volume.""" try: @@ -622,5 +658,69 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver): 'current_size': volume['size'], 'new_size': new_size}) + def _get_existing_volume_ref_name(self, ref): + existing_volume_name = None + if 'source-name' in ref: + existing_volume_name = ref['source-name'] + elif 'source-id' in ref: + existing_volume_name = ref['source-id'] + else: + msg = _('Reference must contain source-id or source-name.') + LOG.error(msg) + raise exception.InvalidInput(reason=msg) + + return existing_volume_name + + def manage_existing(self, volume, existing_ref): + """Manage an existing volume on the backend storage.""" + existing_volume_name = self._get_existing_volume_ref_name(existing_ref) + try: + cmd = ['volume', 'rename', + existing_volume_name, volume['name']] + self._eql_execute(*cmd) + self._set_volume_description(volume, '"OpenStack Managed"') + self.add_multihost_access(volume) + data = self._get_volume_info(volume['name']) + updates = self._get_model_update(data['iSCSI_Name']) + LOG.info(_LI("Backend volume %(back_vol)s renamed to " + "%(vol)s and is now managed by cinder."), + {'back_vol': existing_volume_name, + 'vol': volume['name']}) + return updates + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to manage volume "%s".'), volume['name']) + + def manage_existing_get_size(self, volume, existing_ref): + """Return size of volume to be managed by manage_existing. + + When calculating the size, round up to the next GB. + + :param volume: Cinder volume to manage + :param existing_ref: Driver-specific information used to identify a + volume + """ + existing_volume_name = self._get_existing_volume_ref_name(existing_ref) + data = self._get_volume_info(existing_volume_name) + return data['size'] + + def unmanage(self, volume): + """Removes the specified volume from Cinder management. + + Does not delete the underlying backend storage object. + + :param volume: Cinder volume to unmanage + """ + try: + self._set_volume_description(volume, '"OpenStack UnManaged"') + LOG.info(_LI("Virtual volume %(disp)s '%(vol)s' is no " + "longer managed."), + {'disp': volume['display_name'], + 'vol': volume['name']}) + except Exception: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('Failed to unmanage volume "%s".'), + volume['name']) + def local_path(self, volume): raise NotImplementedError() diff --git a/releasenotes/notes/eqlx-volume-manage-unmanage-a24ec7f0d9989df3.yaml b/releasenotes/notes/eqlx-volume-manage-unmanage-a24ec7f0d9989df3.yaml new file mode 100644 index 000000000..252263bbe --- /dev/null +++ b/releasenotes/notes/eqlx-volume-manage-unmanage-a24ec7f0d9989df3.yaml @@ -0,0 +1,3 @@ +--- +features: + - Added manage/unmanage volume support for Dell Equallogic driver.