New HP LeftHand array iSCSI driver

This driver is intended to replace the current OpenStack Block
Storage HP LeftHand (LH) StoreVirtual iSCSI Driver,
(cinder.volume.drivers.san.HpSanISCSIDriver), by moving the
existing SSH interface into the new driver to maintain backwards
compatibility, and add the new LH REST interface for new driver
features.

We have the driver broken into 3 files:
hp_lefthand_iscis.py (common interface)
hp_lefthand_cliq_proxy.py (old SSH interface)
hp_lefthand_rest_proxy.py (new REST interface)

The reason we are doing this is because the SSH interface on LH
array has connections and performance limitations. These problems
will be resolved by moving to the new LH OS REST interface. Also,
new LeftHand array capabilities will only be supported in the
REST(hplefthandclient) interface.

To support new driver capabilities (create cloned volume), the
python REST client (hplefthandclient) is required and can be
downloaded from the pypi repository:
  http://pypi.python.org/pypi/hplefthandclient.
This REST client requires LeftHand firmware version 11.5 or
greater.

The SSH interface will be phased out over time.

Driver cert test results;
Related-Bug: 1276809

Closes-Bug: 1277339

DocImpact: Document new driver configuration.

Implements blueprint lefthand-cinder-driver

Change-Id: Id557cab69022c3f7851be14cd82bdab0e4157e55
This commit is contained in:
Jim Branen 2014-01-06 16:14:35 -08:00
parent 87e906c168
commit 0e83faf4b8
10 changed files with 1598 additions and 388 deletions

View File

@ -1,360 +0,0 @@
# Copyright 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mox
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import test
from cinder.volume import configuration as conf
from cinder.volume.drivers.san.hp_lefthand import HpSanISCSIDriver
LOG = logging.getLogger(__name__)
class HpSanISCSITestCase(test.TestCase):
def setUp(self):
super(HpSanISCSITestCase, self).setUp()
self.stubs.Set(HpSanISCSIDriver, "_cliq_run",
self._fake_cliq_run)
self.stubs.Set(HpSanISCSIDriver, "_get_iscsi_properties",
self._fake_get_iscsi_properties)
configuration = mox.MockObject(conf.Configuration)
configuration.san_is_local = False
configuration.san_ip = "10.0.0.1"
configuration.san_login = "foo"
configuration.san_password = "bar"
configuration.san_ssh_port = 16022
configuration.san_clustername = "CloudCluster1"
configuration.san_thin_provision = True
configuration.append_config_values(mox.IgnoreArg())
self.driver = HpSanISCSIDriver(configuration=configuration)
self.volume_name = "fakevolume"
self.snapshot_name = "fakeshapshot"
self.connector = {'ip': '10.0.0.2',
'initiator': 'iqn.1993-08.org.debian:01:222',
'host': 'fakehost'}
self.properties = {
'target_discoverd': True,
'target_portal': '10.0.1.6:3260',
'target_iqn':
'iqn.2003-10.com.lefthandnetworks:group01:25366:fakev',
'volume_id': 1}
def tearDown(self):
super(HpSanISCSITestCase, self).tearDown()
def _fake_get_iscsi_properties(self, volume):
return self.properties
def _fake_cliq_run(self, verb, cliq_args, check_exit_code=True):
"""Return fake results for the various methods."""
def create_volume(cliq_args):
"""Create volume CLIQ input for test.
input = "createVolume description="fake description"
clusterName=Cluster01 volumeName=fakevolume
thinProvision=0 output=XML size=1GB"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="181" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['volumeName'], self.volume_name)
self.assertEqual(cliq_args['thinProvision'], '1')
self.assertEqual(cliq_args['size'], '1GB')
return output, None
def delete_volume(cliq_args):
"""Delete volume CLIQ input for test.
input = "deleteVolume volumeName=fakevolume prompt=false
output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="164" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['volumeName'], self.volume_name)
self.assertEqual(cliq_args['prompt'], 'false')
return output, None
def extend_volume(cliq_args):
"""Extend volume CLIQ input for test.
input = "modifyVolume description="fake description"
volumeName=fakevolume
output=XML size=2GB"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="181" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['volumeName'], self.volume_name)
self.assertEqual(cliq_args['size'], '2GB')
return output, None
def assign_volume(cliq_args):
"""Assign volume CLIQ input for test.
input = "assignVolumeToServer volumeName=fakevolume
serverName=fakehost
output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="174" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['volumeName'], self.volume_name)
self.assertEqual(cliq_args['serverName'], self.connector['host'])
return output, None
def unassign_volume(cliq_args):
"""Unassign volume CLIQ input for test.
input = "unassignVolumeToServer volumeName=fakevolume
serverName=fakehost output=XML
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="205" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['volumeName'], self.volume_name)
self.assertEqual(cliq_args['serverName'], self.connector['host'])
return output, None
def create_snapshot(cliq_args):
"""Create snapshot CLIQ input for test.
input = "createSnapshot description="fake description"
snapshotName=fakesnapshot
volumeName=fakevolume
output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="181" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['snapshotName'], self.snapshot_name)
self.assertEqual(cliq_args['volumeName'], self.volume_name)
return output, None
def delete_snapshot(cliq_args):
"""Delete shapshot CLIQ input for test.
input = "deleteSnapshot snapshotName=fakesnapshot prompt=false
output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="164" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['snapshotName'], self.snapshot_name)
self.assertEqual(cliq_args['prompt'], 'false')
return output, None
def create_volume_from_snapshot(cliq_args):
"""Create volume from snapshot CLIQ input for test.
input = "cloneSnapshot description="fake description"
snapshotName=fakesnapshot
volumeName=fakevolume
output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded."
name="CliqSuccess" processingTime="181" result="0"/>
</gauche>"""
self.assertEqual(cliq_args['snapshotName'], self.snapshot_name)
self.assertEqual(cliq_args['volumeName'], self.volume_name)
return output, None
def get_cluster_info(cliq_args):
"""Get cluster info CLIQ input for test.
input = "getClusterInfo clusterName=Cluster01 searchDepth=1
verbose=0 output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded." name="CliqSuccess"
processingTime="1164" result="0">
<cluster blockSize="1024" description=""
maxVolumeSizeReplication1="622957690"
maxVolumeSizeReplication2="311480287"
minVolumeSize="262144" name="Cluster01"
pageSize="262144" spaceTotal="633697992"
storageNodeCount="2" unprovisionedSpace="622960574"
useVip="true">
<nsm ipAddress="10.0.1.7" name="111-vsa"/>
<nsm ipAddress="10.0.1.8" name="112-vsa"/>
<vip ipAddress="10.0.1.6" subnetMask="255.255.255.0"/>
</cluster></response></gauche>"""
return output, None
def get_volume_info(cliq_args):
"""Get volume info CLIQ input for test.
input = "getVolumeInfo volumeName=fakevolume output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded." name="CliqSuccess"
processingTime="87" result="0">
<volume autogrowPages="4" availability="online"
blockSize="1024" bytesWritten="0" checkSum="false"
clusterName="Cluster01" created="2011-02-08T19:56:53Z"
deleting="false" description="" groupName="Group01"
initialQuota="536870912" isPrimary="true"
iscsiIqn="iqn.2003-10.com.lefthandnetworks:group01:25366:fakev"
maxSize="6865387257856" md5="9fa5c8b2cca54b2948a63d833097e1ca"
minReplication="1" name="vol-b" parity="0" replication="2"
reserveQuota="536870912" scratchQuota="4194304"
serialNumber="9fa5c8b2cca54b2948a63d8"
size="1073741824" stridePages="32" thinProvision="true">
<status description="OK" value="2"/>
<permission access="rw" authGroup="api-1"
chapName="chapusername" chapRequired="true"
id="25369" initiatorSecret="" iqn=""
iscsiEnabled="true" loadBalance="true"
targetSecret="supersecret"/>
</volume></response></gauche>"""
return output, None
def get_snapshot_info(cliq_args):
"""Get snapshot info CLIQ input for test.
input = "getSnapshotInfo snapshotName=fakesnapshot output=XML"
"""
output = """<gauche version="1.0">
<response description="Operation succeeded." name="CliqSuccess"
processingTime="87" result="0">
<snapshot applicationManaged="false" autogrowPages="32768"
automatic="false" availability="online" bytesWritten="0"
clusterName="CloudCluster1" created="2013-08-26T07:03:44Z"
deleting="false" description="" groupName="CloudGroup1"
id="730" initialQuota="536870912" isPrimary="true"
iscsiIqn="iqn.2003-10.com.lefthandnetworks:cloudgroup1:73"
md5="a64b4f850539c07fb5ce3cee5db1fcce" minReplication="1"
name="snapshot-7849288e-e5e8-42cb-9687-9af5355d674b"
replication="2" reserveQuota="536870912" scheduleId="0"
scratchQuota="4194304" scratchWritten="0"
serialNumber="a64b4f850539c07fb5ce3cee5db1fcce"
size="2147483648" stridePages="32"
volumeSerial="a64b4f850539c07fb5ce3cee5db1fcce">
<status description="OK" value="2"/>
<permission access="rw"
authGroup="api-34281B815713B78-(trimmed)51ADD4B7030853AA7"
chapName="chapusername" chapRequired="true" id="25369"
initiatorSecret="" iqn="" iscsiEnabled="true"
loadBalance="true" targetSecret="supersecret"/>
</snapshot></response></gauche>"""
return output, None
def get_server_info(cliq_args):
"""Get server info CLIQ input for test.
input = "getServerInfo serverName=fakeName"
"""
output = """<gauche version="1.0"><response result="0"/>
</gauche>"""
return output, None
def create_server(cliq_args):
"""Create server CLIQ input for test.
input = "createServer serverName=fakeName initiator=something"
"""
output = """<gauche version="1.0"><response result="0"/>
</gauche>"""
return output, None
def test_error(cliq_args):
output = """<gauche version="1.0">
<response description="Volume '134234' not found."
name="CliqVolumeNotFound" processingTime="1083"
result="8000100c"/>
</gauche>"""
return output, None
self.assertEqual(cliq_args['output'], 'XML')
try:
verbs = {'createVolume': create_volume,
'deleteVolume': delete_volume,
'modifyVolume': extend_volume,
'assignVolumeToServer': assign_volume,
'unassignVolumeToServer': unassign_volume,
'createSnapshot': create_snapshot,
'deleteSnapshot': delete_snapshot,
'cloneSnapshot': create_volume_from_snapshot,
'getClusterInfo': get_cluster_info,
'getVolumeInfo': get_volume_info,
'getSnapshotInfo': get_snapshot_info,
'getServerInfo': get_server_info,
'createServer': create_server,
'testError': test_error}
except KeyError:
raise NotImplementedError()
return verbs[verb](cliq_args)
def test_create_volume(self):
volume = {'name': self.volume_name, 'size': 1}
model_update = self.driver.create_volume(volume)
expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev 0"
expected_location = "10.0.1.6:3260,1 %s" % expected_iqn
self.assertEqual(model_update['provider_location'], expected_location)
def test_delete_volume(self):
volume = {'name': self.volume_name}
self.driver.delete_volume(volume)
def test_extend_volume(self):
volume = {'name': self.volume_name}
self.driver.extend_volume(volume, 2)
def test_initialize_connection(self):
volume = {'name': self.volume_name}
result = self.driver.initialize_connection(volume, self.connector)
self.assertEqual(result['driver_volume_type'], 'iscsi')
self.assertDictMatch(result['data'], self.properties)
def test_terminate_connection(self):
volume = {'name': self.volume_name}
self.driver.terminate_connection(volume, self.connector)
def test_create_snapshot(self):
snapshot = {'name': self.snapshot_name,
'volume_name': self.volume_name}
self.driver.create_snapshot(snapshot)
def test_delete_snapshot(self):
snapshot = {'name': self.snapshot_name}
self.driver.delete_snapshot(snapshot)
def test_create_volume_from_snapshot(self):
volume = {'name': self.volume_name}
snapshot = {'name': self.snapshot_name}
model_update = self.driver.create_volume_from_snapshot(volume,
snapshot)
expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev 0"
expected_location = "10.0.1.6:3260,1 %s" % expected_iqn
self.assertEqual(model_update['provider_location'], expected_location)
def test_cliq_error(self):
try:
self.driver._cliq_run_xml("testError", {})
except exception.VolumeBackendAPIException:
pass

View File

@ -28,7 +28,6 @@ SHEEPDOG_MODULE = "cinder.volume.drivers.sheepdog.SheepdogDriver"
NEXENTA_MODULE = "cinder.volume.drivers.nexenta.iscsi.NexentaISCSIDriver"
SAN_MODULE = "cinder.volume.drivers.san.san.SanISCSIDriver"
SOLARIS_MODULE = "cinder.volume.drivers.san.solaris.SolarisISCSIDriver"
LEFTHAND_MODULE = "cinder.volume.drivers.san.hp_lefthand.HpSanISCSIDriver"
NFS_MODULE = "cinder.volume.drivers.nfs.NfsDriver"
SOLIDFIRE_MODULE = "cinder.volume.drivers.solidfire.SolidFireDriver"
STORWIZE_MODULE = "cinder.volume.drivers.ibm.storwize_svc.StorwizeSVCDriver"
@ -36,6 +35,8 @@ WINDOWS_MODULE = "cinder.volume.drivers.windows.windows.WindowsDriver"
XIV_DS8K_MODULE = "cinder.volume.drivers.xiv_ds8k.XIVDS8KDriver"
ZADARA_MODULE = "cinder.volume.drivers.zadara.ZadaraVPSAISCSIDriver"
NETAPP_MODULE = "cinder.volume.drivers.netapp.common.Deprecated"
LEFTHAND_REST_MODULE = ("cinder.volume.drivers.san.hp.hp_lefthand_iscsi."
"HPLeftHandISCSIDriver")
class VolumeDriverCompatibility(test.TestCase):
@ -103,14 +104,6 @@ class VolumeDriverCompatibility(test.TestCase):
self._load_driver(SOLARIS_MODULE)
self.assertEqual(self._driver_module_name(), SOLARIS_MODULE)
def test_hp_lefthand_old(self):
self._load_driver('cinder.volume.san.HpSanISCSIDriver')
self.assertEqual(self._driver_module_name(), LEFTHAND_MODULE)
def test_hp_lefthand_new(self):
self._load_driver(LEFTHAND_MODULE)
self.assertEqual(self._driver_module_name(), LEFTHAND_MODULE)
def test_nfs_old(self):
self._load_driver('cinder.volume.nfs.NfsDriver')
self.assertEqual(self._driver_module_name(), NFS_MODULE)
@ -198,3 +191,12 @@ class VolumeDriverCompatibility(test.TestCase):
self._load_driver(
'cinder.volume.drivers.netapp.nfs.NetAppCmodeNfsDriver')
self.assertEqual(self._driver_module_name(), NETAPP_MODULE)
def test_hp_lefthand_rest_old(self):
self._load_driver(
'cinder.volume.drivers.san.hp_lefthand.HpSanISCSIDriver')
self.assertEqual(self._driver_module_name(), LEFTHAND_REST_MODULE)
def test_hp_lefthand_rest_new(self):
self._load_driver(LEFTHAND_REST_MODULE)
self.assertEqual(self._driver_module_name(), LEFTHAND_REST_MODULE)

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,5 @@
"""
# Adding imports for backwards compatibility in loading volume_driver.
from hp_lefthand import HpSanISCSIDriver # noqa
from san import SanISCSIDriver # noqa
from solaris import SolarisISCSIDriver # noqa

View File

@ -1,4 +1,5 @@
# Copyright 2012 OpenStack Foundation
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
@ -11,8 +12,9 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
HP Lefthand SAN ISCSI Driver.
HP LeftHand SAN ISCSI Driver.
The driver communicates to the backend aka Cliq via SSH to perform all the
operations on the SAN.
@ -22,14 +24,15 @@ from lxml import etree
from cinder import exception
from cinder.openstack.common import log as logging
from cinder.openstack.common import processutils
from cinder import units
from cinder.volume.drivers.san.san import SanISCSIDriver
LOG = logging.getLogger(__name__)
class HpSanISCSIDriver(SanISCSIDriver):
"""Executes commands relating to HP/Lefthand SAN ISCSI volumes.
class HPLeftHandCLIQProxy(SanISCSIDriver):
"""Executes commands relating to HP/LeftHand SAN ISCSI volumes.
We use the CLIQ interface, over SSH.
@ -53,8 +56,6 @@ class HpSanISCSIDriver(SanISCSIDriver):
:getClusterInfo: (to discover the iSCSI target IP address)
:assignVolumeChap: (exports it with CHAP security)
The 'trick' here is that the HP SAN enforces security by default, so
normally a volume mount would need both to configure the SAN in the volume
layer and do the mount on the compute layer. Multi-layer operations are
@ -67,16 +68,26 @@ class HpSanISCSIDriver(SanISCSIDriver):
1.0.0 - Initial driver
1.1.0 - Added create/delete snapshot, extend volume, create volume
from snapshot support.
1.2.0 - Ported into the new HP LeftHand driver.
"""
VERSION = "1.1.0"
VERSION = "1.2.0"
device_stats = {}
def __init__(self, *args, **kwargs):
super(HpSanISCSIDriver, self).__init__(*args, **kwargs)
super(HPLeftHandCLIQProxy, self).__init__(*args, **kwargs)
self.cluster_vip = None
def do_setup(self, context):
pass
def check_for_setup_error(self):
pass
def get_version_string(self):
return (_('CLIQ %(proxy_ver)s') % {'proxy_ver': self.VERSION})
def _cliq_run(self, verb, cliq_args, check_exit_code=True):
"""Runs a CLIQ command over SSH, without doing any result parsing."""
cmd_list = [verb]
@ -301,7 +312,7 @@ class HpSanISCSIDriver(SanISCSIDriver):
try:
volume_info = self._cliq_get_volume_info(volume['name'])
except processutils.ProcessExecutionError:
LOG.error_("Volume did not exist. It will not be deleted")
LOG.error(_("Volume did not exist. It will not be deleted"))
return
self._cliq_run_xml("deleteVolume", cliq_args)
@ -313,9 +324,16 @@ class HpSanISCSIDriver(SanISCSIDriver):
try:
volume_info = self._cliq_get_snapshot_info(snapshot['name'])
except processutils.ProcessExecutionError:
LOG.error_("Snapshot did not exist. It will not be deleted")
LOG.error(_("Snapshot did not exist. It will not be deleted"))
return
self._cliq_run_xml("deleteSnapshot", cliq_args)
try:
self._cliq_run_xml("deleteSnapshot", cliq_args)
except Exception as ex:
in_use_msg = 'cannot be deleted because it is a clone point'
if in_use_msg in ex.message:
raise exception.SnapshotIsBusy(str(ex))
raise exception.VolumeBackendAPIException(str(ex))
def local_path(self, volume):
msg = _("local_path not supported")
@ -349,10 +367,10 @@ class HpSanISCSIDriver(SanISCSIDriver):
cliq_args['serverName'] = connector['host']
self._cliq_run_xml("assignVolumeToServer", cliq_args)
iscsi_properties = self._get_iscsi_properties(volume)
iscsi_data = self._get_iscsi_properties(volume)
return {
'driver_volume_type': 'iscsi',
'data': iscsi_properties
'data': iscsi_data
}
def _create_server(self, connector):
@ -405,7 +423,6 @@ class HpSanISCSIDriver(SanISCSIDriver):
data = {}
backend_name = self.configuration.safe_get('volume_backend_name')
data['volume_backend_name'] = backend_name or self.__class__.__name__
data['driver_version'] = self.VERSION
data['reserved_percentage'] = 0
data['storage_protocol'] = 'iSCSI'
data['vendor_name'] = 'Hewlett-Packard'
@ -414,8 +431,20 @@ class HpSanISCSIDriver(SanISCSIDriver):
cluster_node = result_xml.find("response/cluster")
total_capacity = cluster_node.attrib.get("spaceTotal")
free_capacity = cluster_node.attrib.get("unprovisionedSpace")
GB = 1073741824
GB = units.GiB
data['total_capacity_gb'] = int(total_capacity) / GB
data['free_capacity_gb'] = int(free_capacity) / GB
self.device_stats = data
def create_cloned_volume(self, volume, src_vref):
raise NotImplementedError()
def create_export(self, context, volume):
pass
def ensure_export(self, context, volume):
pass
def remove_export(self, context, volume):
pass

View File

@ -0,0 +1,137 @@
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""
Volume driver for HP LeftHand Storage array.
This driver requires 11.5 or greater firmware on the LeftHand array, using
the 1.0 or greater version of the hplefthandclient.
You will need to install the python hplefthandclient.
sudo pip install hplefthandclient
Set the following in the cinder.conf file to enable the
LeftHand Channel Driver along with the required flags:
volume_driver=cinder.volume.drivers.san.hp.hp_lefthand_iscsi.
HPLeftHandISCSIDriver
It also requires the setting of hplefthand_api_url, hplefthand_username,
hplefthand_password for credentials to talk to the REST service on the
LeftHand array.
"""
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import utils
from cinder.volume.driver import VolumeDriver
from cinder.volume.drivers.san.hp import hp_lefthand_cliq_proxy as cliq_proxy
from cinder.volume.drivers.san.hp import hp_lefthand_rest_proxy as rest_proxy
LOG = logging.getLogger(__name__)
class HPLeftHandISCSIDriver(VolumeDriver):
"""Executes commands relating to HP/LeftHand SAN ISCSI volumes.
Version history:
1.0.0 - Initial driver
"""
VERSION = "1.0.0"
def __init__(self, *args, **kwargs):
super(HPLeftHandISCSIDriver, self).__init__(*args, **kwargs)
self.proxy = self._create_proxy(*args, **kwargs)
def _create_proxy(self, *args, **kwargs):
try:
proxy = rest_proxy.HPLeftHandRESTProxy(*args, **kwargs)
except exception.NotFound:
proxy = cliq_proxy.HPLeftHandCLIQProxy(*args, **kwargs)
return proxy
@utils.synchronized('lefthand', external=True)
def check_for_setup_error(self):
self.proxy.check_for_setup_error()
@utils.synchronized('lefthand', external=True)
def do_setup(self, context):
self.proxy.do_setup(context)
LOG.info(_("HPLeftHand driver %(driver_ver)s, proxy %(proxy_ver)s") % {
"driver_ver": self.VERSION,
"proxy_ver": self.proxy.get_version_string()})
@utils.synchronized('lefthand', external=True)
def create_volume(self, volume):
"""Creates a volume."""
return self.proxy.create_volume(volume)
@utils.synchronized('lefthand', external=True)
def extend_volume(self, volume, new_size):
"""Extend the size of an existing volume."""
self.proxy.extend_volume(volume, new_size)
@utils.synchronized('lefthand', external=True)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
return self.proxy.create_volume_from_snapshot(volume, snapshot)
@utils.synchronized('lefthand', external=True)
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
self.proxy.create_snapshot(snapshot)
@utils.synchronized('lefthand', external=True)
def delete_volume(self, volume):
"""Deletes a volume."""
self.proxy.delete_volume(volume)
@utils.synchronized('lefthand', external=True)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
self.proxy.delete_snapshot(snapshot)
@utils.synchronized('lefthand', external=True)
def initialize_connection(self, volume, connector):
"""Assigns the volume to a server."""
return self.proxy.initialize_connection(volume, connector)
@utils.synchronized('lefthand', external=True)
def terminate_connection(self, volume, connector, **kwargs):
"""Unassign the volume from the host."""
self.proxy.terminate_connection(volume, connector)
@utils.synchronized('lefthand', external=True)
def get_volume_stats(self, refresh):
data = self.proxy.get_volume_stats(refresh)
data['driver_version'] = self.VERSION
return data
@utils.synchronized('lefthand', external=True)
def create_cloned_volume(self, volume, src_vref):
return self.proxy.create_cloned_volume(volume, src_vref)
@utils.synchronized('lefthand', external=True)
def create_export(self, context, volume):
return self.proxy.create_export(context, volume)
@utils.synchronized('lefthand', external=True)
def ensure_export(self, context, volume):
return self.proxy.ensure_export(context, volume)
@utils.synchronized('lefthand', external=True)
def remove_export(self, context, volume):
return self.proxy.remove_export(context, volume)

View File

@ -0,0 +1,363 @@
# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""HP LeftHand SAN ISCSI REST Proxy."""
from cinder import context
from cinder import exception
from cinder.openstack.common import log as logging
from cinder import units
from cinder import utils
from cinder.volume.driver import ISCSIDriver
from cinder.volume import volume_types
from oslo.config import cfg
LOG = logging.getLogger(__name__)
try:
import hplefthandclient
from hplefthandclient import client
from hplefthandclient import exceptions as hpexceptions
except ImportError:
LOG.error(_('Module hplefthandclient not installed.'))
hplefthand_opts = [
cfg.StrOpt('hplefthand_api_url',
default=None,
help="HP LeftHand WSAPI Server Url like "
"https://<LeftHand ip>:8081/lhos"),
cfg.StrOpt('hplefthand_username',
default=None,
help="HP LeftHand Super user username"),
cfg.StrOpt('hplefthand_password',
default=None,
help="HP LeftHand Super user password",
secret=True),
cfg.StrOpt('hplefthand_clustername',
default=None,
help="HP LeftHand cluster name"),
cfg.BoolOpt('hplefthand_iscsi_chap_enabled',
default=False,
help='Configure CHAP authentication for iSCSI connections '
'(Default: Disabled)'),
cfg.BoolOpt('hplefthand_debug',
default=False,
help="Enable HTTP debugging to LeftHand"),
]
CONF = cfg.CONF
CONF.register_opts(hplefthand_opts)
# map the extra spec key to the REST client option key
extra_specs_key_map = {
'hplh:provisioning': 'isThinProvisioned',
'hplh:ao': 'isAdaptiveOptimizationEnabled',
'hplh:data_pl': 'dataProtectionLevel',
}
# map the extra spec value to the REST client option value
extra_specs_value_map = {
'isThinProvisioned': {'thin': True, 'full': False},
'isAdaptiveOptimizationEnabled': {'true': True, 'false': False},
'dataProtectionLevel': {
'r-0': 0, 'r-5': 1, 'r-10-2': 2, 'r-10-3': 3, 'r-10-4': 4, 'r-6': 5}
}
class HPLeftHandRESTProxy(ISCSIDriver):
"""Executes REST commands relating to HP/LeftHand SAN ISCSI volumes.
Version history:
1.0.0 - Initial REST iSCSI proxy
"""
VERSION = "1.0.0"
device_stats = {}
def __init__(self, *args, **kwargs):
super(HPLeftHandRESTProxy, self).__init__(*args, **kwargs)
self.configuration.append_config_values(hplefthand_opts)
if not self.configuration.hplefthand_api_url:
raise exception.NotFound(_("HPLeftHand url not found"))
def do_setup(self, context):
"""Set up LeftHand client."""
try:
self.client = client.HPLeftHandClient(
self.configuration.hplefthand_api_url)
self.client.login(
self.configuration.hplefthand_username,
self.configuration.hplefthand_password)
if self.configuration.hplefthand_debug:
self.client.debug_rest(True)
cluster_info = self.client.getClusterByName(
self.configuration.hplefthand_clustername)
self.cluster_id = cluster_info['id']
virtual_ips = cluster_info['virtualIPAddresses']
self.cluster_vip = virtual_ips[0]['ipV4Address']
self._update_backend_status()
except hpexceptions.HTTPNotFound:
raise exception.DriverNotInitialized(
_('LeftHand cluster not found'))
except Exception as ex:
raise exception.DriverNotInitialized(str(ex))
def check_for_setup_error(self):
pass
def get_version_string(self):
return (_('REST %(proxy_ver)s hplefthandclient %(rest_ver)s') % {
'proxy_ver': self.VERSION,
'rest_ver': hplefthandclient.get_version_string()})
def create_volume(self, volume):
"""Creates a volume."""
try:
# get the extra specs of interest from this volume's volume type
extra_specs = self._get_extra_specs(
volume,
extra_specs_key_map.keys())
# map the extra specs key/value pairs to key/value pairs
# used as optional configuration values by the LeftHand backend
optional = self._map_extra_specs(extra_specs)
# if provisioning is not set, default to thin
if 'isThinProvisioned' not in optional:
optional['isThinProvisioned'] = True
clusterName = self.configuration.hplefthand_clustername
optional['clusterName'] = clusterName
volume_info = self.client.createVolume(
volume['name'], self.cluster_id,
volume['size'] * units.GiB,
optional)
return self._update_provider(volume_info)
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def delete_volume(self, volume):
"""Deletes a volume."""
try:
volume_info = self.client.getVolumeByName(volume['name'])
self.client.deleteVolume(volume_info['id'])
except hpexceptions.HTTPNotFound:
LOG.error(_("Volume did not exist. It will not be deleted"))
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def extend_volume(self, volume, new_size):
"""Extend the size of an existing volume."""
try:
volume_info = self.client.getVolumeByName(volume['name'])
# convert GB to bytes
options = {'size': int(new_size) * units.GiB}
self.client.modifyVolume(volume_info['id'], options)
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
try:
volume_info = self.client.getVolumeByName(snapshot['volume_name'])
option = {'inheritAccess': True}
self.client.createSnapshot(snapshot['name'],
volume_info['id'],
option)
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
try:
snap_info = self.client.getSnapshotByName(snapshot['name'])
self.client.deleteSnapshot(snap_info['id'])
except hpexceptions.HTTPNotFound:
LOG.error(_("Snapshot did not exist. It will not be deleted"))
except hpexceptions.HTTPServerError as ex:
in_use_msg = 'cannot be deleted because it is a clone point'
if in_use_msg in ex.get_description():
raise exception.SnapshotIsBusy(str(ex))
raise exception.VolumeBackendAPIException(str(ex))
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def get_volume_stats(self, refresh):
"""Gets volume stats."""
if refresh:
self._update_backend_status()
return self.device_stats
def _update_backend_status(self):
data = {}
backend_name = self.configuration.safe_get('volume_backend_name')
data['volume_backend_name'] = backend_name or self.__class__.__name__
data['reserved_percentage'] = 0
data['storage_protocol'] = 'iSCSI'
data['vendor_name'] = 'Hewlett-Packard'
cluster_info = self.client.getCluster(self.cluster_id)
total_capacity = cluster_info['spaceTotal']
free_capacity = cluster_info['spaceAvailable']
# convert to GB
data['total_capacity_gb'] = int(total_capacity) / units.GiB
data['free_capacity_gb'] = int(free_capacity) / units.GiB
self.device_stats = data
def initialize_connection(self, volume, connector):
"""Assigns the volume to a server.
Assign any created volume to a compute node/host so that it can be
used from that host. HP VSA requires a volume to be assigned
to a server.
"""
try:
server_info = self._create_server(connector)
volume_info = self.client.getVolumeByName(volume['name'])
self.client.addServerAccess(volume_info['id'], server_info['id'])
iscsi_properties = self._get_iscsi_properties(volume)
if ('chapAuthenticationRequired' in server_info
and server_info['chapAuthenticationRequired']):
iscsi_properties['auth_method'] = 'CHAP'
iscsi_properties['auth_username'] = connector['initiator']
iscsi_properties['auth_password'] = (
server_info['chapTargetSecret'])
return {'driver_volume_type': 'iscsi', 'data': iscsi_properties}
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def terminate_connection(self, volume, connector, **kwargs):
"""Unassign the volume from the host."""
try:
volume_info = self.client.getVolumeByName(volume['name'])
server_info = self.client.getServerByName(connector['host'])
self.client.removeServerAccess(
volume_info['id'],
server_info['id'])
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
try:
snap_info = self.client.getSnapshotByName(snapshot['name'])
volume_info = self.client.cloneSnapshot(
volume['name'],
snap_info['id'])
return self._update_provider(volume_info)
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def create_cloned_volume(self, volume, src_vref):
try:
volume_info = self.client.getVolumeByName(src_vref['name'])
self.client.cloneVolume(volume['name'], volume_info['id'])
except Exception as ex:
raise exception.VolumeBackendAPIException(str(ex))
def _get_extra_specs(self, volume, valid_keys):
"""Get extra specs of interest (valid_keys) from volume type."""
extra_specs = {}
type_id = volume.get('volume_type_id', None)
if type_id is not None:
ctxt = context.get_admin_context()
volume_type = volume_types.get_volume_type(ctxt, type_id)
specs = volume_type.get('extra_specs')
for key, value in specs.iteritems():
if key in valid_keys:
extra_specs[key] = value
return extra_specs
def _map_extra_specs(self, extra_specs):
"""Map the extra spec key/values to LeftHand key/values."""
client_options = {}
for key, value in extra_specs.iteritems():
# map extra spec key to lh client option key
client_key = extra_specs_key_map[key]
# map extra spect value to lh client option value
try:
value_map = extra_specs_value_map[client_key]
# an invalid value will throw KeyError
client_value = value_map[value]
client_options[client_key] = client_value
except KeyError:
LOG.error(_("'%(value)s' is an invalid value "
"for extra spec '%(key)s'") %
{'value': value, 'key': key})
return client_options
def _update_provider(self, volume_info):
# TODO(justinsb): Is this always 1? Does it matter?
cluster_interface = '1'
iscsi_portal = self.cluster_vip + ":3260," + cluster_interface
return {'provider_location': (
"%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))}
def _create_server(self, connector):
server_info = None
chap_enabled = self.configuration.hplefthand_iscsi_chap_enabled
try:
server_info = self.client.getServerByName(connector['host'])
chap_secret = server_info['chapTargetSecret']
if not chap_enabled and chap_secret:
LOG.warning(_('CHAP secret exists for host %s but CHAP is '
'disabled') % connector['host'])
if chap_enabled and chap_secret is None:
LOG.warning(_('CHAP is enabled, but server secret not '
'configured on server %s') % connector['host'])
return server_info
except hpexceptions.HTTPNotFound:
# server does not exist, so create one
pass
optional = None
if chap_enabled:
chap_secret = utils.generate_password()
optional = {'chapName': connector['initiator'],
'chapTargetSecret': chap_secret,
'chapAuthenticationRequired': True
}
server_info = self.client.createServer(connector['host'],
connector['initiator'],
optional)
return server_info
def create_export(self, context, volume):
pass
def ensure_export(self, context, volume):
pass
def remove_export(self, context, volume):
pass

View File

@ -94,8 +94,6 @@ MAPPING = {
'cinder.volume.drivers.san.san.SanISCSIDriver',
'cinder.volume.san.SolarisISCSIDriver':
'cinder.volume.drivers.san.solaris.SolarisISCSIDriver',
'cinder.volume.san.HpSanISCSIDriver':
'cinder.volume.drivers.san.hp_lefthand.HpSanISCSIDriver',
'cinder.volume.nfs.NfsDriver':
'cinder.volume.drivers.nfs.NfsDriver',
'cinder.volume.solidfire.SolidFire':
@ -133,7 +131,9 @@ MAPPING = {
'cinder.volume.drivers.netapp.nfs.NetAppCmodeNfsDriver':
'cinder.volume.drivers.netapp.common.Deprecated',
'cinder.volume.drivers.huawei.HuaweiISCSIDriver':
'cinder.volume.drivers.huawei.HuaweiVolumeDriver'}
'cinder.volume.drivers.huawei.HuaweiVolumeDriver',
'cinder.volume.drivers.san.hp_lefthand.HpSanISCSIDriver':
'cinder.volume.drivers.san.hp.hp_lefthand_iscsi.HPLeftHandISCSIDriver'}
def locked_volume_operation(f):

View File

@ -1491,6 +1491,31 @@
#hp3par_iscsi_ips=
#
# Options defined in cinder.volume.drivers.san.hp.hp_lefthand_rest_proxy
#
# HP LeftHand WSAPI Server Url like https://<LeftHand
# ip>:8081/lhos (string value)
#hplefthand_api_url=<None>
# HP LeftHand Super user username (string value)
#hplefthand_username=<None>
# HP LeftHand Super user password (string value)
#hplefthand_password=<None>
# HP LeftHand cluster name (string value)
#hplefthand_clustername=<None>
# Configure CHAP authentication for iSCSI connections
# (Default: Disabled) (boolean value)
#hplefthand_iscsi_chap_enabled=false
# Enable HTTP debugging to LeftHand (boolean value)
#hplefthand_debug=false
#
# Options defined in cinder.volume.drivers.san.san
#

View File

@ -4,6 +4,7 @@ coverage>=3.6
discover
fixtures>=0.3.14
hp3parclient>=2.0,<3.0
hplefthandclient>=1.0.0,<2.0.0
mock>=1.0
mox>=0.5.3
MySQL-python