diff --git a/libcloud/compute/drivers/rackspace.py b/libcloud/compute/drivers/rackspace.py index 42d6cbb29e..a6725e96f7 100644 --- a/libcloud/compute/drivers/rackspace.py +++ b/libcloud/compute/drivers/rackspace.py @@ -23,7 +23,7 @@ from xml.etree import ElementTree as ET from xml.parsers.expat import ExpatError -from libcloud.pricing import get_pricing +from libcloud.pricing import get_pricing, get_size_price, PRICING_DATA from libcloud.common.base import Response from libcloud.common.types import MalformedResponseError from libcloud.compute.types import NodeState, Provider @@ -35,7 +35,6 @@ NAMESPACE='http://docs.rackspacecloud.com/servers/api/v1.0' - class RackspaceResponse(Response): def success(self): @@ -555,8 +554,35 @@ class RackspaceUKNodeDriver(RackspaceNodeDriver): def list_locations(self): return [NodeLocation(0, 'Rackspace UK London', 'UK', self)] +class OpenStackResponse(RackspaceResponse): + + def has_content_type(self, content_type): + content_type_headers = filter(lambda key: key[0].lower() == 'content-type', self.headers.items()) + + if not content_type_headers: + return False + + content_type_value = content_type_headers[-1][1].lower() + + return content_type_value.find(content_type.lower()) > -1 + + def parse_body(self): + if not self.has_content_type("application/xml") or not self.body: + return self.body + + try: + return ET.XML(self.body) + except: + raise MalformedResponseError( + "Failed to parse XML", + body=self.body, + driver=RackspaceNodeDriver) + + class OpenStackConnection(RackspaceConnection): + responseCls = OpenStackResponse + def __init__(self, user_id, key, secure, host, port): super(OpenStackConnection, self).__init__(user_id, key, secure=secure) self.auth_host = host @@ -565,3 +591,11 @@ def __init__(self, user_id, key, secure, host, port): class OpenStackNodeDriver(RackspaceNodeDriver): name = 'OpenStack' connectionCls = OpenStackConnection + + def _get_size_price(self, size_id): + if 'openstack' not in PRICING_DATA['compute']: + return 0.0 + + return get_size_price(driver_type='compute', + driver_name='openstack', + size_id=size_id) diff --git a/libcloud/utils.py b/libcloud/utils.py index a9ab20beff..5a11b46dfb 100644 --- a/libcloud/utils.py +++ b/libcloud/utils.py @@ -193,3 +193,5 @@ def get_driver(drivers, provider): return getattr(_mod, driver_name) raise AttributeError('Provider %s does not exist' % (provider)) + + diff --git a/test/compute/test_rackspace.py b/test/compute/test_rackspace.py index 5a10e37837..a84f8354b0 100644 --- a/test/compute/test_rackspace.py +++ b/test/compute/test_rackspace.py @@ -17,10 +17,11 @@ import httplib from libcloud.common.types import InvalidCredsError, MalformedResponseError -from libcloud.compute.drivers.rackspace import RackspaceNodeDriver as Rackspace +from libcloud.compute.drivers.rackspace import RackspaceNodeDriver as Rackspace, OpenStackResponse, OpenStackNodeDriver as OpenStack from libcloud.compute.base import Node, NodeImage, NodeSize +from libcloud.pricing import set_pricing -from test import MockHttpTestCase +from test import MockHttp, MockResponse, MockHttpTestCase from test.compute import TestCaseMixin from test.file_fixtures import ComputeFileFixtures @@ -305,5 +306,108 @@ def _v1_0_slug_shared_ip_groups_detail(self, method, url, body, headers): def _v1_0_slug_servers_3445_ips_public_67_23_21_133(self, method, url, body, headers): return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED]) + +# +# OpenStack +# + +class OpenStackResponseTestCase(unittest.TestCase): + XML = """""" + + def test_simple_xml_content_type_handling(self): + http_response = MockResponse(200, OpenStackResponseTestCase.XML, headers={'content-type': 'application/xml'}) + body = OpenStackResponse(http_response).parse_body() + + self.assertTrue(hasattr(body, 'tag'), "Body should be parsed as XML") + + def test_extended_xml_content_type_handling(self): + http_response = MockResponse(200, + OpenStackResponseTestCase.XML, + headers={'content-type': 'application/xml; charset=UTF-8'}) + body = OpenStackResponse(http_response).parse_body() + + self.assertTrue(hasattr(body, 'tag'), "Body should be parsed as XML") + + def test_non_xml_content_type_handling(self): + RESPONSE_BODY = "Accepted" + + http_response = MockResponse(202, RESPONSE_BODY, headers={'content-type': 'text/html'}) + body = OpenStackResponse(http_response).parse_body() + + self.assertEqual(body, RESPONSE_BODY, "Non-XML body should be returned as is") + + +from test.secrets import NOVA_USERNAME, NOVA_API_KEY, NOVA_HOST, NOVA_PORT, NOVA_SECURE + + +class OpenStackTests(unittest.TestCase): + def setUp(self): + OpenStack.connectionCls.conn_classes = (OpenStackMockHttp, None) + OpenStackMockHttp.type = None + self.driver = OpenStack(NOVA_USERNAME, NOVA_API_KEY, NOVA_SECURE, NOVA_HOST, NOVA_PORT) + + def test_destroy_node(self): + node = Node(id=72258, name=None, state=None, public_ip=None, private_ip=None, + driver=self.driver) + ret = node.destroy() + self.assertTrue(ret is True, "Unsuccessful node destroying") + + def test_list_sizes(self): + sizes = self.driver.list_sizes() + self.assertEqual(len(sizes), 8, "Wrong sizes count") + + for size in sizes: + self.assertTrue(isinstance(size.price, float), "Wrong size price type") + self.assertEqual(size.price, 0, "Size price should be zero by default") + + def test_list_sizes_with_specified_pricing(self): + pricing = dict((str(i), i) for i in range(1, 9)) + + set_pricing(driver_type='compute', driver_name='openstack', pricing=pricing) + + sizes = self.driver.list_sizes() + self.assertEqual(len(sizes), 8, "Wrong sizes count") + + for size in sizes: + self.assertTrue(isinstance(size.price, float), "Wrong size price type") + self.assertEqual(size.price, pricing[size.id], "Size price should be zero by default") + + +class OpenStackMockHttp(MockHttp): + def _v1_0(self, method, url, body, headers): + headers = {'x-server-management-url': 'https://servers.api.rackspacecloud.com/v1.0/slug', + 'x-auth-token': 'FE011C19-CF86-4F87-BE5D-9229145D7A06', + 'x-cdn-management-url': 'https://cdn.clouddrive.com/v1/MossoCloudFS_FE011C19-CF86-4F87-BE5D-9229145D7A06', + 'x-storage-token': 'FE011C19-CF86-4F87-BE5D-9229145D7A06', + 'x-storage-url': 'https://storage4.clouddrive.com/v1/MossoCloudFS_FE011C19-CF86-4F87-BE5D-9229145D7A06'} + return (httplib.NO_CONTENT, "", headers, httplib.responses[httplib.NO_CONTENT]) + + def _v1_0_slug_servers_72258(self, method, url, body, headers): + if method != "DELETE": + raise NotImplemented + # only used by destroy node() + return (httplib.ACCEPTED, + "202 Accepted\n\nThe request is accepted for processing.\n\n ", + {'date': 'Thu, 09 Jun 2011 10:51:53 GMT', 'content-length': '58', + 'content-type': 'text/html; charset=UTF-8'}, + httplib.responses[httplib.ACCEPTED]) + + def _v1_0_slug_flavors_detail(self, method, url, body, headers): + body = """ + + + + + + + + + + """ + return (httplib.OK, body, + {'date': 'Tue, 14 Jun 2011 09:43:55 GMT', 'content-length': '529', 'content-type': 'application/xml'}, + httplib.responses[httplib.OK]) + + if __name__ == '__main__': sys.exit(unittest.main()) diff --git a/test/secrets.py-dist b/test/secrets.py-dist index cb8bf12ea0..2627d0f79f 100644 --- a/test/secrets.py-dist +++ b/test/secrets.py-dist @@ -66,3 +66,9 @@ OPENNEBULA_KEY = '' OPSOURCE_USER='' OPSOURCE_PASS='' + +NOVA_USERNAME='' +NOVA_API_KEY='' +NOVA_HOST='' +NOVA_PORT=8774 +NOVA_SECURE=False \ No newline at end of file