]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
New HP LeftHand array iSCSI driver
authorJim Branen <james.branen@hp.com>
Tue, 7 Jan 2014 00:14:35 +0000 (16:14 -0800)
committerJim Branen <james.branen@hp.com>
Mon, 10 Feb 2014 20:59:34 +0000 (12:59 -0800)
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

cinder/tests/test_HpSanISCSIDriver.py [deleted file]
cinder/tests/test_drivers_compatibility.py
cinder/tests/test_hplefthand.py [new file with mode: 0644]
cinder/volume/drivers/san/__init__.py
cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py [moved from cinder/volume/drivers/san/hp_lefthand.py with 91% similarity]
cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py [new file with mode: 0644]
cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py [new file with mode: 0644]
cinder/volume/manager.py
etc/cinder/cinder.conf.sample
test-requirements.txt

diff --git a/cinder/tests/test_HpSanISCSIDriver.py b/cinder/tests/test_HpSanISCSIDriver.py
deleted file mode 100644 (file)
index 6541d26..0000000
+++ /dev/null
@@ -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
index 3449d9142a198560e861a72f6d25f0eeff0a1898..63e0cdc6cfa4ae090bfa4ef3f85ad6c0bb023a72 100644 (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)
diff --git a/cinder/tests/test_hplefthand.py b/cinder/tests/test_hplefthand.py
new file mode 100644 (file)
index 0000000..7d3b395
--- /dev/null
@@ -0,0 +1,1014 @@
+#    (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.
+#
+"""Unit tests for OpenStack Cinder volume drivers."""
+import mock
+
+from hplefthandclient import exceptions as hpexceptions
+
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder import test
+from cinder import units
+from cinder.volume.drivers.san.hp import hp_lefthand_iscsi
+from cinder.volume.drivers.san.hp import hp_lefthand_rest_proxy
+from cinder.volume import volume_types
+
+LOG = logging.getLogger(__name__)
+
+
+class HPLeftHandBaseDriver():
+
+    cluster_id = 1
+
+    volume_name = "fakevolume"
+    volume_id = 1
+    volume = {
+        'name': volume_name,
+        'provider_location': ('10.0.1.6 iqn.2003-10.com.lefthandnetworks:'
+                              'group01:25366:fakev 0'),
+        'id': volume_id,
+        'provider_auth': None,
+        'size': 1}
+
+    serverName = 'fakehost'
+    server_id = 0
+
+    snapshot_name = "fakeshapshot"
+    snapshot_id = 3
+    snapshot = {
+        'name': snapshot_name,
+        'volume_name': volume_name}
+
+    cloned_volume_name = "clone_volume"
+    cloned_volume = {'name': cloned_volume_name}
+
+    cloned_snapshot_name = "clonedshapshot"
+    cloned_snapshot_id = 5
+    cloned_snapshot = {
+        'name': cloned_snapshot_name,
+        'volume_name': volume_name}
+
+    volume_type_id = 4
+    init_iqn = 'iqn.1993-08.org.debian:01:222'
+
+    connector = {
+        'ip': '10.0.0.2',
+        'initiator': 'iqn.1993-08.org.debian:01:222',
+        'host': serverName}
+
+    driver_startup_call_stack = [
+        mock.call.login('foo1', 'bar2'),
+        mock.call.getClusterByName('CloudCluster1'),
+        mock.call.getCluster(1)]
+
+
+class TestHPLeftHandCLIQISCSIDriver(HPLeftHandBaseDriver, test.TestCase):
+
+    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 setUp(self):
+        super(TestHPLeftHandCLIQISCSIDriver, self).setUp()
+
+        self.properties = {
+            'target_discoverd': True,
+            'target_portal': '10.0.1.6:3260',
+            'target_iqn':
+            'iqn.2003-10.com.lefthandnetworks:group01:25366:fakev',
+            'volume_id': self.volume_id}
+
+    def tearDown(self):
+        super(TestHPLeftHandCLIQISCSIDriver, self).tearDown()
+
+    def default_mock_conf(self):
+
+        mock_conf = mock.Mock()
+        mock_conf.san_ip = '10.10.10.10'
+        mock_conf.san_login = 'foo'
+        mock_conf.san_password = 'bar'
+        mock_conf.san_ssh_port = 16022
+        mock_conf.san_clustername = 'CloudCluster1'
+        mock_conf.hplefthand_api_url = None
+        return mock_conf
+
+    def setup_driver(self, config=None):
+
+        if config is None:
+            config = self.default_mock_conf()
+
+        self.driver = hp_lefthand_iscsi.HPLeftHandISCSIDriver(
+            configuration=config)
+        self.driver.do_setup(None)
+
+        self.driver.proxy._cliq_run = mock.Mock(
+            side_effect=self._fake_cliq_run)
+        return self.driver.proxy._cliq_run
+
+    def test_create_volume(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        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)
+
+        expected = [
+            mock.call(
+                'createVolume', {
+                    'clusterName': 'CloudCluster1',
+                    'volumeName': 'fakevolume',
+                    'thinProvision': '1',
+                    'output': 'XML',
+                    'size': '1GB'},
+                True),
+            mock.call(
+                'getVolumeInfo', {
+                    'volumeName': 'fakevolume',
+                    'output': 'XML'},
+                True),
+            mock.call(
+                'getClusterInfo', {
+                    'clusterName': 'Cluster01',
+                    'searchDepth': '1',
+                    'verbose': '0',
+                    'output': 'XML'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+    def test_delete_volume(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        volume = {'name': self.volume_name}
+        self.driver.delete_volume(volume)
+
+        expected = [
+            mock.call(
+                'getVolumeInfo', {
+                    'volumeName': 'fakevolume',
+                    'output': 'XML'},
+                True),
+            mock.call(
+                'deleteVolume', {
+                    'volumeName': 'fakevolume',
+                    'prompt': 'false',
+                    'output': 'XML'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+    def test_extend_volume(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        volume = {'name': self.volume_name}
+        self.driver.extend_volume(volume, 2)
+
+        expected = [
+            mock.call(
+                'modifyVolume', {
+                    'volumeName': 'fakevolume',
+                    'output': 'XML',
+                    'size': '2GB'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+    def test_initialize_connection(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        self.driver.proxy._get_iscsi_properties = mock.Mock(
+            return_value=self.properties)
+        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)
+
+        expected = [
+            mock.call(
+                'getServerInfo', {
+                    'output': 'XML',
+                    'serverName': 'fakehost'},
+                False),
+            mock.call(
+                'assignVolumeToServer', {
+                    'volumeName': 'fakevolume',
+                    'serverName': 'fakehost',
+                    'output': 'XML'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+    def test_terminate_connection(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        volume = {'name': self.volume_name}
+        self.driver.terminate_connection(volume, self.connector)
+
+        expected = [
+            mock.call(
+                'unassignVolumeToServer', {
+                    'volumeName': 'fakevolume',
+                    'serverName': 'fakehost',
+                    'output': 'XML'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+    def test_create_snapshot(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        snapshot = {'name': self.snapshot_name,
+                    'volume_name': self.volume_name}
+        self.driver.create_snapshot(snapshot)
+
+        expected = [
+            mock.call(
+                'createSnapshot', {
+                    'snapshotName': 'fakeshapshot',
+                    'output': 'XML',
+                    'inheritAccess': 1,
+                    'volumeName': 'fakevolume'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+    def test_delete_snapshot(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        snapshot = {'name': self.snapshot_name}
+        self.driver.delete_snapshot(snapshot)
+
+        expected = [
+            mock.call(
+                'getSnapshotInfo', {
+                    'snapshotName': 'fakeshapshot',
+                    'output': 'XML'},
+                True),
+            mock.call(
+                'deleteSnapshot', {
+                    'snapshotName': 'fakeshapshot',
+                    'prompt': 'false',
+                    'output': 'XML'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+    def test_create_volume_from_snapshot(self):
+
+        # set up driver with default config
+        mock_cliq_run = self.setup_driver()
+
+        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)
+
+        expected = [
+            mock.call(
+                'cloneSnapshot', {
+                    'snapshotName': 'fakeshapshot',
+                    'output': 'XML',
+                    'volumeName': 'fakevolume'},
+                True),
+            mock.call(
+                'getVolumeInfo', {
+                    'volumeName': 'fakevolume',
+                    'output': 'XML'},
+                True),
+            mock.call(
+                'getClusterInfo', {
+                    'clusterName': 'Cluster01',
+                    'searchDepth': '1',
+                    'verbose': '0',
+                    'output': 'XML'},
+                True)]
+
+        # validate call chain
+        mock_cliq_run.assert_has_calls(expected)
+
+
+class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase):
+
+    driver_startup_call_stack = [
+        mock.call.login('foo1', 'bar2'),
+        mock.call.getClusterByName('CloudCluster1'),
+        mock.call.getCluster(1)]
+
+    def setUp(self):
+        super(TestHPLeftHandRESTISCSIDriver, self).setUp()
+
+    def tearDown(self):
+        super(TestHPLeftHandRESTISCSIDriver, self).tearDown()
+
+    def default_mock_conf(self):
+
+        mock_conf = mock.Mock()
+        mock_conf.hplefthand_api_url = 'http://fake.foo:8080/lhos'
+        mock_conf.hplefthand_username = 'foo1'
+        mock_conf.hplefthand_password = 'bar2'
+        mock_conf.hplefthand_iscsi_chap_enabled = False
+        mock_conf.hplefthand_debug = False
+        mock_conf.hplefthand_clustername = "CloudCluster1"
+        return mock_conf
+
+    @mock.patch('hplefthandclient.client.HPLeftHandClient', spec=True)
+    def setup_driver(self, _mock_client, config=None):
+
+        if config is None:
+            config = self.default_mock_conf()
+
+        _mock_client.return_value.getClusterByName.return_value = {
+            'id': 1, 'virtualIPAddresses': [{'ipV4Address': '10.0.1.6'}]}
+        _mock_client.return_value.getCluster.return_value = {
+            'spaceTotal': units.GiB * 500,
+            'spaceAvailable': units.GiB * 250}
+        self.driver = hp_lefthand_iscsi.HPLeftHandISCSIDriver(
+            configuration=config)
+        self.driver.do_setup(None)
+        return _mock_client.return_value
+
+    def test_create_volume(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        # mock return value of createVolume
+        mock_client.createVolume.return_value = {
+            'iscsiIqn': self.connector['initiator']}
+
+        # execute driver
+        volume_info = self.driver.create_volume(self.volume)
+
+        self.assertEqual('10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0',
+                         volume_info['provider_location'])
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.createVolume(
+                'fakevolume',
+                1,
+                units.GiB,
+                {'isThinProvisioned': True, 'clusterName': 'CloudCluster1'})]
+
+        mock_client.assert_has_calls(expected)
+
+        # mock HTTPServerError
+        mock_client.createVolume.side_effect = hpexceptions.HTTPServerError()
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.create_volume, self.volume)
+
+    @mock.patch.object(
+        volume_types,
+        'get_volume_type',
+        return_value={'extra_specs': {'hplh:provisioning': 'full'}})
+    def test_create_volume_with_es(self, _mock_volume_type):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        volume_with_vt = self.volume
+        volume_with_vt['volume_type_id'] = 1
+
+        # mock return value of createVolume
+        mock_client.createVolume.return_value = {
+            'iscsiIqn': self.connector['initiator']}
+
+        # execute creat_volume
+        volume_info = self.driver.create_volume(volume_with_vt)
+
+        self.assertEqual('10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0',
+                         volume_info['provider_location'])
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.createVolume(
+                'fakevolume',
+                1,
+                units.GiB,
+                {'isThinProvisioned': False, 'clusterName': 'CloudCluster1'})]
+
+        mock_client.assert_has_calls(expected)
+
+    def test_delete_volume(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        # mock return value of getVolumeByName
+        mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+        # execute delete_volume
+        self.driver.delete_volume(self.volume)
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getVolumeByName('fakevolume'),
+            mock.call.deleteVolume(self.volume_id)]
+
+        mock_client.assert_has_calls(expected)
+
+        # mock HTTPNotFound (volume not found)
+        mock_client.getVolumeByName.side_effect = hpexceptions.HTTPNotFound()
+        # no exception should escape method
+        self.driver.delete_volume(self.volume)
+
+        # mock HTTPConflict
+        mock_client.deleteVolume.side_effect = hpexceptions.HTTPConflict()
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.delete_volume, self.volume_id)
+
+    def test_extend_volume(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        # mock return value of getVolumeByName
+        mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+        # execute extend_volume
+        self.driver.extend_volume(self.volume, 2)
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getVolumeByName('fakevolume'),
+            mock.call.modifyVolume(1, {'size': 2 * units.GiB})]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+        # mock HTTPServerError (array failure)
+        mock_client.modifyVolume.side_effect = hpexceptions.HTTPServerError()
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(exception.VolumeBackendAPIException,
+                          self.driver.extend_volume, self.volume, 2)
+
+    def test_initialize_connection(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        # mock return value of getVolumeByName
+        mock_client.getServerByName.side_effect = hpexceptions.HTTPNotFound()
+        mock_client.createServer.return_value = {'id': self.server_id}
+        mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+        # execute initialize_connection
+        result = self.driver.initialize_connection(
+            self.volume,
+            self.connector)
+
+        # validate
+        self.assertEqual(result['driver_volume_type'], 'iscsi')
+        self.assertEqual(result['data']['target_discovered'], False)
+        self.assertEqual(result['data']['volume_id'], self.volume_id)
+        self.assertTrue('auth_method' not in result['data'])
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getServerByName('fakehost'),
+            mock.call.createServer
+            (
+                'fakehost',
+                'iqn.1993-08.org.debian:01:222',
+                None
+            ),
+            mock.call.getVolumeByName('fakevolume'),
+            mock.call.addServerAccess(1, 0)]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+        # mock HTTPServerError (array failure)
+        mock_client.createServer.side_effect = hpexceptions.HTTPServerError()
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(
+            exception.VolumeBackendAPIException,
+            self.driver.initialize_connection, self.volume, self.connector)
+
+    def test_initialize_connection_with_chaps(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        # mock return value of getVolumeByName
+        mock_client.getServerByName.side_effect = hpexceptions.HTTPNotFound()
+        mock_client.createServer.return_value = {
+            'id': self.server_id,
+            'chapAuthenticationRequired': True,
+            'chapTargetSecret': 'dont_tell'}
+        mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+        # execute initialize_connection
+        result = self.driver.initialize_connection(
+            self.volume,
+            self.connector)
+
+        # validate
+        self.assertEqual(result['driver_volume_type'], 'iscsi')
+        self.assertEqual(result['data']['target_discovered'], False)
+        self.assertEqual(result['data']['volume_id'], self.volume_id)
+        self.assertEqual(result['data']['auth_method'], 'CHAP')
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getServerByName('fakehost'),
+            mock.call.createServer
+            (
+                'fakehost',
+                'iqn.1993-08.org.debian:01:222',
+                None
+            ),
+            mock.call.getVolumeByName('fakevolume'),
+            mock.call.addServerAccess(1, 0)]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+    def test_terminate_connection(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+        mock_client.getServerByName.return_value = {'id': self.server_id}
+
+        # execute terminate_connection
+        self.driver.terminate_connection(self.volume, self.connector)
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getVolumeByName('fakevolume'),
+            mock.call.getServerByName('fakehost'),
+            mock.call.removeServerAccess(1, 0)]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+        mock_client.getVolumeByName.side_effect = hpexceptions.HTTPNotFound()
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(
+            exception.VolumeBackendAPIException,
+            self.driver.terminate_connection,
+            self.volume,
+            self.connector)
+
+    def test_create_snapshot(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+        # execute create_snapshot
+        self.driver.create_snapshot(self.snapshot)
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getVolumeByName('fakevolume'),
+            mock.call.createSnapshot(
+                'fakeshapshot',
+                1,
+                {'inheritAccess': True})]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+        # mock HTTPServerError (array failure)
+        mock_client.getVolumeByName.side_effect = hpexceptions.HTTPNotFound()
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(
+            exception.VolumeBackendAPIException,
+            self.driver.create_snapshot, self.snapshot)
+
+    def test_delete_snapshot(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id}
+
+        # execute delete_snapshot
+        self.driver.delete_snapshot(self.snapshot)
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getSnapshotByName('fakeshapshot'),
+            mock.call.deleteSnapshot(3)]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+        mock_client.getSnapshotByName.side_effect = hpexceptions.HTTPNotFound()
+        # no exception is thrown, just error msg is logged
+        self.driver.delete_snapshot(self.snapshot)
+
+        # mock HTTPServerError (array failure)
+        ex = hpexceptions.HTTPServerError({'message': 'Some message.'})
+        mock_client.getSnapshotByName.side_effect = ex
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(
+            exception.VolumeBackendAPIException,
+            self.driver.delete_snapshot,
+            self.snapshot)
+
+        # mock HTTPServerError because the snap is in use
+        ex = hpexceptions.HTTPServerError({
+            'message':
+            'Hey, dude cannot be deleted because it is a clone point duh.'})
+        mock_client.getSnapshotByName.side_effect = ex
+        # ensure the raised exception is a cinder exception
+        self.assertRaises(
+            exception.SnapshotIsBusy,
+            self.driver.delete_snapshot,
+            self.snapshot)
+
+    def test_create_volume_from_snapshot(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id}
+        mock_client.cloneSnapshot.return_value = {
+            'iscsiIqn': self.connector['initiator']}
+
+        # execute create_volume_from_snapshot
+        model_update = self.driver.create_volume_from_snapshot(
+            self.volume, self.snapshot)
+
+        expected_iqn = 'iqn.1993-08.org.debian:01:222 0'
+        expected_location = "10.0.1.6:3260,1 %s" % expected_iqn
+        self.assertEqual(model_update['provider_location'], expected_location)
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getSnapshotByName('fakeshapshot'),
+            mock.call.cloneSnapshot('fakevolume', 3)]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+    def test_create_cloned_volume(self):
+
+        # setup drive with default configuration
+        # and return the mock HTTP LeftHand client
+        mock_client = self.setup_driver()
+
+        mock_client.getVolumeByName.return_value = {'id': self.volume_id}
+
+        # execute create_cloned_volume
+        self.driver.create_cloned_volume(
+            self.cloned_volume, self.volume)
+
+        expected = self.driver_startup_call_stack + [
+            mock.call.getVolumeByName('fakevolume'),
+            mock.call.cloneVolume('clone_volume', 1)]
+
+        # validate call chain
+        mock_client.assert_has_calls(expected)
+
+    @mock.patch.object(volume_types, 'get_volume_type')
+    def test_extra_spec_mapping(self, _mock_get_volume_type):
+
+        # setup drive with default configuration
+        self.setup_driver()
+
+        # 2 extra specs we don't care about, and
+        # 1 that will get mapped
+        _mock_get_volume_type.return_value = {
+            'extra_specs': {
+                'foo:bar': 'fake',
+                'bar:foo': 1234,
+                'hplh:provisioning': 'full'}}
+
+        volume_with_vt = self.volume
+        volume_with_vt['volume_type_id'] = self.volume_type_id
+
+        # get the extra specs of interest from this volume's volume type
+        extra_specs = self.driver.proxy._get_extra_specs(
+            volume_with_vt,
+            hp_lefthand_rest_proxy.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.driver.proxy._map_extra_specs(extra_specs)
+
+        self.assertDictMatch({'isThinProvisioned': False}, optional)
+
+    @mock.patch.object(volume_types, 'get_volume_type')
+    def test_extra_spec_mapping_invalid_value(self, _mock_get_volume_type):
+
+        # setup drive with default configuration
+        self.setup_driver()
+
+        volume_with_vt = self.volume
+        volume_with_vt['volume_type_id'] = self.volume_type_id
+
+        _mock_get_volume_type.return_value = {
+            'extra_specs': {
+                # r-07 is an invalid value for hplh:ao
+                'hplh:data_pl': 'r-07',
+                'hplh:ao': 'true'}}
+
+        # get the extra specs of interest from this volume's volume type
+        extra_specs = self.driver.proxy._get_extra_specs(
+            volume_with_vt,
+            hp_lefthand_rest_proxy.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.driver.proxy._map_extra_specs(extra_specs)
+
+        # {'hplh:ao': 'true'} should map to
+        # {'isAdaptiveOptimizationEnabled': True}
+        # without hplh:data_pl since r-07 is an invalid value
+        self.assertDictMatch({'isAdaptiveOptimizationEnabled': True}, optional)
index bfc9cda666f42157fd3ed14f7819bf876a55b7c5..9f68eb143dc5c1c8393b58fd0b47f69bfe68c444 100644 (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
similarity index 91%
rename from cinder/volume/drivers/san/hp_lefthand.py
rename to cinder/volume/drivers/san/hp/hp_lefthand_cliq_proxy.py
index 8a96d25d4808a657c3b233809a7d6cfb8b1c336e..15f7dc0578186a495610d851b5a6494ff27c94e5 100644 (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
diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py b/cinder/volume/drivers/san/hp/hp_lefthand_iscsi.py
new file mode 100644 (file)
index 0000000..65ccf11
--- /dev/null
@@ -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)
diff --git a/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py b/cinder/volume/drivers/san/hp/hp_lefthand_rest_proxy.py
new file mode 100644 (file)
index 0000000..0bdaab1
--- /dev/null
@@ -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
index 100cc422c920a3c1fabf312a608c19a02c827984..70cb5ff5aee68636814d9e26e54914cea086dfe7 100644 (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):
index d745404488cd831ca7ce2dfc9b67c51cd2cc74b2..a21fba003e5c0e5b28c131e3383c15e0d91fe949 100644 (file)
 #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
 #
index 83eb0ac2a2be1af8e568f09c65fb51532ff3791f..85ea09c49b341e1c9f989d4af27931dfb20f1406 100644 (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