+++ /dev/null
-# 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
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"
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):
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)
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)
--- /dev/null
+# (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)
"""
# 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
-# 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
# 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.
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.
: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
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]
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)
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")
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):
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'
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
--- /dev/null
+# (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)
--- /dev/null
+# (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
'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':
'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):
#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
#
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