From 953c9e8a1ce52b181d4489bc50c3e0f7c9b17603 Mon Sep 17 00:00:00 2001 From: Kurt Martin Date: Wed, 28 Aug 2013 14:08:49 -0700 Subject: [PATCH] Add missing LH SAN driver features for Havana The LeftHand driver did not support snapshot or extend volume. The following features are part of the minimum driver features that should be implemented in every cinder driver. This patch adds the following missing features; create snapshot delete snapshot create volume from snapshot extend volume It also fixes the DocStrings in the unit test so they pass the checks. Change-Id: Icda790fcddf4ea11782a8d6ec57b1e8f871eb167 Fixes: bug 1217118 --- cinder/tests/test_HpSanISCSIDriver.py | 147 +++++++++++++++++++--- cinder/volume/drivers/san/hp_lefthand.py | 152 +++++++++++++++++++---- 2 files changed, 259 insertions(+), 40 deletions(-) diff --git a/cinder/tests/test_HpSanISCSIDriver.py b/cinder/tests/test_HpSanISCSIDriver.py index a9ef2106c..ee5424ec2 100644 --- a/cinder/tests/test_HpSanISCSIDriver.py +++ b/cinder/tests/test_HpSanISCSIDriver.py @@ -43,6 +43,7 @@ class HpSanISCSITestCase(test.TestCase): 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'} @@ -63,7 +64,8 @@ class HpSanISCSITestCase(test.TestCase): """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" @@ -78,7 +80,8 @@ class HpSanISCSITestCase(test.TestCase): return output, None def delete_volume(cliq_args): - """ + """Delete volume CLIQ input for test. + input = "deleteVolume volumeName=fakevolume prompt=false output=XML" """ @@ -90,8 +93,24 @@ class HpSanISCSITestCase(test.TestCase): 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 = """ + + """ + 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" @@ -105,7 +124,8 @@ class HpSanISCSITestCase(test.TestCase): return output, None def unassign_volume(cliq_args): - """ + """Unassign volume CLIQ input for test. + input = "unassignVolumeToServer volumeName=fakevolume serverName=fakehost output=XML """ @@ -117,8 +137,55 @@ class HpSanISCSITestCase(test.TestCase): 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 = """ + + """ + 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 = """ + + """ + 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 = """ + + """ + 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" """ @@ -139,7 +206,8 @@ class HpSanISCSITestCase(test.TestCase): return output, None def get_volume_info(cliq_args): - """ + """Get volume info CLIQ input for test. + input = "getVolumeInfo volumeName=fakevolume output=XML" """ output = """ @@ -165,8 +233,39 @@ class HpSanISCSITestCase(test.TestCase): """ 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 = """ + + + + + """ + return output, None + + def get_server_info(cliq_args): + """Get server info CLIQ input for test. + input = "getServerInfo serverName=fakeName" """ output = """ @@ -174,7 +273,8 @@ class HpSanISCSITestCase(test.TestCase): return output, None def create_server(cliq_args): - """ + """Create server CLIQ input for test. + input = "createServer serverName=fakeName initiator=something" """ output = """ @@ -193,10 +293,15 @@ class HpSanISCSITestCase(test.TestCase): 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} @@ -216,6 +321,10 @@ class HpSanISCSITestCase(test.TestCase): 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) @@ -227,16 +336,22 @@ class HpSanISCSITestCase(test.TestCase): 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: diff --git a/cinder/volume/drivers/san/hp_lefthand.py b/cinder/volume/drivers/san/hp_lefthand.py index 8e22662d9..566734be7 100644 --- a/cinder/volume/drivers/san/hp_lefthand.py +++ b/cinder/volume/drivers/san/hp_lefthand.py @@ -37,8 +37,20 @@ class HpSanISCSIDriver(SanISCSIDriver): :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) @@ -50,9 +62,14 @@ class HpSanISCSIDriver(SanISCSIDriver): 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 = {} @@ -129,7 +146,7 @@ class HpSanISCSIDriver(SanISCSIDriver): result_xml = self._cliq_run_xml("getVolumeInfo", cliq_args) # Result looks like this: - # + # # # # # - # + # # Flatten the nodes into a dictionary; use prefixes to avoid collisions volume_attributes = {} @@ -175,6 +192,61 @@ class HpSanISCSIDriver(SanISCSIDriver): '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: + # + # + # + # + # + # + # + # + + # 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 = {} @@ -193,34 +265,33 @@ class HpSanISCSIDriver(SanISCSIDriver): 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.""" @@ -230,10 +301,22 @@ 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) + 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) @@ -284,6 +367,27 @@ class HpSanISCSIDriver(SanISCSIDriver): 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 = {} -- 2.45.2