diff --git libcloud/common/gandi.py libcloud/common/gandi.py index c4a6fdf..3f2a3a8 100644 --- libcloud/common/gandi.py +++ libcloud/common/gandi.py @@ -26,8 +26,8 @@ from libcloud.utils.py3 import b from libcloud.common.base import ConnectionKey # Global constants -API_VERSION = '2.0' -API_PREFIX = "https://rpc.gandi.net/xmlrpc/%s/" % API_VERSION + +API_URL = "https://rpc.gandi.net/xmlrpc/" DEFAULT_TIMEOUT = 600 # operation pooling max seconds DEFAULT_INTERVAL = 20 # seconds between 2 operation.info @@ -57,13 +57,13 @@ class GandiProxy(xmlrpclib.ServerProxy): def __init__(self, user_agent, verbose=0): cls = self.transportCls[0] - if API_PREFIX.startswith("https://"): + if API_URL.startswith("https://"): cls = self.transportCls[1] t = cls(use_datetime=0) t.user_agent = user_agent xmlrpclib.ServerProxy.__init__( self, - uri="%s" % (API_PREFIX), + uri=API_URL, transport=t, verbose=verbose, allow_none=True diff --git libcloud/compute/drivers/gandi.py libcloud/compute/drivers/gandi.py index ac6dde6..01f7473 100644 --- libcloud/compute/drivers/gandi.py +++ libcloud/compute/drivers/gandi.py @@ -19,10 +19,11 @@ import sys from datetime import datetime from libcloud.common.gandi import BaseGandiDriver, GandiException, \ - NetworkInterface, IPAddress, Disk + NetworkInterface, IPAddress from libcloud.compute.types import NodeState, Provider from libcloud.compute.base import Node, NodeDriver -from libcloud.compute.base import NodeSize, NodeImage, NodeLocation +from libcloud.compute.base import NodeSize, NodeImage, \ + NodeLocation, StorageVolume NODE_STATE_MAP = { @@ -52,15 +53,21 @@ class GandiNodeDriver(BaseGandiDriver, NodeDriver): # TODO : which features to enable ? features = {} - def _node_info(self, id): + def _resource_info(self, type, id): try: - obj = self.connection.request('vm.info', int(id)) + obj = self.connection.request('%s.info' % type, int(id)) return obj except Exception: e = sys.exc_info()[1] raise GandiException(1003, e) return None + def _node_info(self, id): + return self._resource_info('vm', id) + + def _volume_info(self, id): + return self._resource_info('disk', id) + # Generic methods for driver def _to_node(self, vm): return Node( @@ -83,6 +90,18 @@ class GandiNodeDriver(BaseGandiDriver, NodeDriver): def _to_nodes(self, vms): return [self._to_node(v) for v in vms] + def _to_volume(self, disk): + extra = {'can_snapshot': disk['can_snapshot']} + return StorageVolume( + id=disk['id'], + name=disk['name'], + size=int(disk['size']), + driver=self, + extra=extra) + + def _to_volumes(self, disks): + return [self._to_volume(d) for d in disks] + def list_nodes(self): vms = self.connection.request('vm.list') ips = self.connection.request('ip.list') @@ -100,7 +119,7 @@ class GandiNodeDriver(BaseGandiDriver, NodeDriver): def reboot_node(self, node): op = self.connection.request('vm.reboot', int(node.id)) self._wait_operation(op['id']) - vm = self.connection.request('vm.info', int(node.id)) + vm = self._node_info(int(node.id)) if vm['state'] == 'running': return True return False @@ -276,6 +295,49 @@ class GandiNodeDriver(BaseGandiDriver, NodeDriver): res = self.connection.request("datacenter.list") return [self._to_loc(l) for l in res] + def list_volumes(self): + """List all volumes""" + res = self.connection.request('disk.list', {}) + return self._to_volumes(res) + + def create_volume(self, size, name, location=None, snapshot=None): + disk_param = { + 'name': name, + 'size': int(size), + 'datacenter_id': int(location.id) + } + if snapshot: + op = self.connection.request('disk.create_from', + disk_param, int(snapshot.id)) + else: + op = self.connection.request('disk.create', disk_param) + if self._wait_operation(op['id']): + disk = self._volume_info(op['disk_id']) + return self._to_volume(disk) + return None + + def attach_volume(self, node, volume, device=None): + """Attach a volume to a node""" + op = self.connection.request('vm.disk_attach', + int(node.id), int(volume.id)) + if self._wait_operation(op['id']): + return True + return False + + def detach_volume(self, node, volume): + """Detach a volume from a node""" + op = self.connection.request('vm.disk_detach', + int(node.id), int(volume.id)) + if self._wait_operation(op['id']): + return True + return False + + def destroy_volume(self, volume): + op = self.connection.request('disk.delete', int(volume.id)) + if self._wait_operation(op['id']): + return True + return False + def _to_iface(self, iface): ips = [] for ip in iface.get('ips', []): @@ -312,47 +374,10 @@ class GandiNodeDriver(BaseGandiDriver, NodeDriver): ifaces = self.connection.request('iface.list') ips = self.connection.request('ip.list') for iface in ifaces: - iface['ips'] = list(filter(lambda i: i['iface_id'] == iface['id'], ips)) + iface['ips'] = \ + list(filter(lambda i: i['iface_id'] == iface['id'], ips)) return self._to_ifaces(ifaces) - def _to_disk(self, element): - disk = Disk( - id=element['id'], - state=NODE_STATE_MAP.get( - element['state'], - NodeState.UNKNOWN - ), - name=element['name'], - driver=self.connection.driver, - size=element['size'], - extra={'can_snapshot': element['can_snapshot']} - ) - return disk - - def _to_disks(self, elements): - return [self._to_disk(el) for el in elements] - - def ex_list_disks(self): - """Specific method to list all disk""" - res = self.connection.request('disk.list', {}) - return self._to_disks(res) - - def ex_node_attach_disk(self, node, disk): - """Specific method to attach a disk to a node""" - op = self.connection.request('vm.disk_attach', - int(node.id), int(disk.id)) - if self._wait_operation(op['id']): - return True - return False - - def ex_node_detach_disk(self, node, disk): - """Specific method to detach a disk from a node""" - op = self.connection.request('vm.disk_detach', - int(node.id), int(disk.id)) - if self._wait_operation(op['id']): - return True - return False - def ex_node_attach_interface(self, node, iface): """Specific method to attach an interface to a node""" op = self.connection.request('vm.iface_attach', diff --git test/compute/fixtures/gandi/disk_create.xml test/compute/fixtures/gandi/disk_create.xml new file mode 100644 index 0000000..1fffb08 --- /dev/null +++ test/compute/fixtures/gandi/disk_create.xml @@ -0,0 +1,49 @@ + + + + + + +iface_id + + +date_updated +20120629T11:48:20 + + +vm_id + + +date_start + + +disk_id +1263 + + +source +AB3917-GANDI + + +step +DONE + + +ip_id + + +date_created +20120629T11:48:20 + + +type +disk_create + + +id +10895 + + + + + \ No newline at end of file diff --git test/compute/fixtures/gandi/disk_delete.xml test/compute/fixtures/gandi/disk_delete.xml new file mode 100644 index 0000000..1b39803 --- /dev/null +++ test/compute/fixtures/gandi/disk_delete.xml @@ -0,0 +1,49 @@ + + + + + + +iface_id + + +date_updated +20120629T11:47:06 + + +vm_id + + +date_start + + +disk_id +1262 + + +source +AB3917-GANDI + + +step +WAIT + + +ip_id + + +date_created +20120629T11:47:06 + + +type +disk_delete + + +id +10894 + + + + + \ No newline at end of file diff --git test/compute/fixtures/gandi/disk_info.xml test/compute/fixtures/gandi/disk_info.xml new file mode 100644 index 0000000..390d4a4 --- /dev/null +++ test/compute/fixtures/gandi/disk_info.xml @@ -0,0 +1,73 @@ + + + + + + +datacenter_id +1 + + +name +libcloud + + +snapshot_profile + + +kernel_version + + +can_snapshot +1 + + +kernel_cmdline + + +visibility +private + + +label + + +vms_id + + + + +source + + +state +created + + +is_boot_disk +0 + + +date_updated +20120629T11:49:00 + + +date_created +20120629T11:48:20 + + +type +data + + +id +1263 + + +size +1024 + + + + + \ No newline at end of file diff --git test/compute/test_gandi.py test/compute/test_gandi.py index d763eff..7b8fd66 100644 --- test/compute/test_gandi.py +++ test/compute/test_gandi.py @@ -109,10 +109,36 @@ class GandiTests(unittest.TestCase): password=passwd, image=img, location=loc, size=size) self.assertEqual(node.name, self.node_name) - def test_ex_list_disks(self): - disks = self.driver.ex_list_disks() + def test_create_volume(self): + loc = list(filter(lambda x: 'france' in x.country.lower(), + self.driver.list_locations()))[0] + volume = self.driver.create_volume( + size=1024, name='libcloud', location=loc) + self.assertEqual(volume.name, 'libcloud') + self.assertEqual(volume.size, 1024) + + def test_list_volumes(self): + disks = self.driver.list_volumes() self.assertTrue(len(disks) > 0) + def test_destroy_volume(self): + volumes = self.driver.list_volumes() + test_vol = list(filter(lambda x: x.name == 'test_disk', + volumes))[0] + self.assertTrue(self.driver.destroy_volume(test_vol)) + + def test_attach_volume(self): + disks = self.driver.list_volumes() + nodes = self.driver.list_nodes() + res = self.driver.attach_volume(nodes[0], disks[0]) + self.assertTrue(res) + + def test_detach_volume(self): + disks = self.driver.list_volumes() + nodes = self.driver.list_nodes() + res = self.driver.detach_volume(nodes[0], disks[0]) + self.assertTrue(res) + def test_ex_list_interfaces(self): ifaces = self.driver.ex_list_interfaces() self.assertTrue(len(ifaces) > 0) @@ -129,26 +155,14 @@ class GandiTests(unittest.TestCase): res = self.driver.ex_node_detach_interface(nodes[0], ifaces[0]) self.assertTrue(res) - def test_ex_attach_disk(self): - disks = self.driver.ex_list_disks() - nodes = self.driver.list_nodes() - res = self.driver.ex_node_attach_disk(nodes[0], disks[0]) - self.assertTrue(res) - - def test_ex_detach_disk(self): - disks = self.driver.ex_list_disks() - nodes = self.driver.list_nodes() - res = self.driver.ex_node_detach_disk(nodes[0], disks[0]) - self.assertTrue(res) - def test_ex_snapshot_disk(self): - disks = self.driver.ex_list_disks() + disks = self.driver.list_volumes() self.assertTrue(self.driver.ex_snapshot_disk(disks[2])) self.assertRaises(GandiException, self.driver.ex_snapshot_disk, disks[0]) def test_ex_update_disk(self): - disks = self.driver.ex_list_disks() + disks = self.driver.list_volumes() self.assertTrue(self.driver.ex_update_disk(disks[0], new_size=4096)) @@ -156,80 +170,92 @@ class GandiMockHttp(MockHttp): fixtures = ComputeFileFixtures('gandi') - def _xmlrpc_2_0__datacenter_list(self, method, url, body, headers): + def _xmlrpc__datacenter_list(self, method, url, body, headers): body = self.fixtures.load('datacenter_list.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__image_list(self, method, url, body, headers): + def _xmlrpc__image_list(self, method, url, body, headers): body = self.fixtures.load('image_list_dc0.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_list(self, method, url, body, headers): + def _xmlrpc__vm_list(self, method, url, body, headers): body = self.fixtures.load('vm_list.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__ip_list(self, method, url, body, headers): + def _xmlrpc__ip_list(self, method, url, body, headers): body = self.fixtures.load('ip_list.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__account_info(self, method, url, body, headers): + def _xmlrpc__account_info(self, method, url, body, headers): body = self.fixtures.load('account_info.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_info(self, method, url, body, headers): + def _xmlrpc__vm_info(self, method, url, body, headers): body = self.fixtures.load('vm_info.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_delete(self, method, url, body, headers): + def _xmlrpc__vm_delete(self, method, url, body, headers): body = self.fixtures.load('vm_delete.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__operation_info(self, method, url, body, headers): + def _xmlrpc__operation_info(self, method, url, body, headers): body = self.fixtures.load('operation_info.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_create_from(self, method, url, body, headers): + def _xmlrpc__vm_create_from(self, method, url, body, headers): body = self.fixtures.load('vm_create_from.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_reboot(self, method, url, body, headers): + def _xmlrpc__vm_reboot(self, method, url, body, headers): body = self.fixtures.load('vm_reboot.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_stop(self, method, url, body, headers): + def _xmlrpc__vm_stop(self, method, url, body, headers): body = self.fixtures.load('vm_stop.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__iface_list(self, method, url, body, headers): + def _xmlrpc__iface_list(self, method, url, body, headers): body = self.fixtures.load('iface_list.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__disk_list(self, method, url, body, headers): + def _xmlrpc__disk_list(self, method, url, body, headers): body = self.fixtures.load('disk_list.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_iface_attach(self, method, url, body, headers): + def _xmlrpc__vm_iface_attach(self, method, url, body, headers): body = self.fixtures.load('iface_attach.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_iface_detach(self, method, url, body, headers): - body = self.fixtures.load('iface_detach.xml') - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _xmlrpc__vm_iface_detach(self, method, url, body, headers): + body = self.fixtures.load('iface_detach.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_disk_attach(self, method, url, body, headers): + def _xmlrpc__vm_disk_attach(self, method, url, body, headers): body = self.fixtures.load('disk_attach.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__vm_disk_detach(self, method, url, body, headers): - body = self.fixtures.load('disk_detach.xml') - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _xmlrpc__vm_disk_detach(self, method, url, body, headers): + body = self.fixtures.load('disk_detach.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__disk_create_from(self, method, url, body, headers): - body = self.fixtures.load('disk_create_from.xml') - return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + def _xmlrpc__disk_create(self, method, url, body, headers): + body = self.fixtures.load('disk_create.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _xmlrpc__disk_create_from(self, method, url, body, headers): + body = self.fixtures.load('disk_create_from.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _xmlrpc__disk_info(self, method, url, body, headers): + body = self.fixtures.load('disk_info.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _xmlrpc__disk_update(self, method, url, body, headers): + body = self.fixtures.load('disk_update.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) - def _xmlrpc_2_0__disk_update(self, method, url, body, headers): - body = self.fixtures.load('disk_update.xml') + def _xmlrpc__disk_delete(self, method, url, body, headers): + body = self.fixtures.load('disk_delete.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) if __name__ == '__main__':