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
This commit is contained in:
parent
e43aa0061b
commit
62b0acb503
@ -66,6 +66,8 @@ class DellEQLSanISCSIDriverTestCase(test.TestCase):
|
|||||||
configuration=self.configuration)
|
configuration=self.configuration)
|
||||||
self.volume_name = "fakevolume"
|
self.volume_name = "fakevolume"
|
||||||
self.volid = "fakeid"
|
self.volid = "fakeid"
|
||||||
|
self.volume = {'name': self.volume_name,
|
||||||
|
'display_name': 'fake_display_name'}
|
||||||
self.connector = {
|
self.connector = {
|
||||||
'ip': '10.0.0.2',
|
'ip': '10.0.0.2',
|
||||||
'initiator': 'iqn.1993-08.org.debian:01:2227dab76162',
|
'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 = 'iqn.2003-10.com.equallogic:group01:25366:fakev'
|
||||||
self.fake_iqn_return = ['iSCSI target name is %s.' % self.fake_iqn]
|
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.driver._group_ip = '10.0.1.6'
|
||||||
self.properties = {
|
self.properties = {
|
||||||
'target_discovered': True,
|
'target_discovered': True,
|
||||||
@ -240,6 +247,63 @@ class DellEQLSanISCSIDriverTestCase(test.TestCase):
|
|||||||
mock_eql_execute.configure_mock(**mock_attrs)
|
mock_eql_execute.configure_mock(**mock_attrs)
|
||||||
self.driver.extend_volume(volume, new_size)
|
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):
|
def test_initialize_connection(self):
|
||||||
volume = {'name': self.volume_name}
|
volume = {'name': self.volume_name}
|
||||||
mock_attrs = {'args': ['volume', 'select', volume['name'], 'access',
|
mock_attrs = {'args': ['volume', 'select', volume['name'], 'access',
|
||||||
@ -349,8 +413,10 @@ class DellEQLSanISCSIDriverTestCase(test.TestCase):
|
|||||||
|
|
||||||
def test_get_space_in_gb(self):
|
def test_get_space_in_gb(self):
|
||||||
self.assertEqual(123.0, self.driver._get_space_in_gb('123.0GB'))
|
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(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(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):
|
def test_get_output(self):
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
"""Volume driver for Dell EqualLogic Storage."""
|
"""Volume driver for Dell EqualLogic Storage."""
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import math
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
@ -153,10 +154,11 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver):
|
|||||||
1.0 - Initial driver
|
1.0 - Initial driver
|
||||||
1.1.0 - Misc fixes
|
1.1.0 - Misc fixes
|
||||||
1.2.0 - Deprecated eqlx_cli_timeout infavor of ssh_conn_timeout
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DellEQLSanISCSIDriver, self).__init__(*args, **kwargs)
|
super(DellEQLSanISCSIDriver, self).__init__(*args, **kwargs)
|
||||||
@ -300,6 +302,9 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver):
|
|||||||
def _get_volume_data(self, lines):
|
def _get_volume_data(self, lines):
|
||||||
prefix = 'iSCSI target name is '
|
prefix = 'iSCSI target name is '
|
||||||
target_name = self._get_prefixed_value(lines, prefix)[:-1]
|
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)
|
lun_id = "%s:%s,1 %s 0" % (self._group_ip, '3260', target_name)
|
||||||
model_update = {}
|
model_update = {}
|
||||||
model_update['provider_location'] = lun_id
|
model_update['provider_location'] = lun_id
|
||||||
@ -318,7 +323,7 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver):
|
|||||||
elif val.endswith('TB'):
|
elif val.endswith('TB'):
|
||||||
scale = 1.0 * 1024
|
scale = 1.0 * 1024
|
||||||
part = 'TB'
|
part = 'TB'
|
||||||
return scale * float(val.partition(part)[0])
|
return math.ceil(scale * float(val.partition(part)[0]))
|
||||||
|
|
||||||
def _update_volume_stats(self):
|
def _update_volume_stats(self):
|
||||||
"""Retrieve stats info from eqlx group."""
|
"""Retrieve stats info from eqlx group."""
|
||||||
@ -369,6 +374,25 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver):
|
|||||||
|
|
||||||
self._stats = data
|
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):
|
def _check_volume(self, volume):
|
||||||
"""Check if the volume exists on the Array."""
|
"""Check if the volume exists on the Array."""
|
||||||
command = ['volume', 'select', volume['name'], 'show']
|
command = ['volume', 'select', volume['name'], 'show']
|
||||||
@ -452,6 +476,18 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver):
|
|||||||
'for volume "%s".'),
|
'for volume "%s".'),
|
||||||
volume['name'])
|
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):
|
def delete_volume(self, volume):
|
||||||
"""Delete a volume."""
|
"""Delete a volume."""
|
||||||
try:
|
try:
|
||||||
@ -622,5 +658,69 @@ class DellEQLSanISCSIDriver(san.SanISCSIDriver):
|
|||||||
'current_size': volume['size'],
|
'current_size': volume['size'],
|
||||||
'new_size': new_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):
|
def local_path(self, volume):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added manage/unmanage volume support for Dell Equallogic driver.
|
Loading…
Reference in New Issue
Block a user