summaryrefslogtreecommitdiff
path: root/quantumclient
diff options
context:
space:
mode:
authorgongysh <gongysh@cn.ibm.com>2013-01-18 23:37:01 +0800
committergongysh <gongysh@linux.vnet.ibm.com>2013-02-06 18:56:10 +0800
commit5117731a6d55651adcd2277fb65b977a1ec8e970 (patch)
treea1222d765020c59ef54c8eb44000153c2fc12fac /quantumclient
parentaa41734347284a2430dc6f32ce4af23ba84d271d (diff)
downloadpython-neutronclient-5117731a6d55651adcd2277fb65b977a1ec8e970.tar.gz
Support XML request format
blueprint quantum-client-xml Change-Id: I9db8ea7c395909def00d6f25c9c1a98c07fdde68
Diffstat (limited to 'quantumclient')
-rw-r--r--quantumclient/common/constants.py40
-rw-r--r--quantumclient/common/serializer.py465
-rw-r--r--quantumclient/openstack/common/jsonutils.py148
-rw-r--r--quantumclient/openstack/common/timeutils.py164
-rw-r--r--quantumclient/v2_0/client.py64
5 files changed, 746 insertions, 135 deletions
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 9c045fc..202e1a7 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"
@@ -182,6 +171,33 @@ class Client(object):
disassociate_pool_health_monitors_path = (
"/lb/pools/%(pool)s/health_monitors/%(health_monitor)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
@@ -669,16 +685,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)
@@ -719,7 +733,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))
@@ -730,17 +745,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):