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