Index: test/secrets.py-dist =================================================================== --- test/secrets.py-dist (revision 933122) +++ test/secrets.py-dist (working copy) @@ -44,3 +44,7 @@ VOXEL_KEY='' VOXEL_SECRET='' + +OPENNEBULA_USER='' +OPENNEBULA_KEY='' + Index: test/fixtures/opennebula/storage.xml =================================================================== --- test/fixtures/opennebula/storage.xml (revision 0) +++ test/fixtures/opennebula/storage.xml (revision 0) @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file Index: test/fixtures/opennebula/disk.xml =================================================================== --- test/fixtures/opennebula/disk.xml (revision 0) +++ test/fixtures/opennebula/disk.xml (revision 0) @@ -0,0 +1,7 @@ + + + 1 + UbuntuServer9.04-Contextualized + 5120 + file:///Users/oneuser/ubuntu-server-9.04/ubuntu-server-9.04.img + \ No newline at end of file Index: test/fixtures/opennebula/compute.xml =================================================================== --- test/fixtures/opennebula/compute.xml (revision 0) +++ test/fixtures/opennebula/compute.xml (revision 0) @@ -0,0 +1,15 @@ + + + 5 + MyCompute + ACTIVE + + + + + + + + + small + \ No newline at end of file Index: test/fixtures/opennebula/computes.xml =================================================================== --- test/fixtures/opennebula/computes.xml (revision 0) +++ test/fixtures/opennebula/computes.xml (revision 0) @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file Index: test/test_opennebula.py =================================================================== --- test/test_opennebula.py (revision 0) +++ test/test_opennebula.py (revision 0) @@ -0,0 +1,122 @@ +# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad +# Complutense de Madrid (dsa-research.org) +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# libcloud.org licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +import unittest + +from libcloud.drivers.opennebula import OpenNebulaNodeDriver +from libcloud.base import Node, NodeImage, NodeSize + +from test import MockHttp, TestCaseMixin +from test.file_fixtures import FileFixtures + +import httplib + +from secrets import OPENNEBULA_USER, OPENNEBULA_KEY + +class OpenNebulaTests(unittest.TestCase, TestCaseMixin): + + def setUp(self): + OpenNebulaNodeDriver.connectionCls.conn_classes = (None, OpenNebulaMockHttp) + self.driver = OpenNebulaNodeDriver(OPENNEBULA_USER, OPENNEBULA_KEY) + + def test_create_node(self): + image = NodeImage(id=1, name='UbuntuServer9.04-Contextualized', driver=self.driver) + size = NodeSize(1, 'small', None, None, None, None, driver=self.driver) + node = self.driver.create_node(name='MyCompute', image=image, size=size) + self.assertEqual(node.id, '5') + self.assertEqual(node.name, 'MyCompute') + + def test_list_nodes(self): + nodes = self.driver.list_nodes() + self.assertEqual(len(nodes), 2) + node = nodes[0] + self.assertEqual(node.id, '5') + self.assertEqual(node.name, 'MyCompute') + + def test_reboot_node(self): + node = Node(5, None, None, None, None, self.driver) + ret = self.driver.reboot_node(node) + self.assertTrue(ret) + + def test_destroy_node(self): + node = Node(5, None, None, None, None, self.driver) + ret = self.driver.destroy_node(node) + self.assertTrue(ret) + + def test_list_sizes(self): + sizes = self.driver.list_sizes() + self.assertEqual(len(sizes), 3) + self.assertTrue('small' in [ s.name for s in sizes]) + self.assertTrue('medium' in [ s.name for s in sizes]) + self.assertTrue('large' in [ s.name for s in sizes]) + + def test_list_images(self): + images = self.driver.list_images() + self.assertEqual(len(images), 2) + image = images[0] + self.assertEqual(image.id, '1') + self.assertEqual(image.name, 'UbuntuServer9.04-Contextualized') + +class OpenNebulaMockHttp(MockHttp): + + fixtures = FileFixtures('opennebula') + + def _compute(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('computes.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + if method == 'POST': + body = self.fixtures.load('compute.xml') + return (httplib.CREATED, body, {}, httplib.responses[httplib.CREATED]) + + def _storage(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('storage.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _compute_5(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('compute.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_15(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('compute.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _storage_1(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('disk.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _storage_8(self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('disk.xml') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) \ No newline at end of file Index: libcloud/providers.py =================================================================== --- libcloud/providers.py (revision 933122) +++ libcloud/providers.py (working copy) @@ -43,6 +43,8 @@ ('libcloud.drivers.voxel', 'VoxelNodeDriver'), Provider.SOFTLAYER: ('libcloud.drivers.softlayer', 'SoftLayerNodeDriver'), + Provider.OPENNEBULA: + ('libcloud.drivers.opennebula', 'OpenNebulaNodeDriver'), } def get_driver(provider): Index: libcloud/types.py =================================================================== --- libcloud/types.py (revision 933122) +++ libcloud/types.py (working copy) @@ -31,6 +31,7 @@ @cvar LINODE: Linode.com @cvar VCLOUD: vmware vCloud @cvar RIMUHOSTING: RimuHosting.com + @cvar OPENNEBULA: OpenNebula.org """ DUMMY = 0 EC2 = 1 # deprecated name @@ -47,6 +48,7 @@ EC2_US_WEST = 10 VOXEL = 11 SOFTLAYER = 12 + OPENNEBULA = 13 class NodeState(object): """ Index: libcloud/drivers/opennebula.py =================================================================== --- libcloud/drivers/opennebula.py (revision 0) +++ libcloud/drivers/opennebula.py (revision 0) @@ -0,0 +1,203 @@ +# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad +# Complutense de Madrid (dsa-research.org) +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# libcloud.org licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +OpenNebula driver +""" +from libcloud.providers import Provider +from libcloud.base import Response, ConnectionUserAndKey +from libcloud.base import NodeDriver, Node, NodeLocation, NodeState +from libcloud.base import NodeImage, NodeSize +import hashlib +from base64 import b64encode +from xml.etree import ElementTree as ET + + + +API_HOST = 'devel.cloud.opennebula.org' +API_PORT = (4567,443) + +class OpenNebulaResponse(Response): + + def success(self): + i = int(self.status) + return i >= 200 and i <= 299 + + def parse_body(self): + if not self.body: + return None + return ET.XML(self.body) + + def parse_error(self): + return self.body + + + +class OpenNebulaConnection(ConnectionUserAndKey): + + host = API_HOST + port = API_PORT + responseCls = OpenNebulaResponse + secure = 1 + + def add_default_headers(self, headers): + pass_sha1 = hashlib.sha1(self.key).hexdigest() + headers['Authorization'] = ("Basic %s" % b64encode("%s:%s" % (self.user_id, pass_sha1))) + return headers + + +class OpenNebulaNodeDriver(NodeDriver): + + connectionCls = OpenNebulaConnection + type = Provider.OPENNEBULA + name = 'OpenNebula' + + NODE_STATE_MAP = { + 'PENDING': NodeState.PENDING, + 'ACTIVE': NodeState.RUNNING, + 'DONE': NodeState.TERMINATED, + 'STOPPED': NodeState.TERMINATED + } + + def list_sizes(self, location=None): + return [ + NodeSize(id=1, + name="small", + ram=None, + disk=None, + bandwidth=None, + price=None, + driver=self), + NodeSize(id=2, + name="medium", + ram=None, + disk=None, + bandwidth=None, + price=None, + driver=self), + NodeSize(id=3, + name="large", + ram=None, + disk=None, + bandwidth=None, + price=None, + driver=self), + ] + + def list_nodes(self): + return self._to_nodes(self.connection.request('/compute').object) + + def list_images(self, location=None): + return self._to_images(self.connection.request('/storage').object) + + def list_locations(self): + return [NodeLocation(0, 'OpenNebula', 'ONE', self)] + + def reboot_node(self, node): + compute_id = str(node.id) + + url = '/compute/%s' % compute_id + resp1 = self.connection.request(url,method='PUT',data=self._xml_action(compute_id,'STOPPED')) + + if resp1.status == 400: + return False + + resp2 = self.connection.request(url,method='PUT',data=self._xml_action(compute_id,'RESUME')) + + if resp2.status == 400: + return False + + return True + + def destroy_node(self, node): + url = '/compute/%s' % (str(node.id)) + resp = self.connection.request(url,method='DELETE') + + return resp.status == 204 + + def create_node(self, **kwargs): + compute = ET.Element('COMPUTE') + + name = ET.SubElement(compute, 'NAME') + name.text = kwargs['name'] + + instance_type = ET.SubElement(compute, 'INSTANCE_TYPE') + instance_type.text = kwargs['size'].name + + storage = ET.SubElement(compute, 'STORAGE') + disk = ET.SubElement(storage, 'DISK', {'image' : str(kwargs['image'].id), + 'dev' : 'sda1'}) + + xml = ET.tostring(compute) + + node = self.connection.request('/compute',method='POST',data=xml).object + + return self._to_node(node) + + def _to_images(self, object): + images = [] + for element in object.findall("DISK"): + image_id = element.attrib["href"].partition("/storage/")[2] + image = self.connection.request(("/storage/%s" % (image_id))).object + images.append(self._to_image(image)) + + return images + + def _to_image(self, image): + return NodeImage(id = image.findtext("ID"), + name = image.findtext("NAME"), + driver = self.connection.driver) + + def _to_nodes(self, object): + computes = [] + for element in object.findall("COMPUTE"): + compute_id = element.attrib["href"].partition("/compute/")[2] + compute = self.connection.request(("/compute/%s" % (compute_id))).object + computes.append(self._to_node(compute)) + + return computes + + def _to_node(self, compute): + try: + state = self.NODE_STATE_MAP[compute.findtext("STATE")] + except KeyError: + state = NodeState.UNKNOWN + + networks = [] + for element in compute.findall("NIC"): + networks.append(element.attrib["ip"]) + + return Node(id = compute.findtext("ID"), + name = compute.findtext("NAME"), + state = state, + public_ip = networks, + private_ip = [], + driver = self.connection.driver) + + def _xml_action(self, compute_id, action): + compute = ET.Element('COMPUTE') + + compute_id = ET.SubElement(compute, 'ID') + compute_id.text = str(compute_id) + + state = ET.SubElement(compute, 'STATE') + state.text = action + + xml = ET.tostring(compute) + return xml + +