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__':