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'}
"""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"
return output, None
def delete_volume(cliq_args):
- """
+ """Delete volume CLIQ input for test.
+
input = "deleteVolume volumeName=fakevolume prompt=false
output=XML"
"""
self.assertEqual(cliq_args['prompt'], 'false')
return output, None
- def assign_volume(cliq_args):
+ 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"
return output, None
def unassign_volume(cliq_args):
- """
+ """Unassign volume CLIQ input for test.
+
input = "unassignVolumeToServer volumeName=fakevolume
serverName=fakehost output=XML
"""
self.assertEqual(cliq_args['serverName'], self.connector['host'])
return output, None
- def get_cluster_info(cliq_args):
+ 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"
"""
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">
</volume></response></gauche>"""
return output, None
- def get_server_info(cliq_args):
+ 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"/>
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"/>
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}
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.driver.terminate_connection(volume, self.connector)
def test_create_snapshot(self):
- try:
- self.driver.create_snapshot("")
- except NotImplementedError:
- pass
+ 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):
- try:
- self.driver.create_volume_from_snapshot("", "")
- except NotImplementedError:
- pass
+ 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:
:createVolume: (creates the volume)
+ :deleteVolume: (deletes the volume)
+
+ :modifyVolume: (extends the volume)
+
+ :createSnapshot: (creates the snapshot)
+
+ :deleteSnapshot: (deletes the snapshot)
+
+ :cloneSnapshot: (creates the volume from a snapshot)
+
:getVolumeInfo: (to discover the IQN etc)
+ :getSnapshotInfo: (to discover the IQN etc)
+
:getClusterInfo: (to discover the iSCSI target IP address)
:assignVolumeChap: (exports it with CHAP security)
share the volume using CHAP at volume creation time. Then the mount need
only use those CHAP credentials, so can take place exclusively in the
compute layer.
+
+ Version history:
+ 1.0.0 - Initial driver
+ 1.1.0 - Added create/delete snapshot, extend volume, create volume
+ from snapshot support.
"""
- VERSION = "1.0.0"
+ VERSION = "1.1.0"
device_stats = {}
result_xml = self._cliq_run_xml("getVolumeInfo", cliq_args)
# Result looks like this:
- #<gauche version="1.0">
+ # <gauche version="1.0">
# <response description="Operation succeeded." name="CliqSuccess"
# processingTime="87" result="0">
# <volume autogrowPages="4" availability="online" blockSize="1024"
# loadBalance="true" targetSecret="supersecret"/>
# </volume>
# </response>
- #</gauche>
+ # </gauche>
# Flatten the nodes into a dictionary; use prefixes to avoid collisions
volume_attributes = {}
'volume_attributes': volume_attributes})
return volume_attributes
+ def _cliq_get_snapshot_info(self, snapshot_name):
+ """Gets the snapshot info, including IQN"""
+ cliq_args = {}
+ cliq_args['snapshotName'] = snapshot_name
+ result_xml = self._cliq_run_xml("getSnapshotInfo", cliq_args)
+
+ # Result looks like this:
+ # <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="CloudMgmtGroup1"
+ # id="730" initialQuota="536870912" isPrimary="true"
+ # iscsiIqn="iqn.2003-10.com.lefthandnetworks:cloudmgmtgroup1:73"
+ # md5="a64b4f850539c07fb5ce3cee5db1fcce" minReplication="1"
+ # name="snapshot-7849288e-e5e8-42cb-9687-9af5355d674b"
+ # replication="2" reserveQuota="536870912" scheduleId="0"
+ # scratchQuota="4194304" scratchWritten="0"
+ # serialNumber="a64b4f850539c07fb5ce3cee5db1fcce00000000000002da"
+ # size="2147483648" stridePages="32"
+ # volumeSerial="a64b4f850539c07fb5ce3cee5db1fcce00000000000002d">
+ # <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>
+
+ # Flatten the nodes into a dictionary; use prefixes to avoid collisions
+ snapshot_attributes = {}
+
+ snapshot_node = result_xml.find("response/snapshot")
+ for k, v in snapshot_node.attrib.items():
+ snapshot_attributes["snapshot." + k] = v
+
+ status_node = snapshot_node.find("status")
+ if status_node is not None:
+ for k, v in status_node.attrib.items():
+ snapshot_attributes["status." + k] = v
+
+ # We only consider the first permission node
+ permission_node = snapshot_node.find("permission")
+ if permission_node is not None:
+ for k, v in status_node.attrib.items():
+ snapshot_attributes["permission." + k] = v
+
+ LOG.debug(_("Snapshot info: %(name)s => %(attributes)s") %
+ {'name': snapshot_name, 'attributes': snapshot_attributes})
+ return snapshot_attributes
+
def create_volume(self, volume):
"""Creates a volume."""
cliq_args = {}
self._cliq_run_xml("createVolume", cliq_args)
- volume_info = self._cliq_get_volume_info(volume['name'])
- cluster_name = volume_info['volume.clusterName']
- iscsi_iqn = volume_info['volume.iscsiIqn']
-
- #TODO(justinsb): Is this always 1? Does it matter?
- cluster_interface = '1'
-
- if not self.cluster_vip:
- self.cluster_vip = self._cliq_get_cluster_vip(cluster_name)
- iscsi_portal = self.cluster_vip + ":3260," + cluster_interface
-
- model_update = {}
+ return self._get_model_update(volume['name'])
- # NOTE(jdg): LH volumes always at lun 0 ?
- model_update['provider_location'] = ("%s %s %s" %
- (iscsi_portal,
- iscsi_iqn,
- 0))
+ def extend_volume(self, volume, new_size):
+ """Extend the size of an existing volume."""
+ cliq_args = {}
+ cliq_args['volumeName'] = volume['name']
+ cliq_args['size'] = '%sGB' % new_size
- return model_update
+ self._cliq_run_xml("modifyVolume", cliq_args)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
- raise NotImplementedError()
+ cliq_args = {}
+ cliq_args['snapshotName'] = snapshot['name']
+ cliq_args['volumeName'] = volume['name']
+
+ self._cliq_run_xml("cloneSnapshot", cliq_args)
+
+ return self._get_model_update(volume['name'])
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
- raise NotImplementedError()
+ cliq_args = {}
+ cliq_args['snapshotName'] = snapshot['name']
+ cliq_args['volumeName'] = snapshot['volume_name']
+ cliq_args['inheritAccess'] = 1
+ self._cliq_run_xml("createSnapshot", cliq_args)
def delete_volume(self, volume):
"""Deletes a volume."""
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)
+ def delete_snapshot(self, snapshot):
+ """Deletes a snapshot."""
+ cliq_args = {}
+ cliq_args['snapshotName'] = snapshot['name']
+ cliq_args['prompt'] = 'false' # Don't confirm
+ try:
+ volume_info = self._cliq_get_snapshot_info(snapshot['name'])
+ except processutils.ProcessExecutionError:
+ LOG.error_("Snapshot did not exist. It will not be deleted")
+ return
+ self._cliq_run_xml("deleteSnapshot", cliq_args)
+
def local_path(self, volume):
msg = _("local_path not supported")
raise exception.VolumeBackendAPIException(data=msg)
cliq_args['initiator'] = connector['initiator']
self._cliq_run_xml("createServer", cliq_args)
+ def _get_model_update(self, volume_name):
+ volume_info = self._cliq_get_volume_info(volume_name)
+ cluster_name = volume_info['volume.clusterName']
+ iscsi_iqn = volume_info['volume.iscsiIqn']
+
+ # TODO(justinsb): Is this always 1? Does it matter?
+ cluster_interface = '1'
+
+ if not self.cluster_vip:
+ self.cluster_vip = self._cliq_get_cluster_vip(cluster_name)
+ iscsi_portal = self.cluster_vip + ":3260," + cluster_interface
+
+ model_update = {}
+
+ # NOTE(jdg): LH volumes always at lun 0 ?
+ model_update['provider_location'] = ("%s %s %s" %
+ (iscsi_portal,
+ iscsi_iqn,
+ 0))
+ return model_update
+
def terminate_connection(self, volume, connector, **kwargs):
"""Unassign the volume from the host."""
cliq_args = {}