Index: trunk/libcloud/test/compute/test_ibm_sce.py =================================================================== --- trunk/libcloud/test/compute/test_ibm_sce.py (revision 1374739) +++ trunk/libcloud/test/compute/test_ibm_sce.py (working copy) @@ -26,7 +26,7 @@ class IBMTests(unittest.TestCase, TestCaseMixin): """ - Tests the IBM Developer Cloud driver. + Tests the IBM SmartCloud Enterprise driver. """ def setUp(self): @@ -117,7 +117,7 @@ self.fail('test should have thrown') def test_destroy_node(self): - # Delete existant node + # Delete existent node nodes = self.driver.list_nodes() # retrieves 3 nodes self.assertEquals(len(nodes), 3) IBMMockHttp.type = 'DELETE' @@ -155,6 +155,71 @@ else: self.fail('test should have thrown') + def test_list_volumes(self): + ret = self.driver.list_volumes() + self.assertEqual(len(ret), 1) + self.assertEqual(ret[0].name, 'libcloudvol') + self.assertEqual(ret[0].extra['location'], '141') + self.assertEqual(ret[0].size, '2048') + self.assertEqual(ret[0].id, '39281') + + def test_attach_volume(self): + vols = self.driver.list_volumes() + nodes = self.driver.list_nodes() + IBMMockHttp.type = 'ATTACH' + ret = self.driver.attach_volume(nodes[0], vols[0]) + self.assertTrue(ret) + + def test_create_volume(self): + IBMMockHttp.type = 'CREATE' + ret = self.driver.create_volume('256', + 'test-volume', + location='141', + format='RAW', + offering_id='20001208') + self.assertEqual(ret.id, '39293') + self.assertEqual(ret.size, '256') + self.assertEqual(ret.name, 'test-volume') + self.assertEqual(ret.extra['location'], '141') + + def test_destroy_volume(self): + vols = self.driver.list_volumes() + IBMMockHttp.type = 'DESTROY' + ret = self.driver.destroy_volume(vols[0]) + self.assertTrue(ret) + + def test_detach_volume(self): + nodes = self.driver.list_nodes() + vols = self.driver.list_volumes() + IBMMockHttp.type = 'DETACH' + ret = self.driver.detach_volume(nodes[0], vols[0]) + self.assertTrue(ret) + + def test_ex_allocate_address(self): + IBMMockHttp.type = 'ALLOCATE' + ret = self.driver.ex_allocate_address('141', '20001223') + self.assertEqual(ret.id, '292795') + self.assertEqual(ret.state, '0') + self.assertEqual(ret.options['location'], '141') + + def test_ex_delete_address(self): + IBMMockHttp.type = 'DELETE' + ret = self.driver.ex_delete_address('292795') + self.assertTrue(ret) + + def test_ex_list_addresses(self): + ret = self.driver.ex_list_addresses() + self.assertEqual(ret[0].ip, '170.225.160.218') + self.assertEqual(ret[0].options['location'], '141') + self.assertEqual(ret[0].id, '292795') + self.assertEqual(ret[0].state, '2') + + def test_ex_list_storage_offerings(self): + ret = self.driver.ex_list_storage_offerings() + self.assertEqual(ret[0].name, 'Small') + self.assertEqual(ret[0].location, '61') + self.assertEqual(ret[0].id, '20001208') + class IBMMockHttp(MockHttp): fixtures = ComputeFileFixtures('ibm_sce') @@ -198,6 +263,42 @@ def _computecloud_enterprise_api_rest_20100331_instances_CREATE_INVALID(self, method, url, body, headers): return (412, 'Error 412: No DataCenter with id: 3', {}, 'Precondition Failed') + def _computecloud_enterprise_api_rest_20100331_storage(self, method, url, body, headers): + body = self.fixtures.load('list_volumes.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_instances_26557_ATTACH(self, method, url, body, headers): + body = self.fixtures.load('attach_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_storage_CREATE(self, method, url, body, headers): + body = self.fixtures.load('create_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_storage_39281_DESTROY(self, method, url, body, headers): + body = self.fixtures.load('destroy_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_instances_26557_DETACH(self, method, url, body, headers): + body = self.fixtures.load('detach_volume.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_addresses_ALLOCATE(self, method, url, body, headers): + body = self.fixtures.load('allocate_address.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_addresses_292795_DELETE(self, method, url, body, headers): + body = self.fixtures.load('delete_address.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_addresses(self, method, url, body, headers): + body = self.fixtures.load('list_addresses.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _computecloud_enterprise_api_rest_20100331_offerings_storage(self, method, url, body, headers): + body = self.fixtures.load('list_storage_offerings.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + # This is only to accomodate the response tests built into test\__init__.py def _computecloud_enterprise_api_rest_20100331_instances_26557(self, method, url, body, headers): if method == 'DELETE': Index: trunk/libcloud/test/compute/fixtures/ibm_sce/destroy_volume.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/destroy_volume.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/destroy_volume.xml (working copy) @@ -0,0 +1 @@ + \ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/destroy_volume.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/attach_volume.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/attach_volume.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/attach_volume.xml (working copy) @@ -0,0 +1 @@ + \ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/attach_volume.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/delete_address.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/delete_address.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/delete_address.xml (working copy) @@ -0,0 +1 @@ + \ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/delete_address.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/detach_volume.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/detach_volume.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/detach_volume.xml (working copy) @@ -0,0 +1 @@ + \ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/detach_volume.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/list_volumes.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/list_volumes.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/list_volumes.xml (working copy) @@ -0,0 +1 @@ +39281141200012100user@domain.comlibcloudvolraw204842012-08-19T13:46:50.000Z2011-08-12T00:00:00.000ZUSD0.001CNT020 \ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/list_volumes.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/allocate_address.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/allocate_address.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/allocate_address.xml (working copy) @@ -0,0 +1 @@ +
RESERVED14129279520001223PRIMARY0user@domain.com
\ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/allocate_address.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/create_volume.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/create_volume.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/create_volume.xml (working copy) @@ -0,0 +1 @@ +3929314120001208user@domain.comtest-volumeraw25602012-08-20T12:25:02.792Z2011-08-12T00:00:00.000ZUSD0.001CNT020 \ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/create_volume.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/list_storage_offerings.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/list_storage_offerings.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/list_storage_offerings.xml (working copy) @@ -0,0 +1 @@ +2000120861Small256256EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000120882Small256256EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000120961Medium512512EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000120982Medium512512EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000120841Small256256EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000120941Medium512512EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001208121Small256256EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001209121Medium512512EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000121061Large20482048EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000121082Large20482048EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR2000121041Large20482048EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001210121Large20482048EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001208101Small256256EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001209101Medium512512EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001208141Small256256EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001209141Medium512512EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001210101Large20482048EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR20001210141Large20482048EXT3RAW2011-08-12T00:00:00.000ZUSD0.00001UHR \ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/list_storage_offerings.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/test/compute/fixtures/ibm_sce/list_addresses.xml =================================================================== --- trunk/libcloud/test/compute/fixtures/ibm_sce/list_addresses.xml (revision 0) +++ trunk/libcloud/test/compute/fixtures/ibm_sce/list_addresses.xml (working copy) @@ -0,0 +1 @@ +
RESERVED170.225.160.218vhost021814129279520001223PRIMARY2user@dmain.com
\ No newline at end of file Property changes on: trunk/libcloud/test/compute/fixtures/ibm_sce/list_addresses.xml ___________________________________________________________________ Added: svn:mime-type ## -0,0 +1 ## +text/plain Index: trunk/libcloud/compute/drivers/ibm_sce.py =================================================================== --- trunk/libcloud/compute/drivers/ibm_sce.py (revision 1374739) +++ trunk/libcloud/compute/drivers/ibm_sce.py (working copy) @@ -17,20 +17,22 @@ Driver for IBM SmartCloud Enterprise Formerly known as: -- IBM Developer Cloud. +- IBM Developer Cloud +- IBM Smart Business Development and Test on the IBM Cloud - IBM SmartBusiness Cloud """ import base64 +import time from libcloud.utils.py3 import urlencode from libcloud.utils.py3 import b from libcloud.common.base import XmlResponse, ConnectionUserAndKey -from libcloud.common.types import InvalidCredsError +from libcloud.common.types import InvalidCredsError, LibcloudError from libcloud.compute.types import NodeState, Provider from libcloud.compute.base import NodeDriver, Node, NodeImage, \ - NodeSize, NodeLocation, NodeAuthSSHKey + NodeSize, NodeLocation, NodeAuthSSHKey, StorageVolume HOST = 'www-147.ibm.com' REST_BASE = '/computecloud/enterprise/api/rest/20100331' @@ -51,7 +53,7 @@ class IBMConnection(ConnectionUserAndKey): """ - Connection class for the IBM Developer Cloud driver + Connection class for the IBM SmartCloud Enterprise driver """ host = HOST @@ -69,6 +71,76 @@ return urlencode(data) +class IBMNodeLocation(NodeLocation): + """ + Extends the base LibCloud NodeLocation to contain additional attributes + """ + def __init__(self, id, name, country, driver, extra=None): + self.id = str(id) + self.name = name + self.country = country + self.driver = driver + self.extra = extra + + def __repr__(self): + return (('').format( + self.id, self.name, self.country, self.driver.name, self.extra)) + + +class VolumeState(object): + """ + The SCE specific states for a storage volume + """ + NEW = '0' + CREATING = '1' + DELETING = '2' + DELETED = '3' + DETACHED = '4' + ATTACHED = '5' + FAILED = '6' + DELETE_PENDING = '7' + BEING_CLONED = '8' + CLONING = '9' + ATTACHING = '10' + DETACHING = '11' + ATTACHIED = '12' + IMPORTING = '13' + TRANSFER_RETRYING = '14' + + +class VolumeOffering(object): + """ + An SCE specific storage volume offering class. + The volume offering ID is needed to create a volume. + Volume offering IDs are different for each data center. + """ + def __init__(self, id, name, location, extra=None): + self.id = id + self.location = location + self.name = name + self.extra = extra + + def __repr__(self): + return (('').format( + self.id, self.location, self.name, self.extra)) + + +class Address(object): + """ + A reserved IP address that can be attached to an instance. + Properties: id, ip, state, options(location, type, created_time, state, hostname, instance_ids, vlan, owner, + mode, offering_id) + """ + def __init__(self, id, ip, state, options): + self.id = id + self.ip = ip + self.state = state + self.options = options + + def __repr__(self): + return (''.format( + self.id, self.ip, self.state, self.options)) + class IBMNodeDriver(NodeDriver): """ Node driver for IBM SmartCloud Enterprise @@ -94,13 +166,15 @@ 12: NodeState.PENDING, # Deprovision Pending 13: NodeState.PENDING, # Restart Pending 14: NodeState.PENDING, # Attaching - 15: NodeState.PENDING, # Detaching + 15: NodeState.PENDING, # Detaching } def create_node(self, **kwargs): """ - Creates a node in the IBM Developer Cloud. + Creates a node in the IBM SmartCloud Enterprise. + See L{NodeDriver.create_node} for more keyword args. + @inherits: L{NodeDriver.create_node} @keyword auth: Name of the pubkey to use. When constructing @@ -131,8 +205,9 @@ data.update({'publicKey': kwargs['auth'].pubkey}) if 'ex_configurationData' in kwargs: configurationData = kwargs['ex_configurationData'] - for key in configurationData.keys(): - data.update({key: configurationData.get(key)}) + if configurationData: + for key in configurationData.keys(): + data.update({key: configurationData.get(key)}) # Send request! resp = self.connection.request( @@ -142,12 +217,155 @@ data=data).object return self._to_nodes(resp)[0] + def create_volume(self, size, name, location, **kwargs): + """ + Create a new block storage volume (virtual disk) + + @param size: Size of volume in gigabytes (required). + Find out the possible sizes from the offerings/storage REST interface + @type size: C{int} + + @keyword name: Name of the volume to be created (required) + @type name: C{str} + + @keyword location: Which data center to create a volume in. If + empty, it will fail for IBM SmartCloud Enterprise + (required) + @type location: L{NodeLocation} + + @keyword snapshot: Not supported for IBM SmartCloud Enterprise + @type snapshot: C{str} + + @keyword kwargs.format: Either RAW or EXT3 for IBM SmartCloud Enterprise (optional) + @type kwargs.format: C{str} + + @keyword kwargs.offering_id: The storage offering ID for IBM SmartCloud Enterprise + Find this from the REST interface storage/offerings. (optional) + @type kwargs.offering_id: C{str} + + @keyword kwargs.source_disk_id: If cloning a volume, the storage disk to make a copy from (optional) + @type kwargs.source_disk_id: C{str} + + @keyword kwargs.storage_area_id: The id of the storage availability area to create the volume in (optional) + @type kwargs.storage_area_id: C{str} + + @keyword kwargs.target_location_id: If cloning a volume, the storage disk to make a copy from (optional) + @type kwargs.target_location_id: C{str} + + @return: The newly created L{StorageVolume}. + """ + data = {} + data.update({'name': name}) + data.update({'size': size}) + data.update({'location': location}) + if (('format' in kwargs) and (kwargs['format'] is not None)): + data.update({'format': kwargs['format']}) + if (('offering_id' in kwargs) and (kwargs['offering_id'] is not None)): + data.update({'offeringID': kwargs['offering_id']}) + if (('storage_area_id' in kwargs) and (kwargs['storage_area_id'] is not None)): + data.update({'storageAreaID': kwargs['storage_area_id']}) + if 'source_disk_id' in kwargs: + data.update({'sourceDiskID': kwargs['source_disk_id']}) + data.update({'type': 'clone'}) + if 'target_location_id' in kwargs: + data.update({'targetLocationID': kwargs['target_location_id']}) + resp = self.connection.request(action=REST_BASE + '/storage', + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + method='POST', + data=data).object + return self._to_volumes(resp)[0] + + def create_image(self, name, description=None, **kwargs): + """ + Create a new node image from an existing volume or image. + + @param name: Name of the image to be created (required) + @type name: C{str} + + @param description: Description of the image to be created (optional) + @type description: L{str} + + @keyword kwarge.image_id: The ID of the source image if cloning the image + @type kwargs.image_id: C{str} + + @keyword kwargs.volume_id: The ID of the storage volume if importing the image + @type kwargs.volume_id: C{str} + + @return: The newly created L{NodeImage}. + """ + data = {} + data.update({'name': name}) + if description is not None: + data.update({'description': description}) + if (('image_id' in kwargs) and (kwargs['image_id'] is not None)): + data.update({'imageId': kwargs['image_id']}) + if (('volume_id' in kwargs) and (kwargs['volume_id'] is not None)): + data.update({'volumeId': kwargs['volume_id']}) + resp = self.connection.request(action=REST_BASE + '/offerings/image', + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + method='POST', + data=data).object + return self._to_images(resp)[0] + def destroy_node(self, node): url = REST_BASE + '/instances/%s' % (node.id) status = int(self.connection.request(action=url, method='DELETE').status) return status == 200 + def destroy_volume(self, volume): + """ + Destroys a storage volume. + + @param volume: Volume to be destroyed + @type volume: L{StorageVolume} + + @return: C{bool} + """ + url = REST_BASE + '/storage/%s' % (volume.id) + status = int(self.connection.request(action=url, + method='DELETE').status) + return status == 200 + + def attach_volume(self, node, volume): + """ + Attaches volume to node. + + @param node: Node to attach volume to + @type node: L{Node} + + @param volume: Volume to attach + @type volume: L{StorageVolume} + + @return: C{bool} + """ + url = REST_BASE + '/instances/%s' % (node.id) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + data = {'storageID': volume.id, 'type': 'attach'} + resp = self.connection.request(action=url, + method='PUT', + headers=headers, + data=data) + return int(resp.status) == 200 + + def detach_volume(self, node, volume): + """ + Detaches a volume from a node. + + @param volume: Volume to be detached + @type volume: L{StorageVolume} + + @returns C{bool} + """ + url = REST_BASE + '/instances/%s' % (node.id) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + data = {'storageID': volume.id, 'type': 'detach'} + resp = self.connection.request(action=url, + method='PUT', + headers=headers, + data=data) + return int(resp.status) == 200 + def reboot_node(self, node): url = REST_BASE + '/instances/%s' % (node.id) headers = {'Content-Type': 'application/x-www-form-urlencoded'} @@ -167,10 +385,21 @@ return self._to_images( self.connection.request(REST_BASE + '/offerings/image').object) + def list_volumes(self): + """ + List storage volumes. + + @return: C{list} of L{StorageVolume} objects + """ + return self._to_volumes( + self.connection.request(REST_BASE + '/storage').object) + def list_sizes(self, location=None): """ - Returns a generic list of sizes. See list_images() for a - list of supported sizes for specific images. + Returns a generic list of sizes. See list_images() for a list of supported + sizes for specific images. In particular, you need to have a size that + matches the architecture (32-bit vs 64-bit) of the virtual machine image + operating system. @inherits: L{NodeDriver.list_sizes} """ @@ -195,9 +424,125 @@ None, None, None, None, self.connection.driver)] def list_locations(self): + """ + List the data center locations + """ return self._to_locations( self.connection.request(REST_BASE + '/locations').object) + def ex_list_storage_offerings(self): + """ + List the storage center offerings + """ + return self._to_volume_offerings( + self.connection.request(REST_BASE + '/offerings/storage').object) + + def ex_allocate_address(self, location_id, offering_id, vlan_id=None): + """ + Allocate a new reserved IP address + + @param location_id: Target data center + @type location_id: L{str} + @param offering_id: Offering ID for address to create + @type offering_id: L{str} + @param vlan_id: ID of target VLAN + @type vlan_id: L{str} + @return: L{Address} object + """ + url = REST_BASE + '/addresses' + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + data = {'location': location_id, 'offeringID': offering_id} + if vlan_id is not None: + data.update({'vlanID': vlan_id}) + resp = self.connection.request(action=url, + method='POST', + headers=headers, + data=data).object + return self._to_addresses(resp)[0] + + def ex_list_addresses(self, resource_id=None): + """ + List the reserved IP addresses + + @param resource_id: If this is supplied only a single address will be returned (optional) + @type resource_id: L{str} + """ + url = REST_BASE + '/addresses' + if resource_id: + url += '/' + resource_id + return self._to_addresses(self.connection.request(url).object) + + def ex_copy_to(self, image, volume): + """ + Copies a node image to a storage volume + + @param image: source image to copy + @type image: L{NodeImage} + + @param volume: Target storage volume to copy to + @type volume: L{StorageVolume} + + @return: C{bool} The success of the operation + """ + url = REST_BASE + '/storage/%s' % (volume.id) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + data = {'imageId': image.id} + resp = self.connection.request(action=url, + method='PUT', + headers=headers, + data=data) + return int(resp.status) == 200 + + def ex_delete_address(self, resource_id): + """ + Delete a reserved IP address + + @param resource_id: The address to delete (required) + @type resource_id: L{str} + """ + url = REST_BASE + '/addresses/' + resource_id + status = int(self.connection.request(action=url, + method='DELETE').status) + return status == 200 + + def ex_wait_storage_state(self, volume, state=VolumeState.DETACHED, wait_period=60, timeout=1200): + """ + Block until storage volume state changes to the given value + + @param volume: Storage volume. + @type node: C{StorageVolume} + + @param state: The target state to wait for + @type state: C{int} + + @param wait_period: How many seconds to between each loop + iteration (default is 3) + @type wait_period: C{int} + + @param timeout: How many seconds to wait before timing out + (default is 1200) + @type timeout: C{int} + + @return: C{StorageVolume} + """ + start = time.time() + end = start + timeout + + while time.time() < end: + volumes = self.list_volumes() + volumes = list([v for v in volumes if v.uuid == volume.uuid]) + + print('ex_wait_storage_state, len(volumes): {0}, volumes[0]: {1}, extra[0]: {2}, wait_period: {3}, '\ + 'target state: {4}'.format(len(volumes), volumes[0], volumes[0].extra, wait_period, state)) + if (len(volumes) == 1 and volumes[0].extra['state'] == state): + return volumes[0] + else: + time.sleep(wait_period) + continue + + raise LibcloudError(value='Timed out after {0} seconds'.format(timeout), + driver=self) + def _to_nodes(self, object): return [self._to_node(instance) for instance in object.findall('Instance')] @@ -210,16 +555,16 @@ public_ips.append(ip) return Node( - id=instance.findtext('ID'), - name=instance.findtext('Name'), - state=self.NODE_STATE_MAP[int(instance.findtext('Status'))], - public_ips=public_ips, - private_ips=[], - driver=self.connection.driver + id=instance.findtext('ID'), + name=instance.findtext('Name'), + state=self.NODE_STATE_MAP[int(instance.findtext('Status'))], + public_ips=public_ips, + private_ips=[], + driver=self.connection.driver ) def _to_images(self, object): - #Converts data retrieved from SCE /offerings/image REST call to a NodeImage + # Converts data retrieved from SCE /offerings/image REST call to a NodeImage return [self._to_image(image) for image in object.findall('Image')] def _to_image(self, image): @@ -248,7 +593,7 @@ 'platform': platform, 'description': description, 'documentation': documentation, - 'supportedInstanceTypes': nodeSizes + 'node_sizes': nodeSizes } ) @@ -259,11 +604,30 @@ def _to_location(self, location): # Converts an SCE Location object to a Libcloud NodeLocation object name_text = location.findtext('Name') + description = location.findtext('Description') + state = location.findtext('State') (nameVal, separator, countryVal) = name_text.partition(',') - return NodeLocation(id=location.findtext('ID'), - name=nameVal, - country=countryVal.strip(), - driver=self.connection.driver) + capabiltyElements = location.findall('Capabilities/Capability') + capabilities = {} + for elem in capabiltyElements: + capabilityID = elem.attrib['id'] + entryElements = elem.findall('Entry') + entries = [] + for entryElem in entryElements: + key = entryElem.attrib['key'] + valueElements = elem.findall('Value') + values = [] + for valueElem in valueElements: + values.append(valueElem.text) + entry = {'key': key, 'values': values} + entries.append(entry) + capabilities[capabilityID] = entries + extra = {'description': description, 'state': state, 'capabilities': capabilities} + return IBMNodeLocation(id=location.findtext('ID'), + name=nameVal, + country=countryVal.strip(), + driver=self.connection.driver, + extra=extra) def _to_node_sizes(self, object): # Converts SCE SupportedInstanceTypes object to @@ -273,10 +637,67 @@ def _to_node_size(self, object): # Converts to an SCE InstanceType to a Libcloud NodeSize - return NodeSize(object.findtext('ID'), - object.findtext('Label'), + return NodeSize(object.findtext('ID'), + object.findtext('Label'), None, None, None, object.findtext('Price/Rate'), self.connection.driver) + + def _to_volumes(self, object): + return [self._to_volume(iType) for iType in + object.findall('Volume')] + + def _to_volume(self, object): + # Converts an SCE Volume to a Libcloud StorageVolume + extra = {'state': object.findtext('State'), + 'location': object.findtext('Location'), + 'instanceID': object.findtext('instanceID'), + 'owner': object.findtext('Owner'), + 'format': object.findtext('Format'), + 'createdTime': object.findtext('CreatedTime'), + 'storageAreaID': object.findtext('StorageArea/ID')} + return StorageVolume(object.findtext('ID'), + object.findtext('Name'), + object.findtext('Size'), + self.connection.driver, + extra) + + def _to_volume_offerings(self, object): + return [self._to_volume_offering(iType) for iType in + object.findall('Offerings')] + + def _to_volume_offering(self, object): + # Converts an SCE DescribeVolumeOfferingsResponse/Offerings XML object to an SCE VolumeOffering + extra = {'label': object.findtext('Label'), + 'supported_sizes': object.findtext('SupportedSizes'), + 'formats': object.findall('SupportedFormats/Format/ID'), + 'price': object.findall('Price')} + return VolumeOffering(object.findtext('ID'), + object.findtext('Name'), + object.findtext('Location'), + extra) + + def _to_addresses(self, object): + # Converts an SCE DescribeAddressesResponse XML object to a list of Address objects + return [self._to_address(iType) for iType in + object.findall('Address')] + + def _to_address(self, object): + # Converts an SCE DescribeAddressesResponse/Address XML object to an Address object + extra = {'location': object.findtext('Location'), + 'type': object.findtext('Label'), + 'created_time': object.findtext('SupportedSizes'), + 'hostname': object.findtext('Hostname'), + 'instance_ids': object.findtext('InstanceID'), + 'vlan': object.findtext('VLAN'), + 'owner': object.findtext('owner'), + 'mode': object.findtext('Mode'), + 'offering_id': object.findtext('OfferingID')} + return Address(object.findtext('ID'), + object.findtext('IP'), + object.findtext('State'), + extra) + #Properties: id, ip, state, location, options(type, created_time, hostname, instance_ids, vlan, owner, + #mode, price, offering_id)