From c129cdf3040c9be4fb5a2d5317c1aedd5befc454 Mon Sep 17 00:00:00 2001
From: Gabriel Reid <gabriel.reid@gmail.com>
Date: Tue, 5 Jun 2012 10:23:08 +0200
Subject: [PATCH 1/2] Add CloudStack support for block storage

Add initial internal methods in the CloudStack driver for the creation
and attaching of block storage volumes.
---
 libcloud/compute/drivers/cloudstack.py             |   78 ++++++++++++++++++++
 .../fixtures/cloudstack/attachVolume_default.json  |    1 +
 .../fixtures/cloudstack/createVolume_default.json  |    1 +
 .../createVolume_withcustomdisksize.json           |    1 +
 .../cloudstack/listDiskOfferings_default.json      |    1 +
 .../listDiskOfferings_withcustomdisksize.json      |    1 +
 .../cloudstack/listZones_withcustomdisksize.json   |    1 +
 .../queryAsyncJobResult_attachvolumejob.json       |    1 +
 .../queryAsyncJobResult_createvolumejob.json       |    1 +
 test/compute/test_cloudstack.py                    |   61 ++++++++++++++-
 10 files changed, 146 insertions(+), 1 deletion(-)
 create mode 100644 test/compute/fixtures/cloudstack/attachVolume_default.json
 create mode 100644 test/compute/fixtures/cloudstack/createVolume_default.json
 create mode 100644 test/compute/fixtures/cloudstack/createVolume_withcustomdisksize.json
 create mode 100644 test/compute/fixtures/cloudstack/listDiskOfferings_default.json
 create mode 100644 test/compute/fixtures/cloudstack/listDiskOfferings_withcustomdisksize.json
 create mode 100644 test/compute/fixtures/cloudstack/listZones_withcustomdisksize.json
 create mode 100644 test/compute/fixtures/cloudstack/queryAsyncJobResult_attachvolumejob.json
 create mode 100644 test/compute/fixtures/cloudstack/queryAsyncJobResult_createvolumejob.json

diff --git libcloud/compute/drivers/cloudstack.py libcloud/compute/drivers/cloudstack.py
index 6ba8a73..e1d2717 100644
--- libcloud/compute/drivers/cloudstack.py
+++ libcloud/compute/drivers/cloudstack.py
@@ -78,6 +78,31 @@ class CloudStackForwardingRule(object):
         return self.__class__ is other.__class__ and self.id == other.id
 
 
+class CloudStackDiskOffering(object):
+    """A disk offering within CloudStack."""
+
+    def __init__(self, id, name, size, customizable):
+        self.id = id
+        self.name = name
+        self.size = size
+        self.customizable = customizable
+
+    def __eq__(self, other):
+        return self.__class__ is other.__class__ and self.id == other.id
+
+
+
+class CloudStackVolume(object):
+    """A storage volume within CloudStack."""
+
+    def __init__(self, id, name):
+        self.id = id
+        self.name = name
+
+    def __eq__(self, other):
+        return self.__class__ is other.__class__ and self.id == other.id
+
+
 class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver):
     """Driver for the CloudStack API.
 
@@ -200,6 +225,7 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver):
                                   0, self))
         return sizes
 
+
     def create_node(self, name, size, image, location=None, **kwargs):
         extra_args = {}
         if location is None:
@@ -247,6 +273,58 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver):
         self._async_request('rebootVirtualMachine', id=node.id)
         return True
 
+    def ex_list_disk_offerings(self):
+        """Fetch a list of all available disk offerings."""
+
+        diskOfferings = []
+
+        diskOfferResponse = self._sync_request('listDiskOfferings')
+        for diskOfferDict in diskOfferResponse.get('diskoffering', ()):
+            diskOfferings.append(
+                    CloudStackDiskOffering(
+                        id=diskOfferDict['id'],
+                        name=diskOfferDict['name'],
+                        size=diskOfferDict['disksize'],
+                        customizable=diskOfferDict['iscustomized']))
+
+        return diskOfferings
+
+    def ex_create_volume(self, name, location, size):
+        """Create a new detached storage volume."""
+
+        for diskOffering in self.ex_list_disk_offerings():
+            if diskOffering.size == size or diskOffering.customizable:
+                break
+        else:
+            raise LibcloudError(
+                    "Disk offering with size=%s not found" % size)
+
+        extraParams = dict()
+        if diskOffering.customizable:
+            extraParams['size'] = size
+
+        requestResult = self._async_request('createVolume', 
+                                name=name,
+                                diskOfferingId=diskOffering.id,
+                                zoneId=location.id,
+                                **extraParams)
+
+        volumeResponse = requestResult['volume']
+
+        return CloudStackVolume(
+                            id = volumeResponse['id'],
+                            name = volumeResponse['name'])
+
+
+    def ex_attach_volume(self, node, volume):
+        """Attach a storage volume to a node"""
+
+        self._async_request('attachVolume', 
+                                id=volume.id, 
+                                virtualMachineId=node.id)
+        return True
+
+
     def ex_allocate_public_ip(self, node):
         "Allocate a public IP and bind it to a node."
 
diff --git test/compute/fixtures/cloudstack/attachVolume_default.json test/compute/fixtures/cloudstack/attachVolume_default.json
new file mode 100644
index 0000000..27535ba
--- /dev/null
+++ test/compute/fixtures/cloudstack/attachVolume_default.json
@@ -0,0 +1 @@
+{ "attachvolumeresponse" : {"jobid":"attachvolumejob"} }
diff --git test/compute/fixtures/cloudstack/createVolume_default.json test/compute/fixtures/cloudstack/createVolume_default.json
new file mode 100644
index 0000000..dc51b35
--- /dev/null
+++ test/compute/fixtures/cloudstack/createVolume_default.json
@@ -0,0 +1 @@
+{ "createvolumeresponse" : {"id":"60338035-92fb-4d27-98d4-b60ad4b38b87","jobid":"createvolumejob"} }
diff --git test/compute/fixtures/cloudstack/createVolume_withcustomdisksize.json test/compute/fixtures/cloudstack/createVolume_withcustomdisksize.json
new file mode 100644
index 0000000..dc51b35
--- /dev/null
+++ test/compute/fixtures/cloudstack/createVolume_withcustomdisksize.json
@@ -0,0 +1 @@
+{ "createvolumeresponse" : {"id":"60338035-92fb-4d27-98d4-b60ad4b38b87","jobid":"createvolumejob"} }
diff --git test/compute/fixtures/cloudstack/listDiskOfferings_default.json test/compute/fixtures/cloudstack/listDiskOfferings_default.json
new file mode 100644
index 0000000..f279b95
--- /dev/null
+++ test/compute/fixtures/cloudstack/listDiskOfferings_default.json
@@ -0,0 +1 @@
+{ "listdiskofferingsresponse" : { "count":1 ,"diskoffering" : [  {"id":"6345e3b7-227e-4209-8f8c-1f94219696e6","name":"Disk offer 1","displaytext":"Disk offer 1 display name","disksize":10,"created":"2012-04-24T16:35:55+0200","iscustomized":false}  ] } }
diff --git test/compute/fixtures/cloudstack/listDiskOfferings_withcustomdisksize.json test/compute/fixtures/cloudstack/listDiskOfferings_withcustomdisksize.json
new file mode 100644
index 0000000..f4660e0
--- /dev/null
+++ test/compute/fixtures/cloudstack/listDiskOfferings_withcustomdisksize.json
@@ -0,0 +1 @@
+{ "listdiskofferingsresponse" : { "count":1 ,"diskoffering" : [  {"id":"6345e3b7-227e-4209-8f8c-1f94219696e6","name":"Disk offer 1","displaytext":"Disk offer 1 display name","disksize":10,"created":"2012-04-24T16:35:55+0200","iscustomized":true}  ] } }
diff --git test/compute/fixtures/cloudstack/listZones_withcustomdisksize.json test/compute/fixtures/cloudstack/listZones_withcustomdisksize.json
new file mode 100644
index 0000000..0316936
--- /dev/null
+++ test/compute/fixtures/cloudstack/listZones_withcustomdisksize.json
@@ -0,0 +1 @@
+{ "listzonesresponse" : { "zone" : [  {"id":1,"name":"Sydney","networktype":"Advanced","securitygroupsenabled":false} ] } }
diff --git test/compute/fixtures/cloudstack/queryAsyncJobResult_attachvolumejob.json test/compute/fixtures/cloudstack/queryAsyncJobResult_attachvolumejob.json
new file mode 100644
index 0000000..cd5278f
--- /dev/null
+++ test/compute/fixtures/cloudstack/queryAsyncJobResult_attachvolumejob.json
@@ -0,0 +1 @@
+{ "queryasyncjobresultresponse" : {"accountid":"be7d76b3-8823-49c0-86e1-29efd9ea1eb0","userid":"a8bd3087-edc1-4e94-8470-6830404b7292","cmd":"com.cloud.api.commands.AttachVolumeCmd","jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"volume":{"id":"60338035-92fb-4d27-98d4-b60ad4b38b87","name":"vol-0","zoneid":"58624957-a150-46a3-acbf-4088776161e5","zonename":"EQ-AMS2-Z01","type":"DATADISK","deviceid":5,"virtualmachineid":"ab2c18f6-00a6-43f8-9fe0-efecb3165dd7","vmname":"ab2c18f6-00a6-43f8-9fe0-efecb3165dd7","vmdisplayname":"gre-kickstart","vmstate":"Running","size":10737418240,"created":"2012-06-05T08:47:54+0200","state":"Ready","account":"admin","domainid":"bfc35f83-8589-4e93-9150-d57e8479f772","domain":"ROOT","storagetype":"shared","hypervisor":"KVM","diskofferingid":"6345e3b7-227e-4209-8f8c-1f94219696e6","diskofferingname":"Disk offering 1","diskofferingdisplaytext":"Disk offering 1 display name","storage":"Shared Storage CL01","attached":"2012-06-05T09:17:38+0200","destroyed":false,"isextractable":false}},"created":"2012-06-05T09:17:38+0200","jobid":"e07d6b9b-2b6c-45bd-840b-3c4c3d890168"} }
diff --git test/compute/fixtures/cloudstack/queryAsyncJobResult_createvolumejob.json test/compute/fixtures/cloudstack/queryAsyncJobResult_createvolumejob.json
new file mode 100644
index 0000000..9955c93
--- /dev/null
+++ test/compute/fixtures/cloudstack/queryAsyncJobResult_createvolumejob.json
@@ -0,0 +1 @@
+{ "queryasyncjobresultresponse" : {"accountid":"be7d76b3-8823-49c0-86e1-29efd9ea1eb0","userid":"a8bd3087-edc1-4e94-8470-6830404b7292","cmd":"com.cloud.api.commands.CreateVolumeCmd","jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"volume":{"id":"60338035-92fb-4d27-98d4-b60ad4b38b87","name":"vol-0","zoneid":"58624957-a150-46a3-acbf-4088776161e5","zonename":"EQ-AMS2-Z01","type":"DATADISK","size":10737418240,"created":"2012-06-05T08:47:54+0200","state":"Allocated","account":"admin","domainid":"bfc35f83-8589-4e93-9150-d57e8479f772","domain":"ROOT","storagetype":"shared","hypervisor":"None","diskofferingid":"6345e3b7-227e-4209-8f8c-1f94219696e6","diskofferingname":"Disk offering","diskofferingdisplaytext":"Disk offering display name","storage":"none","destroyed":false,"isextractable":false}},"created":"2012-06-05T08:47:54+0200","jobid":"35416f6d-1b5b-4ceb-a7d4-aab0deede71b"} }
diff --git test/compute/test_cloudstack.py test/compute/test_cloudstack.py
index aa973cb..e5ff5d9 100644
--- test/compute/test_cloudstack.py
+++ test/compute/test_cloudstack.py
@@ -16,7 +16,7 @@ except AttributeError:
     parse_qsl = cgi.parse_qsl
 
 from libcloud.compute.drivers.cloudstack import CloudStackNodeDriver
-from libcloud.compute.types import DeploymentError
+from libcloud.compute.types import DeploymentError, LibcloudError
 
 from test import MockHttpTestCase
 from test.compute import TestCaseMixin
@@ -64,6 +64,65 @@ class CloudStackNodeDriverTest(unittest.TestCase, TestCaseMixin):
         images = self.driver.list_images()
         self.assertEquals(0, len(images))
 
+
+    def test_ex_list_disk_offerings(self):
+        diskOfferings = self.driver.ex_list_disk_offerings()
+        self.assertEquals(1, len(diskOfferings))
+
+        diskOffering, = diskOfferings
+
+        self.assertEquals('Disk offer 1', diskOffering.name)
+        self.assertEquals(10, diskOffering.size)
+
+
+    def test_ex_create_volume(self):
+        volumeName = 'vol-0'
+        location = self.driver.list_locations()[0]
+
+        volume = self.driver.ex_create_volume(
+                                volumeName, location, size=10)
+
+        self.assertEquals(volumeName, volume.name)
+
+    def test_ex_create_volume_no_noncustomized_offering_with_size(self):
+        """If the sizes of disk offerings are not configurable and there
+        are no disk offerings with the requested size, an exception should
+        be thrown."""
+
+        location = self.driver.list_locations()[0]
+
+        self.assertRaises(
+                LibcloudError,
+                self.driver.ex_create_volume,
+                    'vol-0', location, 11)
+
+    def test_ex_create_volume_with_custom_disk_size_offering(self):
+
+        CloudStackMockHttp.fixture_tag = 'withcustomdisksize'
+
+        volumeName = 'vol-0'
+        location = self.driver.list_locations()[0]
+
+        volume = self.driver.ex_create_volume(
+                                volumeName, location, size=11)
+
+        self.assertEquals(volumeName, volume.name)
+
+
+
+    def test_ex_attach_volume(self):
+        node = self.driver.list_nodes()[0]
+        volumeName = 'vol-0'
+        location = self.driver.list_locations()[0]
+
+        volume = self.driver.ex_create_volume(
+                                volumeName, location, 10)
+
+        attachReturnVal = self.driver.ex_attach_volume(volume, node)
+
+        self.assertTrue(attachReturnVal)
+
+
 class CloudStackMockHttp(MockHttpTestCase):
     fixtures = ComputeFileFixtures('cloudstack')
     fixture_tag = 'default'
-- 
1.7.9.5


From d7ce8e77e67bd85c228e424a80df5c25d5db0635 Mon Sep 17 00:00:00 2001
From: Gabriel Reid <gabriel.reid@gmail.com>
Date: Thu, 7 Jun 2012 16:21:17 +0200
Subject: [PATCH 2/2] Add documentation for block storate methods

---
 libcloud/compute/drivers/cloudstack.py |   25 +++++++++++++++++++++++--
 1 file changed, 23 insertions(+), 2 deletions(-)

diff --git libcloud/compute/drivers/cloudstack.py libcloud/compute/drivers/cloudstack.py
index e1d2717..0c07e27 100644
--- libcloud/compute/drivers/cloudstack.py
+++ libcloud/compute/drivers/cloudstack.py
@@ -290,7 +290,20 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver):
         return diskOfferings
 
     def ex_create_volume(self, name, location, size):
-        """Create a new detached storage volume."""
+        """
+        Create a new detached storage volume.
+
+        @type name: C{str}
+        @param name: Name to be given to the created volume
+
+        @type location: L{NodeLocation}
+        @param location: The location where the volume is to be created
+
+        @type size: C{int}
+        @param location: The size of the volume to be created, in GB
+
+        @returns: The newly-created detached volume
+        """
 
         for diskOffering in self.ex_list_disk_offerings():
             if diskOffering.size == size or diskOffering.customizable:
@@ -317,7 +330,15 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver):
 
 
     def ex_attach_volume(self, node, volume):
-        """Attach a storage volume to a node"""
+        """
+        Attach a storage volume to a node.
+
+        @type node: L{CloudStackNode}
+        @param node: The node to which the volume is to be attached
+
+        @type volume: L{CloudStackVolume}
+        @param volume: The volume to be attached
+        """
 
         self._async_request('attachVolume', 
                                 id=volume.id, 
-- 
1.7.9.5

