Updated Branches: refs/heads/0.13.x c79d0009e -> 36f8a94d2 refs/heads/trunk 3fc07bb14 -> c0cc8f986
Issue LIBCLOUD-354: Add support for volume-related functions to OpenNebula compute driver Signed-off-by: Tomaz Muraus <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/8c6ec20d Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/8c6ec20d Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/8c6ec20d Branch: refs/heads/trunk Commit: 8c6ec20d971850edbf4bb5529374a242b57ce03d Parents: 3fc07bb Author: Emanuele Rocca <[email protected]> Authored: Mon Jul 1 20:36:19 2013 +0200 Committer: Tomaz Muraus <[email protected]> Committed: Mon Jul 8 12:48:01 2013 +0200 ---------------------------------------------------------------------- libcloud/compute/drivers/opennebula.py | 148 +++++++++++++++++-- .../fixtures/opennebula_3_6/compute_15.xml | 17 +++ .../fixtures/opennebula_3_6/compute_5.xml | 22 +++ .../compute/fixtures/opennebula_3_6/disk_10.xml | 7 + .../compute/fixtures/opennebula_3_6/disk_15.xml | 7 + .../fixtures/opennebula_3_6/storage_5.xml | 13 ++ libcloud/test/compute/test_opennebula.py | 139 +++++++++++++++++ 7 files changed, 342 insertions(+), 11 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c6ec20d/libcloud/compute/drivers/opennebula.py ---------------------------------------------------------------------- diff --git a/libcloud/compute/drivers/opennebula.py b/libcloud/compute/drivers/opennebula.py index 7700198..da7af07 100644 --- a/libcloud/compute/drivers/opennebula.py +++ b/libcloud/compute/drivers/opennebula.py @@ -32,7 +32,7 @@ from libcloud.utils.py3 import b from libcloud.compute.base import NodeState, NodeDriver, Node, NodeLocation from libcloud.common.base import ConnectionUserAndKey, XmlResponse -from libcloud.compute.base import NodeImage, NodeSize +from libcloud.compute.base import NodeImage, NodeSize, StorageVolume from libcloud.common.types import InvalidCredsError from libcloud.compute.providers import Provider @@ -301,6 +301,8 @@ class OpenNebulaNodeDriver(NodeDriver): cls = OpenNebula_3_0_NodeDriver elif api_version in ['3.2']: cls = OpenNebula_3_2_NodeDriver + elif api_version in ['3.6']: + cls = OpenNebula_3_6_NodeDriver elif api_version in ['3.8']: cls = OpenNebula_3_8_NodeDriver if 'plain_auth' not in kwargs: @@ -868,28 +870,35 @@ class OpenNebula_2_0_NodeDriver(OpenNebulaNodeDriver): @type compute: L{ElementTree} @param compute: XML representation of a compute node. - @rtype: L{NodeImage} - @return: First disk attached to a compute node. + @rtype: C{list} of L{NodeImage} + @return: Disks attached to a compute node. """ disks = list() for element in compute.findall('DISK'): disk = element.find('STORAGE') - disk_id = disk.attrib['href'].partition('/storage/')[2] + image_id = disk.attrib['href'].partition('/storage/')[2] + + if 'id' in element.attrib: + disk_id = element.attrib['id'] + else: + disk_id = None disks.append( - NodeImage(id=disk_id, + NodeImage(id=image_id, name=disk.attrib.get('name', None), driver=self.connection.driver, extra={'type': element.findtext('TYPE'), + 'disk_id': disk_id, 'target': element.findtext('TARGET')})) - # @TODO: Return all disks when the Node type accepts multiple - # attached disks per node. - if len(disks) > 0: + # Return all disks when the Node type accepts multiple attached disks + # per node. + if len(disks) > 1: + return disks + + if len(disks) == 1: return disks[0] - else: - return None def _extract_size(self, compute): """ @@ -1071,7 +1080,124 @@ class OpenNebula_3_2_NodeDriver(OpenNebula_3_0_NodeDriver): return values -class OpenNebula_3_8_NodeDriver(OpenNebula_3_2_NodeDriver): +class OpenNebula_3_6_NodeDriver(OpenNebula_3_2_NodeDriver): + """ + OpenNebula.org node driver for OpenNebula.org v3.6. + """ + + def create_volume(self, size, name, location=None, snapshot=None): + storage = ET.Element('STORAGE') + + vol_name = ET.SubElement(storage, 'NAME') + vol_name.text = name + + vol_type = ET.SubElement(storage, 'TYPE') + vol_type.text = 'DATABLOCK' + + description = ET.SubElement(storage, 'DESCRIPTION') + description.text = 'Attached storage' + + public = ET.SubElement(storage, 'PUBLIC') + public.text = 'NO' + + persistent = ET.SubElement(storage, 'PERSISTENT') + persistent.text = 'YES' + + fstype = ET.SubElement(storage, 'FSTYPE') + fstype.text = 'ext3' + + vol_size = ET.SubElement(storage, 'SIZE') + vol_size.text = str(size) + + xml = ET.tostring(storage) + volume = self.connection.request('/storage', + { 'occixml': xml }, method='POST').object + + return self._to_volume(volume) + + def destroy_volume(self, volume): + url = '/storage/%s' % (str(volume.id)) + resp = self.connection.request(url, method='DELETE') + + return resp.status == httplib.NO_CONTENT + + def attach_volume(self, node, volume, device): + action = ET.Element('ACTION') + + perform = ET.SubElement(action, 'PERFORM') + perform.text = 'ATTACHDISK' + + params = ET.SubElement(action, 'PARAMS') + + ET.SubElement(params, + 'STORAGE', + {'href': '/storage/%s' % (str(volume.id))}) + + target = ET.SubElement(params, 'TARGET') + target.text = device + + xml = ET.tostring(action) + + url = '/compute/%s/action' % node.id + + resp = self.connection.request(url, method='POST', data=xml) + return resp.status == httplib.ACCEPTED + + def _do_detach_volume(self, node_id, disk_id): + action = ET.Element('ACTION') + + perform = ET.SubElement(action, 'PERFORM') + perform.text = 'DETACHDISK' + + params = ET.SubElement(action, 'PARAMS') + + ET.SubElement(params, + 'DISK', + {'id': disk_id}) + + xml = ET.tostring(action) + + url = '/compute/%s/action' % node_id + + resp = self.connection.request(url, method='POST', data=xml) + return resp.status == httplib.ACCEPTED + + def detach_volume(self, volume): + # We need to find the node using this volume + for node in self.list_nodes(): + if type(node.image) is not list: + # This node has only one associated image. It is not the one we + # are after. + continue + + for disk in node.image: + if disk.id == volume.id: + # Node found. We can now detach the volume + disk_id = disk.extra['disk_id'] + return self._do_detach_volume(node.id, disk_id) + + return False + + def list_volumes(self): + return self._to_volumes(self.connection.request('/storage').object) + + def _to_volume(self, storage): + return StorageVolume(id=storage.findtext('ID'), + name=storage.findtext('NAME'), + size=int(storage.findtext('SIZE')), + driver=self.connection.driver) + + def _to_volumes(self, object): + volumes = [] + for storage in object.findall('STORAGE'): + storage_id = storage.attrib['href'].partition('/storage/')[2] + + volumes.append(self._to_volume( + self.connection.request('/storage/%s' % storage_id).object)) + + return volumes + +class OpenNebula_3_8_NodeDriver(OpenNebula_3_6_NodeDriver): """ OpenNebula.org node driver for OpenNebula.org v3.8. """ http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c6ec20d/libcloud/test/compute/fixtures/opennebula_3_6/compute_15.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/opennebula_3_6/compute_15.xml b/libcloud/test/compute/fixtures/opennebula_3_6/compute_15.xml new file mode 100644 index 0000000..ce928ec --- /dev/null +++ b/libcloud/test/compute/fixtures/opennebula_3_6/compute_15.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<COMPUTE href='http://www.opennebula.org/compute/15'> + <ID>15</ID> + <NAME>Compute 15 Test</NAME> + <INSTANCE_TYPE>small</INSTANCE_TYPE> + <STATE>ACTIVE</STATE> + <DISK id="0"> + <STORAGE href="http://www.opennebula.org/storage/10" name="Debian"/> + <TYPE>FILE</TYPE> + <TARGET>hda</TARGET> + </DISK> + <NIC> + <NETWORK href="http://www.opennebula.org/network/5" name="Small network"/> + <IP>192.168.122.2</IP> + <MAC>02:00:c0:a8:7a:02</MAC> + </NIC> +</COMPUTE> http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c6ec20d/libcloud/test/compute/fixtures/opennebula_3_6/compute_5.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/opennebula_3_6/compute_5.xml b/libcloud/test/compute/fixtures/opennebula_3_6/compute_5.xml new file mode 100644 index 0000000..6767122 --- /dev/null +++ b/libcloud/test/compute/fixtures/opennebula_3_6/compute_5.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<COMPUTE href='http://www.opennebula.org/compute/5'> + <ID>5</ID> + <NAME>Compute 5 Test</NAME> + <INSTANCE_TYPE>small</INSTANCE_TYPE> + <STATE>ACTIVE</STATE> + <DISK id="0"> + <STORAGE href="http://www.opennebula.org/storage/5" name="Conpaas2"/> + <TYPE>FILE</TYPE> + <TARGET>hda</TARGET> + </DISK> + <DISK id="2"> + <STORAGE href="http://www.opennebula.org/storage/15" name="test-volume"/> + <TYPE>FILE</TYPE> + <TARGET>sda</TARGET> + </DISK> + <NIC> + <NETWORK href="http://www.opennebula.org/network/5" name="Small network"/> + <IP>192.168.122.2</IP> + <MAC>02:00:c0:a8:7a:02</MAC> + </NIC> +</COMPUTE> http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c6ec20d/libcloud/test/compute/fixtures/opennebula_3_6/disk_10.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/opennebula_3_6/disk_10.xml b/libcloud/test/compute/fixtures/opennebula_3_6/disk_10.xml new file mode 100644 index 0000000..1da6fa2 --- /dev/null +++ b/libcloud/test/compute/fixtures/opennebula_3_6/disk_10.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<DISK> + <ID>10</ID> + <NAME>Debian 7.1 LAMP</NAME> + <SIZE>2048</SIZE> + <URL>file:///images/debian/wheezy.img</URL> +</DISK> http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c6ec20d/libcloud/test/compute/fixtures/opennebula_3_6/disk_15.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/opennebula_3_6/disk_15.xml b/libcloud/test/compute/fixtures/opennebula_3_6/disk_15.xml new file mode 100644 index 0000000..811369b --- /dev/null +++ b/libcloud/test/compute/fixtures/opennebula_3_6/disk_15.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<DISK> + <ID>15</ID> + <NAME>Debian Sid</NAME> + <SIZE>1024</SIZE> + <URL>file:///images/debian/sid.img</URL> +</DISK> http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c6ec20d/libcloud/test/compute/fixtures/opennebula_3_6/storage_5.xml ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/fixtures/opennebula_3_6/storage_5.xml b/libcloud/test/compute/fixtures/opennebula_3_6/storage_5.xml new file mode 100644 index 0000000..27aaf73 --- /dev/null +++ b/libcloud/test/compute/fixtures/opennebula_3_6/storage_5.xml @@ -0,0 +1,13 @@ +<STORAGE href="http://www.opennebula.org/storage/5"> + <ID>5</ID> + <NAME>test-volume</NAME> + <USER href="http://www.opennebula.org/user/0" name="oneadmin"/> + <GROUP>oneadmin</GROUP> + <STATE>READY</STATE> + <TYPE>DATABLOCK</TYPE> + <DESCRIPTION>Attached storage</DESCRIPTION> + <SIZE>1000</SIZE> + <FSTYPE>ext3</FSTYPE> + <PUBLIC>NO</PUBLIC> + <PERSISTENT>YES</PERSISTENT> +</STORAGE> http://git-wip-us.apache.org/repos/asf/libcloud/blob/8c6ec20d/libcloud/test/compute/test_opennebula.py ---------------------------------------------------------------------- diff --git a/libcloud/test/compute/test_opennebula.py b/libcloud/test/compute/test_opennebula.py index 9cee04d..d978a5d 100644 --- a/libcloud/test/compute/test_opennebula.py +++ b/libcloud/test/compute/test_opennebula.py @@ -631,6 +631,74 @@ class OpenNebula_3_2_Tests(unittest.TestCase, OpenNebulaCaseMixin): self.assertEqual(size.bandwidth, None) self.assertEqual(size.price, None) +class OpenNebula_3_6_Tests(unittest.TestCase, OpenNebulaCaseMixin): + """ + OpenNebula.org test suite for OpenNebula v3.6. + """ + + def setUp(self): + """ + Setup test environment. + """ + OpenNebulaNodeDriver.connectionCls.conn_classes = ( + OpenNebula_3_6_MockHttp, OpenNebula_3_6_MockHttp) + self.driver = OpenNebulaNodeDriver(*OPENNEBULA_PARAMS + ('3.6',)) + + def test_create_volume(self): + new_volume = self.driver.create_volume(1000, 'test-volume') + + self.assertEquals(new_volume.id, '5') + self.assertEquals(new_volume.size, 1000) + self.assertEquals(new_volume.name, 'test-volume') + + def test_destroy_volume(self): + images = self.driver.list_images() + + self.assertEqual(len(images), 2) + image = images[0] + + ret = self.driver.destroy_volume(image) + self.assertTrue(ret) + + def test_attach_volume(self): + nodes = self.driver.list_nodes() + node = nodes[0] + + images = self.driver.list_images() + image = images[0] + + ret = self.driver.attach_volume(node, image, 'sda') + self.assertTrue(ret) + + def test_detach_volume(self): + images = self.driver.list_images() + image = images[1] + + ret = self.driver.detach_volume(image) + self.assertTrue(ret) + + nodes = self.driver.list_nodes() + # node with only a single associated image + node = nodes[1] + + ret = self.driver.detach_volume(node.image) + self.assertFalse(ret) + + def test_list_volumes(self): + volumes = self.driver.list_volumes() + + self.assertEqual(len(volumes), 2) + + volume = volumes[0] + self.assertEqual(volume.id, '5') + self.assertEqual(volume.size, 2048) + self.assertEqual(volume.name, 'Ubuntu 9.04 LAMP') + + volume = volumes[1] + self.assertEqual(volume.id, '15') + self.assertEqual(volume.size, 1024) + self.assertEqual(volume.name, 'Debian Sid') + class OpenNebula_3_8_Tests(unittest.TestCase, OpenNebulaCaseMixin): """ OpenNebula.org test suite for OpenNebula v3.8. @@ -1069,6 +1137,77 @@ class OpenNebula_3_2_MockHttp(OpenNebula_3_0_MockHttp): body = self.fixtures_3_2.load('instance_type_collection.xml') return (httplib.OK, body, {}, httplib.responses[httplib.OK]) +class OpenNebula_3_6_MockHttp(OpenNebula_3_2_MockHttp): + """ + Mock HTTP server for testing v3.6 of the OpenNebula.org compute driver. + """ + + fixtures_3_6 = ComputeFileFixtures('opennebula_3_6') + + def _storage(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('storage_collection.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + if method == 'POST': + body = self.fixtures_3_6.load('storage_5.xml') + return (httplib.CREATED, body, {}, + httplib.responses[httplib.CREATED]) + + def _compute_5(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures_3_6.load('compute_5.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + if method == 'PUT': + body = "" + return (httplib.ACCEPTED, body, {}, + httplib.responses[httplib.ACCEPTED]) + + if method == 'DELETE': + body = "" + return (httplib.NO_CONTENT, body, {}, + httplib.responses[httplib.NO_CONTENT]) + + def _compute_5_action(self, method, url, body, headers): + body = self.fixtures_3_6.load('compute_5.xml') + if method == 'POST': + return (httplib.ACCEPTED, body, {}, + httplib.responses[httplib.ACCEPTED]) + + if method == 'GET': + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _compute_15(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures_3_6.load('compute_15.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + if method == 'PUT': + body = "" + return (httplib.ACCEPTED, body, {}, + httplib.responses[httplib.ACCEPTED]) + + if method == 'DELETE': + body = "" + return (httplib.NO_CONTENT, body, {}, + httplib.responses[httplib.NO_CONTENT]) + + def _storage_10(self, method, url, body, headers): + """ + Storage entry resource. + """ + if method == 'GET': + body = self.fixtures_3_6.load('disk_10.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _storage_15(self, method, url, body, headers): + """ + Storage entry resource. + """ + if method == 'GET': + body = self.fixtures_3_6.load('disk_15.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) class OpenNebula_3_8_MockHttp(OpenNebula_3_2_MockHttp): """
