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,
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',
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
"""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
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")
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 = {
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 = {}
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') % {
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)