diff --git a/openstack-common.conf b/openstack-common.conf index 35d48d06e..d52611484 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 e19600bec..b42299beb 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 000000000..572baa941 --- /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 6e914673a..7eba93069 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 000000000..ad76e0655 --- /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 000000000..0f346087f --- /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 9c045fc92..202e1a7f2 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): diff --git a/tools/pip-requires b/tools/pip-requires index 2deb910ce..59ee8fa34 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 c9d960819..dbb367d4d 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}