Author: tomaz
Date: Mon Jun 27 14:10:14 2011
New Revision: 1140170

URL: http://svn.apache.org/viewvc?rev=1140170&view=rev
Log:
1. Don't use Rackspace pricing for OpenStack drivers
2. Handle non-xml responses better in the OpenStack driver
3. pep8 & styling fixes (tomaz)

This patch has been contributed by Andrey Zhuchkov and is part of LIBCLOUD-92.

Modified:
    libcloud/trunk/libcloud/compute/drivers/rackspace.py
    libcloud/trunk/test/compute/test_rackspace.py
    libcloud/trunk/test/secrets.py-dist

Modified: libcloud/trunk/libcloud/compute/drivers/rackspace.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/libcloud/compute/drivers/rackspace.py?rev=1140170&r1=1140169&r2=1140170&view=diff
==============================================================================
--- libcloud/trunk/libcloud/compute/drivers/rackspace.py (original)
+++ libcloud/trunk/libcloud/compute/drivers/rackspace.py Mon Jun 27 14:10:14 
2011
@@ -23,7 +23,7 @@ import warnings
 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
@@ -33,7 +33,8 @@ from libcloud.compute.base import NodeSi
 from libcloud.common.rackspace import (
     AUTH_HOST_US, AUTH_HOST_UK, RackspaceBaseConnection)
 
-NAMESPACE='http://docs.rackspacecloud.com/servers/api/v1.0'
+
+NAMESPACE = 'http://docs.rackspacecloud.com/servers/api/v1.0'
 
 
 class RackspaceResponse(Response):
@@ -53,6 +54,7 @@ class RackspaceResponse(Response):
                 body=self.body,
                 driver=RackspaceNodeDriver)
         return body
+
     def parse_error(self):
         # TODO: fixup, Rackspace only uses response codes really!
         try:
@@ -62,10 +64,10 @@ class RackspaceResponse(Response):
                 "Failed to parse XML",
                 body=self.body, driver=RackspaceNodeDriver)
         try:
-            text = "; ".join([ err.text or ''
-                               for err in
-                               body.getiterator()
-                               if err.text])
+            text = "; ".join([err.text or ''
+                              for err in
+                              body.getiterator()
+                              if err.text])
         except ExpatError:
             text = self.body
         return '%s %s %s' % (self.status, self.error, text)
@@ -85,7 +87,8 @@ class RackspaceConnection(RackspaceBaseC
         self.api_version = 'v1.0'
         self.accept_format = 'application/xml'
 
-    def request(self, action, params=None, data='', headers=None, 
method='GET'):
+    def request(self, action, params=None, data='', headers=None,
+                method='GET'):
         if not headers:
             headers = {}
         if not params:
@@ -145,31 +148,34 @@ class RackspaceNodeDriver(NodeDriver):
 
     features = {"create_node": ["generates_password"]}
 
-    NODE_STATE_MAP = { 'BUILD': NodeState.PENDING,
-                       'REBUILD': NodeState.PENDING,
-                       'ACTIVE': NodeState.RUNNING,
-                       'SUSPENDED': NodeState.TERMINATED,
-                       'QUEUE_RESIZE': NodeState.PENDING,
-                       'PREP_RESIZE': NodeState.PENDING,
-                       'VERIFY_RESIZE': NodeState.RUNNING,
-                       'PASSWORD': NodeState.PENDING,
-                       'RESCUE': NodeState.PENDING,
-                       'REBUILD': NodeState.PENDING,
-                       'REBOOT': NodeState.REBOOTING,
-                       'HARD_REBOOT': NodeState.REBOOTING,
-                       'SHARE_IP': NodeState.PENDING,
-                       'SHARE_IP_NO_CONFIG': NodeState.PENDING,
-                       'DELETE_IP': NodeState.PENDING,
-                       'UNKNOWN': NodeState.UNKNOWN}
+    NODE_STATE_MAP = {'BUILD': NodeState.PENDING,
+                      'REBUILD': NodeState.PENDING,
+                      'ACTIVE': NodeState.RUNNING,
+                      'SUSPENDED': NodeState.TERMINATED,
+                      'QUEUE_RESIZE': NodeState.PENDING,
+                      'PREP_RESIZE': NodeState.PENDING,
+                      'VERIFY_RESIZE': NodeState.RUNNING,
+                      'PASSWORD': NodeState.PENDING,
+                      'RESCUE': NodeState.PENDING,
+                      'REBUILD': NodeState.PENDING,
+                      'REBOOT': NodeState.REBOOTING,
+                      'HARD_REBOOT': NodeState.REBOOTING,
+                      'SHARE_IP': NodeState.PENDING,
+                      'SHARE_IP_NO_CONFIG': NodeState.PENDING,
+                      'DELETE_IP': NodeState.PENDING,
+                      'UNKNOWN': NodeState.UNKNOWN}
 
     def list_nodes(self):
-        return 
self._to_nodes(self.connection.request('/servers/detail').object)
+        return self._to_nodes(self.connection.request('/servers/detail')
+                                             .object)
 
     def list_sizes(self, location=None):
-        return 
self._to_sizes(self.connection.request('/flavors/detail').object)
+        return self._to_sizes(self.connection.request('/flavors/detail')
+                                             .object)
 
     def list_images(self, location=None):
-        return 
self._to_images(self.connection.request('/images/detail').object)
+        return self._to_images(self.connection.request('/images/detail')
+                                              .object)
 
     def list_locations(self):
         """Lists available locations
@@ -185,7 +191,7 @@ class RackspaceNodeDriver(NodeDriver):
         if not name:
             name = node.name
 
-        body = { 'xmlns': NAMESPACE,
+        body = {'xmlns': NAMESPACE,
                  'name': name}
 
         if password != None:
@@ -227,7 +233,8 @@ class RackspaceNodeDriver(NodeDriver):
         @keyword    ex_metadata: Key/Value metadata to associate with a node
         @type       ex_metadata: C{dict}
 
-        @keyword    ex_files:   File Path => File contents to create on the 
node
+        @keyword    ex_files:   File Path => File contents to create on
+                                the node
         @type       ex_files:   C{dict}
         """
         name = kwargs['name']
@@ -247,7 +254,6 @@ class RackspaceNodeDriver(NodeDriver):
             warnings.warn('ex_shared_ip_group argument is deprecated. Please'
                           + ' use ex_shared_ip_group_id')
 
-
         if 'ex_shared_ip_group_id' in kwargs:
             shared_ip_group_id = kwargs['ex_shared_ip_group_id']
             attributes['sharedIpGroupId'] = shared_ip_group_id
@@ -317,8 +323,8 @@ class RackspaceNodeDriver(NodeDriver):
         elm = ET.Element(
             'shareIp',
             {'xmlns': NAMESPACE,
-             'sharedIpGroupId' : group_id,
-             'configureServer' : str_configure}
+             'sharedIpGroupId': group_id,
+             'configureServer': str_configure}
         )
 
         uri = '/servers/%s/ips/public/%s' % (node_id, ip)
@@ -347,7 +353,7 @@ class RackspaceNodeDriver(NodeDriver):
 
         metadata_elm = ET.Element('metadata')
         for k, v in metadata.items():
-            meta_elm = ET.SubElement(metadata_elm, 'meta', {'key': str(k) })
+            meta_elm = ET.SubElement(metadata_elm, 'meta', {'key': str(k)})
             meta_elm.text = str(v)
 
         return metadata_elm
@@ -401,7 +407,7 @@ class RackspaceNodeDriver(NodeDriver):
 
     def _to_nodes(self, object):
         node_elements = self._findall(object, 'server')
-        return [ self._to_node(el) for el in node_elements ]
+        return [self._to_node(el) for el in node_elements]
 
     def _fixxpath(self, xpath):
         # ElementTree wants namespaces in its xpaths, so here we add them.
@@ -417,7 +423,7 @@ class RackspaceNodeDriver(NodeDriver):
         def get_meta_dict(el):
             d = {}
             for meta in el:
-                d[meta.get('key')] =  meta.text
+                d[meta.get('key')] = meta.text
             return d
 
         public_ip = get_ips(self._findall(el,
@@ -447,23 +453,23 @@ class RackspaceNodeDriver(NodeDriver):
 
     def _to_sizes(self, object):
         elements = self._findall(object, 'flavor')
-        return [ self._to_size(el) for el in elements ]
+        return [self._to_size(el) for el in elements]
 
     def _to_size(self, el):
         s = NodeSize(id=el.get('id'),
                      name=el.get('name'),
                      ram=int(el.get('ram')),
                      disk=int(el.get('disk')),
-                     bandwidth=None, # XXX: needs hardcode
-                     price=self._get_size_price(el.get('id')), # Hardcoded,
+                     bandwidth=None,  # XXX: needs hardcode
+                     price=self._get_size_price(el.get('id')),  # Hardcoded,
                      driver=self.connection.driver)
         return s
 
     def _to_images(self, object):
         elements = self._findall(object, "image")
-        return [ self._to_image(el)
-                 for el in elements
-                 if el.get('status') == 'ACTIVE' ]
+        return [self._to_image(el)
+                for el in elements
+                if el.get('status') == 'ACTIVE']
 
     def _to_image(self, el):
         i = NodeImage(id=el.get('id'),
@@ -493,7 +499,7 @@ class RackspaceNodeDriver(NodeDriver):
             return {el.get('name'): el.get('value')}
 
         limits = self.connection.request("/limits").object
-        rate = [ _to_rate(el) for el in self._findall(limits, 'rate/limit') ]
+        rate = [_to_rate(el) for el in self._findall(limits, 'rate/limit')]
         absolute = {}
         for item in self._findall(limits, 'absolute/limit'):
             absolute.update(_to_absolute(item))
@@ -539,12 +545,14 @@ class RackspaceNodeDriver(NodeDriver):
              self._findall(self._findall(el, 'private')[0], 'ip')]
         )
 
+
 class RackspaceUKConnection(RackspaceConnection):
     """
     Connection class for the Rackspace UK driver
     """
     auth_host = AUTH_HOST_UK
 
+
 class RackspaceUKNodeDriver(RackspaceNodeDriver):
     """Driver for Rackspace in the UK (London)
     """
@@ -555,13 +563,51 @@ class RackspaceUKNodeDriver(RackspaceNod
     def list_locations(self):
         return [NodeLocation(0, 'Rackspace UK London', 'UK', self)]
 
+
+class OpenStackResponse(RackspaceResponse):
+
+    def has_content_type(self, content_type):
+        content_type_header = dict([(key, value) for key, value in
+                                    self.headers.items()
+                                    if key.lower() == 'content-type'])
+        if not content_type_header:
+            return False
+
+        content_type_value = content_type_header['content-type'].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
         self.port = (port, 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)

Modified: libcloud/trunk/test/compute/test_rackspace.py
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/compute/test_rackspace.py?rev=1140170&r1=1140169&r2=1140170&view=diff
==============================================================================
--- libcloud/trunk/test/compute/test_rackspace.py (original)
+++ libcloud/trunk/test/compute/test_rackspace.py Mon Jun 27 14:10:14 2011
@@ -18,13 +18,19 @@ import httplib
 
 from libcloud.common.types import InvalidCredsError, MalformedResponseError
 from libcloud.compute.drivers.rackspace import RackspaceNodeDriver as Rackspace
+from libcloud.compute.drivers.rackspace import OpenStackResponse
+from libcloud.compute.drivers.rackspace import 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
 
 from test.secrets import RACKSPACE_USER, RACKSPACE_KEY
+from test.secrets import NOVA_USERNAME, NOVA_API_KEY, NOVA_HOST, NOVA_PORT
+from test.secrets import NOVA_SECURE
+
 
 class RackspaceTests(unittest.TestCase, TestCaseMixin):
 
@@ -78,7 +84,8 @@ class RackspaceTests(unittest.TestCase, 
         self.assertEqual(len(ret), 1)
         node = ret[0]
         self.assertEqual(type(node.extra.get('metadata')), type(dict()))
-        self.assertEqual(node.extra.get('metadata').get('somekey'), 
'somevalue')
+        self.assertEqual(node.extra.get('metadata').get('somekey'),
+                         'somevalue')
         RackspaceMockHttp.type = None
 
     def test_list_sizes(self):
@@ -94,16 +101,20 @@ class RackspaceTests(unittest.TestCase, 
         self.assertEqual(ret[11].extra['serverId'], '91221')
 
     def test_create_node(self):
-        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)', 
driver=self.driver)
-        size = NodeSize(1, '256 slice', None, None, None, None, 
driver=self.driver)
+        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)',
+                          driver=self.driver)
+        size = NodeSize(1, '256 slice', None, None, None, None,
+                        driver=self.driver)
         node = self.driver.create_node(name='racktest', image=image, size=size)
         self.assertEqual(node.name, 'racktest')
         self.assertEqual(node.extra.get('password'), 'racktestvJq7d3')
 
     def test_create_node_ex_shared_ip_group(self):
         RackspaceMockHttp.type = 'EX_SHARED_IP_GROUP'
-        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)', 
driver=self.driver)
-        size = NodeSize(1, '256 slice', None, None, None, None, 
driver=self.driver)
+        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)',
+                          driver=self.driver)
+        size = NodeSize(1, '256 slice', None, None, None, None,
+                        driver=self.driver)
         node = self.driver.create_node(name='racktest', image=image, size=size,
                                        ex_shared_ip_group_id='12345')
         self.assertEqual(node.name, 'racktest')
@@ -111,24 +122,27 @@ class RackspaceTests(unittest.TestCase, 
 
     def test_create_node_with_metadata(self):
         RackspaceMockHttp.type = 'METADATA'
-        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)', 
driver=self.driver)
-        size = NodeSize(1, '256 slice', None, None, None, None, 
driver=self.driver)
-        metadata = { 'a': 'b', 'c': 'd' }
-        files = { '/file1': 'content1', '/file2': 'content2' }
-        node = self.driver.create_node(name='racktest', image=image, 
size=size, metadata=metadata, files=files)
+        image = NodeImage(id=11, name='Ubuntu 8.10 (intrepid)',
+                          driver=self.driver)
+        size = NodeSize(1, '256 slice', None, None, None, None,
+                        driver=self.driver)
+        metadata = {'a': 'b', 'c': 'd'}
+        files = {'/file1': 'content1', '/file2': 'content2'}
+        node = self.driver.create_node(name='racktest', image=image, size=size,
+                                       metadata=metadata, files=files)
         self.assertEqual(node.name, 'racktest')
         self.assertEqual(node.extra.get('password'), 'racktestvJq7d3')
         self.assertEqual(node.extra.get('metadata'), metadata)
 
     def test_reboot_node(self):
-        node = Node(id=72258, name=None, state=None, public_ip=None, 
private_ip=None,
-                    driver=self.driver)
+        node = Node(id=72258, name=None, state=None, public_ip=None,
+                    private_ip=None, driver=self.driver)
         ret = node.reboot()
         self.assertTrue(ret is True)
 
     def test_destroy_node(self):
-        node = Node(id=72258, name=None, state=None, public_ip=None, 
private_ip=None,
-                    driver=self.driver)
+        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)
 
@@ -138,8 +152,8 @@ class RackspaceTests(unittest.TestCase, 
         self.assertTrue("absolute" in limits)
 
     def test_ex_save_image(self):
-        node = Node(id=444222, name=None, state=None, public_ip=None, 
private_ip=None,
-                driver=self.driver)
+        node = Node(id=444222, name=None, state=None, public_ip=None,
+                    private_ip=None, driver=self.driver)
         image = self.driver.ex_save_image(node, "imgtest")
         self.assertEqual(image.name, "imgtest")
         self.assertEqual(image.id, "12345")
@@ -305,5 +319,107 @@ class RackspaceMockHttp(MockHttpTestCase
     def _v1_0_slug_servers_3445_ips_public_67_23_21_133(self, method, url, 
body, headers):
         return (httplib.ACCEPTED, "", {}, httplib.responses[httplib.ACCEPTED])
 
+
+class OpenStackResponseTestCase(unittest.TestCase):
+    XML = """<?xml version="1.0" encoding="UTF-8"?><root/>"""
+
+    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")
+
+
+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 = """<flavors 
xmlns="http://docs.rackspacecloud.com/servers/api/v1.0";>
+                    <flavor disk="40" id="3" name="m1.medium" ram="4096"/>
+                    <flavor disk="20" id="2" name="m1.small" ram="2048"/>
+                    <flavor disk="80" id="4" name="m1.large" ram="8192"/>
+                    <flavor disk="0" id="6" name="s1" ram="256"/>
+                    <flavor disk="0" id="7" name="s1.swap" ram="256"/>
+                    <flavor disk="0" id="1" name="m1.tiny" ram="512"/>
+                    <flavor disk="10" id="8" name="s1.tiny" ram="512"/>
+                    <flavor disk="160" id="5" name="m1.xlarge" ram="16384"/>
+                </flavors>
+                """
+        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())

Modified: libcloud/trunk/test/secrets.py-dist
URL: 
http://svn.apache.org/viewvc/libcloud/trunk/test/secrets.py-dist?rev=1140170&r1=1140169&r2=1140170&view=diff
==============================================================================
--- libcloud/trunk/test/secrets.py-dist (original)
+++ libcloud/trunk/test/secrets.py-dist Mon Jun 27 14:10:14 2011
@@ -66,3 +66,9 @@ OPENNEBULA_KEY = ''
 
 OPSOURCE_USER=''
 OPSOURCE_PASS=''
+
+NOVA_USERNAME = ''
+NOVA_API_KEY = ''
+NOVA_HOST = ''
+NOVA_PORT = 8774
+NOVA_SECURE = False


Reply via email to