Sophie

Sophie

distrib > Fedora > 18 > i386 > by-pkgid > 0305113317f9e80328b139dce3f45533 > files > 26

python-quantumclient-2.1-1.fc18.src.rpm

From 163765976897ba3991269059a7b09e5904d516ae Mon Sep 17 00:00:00 2001
From: gongysh <gongysh@cn.ibm.com>
Date: Fri, 18 Jan 2013 23:37:01 +0800
Subject: [PATCH] Support XML request format

blueprint quantum-client-xml

Change-Id: I9db8ea7c395909def00d6f25c9c1a98c07fdde68
---
 openstack-common.conf                       |   2 +-
 quantum_test.sh                             |  75 ++---
 quantumclient/common/constants.py           |  40 +++
 quantumclient/common/serializer.py          | 465 +++++++++++++++++++++-------
 quantumclient/openstack/common/jsonutils.py | 148 +++++++++
 quantumclient/openstack/common/timeutils.py | 164 ++++++++++
 quantumclient/v2_0/client.py                |  64 ++--
 tools/pip-requires                          |   5 +-
 tox.ini                                     |   2 +-
 9 files changed, 789 insertions(+), 176 deletions(-)
 create mode 100644 quantumclient/common/constants.py
 create mode 100644 quantumclient/openstack/common/jsonutils.py
 create mode 100644 quantumclient/openstack/common/timeutils.py

diff --git a/openstack-common.conf b/openstack-common.conf
index 35d48d0..d526114 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -1,7 +1,7 @@
 [DEFAULT]
 
 # The list of modules to copy from openstack-common
-modules=setup
+modules=jsonutils,setup,timeutils
 
 # The base module to hold the copy of openstack.common
 base=quantumclient
diff --git a/quantum_test.sh b/quantum_test.sh
index e19600b..b42299b 100755
--- a/quantum_test.sh
+++ b/quantum_test.sh
@@ -14,11 +14,12 @@ else
     NOAUTH=
 fi
 
+FORMAT=" --request-format xml"
 
 # test the CRUD of network
 network=mynet1
-quantum net-create $NOAUTH $network || die "fail to create network $network"
-temp=`quantum net-list -- --name $network --fields id | wc -l`
+quantum net-create $FORMAT $NOAUTH $network || die "fail to create network $network"
+temp=`quantum net-list $FORMAT -- --name $network --fields id | wc -l`
 echo $temp
 if [ $temp -ne 5 ]; then
    die "networks with name $network is not unique or found"
@@ -26,102 +27,102 @@ fi
 network_id=`quantum net-list -- --name $network --fields id | tail -n 2 | head -n 1 |  cut -d' ' -f 2`
 echo "ID of network with name $network is $network_id"
 
-quantum net-show $network ||  die "fail to show network $network"
-quantum net-show $network_id ||  die "fail to show network $network_id"
+quantum net-show $FORMAT $network ||  die "fail to show network $network"
+quantum net-show $FORMAT $network_id ||  die "fail to show network $network_id"
 
-quantum  net-update $network --admin_state_up False  ||  die "fail to update network $network"
-quantum  net-update $network_id --admin_state_up True  ||  die "fail to update network $network_id"
+quantum  net-update $FORMAT $network --admin_state_up False  ||  die "fail to update network $network"
+quantum  net-update $FORMAT $network_id --admin_state_up True  ||  die "fail to update network $network_id"
 
-quantum net-list -c id -- --id fakeid  || die "fail to list networks with column selection on empty list"
+quantum net-list $FORMAT -c id -- --id fakeid  || die "fail to list networks with column selection on empty list"
 
 # test the CRUD of subnet
 subnet=mysubnet1
 cidr=10.0.1.3/24
-quantum subnet-create $NOAUTH $network $cidr --name $subnet  || die "fail to create subnet $subnet"
-tempsubnet=`quantum subnet-list -- --name $subnet --fields id | wc -l`
+quantum subnet-create $FORMAT $NOAUTH $network $cidr --name $subnet  || die "fail to create subnet $subnet"
+tempsubnet=`quantum subnet-list $FORMAT -- --name $subnet --fields id | wc -l`
 echo $tempsubnet
 if [ $tempsubnet -ne 5 ]; then
    die "subnets with name $subnet is not unique or found"
 fi
-subnet_id=`quantum subnet-list -- --name $subnet --fields id | tail -n 2 | head -n 1 |  cut -d' ' -f 2`
+subnet_id=`quantum subnet-list $FORMAT -- --name $subnet --fields id | tail -n 2 | head -n 1 |  cut -d' ' -f 2`
 echo "ID of subnet with name $subnet is $subnet_id"
-quantum subnet-show $subnet ||  die "fail to show subnet $subnet"
-quantum subnet-show $subnet_id ||  die "fail to show subnet $subnet_id"
+quantum subnet-show $FORMAT $subnet ||  die "fail to show subnet $subnet"
+quantum subnet-show $FORMAT $subnet_id ||  die "fail to show subnet $subnet_id"
 
-quantum  subnet-update $subnet --dns_namesevers host1  ||  die "fail to update subnet $subnet"
-quantum  subnet-update $subnet_id --dns_namesevers host2  ||  die "fail to update subnet $subnet_id"
+quantum  subnet-update $FORMAT $subnet --dns_namesevers host1  ||  die "fail to update subnet $subnet"
+quantum  subnet-update $FORMAT $subnet_id --dns_namesevers host2  ||  die "fail to update subnet $subnet_id"
 
 # test the crud of ports
 port=myport1
-quantum port-create $NOAUTH $network --name $port  || die "fail to create port $port"
-tempport=`quantum port-list -- --name $port --fields id | wc -l`
+quantum port-create $FORMAT $NOAUTH $network --name $port  || die "fail to create port $port"
+tempport=`quantum port-list $FORMAT -- --name $port --fields id | wc -l`
 echo $tempport
 if [ $tempport -ne 5 ]; then
    die "ports with name $port is not unique or found"
 fi
-port_id=`quantum port-list -- --name $port --fields id | tail -n 2 | head -n 1 |  cut -d' ' -f 2`
+port_id=`quantum port-list $FORMAT -- --name $port --fields id | tail -n 2 | head -n 1 |  cut -d' ' -f 2`
 echo "ID of port with name $port is $port_id"
-quantum port-show $port ||  die "fail to show port $port"
-quantum port-show $port_id ||  die "fail to show port $port_id"
+quantum port-show $FORMAT $port ||  die "fail to show port $port"
+quantum port-show $FORMAT $port_id ||  die "fail to show port $port_id"
 
-quantum  port-update $port --device_id deviceid1  ||  die "fail to update port $port"
-quantum  port-update $port_id --device_id deviceid2  ||  die "fail to update port $port_id"
+quantum  port-update $FORMAT $port --device_id deviceid1  ||  die "fail to update port $port"
+quantum  port-update $FORMAT $port_id --device_id deviceid2  ||  die "fail to update port $port_id"
 
 # test quota commands RUD
 DEFAULT_NETWORKS=10
 DEFAULT_PORTS=50
 tenant_id=tenant_a
 tenant_id_b=tenant_b
-quantum quota-update --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id"
-quantum quota-update --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id"
-networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id | awk '{print $2}'`
+quantum quota-update $FORMAT --tenant_id $tenant_id --network 30 || die "fail to update quota for tenant $tenant_id"
+quantum quota-update $FORMAT --tenant_id $tenant_id_b --network 20 || die "fail to update quota for tenant $tenant_id"
+networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id | awk '{print $2}'`
 if [ $networks -ne 30 ]; then
    die "networks quota should be 30"
 fi
-networks=`quantum quota-list -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'`
+networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id_b | awk '{print $2}'`
 if [ $networks -ne 20 ]; then
    die "networks quota should be 20"
 fi
-networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|'  '{print $3}'`
+networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|'  '{print $3}'`
 if [ $networks -ne 30 ]; then
    die "networks quota should be 30"
 fi
-quantum quota-delete --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id"
-networks=`quantum quota-show --tenant_id $tenant_id | grep network | awk -F'|'  '{print $3}'`
+quantum quota-delete $FORMAT --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id"
+networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|'  '{print $3}'`
 if [ $networks -ne $DEFAULT_NETWORKS ]; then
    die "networks quota should be $DEFAULT_NETWORKS"
 fi
 # update self
 if [ "t$NOAUTH" = "t" ]; then
     # with auth
-    quantum quota-update --port 99 || die "fail to update quota for self"
-    ports=`quantum quota-show | grep port | awk -F'|'  '{print $3}'`
+    quantum quota-update $FORMAT --port 99 || die "fail to update quota for self"
+    ports=`quantum quota-show $FORMAT | grep port | awk -F'|'  '{print $3}'`
     if [ $ports -ne 99 ]; then
        die "ports quota should be 99"
     fi
     
-    ports=`quantum quota-list -c port | grep 99 | awk '{print $2}'`
+    ports=`quantum quota-list $FORMAT -c port | grep 99 | awk '{print $2}'`
     if [ $ports -ne 99 ]; then
        die "ports quota should be 99"
     fi
-    quantum quota-delete || die "fail to delete quota for tenant self"
-    ports=`quantum quota-show | grep port | awk -F'|'  '{print $3}'`
+    quantum quota-delete $FORMAT || die "fail to delete quota for tenant self"
+    ports=`quantum quota-show $FORMAT | grep port | awk -F'|'  '{print $3}'`
     if [ $ports -ne $DEFAULT_PORTS ]; then
        die "ports quota should be $DEFAULT_PORTS"
     fi
 else
     # without auth
-    quantum quota-update --port 100
+    quantum quota-update $FORMAT --port 100
     if [ $? -eq 0 ]; then
         die "without valid context on server, quota update command should fail."
     fi
-    quantum quota-show
+    quantum quota-show $FORMAT
     if [ $? -eq 0 ]; then
         die "without valid context on server, quota show command should fail."
     fi
-    quantum quota-delete
+    quantum quota-delete $FORMAT
     if [ $? -eq 0 ]; then
         die "without valid context on server, quota delete command should fail."
     fi
-    quantum quota-list || die "fail to update quota for self"
+    quantum quota-list $FORMAT || die "fail to update quota for self"
 fi
diff --git a/quantumclient/common/constants.py b/quantumclient/common/constants.py
new file mode 100644
index 0000000..572baa9
--- /dev/null
+++ b/quantumclient/common/constants.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2012 OpenStack, LLC.
+#
+# 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.
+
+
+EXT_NS = '_extension_ns'
+XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
+XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance"
+XSI_ATTR = "xsi:nil"
+XSI_NIL_ATTR = "xmlns:xsi"
+TYPE_XMLNS = "xmlns:quantum"
+TYPE_ATTR = "quantum:type"
+VIRTUAL_ROOT_KEY = "_v_root"
+
+TYPE_BOOL = "bool"
+TYPE_INT = "int"
+TYPE_LONG = "long"
+TYPE_FLOAT = "float"
+TYPE_LIST = "list"
+TYPE_DICT = "dict"
+
+PLURALS = {'networks': 'network',
+           'ports': 'port',
+           'subnets': 'subnet',
+           'dns_nameservers': 'dns_nameserver',
+           'host_routes': 'host_route',
+           'allocation_pools': 'allocation_pool',
+           'fixed_ips': 'fixed_ip',
+           'extensions': 'extension'}
diff --git a/quantumclient/common/serializer.py b/quantumclient/common/serializer.py
index 6e91467..7eba930 100644
--- a/quantumclient/common/serializer.py
+++ b/quantumclient/common/serializer.py
@@ -1,7 +1,353 @@
-from xml.dom import minidom
+# Copyright 2013 OpenStack LLC.
+# 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.
+#
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
+###
+### Codes from quantum wsgi
+###
+
+import logging
+
+from xml.etree import ElementTree as etree
+from xml.parsers import expat
+
+from quantumclient.common import constants
 from quantumclient.common import exceptions as exception
-from quantumclient.common import utils
+from quantumclient.openstack.common import jsonutils
+
+LOG = logging.getLogger(__name__)
+
+
+class ActionDispatcher(object):
+    """Maps method name to local methods through action name."""
+
+    def dispatch(self, *args, **kwargs):
+        """Find and call local method."""
+        action = kwargs.pop('action', 'default')
+        action_method = getattr(self, str(action), self.default)
+        return action_method(*args, **kwargs)
+
+    def default(self, data):
+        raise NotImplementedError()
+
+
+class DictSerializer(ActionDispatcher):
+    """Default request body serialization"""
+
+    def serialize(self, data, action='default'):
+        return self.dispatch(data, action=action)
+
+    def default(self, data):
+        return ""
+
+
+class JSONDictSerializer(DictSerializer):
+    """Default JSON request body serialization"""
+
+    def default(self, data):
+        return jsonutils.dumps(data)
+
+
+class XMLDictSerializer(DictSerializer):
+
+    def __init__(self, metadata=None, xmlns=None):
+        """
+        :param metadata: information needed to deserialize xml into
+                         a dictionary.
+        :param xmlns: XML namespace to include with serialized xml
+        """
+        super(XMLDictSerializer, self).__init__()
+        self.metadata = metadata or {}
+        if not xmlns:
+            xmlns = self.metadata.get('xmlns')
+        if not xmlns:
+            xmlns = constants.XML_NS_V20
+        self.xmlns = xmlns
+
+    def default(self, data):
+        # We expect data to contain a single key which is the XML root or
+        # non root
+        try:
+            key_len = data and len(data.keys()) or 0
+            if (key_len == 1):
+                root_key = data.keys()[0]
+                root_value = data[root_key]
+            else:
+                root_key = constants.VIRTUAL_ROOT_KEY
+                root_value = data
+            doc = etree.Element("_temp_root")
+            used_prefixes = []
+            self._to_xml_node(doc, self.metadata, root_key,
+                              root_value, used_prefixes)
+            return self.to_xml_string(list(doc)[0], used_prefixes)
+        except AttributeError as e:
+            LOG.exception(str(e))
+            return ''
+
+    def __call__(self, data):
+        # Provides a migration path to a cleaner WSGI layer, this
+        # "default" stuff and extreme extensibility isn't being used
+        # like originally intended
+        return self.default(data)
+
+    def to_xml_string(self, node, used_prefixes, has_atom=False):
+        self._add_xmlns(node, used_prefixes, has_atom)
+        return etree.tostring(node, encoding='UTF-8')
+
+    #NOTE (ameade): the has_atom should be removed after all of the
+    # xml serializers and view builders have been updated to the current
+    # spec that required all responses include the xmlns:atom, the has_atom
+    # flag is to prevent current tests from breaking
+    def _add_xmlns(self, node, used_prefixes, has_atom=False):
+        node.set('xmlns', self.xmlns)
+        node.set(constants.TYPE_XMLNS, self.xmlns)
+        if has_atom:
+            node.set('xmlns:atom', "http://www.w3.org/2005/Atom")
+        node.set(constants.XSI_NIL_ATTR, constants.XSI_NAMESPACE)
+        ext_ns = self.metadata.get(constants.EXT_NS, {})
+        for prefix in used_prefixes:
+            if prefix in ext_ns:
+                node.set('xmlns:' + prefix, ext_ns[prefix])
+
+    def _to_xml_node(self, parent, metadata, nodename, data, used_prefixes):
+        """Recursive method to convert data members to XML nodes."""
+        result = etree.SubElement(parent, nodename)
+        if ":" in nodename:
+            used_prefixes.append(nodename.split(":", 1)[0])
+        #TODO(bcwaldon): accomplish this without a type-check
+        if isinstance(data, list):
+            if not data:
+                result.set(
+                    constants.TYPE_ATTR,
+                    constants.TYPE_LIST)
+                return result
+            singular = metadata.get('plurals', {}).get(nodename, None)
+            if singular is None:
+                if nodename.endswith('s'):
+                    singular = nodename[:-1]
+                else:
+                    singular = 'item'
+            for item in data:
+                self._to_xml_node(result, metadata, singular, item,
+                                  used_prefixes)
+        #TODO(bcwaldon): accomplish this without a type-check
+        elif isinstance(data, dict):
+            if not data:
+                result.set(
+                    constants.TYPE_ATTR,
+                    constants.TYPE_DICT)
+                return result
+            attrs = metadata.get('attributes', {}).get(nodename, {})
+            for k, v in data.items():
+                if k in attrs:
+                    result.set(k, str(v))
+                else:
+                    self._to_xml_node(result, metadata, k, v,
+                                      used_prefixes)
+        elif data is None:
+            result.set(constants.XSI_ATTR, 'true')
+        else:
+            if isinstance(data, bool):
+                result.set(
+                    constants.TYPE_ATTR,
+                    constants.TYPE_BOOL)
+            elif isinstance(data, int):
+                result.set(
+                    constants.TYPE_ATTR,
+                    constants.TYPE_INT)
+            elif isinstance(data, long):
+                result.set(
+                    constants.TYPE_ATTR,
+                    constants.TYPE_LONG)
+            elif isinstance(data, float):
+                result.set(
+                    constants.TYPE_ATTR,
+                    constants.TYPE_FLOAT)
+            LOG.debug(_("Data %(data)s type is %(type)s"),
+                      {'data': data,
+                       'type': type(data)})
+            result.text = str(data)
+        return result
+
+    def _create_link_nodes(self, xml_doc, links):
+        link_nodes = []
+        for link in links:
+            link_node = xml_doc.createElement('atom:link')
+            link_node.set('rel', link['rel'])
+            link_node.set('href', link['href'])
+            if 'type' in link:
+                link_node.set('type', link['type'])
+            link_nodes.append(link_node)
+        return link_nodes
+
+
+class TextDeserializer(ActionDispatcher):
+    """Default request body deserialization"""
+
+    def deserialize(self, datastring, action='default'):
+        return self.dispatch(datastring, action=action)
+
+    def default(self, datastring):
+        return {}
+
+
+class JSONDeserializer(TextDeserializer):
+
+    def _from_json(self, datastring):
+        try:
+            return jsonutils.loads(datastring)
+        except ValueError:
+            msg = _("Cannot understand JSON")
+            raise exception.MalformedRequestBody(reason=msg)
+
+    def default(self, datastring):
+        return {'body': self._from_json(datastring)}
+
+
+class XMLDeserializer(TextDeserializer):
+
+    def __init__(self, metadata=None):
+        """
+        :param metadata: information needed to deserialize xml into
+                         a dictionary.
+        """
+        super(XMLDeserializer, self).__init__()
+        self.metadata = metadata or {}
+        xmlns = self.metadata.get('xmlns')
+        if not xmlns:
+            xmlns = constants.XML_NS_V20
+        self.xmlns = xmlns
+
+    def _get_key(self, tag):
+        tags = tag.split("}", 1)
+        if len(tags) == 2:
+            ns = tags[0][1:]
+            bare_tag = tags[1]
+            ext_ns = self.metadata.get(constants.EXT_NS, {})
+            if ns == self.xmlns:
+                return bare_tag
+            for prefix, _ns in ext_ns.items():
+                if ns == _ns:
+                    return prefix + ":" + bare_tag
+        else:
+            return tag
+
+    def _from_xml(self, datastring):
+        if datastring is None:
+            return None
+        plurals = set(self.metadata.get('plurals', {}))
+        try:
+            node = etree.fromstring(datastring)
+            result = self._from_xml_node(node, plurals)
+            root_tag = self._get_key(node.tag)
+            if root_tag == constants.VIRTUAL_ROOT_KEY:
+                return result
+            else:
+                return {root_tag: result}
+        except Exception as e:
+            parseError = False
+            # Python2.7
+            if (hasattr(etree, 'ParseError') and
+                isinstance(e, getattr(etree, 'ParseError'))):
+                parseError = True
+            # Python2.6
+            elif isinstance(e, expat.ExpatError):
+                parseError = True
+            if parseError:
+                msg = _("Cannot understand XML")
+                raise exception.MalformedRequestBody(reason=msg)
+            else:
+                raise
+
+    def _from_xml_node(self, node, listnames):
+        """Convert a minidom node to a simple Python type.
+
+        :param listnames: list of XML node names whose subnodes should
+                          be considered list items.
+
+        """
+        attrNil = node.get(str(etree.QName(constants.XSI_NAMESPACE, "nil")))
+        attrType = node.get(str(etree.QName(
+            self.metadata.get('xmlns'), "type")))
+        if (attrNil and attrNil.lower() == 'true'):
+            return None
+        elif not len(node) and not node.text:
+            if (attrType and attrType == constants.TYPE_DICT):
+                return {}
+            elif (attrType and attrType == constants.TYPE_LIST):
+                return []
+            else:
+                return ''
+        elif (len(node) == 0 and node.text):
+            converters = {constants.TYPE_BOOL:
+                          lambda x: x.lower() == 'true',
+                          constants.TYPE_INT:
+                          lambda x: int(x),
+                          constants.TYPE_LONG:
+                          lambda x: long(x),
+                          constants.TYPE_FLOAT:
+                          lambda x: float(x)}
+            if attrType and attrType in converters:
+                return converters[attrType](node.text)
+            else:
+                return node.text
+        elif self._get_key(node.tag) in listnames:
+            return [self._from_xml_node(n, listnames) for n in node]
+        else:
+            result = dict()
+            for attr in node.keys():
+                if (attr == 'xmlns' or
+                    attr.startswith('xmlns:') or
+                    attr == constants.XSI_ATTR or
+                    attr == constants.TYPE_ATTR):
+                    continue
+                result[self._get_key(attr)] = node.get[attr]
+            children = list(node)
+            for child in children:
+                result[self._get_key(child.tag)] = self._from_xml_node(
+                    child, listnames)
+            return result
+
+    def find_first_child_named(self, parent, name):
+        """Search a nodes children for the first child with a given name"""
+        for node in parent.childNodes:
+            if node.nodeName == name:
+                return node
+        return None
+
+    def find_children_named(self, parent, name):
+        """Return all of a nodes children who have the given name"""
+        for node in parent.childNodes:
+            if node.nodeName == name:
+                yield node
+
+    def extract_text(self, node):
+        """Get the text field contained by the given node"""
+        if len(node.childNodes) == 1:
+            child = node.childNodes[0]
+            if child.nodeType == child.TEXT_NODE:
+                return child.nodeValue
+        return ""
+
+    def default(self, datastring):
+        return {'body': self._from_xml(datastring)}
+
+    def __call__(self, datastring):
+        # Adding a migration path to allow us to remove unncessary classes
+        return self.default(datastring)
 
 
 # NOTE(maru): this class is duplicated from quantum.wsgi
@@ -20,8 +366,8 @@ class Serializer(object):
 
     def _get_serialize_handler(self, content_type):
         handlers = {
-            'application/json': self._to_json,
-            'application/xml': self._to_xml,
+            'application/json': JSONDictSerializer(),
+            'application/xml': XMLDictSerializer(self.metadata),
         }
 
         try:
@@ -31,7 +377,7 @@ class Serializer(object):
 
     def serialize(self, data, content_type):
         """Serialize a dictionary into the specified content type."""
-        return self._get_serialize_handler(content_type)(data)
+        return self._get_serialize_handler(content_type).serialize(data)
 
     def deserialize(self, datastring, content_type):
         """Deserialize a string to a dictionary.
@@ -39,117 +385,16 @@ class Serializer(object):
         The string must be in the format of a supported MIME type.
 
         """
-        try:
-            return self.get_deserialize_handler(content_type)(datastring)
-        except Exception:
-            raise exception.MalformedResponseBody(
-                reason="Unable to deserialize response body")
+        return self.get_deserialize_handler(content_type).deserialize(
+            datastring)
 
     def get_deserialize_handler(self, content_type):
         handlers = {
-            'application/json': self._from_json,
-            'application/xml': self._from_xml,
+            'application/json': JSONDeserializer(),
+            'application/xml': XMLDeserializer(self.metadata),
         }
 
         try:
             return handlers[content_type]
         except Exception:
             raise exception.InvalidContentType(content_type=content_type)
-
-    def _from_json(self, datastring):
-        return utils.loads(datastring)
-
-    def _from_xml(self, datastring):
-        xmldata = self.metadata.get('application/xml', {})
-        plurals = set(xmldata.get('plurals', {}))
-        node = minidom.parseString(datastring).childNodes[0]
-        return {node.nodeName: self._from_xml_node(node, plurals)}
-
-    def _from_xml_node(self, node, listnames):
-        """Convert a minidom node to a simple Python type.
-
-        listnames is a collection of names of XML nodes whose subnodes should
-        be considered list items.
-
-        """
-        if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
-            return node.childNodes[0].nodeValue
-        elif node.nodeName in listnames:
-            return [self._from_xml_node(n, listnames)
-                    for n in node.childNodes if n.nodeType != node.TEXT_NODE]
-        else:
-            result = dict()
-            for attr in node.attributes.keys():
-                result[attr] = node.attributes[attr].nodeValue
-            for child in node.childNodes:
-                if child.nodeType != node.TEXT_NODE:
-                    result[child.nodeName] = self._from_xml_node(child,
-                                                                 listnames)
-            return result
-
-    def _to_json(self, data):
-        return utils.dumps(data)
-
-    def _to_xml(self, data):
-        metadata = self.metadata.get('application/xml', {})
-        # We expect data to contain a single key which is the XML root.
-        root_key = data.keys()[0]
-        doc = minidom.Document()
-        node = self._to_xml_node(doc, metadata, root_key, data[root_key])
-
-        xmlns = node.getAttribute('xmlns')
-        if not xmlns and self.default_xmlns:
-            node.setAttribute('xmlns', self.default_xmlns)
-
-        return node.toprettyxml(indent='', newl='')
-
-    def _to_xml_node(self, doc, metadata, nodename, data):
-        """Recursive method to convert data members to XML nodes."""
-        result = doc.createElement(nodename)
-
-        # Set the xml namespace if one is specified
-        # TODO(justinsb): We could also use prefixes on the keys
-        xmlns = metadata.get('xmlns', None)
-        if xmlns:
-            result.setAttribute('xmlns', xmlns)
-        if type(data) is list:
-            collections = metadata.get('list_collections', {})
-            if nodename in collections:
-                metadata = collections[nodename]
-                for item in data:
-                    node = doc.createElement(metadata['item_name'])
-                    node.setAttribute(metadata['item_key'], str(item))
-                    result.appendChild(node)
-                return result
-            singular = metadata.get('plurals', {}).get(nodename, None)
-            if singular is None:
-                if nodename.endswith('s'):
-                    singular = nodename[:-1]
-                else:
-                    singular = 'item'
-            for item in data:
-                node = self._to_xml_node(doc, metadata, singular, item)
-                result.appendChild(node)
-        elif type(data) is dict:
-            collections = metadata.get('dict_collections', {})
-            if nodename in collections:
-                metadata = collections[nodename]
-                for k, v in data.items():
-                    node = doc.createElement(metadata['item_name'])
-                    node.setAttribute(metadata['item_key'], str(k))
-                    text = doc.createTextNode(str(v))
-                    node.appendChild(text)
-                    result.appendChild(node)
-                return result
-            attrs = metadata.get('attributes', {}).get(nodename, {})
-            for k, v in data.items():
-                if k in attrs:
-                    result.setAttribute(k, str(v))
-                else:
-                    node = self._to_xml_node(doc, metadata, k, v)
-                    result.appendChild(node)
-        else:
-            # Type is atom.
-            node = doc.createTextNode(str(data))
-            result.appendChild(node)
-        return result
diff --git a/quantumclient/openstack/common/jsonutils.py b/quantumclient/openstack/common/jsonutils.py
new file mode 100644
index 0000000..ad76e06
--- /dev/null
+++ b/quantumclient/openstack/common/jsonutils.py
@@ -0,0 +1,148 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 Justin Santa Barbara
+# 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.
+
+'''
+JSON related utilities.
+
+This module provides a few things:
+
+    1) A handy function for getting an object down to something that can be
+    JSON serialized.  See to_primitive().
+
+    2) Wrappers around loads() and dumps().  The dumps() wrapper will
+    automatically use to_primitive() for you if needed.
+
+    3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson
+    is available.
+'''
+
+
+import datetime
+import inspect
+import itertools
+import json
+import xmlrpclib
+
+from quantumclient.openstack.common import timeutils
+
+
+def to_primitive(value, convert_instances=False, level=0):
+    """Convert a complex object into primitives.
+
+    Handy for JSON serialization. We can optionally handle instances,
+    but since this is a recursive function, we could have cyclical
+    data structures.
+
+    To handle cyclical data structures we could track the actual objects
+    visited in a set, but not all objects are hashable. Instead we just
+    track the depth of the object inspections and don't go too deep.
+
+    Therefore, convert_instances=True is lossy ... be aware.
+
+    """
+    nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod,
+             inspect.isfunction, inspect.isgeneratorfunction,
+             inspect.isgenerator, inspect.istraceback, inspect.isframe,
+             inspect.iscode, inspect.isbuiltin, inspect.isroutine,
+             inspect.isabstract]
+    for test in nasty:
+        if test(value):
+            return unicode(value)
+
+    # value of itertools.count doesn't get caught by inspects
+    # above and results in infinite loop when list(value) is called.
+    if type(value) == itertools.count:
+        return unicode(value)
+
+    # FIXME(vish): Workaround for LP bug 852095. Without this workaround,
+    #              tests that raise an exception in a mocked method that
+    #              has a @wrap_exception with a notifier will fail. If
+    #              we up the dependency to 0.5.4 (when it is released) we
+    #              can remove this workaround.
+    if getattr(value, '__module__', None) == 'mox':
+        return 'mock'
+
+    if level > 3:
+        return '?'
+
+    # The try block may not be necessary after the class check above,
+    # but just in case ...
+    try:
+        # It's not clear why xmlrpclib created their own DateTime type, but
+        # for our purposes, make it a datetime type which is explicitly
+        # handled
+        if isinstance(value, xmlrpclib.DateTime):
+            value = datetime.datetime(*tuple(value.timetuple())[:6])
+
+        if isinstance(value, (list, tuple)):
+            o = []
+            for v in value:
+                o.append(to_primitive(v, convert_instances=convert_instances,
+                                      level=level))
+            return o
+        elif isinstance(value, dict):
+            o = {}
+            for k, v in value.iteritems():
+                o[k] = to_primitive(v, convert_instances=convert_instances,
+                                    level=level)
+            return o
+        elif isinstance(value, datetime.datetime):
+            return timeutils.strtime(value)
+        elif hasattr(value, 'iteritems'):
+            return to_primitive(dict(value.iteritems()),
+                                convert_instances=convert_instances,
+                                level=level + 1)
+        elif hasattr(value, '__iter__'):
+            return to_primitive(list(value),
+                                convert_instances=convert_instances,
+                                level=level)
+        elif convert_instances and hasattr(value, '__dict__'):
+            # Likely an instance of something. Watch for cycles.
+            # Ignore class member vars.
+            return to_primitive(value.__dict__,
+                                convert_instances=convert_instances,
+                                level=level + 1)
+        else:
+            return value
+    except TypeError:
+        # Class objects are tricky since they may define something like
+        # __iter__ defined but it isn't callable as list().
+        return unicode(value)
+
+
+def dumps(value, default=to_primitive, **kwargs):
+    return json.dumps(value, default=default, **kwargs)
+
+
+def loads(s):
+    return json.loads(s)
+
+
+def load(s):
+    return json.load(s)
+
+
+try:
+    import anyjson
+except ImportError:
+    pass
+else:
+    anyjson._modules.append((__name__, 'dumps', TypeError,
+                                       'loads', ValueError, 'load'))
+    anyjson.force_implementation(__name__)
diff --git a/quantumclient/openstack/common/timeutils.py b/quantumclient/openstack/common/timeutils.py
new file mode 100644
index 0000000..0f34608
--- /dev/null
+++ b/quantumclient/openstack/common/timeutils.py
@@ -0,0 +1,164 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# 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.
+
+"""
+Time related utilities and helper functions.
+"""
+
+import calendar
+import datetime
+
+import iso8601
+
+
+TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
+PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f"
+
+
+def isotime(at=None):
+    """Stringify time in ISO 8601 format"""
+    if not at:
+        at = utcnow()
+    str = at.strftime(TIME_FORMAT)
+    tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
+    str += ('Z' if tz == 'UTC' else tz)
+    return str
+
+
+def parse_isotime(timestr):
+    """Parse time from ISO 8601 format"""
+    try:
+        return iso8601.parse_date(timestr)
+    except iso8601.ParseError as e:
+        raise ValueError(e.message)
+    except TypeError as e:
+        raise ValueError(e.message)
+
+
+def strtime(at=None, fmt=PERFECT_TIME_FORMAT):
+    """Returns formatted utcnow."""
+    if not at:
+        at = utcnow()
+    return at.strftime(fmt)
+
+
+def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT):
+    """Turn a formatted time back into a datetime."""
+    return datetime.datetime.strptime(timestr, fmt)
+
+
+def normalize_time(timestamp):
+    """Normalize time in arbitrary timezone to UTC naive object"""
+    offset = timestamp.utcoffset()
+    if offset is None:
+        return timestamp
+    return timestamp.replace(tzinfo=None) - offset
+
+
+def is_older_than(before, seconds):
+    """Return True if before is older than seconds."""
+    if isinstance(before, basestring):
+        before = parse_strtime(before).replace(tzinfo=None)
+    return utcnow() - before > datetime.timedelta(seconds=seconds)
+
+
+def is_newer_than(after, seconds):
+    """Return True if after is newer than seconds."""
+    if isinstance(after, basestring):
+        after = parse_strtime(after).replace(tzinfo=None)
+    return after - utcnow() > datetime.timedelta(seconds=seconds)
+
+
+def utcnow_ts():
+    """Timestamp version of our utcnow function."""
+    return calendar.timegm(utcnow().timetuple())
+
+
+def utcnow():
+    """Overridable version of utils.utcnow."""
+    if utcnow.override_time:
+        try:
+            return utcnow.override_time.pop(0)
+        except AttributeError:
+            return utcnow.override_time
+    return datetime.datetime.utcnow()
+
+
+utcnow.override_time = None
+
+
+def set_time_override(override_time=datetime.datetime.utcnow()):
+    """
+    Override utils.utcnow to return a constant time or a list thereof,
+    one at a time.
+    """
+    utcnow.override_time = override_time
+
+
+def advance_time_delta(timedelta):
+    """Advance overridden time using a datetime.timedelta."""
+    assert(not utcnow.override_time is None)
+    try:
+        for dt in utcnow.override_time:
+            dt += timedelta
+    except TypeError:
+        utcnow.override_time += timedelta
+
+
+def advance_time_seconds(seconds):
+    """Advance overridden time by seconds."""
+    advance_time_delta(datetime.timedelta(0, seconds))
+
+
+def clear_time_override():
+    """Remove the overridden time."""
+    utcnow.override_time = None
+
+
+def marshall_now(now=None):
+    """Make an rpc-safe datetime with microseconds.
+
+    Note: tzinfo is stripped, but not required for relative times."""
+    if not now:
+        now = utcnow()
+    return dict(day=now.day, month=now.month, year=now.year, hour=now.hour,
+                minute=now.minute, second=now.second,
+                microsecond=now.microsecond)
+
+
+def unmarshall_time(tyme):
+    """Unmarshall a datetime dict."""
+    return datetime.datetime(day=tyme['day'],
+                             month=tyme['month'],
+                             year=tyme['year'],
+                             hour=tyme['hour'],
+                             minute=tyme['minute'],
+                             second=tyme['second'],
+                             microsecond=tyme['microsecond'])
+
+
+def delta_seconds(before, after):
+    """
+    Compute the difference in seconds between two date, time, or
+    datetime objects (as a float, to microsecond resolution).
+    """
+    delta = after - before
+    try:
+        return delta.total_seconds()
+    except AttributeError:
+        return ((delta.days * 24 * 3600) + delta.seconds +
+                float(delta.microseconds) / (10 ** 6))
diff --git a/quantumclient/v2_0/client.py b/quantumclient/v2_0/client.py
index 8339d19..53b597d 100644
--- a/quantumclient/v2_0/client.py
+++ b/quantumclient/v2_0/client.py
@@ -22,8 +22,9 @@ import urllib
 
 from quantumclient.client import HTTPClient
 from quantumclient.common import _
+from quantumclient.common import constants
 from quantumclient.common import exceptions
-from quantumclient.common.serializer import Serializer
+from quantumclient.common import serializer
 
 
 _logger = logging.getLogger(__name__)
@@ -139,18 +140,6 @@ class Client(object):
 
     """
 
-    #Metadata for deserializing xml
-    _serialization_metadata = {
-        "application/xml": {
-            "attributes": {
-                "network": ["id", "name"],
-                "port": ["id", "mac_address"],
-                "subnet": ["id", "prefix"]},
-            "plurals": {
-                "networks": "network",
-                "ports": "port",
-                "subnets": "subnet", }, }, }
-
     networks_path = "/networks"
     network_path = "/networks/%s"
     ports_path = "/ports"
@@ -166,6 +155,33 @@ class Client(object):
     floatingips_path = "/floatingips"
     floatingip_path = "/floatingips/%s"
 
+    # API has no way to report plurals, so we have to hard code them
+    EXTED_PLURALS = {'routers': 'router',
+                     'floatingips': 'floatingip',
+                     'service_types': 'service_type',
+                     'service_definitions': 'service_definition',
+                     'security_groups': 'security_group',
+                     'security_group_rules': 'security_group_rule',
+                     'vips': 'vip',
+                     'pools': 'pool',
+                     'members': 'member',
+                     'health_monitors': 'health_monitor',
+                     'quotas': 'quota',
+                     }
+
+    def get_attr_metadata(self):
+        if self.format == 'json':
+            return {}
+        old_request_format = self.format
+        self.format = 'json'
+        exts = self.list_extensions()['extensions']
+        self.format = old_request_format
+        ns = dict([(ext['alias'], ext['namespace']) for ext in exts])
+        self.EXTED_PLURALS.update(constants.PLURALS)
+        return {'plurals': self.EXTED_PLURALS,
+                'xmlns': constants.XML_NS_V20,
+                constants.EXT_NS: ns}
+
     @APIParamsCall
     def get_quotas_tenant(self, **_params):
         """Fetch tenant info in server's context for
@@ -425,16 +441,14 @@ class Client(object):
 
     def _handle_fault_response(self, status_code, response_body):
         # Create exception with HTTP status code and message
-        error_message = response_body
-        _logger.debug("Error message: %s", error_message)
+        _logger.debug("Error message: %s", response_body)
         # Add deserialized error message to exception arguments
         try:
-            des_error_body = Serializer().deserialize(error_message,
-                                                      self.content_type())
+            des_error_body = self.deserialize(response_body, status_code)
         except:
             # If unable to deserialized body it is probably not a
             # Quantum error
-            des_error_body = {'message': error_message}
+            des_error_body = {'message': response_body}
         # Raise the appropriate exception
         exception_handler_v20(status_code, des_error_body)
 
@@ -475,7 +489,8 @@ class Client(object):
         if data is None:
             return None
         elif type(data) is dict:
-            return Serializer().serialize(data, self.content_type())
+            return serializer.Serializer(
+                self.get_attr_metadata()).serialize(data, self.content_type())
         else:
             raise Exception("unable to serialize object of type = '%s'" %
                             type(data))
@@ -486,17 +501,16 @@ class Client(object):
         """
         if status_code == 204:
             return data
-        return Serializer(self._serialization_metadata).deserialize(
-            data, self.content_type())
+        return serializer.Serializer(self.get_attr_metadata()).deserialize(
+            data, self.content_type())['body']
 
-    def content_type(self, format=None):
+    def content_type(self, _format=None):
         """
         Returns the mime-type for either 'xml' or 'json'.  Defaults to the
         currently set format
         """
-        if not format:
-            format = self.format
-        return "application/%s" % (format)
+        _format = _format or self.format
+        return "application/%s" % (_format)
 
     def retry_request(self, method, action, body=None,
                       headers=None, params=None):
diff --git a/tools/pip-requires b/tools/pip-requires
index 2deb910..59ee8fa 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -1,6 +1,7 @@
-cliff>=1.2.1
 argparse
+cliff>=1.2.1
 httplib2
+iso8601
 prettytable>=0.6.0
-simplejson
 pyparsing>=1.5.6,<2.0
+simplejson
diff --git a/tox.ini b/tox.ini
index c9d9608..dbb367d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,7 @@ deps = -r{toxinidir}/tools/test-requires
 commands = python setup.py testr --testr-args='{posargs}'
 
 [testenv:pep8]
-commands = pep8 --repeat --show-source --exclude=.venv,.tox,dist,doc .
+commands = pep8 --repeat --show-source --ignore=E125 --exclude=.venv,.tox,dist,doc .
 
 [testenv:venv]
 commands = {posargs}