Support XML request format
blueprint quantum-client-xml Change-Id: I9db8ea7c395909def00d6f25c9c1a98c07fdde68
This commit is contained in:
parent
aa41734347
commit
5117731a6d
@ -1,7 +1,7 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
|
||||||
# The list of modules to copy from openstack-common
|
# 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
|
# The base module to hold the copy of openstack.common
|
||||||
base=quantumclient
|
base=quantumclient
|
||||||
|
@ -14,11 +14,12 @@ else
|
|||||||
NOAUTH=
|
NOAUTH=
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
FORMAT=" --request-format xml"
|
||||||
|
|
||||||
# test the CRUD of network
|
# test the CRUD of network
|
||||||
network=mynet1
|
network=mynet1
|
||||||
quantum net-create $NOAUTH $network || die "fail to create network $network"
|
quantum net-create $FORMAT $NOAUTH $network || die "fail to create network $network"
|
||||||
temp=`quantum net-list -- --name $network --fields id | wc -l`
|
temp=`quantum net-list $FORMAT -- --name $network --fields id | wc -l`
|
||||||
echo $temp
|
echo $temp
|
||||||
if [ $temp -ne 5 ]; then
|
if [ $temp -ne 5 ]; then
|
||||||
die "networks with name $network is not unique or found"
|
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`
|
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"
|
echo "ID of network with name $network is $network_id"
|
||||||
|
|
||||||
quantum net-show $network || die "fail to show network $network"
|
quantum net-show $FORMAT $network || die "fail to show network $network"
|
||||||
quantum net-show $network_id || die "fail to show network $network_id"
|
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 $FORMAT $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_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
|
# test the CRUD of subnet
|
||||||
subnet=mysubnet1
|
subnet=mysubnet1
|
||||||
cidr=10.0.1.3/24
|
cidr=10.0.1.3/24
|
||||||
quantum subnet-create $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet"
|
quantum subnet-create $FORMAT $NOAUTH $network $cidr --name $subnet || die "fail to create subnet $subnet"
|
||||||
tempsubnet=`quantum subnet-list -- --name $subnet --fields id | wc -l`
|
tempsubnet=`quantum subnet-list $FORMAT -- --name $subnet --fields id | wc -l`
|
||||||
echo $tempsubnet
|
echo $tempsubnet
|
||||||
if [ $tempsubnet -ne 5 ]; then
|
if [ $tempsubnet -ne 5 ]; then
|
||||||
die "subnets with name $subnet is not unique or found"
|
die "subnets with name $subnet is not unique or found"
|
||||||
fi
|
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"
|
echo "ID of subnet with name $subnet is $subnet_id"
|
||||||
quantum subnet-show $subnet || die "fail to show subnet $subnet"
|
quantum subnet-show $FORMAT $subnet || die "fail to show subnet $subnet"
|
||||||
quantum subnet-show $subnet_id || die "fail to show subnet $subnet_id"
|
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 $FORMAT $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_id --dns_namesevers host2 || die "fail to update subnet $subnet_id"
|
||||||
|
|
||||||
# test the crud of ports
|
# test the crud of ports
|
||||||
port=myport1
|
port=myport1
|
||||||
quantum port-create $NOAUTH $network --name $port || die "fail to create port $port"
|
quantum port-create $FORMAT $NOAUTH $network --name $port || die "fail to create port $port"
|
||||||
tempport=`quantum port-list -- --name $port --fields id | wc -l`
|
tempport=`quantum port-list $FORMAT -- --name $port --fields id | wc -l`
|
||||||
echo $tempport
|
echo $tempport
|
||||||
if [ $tempport -ne 5 ]; then
|
if [ $tempport -ne 5 ]; then
|
||||||
die "ports with name $port is not unique or found"
|
die "ports with name $port is not unique or found"
|
||||||
fi
|
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"
|
echo "ID of port with name $port is $port_id"
|
||||||
quantum port-show $port || die "fail to show port $port"
|
quantum port-show $FORMAT $port || die "fail to show port $port"
|
||||||
quantum port-show $port_id || die "fail to show port $port_id"
|
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 $FORMAT $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_id --device_id deviceid2 || die "fail to update port $port_id"
|
||||||
|
|
||||||
# test quota commands RUD
|
# test quota commands RUD
|
||||||
DEFAULT_NETWORKS=10
|
DEFAULT_NETWORKS=10
|
||||||
DEFAULT_PORTS=50
|
DEFAULT_PORTS=50
|
||||||
tenant_id=tenant_a
|
tenant_id=tenant_a
|
||||||
tenant_id_b=tenant_b
|
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 $FORMAT --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"
|
quantum quota-update $FORMAT --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}'`
|
networks=`quantum quota-list $FORMAT -c network -c tenant_id | grep $tenant_id | awk '{print $2}'`
|
||||||
if [ $networks -ne 30 ]; then
|
if [ $networks -ne 30 ]; then
|
||||||
die "networks quota should be 30"
|
die "networks quota should be 30"
|
||||||
fi
|
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
|
if [ $networks -ne 20 ]; then
|
||||||
die "networks quota should be 20"
|
die "networks quota should be 20"
|
||||||
fi
|
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
|
if [ $networks -ne 30 ]; then
|
||||||
die "networks quota should be 30"
|
die "networks quota should be 30"
|
||||||
fi
|
fi
|
||||||
quantum quota-delete --tenant_id $tenant_id || die "fail to delete quota for tenant $tenant_id"
|
quantum quota-delete $FORMAT --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}'`
|
networks=`quantum quota-show $FORMAT --tenant_id $tenant_id | grep network | awk -F'|' '{print $3}'`
|
||||||
if [ $networks -ne $DEFAULT_NETWORKS ]; then
|
if [ $networks -ne $DEFAULT_NETWORKS ]; then
|
||||||
die "networks quota should be $DEFAULT_NETWORKS"
|
die "networks quota should be $DEFAULT_NETWORKS"
|
||||||
fi
|
fi
|
||||||
# update self
|
# update self
|
||||||
if [ "t$NOAUTH" = "t" ]; then
|
if [ "t$NOAUTH" = "t" ]; then
|
||||||
# with auth
|
# with auth
|
||||||
quantum quota-update --port 99 || die "fail to update quota for self"
|
quantum quota-update $FORMAT --port 99 || die "fail to update quota for self"
|
||||||
ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'`
|
ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'`
|
||||||
if [ $ports -ne 99 ]; then
|
if [ $ports -ne 99 ]; then
|
||||||
die "ports quota should be 99"
|
die "ports quota should be 99"
|
||||||
fi
|
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
|
if [ $ports -ne 99 ]; then
|
||||||
die "ports quota should be 99"
|
die "ports quota should be 99"
|
||||||
fi
|
fi
|
||||||
quantum quota-delete || die "fail to delete quota for tenant self"
|
quantum quota-delete $FORMAT || die "fail to delete quota for tenant self"
|
||||||
ports=`quantum quota-show | grep port | awk -F'|' '{print $3}'`
|
ports=`quantum quota-show $FORMAT | grep port | awk -F'|' '{print $3}'`
|
||||||
if [ $ports -ne $DEFAULT_PORTS ]; then
|
if [ $ports -ne $DEFAULT_PORTS ]; then
|
||||||
die "ports quota should be $DEFAULT_PORTS"
|
die "ports quota should be $DEFAULT_PORTS"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# without auth
|
# without auth
|
||||||
quantum quota-update --port 100
|
quantum quota-update $FORMAT --port 100
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
die "without valid context on server, quota update command should fail."
|
die "without valid context on server, quota update command should fail."
|
||||||
fi
|
fi
|
||||||
quantum quota-show
|
quantum quota-show $FORMAT
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
die "without valid context on server, quota show command should fail."
|
die "without valid context on server, quota show command should fail."
|
||||||
fi
|
fi
|
||||||
quantum quota-delete
|
quantum quota-delete $FORMAT
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
die "without valid context on server, quota delete command should fail."
|
die "without valid context on server, quota delete command should fail."
|
||||||
fi
|
fi
|
||||||
quantum quota-list || die "fail to update quota for self"
|
quantum quota-list $FORMAT || die "fail to update quota for self"
|
||||||
fi
|
fi
|
||||||
|
40
quantumclient/common/constants.py
Normal file
40
quantumclient/common/constants.py
Normal file
@ -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'}
|
@ -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 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
|
# NOTE(maru): this class is duplicated from quantum.wsgi
|
||||||
@ -20,8 +366,8 @@ class Serializer(object):
|
|||||||
|
|
||||||
def _get_serialize_handler(self, content_type):
|
def _get_serialize_handler(self, content_type):
|
||||||
handlers = {
|
handlers = {
|
||||||
'application/json': self._to_json,
|
'application/json': JSONDictSerializer(),
|
||||||
'application/xml': self._to_xml,
|
'application/xml': XMLDictSerializer(self.metadata),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -31,7 +377,7 @@ class Serializer(object):
|
|||||||
|
|
||||||
def serialize(self, data, content_type):
|
def serialize(self, data, content_type):
|
||||||
"""Serialize a dictionary into the specified 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):
|
def deserialize(self, datastring, content_type):
|
||||||
"""Deserialize a string to a dictionary.
|
"""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.
|
The string must be in the format of a supported MIME type.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
return self.get_deserialize_handler(content_type).deserialize(
|
||||||
return self.get_deserialize_handler(content_type)(datastring)
|
datastring)
|
||||||
except Exception:
|
|
||||||
raise exception.MalformedResponseBody(
|
|
||||||
reason="Unable to deserialize response body")
|
|
||||||
|
|
||||||
def get_deserialize_handler(self, content_type):
|
def get_deserialize_handler(self, content_type):
|
||||||
handlers = {
|
handlers = {
|
||||||
'application/json': self._from_json,
|
'application/json': JSONDeserializer(),
|
||||||
'application/xml': self._from_xml,
|
'application/xml': XMLDeserializer(self.metadata),
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return handlers[content_type]
|
return handlers[content_type]
|
||||||
except Exception:
|
except Exception:
|
||||||
raise exception.InvalidContentType(content_type=content_type)
|
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
|
|
||||||
|
148
quantumclient/openstack/common/jsonutils.py
Normal file
148
quantumclient/openstack/common/jsonutils.py
Normal file
@ -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__)
|
164
quantumclient/openstack/common/timeutils.py
Normal file
164
quantumclient/openstack/common/timeutils.py
Normal file
@ -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))
|
@ -22,8 +22,9 @@ import urllib
|
|||||||
|
|
||||||
from quantumclient.client import HTTPClient
|
from quantumclient.client import HTTPClient
|
||||||
from quantumclient.common import _
|
from quantumclient.common import _
|
||||||
|
from quantumclient.common import constants
|
||||||
from quantumclient.common import exceptions
|
from quantumclient.common import exceptions
|
||||||
from quantumclient.common.serializer import Serializer
|
from quantumclient.common import serializer
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_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"
|
networks_path = "/networks"
|
||||||
network_path = "/networks/%s"
|
network_path = "/networks/%s"
|
||||||
ports_path = "/ports"
|
ports_path = "/ports"
|
||||||
@ -182,6 +171,33 @@ class Client(object):
|
|||||||
disassociate_pool_health_monitors_path = (
|
disassociate_pool_health_monitors_path = (
|
||||||
"/lb/pools/%(pool)s/health_monitors/%(health_monitor)s")
|
"/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
|
@APIParamsCall
|
||||||
def get_quotas_tenant(self, **_params):
|
def get_quotas_tenant(self, **_params):
|
||||||
"""Fetch tenant info in server's context for
|
"""Fetch tenant info in server's context for
|
||||||
@ -669,16 +685,14 @@ class Client(object):
|
|||||||
|
|
||||||
def _handle_fault_response(self, status_code, response_body):
|
def _handle_fault_response(self, status_code, response_body):
|
||||||
# Create exception with HTTP status code and message
|
# Create exception with HTTP status code and message
|
||||||
error_message = response_body
|
_logger.debug("Error message: %s", response_body)
|
||||||
_logger.debug("Error message: %s", error_message)
|
|
||||||
# Add deserialized error message to exception arguments
|
# Add deserialized error message to exception arguments
|
||||||
try:
|
try:
|
||||||
des_error_body = Serializer().deserialize(error_message,
|
des_error_body = self.deserialize(response_body, status_code)
|
||||||
self.content_type())
|
|
||||||
except:
|
except:
|
||||||
# If unable to deserialized body it is probably not a
|
# If unable to deserialized body it is probably not a
|
||||||
# Quantum error
|
# Quantum error
|
||||||
des_error_body = {'message': error_message}
|
des_error_body = {'message': response_body}
|
||||||
# Raise the appropriate exception
|
# Raise the appropriate exception
|
||||||
exception_handler_v20(status_code, des_error_body)
|
exception_handler_v20(status_code, des_error_body)
|
||||||
|
|
||||||
@ -719,7 +733,8 @@ class Client(object):
|
|||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
elif type(data) is dict:
|
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:
|
else:
|
||||||
raise Exception("unable to serialize object of type = '%s'" %
|
raise Exception("unable to serialize object of type = '%s'" %
|
||||||
type(data))
|
type(data))
|
||||||
@ -730,17 +745,16 @@ class Client(object):
|
|||||||
"""
|
"""
|
||||||
if status_code == 204:
|
if status_code == 204:
|
||||||
return data
|
return data
|
||||||
return Serializer(self._serialization_metadata).deserialize(
|
return serializer.Serializer(self.get_attr_metadata()).deserialize(
|
||||||
data, self.content_type())
|
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
|
Returns the mime-type for either 'xml' or 'json'. Defaults to the
|
||||||
currently set format
|
currently set format
|
||||||
"""
|
"""
|
||||||
if not format:
|
_format = _format or self.format
|
||||||
format = self.format
|
return "application/%s" % (_format)
|
||||||
return "application/%s" % (format)
|
|
||||||
|
|
||||||
def retry_request(self, method, action, body=None,
|
def retry_request(self, method, action, body=None,
|
||||||
headers=None, params=None):
|
headers=None, params=None):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
cliff>=1.2.1
|
|
||||||
argparse
|
argparse
|
||||||
|
cliff>=1.2.1
|
||||||
httplib2
|
httplib2
|
||||||
|
iso8601
|
||||||
prettytable>=0.6.0
|
prettytable>=0.6.0
|
||||||
simplejson
|
|
||||||
pyparsing>=1.5.6,<2.0
|
pyparsing>=1.5.6,<2.0
|
||||||
|
simplejson
|
||||||
|
2
tox.ini
2
tox.ini
@ -11,7 +11,7 @@ deps = -r{toxinidir}/tools/test-requires
|
|||||||
commands = python setup.py testr --testr-args='{posargs}'
|
commands = python setup.py testr --testr-args='{posargs}'
|
||||||
|
|
||||||
[testenv:pep8]
|
[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]
|
[testenv:venv]
|
||||||
commands = {posargs}
|
commands = {posargs}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user