Add a RestClient variant that sends and expects XML
For now, this leaves the base class as defaulting to JSON and
just adds RestClientXML as a variant that does XMLish things.
This adds a headers argument to get and delete so that we can
declare to the API that we speak XML.
Also, add some common XML utilities.
Change-Id: I883de8e21ae18eed929705cff49b6dfb112d20c2
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index d9608ef..d2babcb 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -18,16 +18,18 @@
import json
import httplib2
import logging
+from lxml import etree
import time
from tempest import exceptions
-
+from tempest.services.nova.xml.common import xml_to_json
# redrive rate limited calls at most twice
MAX_RECURSION_DEPTH = 2
class RestClient(object):
+ TYPE = "json"
def __init__(self, config, user, password, auth_url, tenant_name=None):
self.log = logging.getLogger(__name__)
@@ -45,8 +47,8 @@
self.region = 0
self.endpoint_url = 'publicURL'
self.strategy = self.config.identity.strategy
- self.headers = {'Content-Type': 'application/json',
- 'Accept': 'application/json'}
+ self.headers = {'Content-Type': 'application/%s' % self.TYPE,
+ 'Accept': 'application/%s' % self.TYPE}
self.build_interval = config.compute.build_interval
self.build_timeout = config.compute.build_timeout
@@ -153,11 +155,11 @@
def post(self, url, body, headers):
return self.request('POST', url, headers, body)
- def get(self, url):
- return self.request('GET', url)
+ def get(self, url, headers=None):
+ return self.request('GET', url, headers)
- def delete(self, url):
- return self.request('DELETE', url)
+ def delete(self, url, headers=None):
+ return self.request('DELETE', url, headers)
def put(self, url, body, headers):
return self.request('PUT', url, headers, body)
@@ -168,6 +170,9 @@
self.log.error('Response Headers: ' + str(resp))
self.log.error('Response Body: ' + str(resp_body))
+ def _parse_resp(self, body):
+ return json.loads(body)
+
def request(self, method, url, headers=None, body=None, depth=0):
"""A simple HTTP request interface."""
@@ -191,20 +196,22 @@
raise exceptions.NotFound(resp_body)
if resp.status == 400:
- resp_body = json.loads(resp_body)
+ resp_body = self._parse_resp(resp_body)
self._log(req_url, body, resp, resp_body)
raise exceptions.BadRequest(resp_body)
if resp.status == 409:
- resp_body = json.loads(resp_body)
+ resp_body = self._parse_resp(resp_body)
self._log(req_url, body, resp, resp_body)
raise exceptions.Duplicate(resp_body)
if resp.status == 413:
- resp_body = json.loads(resp_body)
+ resp_body = self._parse_resp(resp_body)
self._log(req_url, body, resp, resp_body)
if 'overLimit' in resp_body:
raise exceptions.OverLimit(resp_body['overLimit']['message'])
+ elif 'limit' in resp_body['message']:
+ raise exceptions.OverLimit(resp_body['message'])
elif depth < MAX_RECURSION_DEPTH:
delay = resp['Retry-After'] if 'Retry-After' in resp else 60
time.sleep(int(delay))
@@ -215,7 +222,7 @@
details=resp_body['overLimitFault']['details'])
if resp.status in (500, 501):
- resp_body = json.loads(resp_body)
+ resp_body = self._parse_resp(resp_body)
self._log(req_url, body, resp, resp_body)
#I'm seeing both computeFault and cloudServersFault come back.
#Will file a bug to fix, but leave as is for now.
@@ -230,7 +237,7 @@
raise exceptions.ComputeFault(message)
if resp.status >= 400:
- resp_body = json.loads(resp_body)
+ resp_body = self._parse_resp(resp_body)
self._log(req_url, body, resp, resp_body)
raise exceptions.TempestException(str(resp.status))
@@ -251,3 +258,10 @@
Subclasses override with specific deletion detection.
"""
return False
+
+
+class RestClientXML(RestClient):
+ TYPE = "xml"
+
+ def _parse_resp(self, body):
+ return xml_to_json(etree.fromstring(body))
diff --git a/tempest/services/nova/xml/common.py b/tempest/services/nova/xml/common.py
new file mode 100644
index 0000000..9bb1d11
--- /dev/null
+++ b/tempest/services/nova/xml/common.py
@@ -0,0 +1,112 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# All Rights Reserved.
+#
+# Licensed 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.
+
+from lxml import etree
+
+XMLNS_11 = "http://docs.openstack.org/compute/api/v1.1"
+
+
+# NOTE(danms): This is just a silly implementation to help make generating
+# XML faster for prototyping. Could be replaced with proper etree gorp
+# if desired
+class Element(object):
+ def __init__(self, element_name, *args, **kwargs):
+ self.element_name = element_name
+ self._attrs = kwargs
+ self._elements = list(args)
+
+ def add_attr(self, name, value):
+ self._attrs[name] = value
+
+ def append(self, element):
+ self._elements.append(element)
+
+ def __str__(self):
+ args = " ".join(['%s="%s"' % (k, v) for k, v in self._attrs.items()])
+ string = '<%s %s' % (self.element_name, args)
+ if not self._elements:
+ string += '/>'
+ return string
+
+ string += '>'
+
+ for element in self._elements:
+ string += str(element)
+
+ string += '</%s>' % self.element_name
+
+ return string
+
+ def __getitem__(self, name):
+ for element in self._elements:
+ if element.element_name == name:
+ return element
+ raise KeyError("No such element `%s'" % name)
+
+ def __getattr__(self, name):
+ if name in self._attrs:
+ return self._attrs[name]
+ return object.__getattr__(self, name)
+
+ def attributes(self):
+ return self._attrs.items()
+
+ def children(self):
+ return self._elements
+
+
+class Document(Element):
+ def __init__(self, *args, **kwargs):
+ if 'version' not in kwargs:
+ kwargs['version'] = '1.0'
+ if 'encoding' not in kwargs:
+ kwargs['encoding'] = 'UTF-8'
+ Element.__init__(self, '?xml', *args, **kwargs)
+
+ def __str__(self):
+ args = " ".join(['%s="%s"' % (k, v) for k, v in self._attrs.items()])
+ string = '<?xml %s?>\n' % args
+ for element in self._elements:
+ string += str(element)
+ return string
+
+
+class Text(Element):
+ def __init__(self, content=""):
+ Element.__init__(self, None)
+ self.__content = content
+
+ def __str__(self):
+ return self.__content
+
+
+def xml_to_json(node):
+ """This does a really braindead conversion of an XML tree to
+ something that looks like a json dump. In cases where the XML
+ and json structures are the same, then this "just works". In
+ others, it requires a little hand-editing of the result."""
+ json = {}
+ for attr in node.keys():
+ json[attr] = node.get(attr)
+ if not node.getchildren():
+ return node.text or json
+ for child in node.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ json[tag] = xml_to_json(child)
+ return json