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