diff --git libcloud/compute/base.py libcloud/compute/base.py index 2e32f95..8a0e8b5 100644 --- libcloud/compute/base.py +++ libcloud/compute/base.py @@ -362,6 +362,58 @@ class NodeAuthPassword(object): def __repr__(self): return '' +class StorageVolume(UuidMixin): + """ + A base StorageVolume class to derive from. + """ + + def __init__(self, id, name, size, driver, extra=None): + self.id = id + self.name = name + self.size = size + self.driver = driver + self.extra = extra + UuidMixin.__init__(self) + + + def attach(self, node, device=None): + """Attach this volume to a node. + + @param node: Node to attach volume to + @type node: L{Node} + + @param device: Where the device is exposed, + e.g. '/dev/sdb (optional) + @type device: C{str} + + @returns C{bool} + """ + + return self.driver.attach(node, self, device) + + + def detach(self): + """Detach this volume from its node + + @returns C{bool} + """ + + return self.driver.detach(self) + + + def destroy(self): + """Destroy this storage volume. + + @returns C{bool} + """ + + return self.driver.destroy_volume(self) + + + def __repr__(self): + return "" % ( + self.id, self.size, self.driver.name) + class NodeDriver(BaseDriver): """ @@ -617,6 +669,73 @@ class NodeDriver(BaseDriver): return node + + def create_volume(self, size, name, location=None, snapshot=None): + """Create a new volume. + + @param size: Size of volume in gigabytes (required) + @type size: C{int} + + @keyword name: Name of the volume to be created + @type name: C{str} + + @keyword location: Which data center to create a volume in. If empty, + undefined behavoir will be selected. (optional) + @type location: L{NodeLocation} + + @keyword snapshot: Name of snapshot from which to create the new + volume. (optional) + @type snapshot: C{str} + + @return: The newly created L{StorageVolume}. + """ + raise NotImplementedError( + 'create_volume not implemented for this driver') + + + def destroy_volume(self, volume): + """Destroys a storage volume + + @param volume: Volume to be destroyed + @type volume: L{StorageVolume} + + @return: C{bool} + """ + + raise NotImplementedError( + 'destroy_volume not implemented for this driver') + + + def attach(self, node, volume, device=None): + """Attaches volume to node + + @param node: Node to attach volume to + @type node: L{Node} + + @param volume: Volume to attach + @type volume: L{StorageVolume} + + @param device: Where the device is exposed, + e.g. '/dev/sdb (optional) + @type device: C{str} + + @return: C{bool} + """ + raise NotImplementedError('attach not implemented for this driver') + + + def detach(self, volume): + """Detaches a volume from a node + + @param volume: Volume to be detached + @type volume: L{StorageVolume} + + @returns C{bool} + """ + + raise NotImplementedError('detach not implemented for this driver') + + def _wait_until_running(self, node, wait_period=3, timeout=600, ssh_interface='public_ips', force_ipv4=True): """ diff --git libcloud/compute/drivers/cloudstack.py libcloud/compute/drivers/cloudstack.py index 0c07e27..4744cb5 100644 --- libcloud/compute/drivers/cloudstack.py +++ libcloud/compute/drivers/cloudstack.py @@ -16,7 +16,7 @@ from libcloud.compute.providers import Provider from libcloud.common.cloudstack import CloudStackDriverMixIn from libcloud.compute.base import Node, NodeDriver, NodeImage, NodeLocation, \ - NodeSize + NodeSize, StorageVolume from libcloud.compute.types import NodeState, LibcloudError @@ -91,18 +91,6 @@ class CloudStackDiskOffering(object): 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. @@ -289,21 +277,10 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): return diskOfferings - def ex_create_volume(self, name, location, size): - """ - Create a new detached storage volume. + def create_volume(self, size, name, location, snapshot=None): - @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 - """ + # TODO Add snapshot handling for diskOffering in self.ex_list_disk_offerings(): if diskOffering.size == size or diskOffering.customizable: @@ -324,21 +301,17 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): volumeResponse = requestResult['volume'] - return CloudStackVolume( + return StorageVolume( id = volumeResponse['id'], - name = volumeResponse['name']) - + name = name, + size = size, + driver = self, + extra = dict(name = volumeResponse['name'])) - def ex_attach_volume(self, node, volume): - """ - Attach a storage volume to a node. - @type node: L{CloudStackNode} - @param node: The node to which the volume is to be attached + def attach(self, node, volume, device=None): - @type volume: L{CloudStackVolume} - @param volume: The volume to be attached - """ + # TODO Add handling for device name self._async_request('attachVolume', id=volume.id, @@ -346,6 +319,16 @@ class CloudStackNodeDriver(CloudStackDriverMixIn, NodeDriver): return True + def detach(self, volume): + self._async_request('detachVolume', id=volume.id) + return True + + + def destroy_volume(self, volume): + self._sync_request('deleteVolume', id=volume.id) + return True + + def ex_allocate_public_ip(self, node): "Allocate a public IP and bind it to a node." diff --git libcloud/compute/drivers/ec2.py libcloud/compute/drivers/ec2.py index 0179b26..49cf61c 100644 --- libcloud/compute/drivers/ec2.py +++ libcloud/compute/drivers/ec2.py @@ -39,7 +39,7 @@ from libcloud.common.types import (InvalidCredsError, MalformedResponseError, from libcloud.compute.providers import Provider from libcloud.compute.types import NodeState from libcloud.compute.base import Node, NodeDriver, NodeLocation, NodeSize -from libcloud.compute.base import NodeImage +from libcloud.compute.base import NodeImage, StorageVolume EC2_US_EAST_HOST = 'ec2.us-east-1.amazonaws.com' EC2_US_WEST_HOST = 'ec2.us-west-1.amazonaws.com' @@ -460,6 +460,16 @@ class EC2NodeDriver(NodeDriver): ) return n + def _to_volume(self, element, name): + volId = findtext(element=element, xpath='volumeId', namespace=NAMESPACE) + size = findtext(element=element, xpath='size', namespace=NAMESPACE) + + return StorageVolume( + id=volId, + name=name, + size=int(size), + driver=self) + def list_nodes(self, ex_node_ids=None): """ @type node.id: C{list} @@ -523,6 +533,51 @@ class EC2NodeDriver(NodeDriver): availability_zone)) return locations + + def create_volume(self, size, name, location=None, snapshot=None): + params = { + 'Action': 'CreateVolume', + 'Size': str(size) } + + if location != None: + params['AvailabilityZone'] = location.availability_zone.name + + volume = self._to_volume( + self.connection.request(self.path, params=params).object, + name=name) + self.ex_create_tags(volume, {'Name': name}) + + return volume + + + def destroy_volume(self, volume): + params = { + 'Action': 'DeleteVolume', + 'VolumeId': volume.id } + response = self.connection.request(self.path, params=params).object + return self._get_boolean(response) + + + def attach(self, node, volume, device): + params = { + 'Action': 'AttachVolume', + 'VolumeId': volume.id, + 'InstanceId': node.id, + 'Device': device } + + self.connection.request(self.path, params=params) + return True + + + def detach(self, volume): + params = { + 'Action': 'DetachVolume', + 'VolumeId': volume.id } + + self.connection.request(self.path, params=params) + return True + + def ex_create_keypair(self, name): """Creates a new keypair @@ -740,12 +795,12 @@ class EC2NodeDriver(NodeDriver): tags[key] = value return tags - def ex_create_tags(self, node, tags): + def ex_create_tags(self, resource, tags): """ - Create tags for an instance. + Create tags for a resource. - @type node: C{Node} - @param node: Node instance + @type resource: EC2 resource + @param resource: Resource to be tagged @param tags: A dictionary or other mapping of strings to strings, associating tag names with tag values. """ @@ -753,7 +808,7 @@ class EC2NodeDriver(NodeDriver): return params = {'Action': 'CreateTags', - 'ResourceId.0': node.id} + 'ResourceId.0': resource.id} for i, key in enumerate(tags): params['Tag.%d.Key' % i] = key params['Tag.%d.Value' % i] = tags[key] @@ -761,12 +816,12 @@ class EC2NodeDriver(NodeDriver): self.connection.request(self.path, params=params.copy()).object - def ex_delete_tags(self, node, tags): + def ex_delete_tags(self, resource, tags): """ - Delete tags from an instance. + Delete tags from a resource. - @type node: C{Node} - @param node: Node instance + @type resource: EC2 resource + @param resource: Resource to be tagged @param tags: A dictionary or other mapping of strings to strings, specifying the tag names and tag values to be deleted. """ @@ -774,7 +829,7 @@ class EC2NodeDriver(NodeDriver): return params = {'Action': 'DeleteTags', - 'ResourceId.0': node.id} + 'ResourceId.0': resource.id} for i, key in enumerate(tags): params['Tag.%d.Key' % i] = key params['Tag.%d.Value' % i] = tags[key] @@ -1006,7 +1061,7 @@ class EC2NodeDriver(NodeDriver): tags = {'Name': kwargs['name']} try: - self.ex_create_tags(node=node, tags=tags) + self.ex_create_tags(resource=node, tags=tags) except Exception: continue @@ -1291,7 +1346,7 @@ class NimbusNodeDriver(EC2NodeDriver): nodes_elastic_ip_mappings[node.id] = [] return nodes_elastic_ip_mappings - def ex_create_tags(self, node, tags): + def ex_create_tags(self, resource, tags): """ Nimbus doesn't support creating tags, so this is a passthrough """ diff --git test/compute/fixtures/cloudstack/deleteVolume_default.json test/compute/fixtures/cloudstack/deleteVolume_default.json new file mode 100644 index 0000000..3786dab --- /dev/null +++ test/compute/fixtures/cloudstack/deleteVolume_default.json @@ -0,0 +1 @@ +{ "deletevolumeresponse" : { "success" : "true"} } diff --git test/compute/fixtures/cloudstack/detachVolume_default.json test/compute/fixtures/cloudstack/detachVolume_default.json new file mode 100644 index 0000000..6266238 --- /dev/null +++ test/compute/fixtures/cloudstack/detachVolume_default.json @@ -0,0 +1 @@ +{ "detachvolumeresponse" : {"jobid":"detachvolumejob"} } diff --git test/compute/fixtures/cloudstack/queryAsyncJobResult_detachvolumejob.json test/compute/fixtures/cloudstack/queryAsyncJobResult_detachvolumejob.json new file mode 100644 index 0000000..6d48599 --- /dev/null +++ test/compute/fixtures/cloudstack/queryAsyncJobResult_detachvolumejob.json @@ -0,0 +1 @@ +{ "queryasyncjobresultresponse" : {"accountid":"be7d76b3-8823-49c0-86e1-29efd9ea1eb0","userid":"a8bd3087-edc1-4e94-8470-6830404b7292","cmd":"com.cloud.api.commands.DetachVolumeCmd","jobstatus":1,"jobprocstatus":0,"jobresultcode":0,"jobresulttype":"object","jobresult":{"volume":{"id":"5931d2ca-4e90-4915-88a8-32b38b3991a3","name":"gre-test-volume","zoneid":"58624957-a150-46a3-acbf-4088776161e5","zonename":"EQ-AMS2-Z01","type":"DATADISK","size":10737418240,"created":"2012-06-15T14:56:40+0200","state":"Ready","account":"admin","domainid":"bfc35f83-8589-4e93-9150-d57e8479f772","domain":"ROOT","storagetype":"shared","hypervisor":"KVM","diskofferingid":"6345e3b7-227e-4209-8f8c-1f94219696e6","diskofferingname":"OS disk for Windows","diskofferingdisplaytext":"OS disk for Windows","storage":"Shared Storage CL01","destroyed":false,"isextractable":false}},"created":"2012-06-15T15:08:39+0200","jobid":"ca6c856d-1f36-4e27-989e-09cad2dad808"} } diff --git test/compute/fixtures/ec2/attach_volume.xml test/compute/fixtures/ec2/attach_volume.xml new file mode 100644 index 0000000..21cc8c0 --- /dev/null +++ test/compute/fixtures/ec2/attach_volume.xml @@ -0,0 +1,8 @@ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + vol-4d826724 + i-6058a509 + /dev/sdh + attaching + 2008-05-07T11:51:50.000Z + diff --git test/compute/fixtures/ec2/create_volume.xml test/compute/fixtures/ec2/create_volume.xml new file mode 100644 index 0000000..46de1f2 --- /dev/null +++ test/compute/fixtures/ec2/create_volume.xml @@ -0,0 +1,9 @@ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + vol-4d826724 + 10 + + us-east-1a + creating + 2008-05-07T11:51:50.000Z + diff --git test/compute/fixtures/ec2/delete_volume.xml test/compute/fixtures/ec2/delete_volume.xml new file mode 100644 index 0000000..9435efb --- /dev/null +++ test/compute/fixtures/ec2/delete_volume.xml @@ -0,0 +1,4 @@ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + diff --git test/compute/fixtures/ec2/detach_volume.xml test/compute/fixtures/ec2/detach_volume.xml new file mode 100644 index 0000000..f254506 --- /dev/null +++ test/compute/fixtures/ec2/detach_volume.xml @@ -0,0 +1,8 @@ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + vol-4d826724 + i-6058a509 + /dev/sdh + detaching + 2008-05-08T11:51:50.000Z + diff --git test/compute/test_cloudstack.py test/compute/test_cloudstack.py index e5ff5d9..dd3b625 100644 --- test/compute/test_cloudstack.py +++ test/compute/test_cloudstack.py @@ -75,16 +75,16 @@ class CloudStackNodeDriverTest(unittest.TestCase, TestCaseMixin): self.assertEquals(10, diskOffering.size) - def test_ex_create_volume(self): + def test_create_volume(self): volumeName = 'vol-0' location = self.driver.list_locations()[0] - volume = self.driver.ex_create_volume( - volumeName, location, size=10) + volume = self.driver.create_volume(10, volumeName, location) self.assertEquals(volumeName, volume.name) + self.assertEquals(10, volume.size) - def test_ex_create_volume_no_noncustomized_offering_with_size(self): + def test_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.""" @@ -93,36 +93,61 @@ class CloudStackNodeDriverTest(unittest.TestCase, TestCaseMixin): self.assertRaises( LibcloudError, - self.driver.ex_create_volume, - 'vol-0', location, 11) + self.driver.create_volume, + 11, 'vol-0', location) - def test_ex_create_volume_with_custom_disk_size_offering(self): + def test_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) + volume = self.driver.create_volume( + 11, volumeName, location) - self.assertEquals(volumeName, volume.name) + self.assertEquals(volumeName, volume.extra['name']) - def test_ex_attach_volume(self): + def test_attach(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) + volume = self.driver.create_volume( + 10, volumeName, location) - attachReturnVal = self.driver.ex_attach_volume(volume, node) + attachReturnVal = self.driver.attach(volume, node) self.assertTrue(attachReturnVal) + def test_detach(self): + volumeName = 'vol-0' + location = self.driver.list_locations()[0] + + volume = self.driver.create_volume( + 10, volumeName, location) + + detachReturnVal = self.driver.detach(volume) + + self.assertTrue(detachReturnVal) + + def test_destroy_volume(self): + + node = self.driver.list_nodes()[0] + volumeName = 'vol-0' + location = self.driver.list_locations()[0] + + volume = self.driver.create_volume( + 10, volumeName, location) + + destroyReturnVal = self.driver.destroy_volume(volume) + + self.assertTrue(destroyReturnVal) + + class CloudStackMockHttp(MockHttpTestCase): fixtures = ComputeFileFixtures('cloudstack') fixture_tag = 'default' diff --git test/compute/test_ec2.py test/compute/test_ec2.py index d7cf895..75da2a8 100644 --- test/compute/test_ec2.py +++ test/compute/test_ec2.py @@ -22,6 +22,7 @@ from libcloud.compute.drivers.ec2 import NimbusNodeDriver, EucNodeDriver from libcloud.compute.drivers.ec2 import EC2APNENodeDriver from libcloud.compute.drivers.ec2 import IdempotentParamError from libcloud.compute.base import Node, NodeImage, NodeSize, NodeLocation +from libcloud.compute.base import StorageVolume from test import MockHttp, LibcloudTestCase from test.compute import TestCaseMixin @@ -294,6 +295,50 @@ class EC2Tests(LibcloudTestCase, TestCaseMixin): self.assertTrue(result) + def test_create_volume(self): + + location = self.driver.list_locations()[0] + vol = self.driver.create_volume(10, 'vol', location) + + self.assertEquals(10, vol.size) + self.assertEquals('vol', vol.name) + + + def test_destroy_volume(self): + vol = StorageVolume( + id='vol-4282672b', name='test', + size=10, driver=self.driver) + + retValue = self.driver.destroy_volume(vol) + + self.assertTrue(retValue) + + + def test_attach(self): + vol = StorageVolume( + id='vol-4282672b', name='test', + size=10, driver=self.driver) + + node = Node('i-4382922a', None, None, None, None, self.driver) + + retValue = self.driver.attach(node, vol, '/dev/sdh') + + self.assertTrue(retValue) + + + def test_detach(self): + vol = StorageVolume( + id='vol-4282672b', name='test', + size=10, driver=self.driver) + + retValue = self.driver.detach(vol) + + self.assertTrue(retValue) + + + + + class EC2MockHttp(MockHttp): fixtures = ComputeFileFixtures('ec2') @@ -378,6 +423,22 @@ class EC2MockHttp(MockHttp): body = self.fixtures.load('create_tags.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _CreateVolume(self, method, url, body, headers): + body = self.fixtures.load('create_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DeleteVolume(self, method, url, body, headers): + body = self.fixtures.load('delete_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _AttachVolume(self, method, url, body, headers): + body = self.fixtures.load('attach_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _DetachVolume(self, method, url, body, headers): + body = self.fixtures.load('detach_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + class EucMockHttp(EC2MockHttp): fixtures = ComputeFileFixtures('ec2') @@ -479,7 +540,7 @@ class NimbusTests(EC2Tests): # Nimbus doesn't support creating tags so this one should be a # passthrough node = self.driver.list_nodes()[0] - self.driver.ex_create_tags(node=node, tags={'foo': 'bar'}) + self.driver.ex_create_tags(resource=node, tags={'foo': 'bar'}) self.assertExecutedMethodCount(0)