You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2013/07/08 13:05:41 UTC

[4/6] git commit: Issue LIBCLOUD-354: Add support for volume-related functions to OpenNebula compute driver

Issue LIBCLOUD-354: Add support for volume-related functions to OpenNebula compute driver

Signed-off-by: Tomaz Muraus <to...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/7a7b2f45
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/7a7b2f45
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/7a7b2f45

Branch: refs/heads/0.13.x
Commit: 7a7b2f4593b8ad02c540cbed37c2473907e7d8eb
Parents: c79d000
Author: Emanuele Rocca <em...@linux.it>
Authored: Mon Jul 1 20:36:19 2013 +0200
Committer: Tomaz Muraus <to...@apache.org>
Committed: Mon Jul 8 12:59:55 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/7a7b2f45/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/7a7b2f45/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/7a7b2f45/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/7a7b2f45/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/7a7b2f45/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/7a7b2f45/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/7a7b2f45/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):
     """