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